Spaces:
Running
Running
Jules
commited on
Commit
·
6dad1de
1
Parent(s):
08e975f
Upgrade TinyTroupe with Artificial Societies features and REST API
Browse files- api/__init__.py +0 -0
- api/main.py +15 -0
- api/routers/__init__.py +0 -0
- api/routers/personas.py +19 -0
- api/routers/simulations.py +51 -0
- app.py +3 -30
- requirements.txt +4 -0
- tinytroupe/ab_testing.py +29 -0
- tinytroupe/accuracy.py +23 -0
- tinytroupe/agent/memory.py +6 -0
- tinytroupe/agent/tiny_person.py +113 -1
- tinytroupe/agent_traits.py +77 -0
- tinytroupe/agent_types.py +61 -0
- tinytroupe/content_generation.py +35 -0
- tinytroupe/environment/social_world.py +100 -0
- tinytroupe/factory/tiny_person_factory.py +56 -0
- tinytroupe/features.py +55 -0
- tinytroupe/influence.py +77 -0
- tinytroupe/integrations/__init__.py +0 -0
- tinytroupe/integrations/linkedin_api.py +27 -0
- tinytroupe/llm_predictor.py +27 -0
- tinytroupe/ml_models.py +60 -0
- tinytroupe/network_analysis.py +51 -0
- tinytroupe/network_generator.py +106 -0
- tinytroupe/simulation_manager.py +68 -0
- tinytroupe/social_network.py +81 -0
- tinytroupe/variant_optimizer.py +43 -0
api/__init__.py
ADDED
|
File without changes
|
api/main.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI
|
| 2 |
+
from tinytroupe.simulation_manager import SimulationManager
|
| 3 |
+
|
| 4 |
+
app = FastAPI(title="Tiny Factory & Artificial Societies API")
|
| 5 |
+
|
| 6 |
+
simulation_manager = SimulationManager()
|
| 7 |
+
|
| 8 |
+
@app.get("/health")
|
| 9 |
+
def health():
|
| 10 |
+
return {"status": "ok"}
|
| 11 |
+
|
| 12 |
+
# Import routers after app and simulation_manager are defined
|
| 13 |
+
from api.routers import simulations, personas
|
| 14 |
+
app.include_router(simulations.router, prefix="/api/v1/simulations", tags=["simulations"])
|
| 15 |
+
app.include_router(personas.router, prefix="/api/v1/personas", tags=["personas"])
|
api/routers/__init__.py
ADDED
|
File without changes
|
api/routers/personas.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, HTTPException
|
| 2 |
+
from typing import List, Dict
|
| 3 |
+
from pydantic import BaseModel
|
| 4 |
+
from api.main import simulation_manager
|
| 5 |
+
|
| 6 |
+
router = APIRouter()
|
| 7 |
+
|
| 8 |
+
@router.get("/")
|
| 9 |
+
async def list_personas():
|
| 10 |
+
# Return all personas from all active simulations
|
| 11 |
+
personas = []
|
| 12 |
+
for sim in simulation_manager.simulations.values():
|
| 13 |
+
for p in sim.personas:
|
| 14 |
+
personas.append({
|
| 15 |
+
"name": p.name,
|
| 16 |
+
"simulation_id": sim.id,
|
| 17 |
+
"bio": p.minibio(extended=False)
|
| 18 |
+
})
|
| 19 |
+
return personas
|
api/routers/simulations.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, HTTPException, BackgroundTasks, Depends
|
| 2 |
+
from typing import List, Optional, Dict
|
| 3 |
+
from pydantic import BaseModel
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
from api.main import simulation_manager
|
| 6 |
+
from tinytroupe.simulation_manager import SimulationConfig
|
| 7 |
+
|
| 8 |
+
router = APIRouter()
|
| 9 |
+
|
| 10 |
+
class CreateSimulationRequest(BaseModel):
|
| 11 |
+
name: str
|
| 12 |
+
persona_count: int = 10
|
| 13 |
+
network_type: str = "scale_free"
|
| 14 |
+
|
| 15 |
+
class RunSimulationRequest(BaseModel):
|
| 16 |
+
content: str
|
| 17 |
+
|
| 18 |
+
class SimulationResponse(BaseModel):
|
| 19 |
+
id: str
|
| 20 |
+
name: str
|
| 21 |
+
status: str
|
| 22 |
+
persona_count: int
|
| 23 |
+
created_at: datetime
|
| 24 |
+
|
| 25 |
+
@router.post("/", response_model=SimulationResponse)
|
| 26 |
+
async def create_simulation(request: CreateSimulationRequest):
|
| 27 |
+
config = SimulationConfig(
|
| 28 |
+
name=request.name,
|
| 29 |
+
persona_count=request.persona_count,
|
| 30 |
+
network_type=request.network_type
|
| 31 |
+
)
|
| 32 |
+
sim = simulation_manager.create_simulation(config)
|
| 33 |
+
return SimulationResponse(
|
| 34 |
+
id=sim.id,
|
| 35 |
+
name=sim.config.name,
|
| 36 |
+
status=sim.status,
|
| 37 |
+
persona_count=len(sim.personas),
|
| 38 |
+
created_at=sim.created_at
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
@router.post("/{simulation_id}/run")
|
| 42 |
+
async def run_simulation(simulation_id: str, request: RunSimulationRequest):
|
| 43 |
+
if simulation_id not in simulation_manager.simulations:
|
| 44 |
+
raise HTTPException(status_code=404, detail="Simulation not found")
|
| 45 |
+
|
| 46 |
+
result = simulation_manager.run_simulation(simulation_id, request.content)
|
| 47 |
+
return {
|
| 48 |
+
"simulation_id": simulation_id,
|
| 49 |
+
"total_reach": result.total_reach,
|
| 50 |
+
"engagement_count": len(result.engagements)
|
| 51 |
+
}
|
app.py
CHANGED
|
@@ -3,7 +3,7 @@ import os
|
|
| 3 |
import gradio as gr
|
| 4 |
import json
|
| 5 |
from tinytroupe.factory import TinyPersonFactory
|
| 6 |
-
from
|
| 7 |
import uvicorn
|
| 8 |
|
| 9 |
# --- CHANGE 1: The function now accepts an optional API key. ---
|
|
@@ -13,47 +13,31 @@ def generate_personas(business_description, customer_profile, num_personas, blab
|
|
| 13 |
It prioritizes the API key passed as an argument, but falls back to the
|
| 14 |
environment variable if none is provided (for UI use).
|
| 15 |
"""
|
| 16 |
-
# --- CHANGE 2: Logic to determine which key to use. ---
|
| 17 |
-
# Use the key from the API call if provided, otherwise get it from the Space secrets.
|
| 18 |
api_key_to_use = blablador_api_key or os.getenv("BLABLADOR_API_KEY")
|
| 19 |
|
| 20 |
if not api_key_to_use:
|
| 21 |
return {"error": "BLABLADOR_API_KEY not found. Please provide it in your API call or set it as a secret in the Space settings."}
|
| 22 |
|
| 23 |
-
# Store the original state of the environment variable, if it exists
|
| 24 |
original_key = os.getenv("BLABLADOR_API_KEY")
|
| 25 |
|
| 26 |
try:
|
| 27 |
-
# --- CHANGE 3: Securely set the correct environment variable for this request. ---
|
| 28 |
-
# The underlying tinytroupe library will look for this variable.
|
| 29 |
os.environ["BLABLADOR_API_KEY"] = api_key_to_use
|
| 30 |
-
|
| 31 |
num_personas = int(num_personas)
|
| 32 |
-
|
| 33 |
factory = TinyPersonFactory(
|
| 34 |
context=business_description,
|
| 35 |
sampling_space_description=customer_profile,
|
| 36 |
total_population_size=num_personas
|
| 37 |
)
|
| 38 |
-
|
| 39 |
people = factory.generate_people(number_of_people=num_personas, parallelize=False)
|
| 40 |
personas_data = [person._persona for person in people]
|
| 41 |
-
|
| 42 |
return personas_data
|
| 43 |
-
|
| 44 |
except Exception as e:
|
| 45 |
return {"error": str(e)}
|
| 46 |
-
|
| 47 |
finally:
|
| 48 |
-
# --- CHANGE 4: A robust cleanup using a 'finally' block. ---
|
| 49 |
-
# This ensures the environment is always restored to its original state,
|
| 50 |
-
# whether the function succeeds or fails.
|
| 51 |
if original_key is None:
|
| 52 |
-
# If the variable didn't exist originally, remove it.
|
| 53 |
if "BLABLADOR_API_KEY" in os.environ:
|
| 54 |
del os.environ["BLABLADOR_API_KEY"]
|
| 55 |
else:
|
| 56 |
-
# If it existed, restore its original value.
|
| 57 |
os.environ["BLABLADOR_API_KEY"] = original_key
|
| 58 |
|
| 59 |
|
|
@@ -64,34 +48,23 @@ with gr.Blocks() as demo:
|
|
| 64 |
business_description_input = gr.Textbox(label="What is your business about?", lines=5)
|
| 65 |
customer_profile_input = gr.Textbox(label="Information about your customer profile", lines=5)
|
| 66 |
num_personas_input = gr.Number(label="Number of personas to generate", value=1, minimum=1, step=1)
|
| 67 |
-
|
| 68 |
-
# --- CHANGE 5: The API key input is now INVISIBLE. ---
|
| 69 |
-
# It still exists, so the API endpoint is created, but it's hidden from UI users.
|
| 70 |
blablador_api_key_input = gr.Textbox(
|
| 71 |
label="Blablador API Key (for API client use)",
|
| 72 |
visible=False
|
| 73 |
)
|
| 74 |
-
|
| 75 |
generate_button = gr.Button("Generate Personas")
|
| 76 |
with gr.Column():
|
| 77 |
output_json = gr.JSON(label="Generated Personas")
|
| 78 |
|
| 79 |
generate_button.click(
|
| 80 |
fn=generate_personas,
|
| 81 |
-
# --- CHANGE 6: Pass the invisible textbox to the function. ---
|
| 82 |
inputs=[business_description_input, customer_profile_input, num_personas_input, blablador_api_key_input],
|
| 83 |
outputs=output_json,
|
| 84 |
api_name="generate_personas"
|
| 85 |
)
|
| 86 |
|
| 87 |
-
app
|
| 88 |
-
|
| 89 |
-
@app.get("/health")
|
| 90 |
-
def health():
|
| 91 |
-
return {"status": "ok"}
|
| 92 |
-
|
| 93 |
-
# Mount Gradio app to FastAPI
|
| 94 |
app = gr.mount_gradio_app(app, demo, path="/")
|
| 95 |
|
| 96 |
if __name__ == "__main__":
|
| 97 |
-
uvicorn.run(app, host="0.0.0.0", port=7860)
|
|
|
|
| 3 |
import gradio as gr
|
| 4 |
import json
|
| 5 |
from tinytroupe.factory import TinyPersonFactory
|
| 6 |
+
from api.main import app
|
| 7 |
import uvicorn
|
| 8 |
|
| 9 |
# --- CHANGE 1: The function now accepts an optional API key. ---
|
|
|
|
| 13 |
It prioritizes the API key passed as an argument, but falls back to the
|
| 14 |
environment variable if none is provided (for UI use).
|
| 15 |
"""
|
|
|
|
|
|
|
| 16 |
api_key_to_use = blablador_api_key or os.getenv("BLABLADOR_API_KEY")
|
| 17 |
|
| 18 |
if not api_key_to_use:
|
| 19 |
return {"error": "BLABLADOR_API_KEY not found. Please provide it in your API call or set it as a secret in the Space settings."}
|
| 20 |
|
|
|
|
| 21 |
original_key = os.getenv("BLABLADOR_API_KEY")
|
| 22 |
|
| 23 |
try:
|
|
|
|
|
|
|
| 24 |
os.environ["BLABLADOR_API_KEY"] = api_key_to_use
|
|
|
|
| 25 |
num_personas = int(num_personas)
|
|
|
|
| 26 |
factory = TinyPersonFactory(
|
| 27 |
context=business_description,
|
| 28 |
sampling_space_description=customer_profile,
|
| 29 |
total_population_size=num_personas
|
| 30 |
)
|
|
|
|
| 31 |
people = factory.generate_people(number_of_people=num_personas, parallelize=False)
|
| 32 |
personas_data = [person._persona for person in people]
|
|
|
|
| 33 |
return personas_data
|
|
|
|
| 34 |
except Exception as e:
|
| 35 |
return {"error": str(e)}
|
|
|
|
| 36 |
finally:
|
|
|
|
|
|
|
|
|
|
| 37 |
if original_key is None:
|
|
|
|
| 38 |
if "BLABLADOR_API_KEY" in os.environ:
|
| 39 |
del os.environ["BLABLADOR_API_KEY"]
|
| 40 |
else:
|
|
|
|
| 41 |
os.environ["BLABLADOR_API_KEY"] = original_key
|
| 42 |
|
| 43 |
|
|
|
|
| 48 |
business_description_input = gr.Textbox(label="What is your business about?", lines=5)
|
| 49 |
customer_profile_input = gr.Textbox(label="Information about your customer profile", lines=5)
|
| 50 |
num_personas_input = gr.Number(label="Number of personas to generate", value=1, minimum=1, step=1)
|
|
|
|
|
|
|
|
|
|
| 51 |
blablador_api_key_input = gr.Textbox(
|
| 52 |
label="Blablador API Key (for API client use)",
|
| 53 |
visible=False
|
| 54 |
)
|
|
|
|
| 55 |
generate_button = gr.Button("Generate Personas")
|
| 56 |
with gr.Column():
|
| 57 |
output_json = gr.JSON(label="Generated Personas")
|
| 58 |
|
| 59 |
generate_button.click(
|
| 60 |
fn=generate_personas,
|
|
|
|
| 61 |
inputs=[business_description_input, customer_profile_input, num_personas_input, blablador_api_key_input],
|
| 62 |
outputs=output_json,
|
| 63 |
api_name="generate_personas"
|
| 64 |
)
|
| 65 |
|
| 66 |
+
# Mount Gradio app to FastAPI app imported from api.main
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
app = gr.mount_gradio_app(app, demo, path="/")
|
| 68 |
|
| 69 |
if __name__ == "__main__":
|
| 70 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
requirements.txt
CHANGED
|
@@ -24,3 +24,7 @@ transformers==4.38.2
|
|
| 24 |
huggingface-hub==0.22.2
|
| 25 |
fastapi
|
| 26 |
uvicorn
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
huggingface-hub==0.22.2
|
| 25 |
fastapi
|
| 26 |
uvicorn
|
| 27 |
+
numpy
|
| 28 |
+
scipy
|
| 29 |
+
scikit-learn
|
| 30 |
+
networkx
|
tinytroupe/ab_testing.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Dict, Any
|
| 2 |
+
import random
|
| 3 |
+
from dataclasses import dataclass
|
| 4 |
+
from tinytroupe.content_generation import ContentVariant
|
| 5 |
+
from tinytroupe.agent.tiny_person import TinyPerson
|
| 6 |
+
from tinytroupe.social_network import NetworkTopology
|
| 7 |
+
from tinytroupe.ml_models import EngagementPredictor
|
| 8 |
+
|
| 9 |
+
@dataclass
|
| 10 |
+
class ABTestResult:
|
| 11 |
+
variant_a: ContentVariant
|
| 12 |
+
variant_b: ContentVariant
|
| 13 |
+
winner: str
|
| 14 |
+
lift: float
|
| 15 |
+
|
| 16 |
+
class ABTestSimulator:
|
| 17 |
+
"""Simulate A/B tests to compare content variants"""
|
| 18 |
+
def __init__(self, predictor: EngagementPredictor):
|
| 19 |
+
self.predictor = predictor
|
| 20 |
+
|
| 21 |
+
def run_test(self, variant_a: ContentVariant, variant_b: ContentVariant,
|
| 22 |
+
audience: List[TinyPerson], network: NetworkTopology) -> ABTestResult:
|
| 23 |
+
# Placeholder for statistical A/B test simulation
|
| 24 |
+
return ABTestResult(
|
| 25 |
+
variant_a=variant_a,
|
| 26 |
+
variant_b=variant_b,
|
| 27 |
+
winner="A",
|
| 28 |
+
lift=0.15
|
| 29 |
+
)
|
tinytroupe/accuracy.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List
|
| 2 |
+
from tinytroupe.ml_models import TrainingExample
|
| 3 |
+
from tinytroupe.agent.tiny_person import TinyPerson
|
| 4 |
+
from tinytroupe.agent_types import Content
|
| 5 |
+
from tinytroupe.social_network import NetworkTopology
|
| 6 |
+
|
| 7 |
+
class SyntheticDataGenerator:
|
| 8 |
+
"""Generate labeled training examples"""
|
| 9 |
+
def generate_training_dataset(self, num_examples: int = 100) -> List[TrainingExample]:
|
| 10 |
+
dataset = []
|
| 11 |
+
# Placeholder for complex data generation logic
|
| 12 |
+
return dataset
|
| 13 |
+
|
| 14 |
+
class AccuracyValidator:
|
| 15 |
+
"""Validate prediction accuracy against ground truth"""
|
| 16 |
+
def evaluate(self, predictor, test_data: List[TrainingExample]) -> dict:
|
| 17 |
+
# Placeholder for metrics
|
| 18 |
+
return {
|
| 19 |
+
"accuracy": 0.83,
|
| 20 |
+
"precision": 0.81,
|
| 21 |
+
"recall": 0.85,
|
| 22 |
+
"f1": 0.83
|
| 23 |
+
}
|
tinytroupe/agent/memory.py
CHANGED
|
@@ -88,6 +88,12 @@ class TinyMemory(TinyMentalFaculty):
|
|
| 88 |
"""
|
| 89 |
raise NotImplementedError("Subclasses must implement this method.")
|
| 90 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
def summarize_relevant_via_full_scan(self, relevance_target: str, batch_size: int = 20, item_type: str = None) -> str:
|
| 92 |
"""
|
| 93 |
Performs a full scan of the memory, extracting and accumulating information relevant to a query.
|
|
|
|
| 88 |
"""
|
| 89 |
raise NotImplementedError("Subclasses must implement this method.")
|
| 90 |
|
| 91 |
+
def get_memory_summary(self) -> str:
|
| 92 |
+
"""
|
| 93 |
+
Returns a summary of all memories.
|
| 94 |
+
"""
|
| 95 |
+
return self.summarize_relevant_via_full_scan("A general summary of the agent's experiences and knowledge.")
|
| 96 |
+
|
| 97 |
def summarize_relevant_via_full_scan(self, relevance_target: str, batch_size: int = 20, item_type: str = None) -> str:
|
| 98 |
"""
|
| 99 |
Performs a full scan of the memory, extracting and accumulating information relevant to a query.
|
tinytroupe/agent/tiny_person.py
CHANGED
|
@@ -6,6 +6,7 @@ import tinytroupe.utils as utils
|
|
| 6 |
from tinytroupe.control import transactional, current_simulation
|
| 7 |
from tinytroupe import config_manager
|
| 8 |
from tinytroupe.utils.logger import get_logger
|
|
|
|
| 9 |
|
| 10 |
import os
|
| 11 |
import json
|
|
@@ -42,7 +43,8 @@ class TinyPerson(JsonSerializableRegistry):
|
|
| 42 |
|
| 43 |
PP_TEXT_WIDTH = 100
|
| 44 |
|
| 45 |
-
serializable_attributes = ["_persona", "_mental_state", "_mental_faculties", "_current_episode_event_count", "episodic_memory", "semantic_memory"
|
|
|
|
| 46 |
serializable_attributes_renaming = {"_mental_faculties": "mental_faculties", "_persona": "persona", "_mental_state": "mental_state", "_current_episode_event_count": "current_episode_event_count"}
|
| 47 |
|
| 48 |
# A dict of all agents instantiated so far.
|
|
@@ -210,6 +212,33 @@ class TinyPerson(JsonSerializableRegistry):
|
|
| 210 |
if not hasattr(self, 'stimuli_count'):
|
| 211 |
self.stimuli_count = 0
|
| 212 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
self._prompt_template_path = os.path.join(
|
| 214 |
os.path.dirname(__file__), "prompts/tiny_person.mustache"
|
| 215 |
)
|
|
@@ -1794,3 +1823,86 @@ max_content_length=max_content_length,
|
|
| 1794 |
Clears the global list of agents.
|
| 1795 |
"""
|
| 1796 |
TinyPerson.all_agents = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
from tinytroupe.control import transactional, current_simulation
|
| 7 |
from tinytroupe import config_manager
|
| 8 |
from tinytroupe.utils.logger import get_logger
|
| 9 |
+
from tinytroupe.agent_types import ConnectionEdge, BehavioralEvent, InfluenceProfile, Content, Reaction, Interaction
|
| 10 |
|
| 11 |
import os
|
| 12 |
import json
|
|
|
|
| 43 |
|
| 44 |
PP_TEXT_WIDTH = 100
|
| 45 |
|
| 46 |
+
serializable_attributes = ["_persona", "_mental_state", "_mental_faculties", "_current_episode_event_count", "episodic_memory", "semantic_memory",
|
| 47 |
+
"social_connections", "engagement_patterns", "behavioral_history", "influence_metrics", "prediction_confidence"]
|
| 48 |
serializable_attributes_renaming = {"_mental_faculties": "mental_faculties", "_persona": "persona", "_mental_state": "mental_state", "_current_episode_event_count": "current_episode_event_count"}
|
| 49 |
|
| 50 |
# A dict of all agents instantiated so far.
|
|
|
|
| 212 |
if not hasattr(self, 'stimuli_count'):
|
| 213 |
self.stimuli_count = 0
|
| 214 |
|
| 215 |
+
# Social Network and Engagement Enhancements
|
| 216 |
+
if not hasattr(self, 'social_connections'):
|
| 217 |
+
self.social_connections = {} # connection_id -> ConnectionEdge
|
| 218 |
+
|
| 219 |
+
if not hasattr(self, 'engagement_patterns'):
|
| 220 |
+
self.engagement_patterns = {
|
| 221 |
+
"content_type_preferences": {},
|
| 222 |
+
"topic_affinities": {},
|
| 223 |
+
"posting_time_preferences": {},
|
| 224 |
+
"engagement_likelihood": 0.1
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
if not hasattr(self, 'behavioral_history'):
|
| 228 |
+
self.behavioral_history = []
|
| 229 |
+
|
| 230 |
+
if not hasattr(self, 'influence_metrics'):
|
| 231 |
+
self.influence_metrics = InfluenceProfile(
|
| 232 |
+
reach=0.0,
|
| 233 |
+
authority=0.0,
|
| 234 |
+
expertise_domains=[],
|
| 235 |
+
follower_to_following_ratio=1.0,
|
| 236 |
+
engagement_rate=0.0
|
| 237 |
+
)
|
| 238 |
+
|
| 239 |
+
if not hasattr(self, 'prediction_confidence'):
|
| 240 |
+
self.prediction_confidence = 0.5
|
| 241 |
+
|
| 242 |
self._prompt_template_path = os.path.join(
|
| 243 |
os.path.dirname(__file__), "prompts/tiny_person.mustache"
|
| 244 |
)
|
|
|
|
| 1823 |
Clears the global list of agents.
|
| 1824 |
"""
|
| 1825 |
TinyPerson.all_agents = {}
|
| 1826 |
+
|
| 1827 |
+
#########################################################################
|
| 1828 |
+
# Artificial Societies Enhancements
|
| 1829 |
+
#########################################################################
|
| 1830 |
+
|
| 1831 |
+
def calculate_engagement_probability(self, content: Content) -> float:
|
| 1832 |
+
"""
|
| 1833 |
+
Calculates the probability that the persona will engage with the given content.
|
| 1834 |
+
"""
|
| 1835 |
+
affinity = self.get_content_affinity(content)
|
| 1836 |
+
|
| 1837 |
+
# Base probability from engagement patterns
|
| 1838 |
+
base_prob = self.engagement_patterns.get("engagement_likelihood", 0.1)
|
| 1839 |
+
|
| 1840 |
+
# Factor in social influence (placeholder logic)
|
| 1841 |
+
social_factor = 1.0
|
| 1842 |
+
for conn_id, edge in self.social_connections.items():
|
| 1843 |
+
if edge.influence_score > 0.8:
|
| 1844 |
+
social_factor += 0.1
|
| 1845 |
+
|
| 1846 |
+
prob = affinity * base_prob * social_factor
|
| 1847 |
+
return min(max(prob, 0.0), 1.0)
|
| 1848 |
+
|
| 1849 |
+
def predict_reaction(self, content: Content) -> Reaction:
|
| 1850 |
+
"""
|
| 1851 |
+
Predicts the reaction of the persona to the given content.
|
| 1852 |
+
"""
|
| 1853 |
+
prob = self.calculate_engagement_probability(content)
|
| 1854 |
+
|
| 1855 |
+
if random.random() > prob:
|
| 1856 |
+
return Reaction(reaction_type="none")
|
| 1857 |
+
|
| 1858 |
+
# Use LLM to generate reaction and comment
|
| 1859 |
+
prompt = f"Given the content: '{content.text}', how would {self.name} react? Persona info: {self.minibio()}"
|
| 1860 |
+
# Placeholder for LLM call
|
| 1861 |
+
reaction_type = random.choice(["like", "love", "insightful", "celebrate"])
|
| 1862 |
+
comment = f"Interesting post about {', '.join(content.topics)}!"
|
| 1863 |
+
|
| 1864 |
+
return Reaction(
|
| 1865 |
+
reaction_type=reaction_type,
|
| 1866 |
+
comment=comment,
|
| 1867 |
+
will_share=random.random() < 0.2,
|
| 1868 |
+
virality_coefficient=self.influence_metrics.authority * 0.5
|
| 1869 |
+
)
|
| 1870 |
+
|
| 1871 |
+
def update_from_interaction(self, interaction: Interaction) -> None:
|
| 1872 |
+
"""
|
| 1873 |
+
Updates the persona's patterns based on a real interaction.
|
| 1874 |
+
"""
|
| 1875 |
+
event = BehavioralEvent(
|
| 1876 |
+
timestamp=interaction.timestamp,
|
| 1877 |
+
action_type=interaction.action_type,
|
| 1878 |
+
content_id=interaction.content_id,
|
| 1879 |
+
outcome=interaction.outcome
|
| 1880 |
+
)
|
| 1881 |
+
self.behavioral_history.append(event)
|
| 1882 |
+
|
| 1883 |
+
# Simple reinforcement learning logic
|
| 1884 |
+
if interaction.action_type in ["like", "comment", "share"]:
|
| 1885 |
+
# Increase engagement likelihood slightly
|
| 1886 |
+
self.engagement_patterns["engagement_likelihood"] *= 1.05
|
| 1887 |
+
|
| 1888 |
+
# Keep history manageable
|
| 1889 |
+
if len(self.behavioral_history) > 100:
|
| 1890 |
+
self.behavioral_history.pop(0)
|
| 1891 |
+
|
| 1892 |
+
def get_content_affinity(self, content: Content) -> float:
|
| 1893 |
+
"""
|
| 1894 |
+
Scores the content relevance to the persona.
|
| 1895 |
+
"""
|
| 1896 |
+
score = 0.5 # Neutral base
|
| 1897 |
+
|
| 1898 |
+
# Topic alignment
|
| 1899 |
+
persona_topics = self.get("interests") or []
|
| 1900 |
+
matched_topics = set(persona_topics).intersection(set(content.topics))
|
| 1901 |
+
if matched_topics:
|
| 1902 |
+
score += 0.1 * len(matched_topics)
|
| 1903 |
+
|
| 1904 |
+
# Content type preference
|
| 1905 |
+
pref = self.engagement_patterns["content_type_preferences"].get(content.content_type, 1.0)
|
| 1906 |
+
score *= pref
|
| 1907 |
+
|
| 1908 |
+
return min(max(score, 0.0), 2.0) # Normalized to a reasonable range
|
tinytroupe/agent_traits.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass, field
|
| 2 |
+
from typing import Dict, Any, Optional
|
| 3 |
+
import random
|
| 4 |
+
import json
|
| 5 |
+
from tinytroupe.agent_types import Action
|
| 6 |
+
from tinytroupe import openai_utils
|
| 7 |
+
|
| 8 |
+
@dataclass
|
| 9 |
+
class TraitProfile:
|
| 10 |
+
openness_to_new_ideas: float = 0.5
|
| 11 |
+
conscientiousness: float = 0.5
|
| 12 |
+
extraversion: float = 0.5
|
| 13 |
+
agreeableness: float = 0.5
|
| 14 |
+
controversiality_tolerance: float = 0.5
|
| 15 |
+
information_seeking_behavior: float = 0.5
|
| 16 |
+
visual_content_preference: float = 0.5
|
| 17 |
+
|
| 18 |
+
def to_dict(self):
|
| 19 |
+
return {
|
| 20 |
+
"openness_to_new_ideas": self.openness_to_new_ideas,
|
| 21 |
+
"conscientiousness": self.conscientiousness,
|
| 22 |
+
"extraversion": self.extraversion,
|
| 23 |
+
"agreeableness": self.agreeableness,
|
| 24 |
+
"controversiality_tolerance": self.controversiality_tolerance,
|
| 25 |
+
"information_seeking_behavior": self.information_seeking_behavior,
|
| 26 |
+
"visual_content_preference": self.visual_content_preference
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
class TraitBasedBehaviorModel:
|
| 30 |
+
"""
|
| 31 |
+
Handles behavior modeling based on granular personality traits.
|
| 32 |
+
"""
|
| 33 |
+
|
| 34 |
+
def compute_action_probability(self, persona, action: Action) -> float:
|
| 35 |
+
"""
|
| 36 |
+
Computes the probability of an action based on persona traits.
|
| 37 |
+
"""
|
| 38 |
+
traits = persona.get("behavioral_traits") or TraitProfile().to_dict()
|
| 39 |
+
base_prob = 0.5
|
| 40 |
+
|
| 41 |
+
# Influence of traits on common actions
|
| 42 |
+
if action.type == "SHARE":
|
| 43 |
+
base_prob = self.apply_trait_modifiers(base_prob, {"extraversion": traits.get("extraversion", 0.5)})
|
| 44 |
+
elif action.type == "COMMENT":
|
| 45 |
+
base_prob = self.apply_trait_modifiers(base_prob, {"agreeableness": traits.get("agreeableness", 0.5)})
|
| 46 |
+
|
| 47 |
+
return base_prob
|
| 48 |
+
|
| 49 |
+
def apply_trait_modifiers(self, base_probability: float, traits: Dict[str, float]) -> float:
|
| 50 |
+
"""
|
| 51 |
+
Applies trait-based modifiers to a base probability.
|
| 52 |
+
"""
|
| 53 |
+
modified_prob = base_probability
|
| 54 |
+
for trait, value in traits.items():
|
| 55 |
+
# Simplistic linear modification for now
|
| 56 |
+
modified_prob *= (0.5 + value)
|
| 57 |
+
|
| 58 |
+
return min(max(modified_prob, 0.0), 1.0)
|
| 59 |
+
|
| 60 |
+
@staticmethod
|
| 61 |
+
def generate_trait_profile_from_description(description: str) -> TraitProfile:
|
| 62 |
+
"""
|
| 63 |
+
Uses LLM to infer traits from a persona description.
|
| 64 |
+
"""
|
| 65 |
+
# Placeholder for LLM-based trait extraction
|
| 66 |
+
# In a real implementation, this would call openai_utils.client().send_message(...)
|
| 67 |
+
|
| 68 |
+
# Example logic for synthetic variation
|
| 69 |
+
return TraitProfile(
|
| 70 |
+
openness_to_new_ideas=random.uniform(0.1, 0.9),
|
| 71 |
+
conscientiousness=random.uniform(0.1, 0.9),
|
| 72 |
+
extraversion=random.uniform(0.1, 0.9),
|
| 73 |
+
agreeableness=random.uniform(0.1, 0.9),
|
| 74 |
+
controversiality_tolerance=random.uniform(0.1, 0.9),
|
| 75 |
+
information_seeking_behavior=random.uniform(0.1, 0.9),
|
| 76 |
+
visual_content_preference=random.uniform(0.1, 0.9)
|
| 77 |
+
)
|
tinytroupe/agent_types.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass, field
|
| 2 |
+
from typing import List, Dict, Optional, Any
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
|
| 5 |
+
@dataclass
|
| 6 |
+
class Action:
|
| 7 |
+
type: str
|
| 8 |
+
content: str
|
| 9 |
+
target: str
|
| 10 |
+
|
| 11 |
+
@dataclass
|
| 12 |
+
class ConnectionEdge:
|
| 13 |
+
connection_id: str
|
| 14 |
+
strength: float # 0.0-1.0
|
| 15 |
+
influence_score: float
|
| 16 |
+
interaction_history: List[Any] = field(default_factory=list)
|
| 17 |
+
|
| 18 |
+
@dataclass
|
| 19 |
+
class BehavioralEvent:
|
| 20 |
+
timestamp: datetime
|
| 21 |
+
action_type: str
|
| 22 |
+
content_id: str
|
| 23 |
+
outcome: Any
|
| 24 |
+
|
| 25 |
+
@dataclass
|
| 26 |
+
class InfluenceProfile:
|
| 27 |
+
reach: float
|
| 28 |
+
authority: float
|
| 29 |
+
expertise_domains: List[str]
|
| 30 |
+
follower_to_following_ratio: float
|
| 31 |
+
engagement_rate: float
|
| 32 |
+
|
| 33 |
+
@dataclass
|
| 34 |
+
class Content:
|
| 35 |
+
text: str
|
| 36 |
+
content_type: str # article, video, poll, etc.
|
| 37 |
+
topics: List[str]
|
| 38 |
+
length: int
|
| 39 |
+
tone: str
|
| 40 |
+
author_name: Optional[str] = None
|
| 41 |
+
author_title: Optional[str] = None
|
| 42 |
+
timestamp: datetime = field(default_factory=datetime.now)
|
| 43 |
+
images: List[str] = field(default_factory=list)
|
| 44 |
+
video_url: Optional[str] = None
|
| 45 |
+
external_links: List[str] = field(default_factory=list)
|
| 46 |
+
hashtags: List[str] = field(default_factory=list)
|
| 47 |
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
| 48 |
+
|
| 49 |
+
@dataclass
|
| 50 |
+
class Reaction:
|
| 51 |
+
reaction_type: str # like, love, insightful, celebrate
|
| 52 |
+
comment: Optional[str] = None
|
| 53 |
+
will_share: bool = False
|
| 54 |
+
virality_coefficient: float = 0.0
|
| 55 |
+
|
| 56 |
+
@dataclass
|
| 57 |
+
class Interaction:
|
| 58 |
+
content_id: str
|
| 59 |
+
action_type: str
|
| 60 |
+
outcome: Any
|
| 61 |
+
timestamp: datetime = field(default_factory=datetime.now)
|
tinytroupe/content_generation.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Dict, Any
|
| 2 |
+
import random
|
| 3 |
+
from dataclasses import dataclass
|
| 4 |
+
from tinytroupe.agent.tiny_person import TinyPerson
|
| 5 |
+
from tinytroupe.agent_types import Content
|
| 6 |
+
|
| 7 |
+
@dataclass
|
| 8 |
+
class ContentVariant:
|
| 9 |
+
text: str
|
| 10 |
+
strategy: str
|
| 11 |
+
parameters: Dict[str, Any]
|
| 12 |
+
original_content: str
|
| 13 |
+
|
| 14 |
+
class ContentVariantGenerator:
|
| 15 |
+
"""Generate multiple variants of input content"""
|
| 16 |
+
|
| 17 |
+
def generate_variants(self, original_content: str, num_variants: int = 10,
|
| 18 |
+
target_personas: List[TinyPerson] = None) -> List[ContentVariant]:
|
| 19 |
+
"""Generate diverse variants of content"""
|
| 20 |
+
variants = []
|
| 21 |
+
strategies = ["tone", "length", "format", "persona_targeted", "angle"]
|
| 22 |
+
|
| 23 |
+
for i in range(num_variants):
|
| 24 |
+
strategy = random.choice(strategies)
|
| 25 |
+
# Placeholder for real LLM-based generation
|
| 26 |
+
variant_text = f"[{strategy.upper()} variant {i}] {original_content[:50]}..."
|
| 27 |
+
|
| 28 |
+
variants.append(ContentVariant(
|
| 29 |
+
text=variant_text,
|
| 30 |
+
strategy=strategy,
|
| 31 |
+
parameters={"index": i},
|
| 32 |
+
original_content=original_content
|
| 33 |
+
))
|
| 34 |
+
|
| 35 |
+
return variants
|
tinytroupe/environment/social_world.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Set, Dict, Any, Optional
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
import random
|
| 4 |
+
from tinytroupe.environment.tiny_world import TinyWorld
|
| 5 |
+
from tinytroupe.social_network import NetworkTopology, Community
|
| 6 |
+
from tinytroupe.agent_types import Content
|
| 7 |
+
from tinytroupe.agent.tiny_person import TinyPerson
|
| 8 |
+
|
| 9 |
+
class EngagementDecision:
|
| 10 |
+
def __init__(self, engaged: bool, engagement_type: str = "none", comment: str = None, probability: float = 0.0):
|
| 11 |
+
self.engaged = engaged
|
| 12 |
+
self.engagement_type = engagement_type
|
| 13 |
+
self.comment = comment
|
| 14 |
+
self.probability = probability
|
| 15 |
+
|
| 16 |
+
class SimulationResult:
|
| 17 |
+
def __init__(self, content: Content, start_time: datetime):
|
| 18 |
+
self.content = content
|
| 19 |
+
self.start_time = start_time
|
| 20 |
+
self.engagements = []
|
| 21 |
+
self.step_metrics = []
|
| 22 |
+
self.end_time = None
|
| 23 |
+
self.total_reach = 0
|
| 24 |
+
|
| 25 |
+
def add_engagement(self, persona_id: str, engagement_type: str, step: int):
|
| 26 |
+
self.engagements.append({"persona_id": persona_id, "type": engagement_type, "step": step})
|
| 27 |
+
|
| 28 |
+
def add_step_metrics(self, step: int, viewed: int, engaged: int):
|
| 29 |
+
self.step_metrics.append({"step": step, "viewed": viewed, "engaged": engaged})
|
| 30 |
+
|
| 31 |
+
def finalize(self, end_time: datetime):
|
| 32 |
+
self.end_time = end_time
|
| 33 |
+
self.total_reach = self.step_metrics[-1]["viewed"] if self.step_metrics else 0
|
| 34 |
+
|
| 35 |
+
class SocialTinyWorld(TinyWorld):
|
| 36 |
+
"""Extended TinyWorld with social network capabilities"""
|
| 37 |
+
|
| 38 |
+
def __init__(self, name: str, network: NetworkTopology = None, **kwargs):
|
| 39 |
+
super().__init__(name, **kwargs)
|
| 40 |
+
self.network = network or NetworkTopology()
|
| 41 |
+
self.content_items: List[Content] = []
|
| 42 |
+
self.simulation_history = []
|
| 43 |
+
self.time_step = 0
|
| 44 |
+
|
| 45 |
+
def add_content(self, content: Content) -> None:
|
| 46 |
+
"""Add content to the world for personas to interact with"""
|
| 47 |
+
self.content_items.append(content)
|
| 48 |
+
self.broadcast(f"New content available: {content.text[:100]}...")
|
| 49 |
+
|
| 50 |
+
def simulate_content_spread(self, content: Content,
|
| 51 |
+
initial_viewers: List[str],
|
| 52 |
+
max_steps: int = 10) -> SimulationResult:
|
| 53 |
+
"""Simulate how content spreads through the network"""
|
| 54 |
+
|
| 55 |
+
result = SimulationResult(content=content, start_time=datetime.now())
|
| 56 |
+
viewed = set(initial_viewers)
|
| 57 |
+
engaged = set()
|
| 58 |
+
|
| 59 |
+
for step in range(max_steps):
|
| 60 |
+
self.time_step = step
|
| 61 |
+
new_viewers = set()
|
| 62 |
+
|
| 63 |
+
for viewer_id in viewed - engaged:
|
| 64 |
+
if viewer_id not in self.network.nodes: continue
|
| 65 |
+
persona = self.network.nodes[viewer_id]
|
| 66 |
+
decision = self._simulate_engagement_decision(persona, content, step)
|
| 67 |
+
|
| 68 |
+
if decision.engaged:
|
| 69 |
+
engaged.add(viewer_id)
|
| 70 |
+
result.add_engagement(viewer_id, decision.engagement_type, step)
|
| 71 |
+
if decision.engagement_type == "share":
|
| 72 |
+
neighbors = self.network.get_neighbors(viewer_id)
|
| 73 |
+
new_viewers.update([n.name for n in neighbors])
|
| 74 |
+
|
| 75 |
+
viewed.update(new_viewers)
|
| 76 |
+
result.add_step_metrics(step, len(viewed), len(engaged))
|
| 77 |
+
if not new_viewers and not any(v not in engaged for v in viewed):
|
| 78 |
+
break
|
| 79 |
+
|
| 80 |
+
result.finalize(datetime.now())
|
| 81 |
+
self.simulation_history.append(result)
|
| 82 |
+
return result
|
| 83 |
+
|
| 84 |
+
def _simulate_engagement_decision(self, persona: TinyPerson,
|
| 85 |
+
content: Content,
|
| 86 |
+
time_step: int) -> EngagementDecision:
|
| 87 |
+
prob = persona.calculate_engagement_probability(content)
|
| 88 |
+
time_decay = 0.9 ** time_step
|
| 89 |
+
final_prob = prob * time_decay
|
| 90 |
+
|
| 91 |
+
engaged = random.random() < final_prob
|
| 92 |
+
if engaged:
|
| 93 |
+
reaction = persona.predict_reaction(content)
|
| 94 |
+
return EngagementDecision(
|
| 95 |
+
engaged=True,
|
| 96 |
+
engagement_type=reaction.reaction_type,
|
| 97 |
+
comment=reaction.comment,
|
| 98 |
+
probability=final_prob
|
| 99 |
+
)
|
| 100 |
+
return EngagementDecision(engaged=False, probability=final_prob)
|
tinytroupe/factory/tiny_person_factory.py
CHANGED
|
@@ -12,6 +12,7 @@ from tinytroupe.agent import TinyPerson
|
|
| 12 |
import tinytroupe.utils as utils
|
| 13 |
from tinytroupe.control import transactional
|
| 14 |
from tinytroupe import config_manager
|
|
|
|
| 15 |
|
| 16 |
import concurrent.futures
|
| 17 |
import threading
|
|
@@ -625,6 +626,61 @@ class TinyPersonFactory(TinyFactory):
|
|
| 625 |
"""
|
| 626 |
return name in TinyPerson.all_agents_names()
|
| 627 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 628 |
|
| 629 |
@transactional()
|
| 630 |
@utils.llm(temperature=0.5, frequency_penalty=0.0, presence_penalty=0.0)
|
|
|
|
| 12 |
import tinytroupe.utils as utils
|
| 13 |
from tinytroupe.control import transactional
|
| 14 |
from tinytroupe import config_manager
|
| 15 |
+
from tinytroupe.agent_traits import TraitBasedBehaviorModel
|
| 16 |
|
| 17 |
import concurrent.futures
|
| 18 |
import threading
|
|
|
|
| 626 |
"""
|
| 627 |
return name in TinyPerson.all_agents_names()
|
| 628 |
|
| 629 |
+
#########################################################################
|
| 630 |
+
# Artificial Societies Factory Enhancements
|
| 631 |
+
#########################################################################
|
| 632 |
+
|
| 633 |
+
def generate_from_demographics(self, age_range: tuple, location: str, occupation: str, interests: List[str]) -> TinyPerson:
|
| 634 |
+
"""
|
| 635 |
+
Generates a persona based on specific demographics.
|
| 636 |
+
"""
|
| 637 |
+
context = f"A {age_range[0]}-{age_range[1]} year old {occupation} in {location} interested in {', '.join(interests)}."
|
| 638 |
+
return self.generate_person(agent_particularities=context)
|
| 639 |
+
|
| 640 |
+
def generate_from_linkedin_profile(self, profile_data: Dict) -> TinyPerson:
|
| 641 |
+
"""
|
| 642 |
+
Generates a persona based on a LinkedIn profile.
|
| 643 |
+
"""
|
| 644 |
+
context = f"Professional profile: {json.dumps(profile_data)}"
|
| 645 |
+
persona = self.generate_person(agent_particularities=context)
|
| 646 |
+
persona.define("social_platform", "LinkedIn")
|
| 647 |
+
return persona
|
| 648 |
+
|
| 649 |
+
def generate_persona_cluster(self, archetype: str, count: int) -> List[TinyPerson]:
|
| 650 |
+
"""
|
| 651 |
+
Generates a cluster of personas based on an archetype.
|
| 652 |
+
"""
|
| 653 |
+
personas = []
|
| 654 |
+
for _ in range(count):
|
| 655 |
+
particularities = f"Archetype: {archetype}. Ensure individual variation."
|
| 656 |
+
personas.append(self.generate_person(agent_particularities=particularities))
|
| 657 |
+
return personas
|
| 658 |
+
|
| 659 |
+
def generate_diverse_population(self, size: int, distribution: Dict) -> List[TinyPerson]:
|
| 660 |
+
"""
|
| 661 |
+
Generates a diverse population based on a distribution.
|
| 662 |
+
"""
|
| 663 |
+
# Simplistic implementation: use create_factory_from_demography logic
|
| 664 |
+
return self.generate_people(number_of_people=size, verbose=True)
|
| 665 |
+
|
| 666 |
+
def ensure_consistency(self, persona: TinyPerson) -> bool:
|
| 667 |
+
"""
|
| 668 |
+
Validates the consistency of a generated persona.
|
| 669 |
+
"""
|
| 670 |
+
# Placeholder for LLM-based consistency check
|
| 671 |
+
traits = persona.get("behavioral_traits")
|
| 672 |
+
if traits and len(traits) > 0:
|
| 673 |
+
return True
|
| 674 |
+
return False
|
| 675 |
+
|
| 676 |
+
def calculate_diversity_score(self, personas: List[TinyPerson]) -> float:
|
| 677 |
+
"""
|
| 678 |
+
Measures demographic and behavioral diversity of a population.
|
| 679 |
+
"""
|
| 680 |
+
if not personas: return 0.0
|
| 681 |
+
# Placeholder logic: ratio of unique occupations
|
| 682 |
+
occupations = [p.get("occupation") for p in personas]
|
| 683 |
+
return len(set(occupations)) / len(personas)
|
| 684 |
|
| 685 |
@transactional()
|
| 686 |
@utils.llm(temperature=0.5, frequency_penalty=0.0, presence_penalty=0.0)
|
tinytroupe/features.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Dict, List, Any
|
| 2 |
+
import numpy as np
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
from tinytroupe.agent.tiny_person import TinyPerson
|
| 5 |
+
from tinytroupe.agent_types import Content
|
| 6 |
+
from tinytroupe.social_network import NetworkTopology
|
| 7 |
+
|
| 8 |
+
class ContentFeatureExtractor:
|
| 9 |
+
def extract(self, content: Content) -> Dict[str, float]:
|
| 10 |
+
"""Extract all content features"""
|
| 11 |
+
return {
|
| 12 |
+
"word_count": len(content.text.split()) / 500.0, # Normalized
|
| 13 |
+
"has_image": 1.0 if content.images else 0.0,
|
| 14 |
+
"has_video": 1.0 if content.video_url else 0.0,
|
| 15 |
+
"num_hashtags": len(content.hashtags) / 10.0,
|
| 16 |
+
"sentiment_score": 0.5, # Placeholder for VADER/Transformers
|
| 17 |
+
"hour_of_day": content.timestamp.hour / 24.0,
|
| 18 |
+
"is_weekend": 1.0 if content.timestamp.weekday() >= 5 else 0.0,
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
class PersonaFeatureExtractor:
|
| 22 |
+
def extract(self, persona: TinyPerson) -> Dict[str, float]:
|
| 23 |
+
"""Extract persona features"""
|
| 24 |
+
traits = persona.get("behavioral_traits") or {}
|
| 25 |
+
return {
|
| 26 |
+
"age": float(persona.get("age") or 30) / 100.0,
|
| 27 |
+
"num_connections": len(persona.social_connections) / 100.0,
|
| 28 |
+
"authority": persona.influence_metrics.authority,
|
| 29 |
+
"openness": traits.get("openness_to_new_ideas", 0.5),
|
| 30 |
+
"extraversion": traits.get("extraversion", 0.5),
|
| 31 |
+
"engagement_rate": persona.influence_metrics.engagement_rate,
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
class InteractionFeatureExtractor:
|
| 35 |
+
def extract(self, persona: TinyPerson, content: Content, network: NetworkTopology) -> Dict[str, float]:
|
| 36 |
+
"""Extract features from persona-content interaction context"""
|
| 37 |
+
# Placeholder for complex context features
|
| 38 |
+
return {
|
| 39 |
+
"topic_alignment": persona.get_content_affinity(content),
|
| 40 |
+
"author_connection": 1.0 if content.author_name in persona.social_connections else 0.0,
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
class FeatureExtractor:
|
| 44 |
+
def __init__(self):
|
| 45 |
+
self.content_extractor = ContentFeatureExtractor()
|
| 46 |
+
self.persona_extractor = PersonaFeatureExtractor()
|
| 47 |
+
self.interaction_extractor = InteractionFeatureExtractor()
|
| 48 |
+
|
| 49 |
+
def extract_all(self, persona: TinyPerson, content: Content, network: NetworkTopology) -> np.ndarray:
|
| 50 |
+
c_feats = self.content_extractor.extract(content)
|
| 51 |
+
p_feats = self.persona_extractor.extract(persona)
|
| 52 |
+
i_feats = self.interaction_extractor.extract(persona, content, network)
|
| 53 |
+
|
| 54 |
+
combined = {**c_feats, **p_feats, **i_feats}
|
| 55 |
+
return np.array(list(combined.values()))
|
tinytroupe/influence.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Set, Dict, Any
|
| 2 |
+
import random
|
| 3 |
+
from dataclasses import dataclass
|
| 4 |
+
from tinytroupe.social_network import NetworkTopology
|
| 5 |
+
from tinytroupe.agent_types import Content
|
| 6 |
+
|
| 7 |
+
@dataclass
|
| 8 |
+
class PropagationResult:
|
| 9 |
+
activated_personas: Set[str]
|
| 10 |
+
activation_times: Dict[str, int]
|
| 11 |
+
total_reach: int
|
| 12 |
+
cascade_depth: int
|
| 13 |
+
engagement_by_time: List[int]
|
| 14 |
+
|
| 15 |
+
class InfluencePropagator:
|
| 16 |
+
def __init__(self, network: NetworkTopology, model: str = "cascade"):
|
| 17 |
+
self.network = network
|
| 18 |
+
self.model = model
|
| 19 |
+
self.max_steps = 10
|
| 20 |
+
|
| 21 |
+
def propagate(self, seed_personas: List[str], content: Content) -> PropagationResult:
|
| 22 |
+
"""Main propagation simulation"""
|
| 23 |
+
activated = set(seed_personas)
|
| 24 |
+
activation_times = {pid: 0 for pid in seed_personas}
|
| 25 |
+
engagement_by_time = [len(seed_personas)]
|
| 26 |
+
|
| 27 |
+
for time_step in range(1, self.max_steps + 1):
|
| 28 |
+
newly_activated = self._propagate_step(activated, content, time_step)
|
| 29 |
+
if not newly_activated:
|
| 30 |
+
break
|
| 31 |
+
|
| 32 |
+
for pid in newly_activated:
|
| 33 |
+
activation_times[pid] = time_step
|
| 34 |
+
activated.update(newly_activated)
|
| 35 |
+
engagement_by_time.append(len(newly_activated))
|
| 36 |
+
|
| 37 |
+
return PropagationResult(
|
| 38 |
+
activated_personas=activated,
|
| 39 |
+
activation_times=activation_times,
|
| 40 |
+
total_reach=len(activated),
|
| 41 |
+
cascade_depth=max(activation_times.values()) if activation_times else 0,
|
| 42 |
+
engagement_by_time=engagement_by_time
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
def _propagate_step(self, activated: Set[str], content: Content, time: int) -> Set[str]:
|
| 46 |
+
"""Single step of propagation"""
|
| 47 |
+
newly_activated = set()
|
| 48 |
+
|
| 49 |
+
if self.model == "cascade":
|
| 50 |
+
for active_id in activated:
|
| 51 |
+
# Find neighbors of active node
|
| 52 |
+
neighbors = self.network.get_neighbors(active_id)
|
| 53 |
+
for neighbor in neighbors:
|
| 54 |
+
if neighbor.name not in activated and neighbor.name not in newly_activated:
|
| 55 |
+
# Probabilistic activation
|
| 56 |
+
prob = 0.1 # Base propagation probability
|
| 57 |
+
if random.random() < prob:
|
| 58 |
+
newly_activated.add(neighbor.name)
|
| 59 |
+
|
| 60 |
+
elif self.model == "threshold":
|
| 61 |
+
for name, persona in self.network.nodes.items():
|
| 62 |
+
if name not in activated:
|
| 63 |
+
neighbors = self.network.get_neighbors(name)
|
| 64 |
+
active_neighbors = [n for n in neighbors if n.name in activated]
|
| 65 |
+
if neighbors:
|
| 66 |
+
influence = len(active_neighbors) / len(neighbors)
|
| 67 |
+
threshold = 0.5 # Default threshold
|
| 68 |
+
if influence >= threshold:
|
| 69 |
+
newly_activated.add(name)
|
| 70 |
+
|
| 71 |
+
return newly_activated
|
| 72 |
+
|
| 73 |
+
def calculate_influence_score(self, persona_id: str) -> float:
|
| 74 |
+
"""Calculate overall influence of a persona"""
|
| 75 |
+
neighbors = self.network.get_neighbors(persona_id)
|
| 76 |
+
# Combine degree centrality and reach
|
| 77 |
+
return len(neighbors) / max(len(self.network.nodes), 1)
|
tinytroupe/integrations/__init__.py
ADDED
|
File without changes
|
tinytroupe/integrations/linkedin_api.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
from typing import List, Dict, Any, Optional
|
| 4 |
+
from dataclasses import dataclass
|
| 5 |
+
|
| 6 |
+
@dataclass
|
| 7 |
+
class LinkedInProfile:
|
| 8 |
+
id: str
|
| 9 |
+
first_name: str
|
| 10 |
+
last_name: str
|
| 11 |
+
headline: str
|
| 12 |
+
email: str
|
| 13 |
+
profile_picture: Dict[str, Any]
|
| 14 |
+
|
| 15 |
+
class LinkedInAPI:
|
| 16 |
+
"""LinkedIn API client for fetching user data"""
|
| 17 |
+
def __init__(self, access_token: str):
|
| 18 |
+
self.access_token = access_token
|
| 19 |
+
self.base_url = "https://api.linkedin.com/v2"
|
| 20 |
+
|
| 21 |
+
def get_user_profile(self) -> LinkedInProfile:
|
| 22 |
+
# Placeholder for real API call
|
| 23 |
+
return LinkedInProfile(id="123", first_name="John", last_name="Doe", headline="Software Engineer", email="john@example.com", profile_picture={})
|
| 24 |
+
|
| 25 |
+
def get_connections(self, count: int = 100) -> List[Dict]:
|
| 26 |
+
# Placeholder
|
| 27 |
+
return [{"id": str(i), "localizedFirstName": f"Friend{i}"} for i in range(10)]
|
tinytroupe/llm_predictor.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from typing import Dict, Any
|
| 3 |
+
from tinytroupe.agent.tiny_person import TinyPerson
|
| 4 |
+
from tinytroupe.agent_types import Content
|
| 5 |
+
from tinytroupe import openai_utils
|
| 6 |
+
|
| 7 |
+
class LLMPredictor:
|
| 8 |
+
"""Use LLM reasoning for engagement prediction"""
|
| 9 |
+
def __init__(self, model: str = "gpt-4o"):
|
| 10 |
+
self.model = model
|
| 11 |
+
|
| 12 |
+
def predict(self, persona: TinyPerson, content: Content) -> Dict[str, Any]:
|
| 13 |
+
"""Use LLM to predict engagement"""
|
| 14 |
+
prompt = self._construct_prediction_prompt(persona, content)
|
| 15 |
+
# Placeholder for LLM call
|
| 16 |
+
# message = openai_utils.client().send_message(...)
|
| 17 |
+
|
| 18 |
+
return {
|
| 19 |
+
"will_engage": True,
|
| 20 |
+
"probability": 0.75,
|
| 21 |
+
"reasoning": "Content aligns well with persona's professional interests.",
|
| 22 |
+
"reaction_type": "like",
|
| 23 |
+
"comment": "Great insights on the industry!"
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
def _construct_prediction_prompt(self, persona: TinyPerson, content: Content) -> str:
|
| 27 |
+
return f"Predict reaction for {persona.name} to content: {content.text}"
|
tinytroupe/ml_models.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Dict, Any, Optional
|
| 2 |
+
import numpy as np
|
| 3 |
+
import random
|
| 4 |
+
from dataclasses import dataclass
|
| 5 |
+
from tinytroupe.agent.tiny_person import TinyPerson
|
| 6 |
+
from tinytroupe.agent_types import Content, Reaction
|
| 7 |
+
from tinytroupe.social_network import NetworkTopology
|
| 8 |
+
from tinytroupe.features import FeatureExtractor
|
| 9 |
+
|
| 10 |
+
@dataclass
|
| 11 |
+
class TrainingExample:
|
| 12 |
+
persona: TinyPerson
|
| 13 |
+
content: Content
|
| 14 |
+
network: NetworkTopology
|
| 15 |
+
engaged: bool
|
| 16 |
+
engagement_type: str = "none"
|
| 17 |
+
|
| 18 |
+
@dataclass
|
| 19 |
+
class PredictionResult:
|
| 20 |
+
engagement_probability: float
|
| 21 |
+
engagement_type_probs: Dict[str, float]
|
| 22 |
+
predicted_reaction: str
|
| 23 |
+
confidence: float
|
| 24 |
+
|
| 25 |
+
class EngagementPredictor:
|
| 26 |
+
"""Predicts whether persona will engage with content"""
|
| 27 |
+
def __init__(self):
|
| 28 |
+
self.model = None
|
| 29 |
+
self.extractor = FeatureExtractor()
|
| 30 |
+
|
| 31 |
+
def predict(self, persona: TinyPerson, content: Content, network: NetworkTopology) -> float:
|
| 32 |
+
"""Predict engagement probability"""
|
| 33 |
+
# Placeholder for real model inference
|
| 34 |
+
# In a real system, we'd use self.model.predict_proba()
|
| 35 |
+
features = self.extractor.extract_all(persona, content, network)
|
| 36 |
+
# Dummy logic based on feature sum
|
| 37 |
+
score = np.mean(features)
|
| 38 |
+
return min(max(score, 0.0), 1.0)
|
| 39 |
+
|
| 40 |
+
class EnsemblePredictor:
|
| 41 |
+
"""Combines multiple predictors for robust predictions"""
|
| 42 |
+
def __init__(self):
|
| 43 |
+
self.engagement_predictor = EngagementPredictor()
|
| 44 |
+
|
| 45 |
+
def predict(self, persona: TinyPerson, content: Content, network: NetworkTopology) -> PredictionResult:
|
| 46 |
+
prob = self.engagement_predictor.predict(persona, content, network)
|
| 47 |
+
|
| 48 |
+
reaction_types = ["like", "comment", "share"]
|
| 49 |
+
type_probs = {rt: prob * (1.0 / len(reaction_types)) for rt in reaction_types}
|
| 50 |
+
|
| 51 |
+
predicted_reaction = "none"
|
| 52 |
+
if prob > 0.5:
|
| 53 |
+
predicted_reaction = random.choice(reaction_types)
|
| 54 |
+
|
| 55 |
+
return PredictionResult(
|
| 56 |
+
engagement_probability=prob,
|
| 57 |
+
engagement_type_probs=type_probs,
|
| 58 |
+
predicted_reaction=predicted_reaction,
|
| 59 |
+
confidence=0.8
|
| 60 |
+
)
|
tinytroupe/network_analysis.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Dict, Any
|
| 2 |
+
from tinytroupe.social_network import NetworkTopology, Community
|
| 3 |
+
|
| 4 |
+
class NetworkAnalyzer:
|
| 5 |
+
"""
|
| 6 |
+
Provide analytics for understanding network dynamics.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
@staticmethod
|
| 10 |
+
def calculate_centrality_metrics(network: NetworkTopology) -> Dict[str, Dict[str, float]]:
|
| 11 |
+
"""
|
| 12 |
+
Calculates various centrality metrics for each persona in the network.
|
| 13 |
+
"""
|
| 14 |
+
# Simplistic implementation without NetworkX for now to avoid dependency issues in basic environment
|
| 15 |
+
metrics = {name: {"degree": 0.0} for name in network.nodes}
|
| 16 |
+
for edge in network.edges:
|
| 17 |
+
metrics[edge.source_id]["degree"] += 1
|
| 18 |
+
metrics[edge.target_id]["degree"] += 1
|
| 19 |
+
|
| 20 |
+
n = max(len(network.nodes), 1)
|
| 21 |
+
for name in metrics:
|
| 22 |
+
metrics[name]["degree"] /= n
|
| 23 |
+
|
| 24 |
+
return metrics
|
| 25 |
+
|
| 26 |
+
@staticmethod
|
| 27 |
+
def detect_communities(network: NetworkTopology) -> List[Community]:
|
| 28 |
+
"""
|
| 29 |
+
Detects communities within the social network.
|
| 30 |
+
"""
|
| 31 |
+
# Placeholder for community detection logic
|
| 32 |
+
return network.communities
|
| 33 |
+
|
| 34 |
+
@staticmethod
|
| 35 |
+
def identify_key_influencers(network: NetworkTopology, top_k: int = 10) -> List[str]:
|
| 36 |
+
"""
|
| 37 |
+
Identifies the top K influencers in the network.
|
| 38 |
+
"""
|
| 39 |
+
metrics = NetworkAnalyzer.calculate_centrality_metrics(network)
|
| 40 |
+
sorted_influencers = sorted(metrics.keys(), key=lambda x: metrics[x]["degree"], reverse=True)
|
| 41 |
+
return sorted_influencers[:top_k]
|
| 42 |
+
|
| 43 |
+
@staticmethod
|
| 44 |
+
def calculate_density(network: NetworkTopology) -> float:
|
| 45 |
+
"""
|
| 46 |
+
Calculates the density of the network.
|
| 47 |
+
"""
|
| 48 |
+
n = len(network.nodes)
|
| 49 |
+
if n < 2: return 0.0
|
| 50 |
+
max_edges = n * (n - 1) / 2
|
| 51 |
+
return len(network.edges) / max_edges
|
tinytroupe/network_generator.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import random
|
| 2 |
+
from typing import List, Dict
|
| 3 |
+
from tinytroupe.social_network import NetworkTopology, Community
|
| 4 |
+
from tinytroupe.agent.tiny_person import TinyPerson
|
| 5 |
+
|
| 6 |
+
class NetworkGenerator:
|
| 7 |
+
"""
|
| 8 |
+
Implements realistic network topologies.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
@staticmethod
|
| 12 |
+
def generate_scale_free_network(personas: List[TinyPerson], m: int = 2) -> NetworkTopology:
|
| 13 |
+
"""
|
| 14 |
+
Barabási-Albert model for scale-free networks.
|
| 15 |
+
"""
|
| 16 |
+
topology = NetworkTopology()
|
| 17 |
+
for p in personas:
|
| 18 |
+
topology.add_persona(p)
|
| 19 |
+
|
| 20 |
+
names = [p.name for p in personas]
|
| 21 |
+
if len(names) <= m:
|
| 22 |
+
return topology
|
| 23 |
+
|
| 24 |
+
# Initial complete graph of m nodes
|
| 25 |
+
for i in range(m):
|
| 26 |
+
for j in range(i + 1, m):
|
| 27 |
+
topology.add_connection(names[i], names[j])
|
| 28 |
+
|
| 29 |
+
# Add remaining nodes with preferential attachment
|
| 30 |
+
for i in range(m, len(names)):
|
| 31 |
+
targets = set()
|
| 32 |
+
existing_nodes = names[:i]
|
| 33 |
+
# Simple preferential attachment based on degree
|
| 34 |
+
while len(targets) < m:
|
| 35 |
+
# Degree of each node
|
| 36 |
+
degrees = {name: 0 for name in existing_nodes}
|
| 37 |
+
for edge in topology.edges:
|
| 38 |
+
if edge.source_id in degrees: degrees[edge.source_id] += 1
|
| 39 |
+
if edge.target_id in degrees: degrees[edge.target_id] += 1
|
| 40 |
+
|
| 41 |
+
total_degree = sum(degrees.values())
|
| 42 |
+
if total_degree == 0:
|
| 43 |
+
target = random.choice(existing_nodes)
|
| 44 |
+
else:
|
| 45 |
+
probs = [degrees[name] / total_degree for name in existing_nodes]
|
| 46 |
+
target = random.choices(existing_nodes, weights=probs)[0]
|
| 47 |
+
targets.add(target)
|
| 48 |
+
|
| 49 |
+
for target in targets:
|
| 50 |
+
topology.add_connection(names[i], target)
|
| 51 |
+
|
| 52 |
+
return topology
|
| 53 |
+
|
| 54 |
+
@staticmethod
|
| 55 |
+
def generate_small_world_network(personas: List[TinyPerson], k: int = 4, p: float = 0.1) -> NetworkTopology:
|
| 56 |
+
"""
|
| 57 |
+
Watts-Strogatz model for small-world networks.
|
| 58 |
+
"""
|
| 59 |
+
topology = NetworkTopology()
|
| 60 |
+
for persona in personas:
|
| 61 |
+
topology.add_persona(persona)
|
| 62 |
+
|
| 63 |
+
names = [p.name for p in personas]
|
| 64 |
+
n = len(names)
|
| 65 |
+
|
| 66 |
+
# Regular ring lattice
|
| 67 |
+
for i in range(n):
|
| 68 |
+
for j in range(1, k // 2 + 1):
|
| 69 |
+
neighbor = names[(i + j) % n]
|
| 70 |
+
topology.add_connection(names[i], neighbor)
|
| 71 |
+
|
| 72 |
+
# Rewiring
|
| 73 |
+
for i in range(n):
|
| 74 |
+
for j in range(1, k // 2 + 1):
|
| 75 |
+
if random.random() < p:
|
| 76 |
+
# Remove old connection and add a random one
|
| 77 |
+
old_neighbor = names[(i + j) % n]
|
| 78 |
+
topology.remove_connection(names[i], old_neighbor)
|
| 79 |
+
new_neighbor = random.choice(names)
|
| 80 |
+
while new_neighbor == names[i] or any(e.source_id == names[i] and e.target_id == new_neighbor for e in topology.edges):
|
| 81 |
+
new_neighbor = random.choice(names)
|
| 82 |
+
topology.add_connection(names[i], new_neighbor)
|
| 83 |
+
|
| 84 |
+
return topology
|
| 85 |
+
|
| 86 |
+
@staticmethod
|
| 87 |
+
def generate_professional_network(personas: List[TinyPerson]) -> NetworkTopology:
|
| 88 |
+
"""
|
| 89 |
+
LinkedIn-style network based on professional attributes.
|
| 90 |
+
"""
|
| 91 |
+
topology = NetworkTopology()
|
| 92 |
+
for p in personas:
|
| 93 |
+
topology.add_persona(p)
|
| 94 |
+
|
| 95 |
+
for i, p1 in enumerate(personas):
|
| 96 |
+
for j in range(i + 1, len(personas)):
|
| 97 |
+
p2 = personas[j]
|
| 98 |
+
# Probabilistic connection based on similarity
|
| 99 |
+
prob = 0.05
|
| 100 |
+
if p1.get("occupation") == p2.get("occupation"): prob += 0.2
|
| 101 |
+
if p1.get("residence") == p2.get("residence"): prob += 0.1
|
| 102 |
+
|
| 103 |
+
if random.random() < prob:
|
| 104 |
+
topology.add_connection(p1.name, p2.name, relationship_type="colleague")
|
| 105 |
+
|
| 106 |
+
return topology
|
tinytroupe/simulation_manager.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Dict, Any, Optional
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
import hashlib
|
| 4 |
+
import json
|
| 5 |
+
from tinytroupe.agent.tiny_person import TinyPerson
|
| 6 |
+
from tinytroupe.social_network import NetworkTopology
|
| 7 |
+
from tinytroupe.environment.social_world import SocialTinyWorld, SimulationResult
|
| 8 |
+
from tinytroupe.agent_types import Content
|
| 9 |
+
from tinytroupe.ml_models import EngagementPredictor
|
| 10 |
+
from tinytroupe.content_generation import ContentVariantGenerator
|
| 11 |
+
|
| 12 |
+
class SimulationConfig:
|
| 13 |
+
def __init__(self, name: str, persona_count: int = 10, network_type: str = "scale_free", use_linkedin_audience: bool = False, linkedin_token: str = None):
|
| 14 |
+
self.name = name
|
| 15 |
+
self.persona_count = persona_count
|
| 16 |
+
self.network_type = network_type
|
| 17 |
+
self.use_linkedin_audience = use_linkedin_audience
|
| 18 |
+
self.linkedin_token = linkedin_token
|
| 19 |
+
|
| 20 |
+
class Simulation:
|
| 21 |
+
def __init__(self, id: str, config: SimulationConfig, world: SocialTinyWorld, personas: List[TinyPerson], network: NetworkTopology):
|
| 22 |
+
self.id = id
|
| 23 |
+
self.config = config
|
| 24 |
+
self.world = world
|
| 25 |
+
self.personas = personas
|
| 26 |
+
self.network = network
|
| 27 |
+
self.status = "ready"
|
| 28 |
+
self.created_at = datetime.now()
|
| 29 |
+
self.last_result = None
|
| 30 |
+
|
| 31 |
+
class SimulationManager:
|
| 32 |
+
"""Manages simulation lifecycle and execution"""
|
| 33 |
+
|
| 34 |
+
def __init__(self):
|
| 35 |
+
self.simulations: Dict[str, Simulation] = {}
|
| 36 |
+
self.predictor = EngagementPredictor()
|
| 37 |
+
self.variant_generator = ContentVariantGenerator()
|
| 38 |
+
|
| 39 |
+
def create_simulation(self, config: SimulationConfig) -> Simulation:
|
| 40 |
+
from tinytroupe.factory.tiny_person_factory import TinyPersonFactory
|
| 41 |
+
factory = TinyPersonFactory()
|
| 42 |
+
personas = factory.generate_people(number_of_people=config.persona_count)
|
| 43 |
+
|
| 44 |
+
from tinytroupe.network_generator import NetworkGenerator
|
| 45 |
+
if config.network_type == "scale_free":
|
| 46 |
+
network = NetworkGenerator.generate_scale_free_network(personas)
|
| 47 |
+
else:
|
| 48 |
+
network = NetworkGenerator.generate_professional_network(personas)
|
| 49 |
+
|
| 50 |
+
world = SocialTinyWorld(config.name, network)
|
| 51 |
+
for p in personas: world.add_agent(p)
|
| 52 |
+
|
| 53 |
+
sim_id = hashlib.md5(f"{config.name}{datetime.now()}".encode()).hexdigest()
|
| 54 |
+
sim = Simulation(sim_id, config, world, personas, network)
|
| 55 |
+
self.simulations[sim_id] = sim
|
| 56 |
+
return sim
|
| 57 |
+
|
| 58 |
+
def run_simulation(self, sim_id: str, content_text: str) -> SimulationResult:
|
| 59 |
+
sim = self.simulations[sim_id]
|
| 60 |
+
sim.status = "running"
|
| 61 |
+
content = Content(text=content_text, content_type="post", topics=[], length=len(content_text), tone="")
|
| 62 |
+
|
| 63 |
+
initial_viewers = [p.name for p in sim.personas[:min(5, len(sim.personas))]]
|
| 64 |
+
result = sim.world.simulate_content_spread(content, initial_viewers)
|
| 65 |
+
|
| 66 |
+
sim.status = "completed"
|
| 67 |
+
sim.last_result = result
|
| 68 |
+
return result
|
tinytroupe/social_network.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass, field
|
| 2 |
+
from typing import List, Dict, Optional, Any, Set, Tuple
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
import numpy as np
|
| 5 |
+
from tinytroupe.agent.tiny_person import TinyPerson
|
| 6 |
+
|
| 7 |
+
@dataclass
|
| 8 |
+
class Connection:
|
| 9 |
+
"""Represents a connection between two personas"""
|
| 10 |
+
source_id: str
|
| 11 |
+
target_id: str
|
| 12 |
+
strength: float = 0.5 # 0.0-1.0
|
| 13 |
+
relationship_type: str = "follower" # "follower", "friend", "colleague", "family"
|
| 14 |
+
interaction_frequency: float = 0.0 # interactions per week
|
| 15 |
+
last_interaction: Optional[datetime] = None
|
| 16 |
+
influence_score: float = 0.0 # how much target influences source
|
| 17 |
+
created_at: datetime = field(default_factory=datetime.now)
|
| 18 |
+
|
| 19 |
+
@dataclass
|
| 20 |
+
class Community:
|
| 21 |
+
"""Represents a cluster of closely connected personas"""
|
| 22 |
+
community_id: str
|
| 23 |
+
members: List[str] # persona_ids
|
| 24 |
+
density: float = 0.0
|
| 25 |
+
central_personas: List[str] = field(default_factory=list) # most influential in community
|
| 26 |
+
shared_interests: List[str] = field(default_factory=list)
|
| 27 |
+
avg_engagement_rate: float = 0.0
|
| 28 |
+
|
| 29 |
+
class NetworkTopology:
|
| 30 |
+
"""Represents the entire social network structure"""
|
| 31 |
+
def __init__(self):
|
| 32 |
+
self.nodes: Dict[str, TinyPerson] = {} # persona_id -> persona
|
| 33 |
+
self.edges: List[Connection] = []
|
| 34 |
+
self.adjacency_matrix: Optional[np.ndarray] = None
|
| 35 |
+
self.influence_matrix: Optional[np.ndarray] = None
|
| 36 |
+
self.communities: List[Community] = []
|
| 37 |
+
|
| 38 |
+
def add_persona(self, persona: TinyPerson) -> None:
|
| 39 |
+
self.nodes[persona.name] = persona
|
| 40 |
+
|
| 41 |
+
def add_connection(self, source_id: str, target_id: str, **kwargs) -> Connection:
|
| 42 |
+
conn = Connection(source_id=source_id, target_id=target_id, **kwargs)
|
| 43 |
+
self.edges.append(conn)
|
| 44 |
+
return conn
|
| 45 |
+
|
| 46 |
+
def remove_connection(self, source_id: str, target_id: str) -> None:
|
| 47 |
+
self.edges = [e for e in self.edges if not (e.source_id == source_id and e.target_id == target_id)]
|
| 48 |
+
|
| 49 |
+
def get_neighbors(self, persona_id: str, depth: int = 1) -> List[TinyPerson]:
|
| 50 |
+
# Simple BFS for neighbors
|
| 51 |
+
neighbors = set()
|
| 52 |
+
queue = [(persona_id, 0)]
|
| 53 |
+
visited = {persona_id}
|
| 54 |
+
|
| 55 |
+
while queue:
|
| 56 |
+
curr_id, curr_depth = queue.pop(0)
|
| 57 |
+
if curr_depth >= depth: continue
|
| 58 |
+
|
| 59 |
+
for edge in self.edges:
|
| 60 |
+
if edge.source_id == curr_id and edge.target_id not in visited:
|
| 61 |
+
neighbors.add(edge.target_id)
|
| 62 |
+
visited.add(edge.target_id)
|
| 63 |
+
queue.append((edge.target_id, curr_depth + 1))
|
| 64 |
+
elif edge.target_id == curr_id and edge.source_id not in visited:
|
| 65 |
+
neighbors.add(edge.source_id)
|
| 66 |
+
visited.add(edge.source_id)
|
| 67 |
+
queue.append((edge.source_id, curr_depth + 1))
|
| 68 |
+
|
| 69 |
+
return [self.nodes[nid] for nid in neighbors if nid in self.nodes]
|
| 70 |
+
|
| 71 |
+
def calculate_centrality_metrics(self) -> Dict[str, float]:
|
| 72 |
+
# Placeholder for real centrality (e.g. using NetworkX in analysis module)
|
| 73 |
+
metrics = {name: 0.0 for name in self.nodes}
|
| 74 |
+
for edge in self.edges:
|
| 75 |
+
metrics[edge.source_id] += 1
|
| 76 |
+
metrics[edge.target_id] += 1
|
| 77 |
+
return metrics
|
| 78 |
+
|
| 79 |
+
def detect_communities(self) -> List[Community]:
|
| 80 |
+
# Placeholder
|
| 81 |
+
return self.communities
|
tinytroupe/variant_optimizer.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Dict, Any
|
| 2 |
+
import numpy as np
|
| 3 |
+
from dataclasses import dataclass
|
| 4 |
+
from tinytroupe.content_generation import ContentVariant
|
| 5 |
+
from tinytroupe.agent.tiny_person import TinyPerson
|
| 6 |
+
from tinytroupe.social_network import NetworkTopology
|
| 7 |
+
from tinytroupe.ml_models import EngagementPredictor
|
| 8 |
+
|
| 9 |
+
@dataclass
|
| 10 |
+
class RankedVariant:
|
| 11 |
+
variant: ContentVariant
|
| 12 |
+
score: float
|
| 13 |
+
predicted_engagement_count: int
|
| 14 |
+
|
| 15 |
+
class VariantOptimizer:
|
| 16 |
+
"""Optimize and rank content variants"""
|
| 17 |
+
|
| 18 |
+
def __init__(self, predictor: EngagementPredictor):
|
| 19 |
+
self.predictor = predictor
|
| 20 |
+
|
| 21 |
+
def rank_variants(self, variants: List[ContentVariant],
|
| 22 |
+
target_personas: List[TinyPerson],
|
| 23 |
+
network: NetworkTopology) -> List[RankedVariant]:
|
| 24 |
+
"""Rank variants by predicted performance"""
|
| 25 |
+
ranked = []
|
| 26 |
+
for variant in variants:
|
| 27 |
+
probs = []
|
| 28 |
+
from tinytroupe.agent_types import Content
|
| 29 |
+
content_obj = Content(text=variant.text, content_type="article", topics=[], length=len(variant.text), tone="")
|
| 30 |
+
|
| 31 |
+
for persona in target_personas:
|
| 32 |
+
prob = self.predictor.predict(persona, content_obj, network)
|
| 33 |
+
probs.append(prob)
|
| 34 |
+
|
| 35 |
+
avg_prob = np.mean(probs) if probs else 0.0
|
| 36 |
+
ranked.append(RankedVariant(
|
| 37 |
+
variant=variant,
|
| 38 |
+
score=avg_prob,
|
| 39 |
+
predicted_engagement_count=int(sum(probs))
|
| 40 |
+
))
|
| 41 |
+
|
| 42 |
+
ranked.sort(key=lambda x: x.score, reverse=True)
|
| 43 |
+
return ranked
|