feat(agent): complete EvoAgent integration for all 6 agent roles

Migrate all agent roles from Legacy to EvoAgent architecture:
- fundamentals_analyst, technical_analyst, sentiment_analyst, valuation_analyst
- risk_manager, portfolio_manager

Key changes:
- EvoAgent now supports Portfolio Manager compatibility methods (_make_decision,
  get_decisions, get_portfolio_state, load_portfolio_state, update_portfolio)
- Add UnifiedAgentFactory for centralized agent creation
- ToolGuard with batch approval API and WebSocket broadcast
- Legacy agents marked deprecated (AnalystAgent, RiskAgent, PMAgent)
- Remove backend/agents/compat.py migration shim
- Add run_id alongside workspace_id for semantic clarity
- Complete integration test coverage (13 tests)
- All smoke tests passing for 6 agent roles

Constraint: Must maintain backward compatibility with existing run configs
Constraint: Memory support must work with EvoAgent (no fallback to Legacy)
Rejected: Separate PM implementation for EvoAgent | unified approach cleaner
Confidence: high
Scope-risk: broad
Directive: EVO_AGENT_IDS env var still respected but defaults to all roles
Not-tested: Kubernetes sandbox mode for skill execution
This commit is contained in:
2026-04-02 00:55:08 +08:00
parent 0fa413380c
commit 16b54d5ccc
73 changed files with 9454 additions and 904 deletions

View File

