Skip to content

Commit 002c82c

Browse files
committed
test: update inbuilt tool tests for spatial standardization
1 parent 66e36ea commit 002c82c

File tree

3 files changed

+254
-1
lines changed

3 files changed

+254
-1
lines changed

mesa_llm/tools/tool_decorator.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,27 @@ def _python_to_json_type(py_type: Any) -> dict[str, Any]:
113113
"type": "array",
114114
"items": _python_to_json_type(item_type),
115115
}
116+
elif base is dict:
117+
# Handle dict[str, int]
118+
if "," in inner_content:
119+
parts = inner_content.split(",")
120+
if len(parts) >= 2:
121+
value_type_str = parts[1].strip()
122+
# We assume key is string for LLM tools
123+
type_mapping = {
124+
"int": int,
125+
"str": str,
126+
"float": float,
127+
"bool": bool,
128+
}
129+
value_type = type_mapping.get(value_type_str, str)
130+
return {
131+
"type": "object",
132+
"additionalProperties": _python_to_json_type(
133+
value_type
134+
),
135+
}
136+
return {"type": "object"}
116137

117138
# Try to get the base type for simple cases
118139
base_type = py_type.split("[")[0].strip()
@@ -127,6 +148,10 @@ def _python_to_json_type(py_type: Any) -> dict[str, Any]:
127148
}
128149
if base_type in type_mapping:
129150
py_type = type_mapping[base_type]
151+
else:
152+
# If it's a string that doesn't match any known basic type,
153+
# we default to string as per standard LLM tool practices.
154+
return {"type": "string"}
130155

131156
except Exception:
132157
# If parsing fails, default to string

tests/test_coverage_gap.py

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import asyncio
2+
3+
import pytest
4+
from mesa.discrete_space import OrthogonalMooreGrid
5+
from mesa.space import ContinuousSpace, SingleGrid
6+
7+
from mesa_llm.llm_agent import LLMAgent
8+
from mesa_llm.reasoning.reasoning import Reasoning
9+
from mesa_llm.tools.tool_decorator import _python_to_json_type, tool
10+
11+
12+
class DummyReasoning(Reasoning):
13+
def plan(self, *args, **kwargs):
14+
pass
15+
16+
async def aplan(self, *args, **kwargs):
17+
pass
18+
19+
20+
class AsyncAgent(LLMAgent):
21+
def __init__(self, model):
22+
super().__init__(model, reasoning=DummyReasoning)
23+
self.step_called = False
24+
self.astep_called = False
25+
26+
async def astep(self):
27+
self.astep_called = True
28+
return await super().astep()
29+
30+
31+
@pytest.mark.asyncio
32+
async def test_llm_agent_async_features(mocker):
33+
model = mocker.Mock()
34+
model.steps = 1
35+
# Mocking memory methods to avoid actual LLM calls
36+
mocker.patch(
37+
"mesa_llm.memory.st_lt_memory.STLTMemory.aadd_to_memory",
38+
side_effect=lambda **kwargs: None,
39+
)
40+
mocker.patch(
41+
"mesa_llm.memory.st_lt_memory.STLTMemory.aprocess_step",
42+
side_effect=lambda **kwargs: None,
43+
)
44+
45+
agent = AsyncAgent(model)
46+
47+
# Test astep wrapper
48+
await agent.astep()
49+
assert agent.astep_called is True
50+
51+
# Test apre_step and apost_step directly
52+
await agent.apre_step()
53+
await agent.apost_step()
54+
55+
# Test asend_message
56+
recipients = [mocker.Mock(memory=mocker.Mock(aadd_to_memory=mocker.AsyncMock()))]
57+
await agent.asend_message("hello", recipients)
58+
for r in recipients:
59+
r.memory.aadd_to_memory.assert_called()
60+
61+
62+
def test_llm_agent_pos_none_clears_cell(mocker):
63+
grid = OrthogonalMooreGrid(dimensions=(5, 5), torus=False)
64+
model = mocker.Mock(grid=grid)
65+
agent = LLMAgent(model, reasoning=DummyReasoning)
66+
67+
# Place in a cell
68+
cell = grid._cells[(2, 2)]
69+
agent.cell = cell
70+
assert agent.pos == (2, 2)
71+
72+
# Set pos to None
73+
agent.pos = None
74+
assert agent.pos is None
75+
assert agent.cell is None
76+
77+
78+
def test_llm_agent_move_to_all_spaces(mocker):
79+
# Test Orthogonal Grid
80+
grid = OrthogonalMooreGrid(dimensions=(5, 5), torus=False)
81+
model = mocker.Mock(grid=grid, space=None)
82+
agent = LLMAgent(model, reasoning=DummyReasoning)
83+
agent.cell = grid._cells[(0, 0)]
84+
85+
agent.move_to((1, 1))
86+
assert agent.pos == (1, 1)
87+
assert agent.cell is grid._cells[(1, 1)]
88+
89+
# Test Continuous Space
90+
space = ContinuousSpace(10, 10, False)
91+
model = mocker.Mock(space=space, grid=None)
92+
agent = LLMAgent(model, reasoning=DummyReasoning)
93+
space.place_agent(agent, (1, 1))
94+
95+
agent.move_to((5.5, 6.6))
96+
assert agent.pos == (5.5, 6.6)
97+
98+
# Test Unsupported
99+
model = mocker.Mock(grid=None, space=None)
100+
agent = LLMAgent(model, reasoning=DummyReasoning)
101+
with pytest.raises(ValueError, match="Unsupported environment"):
102+
agent.move_to((1, 1))
103+
104+
# Test SingleGrid (Line 112)
105+
grid = SingleGrid(5, 5, False)
106+
model = mocker.Mock(grid=grid, space=None)
107+
agent = LLMAgent(model, reasoning=DummyReasoning)
108+
grid.place_agent(agent, (0, 0))
109+
agent.move_to((1, 1))
110+
assert agent.pos == (1, 1)
111+
112+
113+
def test_llm_agent_step_wrapper(mocker):
114+
# Test sync step wrapper (Lines 381-390)
115+
class SyncSubAgent(LLMAgent):
116+
def __init__(self, model):
117+
super().__init__(model, reasoning=DummyReasoning)
118+
self.step_called = False
119+
120+
def step(self):
121+
self.step_called = True
122+
123+
model = mocker.Mock()
124+
model.steps = 1
125+
mocker.patch(
126+
"mesa_llm.memory.st_lt_memory.STLTMemory.process_step",
127+
side_effect=lambda **kwargs: None,
128+
)
129+
130+
agent = SyncSubAgent(model)
131+
agent.step()
132+
assert agent.step_called is True
133+
134+
# Test astep fallback to step (Line 366)
135+
mocker.patch(
136+
"mesa_llm.memory.st_lt_memory.STLTMemory.aprocess_step",
137+
side_effect=lambda **kwargs: None,
138+
)
139+
140+
async def run_astep():
141+
await agent.astep()
142+
143+
loop = asyncio.new_event_loop()
144+
asyncio.set_event_loop(loop)
145+
loop.run_until_complete(run_astep())
146+
assert agent.step_called is True
147+
148+
149+
def test_tool_decorator_string_annotations():
150+
# Test _python_to_json_type with string literals
151+
assert _python_to_json_type("int") == {"type": "integer"}
152+
assert _python_to_json_type("list[int]") == {
153+
"type": "array",
154+
"items": {"type": "integer"},
155+
}
156+
assert _python_to_json_type("list[str]") == {
157+
"type": "array",
158+
"items": {"type": "string"},
159+
}
160+
assert _python_to_json_type("dict[str, int]") == {
161+
"type": "object",
162+
"additionalProperties": {"type": "integer"},
163+
}
164+
165+
# Test invalid string fallback
166+
assert _python_to_json_type("SomethingUnknown") == {"type": "string"}
167+
168+
# Test None type
169+
assert _python_to_json_type(type(None)) == {"type": "null"}
170+
171+
172+
def test_optional_union_edge_cases():
173+
# Test Union with more than 2 types
174+
schema = _python_to_json_type(int | str | float)
175+
assert "anyOf" in schema
176+
assert len(schema["anyOf"]) == 3
177+
178+
# Test Optional with None only (unusual but possible)
179+
schema = _python_to_json_type(type(None) | type(None))
180+
assert schema == {"type": "null"}
181+
182+
183+
@tool
184+
def string_annotated_tool(agent, data: "list[int]") -> str:
185+
"""A tool with string annotations.
186+
Args:
187+
data: describe data
188+
"""
189+
return str(data)
190+
191+
192+
def test_string_annotated_tool_schema():
193+
schema = string_annotated_tool.__tool_schema__
194+
props = schema["function"]["parameters"]["properties"]
195+
assert props["data"]["type"] == "array"
196+
assert props["data"]["items"]["type"] == "integer"

