From cb1779d9bd4e0017d429547645d2ffe2e8cddfea Mon Sep 17 00:00:00 2001 From: MichaelDecent Date: Fri, 20 Dec 2024 13:26:00 +0100 Subject: [PATCH 1/7] swarm - Refactor agent APIs: move to deprecated directory, add new AgentAPI and IAgentAPI implementations --- pkgs/core/swarmauri_core/ComponentBase.py | 3 + .../IAgentCommands.py | 0 .../IAgentRouterCRUD.py | 0 .../agent_apis(deprecated)/__init__.py | 4 + .../swarmauri_core/agent_apis/IAgentAPI.py | 15 ++++ .../swarmauri_core/agent_apis/__init__.py | 4 - .../swarmauri/swarmauri/agent_apis/__init_.py | 0 .../swarmauri/agent_apis/base/AgentAPIBase.py | 31 +++++++ .../swarmauri/agent_apis/base/__init__.py | 0 .../swarmauri/agent_apis/concrete/AgentAPI.py | 10 +++ .../swarmauri/agent_apis/concrete/__init__.py | 0 .../concrete/MaxSystemContextConversation.py | 40 ++++++--- .../factories/concrete/AgentFactory.py | 5 +- .../swarmauri/utils/_get_subclasses.py | 85 +++++-------------- .../unit/agent_apis/AgentAPI_unit_test.py | 80 +++++++++++++++++ .../unit/factories/AgentFactory_unit_test.py | 23 +++-- .../tests/unit/factories/Factory_unit_test.py | 5 +- 17 files changed, 210 insertions(+), 95 deletions(-) rename pkgs/core/swarmauri_core/{agent_apis => agent_apis(deprecated)}/IAgentCommands.py (100%) rename pkgs/core/swarmauri_core/{agent_apis => agent_apis(deprecated)}/IAgentRouterCRUD.py (100%) create mode 100644 pkgs/core/swarmauri_core/agent_apis(deprecated)/__init__.py create mode 100644 pkgs/core/swarmauri_core/agent_apis/IAgentAPI.py create mode 100644 pkgs/swarmauri/swarmauri/agent_apis/__init_.py create mode 100644 pkgs/swarmauri/swarmauri/agent_apis/base/AgentAPIBase.py create mode 100644 pkgs/swarmauri/swarmauri/agent_apis/base/__init__.py create mode 100644 pkgs/swarmauri/swarmauri/agent_apis/concrete/AgentAPI.py create mode 100644 pkgs/swarmauri/swarmauri/agent_apis/concrete/__init__.py create mode 100644 pkgs/swarmauri/tests/unit/agent_apis/AgentAPI_unit_test.py diff --git a/pkgs/core/swarmauri_core/ComponentBase.py b/pkgs/core/swarmauri_core/ComponentBase.py index f4cc811da..f25bf5f38 100644 --- a/pkgs/core/swarmauri_core/ComponentBase.py +++ b/pkgs/core/swarmauri_core/ComponentBase.py @@ -64,10 +64,13 @@ class ResourceTypes(Enum): CONTROL_PANEL = "ControlPanel" TASK_MGT_STRATEGY = "TaskMgtStrategy" MAS = "Mas" + AGENT_API = "AgentAPI" + def generate_id() -> str: return str(uuid4()) + class ComponentBase(BaseModel): name: Optional[str] = None id: str = Field(default_factory=generate_id) diff --git a/pkgs/core/swarmauri_core/agent_apis/IAgentCommands.py b/pkgs/core/swarmauri_core/agent_apis(deprecated)/IAgentCommands.py similarity index 100% rename from pkgs/core/swarmauri_core/agent_apis/IAgentCommands.py rename to pkgs/core/swarmauri_core/agent_apis(deprecated)/IAgentCommands.py diff --git a/pkgs/core/swarmauri_core/agent_apis/IAgentRouterCRUD.py b/pkgs/core/swarmauri_core/agent_apis(deprecated)/IAgentRouterCRUD.py similarity index 100% rename from pkgs/core/swarmauri_core/agent_apis/IAgentRouterCRUD.py rename to pkgs/core/swarmauri_core/agent_apis(deprecated)/IAgentRouterCRUD.py diff --git a/pkgs/core/swarmauri_core/agent_apis(deprecated)/__init__.py b/pkgs/core/swarmauri_core/agent_apis(deprecated)/__init__.py new file mode 100644 index 000000000..27f8d7f14 --- /dev/null +++ b/pkgs/core/swarmauri_core/agent_apis(deprecated)/__init__.py @@ -0,0 +1,4 @@ +from .IAgentCommands import IAgentCommands +from .IAgentRouterCRUD import IAgentRouterCRUD + +__all__ = ['IAgentCommands', 'IAgentRouterCRUD'] \ No newline at end of file diff --git a/pkgs/core/swarmauri_core/agent_apis/IAgentAPI.py b/pkgs/core/swarmauri_core/agent_apis/IAgentAPI.py new file mode 100644 index 000000000..79a90d748 --- /dev/null +++ b/pkgs/core/swarmauri_core/agent_apis/IAgentAPI.py @@ -0,0 +1,15 @@ +from abc import ABC, abstractmethod +from typing import Any, Coroutine, Dict + + +class IAgentAPI(ABC): + + @abstractmethod + def invoke(self, agent_id: str, **kwargs: Dict[str, Any]) -> Any: + """Invoke an agent synchronously.""" + pass + + @abstractmethod + async def ainvoke(self, agent_id: str, **kwargs: Dict[str, Any]) -> Any: + """Invoke an agent asynchronously.""" + pass diff --git a/pkgs/core/swarmauri_core/agent_apis/__init__.py b/pkgs/core/swarmauri_core/agent_apis/__init__.py index 27f8d7f14..e69de29bb 100644 --- a/pkgs/core/swarmauri_core/agent_apis/__init__.py +++ b/pkgs/core/swarmauri_core/agent_apis/__init__.py @@ -1,4 +0,0 @@ -from .IAgentCommands import IAgentCommands -from .IAgentRouterCRUD import IAgentRouterCRUD - -__all__ = ['IAgentCommands', 'IAgentRouterCRUD'] \ No newline at end of file diff --git a/pkgs/swarmauri/swarmauri/agent_apis/__init_.py b/pkgs/swarmauri/swarmauri/agent_apis/__init_.py new file mode 100644 index 000000000..e69de29bb diff --git a/pkgs/swarmauri/swarmauri/agent_apis/base/AgentAPIBase.py b/pkgs/swarmauri/swarmauri/agent_apis/base/AgentAPIBase.py new file mode 100644 index 000000000..8c2cb8ad4 --- /dev/null +++ b/pkgs/swarmauri/swarmauri/agent_apis/base/AgentAPIBase.py @@ -0,0 +1,31 @@ +from typing import Any, Dict, Literal, Optional + +from pydantic import ConfigDict, Field +from swarmauri_core.ComponentBase import ComponentBase, ResourceTypes +from swarmauri_core.agent_apis.IAgentAPI import IAgentAPI +from swarmauri.service_registries.concrete.ServiceRegistry import ServiceRegistry + + +class AgentAPIBase(IAgentAPI, ComponentBase): + + resource: Optional[str] = Field(default=ResourceTypes.AGENT_API.value, frozen=True) + model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True) + type: Literal["AgentAPIBase"] = "AgentAPIBase" + + agent_registry: ServiceRegistry + + def invoke(self, agent_id: str, **kwargs: Dict[str, Any]) -> Any: + agent = self.agent_registry.get_service(agent_id) + if not agent: + raise ValueError(f"Agent with ID {agent_id} not found.") + return agent.exec(**kwargs) + + async def ainvoke(self, agent_id: str, **kwargs: Dict[str, Any]) -> Any: + agent = self.agent_registry.get_service(agent_id) + if not agent: + raise ValueError(f"Agent with ID {agent_id} not found.") + if not hasattr(agent, "aexec"): + raise NotImplementedError( + f"Agent with ID {agent_id} does not support async execution." + ) + return await agent.aexec(**kwargs) diff --git a/pkgs/swarmauri/swarmauri/agent_apis/base/__init__.py b/pkgs/swarmauri/swarmauri/agent_apis/base/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pkgs/swarmauri/swarmauri/agent_apis/concrete/AgentAPI.py b/pkgs/swarmauri/swarmauri/agent_apis/concrete/AgentAPI.py new file mode 100644 index 000000000..312f9156a --- /dev/null +++ b/pkgs/swarmauri/swarmauri/agent_apis/concrete/AgentAPI.py @@ -0,0 +1,10 @@ +from typing import Literal +from swarmauri.agent_apis.base.AgentAPIBase import AgentAPIBase + + +class AgentAPI(AgentAPIBase): + """ + Concrete implementation of the AgentAPIBase. + """ + + type: Literal["AgentAPI"] = "AgentAPI" diff --git a/pkgs/swarmauri/swarmauri/agent_apis/concrete/__init__.py b/pkgs/swarmauri/swarmauri/agent_apis/concrete/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pkgs/swarmauri/swarmauri/conversations/concrete/MaxSystemContextConversation.py b/pkgs/swarmauri/swarmauri/conversations/concrete/MaxSystemContextConversation.py index 723f4b49a..4fed78122 100644 --- a/pkgs/swarmauri/swarmauri/conversations/concrete/MaxSystemContextConversation.py +++ b/pkgs/swarmauri/swarmauri/conversations/concrete/MaxSystemContextConversation.py @@ -3,22 +3,29 @@ from swarmauri_core.messages.IMessage import IMessage from swarmauri_core.conversations.IMaxSize import IMaxSize from swarmauri.conversations.base.ConversationBase import ConversationBase -from swarmauri.conversations.base.ConversationSystemContextMixin import ConversationSystemContextMixin -from swarmauri.messages.concrete import SystemMessage, AgentMessage, HumanMessage +from swarmauri.conversations.base.ConversationSystemContextMixin import ( + ConversationSystemContextMixin, +) +from swarmauri.messages.concrete.SystemMessage import SystemMessage +from swarmauri.messages.concrete.AgentMessage import AgentMessage +from swarmauri.messages.concrete.HumanMessage import HumanMessage from swarmauri.exceptions.concrete import IndexErrorWithContext -class MaxSystemContextConversation(IMaxSize, ConversationSystemContextMixin, ConversationBase): + +class MaxSystemContextConversation( + IMaxSize, ConversationSystemContextMixin, ConversationBase +): system_context: Optional[SystemMessage] = SystemMessage(content="") max_size: int = Field(default=2, gt=1) - model_config = ConfigDict(extra='forbid', arbitrary_types_allowed=True) - type: Literal['MaxSystemContextConversation'] = 'MaxSystemContextConversation' - - @field_validator('system_context', mode='before') + model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True) + type: Literal["MaxSystemContextConversation"] = "MaxSystemContextConversation" + + @field_validator("system_context", mode="before") def set_system_context(cls, value: Union[str, SystemMessage]) -> SystemMessage: if isinstance(value, str): return SystemMessage(content=value) return value - + @property def history(self) -> List[IMessage]: """ @@ -41,11 +48,16 @@ def history(self) -> List[IMessage]: # Build history from the first 'user' message ensuring alternating roles. res.append(self.system_context) alternating = True - count = 0 + count = 0 for message in self._history[user_start_index:]: - if count >= self.max_size: # max size + if count >= self.max_size: # max size break - if alternating and isinstance(message, HumanMessage) or not alternating and isinstance(message, AgentMessage): + if ( + alternating + and isinstance(message, HumanMessage) + or not alternating + and isinstance(message, AgentMessage) + ): res.append(message) alternating = not alternating count += 1 @@ -63,13 +75,15 @@ def add_message(self, message: IMessage): Adds a message to the conversation history and ensures history does not exceed the max size. """ if isinstance(message, SystemMessage): - raise ValueError(f"System context cannot be set through this method on {self.__class_name__}.") + raise ValueError( + f"System context cannot be set through this method on {self.__class_name__}." + ) elif isinstance(message, IMessage): self._history.append(message) else: raise ValueError(f"Must use a subclass of IMessage") self._enforce_max_size_limit() - + def _enforce_max_size_limit(self): """ Remove messages from the beginning of the conversation history if the limit is exceeded. diff --git a/pkgs/swarmauri/swarmauri/factories/concrete/AgentFactory.py b/pkgs/swarmauri/swarmauri/factories/concrete/AgentFactory.py index 0649ca928..6cf722eaf 100644 --- a/pkgs/swarmauri/swarmauri/factories/concrete/AgentFactory.py +++ b/pkgs/swarmauri/swarmauri/factories/concrete/AgentFactory.py @@ -1,5 +1,7 @@ +import logging from typing import Any, Callable, Dict, Literal from swarmauri.factories.base.FactoryBase import FactoryBase +from swarmauri.utils._get_subclasses import get_classes_from_module class AgentFactory(FactoryBase): @@ -8,7 +10,7 @@ class AgentFactory(FactoryBase): """ type: Literal["AgentFactory"] = "AgentFactory" - _registry: Dict[str, Callable] = {} + _registry: Dict[str, Callable] = get_classes_from_module("Agent") def register(self, type: str, resource_class: Callable) -> None: """ @@ -22,6 +24,7 @@ def create(self, type: str, *args: Any, **kwargs: Any) -> Any: """ Create an instance of the class associated with the given type name. """ + logging.info(self._registry) if type not in self._registry: raise ValueError(f"Type '{type}' is not registered.") diff --git a/pkgs/swarmauri/swarmauri/utils/_get_subclasses.py b/pkgs/swarmauri/swarmauri/utils/_get_subclasses.py index adf100601..4f105eb75 100644 --- a/pkgs/swarmauri/swarmauri/utils/_get_subclasses.py +++ b/pkgs/swarmauri/swarmauri/utils/_get_subclasses.py @@ -1,72 +1,25 @@ -import importlib -import re +from swarmauri.utils.LazyLoader import LazyLoader -def get_classes_from_module(module_name: str): - """ - Dynamically imports a module and retrieves a dictionary of class names and their corresponding class objects. +def get_classes_from_module(module): + import inspect - :param module_name: The name of the module (e.g., "parsers", "agent"). - :return: A dictionary with class names as keys and class objects as values. - """ - # Convert module name to lowercase to ensure consistency - module_name_lower = module_name.lower() + classes = {} + for name, obj in inspect.getmembers(module): + if isinstance(obj, LazyLoader): + obj = obj._load_class() # Load the class from LazyLoader + if inspect.isclass(obj): + classes[name] = obj + return classes - # Construct the full module path dynamically - full_module_path = f"swarmauri.{module_name_lower}s.concrete" - try: - # Import the module dynamically - module = importlib.import_module(full_module_path) +def get_class_from_module(module, class_name): + import inspect - # Get the list of class names from __all__ - class_names = getattr(module, "__all__", []) - - # Create a dictionary with class names and their corresponding class objects - classes_dict = { - class_name: getattr(module, class_name) for class_name in class_names - } - - return classes_dict - except ImportError as e: - print(f"Error importing module {full_module_path}: {e}") - raise ModuleNotFoundError(f"Resource '{module_name}' is not registered.") - except AttributeError as e: - print(f"Error accessing class in {full_module_path}: {e}") - raise e - - -def get_class_from_module(module_name: str, class_name: str): - """ - Dynamically imports a module and retrieves the class name of the module. - - :param module_name: The name of the module (e.g., "parsers", "agent"). - :return: The class name of the module. - """ - # Convert module name to lowercase to ensure consistency - module_name_lower = module_name.lower() - - # Construct the full module path dynamically - full_module_path = f"swarmauri.{module_name_lower}s.concrete" - - try: - # Import the module dynamically - module = importlib.import_module(full_module_path) - - # Get the list of class names from __all__ - class_names = getattr(module, "__all__", []) - - if not class_names: - raise AttributeError(f"No classes found in module {full_module_path}") - - for cls_name in class_names: - if cls_name == class_name: - return getattr(module, class_name) - return None - - except ImportError as e: - print(f"Error importing module {full_module_path}: {e}") - raise ModuleNotFoundError(f"Resource '{module_name}' is not found.") - except AttributeError as e: - print(f"Error accessing class in {full_module_path}: {e}") - raise e + if hasattr(module, class_name): + obj = getattr(module, class_name) + if isinstance(obj, LazyLoader): + obj = obj._load_class() + if inspect.isclass(obj): + return obj + return None diff --git a/pkgs/swarmauri/tests/unit/agent_apis/AgentAPI_unit_test.py b/pkgs/swarmauri/tests/unit/agent_apis/AgentAPI_unit_test.py new file mode 100644 index 000000000..810a66da1 --- /dev/null +++ b/pkgs/swarmauri/tests/unit/agent_apis/AgentAPI_unit_test.py @@ -0,0 +1,80 @@ +import logging +import os +import pytest +from swarmauri.agent_apis.concrete.AgentAPI import AgentAPI +from swarmauri.factories.concrete.AgentFactory import AgentFactory +from swarmauri.llms.concrete.GroqModel import GroqModel +from swarmauri.service_registries.concrete.ServiceRegistry import ServiceRegistry +from dotenv import load_dotenv + +load_dotenv() + + +@pytest.fixture(scope="module") +def groq_model(): + API_KEY = os.getenv("GROQ_API_KEY") + if not API_KEY: + pytest.skip("Skipping due to environment variable not set") + llm = GroqModel(api_key=API_KEY) + return llm + + +@pytest.fixture(scope="module") +def agent_api(groq_model): + agent = AgentFactory().create("QAAgent", llm=groq_model) + agent_registry = ServiceRegistry() + agent_registry.register_service(agent, "agent1") + return AgentAPI(agent_registry=agent_registry) + + +@pytest.mark.unit +def test_ubc_resource(agent_api): + assert agent_api.resource == "AgentAPI" + + +@pytest.mark.unit +def test_ubc_type(agent_api): + assert agent_api.type == "AgentAPI" + + +@pytest.mark.unit +def test_serialization(agent_api): + assert agent_api.id == AgentAPI.model_validate_json(agent_api.model_dump_json()).id + + +def test_invoke(agent_api): + agent_id = "agent1" + + result = agent_api.invoke(agent_id, input_str="Hello") + + logging.info(result) + + assert isinstance(result, str) + + +def test_invoke_agent_not_found(agent_api): + agent_id = "nonexistent_agent" + + with pytest.raises(ValueError) as exc_info: + agent_api.invoke(agent_id) + + assert str(exc_info.value) == f"Agent with ID {agent_id} not found." + + +@pytest.mark.asyncio +async def test_ainvoke(agent_api): + agent_id = "agent1" + + result = await agent_api.ainvoke(agent_id, param="value") + + assert isinstance(result, str) + + +@pytest.mark.asyncio +async def test_ainvoke_agent_not_found(agent_api): + agent_id = "nonexistent_agent" + + with pytest.raises(ValueError) as exc_info: + await agent_api.ainvoke(agent_id) + + assert str(exc_info.value) == f"Agent with ID {agent_id} not found." diff --git a/pkgs/swarmauri/tests/unit/factories/AgentFactory_unit_test.py b/pkgs/swarmauri/tests/unit/factories/AgentFactory_unit_test.py index 0aac46614..37d641f17 100644 --- a/pkgs/swarmauri/tests/unit/factories/AgentFactory_unit_test.py +++ b/pkgs/swarmauri/tests/unit/factories/AgentFactory_unit_test.py @@ -1,13 +1,22 @@ +from typing import Literal import pytest from swarmauri.factories.concrete.AgentFactory import AgentFactory import os from swarmauri.llms.concrete.GroqModel import GroqModel -from swarmauri.agents.concrete.QAAgent import QAAgent +from swarmauri.utils._get_subclasses import get_classes_from_module + from dotenv import load_dotenv load_dotenv() +class TestAgent: + type: Literal["TestAgent"] = "TestAgent" + + def exec(self, **kwargs): + return "TestAgent execution result" + + @pytest.fixture(scope="module") def groq_model(): API_KEY = os.getenv("GROQ_API_KEY") @@ -43,12 +52,12 @@ def test_serialization(agent_factory): @pytest.mark.unit def test_agent_factory_register_and_create(agent_factory, groq_model): - agent_factory.register(type="QAAgent", resource_class=QAAgent) + agent_factory.register(type="TestAgent", resource_class=TestAgent) # Create an instance - instance = agent_factory.create(type="QAAgent", llm=groq_model) - assert isinstance(instance, QAAgent) - assert instance.type == "QAAgent" + instance = agent_factory.create(type="TestAgent") + assert isinstance(instance, TestAgent) + assert instance.type == "TestAgent" @pytest.mark.unit @@ -61,6 +70,4 @@ def test_agent_factory_create_unregistered_type(agent_factory): @pytest.mark.unit def test_agent_factory_get_agents(agent_factory): - - assert agent_factory.get() == ["QAAgent"] - assert len(agent_factory.get()) == 1 + assert len(agent_factory.get()) == len(get_classes_from_module("Agent").keys()) + 1 diff --git a/pkgs/swarmauri/tests/unit/factories/Factory_unit_test.py b/pkgs/swarmauri/tests/unit/factories/Factory_unit_test.py index 11695f7ec..f3048d289 100644 --- a/pkgs/swarmauri/tests/unit/factories/Factory_unit_test.py +++ b/pkgs/swarmauri/tests/unit/factories/Factory_unit_test.py @@ -1,10 +1,9 @@ import pytest from swarmauri.factories.concrete.Factory import Factory from swarmauri.parsers.concrete.BeautifulSoupElementParser import ( - BeautifulSoupElementParser, + BeautifulSoupElementParser ) - @pytest.fixture(scope="module") def factory(): return Factory() @@ -46,7 +45,7 @@ def test_factory_create_unregistered_resource(factory): # Attempt to create an instance of an unregistered resource with pytest.raises( - ModuleNotFoundError, match="Resource 'UnknownResource' is not registered." + ValueError, match="Type 'BeautifulSoupElementParser' is not registered under resource 'UnknownResource'." ): factory.create("UnknownResource", "BeautifulSoupElementParser") From 799558af5556ccce44b92ae2b4fda59bd56df750 Mon Sep 17 00:00:00 2001 From: MichaelDecent Date: Fri, 20 Dec 2024 17:09:34 +0100 Subject: [PATCH 2/7] Enhance Factory and get_classes_from_module --- .../swarmauri/factories/concrete/Factory.py | 9 ++++--- .../swarmauri/utils/_get_subclasses.py | 26 +++++++++++++++---- .../unit/factories/AgentFactory_unit_test.py | 13 ++++++++-- .../tests/unit/factories/Factory_unit_test.py | 19 ++++++++++++-- 4 files changed, 55 insertions(+), 12 deletions(-) diff --git a/pkgs/swarmauri/swarmauri/factories/concrete/Factory.py b/pkgs/swarmauri/swarmauri/factories/concrete/Factory.py index d3bca5af5..7908dddae 100644 --- a/pkgs/swarmauri/swarmauri/factories/concrete/Factory.py +++ b/pkgs/swarmauri/swarmauri/factories/concrete/Factory.py @@ -1,7 +1,6 @@ +import logging from typing import Any, Callable, Dict, Literal from swarmauri.factories.base.FactoryBase import FactoryBase -from swarmauri.utils._get_subclasses import get_classes_from_module - class Factory(FactoryBase): """ @@ -15,6 +14,7 @@ def register(self, resource: str, type: str, resource_class: Callable) -> None: """ Register a resource class under a specific resource. """ + from swarmauri.utils._get_subclasses import get_classes_from_module if type in self._resource_registry.get(resource, {}): raise ValueError( f"Type '{type}' is already registered under resource '{resource}'." @@ -30,10 +30,13 @@ def create(self, resource: str, type: str, *args: Any, **kwargs: Any) -> Any: """ Create an instance of the class associated with the given resource and type. """ + from swarmauri.utils._get_subclasses import get_classes_from_module + if resource not in self._resource_registry: self._resource_registry[resource] = get_classes_from_module(resource) + logging.info(self._resource_registry) - if type not in self._resource_registry[resource]: + if type not in self._resource_registry[resource].keys(): raise ValueError( f"Type '{type}' is not registered under resource '{resource}'." ) diff --git a/pkgs/swarmauri/swarmauri/utils/_get_subclasses.py b/pkgs/swarmauri/swarmauri/utils/_get_subclasses.py index 4f105eb75..0a74e860c 100644 --- a/pkgs/swarmauri/swarmauri/utils/_get_subclasses.py +++ b/pkgs/swarmauri/swarmauri/utils/_get_subclasses.py @@ -1,20 +1,36 @@ +import importlib +import inspect +import logging + from swarmauri.utils.LazyLoader import LazyLoader -def get_classes_from_module(module): - import inspect +def get_classes_from_module(resource_name: str): + """ + Pass something like 'llms' to import 'swarmauri.llms.concrete' + and retrieve all loaded classes. + """ + resource_name = resource_name.lower() + + full_module = f"swarmauri.{resource_name}s.concrete" + module = importlib.import_module(full_module) classes = {} for name, obj in inspect.getmembers(module): if isinstance(obj, LazyLoader): - obj = obj._load_class() # Load the class from LazyLoader + obj = obj._load_class() if inspect.isclass(obj): classes[name] = obj + + logging.info(f"Classes found in module {module}: {classes}") return classes -def get_class_from_module(module, class_name): - import inspect +def get_class_from_module(resource_name, class_name): + resource_name = resource_name.lower() + + full_module = f"swarmauri.{resource_name}s.concrete" + module = importlib.import_module(full_module) if hasattr(module, class_name): obj = getattr(module, class_name) diff --git a/pkgs/swarmauri/tests/unit/factories/AgentFactory_unit_test.py b/pkgs/swarmauri/tests/unit/factories/AgentFactory_unit_test.py index 37d641f17..6ec7fc117 100644 --- a/pkgs/swarmauri/tests/unit/factories/AgentFactory_unit_test.py +++ b/pkgs/swarmauri/tests/unit/factories/AgentFactory_unit_test.py @@ -3,7 +3,7 @@ from swarmauri.factories.concrete.AgentFactory import AgentFactory import os from swarmauri.llms.concrete.GroqModel import GroqModel -from swarmauri.utils._get_subclasses import get_classes_from_module +from swarmauri.agents.concrete.QAAgent import QAAgent from dotenv import load_dotenv @@ -60,6 +60,15 @@ def test_agent_factory_register_and_create(agent_factory, groq_model): assert instance.type == "TestAgent" +@pytest.mark.unit +def test_agent_factory_create(agent_factory, groq_model): + + # Create an instance + instance = agent_factory.create(type="QAAgent", llm=groq_model) + assert isinstance(instance, QAAgent) + assert instance.type == "QAAgent" + + @pytest.mark.unit def test_agent_factory_create_unregistered_type(agent_factory): @@ -70,4 +79,4 @@ def test_agent_factory_create_unregistered_type(agent_factory): @pytest.mark.unit def test_agent_factory_get_agents(agent_factory): - assert len(agent_factory.get()) == len(get_classes_from_module("Agent").keys()) + 1 + assert len(agent_factory.get()) == len(agent_factory._registry) diff --git a/pkgs/swarmauri/tests/unit/factories/Factory_unit_test.py b/pkgs/swarmauri/tests/unit/factories/Factory_unit_test.py index f3048d289..0a1165e8e 100644 --- a/pkgs/swarmauri/tests/unit/factories/Factory_unit_test.py +++ b/pkgs/swarmauri/tests/unit/factories/Factory_unit_test.py @@ -1,9 +1,10 @@ import pytest from swarmauri.factories.concrete.Factory import Factory from swarmauri.parsers.concrete.BeautifulSoupElementParser import ( - BeautifulSoupElementParser + BeautifulSoupElementParser, ) + @pytest.fixture(scope="module") def factory(): return Factory() @@ -40,12 +41,26 @@ def test_factory_register_create_resource(factory): assert instance.type == "BeautifulSoupElementParser" +@pytest.mark.unit +def test_factory_create_resource(factory): + + html_content = "

Sample HTML content

" + + # Create an instance of a registered resource + instance = factory.create( + "Parser", "BeautifulSoupElementParser", element=html_content + ) + assert isinstance(instance, BeautifulSoupElementParser) + assert instance.type == "BeautifulSoupElementParser" + + @pytest.mark.unit def test_factory_create_unregistered_resource(factory): # Attempt to create an instance of an unregistered resource with pytest.raises( - ValueError, match="Type 'BeautifulSoupElementParser' is not registered under resource 'UnknownResource'." + ModuleNotFoundError, + match="No module named 'swarmauri.unknownresources'", ): factory.create("UnknownResource", "BeautifulSoupElementParser") From 334a1d491be9183fcb9b56989398dae316086368 Mon Sep 17 00:00:00 2001 From: MichaelDecent Date: Fri, 20 Dec 2024 17:12:23 +0100 Subject: [PATCH 3/7] Remove deprecated agent API interfaces and related initialization --- .../agent_apis(deprecated)/IAgentCommands.py | 83 ------------------- .../IAgentRouterCRUD.py | 56 ------------- .../agent_apis(deprecated)/__init__.py | 4 - 3 files changed, 143 deletions(-) delete mode 100644 pkgs/core/swarmauri_core/agent_apis(deprecated)/IAgentCommands.py delete mode 100644 pkgs/core/swarmauri_core/agent_apis(deprecated)/IAgentRouterCRUD.py delete mode 100644 pkgs/core/swarmauri_core/agent_apis(deprecated)/__init__.py diff --git a/pkgs/core/swarmauri_core/agent_apis(deprecated)/IAgentCommands.py b/pkgs/core/swarmauri_core/agent_apis(deprecated)/IAgentCommands.py deleted file mode 100644 index 175848a6c..000000000 --- a/pkgs/core/swarmauri_core/agent_apis(deprecated)/IAgentCommands.py +++ /dev/null @@ -1,83 +0,0 @@ -from abc import ABC, abstractmethod -from typing import Callable, Any, List - -class IAgentCommands(ABC): - """ - Interface for the API object that enables a SwarmAgent to host various API routes. - """ - - - @abstractmethod - def invoke(self, request: Any) -> Any: - """ - Handles invocation requests synchronously. - - Parameters: - request (Any): The incoming request payload. - - Returns: - Any: The response payload. - """ - pass - - @abstractmethod - async def ainvoke(self, request: Any) -> Any: - """ - Handles invocation requests asynchronously. - - Parameters: - request (Any): The incoming request payload. - - Returns: - Any: The response payload. - """ - pass - - @abstractmethod - def batch(self, requests: List[Any]) -> List[Any]: - """ - Handles batched invocation requests synchronously. - - Parameters: - requests (List[Any]): A list of incoming request payloads. - - Returns: - List[Any]: A list of responses. - """ - pass - - @abstractmethod - async def abatch(self, requests: List[Any]) -> List[Any]: - """ - Handles batched invocation requests asynchronously. - - Parameters: - requests (List[Any]): A list of incoming request payloads. - - Returns: - List[Any]: A list of responses. - """ - pass - - @abstractmethod - def stream(self, request: Any) -> Any: - """ - Handles streaming requests. - - Parameters: - request (Any): The incoming request payload. - - Returns: - Any: A streaming response. - """ - pass - - @abstractmethod - def get_schema_config(self) -> dict: - """ - Retrieves the schema configuration for the API. - - Returns: - dict: The schema configuration. - """ - pass \ No newline at end of file diff --git a/pkgs/core/swarmauri_core/agent_apis(deprecated)/IAgentRouterCRUD.py b/pkgs/core/swarmauri_core/agent_apis(deprecated)/IAgentRouterCRUD.py deleted file mode 100644 index 91eecbc4e..000000000 --- a/pkgs/core/swarmauri_core/agent_apis(deprecated)/IAgentRouterCRUD.py +++ /dev/null @@ -1,56 +0,0 @@ -from abc import ABC, abstractmethod -from typing import Callable, Any, Dict - -class IAgentRouterCRUD(ABC): - """ - Interface for managing API routes within a SwarmAgent. - """ - - @abstractmethod - def create_route(self, path: str, method: str, handler: Callable[[Any], Any]) -> None: - """ - Create a new route for the API. - - Parameters: - - path (str): The URL path for the route. - - method (str): The HTTP method (e.g., 'GET', 'POST'). - - handler (Callable[[Any], Any]): The function that handles requests to this route. - """ - pass - - @abstractmethod - def read_route(self, path: str, method: str) -> Dict: - """ - Retrieve information about a specific route. - - Parameters: - - path (str): The URL path for the route. - - method (str): The HTTP method. - - Returns: - - Dict: Information about the route, including path, method, and handler. - """ - pass - - @abstractmethod - def update_route(self, path: str, method: str, new_handler: Callable[[Any], Any]) -> None: - """ - Update the handler function for an existing route. - - Parameters: - - path (str): The URL path for the route. - - method (str): The HTTP method. - - new_handler (Callable[[Any], Any]): The new function that handles requests to this route. - """ - pass - - @abstractmethod - def delete_route(self, path: str, method: str) -> None: - """ - Delete a specific route from the API. - - Parameters: - - path (str): The URL path for the route. - - method (str): The HTTP method. - """ - pass \ No newline at end of file diff --git a/pkgs/core/swarmauri_core/agent_apis(deprecated)/__init__.py b/pkgs/core/swarmauri_core/agent_apis(deprecated)/__init__.py deleted file mode 100644 index 27f8d7f14..000000000 --- a/pkgs/core/swarmauri_core/agent_apis(deprecated)/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .IAgentCommands import IAgentCommands -from .IAgentRouterCRUD import IAgentRouterCRUD - -__all__ = ['IAgentCommands', 'IAgentRouterCRUD'] \ No newline at end of file From afe570cbe33fe2f5e6643c09995f1213c4f04766 Mon Sep 17 00:00:00 2001 From: MichaelDecent Date: Mon, 23 Dec 2024 10:17:18 +0100 Subject: [PATCH 4/7] Add aexec method for Agents --- .../swarmauri/agents/base/AgentBase.py | 28 +++- .../agents/base/AgentConversationMixin.py | 5 +- .../agents/base/AgentRetrieveMixin.py | 7 +- .../agents/base/AgentSystemContextMixin.py | 6 +- .../swarmauri/agents/base/AgentToolMixin.py | 4 +- .../agents/base/AgentVectorStoreMixin.py | 3 +- .../swarmauri/agents/concrete/QAAgent.py | 28 ++-- .../swarmauri/agents/concrete/RagAgent.py | 134 +++++++++++------- .../concrete/SimpleConversationAgent.py | 18 ++- .../swarmauri/agents/concrete/ToolAgent.py | 50 +++++-- .../tests/unit/agents/QAAgent_unit_test.py | 34 +++-- .../tests/unit/agents/RagAgent_unit_test.py | 13 ++ .../SimpleConversationAgent_unit_test.py | 9 +- .../tests/unit/agents/ToolAgent_unit_test.py | 7 + 14 files changed, 233 insertions(+), 113 deletions(-) diff --git a/pkgs/swarmauri/swarmauri/agents/base/AgentBase.py b/pkgs/swarmauri/swarmauri/agents/base/AgentBase.py index 307e10106..1d6f3639c 100644 --- a/pkgs/swarmauri/swarmauri/agents/base/AgentBase.py +++ b/pkgs/swarmauri/swarmauri/agents/base/AgentBase.py @@ -1,16 +1,32 @@ from typing import Any, Optional, Dict, Union, Literal -from pydantic import ConfigDict, Field, field_validator +from pydantic import ConfigDict, Field from swarmauri_core.typing import SubclassUnion from swarmauri_core.ComponentBase import ComponentBase, ResourceTypes from swarmauri_core.messages.IMessage import IMessage from swarmauri_core.agents.IAgent import IAgent from swarmauri.llms.base.LLMBase import LLMBase + class AgentBase(IAgent, ComponentBase): llm: SubclassUnion[LLMBase] - resource: ResourceTypes = Field(default=ResourceTypes.AGENT.value) - model_config = ConfigDict(extra='forbid', arbitrary_types_allowed=True) - type: Literal['AgentBase'] = 'AgentBase' + resource: ResourceTypes = Field(default=ResourceTypes.AGENT.value) + model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True) + type: Literal["AgentBase"] = "AgentBase" + + def exec( + self, + input_str: Optional[Union[str, IMessage]] = "", + llm_kwargs: Optional[Dict] = {}, + ) -> Any: + raise NotImplementedError( + "The `exec` function has not been implemeneted on this class." + ) - def exec(self, input_str: Optional[Union[str, IMessage]] = "", llm_kwargs: Optional[Dict] = {}) -> Any: - raise NotImplementedError('The `exec` function has not been implemeneted on this class.') \ No newline at end of file + async def aexec( + self, + input_str: Optional[Union[str, IMessage]] = "", + llm_kwargs: Optional[Dict] = {}, + ) -> Any: + raise NotImplementedError( + "The `aexec` function has not been implemented on this class." + ) diff --git a/pkgs/swarmauri/swarmauri/agents/base/AgentConversationMixin.py b/pkgs/swarmauri/swarmauri/agents/base/AgentConversationMixin.py index 140ab5619..5c06a44d8 100644 --- a/pkgs/swarmauri/swarmauri/agents/base/AgentConversationMixin.py +++ b/pkgs/swarmauri/swarmauri/agents/base/AgentConversationMixin.py @@ -3,6 +3,7 @@ from swarmauri_core.agents.IAgentConversation import IAgentConversation from swarmauri.conversations.base.ConversationBase import ConversationBase + class AgentConversationMixin(IAgentConversation, BaseModel): - conversation: SubclassUnion[ConversationBase] # 🚧 Placeholder - model_config = ConfigDict(extra='forbid', arbitrary_types_allowed=True) \ No newline at end of file + conversation: SubclassUnion[ConversationBase] # 🚧 Placeholder + model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True) diff --git a/pkgs/swarmauri/swarmauri/agents/base/AgentRetrieveMixin.py b/pkgs/swarmauri/swarmauri/agents/base/AgentRetrieveMixin.py index 792cdeca0..1bb353663 100644 --- a/pkgs/swarmauri/swarmauri/agents/base/AgentRetrieveMixin.py +++ b/pkgs/swarmauri/swarmauri/agents/base/AgentRetrieveMixin.py @@ -1,10 +1,9 @@ -from abc import ABC from typing import List -from pydantic import BaseModel, ConfigDict, field_validator, Field +from pydantic import BaseModel, ConfigDict, Field from swarmauri.documents.concrete.Document import Document from swarmauri_core.agents.IAgentRetrieve import IAgentRetrieve + class AgentRetrieveMixin(IAgentRetrieve, BaseModel): last_retrieved: List[Document] = Field(default_factory=list) - model_config = ConfigDict(extra='forbid', arbitrary_types_allowed=True) - + model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True) diff --git a/pkgs/swarmauri/swarmauri/agents/base/AgentSystemContextMixin.py b/pkgs/swarmauri/swarmauri/agents/base/AgentSystemContextMixin.py index 5080fb842..fc77a6247 100644 --- a/pkgs/swarmauri/swarmauri/agents/base/AgentSystemContextMixin.py +++ b/pkgs/swarmauri/swarmauri/agents/base/AgentSystemContextMixin.py @@ -6,10 +6,10 @@ class AgentSystemContextMixin(IAgentSystemContext, BaseModel): - system_context: Union[SystemMessage, str] + system_context: Union[SystemMessage, str] - @field_validator('system_context', mode='before') + @field_validator("system_context", mode="before") def set_system_context(cls, value: Union[str, SystemMessage]) -> SystemMessage: if isinstance(value, str): return SystemMessage(content=value) - return value \ No newline at end of file + return value diff --git a/pkgs/swarmauri/swarmauri/agents/base/AgentToolMixin.py b/pkgs/swarmauri/swarmauri/agents/base/AgentToolMixin.py index 2d046f8ff..ff348d788 100644 --- a/pkgs/swarmauri/swarmauri/agents/base/AgentToolMixin.py +++ b/pkgs/swarmauri/swarmauri/agents/base/AgentToolMixin.py @@ -3,7 +3,7 @@ from swarmauri.toolkits.base.ToolkitBase import ToolkitBase from swarmauri_core.agents.IAgentToolkit import IAgentToolkit + class AgentToolMixin(IAgentToolkit, BaseModel): toolkit: SubclassUnion[ToolkitBase] - model_config = ConfigDict(extra='forbid', arbitrary_types_allowed=True) - \ No newline at end of file + model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True) diff --git a/pkgs/swarmauri/swarmauri/agents/base/AgentVectorStoreMixin.py b/pkgs/swarmauri/swarmauri/agents/base/AgentVectorStoreMixin.py index b709ab1eb..c7662dbb2 100644 --- a/pkgs/swarmauri/swarmauri/agents/base/AgentVectorStoreMixin.py +++ b/pkgs/swarmauri/swarmauri/agents/base/AgentVectorStoreMixin.py @@ -3,6 +3,7 @@ from swarmauri_core.agents.IAgentVectorStore import IAgentVectorStore from swarmauri.vector_stores.base.VectorStoreBase import VectorStoreBase + class AgentVectorStoreMixin(IAgentVectorStore, BaseModel): vector_store: SubclassUnion[VectorStoreBase] - model_config = ConfigDict(extra='forbid', arbitrary_types_allowed=True) \ No newline at end of file + model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True) diff --git a/pkgs/swarmauri/swarmauri/agents/concrete/QAAgent.py b/pkgs/swarmauri/swarmauri/agents/concrete/QAAgent.py index 1a2e8e68d..8aab54c08 100644 --- a/pkgs/swarmauri/swarmauri/agents/concrete/QAAgent.py +++ b/pkgs/swarmauri/swarmauri/agents/concrete/QAAgent.py @@ -1,19 +1,29 @@ from typing import Any, Optional, Dict, Literal from swarmauri.agents.base.AgentBase import AgentBase -from swarmauri.conversations.concrete.MaxSystemContextConversation import MaxSystemContextConversation +from swarmauri.conversations.concrete.MaxSystemContextConversation import ( + MaxSystemContextConversation, +) from swarmauri.messages.concrete.HumanMessage import HumanMessage class QAAgent(AgentBase): - conversation: MaxSystemContextConversation = MaxSystemContextConversation(max_size=2) - type: Literal['QAAgent'] = 'QAAgent' + conversation: MaxSystemContextConversation = MaxSystemContextConversation( + max_size=2 + ) + type: Literal["QAAgent"] = "QAAgent" + + def exec( + self, input_str: Optional[str] = "", llm_kwargs: Optional[Dict] = {} + ) -> Any: - def exec(self, - input_str: Optional[str] = "", - llm_kwargs: Optional[Dict] = {} - ) -> Any: - self.conversation.add_message(HumanMessage(content=input_str)) self.llm.predict(conversation=self.conversation, **llm_kwargs) - + + return self.conversation.get_last().content + + async def aexec( + self, input_str: Optional[str] = "", llm_kwargs: Optional[Dict] = {} + ) -> Any: + self.conversation.add_message(HumanMessage(content=input_str)) + await self.llm.apredict(conversation=self.conversation, **llm_kwargs) return self.conversation.get_last().content diff --git a/pkgs/swarmauri/swarmauri/agents/concrete/RagAgent.py b/pkgs/swarmauri/swarmauri/agents/concrete/RagAgent.py index 7ae399075..5348991f6 100644 --- a/pkgs/swarmauri/swarmauri/agents/concrete/RagAgent.py +++ b/pkgs/swarmauri/swarmauri/agents/concrete/RagAgent.py @@ -7,93 +7,117 @@ from swarmauri.agents.base.AgentVectorStoreMixin import AgentVectorStoreMixin from swarmauri.agents.base.AgentSystemContextMixin import AgentSystemContextMixin -from swarmauri.messages.concrete import (HumanMessage, - SystemMessage, - AgentMessage) +from swarmauri.messages.concrete.HumanMessage import HumanMessage +from swarmauri.messages.concrete.SystemMessage import SystemMessage from swarmauri_core.typing import SubclassUnion from swarmauri.llms.base.LLMBase import LLMBase from swarmauri.conversations.base.ConversationBase import ConversationBase from swarmauri.vector_stores.base.VectorStoreBase import VectorStoreBase -class RagAgent(AgentRetrieveMixin, - AgentVectorStoreMixin, - AgentSystemContextMixin, - AgentConversationMixin, - AgentBase): + +class RagAgent( + AgentRetrieveMixin, + AgentVectorStoreMixin, + AgentSystemContextMixin, + AgentConversationMixin, + AgentBase, +): """ RagAgent (Retriever-And-Generator Agent) extends DocumentAgentBase, specialized in retrieving documents based on input queries and generating responses. """ + llm: SubclassUnion[LLMBase] conversation: SubclassUnion[ConversationBase] vector_store: SubclassUnion[VectorStoreBase] - system_context: Union[SystemMessage, str] - type: Literal['RagAgent'] = 'RagAgent' - + system_context: Union[SystemMessage, str] + type: Literal["RagAgent"] = "RagAgent" + def _create_preamble_context(self): substr = self.system_context.content - substr += '\n\n' - substr += '\n'.join([doc.content for doc in self.last_retrieved]) + substr += "\n\n" + substr += "\n".join([doc.content for doc in self.last_retrieved]) return substr def _create_post_context(self): - substr = '\n'.join([doc.content for doc in self.last_retrieved]) - substr += '\n\n' + substr = "\n".join([doc.content for doc in self.last_retrieved]) + substr += "\n\n" substr += self.system_context.content return substr - def exec(self, - input_data: Optional[Union[str, IMessage]] = "", - top_k: int = 5, - preamble: bool = True, - fixed: bool = False, - llm_kwargs: Optional[Dict] = {} - ) -> Any: - try: - # Check if the input is a string, then wrap it in a HumanMessage - if isinstance(input_data, str): - human_message = HumanMessage(content=input_data) - elif isinstance(input_data, IMessage): - human_message = input_data - else: - raise TypeError("Input data must be a string or an instance of Message.") - - # Add the human message to the conversation - self.conversation.add_message(human_message) + def _prepare_context( + self, + input_data: Union[str, IMessage], + top_k: int, + preamble: bool, + fixed: bool, + ) -> None: + # Wrap input in a HumanMessage if it is a string + if isinstance(input_data, str): + human_message = HumanMessage(content=input_data) + elif isinstance(input_data, IMessage): + human_message = input_data + else: + raise TypeError("Input data must be a string or an instance of IMessage.") - # Retrieval and set new substr for system context - if top_k > 0 and len(self.vector_store.documents) > 0: - self.last_retrieved = self.vector_store.retrieve(query=input_data, top_k=top_k) + self.conversation.add_message(human_message) + # Retrieval logic + if top_k > 0 and len(self.vector_store.documents) > 0: + self.last_retrieved = self.vector_store.retrieve( + query=input_data, top_k=top_k + ) + if preamble: + new_context = self._create_preamble_context() + else: + new_context = self._create_post_context() + else: + if fixed: if preamble: - substr = self._create_preamble_context() + new_context = self._create_preamble_context() else: - substr = self._create_post_context() - + new_context = self._create_post_context() else: - if fixed: - if preamble: - substr = self._create_preamble_context() - else: - substr = self._create_post_context() - else: - substr = self.system_context.content - self.last_retrieved = [] - - # Use substr to set system context - system_context = SystemMessage(content=substr) - self.conversation.system_context = system_context - + new_context = self.system_context.content + self.last_retrieved = [] - # Retrieve the conversation history and predict a response + self.conversation.system_context = SystemMessage(content=new_context) + + def exec( + self, + input_data: Optional[Union[str, IMessage]] = "", + top_k: int = 5, + preamble: bool = True, + fixed: bool = False, + llm_kwargs: Optional[Dict] = {}, + ) -> Any: + try: + self._prepare_context(input_data, top_k, preamble, fixed) if llm_kwargs: self.llm.predict(conversation=self.conversation, **llm_kwargs) else: self.llm.predict(conversation=self.conversation) - return self.conversation.get_last().content + except Exception as e: + print(f"RagAgent error: {e}") + raise e + async def aexec( + self, + input_data: Optional[Union[str, IMessage]] = "", + top_k: int = 5, + preamble: bool = True, + fixed: bool = False, + llm_kwargs: Optional[Dict] = {}, + ) -> Any: + try: + self._prepare_context(input_data, top_k, preamble, fixed) + if llm_kwargs: + await self.llm.apredict(conversation=self.conversation, **llm_kwargs) + else: + await self.llm.apredict(conversation=self.conversation) + return self.conversation.get_last().content except Exception as e: print(f"RagAgent error: {e}") - raise e \ No newline at end of file + raise e diff --git a/pkgs/swarmauri/swarmauri/agents/concrete/SimpleConversationAgent.py b/pkgs/swarmauri/swarmauri/agents/concrete/SimpleConversationAgent.py index 3dcd0d4bf..6c767f859 100644 --- a/pkgs/swarmauri/swarmauri/agents/concrete/SimpleConversationAgent.py +++ b/pkgs/swarmauri/swarmauri/agents/concrete/SimpleConversationAgent.py @@ -2,7 +2,7 @@ from swarmauri.agents.base.AgentBase import AgentBase from swarmauri.agents.base.AgentConversationMixin import AgentConversationMixin -from swarmauri.messages.concrete import HumanMessage +from swarmauri.messages.concrete.HumanMessage import HumanMessage from swarmauri_core.typing import SubclassUnion from swarmauri.conversations.base.ConversationBase import ConversationBase @@ -20,9 +20,21 @@ def exec( llm_kwargs: Optional[Dict] = {}, ) -> Any: - if input_str: - human_message = HumanMessage(content=input_str) + if input_data: + human_message = HumanMessage(content=input_data) self.conversation.add_message(human_message) self.llm.predict(conversation=self.conversation, **llm_kwargs) return self.conversation.get_last().content + + async def aexec( + self, + input_data: Optional[Union[str, List[contentItem]]] = "", + llm_kwargs: Optional[Dict] = {}, + ) -> Any: + if input_data: + human_message = HumanMessage(content=input_data) + self.conversation.add_message(human_message) + + await self.llm.apredict(conversation=self.conversation, **llm_kwargs) + return self.conversation.get_last().content diff --git a/pkgs/swarmauri/swarmauri/agents/concrete/ToolAgent.py b/pkgs/swarmauri/swarmauri/agents/concrete/ToolAgent.py index 550e6e56b..da79fc805 100644 --- a/pkgs/swarmauri/swarmauri/agents/concrete/ToolAgent.py +++ b/pkgs/swarmauri/swarmauri/agents/concrete/ToolAgent.py @@ -1,6 +1,5 @@ from pydantic import ConfigDict from typing import Any, Optional, Union, Dict, Literal -import json import logging from swarmauri_core.messages import IMessage @@ -8,22 +7,25 @@ from swarmauri.agents.base.AgentBase import AgentBase from swarmauri.agents.base.AgentConversationMixin import AgentConversationMixin from swarmauri.agents.base.AgentToolMixin import AgentToolMixin -from swarmauri.messages.concrete import HumanMessage, AgentMessage, FunctionMessage +from swarmauri.messages.concrete.HumanMessage import HumanMessage from swarmauri_core.typing import SubclassUnion from swarmauri.toolkits.base.ToolkitBase import ToolkitBase from swarmauri.conversations.base.ConversationBase import ConversationBase + class ToolAgent(AgentToolMixin, AgentConversationMixin, AgentBase): llm: SubclassUnion[LLMBase] toolkit: SubclassUnion[ToolkitBase] - conversation: SubclassUnion[ConversationBase] # 🚧 Placeholder - model_config = ConfigDict(extra='forbid', arbitrary_types_allowed=True) - type: Literal['ToolAgent'] = 'ToolAgent' - - def exec(self, - input_data: Optional[Union[str, IMessage]] = "", - llm_kwargs: Optional[Dict] = {}) -> Any: + # conversation: SubclassUnion[ConversationBase] # 🚧 Placeholder + model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True) + type: Literal["ToolAgent"] = "ToolAgent" + + def exec( + self, + input_data: Optional[Union[str, IMessage]] = "", + llm_kwargs: Optional[Dict] = {}, + ) -> Any: # Check if the input is a string, then wrap it in a HumanMessage if isinstance(input_data, str): @@ -36,12 +38,32 @@ def exec(self, # Add the human message to the conversation self.conversation.add_message(human_message) - #predict a response + # predict a response self.conversation = self.llm.predict( - conversation=self.conversation, - toolkit=self.toolkit, - **llm_kwargs) + conversation=self.conversation, toolkit=self.toolkit, **llm_kwargs + ) logging.info(self.conversation.get_last().content) - return self.conversation.get_last().content \ No newline at end of file + return self.conversation.get_last().content + + async def aexec( + self, + input_data: Optional[Union[str, IMessage]] = "", + llm_kwargs: Optional[Dict] = {}, + ) -> Any: + + # Check if the input is a string, then wrap it in a HumanMessage + if isinstance(input_data, str): + human_message = HumanMessage(content=input_data) + elif isinstance(input_data, IMessage): + human_message = input_data + else: + raise TypeError("Input data must be a string or an instance of Message.") + + # Add input to conversation + self.conversation.add_message(human_message) + + # Use the LLM in async mode + await self.llm.apredict(conversation=self.conversation, **llm_kwargs) + return self.conversation.get_last().content diff --git a/pkgs/swarmauri/tests/unit/agents/QAAgent_unit_test.py b/pkgs/swarmauri/tests/unit/agents/QAAgent_unit_test.py index c309ff1da..426477f27 100644 --- a/pkgs/swarmauri/tests/unit/agents/QAAgent_unit_test.py +++ b/pkgs/swarmauri/tests/unit/agents/QAAgent_unit_test.py @@ -16,26 +16,34 @@ def groq_model(): return llm +@pytest.fixture(scope="module") +def qa_agent(groq_model): + return QAAgent(llm=groq_model) + + +@pytest.mark.unit +def test_ubc_resource(qa_agent): + assert qa_agent.resource == "Agent" + + @pytest.mark.unit -def test_ubc_resource(groq_model): - agent = QAAgent(llm=groq_model) - assert agent.resource == "Agent" +def test_ubc_type(qa_agent): + assert qa_agent.type == "QAAgent" @pytest.mark.unit -def test_ubc_type(groq_model): - agent = QAAgent(llm=groq_model) - assert agent.type == "QAAgent" +def test_agent_exec(qa_agent): + result = qa_agent.exec("hello") + assert type(result) is str @pytest.mark.unit -def test_agent_exec(groq_model): - agent = QAAgent(llm=groq_model) - result = agent.exec("hello") - assert type(result) == str +def test_serialization(qa_agent): + assert qa_agent.id == QAAgent.model_validate_json(qa_agent.model_dump_json()).id +@pytest.mark.asyncio @pytest.mark.unit -def test_serialization(groq_model): - agent = QAAgent(llm=groq_model) - assert agent.id == QAAgent.model_validate_json(agent.model_dump_json()).id +async def test_agent_aexec(qa_agent): + result = await qa_agent.aexec("hello") + assert isinstance(result, str) diff --git a/pkgs/swarmauri/tests/unit/agents/RagAgent_unit_test.py b/pkgs/swarmauri/tests/unit/agents/RagAgent_unit_test.py index 7510fb152..5dcd03684 100644 --- a/pkgs/swarmauri/tests/unit/agents/RagAgent_unit_test.py +++ b/pkgs/swarmauri/tests/unit/agents/RagAgent_unit_test.py @@ -56,3 +56,16 @@ def test_ubc_type(rag_agent): @pytest.mark.unit def test_serialization(rag_agent): assert rag_agent.id == RagAgent.model_validate_json(rag_agent.model_dump_json()).id + + +@pytest.mark.unit +def test_agent_exec(rag_agent): + result = rag_agent.exec("Hello") + assert isinstance(result, str) + + +@pytest.mark.asyncio +@pytest.mark.unit +async def test_agent_aexec(rag_agent): + result = await rag_agent.aexec("Hello") + assert isinstance(result, str) diff --git a/pkgs/swarmauri/tests/unit/agents/SimpleConversationAgent_unit_test.py b/pkgs/swarmauri/tests/unit/agents/SimpleConversationAgent_unit_test.py index 3b75ce72c..ba26be554 100644 --- a/pkgs/swarmauri/tests/unit/agents/SimpleConversationAgent_unit_test.py +++ b/pkgs/swarmauri/tests/unit/agents/SimpleConversationAgent_unit_test.py @@ -43,4 +43,11 @@ def test_serialization(simple_conversation_agent): @pytest.mark.unit def test_agent_exec(simple_conversation_agent): result = simple_conversation_agent.exec("hello") - assert type(result) == str + assert type(result) is str + + +@pytest.mark.asyncio +@pytest.mark.unit +async def test_agent_aexec(simple_conversation_agent): + result = await simple_conversation_agent.aexec("Hello") + assert isinstance(result, str) diff --git a/pkgs/swarmauri/tests/unit/agents/ToolAgent_unit_test.py b/pkgs/swarmauri/tests/unit/agents/ToolAgent_unit_test.py index ac455a579..c95151007 100644 --- a/pkgs/swarmauri/tests/unit/agents/ToolAgent_unit_test.py +++ b/pkgs/swarmauri/tests/unit/agents/ToolAgent_unit_test.py @@ -47,3 +47,10 @@ def test_serialization(tool_agent): def test_agent_exec(tool_agent): result = tool_agent.exec("Add(512, 671)") assert type(result) is str + + +@pytest.mark.asyncio +@pytest.mark.unit +async def test_agent_aexec(tool_agent): + result = await tool_agent.aexec("Add(512, 671)") + assert isinstance(result, str) From ac73aa0fe6b7a13fd5716b7a8c91fdfd88ce763f Mon Sep 17 00:00:00 2001 From: MichaelDecent Date: Mon, 23 Dec 2024 10:49:18 +0100 Subject: [PATCH 5/7] Refactor import statements --- .../conversations/MaxSizeConversation_unit_test.py | 3 ++- .../MaxSystemContextConversation_unit_test.py | 8 +++----- .../SessionCacheConversation_unit_test.py | 10 ++++------ 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/pkgs/swarmauri/tests/unit/conversations/MaxSizeConversation_unit_test.py b/pkgs/swarmauri/tests/unit/conversations/MaxSizeConversation_unit_test.py index 57307085b..4e1da83a8 100644 --- a/pkgs/swarmauri/tests/unit/conversations/MaxSizeConversation_unit_test.py +++ b/pkgs/swarmauri/tests/unit/conversations/MaxSizeConversation_unit_test.py @@ -1,5 +1,6 @@ import pytest -from swarmauri.messages.concrete import HumanMessage, AgentMessage +from swarmauri.messages.concrete.HumanMessage import HumanMessage +from swarmauri.messages.concrete.AgentMessage import AgentMessage from swarmauri.conversations.concrete.MaxSizeConversation import ( MaxSizeConversation, ) diff --git a/pkgs/swarmauri/tests/unit/conversations/MaxSystemContextConversation_unit_test.py b/pkgs/swarmauri/tests/unit/conversations/MaxSystemContextConversation_unit_test.py index 9e7aeb4e7..290c7ae33 100644 --- a/pkgs/swarmauri/tests/unit/conversations/MaxSystemContextConversation_unit_test.py +++ b/pkgs/swarmauri/tests/unit/conversations/MaxSystemContextConversation_unit_test.py @@ -1,9 +1,7 @@ import pytest -from swarmauri.messages.concrete import ( - SystemMessage, - AgentMessage, - HumanMessage, -) +from swarmauri.messages.concrete.HumanMessage import HumanMessage +from swarmauri.messages.concrete.AgentMessage import AgentMessage +from swarmauri.messages.concrete.SystemMessage import SystemMessage from swarmauri.conversations.concrete.MaxSystemContextConversation import ( MaxSystemContextConversation, ) diff --git a/pkgs/swarmauri/tests/unit/conversations/SessionCacheConversation_unit_test.py b/pkgs/swarmauri/tests/unit/conversations/SessionCacheConversation_unit_test.py index 28bf52b22..b90847907 100644 --- a/pkgs/swarmauri/tests/unit/conversations/SessionCacheConversation_unit_test.py +++ b/pkgs/swarmauri/tests/unit/conversations/SessionCacheConversation_unit_test.py @@ -1,10 +1,8 @@ import pytest -from swarmauri.messages.concrete import ( - SystemMessage, - AgentMessage, - HumanMessage, - FunctionMessage, -) +from swarmauri.messages.concrete.HumanMessage import HumanMessage +from swarmauri.messages.concrete.AgentMessage import AgentMessage +from swarmauri.messages.concrete.SystemMessage import SystemMessage + from swarmauri.conversations.concrete.SessionCacheConversation import ( SessionCacheConversation, ) From 63d114cd882582ada33623be05d578e86cd28a97 Mon Sep 17 00:00:00 2001 From: MichaelDecent Date: Mon, 23 Dec 2024 10:55:35 +0100 Subject: [PATCH 6/7] Refactor SessionCacheConversation: clean up imports --- .../concrete/SessionCacheConversation.py | 49 +++++++++++-------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/pkgs/swarmauri/swarmauri/conversations/concrete/SessionCacheConversation.py b/pkgs/swarmauri/swarmauri/conversations/concrete/SessionCacheConversation.py index 18bb7a70b..4ab991c82 100644 --- a/pkgs/swarmauri/swarmauri/conversations/concrete/SessionCacheConversation.py +++ b/pkgs/swarmauri/swarmauri/conversations/concrete/SessionCacheConversation.py @@ -1,20 +1,23 @@ -from typing import Optional, Union, List, Literal +from typing import Optional, List, Literal from pydantic import Field, ConfigDict -from collections import deque from swarmauri_core.messages.IMessage import IMessage from swarmauri_core.conversations.IMaxSize import IMaxSize from swarmauri.conversations.base.ConversationBase import ConversationBase -from swarmauri.conversations.base.ConversationSystemContextMixin import ConversationSystemContextMixin -from swarmauri.messages.concrete import SystemMessage, AgentMessage, HumanMessage, FunctionMessage -from swarmauri.exceptions.concrete import IndexErrorWithContext +from swarmauri.conversations.base.ConversationSystemContextMixin import ( + ConversationSystemContextMixin, +) +from swarmauri.messages.concrete.HumanMessage import HumanMessage +from swarmauri.messages.concrete.AgentMessage import AgentMessage +from swarmauri.messages.concrete.SystemMessage import SystemMessage - -class SessionCacheConversation(IMaxSize, ConversationSystemContextMixin, ConversationBase): +class SessionCacheConversation( + IMaxSize, ConversationSystemContextMixin, ConversationBase +): max_size: int = Field(default=2, gt=1) system_context: Optional[SystemMessage] = None session_max_size: int = Field(default=-1) - model_config = ConfigDict(extra='forbid', arbitrary_types_allowed=True) - type: Literal['SessionCacheConversation'] = 'SessionCacheConversation' + model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True) + type: Literal["SessionCacheConversation"] = "SessionCacheConversation" def __init__(self, **data): super().__init__(**data) @@ -28,14 +31,21 @@ def add_message(self, message: IMessage): We are forcing the SystemContext to be a preamble only. """ if isinstance(message, SystemMessage): - raise ValueError(f"System context cannot be set through this method on {self.__class_name__}.") + raise ValueError( + f"System context cannot be set through this method on {self.__class_name__}." + ) if not self._history and not isinstance(message, HumanMessage): - raise ValueError("The first message in the history must be an HumanMessage.") - if self._history and isinstance(self._history[-1], HumanMessage) and isinstance(message, HumanMessage): + raise ValueError( + "The first message in the history must be an HumanMessage." + ) + if ( + self._history + and isinstance(self._history[-1], HumanMessage) + and isinstance(message, HumanMessage) + ): raise ValueError("Cannot have two repeating HumanMessages.") - - super().add_message(message) + super().add_message(message) def session_to_dict(self) -> List[dict]: """ @@ -43,10 +53,10 @@ def session_to_dict(self) -> List[dict]: """ included_fields = {"role", "content"} return [message.dict(include=included_fields) for message in self.session] - + @property def session(self) -> List[IMessage]: - return self._history[-self.session_max_size:] + return self._history[-self.session_max_size :] @property def history(self): @@ -61,7 +71,7 @@ def history(self): alternating = True count = 0 - for message in self._history[-self.max_size:]: + for message in self._history[-self.max_size :]: if isinstance(message, HumanMessage) and alternating: res.append(message) alternating = not alternating # Switch to expecting AgentMessage @@ -73,9 +83,8 @@ def history(self): if count >= self.max_size: break - + if self.system_context: res = [self.system_context] + res - - return res + return res From 41b0b22177b8e3839088702e5c87454893475be1 Mon Sep 17 00:00:00 2001 From: MichaelDecent Date: Mon, 23 Dec 2024 18:14:38 +0100 Subject: [PATCH 7/7] Add MAS-specific agent-local API interface and implementation --- pkgs/core/swarmauri_core/ComponentBase.py | 1 + .../mas_agent_apis/IMasAgentAPI.py | 21 ++++++++ .../swarmauri_core/mas_agent_apis/__init__.py | 0 .../swarmauri/mas_agent_apis/__init__.py | 0 .../mas_agent_apis/base/MasAgentAPIBase.py | 28 ++++++++++ .../swarmauri/mas_agent_apis/base/__init__.py | 0 .../mas_agent_apis/concrete/MasAgentAPI.py | 8 +++ .../mas_agent_apis/concrete/__init__.py | 13 +++++ .../mas_agent_apis/MasAgentAPI_unit_test.py | 53 +++++++++++++++++++ 9 files changed, 124 insertions(+) create mode 100644 pkgs/core/swarmauri_core/mas_agent_apis/IMasAgentAPI.py create mode 100644 pkgs/core/swarmauri_core/mas_agent_apis/__init__.py create mode 100644 pkgs/swarmauri/swarmauri/mas_agent_apis/__init__.py create mode 100644 pkgs/swarmauri/swarmauri/mas_agent_apis/base/MasAgentAPIBase.py create mode 100644 pkgs/swarmauri/swarmauri/mas_agent_apis/base/__init__.py create mode 100644 pkgs/swarmauri/swarmauri/mas_agent_apis/concrete/MasAgentAPI.py create mode 100644 pkgs/swarmauri/swarmauri/mas_agent_apis/concrete/__init__.py create mode 100644 pkgs/swarmauri/tests/unit/mas_agent_apis/MasAgentAPI_unit_test.py diff --git a/pkgs/core/swarmauri_core/ComponentBase.py b/pkgs/core/swarmauri_core/ComponentBase.py index f25bf5f38..9a4a67633 100644 --- a/pkgs/core/swarmauri_core/ComponentBase.py +++ b/pkgs/core/swarmauri_core/ComponentBase.py @@ -65,6 +65,7 @@ class ResourceTypes(Enum): TASK_MGT_STRATEGY = "TaskMgtStrategy" MAS = "Mas" AGENT_API = "AgentAPI" + MAS_AGENT_API = "MasAgentAPI" def generate_id() -> str: diff --git a/pkgs/core/swarmauri_core/mas_agent_apis/IMasAgentAPI.py b/pkgs/core/swarmauri_core/mas_agent_apis/IMasAgentAPI.py new file mode 100644 index 000000000..ec777b402 --- /dev/null +++ b/pkgs/core/swarmauri_core/mas_agent_apis/IMasAgentAPI.py @@ -0,0 +1,21 @@ +from abc import ABC, abstractmethod +from typing import Any + + +class IMasAgentAPI(ABC): + """Interface for MAS-specific agent-local APIs.""" + + @abstractmethod + def send_message(self, message: Any) -> None: + """Send a message to the MAS agent.""" + pass + + @abstractmethod + def subscribe(self, topic: str) -> None: + """Subscribe to a topic.""" + pass + + @abstractmethod + def publish(self, topic: str) -> None: + """Publish a message to a topic.""" + pass diff --git a/pkgs/core/swarmauri_core/mas_agent_apis/__init__.py b/pkgs/core/swarmauri_core/mas_agent_apis/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pkgs/swarmauri/swarmauri/mas_agent_apis/__init__.py b/pkgs/swarmauri/swarmauri/mas_agent_apis/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pkgs/swarmauri/swarmauri/mas_agent_apis/base/MasAgentAPIBase.py b/pkgs/swarmauri/swarmauri/mas_agent_apis/base/MasAgentAPIBase.py new file mode 100644 index 000000000..900ed6071 --- /dev/null +++ b/pkgs/swarmauri/swarmauri/mas_agent_apis/base/MasAgentAPIBase.py @@ -0,0 +1,28 @@ +from typing import Any, Literal, Optional + +from pydantic import ConfigDict, Field +from swarmauri_core.ComponentBase import ResourceTypes, ComponentBase +from swarmauri_core.mas_agent_apis.IMasAgentAPI import IMasAgentAPI +from swarmauri.transports.base.TransportBase import TransportBase +from swarmauri_core.typing import SubclassUnion + + +class MasAgentAPIBase(IMasAgentAPI, ComponentBase): + """Base implementation of the MAS-specific agent-local APIs.""" + + transport: SubclassUnion[TransportBase] + resource: Optional[str] = Field(default=ResourceTypes.MAS_AGENT_API.value, frozen=True) + model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True) + type: Literal["MasAgentAPIBase"] = "MasAgentAPIBase" + + def send_message(self, message: Any, recipient_id: str) -> None: + """Send a message to a specific recipient.""" + self.transport.send(sender=self, message=message, recipient=recipient_id) + + def subscribe(self, topic: str) -> None: + """Subscribe to a topic.""" + self.transport.subscribe(topic) + + def publish(self, topic: str) -> None: + """Publish a message to a topic""" + self.transport.publish(topic) diff --git a/pkgs/swarmauri/swarmauri/mas_agent_apis/base/__init__.py b/pkgs/swarmauri/swarmauri/mas_agent_apis/base/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pkgs/swarmauri/swarmauri/mas_agent_apis/concrete/MasAgentAPI.py b/pkgs/swarmauri/swarmauri/mas_agent_apis/concrete/MasAgentAPI.py new file mode 100644 index 000000000..0ef086189 --- /dev/null +++ b/pkgs/swarmauri/swarmauri/mas_agent_apis/concrete/MasAgentAPI.py @@ -0,0 +1,8 @@ +from typing import Literal +from swarmauri.mas_agent_apis.base.MasAgentAPIBase import MasAgentAPIBase + + +class MasAgentAPI(MasAgentAPIBase): + """Concrete implementation of MAS-specific agent-local APIs.""" + + type: Literal["MasAgentAPI"] = "MasAgentAPI" diff --git a/pkgs/swarmauri/swarmauri/mas_agent_apis/concrete/__init__.py b/pkgs/swarmauri/swarmauri/mas_agent_apis/concrete/__init__.py new file mode 100644 index 000000000..b79c7f72e --- /dev/null +++ b/pkgs/swarmauri/swarmauri/mas_agent_apis/concrete/__init__.py @@ -0,0 +1,13 @@ +from swarmauri.utils.LazyLoader import LazyLoader + +# List of mas_agent_apis names (file names without the ".py" extension) and corresponding class names +mas_agent_apis_files = [ + ("swarmauri.mas_agent_apis.concrete.MasAgentAPI", "MasAgentAPI"), +] + +# Lazy loading of mas_agent_apis classes, storing them in variables +for module_name, class_name in mas_agent_apis_files: + globals()[class_name] = LazyLoader(module_name, class_name) + +# Adding the lazy-loaded mas_agent_apis classes to __all__ +__all__ = [class_name for _, class_name in mas_agent_apis_files] diff --git a/pkgs/swarmauri/tests/unit/mas_agent_apis/MasAgentAPI_unit_test.py b/pkgs/swarmauri/tests/unit/mas_agent_apis/MasAgentAPI_unit_test.py new file mode 100644 index 000000000..bfb078c7e --- /dev/null +++ b/pkgs/swarmauri/tests/unit/mas_agent_apis/MasAgentAPI_unit_test.py @@ -0,0 +1,53 @@ +import pytest +from swarmauri.mas_agent_apis.concrete.MasAgentAPI import MasAgentAPI +from swarmauri.transports.concrete.PubSubTransport import PubSubTransport as Transport + + +@pytest.fixture +def mas_agent_api(): + """Set up the MasAgentAPIBase instance with a mocked transport.""" + transport = Transport() + api = MasAgentAPI(transport=transport) + return api + + +@pytest.mark.unit +def test_ubc_resource(mas_agent_api): + assert mas_agent_api.resource == "MasAgentAPI" + + +@pytest.mark.unit +def test_ubc_type(mas_agent_api): + assert mas_agent_api.type == "MasAgentAPI" + + +@pytest.mark.unit +def test_serialization(mas_agent_api): + assert ( + mas_agent_api.id + == MasAgentAPI.model_validate_json(mas_agent_api.model_dump_json()).id + ) + + +def test_send_message(mas_agent_api): + """Test that send_message calls transport.send_message with correct arguments.""" + message = {"content": "Test Message"} + recipient_id = "agent_123" + + mas_agent_api.send_message(message, recipient_id) + + mas_agent_api.transport.send_message.assert_called_once_with(message, recipient_id) + + +def test_subscribe(mas_agent_api): + """Test that subscribe calls transport.subscribe with correct topic.""" + topic = "test_topic" + + mas_agent_api.subscribe(topic) + + +def test_publish(mas_agent_api): + """Test that publish calls transport.publish with correct topic.""" + topic = "test_topic" + + mas_agent_api.publish(message=topic)