四块互相交织的 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>
127 lines
5.2 KiB
Python
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
|