Spaces:
Sleeping
Sleeping
Download simulator.py from build-small-hackathon/analog-town: direct link, hf CLI and curl.
- Browser
- Download file 8.26 kB
-
https://huggingface.co/spaces/build-small-hackathon/analog-town/resolve/main/simulator.py
- Command line
-
hf download hf://spaces/build-small-hackathon/analog-town/simulator.py
-
curl -L -o simulator.py https://huggingface.co/spaces/build-small-hackathon/analog-town/resolve/main/simulator.py
8.26 kB
| """Simulation engine for Analog Town state transitions.""" | |
| import json | |
| import traceback | |
| from datetime import datetime | |
| from schemas import ( | |
| AgentProfile, | |
| AgentState, | |
| BroadcastEvent, | |
| SimulationResult, | |
| StateTransition, | |
| Town, | |
| ) | |
| from prompts import SYSTEM_PROMPT, STATE_TRANSITION_PROMPT | |
| from model_client import ModelClient | |
| class Simulator: | |
| """Runs state-transition simulations for all agents in a town.""" | |
| def __init__(self, model_client: ModelClient | None = None): | |
| self.model_client = model_client or ModelClient() | |
| def _get_initial_state(self, agent: AgentProfile) -> AgentState: | |
| """Create initial agent state from profile defaults.""" | |
| return AgentState( | |
| agent_id=agent.id, | |
| mood_label="baseline", | |
| trust=0.5, | |
| anger=0.2, | |
| fear=0.3, | |
| hope=0.5, | |
| curiosity=0.5, | |
| social_energy=0.5, | |
| current_belief="No strong opinion yet.", | |
| active_memory=None, | |
| unresolved_tension=None, | |
| ) | |
| def _clamp(self, value: float, min_val: float = 0.0, max_val: float = 1.0) -> float: | |
| """Clamp a value between min and max.""" | |
| return max(min_val, min(max_val, value)) | |
| def _apply_emotion_deltas( | |
| self, old_state: AgentState, deltas: dict[str, float] | |
| ) -> dict[str, float]: | |
| """Apply emotion deltas to old state, clamping to [0.0, 1.0].""" | |
| emotions = {} | |
| for key in ["trust", "anger", "fear", "hope", "curiosity", "social_energy"]: | |
| old_val = getattr(old_state, key, 0.5) | |
| delta = deltas.get(key, 0.0) | |
| emotions[key] = self._clamp(old_val + delta) | |
| return emotions | |
| def _validate_transition( | |
| self, transition_data: dict, agent: AgentProfile | |
| ) -> tuple[bool, str]: | |
| """Validate a raw transition dict before parsing into Pydantic model.""" | |
| errors = [] | |
| # Check agent_id matches | |
| if transition_data.get("agent_id") != agent.id: | |
| transition_data["agent_id"] = agent.id | |
| # Check required fields | |
| required = [ | |
| "event_summary_from_agent_view", | |
| "noticed_detail", | |
| "activated_memory", | |
| "value_conflict", | |
| "emotion_delta", | |
| "updated_state", | |
| "internal_monologue", | |
| "likely_private_action", | |
| "likely_public_action", | |
| "uncertainty", | |
| ] | |
| for field in required: | |
| if field not in transition_data: | |
| errors.append(f"Missing required field: {field}") | |
| # Check emotion values in updated_state | |
| updated = transition_data.get("updated_state", {}) | |
| for emotion in ["trust", "anger", "fear", "hope", "curiosity", "social_energy"]: | |
| val = updated.get(emotion) | |
| if val is not None: | |
| if not isinstance(val, (int, float)): | |
| errors.append(f"Emotion '{emotion}' must be a number, got {type(val)}") | |
| elif val < 0.0 or val > 1.0: | |
| # Clamp instead of error | |
| updated[emotion] = self._clamp(float(val)) | |
| # Ensure agent_id in updated_state | |
| if "updated_state" in transition_data: | |
| transition_data["updated_state"]["agent_id"] = agent.id | |
| # Check internal monologue is not empty | |
| monologue = transition_data.get("internal_monologue", "") | |
| if not monologue or not monologue.strip(): | |
| errors.append("Internal monologue is empty") | |
| # Ensure safety note | |
| if "safety_note" not in transition_data or not transition_data["safety_note"]: | |
| transition_data["safety_note"] = ( | |
| "This is a fictional perspective rehearsal, not a prediction." | |
| ) | |
| if errors: | |
| return False, "; ".join(errors) | |
| return True, "" | |
| def run_agent_transition( | |
| self, | |
| agent: AgentProfile, | |
| state: AgentState, | |
| event: BroadcastEvent, | |
| day: int = 1, | |
| ) -> StateTransition: | |
| """Run a single agent through the state transition. | |
| Args: | |
| agent: The agent's profile | |
| state: The agent's current state | |
| event: The broadcast event | |
| day: 1-indexed broadcast day, used to instruct the model to vary follow-up beats | |
| Returns: | |
| StateTransition with updated state and monologue | |
| Raises: | |
| RuntimeError: If both generation and repair fail | |
| """ | |
| user_prompt = STATE_TRANSITION_PROMPT.format( | |
| agent_profile=agent.model_dump_json(indent=2), | |
| previous_state=state.model_dump_json(indent=2), | |
| broadcast_event=event.model_dump_json(indent=2), | |
| ) | |
| if day > 1: | |
| user_prompt = ( | |
| f"DAY {day} OF THIS SCENARIO. " | |
| f"The agent has already lived through {day - 1} previous broadcast(s). " | |
| "The fields current_belief, active_memory, and unresolved_tension in 'Previous State' " | |
| "capture where the agent ENDED LAST TIME — they are CONTEXT, not your script. " | |
| "Write a FRESH internal_monologue that is clearly different in wording from any prior beat: " | |
| "the agent has had time to process, talk to others, sleep on it, or harden their view. " | |
| "Make at least one emotion_delta non-zero. Do NOT echo any sentence verbatim from current_belief.\n\n" | |
| + user_prompt | |
| ) | |
| original_temp = getattr(self.model_client, "temperature", 0.3) | |
| try: | |
| if day > 1: | |
| self.model_client.temperature = min(0.85, original_temp + 0.25) | |
| transition_data = self.model_client.generate_json( | |
| system_prompt=SYSTEM_PROMPT, | |
| user_prompt=user_prompt, | |
| ) | |
| finally: | |
| self.model_client.temperature = original_temp | |
| # Validate and fix | |
| is_valid, error_msg = self._validate_transition(transition_data, agent) | |
| if not is_valid: | |
| raise RuntimeError(f"Transition validation failed: {error_msg}") | |
| # Parse into Pydantic model | |
| transition = StateTransition(**transition_data) | |
| return transition | |
| def run_town_simulation( | |
| self, | |
| town: Town, | |
| event: BroadcastEvent, | |
| previous_states: dict[str, AgentState] | None = None, | |
| progress_callback=None, | |
| day: int = 1, | |
| ) -> SimulationResult: | |
| """Run simulation for all agents in the town. | |
| Args: | |
| town: The town with agents | |
| event: The broadcast event | |
| previous_states: Optional dict of agent_id -> AgentState seeded from prior run | |
| progress_callback: Optional callback(agent_name, status, index, total) | |
| Returns: | |
| SimulationResult with all transitions (failed agents are skipped) | |
| """ | |
| transitions = [] | |
| total = len(town.agents) | |
| for i, agent in enumerate(town.agents): | |
| agent_name = agent.name | |
| try: | |
| if progress_callback: | |
| progress_callback(agent_name, "processing", i, total) | |
| state = ( | |
| previous_states.get(agent.id, self._get_initial_state(agent)) | |
| if previous_states | |
| else self._get_initial_state(agent) | |
| ) | |
| transition = self.run_agent_transition(agent, state, event, day=day) | |
| transitions.append(transition) | |
| if progress_callback: | |
| progress_callback(agent_name, "complete", i, total) | |
| except Exception as e: | |
| # Don't crash on individual agent failures | |
| print(f"⚠ Agent '{agent_name}' failed: {e}") | |
| traceback.print_exc() | |
| if progress_callback: | |
| progress_callback(agent_name, f"failed: {str(e)[:100]}", i, total) | |
| continue | |
| return SimulationResult( | |
| town_id=town.id, | |
| event=event, | |
| transitions=transitions, | |
| created_at=datetime.now().isoformat(), | |
| ) | |