Files
Agent-System-Dev/.task/genui-u1/run_trial.py
T
weichen 1c25d65ae9 Record the U1 trial: harness, results, and task-card outcomes
Why: The U1 controlled trial finished; its fixtures, validator, runner,
per-run results, report, and renderer spike must be reviewable from the
task branch, and the task card needs the final PASS/FAIL record.

Evidence: results/summary.json + report.md (SELF, MiniMax official
channel, CNY 3 of 200 cap); make evidence receipts for precheck,
task-sync, and make test follow in this branch.

Impact: Task-scoped experiment artifacts only (.task/genui-u1/); no
production code changes.

Refs: Documents PR #62 (proposal r2); task card feature/generative-ui-u1
2026-10-11 04:43:03 +00:00

157 lines
6.4 KiB
Python
Raw 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.
#!/usr/bin/env python3
"""U1 试验运行器(原型):B=Markdown 基线,G=UINode 生成(含一次修复),G-hs=轻量档子集。
用法:python3 run_trial.py --arm G [--limit N] [--only rc-01,rc-02]
密钥经环境 MINIMAX_API_KEY 注入;预算护栏 >¥200 或单臂调用超限时中止。
"""
import argparse
import json
import os
import sys
import time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from harness import (BASELINE_PROMPT, CATALOG_PROMPT, build_user_message,
call_llm, extract_json, validate_spec)
HERE = os.path.dirname(os.path.abspath(__file__))
RESULTS = os.path.join(HERE, "results")
# E5 任务卡记录的 MiniMax 档位价($/M tokens:入/出),汇率按 7.2 折 CNY;保守不计缓存折扣。
PRICE = {"MiniMax-M3": (0.3, 1.2), "MiniMax-M2.7-highspeed": (0.6, 2.4)}
BUDGET_CNY, ARM_CALL_CAPS = 200.0, {"B": 60, "G": 60, "G-hs": 20}
def spent_cny():
total = 0.0
for arm in os.listdir(RESULTS) if os.path.isdir(RESULTS) else []:
d = os.path.join(RESULTS, arm)
if not os.path.isdir(d):
continue
for fn in os.listdir(d):
if not fn.endswith(".json"):
continue
try:
rec = json.load(open(os.path.join(d, fn)))
except Exception:
continue
for c in rec.get("calls", []):
u = c.get("usage", {})
pin, pout = PRICE.get(rec.get("model", ""), (0, 0))
total += (u.get("prompt_tokens", 0) / 1e6 * pin + u.get("completion_tokens", 0) / 1e6 * pout) * 7.2
return total
def call_count(arm):
d = os.path.join(RESULTS, arm)
if not os.path.isdir(d):
return 0
return sum(json.load(open(os.path.join(d, fn))).get("callCount", 1)
for fn in os.listdir(d) if fn.endswith(".json"))
def run_task(arm, model, task):
user = build_user_message(task)
sids = {m["sourceId"] for m in task["materials"]}
rec = {"task": task["id"], "family": task["family"], "arm": arm, "model": model,
"goalKind": task["expect"]["goalKind"], "calls": [], "callCount": 0}
def call(system, user, max_tokens):
r = call_llm(model, system, user, max_tokens)
rec["calls"].append({"usage": r["usage"], "latency_s": r["latency_s"]})
rec["callCount"] += 1
return r
if arm == "B":
r = call(BASELINE_PROMPT, user, 2500)
rec["markdown"] = r["content"]
rec["ok"] = bool(r["content"].strip())
return rec
# G / G-hs:生成 + 至多一次修复。max_tokens 需容纳内联 <think> + 完整 JSON(6000 会在
# M3 上被思考段挤占导致截断,见 results/ 首轮 rc-03..08 失败样本)。
r1 = call(CATALOG_PROMPT, user, 14000)
rec["raw_first"] = r1["content"]
errors, stats, spec = [], None, None
try:
spec = extract_json(r1["content"])
errors, stats = validate_spec(spec, sids)
except Exception as e:
errors = [f"JSON 解析失败: {e}"]
rec["valid_first"] = not errors
rec["errors_first"] = errors[:15]
rec["stats_first"] = {"nodes": stats["nodes"], "maxDepth": stats["maxDepth"],
"types": stats["types"], "refsUsed": sorted(stats["refsUsed"]),
"unknownRefs": sorted(stats["unknownRefs"])} if stats else None
if errors:
fix_user = user + "\n\n你上一次的输出被宿主拒绝,错误如下:\n" + \
"\n".join(f"- {e}" for e in errors[:15]) + \
"\n\n请修正后重新输出完整 JSON 对象(仍只输出 JSON)。"
r2 = call(CATALOG_PROMPT, fix_user, 14000)
rec["raw_repair"] = r2["content"]
try:
spec = extract_json(r2["content"])
errors, stats = validate_spec(spec, sids)
except Exception as e:
errors = [f"JSON 解析失败: {e}"]
rec["repaired"] = True
else:
rec["repaired"] = False
rec["valid_final"] = not errors
rec["errors_final"] = errors[:15]
rec["spec"] = spec if not errors else None
if stats and not errors:
rec["stats_final"] = {"nodes": stats["nodes"], "maxDepth": stats["maxDepth"],
"types": stats["types"], "refsUsed": sorted(stats["refsUsed"]),
"idsProvided": len(stats["idsSeen"])}
return rec
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--arm", required=True, choices=list(ARM_CALL_CAPS))
ap.add_argument("--limit", type=int, default=0)
ap.add_argument("--only", default="")
args = ap.parse_args()
tasks = json.load(open(os.path.join(HERE, "fixtures.json")))["tasks"]
if args.only:
keep = set(args.only.split(","))
tasks = [t for t in tasks if t["id"] in keep]
if args.arm == "G-hs":
tasks = [t for t in tasks if t["family"] != "research-compare" or t.get("sharedGroup")][:10]
if args.limit:
tasks = tasks[:args.limit]
model = "MiniMax-M2.7-highspeed" if args.arm == "G-hs" else "MiniMax-M3"
outdir = os.path.join(RESULTS, args.arm)
os.makedirs(outdir, exist_ok=True)
for t in tasks:
out = os.path.join(outdir, t["id"] + ".json")
if os.path.exists(out):
old = json.load(open(out))
if old.get("arm") == args.arm and old.get("model") == model and (
old.get("ok") or old.get("valid_final")):
print(f"[skip] {t['id']} (已完成)")
continue
if call_count(args.arm) >= ARM_CALL_CAPS[args.arm]:
print(f"[stop] 臂 {args.arm} 达调用上限 {ARM_CALL_CAPS[args.arm]}")
break
spent, est = spent_cny(), 0.35 # 单次最坏估价(¥)先行校验
if spent + est > BUDGET_CNY:
print(f"[stop] 预算护栏:已花 ¥{spent:.2f} + 单次估价 ¥{est:.2f} 将超 ¥{BUDGET_CNY}")
break
t0 = time.time()
rec = run_task(args.arm, model, t)
rec["wall_s"] = round(time.time() - t0, 1)
json.dump(rec, open(out, "w"), ensure_ascii=False, indent=1)
flag = "OK " if (rec.get("ok") or rec.get("valid_final")) else "ERR"
print(f"[{flag}] {t['id']} calls={rec['callCount']} wall={rec['wall_s']}s "
f"spent≈¥{spent_cny():.2f}")
print(f"[done] arm={args.arm} calls={call_count(args.arm)}/{ARM_CALL_CAPS[args.arm]} "
f"spent≈¥{spent_cny():.2f}")
if __name__ == "__main__":
main()