deerflow-code/offline-backend-20260512/backend/scripts/probe_roundtable_dag_job.py
2026-09-07 18:24:55 +08:00

111 lines
4.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""后台挂起「分层 DAG」实测脚本:带一个并行 stage 的业务链。
POST 启动一个 dag 作业(stage1 两席位并行 → stage2 → stage3),轮询打印每帧的
dispatchChain 节点状态,直观看到:①stage1 两席位**同时 active**(并行);②stage2/3
在前一 stage 全 done 后才 active(串行);③orchestrationPlan 在响应里回得来(持久化生效)。
用法:PYTHONPATH=. .venv/Scripts/python scripts/probe_roundtable_dag_job.py
环境:GATEWAY_URL 默认 http://127.0.0.1:8001;AUTH_COOKIE 可选。
"""
from __future__ import annotations
import asyncio
import os
import time
import httpx
GATEWAY_URL = os.getenv("GATEWAY_URL", "http://127.0.0.1:8001").rstrip("/")
AUTH_COOKIE = os.getenv("AUTH_COOKIE", "")
AUTH_BEARER = os.getenv("AUTH_BEARER", "")
# 4 个已 seed 的圆桌席位。stage1 = 风险审查 ‖ 方案设计(并行),stage2 = 执行规划,stage3 = 总结。
AGENTS = [
{"agentId": "roundtable-risk-review", "name": "风险审查"},
{"agentId": "roundtable-solution-design", "name": "方案设计"},
{"agentId": "roundtable-execution-plan", "name": "执行规划"},
{"agentId": "roundtable-summary", "name": "方案总结"},
]
PLAN = {
"mode": "dag",
"stages": [
{"id": "s0", "agentIds": ["roundtable-risk-review", "roundtable-solution-design"], "goal": "并行评估风险并产出初步方案"},
{"id": "s1", "agentIds": ["roundtable-execution-plan"], "goal": "据风险与方案制定执行规划"},
{"id": "s2", "agentIds": ["roundtable-summary"]},
],
"finalSynthesis": True,
"coordinatorPrompt": "先并行评估、再规划执行、最后总结,逐阶段承接。",
}
INTENT = "为一家中型制造企业设计一套数字化转型的整体方案"
TERMINAL = {"done", "error", "cancelled"}
def _headers() -> dict[str, str]:
h = {"Content-Type": "application/json"}
if AUTH_COOKIE:
h["Cookie"] = AUTH_COOKIE
if AUTH_BEARER:
h["Authorization"] = f"Bearer {AUTH_BEARER}"
return h
def _chain_str(nodes: list[dict]) -> str:
sym = {"pending": ".", "active": ">RUN", "done": "DONE"}
return " ".join(f"{n['name']}={sym.get(n['state'], '?')}" for n in nodes)
async def main() -> None:
async with httpx.AsyncClient(timeout=30.0, trust_env=False) as client:
print(f"[probe] POST {GATEWAY_URL}/api/roundtable-jobs mode=dag (stage1 并行)")
resp = await client.post(
f"{GATEWAY_URL}/api/roundtable-jobs",
headers=_headers(),
json={"intent": INTENT, "agents": AGENTS, "orchestrationMode": "dag", "orchestrationPlan": PLAN},
)
if resp.status_code != 201:
print(f"[probe] start failed: HTTP {resp.status_code}: {resp.text[:500]}")
return
job = resp.json()
job_id = job["id"]
# 验证 plan 在响应里回得来(持久化 + _to_response 生效)
plan_back = job.get("orchestrationPlan")
print(f"[probe] started {job_id} orchestrationMode={job.get('orchestrationMode')} "
f"orchestrationPlan stages={len((plan_back or {}).get('stages') or [])}")
last = ""
t0 = time.perf_counter()
saw_parallel = False
while True:
await asyncio.sleep(1.0)
r = await client.get(f"{GATEWAY_URL}/api/roundtable-jobs/{job_id}", headers=_headers())
if r.status_code != 200:
print(f"[probe] get failed: HTTP {r.status_code}: {r.text[:300]}")
return
j = r.json()
nodes = j.get("dispatchChain") or []
active_seats = [n["name"] for n in nodes if n["state"] == "active"]
if len(active_seats) >= 2:
saw_parallel = True
line = f"status={j['status']} phase={j['phase']} consensus={j['consensusPercentage']}% [{_chain_str(nodes)}]"
if line != last:
print(f"[probe] +{time.perf_counter() - t0:5.1f}s {line}")
last = line
if j["status"] in TERMINAL:
print(f"\n[probe] terminal: {j['status']}")
if j["status"] == "error":
print(f"[probe] error detail: {j.get('error')}")
# 对话顺序证明 stage 串行
seats = [d for d in (j.get("dialogues") or []) if d["role"] == "seat"]
print(f"[probe] 席位交付顺序: {[d['agentId'] for d in seats]}")
leaders = [d for d in (j.get("dialogues") or []) if d["role"] == "leader"]
print(f"[probe] 总控发言轮数: {len(leaders)}(3 stage 派活 + 1 收口 = 4)")
print(f"[probe] 是否观察到 stage 内并行(≥2 席位同时 active): {'是 ✅' if saw_parallel else '否 ❌'}")
sample = next((d["content"] for d in seats if d.get("content")), "")
print(f"[probe] 席位交付样例: {sample[:80]}")
return
if __name__ == "__main__":
asyncio.run(main())