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:
@@ -5,6 +5,8 @@
|
||||
# 用法:
|
||||
# ./scripts/check-prod-env.sh
|
||||
# ./scripts/check-prod-env.sh --strict
|
||||
# ./scripts/check-prod-env.sh --smoke-evo
|
||||
# ./scripts/check-prod-env.sh --strict --smoke-evo
|
||||
#
|
||||
# 检查内容:
|
||||
# - Python / Node / npm 是否可用
|
||||
@@ -12,6 +14,7 @@
|
||||
# - frontend/package-lock.json 与 npm ci 是否可消费
|
||||
# - .env 是否存在以及关键变量是否配置
|
||||
# - 前端是否可构建
|
||||
# - 可选:EvoAgent 运行时 smoke 检查(默认覆盖 fundamentals_analyst + risk_manager + portfolio_manager)
|
||||
# ============================================================
|
||||
set -euo pipefail
|
||||
|
||||
@@ -22,9 +25,11 @@ CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
STRICT=false
|
||||
SMOKE_EVO=false
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--strict) STRICT=true ;;
|
||||
--smoke-evo) SMOKE_EVO=true ;;
|
||||
*) echo -e "${YELLOW}忽略未知参数: ${arg}${NC}" ;;
|
||||
esac
|
||||
done
|
||||
@@ -34,6 +39,8 @@ PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
cd "${PROJECT_ROOT}"
|
||||
|
||||
WARNINGS=0
|
||||
PYTHON_BIN=""
|
||||
PROJECT_PYTHONPATH=""
|
||||
|
||||
ok() {
|
||||
echo -e "${GREEN}✔${NC} $1"
|
||||
@@ -54,8 +61,24 @@ require_cmd() {
|
||||
command -v "${cmd}" >/dev/null 2>&1 || fail "未找到命令: ${cmd}"
|
||||
}
|
||||
|
||||
resolve_python() {
|
||||
if command -v python >/dev/null 2>&1; then
|
||||
PYTHON_BIN="python"
|
||||
return
|
||||
fi
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
PYTHON_BIN="python3"
|
||||
return
|
||||
fi
|
||||
fail "未找到命令: python 或 python3"
|
||||
}
|
||||
|
||||
init_pythonpath() {
|
||||
PROJECT_PYTHONPATH="${PROJECT_ROOT}/.pydeps:."
|
||||
}
|
||||
|
||||
check_python_modules() {
|
||||
python - <<'PY'
|
||||
PYTHONPATH="${PROJECT_PYTHONPATH}" "${PYTHON_BIN}" - <<'PY'
|
||||
mods = [
|
||||
'fastapi', 'uvicorn', 'yaml', 'httpx', 'cryptography', 'websockets',
|
||||
'rich', 'dotenv', 'pandas_market_calendars', 'finnhub', 'openai',
|
||||
@@ -100,12 +123,13 @@ check_frontend_install() {
|
||||
[ -f frontend/package-lock.json ] || fail "frontend/package-lock.json 缺失,生产部署建议保留锁文件"
|
||||
(
|
||||
cd frontend
|
||||
npm ci --dry-run >/tmp/bigtime-npm-ci.log 2>&1 || {
|
||||
cat /tmp/bigtime-npm-ci.log
|
||||
exit 1
|
||||
}
|
||||
npm ci --dry-run >/tmp/bigtime-npm-ci.log 2>&1 || true
|
||||
)
|
||||
if rg -n "@emoji-mart/react|@lobehub/ui|ERESOLVE overriding peer dependency" /tmp/bigtime-npm-ci.log >/dev/null 2>&1; then
|
||||
if rg -n "npm error code EUSAGE|can only install packages when your package.json and package-lock.json.*in sync|Missing: .* from lock file|Invalid: lock file's " /tmp/bigtime-npm-ci.log >/dev/null 2>&1; then
|
||||
warn "frontend package-lock.json 与 package.json 不一致;需在 frontend/ 重新生成锁文件,但这不阻断当前后端 smoke 检查"
|
||||
elif rg -n "ERESOLVE could not resolve|Conflicting peer dependency" /tmp/bigtime-npm-ci.log >/dev/null 2>&1; then
|
||||
warn "frontend npm ci --dry-run 存在 peer 依赖冲突,当前以后续构建结果为准"
|
||||
elif rg -n "@emoji-mart/react|@lobehub/ui|ERESOLVE overriding peer dependency" /tmp/bigtime-npm-ci.log >/dev/null 2>&1; then
|
||||
warn "frontend npm ci 存在已知非阻塞 peer warning(@lobehub/icons 依赖链),可忽略"
|
||||
elif rg -n "npm warn" /tmp/bigtime-npm-ci.log >/dev/null 2>&1; then
|
||||
warn "frontend npm ci 存在 warning,建议查看 /tmp/bigtime-npm-ci.log"
|
||||
@@ -125,13 +149,42 @@ check_frontend_build() {
|
||||
ok "frontend 构建通过"
|
||||
}
|
||||
|
||||
check_evo_runtime_smoke() {
|
||||
local configured_ids="${EVO_AGENT_IDS:-}"
|
||||
local -a smoke_agent_ids=()
|
||||
local raw_id=""
|
||||
|
||||
if [ -n "${configured_ids}" ]; then
|
||||
IFS=',' read -r -a smoke_agent_ids <<< "${configured_ids}"
|
||||
else
|
||||
smoke_agent_ids=("fundamentals_analyst" "risk_manager" "portfolio_manager")
|
||||
fi
|
||||
|
||||
for raw_id in "${smoke_agent_ids[@]}"; do
|
||||
local agent_id
|
||||
agent_id="$(printf '%s' "${raw_id}" | xargs)"
|
||||
[ -n "${agent_id}" ] || continue
|
||||
|
||||
echo -e "${CYAN}运行 EvoAgent smoke 检查(agent=${agent_id})${NC}"
|
||||
PYTHONPATH="${PROJECT_PYTHONPATH}" "${PYTHON_BIN}" \
|
||||
"${PROJECT_ROOT}/scripts/smoke_evo_runtime.py" \
|
||||
--agent-id "${agent_id}" >/tmp/bigtime-evo-smoke.log 2>&1 || {
|
||||
cat /tmp/bigtime-evo-smoke.log
|
||||
exit 1
|
||||
}
|
||||
cat /tmp/bigtime-evo-smoke.log
|
||||
ok "EvoAgent smoke 检查通过(agent=${agent_id})"
|
||||
done
|
||||
}
|
||||
|
||||
echo -e "${CYAN}大时代 · 生产环境检查${NC}"
|
||||
|
||||
require_cmd python
|
||||
resolve_python
|
||||
init_pythonpath
|
||||
require_cmd node
|
||||
require_cmd npm
|
||||
|
||||
ok "python: $(python -V 2>&1)"
|
||||
ok "python: $(${PYTHON_BIN} -V 2>&1)"
|
||||
ok "node: $(node -v)"
|
||||
ok "npm: $(npm -v)"
|
||||
|
||||
@@ -140,6 +193,10 @@ check_env_file
|
||||
check_frontend_install
|
||||
check_frontend_build
|
||||
|
||||
if ${SMOKE_EVO}; then
|
||||
check_evo_runtime_smoke
|
||||
fi
|
||||
|
||||
if [ "${WARNINGS}" -gt 0 ]; then
|
||||
echo -e "${YELLOW}检查完成:有 ${WARNINGS} 项 warning${NC}"
|
||||
${STRICT} && exit 1 || exit 0
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
# COMPATIBILITY_SURFACE: stable
|
||||
# OWNER: ops-team
|
||||
# SEE: docs/legacy-inventory.md#gateway-first-production-example
|
||||
#
|
||||
# Gateway-first production launch script.
|
||||
# This is the current checked-in production example, running the gateway
|
||||
# directly and proxying /ws instead of exposing every split FastAPI service.
|
||||
# For split-service topology, see start-dev.sh and docs/current-architecture.md
|
||||
set -euo pipefail
|
||||
|
||||
cd /root/code/evotraders
|
||||
@@ -6,6 +14,17 @@ cd /root/code/evotraders
|
||||
export PYTHONPATH=/root/code/evotraders/.pydeps:.
|
||||
export TICKERS="${TICKERS:-AAPL,MSFT,GOOGL,AMZN,NVDA,META,TSLA,AMD,NFLX,AVGO,PLTR,COIN}"
|
||||
|
||||
# 技能沙盒配置(生产环境建议使用 docker)
|
||||
export SKILL_SANDBOX_MODE="${SKILL_SANDBOX_MODE:-docker}"
|
||||
export SKILL_SANDBOX_IMAGE="${SKILL_SANDBOX_IMAGE:-python:3.11-slim}"
|
||||
export SKILL_SANDBOX_MEMORY_LIMIT="${SKILL_SANDBOX_MEMORY_LIMIT:-512m}"
|
||||
export SKILL_SANDBOX_CPU_LIMIT="${SKILL_SANDBOX_CPU_LIMIT:-1.0}"
|
||||
export SKILL_SANDBOX_NETWORK="${SKILL_SANDBOX_NETWORK:-none}"
|
||||
export SKILL_SANDBOX_TIMEOUT="${SKILL_SANDBOX_TIMEOUT:-60}"
|
||||
|
||||
# "production" here is an explicit deployment run label, not a required
|
||||
# root-level runtime directory name.
|
||||
|
||||
exec python3 -m backend.main \
|
||||
--mode live \
|
||||
--config-name production \
|
||||
|
||||
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())
|
||||
203
scripts/test_sandbox.py
Normal file
203
scripts/test_sandbox.py
Normal file
@@ -0,0 +1,203 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
沙盒执行器测试脚本
|
||||
|
||||
测试多模式技能沙盒执行器的基本功能。
|
||||
默认使用 none 模式(无沙盒)。
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# 确保后端目录在路径中
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "backend"))
|
||||
|
||||
|
||||
def test_sandbox_initialization():
|
||||
"""测试沙盒初始化"""
|
||||
print("=" * 60)
|
||||
print("测试 1: 沙盒初始化")
|
||||
print("=" * 60)
|
||||
|
||||
from backend.tools.sandboxed_executor import get_sandbox, SkillSandbox
|
||||
|
||||
# 重置单例(确保测试干净)
|
||||
SkillSandbox._instance = None
|
||||
|
||||
# 默认应该使用 none 模式
|
||||
sandbox = get_sandbox()
|
||||
|
||||
assert sandbox.current_mode == "none", f"期望模式 'none',实际 '{sandbox.current_mode}'"
|
||||
print(f"✓ 沙盒模式: {sandbox.current_mode}")
|
||||
print(f"✓ 后端类型: {type(sandbox._backend).__name__}")
|
||||
|
||||
return sandbox
|
||||
|
||||
|
||||
def test_no_sandbox_warning():
|
||||
"""测试无沙盒模式的安全警告"""
|
||||
print("\n" + "=" * 60)
|
||||
print("测试 2: 无沙盒模式安全警告")
|
||||
print("=" * 60)
|
||||
|
||||
import warnings
|
||||
|
||||
from backend.tools.sandboxed_executor import NoSandboxBackend
|
||||
|
||||
backend = NoSandboxBackend()
|
||||
|
||||
# 捕获警告
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
|
||||
# 执行会触发警告
|
||||
try:
|
||||
backend.execute(
|
||||
skill_name="builtin/valuation_review",
|
||||
function_name="build_dcf_report",
|
||||
function_args={"rows": [], "current_date": "2024-01-01"},
|
||||
)
|
||||
except Exception:
|
||||
pass # 我们不关心执行结果,只关心警告
|
||||
|
||||
# 检查是否产生了警告
|
||||
runtime_warnings = [x for x in w if issubclass(x.category, RuntimeWarning)]
|
||||
if runtime_warnings:
|
||||
print("✓ 安全警告已触发")
|
||||
print(f" 警告内容: {runtime_warnings[0].message}")
|
||||
else:
|
||||
print("⚠ 未触发安全警告(可能已缓存)")
|
||||
|
||||
|
||||
def test_docker_config():
|
||||
"""测试 Docker 模式配置解析"""
|
||||
print("\n" + "=" * 60)
|
||||
print("测试 3: Docker 模式配置解析")
|
||||
print("=" * 60)
|
||||
|
||||
# 设置环境变量
|
||||
os.environ["SKILL_SANDBOX_MODE"] = "docker"
|
||||
os.environ["SKILL_SANDBOX_MEMORY_LIMIT"] = "1g"
|
||||
os.environ["SKILL_SANDBOX_CPU_LIMIT"] = "2.0"
|
||||
|
||||
from backend.tools.sandboxed_executor import SkillSandbox
|
||||
|
||||
# 重置单例
|
||||
SkillSandbox._instance = None
|
||||
|
||||
try:
|
||||
sandbox = SkillSandbox()
|
||||
print(f"✓ 沙盒模式: {sandbox.current_mode}")
|
||||
print(f"✓ 后端类型: {type(sandbox._backend).__name__}")
|
||||
|
||||
# 检查配置
|
||||
backend = sandbox._backend
|
||||
assert backend.config["memory_limit"] == "1g"
|
||||
assert backend.config["cpu_limit"] == 2.0
|
||||
print(f"✓ 内存限制: {backend.config['memory_limit']}")
|
||||
print(f"✓ CPU 限制: {backend.config['cpu_limit']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠ Docker 后端创建失败(预期,可能未安装 agentscope-runtime): {e}")
|
||||
|
||||
# 恢复环境变量
|
||||
os.environ["SKILL_SANDBOX_MODE"] = "none"
|
||||
SkillSandbox._instance = None
|
||||
|
||||
|
||||
def test_analysis_tools_import():
|
||||
"""测试分析工具导入"""
|
||||
print("\n" + "=" * 60)
|
||||
print("测试 4: 分析工具导入")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
from backend.tools.analysis_tools import (
|
||||
TOOL_REGISTRY,
|
||||
_sandbox,
|
||||
dcf_valuation_analysis,
|
||||
)
|
||||
|
||||
print(f"✓ TOOL_REGISTRY 包含 {len(TOOL_REGISTRY)} 个工具")
|
||||
print(f"✓ _sandbox 实例模式: {_sandbox.current_mode}")
|
||||
print(f"✓ dcf_valuation_analysis 函数可用")
|
||||
|
||||
# 检查估值分析工具是否都在
|
||||
valuation_tools = [
|
||||
"dcf_valuation_analysis",
|
||||
"owner_earnings_valuation_analysis",
|
||||
"ev_ebitda_valuation_analysis",
|
||||
"residual_income_valuation_analysis",
|
||||
]
|
||||
|
||||
for tool in valuation_tools:
|
||||
if tool in TOOL_REGISTRY:
|
||||
print(f" ✓ {tool}")
|
||||
else:
|
||||
print(f" ✗ {tool} 缺失")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 导入失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
def test_skill_execution_mock():
|
||||
"""测试技能执行(模拟)"""
|
||||
print("\n" + "=" * 60)
|
||||
print("测试 5: 技能执行(无沙盒模式)")
|
||||
print("=" * 60)
|
||||
|
||||
from backend.tools.sandboxed_executor import get_sandbox
|
||||
|
||||
sandbox = get_sandbox()
|
||||
|
||||
# 使用模拟参数调用
|
||||
try:
|
||||
# 注意:这需要实际的技能模块存在
|
||||
result = sandbox.execute_skill(
|
||||
skill_name="builtin/valuation_review",
|
||||
function_name="build_dcf_report",
|
||||
function_args={
|
||||
"rows": [{"ticker": "AAPL", "current_fcf": 100000000}],
|
||||
"current_date": "2024-01-01",
|
||||
},
|
||||
)
|
||||
print(f"✓ 技能执行成功")
|
||||
print(f" 结果类型: {type(result)}")
|
||||
print(f" 结果预览: {str(result)[:100]}...")
|
||||
except Exception as e:
|
||||
print(f"⚠ 技能执行失败(可能缺少依赖或数据): {e}")
|
||||
|
||||
|
||||
def main():
|
||||
"""主测试函数"""
|
||||
print("\n" + "=" * 60)
|
||||
print("技能沙盒执行器测试")
|
||||
print("=" * 60)
|
||||
print(f"当前 SKILL_SANDBOX_MODE: {os.getenv('SKILL_SANDBOX_MODE', '未设置(默认 none)')}")
|
||||
|
||||
# 确保使用默认模式测试
|
||||
os.environ["SKILL_SANDBOX_MODE"] = "none"
|
||||
|
||||
try:
|
||||
test_sandbox_initialization()
|
||||
test_no_sandbox_warning()
|
||||
test_docker_config()
|
||||
test_analysis_tools_import()
|
||||
test_skill_execution_mock()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("测试完成")
|
||||
print("=" * 60)
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n✗ 测试失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
96
scripts/test_sandbox_simple.py
Normal file
96
scripts/test_sandbox_simple.py
Normal file
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
简化测试 - 验证沙盒执行器基本功能
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "backend"))
|
||||
|
||||
os.environ["SKILL_SANDBOX_MODE"] = "none"
|
||||
|
||||
|
||||
def test_import():
|
||||
"""测试导入"""
|
||||
print("测试 1: 导入沙盒执行器")
|
||||
from backend.tools.sandboxed_executor import get_sandbox, SkillSandbox
|
||||
|
||||
# 重置单例
|
||||
SkillSandbox._instance = None
|
||||
|
||||
sandbox = get_sandbox()
|
||||
print(f" ✓ 模式: {sandbox.current_mode}")
|
||||
print(f" ✓ 后端: {type(sandbox._backend).__name__}")
|
||||
return sandbox
|
||||
|
||||
|
||||
def test_no_sandbox_backend():
|
||||
"""测试无沙盒后端"""
|
||||
print("\n测试 2: 无沙盒后端")
|
||||
from backend.tools.sandboxed_executor import NoSandboxBackend
|
||||
|
||||
backend = NoSandboxBackend()
|
||||
|
||||
# 测试函数名解析
|
||||
test_cases = [
|
||||
("build_dcf_report", "dcf_report"),
|
||||
("build_ev_ebitda_report", "multiple_valuation_report"),
|
||||
("build_owner_earnings_report", "owner_earnings_report"),
|
||||
("build_residual_income_report", "multiple_valuation_report"),
|
||||
]
|
||||
|
||||
for func_name, expected_script in test_cases:
|
||||
script_name = backend._get_script_name(func_name)
|
||||
assert script_name == expected_script, f"期望 {expected_script}, 实际 {script_name}"
|
||||
print(f" ✓ {func_name} -> {script_name}")
|
||||
|
||||
|
||||
def test_module_resolution():
|
||||
"""测试模块解析"""
|
||||
print("\n测试 3: 模块路径解析")
|
||||
|
||||
from backend.tools.sandboxed_executor import NoSandboxBackend
|
||||
|
||||
backend = NoSandboxBackend()
|
||||
skill_name = "builtin/valuation_review"
|
||||
function_name = "build_dcf_report"
|
||||
|
||||
module_path = f"backend.skills.{skill_name.replace('/', '.')}.scripts"
|
||||
script_name = backend._get_script_name(function_name)
|
||||
submodule_path = f"{module_path}.{script_name}"
|
||||
|
||||
print(f" 技能名: {skill_name}")
|
||||
print(f" 函数名: {function_name}")
|
||||
print(f" 模块路径: {submodule_path}")
|
||||
|
||||
# 尝试导入
|
||||
try:
|
||||
module = __import__(submodule_path, fromlist=[function_name])
|
||||
func = getattr(module, function_name)
|
||||
print(f" ✓ 成功导入函数: {func.__name__}")
|
||||
except Exception as e:
|
||||
print(f" ✗ 导入失败: {e}")
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 50)
|
||||
print("沙盒执行器简化测试")
|
||||
print("=" * 50)
|
||||
|
||||
# 抑制警告
|
||||
warnings.filterwarnings("ignore", category=RuntimeWarning)
|
||||
|
||||
test_import()
|
||||
test_no_sandbox_backend()
|
||||
test_module_resolution()
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("测试完成")
|
||||
print("=" * 50)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
168
scripts/verify_docs_consistency.py
Normal file
168
scripts/verify_docs_consistency.py
Normal file
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify documentation and script consistency.
|
||||
|
||||
This script checks that:
|
||||
1. README.md mentions correct service ports
|
||||
2. start-dev.sh starts services on documented ports
|
||||
3. deploy/README.md is consistent with production scripts
|
||||
4. Service ports match across all documentation
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
# Expected service ports (source of truth)
|
||||
SERVICE_PORTS = {
|
||||
"agent_service": 8000,
|
||||
"trading_service": 8001,
|
||||
"news_service": 8002,
|
||||
"runtime_service": 8003,
|
||||
"gateway_websocket": 8765,
|
||||
}
|
||||
|
||||
|
||||
def check_readme_ports() -> list[str]:
|
||||
"""Check that README.md documents correct ports."""
|
||||
errors = []
|
||||
readme_path = PROJECT_ROOT / "README.md"
|
||||
readme_content = readme_path.read_text(encoding="utf-8")
|
||||
|
||||
# Check for each service port mention
|
||||
for service, port in SERVICE_PORTS.items():
|
||||
port_patterns = [
|
||||
f":{port}",
|
||||
f"port {port}",
|
||||
f"localhost:{port}",
|
||||
]
|
||||
found = any(pattern in readme_content for pattern in port_patterns)
|
||||
if not found:
|
||||
errors.append(f"README.md: Missing documentation for {service} on port {port}")
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def check_start_dev_sh_ports() -> list[str]:
|
||||
"""Check that start-dev.sh uses correct ports."""
|
||||
errors = []
|
||||
script_path = PROJECT_ROOT / "start-dev.sh"
|
||||
script_content = script_path.read_text(encoding="utf-8")
|
||||
|
||||
# Check for port declarations in start_service calls
|
||||
for service, port in SERVICE_PORTS.items():
|
||||
if service == "gateway_websocket":
|
||||
# Gateway uses --port flag
|
||||
if f"--port {port}" not in script_content:
|
||||
errors.append(f"start-dev.sh: Gateway not using port {port}")
|
||||
else:
|
||||
# Services use port parameter in start_service
|
||||
pattern = rf'start_service\s+"{service}"\s+"[^"]+"\s+{port}'
|
||||
if not re.search(pattern, script_content):
|
||||
# Also check for explicit port mentions
|
||||
if f"port {port}" not in script_content and f":{port}" not in script_content:
|
||||
errors.append(f"start-dev.sh: {service} not using port {port}")
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def check_deploy_readme_consistency() -> list[str]:
|
||||
"""Check that deploy/README.md is consistent with scripts."""
|
||||
errors = []
|
||||
deploy_readme_path = PROJECT_ROOT / "deploy" / "README.md"
|
||||
deploy_content = deploy_readme_path.read_text(encoding="utf-8")
|
||||
|
||||
# Check for gateway port consistency
|
||||
if "127.0.0.1:8765" not in deploy_content:
|
||||
errors.append("deploy/README.md: Gateway port 8765 not documented correctly")
|
||||
|
||||
# Check for production script reference
|
||||
if "scripts/run_prod.sh" not in deploy_content:
|
||||
errors.append("deploy/README.md: Missing reference to scripts/run_prod.sh")
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def check_run_prod_sh_ports() -> list[str]:
|
||||
"""Check that run_prod.sh uses correct ports."""
|
||||
errors = []
|
||||
script_path = PROJECT_ROOT / "scripts" / "run_prod.sh"
|
||||
script_content = script_path.read_text(encoding="utf-8")
|
||||
|
||||
# Production script should use port 8765 for gateway
|
||||
if "--port 8765" not in script_content:
|
||||
errors.append("scripts/run_prod.sh: Not using gateway port 8765")
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def check_service_main_blocks() -> list[str]:
|
||||
"""Check that service modules use correct ports in __main__ blocks."""
|
||||
errors = []
|
||||
|
||||
service_files = {
|
||||
"agent_service": PROJECT_ROOT / "backend" / "apps" / "agent_service.py",
|
||||
"trading_service": PROJECT_ROOT / "backend" / "apps" / "trading_service.py",
|
||||
"news_service": PROJECT_ROOT / "backend" / "apps" / "news_service.py",
|
||||
"runtime_service": PROJECT_ROOT / "backend" / "apps" / "runtime_service.py",
|
||||
}
|
||||
|
||||
for service, file_path in service_files.items():
|
||||
if not file_path.exists():
|
||||
errors.append(f"{service}: File not found at {file_path}")
|
||||
continue
|
||||
|
||||
content = file_path.read_text(encoding="utf-8")
|
||||
expected_port = SERVICE_PORTS[service]
|
||||
|
||||
# Check for port= in uvicorn.run or app.run
|
||||
if f"port={expected_port}" not in content and f"port= {expected_port}" not in content:
|
||||
errors.append(f"{file_path}: Not using expected port {expected_port}")
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Verify documentation and script consistency.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--strict",
|
||||
action="store_true",
|
||||
help="Treat warnings as errors",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
all_errors = []
|
||||
|
||||
print("Checking README.md ports...")
|
||||
all_errors.extend(check_readme_ports())
|
||||
|
||||
print("Checking start-dev.sh ports...")
|
||||
all_errors.extend(check_start_dev_sh_ports())
|
||||
|
||||
print("Checking deploy/README.md consistency...")
|
||||
all_errors.extend(check_deploy_readme_consistency())
|
||||
|
||||
print("Checking scripts/run_prod.sh ports...")
|
||||
all_errors.extend(check_run_prod_sh_ports())
|
||||
|
||||
print("Checking service __main__ blocks...")
|
||||
all_errors.extend(check_service_main_blocks())
|
||||
|
||||
if all_errors:
|
||||
print("\nConsistency errors found:")
|
||||
for error in all_errors:
|
||||
print(f" - {error}")
|
||||
return 1 if args.strict else 0
|
||||
else:
|
||||
print("\nAll consistency checks passed!")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user