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)
426 lines
20 KiB
Python
426 lines
20 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Runtime/workspace/skills handlers extracted from the main Gateway module.
|
|
|
|
Deprecated note:
|
|
Agent/workspace/skill read-write operations are being migrated to
|
|
agent_service REST endpoints. These websocket handlers remain as a
|
|
compatibility fallback and should not be considered the primary control
|
|
plane path for frontend reads/writes.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from datetime import datetime
|
|
from typing import Any
|
|
|
|
from backend.agents.agent_workspace import load_agent_workspace_config
|
|
from backend.agents.skills_manager import SkillsManager
|
|
from backend.agents.toolkit_factory import load_agent_profiles
|
|
from backend.config.bootstrap_config import (
|
|
get_bootstrap_config_for_run,
|
|
resolve_runtime_config,
|
|
update_bootstrap_values_for_run,
|
|
)
|
|
from backend.llm.models import get_agent_model_info
|
|
|
|
|
|
async def handle_reload_runtime_assets(gateway: Any) -> None:
|
|
config_name = gateway.config.get("config_name", "default")
|
|
runtime_config = resolve_runtime_config(
|
|
project_root=gateway._project_root,
|
|
config_name=config_name,
|
|
enable_memory=gateway.config.get("enable_memory", False),
|
|
schedule_mode=gateway.config.get("schedule_mode", "daily"),
|
|
interval_minutes=gateway.config.get("interval_minutes", 60),
|
|
trigger_time=gateway.config.get("trigger_time", "09:30"),
|
|
)
|
|
result = gateway.pipeline.reload_runtime_assets(runtime_config=runtime_config)
|
|
runtime_updates = gateway._apply_runtime_config(runtime_config)
|
|
await gateway.state_sync.on_system_message("Runtime assets reloaded.")
|
|
await gateway.broadcast({"type": "runtime_assets_reloaded", **result, **runtime_updates})
|
|
|
|
|
|
async def handle_update_runtime_config(gateway: Any, websocket: Any, data: dict[str, Any]) -> None:
|
|
updates: dict[str, Any] = {}
|
|
|
|
schedule_mode = str(data.get("schedule_mode", "")).strip().lower()
|
|
if schedule_mode:
|
|
if schedule_mode not in {"daily", "intraday"}:
|
|
await websocket.send(json.dumps({"type": "error", "message": "schedule_mode must be 'daily' or 'intraday'."}, ensure_ascii=False))
|
|
return
|
|
updates["schedule_mode"] = schedule_mode
|
|
|
|
interval_minutes = data.get("interval_minutes")
|
|
if interval_minutes is not None:
|
|
try:
|
|
parsed_interval = int(interval_minutes)
|
|
except (TypeError, ValueError):
|
|
parsed_interval = 0
|
|
if parsed_interval <= 0:
|
|
await websocket.send(json.dumps({"type": "error", "message": "interval_minutes must be a positive integer."}, ensure_ascii=False))
|
|
return
|
|
updates["interval_minutes"] = parsed_interval
|
|
|
|
trigger_time = data.get("trigger_time")
|
|
if trigger_time is not None:
|
|
raw_trigger = str(trigger_time).strip()
|
|
if raw_trigger and raw_trigger != "now":
|
|
try:
|
|
datetime.strptime(raw_trigger, "%H:%M")
|
|
except ValueError:
|
|
await websocket.send(json.dumps({"type": "error", "message": "trigger_time must use HH:MM or 'now'."}, ensure_ascii=False))
|
|
return
|
|
updates["trigger_time"] = raw_trigger or "09:30"
|
|
|
|
max_comm_cycles = data.get("max_comm_cycles")
|
|
if max_comm_cycles is not None:
|
|
try:
|
|
parsed_cycles = int(max_comm_cycles)
|
|
except (TypeError, ValueError):
|
|
parsed_cycles = 0
|
|
if parsed_cycles <= 0:
|
|
await websocket.send(json.dumps({"type": "error", "message": "max_comm_cycles must be a positive integer."}, ensure_ascii=False))
|
|
return
|
|
updates["max_comm_cycles"] = parsed_cycles
|
|
|
|
initial_cash = data.get("initial_cash")
|
|
if initial_cash is not None:
|
|
try:
|
|
parsed_initial_cash = float(initial_cash)
|
|
except (TypeError, ValueError):
|
|
parsed_initial_cash = 0.0
|
|
if parsed_initial_cash <= 0:
|
|
await websocket.send(json.dumps({"type": "error", "message": "initial_cash must be a positive number."}, ensure_ascii=False))
|
|
return
|
|
updates["initial_cash"] = parsed_initial_cash
|
|
|
|
margin_requirement = data.get("margin_requirement")
|
|
if margin_requirement is not None:
|
|
try:
|
|
parsed_margin_requirement = float(margin_requirement)
|
|
except (TypeError, ValueError):
|
|
parsed_margin_requirement = -1.0
|
|
if parsed_margin_requirement < 0:
|
|
await websocket.send(json.dumps({"type": "error", "message": "margin_requirement must be a non-negative number."}, ensure_ascii=False))
|
|
return
|
|
updates["margin_requirement"] = parsed_margin_requirement
|
|
|
|
enable_memory = data.get("enable_memory")
|
|
if enable_memory is not None:
|
|
updates["enable_memory"] = bool(enable_memory)
|
|
|
|
if not updates:
|
|
await websocket.send(json.dumps({"type": "error", "message": "No runtime settings were provided."}, ensure_ascii=False))
|
|
return
|
|
|
|
config_name = gateway.config.get("config_name", "default")
|
|
update_bootstrap_values_for_run(
|
|
project_root=gateway._project_root,
|
|
config_name=config_name,
|
|
updates=updates,
|
|
)
|
|
await gateway.state_sync.on_system_message("运行时调度配置已保存,正在热更新")
|
|
await handle_reload_runtime_assets(gateway)
|
|
|
|
|
|
async def handle_update_watchlist(gateway: Any, websocket: Any, data: dict[str, Any]) -> None:
|
|
tickers = gateway._normalize_watchlist(data.get("tickers"))
|
|
if not tickers:
|
|
await websocket.send(json.dumps({"type": "error", "message": "update_watchlist requires at least one valid ticker."}, ensure_ascii=False))
|
|
return
|
|
|
|
config_name = gateway.config.get("config_name", "default")
|
|
update_bootstrap_values_for_run(
|
|
project_root=gateway._project_root,
|
|
config_name=config_name,
|
|
updates={"tickers": tickers},
|
|
)
|
|
await gateway.state_sync.on_system_message(f"Watchlist updated: {', '.join(tickers)}")
|
|
await gateway.broadcast({"type": "watchlist_updated", "config_name": config_name, "tickers": tickers})
|
|
await handle_reload_runtime_assets(gateway)
|
|
gateway._schedule_watchlist_market_store_refresh(tickers)
|
|
|
|
|
|
async def handle_get_agent_skills(gateway: Any, websocket: Any, data: dict[str, Any]) -> None:
|
|
agent_id = str(data.get("agent_id", "")).strip()
|
|
if not agent_id:
|
|
await websocket.send(json.dumps({"type": "error", "message": "get_agent_skills requires agent_id."}, ensure_ascii=False))
|
|
return
|
|
|
|
config_name = gateway.config.get("config_name", "default")
|
|
skills_manager = SkillsManager(project_root=gateway._project_root)
|
|
agent_asset_dir = skills_manager.get_agent_asset_dir(config_name, agent_id)
|
|
agent_config = load_agent_workspace_config(agent_asset_dir / "agent.yaml")
|
|
resolved_skills = set(skills_manager.resolve_agent_skill_names(config_name=config_name, agent_id=agent_id, default_skills=[]))
|
|
enabled = set(agent_config.enabled_skills)
|
|
disabled = set(agent_config.disabled_skills)
|
|
|
|
payload = []
|
|
for item in skills_manager.list_agent_skill_catalog(config_name, agent_id):
|
|
if item.skill_name in disabled:
|
|
status = "disabled"
|
|
elif item.skill_name in enabled:
|
|
status = "enabled"
|
|
elif item.skill_name in resolved_skills:
|
|
status = "active"
|
|
else:
|
|
status = "available"
|
|
payload.append({
|
|
"skill_name": item.skill_name,
|
|
"name": item.name,
|
|
"description": item.description,
|
|
"version": item.version,
|
|
"source": item.source,
|
|
"tools": item.tools,
|
|
"status": status,
|
|
})
|
|
|
|
await websocket.send(json.dumps({
|
|
"type": "agent_skills_loaded",
|
|
"config_name": config_name,
|
|
"agent_id": agent_id,
|
|
"skills": payload,
|
|
}, ensure_ascii=False))
|
|
|
|
|
|
async def handle_get_agent_profile(gateway: Any, websocket: Any, data: dict[str, Any]) -> None:
|
|
agent_id = str(data.get("agent_id", "")).strip()
|
|
if not agent_id:
|
|
await websocket.send(json.dumps({"type": "error", "message": "get_agent_profile requires agent_id."}, ensure_ascii=False))
|
|
return
|
|
|
|
config_name = gateway.config.get("config_name", "default")
|
|
skills_manager = SkillsManager(project_root=gateway._project_root)
|
|
asset_dir = skills_manager.get_agent_asset_dir(config_name, agent_id)
|
|
agent_config = load_agent_workspace_config(asset_dir / "agent.yaml")
|
|
profiles = load_agent_profiles()
|
|
profile = profiles.get(agent_id, {})
|
|
bootstrap = get_bootstrap_config_for_run(gateway._project_root, config_name)
|
|
override = bootstrap.agent_override(agent_id)
|
|
active_tool_groups = override.get("active_tool_groups", agent_config.active_tool_groups or profile.get("active_tool_groups", []))
|
|
if not isinstance(active_tool_groups, list):
|
|
active_tool_groups = []
|
|
disabled_tool_groups = agent_config.disabled_tool_groups
|
|
if disabled_tool_groups:
|
|
disabled_set = set(disabled_tool_groups)
|
|
active_tool_groups = [group_name for group_name in active_tool_groups if group_name not in disabled_set]
|
|
|
|
default_skills = profile.get("skills", [])
|
|
if not isinstance(default_skills, list):
|
|
default_skills = []
|
|
resolved_skills = skills_manager.resolve_agent_skill_names(
|
|
config_name=config_name,
|
|
agent_id=agent_id,
|
|
default_skills=default_skills,
|
|
)
|
|
prompt_files = agent_config.prompt_files or ["SOUL.md", "PROFILE.md", "AGENTS.md", "POLICY.md", "MEMORY.md"]
|
|
model_name, model_provider = get_agent_model_info(agent_id)
|
|
|
|
await websocket.send(json.dumps({
|
|
"type": "agent_profile_loaded",
|
|
"config_name": config_name,
|
|
"agent_id": agent_id,
|
|
"profile": {
|
|
"model_name": model_name,
|
|
"model_provider": model_provider,
|
|
"prompt_files": prompt_files,
|
|
"default_skills": default_skills,
|
|
"resolved_skills": resolved_skills,
|
|
"active_tool_groups": active_tool_groups,
|
|
"disabled_tool_groups": disabled_tool_groups,
|
|
"enabled_skills": agent_config.enabled_skills,
|
|
"disabled_skills": agent_config.disabled_skills,
|
|
},
|
|
}, ensure_ascii=False))
|
|
|
|
|
|
async def handle_get_skill_detail(gateway: Any, websocket: Any, data: dict[str, Any]) -> None:
|
|
agent_id = str(data.get("agent_id", "")).strip()
|
|
skill_name = str(data.get("skill_name", "")).strip()
|
|
if not skill_name:
|
|
await websocket.send(json.dumps({"type": "error", "message": "get_skill_detail requires skill_name."}, ensure_ascii=False))
|
|
return
|
|
|
|
skills_manager = SkillsManager(project_root=gateway._project_root)
|
|
try:
|
|
if agent_id:
|
|
config_name = gateway.config.get("config_name", "default")
|
|
detail = skills_manager.load_agent_skill_document(config_name=config_name, agent_id=agent_id, skill_name=skill_name)
|
|
else:
|
|
detail = skills_manager.load_skill_document(skill_name)
|
|
except FileNotFoundError:
|
|
await websocket.send(json.dumps({"type": "error", "message": f"Unknown skill: {skill_name}"}, ensure_ascii=False))
|
|
return
|
|
|
|
await websocket.send(json.dumps({
|
|
"type": "skill_detail_loaded",
|
|
"agent_id": agent_id,
|
|
"skill": detail,
|
|
}, ensure_ascii=False))
|
|
|
|
|
|
async def handle_create_agent_local_skill(gateway: Any, websocket: Any, data: dict[str, Any]) -> None:
|
|
agent_id = str(data.get("agent_id", "")).strip()
|
|
skill_name = str(data.get("skill_name", "")).strip()
|
|
if not agent_id or not skill_name:
|
|
await websocket.send(json.dumps({"type": "error", "message": "create_agent_local_skill requires agent_id and skill_name."}, ensure_ascii=False))
|
|
return
|
|
|
|
config_name = gateway.config.get("config_name", "default")
|
|
skills_manager = SkillsManager(project_root=gateway._project_root)
|
|
try:
|
|
skills_manager.create_agent_local_skill(config_name=config_name, agent_id=agent_id, skill_name=skill_name)
|
|
except (ValueError, FileExistsError) as exc:
|
|
await websocket.send(json.dumps({"type": "error", "message": str(exc)}, ensure_ascii=False))
|
|
return
|
|
|
|
await gateway.state_sync.on_system_message(f"Created local skill {skill_name} for {agent_id}")
|
|
await gateway._handle_reload_runtime_assets()
|
|
await websocket.send(json.dumps({"type": "agent_local_skill_created", "agent_id": agent_id, "skill_name": skill_name}, ensure_ascii=False))
|
|
await handle_get_agent_skills(gateway, websocket, {"agent_id": agent_id})
|
|
await handle_get_skill_detail(gateway, websocket, {"agent_id": agent_id, "skill_name": skill_name})
|
|
|
|
|
|
async def handle_update_agent_local_skill(gateway: Any, websocket: Any, data: dict[str, Any]) -> None:
|
|
agent_id = str(data.get("agent_id", "")).strip()
|
|
skill_name = str(data.get("skill_name", "")).strip()
|
|
content = data.get("content")
|
|
if not agent_id or not skill_name or not isinstance(content, str):
|
|
await websocket.send(json.dumps({"type": "error", "message": "update_agent_local_skill requires agent_id, skill_name, and string content."}, ensure_ascii=False))
|
|
return
|
|
|
|
config_name = gateway.config.get("config_name", "default")
|
|
skills_manager = SkillsManager(project_root=gateway._project_root)
|
|
try:
|
|
skills_manager.update_agent_local_skill(config_name=config_name, agent_id=agent_id, skill_name=skill_name, content=content)
|
|
except (ValueError, FileNotFoundError) as exc:
|
|
await websocket.send(json.dumps({"type": "error", "message": str(exc)}, ensure_ascii=False))
|
|
return
|
|
|
|
await gateway.state_sync.on_system_message(f"Updated local skill {skill_name} for {agent_id}")
|
|
await gateway._handle_reload_runtime_assets()
|
|
await websocket.send(json.dumps({"type": "agent_local_skill_updated", "agent_id": agent_id, "skill_name": skill_name}, ensure_ascii=False))
|
|
await handle_get_skill_detail(gateway, websocket, {"agent_id": agent_id, "skill_name": skill_name})
|
|
|
|
|
|
async def handle_delete_agent_local_skill(gateway: Any, websocket: Any, data: dict[str, Any]) -> None:
|
|
agent_id = str(data.get("agent_id", "")).strip()
|
|
skill_name = str(data.get("skill_name", "")).strip()
|
|
if not agent_id or not skill_name:
|
|
await websocket.send(json.dumps({"type": "error", "message": "delete_agent_local_skill requires agent_id and skill_name."}, ensure_ascii=False))
|
|
return
|
|
|
|
config_name = gateway.config.get("config_name", "default")
|
|
skills_manager = SkillsManager(project_root=gateway._project_root)
|
|
try:
|
|
skills_manager.delete_agent_local_skill(config_name=config_name, agent_id=agent_id, skill_name=skill_name)
|
|
skills_manager.forget_agent_skill_overrides(config_name=config_name, agent_id=agent_id, skill_names=[skill_name])
|
|
except (ValueError, FileNotFoundError) as exc:
|
|
await websocket.send(json.dumps({"type": "error", "message": str(exc)}, ensure_ascii=False))
|
|
return
|
|
|
|
await gateway.state_sync.on_system_message(f"Deleted local skill {skill_name} for {agent_id}")
|
|
await gateway._handle_reload_runtime_assets()
|
|
await websocket.send(json.dumps({"type": "agent_local_skill_deleted", "agent_id": agent_id, "skill_name": skill_name}, ensure_ascii=False))
|
|
await handle_get_agent_skills(gateway, websocket, {"agent_id": agent_id})
|
|
|
|
|
|
async def handle_remove_agent_skill(gateway: Any, websocket: Any, data: dict[str, Any]) -> None:
|
|
agent_id = str(data.get("agent_id", "")).strip()
|
|
skill_name = str(data.get("skill_name", "")).strip()
|
|
if not agent_id or not skill_name:
|
|
await websocket.send(json.dumps({"type": "error", "message": "remove_agent_skill requires agent_id and skill_name."}, ensure_ascii=False))
|
|
return
|
|
|
|
config_name = gateway.config.get("config_name", "default")
|
|
skills_manager = SkillsManager(project_root=gateway._project_root)
|
|
skill_names = {
|
|
item.skill_name
|
|
for item in skills_manager.list_agent_skill_catalog(config_name, agent_id)
|
|
if item.source != "local"
|
|
}
|
|
if skill_name not in skill_names:
|
|
await websocket.send(json.dumps({"type": "error", "message": f"Unknown shared skill: {skill_name}"}, ensure_ascii=False))
|
|
return
|
|
|
|
skills_manager.update_agent_skill_overrides(config_name=config_name, agent_id=agent_id, disable=[skill_name])
|
|
await gateway.state_sync.on_system_message(f"Removed shared skill {skill_name} from {agent_id}")
|
|
await gateway._handle_reload_runtime_assets()
|
|
await websocket.send(json.dumps({"type": "agent_skill_removed", "agent_id": agent_id, "skill_name": skill_name}, ensure_ascii=False))
|
|
await handle_get_agent_skills(gateway, websocket, {"agent_id": agent_id})
|
|
|
|
|
|
async def handle_update_agent_skill(gateway: Any, websocket: Any, data: dict[str, Any]) -> None:
|
|
agent_id = str(data.get("agent_id", "")).strip()
|
|
skill_name = str(data.get("skill_name", "")).strip()
|
|
enabled = data.get("enabled")
|
|
if not agent_id or not skill_name or not isinstance(enabled, bool):
|
|
await websocket.send(json.dumps({"type": "error", "message": "update_agent_skill requires agent_id, skill_name, and boolean enabled."}, ensure_ascii=False))
|
|
return
|
|
|
|
config_name = gateway.config.get("config_name", "default")
|
|
skills_manager = SkillsManager(project_root=gateway._project_root)
|
|
skill_names = {item.skill_name for item in skills_manager.list_agent_skill_catalog(config_name, agent_id)}
|
|
if skill_name not in skill_names:
|
|
await websocket.send(json.dumps({"type": "error", "message": f"Unknown skill: {skill_name}"}, ensure_ascii=False))
|
|
return
|
|
|
|
if enabled:
|
|
skills_manager.update_agent_skill_overrides(config_name=config_name, agent_id=agent_id, enable=[skill_name])
|
|
await gateway.state_sync.on_system_message(f"Enabled skill {skill_name} for {agent_id}")
|
|
else:
|
|
skills_manager.update_agent_skill_overrides(config_name=config_name, agent_id=agent_id, disable=[skill_name])
|
|
await gateway.state_sync.on_system_message(f"Disabled skill {skill_name} for {agent_id}")
|
|
|
|
await websocket.send(json.dumps({
|
|
"type": "agent_skill_updated",
|
|
"agent_id": agent_id,
|
|
"skill_name": skill_name,
|
|
"enabled": enabled,
|
|
}, ensure_ascii=False))
|
|
await gateway._handle_reload_runtime_assets()
|
|
await handle_get_agent_skills(gateway, websocket, {"agent_id": agent_id})
|
|
|
|
|
|
async def handle_get_agent_workspace_file(gateway: Any, websocket: Any, data: dict[str, Any]) -> None:
|
|
agent_id = str(data.get("agent_id", "")).strip()
|
|
filename = gateway._normalize_agent_workspace_filename(data.get("filename"))
|
|
if not agent_id or not filename:
|
|
await websocket.send(json.dumps({"type": "error", "message": "get_agent_workspace_file requires agent_id and supported filename."}, ensure_ascii=False))
|
|
return
|
|
|
|
config_name = gateway.config.get("config_name", "default")
|
|
skills_manager = SkillsManager(project_root=gateway._project_root)
|
|
asset_dir = skills_manager.get_agent_asset_dir(config_name, agent_id)
|
|
asset_dir.mkdir(parents=True, exist_ok=True)
|
|
path = asset_dir / filename
|
|
content = path.read_text(encoding="utf-8") if path.exists() else ""
|
|
await websocket.send(json.dumps({
|
|
"type": "agent_workspace_file_loaded",
|
|
"config_name": config_name,
|
|
"agent_id": agent_id,
|
|
"filename": filename,
|
|
"content": content,
|
|
}, ensure_ascii=False))
|
|
|
|
|
|
async def handle_update_agent_workspace_file(gateway: Any, websocket: Any, data: dict[str, Any]) -> None:
|
|
agent_id = str(data.get("agent_id", "")).strip()
|
|
filename = gateway._normalize_agent_workspace_filename(data.get("filename"))
|
|
content = data.get("content")
|
|
if not agent_id or not filename or not isinstance(content, str):
|
|
await websocket.send(json.dumps({"type": "error", "message": "update_agent_workspace_file requires agent_id, supported filename, and string content."}, ensure_ascii=False))
|
|
return
|
|
|
|
config_name = gateway.config.get("config_name", "default")
|
|
skills_manager = SkillsManager(project_root=gateway._project_root)
|
|
asset_dir = skills_manager.get_agent_asset_dir(config_name, agent_id)
|
|
asset_dir.mkdir(parents=True, exist_ok=True)
|
|
path = asset_dir / filename
|
|
path.write_text(content, encoding="utf-8")
|
|
await gateway.state_sync.on_system_message(f"Updated {filename} for {agent_id}")
|
|
await websocket.send(json.dumps({"type": "agent_workspace_file_updated", "agent_id": agent_id, "filename": filename}, ensure_ascii=False))
|
|
await gateway._handle_reload_runtime_assets()
|
|
await handle_get_agent_workspace_file(gateway, websocket, {"agent_id": agent_id, "filename": filename})
|