Add dynamic analyst runtime updates and deployment guides
This commit is contained in:
@@ -7,6 +7,7 @@ import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -154,6 +155,7 @@ class RunContextResponse(BaseModel):
|
||||
|
||||
class RuntimeAgentState(BaseModel):
|
||||
agent_id: str
|
||||
display_name: Optional[str] = None
|
||||
status: str
|
||||
last_session: Optional[str] = None
|
||||
last_updated: str
|
||||
@@ -300,6 +302,70 @@ def _load_run_server_state(run_dir: Path) -> Dict[str, Any]:
|
||||
return {}
|
||||
|
||||
|
||||
def _resolve_runtime_agent_display_name(run_id: str, agent_id: str) -> Optional[str]:
|
||||
"""Best-effort display name for one runtime agent.
|
||||
|
||||
Priority:
|
||||
1. PROFILE.md line like `角色定位:中文名`
|
||||
2. PROFILE.md YAML frontmatter field `name`
|
||||
"""
|
||||
asset_dir = PROJECT_ROOT / "runs" / run_id / "agents" / agent_id
|
||||
profile_path = asset_dir / "PROFILE.md"
|
||||
if not profile_path.exists():
|
||||
return None
|
||||
|
||||
try:
|
||||
raw = profile_path.read_text(encoding="utf-8").strip()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
if not raw:
|
||||
return None
|
||||
|
||||
frontmatter_name: Optional[str] = None
|
||||
if raw.startswith("---"):
|
||||
parts = raw.split("---", 2)
|
||||
if len(parts) >= 3:
|
||||
try:
|
||||
import yaml
|
||||
parsed = yaml.safe_load(parts[1].strip()) or {}
|
||||
if isinstance(parsed, dict):
|
||||
value = parsed.get("name")
|
||||
if isinstance(value, str) and value.strip():
|
||||
frontmatter_name = value.strip()
|
||||
except Exception:
|
||||
pass
|
||||
raw = parts[2].strip()
|
||||
|
||||
for line in raw.splitlines():
|
||||
normalized = line.strip()
|
||||
if normalized.startswith("角色定位:"):
|
||||
value = normalized.split(":", 1)[1].strip()
|
||||
if value:
|
||||
return value
|
||||
if normalized.lower().startswith("role:"):
|
||||
value = normalized.split(":", 1)[1].strip()
|
||||
if value:
|
||||
return value
|
||||
|
||||
return frontmatter_name
|
||||
|
||||
|
||||
def _enrich_runtime_agents(run_id: Optional[str], agents: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
if not run_id:
|
||||
return agents
|
||||
|
||||
enriched: List[Dict[str, Any]] = []
|
||||
for item in agents:
|
||||
payload = dict(item)
|
||||
display_name = payload.get("display_name")
|
||||
agent_id = str(payload.get("agent_id") or "").strip()
|
||||
if agent_id and (not isinstance(display_name, str) or not display_name.strip()):
|
||||
payload["display_name"] = _resolve_runtime_agent_display_name(run_id, agent_id)
|
||||
enriched.append(payload)
|
||||
return enriched
|
||||
|
||||
|
||||
def _extract_history_metrics(run_dir: Path) -> tuple[int, Optional[float]]:
|
||||
"""Prefer runtime state files over dashboard exports for history summaries."""
|
||||
server_state = _load_run_server_state(run_dir)
|
||||
@@ -566,10 +632,11 @@ def _validate_gateway_config(bootstrap: Dict[str, Any]) -> List[str]:
|
||||
# Check LLM configuration
|
||||
model_name = os.getenv("MODEL_NAME")
|
||||
openai_key = os.getenv("OPENAI_API_KEY")
|
||||
dashscope_key = os.getenv("DASHSCOPE_API_KEY")
|
||||
if not model_name:
|
||||
errors.append("MODEL_NAME environment variable is not set")
|
||||
if not openai_key:
|
||||
errors.append("OPENAI_API_KEY environment variable is not set")
|
||||
if not openai_key and not dashscope_key:
|
||||
errors.append("Either OPENAI_API_KEY or DASHSCOPE_API_KEY environment variable must be set")
|
||||
|
||||
# Validate tickers
|
||||
tickers = bootstrap.get("tickers", [])
|
||||
@@ -722,7 +789,8 @@ async def get_run_context() -> RunContextResponse:
|
||||
async def get_runtime_agents() -> RuntimeAgentsResponse:
|
||||
"""Return agent states from the active runtime, or latest persisted run."""
|
||||
snapshot = _get_active_runtime_snapshot() if _is_gateway_running() else _load_latest_runtime_snapshot()
|
||||
agents = snapshot.get("agents", [])
|
||||
run_id = snapshot.get("context", {}).get("config_name")
|
||||
agents = _enrich_runtime_agents(run_id, snapshot.get("agents", []))
|
||||
|
||||
return RuntimeAgentsResponse(
|
||||
agents=[RuntimeAgentState(**a) for a in agents]
|
||||
@@ -869,11 +937,24 @@ def _load_latest_runtime_snapshot() -> Dict[str, Any]:
|
||||
|
||||
|
||||
def _get_active_runtime_snapshot() -> Dict[str, Any]:
|
||||
"""Return the active runtime snapshot, preferring in-memory manager state."""
|
||||
"""Return the active runtime snapshot.
|
||||
|
||||
For a running Gateway, the canonical runtime source of truth is the
|
||||
run-scoped snapshot file under `runs/<run_id>/state/runtime_state.json`,
|
||||
because the Gateway subprocess mutates it directly while the parent
|
||||
runtime_service process may still hold a stale in-memory manager snapshot.
|
||||
"""
|
||||
if not _is_gateway_running():
|
||||
raise HTTPException(status_code=404, detail="No runtime is currently running")
|
||||
|
||||
manager = _runtime_state.runtime_manager
|
||||
if manager is not None:
|
||||
run_id = str(getattr(manager, "config_name", "") or "").strip()
|
||||
if run_id:
|
||||
snapshot_path = _get_run_dir(run_id) / "state" / "runtime_state.json"
|
||||
if snapshot_path.exists():
|
||||
return json.loads(snapshot_path.read_text(encoding="utf-8"))
|
||||
|
||||
if manager is not None and hasattr(manager, "build_snapshot"):
|
||||
snapshot = manager.build_snapshot()
|
||||
context = snapshot.get("context") or {}
|
||||
@@ -900,11 +981,32 @@ def _read_log_tail(path: Path, max_chars: int = 120_000) -> str:
|
||||
if not path.exists() or not path.is_file():
|
||||
return ""
|
||||
text = path.read_text(encoding="utf-8", errors="replace")
|
||||
text = _sanitize_runtime_log_text(text)
|
||||
if len(text) <= max_chars:
|
||||
return text
|
||||
return text[-max_chars:]
|
||||
|
||||
|
||||
def _sanitize_runtime_log_text(text: str) -> str:
|
||||
if not text:
|
||||
return ""
|
||||
|
||||
# Drop repetitive development-only warnings for unsandboxed skill execution.
|
||||
text = re.sub(
|
||||
r"(?:^|\n)=+\n"
|
||||
r"⚠️\s+\[安全警告\]\s+技能在无沙盒模式下运行\s+\(SKILL_SANDBOX_MODE=none\)\n"
|
||||
r"\s+技能脚本将直接在当前进程中执行,无隔离保护。\n"
|
||||
r"\s+建议:生产环境请设置\s+SKILL_SANDBOX_MODE=docker\n"
|
||||
r"=+\n?",
|
||||
"\n",
|
||||
text,
|
||||
flags=re.MULTILINE,
|
||||
)
|
||||
|
||||
text = re.sub(r"\n{3,}", "\n\n", text)
|
||||
return text.strip()
|
||||
|
||||
|
||||
def _get_current_runtime_context() -> Dict[str, Any]:
|
||||
"""Return the active runtime context from the latest snapshot."""
|
||||
if not _is_gateway_running():
|
||||
|
||||
Reference in New Issue
Block a user