Skip to content

Meta agents #2748

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 39 commits into from
Apr 28, 2025
Merged

Meta agents #2748

merged 39 commits into from
Apr 28, 2025

Conversation

tpike3
Copy link
Member

@tpike3 tpike3 commented Apr 4, 2025

Summary

Adds an experimental feature to allow for the deliberate or emergent creation of meta_agents (agents who are compromised of other agents)

Motive

Allow Mesa users to explore complex phenomenon of multi-level systems, while also providing the ability to more effectively leverage threading and multiprocessing for large complex models. (For example each meta-agent can be on a thread and then process their internal dynamics before externally engaging with other meta-agents.)

Implementation

Core additions

  • MetaAgent class extends agent class
  • create_met_agent function to create meta_agent class, instance or add agents to existing meta-agent (agents are currently restricted to being part of meta-agent)
  • find_combinations in the agentset find different possible meta_agent combinations and assess them based on user criteria for optimal combinations.

Examples

  • basic >> warehouse model -- deliberate meta-agent creation
  • basic >> alliance formation -- emergent agent creation

Usage Examples

Deliberate meta-agent creation. The following is from the warehouse model added to examples (inspired by a project from the team in Brazil who used Mesa to do an actual autonomous warehouse). In this case different pseudo-systems (e.g. sensors) are combined to create a RobotAgent

# Create Robot Agents
for idx in range(len(self.loading_docks)):
    # Create sub-agents
    router = RouteAgent(self)
    sensor = SensorAgent(self)
    worker = WorkerAgent(
        self,
        self.warehouse[self.loading_docks[idx]],
        self.warehouse[self.charging_stations[idx]],
    )

    # Create meta-agent and place in warehouse
    create_meta_agent(
        self,
        "RobotAgent",
        [router, sensor, worker],
        mesa_agent_type=CellAgent,
        meta_attributes={
            "cell": self.warehouse[self.charging_stations[idx]],
            "status": "open",
        },
        assume_subagent_attributes=True,
        assume_subagent_methods=True,
    )

Emergent meta-agent creation. The following is from the alliance formation model added to examples. It uses the bilateral shapely value to assess pairwise alliances and creates an emergent hierarchy of alliances. Also demonstrates use of find and evaluate combinations.

# Get hierarchies of agents
agent_types = list(self.agents_by_type.keys())

for agent_type in agent_types:
    similar_agents = self.agents_by_type[agent_type]

    # Find the best combinations using find_combinations
    if (
        len(similar_agents) > 1
    ):  # only form alliances if there are more than 1 agent
        combinations = find_combinations(
            self,
            similar_agents,
            size=2,
            evaluation_func=self.calculate_shapley_value,
            filter_func=self.only_best_combination,
        )

        for alliance, attributes in combinations:
            class_name = f"MetaAgentLevel{attributes[2]}"
            meta = create_meta_agent(
                self,
                class_name,
                alliance,
                meta_attributes={
                    "level": attributes[2],
                    "power": attributes[0],
                    "position": attributes[1],
                },
            )

For fun some pictures

Alliance formation---
image
image

Warehouse Model---
image
image

Additional Notes

The interesting problem with objects "existing in" objects and dynamically creating new objects is referencing them. Examples of this are shown through out the Warehouse model.

Of most significant note was dynamically creating RobotAgent in the model class and then the need to reference it, with no import of an Agent Class. I tried a few different options (e.g. make a model attribute), but arguably the one I went with was adding a kwarg to agents called key_by_name this allows users to store the key in agents_by_type as a string vs and object type and seemed to be the most user friendly.

For this dynamic, I feel like there are more eloquent ways to handle it, so open to any suggestions. However, it may need to be a protocol that evolves over time and becomes a Mesa standard.

tpike3 and others added 30 commits December 28, 2024 11:10
- Add create meta-agents to experimental
- Add tests of meta-agents
- Add example with an alliance formation model in basic examples
- add alliance formation model to example_tests
- allow for deliberate meta-agents creation
- allow for combinatorics
- allow for dynamic agent creation

