| """Simulation — the main loop that orchestrates the entire city simulation.""" |
|
|
| from __future__ import annotations |
|
|
| import asyncio |
| import logging |
| import random |
| from typing import Callable, Optional |
|
|
| from soci.agents.agent import Agent, AgentAction, AgentState |
| from soci.agents.memory import MemoryType |
| from soci.agents.persona import Persona, load_personas |
| from soci.agents.generator import generate_personas |
| from soci.agents.routine import DailyRoutine, build_routine, check_motivation_override |
| from soci.actions.registry import resolve_action, ACTION_NEEDS, ACTION_DURATIONS |
| from soci.actions.movement import execute_move |
| from soci.actions.activities import execute_activity |
| from soci.actions.conversation import ( |
| Conversation, initiate_conversation, continue_conversation, |
| ) |
| from soci.actions.social import should_initiate_conversation, pick_conversation_partner |
| from soci.engine.llm import ( |
| ClaudeClient, MODEL_SONNET, MODEL_HAIKU, |
| PLAN_DAY_PROMPT, DECIDE_ACTION_PROMPT, OBSERVE_PROMPT, REFLECT_PROMPT, |
| ) |
| from soci.engine.scheduler import prioritize_agents, batch_llm_calls, should_skip_llm |
| from soci.engine.entropy import EntropyManager |
| from soci.world.city import City, generate_houses |
| from soci.world.clock import SimClock |
| from soci.world.events import EventSystem |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| class Simulation: |
| """The main simulation engine — manages the city, agents, and time.""" |
|
|
| def __init__( |
| self, |
| city: City, |
| clock: SimClock, |
| llm: ClaudeClient, |
| max_concurrent_llm: int = 10, |
| ) -> None: |
| self.city = city |
| self.clock = clock |
| self.llm = llm |
| self.agents: dict[str, Agent] = {} |
| self.events = EventSystem() |
| self.entropy = EntropyManager() |
| self.active_conversations: dict[str, Conversation] = {} |
| self.conversation_history: list[dict] = [] |
| self._max_conversation_history: int = 50 |
| self._conversation_counter: int = 0 |
| self._max_concurrent = max_concurrent_llm |
| self._tick_log: list[str] = [] |
| self._event_history: list[dict] = [] |
| self._max_event_history: int = 200 |
| |
| self.routines: dict[str, DailyRoutine] = {} |
| self._last_routine_day: int = -1 |
| |
| self._skip_llm_this_tick: bool = False |
| self._max_convos_this_tick: int = 0 |
| self._max_llm_calls_this_tick: int = 0 |
| self._llm_calls_this_tick: int = 0 |
| |
| |
| self.llm_call_probability: float = 1.0 |
| |
| self.on_event: Optional[Callable[[str], None]] = None |
|
|
| def _llm_budget_remaining(self) -> int: |
| """How many LLM calls can still be made this tick. 0 = unlimited.""" |
| if self._max_llm_calls_this_tick <= 0: |
| return 999 |
| return max(0, self._max_llm_calls_this_tick - self._llm_calls_this_tick) |
|
|
| def add_agent(self, agent: Agent) -> None: |
| """Add an agent to the simulation and place them in the city.""" |
| self.agents[agent.id] = agent |
| self.city.place_agent(agent.id, agent.location) |
| |
| if not agent.life_events: |
| agent.seed_biography(self.clock.day, self.clock.total_ticks) |
|
|
| def load_agents_from_yaml(self, path: str) -> None: |
| """Load all personas from YAML and create agents.""" |
| personas = load_personas(path) |
| for persona in personas: |
| agent = Agent(persona) |
| self.add_agent(agent) |
| logger.info(f"Loaded {len(personas)} agents from {path}") |
|
|
| def generate_agents(self, count: int) -> None: |
| """Procedurally generate `count` agents with houses and routines.""" |
| |
| house_ids = generate_houses(self.city, count) |
| logger.info(f"Generated {len(house_ids)} houses for new agents") |
|
|
| |
| personas = generate_personas(count, self.city) |
| for persona in personas: |
| agent = Agent(persona) |
| self.add_agent(agent) |
| logger.info(f"Generated {len(personas)} procedural agents") |
|
|
| def _rebuild_routines(self) -> None: |
| """Rebuild daily routines for all agents (called at start of each day).""" |
| day = self.clock.day |
| if self._last_routine_day == day: |
| return |
| self._last_routine_day = day |
| for agent in self.agents.values(): |
| if not agent.is_player: |
| self.routines[agent.id] = build_routine(agent.persona, day) |
| logger.info(f"Built daily routines for {len(self.routines)} agents (day {day})") |
|
|
| def _emit(self, message: str) -> None: |
| """Emit an event message.""" |
| self._tick_log.append(message) |
| self._event_history.append({ |
| "tick": self.clock.total_ticks, |
| "time": self.clock.datetime_str, |
| "message": message, |
| }) |
| if len(self._event_history) > self._max_event_history: |
| self._event_history = self._event_history[-self._max_event_history:] |
| if self.on_event: |
| self.on_event(message) |
|
|
| async def tick(self) -> list[str]: |
| """Advance the simulation by one tick. Returns list of event descriptions.""" |
| self._tick_log = [] |
| self._llm_calls_this_tick = 0 |
| self._emit(f"\n--- {self.clock.datetime_str} ({self.clock.time_of_day.value}) ---") |
|
|
| |
| self._rebuild_routines() |
|
|
| |
| entropy_messages = self.entropy.tick( |
| list(self.agents.values()), |
| self.events, |
| self.clock, |
| list(self.city.locations.keys()), |
| ) |
| for msg in entropy_messages: |
| self._emit(msg) |
|
|
| |
| if self.clock.hour == 6 and self.clock.minute == 0: |
| for agent in self.agents.values(): |
| agent.reset_daily_plan() |
|
|
| |
| ordered_agents = prioritize_agents(list(self.agents.values()), self.clock) |
|
|
| |
| plan_coros = [] |
| plan_agents = [] |
| for agent in ordered_agents: |
| if agent.needs_new_plan(self.clock) and not should_skip_llm(agent, self.clock): |
| |
| if agent.id in self.routines: |
| routine = self.routines[agent.id] |
| plan_items = [] |
| seen = set() |
| for slot in routine.slots: |
| label = slot.detail |
| if label not in seen: |
| plan_items.append(label) |
| seen.add(label) |
| agent.set_daily_plan( |
| plan_items[:8], self.clock.day, |
| self.clock.total_ticks, self.clock.time_str, |
| ) |
| elif random.random() < self.llm_call_probability: |
| plan_coros.append(self._generate_daily_plan(agent)) |
| plan_agents.append(agent) |
|
|
| |
| budget = self._llm_budget_remaining() |
| if budget < len(plan_coros): |
| for c in plan_coros[budget:]: |
| c.close() |
| plan_coros = plan_coros[:budget] |
| plan_agents = plan_agents[:budget] |
|
|
| if plan_coros: |
| await batch_llm_calls(plan_coros, self._max_concurrent) |
| self._llm_calls_this_tick += len(plan_coros) |
| for agent in plan_agents: |
| self._emit(f"[PLAN] {agent.name} planned their day: {'; '.join(agent.daily_plan[:3])}...") |
|
|
| |
| action_coros = [] |
| action_agents = [] |
| routine_actions: list[tuple[Agent, AgentAction]] = [] |
|
|
| for agent in ordered_agents: |
| |
| if not agent.alive or agent.persona.age < 4: |
| continue |
|
|
| |
| is_sleeping = agent.state.value == "sleeping" |
| agent.tick_needs(is_sleeping=is_sleeping) |
|
|
| |
| if agent.is_busy: |
| completed = agent.tick_action() |
| if completed and agent.current_action: |
| self._emit(f" {agent.name} finished: {agent.current_action.detail}") |
| continue |
|
|
| |
| if should_skip_llm(agent, self.clock, self.routines.get(agent.id)): |
| continue |
|
|
| |
| routine = self.routines.get(agent.id) |
| if routine: |
| slot = routine.get_action_for_time(self.clock.hour, self.clock.minute) |
| if slot: |
| |
| override = check_motivation_override( |
| slot, agent.needs, agent.mood, |
| agent.persona.extraversion, |
| agent.persona.conscientiousness, |
| ) |
| if override: |
| slot = override |
| self._emit(f" [MOTIVATION] {agent.name}: {override.detail}") |
| action = AgentAction( |
| type=slot.action_type, |
| target=slot.target_location, |
| detail=slot.detail, |
| duration_ticks=slot.duration_ticks, |
| needs_satisfied=slot.needs_satisfied, |
| ) |
| routine_actions.append((agent, action)) |
| continue |
|
|
| |
| if not self._skip_llm_this_tick and random.random() < self.llm_call_probability: |
| action_coros.append(self._decide_action(agent)) |
| action_agents.append(agent) |
|
|
| |
| for agent, action in routine_actions: |
| await self._execute_action(agent, action) |
|
|
| |
| |
| cap = min( |
| self._max_convos_this_tick if self._max_convos_this_tick > 0 else len(action_coros), |
| self._llm_budget_remaining(), |
| ) |
| if len(action_coros) > cap: |
| for c in action_coros[cap:]: |
| c.close() |
| action_coros = action_coros[:cap] |
| action_agents = action_agents[:cap] |
| if action_coros and not self._skip_llm_this_tick: |
| action_results = await batch_llm_calls(action_coros, self._max_concurrent) |
| self._llm_calls_this_tick += len(action_coros) |
| for agent, result in zip(action_agents, action_results): |
| if result and isinstance(result, AgentAction): |
| await self._execute_action(agent, result) |
|
|
| |
| self._handle_player_sleep() |
|
|
| |
| if not self._skip_llm_this_tick: |
| conv_coros = [] |
| for conv_id, conv in list(self.active_conversations.items()): |
| if conv.is_finished: |
| self._finish_conversation(conv) |
| del self.active_conversations[conv_id] |
| continue |
| |
| last_speaker = conv.turns[-1].speaker_id if conv.turns else None |
| next_speaker_id = [p for p in conv.participants if p != last_speaker] |
| if next_speaker_id: |
| responder = self.agents.get(next_speaker_id[0]) |
| other = self.agents.get(last_speaker) if last_speaker else None |
| if responder and other: |
| |
| |
| conv_coros.append( |
| continue_conversation(conv, responder, other, self.llm, self.clock) |
| ) |
|
|
| |
| conv_cap = min( |
| self._max_convos_this_tick if self._max_convos_this_tick > 0 else len(conv_coros), |
| self._llm_budget_remaining(), |
| ) |
| if len(conv_coros) > conv_cap: |
| for c in conv_coros[conv_cap:]: |
| c.close() |
| conv_coros = conv_coros[:conv_cap] |
|
|
| if conv_coros: |
| await batch_llm_calls(conv_coros, self._max_concurrent) |
| self._llm_calls_this_tick += len(conv_coros) |
| else: |
| |
| for conv_id, conv in list(self.active_conversations.items()): |
| self._finish_conversation(conv) |
| self.active_conversations.clear() |
|
|
| |
| if not self._skip_llm_this_tick and self._llm_budget_remaining() > 0: |
| if self._max_convos_this_tick == 0 or len(self.active_conversations) < self._max_convos_this_tick: |
| if random.random() < self.llm_call_probability: |
| await self._handle_social_interactions(ordered_agents) |
|
|
| |
| if not self._skip_llm_this_tick and self._llm_budget_remaining() > 0: |
| reflect_coros = [] |
| reflect_agents = [] |
| for agent in ordered_agents: |
| if agent.memory.should_reflect() and not agent.is_player: |
| if random.random() < self.llm_call_probability: |
| reflect_coros.append(self._generate_reflection(agent)) |
| reflect_agents.append(agent) |
|
|
| |
| reflect_cap = min( |
| 1 if self._max_convos_this_tick > 0 else len(reflect_coros), |
| self._llm_budget_remaining(), |
| ) |
| if len(reflect_coros) > reflect_cap: |
| for c in reflect_coros[reflect_cap:]: |
| c.close() |
| reflect_coros = reflect_coros[:reflect_cap] |
|
|
| if reflect_coros: |
| await batch_llm_calls(reflect_coros, self._max_concurrent) |
| self._llm_calls_this_tick += len(reflect_coros) |
|
|
| |
| self._tick_romance() |
|
|
| |
| self._tick_pregnancy() |
|
|
| |
| if self.clock.hour == 0 and self.clock.minute == 0: |
| self._tick_aging() |
|
|
| |
| if self.clock.hour == 12 and self.clock.minute == 0: |
| self._tick_divorce() |
|
|
| |
| if self.clock.hour == 8 and self.clock.minute == 0: |
| self._tick_cohabitation() |
|
|
| |
| if self.clock.day > 0 and self.clock.day % 365 == 0 and self.clock.hour == 10 and self.clock.minute == 0: |
| self._tick_election() |
|
|
| |
| if self.clock.day > 0 and self.clock.day % 7 == 0 and self.clock.hour == 6 and self.clock.minute == 0: |
| self._tick_short_term_goals() |
|
|
| |
| self.clock.tick() |
|
|
| return self._tick_log |
|
|
| async def _generate_daily_plan(self, agent: Agent) -> None: |
| """Generate a daily plan for an agent via LLM.""" |
| world_desc = self.events.get_world_description() |
| loc_desc = self.city.describe_location(agent.location, exclude_agent=agent.id) |
|
|
| prompt = PLAN_DAY_PROMPT.format( |
| time_str=self.clock.time_str, |
| day=self.clock.day, |
| context=agent.build_context(self.clock.total_ticks, world_desc, loc_desc), |
| ) |
|
|
| result = await self.llm.complete_json( |
| system=agent.persona.system_prompt(), |
| user_message=prompt, |
| model=MODEL_HAIKU, |
| temperature=agent.persona.llm_temperature, |
| max_tokens=512, |
| ) |
|
|
| plan = result.get("plan", ["Go about my day"]) |
| if isinstance(plan, list): |
| |
| clean_plan = [] |
| for item in plan: |
| if isinstance(item, str): |
| clean_plan.append(item) |
| elif isinstance(item, dict): |
| |
| for key in ("description", "activity", "task", "text", "name", "item"): |
| if key in item: |
| clean_plan.append(str(item[key])) |
| break |
| else: |
| clean_plan.append(str(item)) |
| else: |
| clean_plan.append(str(item)) |
| agent.set_daily_plan(clean_plan, self.clock.day, self.clock.total_ticks, self.clock.time_str) |
|
|
| async def _decide_action(self, agent: Agent) -> Optional[AgentAction]: |
| """Ask the LLM what action an agent should take next.""" |
| world_desc = self.events.get_world_description() |
| loc_desc = self.city.describe_location(agent.location, exclude_agent=agent.id) |
|
|
| |
| current_loc = self.city.get_location(agent.location) |
| connected = [] |
| if current_loc: |
| for cid in current_loc.connected_to: |
| cloc = self.city.get_location(cid) |
| if cloc: |
| connected.append(f"{cid} ({cloc.name})") |
|
|
| |
| people_here = [ |
| self.agents[aid].name |
| for aid in self.city.get_agents_at(agent.location) |
| if aid != agent.id and aid in self.agents |
| ] |
|
|
| last_activity = "nothing in particular" |
| if agent.current_action: |
| last_activity = agent.current_action.detail or agent.current_action.type |
|
|
| prompt = DECIDE_ACTION_PROMPT.format( |
| time_str=self.clock.time_str, |
| day=self.clock.day, |
| context=agent.build_context(self.clock.total_ticks, world_desc, loc_desc), |
| location_name=loc_desc, |
| last_activity=last_activity, |
| connected_locations=", ".join(connected) if connected else "none visible", |
| people_here=", ".join(people_here) if people_here else "no one", |
| ) |
|
|
| |
| model = MODEL_HAIKU |
| if agent.needs.is_critical or self.events.active_events: |
| model = MODEL_SONNET |
|
|
| result = await self.llm.complete_json( |
| system=agent.persona.system_prompt(), |
| user_message=prompt, |
| model=model, |
| temperature=agent.persona.llm_temperature, |
| max_tokens=512, |
| ) |
|
|
| if not result: |
| |
| return AgentAction(type="wander", detail=f"{agent.name} wanders aimlessly") |
|
|
| action = resolve_action(result, agent, self.city) |
| agent._last_llm_tick = self.clock.total_ticks |
| return action |
|
|
| async def _execute_action(self, agent: Agent, action: AgentAction) -> None: |
| """Execute an agent's chosen action.""" |
| if action.type == "move": |
| desc = execute_move(agent, action, self.city, self.clock) |
| elif action.type == "talk": |
| |
| target_id = action.target |
| if target_id and target_id in self.agents: |
| await self._start_conversation(agent, self.agents[target_id]) |
| desc = f"{agent.name} starts talking to {self.agents[target_id].name}." |
| else: |
| desc = f"{agent.name} looks around for someone to talk to." |
| else: |
| desc = execute_activity(agent, action, self.city, self.clock) |
|
|
| agent.start_action(action) |
| self._emit(f" {desc}") |
|
|
| |
| agent.add_observation( |
| tick=self.clock.total_ticks, |
| day=self.clock.day, |
| time_str=self.clock.time_str, |
| content=desc, |
| importance=3, |
| ) |
|
|
| async def _handle_social_interactions(self, agents: list[Agent]) -> None: |
| """Check if any co-located agents should start conversations.""" |
| |
| max_convos = 3 |
| if len(self.active_conversations) >= max_convos: |
| return |
|
|
| |
| _CONVERSABLE = {"idle", "eating", "relaxing", "working", "exercising", "shopping"} |
|
|
| for agent in agents: |
| |
| if agent.state.value not in _CONVERSABLE: |
| continue |
| |
| if agent.is_player: |
| continue |
| |
| in_conv = any( |
| agent.id in c.participants |
| for c in self.active_conversations.values() |
| ) |
| if in_conv: |
| continue |
|
|
| |
| others = [ |
| aid for aid in self.city.get_agents_at(agent.location) |
| if aid != agent.id |
| and aid in self.agents |
| and self.agents[aid].state.value in _CONVERSABLE |
| and not any(aid in c.participants for c in self.active_conversations.values()) |
| ] |
| if not others: |
| continue |
|
|
| partner_id = pick_conversation_partner(agent, others, self.clock) |
| if partner_id and should_initiate_conversation(agent, partner_id, self.clock): |
| await self._start_conversation(agent, self.agents[partner_id]) |
| break |
|
|
| def _handle_player_sleep(self) -> None: |
| """Auto-sleep idle players during sleeping hours, wake them in the morning.""" |
| for agent in self.agents.values(): |
| if not agent.is_player: |
| continue |
| if self.clock.is_sleeping_hours: |
| if not agent.is_busy and agent.state.value != "sleeping": |
| sleep_action = AgentAction( |
| type="sleep", |
| target=agent.persona.home_location, |
| detail=f"{agent.name} goes to sleep for the night", |
| duration_ticks=32, |
| needs_satisfied={"energy": 0.9}, |
| ) |
| |
| home = agent.persona.home_location |
| if agent.location != home and self.city.get_location(home): |
| old_loc = self.city.get_location(agent.location) |
| new_loc = self.city.get_location(home) |
| if old_loc: |
| old_loc.remove_occupant(agent.id) |
| new_loc.add_occupant(agent.id) |
| agent.location = home |
| agent.start_action(sleep_action) |
| self._emit(f" {agent.name} goes to sleep for the night.") |
| else: |
| |
| if agent.state.value == "sleeping": |
| agent.state = AgentState.IDLE |
| agent.current_action = None |
| agent._action_ticks_remaining = 0 |
|
|
| async def _start_conversation(self, initiator: Agent, target: Agent) -> None: |
| """Start a conversation between two agents.""" |
| self._conversation_counter += 1 |
| conv_id = f"conv_{self._conversation_counter}" |
|
|
| conv = await initiate_conversation( |
| initiator, target, self.llm, self.clock, conv_id, |
| ) |
| if conv is None: |
| return |
| self.active_conversations[conv_id] = conv |
|
|
| |
| from soci.agents.agent import AgentAction |
| talk_action = AgentAction( |
| type="talk", |
| target=target.id, |
| detail=f"talking to {target.name}", |
| duration_ticks=conv.max_turns, |
| needs_satisfied={"social": 0.3}, |
| ) |
| initiator.start_action(talk_action) |
|
|
| talk_action_target = AgentAction( |
| type="talk", |
| target=initiator.id, |
| detail=f"talking to {initiator.name}", |
| duration_ticks=conv.max_turns, |
| needs_satisfied={"social": 0.3}, |
| ) |
| target.start_action(talk_action_target) |
|
|
| self._emit(f" [CONV] {initiator.name} starts talking to {target.name}") |
|
|
| |
| for agent, other in [(initiator, target), (target, initiator)]: |
| agent.add_observation( |
| tick=self.clock.total_ticks, |
| day=self.clock.day, |
| time_str=self.clock.time_str, |
| content=f"Started a conversation with {other.name} at {agent.location}", |
| importance=5, |
| involved_agents=[other.id], |
| ) |
| |
| agent.relationships.get_or_create(other.id, other.name) |
|
|
| def _finish_conversation(self, conv: Conversation) -> None: |
| """Record a finished conversation in both agents' memories.""" |
| if len(conv.turns) < 2: |
| return |
|
|
| summary = f"Had a conversation about '{conv.topic}' with " |
| for agent_id in conv.participants: |
| agent = self.agents.get(agent_id) |
| if not agent: |
| continue |
| other_ids = [p for p in conv.participants if p != agent_id] |
| other_names = [self.agents[oid].name for oid in other_ids if oid in self.agents] |
| agent.memory.add( |
| tick=self.clock.total_ticks, |
| day=self.clock.day, |
| time_str=self.clock.time_str, |
| memory_type=MemoryType.CONVERSATION, |
| content=f"Had a conversation about '{conv.topic}' with {', '.join(other_names)}.", |
| importance=6, |
| location=conv.location, |
| involved_agents=other_ids, |
| ) |
|
|
| |
| self.conversation_history.append(conv.to_dict()) |
| if len(self.conversation_history) > self._max_conversation_history: |
| self.conversation_history = self.conversation_history[-self._max_conversation_history:] |
|
|
| |
| for agent_id in conv.participants: |
| agent = self.agents.get(agent_id) |
| if agent and agent.alive: |
| agent.community_score += 0.5 |
|
|
| self._emit( |
| f" [CONV END] Conversation about '{conv.topic}' between " |
| f"{', '.join(self.agents[p].name for p in conv.participants if p in self.agents)} ended." |
| ) |
|
|
| |
| self._maybe_gossip(conv) |
|
|
| def _maybe_gossip(self, conv: Conversation) -> None: |
| """After a conversation, participants might share info about a third person.""" |
| if len(conv.participants) < 2 or random.random() > 0.35: |
| return |
|
|
| from soci.actions.social import propagate_gossip |
|
|
| p1 = self.agents.get(conv.participants[0]) |
| p2 = self.agents.get(conv.participants[1]) |
| if not p1 or not p2: |
| return |
|
|
| |
| p1_known = {r.agent_id for r in p1.relationships.get_closest(10) if r.familiarity > 0.2} |
| p2_known = {r.agent_id for r in p2.relationships.get_closest(10) if r.familiarity > 0.2} |
| mutual = (p1_known & p2_known) - {p1.id, p2.id} |
|
|
| if not mutual: |
| return |
|
|
| about_id = random.choice(list(mutual)) |
| about = self.agents.get(about_id) |
| if not about: |
| return |
|
|
| |
| speaker, listener = (p1, p2) if random.random() < 0.5 else (p2, p1) |
| rel = speaker.relationships.get(about_id) |
| if not rel: |
| return |
|
|
| |
| if rel.sentiment > 0.7: |
| note = f"{about.name} is really great, always so helpful" |
| elif rel.sentiment < 0.3: |
| note = f"I've had some issues with {about.name} lately" |
| else: |
| note = f"I ran into {about.name} the other day" |
|
|
| propagate_gossip(speaker, listener, about_id, about.name, note, self.clock.total_ticks) |
| self._emit(f" [GOSSIP] {speaker.name} told {listener.name} about {about.name}") |
|
|
| async def _generate_reflection(self, agent: Agent) -> None: |
| """Generate a reflection for an agent about recent experiences.""" |
| recent = agent.memory.get_recent(15) |
| recent_text = "\n".join( |
| f"- [{m.time_str}] {m.content}" for m in recent |
| ) |
|
|
| world_desc = self.events.get_world_description() |
| loc_desc = self.city.describe_location(agent.location, exclude_agent=agent.id) |
|
|
| prompt = REFLECT_PROMPT.format( |
| time_str=self.clock.time_str, |
| day=self.clock.day, |
| context=agent.build_context(self.clock.total_ticks, world_desc, loc_desc), |
| recent_memories=recent_text, |
| ) |
|
|
| result = await self.llm.complete_json( |
| system=agent.persona.system_prompt(), |
| user_message=prompt, |
| model=MODEL_HAIKU, |
| temperature=agent.persona.llm_temperature, |
| max_tokens=512, |
| ) |
|
|
| reflections = result.get("reflections", []) |
| mood_shift = result.get("mood_shift", 0.0) |
| |
| try: |
| mood_shift = max(-0.3, min(0.3, float(mood_shift))) |
| except (TypeError, ValueError): |
| mood_shift = 0.0 |
|
|
| for ref_text in reflections: |
| if not isinstance(ref_text, str): |
| ref_text = str(ref_text) |
| agent.add_reflection( |
| tick=self.clock.total_ticks, |
| day=self.clock.day, |
| time_str=self.clock.time_str, |
| content=ref_text, |
| ) |
|
|
| agent.mood = max(-1.0, min(1.0, agent.mood + mood_shift)) |
| agent.memory.reset_reflection_accumulator() |
|
|
| if reflections: |
| self._emit(f" [REFLECT] {agent.name}: {reflections[0]}") |
|
|
| |
| life_event = result.get("life_event") |
| if life_event and isinstance(life_event, dict): |
| evt_type = life_event.get("type", "milestone") |
| evt_desc = life_event.get("description", "") |
| if evt_desc: |
| agent.add_life_event(self.clock.day, self.clock.total_ticks, evt_type, evt_desc) |
| agent.add_observation( |
| tick=self.clock.total_ticks, day=self.clock.day, |
| time_str=self.clock.time_str, |
| content=f"Life milestone: {evt_desc}", importance=9, |
| ) |
| self._emit(f" [LIFE] {agent.name}: {evt_desc}") |
|
|
| |
| goal_update = result.get("goal_update") |
| if goal_update and isinstance(goal_update, dict): |
| action = goal_update.get("action", "") |
| if action == "add" and goal_update.get("description"): |
| agent.add_goal(goal_update["description"]) |
| self._emit(f" [GOAL] {agent.name} new goal: {goal_update['description']}") |
| elif action == "complete" and goal_update.get("goal_id") is not None: |
| try: |
| gid = int(goal_update["goal_id"]) |
| agent.update_goal(gid, status="completed", progress=1.0) |
| self._emit(f" [GOAL] {agent.name} completed a goal!") |
| except (TypeError, ValueError): |
| pass |
| elif action == "progress" and goal_update.get("goal_id") is not None: |
| try: |
| gid = int(goal_update["goal_id"]) |
| prog = float(goal_update.get("progress", 0.5)) |
| agent.update_goal(gid, progress=prog) |
| except (TypeError, ValueError): |
| pass |
|
|
| def _tick_romance(self) -> None: |
| """Develop romantic attractions between compatible agents at the same location.""" |
| agents_list = list(self.agents.values()) |
|
|
| for agent in agents_list: |
| if not agent.alive or agent.persona.age < 16: |
| continue |
| loc = self.city.get_location(agent.location) |
| if not loc: |
| continue |
|
|
| for other_id in loc.occupants: |
| if other_id == agent.id or other_id not in self.agents: |
| continue |
| other = self.agents[other_id] |
|
|
| rel = agent.relationships.get(other_id) |
| if not rel or rel.familiarity < 0.15: |
| continue |
|
|
| |
| if agent.partner_id and agent.partner_id != other_id: |
| continue |
|
|
| |
| a_gender = agent.persona.gender |
| o_gender = other.persona.gender |
| attracted = ( |
| a_gender == "unknown" |
| or a_gender == "nonbinary" |
| or o_gender == "unknown" |
| or o_gender == "nonbinary" |
| or a_gender != o_gender |
| ) |
| if not attracted: |
| continue |
|
|
| |
| if rel.sentiment > 0.6 and rel.trust > 0.5: |
| |
| growth = 0.008 |
| |
| growth += (agent.persona.agreeableness / 100.0) * 0.005 |
| |
| growth += rel.familiarity * 0.005 |
| |
| if agent.state.value == "in_conversation": |
| growth += 0.01 |
|
|
| rel.romantic_interest = min(1.0, rel.romantic_interest + growth) |
|
|
| |
| ri = rel.romantic_interest |
| status = rel.relationship_status |
|
|
| if status == "none" and ri > 0.25: |
| rel.relationship_status = "crushing" |
| self._emit(f" [ROMANCE] {agent.name} has developed a crush on {other.name}!") |
| agent.add_observation( |
| tick=self.clock.total_ticks, day=self.clock.day, |
| time_str=self.clock.time_str, |
| content=f"I think I'm developing feelings for {other.name}...", |
| importance=7, involved_agents=[other_id], |
| ) |
|
|
| elif status == "crushing" and ri > 0.5 and rel.familiarity > 0.4: |
| |
| other_rel = other.relationships.get(agent.id) |
| if other_rel and other_rel.romantic_interest > 0.3: |
| rel.relationship_status = "dating" |
| other_rel.relationship_status = "dating" |
| agent.partner_id = other_id |
| other.partner_id = agent.id |
| self._emit(f" [ROMANCE] {agent.name} and {other.name} have started dating!") |
| for a, o in [(agent, other), (other, agent)]: |
| a.add_observation( |
| tick=self.clock.total_ticks, day=self.clock.day, |
| time_str=self.clock.time_str, |
| content=f"I'm now dating {o.name}! I feel excited and nervous.", |
| importance=9, involved_agents=[o.id], |
| ) |
| a.add_life_event(self.clock.day, self.clock.total_ticks, |
| "dating", f"Started dating {o.name}") |
| agent.mood = min(1.0, agent.mood + 0.3) |
| other.mood = min(1.0, other.mood + 0.3) |
|
|
| elif status == "dating" and ri > 0.75 and rel.familiarity > 0.7: |
| other_rel = other.relationships.get(agent.id) |
| if other_rel and other_rel.romantic_interest > 0.65: |
| rel.relationship_status = "engaged" |
| other_rel.relationship_status = "engaged" |
| self._emit(f" [ROMANCE] {agent.name} and {other.name} got engaged!") |
| for a, o in [(agent, other), (other, agent)]: |
| a.add_observation( |
| tick=self.clock.total_ticks, day=self.clock.day, |
| time_str=self.clock.time_str, |
| content=f"{o.name} and I are engaged! This is the happiest day of my life.", |
| importance=10, involved_agents=[o.id], |
| ) |
| a.add_life_event(self.clock.day, self.clock.total_ticks, |
| "engaged", f"Got engaged to {o.name}") |
| agent.mood = min(1.0, agent.mood + 0.4) |
| other.mood = min(1.0, other.mood + 0.4) |
|
|
| elif status == "engaged" and ri > 0.9 and rel.interaction_count > 15: |
| other_rel = other.relationships.get(agent.id) |
| if other_rel and other_rel.romantic_interest > 0.8: |
| rel.relationship_status = "married" |
| other_rel.relationship_status = "married" |
| |
| church = self.city.get_location("church") |
| if church: |
| for a in [agent, other]: |
| old_loc = self.city.get_location(a.location) |
| if old_loc and a.location != "church": |
| old_loc.remove_occupant(a.id) |
| church.add_occupant(a.id) |
| a.location = "church" |
| self._emit(f" [ROMANCE] {agent.name} and {other.name} got married at the church!") |
| for a, o in [(agent, other), (other, agent)]: |
| a.add_observation( |
| tick=self.clock.total_ticks, day=self.clock.day, |
| time_str=self.clock.time_str, |
| content=f"I married {o.name} today at the church. I couldn't be happier.", |
| importance=10, involved_agents=[o.id], |
| ) |
| a.add_life_event(self.clock.day, self.clock.total_ticks, |
| "married", f"Married {o.name} at St. Mary's Church") |
| |
| a.community_score += 5.0 |
|
|
| def _tick_pregnancy(self) -> None: |
| """Handle pregnancy for married couples. Children are born at the hospital after ~9 sim-months.""" |
| import random as _rand |
| |
| PREGNANCY_DURATION_TICKS = 864 |
|
|
| for agent in list(self.agents.values()): |
| |
| if (agent.persona.gender == "female" |
| and not agent.pregnant |
| and agent.partner_id |
| and agent.partner_id in self.agents |
| and len(agent.children) < 3): |
| partner = self.agents[agent.partner_id] |
| rel = agent.relationships.get(partner.id) |
| if (rel and rel.relationship_status == "married" |
| and agent.location == partner.location |
| and agent.location == agent.persona.home_location |
| and _rand.random() < 0.002): |
| agent.pregnant = True |
| agent.pregnancy_start_tick = self.clock.total_ticks |
| agent.pregnancy_partner_id = partner.id |
| agent.add_life_event(self.clock.day, self.clock.total_ticks, |
| "pregnant", f"Expecting a baby with {partner.name}!") |
| partner.add_life_event(self.clock.day, self.clock.total_ticks, |
| "pregnant", f"{agent.name} and I are expecting a baby!") |
| for a in (agent, partner): |
| a.add_observation( |
| tick=self.clock.total_ticks, day=self.clock.day, |
| time_str=self.clock.time_str, |
| content=f"We're going to have a baby!", |
| importance=10, involved_agents=[partner.id if a == agent else agent.id], |
| ) |
| a.mood = min(1.0, a.mood + 0.4) |
| self._emit(f" [LIFE] {agent.name} and {partner.name} are expecting!") |
|
|
| |
| if agent.pregnant: |
| elapsed = self.clock.total_ticks - agent.pregnancy_start_tick |
| if elapsed >= (PREGNANCY_DURATION_TICKS - 96) and agent.location != "hospital": |
| hospital = self.city.get_location("hospital") |
| if hospital: |
| old_loc = self.city.get_location(agent.location) |
| if old_loc: |
| old_loc.remove_occupant(agent.id) |
| hospital.add_occupant(agent.id) |
| agent.location = "hospital" |
| agent.add_observation( |
| tick=self.clock.total_ticks, day=self.clock.day, |
| time_str=self.clock.time_str, |
| content="Going to the hospital — the baby is coming soon!", |
| importance=8, |
| ) |
| self._emit(f" [LIFE] {agent.name} went to the hospital for delivery!") |
|
|
| if elapsed >= PREGNANCY_DURATION_TICKS: |
| partner = self.agents.get(agent.pregnancy_partner_id) |
| |
| import random as _r |
| baby_names_m = ["Oliver", "Liam", "Noah", "Elias", "Lucas", "Theo", "Leo", "Max"] |
| baby_names_f = ["Emma", "Olivia", "Sophia", "Mia", "Isabella", "Zoe", "Luna", "Aria"] |
| is_girl = _r.random() < 0.5 |
| pool = baby_names_f if is_girl else baby_names_m |
| |
| used = set(agent.children) | {a.name.split()[-1] for a in self.agents.values()} |
| available = [n for n in pool if n not in used] |
| baby_name = _r.choice(available) if available else _r.choice(pool) |
| |
| surname = agent.name.split()[-1] if " " in agent.name else "" |
| full_baby_name = f"{baby_name} {surname}".strip() |
|
|
| agent.pregnant = False |
| agent.children.append(full_baby_name) |
| agent.add_life_event(self.clock.day, self.clock.total_ticks, |
| "child_born", f"Gave birth to {full_baby_name}!") |
| agent.add_observation( |
| tick=self.clock.total_ticks, day=self.clock.day, |
| time_str=self.clock.time_str, |
| content=f"Our baby {full_baby_name} was born today! I'm overwhelmed with joy.", |
| importance=10, |
| ) |
| if partner: |
| partner.children.append(full_baby_name) |
| partner.add_life_event(self.clock.day, self.clock.total_ticks, |
| "child_born", f"{agent.name} and I welcomed {full_baby_name}!") |
| partner.add_observation( |
| tick=self.clock.total_ticks, day=self.clock.day, |
| time_str=self.clock.time_str, |
| content=f"Our baby {full_baby_name} was born! I'm a parent now!", |
| importance=10, |
| ) |
| partner.mood = min(1.0, partner.mood + 0.5) |
| agent.mood = min(1.0, agent.mood + 0.5) |
| self._emit(f" [LIFE] {agent.name} gave birth to {full_baby_name}!") |
|
|
| |
| baby_id = f"baby_{full_baby_name.lower().replace(' ', '_')}_{self.clock.total_ticks}" |
| baby_gender = "female" if is_girl else "male" |
| baby_persona = Persona( |
| id=baby_id, |
| name=full_baby_name, |
| age=0, |
| occupation="infant", |
| gender=baby_gender, |
| background=f"Born to {agent.name}" + (f" and {partner.name}" if partner else "") + " in Soci City.", |
| home_location=agent.persona.home_location, |
| work_location="", |
| openness=_r.randint(3, 8), |
| conscientiousness=_r.randint(3, 8), |
| extraversion=_r.randint(3, 8), |
| agreeableness=_r.randint(4, 9), |
| neuroticism=_r.randint(2, 7), |
| ) |
| baby_agent = Agent(baby_persona) |
| baby_agent._birth_day = self.clock.day |
| baby_agent._birth_age = 0 |
| baby_agent.parent_ids = [agent.id] + ([partner.id] if partner else []) |
| baby_agent.lifecycle_stage = "infant" |
| |
| self.add_agent(baby_agent) |
| self._emit(f" [LIFE] New citizen: {full_baby_name} born!") |
|
|
| def _tick_aging(self) -> None: |
| """Update ages for all agents. Check for death of elderly agents.""" |
| for agent in list(self.agents.values()): |
| if not agent.alive: |
| continue |
| old_age = agent.persona.age |
| changed = agent.tick_age(self.clock.day) |
| if changed: |
| new_age = agent.persona.age |
| |
| agent.add_life_event( |
| self.clock.day, self.clock.total_ticks, |
| "birthday", f"Turned {new_age} years old", |
| ) |
| agent.add_observation( |
| tick=self.clock.total_ticks, day=self.clock.day, |
| time_str=self.clock.time_str, |
| content=f"Today is my birthday! I'm now {new_age} years old.", |
| importance=6, |
| ) |
| self._emit(f" [LIFE] {agent.name} turned {new_age}!") |
|
|
| |
| if new_age == 4: |
| agent.add_goal("Go to kindergarten", term="long") |
| self._emit(f" [LIFE] {agent.name} is old enough for kindergarten!") |
| elif new_age == 6: |
| agent.add_goal("Do well in school", term="long") |
| elif new_age == 12: |
| agent.add_goal("Graduate high school", term="long") |
| elif new_age == 18: |
| agent.add_goal("Go to university", term="long") |
| agent.add_goal("Get a job", term="long") |
| elif new_age == 22: |
| agent.add_goal("Start a career", term="long") |
|
|
| |
| if agent.persona.age >= 80 and agent.check_death(self.clock.day): |
| agent.die(self.clock.day, self.clock.total_ticks) |
| self._emit(f" [DEATH] {agent.name} passed away at age {agent.persona.age}.") |
| |
| if agent.partner_id and agent.partner_id in self.agents: |
| partner = self.agents[agent.partner_id] |
| partner.partner_id = None |
| partner.mood = max(-1.0, partner.mood - 0.6) |
| partner.add_life_event( |
| self.clock.day, self.clock.total_ticks, |
| "bereavement", f"Lost my partner {agent.name}", |
| ) |
| partner.add_observation( |
| tick=self.clock.total_ticks, day=self.clock.day, |
| time_str=self.clock.time_str, |
| content=f"My partner {agent.name} passed away. I'm devastated.", |
| importance=10, involved_agents=[agent.id], |
| ) |
| |
| loc = self.city.get_location(agent.location) |
| if loc: |
| loc.remove_occupant(agent.id) |
|
|
| def _tick_divorce(self) -> None: |
| """Check for unhappy marriages that lead to divorce.""" |
| for agent in list(self.agents.values()): |
| if not agent.alive or not agent.partner_id: |
| continue |
| partner = self.agents.get(agent.partner_id) |
| if not partner or not partner.alive: |
| continue |
| rel = agent.relationships.get(partner.id) |
| if not rel or rel.relationship_status != "married": |
| continue |
| |
| if (rel.sentiment < 0.2 and rel.trust < 0.25 |
| and agent.mood < -0.3 |
| and random.random() < 0.01): |
| |
| rel.relationship_status = "divorced" |
| rel.romantic_interest = max(0, rel.romantic_interest - 0.5) |
| other_rel = partner.relationships.get(agent.id) |
| if other_rel: |
| other_rel.relationship_status = "divorced" |
| other_rel.romantic_interest = max(0, other_rel.romantic_interest - 0.5) |
| agent.partner_id = None |
| partner.partner_id = None |
| for a, o in [(agent, partner), (partner, agent)]: |
| a.add_life_event( |
| self.clock.day, self.clock.total_ticks, |
| "divorce", f"Divorced {o.name}", |
| ) |
| a.add_observation( |
| tick=self.clock.total_ticks, day=self.clock.day, |
| time_str=self.clock.time_str, |
| content=f"I divorced {o.name}. Our marriage wasn't working anymore.", |
| importance=9, involved_agents=[o.id], |
| ) |
| a.mood = max(-1.0, a.mood - 0.4) |
| self._emit(f" [ROMANCE] {agent.name} and {partner.name} got divorced!") |
|
|
| def _tick_cohabitation(self) -> None: |
| """Married couples move in together — move to one partner's home.""" |
| processed = set() |
| for agent in list(self.agents.values()): |
| if not agent.alive or not agent.partner_id or agent.id in processed: |
| continue |
| partner = self.agents.get(agent.partner_id) |
| if not partner or not partner.alive or partner.id in processed: |
| continue |
| rel = agent.relationships.get(partner.id) |
| if not rel or rel.relationship_status != "married": |
| continue |
| |
| if agent.persona.home_location == partner.persona.home_location: |
| processed.add(agent.id) |
| processed.add(partner.id) |
| continue |
| |
| new_home = agent.persona.home_location |
| mover = partner |
| partner.persona.home_location = new_home |
| mover.add_life_event( |
| self.clock.day, self.clock.total_ticks, |
| "moved", f"Moved in with {agent.name} at {new_home}", |
| ) |
| agent.add_observation( |
| tick=self.clock.total_ticks, day=self.clock.day, |
| time_str=self.clock.time_str, |
| content=f"{partner.name} moved in with me!", |
| importance=8, involved_agents=[partner.id], |
| ) |
| self._emit(f" [LIFE] {partner.name} moved in with {agent.name}") |
| processed.add(agent.id) |
| processed.add(partner.id) |
|
|
| def _tick_election(self) -> None: |
| """Annual mayor election — all citizens 18+ vote for the most community-oriented agent.""" |
| eligible_voters = [ |
| a for a in self.agents.values() |
| if a.alive and a.persona.age >= 18 |
| ] |
| if len(eligible_voters) < 3: |
| return |
|
|
| |
| candidates = [a for a in eligible_voters if not a.is_player] |
| if not candidates: |
| return |
|
|
| |
| |
| |
| |
| |
| scores: dict[str, float] = {} |
| for c in candidates: |
| score = 0.0 |
| |
| known = c.relationships.get_closest(20) |
| score += len(known) * 2.0 |
| score += sum(r.sentiment for r in known) * 3.0 |
| |
| score += c.persona.agreeableness * 1.5 |
| score += c.persona.extraversion * 0.5 |
| |
| score += max(0, c.mood) * 5.0 |
| score += c.community_score |
| scores[c.id] = score |
|
|
| |
| votes: dict[str, int] = {c.id: 0 for c in candidates} |
| for voter in eligible_voters: |
| |
| voter_scores = {} |
| for c in candidates: |
| if c.id == voter.id: |
| continue |
| base = scores.get(c.id, 0) |
| rel = voter.relationships.get(c.id) |
| if rel: |
| base += rel.sentiment * 10 + rel.trust * 5 |
| voter_scores[c.id] = max(0.1, base) |
| if not voter_scores: |
| continue |
| total = sum(voter_scores.values()) |
| r = random.random() * total |
| cumulative = 0.0 |
| chosen = list(voter_scores.keys())[0] |
| for cid, s in voter_scores.items(): |
| cumulative += s |
| if r <= cumulative: |
| chosen = cid |
| break |
| votes[chosen] = votes.get(chosen, 0) + 1 |
|
|
| |
| winner_id = max(votes, key=lambda k: votes[k]) |
| winner = self.agents[winner_id] |
| vote_count = votes[winner_id] |
|
|
| |
| for a in self.agents.values(): |
| if a.is_mayor: |
| a.is_mayor = False |
| a.add_life_event( |
| self.clock.day, self.clock.total_ticks, |
| "politics", "Term as Mayor ended", |
| ) |
|
|
| |
| winner.is_mayor = True |
| winner.mayor_term_start_day = self.clock.day |
| winner.add_life_event( |
| self.clock.day, self.clock.total_ticks, |
| "politics", f"Elected as Mayor of Soci City with {vote_count} votes!", |
| ) |
| winner.add_observation( |
| tick=self.clock.total_ticks, day=self.clock.day, |
| time_str=self.clock.time_str, |
| content=f"I was elected Mayor of Soci City! {vote_count} people voted for me.", |
| importance=10, |
| ) |
| winner.mood = min(1.0, winner.mood + 0.5) |
| self._emit(f" [ELECTION] {winner.name} elected Mayor with {vote_count}/{len(eligible_voters)} votes!") |
|
|
| |
| for voter in eligible_voters: |
| if voter.id != winner_id: |
| voter.add_observation( |
| tick=self.clock.total_ticks, day=self.clock.day, |
| time_str=self.clock.time_str, |
| content=f"{winner.name} was elected as our new Mayor.", |
| importance=6, involved_agents=[winner_id], |
| ) |
|
|
| def _tick_short_term_goals(self) -> None: |
| """Refresh short-term goals weekly — expire old ones, add new ones.""" |
| import random as _r |
| short_goals_pool = [ |
| "Meet someone new this week", |
| "Try a new restaurant", |
| "Exercise at least twice", |
| "Read something interesting", |
| "Help a neighbor", |
| "Visit the park", |
| "Have a deep conversation", |
| "Cook something special", |
| "Explore a new part of town", |
| "Catch up with an old friend", |
| "Organize my home", |
| "Learn something new", |
| "Spend time outdoors", |
| "Do something creative", |
| ] |
| for agent in self.agents.values(): |
| if not agent.alive or agent.persona.age < 6: |
| continue |
| |
| for g in agent.goals: |
| if g.get("term") == "short" and g["status"] == "active": |
| if g["progress"] >= 0.7: |
| g["status"] = "completed" |
| g["progress"] = 1.0 |
| else: |
| g["status"] = "abandoned" |
| |
| count = _r.randint(1, 2) |
| for _ in range(count): |
| goal_desc = _r.choice(short_goals_pool) |
| agent.add_goal(goal_desc, term="short") |
|
|
| def get_state_summary(self) -> dict: |
| """Get a summary of the current simulation state.""" |
| |
| mayor = next((a for a in self.agents.values() if a.is_mayor and a.alive), None) |
| return { |
| "clock": self.clock.to_dict(), |
| "weather": self.events.weather.value, |
| "active_events": [e.to_dict() for e in self.events.active_events], |
| "mayor": {"id": mayor.id, "name": mayor.name} if mayor else None, |
| "agents": { |
| aid: { |
| "name": a.name, |
| "age": a.persona.age, |
| "gender": a.persona.gender, |
| "occupation": a.persona.occupation, |
| "location": a.location, |
| "state": a.state.value, |
| "mood": round(a.mood, 2), |
| "needs": a.needs.to_dict(), |
| "action": a.current_action.detail if a.current_action else "idle", |
| "daily_plan": a.daily_plan, |
| "partner_id": a.partner_id, |
| "is_player": a.is_player, |
| "pregnant": a.pregnant, |
| "children_count": len(a.children), |
| "alive": a.alive, |
| "lifecycle_stage": a.lifecycle_stage, |
| "is_mayor": a.is_mayor, |
| } |
| for aid, a in self.agents.items() |
| }, |
| "active_conversations": len(self.active_conversations), |
| "llm_provider": getattr(self.llm, "provider", "unknown"), |
| "llm_model": getattr(self.llm, "default_model", "unknown"), |
| "llm_status": getattr(self.llm, "llm_status", "active"), |
| "llm_calls_last_tick": self._llm_calls_this_tick, |
| "llm_skipped": self._skip_llm_this_tick, |
| "llm_usage": self.llm.usage.summary(), |
| } |
|
|
| def to_dict(self) -> dict: |
| """Serialize full simulation state.""" |
| return { |
| "city": self.city.to_dict(), |
| "clock": self.clock.to_dict(), |
| "agents": {aid: a.to_dict() for aid, a in self.agents.items()}, |
| "events": self.events.to_dict(), |
| "entropy": self.entropy.to_dict(), |
| "conversation_counter": self._conversation_counter, |
| "conversation_history": self.conversation_history, |
| } |
|
|
| @classmethod |
| def from_dict(cls, data: dict, llm: ClaudeClient) -> Simulation: |
| """Restore a simulation from serialized state.""" |
| city = City.from_dict(data["city"]) |
| clock = SimClock.from_dict(data["clock"]) |
| sim = cls(city=city, clock=clock, llm=llm) |
| sim.events = EventSystem.from_dict(data["events"]) |
| sim.entropy = EntropyManager.from_dict(data["entropy"]) |
| sim._conversation_counter = data.get("conversation_counter", 0) |
| sim.conversation_history = data.get("conversation_history", []) |
| for aid, agent_data in data["agents"].items(): |
| agent = Agent.from_dict(agent_data) |
| sim.agents[agent.id] = agent |
| |
| sim._rebuild_routines() |
| return sim |
|
|