/
githubmirror
/
sgr-agent-core
Обзор
Документация
Войти
/
githubmirror
/
sgr-agent-core
Код
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
tests/test_agent_factory.py
1 099 строк
47 KB
hijera
pre-commit update
02 апр 2026, 08:57
02 апр 2026, 08:57
8e1c3b5
Код
Авторство
О чём код?
"""Tests for agent factory and configuration-based agent creation. This module contains tests for AgentFactory and dynamic agent instantiation. """ from unittest.mock import AsyncMock, Mock, patch import httpx import pytest from openai import AsyncOpenAI from sgr_agent_core.agent_definition import ( AgentDefinition, ExecutionConfig, LangfuseConfig, LLMConfig, PromptsConfig, ToolDefinition, ) from sgr_agent_core.agent_factory import AgentFactory, LangfuseImportError from sgr_agent_core.agents import ( DialogAgent, SGRAgent, SGRToolCallingAgent, ToolCallingAgent, ) from sgr_agent_core.base_agent import BaseAgent from sgr_agent_core.stream import OpenAIStreamingGenerator, OpenWebUIStreamingGenerator from sgr_agent_core.tools import BaseTool, ReasoningTool, RunCommandTool def mock_global_config(): """Create a mock GlobalConfig for tests.""" mock_config = Mock() mock_config.llm = LLMConfig(api_key="default-key", base_url="https://api.openai.com/v1") mock_config.prompts = PromptsConfig( system_prompt_str="Default system prompt", initial_user_request_str="Default initial request", clarification_response_str="Default clarification response", ) mock_config.execution = ExecutionConfig() mock_config.search = None mock_config.langfuse = LangfuseConfig() mock_config.tools = {} # Create a mock MCP config that has model_copy and model_dump methods mock_mcp = Mock() mock_mcp.model_copy.return_value = mock_mcp mock_mcp.model_dump.return_value = {} mock_config.mcp = mock_mcp # Patch GlobalConfig where it's imported inside the validator # GlobalConfig is imported inside the method from agent_config, so we need to patch it there # The import happens at runtime inside the validator method return patch("sgr_agent_core.agent_config.GlobalConfig", return_value=mock_config) class TestAgentFactory: """Tests for dynamic agent creation from configuration.""" @pytest.mark.asyncio async def test_create_agent_from_definition(self): """Test creating agent from AgentDefinition.""" with ( patch("sgr_agent_core.agent_factory.MCP2ToolConverter.build_tools_from_mcp", return_value=[]), mock_global_config(), ): agent_def = AgentDefinition( name="sgr_agent", base_class=SGRAgent, tools=["reasoningtool"], # Use tool name from registry llm={"api_key": "test-key", "base_url": "https://api.openai.com/v1"}, prompts={ "system_prompt_str": "Test system prompt", "initial_user_request_str": "Test initial request", "clarification_response_str": "Test clarification response", }, execution={}, ) agent = await AgentFactory.create(agent_def, task_messages=[{"role": "user", "content": "Test task"}]) assert isinstance(agent, SGRAgent) assert len(agent.task_messages) == 1 assert agent.task_messages[0]["content"] == "Test task" assert agent.name == "sgr_agent" @pytest.mark.asyncio async def test_create_all_agent_types(self): """Test creating all available agent types.""" with ( patch("sgr_agent_core.agent_factory.MCP2ToolConverter.build_tools_from_mcp", return_value=[]), mock_global_config(), ): task = "Universal test task" agent_classes = [ DialogAgent, SGRAgent, SGRToolCallingAgent, ToolCallingAgent, ] for agent_class in agent_classes: agent_def = AgentDefinition( name=agent_class.name, base_class=agent_class, tools=["reasoningtool"], # Use tool name from registry llm={"api_key": "test-key", "base_url": "https://api.openai.com/v1"}, prompts={ "system_prompt_str": "Test system prompt", "initial_user_request_str": "Test initial request", "clarification_response_str": "Test clarification response", }, execution={}, ) agent = await AgentFactory.create(agent_def, task_messages=[{"role": "user", "content": task}]) assert isinstance(agent, BaseAgent) assert len(agent.task_messages) == 1 assert agent.task_messages[0]["content"] == task assert agent.name == agent_class.name @pytest.mark.asyncio async def test_agent_factory_with_custom_params(self): """Test creating agents with custom execution parameters.""" with ( patch("sgr_agent_core.agent_factory.MCP2ToolConverter.build_tools_from_mcp", return_value=[]), mock_global_config(), ): agent_def = AgentDefinition( name="sgr_tool_calling_agent", base_class=SGRToolCallingAgent, tools=["reasoningtool"], # Use tool name from registry llm={"api_key": "test-key", "base_url": "https://api.openai.com/v1"}, prompts={ "system_prompt_str": "Test system prompt", "initial_user_request_str": "Test initial request", "clarification_response_str": "Test clarification response", }, execution={"max_clarifications": 5, "max_iterations": 15, "max_searches": 10}, ) agent = await AgentFactory.create(agent_def, task_messages=[{"role": "user", "content": "Custom task"}]) assert len(agent.task_messages) == 1 assert agent.task_messages[0]["content"] == "Custom task" assert agent.config.execution.max_clarifications == 5 assert agent.config.execution.max_iterations == 15 assert agent.config.execution.max_searches == 10 @pytest.mark.asyncio async def test_create_agent_with_streaming_generator_open_webui(self): """Test creating agent with streaming_generator open_webui uses OpenWebUIStreamingGenerator.""" with ( patch("sgr_agent_core.agent_factory.MCP2ToolConverter.build_tools_from_mcp", return_value=[]), mock_global_config(), ): agent_def = AgentDefinition( name="sgr_tool_calling_agent", base_class=SGRToolCallingAgent, tools=["reasoningtool"], llm={"api_key": "test-key", "base_url": "https://api.openai.com/v1"}, prompts={ "system_prompt_str": "Test", "initial_user_request_str": "Test", "clarification_response_str": "Test", }, execution={"streaming_generator": "open_webui"}, ) agent = await AgentFactory.create(agent_def, task_messages=[{"role": "user", "content": "Test"}]) assert type(agent.streaming_generator) is OpenWebUIStreamingGenerator @pytest.mark.asyncio async def test_create_agent_with_streaming_generator_openai(self): """Test creating agent with streaming_generator openai or default uses OpenAIStreamingGenerator.""" with ( patch("sgr_agent_core.agent_factory.MCP2ToolConverter.build_tools_from_mcp", return_value=[]), mock_global_config(), ): for execution in ({"streaming_generator": "openai"}, {}): agent_def = AgentDefinition( name="sgr_agent", base_class=SGRAgent, tools=["reasoningtool"], llm={"api_key": "test-key", "base_url": "https://api.openai.com/v1"}, prompts={ "system_prompt_str": "Test", "initial_user_request_str": "Test", "clarification_response_str": "Test", }, execution=execution, ) agent = await AgentFactory.create(agent_def, task_messages=[{"role": "user", "content": "Test"}]) assert type(agent.streaming_generator) is OpenAIStreamingGenerator def test_resolve_streaming_generator_unknown_raises(self): """Test that _resolve_streaming_generator with unknown name raises ValueError with available names.""" with pytest.raises(ValueError) as exc_info: AgentFactory._resolve_streaming_generator("unknown_format") assert "Streaming generator 'unknown_format' not found" in str(exc_info.value) assert "Available:" in str(exc_info.value) @pytest.mark.asyncio async def test_agent_creation_preserves_agent_properties(self): """Test that agent creation preserves specific agent properties.""" with ( patch("sgr_agent_core.agent_factory.MCP2ToolConverter.build_tools_from_mcp", return_value=[]), mock_global_config(), ): agent_def = AgentDefinition( name="sgr_tool_calling_agent", base_class=SGRToolCallingAgent, tools=["reasoningtool"], # Use tool name from registry llm={"api_key": "test-key", "base_url": "https://api.openai.com/v1"}, prompts={ "system_prompt_str": "Test system prompt", "initial_user_request_str": "Test initial request", "clarification_response_str": "Test clarification response", }, execution={}, ) agent = await AgentFactory.create(agent_def, task_messages=[{"role": "user", "content": "Test"}]) # Should have tool_choice property for tool calling agents if hasattr(agent, "tool_choice"): assert agent.tool_choice == "required" class TestConfigurationBasedAgentCreation: """Tests for creating agents based on configuration patterns.""" @pytest.mark.asyncio async def test_agent_config_integration(self): """Test that agents properly integrate configuration from settings.""" with ( patch("sgr_agent_core.agent_factory.MCP2ToolConverter.build_tools_from_mcp", return_value=[]), mock_global_config(), ): agent_def = AgentDefinition( name="sgr_agent", base_class=SGRAgent, tools=["reasoningtool"], # Use tool name from registry llm={"api_key": "test-key", "base_url": "https://api.openai.com/v1"}, prompts={ "system_prompt_str": "Test system prompt", "initial_user_request_str": "Test initial request", "clarification_response_str": "Test clarification response", }, execution={}, ) agent = await AgentFactory.create( agent_def, task_messages=[{"role": "user", "content": "Test config integration"}] ) assert len(agent.task_messages) == 1 assert agent.task_messages[0]["content"] == "Test config integration" assert agent.name == "sgr_agent" def test_agent_name_consistency(self): """Test that agent names are consistent with class names.""" agent_classes = [ SGRAgent, SGRToolCallingAgent, ToolCallingAgent, ] for agent_class in agent_classes: assert hasattr(agent_class, "name") assert agent_class.name in [ "sgr_agent", "sgr_tool_calling_agent", "tool_calling_agent", ] @pytest.mark.asyncio async def test_multiple_agent_creation_independence(self): """Test that multiple agents can be created independently.""" with ( patch("sgr_agent_core.agent_factory.MCP2ToolConverter.build_tools_from_mcp", return_value=[]), mock_global_config(), ): tasks = ["Task 1", "Task 2", "Task 3"] agent_classes = [SGRAgent, SGRToolCallingAgent, ToolCallingAgent] agents = [] for i, agent_class in enumerate(agent_classes): agent_def = AgentDefinition( name=agent_class.name, base_class=agent_class, tools=["reasoningtool"], # Use tool name from registry llm={"api_key": "test-key", "base_url": "https://api.openai.com/v1"}, prompts={ "system_prompt_str": "Test system prompt", "initial_user_request_str": "Test initial request", "clarification_response_str": "Test clarification response", }, execution={}, ) agent = await AgentFactory.create(agent_def, task_messages=[{"role": "user", "content": tasks[i]}]) agents.append(agent) # Verify all agents are independent for i, agent in enumerate(agents): assert len(agent.task_messages) == 1 assert agent.task_messages[0]["content"] == tasks[i] assert agent.id != agents[(i + 1) % len(agents)].id # Different IDs # Verify different types if len(agents) > 1: assert type(agents[0]) is not type(agents[1]) # noqa class TestAgentCreationEdgeCases: """Tests for edge cases in agent creation.""" @pytest.mark.asyncio async def test_empty_task_creation(self): """Test creating agent with empty task.""" with ( patch("sgr_agent_core.agent_factory.MCP2ToolConverter.build_tools_from_mcp", return_value=[]), mock_global_config(), ): agent_def = AgentDefinition( name="sgr_agent", base_class=SGRAgent, tools=["reasoningtool"], # Use tool name from registry llm={"api_key": "test-key", "base_url": "https://api.openai.com/v1"}, prompts={ "system_prompt_str": "Test system prompt", "initial_user_request_str": "Test initial request", "clarification_response_str": "Test clarification response", }, execution={}, ) agent = await AgentFactory.create(agent_def, task_messages=[{"role": "user", "content": ""}]) assert len(agent.task_messages) == 1 assert agent.task_messages[0]["content"] == "" assert agent.name == "sgr_agent" @pytest.mark.asyncio async def test_agent_creation_with_toolkit(self): """Test creating agent with custom toolkit.""" class CustomTool(BaseTool): tool_name = "custom_tool" description = "A custom test tool" with ( patch("sgr_agent_core.agent_factory.MCP2ToolConverter.build_tools_from_mcp", return_value=[]), mock_global_config(), ): agent_def = AgentDefinition( name="sgr_agent", base_class=SGRAgent, tools=["custom_tool"], # Use tool name from registry llm={"api_key": "test-key", "base_url": "https://api.openai.com/v1"}, prompts={ "system_prompt_str": "Test system prompt", "initial_user_request_str": "Test initial request", "clarification_response_str": "Test clarification response", }, execution={}, ) agent = await AgentFactory.create(agent_def, task_messages=[{"role": "user", "content": "Test"}]) # Verify custom tool was added to toolkit assert CustomTool in agent.toolkit class TestAgentFactoryClientCreation: """Tests for OpenAI client creation in AgentFactory.""" def test_create_client_without_proxy(self): """Test creating OpenAI client without proxy.""" llm_config = LLMConfig( api_key="test-key", base_url="https://api.openai.com/v1", ) client = AgentFactory._create_client(llm_config) assert client is not None assert client.api_key == "test-key" assert str(client.base_url).rstrip("/") == "https://api.openai.com/v1" def test_create_client_with_proxy(self): """Test creating OpenAI client with proxy.""" llm_config = LLMConfig( api_key="test-key", base_url="https://api.openai.com/v1", proxy="http://127.0.0.1:8080", ) client = AgentFactory._create_client(llm_config) assert client is not None assert client.api_key == "test-key" assert str(client.base_url).rstrip("/") == "https://api.openai.com/v1" assert client._client is not None def test_create_client_with_socks_proxy(self): """Test creating OpenAI client with SOCKS proxy.""" llm_config = LLMConfig( api_key="test-key", base_url="https://api.openai.com/v1", proxy="socks5://127.0.0.1:1081", ) client = AgentFactory._create_client(llm_config) assert client is not None assert client.api_key == "test-key" assert client._client is not None def test_create_client_uses_langfuse_when_enabled(self, monkeypatch): """Test that _create_client uses Langfuse AsyncOpenAI when enabled.""" from types import SimpleNamespace class DummyAsyncOpenAI: def __init__(self, **kwargs): self.kwargs = kwargs mock_config = Mock() mock_config.langfuse = LangfuseConfig(enabled=True) # no credentials with ( patch("sgr_agent_core.agent_factory.GlobalConfig", return_value=mock_config), patch( "sgr_agent_core.agent_factory.import_module", return_value=SimpleNamespace(AsyncOpenAI=DummyAsyncOpenAI), ), ): llm_config = LLMConfig( api_key="test-key", base_url="https://api.openai.com/v1", ) client = AgentFactory._create_client(llm_config) assert isinstance(client, DummyAsyncOpenAI) assert client.kwargs["api_key"] == "test-key" assert client.kwargs["base_url"] == "https://api.openai.com/v1" def test_create_client_raises_when_langfuse_missing(self): """Test that _create_client raises if Langfuse is enabled but package is missing.""" mock_config = Mock() mock_config.langfuse = LangfuseConfig(enabled=True) with ( patch("sgr_agent_core.agent_factory.GlobalConfig", return_value=mock_config), patch("sgr_agent_core.agent_factory.import_module", side_effect=ImportError), ): llm_config = LLMConfig( api_key="test-key", base_url="https://api.openai.com/v1", ) with pytest.raises(LangfuseImportError): AgentFactory._create_client(llm_config) def test_create_client_inits_langfuse_with_credentials(self): """Test that Langfuse() is initialized with explicit credentials from config.""" from types import SimpleNamespace class DummyAsyncOpenAI: def __init__(self, **kwargs): self.kwargs = kwargs class DummyLangfuse: def __init__(self, **kwargs): self.kwargs = kwargs dummy_langfuse_module = SimpleNamespace(Langfuse=DummyLangfuse) dummy_openai_module = SimpleNamespace(AsyncOpenAI=DummyAsyncOpenAI) mock_config = Mock() mock_config.langfuse = LangfuseConfig( enabled=True, public_key="pk-test", secret_key="sk-test", host="http://localhost:3000", ) def fake_import(name): if name == "langfuse": return dummy_langfuse_module if name == "langfuse.openai": return dummy_openai_module raise ImportError(name) with ( patch("sgr_agent_core.agent_factory.GlobalConfig", return_value=mock_config), patch("sgr_agent_core.agent_factory.import_module", side_effect=fake_import), ): llm_config = LLMConfig(api_key="test-key", base_url="https://api.openai.com/v1") client = AgentFactory._create_client(llm_config) assert isinstance(client, DummyAsyncOpenAI) # Verify Langfuse was initialized with explicit credentials @pytest.mark.asyncio async def test_stream_request_with_extra_parameters(self): """Test that additional parameters from LLMConfig (extra='allow') are passed to stream requests.""" from sgr_agent_core.agents.tool_calling_agent import ToolCallingAgent # Create LLMConfig with additional parameters for API requests llm_config = LLMConfig( api_key="test-key", base_url="https://api.openai.com/v1", model="gpt-4o-mini", top_p=0.9, # Additional parameter for API requests top_k=40, # Additional parameter for API requests ) # Create real AsyncOpenAI client with mocked HTTP client # This allows us to test real OpenAI SDK behavior without making actual HTTP requests mock_http_client = AsyncMock(spec=httpx.AsyncClient) real_client = AsyncOpenAI( api_key="test-key", base_url="https://api.openai.com/v1", http_client=mock_http_client, ) # Create mock stream response async def async_iter(self): return yield mock_stream = AsyncMock() mock_stream.__aiter__ = async_iter mock_stream.get_final_completion = AsyncMock( return_value=Mock( choices=[ Mock( message=Mock( tool_calls=[ Mock( function=Mock( parsed_arguments=ReasoningTool( reasoning_steps=["Step 1", "Step 2"], current_situation="Test", plan_status="Test", enough_data=True, remaining_steps=["Next step"], task_completed=True, ) ) ) ] ) ) ] ) ) mock_stream_context = AsyncMock() mock_stream_context.__aenter__ = AsyncMock(return_value=mock_stream) mock_stream_context.__aexit__ = AsyncMock(return_value=None) # Patch chat.completions.stream to capture arguments while using real client with patch.object( real_client.chat.completions, "stream", return_value=mock_stream_context ) as mock_stream_method: # Create agent with real client (OpenWebUIStreamingGenerator has add_tool_call) agent = ToolCallingAgent( task_messages=[{"role": "user", "content": "Test task"}], openai_client=real_client, agent_config=AgentDefinition( name="test_agent", base_class=ToolCallingAgent, tools=["reasoningtool"], # Use tool name from registry llm=llm_config, ), toolkit=[ReasoningTool], streaming_generator=OpenWebUIStreamingGenerator, ) await agent._select_action_phase() # Verify additional parameters from extra="allow" are passed to stream request call_kwargs = mock_stream_method.call_args.kwargs assert call_kwargs["top_p"] == 0.9 assert call_kwargs["top_k"] == 40 assert call_kwargs["model"] == "gpt-4o-mini" @pytest.mark.asyncio async def test_stream_request_with_invalid_parameter_raises_error(self): """Test that invalid/unsupported parameters raise TypeError when passed to stream requests.""" from sgr_agent_core.agents.tool_calling_agent import ToolCallingAgent # Create LLMConfig with invalid parameter (not supported by OpenAI API) llm_config = LLMConfig( api_key="test-key", base_url="https://api.openai.com/v1", model="gpt-4o-mini", my_custom_parameter="invalid_value", # Invalid parameter ) # Create real AsyncOpenAI client with mocked HTTP client # This allows us to test real OpenAI SDK validation without making actual HTTP requests mock_http_client = AsyncMock(spec=httpx.AsyncClient) real_client = AsyncOpenAI( api_key="test-key", base_url="https://api.openai.com/v1", http_client=mock_http_client, ) # Create agent with invalid parameter agent = ToolCallingAgent( task_messages=[{"role": "user", "content": "Test task"}], openai_client=real_client, agent_config=AgentDefinition( name="test_agent", base_class=ToolCallingAgent, tools=["reasoningtool"], # Use tool name from registry llm=llm_config, ), toolkit=[ReasoningTool], ) # Verify that invalid parameter raises TypeError from real OpenAI client # OpenAI SDK validates parameters before making HTTP requests with pytest.raises(TypeError, match="got an unexpected keyword argument 'my_custom_parameter'"): await agent._select_action_phase() class TestAgentFactoryRegistryIntegration: """Tests for AgentFactory integration with registries.""" @pytest.mark.asyncio async def test_create_agent_with_string_base_class(self): """Test creating agent with string base_class name from registry.""" with ( patch("sgr_agent_core.agent_factory.MCP2ToolConverter.build_tools_from_mcp", return_value=[]), mock_global_config(), ): # Use string name instead of class agent_def = AgentDefinition( name="sgr_agent", base_class="sgr_agent", # String name tools=["reasoningtool"], # Use tool name from registry llm={"api_key": "test-key", "base_url": "https://api.openai.com/v1"}, prompts={ "system_prompt_str": "Test system prompt", "initial_user_request_str": "Test initial request", "clarification_response_str": "Test clarification response", }, execution={}, ) agent = await AgentFactory.create(agent_def, task_messages=[{"role": "user", "content": "Test task"}]) assert isinstance(agent, SGRAgent) assert len(agent.task_messages) == 1 assert agent.task_messages[0]["content"] == "Test task" @pytest.mark.asyncio async def test_create_agent_with_string_tool(self): """Test creating agent with string tool name from registry.""" with ( patch("sgr_agent_core.agent_factory.MCP2ToolConverter.build_tools_from_mcp", return_value=[]), mock_global_config(), ): # Use string name instead of class agent_def = AgentDefinition( name="sgr_agent", base_class=SGRAgent, tools=["reasoningtool"], # String name llm={"api_key": "test-key", "base_url": "https://api.openai.com/v1"}, prompts={ "system_prompt_str": "Test system prompt", "initial_user_request_str": "Test initial request", "clarification_response_str": "Test clarification response", }, execution={}, ) agent = await AgentFactory.create(agent_def, task_messages=[{"role": "user", "content": "Test task"}]) assert isinstance(agent, SGRAgent) # Verify that ReasoningTool was resolved from string and added to toolkit # Note: SGRAgent may transform toolkit, so we check that toolkit is not empty assert len(agent.toolkit) > 0 @pytest.mark.asyncio async def test_create_agent_with_class_tools(self): """Test creating agent with tool classes directly.""" class CustomTool(BaseTool): tool_name = "custom_tool" description = "A custom test tool" with ( patch("sgr_agent_core.agent_factory.MCP2ToolConverter.build_tools_from_mcp", return_value=[]), mock_global_config(), ): agent_def = AgentDefinition( name="sgr_agent", base_class=SGRAgent, tools=[CustomTool, ReasoningTool], # Classes directly llm={"api_key": "test-key", "base_url": "https://api.openai.com/v1"}, prompts={ "system_prompt_str": "Test system prompt", "initial_user_request_str": "Test initial request", "clarification_response_str": "Test clarification response", }, execution={}, ) agent = await AgentFactory.create(agent_def, task_messages=[{"role": "user", "content": "Test task"}]) # Verify that both tools were added to toolkit # Note: SGRAgent may transform toolkit, so we check that toolkit contains expected tools assert CustomTool in agent.toolkit or any( hasattr(tool, "tool_name") and tool.tool_name == "custom_tool" for tool in agent.toolkit ) assert ReasoningTool in agent.toolkit or any( hasattr(tool, "tool_name") and tool.tool_name == "reasoningtool" for tool in agent.toolkit ) assert len(agent.toolkit) >= 1 @pytest.mark.asyncio async def test_create_agent_with_mixed_tools(self): """Test creating agent with both class and string tool names.""" class CustomTool(BaseTool): tool_name = "custom_tool" description = "A custom test tool" with ( patch("sgr_agent_core.agent_factory.MCP2ToolConverter.build_tools_from_mcp", return_value=[]), mock_global_config(), ): agent_def = AgentDefinition( name="sgr_agent", base_class=SGRAgent, tools=[CustomTool, "reasoningtool"], # Class and string mixed llm={"api_key": "test-key", "base_url": "https://api.openai.com/v1"}, prompts={ "system_prompt_str": "Test system prompt", "initial_user_request_str": "Test initial request", "clarification_response_str": "Test clarification response", }, execution={}, ) agent = await AgentFactory.create(agent_def, task_messages=[{"role": "user", "content": "Test task"}]) # Verify that both tools were resolved and added to toolkit # Note: SGRAgent may transform toolkit, so we check that toolkit contains expected tools assert CustomTool in agent.toolkit or any( hasattr(tool, "tool_name") and tool.tool_name == "custom_tool" for tool in agent.toolkit ) assert ReasoningTool in agent.toolkit or any( hasattr(tool, "tool_name") and tool.tool_name == "reasoningtool" for tool in agent.toolkit ) assert len(agent.toolkit) >= 1 @pytest.mark.asyncio async def test_create_agent_with_tools_dict_config_passes_tool_configs(self): """Test that tools as list of dicts (name + kwargs) populate agent.tool_configs.""" with ( patch("sgr_agent_core.agent_factory.MCP2ToolConverter.build_tools_from_mcp", return_value=[]), mock_global_config(), ): agent_def = AgentDefinition( name="sgr_agent", base_class=SGRAgent, tools=[{"reasoningtool": {"custom_key": "custom_value"}}], llm={"api_key": "test-key", "base_url": "https://api.openai.com/v1"}, prompts={ "system_prompt_str": "Test system prompt", "initial_user_request_str": "Test initial request", "clarification_response_str": "Test clarification response", }, execution={}, ) agent = await AgentFactory.create(agent_def, task_messages=[{"role": "user", "content": "Test task"}]) assert isinstance(agent, SGRAgent) assert agent.tool_configs.get("reasoningtool") == {"custom_key": "custom_value"} @pytest.mark.asyncio async def test_global_tool_config_merged_into_tool_configs(self): """Test that global tool definition params (config.tools) are merged at AgentDefinition level and passed as tool kwargs.""" mock_config = Mock() mock_config.llm = LLMConfig(api_key="test-key", base_url="https://api.openai.com/v1") mock_config.prompts = PromptsConfig( system_prompt_str="p", initial_user_request_str="p", clarification_response_str="p" ) mock_config.execution = ExecutionConfig() mock_config.search = None mock_config.tools = { "reasoningtool": ToolDefinition( name="reasoningtool", base_class=ReasoningTool, max_results=5, global_param="from_global", ), } mock_mcp = Mock() mock_mcp.model_copy.return_value = mock_mcp mock_mcp.model_dump.return_value = {} mock_config.mcp = mock_mcp with ( patch("sgr_agent_core.agent_factory.MCP2ToolConverter.build_tools_from_mcp", return_value=[]), patch("sgr_agent_core.agent_config.GlobalConfig", return_value=mock_config), ): agent_def = AgentDefinition( name="sgr_agent", base_class=SGRAgent, tools=["reasoningtool"], llm={"api_key": "test-key", "base_url": "https://api.openai.com/v1"}, prompts={ "system_prompt_str": "p", "initial_user_request_str": "p", "clarification_response_str": "p", }, execution={}, ) agent = await AgentFactory.create(agent_def, task_messages=[{"role": "user", "content": "Test"}]) assert agent.tool_configs.get("reasoningtool") == { "max_results": 5, "global_param": "from_global", } @pytest.mark.asyncio async def test_inline_tool_kwargs_override_global(self): """Test that inline tool dict kwargs override global tool config.""" mock_config = Mock() mock_config.llm = LLMConfig(api_key="test-key", base_url="https://api.openai.com/v1") mock_config.prompts = PromptsConfig( system_prompt_str="p", initial_user_request_str="p", clarification_response_str="p" ) mock_config.execution = ExecutionConfig() mock_config.search = None mock_config.tools = { "reasoningtool": ToolDefinition( name="reasoningtool", base_class=ReasoningTool, max_results=5, ), } mock_mcp = Mock() mock_mcp.model_copy.return_value = mock_mcp mock_mcp.model_dump.return_value = {} mock_config.mcp = mock_mcp with ( patch("sgr_agent_core.agent_factory.MCP2ToolConverter.build_tools_from_mcp", return_value=[]), patch("sgr_agent_core.agent_config.GlobalConfig", return_value=mock_config), ): agent_def = AgentDefinition( name="sgr_agent", base_class=SGRAgent, tools=[{"reasoningtool": {"max_results": 20}}], llm={"api_key": "test-key", "base_url": "https://api.openai.com/v1"}, prompts={ "system_prompt_str": "p", "initial_user_request_str": "p", "clarification_response_str": "p", }, execution={}, ) agent = await AgentFactory.create(agent_def, task_messages=[{"role": "user", "content": "Test"}]) assert agent.tool_configs.get("reasoningtool") == {"max_results": 20} @pytest.mark.asyncio async def test_create_agent_with_run_command_tool_safe_mode(self): """Agent with RunCommandTool in safe mode is created; sandbox is handled by the tool.""" with ( patch("sgr_agent_core.agent_factory.MCP2ToolConverter.build_tools_from_mcp", return_value=[]), mock_global_config(), ): agent_def = AgentDefinition( name="sgr_agent", base_class=SGRAgent, tools=[{"run_command_tool": {"mode": "safe"}}], llm={"api_key": "test-key", "base_url": "https://api.openai.com/v1"}, prompts={ "system_prompt_str": "p", "initial_user_request_str": "p", "clarification_response_str": "p", }, execution={}, ) agent = await AgentFactory.create(agent_def, task_messages=[{"role": "user", "content": "Test"}]) assert any(t is RunCommandTool for t in agent.toolkit) assert agent.tool_configs.get("runcommandtool", {}).get("mode") == "safe" class TestAgentFactoryErrorHandling: """Tests for error handling in AgentFactory.""" @pytest.mark.asyncio async def test_create_agent_with_invalid_base_class_string(self): """Test creating agent with invalid base_class string name.""" with ( patch("sgr_agent_core.agent_factory.MCP2ToolConverter.build_tools_from_mcp", return_value=[]), mock_global_config(), ): agent_def = AgentDefinition( name="invalid_agent", base_class="nonexistent_agent", # Invalid string name tools=["reasoningtool"], # Use tool name from registry llm={"api_key": "test-key", "base_url": "https://api.openai.com/v1"}, prompts={ "system_prompt_str": "Test system prompt", "initial_user_request_str": "Test initial request", "clarification_response_str": "Test clarification response", }, execution={}, ) with pytest.raises(ValueError, match="Agent base class 'nonexistent_agent' not found"): await AgentFactory.create(agent_def, task_messages=[{"role": "user", "content": "Test task"}]) @pytest.mark.asyncio async def test_create_agent_with_invalid_tool_string(self): """Test that AgentDefinition with invalid tool string raises error at definition time.""" from pydantic import ValidationError with mock_global_config(): with pytest.raises(ValidationError, match="Tool 'nonexistent_tool' not found"): AgentDefinition( name="sgr_agent", base_class=SGRAgent, tools=["nonexistent_tool"], # Invalid string name llm={"api_key": "test-key", "base_url": "https://api.openai.com/v1"}, prompts={ "system_prompt_str": "Test system prompt", "initial_user_request_str": "Test initial request", "clarification_response_str": "Test clarification response", }, execution={}, ) @pytest.mark.asyncio async def test_create_agent_with_invalid_tool_class(self): """Test that AgentDefinition with a non-BaseTool class raises TypeError at definition time.""" class NotATool: """A class that is not a BaseTool subclass.""" pass with mock_global_config(): with pytest.raises(TypeError, match="Imported base_class must be a subclass of BaseTool"): AgentDefinition( name="sgr_agent", base_class=SGRAgent, tools=[NotATool], # Invalid class - not a BaseTool subclass llm={"api_key": "test-key", "base_url": "https://api.openai.com/v1"}, prompts={ "system_prompt_str": "Test system prompt", "initial_user_request_str": "Test initial request", "clarification_response_str": "Test clarification response", }, execution={}, ) @pytest.mark.asyncio async def test_create_agent_with_agent_creation_exception(self): """Test handling exception during agent instantiation.""" with ( patch("sgr_agent_core.agent_factory.MCP2ToolConverter.build_tools_from_mcp", return_value=[]), mock_global_config(), patch.object(SGRAgent, "__init__", side_effect=RuntimeError("Failed to initialize")), ): agent_def = AgentDefinition( name="sgr_agent", base_class=SGRAgent, tools=["reasoningtool"], # Use tool name from registry llm={"api_key": "test-key", "base_url": "https://api.openai.com/v1"}, prompts={ "system_prompt_str": "Test system prompt", "initial_user_request_str": "Test initial request", "clarification_response_str": "Test clarification response", }, execution={}, ) with pytest.raises(ValueError, match="Failed to create agent"): await AgentFactory.create(agent_def, task_messages=[{"role": "user", "content": "Test task"}]) class TestAgentFactoryMCPIntegration: """Tests for MCP tools integration in AgentFactory.""" @pytest.mark.asyncio async def test_create_agent_with_mcp_tools(self): """Test creating agent with MCP tools.""" class MockMCPTool(BaseTool): tool_name = "mcp_tool" description = "Mock MCP tool" mock_mcp_tools = [MockMCPTool] with ( patch( "sgr_agent_core.agent_factory.MCP2ToolConverter.build_tools_from_mcp", return_value=mock_mcp_tools, ), mock_global_config(), ): agent_def = AgentDefinition( name="sgr_agent", base_class=SGRAgent, tools=["reasoningtool"], # Use tool name from registry llm={"api_key": "test-key", "base_url": "https://api.openai.com/v1"}, prompts={ "system_prompt_str": "Test system prompt", "initial_user_request_str": "Test initial request", "clarification_response_str": "Test clarification response", }, execution={}, ) agent = await AgentFactory.create(agent_def, task_messages=[{"role": "user", "content": "Test task"}]) assert MockMCPTool in agent.toolkit # SGRAgent transforms toolkit, so check that toolkit is not empty and contains expected tools assert len(agent.toolkit) >= 2 # ReasoningTool was requested, toolkit should not be empty # SGRAgent wraps tools in NextStepTools, so we just verify toolkit is populated @pytest.mark.asyncio async def test_create_agent_with_mcp_and_regular_tools(self): """Test creating agent with both MCP and regular tools.""" class MockMCPTool1(BaseTool): tool_name = "mcp_tool_1" description = "Mock MCP tool 1" class MockMCPTool2(BaseTool): tool_name = "mcp_tool_2" description = "Mock MCP tool 2" mock_mcp_tools = [MockMCPTool1, MockMCPTool2] with ( patch( "sgr_agent_core.agent_factory.MCP2ToolConverter.build_tools_from_mcp", return_value=mock_mcp_tools, ), mock_global_config(), ): agent_def = AgentDefinition( name="sgr_agent", base_class=SGRAgent, tools=["reasoningtool"], # Use tool name from registry llm={"api_key": "test-key", "base_url": "https://api.openai.com/v1"}, prompts={ "system_prompt_str": "Test system prompt", "initial_user_request_str": "Test initial request", "clarification_response_str": "Test clarification response", }, execution={}, ) agent = await AgentFactory.create(agent_def, task_messages=[{"role": "user", "content": "Test task"}]) assert MockMCPTool1 in agent.toolkit assert MockMCPTool2 in agent.toolkit # SGRAgent transforms toolkit, so check that toolkit contains expected tools assert len(agent.toolkit) >= 3 # ReasoningTool was requested, toolkit should not be empty # SGRAgent wraps tools in NextStepTools, so we just verify toolkit is populated class TestAgentFactoryDefinitionsList: """Tests for getting agent definitions list.""" def test_get_definitions_list(self): """Test getting list of agent definitions from config.""" with patch("sgr_agent_core.agent_factory.GlobalConfig") as mock_global_config: mock_config = Mock() mock_agent_def1 = Mock() mock_agent_def1.name = "agent1" mock_agent_def2 = Mock() mock_agent_def2.name = "agent2" mock_config.agents = {"agent1": mock_agent_def1, "agent2": mock_agent_def2} mock_global_config.return_value = mock_config definitions = AgentFactory.get_definitions_list() assert len(definitions) == 2 assert mock_agent_def1 in definitions assert mock_agent_def2 in definitions def test_get_definitions_list_empty(self): """Test getting empty list when no agents in config.""" with patch("sgr_agent_core.agent_factory.GlobalConfig") as mock_global_config: mock_config = Mock() mock_config.agents = {} mock_global_config.return_value = mock_config definitions = AgentFactory.get_definitions_list() assert len(definitions) == 0 assert definitions == []