fix methods-functions; add tests
Co-authored-by: Ewout ter Hoeven <[email protected]>

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Ewout ter Hoeven <[email protected]>
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.8.1 → v0.8.6](astral-sh/ruff-pre-commit@v0.8.1...v0.8.6)
- [github.com/asottile/pyupgrade: v3.19.0 → v3.19.1](asottile/pyupgrade@v3.19.0...v3.19.1)
* reimplementation of draw_voroinoi

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
…rojectmesa#2596)

This PR adds a render interval slider to the SolaraViz visualization system, allowing users to control how frequently plots and visualizations are updated during model simulation. This feature significantly improves performance when working with complex visualizations or multiple plots.
Update version and release notes for the next feature release, Mesa 3.1.3.
* chore: formatting

* fix: changed zsh: command not found: self.rng.randint to zsh: command not found: self.rng.integers
…#2633)

- Install SciPy, since that's now a dependency
- Use uv for pip install, which is way faster
- Add create meta-agents to experimental
- Add tests of meta-agents
- Add example with an alliance formation model in basic examples
- update meta-agent to make more functional
- add warehouse model
- update alliance model
-update tests
- increase test coverage
- fix pre-commit issues
- fix examples autobuild
@tpike3
Copy link
Member Author

tpike3 commented Apr 15, 2025

@tpike3 what did you think of the coderabbit review?

I will try to give it a fully human review this weekend!

Overall: I liked it and thought it was a good addition. Recommend keep.

Had some good features, I really like the sequence diagram, had some standard LLM nonsense (which I resolved) but also had some good catches, which I left and will incorporate into my update.

Of note based on the sequence diagram I am going to refactor create_meta_agent. Currently, the three paths are - add agent to an existing meta_agent, create a new instance of an existing meta_agent class, or create a new class of meta_agent. I will pull these out so they show in the sequence diagram and the general architecture make more sense. A future iteration, will be to replicate the factory create_agents method that @quaquel built but I need to think through that a bit more.

@EwoutH
Copy link
Member

EwoutH commented Apr 16, 2025

I have a personal interview at work tomorrow that was more in-depth than I initially expected - which used up my brain capacity for today.

Still on my list, will try to do it Friday.

@EwoutH
Copy link
Member

EwoutH commented Apr 17, 2025

@colinfrisch, if you would like to give yourself a challenge, could you take a look at this PR?

@EwoutH
Copy link
Member

EwoutH commented Apr 17, 2025

@tpike3 I really love the example models, it really shows what's possible!

We can't include them as basic examples however, since these only use stable features. So I would propose putting one (the cleanest / most representative one) in examples/advanced, and add the others to mesa-examples.

@EwoutH
Copy link
Member

EwoutH commented Apr 17, 2025

@quaquel given your familiarity with agent registration, are you good modifying the core Agent.__init__ and Model.register_agent with the key_by_name flag to enable dynamic type referencing for this experimental feature, or should we explore less invasive alternatives?"

Copy link
Member

@EwoutH EwoutH left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While the module and function docstrings are great for the API reference by explaining what each component does, they don't quite capture the higher-level why and how needed for users to effectively understand and adopt this new experimental feature. We still need a more narrative guide that explains the core meta-agent concept, discusses the deliberate vs. emergent usage patterns, clarifies the implications and potential pitfalls of the assume_* flags, explicitly connects the key_by_name mechanism to dynamic creation, and outlines current limitations. Such a conceptual overview should act as a user manual, complementing the technical specs from the docstrings and making the feature more discoverable and easier to use correctly.

However, since this is an experimental PR, we can decide to pick this up in a later PR. Not blocking.

@EwoutH EwoutH requested review from quaquel and colinfrisch April 17, 2025 18:55
@EwoutH
Copy link
Member

EwoutH commented Apr 23, 2025

Thanks for your continued work! What's the current status? Anything I can help with?

@tpike3
Copy link
Member Author

tpike3 commented Apr 24, 2025

Thanks for your continued work! What's the current status? Anything I can help with?

Thanks @EwoutH! Only thing to be resolved is the key_by_name. I really don't want to touch agents. Short version, I think there is an easy solution, but I am just too close to the problem. I am going to try another idea this weekend. Always happy for another set of eyes. Below is hopefully a concise articulation of the issue, and what I am thinking now.

The challenge: Referencing a class by name created in the model.py. Identified below in comments as Note Part 1 and Note Part 2

