├── 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 |
4 | 5 | [![License: Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-yellow.svg)](https://opensource.org/licenses/Apache-2.0) 6 | [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/) 7 | [![PyPI version](https://badge.fury.io/py/agentarium.svg)](https://badge.fury.io/py/agentarium) 8 | 9 | A powerful Python framework for managing and orchestrating AI agents with ease. Agentarium provides a flexible and intuitive way to create, manage, and coordinate interactions between multiple AI agents in various environments. 10 | 11 | [Installation](#installation) • 12 | [Quick Start](#quick-start) • 13 | [Features](#features) • 14 | [Examples](#examples) • 15 | [Documentation](#documentation) • 16 | [Contributing](#contributing) 17 | 18 |
19 | 20 | ## 🚀 Installation 21 | 22 | ```bash 23 | pip install agentarium 24 | ``` 25 | 26 | ## 🎯 Quick Start 27 | 28 | ```python 29 | from agentarium import Agent 30 | 31 | # Create agents 32 | agent1 = Agent(name="agent1") 33 | agent2 = Agent(name="agent2") 34 | 35 | # Direct communication between agents 36 | alice.talk_to(bob, "Hello Bob! I heard you're working on some interesting ML projects.") 37 | 38 | # Agent autonomously decides its next action based on context 39 | bob.act() 40 | ``` 41 | 42 | ## ✨ Features 43 | 44 | - **🤖 Advanced Agent Management**: Create and orchestrate multiple AI agents with different roles and capabilities 45 | - **🔄 Autonomous Decision Making**: Agents can make decisions and take actions based on their context 46 | - **💾 Checkpoint System**: Save and restore agent states and interactions for reproducibility 47 | - **🎭 Customizable Actions**: Define custom actions beyond the default talk/think capabilities 48 | - **🧠 Memory & Context**: Agents maintain memory of past interactions for contextual responses 49 | - **⚡ AI Integration**: Seamless integration with various AI providers through aisuite 50 | - **⚡ Performance Optimized**: Built for efficiency and scalability 51 | - **🛠️ Extensible Architecture**: Easy to extend and customize for your specific needs 52 | 53 | ## 📚 Examples 54 | 55 | ### Basic Chat Example 56 | Create a simple chat interaction between agents: 57 | 58 | ```python 59 | from agentarium import Agent 60 | 61 | # Create agents with specific characteristics 62 | alice = Agent.create_agent(name="Alice", occupation="Software Engineer") 63 | bob = Agent.create_agent(name="Bob", occupation="Data Scientist") 64 | 65 | # Direct communication 66 | alice.talk_to(bob, "Hello Bob! I heard you're working on some interesting projects.") 67 | 68 | # Let Bob autonomously decide how to respond 69 | bob.act() 70 | ``` 71 | 72 | ### Adding Custom Actions 73 | Add new capabilities to your agents: 74 | 75 | ```python 76 | from agentarium import Agent, Action 77 | 78 | # Define a simple greeting action 79 | def greet(name: str, **kwargs) -> str: 80 | return f"Hello, {name}!" 81 | 82 | # Create an agent and add the greeting action 83 | agent = Agent.create_agent(name="Alice") 84 | agent.add_action( 85 | Action( 86 | name="GREET", 87 | description="Greet someone by name", 88 | parameters=["name"], 89 | function=greet 90 | ) 91 | ) 92 | 93 | # Use the custom action 94 | agent.execute_action("GREET", "Bob") 95 | ``` 96 | 97 | ### Using Checkpoints 98 | Save and restore agent states: 99 | 100 | ```python 101 | from agentarium import Agent 102 | from agentarium.CheckpointManager import CheckpointManager 103 | 104 | # Initialize checkpoint manager 105 | checkpoint = CheckpointManager("demo") 106 | 107 | # Create and interact with agents 108 | alice = Agent.create_agent(name="Alice") 109 | bob = Agent.create_agent(name="Bob") 110 | 111 | alice.talk_to(bob, "What a beautiful day!") 112 | checkpoint.update(step="interaction_1") 113 | 114 | # Save the current state 115 | checkpoint.save() 116 | ``` 117 | 118 | More examples can be found in the [examples/](examples/) directory. 119 | 120 | ## 📖 Documentation 121 | 122 | ### Agent Creation 123 | Create agents with custom characteristics: 124 | 125 | ```python 126 | agent = Agent.create_agent( 127 | name="Alice", 128 | age=28, 129 | occupation="Software Engineer", 130 | location="San Francisco", 131 | bio="A passionate developer who loves AI" 132 | ) 133 | ``` 134 | 135 | ### LLM Configuration 136 | Configure your LLM provider and credentials using a YAML file: 137 | 138 | ```yaml 139 | llm: 140 | provider: "openai" # The LLM provider to use (any provider supported by aisuite) 141 | model: "gpt-4" # The specific model to use from the provider 142 | 143 | aisuite: # (optional) Credentials for aisuite 144 | openai: # Provider-specific configuration 145 | api_key: "sk-..." # Your API key 146 | ``` 147 | 148 | ### Key Components 149 | 150 | - **Agent**: Core class for creating AI agents with personalities and autonomous behavior 151 | - **CheckpointManager**: Handles saving and loading of agent states and interactions 152 | - **Action**: Base class for defining custom agent actions 153 | - **AgentInteractionManager**: Manages and tracks all agent interactions 154 | 155 | ## 🤝 Contributing 156 | 157 | Contributions are welcome! Here's how you can help: 158 | 159 | 1. Fork the repository 160 | 2. Create a new branch (`git checkout -b feature/amazing-feature`) 161 | 3. Make your changes 162 | 4. Commit your changes (`git commit -m 'feat: add amazing feature'`) 163 | 5. Push to the branch (`git push origin feature/amazing-feature`) 164 | 6. Open a Pull Request 165 | 166 | ## 📄 License 167 | 168 | This project is licensed under the Apache 2.0 License - see the [LICENSE](LICENSE) file for details. 169 | 170 | ## 🙏 Acknowledgments 171 | 172 | Thanks to all contributors who have helped shape Agentarium 🫶 173 | 174 | -------------------------------------------------------------------------------- /examples/3_adding_a_custom_action/demo.py: -------------------------------------------------------------------------------- 1 | """ 2 | This demo showcases how to create a custom action for Agentarium agents. 3 | It demonstrates: 4 | 1. Creating an agent with a custom personality 5 | 2. Adding a new action (ChatGPT integration) to the agent 6 | 3. Using both direct action calls and autonomous agent behavior 7 | 8 | The demo specifically shows how to use the `add_action` method to extend an agent's capabilities. 9 | 10 | Action Definition Format: 11 | Action( 12 | name: str, # Unique identifier for the action 13 | description: str, # Description of what the action does 14 | parameters: List[str], # List of parameter names the action accepts 15 | function: Callable # Function that implements the action behavior 16 | ) 17 | 18 | The action function should take (*args, **kwargs) as parameters and return 19 | either a string or a dictionary. If a dictionary is returned, it can contain any keys 20 | except 'action' which is reserved. The function's output will be automatically 21 | standardized to include the action name. 22 | """ 23 | 24 | import aisuite as ai 25 | from typing import Dict, Any 26 | from agentarium import Agent, Action 27 | 28 | 29 | # Initialize the AI client for ChatGPT interactions 30 | llm_client = ai.Client() 31 | 32 | 33 | def use_chatgpt(prompt: str, *args, **kwargs) -> Dict[str, str]: 34 | """ 35 | A custom action that allows an agent to interact with ChatGPT. 36 | This function demonstrates how to: 37 | 1. Maintain conversation history in agent's storage 38 | 2. Make API calls to external services 39 | 3. Update agent's memory with the interaction 40 | 41 | The function follows the required format for custom actions: 42 | - Takes an agent as the first parameter 43 | - Takes action-specific parameters after that 44 | - Returns either a string or a dictionary (dictionary in this case) 45 | 46 | Args: 47 | agent (Agent): The agent instance performing the action 48 | prompt (str): The message to send to ChatGPT 49 | *args, **kwargs: Additional arguments (not used in this demo) 50 | 51 | Returns: 52 | Dict[str, str]: A dictionary containing: 53 | - agent_prompt: The original prompt sent to ChatGPT 54 | - chatgpt_response: ChatGPT's response 55 | Note: The 'action' key will be automatically added by the framework when called by agent.act() 56 | """ 57 | 58 | agent: Agent = kwargs["agent"] # Every action receives the agent in kwargs 59 | 60 | # Initialize chat history if it doesn't exist 61 | if "chatgpt_history" not in agent.storage: 62 | agent.storage["chatgpt_history"] = [] 63 | 64 | # Prepare and send the message to ChatGPT 65 | chatgpt_history = agent.storage["chatgpt_history"] 66 | chatgpt_history.append({"role": "user", "content": prompt}) 67 | 68 | response = llm_client.chat.completions.create( 69 | model="openai:gpt-4o-mini", 70 | messages=chatgpt_history, 71 | temperature=0.2, 72 | timeout=60 73 | ).choices[0].message.content.strip() 74 | 75 | # Store the interaction in agent's memory and history 76 | agent.storage["chatgpt_history"].append({"role": "assistant", "content": response}) 77 | agent.think(f"What you said to ChatGPT: {prompt}") 78 | agent.think(f"ChatGPT response: {response}") 79 | 80 | return { 81 | "agent_prompt": prompt, 82 | "chatgpt_response": response 83 | } 84 | 85 | 86 | def print_agent_output(output: Dict[str, Any]) -> None: 87 | """ 88 | Helper function to format and print agent's action outputs. 89 | 90 | Args: 91 | output (Dict[str, Any]): The output dictionary from an agent's action 92 | """ 93 | print(f"=== Agent executed action '{output['action']}' with the following output ===", end="\n\n") 94 | 95 | for key, value in output.items(): 96 | if key != "action": 97 | print(f" {key}: {value}", end="\n\n") 98 | 99 | print("=" * 65, end="\n\n\n") 100 | 101 | 102 | def main(): 103 | """ 104 | Main demo function showcasing: 105 | 1. Agent creation with personality 106 | 2. Adding a custom action 107 | 3. Direct action calls vs autonomous behavior 108 | """ 109 | # Create an agent with a defined personality 110 | agent = Agent.create_agent( 111 | agent_id="demo_agent", 112 | name="John", 113 | gender="male", 114 | age=25, 115 | occupation="software engineer", 116 | location="San Francisco", 117 | bio="John is a curious software engineer who loves to learn and experiment with AI.", 118 | ) 119 | 120 | # Add the ChatGPT action to the agent's capabilities 121 | # This demonstrates how to use add_action: 122 | # 1. Define the action descriptor with prompt, format, and example 123 | # 2. Provide an action function that implements the behavior 124 | agent.add_action( 125 | # Action descriptor - tells the agent how to use this action 126 | Action( 127 | name="CHATGPT", 128 | description="Use ChatGPT to have a conversation or ask questions.", 129 | parameters=["prompt"], 130 | function=use_chatgpt 131 | ) 132 | ) 133 | 134 | # Initialize the agent's thoughts 135 | agent.think("I've just been created and I can use ChatGPT to interact!") 136 | 137 | # Example 1: Direct action call 138 | output = agent.execute_action("CHATGPT", "Hello! Can you help me learn about artificial intelligence?") 139 | print_agent_output(output) # Add the action name to the output 140 | 141 | # Example 2: Autonomous behavior - agent can now choose to use ChatGPT on its own 142 | output = agent.act() 143 | print_agent_output(output) 144 | 145 | 146 | if __name__ == '__main__': 147 | main() 148 | -------------------------------------------------------------------------------- /agentarium/Environment.py: -------------------------------------------------------------------------------- 1 | # from typing import Dict, List, Tuple, Optional, Any 2 | # from .Agent import Agent 3 | # from .Checkpoint import CheckpointManager 4 | 5 | 6 | # class Environment: 7 | # """ 8 | # A class representing an environment where agents can interact with each other. 9 | 10 | # The Environment class serves as a container and manager for multiple agents, 11 | # providing functionality to: 12 | # - Add and remove agents from the environment 13 | # - Track all agents present in the environment 14 | # - Facilitate interactions between agents 15 | # - Maintain a record of agent relationships and activities 16 | 17 | # This class is the main entry point for creating and managing multi-agent 18 | # simulations or interactive scenarios. 19 | 20 | # Attributes: 21 | # name (str): The name of the environment, used for identification 22 | # agents (Dict[str, Agent]): A dictionary mapping agent IDs to Agent instances 23 | # """ 24 | 25 | # def __init__(self, name: str = "Default Environment"): 26 | # """ 27 | # Initialize a new environment. 28 | 29 | # Creates a new environment instance with the specified name and initializes 30 | # an empty collection of agents. 31 | 32 | # Args: 33 | # name (str, optional): Name of the environment. Defaults to "Default Environment". 34 | # """ 35 | # self.name = name 36 | # self.agents: Dict[str, Agent] = {} 37 | 38 | # def add_agent(self, agent: Agent) -> None: 39 | # """ 40 | # Add an agent to the environment. 41 | 42 | # Registers a new agent in the environment, making it available for 43 | # interactions with other agents. The agent is stored using its unique 44 | # agent_id as the key. 45 | 46 | # Args: 47 | # agent (Agent): The agent instance to add to the environment. 48 | # """ 49 | # self.agents[agent.agent_id] = agent 50 | 51 | # def remove_agent(self, agent_id: str) -> None: 52 | # """ 53 | # Remove an agent from the environment. 54 | 55 | # Removes the specified agent from the environment if it exists. 56 | # This will prevent the agent from participating in future interactions 57 | # within this environment. 58 | 59 | # Args: 60 | # agent_id (str): The unique identifier of the agent to remove. 61 | # """ 62 | # if agent_id in self.agents: 63 | # del self.agents[agent_id] 64 | 65 | # def get_agent(self, agent_id: str) -> Agent: 66 | # """ 67 | # Retrieve an agent from the environment by their ID. 68 | 69 | # Args: 70 | # agent_id (str): The unique identifier of the agent to retrieve. 71 | 72 | # Returns: 73 | # Agent: The requested agent instance if found, None otherwise. 74 | # """ 75 | # return self.agents.get(agent_id) 76 | 77 | # def list_agents(self) -> List[str]: 78 | # """ 79 | # Get a list of all agent IDs currently in the environment. 80 | 81 | # This method provides a way to enumerate all agents currently 82 | # registered in the environment. 83 | 84 | # Returns: 85 | # List[str]: A list containing the unique identifiers of all registered agents. 86 | # """ 87 | # return list(self.agents.keys()) 88 | 89 | # def record_interaction(self, sender_id: str, receiver_id: str, message: str) -> None: 90 | # """ 91 | # Record an interaction between two agents in the environment. 92 | 93 | # This method delegates the interaction recording to both the sender and 94 | # receiver agents, ensuring that both parties maintain a record of their 95 | # communication. 96 | 97 | # Args: 98 | # sender_id (str): The unique identifier of the agent initiating the interaction. 99 | # receiver_id (str): The unique identifier of the agent receiving the interaction. 100 | # message (str): The content of the interaction. 101 | 102 | # Raises: 103 | # ValueError: If either the sender or receiver agent is not found in the environment. 104 | # """ 105 | # sender = self.get_agent(sender_id) 106 | # receiver = self.get_agent(receiver_id) 107 | 108 | # if not sender or not receiver: 109 | # raise ValueError(f"Agent {sender_id} or {receiver_id} not found in the environment") 110 | 111 | # sender.record_interaction(sender_id, receiver_id, message) 112 | # receiver.record_interaction(sender_id, receiver_id, message) 113 | 114 | # def save_checkpoint(self, name: Optional[str] = None) -> str: 115 | # """ 116 | # Save the current state of the environment to a checkpoint. 117 | 118 | # Args: 119 | # name (str, optional): Custom name for the checkpoint 120 | 121 | # Returns: 122 | # str: Path to the saved checkpoint 123 | # """ 124 | # checkpoint = CheckpointManager() 125 | # return checkpoint.save(self, name) 126 | 127 | # def load_checkpoint(self, checkpoint_path: str) -> None: 128 | # """ 129 | # Load the environment state from a checkpoint. 130 | 131 | # Args: 132 | # checkpoint_path (str): Path to the checkpoint directory 133 | # """ 134 | # checkpoint = CheckpointManager() 135 | # loaded_env = checkpoint.load(checkpoint_path) 136 | # self.name = loaded_env.name 137 | # self.agents = loaded_env.agents 138 | 139 | # @staticmethod 140 | # def list_checkpoints() -> Dict[str, Any]: 141 | # """ 142 | # List all available checkpoints. 143 | 144 | # Returns: 145 | # Dict[str, Any]: Dictionary of checkpoint names and their metadata 146 | # """ 147 | # checkpoint = CheckpointManager() 148 | # return checkpoint.list_checkpoints() 149 | 150 | # def __str__(self) -> str: 151 | # """ 152 | # Get a string representation of the environment. 153 | 154 | # Provides a human-readable description of the environment, including 155 | # its name and the number of agents it contains. 156 | 157 | # Returns: 158 | # str: A formatted string describing the environment. 159 | # """ 160 | # return f"Environment: {self.name}\nAgents: {len(self.agents)}" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /agentarium/Agent.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import re 4 | import logging 5 | import aisuite as ai 6 | 7 | from copy import deepcopy 8 | from .actions.Action import Action 9 | from typing import List, Dict, Any 10 | from faker import Faker 11 | from .Interaction import Interaction 12 | from .AgentInteractionManager import AgentInteractionManager 13 | from .Config import Config 14 | from .utils import cache_w_checkpoint_manager 15 | from .actions.default_actions import default_actions 16 | from .constant import DefaultValue 17 | 18 | 19 | faker = Faker() 20 | config = Config() 21 | llm_client = ai.Client({**config.aisuite}) 22 | 23 | 24 | class Agent: 25 | """ 26 | A class representing an autonomous agent in the environment. 27 | 28 | The Agent class is the core component of the Agentarium system, representing 29 | an individual entity capable of: 30 | - Maintaining its own identity and characteristics 31 | - Interacting with other agents through messages 32 | - Making autonomous decisions based on its state and interactions 33 | - Generating responses using natural language models 34 | 35 | Each agent has a unique identifier and a set of characteristics that define 36 | its personality and behavior. These characteristics can be either provided 37 | during initialization or generated automatically. 38 | 39 | Attributes: 40 | agent_id (str): Unique identifier for the agent 41 | agent_informations (dict): Dictionary containing agent characteristics 42 | including name, age, gender, occupation, location, and bio 43 | _interaction_manager (AgentInteractionManager): Singleton instance managing 44 | all agent interactions 45 | """ 46 | 47 | _interaction_manager = AgentInteractionManager() 48 | _allow_init = False 49 | 50 | _default_generate_agent_prompt = """You're goal is to generate the bio of a fictional person. 51 | Make this bio as realistic and as detailed as possible. 52 | You may be given information about the person to generate a bio for, if so, use that information to generate a bio. 53 | If you are not given any information about the person, generate a bio for a random person. 54 | 55 | You must generate the bio in the following format: 56 | Bio: [Bio of the person] 57 | """ 58 | 59 | _default_self_introduction_prompt = """ 60 | Informations about yourself: 61 | {agent_informations} 62 | 63 | Your interactions: 64 | {interactions} 65 | """ 66 | 67 | _default_act_prompt = """{self_introduction} 68 | 69 | Given the above information, think about what you should do next. 70 | 71 | The following are the possible actions you can take: 72 | {actions} 73 | 74 | Write in the following format: 75 | 76 | {{YOUR_THOUGHTS}} 77 | 78 | 79 | 80 | [{{One of the following actions: {list_of_actions}}}] 81 | 82 | 83 | Don't forget to close each tag that you open. 84 | """ 85 | 86 | def __init__(self, **kwargs): 87 | """ 88 | Initialize an agent with given or generated characteristics. 89 | This method should not be called directly - use create_agent() instead. 90 | """ 91 | 92 | if not Agent._allow_init: 93 | raise RuntimeError("Agent instances should be created using Agent.create_agent()") 94 | 95 | if "agent_id" in kwargs: 96 | self.agent_id = kwargs.pop("agent_id") 97 | 98 | self.agent_id = self.agent_id if self.agent_id!= DefaultValue.NOT_PROVIDED else faker.uuid4() 99 | self.agent_informations: dict = kwargs or {} 100 | 101 | if "gender" not in kwargs or kwargs["gender"] == DefaultValue.NOT_PROVIDED: 102 | self.agent_informations["gender"] = faker.random_element(elements=["male", "female"]) 103 | 104 | if "name" not in kwargs or kwargs["name"] == DefaultValue.NOT_PROVIDED: 105 | self.agent_informations["name"] = getattr(faker, f"name_{self.agent_informations['gender']}")() 106 | 107 | if "age" not in kwargs or kwargs["age"] == DefaultValue.NOT_PROVIDED: 108 | self.agent_informations["age"] = faker.random_int(18, 80) 109 | 110 | if "occupation" not in kwargs or kwargs["occupation"] == DefaultValue.NOT_PROVIDED: 111 | self.agent_informations["occupation"] = faker.job() 112 | 113 | if "location" not in kwargs or kwargs["location"] == DefaultValue.NOT_PROVIDED: 114 | self.agent_informations["location"] = faker.city() 115 | 116 | if "bio" not in kwargs or kwargs["bio"] == DefaultValue.NOT_PROVIDED: 117 | self.agent_informations["bio"] = Agent._generate_agent_bio(self.agent_informations) 118 | 119 | self._interaction_manager.register_agent(self) 120 | 121 | self._self_introduction_prompt = deepcopy(Agent._default_self_introduction_prompt) 122 | self._act_prompt = deepcopy(Agent._default_act_prompt) 123 | 124 | self._actions = deepcopy(default_actions) if kwargs["actions"] == DefaultValue.NOT_PROVIDED else kwargs["actions"] 125 | 126 | self.storage = {} # Useful for storing data between actions. Note: not used by the agentarium system. 127 | 128 | def __setstate__(self, state): 129 | self.__dict__.update(state) # default __setstate__ 130 | self._interaction_manager.register_agent(self) # add the agent to the interaction manager 131 | 132 | @staticmethod 133 | @cache_w_checkpoint_manager 134 | def create_agent( 135 | agent_id: str = DefaultValue.NOT_PROVIDED, 136 | gender: str = DefaultValue.NOT_PROVIDED, 137 | name: str = DefaultValue.NOT_PROVIDED, 138 | age: int = DefaultValue.NOT_PROVIDED, 139 | occupation: str = DefaultValue.NOT_PROVIDED, 140 | location: str = DefaultValue.NOT_PROVIDED, 141 | bio: str = DefaultValue.NOT_PROVIDED, 142 | actions: dict = DefaultValue.NOT_PROVIDED, 143 | **kwargs, 144 | ) -> Agent: 145 | """ 146 | Initialize an agent with given or generated characteristics. 147 | 148 | Creates a new agent instance with a unique identifier and a set of 149 | characteristics. If specific characteristics are not provided, they 150 | are automatically generated to create a complete and realistic agent 151 | profile. 152 | 153 | The following characteristics are handled: 154 | - Gender (male/female) 155 | - Name (appropriate for the gender) 156 | - Age (between 18 and 80) 157 | - Occupation (randomly selected job) 158 | - Location (randomly selected city) 159 | - Bio (generated based on other characteristics) 160 | - Actions, by default "talk" and "think" actions are available (default actions or custom actions) 161 | Args: 162 | **kwargs: Dictionary of agent characteristics to use instead of 163 | generating them. Any characteristic not provided will be 164 | automatically generated. 165 | """ 166 | try: 167 | Agent._allow_init = True 168 | return Agent( 169 | agent_id=agent_id, 170 | gender=gender, 171 | name=name, 172 | age=age, 173 | occupation=occupation, 174 | location=location, 175 | bio=bio, 176 | actions=actions, 177 | **kwargs 178 | ) 179 | finally: 180 | Agent._allow_init = False 181 | 182 | @property 183 | def name(self) -> str: 184 | """ 185 | Get the agent's name. 186 | 187 | Returns: 188 | str: The name of the agent. 189 | """ 190 | return self.agent_informations["name"] 191 | 192 | @property 193 | def gender(self) -> str: 194 | """ 195 | Get the agent's gender. 196 | 197 | Returns: 198 | str: The gender of the agent. 199 | """ 200 | return self.agent_informations["gender"] 201 | 202 | @property 203 | def age(self) -> int: 204 | """ 205 | Get the agent's age. 206 | 207 | Returns: 208 | int: The age of the agent. 209 | """ 210 | return self.agent_informations["age"] 211 | 212 | @property 213 | def occupation(self) -> str: 214 | """ 215 | Get the agent's occupation. 216 | 217 | Returns: 218 | str: The occupation of the agent. 219 | """ 220 | return self.agent_informations["occupation"] 221 | 222 | @property 223 | def location(self) -> str: 224 | """ 225 | Get the agent's location. 226 | 227 | Returns: 228 | str: The location of the agent. 229 | """ 230 | return self.agent_informations["location"] 231 | 232 | @property 233 | def bio(self) -> str: 234 | """ 235 | Get the agent's biography. 236 | 237 | Returns: 238 | str: The biography of the agent. 239 | """ 240 | return self.agent_informations["bio"] 241 | 242 | @staticmethod 243 | def _generate_prompt_to_generate_bio(**kwargs) -> str: 244 | """ 245 | Generate a prompt for creating an agent's biography. 246 | 247 | Creates a prompt that will be used by the language model to generate 248 | a realistic biography for the agent. If characteristics are provided, 249 | they are incorporated into the prompt to ensure the generated bio 250 | is consistent with the agent's existing traits. 251 | 252 | Args: 253 | **kwargs: Dictionary of agent characteristics to incorporate 254 | into the biography generation prompt. 255 | 256 | Returns: 257 | str: A formatted prompt string for biography generation. 258 | """ 259 | kwargs = {k: v for k, v in kwargs.items() if v is not None} 260 | 261 | if not kwargs: 262 | return Agent._default_generate_agent_prompt 263 | 264 | prompt = Agent._default_generate_agent_prompt 265 | prompt += "\nInformation about the person to generate a bio for:" 266 | 267 | for key, value in kwargs.items(): 268 | prompt += f"\n{key}: {value}" 269 | 270 | return prompt 271 | 272 | @staticmethod 273 | def _generate_agent_bio(agent_informations: dict) -> str: 274 | """ 275 | Generate a biography for an agent using a language model. 276 | 277 | Uses the OpenAI API to generate a realistic and detailed biography 278 | based on the agent's characteristics. The biography is generated 279 | to be consistent with any existing information about the agent. 280 | 281 | Args: 282 | agent_informations (dict): Dictionary of agent characteristics 283 | to use in generating the biography. 284 | 285 | Returns: 286 | str: A generated biography for the agent. 287 | """ 288 | prompt = Agent._generate_prompt_to_generate_bio(**agent_informations) 289 | 290 | response = llm_client.chat.completions.create( 291 | model=f"{config.llm_provider}:{config.llm_model}", 292 | messages=[ 293 | {"role": "system", "content": "You are a helpful assistant."}, 294 | {"role": "user", "content": prompt}, 295 | ], 296 | temperature=0.8, 297 | ) 298 | 299 | return response.choices[0].message.content 300 | 301 | @cache_w_checkpoint_manager 302 | def act(self) -> str: 303 | """ 304 | Generate and execute the agent's next action based on their current state. 305 | 306 | This method: 307 | 1. Generates a self-introduction based on agent's characteristics and history 308 | 2. Creates a prompt combining the self-introduction and available actions 309 | 3. Uses the language model to decide the next action 310 | 4. Parses and executes the chosen action 311 | 312 | The agent's decision is based on: 313 | - Their characteristics (personality, role, etc.) 314 | - Their interaction history 315 | - Available actions in their action space 316 | 317 | Returns: 318 | Dict[str, Any]: A dictionary containing the action results, including: 319 | - 'action': The name of the executed action 320 | - Additional keys depending on the specific action executed 321 | 322 | Raises: 323 | RuntimeError: If no actions are available or if the chosen action is invalid 324 | """ 325 | 326 | if len(self._actions) == 0: 327 | raise RuntimeError("No actions available for the agent to perform") 328 | 329 | self_introduction = self._self_introduction_prompt.format( 330 | agent_informations=self.agent_informations, 331 | interactions=self.get_interactions(), 332 | ) 333 | 334 | prompt = self._act_prompt.format( 335 | self_introduction=self_introduction, 336 | actions="\n".join([f"{action.get_format()}: {action.description if action.description else ''}" for action in self._actions.values()]), 337 | list_of_actions=list(self._actions.keys()), 338 | ) 339 | 340 | response = llm_client.chat.completions.create( 341 | model=f"{config.llm_provider}:{config.llm_model}", 342 | messages=[ 343 | {"role": "user", "content": prompt}, 344 | ], 345 | temperature=0.4, 346 | ) 347 | 348 | try: 349 | regex_result = re.search(r"(.*?)", response.choices[0].message.content, re.DOTALL).group(1).strip() 350 | except AttributeError as e: 351 | logging.error(f"Received a response without any action: {response.choices[0].message.content=}") 352 | raise e 353 | 354 | action_name, *args = [value.replace("[", "").replace("]", "").strip() for value in regex_result.split("]")] 355 | 356 | if action_name not in self._actions: 357 | logging.error(f"Received an invalid action: '{action_name=}' in the output: {response.choices[0].message.content}") 358 | raise RuntimeError(f"Invalid action received: '{action_name}'.") 359 | 360 | return self._actions[action_name].function(*args, agent=self) # we pass the agent to the action function to allow it to use the agent's methods 361 | 362 | def dump(self) -> dict: 363 | """ 364 | Dump the agent's state to a dictionary. 365 | """ 366 | 367 | return { 368 | "agent_id": self.agent_id, 369 | "agent_informations": self.agent_informations, 370 | "interactions": [interaction.dump() for interaction in self._interaction_manager.get_agent_interactions(self)], 371 | } 372 | 373 | def __str__(self) -> str: 374 | """ 375 | Get a string representation of the agent. 376 | 377 | Returns: 378 | str: A formatted string containing all the agent's characteristics. 379 | """ 380 | return "\n".join([f"{key.capitalize()}: {value}" for key, value in self.agent_informations.items()]) 381 | 382 | def __repr__(self) -> str: 383 | """ 384 | Get a string representation of the agent, same as __str__. 385 | 386 | Returns: 387 | str: A formatted string containing all the agent's characteristics. 388 | """ 389 | return Agent.__str__(self) 390 | 391 | def get_interactions(self) -> List[Interaction]: 392 | """ 393 | Retrieve all interactions involving this agent. 394 | 395 | Returns: 396 | List[Interaction]: A list of all interactions where this agent 397 | was either the sender or receiver. 398 | """ 399 | return self._interaction_manager.get_agent_interactions(self) 400 | 401 | def ask(self, message: str) -> str: 402 | """ 403 | Ask the agent a question and receive a contextually aware response. 404 | 405 | The agent considers its characteristics and interaction history when formulating 406 | the response, maintaining consistency with its persona. 407 | 408 | Note: The agent will not save the question nor the response in its interaction history. 409 | 410 | Args: 411 | message (str): The question to ask the agent. 412 | 413 | Returns: 414 | str: The agent's response to the question. 415 | """ 416 | 417 | prompt = self._self_introduction_prompt.format( 418 | agent_informations=self.agent_informations, 419 | interactions=self.get_interactions(), 420 | ) 421 | 422 | prompt += f"\nYou are asked the following question: {message}. Answer the question as best as you can." 423 | 424 | response = llm_client.chat.completions.create( 425 | model=f"{config.llm_provider}:{config.llm_model}", 426 | messages=[ 427 | {"role": "user", "content": prompt}, 428 | ], 429 | temperature=0.4, 430 | ) 431 | 432 | return response.choices[0].message.content 433 | 434 | def add_action(self, action: Action) -> None: 435 | """ 436 | Add a new action to the agent's capabilities. 437 | 438 | This method allows extending an agent's behavior by adding custom actions. Each action consists of: 439 | 1. A descriptor that tells the agent how to use the action 440 | 2. A function that implements the action's behavior 441 | 442 | Args: 443 | action_descriptor (dict[str, str]): A dictionary describing the action with these required keys: 444 | - "format": Format string showing action syntax. Must start with [ACTION_NAME] in caps, 445 | followed by parameter placeholders in brackets. Example: "[CHATGPT][message]" 446 | - "prompt": Human-readable description of what the action does 447 | - "example": Concrete example showing how to use the action 448 | action_function (Callable[[Agent, str], str | dict]): Function implementing the action. 449 | - First parameter must be the agent instance 450 | - Remaining parameters should match the format string 451 | - Can return either a string or a dictionary 452 | - If returning a dict, the 'action' key is reserved and will be overwritten 453 | - If returning a non-dict value, it will be stored under the 'output' key 454 | 455 | Raises: 456 | RuntimeError: If the action_descriptor is missing required keys or if the action 457 | name conflicts with an existing action 458 | 459 | Example: 460 | ```python 461 | # Adding a ChatGPT integration action 462 | agent.add_action( 463 | action_descriptor={ 464 | "format": "[CHATGPT][Your message here]", 465 | "prompt": "Use ChatGPT to have a conversation or ask questions.", 466 | "example": "[CHATGPT][What's the weather like today?]" 467 | }, 468 | action_function=use_chatgpt 469 | ) 470 | 471 | # The action function could look like: 472 | def use_chatgpt(agent: Agent, message: str) -> dict: 473 | response = call_chatgpt_api(message) 474 | return { 475 | "message": message, 476 | "response": response 477 | } # Will be automatically formatted to include "action": "CHATGPT" 478 | ``` 479 | 480 | Notes: 481 | - The action name is extracted from the first bracket pair in the format string 482 | - The action function's output will be automatically standardized to include the action name 483 | - Any 'action' key in the function's output dictionary will be overwritten 484 | """ 485 | 486 | if action.name in self._actions: 487 | raise RuntimeError(f"Invalid action: {action.name}, action already exists in the agent's action space") 488 | 489 | self._actions[action.name] = action 490 | 491 | def execute_action(self, action_name: str, *args, **kwargs) -> Dict[str, Any]: 492 | """ 493 | Manually execute an action by name. 494 | 495 | Args: 496 | name: Name of the action to execute 497 | *args, **kwargs: Arguments to pass to the action function 498 | 499 | Returns: 500 | Dict containing the action results 501 | """ 502 | 503 | if action_name not in self._actions: 504 | raise RuntimeError(f"Invalid action: {action_name}, action does not exist in the agent's action space") 505 | 506 | return self._actions[action_name].function(*args, **kwargs, agent=self) 507 | 508 | def remove_action(self, action_name: str) -> None: 509 | """ 510 | Remove an action from the agent's action space. 511 | """ 512 | if action_name not in self._actions: 513 | raise RuntimeError(f"Invalid action: {action_name}, action does not exist in the agent's action space") 514 | 515 | del self._actions[action_name] 516 | 517 | def talk_to(self, agent: Agent | list[Agent], message: str) -> Dict[str, Any]: 518 | """ 519 | Send a message from one agent to another and record the interaction. 520 | """ 521 | 522 | if "talk" not in self._actions: 523 | # Did you really removed the default "talk" action and expect the talk_to method to work? 524 | raise RuntimeError("Talk action not found in the agent's action space.") 525 | 526 | if isinstance(agent, Agent): 527 | return self.execute_action("talk", agent.agent_id, message) 528 | else: 529 | return self.execute_action("talk", ','.join([agent.agent_id for agent in agent]), message) 530 | 531 | def think(self, message: str) -> None: 532 | """ 533 | Make the agent think about a message. 534 | """ 535 | 536 | if "think" not in self._actions: 537 | raise RuntimeError("Think action not found in the agent's action space.") 538 | 539 | self.execute_action("think", message) 540 | 541 | def reset(self) -> None: 542 | """ 543 | Reset the agent's state. 544 | """ 545 | self._interaction_manager.reset_agent(self) 546 | self.storage = {} 547 | 548 | 549 | if __name__ == "__main__": 550 | 551 | interaction = Interaction( 552 | sender=Agent.create_agent(name="Alice", bio="Alice is a software engineer."), 553 | receiver=Agent.create_agent(name="Bob", bio="Bob is a data scientist."), 554 | message="Hello Bob! I heard you're working on some interesting data science projects." 555 | ) 556 | 557 | print(interaction) 558 | --------------------------------------------------------------------------------