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:
290
scripts/smoke_evo_runtime.py
Normal file
290
scripts/smoke_evo_runtime.py
Normal file
@@ -0,0 +1,290 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Smoke-test the EvoAgent runtime rollout path.
|
||||
|
||||
This script validates the current staged rollout shape:
|
||||
- start runtime via backend.api.runtime
|
||||
- confirm the gateway starts on an available port
|
||||
- confirm the gateway log shows the selected agent running as EvoAgent
|
||||
- confirm runtime_state.json is written
|
||||
- confirm guard approval API logic wakes a pending ToolApprovalRequest
|
||||
|
||||
It intentionally avoids browser/front-end dependencies and does not require
|
||||
local HTTP callbacks.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import websocket
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
PYDEPS = PROJECT_ROOT / ".pydeps"
|
||||
|
||||
_reordered_sys_path = [
|
||||
str(PROJECT_ROOT),
|
||||
str(PYDEPS),
|
||||
]
|
||||
for entry in list(sys.path):
|
||||
if entry in _reordered_sys_path:
|
||||
continue
|
||||
_reordered_sys_path.append(entry)
|
||||
sys.path[:] = _reordered_sys_path
|
||||
|
||||
from fastapi import BackgroundTasks
|
||||
|
||||
from backend.agents.base.tool_guard import TOOL_GUARD_STORE, ToolApprovalRequest
|
||||
from backend.api.guard import ApprovalRequest, approve_tool_call
|
||||
from backend.api.runtime import (
|
||||
LaunchConfig,
|
||||
_is_gateway_running,
|
||||
get_runtime_state,
|
||||
start_runtime,
|
||||
stop_runtime,
|
||||
)
|
||||
|
||||
|
||||
# All 6 agent roles supported by EvoAgent
|
||||
ALL_EVO_AGENT_ROLES = [
|
||||
"fundamentals_analyst",
|
||||
"technical_analyst",
|
||||
"sentiment_analyst",
|
||||
"valuation_analyst",
|
||||
"risk_manager",
|
||||
"portfolio_manager",
|
||||
]
|
||||
|
||||
|
||||
def _parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Smoke-test the staged EvoAgent runtime rollout.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--agent-id",
|
||||
default="fundamentals_analyst",
|
||||
help="Agent id to enable via EVO_AGENT_IDS (use 'all' to test all 6 roles)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ticker",
|
||||
default="AAPL",
|
||||
help="Ticker to include in the smoke runtime bootstrap",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-wait-seconds",
|
||||
type=float,
|
||||
default=15.0,
|
||||
help="Maximum time to wait for gateway.log to appear",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--test-all-roles",
|
||||
action="store_true",
|
||||
help="Test all 6 EvoAgent roles sequentially",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def _wait_for_file(path: Path, timeout_seconds: float) -> None:
|
||||
deadline = time.time() + timeout_seconds
|
||||
while time.time() < deadline:
|
||||
if path.exists():
|
||||
return
|
||||
time.sleep(0.2)
|
||||
raise TimeoutError(f"Timed out waiting for file: {path}")
|
||||
|
||||
|
||||
def _wait_for_initial_state(gateway_port: int, timeout_seconds: float) -> dict[str, object]:
|
||||
deadline = time.time() + timeout_seconds
|
||||
last_error: Exception | None = None
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
ws = websocket.create_connection(
|
||||
f"ws://127.0.0.1:{gateway_port}",
|
||||
timeout=3,
|
||||
)
|
||||
try:
|
||||
payload = json.loads(ws.recv())
|
||||
return payload
|
||||
finally:
|
||||
ws.close()
|
||||
except Exception as exc: # pragma: no cover - best-effort smoke polling
|
||||
last_error = exc
|
||||
time.sleep(0.2)
|
||||
raise TimeoutError(
|
||||
f"Timed out waiting for gateway initial_state on port {gateway_port}: {last_error}"
|
||||
)
|
||||
|
||||
|
||||
async def _run_smoke(agent_id: str, ticker: str, max_wait_seconds: float) -> dict[str, object]:
|
||||
previous_env = os.environ.get("EVO_AGENT_IDS")
|
||||
os.environ["EVO_AGENT_IDS"] = agent_id
|
||||
|
||||
try:
|
||||
if _is_gateway_running():
|
||||
await stop_runtime(force=True)
|
||||
|
||||
response = await start_runtime(
|
||||
LaunchConfig(
|
||||
launch_mode="fresh",
|
||||
tickers=[ticker],
|
||||
schedule_mode="daily",
|
||||
interval_minutes=60,
|
||||
trigger_time="09:30",
|
||||
max_comm_cycles=1,
|
||||
initial_cash=100000.0,
|
||||
margin_requirement=0.0,
|
||||
enable_memory=False,
|
||||
mode="backtest",
|
||||
start_date="2025-11-01",
|
||||
end_date="2025-11-30",
|
||||
poll_interval=30,
|
||||
),
|
||||
BackgroundTasks(),
|
||||
)
|
||||
|
||||
run_dir = Path(response.run_dir)
|
||||
log_path = run_dir / "logs" / "gateway.log"
|
||||
state_path = run_dir / "state" / "runtime_state.json"
|
||||
|
||||
_wait_for_file(log_path, max_wait_seconds)
|
||||
_wait_for_file(state_path, max_wait_seconds)
|
||||
initial_state_payload = _wait_for_initial_state(
|
||||
response.gateway_port,
|
||||
max_wait_seconds,
|
||||
)
|
||||
|
||||
log_text = log_path.read_text(encoding="utf-8")
|
||||
state = json.loads(state_path.read_text(encoding="utf-8"))
|
||||
|
||||
record = TOOL_GUARD_STORE.create_pending(
|
||||
tool_name="write_file",
|
||||
tool_input={"path": "smoke.txt"},
|
||||
agent_id=agent_id,
|
||||
workspace_id=response.run_id,
|
||||
)
|
||||
pending = ToolApprovalRequest(
|
||||
approval_id=record.approval_id,
|
||||
tool_name=record.tool_name,
|
||||
tool_input=record.tool_input,
|
||||
tool_call_id="smoke_call",
|
||||
)
|
||||
record.pending_request = pending
|
||||
await approve_tool_call(
|
||||
ApprovalRequest(
|
||||
approval_id=record.approval_id,
|
||||
one_time=True,
|
||||
expires_in_minutes=30,
|
||||
)
|
||||
)
|
||||
|
||||
result = {
|
||||
"run_id": response.run_id,
|
||||
"gateway_port": response.gateway_port,
|
||||
"gateway_running": _is_gateway_running(),
|
||||
"runtime_manager": get_runtime_state().runtime_manager is not None,
|
||||
"evo_log_present": f"EvoAgent initialized: {agent_id}" in log_text,
|
||||
"runtime_state_written": state_path.exists(),
|
||||
"registered_agents": [item["agent_id"] for item in state.get("agents", [])],
|
||||
"pending_request_approved": pending.approved is True,
|
||||
"ws_initial_type": initial_state_payload.get("type"),
|
||||
"ws_initial_tickers": (
|
||||
(initial_state_payload.get("state") or {}).get("tickers") or []
|
||||
),
|
||||
}
|
||||
return result
|
||||
finally:
|
||||
if _is_gateway_running():
|
||||
await stop_runtime(force=True)
|
||||
if previous_env is None:
|
||||
os.environ.pop("EVO_AGENT_IDS", None)
|
||||
else:
|
||||
os.environ["EVO_AGENT_IDS"] = previous_env
|
||||
|
||||
|
||||
def _verify_skills_loaded(log_text: str, agent_id: str) -> dict[str, bool]:
|
||||
"""Verify that skills were loaded for the agent."""
|
||||
return {
|
||||
"skills_loaded": f"Loading skills for {agent_id}" in log_text or "skills" in log_text.lower(),
|
||||
"tools_registered": "tool" in log_text.lower(),
|
||||
}
|
||||
|
||||
|
||||
async def _run_smoke_for_role(role: str, ticker: str, max_wait_seconds: float) -> dict[str, object]:
|
||||
"""Run smoke test for a single agent role."""
|
||||
print(f"\n>>> Testing EvoAgent role: {role}", file=sys.stderr)
|
||||
result = await _run_smoke(
|
||||
agent_id=role,
|
||||
ticker=ticker,
|
||||
max_wait_seconds=max_wait_seconds,
|
||||
)
|
||||
result["agent_role"] = role
|
||||
result["success"] = (
|
||||
result.get("evo_log_present", False)
|
||||
and result.get("runtime_state_written", False)
|
||||
and result.get("pending_request_approved", False)
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = _parse_args()
|
||||
|
||||
if args.test_all_roles:
|
||||
# Test all 6 agent roles
|
||||
results = []
|
||||
all_passed = True
|
||||
|
||||
for role in ALL_EVO_AGENT_ROLES:
|
||||
try:
|
||||
result = asyncio.run(
|
||||
_run_smoke_for_role(
|
||||
role=role,
|
||||
ticker=args.ticker,
|
||||
max_wait_seconds=args.max_wait_seconds,
|
||||
)
|
||||
)
|
||||
results.append(result)
|
||||
if not result.get("success", False):
|
||||
all_passed = False
|
||||
print(f" FAILED: {role}", file=sys.stderr)
|
||||
else:
|
||||
print(f" PASSED: {role}", file=sys.stderr)
|
||||
except Exception as e:
|
||||
all_passed = False
|
||||
print(f" ERROR: {role} - {e}", file=sys.stderr)
|
||||
results.append({
|
||||
"agent_role": role,
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
})
|
||||
|
||||
summary = {
|
||||
"test_mode": "all_roles",
|
||||
"total_roles": len(ALL_EVO_AGENT_ROLES),
|
||||
"passed": sum(1 for r in results if r.get("success", False)),
|
||||
"failed": sum(1 for r in results if not r.get("success", False)),
|
||||
"all_passed": all_passed,
|
||||
"results": results,
|
||||
}
|
||||
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
||||
return 0 if all_passed else 1
|
||||
else:
|
||||
# Test single agent
|
||||
result = asyncio.run(
|
||||
_run_smoke(
|
||||
agent_id=args.agent_id,
|
||||
ticker=args.ticker,
|
||||
max_wait_seconds=args.max_wait_seconds,
|
||||
)
|
||||
)
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user