From 25d4f32b52fc84942df608afd7869ee88d60aa59 Mon Sep 17 00:00:00 2001 From: sujal gawas Date: Wed, 19 Nov 2025 16:47:27 +0530 Subject: [PATCH 1/8] inital commit model.py and agent.py --- examples/sugarscrap_g1mt/Readmer.md | 0 examples/sugarscrap_g1mt/__init__.py | 0 examples/sugarscrap_g1mt/agents.py | 145 +++++++++++++++++++++++++++ examples/sugarscrap_g1mt/app.py | 32 ++++++ examples/sugarscrap_g1mt/model.py | 128 +++++++++++++++++++++++ examples/sugarscrap_g1mt/tools.py | 0 6 files changed, 305 insertions(+) create mode 100644 examples/sugarscrap_g1mt/Readmer.md create mode 100644 examples/sugarscrap_g1mt/__init__.py create mode 100644 examples/sugarscrap_g1mt/agents.py create mode 100644 examples/sugarscrap_g1mt/app.py create mode 100644 examples/sugarscrap_g1mt/model.py create mode 100644 examples/sugarscrap_g1mt/tools.py diff --git a/examples/sugarscrap_g1mt/Readmer.md b/examples/sugarscrap_g1mt/Readmer.md new file mode 100644 index 0000000..e69de29 diff --git a/examples/sugarscrap_g1mt/__init__.py b/examples/sugarscrap_g1mt/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/sugarscrap_g1mt/agents.py b/examples/sugarscrap_g1mt/agents.py new file mode 100644 index 0000000..d107be4 --- /dev/null +++ b/examples/sugarscrap_g1mt/agents.py @@ -0,0 +1,145 @@ +import mesa + +from mesa_llm.llm_agent import LLMAgent +from mesa_llm.memory.st_lt_memory import STLTMemory +from mesa_llm.tools.tool_manager import ToolManager + +trader_tool_manager = ToolManager() + + +class Trader(LLMAgent, mesa.discrete_space.CellAgent): + def __init__( + self, + model, + reasoning, + llm_model, + system_prompt, + vision, + internal_state, + step_prompt, + sugar=0, + spice=0, + metabolism_sugar=1, + metabolism_spice=1, + ): + super().__init__( + model=model, + reasoning=reasoning, + llm_model=llm_model, + system_prompt=system_prompt, + vision=vision, + internal_state=internal_state, + step_prompt=step_prompt, + ) + self.sugar = sugar + self.spice = spice + self.metabolism_sugar = metabolism_sugar + self.metabolism_spice = metabolism_spice + + self.memory = STLTMemory( + agent=self, + display=True, + llm_model="openai/gpt-4o-mini", + ) + + self.tool_manager = trader_tool_manager + + self.system_prompt = ( + "You are a Trader agent in a Sugarscape simulation. " + "You need Sugar and Spice to survive. " + "If your MRS (Marginal Rate of Substitution) is high, you desperately need Sugar. " + "If MRS is low, you need Spice. " + "You can move to harvest resources or trade with neighbors." + ) + + self.update_internal_metrics() + + def calculate_mrs(self): + if self.sugar == 0: + return 100.0 + + if self.metabolism_sugar == 0: + return 100.0 + + if self.metabolism_spice == 0: + return 0.0 + + return (self.spice / self.metabolism_spice) / ( + self.sugar / self.metabolism_sugar + ) + + def update_internal_metrics(self): + mrs = self.calculate_mrs() + + self.internal_state = [ + s + for s in self.internal_state + if not any(x in s for x in ["Sugar:", "Spice:", "MRS:", "WARNING:"]) + ] + + self.internal_state.append(f"My Sugar inventory is: {self.sugar}") + self.internal_state.append(f"My Spice inventory is: {self.spice}") + self.internal_state.append( + f"My Marginal Rate of Substitution (MRS) is {mrs:.2f}" + ) + + if self.sugar < self.metabolism_sugar * 2: + self.internal_state.append( + "WARNING: I am in danger of starvation from lack of sugar" + ) + + def step(self): + self.sugar -= self.metabolism_sugar + self.spice -= self.metabolism_spice + + if self.sugar <= 0 or self.spice <= 0: + self.model.grid.remove_agent(self) + self.model.schedule.remove(self) + return + + self.update_internal_metrics() + + observation = self.generate_obs() + + plan = self.reasoning.plan( + obs=observation, + selected_tools=["move_to_best_resource", "propose_trade"], + ) + + self.apply_plan(plan) + + async def astep(self): + self.sugar -= self.metabolism_sugar + self.spice -= self.metabolism_spice + + if self.sugar <= 0 or self.spice <= 0: + self.model.grid.remove_agent(self) + self.model.schedule.remove(self) + return + + self.update_internal_metrics() + observation = self.generate_obs() + + plan = await self.reasoning.aplan( + obs=observation, + selected_tools=["move_to_best_resource", "propose_trade"], + ) + self.apply_plan(plan) + + +class Resource(mesa.discrete_space.CellAgent): + def __init__(self, model, max_capacity=10, current_amount=10, growback=1): + super().__init__(model=model) + + self.max_capacity = max_capacity + self.current_amount = current_amount + self.growback = growback + + def step(self): + if self.current_amount < self.max_capacity: + self.current_amount += self.growback + if self.current_amount > self.max_capacity: + self.current_amount = self.max_capacity + + async def astep(self): + self.step() diff --git a/examples/sugarscrap_g1mt/app.py b/examples/sugarscrap_g1mt/app.py new file mode 100644 index 0000000..06d363f --- /dev/null +++ b/examples/sugarscrap_g1mt/app.py @@ -0,0 +1,32 @@ +# app.py + +from dotenv import load_dotenv + +from examples.sugarscrap_g1mt.agents import Trader +from mesa_llm.reasoning.react import ReActReasoning + +load_dotenv() + +COP_COLOR = "#000000" + +agent_colors = { + Trader.ACTIVE: "#FE6100", + Trader.QUIET: "#648FFF", + Trader.ARRESTED: "#DB28A2", +} + +model_params = { + "seed": { + "type": "InputText", + "value": 42, + "label": "Random Seed", + }, + "initial_citizens": 20, + "initial_cops": 5, + "width": 10, + "height": 10, + "reasoning": ReActReasoning, + "llm_model": "openai/gpt-4o-mini", + "vision": 5, + "parallel_stepping": True, +} diff --git a/examples/sugarscrap_g1mt/model.py b/examples/sugarscrap_g1mt/model.py new file mode 100644 index 0000000..b525a18 --- /dev/null +++ b/examples/sugarscrap_g1mt/model.py @@ -0,0 +1,128 @@ +from mesa.datacollection import DataCollector +from mesa.model import Model +from mesa.space import MultiGrid +from rich import print + +from examples.sugarscrap_g1mt.agents import Resource, Trader +from mesa_llm.reasoning.reasoning import Reasoning +from mesa_llm.recording.record_model import record_model + + +@record_model(output_dir="recordings") +class SugarScapeModel(Model): + def __init__( + self, + initial_traders: int, + initial_resources: int, + width: int, + height: int, + reasoning: type[Reasoning], + llm_model: str, + vision: int, + parallel_stepping=True, + seed=None, + ): + super().__init__(seed=seed) + self.width = width + self.height = height + self.parallel_stepping = parallel_stepping + self.grid = MultiGrid(self.width, self.height, torus=False) + + model_reporters = { + "Trader_Count": lambda m: sum(1 for a in m.agents if isinstance(a, Trader)), + "Total_Sugar": lambda m: sum( + a.sugar for a in m.agents if isinstance(a, Trader) + ), + "Total_Spice": lambda m: sum( + a.spice for a in m.agents if isinstance(a, Trader) + ), + } + + agent_reporters = { + "sugar": lambda a: getattr(a, "sugar", None), + "spice": lambda a: getattr(a, "spice", None), + "mrs": lambda a: a.calculate_mrs() if isinstance(a, Trader) else None, + } + + self.datacollector = DataCollector( + model_reporters=model_reporters, agent_reporters=agent_reporters + ) + + for _i in range(initial_resources): + max_cap = self.random.randint(2, 5) + resource = Resource( + model=self, max_capacity=max_cap, current_amount=max_cap, growback=1 + ) + + x = self.random.randrange(self.width) + y = self.random.randrange(self.height) + + self.grid.place_agent(resource, (x, y)) + + trader_system_prompt = ( + "You are a Trader agent in a Sugarscape simulation. " + "You need Sugar and Spice to survive. " + "If your MRS (Marginal Rate of Substitution) is high, you desperately need Sugar. " + "If MRS is low, you need Spice. " + "You can move to harvest resources or trade with neighbors." + ) + + agents = Trader.create_agents( + self, + n=initial_traders, + reasoning=reasoning, + llm_model=llm_model, + system_prompt=trader_system_prompt, + vision=vision, + internal_state=None, + step_prompt="Observe your inventory and MRS. Move to the best resource or propose a trade.", + ) + + x_pos = self.random.integers(0, self.grid.width, size=(initial_traders,)) + y_pos = self.random.integers(0, self.grid.height, size=(initial_traders,)) + + for agent, i, j in zip(agents, x_pos, y_pos): + agent.sugar = self.random.randint(5, 25) + agent.spice = self.random.randint(5, 25) + agent.metabolism_sugar = self.random.randint(1, 4) + agent.metabolism_spice = self.random.randint(1, 4) + + agent.update_internal_metrics() + + self.grid.place_agent(agent, (i, j)) + + def step(self): + """ + Execute one step of the model. + """ + print( + f"\n[bold purple] step {self.steps} ────────────────────────────────────────────────────────────────────────────────[/bold purple]" + ) + + self.agents.shuffle_do("step") + + self.datacollector.collect(self) + + +# =============================================================== +# RUN WITHOUT GRAPHICS +# =============================================================== + +if __name__ == "__main__": + """ + Run the model without the solara integration + """ + from mesa_llm.reasoning.reasoning import Reasoning + + model = SugarScapeModel( + initial_traders=5, + initial_resources=20, + width=10, + height=10, + reasoning=Reasoning(), + llm_model="openai/gpt-4o-mini", + vision=2, + ) + + for _ in range(5): + model.step() diff --git a/examples/sugarscrap_g1mt/tools.py b/examples/sugarscrap_g1mt/tools.py new file mode 100644 index 0000000..e69de29 From a07e90c5853e542811174c3e086d7a2a772e41d9 Mon Sep 17 00:00:00 2001 From: sujal gawas Date: Wed, 19 Nov 2025 19:44:57 +0530 Subject: [PATCH 2/8] runs with errors --- examples/sugarscrap_g1mt/__init__.py | 1 + examples/sugarscrap_g1mt/agents.py | 11 +++ examples/sugarscrap_g1mt/app.py | 101 +++++++++++++++++++++++++-- examples/sugarscrap_g1mt/model.py | 20 +++--- examples/sugarscrap_g1mt/tools.py | 82 ++++++++++++++++++++++ 5 files changed, 199 insertions(+), 16 deletions(-) diff --git a/examples/sugarscrap_g1mt/__init__.py b/examples/sugarscrap_g1mt/__init__.py index e69de29..3f3e1f2 100644 --- a/examples/sugarscrap_g1mt/__init__.py +++ b/examples/sugarscrap_g1mt/__init__.py @@ -0,0 +1 @@ +import examples.sugarscrap_g1mt.tools # noqa: F401, to register tools diff --git a/examples/sugarscrap_g1mt/agents.py b/examples/sugarscrap_g1mt/agents.py index d107be4..bbed711 100644 --- a/examples/sugarscrap_g1mt/agents.py +++ b/examples/sugarscrap_g1mt/agents.py @@ -1,3 +1,5 @@ +from enum import Enum + import mesa from mesa_llm.llm_agent import LLMAgent @@ -5,6 +7,13 @@ from mesa_llm.tools.tool_manager import ToolManager trader_tool_manager = ToolManager() +resource_tool_manager = ToolManager() + + +class TraderState(Enum): + Total_Spice = 4 + Total_Sugar = 6 + Total_Count = 10 class Trader(LLMAgent, mesa.discrete_space.CellAgent): @@ -135,6 +144,8 @@ def __init__(self, model, max_capacity=10, current_amount=10, growback=1): self.current_amount = current_amount self.growback = growback + self.tool_manager = resource_tool_manager + def step(self): if self.current_amount < self.max_capacity: self.current_amount += self.growback diff --git a/examples/sugarscrap_g1mt/app.py b/examples/sugarscrap_g1mt/app.py index 06d363f..eb92c1c 100644 --- a/examples/sugarscrap_g1mt/app.py +++ b/examples/sugarscrap_g1mt/app.py @@ -1,18 +1,40 @@ # app.py +import logging +import warnings from dotenv import load_dotenv +from mesa.visualization import ( + SolaraViz, + make_plot_component, + make_space_component, +) -from examples.sugarscrap_g1mt.agents import Trader +from examples.sugarscrap_g1mt.agents import TraderState +from examples.sugarscrap_g1mt.model import SugarScapeModel +from mesa_llm.parallel_stepping import enable_automatic_parallel_stepping from mesa_llm.reasoning.react import ReActReasoning +# Suppress Pydantic serialization warnings +warnings.filterwarnings( + "ignore", + category=UserWarning, + module="pydantic.main", + message=r".*Pydantic serializer warnings.*", +) + +# Also suppress through logging +logging.getLogger("pydantic").setLevel(logging.ERROR) + +enable_automatic_parallel_stepping(mode="threading") + load_dotenv() COP_COLOR = "#000000" agent_colors = { - Trader.ACTIVE: "#FE6100", - Trader.QUIET: "#648FFF", - Trader.ARRESTED: "#DB28A2", + TraderState.Total_Count: "#FE6100", + TraderState.Total_Sugar: "#648FFF", + TraderState.Total_Spice: "#DB28A2", } model_params = { @@ -21,12 +43,77 @@ "value": 42, "label": "Random Seed", }, - "initial_citizens": 20, - "initial_cops": 5, + "inital_traders": 10, + "initial_resources": 10, "width": 10, "height": 10, "reasoning": ReActReasoning, - "llm_model": "openai/gpt-4o-mini", + "llm_model": "ollama/gemma 3:1b", "vision": 5, "parallel_stepping": True, } + +model = SugarScapeModel( + initial_traders=model_params["inital_traders"], + initial_resources=model_params["initial_resources"], + width=model_params["width"], + height=model_params["height"], + reasoning=model_params["reasoning"], + llm_model=model_params["llm_model"], + vision=model_params["vision"], + seed=model_params["seed"]["value"], + parallel_stepping=model_params["parallel_stepping"], +) + + +def trader_portrayal(agent): + if agent is None: + return + portrayal = { + "Shape": "circle", + "Filled": "true", + "r": 0.5, + "Layer": 1, + "Color": agent_colors.get(agent.state, "#FFFFFF"), + "text": f"S:{agent.sugar} Sp:{agent.spice} MRS:{agent.calculate_mrs():.2f}", + "text_color": "black", + } + + return portrayal + + +def post_process(ax): + ax.set_aspect("equal") + ax.set_xticks([]) + ax.set_yticks([]) + ax.get_figure().set_size_inches(10, 10) + + +space_component = make_space_component( + model, + portrayal_function=trader_portrayal, + grid_width=10, + grid_height=10, + post_process=post_process, +) + +chart_component = make_plot_component( + {state.name.lower(): state.name for state in TraderState} +) + +if __name__ == "__main__": + page = SolaraViz( + model, + components=[ + space_component, + chart_component, + ], + model_params=model_params, + name="SugarScape G1MT Example", + ) + + """ + run with + cd examples/sugarscrap_g1mt + conda activate mesa-llm && solara run app.py + """ diff --git a/examples/sugarscrap_g1mt/model.py b/examples/sugarscrap_g1mt/model.py index b525a18..225fbe1 100644 --- a/examples/sugarscrap_g1mt/model.py +++ b/examples/sugarscrap_g1mt/model.py @@ -1,3 +1,5 @@ +import random + from mesa.datacollection import DataCollector from mesa.model import Model from mesa.space import MultiGrid @@ -49,13 +51,13 @@ def __init__( ) for _i in range(initial_resources): - max_cap = self.random.randint(2, 5) + max_cap = random.randint(2, 5) resource = Resource( model=self, max_capacity=max_cap, current_amount=max_cap, growback=1 ) - x = self.random.randrange(self.width) - y = self.random.randrange(self.height) + x = random.randrange(self.width) + y = random.randrange(self.height) self.grid.place_agent(resource, (x, y)) @@ -78,14 +80,14 @@ def __init__( step_prompt="Observe your inventory and MRS. Move to the best resource or propose a trade.", ) - x_pos = self.random.integers(0, self.grid.width, size=(initial_traders,)) - y_pos = self.random.integers(0, self.grid.height, size=(initial_traders,)) + x_pos = self.rng.integers(0, self.grid.width, size=(initial_traders,)) + y_pos = self.rng.integers(0, self.grid.height, size=(initial_traders,)) for agent, i, j in zip(agents, x_pos, y_pos): - agent.sugar = self.random.randint(5, 25) - agent.spice = self.random.randint(5, 25) - agent.metabolism_sugar = self.random.randint(1, 4) - agent.metabolism_spice = self.random.randint(1, 4) + agent.sugar = random.randint(5, 25) + agent.spice = random.randint(5, 25) + agent.metabolism_sugar = random.randint(1, 4) + agent.metabolism_spice = random.randint(1, 4) agent.update_internal_metrics() diff --git a/examples/sugarscrap_g1mt/tools.py b/examples/sugarscrap_g1mt/tools.py index e69de29..87c3ee6 100644 --- a/examples/sugarscrap_g1mt/tools.py +++ b/examples/sugarscrap_g1mt/tools.py @@ -0,0 +1,82 @@ +from typing import TYPE_CHECKING + +from examples.sugarscrap_g1mt.agents import ( + Resource, + resource_tool_manager, + trader_tool_manager, +) +from mesa_llm.tools.tool_decorator import tool + +if TYPE_CHECKING: + from mesa_llm.llm_agent import LLMAgent + +if TYPE_CHECKING: + from mesa_llm.llm_agent import LLMAgent + + +@tool(tool_manager=trader_tool_manager) +def move_to_best_resource(agent: "LLMAgent") -> str: + """ + Move the agent to the best resource cell within its vision range. + + Args: + agent: Provided automatically + + Returns: + A string confirming the new position of the agent. + """ + + best_cell = None + best_amount = -1 + + x, y = agent.pos + vision = agent.vision + + for dx in range(-vision, vision + 1): + for dy in range(-vision, vision + 1): + nx, ny = x + dx, y + dy + if not agent.model.grid.out_of_bounds((nx, ny)): + cell_contents = agent.model.grid.get_cell_list_contents((nx, ny)) + for obj in cell_contents: + if isinstance(obj, Resource) and obj.current_amount > best_amount: + best_amount = obj.current_amount + best_cell = (nx, ny) + + if best_cell: + agent.model.grid.move_agent(agent, best_cell) + return f"agent {agent.unique_id} moved to {best_cell}." + else: + return f"agent {agent.unique_id} found no resources to move to." + + +@tool(tool_manager=resource_tool_manager) +def propose_trade( + agent: "LLMAgent", other_agent_id: int, sugar_amount: int, spice_amount: int +) -> str: + """ + Propose a trade to another agent. + + Args: + other_agent_id: The unique id of the other agent to trade with. + sugar_amount: The amount of sugar to offer. + spice_amount: The amount of spice to offer. + agent: Provided automatically + + Returns: + A string confirming the trade proposal. + """ + other_agent = next( + (a for a in agent.model.agents if a.unique_id == other_agent_id), None + ) + if other_agent is None: + return f"agent {other_agent_id} not found." + + # Simple trade acceptance logic for demonstration + if other_agent.calculate_mrs() > agent.calculate_mrs(): + agent.sugar -= sugar_amount + agent.spice += spice_amount + other_agent.sugar += sugar_amount + other_agent.spice -= spice_amount + return f"agent {agent.unique_id} traded {sugar_amount} sugar for {spice_amount} spice with agent {other_agent_id}." + else: + return f"agent {other_agent_id} rejected the trade proposal from agent {agent.unique_id}." From c200a5208d350816de3bee0e9c54b78093629faa Mon Sep 17 00:00:00 2001 From: sujal gawas Date: Sun, 23 Nov 2025 21:13:04 +0530 Subject: [PATCH 3/8] runs with errors --- examples/sugarscrap_g1mt/agents.py | 2 ++ examples/sugarscrap_g1mt/app.py | 44 +++++++++++++++--------------- examples/sugarscrap_g1mt/tools.py | 10 +++++-- 3 files changed, 31 insertions(+), 25 deletions(-) diff --git a/examples/sugarscrap_g1mt/agents.py b/examples/sugarscrap_g1mt/agents.py index bbed711..3d9f86a 100644 --- a/examples/sugarscrap_g1mt/agents.py +++ b/examples/sugarscrap_g1mt/agents.py @@ -144,6 +144,8 @@ def __init__(self, model, max_capacity=10, current_amount=10, growback=1): self.current_amount = current_amount self.growback = growback + self.internal_state = [] + self.tool_manager = resource_tool_manager def step(self): diff --git a/examples/sugarscrap_g1mt/app.py b/examples/sugarscrap_g1mt/app.py index eb92c1c..6ee190b 100644 --- a/examples/sugarscrap_g1mt/app.py +++ b/examples/sugarscrap_g1mt/app.py @@ -9,7 +9,7 @@ make_space_component, ) -from examples.sugarscrap_g1mt.agents import TraderState +from examples.sugarscrap_g1mt.agents import Resource, Trader from examples.sugarscrap_g1mt.model import SugarScapeModel from mesa_llm.parallel_stepping import enable_automatic_parallel_stepping from mesa_llm.reasoning.react import ReActReasoning @@ -29,13 +29,6 @@ load_dotenv() -COP_COLOR = "#000000" - -agent_colors = { - TraderState.Total_Count: "#FE6100", - TraderState.Total_Sugar: "#648FFF", - TraderState.Total_Spice: "#DB28A2", -} model_params = { "seed": { @@ -43,18 +36,18 @@ "value": 42, "label": "Random Seed", }, - "inital_traders": 10, + "initial_traders": 10, "initial_resources": 10, "width": 10, "height": 10, "reasoning": ReActReasoning, - "llm_model": "ollama/gemma 3:1b", + "llm_model": "gemini/gemini-1.5-flash", "vision": 5, "parallel_stepping": True, } model = SugarScapeModel( - initial_traders=model_params["inital_traders"], + initial_traders=model_params["initial_traders"], initial_resources=model_params["initial_resources"], width=model_params["width"], height=model_params["height"], @@ -69,16 +62,29 @@ def trader_portrayal(agent): if agent is None: return + portrayal = { - "Shape": "circle", + "shape": "circle", "Filled": "true", "r": 0.5, "Layer": 1, - "Color": agent_colors.get(agent.state, "#FFFFFF"), - "text": f"S:{agent.sugar} Sp:{agent.spice} MRS:{agent.calculate_mrs():.2f}", "text_color": "black", } + if isinstance(agent, Trader): + portrayal["Color"] = "red" + portrayal["r"] = 0.8 + portrayal["text"] = f"S:{agent.sugar} Sp:{agent.spice}" + + elif isinstance(agent, Resource): + portrayal["Color"] = "green" + portrayal["r"] = 0.4 + portrayal["Layer"] = 0 + if agent.current_amount > 0: + portrayal["alpha"] = agent.current_amount / agent.max_capacity + else: + portrayal["Color"] = "white" + return portrayal @@ -90,16 +96,10 @@ def post_process(ax): space_component = make_space_component( - model, - portrayal_function=trader_portrayal, - grid_width=10, - grid_height=10, - post_process=post_process, + trader_portrayal, post_process=post_process, draw_grid=False ) -chart_component = make_plot_component( - {state.name.lower(): state.name for state in TraderState} -) +chart_component = make_plot_component({"Total_Sugar": "blue", "Total_Spice": "red"}) if __name__ == "__main__": page = SolaraViz( diff --git a/examples/sugarscrap_g1mt/tools.py b/examples/sugarscrap_g1mt/tools.py index 87c3ee6..d3bc3f1 100644 --- a/examples/sugarscrap_g1mt/tools.py +++ b/examples/sugarscrap_g1mt/tools.py @@ -2,7 +2,7 @@ from examples.sugarscrap_g1mt.agents import ( Resource, - resource_tool_manager, + Trader, trader_tool_manager, ) from mesa_llm.tools.tool_decorator import tool @@ -49,7 +49,7 @@ def move_to_best_resource(agent: "LLMAgent") -> str: return f"agent {agent.unique_id} found no resources to move to." -@tool(tool_manager=resource_tool_manager) +@tool(tool_manager=trader_tool_manager) def propose_trade( agent: "LLMAgent", other_agent_id: int, sugar_amount: int, spice_amount: int ) -> str: @@ -68,8 +68,12 @@ def propose_trade( other_agent = next( (a for a in agent.model.agents if a.unique_id == other_agent_id), None ) + if other_agent is None: - return f"agent {other_agent_id} not found." + return f"Agent {other_agent} not found." + + if not isinstance(other_agent, Trader): + return f"agent {other_agent_id} is not a valid trader." # Simple trade acceptance logic for demonstration if other_agent.calculate_mrs() > agent.calculate_mrs(): From 51f912a5e9705b09e0dabe4793c8fb8b90f80937 Mon Sep 17 00:00:00 2001 From: sujal gawas Date: Mon, 24 Nov 2025 18:09:42 +0530 Subject: [PATCH 4/8] fixed harvesting logic --- examples/sugarscrap_g1mt/agents.py | 10 +++++++--- examples/sugarscrap_g1mt/app.py | 7 +++---- examples/sugarscrap_g1mt/tools.py | 12 +++++++++++- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/examples/sugarscrap_g1mt/agents.py b/examples/sugarscrap_g1mt/agents.py index 3d9f86a..47bc630 100644 --- a/examples/sugarscrap_g1mt/agents.py +++ b/examples/sugarscrap_g1mt/agents.py @@ -83,7 +83,7 @@ def update_internal_metrics(self): self.internal_state = [ s for s in self.internal_state - if not any(x in s for x in ["Sugar:", "Spice:", "MRS:", "WARNING:"]) + if not any(x in s for x in ["Sugar", "Spice", "MRS", "WARNING_"]) ] self.internal_state.append(f"My Sugar inventory is: {self.sugar}") @@ -96,6 +96,10 @@ def update_internal_metrics(self): self.internal_state.append( "WARNING: I am in danger of starvation from lack of sugar" ) + if self.spice < self.metabolism_spice * 2: + self.internal_state.append( + "WARNING: I am in danger of starvation from lack of spice" + ) def step(self): self.sugar -= self.metabolism_sugar @@ -103,7 +107,7 @@ def step(self): if self.sugar <= 0 or self.spice <= 0: self.model.grid.remove_agent(self) - self.model.schedule.remove(self) + self.remove() return self.update_internal_metrics() @@ -123,7 +127,7 @@ async def astep(self): if self.sugar <= 0 or self.spice <= 0: self.model.grid.remove_agent(self) - self.model.schedule.remove(self) + self.remove() return self.update_internal_metrics() diff --git a/examples/sugarscrap_g1mt/app.py b/examples/sugarscrap_g1mt/app.py index 6ee190b..d3ebe82 100644 --- a/examples/sugarscrap_g1mt/app.py +++ b/examples/sugarscrap_g1mt/app.py @@ -11,7 +11,6 @@ from examples.sugarscrap_g1mt.agents import Resource, Trader from examples.sugarscrap_g1mt.model import SugarScapeModel -from mesa_llm.parallel_stepping import enable_automatic_parallel_stepping from mesa_llm.reasoning.react import ReActReasoning # Suppress Pydantic serialization warnings @@ -25,7 +24,7 @@ # Also suppress through logging logging.getLogger("pydantic").setLevel(logging.ERROR) -enable_automatic_parallel_stepping(mode="threading") +# enable_automatic_parallel_stepping(mode="threading") load_dotenv() @@ -36,12 +35,12 @@ "value": 42, "label": "Random Seed", }, - "initial_traders": 10, + "initial_traders": 2, "initial_resources": 10, "width": 10, "height": 10, "reasoning": ReActReasoning, - "llm_model": "gemini/gemini-1.5-flash", + "llm_model": "gemini/gemini-2.5-flash", "vision": 5, "parallel_stepping": True, } diff --git a/examples/sugarscrap_g1mt/tools.py b/examples/sugarscrap_g1mt/tools.py index d3bc3f1..a8dfd03 100644 --- a/examples/sugarscrap_g1mt/tools.py +++ b/examples/sugarscrap_g1mt/tools.py @@ -44,7 +44,17 @@ def move_to_best_resource(agent: "LLMAgent") -> str: if best_cell: agent.model.grid.move_agent(agent, best_cell) - return f"agent {agent.unique_id} moved to {best_cell}." + + harvested = 0 + cell_contents = agent.model.grid.get_cell_list_contents(best_cell) + for obj in cell_contents: + if isinstance(obj, Resource): + harvested = obj.current_amount + obj.current_amount = 0 # Harvest all available resource + + agent.sugar += harvested + agent.spice += harvested + return f"agent {agent.unique_id} moved to {best_cell}. and harvested {harvested} resources." else: return f"agent {agent.unique_id} found no resources to move to." From f1e74691f260ad50586123b209c4054ba96e461c Mon Sep 17 00:00:00 2001 From: sujal gawas Date: Thu, 27 Nov 2025 10:57:09 +0530 Subject: [PATCH 5/8] final tests with gemini-2.5-flash-lite --- examples/sugarscrap_g1mt/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/sugarscrap_g1mt/app.py b/examples/sugarscrap_g1mt/app.py index d3ebe82..28e46b3 100644 --- a/examples/sugarscrap_g1mt/app.py +++ b/examples/sugarscrap_g1mt/app.py @@ -40,7 +40,7 @@ "width": 10, "height": 10, "reasoning": ReActReasoning, - "llm_model": "gemini/gemini-2.5-flash", + "llm_model": "gemini/gemini-2.5-flash-lite", "vision": 5, "parallel_stepping": True, } From 8eb0fe1c3db7969997c4bea7c34a596371143237 Mon Sep 17 00:00:00 2001 From: sujal gawas Date: Thu, 27 Nov 2025 21:12:09 +0530 Subject: [PATCH 6/8] Readme.md file added --- examples/sugarscrap_g1mt/Readme.md | 123 ++++++++++++++++++++++++++++ examples/sugarscrap_g1mt/Readmer.md | 0 examples/sugarscrap_g1mt/app.py | 2 +- 3 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 examples/sugarscrap_g1mt/Readme.md delete mode 100644 examples/sugarscrap_g1mt/Readmer.md diff --git a/examples/sugarscrap_g1mt/Readme.md b/examples/sugarscrap_g1mt/Readme.md new file mode 100644 index 0000000..f3286c7 --- /dev/null +++ b/examples/sugarscrap_g1mt/Readme.md @@ -0,0 +1,123 @@ +# Sugarscape Constant Growback Model with Traders + +## Summary + +This model is based on Epstein & Axtell's classic "Sugarscape" simulation from Growing Artificial Societies (1996), specifically the G1MT (Growback 1, Metabolism, Trade) variation. Trader agents wander a grid populated with two unevenly distributed resources: Sugar and Spice. Agents are endowed with individual metabolic rates for each resource and a vision range; there are also Resource agents, which represent the landscape and regenerate food over time. + +The model generates emergent economic dynamics through decentralized interactions. Traders must constantly harvest resources to satisfy their metabolic needs; if they run out of either sugar or spice, they starve. Crucially, agents can trade with neighbors. Decisions are governed by the Marginal Rate of Substitution (MRS); agents rich in sugar but poor in spice will trade sugar to acquire spice, and vice versa. Over time, this decentralized trading allows for the emergence of a price equilibrium and wealth distribution patterns. + +This model is implemented using Mesa-LLM, unlike the original deterministic versions. All Trader agents use Large Language Models to "think" about their survival. They observe their internal inventory and MRS, then autonomously decide to use tools to move to high-value resource tiles or propose trades to neighbors to ensure their continued existence. + +## Technical Details + +Agents + +- `Trader (LLMAgent):` The primary actor equipped with STLTMemory and ReActReasoning. + + Internal State: Dynamically updates a context string with current inventory (Sugar, Spice) and hunger warnings to guide the LLM. + + Metabolism: Consumes a fixed amount of resources per step. Zero inventory results in agent removal (death). + + MRS Calculation: Computes the Marginal Rate of Substitution (MRS) using the Cobb-Douglas formula to value Sugar vs. Spice relative to biological needs. + +- `Resource (CellAgent):` A passive environmental agent that acts as a container for resources. It regenerates its current_amount by 1 unit per step up to a max_capacity. + +Tools + +- `move_to_best_resource:` + + Function: Scans the local grid within the agent's vision radius. + + Action: Identifies the cell with the highest current_amount of resources, moves the agent there, and automatically harvests the full amount into the agent's inventory. + +- `propose_trade:` + + Function: Targets a specific neighbor by unique_id. + + Logic: Executes a trade only if the partner's MRS is higher than the proposer's (indicating the partner values Sugar more highly). This ensures trades are mathematically rational and mutually beneficial. + + +### Movement Rule (Rule M) + +A Trader agent moves to a new location and harvests resources if the following logic, executed by the *move_to_best_resource tool*, is satisfied: + +Scan: The agent inspects all cells within its *vision range* (von Neumann neighborhood). + +Identify: It identifies the cell containing a *Resource* agent with the highest *current_amount* of sugar/spice. + +Harvest: The agent moves to that cell, sets the resource's amount to 0, and adds the harvested amount to its own inventory. + +### Trade Rule (Rule T) + +Agents determine whether to trade based on their Marginal Rate of Substitution (MRS). A trade is proposed via the propose_trade tool and accepted if it is mutually beneficial: + +``` +Trade occurs if: Partner_MRS > Agent_MRS +``` + +Where the MRS is calculated using the agent's inventory and metabolism: + +``` +MRS = (spice_inventory / spice_metabolism) / (sugar_inventory / sugar_metabolism) +``` + +In this implementation: + +- `Agent (Proposer):` Gives Sugar, Receives Spice. + +- `Partner (Receiver):` Receives Sugar, Gives Spice. + +- This flow ensures resources move from agents who value them less to agents who value them more. + +### Resource Behavior + +Resource Agents represent the landscape. They are passive agents that regenerate wealth over time: + +- `Growback:` At every step, a Resource agent increases its current_amount by growback (default: 1). + +- `Capacity:` This growth is capped at the agent's max_capacity. + +### LLM-Powered Agents + +Both Traders and the simulation logic are driven by LLM-powered agents, meaning: + +- Their actions (e.g., `move_to_best_resource`, `propose_trade`) are determined by a ReAct reasoning module. + +- This module takes as input: + + The agent’s internal state (current inventory, metabolic warnings, and calculated MRS). + + Local observations of the grid. + +- A set of available tools defined in `tools.py`. + + + +## How to Run + +If you have cloned the repo into your local machine, ensure you run the following command from the root of the library: ``pip install -e . ``. Then, you will need an api key of an LLM-provider of your choice. (This model in particular makes a large amount of calls per minute and we therefore recommend getting a paid version of an api-key that can offer high rate-limits). Once you have obtained the api-key follow the below steps to set it up for this model. +1) Ensure the dotenv package is installed. If not, run ``pip install python-dotenv``. +2) In the root folder of the project, create a file named .env. +3) If you are using openAI's api key, add the following command in the .env file: ``OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx``. If you have the paid version of Gemini, use this line instead: ``GEMINI_API_KEY=your-gemini-api-key-here``(the free ones tend to not work with this model). +4) Change the ``api_key`` specification in app.py according to the provider you have chosen. +5) Similarly change the ``llm_model`` attribute as well in app.py to the name of a model you have access to. Ensure it is in the form of {provider}/{model_name}. For e.g. ``openai/gpt-4o-mini``. + +Once you have set up the api-key in your system, run the following command from this directory: + +``` + $ solara run app.py +``` + + +## Files + +* ``model.py``: Core model code. +* ``agents.py``: Agent classes. +* ``app.py``: Sets up the interactive visualization. +* ``tools.py``: Tools for the llm-agents to use. + + +## Further Reading + +[Growing Artificial Societies](https://mitpress.mit.edu/9780262550253/growing-artificial-societies/) +[Complexity Explorer Sugarscape with Traders Tutorial](https://www.complexityexplorer.org/courses/172-agent-based-models-with-python-an-introduction-to-mesa#gsc.tab=0) \ No newline at end of file diff --git a/examples/sugarscrap_g1mt/Readmer.md b/examples/sugarscrap_g1mt/Readmer.md deleted file mode 100644 index e69de29..0000000 diff --git a/examples/sugarscrap_g1mt/app.py b/examples/sugarscrap_g1mt/app.py index 28e46b3..53b05a2 100644 --- a/examples/sugarscrap_g1mt/app.py +++ b/examples/sugarscrap_g1mt/app.py @@ -40,7 +40,7 @@ "width": 10, "height": 10, "reasoning": ReActReasoning, - "llm_model": "gemini/gemini-2.5-flash-lite", + "llm_model": "ollama/gemma3:1b", "vision": 5, "parallel_stepping": True, } From 366323a2f523059330d34a6942df80e05eb2062b Mon Sep 17 00:00:00 2001 From: sujal gawas Date: Thu, 27 Nov 2025 21:13:33 +0530 Subject: [PATCH 7/8] readme.md --- examples/sugarscrap_g1mt/Readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/sugarscrap_g1mt/Readme.md b/examples/sugarscrap_g1mt/Readme.md index f3286c7..d14493f 100644 --- a/examples/sugarscrap_g1mt/Readme.md +++ b/examples/sugarscrap_g1mt/Readme.md @@ -10,7 +10,7 @@ This model is implemented using Mesa-LLM, unlike the original deterministic vers ## Technical Details -Agents +### Agents - `Trader (LLMAgent):` The primary actor equipped with STLTMemory and ReActReasoning. @@ -22,7 +22,7 @@ Agents - `Resource (CellAgent):` A passive environmental agent that acts as a container for resources. It regenerates its current_amount by 1 unit per step up to a max_capacity. -Tools +### Tools - `move_to_best_resource:` From 86cb20d84edddc230b14e5685e5ffa08d6c209be Mon Sep 17 00:00:00 2001 From: sujal gawas Date: Fri, 28 Nov 2025 19:32:17 +0530 Subject: [PATCH 8/8] Fix harvesting logic in tools.py --- examples/sugarscrap_g1mt/tools.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/examples/sugarscrap_g1mt/tools.py b/examples/sugarscrap_g1mt/tools.py index a8dfd03..6d9acea 100644 --- a/examples/sugarscrap_g1mt/tools.py +++ b/examples/sugarscrap_g1mt/tools.py @@ -10,9 +10,6 @@ if TYPE_CHECKING: from mesa_llm.llm_agent import LLMAgent -if TYPE_CHECKING: - from mesa_llm.llm_agent import LLMAgent - @tool(tool_manager=trader_tool_manager) def move_to_best_resource(agent: "LLMAgent") -> str: @@ -80,12 +77,21 @@ def propose_trade( ) if other_agent is None: - return f"Agent {other_agent} not found." + return f"Agent {other_agent_id} not found." if not isinstance(other_agent, Trader): return f"agent {other_agent_id} is not a valid trader." # Simple trade acceptance logic for demonstration + if sugar_amount <= 0 or spice_amount <= 0: + return "sugar_amount and spice_amount must be positive." + + if agent.sugar < sugar_amount or other_agent.spice < spice_amount: + return ( + f"agent {agent.unique_id} or agent {other_agent_id} " + "does not have enough resources for this trade." + ) + if other_agent.calculate_mrs() > agent.calculate_mrs(): agent.sugar -= sugar_amount agent.spice += spice_amount