Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions llm/llm_epidemic/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
GEMINI_API_KEY=AIzaSyA_5RZAEQ2BOC0g2PNF5OCN9kv3M7cfgRE
15 changes: 15 additions & 0 deletions llm/llm_epidemic/.env.example
Original file line number Diff line number Diff line change
@@ -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
50 changes: 50 additions & 0 deletions llm/llm_epidemic/README.md
Original file line number Diff line number Diff line change
@@ -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.
81 changes: 81 additions & 0 deletions llm/llm_epidemic/app.py
Original file line number Diff line number Diff line change
@@ -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",
)
Empty file.
112 changes: 112 additions & 0 deletions llm/llm_epidemic/llm_epidemic/agent.py
Original file line number Diff line number Diff line change
@@ -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}"]
107 changes: 107 additions & 0 deletions llm/llm_epidemic/llm_epidemic/model.py
Original file line number Diff line number Diff line change
@@ -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)
Binary file added llm/llm_epidemic/llm_epidemic_dashboard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions llm/llm_epidemic/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mesa>=3.2
mesa-llm>=0.1.0
solara>=1.50
matplotlib>=3.7
python-dotenv
1 change: 1 addition & 0 deletions llm/llm_opinion_dynamics/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
GEMINI_API_KEY=AIzaSyA_5RZAEQ2BOC0g2PNF5OCN9kv3M7cfgRE