@@ -1,14 +1,14 @@
# -*- coding: utf-8 -*-
"""
Agents package - EvoAgent architecture for trading system.
Agents package for the current mixed runtime.
Exports:
- EvoAgent: Next-generation agent with workspace support
- ToolGuardMixin: Tool call approval/denial flow
- CommandHandler: System command handling
- AgentFactory: Dynamic agent creation and management
- WorkspaceManager: Legacy name for the persistent workspace registry
- WorkspaceRegistry: Explicit run-time-agnostic workspace registry
- AgentFactory: Design-time agent creation under `workspaces/`
- WorkspaceManager: Legacy alias for the persistent `workspaces/` registry
- WorkspaceRegistry: Explicit design-time `workspaces/` registry
- RunWorkspaceManager: Run-scoped workspace asset manager
- AgentRegistry: Central agent registry
- Legacy compatibility: AnalystAgent, PMAgent, RiskAgent
@@ -26,9 +26,6 @@ from .analyst import AnalystAgent
from .portfolio_manager import PMAgent
from .risk_manager import RiskAgent
# Compatibility layer
from .compat import LegacyAgentAdapter, adapt_agent, adapt_agents, is_legacy_agent
__all__ = [
# New architecture
"EvoAgent",
@@ -48,9 +45,4 @@ __all__ = [
"AnalystAgent",
"PMAgent",
"RiskAgent",
# Compatibility layer
"LegacyAgentAdapter",
"adapt_agent",
"adapt_agents",
"is_legacy_agent",
]

View File

@@ -2,7 +2,13 @@
"""
Analyst Agent - Based on AgentScope ReActAgent
Performs analysis using tools and LLM
.. deprecated:: 0.2.0
AnalystAgent is deprecated and will be removed in a future version.
Use :class:`backend.agents.base.evo_agent.EvoAgent` instead.
See docs/CRITICAL_FIXES.md for migration guide.
"""
import warnings
from typing import Any, Dict, Optional
from agentscope.agent import ReActAgent
@@ -13,11 +19,23 @@ from ..config.constants import ANALYST_TYPES
from ..utils.progress import progress
from .prompt_factory import build_agent_system_prompt, clear_prompt_factory_cache
# Emit deprecation warning on module import
warnings.warn(
"AnalystAgent is deprecated. Use EvoAgent instead. "
"See docs/CRITICAL_FIXES.md for migration guide.",
DeprecationWarning,
stacklevel=2,
)
class AnalystAgent(ReActAgent):
"""
Analyst Agent - Uses LLM for tool selection and analysis
Inherits from AgentScope's ReActAgent
.. deprecated:: 0.2.0
Use :class:`backend.agents.base.evo_agent.EvoAgent` with
workspace-driven configuration instead.
"""
def __init__(
@@ -33,6 +51,10 @@ class AnalystAgent(ReActAgent):
"""
Initialize Analyst Agent
.. deprecated:: 0.2.0
Use :class:`backend.agents.unified_factory.UnifiedAgentFactory`
or :class:`backend.agents.base.evo_agent.EvoAgent` instead.
Args:
analyst_type: Type of analyst (e.g., "fundamentals", etc.)
toolkit: AgentScope Toolkit instance
@@ -42,6 +64,14 @@ class AnalystAgent(ReActAgent):
config: Configuration dictionary
long_term_memory: Optional ReMeTaskLongTermMemory instance
"""
# Emit runtime deprecation warning
warnings.warn(
f"AnalystAgent('{analyst_type}') is deprecated. "
"Use EvoAgent via UnifiedAgentFactory instead.",
DeprecationWarning,
stacklevel=2,
)
if analyst_type not in ANALYST_TYPES:
raise ValueError(
f"Unknown analyst type: {analyst_type}. "

View File

@@ -90,6 +90,8 @@ class EvoAgent(ToolGuardMixin, ReActAgent):
sys_prompt: Optional[str] = None,
max_iters: int = 10,
memory: Optional[Any] = None,
long_term_memory: Optional[Any] = None,
long_term_memory_mode: str = "static_control",
enable_tool_guard: bool = True,
enable_bootstrap_hook: bool = True,
enable_memory_compaction: bool = False,
@@ -97,6 +99,9 @@ class EvoAgent(ToolGuardMixin, ReActAgent):
memory_compact_threshold: Optional[int] = None,
env_context: Optional[str] = None,
prompt_files: Optional[List[str]] = None,
# Portfolio manager specific parameters
initial_cash: Optional[float] = None,
margin_requirement: Optional[float] = None,
):
"""Initialize EvoAgent.
@@ -144,16 +149,24 @@ class EvoAgent(ToolGuardMixin, ReActAgent):
# Initialize hook manager
self._hook_manager = HookManager()
# Build kwargs for parent ReActAgent
kwargs = {
"name": agent_id,
"model": model,
"sys_prompt": self._sys_prompt,
"toolkit": toolkit,
"memory": memory or InMemoryMemory(),
"formatter": formatter,
"max_iters": max_iters,
}
# Add long-term memory if provided
if long_term_memory:
kwargs["long_term_memory"] = long_term_memory
kwargs["long_term_memory_mode"] = long_term_memory_mode
# Initialize parent ReActAgent
super().__init__(
name=agent_id,
model=model,
sys_prompt=self._sys_prompt,
toolkit=toolkit,
memory=memory or InMemoryMemory(),
formatter=formatter,
max_iters=max_iters,
)
super().__init__(**kwargs)
# Register hooks
self._register_hooks(
@@ -366,6 +379,110 @@ class EvoAgent(ToolGuardMixin, ReActAgent):
self.toolkit = new_toolkit
logger.info("Skills reloaded for agent: %s", self.agent_id)
def _make_decision(
self,
ticker: str,
action: str,
quantity: int,
confidence: int = 50,
reasoning: str = "",
) -> "ToolResponse":
"""Record a trading decision for a ticker (PM agent compatibility).
Args:
ticker: Stock ticker symbol (e.g., "AAPL")
action: Decision - "long", "short" or "hold"
quantity: Number of shares to trade (0 for hold)
confidence: Confidence level 0-100
reasoning: Explanation for this decision
Returns:
ToolResponse confirming decision recorded
"""
from agentscope.message import TextBlock
from agentscope.tool import ToolResponse
if action not in ["long", "short", "hold"]:
return ToolResponse(
content=[
TextBlock(
type="text",
text=f"Invalid action: {action}. Must be 'long', 'short', or 'hold'.",
),
],
)
# Store decision in metadata for retrieval
if not hasattr(self, "_decisions"):
self._decisions = {}
self._decisions[ticker] = {
"action": action,
"quantity": quantity if action != "hold" else 0,
"confidence": confidence,
"reasoning": reasoning,
}
return ToolResponse(
content=[
TextBlock(
type="text",
text=f"Decision recorded: {action} {quantity} shares of {ticker} "
f"(confidence: {confidence}%)",
),
],
)
def get_decisions(self) -> Dict[str, Dict]:
"""Get decisions from current cycle (PM compatibility)."""
return getattr(self, "_decisions", {}).copy()
def get_portfolio_state(self) -> Dict[str, Any]:
"""Get current portfolio state (PM compatibility)."""
return getattr(self, "_portfolio", {}).copy()
def load_portfolio_state(self, portfolio: Dict[str, Any]) -> None:
"""Load portfolio state (PM compatibility).
Args:
portfolio: Portfolio state dict with cash, positions, margin_used
"""
if not portfolio:
return
if not hasattr(self, "_portfolio"):
self._portfolio = {
"cash": 100000.0,
"positions": {},
"margin_used": 0.0,
"margin_requirement": 0.25,
}
self._portfolio = {
"cash": portfolio.get("cash", self._portfolio["cash"]),
"positions": portfolio.get("positions", {}).copy(),
"margin_used": portfolio.get("margin_used", 0.0),
"margin_requirement": portfolio.get(
"margin_requirement",
self._portfolio["margin_requirement"],
),
}
def update_portfolio(self, portfolio: Dict[str, Any]) -> None:
"""Update portfolio after external execution (PM compatibility).
Args:
portfolio: Portfolio updates to apply
"""
if not hasattr(self, "_portfolio"):
self._portfolio = {
"cash": 100000.0,
"positions": {},
"margin_used": 0.0,
"margin_requirement": 0.25,
}
self._portfolio.update(portfolio)
def rebuild_sys_prompt(self) -> None:
"""Rebuild and replace the system prompt at runtime.

View File

@@ -13,7 +13,7 @@ import asyncio
import json
import logging
from dataclasses import dataclass, field
from datetime import datetime
from datetime import UTC, datetime
from enum import Enum
from typing import Any, Callable, Dict, Iterable, List, Optional, Set
@@ -73,11 +73,13 @@ class ApprovalRecord:
self.tool_name = tool_name
self.tool_input = tool_input
self.agent_id = agent_id
# run_id is the new preferred name; workspace_id is kept for backward compatibility
self.run_id = workspace_id
self.workspace_id = workspace_id
self.session_id = session_id
self.status = ApprovalStatus.PENDING
self.findings = findings or []
self.created_at = datetime.utcnow()
self.created_at = datetime.now(UTC)
self.resolved_at: Optional[datetime] = None
self.resolved_by: Optional[str] = None
self.metadata: Dict[str, Any] = {}
@@ -90,6 +92,7 @@ class ApprovalRecord:
"tool_name": self.tool_name,
"tool_input": self.tool_input,
"agent_id": self.agent_id,
"run_id": self.run_id,
"workspace_id": self.workspace_id,
"session_id": self.session_id,
"findings": [f.to_dict() for f in self.findings],
@@ -161,7 +164,7 @@ class ToolGuardStore:
return record
record.status = status
record.resolved_at = datetime.utcnow()
record.resolved_at = datetime.now(UTC)
record.resolved_by = resolved_by
if notify_request and record.pending_request:
if status == ApprovalStatus.APPROVED:
@@ -395,18 +398,34 @@ class ToolGuardMixin:
)
manager = get_global_runtime_manager()
approval_data = {
"tool_name": record.tool_name,
"agent_id": record.agent_id,
"workspace_id": record.workspace_id,
"session_id": record.session_id,
"tool_input": record.tool_input,
}
if manager:
manager.register_pending_approval(
record.approval_id,
{
"tool_name": record.tool_name,
"agent_id": record.agent_id,
"workspace_id": record.workspace_id,
"session_id": record.session_id,
"tool_input": record.tool_input,
},
approval_data,
)
# Broadcast WebSocket event for real-time UI updates
try:
if hasattr(manager, 'broadcast_event'):
await manager.broadcast_event({
"type": "approval_requested",
"approval_id": record.approval_id,
"agent_id": record.agent_id,
"tool_name": record.tool_name,
"timestamp": record.created_at.isoformat(),
"data": approval_data,
})
except Exception as e:
logger.warning(f"Failed to broadcast approval event: {e}")
self._pending_approval = ToolApprovalRequest(
approval_id=record.approval_id,
tool_name=tool_name,

View File

@@ -1,146 +0,0 @@
# -*- coding: utf-8 -*-
"""
Compatibility Layer - Adapters for legacy to EvoAgent migration.
Provides:
- LegacyAgentAdapter: Wraps old AnalystAgent to work with new interfaces
- Migration utilities for gradual adoption
"""
from typing import Any, Dict, Optional
from agentscope.message import Msg
from .agent_core import EvoAgent
class LegacyAgentAdapter:
"""
Adapter to make legacy AnalystAgent compatible with EvoAgent interfaces.
This allows gradual migration by wrapping existing agents.
"""
def __init__(self, legacy_agent: Any):
"""
Initialize adapter.
Args:
legacy_agent: Legacy AnalystAgent instance
"""
self._agent = legacy_agent
self.agent_id = getattr(legacy_agent, 'agent_id', getattr(legacy_agent, 'name', 'unknown'))
self.analyst_type = getattr(legacy_agent, 'analyst_type_key', None)
@property
def name(self) -> str:
"""Get agent name."""
return getattr(self._agent, 'name', self.agent_id)
@property
def toolkit(self) -> Any:
"""Get agent toolkit."""
return getattr(self._agent, 'toolkit', None)
@property
def model(self) -> Any:
"""Get agent model."""
return getattr(self._agent, 'model', None)
@property
def memory(self) -> Any:
"""Get agent memory."""
return getattr(self._agent, 'memory', None)
async def reply(self, x: Msg = None) -> Msg:
"""
Delegate to legacy agent's reply method.
Args:
x: Input message
Returns:
Response message
"""
return await self._agent.reply(x)
def reload_runtime_assets(self, active_skill_dirs: Optional[list] = None) -> None:
"""
Reload runtime assets if supported.
Args:
active_skill_dirs: Optional list of active skill directories
"""
if hasattr(self._agent, 'reload_runtime_assets'):
self._agent.reload_runtime_assets(active_skill_dirs)
def to_evo_agent(
self,
workspace_manager: Optional[Any] = None,
enable_tool_guard: bool = False,
) -> EvoAgent:
"""
Convert legacy agent to EvoAgent.
Args:
workspace_manager: Optional workspace manager
enable_tool_guard: Whether to enable tool guard
Returns:
New EvoAgent instance with same configuration
"""
return EvoAgent(
agent_id=self.agent_id,
model=self.model,
formatter=getattr(self._agent, 'formatter', None),
toolkit=self.toolkit,
workspace_manager=workspace_manager,
config=getattr(self._agent, 'config', {}),
long_term_memory=getattr(self._agent, 'long_term_memory', None),
enable_tool_guard=enable_tool_guard,
sys_prompt=getattr(self._agent, '_sys_prompt', None),
)
def __getattr__(self, name: str) -> Any:
"""Delegate unknown attributes to wrapped agent."""
return getattr(self._agent, name)
def is_legacy_agent(agent: Any) -> bool:
"""
Check if an agent is a legacy agent.
Args:
agent: Agent instance to check
Returns:
True if legacy agent
"""
return hasattr(agent, 'analyst_type_key') and not isinstance(agent, EvoAgent)
def adapt_agent(agent: Any) -> Any:
"""
Wrap agent in adapter if it's a legacy agent.
Args:
agent: Agent instance
Returns:
Adapted agent or original if already EvoAgent
"""
if is_legacy_agent(agent):
return LegacyAgentAdapter(agent)
return agent
def adapt_agents(agents: list) -> list:
"""
Wrap multiple agents in adapters.
Args:
agents: List of agent instances
Returns:
List of adapted agents
"""
return [adapt_agent(agent) for agent in agents]

View File

@@ -2,8 +2,13 @@
"""
Portfolio Manager Agent - Based on AgentScope ReActAgent
Responsible for decision-making (NOT trade execution)
"""
.. deprecated:: 0.2.0
PMAgent is deprecated and will be removed in a future version.
Use :class:`backend.agents.base.evo_agent.EvoAgent` instead.
See docs/CRITICAL_FIXES.md for migration guide.
"""
import warnings
from pathlib import Path
from typing import Any, Dict, Optional, Callable
@@ -17,11 +22,31 @@ from .prompt_factory import build_agent_system_prompt, clear_prompt_factory_cach
from .team_pipeline_config import update_active_analysts
from ..config.constants import ANALYST_TYPES
# Emit deprecation warning on module import
warnings.warn(
"PMAgent is deprecated. Use EvoAgent instead. "
"See docs/CRITICAL_FIXES.md for migration guide.",
DeprecationWarning,
stacklevel=2,
)
class PMAgent(ReActAgent):
"""
Portfolio Manager Agent - Makes investment decisions
Key features:
1. PM outputs decisions only (action + quantity per ticker)
2. Trade execution happens externally (in pipeline/executor)
3. Supports both backtest and live modes
.. deprecated:: 0.2.0
Use :class:`backend.agents.base.evo_agent.EvoAgent` with
workspace-driven configuration instead.
"""
"""
Portfolio Manager Agent - Makes investment decisions
Key features:
1. PM outputs decisions only (action + quantity per ticker)
2. Trade execution happens externally (in pipeline/executor)
@@ -41,6 +66,13 @@ class PMAgent(ReActAgent):
toolkit_factory_kwargs: Optional[Dict[str, Any]] = None,
toolkit: Optional[Toolkit] = None,
):
# Emit runtime deprecation warning
warnings.warn(
"PMAgent is deprecated. Use EvoAgent via UnifiedAgentFactory instead.",
DeprecationWarning,
stacklevel=2,
)
object.__setattr__(self, "config", config or {})
# Portfolio state

View File

@@ -2,7 +2,13 @@
"""
Risk Manager Agent - Based on AgentScope ReActAgent
Uses LLM for risk assessment
.. deprecated:: 0.2.0
RiskAgent is deprecated and will be removed in a future version.
Use :class:`backend.agents.base.evo_agent.EvoAgent` instead.
See docs/CRITICAL_FIXES.md for migration guide.
"""
import warnings
from typing import Any, Dict, Optional
from agentscope.agent import ReActAgent
@@ -13,11 +19,23 @@ from agentscope.tool import Toolkit
from ..utils.progress import progress
from .prompt_factory import build_agent_system_prompt, clear_prompt_factory_cache
# Emit deprecation warning on module import
warnings.warn(
"RiskAgent is deprecated. Use EvoAgent instead. "
"See docs/CRITICAL_FIXES.md for migration guide.",
DeprecationWarning,
stacklevel=2,
)
class RiskAgent(ReActAgent):
"""
Risk Manager Agent - Uses LLM for risk assessment
Inherits from AgentScope's ReActAgent
.. deprecated:: 0.2.0
Use :class:`backend.agents.base.evo_agent.EvoAgent` with
workspace-driven configuration instead.
"""
def __init__(
@@ -32,6 +50,10 @@ class RiskAgent(ReActAgent):
"""
Initialize Risk Manager Agent
.. deprecated:: 0.2.0
Use :class:`backend.agents.unified_factory.UnifiedAgentFactory`
or :class:`backend.agents.base.evo_agent.EvoAgent` instead.
Args:
model: LLM model instance
formatter: Message formatter instance
@@ -39,6 +61,13 @@ class RiskAgent(ReActAgent):
config: Configuration dictionary
long_term_memory: Optional ReMeTaskLongTermMemory instance
"""
# Emit runtime deprecation warning
warnings.warn(
"RiskAgent is deprecated. Use EvoAgent via UnifiedAgentFactory instead.",
DeprecationWarning,
stacklevel=2,
)
object.__setattr__(self, "config", config or {})
object.__setattr__(self, "agent_id", name)

View File

@@ -0,0 +1,433 @@
# -*- coding: utf-8 -*-
"""Unified Agent Factory - Centralized agent creation for 大时代.
This module provides a unified factory for creating all agent types (analysts,
risk manager, portfolio manager) with consistent configuration. It replaces
the scattered agent creation logic in main.py, pipeline.py, and pipeline_runner.py.
Key features:
- Single entry point for all agent creation
- Automatic EvoAgent vs Legacy Agent selection based on _resolve_evo_agent_ids()
- Consistent parameter handling across all agent types
- Support for workspace-driven configuration
- Long-term memory integration
"""
from __future__ import annotations
import os
from pathlib import Path
from typing import TYPE_CHECKING, Any, Optional, Protocol, TypeVar, Union
if TYPE_CHECKING:
from backend.agents.base.evo_agent import EvoAgent
from backend.agents.analyst import AnalystAgent
from backend.agents.risk_manager import RiskAgent
from backend.agents.portfolio_manager import PMAgent
# Type aliases for agent types
AgentType = Union["EvoAgent", "AnalystAgent", "RiskAgent", "PMAgent"]
T = TypeVar("T")
class AgentFactoryProtocol(Protocol):
"""Protocol for agent factory implementations."""
def create_analyst(
self,
analyst_type: str,
model: Any,
formatter: Any,
active_skill_dirs: Optional[list[Path]] = None,
long_term_memory: Optional[Any] = None,
) -> AnalystAgent | EvoAgent: ...
def create_risk_manager(
self,
model: Any,
formatter: Any,
active_skill_dirs: Optional[list[Path]] = None,
long_term_memory: Optional[Any] = None,
) -> RiskAgent | EvoAgent: ...
def create_portfolio_manager(
self,
model: Any,
formatter: Any,
initial_cash: float,
margin_requirement: float,
active_skill_dirs: Optional[list[Path]] = None,
long_term_memory: Optional[Any] = None,
) -> PMAgent | EvoAgent: ...
class UnifiedAgentFactory:
"""Unified factory for creating agents with consistent configuration.
This factory centralizes agent creation logic and automatically selects
between EvoAgent (new) and Legacy Agent based on the EVO_AGENT_IDS
environment variable configuration.
By default, all supported roles use EvoAgent. Set EVO_AGENT_IDS=legacy
to disable EvoAgent entirely.
Example:
factory = UnifiedAgentFactory(
config_name="smoke_fullstack",
skills_manager=skills_manager,
)
# Create analyst
analyst = factory.create_analyst(
analyst_type="fundamentals_analyst",
model=model,
formatter=formatter,
)
# Create risk manager
risk_mgr = factory.create_risk_manager(
model=model,
formatter=formatter,
)
# Create portfolio manager
pm = factory.create_portfolio_manager(
model=model,
formatter=formatter,
initial_cash=100000.0,
margin_requirement=0.5,
)
"""
def __init__(
self,
config_name: str,
skills_manager: Any,
toolkit_factory: Optional[Any] = None,
evo_agent_ids: Optional[set[str]] = None,
):
"""Initialize the agent factory.
Args:
config_name: Run configuration name (e.g., "smoke_fullstack")
skills_manager: SkillsManager instance for skill/asset management
toolkit_factory: Optional factory function for creating toolkits
evo_agent_ids: Optional set of agent IDs to use EvoAgent.
If None, uses _resolve_evo_agent_ids() default.
"""
self.config_name = config_name
self.skills_manager = skills_manager
self.toolkit_factory = toolkit_factory
# Determine which agents should use EvoAgent
if evo_agent_ids is not None:
self._evo_agent_ids = evo_agent_ids
else:
self._evo_agent_ids = self._resolve_evo_agent_ids()
def _resolve_evo_agent_ids(self) -> set[str]:
"""Return agent ids selected to use EvoAgent.
By default, all supported roles use EvoAgent.
EVO_AGENT_IDS can be used to limit to specific roles.
"""
from backend.config.constants import ANALYST_TYPES
all_supported = set(ANALYST_TYPES) | {"risk_manager", "portfolio_manager"}
raw = os.getenv("EVO_AGENT_IDS", "")
if not raw.strip():
# Default: all supported roles use EvoAgent
return all_supported
if raw.strip().lower() in ("legacy", "old", "none"):
return set()
requested = {item.strip() for item in raw.split(",") if item.strip()}
return {
agent_id
for agent_id in requested
if agent_id in ANALYST_TYPES
or agent_id in {"risk_manager", "portfolio_manager"}
}
def _should_use_evo_agent(self, agent_id: str) -> bool:
"""Check if an agent should use EvoAgent."""
return agent_id in self._evo_agent_ids
def _create_toolkit(
self,
agent_type: str,
active_skill_dirs: Optional[list[Path]] = None,
owner: Optional[Any] = None,
) -> Any:
"""Create toolkit for an agent."""
if self.toolkit_factory is None:
from backend.agents.toolkit_factory import create_agent_toolkit
self.toolkit_factory = create_agent_toolkit
kwargs: dict[str, Any] = {
"active_skill_dirs": active_skill_dirs or [],
}
if owner is not None:
kwargs["owner"] = owner
return self.toolkit_factory(agent_type, self.config_name, **kwargs)
def _load_agent_config(self, agent_id: str) -> Any:
"""Load agent configuration from workspace."""
from backend.agents.agent_workspace import load_agent_workspace_config
workspace_dir = self.skills_manager.get_agent_asset_dir(
self.config_name, agent_id
)
config_path = workspace_dir / "agent.yaml"
if config_path.exists():
return load_agent_workspace_config(config_path)
# Return default config if no agent.yaml
return type(
"AgentConfig",
(),
{"prompt_files": ["SOUL.md"]},
)()
def _create_evo_agent(
self,
agent_id: str,
model: Any,
formatter: Any,
toolkit: Any,
agent_config: Any,
long_term_memory: Optional[Any] = None,
extra_kwargs: Optional[dict[str, Any]] = None,
) -> "EvoAgent":
"""Create an EvoAgent instance."""
from backend.agents.base.evo_agent import EvoAgent
workspace_dir = self.skills_manager.get_agent_asset_dir(
self.config_name, agent_id
)
kwargs: dict[str, Any] = {
"agent_id": agent_id,
"config_name": self.config_name,
"workspace_dir": workspace_dir,
"model": model,
"formatter": formatter,
"skills_manager": self.skills_manager,
"prompt_files": getattr(agent_config, "prompt_files", ["SOUL.md"]),
"long_term_memory": long_term_memory,
}
if extra_kwargs:
kwargs.update(extra_kwargs)
agent = EvoAgent(**kwargs)
agent.toolkit = toolkit
setattr(agent, "run_id", self.config_name)
# Keep workspace_id for backward compatibility
setattr(agent, "workspace_id", self.config_name)
return agent
def create_analyst(
self,
analyst_type: str,
model: Any,
formatter: Any,
active_skill_dirs: Optional[list[Path]] = None,
long_term_memory: Optional[Any] = None,
) -> "AnalystAgent | EvoAgent":
"""Create an analyst agent.
Args:
analyst_type: Type of analyst (fundamentals, technical, sentiment, valuation)
model: LLM model instance
formatter: Message formatter instance
active_skill_dirs: Optional list of active skill directories
long_term_memory: Optional long-term memory instance
Returns:
AnalystAgent or EvoAgent instance
"""
toolkit = self._create_toolkit(analyst_type, active_skill_dirs)
if self._should_use_evo_agent(analyst_type):
agent_config = self._load_agent_config(analyst_type)
return self._create_evo_agent(
agent_id=analyst_type,
model=model,
formatter=formatter,
toolkit=toolkit,
agent_config=agent_config,
long_term_memory=long_term_memory,
)
# Legacy path
from backend.agents.analyst import AnalystAgent
return AnalystAgent(
analyst_type=analyst_type,
toolkit=toolkit,
model=model,
formatter=formatter,
agent_id=analyst_type,
config={"config_name": self.config_name},
long_term_memory=long_term_memory,
)
def create_risk_manager(
self,
model: Any,
formatter: Any,
active_skill_dirs: Optional[list[Path]] = None,
long_term_memory: Optional[Any] = None,
) -> "RiskAgent | EvoAgent":
"""Create a risk manager agent.
Args:
model: LLM model instance
formatter: Message formatter instance
active_skill_dirs: Optional list of active skill directories
long_term_memory: Optional long-term memory instance
Returns:
RiskAgent or EvoAgent instance
"""
toolkit = self._create_toolkit("risk_manager", active_skill_dirs)
if self._should_use_evo_agent("risk_manager"):
agent_config = self._load_agent_config("risk_manager")
return self._create_evo_agent(
agent_id="risk_manager",
model=model,
formatter=formatter,
toolkit=toolkit,
agent_config=agent_config,
long_term_memory=long_term_memory,
)
# Legacy path
from backend.agents.risk_manager import RiskAgent
return RiskAgent(
model=model,
formatter=formatter,
name="risk_manager",
config={"config_name": self.config_name},
long_term_memory=long_term_memory,
toolkit=toolkit,
)
def create_portfolio_manager(
self,
model: Any,
formatter: Any,
initial_cash: float,
margin_requirement: float,
active_skill_dirs: Optional[list[Path]] = None,
long_term_memory: Optional[Any] = None,
) -> "PMAgent | EvoAgent":
"""Create a portfolio manager agent.
Args:
model: LLM model instance
formatter: Message formatter instance
initial_cash: Initial cash allocation
margin_requirement: Margin requirement ratio
active_skill_dirs: Optional list of active skill directories
long_term_memory: Optional long-term memory instance
Returns:
PMAgent or EvoAgent instance
"""
if self._should_use_evo_agent("portfolio_manager"):
agent_config = self._load_agent_config("portfolio_manager")
# For PM, toolkit is created after agent (needs owner reference)
from backend.agents.base.evo_agent import EvoAgent
workspace_dir = self.skills_manager.get_agent_asset_dir(
self.config_name, "portfolio_manager"
)
agent = EvoAgent(
agent_id="portfolio_manager",
config_name=self.config_name,
workspace_dir=workspace_dir,
model=model,
formatter=formatter,
skills_manager=self.skills_manager,
prompt_files=getattr(agent_config, "prompt_files", ["SOUL.md"]),
initial_cash=initial_cash,
margin_requirement=margin_requirement,
long_term_memory=long_term_memory,
)
agent.toolkit = self._create_toolkit(
"portfolio_manager", active_skill_dirs, owner=agent
)
setattr(agent, "run_id", self.config_name)
# Keep workspace_id for backward compatibility
setattr(agent, "workspace_id", self.config_name)
return agent
# Legacy path
from backend.agents.portfolio_manager import PMAgent
return PMAgent(
name="portfolio_manager",
model=model,
formatter=formatter,
initial_cash=initial_cash,
margin_requirement=margin_requirement,
config={"config_name": self.config_name},
long_term_memory=long_term_memory,
toolkit_factory=self.toolkit_factory,
toolkit_factory_kwargs={"active_skill_dirs": active_skill_dirs or []},
)
# Singleton factory instance cache
_factory_cache: dict[str, UnifiedAgentFactory] = {}
def get_agent_factory(
config_name: str,
skills_manager: Any,
toolkit_factory: Optional[Any] = None,
) -> UnifiedAgentFactory:
"""Get or create a cached agent factory instance.
Args:
config_name: Run configuration name
skills_manager: SkillsManager instance
toolkit_factory: Optional toolkit factory function
Returns:
UnifiedAgentFactory instance (cached per config_name)
"""
cache_key = f"{config_name}:{id(skills_manager)}"
if cache_key not in _factory_cache:
_factory_cache[cache_key] = UnifiedAgentFactory(
config_name=config_name,
skills_manager=skills_manager,
toolkit_factory=toolkit_factory,
)
return _factory_cache[cache_key]
def clear_factory_cache() -> None:
"""Clear the factory cache. Useful for testing."""
_factory_cache.clear()
__all__ = [
"UnifiedAgentFactory",
"AgentFactoryProtocol",
"get_agent_factory",
"clear_factory_cache",
]

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
"""Workspace Manager - Create and manage agent workspaces."""
"""Design-time workspace registry stored under `workspaces/`."""
import logging
from dataclasses import dataclass, field
@@ -323,5 +323,6 @@ class WorkspaceRegistry:
yaml.safe_dump(config.to_dict(), f, allow_unicode=True, sort_keys=False)
# Backward-compatible alias: legacy imports expect WorkspaceManager.
# Backward-compatible alias: legacy imports expect WorkspaceManager to mean the
# design-time `workspaces/` registry.
WorkspaceManager = WorkspaceRegistry

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
"""Initialize run-scoped agent workspace assets."""
"""Initialize run-scoped agent workspace assets under `runs/<run_id>/`."""
from pathlib import Path
from typing import Dict, Iterable, Optional
@@ -479,5 +479,6 @@ class RunWorkspaceManager:
)
# Backward-compatible alias: code importing WorkspaceManager from this module should continue to work.
# Backward-compatible alias: many runtime paths still import WorkspaceManager
# from this module when they mean the run-scoped manager.
WorkspaceManager = RunWorkspaceManager