Remove deprecated AnalystAgent, PMAgent, and RiskAgent classes. All agent creation now goes through UnifiedAgentFactory creating EvoAgent instances. - Delete backend/agents/analyst.py (169 lines) - Delete backend/agents/portfolio_manager.py (420 lines) - Delete backend/agents/risk_manager.py (139 lines) - Update all imports to use EvoAgent exclusively - Clean up unused imports across 25 files - Update tests to work with simplified agent structure Constraint: EvoAgent is now the single source of truth for all agent roles Constraint: UnifiedAgentFactory handles runtime agent creation Rejected: Keep legacy aliases | creates maintenance burden Confidence: high Scope-risk: moderate (affects agent instantiation paths) Directive: All new agent features must be added to EvoAgent, not legacy classes Not-tested: Kubernetes sandbox executor (marked with TODO)
438 lines
14 KiB
Python
438 lines
14 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Tests for selective EvoAgent construction."""
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
def test_main_resolve_evo_agent_ids_filters_unsupported_roles(monkeypatch):
|
|
from backend import main as main_module
|
|
|
|
monkeypatch.setenv(
|
|
"EVO_AGENT_IDS",
|
|
"fundamentals_analyst,portfolio_manager,unknown,technical_analyst",
|
|
)
|
|
|
|
resolved = main_module._resolve_evo_agent_ids()
|
|
|
|
assert resolved == {"fundamentals_analyst", "portfolio_manager", "technical_analyst"}
|
|
|
|
|
|
def test_pipeline_runner_resolve_evo_agent_ids_keeps_supported_roles(monkeypatch):
|
|
from backend.core import pipeline_runner as runner_module
|
|
|
|
monkeypatch.setenv("EVO_AGENT_IDS", "risk_manager,valuation_analyst")
|
|
|
|
resolved = runner_module._resolve_evo_agent_ids()
|
|
|
|
assert resolved == {"risk_manager", "valuation_analyst"}
|
|
|
|
|
|
def test_main_create_analyst_agent_can_build_evo_agent(monkeypatch, tmp_path):
|
|
from backend import main as main_module
|
|
|
|
created = {}
|
|
|
|
class DummySkillsManager:
|
|
def get_agent_asset_dir(self, config_name, agent_id):
|
|
path = tmp_path / "runs" / config_name / "agents" / agent_id
|
|
path.mkdir(parents=True, exist_ok=True)
|
|
(path / "agent.yaml").write_text(
|
|
"prompt_files:\n - SOUL.md\n",
|
|
encoding="utf-8",
|
|
)
|
|
return path
|
|
|
|
class DummyEvoAgent:
|
|
def __init__(self, **kwargs):
|
|
created.update(kwargs)
|
|
self.toolkit = None
|
|
|
|
monkeypatch.setenv("EVO_AGENT_IDS", "fundamentals_analyst")
|
|
monkeypatch.setattr(main_module, "EvoAgent", DummyEvoAgent)
|
|
monkeypatch.setattr(main_module, "create_agent_toolkit", lambda *args, **kwargs: "toolkit")
|
|
|
|
agent = main_module._create_analyst_agent(
|
|
analyst_type="fundamentals_analyst",
|
|
config_name="demo",
|
|
model="model",
|
|
formatter="formatter",
|
|
skills_manager=DummySkillsManager(),
|
|
active_skill_map={"fundamentals_analyst": [Path("/tmp/skill")]},
|
|
long_term_memory=None,
|
|
)
|
|
|
|
assert isinstance(agent, DummyEvoAgent)
|
|
assert created["agent_id"] == "fundamentals_analyst"
|
|
assert created["config_name"] == "demo"
|
|
assert created["prompt_files"] == ["SOUL.md"]
|
|
assert agent.toolkit == "toolkit"
|
|
assert agent.workspace_id == "demo"
|
|
|
|
|
|
def test_main_create_risk_manager_can_build_evo_agent(monkeypatch, tmp_path):
|
|
from backend import main as main_module
|
|
|
|
created = {}
|
|
|
|
class DummySkillsManager:
|
|
def get_agent_asset_dir(self, config_name, agent_id):
|
|
path = tmp_path / "runs" / config_name / "agents" / agent_id
|
|
path.mkdir(parents=True, exist_ok=True)
|
|
(path / "agent.yaml").write_text(
|
|
"prompt_files:\n - SOUL.md\n",
|
|
encoding="utf-8",
|
|
)
|
|
return path
|
|
|
|
class DummyEvoAgent:
|
|
def __init__(self, **kwargs):
|
|
created.update(kwargs)
|
|
self.toolkit = None
|
|
|
|
monkeypatch.setenv("EVO_AGENT_IDS", "risk_manager")
|
|
monkeypatch.setattr(main_module, "EvoAgent", DummyEvoAgent)
|
|
monkeypatch.setattr(main_module, "create_agent_toolkit", lambda *args, **kwargs: "risk-toolkit")
|
|
|
|
agent = main_module._create_risk_manager_agent(
|
|
config_name="demo",
|
|
model="model",
|
|
formatter="formatter",
|
|
skills_manager=DummySkillsManager(),
|
|
active_skill_map={"risk_manager": [Path("/tmp/skill")]},
|
|
long_term_memory=None,
|
|
)
|
|
|
|
assert isinstance(agent, DummyEvoAgent)
|
|
assert created["agent_id"] == "risk_manager"
|
|
assert created["config_name"] == "demo"
|
|
assert created["prompt_files"] == ["SOUL.md"]
|
|
assert agent.toolkit == "risk-toolkit"
|
|
assert agent.workspace_id == "demo"
|
|
|
|
|
|
def test_main_create_portfolio_manager_can_build_evo_agent(monkeypatch, tmp_path):
|
|
from backend import main as main_module
|
|
|
|
created = {}
|
|
|
|
class DummySkillsManager:
|
|
def get_agent_asset_dir(self, config_name, agent_id):
|
|
path = tmp_path / "runs" / config_name / "agents" / agent_id
|
|
path.mkdir(parents=True, exist_ok=True)
|
|
(path / "agent.yaml").write_text(
|
|
"prompt_files:\n - SOUL.md\n",
|
|
encoding="utf-8",
|
|
)
|
|
return path
|
|
|
|
class DummyEvoAgent:
|
|
def __init__(self, **kwargs):
|
|
created.update(kwargs)
|
|
self.toolkit = None
|
|
|
|
monkeypatch.setenv("EVO_AGENT_IDS", "portfolio_manager")
|
|
monkeypatch.setattr(main_module, "EvoAgent", DummyEvoAgent)
|
|
monkeypatch.setattr(
|
|
main_module,
|
|
"create_agent_toolkit",
|
|
lambda *args, **kwargs: "pm-toolkit",
|
|
)
|
|
|
|
agent = main_module._create_portfolio_manager_agent(
|
|
config_name="demo",
|
|
model="model",
|
|
formatter="formatter",
|
|
initial_cash=12345.0,
|
|
margin_requirement=0.4,
|
|
skills_manager=DummySkillsManager(),
|
|
active_skill_map={"portfolio_manager": [Path("/tmp/skill")]},
|
|
long_term_memory=None,
|
|
)
|
|
|
|
assert isinstance(agent, DummyEvoAgent)
|
|
assert created["agent_id"] == "portfolio_manager"
|
|
assert created["config_name"] == "demo"
|
|
assert created["prompt_files"] == ["SOUL.md"]
|
|
assert created["initial_cash"] == 12345.0
|
|
assert created["margin_requirement"] == 0.4
|
|
assert agent.toolkit == "pm-toolkit"
|
|
assert agent.workspace_id == "demo"
|
|
|
|
|
|
def test_evo_agent_reload_runtime_assets_refreshes_prompt_files(monkeypatch, tmp_path):
|
|
from backend.agents.base.evo_agent import EvoAgent
|
|
|
|
workspace_dir = tmp_path / "runs" / "demo" / "agents" / "fundamentals_analyst"
|
|
workspace_dir.mkdir(parents=True, exist_ok=True)
|
|
(workspace_dir / "SOUL.md").write_text("soul-v1", encoding="utf-8")
|
|
(workspace_dir / "MEMORY.md").write_text("memory-v1", encoding="utf-8")
|
|
(workspace_dir / "agent.yaml").write_text(
|
|
"prompt_files:\n"
|
|
" - SOUL.md\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
class DummyToolkit:
|
|
def __init__(self, *args, **kwargs):
|
|
self.registered = []
|
|
|
|
def register_agent_skill(self, path):
|
|
self.registered.append(path)
|
|
|
|
monkeypatch.setattr(
|
|
"backend.agents.base.evo_agent.Toolkit",
|
|
DummyToolkit,
|
|
)
|
|
|
|
class DummyModel:
|
|
pass
|
|
|
|
class DummyFormatter:
|
|
pass
|
|
|
|
agent = EvoAgent(
|
|
agent_id="fundamentals_analyst",
|
|
config_name="demo",
|
|
workspace_dir=workspace_dir,
|
|
model=DummyModel(),
|
|
formatter=DummyFormatter(),
|
|
prompt_files=["SOUL.md"],
|
|
skills_manager=type(
|
|
"SkillsManagerStub",
|
|
(),
|
|
{
|
|
"get_agent_active_root": staticmethod(lambda config_name, agent_id: workspace_dir / "skills" / "active"),
|
|
"list_active_skill_metadata": staticmethod(lambda config_name, agent_id: []),
|
|
},
|
|
)(),
|
|
)
|
|
|
|
assert "soul-v1" in agent._sys_prompt
|
|
assert "memory-v1" not in agent._sys_prompt
|
|
|
|
(workspace_dir / "agent.yaml").write_text(
|
|
"prompt_files:\n"
|
|
" - SOUL.md\n"
|
|
" - MEMORY.md\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
agent.reload_runtime_assets(active_skill_dirs=[])
|
|
|
|
assert "memory-v1" in agent._sys_prompt
|
|
assert agent.workspace_id == "demo"
|
|
assert agent.config == {"config_name": "demo"}
|
|
|
|
|
|
|
|
|
|
def test_pipeline_resolve_evo_agent_ids_filters_unsupported_roles(monkeypatch):
|
|
"""Test that pipeline._resolve_evo_agent_ids filters unsupported roles."""
|
|
from backend.core import pipeline as pipeline_module
|
|
|
|
monkeypatch.setenv(
|
|
"EVO_AGENT_IDS",
|
|
"fundamentals_analyst,portfolio_manager,unknown,technical_analyst",
|
|
)
|
|
|
|
resolved = pipeline_module._resolve_evo_agent_ids()
|
|
|
|
assert resolved == {"fundamentals_analyst", "portfolio_manager", "technical_analyst"}
|
|
|
|
|
|
def test_pipeline_create_runtime_analyst_uses_evo_agent_when_enabled(monkeypatch, tmp_path):
|
|
"""Test that _create_runtime_analyst creates EvoAgent when in EVO_AGENT_IDS."""
|
|
from backend.core import pipeline as pipeline_module
|
|
|
|
created = {}
|
|
|
|
class DummyEvoAgent:
|
|
name = "test_analyst"
|
|
|
|
def __init__(self, **kwargs):
|
|
created.update(kwargs)
|
|
self.toolkit = None
|
|
|
|
class DummyAnalystAgent:
|
|
name = "test_analyst"
|
|
|
|
def __init__(self, **kwargs):
|
|
created.update(kwargs)
|
|
self.toolkit = None
|
|
|
|
monkeypatch.setenv("EVO_AGENT_IDS", "fundamentals_analyst")
|
|
monkeypatch.setattr(pipeline_module, "EvoAgent", DummyEvoAgent)
|
|
monkeypatch.setattr(pipeline_module, "AnalystAgent", DummyAnalystAgent)
|
|
monkeypatch.setattr(
|
|
pipeline_module,
|
|
"create_agent_toolkit",
|
|
lambda *args, **kwargs: "toolkit",
|
|
)
|
|
monkeypatch.setattr(
|
|
pipeline_module,
|
|
"get_agent_model",
|
|
lambda x: "model",
|
|
)
|
|
monkeypatch.setattr(
|
|
pipeline_module,
|
|
"get_agent_formatter",
|
|
lambda x: "formatter",
|
|
)
|
|
|
|
# Create a mock pipeline instance
|
|
class MockPM:
|
|
def __init__(self):
|
|
self.config = {"config_name": "demo"}
|
|
|
|
pipeline = pipeline_module.TradingPipeline(
|
|
analysts=[],
|
|
risk_manager=None,
|
|
portfolio_manager=MockPM(),
|
|
)
|
|
|
|
# Mock workspace_manager methods
|
|
monkeypatch.setattr(
|
|
pipeline_module.WorkspaceManager,
|
|
"ensure_agent_assets",
|
|
lambda *args, **kwargs: None,
|
|
)
|
|
|
|
result = pipeline._create_runtime_analyst("test_analyst", "fundamentals_analyst")
|
|
|
|
assert "Created runtime analyst" in result
|
|
assert created.get("agent_id") == "test_analyst"
|
|
assert created.get("config_name") == "demo"
|
|
|
|
|
|
def test_pipeline_create_runtime_analyst_uses_legacy_when_not_in_evo_ids(monkeypatch, tmp_path):
|
|
"""Test that _create_runtime_analyst creates legacy AnalystAgent when not in EVO_AGENT_IDS."""
|
|
from backend.core import pipeline as pipeline_module
|
|
|
|
created = {}
|
|
|
|
class DummyEvoAgent:
|
|
name = "test_analyst"
|
|
|
|
def __init__(self, **kwargs):
|
|
created.update(kwargs)
|
|
self.toolkit = None
|
|
|
|
class DummyAnalystAgent:
|
|
name = "test_analyst"
|
|
|
|
def __init__(self, **kwargs):
|
|
created.update(kwargs)
|
|
self.toolkit = None
|
|
|
|
# EVO_AGENT_IDS does not include fundamentals_analyst
|
|
monkeypatch.setenv("EVO_AGENT_IDS", "technical_analyst")
|
|
monkeypatch.setattr(pipeline_module, "EvoAgent", DummyEvoAgent)
|
|
monkeypatch.setattr(pipeline_module, "AnalystAgent", DummyAnalystAgent)
|
|
monkeypatch.setattr(
|
|
pipeline_module,
|
|
"create_agent_toolkit",
|
|
lambda *args, **kwargs: "toolkit",
|
|
)
|
|
monkeypatch.setattr(
|
|
pipeline_module,
|
|
"get_agent_model",
|
|
lambda x: "model",
|
|
)
|
|
monkeypatch.setattr(
|
|
pipeline_module,
|
|
"get_agent_formatter",
|
|
lambda x: "formatter",
|
|
)
|
|
|
|
# Create a mock pipeline instance
|
|
class MockPM:
|
|
def __init__(self):
|
|
self.config = {"config_name": "demo"}
|
|
|
|
pipeline = pipeline_module.TradingPipeline(
|
|
analysts=[],
|
|
risk_manager=None,
|
|
portfolio_manager=MockPM(),
|
|
)
|
|
|
|
# Mock workspace_manager methods
|
|
monkeypatch.setattr(
|
|
pipeline_module.WorkspaceManager,
|
|
"ensure_agent_assets",
|
|
lambda *args, **kwargs: None,
|
|
)
|
|
|
|
result = pipeline._create_runtime_analyst("test_analyst", "fundamentals_analyst")
|
|
|
|
assert "Created runtime analyst" in result
|
|
# Should use legacy AnalystAgent
|
|
assert created.get("analyst_type") == "fundamentals_analyst"
|
|
|
|
|
|
def test_main_resolve_evo_agent_ids_returns_all_by_default(monkeypatch):
|
|
"""Test that _resolve_evo_agent_ids returns all supported roles by default."""
|
|
from backend import main as main_module
|
|
from backend.config.constants import ANALYST_TYPES
|
|
|
|
# Unset EVO_AGENT_IDS to test default behavior
|
|
monkeypatch.delenv("EVO_AGENT_IDS", raising=False)
|
|
|
|
resolved = main_module._resolve_evo_agent_ids()
|
|
|
|
expected = set(ANALYST_TYPES) | {"risk_manager", "portfolio_manager"}
|
|
assert resolved == expected
|
|
|
|
|
|
def test_evo_agent_supports_long_term_memory(monkeypatch, tmp_path):
|
|
"""Test that EvoAgent can be created with long_term_memory."""
|
|
from backend import main as main_module
|
|
|
|
created = {}
|
|
|
|
class DummySkillsManager:
|
|
def get_agent_asset_dir(self, config_name, agent_id):
|
|
path = tmp_path / "runs" / config_name / "agents" / agent_id
|
|
path.mkdir(parents=True, exist_ok=True)
|
|
(path / "agent.yaml").write_text(
|
|
"prompt_files:\n - SOUL.md\n",
|
|
encoding="utf-8",
|
|
)
|
|
return path
|
|
|
|
class DummyEvoAgent:
|
|
def __init__(self, **kwargs):
|
|
created.update(kwargs)
|
|
self.toolkit = None
|
|
|
|
# Default: all roles use EvoAgent
|
|
monkeypatch.delenv("EVO_AGENT_IDS", raising=False)
|
|
monkeypatch.setattr(main_module, "EvoAgent", DummyEvoAgent)
|
|
monkeypatch.setattr(main_module, "create_agent_toolkit", lambda *args, **kwargs: "toolkit")
|
|
|
|
# Create with long_term_memory - should still use EvoAgent
|
|
dummy_memory = {"type": "reme"}
|
|
agent = main_module._create_analyst_agent(
|
|
analyst_type="fundamentals_analyst",
|
|
config_name="demo",
|
|
model="model",
|
|
formatter="formatter",
|
|
skills_manager=DummySkillsManager(),
|
|
active_skill_map={"fundamentals_analyst": []},
|
|
long_term_memory=dummy_memory,
|
|
)
|
|
|
|
assert isinstance(agent, DummyEvoAgent)
|
|
assert created["agent_id"] == "fundamentals_analyst"
|
|
assert created["long_term_memory"] is dummy_memory
|
|
|
|
|
|
def test_evo_agent_legacy_mode(monkeypatch):
|
|
"""Test that EVO_AGENT_IDS=legacy disables EvoAgent."""
|
|
from backend import main as main_module
|
|
|
|
monkeypatch.setenv("EVO_AGENT_IDS", "legacy")
|
|
|
|
resolved = main_module._resolve_evo_agent_ids()
|
|
assert resolved == set()
|