"""

  # Create Robot Agents
  for idx in range(len(self.loading_docks)):
      # Create sub-agents
      router = RouteAgent(self)
      sensor = SensorAgent(self)
      worker = WorkerAgent(
          self,
          self.warehouse[self.loading_docks[idx]],
          self.warehouse[self.charging_stations[idx]],
     )

    # Create meta-agent and place in warehouse
   # NOTE PART 1: RobotAgent meta agent class created here 
    create_meta_agent(
        self,
        "RobotAgent",
        [router, sensor, worker],
        mesa_agent_type=CellAgent,
        meta_attributes={
            "cell": self.warehouse[self.charging_stations[idx]],
            "status": "open",
        },
        assume_subagent_attributes=True,
        assume_subagent_methods=True,
    )

  def step(self):
          """
          Advance the model by one step.
          """
          
          # NOTE PART 2: RobotAgent not imported because it is created here so can't reference e.g. from agents import RobotAgent
          for robot in self.agents_by_type["RobotAgent"]: #key_by_name make the keys values of agents_by_type a string
              if robot.status == "open":  # Assign a task to the robot
                  item = self.random.choice(self.agents_by_type["InventoryAgent"])
                  if item.quantity > 0:
                      robot.initiate_task(item)
                      robot.status = "inventory"
                      self.central_move(robot)
              else:
                  robot.continue_task()

"""

My challenge is what is an easy intuitive solution for a mesa user? I feel there must simple solution, that is staring me in the face, but I keep missing it. Every time I think I got one it becomes convoluted and difficult. I feel like it is going to be one of these so simple it is hard problems.

Writing this out I think I may have one. After initialization of RobotAgent get agent_types and reference the robot_agent class from that. I think I can then make that a property of MetaAgent.

Let me know if you got any thoughts.

@quaquel
Copy link
Member

quaquel commented Apr 24, 2025

I'll try to look at the problem as soon as possible, but my teaching starts today....

@quaquel
Copy link
Member

quaquel commented Apr 25, 2025

I understand the problem and agree that there must be a simple solution to this. I'll play around with some ideas later today.

assume_subagent_attributes (bool): Whether to retain attributes
from sub-agents.

Returns:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we simplify this return to allways return the type?

and perhaps also change the name to make clear that this function creates a class rather than an instance?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did make it consistent, but returned the instance of the meta agent, not the class. Working on/thinking through scenarios for meta-agents there were instances where you wanted the meta-agent instance and not the class.

I am not sure what the best option is but only one of the three results returns a new meta-agent class, while the other two either create a new meta-agent instance of an existing class or add an agent to an existing meta-agent.

The other option would be to make each of these results separate, but that would make the emergent meta-agent approach I think much more difficult, and imply direct creation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The critical thing is to have a single return type. If there is one case that creates a class and two cases that result in instances, I would be inclined to separate those out into two separate functions with clearly distinguished names.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The critical thing is to have a single return type. If there is one case that creates a class and two cases that result in instances, I would be inclined to separate those out into two separate functions with clearly distinguished names.

I don't disagree but then that runs into the problem that as the agents do things the code separate functions will have to be more explicit from the user. My thought is to make it more agent behavior to meta-agent creation driven, not explicit creation by the user of when to add and when to create a new instance. My end goal is to enable a network scheduler with active and dormant links, driven by agents with the goal of more novel emergent behavior. My concern is if the user is calling those functions then they are focused on the creation mechanisms instead of the agent behaviors and letting the novelty emerge.

In short, I appreciate your point and have struggled with this choice, but would prefer to keep it like this as it is experimental

@quaquel
Copy link
Member

quaquel commented Apr 25, 2025

