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/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/IAgentCommands.py b/pkgs/core/swarmauri_core/agent_apis/IAgentCommands.py deleted file mode 100644 index 175848a6c..000000000 --- a/pkgs/core/swarmauri_core/agent_apis/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/IAgentRouterCRUD.py b/pkgs/core/swarmauri_core/agent_apis/IAgentRouterCRUD.py deleted file mode 100644 index 91eecbc4e..000000000 --- a/pkgs/core/swarmauri_core/agent_apis/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/__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/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 adf100601..0a74e860c 100644 --- a/pkgs/swarmauri/swarmauri/utils/_get_subclasses.py +++ b/pkgs/swarmauri/swarmauri/utils/_get_subclasses.py @@ -1,72 +1,41 @@ import importlib -import re +import inspect +import logging +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. - :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. +def get_classes_from_module(resource_name: str): """ - # 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__", []) - - # 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): + Pass something like 'llms' to import 'swarmauri.llms.concrete' + and retrieve all loaded classes. """ - Dynamically imports a module and retrieves the class name of the module. + resource_name = resource_name.lower() - :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() + full_module = f"swarmauri.{resource_name}s.concrete" + module = importlib.import_module(full_module) - # Construct the full module path dynamically - full_module_path = f"swarmauri.{module_name_lower}s.concrete" + classes = {} + for name, obj in inspect.getmembers(module): + if isinstance(obj, LazyLoader): + obj = obj._load_class() + if inspect.isclass(obj): + classes[name] = obj - try: - # Import the module dynamically - module = importlib.import_module(full_module_path) + logging.info(f"Classes found in module {module}: {classes}") + return classes - # 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}") +def get_class_from_module(resource_name, class_name): + resource_name = resource_name.lower() - for cls_name in class_names: - if cls_name == class_name: - return getattr(module, class_name) - return None + full_module = f"swarmauri.{resource_name}s.concrete" + module = importlib.import_module(full_module) - 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..6ec7fc117 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 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,7 +52,16 @@ 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="TestAgent") + assert isinstance(instance, TestAgent) + 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) @@ -61,6 +79,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(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 11695f7ec..0a1165e8e 100644 --- a/pkgs/swarmauri/tests/unit/factories/Factory_unit_test.py +++ b/pkgs/swarmauri/tests/unit/factories/Factory_unit_test.py @@ -41,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( - ModuleNotFoundError, match="Resource 'UnknownResource' is not registered." + ModuleNotFoundError, + match="No module named 'swarmauri.unknownresources'", ): factory.create("UnknownResource", "BeautifulSoupElementParser")