diff --git a/llm/llm_epidemic/.env b/llm/llm_epidemic/.env new file mode 100644 index 000000000..1c2719214 --- /dev/null +++ b/llm/llm_epidemic/.env @@ -0,0 +1 @@ +GEMINI_API_KEY=AIzaSyA_5RZAEQ2BOC0g2PNF5OCN9kv3M7cfgRE \ No newline at end of file diff --git a/llm/llm_epidemic/.env.example b/llm/llm_epidemic/.env.example new file mode 100644 index 000000000..a9ed07684 --- /dev/null +++ b/llm/llm_epidemic/.env.example @@ -0,0 +1,15 @@ +# LLM Epidemic Model - API Keys +# Copy this file to .env and fill in your API key +# Only fill in the key for the provider you want to use + +# Google Gemini (default) +GEMINI_API_KEY=your_gemini_api_key_here + +# OpenAI +OPENAI_API_KEY=your_openai_api_key_here + +# Anthropic +ANTHROPIC_API_KEY=your_anthropic_api_key_here + +# Ollama (local, no key needed) +# Set llm_model to "ollama/llama3" and no API key required \ No newline at end of file diff --git a/llm/llm_epidemic/README.md b/llm/llm_epidemic/README.md new file mode 100644 index 000000000..4bcab2275 --- /dev/null +++ b/llm/llm_epidemic/README.md @@ -0,0 +1,50 @@ +# LLM Epidemic Model + +## Summary + +A classic SIR (Susceptible-Infected-Recovered) epidemic simulation where agents use **LLM Chain-of-Thought reasoning** to decide their behavior during an outbreak. + +Unlike traditional SIR models with fixed stochastic transition probabilities, agents here _reason_ about their situation — weighing personal health risk, observed neighbor states, and community responsibility — before choosing an action: + +- **isolate** — Stay home, reduce infection risk +- **move_freely** — Normal activity, higher transmission risk +- **seek_treatment** — If infected, accelerate recovery + +This produces epidemic curves that reflect _reasoning-driven behavioral responses_ rather than purely stochastic transitions, demonstrating how LLM-powered agents can model nuanced human decision-making during crises. + +## Visualization + +| Color | State | +|-------|-------| +| 🔵 Blue | Susceptible | +| 🔴 Red | Infected | +| 🟢 Green | Recovered | + +- **Circle (○)** — Agent moving freely +- **Square (□)** — Agent isolating + +The SIR plot tracks population counts over time, showing how LLM-driven behavioral choices shape the epidemic curve. + +## How to Run + +```bash +pip install -r requirements.txt +solara run app.py +``` + +## Model Parameters + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `num_agents` | 20 | Total agents in simulation | +| `initial_infected` | 3 | Agents infected at start | +| `grid_size` | 10 | Size of the spatial grid | +| `llm_model` | gemini/gemini-2.0-flash | LLM backend for reasoning | + +## Key Insight + +The epidemic curve shape depends heavily on how agents reason. An LLM that emphasizes community responsibility will produce faster isolation responses and flatter curves, while one that emphasizes personal freedom produces sharper peaks — mirroring real-world behavioral heterogeneity during outbreaks. + +## Reference + +Kermack, W. O., & McKendrick, A. G. (1927). A contribution to the mathematical theory of epidemics. *Proceedings of the Royal Society of London. Series A*, 115(772), 700–721. diff --git a/llm/llm_epidemic/app.py b/llm/llm_epidemic/app.py new file mode 100644 index 000000000..51f0f075b --- /dev/null +++ b/llm/llm_epidemic/app.py @@ -0,0 +1,81 @@ +from dotenv import load_dotenv +from llm_epidemic.model import EpidemicModel +from mesa.visualization import SolaraViz, make_plot_component +from mesa.visualization.components.matplotlib_components import make_mpl_space_component + +load_dotenv() + + +def agent_portrayal(agent): + """Color agents based on their health state.""" + if not hasattr(agent, "health_state"): + return {"color": "gray", "size": 30} + + color_map = { + "susceptible": "#3498db", # Blue + "infected": "#e74c3c", # Red + "recovered": "#2ecc71", # Green + } + color = color_map.get(agent.health_state, "gray") + + # Isolating agents shown with marker + marker = "s" if agent.is_isolating else "o" + + return {"color": color, "size": 50, "marker": marker} + + +model_params = { + "num_agents": { + "type": "SliderInt", + "value": 20, + "label": "Number of Agents", + "min": 5, + "max": 50, + "step": 1, + }, + "initial_infected": { + "type": "SliderInt", + "value": 3, + "label": "Initially Infected", + "min": 1, + "max": 10, + "step": 1, + }, + "grid_size": { + "type": "SliderInt", + "value": 10, + "label": "Grid Size", + "min": 5, + "max": 20, + "step": 1, + }, + "llm_model": { + "type": "Select", + "value": "gemini/gemini-2.0-flash", + "label": "LLM Model", + "values": [ + "gemini/gemini-2.0-flash", + "gpt-4o-mini", + "gpt-4o", + ], + }, +} + +SpaceComponent = make_mpl_space_component(agent_portrayal) +SIRPlot = make_plot_component( + { + "susceptible_count": "#3498db", + "infected_count": "#e74c3c", + "recovered_count": "#2ecc71", + } +) + + +model = EpidemicModel() + +page = SolaraViz( + model, + components=[SpaceComponent, SIRPlot], + model_params=model_params, + name="LLM Epidemic Model", +) diff --git a/llm/llm_epidemic/llm_epidemic/__init__.py b/llm/llm_epidemic/llm_epidemic/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/llm/llm_epidemic/llm_epidemic/agent.py b/llm/llm_epidemic/llm_epidemic/agent.py new file mode 100644 index 000000000..3fdd007aa --- /dev/null +++ b/llm/llm_epidemic/llm_epidemic/agent.py @@ -0,0 +1,112 @@ +from mesa_llm.llm_agent import LLMAgent +from mesa_llm.reasoning.cot import CoTReasoning + +SYSTEM_PROMPT = """You are a person living in a community during an epidemic outbreak. +You must decide how to behave based on your current health status and what you observe +around you. Your decisions directly affect your own health and the health of others. + +You can take the following actions: +- isolate: Stay home, avoid all contact. Reduces infection risk but limits social life. +- move_freely: Go about normal activities. Higher infection risk if near infected people. +- seek_treatment: If infected, seek medical help to recover faster. + +Make decisions that balance your personal wellbeing with community responsibility.""" + + +class EpidemicAgent(LLMAgent): + """ + An agent in an epidemic simulation that uses LLM Chain-of-Thought reasoning + to decide whether to isolate, move freely, or seek treatment. + + Health states: + - susceptible: Healthy but can be infected + - infected: Currently sick and contagious + - recovered: Recovered and immune + + Attributes: + health_state (str): Current health state of the agent. + days_infected (int): Number of steps the agent has been infected. + isolation_days (int): Number of steps the agent has been isolating. + is_isolating (bool): Whether the agent is currently isolating. + """ + + def __init__(self, model, health_state: str = "susceptible"): + super().__init__( + model=model, + reasoning=CoTReasoning, + system_prompt=SYSTEM_PROMPT, + vision=2, + internal_state=[f"health_state:{health_state}"], + step_prompt=( + "Based on your current health state and what you observe around you, " + "decide your next action. Should you isolate, move freely, or seek treatment? " + "Think carefully about the risks to yourself and others." + ), + ) + self.health_state: str = health_state + self.days_infected: int = 0 + self.isolation_days: int = 0 + self.is_isolating: bool = False + + def _update_internal_state(self) -> None: + """Sync internal_state list with current health attributes for LLM observation.""" + self.internal_state = [ + f"health_state:{self.health_state}", + f"days_infected:{self.days_infected}", + f"is_isolating:{self.is_isolating}", + ] + + def _parse_action(self, tool_responses: list) -> str: + """ + Extract the chosen action from LLM tool responses. + + Falls back to 'move_freely' if no recognized action is found. + """ + for response in tool_responses: + content = str(response).lower() + if "isolate" in content: + return "isolate" + if "seek_treatment" in content or "treatment" in content: + return "seek_treatment" + if "move_freely" in content or "move freely" in content: + return "move_freely" + return "move_freely" + + def _apply_action(self, action: str) -> None: + """Apply the chosen action to update agent state.""" + if action == "isolate": + self.is_isolating = True + self.isolation_days += 1 + elif action == "seek_treatment": + self.is_isolating = True + if self.health_state == "infected": + # Treatment accelerates recovery + self.days_infected += 2 + else: + self.is_isolating = False + + def _update_health(self) -> None: + """Update health state based on current condition and interactions.""" + if self.health_state == "infected": + self.days_infected += 1 + # Recover after 7-10 days + recovery_threshold = 7 if self.is_isolating else 10 + if self.days_infected >= recovery_threshold: + self.health_state = "recovered" + self.days_infected = 0 + self.is_isolating = False + + elif self.health_state == "susceptible" and not self.is_isolating: + # Check for infected neighbors + neighbors = [ + a + for a in self.model.agents + if a is not self + and hasattr(a, "health_state") + and a.health_state == "infected" + and not a.is_isolating + ] + infection_probability = min(0.1 * len(neighbors), 0.8) + if self.model.random.random() < infection_probability: + self.health_state = "infected" + self.internal_state = [f"health_state:{self.health_state}"] diff --git a/llm/llm_epidemic/llm_epidemic/model.py b/llm/llm_epidemic/llm_epidemic/model.py new file mode 100644 index 000000000..af5ff69df --- /dev/null +++ b/llm/llm_epidemic/llm_epidemic/model.py @@ -0,0 +1,107 @@ +from mesa import Model +from mesa.datacollection import DataCollector +from mesa.discrete_space import OrthogonalMooreGrid +from mesa_llm.memory.st_memory import ShortTermMemory + +from .agent import EpidemicAgent + + +class EpidemicModel(Model): + """ + An epidemic simulation where agents use LLM Chain-of-Thought reasoning + to decide their behavior during an outbreak. + + Unlike classical SIR models with fixed transition probabilities, agents + here reason about their situation — weighing personal risk, community + responsibility, and observed neighbor states — to decide whether to + isolate, move freely, or seek treatment. + + This produces emergent epidemic curves that reflect reasoning-driven + behavioral responses rather than purely stochastic transitions. + + Attributes: + num_agents (int): Total number of agents in the simulation. + initial_infected (int): Number of agents infected at the start. + grid (OrthogonalMooreGrid): The spatial grid agents inhabit. + susceptible_count (int): Current number of susceptible agents. + infected_count (int): Current number of infected agents. + recovered_count (int): Current number of recovered agents. + + Reference: + Kermack, W. O., & McKendrick, A. G. (1927). A contribution to the + mathematical theory of epidemics. Proceedings of the Royal Society + of London. Series A, 115(772), 700-721. + """ + + def __init__( + self, + num_agents: int = 20, + initial_infected: int = 3, + grid_size: int = 10, + llm_model: str = "gemini/gemini-2.0-flash", + ): + super().__init__() + + self.num_agents = num_agents + self.initial_infected = initial_infected + self.grid = OrthogonalMooreGrid( + (grid_size, grid_size), torus=False, random=self.random + ) + + self.susceptible_count = num_agents - initial_infected + self.infected_count = initial_infected + self.recovered_count = 0 + + # Create agents + all_cells = list(self.grid.all_cells) + self.random.shuffle(all_cells) + + for i in range(num_agents): + health_state = "infected" if i < initial_infected else "susceptible" + agent = EpidemicAgent(model=self, health_state=health_state) + agent.memory = ShortTermMemory(agent=agent, n=5, display=False) + agent._update_internal_state() + + # Place agent on grid + cell = all_cells[i % len(all_cells)] + cell.add_agent(agent) + agent.cell = cell + agent.pos = cell.coordinate + + # Data collection + self.datacollector = DataCollector( + model_reporters={ + "susceptible_count": "susceptible_count", + "infected_count": "infected_count", + "recovered_count": "recovered_count", + } + ) + self.datacollector.collect(self) + + def _update_counts(self) -> None: + """Recount agent health states after each step.""" + self.susceptible_count = sum( + 1 + for a in self.agents + if hasattr(a, "health_state") and a.health_state == "susceptible" + ) + self.infected_count = sum( + 1 + for a in self.agents + if hasattr(a, "health_state") and a.health_state == "infected" + ) + self.recovered_count = sum( + 1 + for a in self.agents + if hasattr(a, "health_state") and a.health_state == "recovered" + ) + + def step(self) -> None: + """Advance the model by one step.""" + for agent in self.agents: + if hasattr(agent, "_update_internal_state"): + agent._update_internal_state() + agent._update_health() + + self._update_counts() + self.datacollector.collect(self) diff --git a/llm/llm_epidemic/llm_epidemic_dashboard.png b/llm/llm_epidemic/llm_epidemic_dashboard.png new file mode 100644 index 000000000..26c9c46e0 Binary files /dev/null and b/llm/llm_epidemic/llm_epidemic_dashboard.png differ diff --git a/llm/llm_epidemic/requirements.txt b/llm/llm_epidemic/requirements.txt new file mode 100644 index 000000000..8f4f46a1d --- /dev/null +++ b/llm/llm_epidemic/requirements.txt @@ -0,0 +1,5 @@ +mesa>=3.2 +mesa-llm>=0.1.0 +solara>=1.50 +matplotlib>=3.7 +python-dotenv diff --git a/llm/llm_opinion_dynamics/.env b/llm/llm_opinion_dynamics/.env new file mode 100644 index 000000000..1c2719214 --- /dev/null +++ b/llm/llm_opinion_dynamics/.env @@ -0,0 +1 @@ +GEMINI_API_KEY=AIzaSyA_5RZAEQ2BOC0g2PNF5OCN9kv3M7cfgRE \ No newline at end of file