feat: Add evaluation hooks, skill adaptation and team pipeline config
- Add EvaluationHook for post-execution agent evaluation - Add SkillAdaptationHook for dynamic skill adaptation - Add team/ directory with team coordination logic - Add TEAM_PIPELINE.yaml for smoke_fullstack pipeline config - Update RuntimeView, TraderView and RuntimeSettingsPanel UI - Add runtimeApi and websocket services - Add runtime_state.json to smoke_fullstack state Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,8 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from contextlib import nullcontext
|
||||
from pathlib import Path
|
||||
from typing import Any, Awaitable, Callable, Dict, List, Optional
|
||||
|
||||
from agentscope.message import Msg
|
||||
@@ -21,6 +23,26 @@ from backend.core.state_sync import StateSync
|
||||
from backend.utils.trade_executor import PortfolioTradeExecutor
|
||||
from backend.runtime.manager import TradingRuntimeManager
|
||||
from backend.runtime.session import TradingSessionKey
|
||||
from backend.agents.team_pipeline_config import (
|
||||
resolve_active_analysts,
|
||||
update_active_analysts,
|
||||
)
|
||||
from backend.agents import AnalystAgent
|
||||
from backend.agents.toolkit_factory import create_agent_toolkit
|
||||
from backend.agents.workspace_manager import WorkspaceManager
|
||||
from backend.agents.prompt_loader import PromptLoader
|
||||
from backend.llm.models import get_agent_formatter, get_agent_model
|
||||
from backend.config.constants import ANALYST_TYPES
|
||||
|
||||
# Team infrastructure imports (graceful import - may not exist yet)
|
||||
try:
|
||||
from backend.agents.team.team_coordinator import TeamCoordinator
|
||||
from backend.agents.team.msg_hub import MsgHub as TeamMsgHub
|
||||
TEAM_COORD_AVAILABLE = True
|
||||
except ImportError:
|
||||
TEAM_COORD_AVAILABLE = False
|
||||
TeamCoordinator = None
|
||||
TeamMsgHub = None
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -77,6 +99,13 @@ class TradingPipeline:
|
||||
self.agent_factory = agent_factory
|
||||
self.runtime_manager = runtime_manager
|
||||
self._session_key: Optional[str] = None
|
||||
self._dynamic_analysts: Dict[str, Any] = {}
|
||||
|
||||
if hasattr(self.pm, "set_team_controller"):
|
||||
self.pm.set_team_controller(
|
||||
create_agent_callback=self._create_runtime_analyst,
|
||||
remove_agent_callback=self._remove_runtime_analyst,
|
||||
)
|
||||
|
||||
async def run_cycle(
|
||||
self,
|
||||
@@ -115,16 +144,17 @@ class TradingPipeline:
|
||||
_log(f"Starting cycle {date} - {len(tickers)} tickers")
|
||||
session_key = TradingSessionKey(date=date).key()
|
||||
self._session_key = session_key
|
||||
active_analysts = self._get_active_analysts()
|
||||
if self.runtime_manager:
|
||||
self.runtime_manager.set_session_key(session_key)
|
||||
self._runtime_log_event("cycle:start", {"tickers": tickers, "date": date})
|
||||
self._runtime_batch_status(self.analysts, "analysis_in_progress")
|
||||
self._runtime_batch_status(active_analysts, "analysis_in_progress")
|
||||
|
||||
# Phase 0: Clear short-term memory to avoid cross-day context pollution
|
||||
_log("Phase 0: Clearing memory")
|
||||
await self._clear_all_agent_memory()
|
||||
|
||||
participants = self.analysts + [self.risk_manager, self.pm]
|
||||
participants = self._all_analysts() + [self.risk_manager, self.pm]
|
||||
|
||||
# Single MsgHub for entire cycle - no nesting
|
||||
async with MsgHub(
|
||||
@@ -135,9 +165,13 @@ class TradingPipeline:
|
||||
"system",
|
||||
),
|
||||
):
|
||||
# Phase 1.1: Analysts
|
||||
_log("Phase 1.1: Analyst analysis")
|
||||
analyst_results = await self._run_analysts_with_sync(tickers, date)
|
||||
# Phase 1.1: Analysts (parallel execution with TeamCoordinator)
|
||||
_log("Phase 1.1: Analyst analysis (parallel)")
|
||||
analyst_results = await self._run_analysts_parallel(
|
||||
tickers,
|
||||
date,
|
||||
active_analysts=active_analysts,
|
||||
)
|
||||
|
||||
# Phase 1.2: Risk Manager
|
||||
_log("Phase 1.2: Risk assessment")
|
||||
@@ -164,6 +198,7 @@ class TradingPipeline:
|
||||
final_predictions = await self._collect_final_predictions(
|
||||
tickers,
|
||||
date,
|
||||
active_analysts=active_analysts,
|
||||
)
|
||||
|
||||
# Record final predictions for leaderboard ranking
|
||||
@@ -212,7 +247,7 @@ class TradingPipeline:
|
||||
if close_prices and self.settlement_coordinator:
|
||||
_log("Phase 5: Daily review and generate memories")
|
||||
self._runtime_batch_status(
|
||||
[self.risk_manager] + self.analysts + [self.pm],
|
||||
[self.risk_manager] + self._all_analysts() + [self.pm],
|
||||
"settlement",
|
||||
)
|
||||
|
||||
@@ -246,13 +281,13 @@ class TradingPipeline:
|
||||
conference_summary=self.conference_summary,
|
||||
)
|
||||
self._runtime_batch_status(
|
||||
[self.risk_manager] + self.analysts + [self.pm],
|
||||
[self.risk_manager] + self._all_analysts() + [self.pm],
|
||||
"reflection",
|
||||
)
|
||||
|
||||
_log(f"Cycle complete: {date}")
|
||||
self._runtime_batch_status(
|
||||
self.analysts + [self.risk_manager, self.pm],
|
||||
self._all_analysts() + [self.risk_manager, self.pm],
|
||||
"idle",
|
||||
)
|
||||
self._runtime_log_event("cycle:end", {"tickers": tickers, "date": date})
|
||||
@@ -288,7 +323,7 @@ class TradingPipeline:
|
||||
},
|
||||
)
|
||||
|
||||
for analyst in self.analysts:
|
||||
for analyst in self._all_analysts():
|
||||
analyst.reload_runtime_assets(
|
||||
active_skill_dirs=active_skill_map.get(analyst.name, []),
|
||||
)
|
||||
@@ -302,7 +337,7 @@ class TradingPipeline:
|
||||
|
||||
return {
|
||||
"config_name": config_name,
|
||||
"reloaded_agents": [agent.name for agent in self.analysts]
|
||||
"reloaded_agents": [agent.name for agent in self._all_analysts()]
|
||||
+ ["risk_manager", "portfolio_manager"],
|
||||
"active_skills": {
|
||||
agent_id: [path.name for path in paths]
|
||||
@@ -313,7 +348,7 @@ class TradingPipeline:
|
||||
|
||||
async def _clear_all_agent_memory(self):
|
||||
"""Clear short-term memory for all agents"""
|
||||
for analyst in self.analysts:
|
||||
for analyst in self._all_analysts():
|
||||
await analyst.memory.clear()
|
||||
|
||||
await self.risk_manager.memory.clear()
|
||||
@@ -395,7 +430,7 @@ class TradingPipeline:
|
||||
trajectories = {}
|
||||
|
||||
# Capture analyst trajectories
|
||||
for analyst in self.analysts:
|
||||
for analyst in self._all_analysts():
|
||||
try:
|
||||
msgs = await analyst.memory.get_memory()
|
||||
if msgs:
|
||||
@@ -605,7 +640,7 @@ class TradingPipeline:
|
||||
)
|
||||
|
||||
# Record for analysts
|
||||
for analyst in self.analysts:
|
||||
for analyst in self._all_analysts():
|
||||
if (
|
||||
hasattr(analyst, "long_term_memory")
|
||||
and analyst.long_term_memory is not None
|
||||
@@ -724,67 +759,82 @@ class TradingPipeline:
|
||||
date=date,
|
||||
)
|
||||
|
||||
# Run discussion cycles (no new MsgHub - use parent's)
|
||||
for cycle in range(self.max_comm_cycles):
|
||||
# Conference participants: analysts + PM
|
||||
conference_participants = self._get_active_analysts() + [self.pm]
|
||||
|
||||
# Use TeamMsgHub for conference if available
|
||||
if TEAM_COORD_AVAILABLE and TeamMsgHub is not None:
|
||||
_log(
|
||||
"Phase 2.1: Conference discussion - "
|
||||
f"Conference {cycle + 1}/{self.max_comm_cycles}",
|
||||
f"Phase 2.1: Conference using TeamMsgHub with "
|
||||
f"{len(conference_participants)} participants"
|
||||
)
|
||||
conference_hub = TeamMsgHub(participants=conference_participants)
|
||||
else:
|
||||
_log("Phase 2.1: Conference using standard MsgHub context")
|
||||
conference_hub = None
|
||||
|
||||
if self.state_sync:
|
||||
await self.state_sync.on_conference_cycle_start(
|
||||
cycle=cycle + 1,
|
||||
total_cycles=self.max_comm_cycles,
|
||||
# Run discussion cycles
|
||||
async with conference_hub if conference_hub else nullcontext(None):
|
||||
for cycle in range(self.max_comm_cycles):
|
||||
_log(
|
||||
"Phase 2.1: Conference discussion - "
|
||||
f"Conference {cycle + 1}/{self.max_comm_cycles}",
|
||||
)
|
||||
|
||||
# PM sets agenda or asks questions
|
||||
pm_prompt = self._build_pm_discussion_prompt(
|
||||
cycle=cycle,
|
||||
tickers=tickers,
|
||||
date=date,
|
||||
prices=prices,
|
||||
analyst_results=analyst_results,
|
||||
risk_assessment=risk_assessment,
|
||||
)
|
||||
if self.state_sync:
|
||||
await self.state_sync.on_conference_cycle_start(
|
||||
cycle=cycle + 1,
|
||||
total_cycles=self.max_comm_cycles,
|
||||
)
|
||||
|
||||
pm_msg = Msg(name="system", content=pm_prompt, role="user")
|
||||
pm_response = await self.pm.reply(pm_msg)
|
||||
|
||||
if self.state_sync:
|
||||
pm_content = self._extract_text_content(pm_response.content)
|
||||
await self.state_sync.on_conference_message(
|
||||
agent_id="portfolio_manager",
|
||||
content=pm_content,
|
||||
)
|
||||
|
||||
# Analysts share perspectives
|
||||
for analyst in self.analysts:
|
||||
analyst_prompt = self._build_analyst_discussion_prompt(
|
||||
# PM sets agenda or asks questions
|
||||
pm_prompt = self._build_pm_discussion_prompt(
|
||||
cycle=cycle,
|
||||
tickers=tickers,
|
||||
date=date,
|
||||
prices=prices,
|
||||
analyst_results=analyst_results,
|
||||
risk_assessment=risk_assessment,
|
||||
)
|
||||
|
||||
analyst_msg = Msg(
|
||||
name="system",
|
||||
content=analyst_prompt,
|
||||
role="user",
|
||||
)
|
||||
analyst_response = await analyst.reply(analyst_msg)
|
||||
pm_msg = Msg(name="system", content=pm_prompt, role="user")
|
||||
pm_response = await self.pm.reply(pm_msg)
|
||||
|
||||
if self.state_sync:
|
||||
analyst_content = self._extract_text_content(
|
||||
analyst_response.content,
|
||||
)
|
||||
pm_content = self._extract_text_content(pm_response.content)
|
||||
await self.state_sync.on_conference_message(
|
||||
agent_id=analyst.name,
|
||||
content=analyst_content,
|
||||
agent_id="portfolio_manager",
|
||||
content=pm_content,
|
||||
)
|
||||
|
||||
if self.state_sync:
|
||||
await self.state_sync.on_conference_cycle_end(
|
||||
cycle=cycle + 1,
|
||||
)
|
||||
# Analysts share perspectives (supports per-round active team updates)
|
||||
for analyst in self._get_active_analysts():
|
||||
analyst_prompt = self._build_analyst_discussion_prompt(
|
||||
cycle=cycle,
|
||||
tickers=tickers,
|
||||
date=date,
|
||||
)
|
||||
|
||||
analyst_msg = Msg(
|
||||
name="system",
|
||||
content=analyst_prompt,
|
||||
role="user",
|
||||
)
|
||||
analyst_response = await analyst.reply(analyst_msg)
|
||||
|
||||
if self.state_sync:
|
||||
analyst_content = self._extract_text_content(
|
||||
analyst_response.content,
|
||||
)
|
||||
await self.state_sync.on_conference_message(
|
||||
agent_id=analyst.name,
|
||||
content=analyst_content,
|
||||
)
|
||||
|
||||
if self.state_sync:
|
||||
await self.state_sync.on_conference_cycle_end(
|
||||
cycle=cycle + 1,
|
||||
)
|
||||
|
||||
# Generate conference summary by PM
|
||||
_log(
|
||||
@@ -885,6 +935,7 @@ class TradingPipeline:
|
||||
self,
|
||||
tickers: List[str],
|
||||
date: str,
|
||||
active_analysts: Optional[List[Any]] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Collect final predictions from all analysts as simple text responses.
|
||||
@@ -892,14 +943,15 @@ class TradingPipeline:
|
||||
"""
|
||||
_log(
|
||||
"Phase 2.2: Analysts generate final structured predictions\n"
|
||||
f" Starting _collect_final_predictions for {len(self.analysts)} analysts",
|
||||
f" Starting _collect_final_predictions for {len(active_analysts or self.analysts)} analysts",
|
||||
)
|
||||
final_predictions = []
|
||||
|
||||
for i, analyst in enumerate(self.analysts):
|
||||
analysts = active_analysts or self.analysts
|
||||
for i, analyst in enumerate(analysts):
|
||||
_log(
|
||||
"Phase 2.2: Analysts generate final structured predictions\n"
|
||||
f" Collecting prediction from analyst {i+1}/{len(self.analysts)}: {analyst.name}",
|
||||
f" Collecting prediction from analyst {i+1}/{len(analysts)}: {analyst.name}",
|
||||
)
|
||||
|
||||
prompt = (
|
||||
@@ -995,11 +1047,13 @@ class TradingPipeline:
|
||||
self,
|
||||
tickers: List[str],
|
||||
date: str,
|
||||
active_analysts: Optional[List[Any]] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Run all analysts with real-time sync after each completion"""
|
||||
results = []
|
||||
analysts = active_analysts or self.analysts
|
||||
|
||||
for analyst in self.analysts:
|
||||
for analyst in analysts:
|
||||
content = (
|
||||
f"Analyze the following stocks for date {date}: {', '.join(tickers)}. "
|
||||
f"Provide investment signals with confidence scores and reasoning."
|
||||
@@ -1029,15 +1083,107 @@ class TradingPipeline:
|
||||
|
||||
return results
|
||||
|
||||
async def _run_analysts_parallel(
|
||||
self,
|
||||
tickers: List[str],
|
||||
date: str,
|
||||
active_analysts: Optional[List[Any]] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Run all analysts in parallel using TeamCoordinator.
|
||||
|
||||
This method replaces the sequential analyst loop with parallel execution
|
||||
using the TeamCoordinator for orchestration.
|
||||
|
||||
Args:
|
||||
tickers: List of stock tickers to analyze
|
||||
date: Trading date
|
||||
active_analysts: Optional list of analysts to run
|
||||
|
||||
Returns:
|
||||
List of analyst result dictionaries
|
||||
"""
|
||||
analysts = active_analysts or self.analysts
|
||||
|
||||
if not analysts:
|
||||
return []
|
||||
|
||||
if not TEAM_COORD_AVAILABLE:
|
||||
_log("TeamCoordinator not available, falling back to sequential execution")
|
||||
return await self._run_analysts_with_sync(
|
||||
tickers=tickers,
|
||||
date=date,
|
||||
active_analysts=active_analysts,
|
||||
)
|
||||
|
||||
_log(
|
||||
f"Phase 1.1: Running {len(analysts)} analysts in parallel "
|
||||
f"[{', '.join(a.name for a in analysts)}]"
|
||||
)
|
||||
|
||||
# Build the analyst prompt
|
||||
content = (
|
||||
f"Analyze the following stocks for date {date}: {', '.join(tickers)}. "
|
||||
f"Provide investment signals with confidence scores and reasoning."
|
||||
)
|
||||
|
||||
# Create coordinator for parallel execution
|
||||
coordinator = TeamCoordinator(
|
||||
participants=analysts,
|
||||
task_content=content,
|
||||
)
|
||||
|
||||
# Run analysts in parallel via TeamCoordinator
|
||||
results = await coordinator.run_phase(
|
||||
"analyst_analysis",
|
||||
metadata={"tickers": tickers, "date": date},
|
||||
)
|
||||
|
||||
# Process results and sync
|
||||
processed_results = []
|
||||
for i, (analyst, result) in enumerate(zip(analysts, results)):
|
||||
if result is not None:
|
||||
extracted = self._extract_result_from_msg(result)
|
||||
processed_results.append(extracted)
|
||||
|
||||
# Sync retrieved memory
|
||||
await self._sync_memory_if_retrieved(analyst)
|
||||
|
||||
# Broadcast agent result via StateSync
|
||||
if self.state_sync:
|
||||
text_content = self._extract_text_content(result.content)
|
||||
await self.state_sync.on_agent_complete(
|
||||
agent_id=analyst.name,
|
||||
content=text_content,
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
"Analyst %s returned no result",
|
||||
analyst.name,
|
||||
)
|
||||
processed_results.append({
|
||||
"agent": analyst.name,
|
||||
"content": "",
|
||||
"success": False,
|
||||
})
|
||||
|
||||
_log(
|
||||
f"Phase 1.1: Parallel analyst execution complete "
|
||||
f"({len(processed_results)}/{len(analysts)} successful)"
|
||||
)
|
||||
|
||||
return processed_results
|
||||
|
||||
async def _run_analysts(
|
||||
self,
|
||||
tickers: List[str],
|
||||
date: str,
|
||||
active_analysts: Optional[List[Any]] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Run all analysts (without sync, for backward compatibility)"""
|
||||
results = []
|
||||
analysts = active_analysts or self.analysts
|
||||
|
||||
for analyst in self.analysts:
|
||||
for analyst in analysts:
|
||||
content = (
|
||||
f"Analyze the following stocks for date {date}: {', '.join(tickers)}. "
|
||||
f"Provide investment signals with confidence scores and reasoning."
|
||||
@@ -1461,6 +1607,83 @@ class TradingPipeline:
|
||||
for agent in agents:
|
||||
self._runtime_update_status(agent, status)
|
||||
|
||||
def _all_analysts(self) -> List[Any]:
|
||||
"""Return static analysts plus runtime-created analysts."""
|
||||
return list(self.analysts) + list(self._dynamic_analysts.values())
|
||||
|
||||
def _create_runtime_analyst(self, agent_id: str, analyst_type: str) -> str:
|
||||
"""Create one runtime analyst instance."""
|
||||
if analyst_type not in ANALYST_TYPES:
|
||||
return (
|
||||
f"Unknown analyst_type '{analyst_type}'. "
|
||||
f"Available: {', '.join(ANALYST_TYPES.keys())}"
|
||||
)
|
||||
if agent_id in {agent.name for agent in self._all_analysts()}:
|
||||
return f"Analyst '{agent_id}' already exists."
|
||||
|
||||
config_name = getattr(self.pm, "config", {}).get("config_name", "default")
|
||||
project_root = Path(__file__).resolve().parents[2]
|
||||
personas = PromptLoader().load_yaml_config("analyst", "personas")
|
||||
persona = personas.get(analyst_type, {})
|
||||
WorkspaceManager(project_root=project_root).ensure_agent_assets(
|
||||
config_name=config_name,
|
||||
agent_id=agent_id,
|
||||
role_seed=persona.get("description", "").strip(),
|
||||
style_seed="\n".join(f"- {item}" for item in persona.get("focus", [])),
|
||||
policy_seed=(
|
||||
"State a clear signal, confidence, and the conditions "
|
||||
"that would invalidate the thesis."
|
||||
),
|
||||
)
|
||||
|
||||
agent = AnalystAgent(
|
||||
analyst_type=analyst_type,
|
||||
toolkit=create_agent_toolkit(
|
||||
agent_id=agent_id,
|
||||
config_name=config_name,
|
||||
active_skill_dirs=[],
|
||||
),
|
||||
model=get_agent_model(analyst_type),
|
||||
formatter=get_agent_formatter(analyst_type),
|
||||
agent_id=agent_id,
|
||||
config={"config_name": config_name},
|
||||
)
|
||||
self._dynamic_analysts[agent_id] = agent
|
||||
update_active_analysts(
|
||||
project_root=project_root,
|
||||
config_name=config_name,
|
||||
available_analysts=[item.name for item in self._all_analysts()],
|
||||
add=[agent_id],
|
||||
)
|
||||
return f"Created runtime analyst '{agent_id}' ({analyst_type})."
|
||||
|
||||
def _remove_runtime_analyst(self, agent_id: str) -> str:
|
||||
"""Remove one runtime-created analyst instance."""
|
||||
if agent_id not in self._dynamic_analysts:
|
||||
return f"Runtime analyst '{agent_id}' not found."
|
||||
self._dynamic_analysts.pop(agent_id, None)
|
||||
config_name = getattr(self.pm, "config", {}).get("config_name", "default")
|
||||
project_root = Path(__file__).resolve().parents[2]
|
||||
update_active_analysts(
|
||||
project_root=project_root,
|
||||
config_name=config_name,
|
||||
available_analysts=[item.name for item in self._all_analysts()],
|
||||
remove=[agent_id],
|
||||
)
|
||||
return f"Removed runtime analyst '{agent_id}'."
|
||||
|
||||
def _get_active_analysts(self) -> List[Any]:
|
||||
"""Resolve active analyst participants from run-scoped team pipeline config."""
|
||||
config_name = getattr(self.pm, "config", {}).get("config_name", "default")
|
||||
project_root = Path(__file__).resolve().parents[2]
|
||||
analyst_map = {agent.name: agent for agent in self._all_analysts()}
|
||||
active_ids = resolve_active_analysts(
|
||||
project_root=project_root,
|
||||
config_name=config_name,
|
||||
available_analysts=list(analyst_map.keys()),
|
||||
)
|
||||
return [analyst_map[agent_id] for agent_id in active_ids if agent_id in analyst_map]
|
||||
|
||||
def _runtime_log_event(self, event: str, details: Optional[Dict[str, Any]] = None) -> None:
|
||||
if not self.runtime_manager:
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user