Files
Agentswarm/orchestrator/quality.py
T
Songhaoz666andClaude Opus 4.8 d487923646 benchmark: 落地决策层(τ/η/P)、质量(Q_quality)、通信遥测;关闭 #10 #23
四块互相交织的 benchmark 覆盖增量,统一提交:

1) 通信遥测(#23):orchestrator 路由 peer 消息时按 correlation_id 计请求/应答到
   SwarmRun.collaboration(内部状态,不进 Manager 事件流);collector 算 s_communication。
   治理计数由 run.approvals 派生(合规/总数)→ s_governance。

2) Q_quality 掩码归一(v2.1 裁定):metrics.quality_score 改为对 present 输入加权归一,
   非编码任务自动忽略 TestPassRate,全缺 → NaN(不伪造)。

3) 质量插桩 / Group B:新增 Pod 内代码测试沙箱(orchestrator/sandbox.py,环境清洗 +
   超时强杀 + 资源限额 + 路径越界校验,门控 ENABLE_QUALITY_EVAL)与 held-out fixture
   (benchmark/fixtures/);run 完成时用留出测试评分得 TestPassRate → Q_quality →
   collector 合成 reward。安全边界见 docs/integration/security-boundary.md §8.1。

4) 决策引擎 / Group A(#10,Option A score-at-pull):新增 orchestrator/decision_engine.py
   —— 信息素 τ(Redis 持久、(role,agent) 键控、冷启动 0.5、ρ 蒸发、夹紧、学习常开)+
   η 启发式评分 + ε-greedy 概率采样;每次 dispatch 产一条 DecisionTrace →
   SwarmRun.decisions;collector 算 tau/eta/p_decision。概率选择门控 ENABLE_ACO_DISPATCH
   (默认关,CI 用 ACO_SEED 固定)。

覆盖:单次 run 真实可算字段由 4 提升至最多 10/15(新增 communication/reward/tau/eta/
p_decision,外加 governance 有条件)。

测试:新增 test-sandbox / test-quality / test-decision-engine;扩充 collector/metrics 用例;
CI 纳入全部 benchmark 套件 + flag-on 的 ACO e2e。本地 11 项 gate 全绿。

诚实边界(未越界声称):
- Group A 为单边匹配(Option B 待 Group C);概率派发优于贪心未证;默认关闭。
- reward 的 CodeReview/UserAcceptance 未采集(掩码忽略);P_risk 为审批派生低估。
- s_gain/s_swarm/g_e/g_e_cost/benchmark 仍 NaN —— 需基线(#21/#13),本 PR 不动验收。

影响范围:Swarm(orchestrator + benchmark + docs + CI)。不改 Manager↔Swarm 事件契约
(遥测均为运行时内部状态);不影响 Client/计费/密钥/发布链路。新增 ENABLE_QUALITY_EVAL /
ENABLE_ACO_DISPATCH 两个开关,默认关闭。

Closes #10
Closes #23

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 12:51:32 +08:00

127 lines
5.2 KiB
Python

"""Quality instrumentation (benchmark Group B): turn a finished run's generated code into a real
TestPassRate, then into Q_quality via the masked/renormalized quality_score.
Pipeline:
1. gather the files the specialist agents generated (from task results) — split implementation
files from the swarm's own test files.
2. grade against the fixture's HELD-OUT tests (authoritative) in the sandbox → TestPassRate.
3. separately run the swarm's OWN tests as a non-grading signal (collaboration/robustness only).
4. Q_quality = quality_score(test_pass_rate=<fixture>, code_review_score=None, user_acceptance=None).
CodeReview / UserAcceptance are not collected here, so the masking rule renormalizes Q_quality
onto the one present input (rule #9: absent ≠ fabricated 0).
SECURITY: step 2/3 execute model-generated code. They run ONLY when ENABLE_QUALITY_EVAL is set,
and only inside the isolated pod (see orchestrator/sandbox.py security model). Default OFF.
"""
from __future__ import annotations
import asyncio
import json
import logging
import os
from typing import Any, Dict, List, Optional
from benchmark.fixtures import Fixture, load_fixture
from benchmark.metrics import quality_score
from .sandbox import SandboxFile, run_tests
logger = logging.getLogger(__name__)
def quality_eval_enabled() -> bool:
return os.getenv("ENABLE_QUALITY_EVAL", "false").lower() in {"1", "true", "yes"}
def _is_test_file(path: str) -> bool:
base = os.path.basename(path or "")
return base.startswith("test_") and base.endswith(".py")
def collect_generated_files(tasks) -> Dict[str, List[SandboxFile]]:
"""Reconstruct files the agents wrote, from task results. Last writer wins per path.
Returns {"impl": [...non-test files...], "agent_tests": [...the swarm's own test_*.py...]}.
"""
impl: Dict[str, str] = {}
agent_tests: Dict[str, str] = {}
for task in tasks:
result = getattr(task, "result", None)
if not result:
continue
try:
data = json.loads(result) if isinstance(result, str) else result
except Exception:
continue
subtasks = data.get("subtasks") if isinstance(data, dict) else None
file_groups = [data] + list(subtasks or []) if isinstance(data, dict) else []
for group in file_groups:
for f in (group.get("files") or []):
if not isinstance(f, dict):
continue
if (f.get("action") or "write") == "delete":
continue
path = f.get("path")
content = f.get("content")
if not path or content is None:
continue
(agent_tests if _is_test_file(path) else impl)[path] = content
return {
"impl": [SandboxFile(p, c) for p, c in impl.items()],
"agent_tests": [SandboxFile(os.path.basename(p), c) for p, c in agent_tests.items()],
}
async def evaluate_run_quality(run, tasks) -> Optional[Dict[str, Any]]:
"""Grade a completed run's code against its fixture. Returns a quality dict or None.
None means: quality eval disabled, no fixture bound, or nothing to grade — caller leaves
Q_quality/reward NaN (coverage=False). Never fabricates a score.
"""
if not quality_eval_enabled():
return None
fixture_id = (run.metadata or {}).get("benchmark_fixture_id")
if not fixture_id:
return None
try:
fixture: Fixture = load_fixture(fixture_id)
except Exception as exc:
logger.warning("quality eval: cannot load fixture %s: %s", fixture_id, exc)
return None
files = collect_generated_files(tasks)
impl = files["impl"]
agent_tests = files["agent_tests"]
quality: Dict[str, Any] = {
"fixture_id": fixture.id,
"expects_code": fixture.expects_code,
"test_pass_rate": None,
"code_review_score": None, # not collected (no reviewer wired)
"user_acceptance": None, # not collected (external Manager/human signal)
"agent_test_pass_rate": None, # SIGNAL ONLY — never part of the grade
"target_time_seconds": fixture.target_time_seconds,
}
# 1) Authoritative grade: held-out fixture tests against the implementation files.
if fixture.expects_code and fixture.test_files and impl:
fix_files = [SandboxFile(t.path, t.content) for t in fixture.test_files]
result = await asyncio.to_thread(run_tests, impl, fix_files)
quality["test_pass_rate"] = result.pass_rate
quality["fixture_total"] = result.total
quality["fixture_passed"] = result.passed
if result.error:
quality["fixture_error"] = result.error
# 2) Signal only: the swarm's own tests (collaboration/robustness, NOT the grade).
if agent_tests and impl:
sig = await asyncio.to_thread(run_tests, impl, agent_tests)
quality["agent_test_pass_rate"] = sig.pass_rate
# 3) Q_quality via the masked/renormalized blend (absent inputs drop out).
quality["q_quality"] = quality_score(
test_pass_rate=quality["test_pass_rate"],
code_review_score=quality["code_review_score"],
user_acceptance=quality["user_acceptance"],
)
return quality