from unittest.mock import patch import torch from core.agent import ( AgentLLMConfig, AgentProfile, TaskNode, extract_agent_profiles, ) class TestAgentLLMConfig: """LLM agent configuration.""" def test_resolve_api_key_from_env(self): """resolve_api_key reads key from environment variable.""" cfg = AgentLLMConfig(api_key="$MY_KEY") with patch.dict("os.environ", {"MY_KEY": "secret123"}): assert cfg.resolve_api_key() == "secret123" def test_resolve_api_key_direct(self): """resolve_api_key returns direct key value.""" cfg = AgentLLMConfig(api_key="sk-abc") assert cfg.resolve_api_key() == "sk-abc" def test_resolve_api_key_none(self): """resolve_api_key returns None when no key is set.""" cfg = AgentLLMConfig() assert cfg.resolve_api_key() is None def test_is_configured(self): """is_configured: True when model_name or base_url is set.""" assert AgentLLMConfig().is_configured() is False assert AgentLLMConfig(model_name="gpt-4").is_configured() is True assert AgentLLMConfig(base_url="http://localhost").is_configured() is True def test_to_generation_params_full(self): """to_generation_params with all parameters set.""" cfg = AgentLLMConfig( max_tokens=100, temperature=0.7, top_p=0.9, stop_sequences=["END"], extra_params={"seed": 42}, ) params = cfg.to_generation_params() assert params["max_tokens"] == 100 assert params["temperature"] == 0.7 assert params["top_p"] == 0.9 assert params["stop"] == ["END"] assert params["seed"] == 42 def test_to_generation_params_empty(self): """to_generation_params returns empty dict when no params are set.""" assert AgentLLMConfig().to_generation_params() == {} class TestAgentProfile: """Agent profile — methods and serialization.""" def test_defaults(self): """Default values of agent profile fields.""" p = AgentProfile(agent_id="a1", display_name="Agent 1") assert p.persona == "" assert p.description == "" assert p.llm_backbone is None assert p.llm_config is None assert p.tools == [] assert p.embedding is None assert p.state == [] assert p.hidden_state is None assert p.input_schema is None assert p.output_schema is None def test_role_property(self): """Role property is an alias for agent_id.""" p = AgentProfile(agent_id="writer", display_name="Writer") assert p.role == "writer" def test_has_tools(self): """has_tools returns True only when tools list is non-empty.""" assert AgentProfile(agent_id="a", display_name="A").has_tools() is False assert AgentProfile(agent_id="a", display_name="A", tools=["search"]).has_tools() is True def test_get_tool_names_strings(self): """get_tool_names returns string tool names as-is.""" p = AgentProfile(agent_id="a", display_name="A", tools=["search", "calc"]) assert p.get_tool_names() == ["search", "calc"] def test_get_model_name_from_config(self): """get_model_name reads model name from llm_config.""" cfg = AgentLLMConfig(model_name="gpt-4") p = AgentProfile(agent_id="a", display_name="A", llm_config=cfg) assert p.get_model_name() == "gpt-4" def test_get_model_name_from_backbone(self): """get_model_name falls back to llm_backbone.""" p = AgentProfile(agent_id="a", display_name="A", llm_backbone="claude-3") assert p.get_model_name() == "claude-3" def test_get_llm_config_existing(self): """get_llm_config returns the existing config object.""" cfg = AgentLLMConfig(model_name="gpt-4") p = AgentProfile(agent_id="a", display_name="A", llm_config=cfg) assert p.get_llm_config() is cfg def test_get_llm_config_default(self): """get_llm_config creates a default config from llm_backbone.""" p = AgentProfile(agent_id="a", display_name="A", llm_backbone="claude-3") result = p.get_llm_config() assert isinstance(result, AgentLLMConfig) assert result.model_name == "claude-3" def test_has_custom_llm(self): """has_custom_llm returns True only when llm_config is set and configured.""" assert AgentProfile(agent_id="a", display_name="A").has_custom_llm() is False cfg = AgentLLMConfig(model_name="gpt-4") assert AgentProfile(agent_id="a", display_name="A", llm_config=cfg).has_custom_llm() is True def test_with_llm_config(self): """with_llm_config returns an immutable copy with the new config.""" p = AgentProfile(agent_id="a", display_name="A") cfg = AgentLLMConfig(model_name="gpt-4") p2 = p.with_llm_config(cfg) assert p2.llm_config is cfg assert p.llm_config is None def test_with_embedding(self): """with_embedding returns a copy with the given embedding tensor.""" p = AgentProfile(agent_id="a", display_name="A") emb = torch.randn(16) p2 = p.with_embedding(emb) assert p2.embedding is emb assert p.embedding is None def test_state_methods(self): """with_state, append_state, and clear_state work immutably.""" p = AgentProfile(agent_id="a", display_name="A") p2 = p.with_state([{"role": "user", "content": "hi"}]) assert len(p2.state) == 1 assert p.state == [] p3 = p2.append_state({"role": "assistant", "content": "hello"}) assert len(p3.state) == 2 assert len(p2.state) == 1 p4 = p3.clear_state() assert p4.state == [] assert len(p3.state) == 2 def test_to_text(self): """to_text produces a human-readable profile representation.""" p = AgentProfile( agent_id="a", display_name="Writer", persona="a creative writer", description="Writes stories", tools=["search"], llm_backbone="gpt-4", ) text = p.to_text() assert "Writer" in text assert "a creative writer" in text assert "Writes stories" in text assert "Tools: search" in text assert "LLM Backbone: gpt-4" in text def test_to_dict(self): """to_dict serializes the profile to a dictionary.""" emb = torch.tensor([1.0, 2.0]) cfg = AgentLLMConfig(model_name="gpt-4") p = AgentProfile( agent_id="a", display_name="A", persona="test", embedding=emb, llm_config=cfg, ) d = p.to_dict() assert d["agent_id"] == "a" assert d["display_name"] == "A" assert d["persona"] == "test" assert d["embedding"] == [1.0, 2.0] assert "llm_config" in d assert d["llm_config"]["model_name"] == "gpt-4" class TestTaskNode: """Virtual task node.""" def test_defaults(self): """Default fields of TaskNode.""" t = TaskNode(query="Solve X") assert t.agent_id == "__task__" assert t.type == "task" assert t.query == "Solve X" assert t.display_name == "Task" assert t.persona == "" assert t.embedding is None assert t.tools == [] assert t.state == [] def test_to_text(self): """to_text includes description and query.""" t = TaskNode(query="Solve X", description="Important task") text = t.to_text() assert "Important task" in text assert "Task: Solve X" in text def test_to_text_empty_query(self): """to_text shows (unspecified) for blank query.""" t = TaskNode(query=" ") assert "(unspecified)" in t.to_text() def test_with_embedding(self): """with_embedding returns a copy with the given embedding tensor.""" t = TaskNode(query="Q") emb = torch.randn(8) t2 = t.with_embedding(emb) assert t2.embedding is emb assert t.embedding is None class TestExtractAgentProfiles: """Parsing agents from a dictionary.""" def test_basic_extraction(self): """Basic parsing from agents_data dict.""" data = { "agents": [ {"agent": {"role": "writer", "name": "Writer", "persona": "writes"}}, {"agent": {"role": "reviewer", "name": "Reviewer"}}, ] } profiles = extract_agent_profiles(data) assert len(profiles) == 2 assert profiles[0].agent_id == "writer" assert profiles[0].display_name == "Writer" assert profiles[1].agent_id == "reviewer" def test_duplicate_agents(self): """Duplicate roles — first occurrence is kept.""" data = { "agents": [ {"agent": {"role": "writer", "name": "First"}}, {"agent": {"role": "writer", "name": "Second"}}, ] } profiles = extract_agent_profiles(data) assert len(profiles) == 1 assert profiles[0].display_name == "First" def test_invalid_entries(self): """Invalid entries are silently skipped.""" data = { "agents": [ "not a dict", {"agent": "not a dict either"}, {"agent": {"no_role": True}}, {"agent": {"role": "valid", "name": "OK"}}, ] } profiles = extract_agent_profiles(data) assert len(profiles) == 1 assert profiles[0].agent_id == "valid" def test_tools_extraction(self): """Tools are extracted from various formats and deduplicated.""" data = { "agents": [ { "agent": { "role": "a1", "name": "A", "tools": [ "search", {"name": "calc"}, {"tool": "browser"}, 42, "search", ], } } ] } profiles = extract_agent_profiles(data) tools = profiles[0].tools assert "search" in tools assert "calc" in tools assert "browser" in tools assert len(tools) == 3 def test_llm_backbone_extraction(self): """LLM backbone is extracted from various field formats.""" data_str = {"agents": [{"agent": {"role": "a", "name": "A", "llm": "gpt-4"}}]} p1 = extract_agent_profiles(data_str) assert p1[0].llm_backbone == "gpt-4" data_dict = {"agents": [{"agent": {"role": "b", "name": "B", "model": {"name": "claude-3"}}}]} p2 = extract_agent_profiles(data_dict) assert p2[0].llm_backbone == "claude-3" class TestAgentProfileMissingCoverage: """Tests for missing lines in core/agent.py.""" def test_get_tool_names_with_base_tool_object(self): """get_tool_names when tools contains BaseTool objects (lines 143-144).""" from tools.shell import ShellTool shell = ShellTool() agent = AgentProfile(agent_id="test", display_name="Test", tools=[shell]) names = agent.get_tool_names() assert "shell" in names def test_get_tool_objects_with_base_tool_object(self): """get_tool_objects when tools contains BaseTool objects (lines 182-183).""" from tools.shell import ShellTool shell = ShellTool() agent = AgentProfile(agent_id="test", display_name="Test", tools=[shell]) objects = agent.get_tool_objects() assert shell in objects def test_with_hidden_state(self): """with_hidden_state returns updated copy (line 240).""" agent = AgentProfile(agent_id="test", display_name="Test") hidden = torch.zeros(10) new_agent = agent.with_hidden_state(hidden) assert new_agent.hidden_state is not None assert torch.equal(new_agent.hidden_state, hidden) def test_to_dict_with_schemas(self): """to_dict includes llm_config, input_schema, output_schema (lines 266, 268).""" agent = AgentProfile( agent_id="test", display_name="Test", llm_config=AgentLLMConfig(model_name="gpt-4"), input_schema={"type": "object"}, output_schema={"type": "string"}, ) d = agent.to_dict() assert "llm_config" in d assert "input_schema" in d assert "output_schema" in d def test_extract_llm_backbone_dict_no_model_name_type(self): """_extract_llm_backbone when candidate is a dict with no model/name/type key (line 366).""" from core.agent import _extract_llm_backbone result = _extract_llm_backbone({"llm": {"unknown_key": "value"}}) assert result is None