165 lines
7.0 KiB
Python
165 lines
7.0 KiB
Python
"""同一任务连续跑 N 次,统计成功率 / 步数 / 时长,结果写入 data/eval/*.jsonl。
|
||
|
||
用法(后端需已启动;端口取 .env 的 API_PORT,可用环境变量 API_PORT 覆盖):
|
||
.venv\\Scripts\\python.exe scripts\\eval_run.py --n 3 --message "帮我填今天的查课日志,班级计科2301班,听课记录还好" --supplement "课程是高等数学,授课教师是张老师"
|
||
|
||
默认是干跑(dry run):Agent 走到「提交前确认」时脚本自动回复取消,不会真正提交,
|
||
用来测「到确认为止」的步数和时长。加 --submit 才会在确认时回复「确认提交」。
|
||
其他中途提问(如问听课地点)自动回复 --auto-answer。
|
||
"""
|
||
|
||
import argparse
|
||
import json
|
||
import os
|
||
import sys
|
||
import time
|
||
import urllib.error
|
||
import urllib.request
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parent.parent
|
||
sys.path.insert(0, str(ROOT))
|
||
|
||
from dotenv import load_dotenv # noqa: E402
|
||
|
||
load_dotenv(ROOT / ".env")
|
||
PORT = int(os.getenv("API_PORT", "8788"))
|
||
BASE = f"http://127.0.0.1:{PORT}/api"
|
||
TERMINAL = {"completed", "submitted", "failed"}
|
||
CANCEL_ANSWER = "取消,不要提交。这次是评测干跑,请保存 cancelled 后 done 说明表单已填未提交。"
|
||
CONFIRM_ANSWER = "确认提交"
|
||
|
||
|
||
def api(method: str, path: str, body: dict | None = None) -> dict:
|
||
data = json.dumps(body, ensure_ascii=False).encode("utf-8") if body is not None else None
|
||
req = urllib.request.Request(BASE + path, data=data, method=method, headers={"Content-Type": "application/json"})
|
||
with urllib.request.urlopen(req, timeout=30) as r:
|
||
return json.loads(r.read().decode("utf-8"))
|
||
|
||
|
||
def count_steps(task_id: str) -> int | None:
|
||
try:
|
||
doc = api("GET", f"/tasks/{task_id}/progress-doc")["content"]
|
||
except Exception: # noqa: BLE001
|
||
return None
|
||
return sum(1 for line in doc.splitlines() if line.startswith("### ") and "步骤 " in line)
|
||
|
||
|
||
def is_confirm_question(q: str) -> bool:
|
||
return ("确认" in q and "提交" in q) or "即将提交" in q
|
||
|
||
|
||
def run_once(args, message: str) -> dict:
|
||
t0 = time.time()
|
||
task = api("POST", "/tasks/start", {"knowledge_id": "xuegong_demo", "message": message, "student_id": args.student})
|
||
tid = task["id"]
|
||
print(f" task {tid} | {task.get('business_name') or ''} | {task['status']} | {task['reply'][:80]}")
|
||
if task["status"] == "collecting":
|
||
if not args.supplement:
|
||
return {"task_id": tid, "status": "collecting", "needs": task.get("needs_input"), "duration_s": 0}
|
||
task = api("POST", f"/tasks/{tid}/inputs", {"message": args.supplement})
|
||
print(f" supplement -> {task['status']} | {task['reply'][:80]}")
|
||
|
||
pauses = 0
|
||
confirm_at = None
|
||
steps_at_confirm = None
|
||
status = task["status"]
|
||
while time.time() - t0 < args.timeout:
|
||
time.sleep(3)
|
||
t = api("GET", f"/tasks/{tid}")
|
||
status = t["status"]
|
||
if status == "paused":
|
||
pauses += 1
|
||
q = t.get("pause_reason") or ""
|
||
print(f" [paused #{pauses} @ {round(time.time() - t0)}s] {q[:160].replace(chr(10), ' ')}")
|
||
if pauses > 3:
|
||
break
|
||
if is_confirm_question(q) and confirm_at is None:
|
||
confirm_at = round(time.time() - t0, 1)
|
||
steps_at_confirm = count_steps(tid)
|
||
api("POST", f"/tasks/{tid}/human", {"answer": CONFIRM_ANSWER if args.submit else CANCEL_ANSWER})
|
||
else:
|
||
api("POST", f"/tasks/{tid}/human", {"answer": args.auto_answer})
|
||
continue
|
||
if status in TERMINAL:
|
||
break
|
||
else:
|
||
status = f"timeout({status})"
|
||
|
||
t = api("GET", f"/tasks/{tid}")
|
||
artifacts = t.get("artifacts") or {}
|
||
drafts = []
|
||
try:
|
||
drafts = api("GET", f"/learned?source_task_id={tid}&status=draft")
|
||
except Exception: # noqa: BLE001
|
||
pass
|
||
return {
|
||
"ts": datetime.now().isoformat(timespec="seconds"),
|
||
"task_id": tid,
|
||
"status": status,
|
||
"dry_run": not args.submit,
|
||
"duration_s": round(time.time() - t0, 1),
|
||
"steps": count_steps(tid),
|
||
"confirm_at_s": confirm_at,
|
||
"steps_to_confirm": steps_at_confirm,
|
||
"pauses": pauses,
|
||
"artifact_keys": sorted(k for k in artifacts if k != "summary"),
|
||
"learned_drafts": [d["key"] for d in drafts],
|
||
"summary": (artifacts.get("summary") or t.get("pause_reason") or "")[:300],
|
||
}
|
||
|
||
|
||
def main() -> int:
|
||
ap = argparse.ArgumentParser()
|
||
ap.add_argument("--n", type=int, default=3)
|
||
ap.add_argument("--message", default="帮我填今天的查课日志,班级计科2301班,听课记录还好")
|
||
ap.add_argument("--supplement", default="", help="任务进入 collecting 时自动补充的一句话")
|
||
ap.add_argument("--student", default="default")
|
||
ap.add_argument("--timeout", type=float, default=900)
|
||
ap.add_argument("--auto-answer", default="不需要,留空继续")
|
||
ap.add_argument("--submit", action="store_true", help="到确认时真正确认提交(默认干跑:取消)")
|
||
args = ap.parse_args()
|
||
|
||
try:
|
||
meta = api("GET", "/meta")
|
||
except urllib.error.URLError as e:
|
||
print(f"后端未启动({BASE}):{e}")
|
||
return 1
|
||
print(f"后端 pid={meta['pid']} version={meta['version']} db={meta['db']} school={meta.get('school_id')} | {'真实提交' if args.submit else '干跑(到确认即取消)'}")
|
||
|
||
out_dir = ROOT / "data" / "eval"
|
||
out_dir.mkdir(parents=True, exist_ok=True)
|
||
out_path = out_dir / f"eval_{datetime.now().strftime('%Y%m%d_%H%M%S')}.jsonl"
|
||
results: list[dict] = []
|
||
for i in range(1, args.n + 1):
|
||
print(f"[{i}/{args.n}] 开始:{args.message}")
|
||
r = run_once(args, args.message)
|
||
results.append(r)
|
||
with out_path.open("a", encoding="utf-8") as f:
|
||
f.write(json.dumps(r, ensure_ascii=False) + "\n")
|
||
print(
|
||
f" -> {r['status']} | {r['duration_s']}s | steps={r.get('steps')} | "
|
||
f"到确认 {r.get('steps_to_confirm')} 步 / {r.get('confirm_at_s')}s | drafts={r.get('learned_drafts')}"
|
||
)
|
||
|
||
ok = [r for r in results if r["status"] in ("completed", "submitted")]
|
||
durs = [r["duration_s"] for r in results if r["duration_s"]]
|
||
steps = [r["steps"] for r in results if r.get("steps") is not None]
|
||
to_confirm = [r["steps_to_confirm"] for r in results if r.get("steps_to_confirm") is not None]
|
||
confirm_s = [r["confirm_at_s"] for r in results if r.get("confirm_at_s") is not None]
|
||
print()
|
||
print(f"成功 {len(ok)}/{len(results)}")
|
||
if durs:
|
||
print(f"平均总时长 {sum(durs) / len(durs):.1f}s(最短 {min(durs)}s,最长 {max(durs)}s)")
|
||
if steps:
|
||
print(f"平均总步数 {sum(steps) / len(steps):.1f}")
|
||
if to_confirm:
|
||
print(f"到确认平均 {sum(to_confirm) / len(to_confirm):.1f} 步 / {sum(confirm_s) / len(confirm_s):.0f}s")
|
||
print(f"明细:{out_path}")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|