# Project export: TwinSphere AI

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2025
- Tagline: TwinSphere AI lets you simulate social media engagement across distinct AI personas — predict likes, comments, and sentiment before posting.
- Devpost: https://devpost.com/software/twinsphere-ai
- GitHub: https://github.com/pvrraju/AI-ad-world
- Team: 1 GitHub contributor(s) — vijay (4 commits)

## Devpost submission (written by the team)

### Inspiration

We were inspired by the gap between cold analytics and real human behavior. We envisioned a platform where businesses could interact with their target audience not as data points, but as dynamic, thinking "digital twins." Our goal was to breathe life into market research, making it more intuitive, predictive, and empathetic.

### What it does

Twinsphere AI creates a virtual focus group of AI-powered customer personas. Companies can upload their ad copy, and our digital twins will react in real-time—commenting, liking, or ignoring it based on their unique personalities. This provides instant feedback on a campaign's potential impact before it goes live.

### How we built it

Our backend is a powerful FastAPI server running Python, orchestrating AI agents built with advanced language models. The frontend is a sleek, responsive interface crafted with React and TypeScript. The entire system is containerized using Docker, ensuring smooth scalability for simulating complex market scenarios with thousands of digital twins.

### Challenges we ran into

Our greatest challenge was avoiding generic AI responses. We fine-tuned our models to ensure each persona had a distinct, consistent personality and didn't just "hallucinate" answers. Optimizing the simulation engine to run complex scenarios with numerous agents in parallel without compromising speed was another significant but rewarding hurdle.

### Accomplishments we're proud of

We're incredibly proud of creating AI agents that feel authentically human. When a "Skeptical Steve" persona gives a cynical but in-character critique of an ad, we know we've succeeded. Moving beyond simple metrics to generate qualitative, actionable insights is our biggest accomplishment, offering a deeper understanding of audience reception.

### What we learned

We learned that the most powerful insights come from combining quantitative data with qualitative reasoning. Seeing why a persona disliked an ad is far more valuable than just knowing the engagement rate. This project reinforced the idea that the future of AI in marketing lies in its ability to simulate human nuance.

### What's next

for Twinsphere AI The future is about deeper simulation. We're developing a "social graph" where digital twins can influence each other's opinions, creating more realistic and dynamic campaign forecasts. We also plan to expand our agent creation process, allowing businesses to build personas from a wider range of data sources for even greater accuracy.

## README (from the GitHub repository)

No README available.

## Detected evidence (automated analysis)

