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
87 changes: 87 additions & 0 deletions llm/llm_epidemic/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# 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 —
instead of following fixed stochastic transition probabilities.

Unlike traditional SIR models, agents here _reason_ about their situation — weighing
personal health risk, observed neighbor states, and community responsibility — before
choosing an action.

## The Disease Dynamics

Each agent is in one of three states:

| Color | State | Behavior |
|-------|-------|----------|
| 🔵 Blue | Susceptible | Healthy, can be infected by neighbors |
| 🔴 Red | Infected | Sick, reasons about isolation vs. movement |
| 🟢 Green | Recovered | Immune, moves freely |

At each step, infected agents decide whether to isolate or continue moving. Susceptible
agents assess neighbor states and choose how cautiously to behave. These decisions,
driven by LLM reasoning rather than probability parameters, shape the epidemic curve.

## What makes this different from classical SIR

Classical SIR uses fixed β (transmission rate) and γ (recovery rate). The epidemic curve
is fully determined by these two numbers.

Here, agents **reason** at each step:

> "I am infected. My neighbors include susceptible individuals. Continuing to move
> freely risks spreading the disease. I should isolate — even though it limits my
> mobility — to protect the community."

This produces **behavioral heterogeneity**: some agents isolate immediately, others
rationalize continued movement. The macro curve still follows SIR dynamics, but
the shape reflects individual decision-making, not just probabilities.

## Visualization

**Step 0 — Initial seeding (3 infected, 17 susceptible):**

![Initial state — blue susceptible agents, red infected seed](epidemic_initial.png)

**Step 3 — Epidemic accelerating (curves crossing):**

![Step 3 — susceptible falling, infected rising, curves cross](epidemic_spreading.png)

**Step 13 — Full SIR arc complete (all recovered):**

![Step 13 — complete bell curve, susceptible=0, recovered=20](epidemic_complete.png)

| Step | Susceptible | Infected | Recovered | Event |
|------|-------------|----------|-----------|-------|
| 0 | ~17 | ~3 | 0 | Initial seeding |
| 1–2 | Falling fast | Rising | 0 | Epidemic accelerating |
| 3 | ~5 | ~18 | ~0 | Near-peak — curves cross |
| 5–7 | ~0 | ~20 | ~0 | Saturation — everyone infected |
| 10+ | 0 | Falling | Rising | Recovery phase begins |
| 13 | 0 | ~0 | ~20 | Full recovery — epidemic over |

**Why this matters:** This is the textbook Kermack-McKendrick (1927) SIR curve reproduced
with **zero hardcoded β or γ parameters**. The epidemic arc — seed → spread → peak →
recovery — emerges entirely from LLM reasoning about individual health decisions.
An infected agent that reasons "I should isolate" slows the curve. One that doesn't
steepens it. Classical SIR cannot represent this individual behavioral variation at all.

## How to Run

```bash
cp .env.example .env # fill in your API key
pip install -r requirements.txt
solara run app.py
```

## Supported LLM Providers

Gemini, OpenAI, Anthropic, Ollama (local) — configured via `.env`.

## 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",
)
Binary file added llm/llm_epidemic/epidemic_complete.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added llm/llm_epidemic/epidemic_initial.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added llm/llm_epidemic/epidemic_spreading.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file.
116 changes: 116 additions & 0 deletions llm/llm_epidemic/llm_epidemic/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
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 agents in spatial neighborhood (Moore radius=1).
# vision=2 lets the LLM observe a wider area; infection requires
# direct contact with an immediate neighbor.
infected_neighbors = []
if hasattr(self, "cell") and self.cell is not None:
for neighbor_cell in self.cell.connections.values():
for agent in neighbor_cell.agents:
if (
hasattr(agent, "health_state")
and agent.health_state == "infected"
and not agent.is_isolating
):
infected_neighbors.append(agent)
infection_probability = min(0.3 * len(infected_neighbors), 0.9)
if self.model.random.random() < infection_probability:
self.health_state = "infected"
self.internal_state = [f"health_state:{self.health_state}"]
Loading