tests/test_tools/test_inbuilt_tools.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,38 @@ class DummyAgent:
2424
def __init__(self, unique_id: int, model: DummyModel):
2525
self.unique_id = unique_id
2626
self.model = model
27-
self.pos = None
27+
self._pos = None
28+
29+
@property
30+
def pos(self):
31+
cell = getattr(self, "cell", None)
32+
if cell is not None and hasattr(cell, "coordinate"):
33+
return cell.coordinate
34+
return self._pos
35+
36+
@pos.setter
37+
def pos(self, value):
38+
self._pos = value
39+
40+
def move_to(self, target_coordinates):
41+
target_coordinates = tuple(target_coordinates)
42+
if hasattr(self.model, "grid") and isinstance(
43+
self.model.grid, SingleGrid | MultiGrid
44+
):
45+
self.model.grid.move_agent(self, target_coordinates)
46+
elif hasattr(self.model, "grid") and isinstance(
47+
self.model.grid, OrthogonalMooreGrid | OrthogonalVonNeumannGrid
48+
):
49+
cell = self.model.grid._cells[target_coordinates]
50+
self.cell = cell
51+
elif hasattr(self.model, "space") and isinstance(
52+
self.model.space, ContinuousSpace
53+
):
54+
self.model.space.move_agent(self, target_coordinates)
55+
else:
56+
raise ValueError(
57+
f"Unsupported environment for move_to. Model has grid={type(getattr(self.model, 'grid', None))}, space={type(getattr(self.model, 'space', None))}"
58+
)
2859

2960

3061
def test_move_one_step_on_singlegrid():
@@ -342,6 +373,7 @@ def test_move_one_step_boundary_on_continuousspace():
342373

343374
result = move_one_step(agent, "North")
344375

376+
# agent should not have moved
345377
assert agent.pos == (2.0, 9.0)
346378
assert "boundary" in result.lower()
347379
assert "North" in result

0 commit comments

Comments
 (0)