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:
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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}. "
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
433
backend/agents/unified_factory.py
Normal file
433
backend/agents/unified_factory.py
Normal 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",
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user