├── agentarium ├── constant.py ├── __init__.py ├── utils.py ├── Interaction.py ├── CheckpointManager.py ├── Config.py ├── actions │ ├── default_actions.py │ └── Action.py ├── AgentInteractionManager.py ├── Environment.py └── Agent.py ├── requirements.txt ├── examples ├── 1_basic_chat │ ├── config.yaml │ └── demo.py ├── 2_checkpointing │ └── demo.py └── 3_adding_a_custom_action │ └── demo.py ├── .github └── workflows │ ├── publish-to-pypi.yml │ └── tests.yml ├── pyproject.toml ├── tests ├── conftest.py ├── test_checkpoint_manager.py ├── test_action.py ├── test_interaction_manager.py └── test_agent.py ├── .gitignore ├── README.md └── LICENSE /agentarium/constant.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | class DefaultValue(Enum): 4 | NOT_PROVIDED = "NOT_PROVIDED" 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | openai>=1.57.2 2 | faker>=33.1.0 3 | PyYAML>=6.0.1 4 | boto3>=1.35.86 5 | aisuite>=0.1.7 6 | dill>=0.3.8 7 | -------------------------------------------------------------------------------- /agentarium/__init__.py: -------------------------------------------------------------------------------- 1 | from .Agent import Agent 2 | from .AgentInteractionManager import AgentInteractionManager 3 | from .actions.Action import Action 4 | from .Interaction import Interaction 5 | 6 | __version__ = "0.3.1" 7 | 8 | __all__ = [ 9 | "Agent", 10 | "AgentInteractionManager", 11 | "Interaction", 12 | "Action", 13 | ] 14 | -------------------------------------------------------------------------------- /examples/1_basic_chat/config.yaml: -------------------------------------------------------------------------------- 1 | # LLM Configuration 2 | llm: 3 | 4 | provider: "openai" # The LLM provider to use (e.g., "openai", "anthropic", etc.) 5 | model: "gpt-4o-mini" # The specific model to use from the provider 6 | 7 | 8 | # aisuite configuration, see https://github.com/andrewyng/aisuite 9 | # aisuite: 10 | 11 | # openai: 12 | # api_key: 13 | -------------------------------------------------------------------------------- /examples/1_basic_chat/demo.py: -------------------------------------------------------------------------------- 1 | from agentarium import Agent 2 | 3 | # Create some agents 4 | alice_agent = Agent.create_agent(name="Alice", occupation="Software Engineer") 5 | bob_agent = Agent.create_agent(name="Bob", occupation="Data Scientist") 6 | 7 | alice_agent.talk_to(bob_agent, "Hello Bob! I heard you're working on some interesting data science projects.") 8 | bob_agent.talk_to(alice_agent, "Hi Alice! Yes, I'm currently working on a machine learning model for natural language processing.") 9 | 10 | alice_agent.act() # Let the agents decide what to do :D 11 | bob_agent.act() 12 | 13 | print("Alice's interactions:") 14 | print(alice_agent.get_interactions()) 15 | 16 | print("\nBob's interactions:") 17 | print(bob_agent.get_interactions()) 18 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | 14 | - name: Set up Python 15 | uses: actions/setup-python@v4 16 | with: 17 | python-version: '3.10' 18 | 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install build twine 23 | 24 | - name: Build package 25 | run: python -m build 26 | 27 | - name: Publish to PyPI 28 | env: 29 | TWINE_USERNAME: __token__ 30 | TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} 31 | run: | 32 | python -m twine upload dist/* -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | push: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python-version: ["3.10"] 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Set up Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install -e ".[test]" 28 | 29 | - name: Run tests with pytest 30 | env: 31 | OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} 32 | run: | 33 | pytest -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "agentarium" 7 | version = "0.3.1" 8 | authors = [ 9 | { name = "thytu" }, 10 | ] 11 | description = "A framework for managing and orchestrating AI agents" 12 | readme = "README.md" 13 | requires-python = ">=3.10" 14 | classifiers = [ 15 | "Programming Language :: Python :: 3", 16 | "License :: OSI Approved :: Apache Software License", 17 | "Operating System :: OS Independent", 18 | ] 19 | dependencies = [ 20 | "openai>=1.57.2", 21 | "faker>=33.1.0", 22 | "PyYAML>=6.0.1", 23 | "boto3>=1.35.86", 24 | "aisuite>=0.1.7", 25 | "dill>=0.3.8", 26 | ] 27 | 28 | [project.optional-dependencies] 29 | test = [ 30 | "pytest>=7.0.0", 31 | "pytest-cov>=4.0.0", 32 | ] 33 | 34 | [tool.pytest.ini_options] 35 | testpaths = ["tests"] 36 | python_files = ["test_*.py"] 37 | addopts = "-v --cov=agentarium --cov-report=term-missing --cov-fail-under=80" 38 | 39 | [project.urls] 40 | Homepage = "https://github.com/thytu/Agentarium" -------------------------------------------------------------------------------- /examples/2_checkpointing/demo.py: -------------------------------------------------------------------------------- 1 | from agentarium import Agent 2 | from agentarium.CheckpointManager import CheckpointManager 3 | 4 | 5 | if __name__ == '__main__': 6 | 7 | # Initialize the CheckpointManager with a unique identifier for this session 8 | # The "demo" identifier is used to detect if this simulation was run before 9 | # If you run this script multiple times, Agentarium will automatically: 10 | # 1. Detect it's the same simulation (based on the "demo" identifier) 11 | # 2. Load cached results instead of re-running expensive LLM calls 12 | # 3. Skip redundant agent interactions that were already computed 13 | checkpoint = CheckpointManager("demo") 14 | 15 | # Create two agents - their states will be tracked by the checkpoint manager 16 | # Even agent creation is cached - if these agents were created before, 17 | # they'll be loaded from cache with their exact same properties 18 | alice = Agent.create_agent(name="Alice") 19 | bob = Agent.create_agent(name="Bob") 20 | 21 | # When this interaction happens: 22 | # - First run: Actually calls the LLM and stores the result 23 | # - Subsequent runs: Loads the cached interaction result automatically 24 | alice.talk_to(bob, "What a beautiful day!") 25 | 26 | # Persist all checkpoints to disk 27 | # This saves the entire simulation state for future runs 28 | # Next time you run this script, it will use this saved state 29 | checkpoint.save() 30 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | 4 | from agentarium import Agent 5 | from agentarium.CheckpointManager import CheckpointManager 6 | from agentarium.AgentInteractionManager import AgentInteractionManager 7 | 8 | 9 | @pytest.fixture(autouse=True) 10 | def cleanup_checkpoint_files(): 11 | """Automatically clean up any checkpoint files after each test.""" 12 | yield 13 | # Clean up any .dill files in the current directory 14 | for file in os.listdir(): 15 | if file.endswith('.dill'): 16 | os.remove(file) 17 | 18 | 19 | @pytest.fixture 20 | def base_agent(): 21 | """Create a basic agent for testing.""" 22 | agent = Agent.create_agent( 23 | name="TestAgent", 24 | age=25, 25 | occupation="Software Engineer", 26 | location="Test City", 27 | bio="A test agent" 28 | ) 29 | yield agent 30 | # Reset the agent after each test that uses this fixture 31 | agent.reset() 32 | 33 | 34 | @pytest.fixture 35 | def interaction_manager(): 36 | """Create a fresh interaction manager for testing.""" 37 | return AgentInteractionManager() 38 | 39 | 40 | @pytest.fixture 41 | def checkpoint_manager(): 42 | """Create a test checkpoint manager.""" 43 | manager = CheckpointManager("test_checkpoint") 44 | yield manager 45 | # Cleanup is handled by cleanup_checkpoint_files fixture 46 | 47 | 48 | @pytest.fixture 49 | def agent_pair(): 50 | """Create a pair of agents for interaction testing.""" 51 | alice = Agent.create_agent(name="Alice", bio="Alice is a test agent") 52 | bob = Agent.create_agent(name="Bob", bio="Bob is a test agent") 53 | yield alice, bob 54 | # Reset both agents after each test that uses this fixture 55 | alice.reset() 56 | bob.reset() 57 | -------------------------------------------------------------------------------- /agentarium/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import hashlib 3 | 4 | from typing import Dict, Any 5 | # from .CheckpointManager import CheckpointManager 6 | 7 | 8 | def dict_hash(dictionary: Dict[str, Any]) -> str: 9 | """MD5 hash of a dictionary.""" 10 | dhash = hashlib.md5() 11 | 12 | encoded = json.dumps(dictionary, sort_keys=True).encode() 13 | dhash.update(encoded) 14 | 15 | return dhash.hexdigest() 16 | 17 | 18 | 19 | def cache_w_checkpoint_manager(function): 20 | """ 21 | A decorator that checks if an action should be skipped based on recorded actions. 22 | 23 | If the action is found in the checkpoint manager's recorded actions, it will be skipped. 24 | This helps in replaying simulations without duplicating actions. 25 | 26 | Args: 27 | function (Callable[..., Any]): The function to decorate 28 | 29 | Returns: 30 | Callable[..., Any]: The decorated function 31 | """ 32 | 33 | 34 | def wrapper(*args, **kwargs): 35 | 36 | from .CheckpointManager import CheckpointManager 37 | 38 | checkpoint_manager = CheckpointManager() 39 | 40 | expected_hash = checkpoint_manager.recorded_actions[checkpoint_manager._action_idx]["hash"] if len(checkpoint_manager.recorded_actions) > checkpoint_manager._action_idx else None 41 | action_hash = str({"function": function.__name__, "args": args, "kwargs": kwargs}) 42 | 43 | if action_hash == expected_hash: 44 | checkpoint_manager._action_idx += 1 45 | return checkpoint_manager.recorded_actions[checkpoint_manager._action_idx - 1]["result"] 46 | 47 | result = function(*args, **kwargs) 48 | checkpoint_manager.recorded_actions = checkpoint_manager.recorded_actions[:checkpoint_manager._action_idx] + [{ 49 | "hash": action_hash, 50 | "result": result, 51 | }] 52 | checkpoint_manager._action_idx += 1 53 | return result 54 | 55 | return wrapper 56 | -------------------------------------------------------------------------------- /agentarium/Interaction.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from dataclasses import dataclass 3 | from typing import TYPE_CHECKING 4 | 5 | if TYPE_CHECKING: 6 | from .Agent import Agent 7 | 8 | @dataclass 9 | class Interaction: 10 | """ 11 | Represents a single interaction between two agents in the environment. 12 | 13 | This class captures the essential details of communication between agents, 14 | including who initiated the interaction (sender), who received it (receiver), 15 | and the content of the interaction (message). 16 | 17 | Attributes: 18 | sender (Agent): The agent who initiated the interaction. 19 | receiver (Agent): The agent who received the interaction. 20 | message (str): The content of the interaction between the agents. 21 | """ 22 | 23 | sender: Agent 24 | """The agent who initiated the interaction.""" 25 | 26 | receiver: list[Agent] 27 | """The agent(s) who received the interaction.""" 28 | 29 | message: str 30 | """The content of the interaction between the agents.""" 31 | 32 | def dump(self) -> dict: 33 | """ 34 | Returns a dictionary representation of the interaction. 35 | """ 36 | return { 37 | "sender": self.sender.agent_id, 38 | "receiver": [receiver.agent_id for receiver in self.receiver], 39 | "message": self.message, 40 | } 41 | 42 | def __str__(self) -> str: 43 | """ 44 | Returns a human-readable string representation of the interaction. 45 | 46 | Returns: 47 | str: A formatted string showing sender, receiver, and the interaction message. 48 | """ 49 | 50 | if len(self.receiver) == 1: 51 | return f"{self.sender.name} ({self.sender.agent_id}) said to {self.receiver[0].name} ({self.receiver[0].agent_id}): {self.message}" 52 | else: 53 | return f"{self.sender.name} ({self.sender.agent_id}) said to {', '.join([_receiver.name + f'({_receiver.agent_id})' for _receiver in self.receiver])}: {self.message}" 54 | 55 | def __repr__(self) -> str: 56 | """ 57 | Returns a string representation of the interaction, same as __str__. 58 | 59 | Returns: 60 | str: A formatted string showing sender, receiver, and the interaction message. 61 | """ 62 | return self.__str__() 63 | -------------------------------------------------------------------------------- /agentarium/CheckpointManager.py: -------------------------------------------------------------------------------- 1 | import os 2 | import dill 3 | 4 | from collections import OrderedDict 5 | from .AgentInteractionManager import AgentInteractionManager 6 | 7 | # NOTE: if someone knows how to fix this, please do it :D 8 | import warnings 9 | warnings.filterwarnings("ignore", category=dill.PicklingWarning) 10 | 11 | 12 | 13 | class CheckpointManager: 14 | """ 15 | A singleton class for managing simulation checkpoints, allowing saving and loading of simulation states. 16 | 17 | This class provides functionality to: 18 | - Save the complete state of a simulation to disk 19 | - Load a previously saved simulation state 20 | """ 21 | 22 | _instance = None 23 | 24 | def __new__(cls, name: str = None): 25 | """ 26 | Create or return the singleton instance of Checkpoint. 27 | 28 | Args: 29 | name (str): Name of the checkpoint 30 | """ 31 | if cls._instance is None: 32 | cls._instance = super(CheckpointManager, cls).__new__(cls) 33 | cls._instance._initialized = False 34 | elif name and name != cls._instance.name: 35 | # Allow only one instance of CheckpointManager 36 | raise RuntimeError(f"CheckpointManager instance already exists with a different name: {name}") 37 | return cls._instance 38 | 39 | def __init__(self, name: str = "default"): 40 | """ 41 | Initialize the checkpoint manager (only runs once for the singleton). 42 | 43 | Args: 44 | name (str): Name of the checkpoint 45 | """ 46 | 47 | if self._initialized: 48 | return 49 | 50 | self.name = name 51 | self.path = f"{self.name}.dill" 52 | 53 | self._interaction_manager = AgentInteractionManager() 54 | 55 | self._action_idx = 0 56 | self.recorded_actions = [] 57 | 58 | if name and os.path.exists(self.path): 59 | self.load() 60 | 61 | self._initialized = True 62 | 63 | def save(self) -> None: 64 | """ 65 | Save the current state of a simulation. 66 | """ 67 | 68 | dill.dump({"actions": self.recorded_actions}, open(self.path, "wb"), byref=True) 69 | 70 | def load(self) -> None: 71 | """ 72 | Load a simulation from a checkpoint. 73 | """ 74 | 75 | env_data = dill.load(open(self.path, "rb")) 76 | 77 | self.recorded_actions = env_data["actions"] 78 | -------------------------------------------------------------------------------- /tests/test_checkpoint_manager.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | from agentarium import Agent 4 | from agentarium.CheckpointManager import CheckpointManager 5 | 6 | @pytest.fixture 7 | def checkpoint_manager(): 8 | """Create a test checkpoint manager.""" 9 | # Reset the singleton instance 10 | CheckpointManager._instance = None 11 | manager = CheckpointManager("test_checkpoint") 12 | yield manager 13 | # Cleanup after tests 14 | if os.path.exists(manager.path): 15 | os.remove(manager.path) 16 | 17 | 18 | def test_checkpoint_manager_initialization(checkpoint_manager): 19 | """Test checkpoint manager initialization.""" 20 | assert checkpoint_manager.name == "test_checkpoint" 21 | assert checkpoint_manager.path == "test_checkpoint.dill" 22 | assert checkpoint_manager._action_idx == 0 23 | assert len(checkpoint_manager.recorded_actions) == 0 24 | 25 | 26 | def test_checkpoint_save_load(checkpoint_manager, agent_pair): 27 | """Test saving and loading checkpoint data.""" 28 | # Create agents and perform actions 29 | alice, bob = agent_pair 30 | 31 | alice.talk_to(bob, "Hello Bob!") 32 | bob.talk_to(alice, "Hi Alice!") 33 | 34 | # Save checkpoint 35 | checkpoint_manager.save() 36 | 37 | # Create a new checkpoint manager and load data 38 | new_manager = CheckpointManager("test_checkpoint") 39 | new_manager.load() 40 | 41 | # Verify recorded actions were loaded 42 | assert len(new_manager.recorded_actions) > 0 43 | assert new_manager.recorded_actions == checkpoint_manager.recorded_actions 44 | 45 | # Reset agents after test 46 | alice.reset() 47 | bob.reset() 48 | 49 | 50 | def test_checkpoint_recording(checkpoint_manager, agent_pair): 51 | """Test that actions are properly recorded.""" 52 | alice, bob = agent_pair 53 | 54 | # Check that actions were recorded 55 | assert len(checkpoint_manager.recorded_actions) == 2 # two agents created 56 | 57 | # Perform some actions 58 | alice.talk_to(bob, "Hello!") 59 | bob.think("I should respond...") 60 | bob.talk_to(alice, "Hi there!") 61 | 62 | # Check that actions were recorded 63 | assert len(checkpoint_manager.recorded_actions) == 3 + 2 # three actions + two agents created 64 | 65 | # Reset agents after test 66 | alice.reset() 67 | bob.reset() 68 | 69 | def test_singleton_behavior(): 70 | """Test that CheckpointManager behaves like a singleton.""" 71 | manager1 = CheckpointManager() 72 | manager2 = CheckpointManager() 73 | 74 | # Both instances should reference the same object 75 | assert manager1 is manager2 76 | 77 | # Clean up 78 | if os.path.exists(manager1.path): 79 | os.remove(manager1.path) 80 | -------------------------------------------------------------------------------- /tests/test_action.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from agentarium import Action, Agent 3 | 4 | def test_action_creation(): 5 | """Test creating an action with valid parameters.""" 6 | def test_function(*args, **kwargs): 7 | return {"result": "success"} 8 | 9 | action = Action( 10 | name="test", 11 | description="A test action", 12 | parameters=["param1", "param2"], 13 | function=test_function 14 | ) 15 | 16 | assert action.name == "test" 17 | assert action.description == "A test action" 18 | assert action.parameters == ["param1", "param2"] 19 | assert action.function is not None 20 | 21 | def test_action_single_parameter(): 22 | """Test creating an action with a single parameter string.""" 23 | def test_function(*args, **kwargs): 24 | return {"result": "success"} 25 | 26 | # Test with a single parameter string 27 | action = Action( 28 | name="test", 29 | description="A test action", 30 | parameters="param1", 31 | function=test_function 32 | ) 33 | 34 | assert action.parameters == ["param1"] 35 | 36 | # Test with a list of parameters 37 | action = Action( 38 | name="test", 39 | description="A test action", 40 | parameters=["param1"], 41 | function=test_function 42 | ) 43 | 44 | assert action.parameters == ["param1"] 45 | 46 | 47 | def test_invalid_action_name(): 48 | """Test that action creation fails with invalid name.""" 49 | def test_function(*args, **kwargs): 50 | return {"result": "success"} 51 | 52 | with pytest.raises(ValueError): 53 | Action( 54 | name="", 55 | description="A test action", 56 | parameters=["param1"], 57 | function=test_function 58 | ) 59 | 60 | 61 | def test_invalid_parameters(): 62 | """Test that action creation fails with invalid parameters.""" 63 | def test_function(*args, **kwargs): 64 | return {"result": "success"} 65 | 66 | # Test empty parameter name 67 | with pytest.raises(ValueError): 68 | Action( 69 | name="test", 70 | description="A test action", 71 | parameters=[""], 72 | function=test_function 73 | ) 74 | 75 | # Test non-string parameter 76 | with pytest.raises(ValueError): 77 | Action( 78 | name="test", 79 | description="A test action", 80 | parameters=[123], 81 | function=test_function 82 | ) 83 | 84 | 85 | def test_action_format(): 86 | """Test the action format string generation.""" 87 | def test_function(*args, **kwargs): 88 | return {"result": "success"} 89 | 90 | action = Action( 91 | name="test", 92 | description="A test action", 93 | parameters=["param1", "param2"], 94 | function=test_function 95 | ) 96 | 97 | expected_format = "[test][param1][param2]" 98 | assert action.get_format() == expected_format 99 | 100 | 101 | def test_action_execution(base_agent): 102 | """Test executing an action with an agent.""" 103 | 104 | def test_function(*args, **kwargs): 105 | agent = kwargs["agent"] 106 | return {"message": f"Action executed by {agent.name}"} 107 | 108 | action = Action( 109 | name="test", 110 | description="A test action", 111 | parameters=["param"], 112 | function=test_function 113 | ) 114 | 115 | result = action.function("test_param", agent=base_agent) 116 | 117 | assert result["action"] == "test" 118 | assert "message" in result 119 | assert "TestAgent" in result["message"] 120 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | #.idea/ 169 | 170 | # PyPI configuration file 171 | .pypirc 172 | 173 | # Pickle files 174 | *.pickle 175 | *.pkl 176 | 177 | *.dill 178 | *.pkl 179 | 180 | # VS Code 181 | *.code-workspace 182 | .DS_Store 183 | -------------------------------------------------------------------------------- /tests/test_interaction_manager.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from agentarium.Agent import Agent 4 | from agentarium.AgentInteractionManager import AgentInteractionManager 5 | 6 | @pytest.fixture 7 | def interaction_manager(): 8 | """Create a test interaction manager.""" 9 | return AgentInteractionManager() 10 | 11 | 12 | def test_agent_registration(interaction_manager, agent_pair): 13 | """Test registering agents with the interaction manager.""" 14 | alice, bob = agent_pair 15 | 16 | # Verify agents are registered 17 | assert alice.agent_id in interaction_manager._agents 18 | assert bob.agent_id in interaction_manager._agents 19 | 20 | # Verify correct agent objects are stored 21 | assert interaction_manager._agents[alice.agent_id] is alice 22 | assert interaction_manager._agents[bob.agent_id] is bob 23 | 24 | 25 | def test_interaction_recording(interaction_manager, agent_pair): 26 | """Test recording interactions between agents.""" 27 | alice, bob = agent_pair 28 | message = "Hello Bob!" 29 | 30 | # Record an interaction 31 | interaction_manager.record_interaction(alice, bob, message) 32 | 33 | # Verify interaction was recorded 34 | assert len(alice.get_interactions()) == 1 35 | 36 | interaction = alice.get_interactions()[0] 37 | assert interaction.sender.agent_id == alice.agent_id 38 | assert interaction.receiver[0].agent_id == bob.agent_id 39 | assert interaction.message == message 40 | 41 | def test_get_agent_interactions(interaction_manager, agent_pair): 42 | """Test retrieving agent interactions.""" 43 | alice, bob = agent_pair 44 | 45 | # Record multiple interactions 46 | interaction_manager.record_interaction(alice, bob, "Hello!") 47 | interaction_manager.record_interaction(bob, alice, "Hi there!") 48 | interaction_manager.record_interaction(alice, bob, "How are you?") 49 | 50 | # Get Alice's interactions 51 | alice_interactions = interaction_manager.get_agent_interactions(alice) 52 | assert len(alice_interactions) == 3 53 | 54 | # Get Bob's interactions 55 | bob_interactions = interaction_manager.get_agent_interactions(bob) 56 | assert len(bob_interactions) == 3 57 | 58 | def test_get_agent(interaction_manager, agent_pair): 59 | """Test retrieving agents by ID.""" 60 | alice, bob = agent_pair 61 | 62 | # Test getting existing agents 63 | assert interaction_manager.get_agent(alice.agent_id) is alice 64 | assert interaction_manager.get_agent(bob.agent_id) is bob 65 | 66 | # Test getting non-existent agent 67 | assert interaction_manager.get_agent("nonexistent_id") is None 68 | 69 | def test_interaction_order(interaction_manager, agent_pair): 70 | """Test that interactions are recorded in order.""" 71 | alice, bob = agent_pair 72 | 73 | messages = ["First", "Second", "Third"] 74 | 75 | for msg in messages: 76 | interaction_manager.record_interaction(alice, bob, msg) 77 | 78 | # Verify interactions are in order 79 | interactions = interaction_manager.get_agent_interactions(alice) 80 | for i, interaction in enumerate(interactions): 81 | assert interaction.message == messages[i] 82 | 83 | def test_self_interaction(interaction_manager, agent_pair): 84 | """Test recording self-interactions (thoughts).""" 85 | alice, _ = agent_pair 86 | thought = "I should learn more" 87 | 88 | interaction_manager.record_interaction(alice, alice, thought) 89 | 90 | interactions = interaction_manager.get_agent_interactions(alice) 91 | assert len(interactions) == 1 92 | assert interactions[0].sender.agent_id == alice.agent_id 93 | assert interactions[0].receiver[0].agent_id == alice.agent_id 94 | assert interactions[0].message == thought 95 | 96 | def test_interaction_validation(interaction_manager, agent_pair): 97 | """Test validation of interaction recording.""" 98 | 99 | alice, _ = agent_pair 100 | 101 | unregistered_agent = type("UnregisteredAgent", (Agent,), { 102 | "agent_id": "some_unregistered_id", 103 | "__init__": lambda self, *args, **kwargs: None, 104 | "agent_informations": {}, 105 | })() 106 | 107 | with pytest.raises(ValueError): 108 | interaction_manager.record_interaction(unregistered_agent, alice, "Hello") 109 | 110 | with pytest.raises(ValueError): 111 | interaction_manager.record_interaction(alice, unregistered_agent, "Hello") 112 | -------------------------------------------------------------------------------- /agentarium/Config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import yaml 3 | from typing import Dict 4 | 5 | 6 | class Config: 7 | """ 8 | A singleton class that manages configuration settings for the Agentarium framework. 9 | 10 | This class handles configuration loading from multiple sources with the following priority: 11 | 1. Environment variables (highest priority) 12 | 2. config.yaml file 13 | 3. Default values (lowest priority) 14 | 15 | The configuration currently supports: 16 | - LLM provider and model settings 17 | 18 | Environment variables: 19 | - AGENTARIUM_LLM_PROVIDER: The LLM provider to use 20 | - AGENTARIUM_LLM_MODEL: The specific model to use 21 | 22 | Example config.yaml: 23 | llm: 24 | provider: "openai" 25 | model: "gpt-4o-mini" 26 | """ 27 | 28 | _instance = None 29 | _initialized = False 30 | 31 | def __new__(cls): 32 | """ 33 | Implement the singleton pattern to ensure only one config instance exists. 34 | 35 | Returns: 36 | Config: The single instance of the Config class. 37 | """ 38 | if cls._instance is None: 39 | cls._instance = super(Config, cls).__new__(cls) 40 | return cls._instance 41 | 42 | def __init__(self): 43 | """ 44 | Initialize the configuration if not already initialized. 45 | 46 | Due to the singleton pattern, this will only execute once, 47 | even if multiple instances are created. 48 | """ 49 | if not self._initialized: 50 | self._config = {} 51 | self._load_config() 52 | self._initialized = True 53 | 54 | def _load_config(self) -> None: 55 | """ 56 | Load configuration from all sources in order of priority. 57 | 58 | The configuration is loaded in the following order: 59 | 1. Set default values 60 | 2. Override with values from config.yaml if it exists 61 | 3. Override with environment variables if they exist 62 | """ 63 | # Default values 64 | self._config = { 65 | "aisuite": dict(), 66 | "llm": { 67 | "provider": "openai", 68 | "model": "gpt-4o-mini" 69 | } 70 | } 71 | 72 | # Try to load from config.yaml in the current directory 73 | config_path = "config.yaml" 74 | if os.path.exists(config_path): 75 | with open(config_path, "r") as f: 76 | yaml_config = yaml.safe_load(f) 77 | if yaml_config: 78 | self._deep_update(self._config, yaml_config) 79 | 80 | # Override with environment variables if they exist 81 | self._config["aisuite"] = os.getenv("AGENTARIUM_AISUITE", self._config["aisuite"]) 82 | self._config["llm"]["provider"] = os.getenv("AGENTARIUM_LLM_PROVIDER", self._config["llm"]["provider"]) 83 | self._config["llm"]["model"] = os.getenv("AGENTARIUM_LLM_MODEL", self._config["llm"]["model"]) 84 | 85 | 86 | def _deep_update(self, d: Dict, u: Dict) -> Dict: 87 | """ 88 | Recursively update a dictionary with values from another dictionary. 89 | 90 | Args: 91 | d (Dict): The base dictionary to update 92 | u (Dict): The dictionary containing update values 93 | 94 | Returns: 95 | Dict: The updated dictionary 96 | """ 97 | for k, v in u.items(): 98 | if isinstance(v, dict): 99 | d[k] = self._deep_update(d.get(k, {}), v) 100 | else: 101 | d[k] = v 102 | return d 103 | 104 | @property 105 | def llm_provider(self) -> str: 106 | """ 107 | Get the configured LLM provider. 108 | 109 | Returns: 110 | str: The name of the LLM provider (e.g., "openai", "anthropic") 111 | """ 112 | return self._config["llm"]["provider"] 113 | 114 | @property 115 | def llm_model(self) -> str: 116 | """ 117 | Get the configured LLM model. 118 | 119 | Returns: 120 | str: The name of the specific model to use (e.g., "gpt-4o-mini") 121 | """ 122 | return self._config["llm"]["model"] 123 | 124 | @property 125 | def aisuite(self) -> dict: 126 | """ 127 | Get the configured aisuite configuration. 128 | """ 129 | return self._config["aisuite"] 130 | -------------------------------------------------------------------------------- /agentarium/actions/default_actions.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from agentarium.utils import cache_w_checkpoint_manager 4 | from agentarium.actions.Action import Action, verify_agent_in_kwargs 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | @verify_agent_in_kwargs 9 | @cache_w_checkpoint_manager 10 | def talk_action_function(*args, **kwargs): 11 | """ 12 | Send a message from one agent to another and record the interaction. 13 | 14 | Args: 15 | *args: Variable length argument list where: 16 | - First argument (str): The ID of the agent to send the message to 17 | - Second argument (str): The content of the message to send 18 | **kwargs: Arbitrary keyword arguments. Must contain 'agent' key with the Agent instance. 19 | 20 | Returns: 21 | dict: A dictionary containing: 22 | - sender (str): The ID of the sending agent 23 | - receiver (str | list[str]): The ID of the receiving agent 24 | - message (str): The content of the message 25 | 26 | Raises: 27 | RuntimeError: If less than 1 argument is provided, if 'agent' is not in kwargs, 28 | or if the receiver agent ID is invalid. 29 | """ 30 | 31 | if len(args) < 1: 32 | raise RuntimeError(f"Received a TALK action with less than 1 argument: {args}") 33 | 34 | if "agent" not in kwargs: 35 | raise RuntimeError(f"Couldn't find agent in {kwargs=} for TALK action") 36 | 37 | # Retrieve the receiver IDs 38 | receiver_ids = [_rec.strip() for _rec in args[0].split(',') if len(_rec.strip()) != 0] 39 | 40 | # If the receiver is "all", talk to all agents 41 | if "all" in receiver_ids: 42 | receiver_ids = [agent.agent_id for agent in kwargs["agent"]._interaction_manager._agents.values()] 43 | 44 | # Retrieve the receivers from their IDs 45 | receivers = [kwargs["agent"]._interaction_manager.get_agent(_receiver_id) for _receiver_id in receiver_ids] 46 | 47 | # Check if the receivers are valid 48 | if any(receiver is None for receiver in receivers): 49 | logger.error(f"Received a TALK action with an invalid agent ID {args[0]=} {args=} {kwargs['agent']._interaction_manager._agents=}") 50 | raise RuntimeError(f"Received a TALK action with an invalid agent ID: {args[0]=}. {args=}") 51 | 52 | message = args[1] 53 | 54 | if len(message) == 0: 55 | logger.warning(f"Received empty message for talk_action_function: {args=}") 56 | 57 | # Record the interaction 58 | kwargs["agent"]._interaction_manager.record_interaction(kwargs["agent"], receivers, message) 59 | 60 | return { 61 | "sender": kwargs["agent"].agent_id, 62 | "receiver": [receiver.agent_id for receiver in receivers], 63 | "message": message, 64 | } 65 | 66 | @verify_agent_in_kwargs 67 | @cache_w_checkpoint_manager 68 | def think_action_function(*args, **kwargs): 69 | """ 70 | Record an agent's internal thought. 71 | 72 | Args: 73 | *params: Variable length argument list where the first argument is the thought content. 74 | **kwargs: Arbitrary keyword arguments. Must contain 'agent' key with the Agent instance. 75 | 76 | Returns: 77 | dict: A dictionary containing: 78 | - sender (str): The ID of the thinking agent 79 | - receiver (str): Same as sender (since it's an internal thought) 80 | - message (str): The content of the thought 81 | 82 | Raises: 83 | RuntimeError: If no parameters are provided for the thought content. 84 | """ 85 | 86 | if len(args) < 1: 87 | raise RuntimeError(f"Received a TALK action with less than 1 argument: {args}") 88 | 89 | if "agent" not in kwargs: 90 | raise RuntimeError(f"Couldn't find agent in {kwargs=} for TALK action") 91 | 92 | message = args[0] 93 | 94 | kwargs["agent"]._interaction_manager.record_interaction(kwargs["agent"], kwargs["agent"], message) 95 | 96 | return { 97 | "sender": kwargs["agent"].agent_id, 98 | "receiver": kwargs["agent"].agent_id, 99 | "message": message, 100 | } 101 | 102 | 103 | # Create action instances at module level 104 | talk_action_description = """\ 105 | Talk to agents by specifying their IDs followed by the content to say: 106 | - To talk to a single agent: Enter an agent ID (e.g. "1") 107 | - To talk to multiple agents: Enter IDs separated by commas (e.g. "1,2,3") 108 | - To talk to all agents: Enter "all"\ 109 | """ 110 | talk_action = Action( 111 | name="talk", 112 | description=talk_action_description, 113 | parameters=["agent_id", "message"], 114 | function=talk_action_function, 115 | ) 116 | 117 | think_action = Action( 118 | name="think", 119 | description="Think about something.", 120 | parameters=["content"], 121 | function=think_action_function, 122 | ) 123 | 124 | default_actions = { 125 | talk_action.name: talk_action, 126 | think_action.name: think_action, 127 | } 128 | 129 | # __all__ = ["talk_action", "think_action", "default_actions"] 130 | -------------------------------------------------------------------------------- /tests/test_agent.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from agentarium import Agent, Action 4 | from agentarium.constant import DefaultValue 5 | 6 | 7 | def test_agent_creation(base_agent): 8 | """Test basic agent creation with default and custom values.""" 9 | # Test with default values 10 | assert base_agent.agent_id is not None 11 | assert base_agent.name is not None 12 | assert base_agent.age is not None 13 | assert base_agent.occupation is not None 14 | assert base_agent.location is not None 15 | assert base_agent.bio is not None 16 | 17 | # Test without default values (nor bio) 18 | custom_agent = Agent.create_agent( 19 | name="Alice", 20 | age=25, 21 | occupation="Software Engineer", 22 | location="San Francisco", 23 | ) 24 | assert isinstance(custom_agent.name, str) 25 | assert isinstance(custom_agent.age, int) 26 | assert isinstance(custom_agent.occupation, str) 27 | assert isinstance(custom_agent.location, str) 28 | assert isinstance(custom_agent.bio, str) 29 | 30 | 31 | def test_agent_default_actions(agent_pair): 32 | """Test that agents are created with default actions.""" 33 | alice, _ = agent_pair 34 | assert "talk" in alice._actions 35 | assert "think" in alice._actions 36 | 37 | 38 | def test_agent_custom_actions(base_agent): 39 | """Test adding and using custom actions.""" 40 | def custom_action(*args, **kwargs): 41 | agent = kwargs["agent"] 42 | return {"message": f"Custom action by {agent.name}"} 43 | 44 | custom_action_obj = Action( 45 | name="custom", 46 | description="A custom action", 47 | parameters=["message"], 48 | function=custom_action 49 | ) 50 | 51 | base_agent.add_action(custom_action_obj) 52 | 53 | assert "custom" in base_agent._actions 54 | result = base_agent.execute_action("custom", "test message") 55 | assert result["action"] == "custom" 56 | assert "message" in result 57 | 58 | 59 | def test_agent_interaction(agent_pair): 60 | """Test basic interaction between agents.""" 61 | 62 | alice, bob = agent_pair 63 | 64 | message = "Hello Bob!" 65 | alice.talk_to(bob, message) 66 | 67 | # Check Alice's interactions 68 | alice_interactions = alice.get_interactions() 69 | assert len(alice_interactions) == 1 70 | assert alice_interactions[0].sender.agent_id == alice.agent_id 71 | assert alice_interactions[0].receiver[0].agent_id == bob.agent_id 72 | assert alice_interactions[0].message == message 73 | 74 | # Check Bob's interactions 75 | bob_interactions = bob.get_interactions() 76 | assert len(bob_interactions) == 1 77 | assert bob_interactions[0].sender.agent_id == alice.agent_id 78 | assert bob_interactions[0].receiver[0].agent_id == bob.agent_id 79 | assert bob_interactions[0].message == message 80 | 81 | 82 | def test_agent_think(agent_pair): 83 | """Test agent's ability to think.""" 84 | 85 | agent, _ = agent_pair 86 | thought = "I should learn more about AI" 87 | 88 | agent.think(thought) 89 | 90 | interactions = agent.get_interactions() 91 | assert len(interactions) == 1 92 | assert interactions[0].sender.agent_id == agent.agent_id 93 | assert interactions[0].receiver[0].agent_id == agent.agent_id 94 | assert interactions[0].message == thought 95 | 96 | 97 | def test_invalid_action(base_agent): 98 | """Test handling of invalid actions.""" 99 | 100 | with pytest.raises(RuntimeError): 101 | base_agent.execute_action("nonexistent_action", "test") 102 | 103 | 104 | def test_agent_reset(base_agent): 105 | """Test resetting agent state.""" 106 | base_agent.think("Initial thought") 107 | 108 | assert len(base_agent.get_interactions()) == 1 109 | 110 | base_agent.reset() 111 | assert len(base_agent.get_interactions()) == 0 112 | assert len(base_agent.storage) == 0 113 | 114 | 115 | def test_display_interaction(agent_pair): 116 | """Test displaying an interaction.""" 117 | alice, bob = agent_pair 118 | message = "Hello Bob!" 119 | 120 | alice.talk_to(bob, message) # One receiver 121 | alice.talk_to([bob, alice], message) # Multiple receivers 122 | 123 | # just check that it doesn't raise an error 124 | print(alice.get_interactions()[0]) # one receiver 125 | print(alice.get_interactions()[1]) # multiple receivers 126 | 127 | 128 | def test_agent_actions_manualy_executed(base_agent): 129 | """Test agent actions.""" 130 | 131 | base_agent.add_action(Action("test", "A test action", ["message"], lambda message, *args, **kwargs: {"message": message})) 132 | 133 | action = base_agent.execute_action("test", "test message") 134 | assert action["message"] == "test message" 135 | 136 | 137 | def test_agent_actions_automatically_executed(base_agent): 138 | """Test agent actions.""" 139 | 140 | base_agent.add_action(Action("test", "A test action", ["message"], lambda message, *args, **kwargs: {"message": message})) 141 | base_agent.think("I need to do the action 'test' with the message 'test message'") 142 | 143 | action = base_agent.act() 144 | assert action["message"] == "test message" 145 | -------------------------------------------------------------------------------- /agentarium/actions/Action.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from functools import wraps 4 | from dataclasses import dataclass 5 | from typing import Callable, Any, Dict, Union, Optional, List 6 | 7 | 8 | # TODO: rename the output key "action" to "_action_name" 9 | def standardize_action_output(action): 10 | """ 11 | Decorator that standardizes the output format of action functions. 12 | 13 | This decorator ensures that all action functions return a dictionary with a 14 | consistent format, containing at minimum an 'action' key with the action name. 15 | If the original function returns a dictionary, its contents are preserved 16 | (except for the 'action' key which may be overwritten). If it returns a 17 | non-dictionary value, it is stored under the 'output' key. 18 | 19 | Args: 20 | fun (Callable): The action function to decorate. 21 | 22 | Returns: 23 | Callable: A wrapped function that standardizes the output format. 24 | """ 25 | 26 | def decorator(function): 27 | @wraps(function) 28 | def wrapper(*args, **kwargs): 29 | 30 | fn_output = function(*args, **kwargs) 31 | 32 | output = fn_output if isinstance(fn_output, dict) else {"output": fn_output} 33 | 34 | if "action" in output: 35 | logging.warning(( 36 | f"The action '{action.name}' returned an output with an 'action' key. " 37 | "This is not allowed, it will be overwritten." 38 | )) 39 | 40 | output["action"] = action.name 41 | 42 | return output 43 | 44 | return wrapper 45 | 46 | return decorator 47 | 48 | 49 | def verify_agent_in_kwargs(function): 50 | """ 51 | Decorator that verifies the presence of an 'agent' key in kwargs. 52 | 53 | This decorator ensures that any action function receives an agent instance 54 | through its kwargs. This is crucial for actions that need to interact with 55 | or modify the agent's state. 56 | 57 | Args: 58 | function (Callable): The action function to decorate. 59 | 60 | Returns: 61 | Callable: A wrapped function that verifies the presence of 'agent' in kwargs. 62 | 63 | Raises: 64 | RuntimeError: If 'agent' key is not found in kwargs. 65 | """ 66 | @wraps(function) 67 | def wrapper(*args, **kwargs): 68 | if "agent" not in kwargs: 69 | raise RuntimeError("Action functions must receive an agent instance through kwargs") 70 | return function(*args, **kwargs) 71 | return wrapper 72 | 73 | 74 | @dataclass 75 | class Action: 76 | """ 77 | Represents an action that an agent can perform. 78 | 79 | Attributes: 80 | name (str): Unique identifier for the action. Also used to remove the action from the agent's action space. 81 | description (str, optional): Short description of what the action does. 82 | parameters (Union[List[str], str]): List of parameters names or a single parameter name. 83 | function: Callable that implements the action. 84 | 85 | Example: 86 | ```python 87 | action = Action( 88 | name="CHATGPT", 89 | description="Use ChatGPT", 90 | parameters=["prompt"], 91 | function=use_chatgpt, 92 | ) 93 | ``` 94 | """ 95 | 96 | name: str 97 | description: Optional[str] 98 | parameters: Union[List[str], str] 99 | function: Callable[[Any, ...], Dict[str, Any]] 100 | 101 | def __post_init__(self): 102 | """Validate action attributes after initialization.""" 103 | 104 | if len(self.name) < 1: 105 | raise ValueError("Action name must be non-empty") 106 | 107 | if isinstance(self.parameters, str): 108 | self.parameters = [self.parameters] 109 | 110 | if not isinstance(self.parameters, list): 111 | raise ValueError("Parameters must be a list of strings or a single string") 112 | 113 | if any(not isinstance(p, str) for p in self.parameters): 114 | raise ValueError("Parameter names must be strings") 115 | 116 | if any(len(p) < 1 for p in self.parameters): 117 | raise ValueError("Parameter names must be non-empty") 118 | 119 | # Store the function's module and name for pickling 120 | if hasattr(self.function, '__module__') and hasattr(self.function, '__name__'): 121 | self._function_module = self.function.__module__ 122 | self._function_name = self.function.__name__ 123 | else: 124 | raise ValueError("Function must be a named function (not a lambda) defined at module level") 125 | 126 | self.function = standardize_action_output(self)(self.function) 127 | 128 | def get_format(self): 129 | """Returns a string representation of the action's format for use in prompts. 130 | 131 | The format consists of the action name followed by its parameters, all enclosed in square brackets. 132 | For example, an action named "TALK" with parameters ["agent_id", "message"] would return: 133 | "[TALK][agent_id][message]" 134 | 135 | Returns: 136 | str: The formatted string showing how to use the action in prompts. 137 | """ 138 | return f"[{self.name}]" + "".join([f"[{p}]" for p in self.parameters]) 139 | -------------------------------------------------------------------------------- /agentarium/AgentInteractionManager.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Dict, List, TYPE_CHECKING 4 | from .Interaction import Interaction 5 | 6 | if TYPE_CHECKING: 7 | from .Agent import Agent 8 | 9 | 10 | class AgentInteractionManager: 11 | """ 12 | A singleton class that manages all interactions between agents in the environment. 13 | 14 | This class is responsible for: 15 | - Keeping track of all registered agents 16 | - Recording and storing interactions between agents 17 | - Providing access to interaction history for both individual agents and the entire system 18 | 19 | The manager implements the Singleton pattern to ensure a single source of truth 20 | for all agent interactions across the environment. 21 | """ 22 | 23 | _instance = None 24 | 25 | def __new__(cls): 26 | """ 27 | Implements the singleton pattern to ensure only one instance exists. 28 | 29 | Returns: 30 | AgentInteractionManager: The single instance of the manager. 31 | """ 32 | if cls._instance is None: 33 | cls._instance = super(AgentInteractionManager, cls).__new__(cls) 34 | cls._instance._initialized = False 35 | return cls._instance 36 | 37 | def __init__(self): 38 | """ 39 | Initializes the interaction manager if not already initialized. 40 | 41 | Due to the singleton pattern, this will only execute once, even if 42 | multiple instances are created. 43 | """ 44 | if not self._initialized: 45 | self._agents: Dict[str, Agent] = {} 46 | self._interactions: List[Interaction] = [] 47 | self._agent_private_interactions: Dict[str, List[Interaction]] = {} 48 | self._initialized = True 49 | 50 | def register_agent(self, agent: Agent) -> None: 51 | """ 52 | Register a new agent with the interaction manager. 53 | 54 | This method adds the agent to the manager's registry and initializes 55 | their private interaction history. 56 | 57 | Args: 58 | agent (Agent): The agent to register with the manager. 59 | """ 60 | self._agents[agent.agent_id] = agent 61 | self._agent_private_interactions[agent.agent_id] = [] 62 | 63 | def reset_agent(self, agent: Agent) -> None: 64 | """ 65 | Reset the agent's state. 66 | """ 67 | self._agent_private_interactions[agent.agent_id] = [] 68 | 69 | def get_agent(self, agent_id: str) -> Agent | None: 70 | """ 71 | Retrieve an agent by their ID. 72 | 73 | Args: 74 | agent_id (str): The unique identifier of the agent. 75 | 76 | Returns: 77 | Agent | None: The requested agent if found, None otherwise. 78 | """ 79 | return self._agents.get(agent_id) 80 | 81 | def record_interaction(self, sender: Agent, receiver: Agent | list[Agent], message: str) -> None: 82 | """ 83 | Record a new interaction between two agents. 84 | 85 | This method creates a new Interaction object and stores it in both the global 86 | interaction history and the private interaction histories of both involved agents. 87 | 88 | Args: 89 | sender (Agent): The agent initiating the interaction. 90 | receiver (Agent | list[Agent]): The agent(s) receiving the interaction. 91 | message (str): The content of the interaction. 92 | """ 93 | 94 | from .Agent import Agent 95 | 96 | if sender.agent_id not in self._agents: 97 | raise ValueError(f"Sender agent {sender.agent_id} is not registered in the interaction manager.") 98 | 99 | if isinstance(receiver, Agent): 100 | receiver = [receiver] 101 | 102 | if any(_receiver.agent_id not in self._agents for _receiver in receiver): 103 | invalid_receivers = [_receiver for _receiver in receiver if _receiver.agent_id not in self._agents] 104 | raise ValueError(f"Receiver agent(s) {invalid_receivers} not registered in the interaction manager.") 105 | 106 | interaction = Interaction(sender=sender, receiver=receiver, message=message) 107 | 108 | # Record in global interactions 109 | self._interactions.append(interaction) 110 | 111 | # Record in private interactions for both sender and receiver 112 | self._agent_private_interactions[sender.agent_id].append(interaction) 113 | 114 | for _receiver in receiver: 115 | if _receiver.agent_id != sender.agent_id: 116 | self._agent_private_interactions[_receiver.agent_id].append(interaction) 117 | 118 | def get_all_interactions(self) -> List[Interaction]: 119 | """ 120 | Retrieve the complete history of all interactions in the environment. 121 | 122 | This method is primarily used for administrative and debugging purposes. 123 | 124 | Returns: 125 | List[Interaction]: A list of all interactions that have occurred. 126 | """ 127 | return self._interactions 128 | 129 | def get_agent_interactions(self, agent: Agent) -> List[Interaction]: 130 | """ 131 | Retrieve all interactions involving a specific agent. 132 | 133 | This includes both interactions where the agent was the sender 134 | and where they were the receiver. 135 | 136 | Args: 137 | agent (Agent): The agent whose interactions to retrieve. 138 | 139 | Returns: 140 | List[Interaction]: A list of all interactions involving the agent. 141 | """ 142 | return self._agent_private_interactions.get(agent.agent_id, []) 143 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🌿 Agentarium 2 | 3 |