feat(v2): T6 CollaborativePipeline 编排(快路径/协作循环/熔断/终审)
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
"""T6 CollaborativePipeline 编排单测(封闭:MockTransport + 假 generate)。"""
|
||||
import json
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from router_system.architect import ArchitectClient
|
||||
from router_system.pipeline import CollaborativePipeline
|
||||
from router_system.verifier import Verifier
|
||||
from router_system.worker import WorkerLoop
|
||||
|
||||
BRIEF_JSON = json.dumps({
|
||||
"goal": "用 Python 实现一个函数",
|
||||
"constraints": ["标准库"],
|
||||
"tags": ["code"],
|
||||
"acceptance": [{"id": "a1", "check": "可运行", "machine_checkable": True}],
|
||||
"plan": [{"id": "s1", "task": "实现函数", "deps": [], "done_criteria": "函数可运行"}],
|
||||
}, ensure_ascii=False)
|
||||
|
||||
DECIDE_JSON = json.dumps({"reply": "改用更简单的实现", "patch_plan": [{"id": "s1", "task": "简化实现"}]},
|
||||
ensure_ascii=False)
|
||||
REVIEW_DONE = json.dumps({"verdict": "done", "notes": "通过", "fix_issues": []}, ensure_ascii=False)
|
||||
|
||||
|
||||
def _architect_handler(brief=BRIEF_JSON, decide=DECIDE_JSON, review=REVIEW_DONE):
|
||||
def handler(request):
|
||||
body = json.loads(request.content)
|
||||
msg = (body.get("messages") or [{}])[-1].get("content", "")
|
||||
if "任务 brief" in msg or "任务分析" in msg:
|
||||
return _resp(brief)
|
||||
if "裁决" in msg:
|
||||
return _resp(decide)
|
||||
if "终审" in msg:
|
||||
return _resp(review)
|
||||
# 兜底(breach:architect_do)
|
||||
return _resp("(兜底答案)")
|
||||
return handler
|
||||
|
||||
|
||||
def _resp(content):
|
||||
return httpx.Response(200, json={
|
||||
"choices": [{"message": {"content": content}}],
|
||||
"usage": {"prompt_tokens": 100, "completion_tokens": 20},
|
||||
})
|
||||
|
||||
|
||||
def _make_architect(handler):
|
||||
return ArchitectClient(model="deepseek-chat", base_url="https://api.deepseek.com/v1",
|
||||
api_key="sk-test", transport=httpx.MockTransport(handler))
|
||||
|
||||
|
||||
def _fake_generate(texts):
|
||||
it = iter(texts)
|
||||
|
||||
async def _g(prompt):
|
||||
try:
|
||||
return next(it)
|
||||
except StopIteration:
|
||||
return ""
|
||||
|
||||
return _g
|
||||
|
||||
|
||||
def _make_worker(generate):
|
||||
return WorkerLoop(generate=generate,
|
||||
verifier=Verifier(sandbox_runner=lambda c, e, t: (0, "", "")),
|
||||
kb=None, max_fix_attempts=2)
|
||||
|
||||
|
||||
def _pipeline(architect, worker, **kw):
|
||||
return CollaborativePipeline(architect=architect, worker=worker,
|
||||
fast_path=kw.get("fast_path", False),
|
||||
rounds_cap=kw.get("rounds_cap", 6),
|
||||
api_token_cap=kw.get("api_token_cap", 8000),
|
||||
breach_policy=kw.get("breach_policy", "architect_do"),
|
||||
run_dir=kw.get("run_dir", "runs"))
|
||||
|
||||
|
||||
def asyncio_run(coro):
|
||||
import asyncio
|
||||
return asyncio.run(coro)
|
||||
|
||||
|
||||
# ---------- 快路径 ----------
|
||||
def test_fast_path_returns_direct(tmp_path):
|
||||
architect = _make_architect(_architect_handler())
|
||||
worker = _make_worker(_fake_generate(["这是一段足够长的、非占位的直接回答内容,用于快路径。"]))
|
||||
pipe = _pipeline(architect, worker, fast_path=True, run_dir=str(tmp_path / "runs"))
|
||||
res = asyncio_run(pipe.run("讲一下排序"))
|
||||
assert res.status == "fast_path"
|
||||
assert res.fast_path is True
|
||||
assert res.api_input_tokens == 0 # 未调用 Architect
|
||||
assert "快路径" in res.response or "直接" in res.response
|
||||
|
||||
|
||||
# ---------- 协作:一次通过 ----------
|
||||
def test_collab_one_pass(tmp_path):
|
||||
architect = _make_architect(_architect_handler())
|
||||
worker = _make_worker(_fake_generate(["def my_func():\n return 42"]))
|
||||
pipe = _pipeline(architect, worker, fast_path=False, run_dir=str(tmp_path / "runs"))
|
||||
res = asyncio_run(pipe.run("写个函数"))
|
||||
assert res.status == "done"
|
||||
assert res.fast_path is False
|
||||
assert res.api_input_tokens >= 100 # brief + 终审 用量已计量
|
||||
assert res.workspace_path is not None
|
||||
# 交流文本已落盘
|
||||
import os
|
||||
assert os.path.exists(res.workspace_path)
|
||||
assert "done" in res.route
|
||||
|
||||
|
||||
# ---------- 协作:带 issue 修复 ----------
|
||||
def test_collab_issue_fix(tmp_path):
|
||||
architect = _make_architect(_architect_handler())
|
||||
# 前两次失败(占位)-> issue;decide 后重跑成功
|
||||
worker = _make_worker(_fake_generate([
|
||||
"待实现", "待实现", "def ok():\n return 1",
|
||||
]))
|
||||
pipe = _pipeline(architect, worker, fast_path=False, run_dir=str(tmp_path / "runs"))
|
||||
res = asyncio_run(pipe.run("写个函数"))
|
||||
assert res.status == "done"
|
||||
assert any("issue" in r for r in res.route)
|
||||
assert any("decide" in r for r in res.route)
|
||||
# 至少有 1 条 decision
|
||||
import json as _json
|
||||
ws = json.load(open(res.workspace_path, encoding="utf-8"))
|
||||
assert len(ws["decisions"]) >= 1
|
||||
|
||||
|
||||
# ---------- 熔断兜底:本地降级 ----------
|
||||
def test_breach_local_degrade(tmp_path):
|
||||
architect = _make_architect(_architect_handler())
|
||||
worker = _make_worker(_fake_generate(["不通过"]))
|
||||
pipe = _pipeline(architect, worker, fast_path=False, api_token_cap=1,
|
||||
breach_policy="local_only", run_dir=str(tmp_path / "runs"))
|
||||
res = asyncio_run(pipe.run("写个函数"))
|
||||
assert res.status == "failed"
|
||||
assert "本地降级" in res.response
|
||||
assert any("breach" in r for r in res.route)
|
||||
|
||||
|
||||
# ---------- 熔断兜底:Architect 代做 ----------
|
||||
def test_breach_architect_do(tmp_path):
|
||||
# review 一直打回 fix(制造压力),最终走 breach:architect_do
|
||||
def handler(request):
|
||||
body = json.loads(request.content)
|
||||
msg = (body.get("messages") or [{}])[-1].get("content", "")
|
||||
if "任务 brief" in msg or "任务分析" in msg:
|
||||
return _resp(BRIEF_JSON)
|
||||
if "自动升级" in msg:
|
||||
return _resp("(Architect 兜底答案)")
|
||||
return _resp(DECIDE_JSON)
|
||||
architect = _make_architect(handler)
|
||||
worker = _make_worker(_fake_generate(["待实现"]))
|
||||
pipe = _pipeline(architect, worker, fast_path=False, rounds_cap=1,
|
||||
api_token_cap=8000, breach_policy="architect_do",
|
||||
run_dir=str(tmp_path / "runs"))
|
||||
res = asyncio_run(pipe.run("写个函数"))
|
||||
# rounds_cap=1:一轮后预算未满但回合触顶 -> breach -> architect_do
|
||||
assert "breach" in " ".join(res.route)
|
||||
assert res.status in ("escalated", "failed")
|
||||
Reference in New Issue
Block a user