from types import SimpleNamespace from unittest.mock import MagicMock import pytest from agents import FunctionTool, ModelSettings, StopAtTools, WebSearchTool from pydantic import BaseModel, Field from agency_swarm import Agent from agency_swarm.integrations.openclaw_model import build_openclaw_responses_model class TaskOutput(BaseModel): task_name: str = Field(..., description="Name of the task") status: str = Field(..., description="Status of the task") priority: int = Field(..., description="Priority level (1-5)") class SimpleOutput(BaseModel): message: str = Field(..., description="Simple message") # --- Initialization Tests --- def test_agent_initialization_with_stop_at_tools_variants(): """Agent should accept StopAtTools-typed and dict-compatible tool_use_behavior values.""" cases = [ StopAtTools(stop_at_tool_names=["ToolA", "ToolB"]), {"stop_at_tool_names": ["ToolC"]}, ] for behavior in cases: agent = Agent(name="AgentStopAtTools", instructions="Test", tool_use_behavior=behavior) assert agent.tool_use_behavior == behavior def test_agent_initialization_core_configuration_variants(): """Core initialization should preserve baseline defaults and explicit tool/model/output settings.""" minimal = Agent(name="Agent1", instructions="Be helpful") assert minimal.name == "Agent1" assert minimal.instructions == "Be helpful" assert minimal.model == "gpt-5.4-mini" assert minimal.tools == [] assert minimal.files_folder is None assert not hasattr(minimal, "response_validator") assert minimal.output_type is None tool = MagicMock(spec=FunctionTool) tool.name = "tool1" configured = Agent( name="ConfiguredAgent", instructions="Use tools", tools=[tool], model_settings=ModelSettings( temperature=0.3, max_tokens=16, top_p=0.5, ), output_type=SimpleOutput, ) assert configured.tools == [tool] assert configured.model_settings.temperature == 0.3 assert configured.model_settings.max_tokens == 16 assert configured.model_settings.top_p == 0.5 assert configured.output_type == SimpleOutput def test_agent_initialization_rejects_deprecated_kwargs() -> None: """Deprecated initialization kwargs should fail fast with clear errors.""" cases: list[tuple[dict[str, object], str]] = [ ( {"temperature": 0.3, "max_prompt_tokens": 16}, r"Deprecated Agent parameters are not supported", ), ( {"reasoning_effort": "medium"}, r"reasoning_effort", ), ( {"truncation_strategy": "auto"}, r"truncation_strategy", ), ( {"response_format": {"type": "json_schema", "json_schema": {"name": "X", "schema": {}}}}, r"response_format", ), ( {"response_format": SimpleOutput}, r"response_format", ), ( {"max_prompt_tokens": 100, "max_completion_tokens": 150}, r"max_prompt_tokens", ), ( { "validation_attempts": 2, "id": "abc123", "tool_resources": {"vs": 1}, "file_ids": ["f1"], "file_search": True, "refresh_from_id": "old", "send_message_tool_class": object, }, r"Deprecated Agent parameters are not supported", ), ] for kwargs, message in cases: with pytest.raises(TypeError, match=message): Agent(name="DeprecatedKwargsAgent", instructions="Test", **kwargs) def test_agent_initialization_output_type_variants(): """Explicit output types should be preserved while omitted output_type stays None.""" assert Agent(name="TaskAgent", instructions="Task agent", output_type=TaskOutput).output_type == TaskOutput assert Agent(name="SimpleAgent", instructions="Simple agent", output_type=SimpleOutput).output_type == SimpleOutput assert Agent(name="BasicAgent", instructions="Basic agent").output_type is None def test_agent_initialization_guardrail_flag_aliases_and_failures() -> None: """Guardrail aliases should map consistently and fail fast for conflicts/legacy kwargs.""" canonical_agent = Agent( name="AliasCanonicalAgent", instructions="Test", raise_input_guardrail_error=True, ) assert canonical_agent.raise_input_guardrail_error is True with pytest.warns(DeprecationWarning, match=r"throw_input_guardrail_error"): deprecated_alias_agent = Agent( name="AliasDeprecatedAgent", instructions="Test", throw_input_guardrail_error=True, ) assert deprecated_alias_agent.raise_input_guardrail_error is True with pytest.warns(DeprecationWarning, match=r"throw_input_guardrail_error"): matching_alias_agent = Agent( name="AliasMatchAgent", instructions="Test", raise_input_guardrail_error=False, throw_input_guardrail_error=False, ) assert matching_alias_agent.raise_input_guardrail_error is False with pytest.raises(TypeError, match=r"Conflicting values for `raise_input_guardrail_error`"): Agent( name="AliasConflictAgent", instructions="Test", raise_input_guardrail_error=True, throw_input_guardrail_error=False, ) with pytest.raises(TypeError) as exc_info: Agent( name="LegacyGuardrailAgent", instructions="Test", return_input_guardrail_errors=False, ) error_message = str(exc_info.value) assert "return_input_guardrail_errors" in error_message assert "raise_input_guardrail_error" in error_message agent = Agent( name="AliasPropertyAgent", instructions="Test", raise_input_guardrail_error=True, ) assert agent.throw_input_guardrail_error is True agent.throw_input_guardrail_error = False assert agent.raise_input_guardrail_error is False def test_agent_initialization_support_flags_override_defaults() -> None: """Capability flags should persist when a plain Agent overrides them.""" agent = Agent( name="RestrictedAgent", instructions="Test", supports_outbound_communication=False, supports_framework_tool_wiring=False, ) assert agent.supports_outbound_communication is False assert agent.supports_framework_tool_wiring is False def test_agent_initialization_skips_framework_tool_wiring_when_disabled(tmp_path) -> None: """Framework-managed tool folders should be ignored when tool wiring is disabled.""" tools_dir = tmp_path / "tools" tools_dir.mkdir() (tools_dir / "loaded_tool.py").write_text( "from agents import function_tool\n\n@function_tool\ndef loaded_tool() -> str:\n return 'loaded'\n", encoding="utf-8", ) agent = Agent( name="RestrictedAgent", instructions="Test", tools_folder=str(tools_dir), supports_framework_tool_wiring=False, ) assert agent.tools == [] def test_agent_initialization_keeps_explicit_files_folder_when_framework_tool_wiring_disabled(tmp_path) -> None: """Explicit files_folder support should survive even when framework-managed tool wiring is disabled.""" files_dir = tmp_path / "docs_vs_abcdefghijklmnop" files_dir.mkdir() agent = Agent( name="RestrictedAgent", instructions="Test", files_folder=str(files_dir), supports_framework_tool_wiring=False, ) assert agent._associated_vector_store_id == "vs_abcdefghijklmnop" assert [tool.__class__.__name__ for tool in agent.tools] == ["FileSearchTool"] def test_agent_initialization_converts_explicit_mcp_servers_when_framework_tool_wiring_disabled( monkeypatch: pytest.MonkeyPatch, ) -> None: converted: list[str] = [] def _convert(agent: Agent) -> None: converted.append(agent.name) monkeypatch.setattr("agency_swarm.agent.core.convert_mcp_servers_to_tools", _convert) Agent( name="RestrictedAgent", instructions="Test", mcp_servers=[SimpleNamespace(name="demo")], supports_framework_tool_wiring=False, ) assert converted == ["RestrictedAgent"] def test_agent_initialization_with_all_parameters(): """Test Agent initialization with all parameters including output_type.""" tool1 = MagicMock(spec=FunctionTool) tool1.name = "tool1" # TEST-ONLY SETUP: Create test directory to enable FileSearchTool auto-addition import tempfile from pathlib import Path from unittest.mock import PropertyMock, patch # Create a temporary test directory with tempfile.TemporaryDirectory(prefix="test_files_") as temp_dir_str: temp_dir = Path(temp_dir_str) test_file = temp_dir / "test.txt" test_file.write_text("test content for FileSearchTool") # Mock the OpenAI client to avoid API key requirement mock_vector_store = MagicMock() mock_vector_store.id = "test_vs_id" mock_client = MagicMock() mock_client.vector_stores.create.return_value = mock_vector_store uploaded_file = MagicMock() uploaded_file.id = "file-1234567890abcdef" uploaded_file.created_at = 1_735_689_600 mock_client.files.create.return_value = uploaded_file vs_file = MagicMock() vs_file.status = "completed" mock_client.vector_stores.files.retrieve.return_value = vs_file # Prevent infinite pagination when syncing vector store files during init list_resp = MagicMock() list_resp.data = [] list_resp.has_more = False list_resp.last_id = None mock_client.vector_stores.files.list.return_value = list_resp with patch.object(Agent, "client_sync", new_callable=PropertyMock) as mock_client_sync: mock_client_sync.return_value = mock_client agent = Agent( name="CompleteAgent", instructions="Complete agent with all params", model="gpt-5.4-mini", tools=[tool1], output_type=TaskOutput, files_folder=str(temp_dir), # Use temporary directory description="A complete test agent", ) assert agent.name == "CompleteAgent" assert agent.instructions == "Complete agent with all params" assert agent.model == "gpt-5.4-mini" assert len(agent.tools) == 2 assert agent.tools[0] == tool1 assert agent.tools[1].__class__.__name__ == "FileSearchTool" # response_validator is completely removed assert not hasattr(agent, "response_validator") assert agent.output_type == TaskOutput assert str(temp_dir) in str(agent.files_folder) # Should contain the temp directory path assert agent.description == "A complete test agent" # --- Instruction File Loading Tests --- def test_agent_instruction_loading_variants(tmp_path): """Instruction inputs should support file paths while preserving plain strings.""" # Create instruction file for absolute path test instruction_file = tmp_path / "agent_instructions.md" instruction_content = "You are a helpful assistant. Always be polite." instruction_file.write_text(instruction_content) # Absolute path agent = Agent(name="TestAgent", instructions=str(instruction_file), model="gpt-5.4-mini") assert agent.instructions == instruction_content # Relative path resolved from caller directory relative_agent = Agent(name="TestAgent", instructions="../data/files/instructions.md", model="gpt-5.4-mini") assert relative_agent.instructions == "Test instructions" instruction_text = "Direct instruction text, not a file path" agent = Agent(name="TestAgent", instructions=instruction_text, model="gpt-5.4-mini") assert agent.instructions == instruction_text def test_agent_initialization_model_settings_defaults_and_overrides(): """Initialization should keep SDK defaults and preserve explicit settings overrides.""" default_agent = Agent(name="TruncDefault", instructions="Test") assert default_agent.model_settings.truncation == "auto" explicit_agent = Agent( name="TruncDisabled", instructions="Test", model_settings=ModelSettings(truncation="disabled"), ) assert explicit_agent.model_settings.truncation == "disabled" gpt5_agent = Agent(name="Gpt5", instructions="Test", model="gpt-5.4-mini") assert gpt5_agent.model_settings.reasoning is not None assert gpt5_agent.model_settings.reasoning.effort == "none" provider_prefixed_gpt5_agent = Agent(name="ProviderPrefixedGpt5", instructions="Test", model="openai/gpt-5.4-mini") assert provider_prefixed_gpt5_agent.model_settings.reasoning is None assert provider_prefixed_gpt5_agent.model_settings.verbosity is None @pytest.mark.parametrize("provider_model", ["openai/gpt-5.4-mini", "azure/gpt-5.4-mini"]) def test_agent_initialization_model_objects_use_openclaw_default_settings_alias( monkeypatch: pytest.MonkeyPatch, provider_model: str, ) -> None: monkeypatch.setenv("OPENCLAW_PROVIDER_MODEL", provider_model) model = build_openclaw_responses_model(base_url="http://127.0.0.1:18789/v1", api_key="test-key") agent = Agent(name="UsageTrackedModel", instructions="Test", model=model) assert agent.model_settings.reasoning is not None assert agent.model_settings.reasoning.effort == "none" assert agent.model_settings.verbosity == "low" def test_agent_initialization_model_objects_preserve_explicit_openclaw_alias_defaults() -> None: model = build_openclaw_responses_model( model="openclaw:custom", base_url="http://127.0.0.1:18789/v1", api_key="test-key", ) agent = Agent(name="UsageTrackedModel", instructions="Test", model=model) assert agent.model_settings.reasoning is None assert agent.model_settings.verbosity is None def test_agent_initialization_adapts_basetool_type(): """Passing a BaseTool subclass should be adapted to a FunctionTool.""" from pydantic import Field from agency_swarm.tools import BaseTool class _T(BaseTool): x: str = Field(..., description="x") def run(self): return self.x agent = Agent(name="ToolsAdapt", instructions="Test", tools=[_T]) # tools should be adapted to FunctionTool instances from agents import FunctionTool assert len(agent.tools) == 1 assert isinstance(agent.tools[0], FunctionTool) def test_agent_initialization_web_search_source_include_behavior() -> None: """Web-search source include should support init and add_tool behavior with merge and opt-out.""" cases: list[tuple[Agent, bool, int, str | None]] = [ ( Agent(name="WebAgentDefault", instructions="Test", tools=[WebSearchTool()]), True, 1, None, ), ( Agent( name="WebAgentNoSources", instructions="Test", tools=[WebSearchTool()], include_web_search_sources=False, ), False, 0, None, ), ( Agent( name="WebAgentMergeSources", instructions="Test", tools=[WebSearchTool()], model_settings=ModelSettings(response_include=["message.output_text.logprobs"]), ), True, 1, "message.output_text.logprobs", ), ( Agent( name="WebAgentDedupSources", instructions="Test", tools=[WebSearchTool()], model_settings=ModelSettings(response_include=["web_search_call.action.sources"]), ), True, 1, None, ), ] for agent, has_sources, count, extra_include in cases: includes = agent.model_settings.response_include or [] assert ("web_search_call.action.sources" in includes) is has_sources assert includes.count("web_search_call.action.sources") == count if extra_include is not None: assert extra_include in includes add_tool_default = Agent(name="WebAgentAddTool", instructions="Test") assert (add_tool_default.model_settings.response_include or []) == [] add_tool_default.add_tool(WebSearchTool()) assert "web_search_call.action.sources" in (add_tool_default.model_settings.response_include or []) add_tool_opt_out = Agent( name="WebAgentAddToolNoSources", instructions="Test", include_web_search_sources=False, ) add_tool_opt_out.add_tool(WebSearchTool()) assert "web_search_call.action.sources" not in (add_tool_opt_out.model_settings.response_include or [])