diff --git a/backend/src/agents/main_agent/__init__.py b/backend/src/agents/main_agent/__init__.py index eef981bb..cbcc55cb 100644 --- a/backend/src/agents/main_agent/__init__.py +++ b/backend/src/agents/main_agent/__init__.py @@ -30,6 +30,7 @@ from .main_agent import MainAgent from .base_agent import BaseAgent from .chat_agent import ChatAgent +from .agent_types import create_agent, register_agent_type, get_available_types from .core import ModelConfig, SystemPromptBuilder from .session import SessionFactory from .tools import ToolRegistry, ToolFilter, GatewayIntegration, create_default_registry @@ -56,6 +57,9 @@ "BaseAgent", "ChatAgent", "MainAgent", + "create_agent", + "register_agent_type", + "get_available_types", # Core components "ModelConfig", diff --git a/backend/src/agents/main_agent/agent_types.py b/backend/src/agents/main_agent/agent_types.py new file mode 100644 index 00000000..ea67e0c6 --- /dev/null +++ b/backend/src/agents/main_agent/agent_types.py @@ -0,0 +1,60 @@ +""" +Agent type registry and factory function. + +Provides a single entry point for creating agents by type string. +New agent types register here as they're implemented. +""" + +import logging +from typing import Optional, List + +from agents.main_agent.base_agent import BaseAgent + +logger = logging.getLogger(__name__) + + +# Registry of agent type string → class +# New agent types add themselves here as they're implemented +_AGENT_TYPES = {} + + +def register_agent_type(type_name: str, agent_class: type) -> None: + """Register an agent class for a given type string.""" + _AGENT_TYPES[type_name] = agent_class + + +def get_available_types() -> List[str]: + """Return list of registered agent type names.""" + return list(_AGENT_TYPES.keys()) + + +def create_agent(agent_type: str = "chat", **kwargs) -> BaseAgent: + """ + Create an agent by type string. + + Args: + agent_type: Agent type ("chat", "skill", "voice"). Default: "chat". + **kwargs: Passed directly to the agent constructor (session_id, user_id, etc.) + + Returns: + BaseAgent subclass instance + + Raises: + ValueError: If agent_type is not registered + """ + agent_class = _AGENT_TYPES.get(agent_type) + if agent_class is None: + available = ", ".join(sorted(_AGENT_TYPES.keys())) + raise ValueError(f"Unknown agent_type '{agent_type}'. Available: {available}") + + logger.info(f"Creating {agent_type} agent ({agent_class.__name__})") + return agent_class(**kwargs) + + +# Register built-in agent types +def _register_defaults(): + from agents.main_agent.chat_agent import ChatAgent + register_agent_type("chat", ChatAgent) + + +_register_defaults() diff --git a/backend/tests/agents/main_agent/test_agent_types.py b/backend/tests/agents/main_agent/test_agent_types.py new file mode 100644 index 00000000..da21a2ec --- /dev/null +++ b/backend/tests/agents/main_agent/test_agent_types.py @@ -0,0 +1,72 @@ +"""Tests for agent type registry and factory function.""" + +import pytest +from agents.main_agent.agent_types import ( + create_agent, + register_agent_type, + get_available_types, + _AGENT_TYPES, +) +from agents.main_agent.base_agent import BaseAgent +from agents.main_agent.chat_agent import ChatAgent +from agents.main_agent.main_agent import MainAgent + + +class TestGetAvailableTypes: + """Req AT-1: Available types discovery.""" + + def test_chat_is_registered_by_default(self): + assert "chat" in get_available_types() + + def test_returns_list_of_strings(self): + types = get_available_types() + assert isinstance(types, list) + assert all(isinstance(t, str) for t in types) + + +class TestRegisterAgentType: + """Req AT-2: Dynamic agent type registration.""" + + def test_register_new_type(self): + class DummyAgent(BaseAgent): + def _create_agent(self): pass + async def stream_async(self, message, **kwargs): yield "" + + register_agent_type("dummy", DummyAgent) + assert "dummy" in get_available_types() + + # Cleanup + del _AGENT_TYPES["dummy"] + + def test_register_overwrites_existing(self): + original = _AGENT_TYPES.get("chat") + try: + register_agent_type("chat", MainAgent) + assert _AGENT_TYPES["chat"] is MainAgent + finally: + _AGENT_TYPES["chat"] = original + + +class TestCreateAgent: + """Req AT-3: Factory function routing.""" + + def test_unknown_type_raises_value_error(self): + with pytest.raises(ValueError, match="Unknown agent_type 'nonexistent'"): + create_agent("nonexistent", session_id="test") + + def test_error_message_lists_available_types(self): + with pytest.raises(ValueError, match="chat"): + create_agent("nonexistent", session_id="test") + + +class TestMainAgentBackwardCompat: + """Req AT-4: MainAgent remains a valid ChatAgent subclass.""" + + def test_mainagent_is_subclass_of_chatagent(self): + assert issubclass(MainAgent, ChatAgent) + + def test_mainagent_is_subclass_of_baseagent(self): + assert issubclass(MainAgent, BaseAgent) + + def test_chatagent_is_subclass_of_baseagent(self): + assert issubclass(ChatAgent, BaseAgent)