Indexed codebase: 25 recognized source files, 48 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- LlamaIndex (technology) — detected in the code
- Mistral AI (technology) — detected in the code
- OpenAI (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- TypeScript (language) — detected in the code
- LangChain (technology) — claimed on Devpost, not found in the code
- Node.js (technology) — claimed on Devpost, not found in the code
- Tailwind CSS (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (41 of 41)

```
.gitignore
backend/agent.py
backend/create_agents.py
backend/debug_test.py
backend/main.py
backend/requirements.txt
backend/simulation.py
backend/tools_v2.py
backend/vercel.json
data/ads/fashion_ad.txt
data/ads/sample_ad.txt
data/agent_personalities.csv
data/personalities/personality_1.txt
data/shared_knowledge.txt
data/simulation_interactions.csv
data/simulation_responses.csv
docker-compose.yml
frontend/.gitignore
frontend/package.json
frontend/public/index.html
frontend/public/manifest.json
frontend/public/robots.txt
frontend/README.md
frontend/src/App.css
frontend/src/App.tsx
frontend/src/components/HomePage.module.css
frontend/src/components/HomePage.tsx
frontend/src/components/OverallAnalytics.module.css
frontend/src/components/OverallAnalytics.tsx
frontend/src/components/ResultsDisplay.module.css
frontend/src/components/ResultsDisplay.tsx
frontend/src/components/SimulationController.module.css
frontend/src/components/SimulationController.tsx
frontend/src/index.css
frontend/src/index.tsx
frontend/src/react-app-env.d.ts
frontend/src/reportWebVitals.ts
frontend/src/setupTests.ts
frontend/tsconfig.json
package.json
README.md
```

### Dependencies

- backend/requirements.txt: aiohappyeyeballs@==2.6.1, aiohttp@==3.12.13, aiomultiprocess@==0.9.1, aiosignal@==1.3.2, aiosqlite@==0.21.0, alembic@==1.16.2, annotated-types@==0.7.0, anthropic@==0.49.0, anyio@==4.9.0, APScheduler@==3.11.0, argcomplete@==3.6.2, async-lru@==2.0.5, attrs@==25.3.0, banks@==2.1.2, bcrypt@==4.3.0, beautifulsoup4@==4.13.4, black@==25.1.0, Brotli@==1.1.0, certifi@==2025.6.15, cffi@==1.17.1, charset-normalizer@==3.4.2, click@==8.1.8, colorama@==0.4.6, composio_core@==0.7.19, contourpy@==1.3.2, cryptography@==45.0.4, cycler@==0.12.1, dataclasses-json@==0.6.7, datamodel-code-generator@==0.25.9, demjson3@==3.0.6, Deprecated@==1.2.18, dirtyjson@==1.0.8, distro@==1.9.0, dnspython@==2.7.0, docstring_parser@==0.16, e2b@==1.5.2, e2b-code-interpreter@==1.5.1, email_validator@==2.2.0, eval_type_backport@==0.2.2, Faker@==36.2.3, fastapi@==0.115.13, filetype@==1.2.0, fonttools@==4.58.4, frozenlist@==1.7.0, fsspec@==2025.5.1, genson@==1.3.0, googleapis-common-protos@==1.70.0, greenlet@==3.2.3, griffe@==1.7.3, grpcio@==1.73.0, grpcio-tools@==1.71.0, h11@==0.16.0, html2text@==2020.1.16, httpcore@==1.0.9, httpx@==0.28.1, httpx-sse@==0.4.0, idna@==3.10, importlib_metadata@==8.5.0, inflect@==5.6.2, inflection@==0.5.1, isort@==5.13.2, Jinja2@==3.1.6, jiter@==0.10.0, joblib@==1.5.1, jsonref@==1.1.0, jsonschema@==4.24.0, jsonschema-specifications@==2025.4.1, kiwisolver@==1.4.8, letta@==0.8.5, letta-client@==0.1.167, llama-cloud@==0.1.26, llama-cloud-services@==0.6.34, llama-index@==0.12.42, llama-index-agent-openai@==0.4.11, llama-index-cli@==0.4.3, llama-index-core@==0.12.42, llama-index-embeddings-openai@==0.3.1, llama-index-indices-managed-llama-cloud@==0.7.7, llama-index-llms-openai@==0.4.7, llama-index-multi-modal-llms-openai@==0.5.1, llama-index-program-openai@==0.3.2, llama-index-question-gen-openai@==0.3.1, llama-index-readers-file@==0.4.9, llama-index-readers-llama-parse@==0.4.0, llama-parse@==0.6.34, Mako@==1.3.10, markdown-it-py@==3.0.0, MarkupSafe@==3.0.2, marshmallow@==3.26.1, marshmallow-sqlalchemy@==1.4.2, matplotlib@==3.10.3, mcp@==1.9.4, mdurl@==0.1.2, mistralai@==1.8.2, multidict@==6.5.0, mypy_extensions@==1.1.0, nest-asyncio@==1.6.0, networkx@==3.5, nltk@==3.9.1, numpy@==1.26.4, openai@==1.90.0, opentelemetry-api@==1.30.0, opentelemetry-exporter-otlp@==1.30.0, opentelemetry-exporter-otlp-proto-common@==1.30.0, opentelemetry-exporter-otlp-proto-grpc@==1.30.0, opentelemetry-exporter-otlp-proto-http@==1.30.0, opentelemetry-instrumentation@==0.51b0, opentelemetry-instrumentation-requests@==0.51b0, opentelemetry-proto@==1.30.0, opentelemetry-sdk@==1.30.0, opentelemetry-semantic-conventions@==0.51b0, opentelemetry-util-http@==0.51b0, orjson@==3.10.18, packaging@==25.0, pandas@==2.2.3, paramiko@==3.5.1, pathspec@==0.12.1, pathvalidate@==3.3.1, pillow@==10.4.0, platformdirs@==4.3.8, prettytable@==3.16.0, prompt_toolkit@==3.0.51, propcache@==0.3.2, protobuf@==5.29.5, pycparser@==2.22, pydantic@==2.11.7, pydantic_core@==2.33.2, pydantic-settings@==2.10.0, Pygments@==2.19.2, pyhumps@==3.8.0, PyNaCl@==1.5.0, pyparsing@==3.2.3, pypdf@==5.6.0, pyperclip@==1.9.0, Pysher@==1.0.8, python-box@==7.3.2, python-dateutil@==2.9.0.post0, python-dotenv@==1.1.0, python-multipart@==0.0.19, pytz@==2023.4, PyYAML@==6.0.2, questionary@==2.1.0, referencing@==0.36.2, regex@==2024.11.6, requests@==2.32.4, rich@==13.9.4, rpds-py@==0.25.1, semver@==3.0.4, sentry-sdk@==2.19.1, setuptools@==70.3.0, shellingham@==1.5.4, six@==1.17.0, sniffio@==1.3.1, soupsieve@==2.7, SQLAlchemy@==2.0.41, sqlalchemy-json@==0.7.0, SQLAlchemy-Utils@==0.41.2, sqlmodel@==0.0.16, sse-starlette@==2.3.6, starlette@==0.46.2, striprtf@==0.0.26, structlog@==25.4.0, tavily-python@==0.7.7, tenacity@==9.1.2, tiktoken@==0.9.0, tqdm@==4.67.1, typer@==0.15.4, typing_extensions@==4.14.0, typing-inspect@==0.9.0, typing-inspection@==0.4.1, tzdata@==2025.2, tzlocal@==5.3.1, urllib3@==2.5.0, uvicorn@==0.34.3, wcwidth@==0.2.13, websocket-client@==1.8.0, wrapt@==1.17.2, yarl@==1.20.1, zipp@==3.23.0
- frontend/package.json: @testing-library/dom@^10.4.0, @testing-library/jest-dom@^6.6.3, @testing-library/react@^16.3.0, @testing-library/user-event@^13.5.0, @types/jest@^27.5.2, @types/node@^16.18.126, @types/react@^19.1.8, @types/react-dom@^19.1.6, react@^19.1.0, react-dom@^19.1.0, react-router-dom@^7.6.2, react-scripts@5.0.1, typescript@^4.9.5, web-vitals@^2.1.4
- package.json: react-router-dom@^7.6.2

### Recent commits (newest first)

- Update requirements.txt
- Create vercel.json
- Update main.py
- Initial commit

## Key source files (fetched from GitHub, selected and truncated for size)

### package.json

```
{
  "dependencies": {
    "react-router-dom": "^7.6.2"
  }
}

```

### frontend/package.json

```
{
  "name": "frontend",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/dom": "^10.4.0",
    "@testing-library/jest-dom": "^6.6.3",
    "@testing-library/react": "^16.3.0",
    "@testing-library/user-event": "^13.5.0",
    "@types/jest": "^27.5.2",
    "@types/node": "^16.18.126",
    "@types/react": "^19.1.8",
    "@types/react-dom": "^19.1.6",
    "react": "^19.1.0",
    "react-dom": "^19.1.0",
    "react-router-dom": "^7.6.2",
    "react-scripts": "5.0.1",
    "typescript": "^4.9.5",
    "web-vitals": "^2.1.4"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

```

### backend/requirements.txt

```
aiohappyeyeballs==2.6.1
aiohttp==3.12.13
aiomultiprocess==0.9.1
aiosignal==1.3.2
aiosqlite==0.21.0
alembic==1.16.2
annotated-types==0.7.0
anthropic==0.49.0
anyio==4.9.0
APScheduler==3.11.0
argcomplete==3.6.2
async-lru==2.0.5
attrs==25.3.0
banks==2.1.2
bcrypt==4.3.0
beautifulsoup4==4.13.4
black==25.1.0
Brotli==1.1.0
certifi==2025.6.15
cffi==1.17.1
charset-normalizer==3.4.2
click==8.1.8
colorama==0.4.6
composio_core==0.7.19
contourpy==1.3.2
cryptography==45.0.4
cycler==0.12.1
dataclasses-json==0.6.7
datamodel-code-generator==0.25.9
demjson3==3.0.6
Deprecated==1.2.18
dirtyjson==1.0.8
distro==1.9.0
dnspython==2.7.0
docstring_parser==0.16
e2b==1.5.2
e2b-code-interpreter==1.5.1
email_validator==2.2.0
eval_type_backport==0.2.2
Faker==36.2.3
fastapi==0.115.13
filetype==1.2.0
fonttools==4.58.4
frozenlist==1.7.0
fsspec==2025.5.1
genson==1.3.0
googleapis-common-protos==1.70.0
greenlet==3.2.3
griffe==1.7.3
grpcio==1.73.0
grpcio-tools==1.71.0
h11==0.16.0
html2text==2020.1.16
httpcore==1.0.9
httpx==0.28.1
httpx-sse==0.4.0
idna==3.10
importlib_metadata==8.5.0
inflect==5.6.2
inflection==0.5.1
isort==5.13.2
Jinja2==3.1.6
jiter==0.10.0
joblib==1.5.1
jsonref==1.1.0
jsonschema==4.24.0
jsonschema-specifications==2025.4.1
kiwisolver==1.4.8
letta==0.8.5
letta-client==0.1.167
llama-cloud==0.1.26
llama-cloud-services==0.6.34
llama-index==0.12.42
llama-index-agent-openai==0.4.11
llama-index-cli==0.4.3
llama-index-core==0.12.42
llama-index-embeddings-openai==0.3.1
llama-index-indices-managed-llama-cloud==0.7.7
llama-index-llms-openai==0.4.7
llama-index-multi-modal-llms-openai==0.5.1
llama-index-program-openai==0.3.2
llama-index-question-gen-openai==0.3.1
llama-index-readers-file==0.4.9
llama-index-readers-llama-parse==0.4.0
llama-parse==0.6.34
Mako==1.3.10
markdown-it-py==3.0.0
MarkupSafe==3.0.2
marshmallow==3.26.1
marshmallow-sqlalchemy==1.4.2
matplotlib==3.10.3
mcp==1.9.4
mdurl==0.1.2
mistralai==1.8.2
multidict==6.5.0
mypy_extensions==1.1.0
nest-asyncio==1.6.0
networkx==3.5
nltk==3.9.1
numpy==1.26.4
openai==1.90.0
opentelemetry-api==1.30.0
opentelemetry-exporter-otlp==1.30.0
opentelemetry-exporter-otlp-proto-common==1.30.0
opentelemetry-exporter-otlp-proto-grpc==1.30.0
opentelemetry-exporter-otlp-proto-http==1.30.0
opentelemetry-instrumentation==0.51b0
opentelemetry-instrumentation-requests==0.51b0
opentelemetry-proto==1.30.0
opentelemetry-sdk==1.30.0
opentelemetry-semantic-conventions==0.51b0
opentelemetry-util-http==0.51b0
orjson==3.10.18
packaging==25.0
pandas==2.2.3
paramiko==3.5.1
pathspec==0.12.1
pathvalidate==3.3.1
pillow==10.4.0
platformdirs==4.3.8
prettytable==3.16.0
prompt_toolkit==3.0.51
propcache==0.3.2
protobuf==5.29.5
pycparser==2.22
pydantic==2.11.7
pydantic-settings==2.10.0
pydantic_core==2.33.2
Pygments==2.19.2
pyhumps==3.8.0
PyNaCl==1.5.0
pyparsing==3.2.3
pypdf==5.6.0
pyperclip==1.9.0
Pysher==1.0.8
python-box==7.3.2
python-dateutil==2.9.0.post0
python-dotenv==1.1.0
python-multipart==0.0.19
pytz==2023.4
PyYAML==6.0.2
questionary==2.1.0
referencing==0.36.2
regex==2024.11.6
requests==2.32.4
rich==13.9.4
rpds-py==0.25.1
semver==3.0.4
sentry-sdk==2.19.1
setuptools==70.3.0
shellingham==1.5.4
six==1.17.0
sniffio==1.3.1
soupsieve==2.7
SQLAlchemy==2.0.41
sqlalchemy-json==0.7.0
SQLAlchemy-Utils==0.41.2
sqlmodel==0.0.16
sse-starlette==2.3.6
starlette==0.46.2
striprtf==0.0.26
structlog==25.4.0
tavily-python==0.7.7
tenacity==9.1.2
tiktoken==0.9.0
tqdm==4.67.1
typer==0.15.4
typing-inspect==0.9.0
typing-inspection==0.4.1
typing_extensions==4.14.0
tzdata==2025.2
tzlocal==5.3.1
urllib3==2.5.0
uvicorn==0.34.3
wcwidth==0.2.13
websocket-client==1.8.0
wrapt==1.17.2
yarl==1.20.1
zipp==3.23.0

```

### backend/main.py

```python
import sys
import uvicorn
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

# We are creating a simple CLI runner, so FastAPI app is not strictly needed for now,
# but we'll keep it for potential future API endpoints.
app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

@app.get("/")
def read_root():
    return {"message": "Digital Clone Simulation Environment"}

def print_usage():
    """Prints the command usage instructions."""
    print("Usage: python -m backend.main [command]")
    print("\nCommands:")
    print("  create       - Run the agent creation script to populate agents.")
    print("  simulate     - Run the ad simulation with the existing agents.")
    print("  serve        - (Future use) Starts the FastAPI web server.")
    print("\nExamples:")
    print("  python -m backend.main create")
    print("  python -m backend.main simulate")

def main():
    """Main entry point to run different modules."""
    if len(sys.argv) < 2:
        print_usage()
        sys.exit(1)

    command = sys.argv[1]

    if command == "create":
        print("Running agent creation module...")
        from backend.create_agents import main as create_main
        create_main()
    elif command == "simulate":
        print("Running simulation module...")
        from backend.simulation import main as simulate_main
        simulate_main()
    elif command == "serve":
        print("Starting FastAPI server...")
        uvicorn.run(app, host="0.0.0.0", port=8000)
    else:
        print(f"Error: Unknown command '{command}'")
        print_usage()
        sys.exit(1)

if __name__ == "__main__":
    main()

```

### frontend/src/index.tsx

```typescript
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

```

### frontend/src/App.tsx

```typescript
import React, { useState } from 'react';
import './App.css';
import SimulationController from './components/SimulationController';
import ResultsDisplay from './components/ResultsDisplay';

// Define the structure of a single persona result based on the backend
// This will be moved to a types file later
interface PersonaResult {
  agent_name: string;
  description: string;
  reaction: 'like' | 'dislike' | 'comment' | 'repost' | 'ignore';
  confidence: number;
  reasoning: string;
  tags: string[];
  final_message: string;
}

function App() {
  const [results, setResults] = useState<PersonaResult[]>([]);
  const [isLoading, setIsLoading] = useState(false);

  return (
    <div className="App">
      <header className="App-header">
        <h1>Persona Engagement Simulator</h1>
      </header>
      <main>
        <SimulationController 
          setResults={setResults}
          isLoading={isLoading}
          setIsLoading={setIsLoading}
        />
        
        {isLoading && <div className="loader"></div>}
        
        {!isLoading && results.length > 0 && (
          <ResultsDisplay results={results} />
        )}
        
        {!isLoading && results.length === 0 && (
          <div style={{ textAlign: 'center', marginTop: '2rem', color: '#888' }}>
            Upload a CSV and run a simulation to see results.
          </div>
        )}
      </main>
    </div>
  );
}

export default App;

```

### backend/agent.py

```python
# LETTA_API_KEY=sk-let-NGU5MzBjZWItODAzZC00MzE4LWIwODEtYTVmMTE1MzU5NTJlOjNkOGQzZGNmLWViNzMtNDVhNC1iNWM0LTI1ZDY3NzQ5OTIxNA==



```

### backend/debug_test.py

```python
import os
from letta_client import Letta, MessageCreate
from dotenv import load_dotenv

# Load environment variables
load_dotenv(dotenv_path=os.path.join(os.path.dirname(__file__), '..', '.env'))

# Initialize client
api_key = os.getenv("LETTA_API_KEY")
client = Letta(token=api_key)

def test_simple_interaction():
    """Test a simple interaction with one agent to debug tool calling."""
    
    # Get the first agent
    agents = client.agents.list()
    if not agents:
        print("No agents found!")
        return
    
    agent = agents[0]
    print(f"Testing with agent: {agent.name} ({agent.id})")
    
    # Send a very simple message
    simple_prompt = f"""
Please use the agent_like_ad tool with these parameters:
- agent_id: {agent.name}
- ad_id: test_ad

Just call the tool and tell me if it worked.
"""
    
    print("\n=== Sending simple prompt ===")
    print(simple_prompt)
    
    try:
        # Try streaming to see the full interaction
        print("\n=== Streaming response ===")
        stream = client.agents.messages.create_stream(
            agent_id=agent.id,
            messages=[MessageCreate(role="user", content=simple_prompt)]
        )
        
        print("Stream chunks:")
        for i, chunk in enumerate(stream):
            print(f"  {i+1}. Type: {chunk.message_type}")
            if hasattr(chunk, 'content') and chunk.content:
                print(f"     Content: {chunk.content}")
            if hasattr(chunk, 'tool_call') and chunk.tool_call:
                print(f"     Tool: {chunk.tool_call.name}")
                print(f"     Args: {chunk.tool_call.arguments}")
            if hasattr(chunk, 'tool_return') and chunk.tool_return:
                print(f"     Tool Return: {chunk.tool_return}")
    
    except Exception as e:
        print(f"Error in streaming: {e}")

if __name__ == "__main__":
    test_simple_interaction() 
```

### backend/tools_v2.py

```python
import os
import json

def agent_like_ad(agent_id: str, ad_id: str, **kwargs) -> str:
    """
    Use this tool to express a 'like' for an advertisement.

    Args:
        agent_id (str): The ID of the agent performing the action.
        ad_id (str): The unique identifier of the ad being liked.

    Returns:
        str: A JSON string confirming the action was successful.
    """
    return json.dumps({"status": "success", "action": "like", "agent": agent_id, "ad": ad_id})

def agent_dislike_ad(agent_id: str, ad_id: str, **kwargs) -> str:
    """
    Use this tool to express a 'dislike' for an advertisement.

    Args:
        agent_id (str): The ID of the agent performing the action.
        ad_id (str): The unique identifier of the ad being disliked.

    Returns:
        str: A JSON string confirming the action was successful.
    """
    return json.dumps({"status": "success", "action": "dislike", "agent": agent_id, "ad": ad_id})

def agent_comment_ad(agent_id: str, ad_id: str, comment_text: str, **kwargs) -> str:
    """
    Use this tool to post a comment on an advertisement.

    Args:
        agent_id (str): The ID of the agent performing the action.
        ad_id (str): The unique identifier of the ad for the comment.
        comment_text (str): The content of the comment.

    Returns:
        str: A JSON string confirming the action was successful.
    """
    return json.dumps({"status": "success", "action": "comment", "agent": agent_id, "ad": ad_id, "comment": comment_text})

def agent_repost_ad(agent_id: str, ad_id: str, repost_reason: str, **kwargs) -> str:
    """
    Use this tool to repost an advertisement, similar to a retweet.

    Args:
        agent_id (str): The ID of the agent performing the action.
        ad_id (str): The unique identifier of the ad being reposted.
        repost_reason (str): The reason or commentary for the repost.

    Returns:
        str: A JSON string confirming the action was successful.
    """
    return json.dumps({"status": "success", "action": "repost", "agent": agent_id, "ad": ad_id, "reason": repost_reason})

def agent_ignore_ad(agent_id: str, ad_id: str, **kwargs) -> str:
    """
    Use this tool to ignore an advertisement without any engagement.
    This represents scrolling past or dismissing an ad without interaction.

    Args:
        agent_id (str): The ID of the agent performing the action.
        ad_id (str): The unique identifier of the ad being ignored.

    Returns:
        str: A JSON string confirming the action was successful.
    """
    return json.dumps({"status": "success", "action": "ignore", "agent": agent_id, "ad": ad_id})

def read_shared_knowledge(**kwargs) -> str:
    """
    Reads the shared knowledge base accessible to all agents.
    Use this to understand collective trends or shared personality traits.

    Returns:
        str: The content of the shared knowledge file.
    """
    SHARED_KNOWLEDGE_PATH = os.path.join(os.path.dirname(__file__), '..', 'data', 'shared_knowledge.txt')
    try:
        with open(SHARED_KNOWLEDGE_PATH, 'r') as f:
            return f.read()
    except FileNotFoundError:
        return "No shared knowledge found."

def write_shared_knowledge(content: str, **kwargs) -> str:
    """
    Writes or appends content to the shared knowledge base.
    Use this to contribute to the collective memory or shared personality.

    Args:
        content (str): The information to add to the shared knowledge base.

    Returns:
        str: A confirmation message.
    """
    SHARED_KNOWLEDGE_PATH = os.path.join(os.path.dirname(__file__), '..', 'data', 'shared_knowledge.txt')
    with open(SHARED_KNOWLEDGE_PATH, 'a') as f:
        f.write(f"\n{content}")
    return "Shared knowledge updated successfully."

# A list of all tool functions for easy registration
ALL_TOOLS_V2 = [
    agent_like_ad,
    agent_dislike_ad,
    agent_comment_ad,
    agent_repost_ad,
    agent_ignore_ad,
    read_shared_knowledge,
    write_shared_knowledge
] 
```

### backend/simulation.py

```python
import os
import sys
import time
import asyncio
import json
import re
from letta_client import Letta, MessageCreate
from dotenv import load_dotenv

# Load environment variables from .env file
load_dotenv(dotenv_path=os.path.join(os.path.dirname(__file__), '..', '.env'))

# --- Configuration ---
api_key = os.getenv("LETTA_API_KEY")
if not api_key:
    raise ValueError("LETTA_API_KEY not found in .env file.")
CLIENT = Letta(token=api_key)

def extract_json_from_string(text: str) -> dict:
    """
    Finds and parses the first valid JSON object within a string.
    Handles cases where the JSON is embedded in other text.
    """
    if not text or not text.strip():
        print(f"Warning: Empty or whitespace-only response received")
        return None
    
    # Try to find JSON object enclosed in curly braces
    # Look for the first { and the last } to handle nested objects
    start_idx = text.find('{')
    end_idx = text.rfind('}')
    
    if start_idx == -1 or end_idx == -1 or start_idx >= end_idx:
        print(f"Warning: No valid JSON structure found in text: '{text[:100]}...'")
        return None
    
    json_str = text[start_idx:end_idx + 1]
    
    try:
        return json.loads(json_str)
    except json.JSONDecodeError as e:
        print(f"Warning: Could not decode JSON from string: '{json_str[:100]}...', Error: {e}")
        
        # Try to clean up common issues
        # Remove any trailing commas before closing braces/brackets
        cleaned = re.sub(r',(\s*[}\]])', r'\1', json_str)
        try:
            return json.loads(cleaned)
        except json.JSONDecodeError:
            print(f"Warning: Even cleaned JSON failed to parse: '{cleaned[:100]}...'")
            return None

async def run_simulation_with_ad_copy(ad_copy: str):
    """Runs the simulation for all agents against a single ad, returning JSON results."""
    ad_id = "user_provided_ad"
    print(f"--- Starting Simulation for Ad: '{ad_id}' ---")

    # 1. Get all available agents
    try:
        agents = CLIENT.agents.list()
        if not agents:
            print("No agents found. Please create them first.")
            raise Exception("No agents available for simulation.")
        
    except Exception as e:
        print(f"Failed to connect to Letta server or fetch agent details. Error: {e}")
        raise e

    print(f"Found {len(agents)} agents. Presenting ad and collecting results...")

    # 2. Present the ad to each agent concurrently
    tasks = [run_agent_interaction(agent, ad_id, ad_copy) for agent in agents]
    results = await asyncio.gather(*tasks)

    # Filter out any None results from failed interactions
    successful_results = [res for res in results if res]
    
    print("\n--- Simulation Complete ---")
    print(f"Successfully collected {len(successful_results)} results.")
    return successful_results

async def run_agent_interaction(agent, ad_id: str, ad_content: str):
    """Presents an ad to a single agent and processes its response."""
    print(f"\n-> Presenting ad to agent: {agent.name} ({agent.id})")

    prompt = f"""
You are on a social media platform and you see the following ad.
Your name is {agent.name}. Your personality is stored in your 'persona' memory block.

Ad Content: "{ad_content}"

You must complete this task in TWO PHASES:

PHASE 1 - TAKE ACTIONS:
Based on your persona, use the provided tools to react to this ad. You can use one or more tools (e.g., like and comment).

PHASE 2 - PROVIDE ANALYSIS (MANDATORY):
After your tool calls, you MUST immediately provide a JSON analysis of your reaction.

Your JSON response must be a single line with no other text, starting with {{{{ and ending with }}}}.

**If you took multiple actions, for the "reaction" field in the JSON, choose the one that you feel is your PRIMARY reaction.** For example, if you liked and commented, and the comment is more significant, use "comment".

Required format:
{{"reaction": "primary_action", "confidence": 0-100, "reasoning": "why you reacted this way", "tags": ["keyword1", "keyword2"], "final_message": "your social media post"}}

The `reaction` value should be one of `like`, `dislike`, `comment`, `repost`, or `ignore`.

IMPORTANT: You MUST complete both phases. Do not stop after phase 1.

Example complete interaction:
1. [Agent uses tool: agent_like_ad]
2. [Agent uses tool: agent_comment_ad]
3. {{"reaction": "comment", "confidence": 90, "reasoning": "I liked it, but my main action is commenting to ask for more details.", "tags": ["eco", "fashion"], "final_message": "Love it! Can you provide more info on your ethical sourcing?"}}
"""
    try:
        # Send the prompt to the agent
        print(f"  - Sending prompt to {agent.name}...")
        response = CLIENT.agents.messages.create_stream(
            agent_id=agent.id,
            messages=[MessageCreate(role="user", content=prompt)],
        )
        
        # Track tool calls and content
        tool_calls = []
        response_content = ""
        
        for chunk in response:
            if chunk.message_type == "assistant_message" and chunk.content:
                response_content += chunk.content
            elif chunk.message_type == "tool_call_message":
                tool_name = chunk.tool_call.name
                tool_calls.append(tool_name)
                print(f"  - Tool Call by {agent.name}: {tool_name}")
        
        print(f"  - Tool calls made: {tool_calls}")
        print(f"  - Raw response from {agent.name} (length: {len(response_content)}): '{response_content}'")
        
        # If we got an empty response but tool calls were made, try to get a follow-up
        if not response_content.strip() and tool_calls:
            print(f"  - Agent {agent.name} made tool calls but gave empty response. Requesting JSON...")
            follow_up_stream = CLIENT.agents.messages.create_stream(
                agent_id=agent.id,
                messages=[MessageCreate(role="user", content="Please provide your JSON anal
[truncated — 2145 more characters]
```

[15 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]