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
83 changes: 83 additions & 0 deletions examples/adaptive_risk_agents/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Adaptive Risk Agents

This example demonstrates agents that **adapt their risk-taking behavior
based on past experiences**, implemented using only core Mesa primitives.

The model is intentionally simple in structure but rich in behavior, making it
useful as a diagnostic example for understanding how adaptive decision-making
is currently modeled in Mesa.



## Motivation

Many real-world agents do not follow fixed rules.
Instead, they:

- make decisions under uncertainty,
- remember past outcomes,
- adapt future behavior based on experience.

In Mesa today, modeling this kind of adaptive behavior often results in
a large amount of logic being concentrated inside `agent.step()`, combining
multiple concerns in a single execution phase.

This example exists to **make that structure explicit**, not to abstract it away.



## Model Overview

- Each agent chooses between:
- a **safe action** (low or zero payoff, no risk),
- a **risky action** (stochastic payoff).
- Agents track recent outcomes of risky actions in a short memory window.
- If recent outcomes are negative, agents become more risk-averse.
- If outcomes are positive, agents increase their risk preference.

All behavior is implemented using plain Python and Mesa’s public APIs.



## Observations From This Example

This model intentionally does **not** introduce new abstractions
(tasks, goals, states, schedulers, etc.).

Instead, it highlights several patterns that commonly arise when modeling
adaptive behavior in Mesa today:

- Decision-making, action execution, memory updates, and learning logic
are handled within a single `step()` method.
- There is no explicit separation between decision phases.
- Actions are instantaneous, with no notion of duration or interruption.
- As behaviors grow richer, agent logic can become deeply nested and harder
to maintain.

These observations may be useful input for ongoing discussions around:

- Behavioral frameworks
- Tasks and continuous states
- Richer agent decision abstractions



## Mesa Version & API Alignment

This example is written to align with the **Mesa 4 design direction**:

- Uses `AgentSet` and `shuffle_do`
- Avoids deprecated schedulers
- Avoids `DataCollector`
- Uses keyword-only arguments for public APIs
- Relies on `model.random` for reproducibility

No experimental or private APIs are used.



## Running the Example

From the Mesa repository root:

python -m mesa.examples.adaptive_risk_agents.run
Empty file.
86 changes: 86 additions & 0 deletions examples/adaptive_risk_agents/agents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Adaptive Risk Agent.

An agent that chooses between safe and risky actions and adapts its
risk preference based on past outcomes.

This example intentionally keeps all decision logic inside `step()`
to highlight current limitations in Mesa's behavior modeling.
"""

from __future__ import annotations

from collections import deque

from mesa import Agent


class AdaptiveRiskAgent(Agent):
"""An agent that adapts its risk-taking behavior over time.

Attributes
----------
risk_preference : float
Probability (0-1) of choosing a risky action.
memory : deque[int]
Recent outcomes of risky actions (+1 reward, -1 loss).
"""

def __init__(
self,
model,
*,
initial_risk_preference: float = 0.5,
memory_size: int = 10,
) -> None:
super().__init__(model)
self.risk_preference = initial_risk_preference
self.memory: deque[int] = deque(maxlen=memory_size)

def choose_action(self) -> str:
"""Choose between a safe or risky action."""
if self.model.random.random() < self.risk_preference:
return "risky"
return "safe"

def risky_action(self) -> int:
"""Perform a risky action.

Returns
-------
int
Outcome of the action (+1 for reward, -1 for loss).
"""
return 1 if self.model.random.random() < 0.5 else -1

def safe_action(self) -> int:
"""Perform a safe action.Returns ------- int Guaranteed neutral outcome."""
return 0

def update_risk_preference(self) -> None:
"""Update risk preference based on recent memory."""
if not self.memory:
return

avg_outcome = sum(self.memory) / len(self.memory)

if avg_outcome < 0:
self.risk_preference = max(0.0, self.risk_preference - 0.05)
else:
self.risk_preference = min(1.0, self.risk_preference + 0.05)

def step(self) -> None:
"""Execute one decision step.

NOTE:
This method intentionally mixes decision-making, action execution,
memory updates, and learning to demonstrate how behavioral
complexity accumulates in current Mesa models.
"""
action = self.choose_action()

if action == "risky":
outcome = self.risky_action()
self.memory.append(outcome)
self.update_risk_preference()
else:
self.safe_action()
20 changes: 20 additions & 0 deletions examples/adaptive_risk_agents/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from __future__ import annotations

from mesa import Model

from examples.adaptive_risk_agents.agents import AdaptiveRiskAgent


class AdaptiveRiskModel(Model):
"""A simple model running adaptive risk-taking agents."""

def __init__(self, n_agents: int = 50, *, seed: int | None = None) -> None:
super().__init__(seed=seed)

# Create agents — Mesa will register them automatically
for _ in range(n_agents):
AdaptiveRiskAgent(self)

def step(self) -> None:
"""Advance the model by one step."""
self.agents.shuffle_do("step")
38 changes: 38 additions & 0 deletions examples/adaptive_risk_agents/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Run script for the Adaptive Risk Agents example.

This script runs the model for a fixed number of steps and prints
aggregate statistics to illustrate how agent behavior evolves over time.

Intentionally simple:
- No DataCollector
- No batch_run
- No visualization
"""

from __future__ import annotations

from examples.adaptive_risk_agents.model import AdaptiveRiskModel


def run_model(*, n_agents: int = 50, steps: int = 100, seed: int | None = None) -> None:
"""Run the AdaptiveRiskModel and print summary statistics."""
model = AdaptiveRiskModel(n_agents=n_agents, seed=seed)

for step in range(steps):
model.step()

total_risk = 0.0
count = 0

for agent in model.agents:
total_risk += agent.risk_preference
count += 1

avg_risk = total_risk / count if count > 0 else 0.0

if step % 10 == 0:
print(f"Step {step:3d} | Average risk preference: {avg_risk:.3f}")


if __name__ == "__main__":
run_model()
11 changes: 11 additions & 0 deletions examples/adaptive_risk_agents/tests/test_agent_smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from examples.adaptive_risk_agents.model import AdaptiveRiskModel


def test_agent_methods_execute():
model = AdaptiveRiskModel(n_agents=1, seed=1)
agent = next(iter(model.agents))

action = agent.choose_action()
assert action in {"safe", "risky"}

agent.step() # should not crash
18 changes: 18 additions & 0 deletions examples/adaptive_risk_agents/tests/test_smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""Smoke tests for the Adaptive Risk Agents example.

These tests only verify that the example runs without crashing.
They intentionally avoid checking model outcomes or behavior.
"""

from examples.adaptive_risk_agents.model import AdaptiveRiskModel


def test_model_initializes():
model = AdaptiveRiskModel(n_agents=10, seed=42)
assert model is not None


def test_model_steps_without_error():
model = AdaptiveRiskModel(n_agents=10, seed=42)
for _ in range(5):
model.step()