Skip to content
Open
121 changes: 44 additions & 77 deletions mesa_llm/llm_agent.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import math

from mesa.agent import Agent
from mesa.discrete_space import (
OrthogonalMooreGrid,
OrthogonalVonNeumannGrid,
)
from mesa.experimental.continuous_space import ContinuousSpace
from mesa.model import Model
from mesa.space import (
ContinuousSpace,
MultiGrid,
SingleGrid,
)

from mesa_llm import Plan
from mesa_llm.memory.st_lt_memory import STLTMemory
Expand Down Expand Up @@ -61,12 +59,13 @@ def __init__(

self.tool_manager = ToolManager()
self.vision = vision
self.reasoning = reasoning(agent=self)
self.reasoning_instance = reasoning(agent=self)
self.reasoning = reasoning
self.system_prompt = system_prompt
self.llm_model = llm_model
self.is_speaking = False
self._current_plan = None # Store current plan for formatting
self._current_plan = None

# display coordination
self._step_display_data = {}

if isinstance(internal_state, str):
Expand All @@ -76,9 +75,26 @@ def __init__(

self.internal_state = internal_state

def __str__(self):
def __str__(self) -> str:
return f"LLMAgent {self.unique_id}"

def __repr__(self) -> str:
memory_size = (
len(self.memory.short_term_memory)
if hasattr(self.memory, "short_term_memory")
else 0
)
return (
f"LLMAgent("
f"unique_id={self.unique_id}, "
f"llm_model='{self.llm.llm_model}', "
f"reasoning={self.reasoning.__name__}, "
f"vision={self.vision}, "
f"memory_size={memory_size}, "
f"internal_state={self.internal_state}"
f")"
)

async def aapply_plan(self, plan: Plan) -> list[dict]:
"""
Asynchronous version of apply_plan.
Expand All @@ -105,15 +121,12 @@ def apply_plan(self, plan: Plan) -> list[dict]:
"""
Execute the plan in the simulation.
"""
# Store current plan for display
self._current_plan = plan

# Execute tool calls
tool_call_resp = self.tool_manager.call_tools(
agent=self, llm_response=plan.llm_plan
)

# Add to memory
self.memory.add_to_memory(
type="action",
content={
Expand All @@ -129,30 +142,13 @@ def apply_plan(self, plan: Plan) -> list[dict]:
def _build_observation(self):
"""
Construct the observation data visible to the agent at the current model step.
This method encapsulates the shared logic used by both sync and
async observation generation.
This method constructs the agent's self state and determines which other
agents are observable based on the configured vision:
- vision > 0:
The agent observes all agents within the specified vision radius.
- vision == -1:
The agent observes all agents present in the simulation.
- vision == 0 or vision is None:
The agent observes no other agents.
The method supports grid-based and continuous spaces and builds a local
state representation for all visible neighboring agents.
Returns self_state and local_state of the agent
"""
self_state = {
"agent_unique_id": self.unique_id,
"system_prompt": self.system_prompt,
"location": (
self.pos
if self.pos is not None
getattr(self, "pos", None)
if getattr(self, "pos", None) is not None
else (
getattr(self, "cell", None).coordinate
if getattr(self, "cell", None) is not None
Expand All @@ -162,18 +158,10 @@ def _build_observation(self):
"internal_state": self.internal_state,
}
if self.vision is not None and self.vision > 0:
# Check which type of space/grid the model uses
grid = getattr(self.model, "grid", None)
space = getattr(self.model, "space", None)

if grid and isinstance(grid, SingleGrid | MultiGrid):
neighbors = grid.get_neighbors(
tuple(self.pos),
moore=True,
include_center=False,
radius=self.vision,
)
elif grid and isinstance(
if grid and isinstance(
grid, OrthogonalMooreGrid | OrthogonalVonNeumannGrid
):
agent_cell = next(
Expand All @@ -187,13 +175,19 @@ def _build_observation(self):
neighbors = []

elif space and isinstance(space, ContinuousSpace):
all_nearby = space.get_neighbors(
self.pos, radius=self.vision, include_center=True
)
neighbors = [a for a in all_nearby if a is not self]
my_pos = getattr(self, "pos", None)
if my_pos is not None:
neighbors = [
a
for a in self.model.agents
if a is not self
and getattr(a, "pos", None) is not None
and math.dist(my_pos, a.pos) <= self.vision
]
else:
neighbors = []

else:
# No recognized grid/space type
neighbors = []

elif self.vision == -1:
Expand Down Expand Up @@ -223,11 +217,9 @@ def _build_observation(self):

async def agenerate_obs(self) -> Observation:
"""
This method builds the agent's observation using the shared observation
construction logic, stores it in the agent's memory module using
async memory operations, and returns it as an Observation instance.
Async observation generation.
"""
step = self.model.steps
step = int(self.model._time) if hasattr(self.model, "_time") else 0
self_state, local_state = self._build_observation()
await self.memory.aadd_to_memory(
type="observation",
Expand All @@ -241,13 +233,10 @@ async def agenerate_obs(self) -> Observation:

def generate_obs(self) -> Observation:
"""
This method delegates observation construction to the shared observation
builder, stores the resulting observation in the agent's memory module,
and returns it as an Observation instance.
Sync observation generation.
"""
step = self.model.steps
step = int(self.model._time) if hasattr(self.model, "_time") else 0
self_state, local_state = self._build_observation()
# Add to memory (memory handles its own display separately)
self.memory.add_to_memory(
type="observation",
content={
Expand Down Expand Up @@ -291,35 +280,20 @@ def send_message(self, message: str, recipients: list[Agent]) -> str:
return f"{self}{recipients} : {message}"

async def apre_step(self):
"""
Asynchronous version of pre_step.
"""
await self.memory.aprocess_step(pre_step=True)

async def apost_step(self):
"""
Asynchronous version of post_step.
"""
await self.memory.aprocess_step()

def pre_step(self):
"""
This is some code that is executed before the step method of the child agent is called.
"""
self.memory.process_step(pre_step=True)

def post_step(self):
"""
This is some code that is executed after the step method of the child agent is called.
It functions because of the __init_subclass__ method that creates a wrapper around the step method of the child agent.
"""
self.memory.process_step()

async def astep(self):
"""
Default asynchronous step method for parallel agent execution.
Subclasses should override this method for custom async behavior.
If not overridden, falls back to calling the synchronous step() method.
"""
await self.apre_step()

Expand All @@ -330,19 +304,15 @@ async def astep(self):

def __init_subclass__(cls, **kwargs):
"""
Wrapper - allows to automatically integrate code to be executed after the step method of the child agent (created by the user) is called.
Wrapper - allows to automatically integrate code to be executed after the step method.
"""
super().__init_subclass__(**kwargs)
# only wrap if subclass actually defines its own step
user_step = cls.__dict__.get("step")
user_astep = cls.__dict__.get("astep")

if user_step:

def wrapped(self, *args, **kwargs):
"""
This is the wrapper that is used to integrate the pre_step and post_step methods into the step method of the child agent.
"""
LLMAgent.pre_step(self, *args, **kwargs)
result = user_step(self, *args, **kwargs)
LLMAgent.post_step(self, *args, **kwargs)
Expand All @@ -353,9 +323,6 @@ def wrapped(self, *args, **kwargs):
if user_astep:

async def awrapped(self, *args, **kwargs):
"""
Async wrapper for astep method.
"""
await self.apre_step()
result = await user_astep(self, *args, **kwargs)
await self.apost_step()
Expand Down
2 changes: 1 addition & 1 deletion mesa_llm/memory/st_lt_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ def _process_step_core(self, pre_step: bool):
new_entry = MemoryEntry(
agent=self.agent,
content=self.step_content,
step=self.agent.model.steps,
step=int(getattr(self.agent.model, "_time", 0)),
)

self.short_term_memory.append(new_entry)
Expand Down
13 changes: 13 additions & 0 deletions mesa_llm/module_llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,19 @@ def __init__(
self.llm_model,
)

def __repr__(self) -> str:
prompt_preview = (
self.system_prompt[:50] + "..."
if self.system_prompt and len(self.system_prompt) > 50
else self.system_prompt
)
return (
f"ModuleLLM("
f"llm_model='{self.llm_model}', "
f"api_base={self.api_base!r}, "
f"system_prompt={prompt_preview!r})"
)

def _build_messages(self, prompt: str | list[str] | None = None) -> list[dict]:
"""
Format the prompt messages for the LLM of the form : {"role": ..., "content": ...}
Expand Down
3 changes: 3 additions & 0 deletions mesa_llm/reasoning/cot.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ class CoTReasoning(Reasoning):
def __init__(self, agent: "LLMAgent"):
super().__init__(agent=agent)

def __repr__(self) -> str:
return f"CoTReasoning(agent_id={self.agent.unique_id})"

def get_cot_system_prompt(self, obs: Observation) -> str:
memory = getattr(self.agent, "memory", None)
long_term_memory = ""
Expand Down
3 changes: 3 additions & 0 deletions mesa_llm/reasoning/react.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ class ReActReasoning(Reasoning):
def __init__(self, agent: "LLMAgent"):
super().__init__(agent=agent)

def __repr__(self) -> str:
return f"ReActReasoning(agent_id={self.agent.unique_id})"

def get_react_system_prompt(self) -> str:
system_prompt = """
You are an autonomous agent in a simulation environment.
Expand Down
3 changes: 3 additions & 0 deletions mesa_llm/reasoning/reasoning.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ class Reasoning(ABC):
def __init__(self, agent: "LLMAgent"):
self.agent = agent

def __repr__(self) -> str:
return f"{self.__class__.__name__}(agent_id={self.agent.unique_id})"

@abstractmethod
def plan(
self,
Expand Down
7 changes: 7 additions & 0 deletions mesa_llm/reasoning/rewoo.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ def __init__(self, agent: "LLMAgent"):
self.current_plan: Plan | None = None
self.current_obs: Observation | None = None

def __repr__(self) -> str:
return (
f"ReWOOReasoning("
f"agent_id={self.agent.unique_id}, "
f"remaining_tool_calls={self.remaining_tool_calls})"
)

def get_rewoo_system_prompt(self, obs: Observation) -> str:
memory = getattr(self.agent, "memory", None)

Expand Down
8 changes: 6 additions & 2 deletions mesa_llm/recording/record_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,9 @@ def _auto_save():
def step_wrapper(self: "Model", *args, **kwargs): # type: ignore[override]
# Record beginning of step
if hasattr(self, "recorder"):
self.recorder.record_model_event("step_start", {"step": self.steps}) # type: ignore[attr-defined]
self.recorder.record_model_event(
"step_start", {"step": int(getattr(self, "_time", 0))}
) # type: ignore[attr-defined]

# Execute the original step logic
result = original_step(self, *args, **kwargs) # type: ignore[misc]
Expand All @@ -110,7 +112,9 @@ def step_wrapper(self: "Model", *args, **kwargs): # type: ignore[override]
if hasattr(self, "recorder"):
_attach_recorder_to_agents(self, self.recorder) # type: ignore[attr-defined]
# Record end of step after agents have acted
self.recorder.record_model_event("step_end", {"step": self.steps}) # type: ignore[attr-defined]
self.recorder.record_model_event(
"step_end", {"step": int(getattr(self, "_time", 0))}
) # type: ignore[attr-defined]

return result

Expand Down
Loading