So looking at the code again, I think part of the solution to your problem is to actually make sure that create_meta_agent allways returns the class. With that, your example can be simplified to the following:

  # Create Robot Agents
  for idx in range(len(self.loading_docks)):
      # Create sub-agents
      router = RouteAgent(self)
      sensor = SensorAgent(self)
      worker = WorkerAgent(
          self,
          self.warehouse[self.loading_docks[idx]],
          self.warehouse[self.charging_stations[idx]],
     )

    # Create meta-agent and place in warehouse
   # we assign the new class to some attribute
    self.RobotAgent = create_meta_agent(
        self,
        "RobotAgent",
        [router, sensor, worker],
        mesa_agent_type=CellAgent,
        meta_attributes={
            "cell": self.warehouse[self.charging_stations[idx]],
            "status": "open",
        },
        assume_subagent_attributes=True,
        assume_subagent_methods=True,
    )

  def step(self):
          """
          Advance the model by one step.
          """
          
          # NOTE PART 2: RobotAgent not imported because it is created here so can't reference e.g. from agents import RobotAgent
          for robot in self.agents_by_type[self.RobotAgent]:  # we can now use the attribute here to do normal by class lookups
              if robot.status == "open":  # Assign a task to the robot
                  item = self.random.choice(self.agents_by_type["InventoryAgent"])
                  if item.quantity > 0:
                      robot.initiate_task(item)
                      robot.status = "inventory"
                      self.central_move(robot)
              else:
                  robot.continue_task()

With this there is no reason to change anything inside agent.py

@tpike3 tpike3 force-pushed the meta-agents branch 2 times, most recently from ff14883 to bc3f9c0 Compare April 26, 2025 13:45
Update model.py

Update tests

Update meta_agent.py

debating names
@tpike3
Copy link
Member Author

tpike3 commented Apr 26, 2025

All comments have been addressed. There is still one debate on the create_meta_agent dynamic, but I would argue, as this is experimental, that we can agree to disagree and see how it plays out.

Thanks @EwoutH and @quaquel for the reviews! I know it was not a small PR.

mesa/model.py Outdated
Copy link
Member

@EwoutH EwoutH Apr 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@quaquel are you good with the changes in model.py?

Default behavior doesn’t change, but I’m curious if this is the only possible implementation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am very reluctant about those. As indicated, there is a simple solution possible, even given how @tpike3 addressed my comment on create_meta_agent. In my view, experimental features should not touch stable features. In fact, in the past, I subclassed Agent or Model into experimental if I needed to make changes to those classes exactly for this reason.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh dammit, sorry, those should have been removed, I forgot to look at that dif. I agree we should not touch stable features, I will remove now.

Copy link
Member Author

@tpike3 tpike3 Apr 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@EwoutH and @quaquel Thanks for the catch!, I cannot believe I didn't remove the changes to mesa/model.py, must have had too many model.py open for the example models and just spaced over it.

All changes to stables features in agent.py and model.py are removed. Only change to agent.py is in the notes section which is autoformatting updates based on the JOSS feedback (i.e. less than 80 characters, but ruff doesn't want two line for the first line below the function and so on).

Just to make some good notes for refresh months from now when we revisit:
One outstanding question for meta_agent.py is in create_meta_agent() whether to split up into three functions of

  • add_agent_to_meta_agent: adds a new constituting_agent to an existing MetaAgent instance. return meta_agent instance
  • create_new_meta_agent_instance: create a new MetaAgent instance of an existing MetaAgent, return meta_agent instance
  • create_new_meta_agent: creates a new class of MetaAgent, return type of meta-agent instance.

My view right now is to keep it as all part of the create_meta_agent function as this makes it more user friendly for dynamic creation of new and novel meta agents based on constituting agent behavior. This does however come at a cost which is the return object of the create_meta_agent function. This then bleeds over into agents_by_type making it harder on the user to know when to use type(meta_agent) or a specific met-agent instance.

The impact of this can be seen the warehouse model in mesa examples. In the model.py file and the app.py files.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One option is to seperate them out into 3 but also add a fourth overarching function that can be used if the caller based on context cannot know which of the 3 underlying functions to use.

Also, with @overload some of the typing might be made clearer.

All this can wait however and I am fine with merging this now.

Remove all traces of key_by_name
Copy link
Member

@EwoutH EwoutH left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm good with this experimental feature as is! It now looks like a clean changeset that can be integrated in main and included with its experimental status, while development continues.

I will leave the merge to you, but I would like one more approving review before being merged, given the size/complexity of this new feature.

Awesome work Tom!

@tpike3 tpike3 merged commit 94c56a3 into projectmesa:main Apr 28, 2025
12 checks passed
@tpike3 tpike3 deleted the meta-agents branch April 28, 2025 09:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
example Changes the examples or adds to them. experimental Release notes label feature Release notes label
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants