run_evals.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. #!/usr/bin/env python3
  2. """第一方 agent 行为回归 — live 评估 runner(手动/nightly,要真 LLM、花钱)。
  3. 用法:
  4. AGENTPAAS_API_KEY=<key> python3 evals/run_evals.py [golden/xxx.yaml ...]
  5. 不传文件则跑 evals/golden/ 下全部。结果打印汇总 + 落 evals/last_report.json。
  6. 框架(打分逻辑)在 agentpaas.engine.agent_eval(已单测,进 CI);本 runner 只负责
  7. 把真实"跑 agent / 收产物 / judge"接进去。judge 复用 pipeline.default_judge。
  8. """
  9. from __future__ import annotations
  10. import glob
  11. import json
  12. import os
  13. import sys
  14. import time
  15. import urllib.request
  16. sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "agentpaas", "src"))
  17. sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "lambdagent", "src"))
  18. from agentpaas.engine.agent_eval import EvalTask, run_eval_task, summarize # noqa: E402
  19. BASE = os.environ.get("AGENTPAAS_URL", "http://127.0.0.1:8000")
  20. KEY = os.environ.get("AGENTPAAS_API_KEY", "")
  21. def _api(method, path, body=None):
  22. url = f"{BASE}{path}"
  23. data = json.dumps(body).encode() if body is not None else None
  24. req = urllib.request.Request(url, data=data, method=method,
  25. headers={"Content-Type": "application/json",
  26. "Authorization": f"Bearer {KEY}"})
  27. with urllib.request.urlopen(req, timeout=1800) as r:
  28. return json.loads(r.read())
  29. def _resolve_agent_id(template: str) -> str:
  30. """按模板找一个已安装实例(取第一个 active)。"""
  31. agents = _api("GET", "/api/v1/agents").get("agents", [])
  32. for a in agents:
  33. if a.get("agent_template") == template and a.get("status", "active") == "active":
  34. return a["id"]
  35. raise RuntimeError(f"未找到模板 {template} 的已安装实例")
  36. def _run_fn(task: EvalTask) -> dict:
  37. aid = task.agent_id or _resolve_agent_id(task.agent_template)
  38. # 同步 /run:返回时已完成
  39. res = _api("POST", f"/api/v1/agents/{aid}/run", {"input": task.input})
  40. return {
  41. "output": res.get("output", ""),
  42. "steps": (res.get("usage") or {}).get("steps", res.get("steps", 0)),
  43. "cost_usd": res.get("cost_usd", 0.0),
  44. "workspace_path": res.get("workspace_path", ""),
  45. "_run_id": res.get("id", ""), "_agent_id": aid,
  46. }
  47. def _collect_fn(workspace_path: str) -> dict:
  48. # 直接读磁盘(workspace_path 是本机绝对路径)
  49. files = {}
  50. if not workspace_path or not os.path.isdir(workspace_path):
  51. return files
  52. skip = {"config.yml", "cost.json", "input.json", "manifest.json", "output.json", "trace.json"}
  53. for dp, _dn, fn in os.walk(workspace_path):
  54. for f in fn:
  55. if f in skip or f.startswith("."):
  56. continue
  57. full = os.path.join(dp, f)
  58. try:
  59. with open(full, encoding="utf-8", errors="ignore") as fh:
  60. files[os.path.relpath(full, workspace_path)] = fh.read()
  61. except Exception:
  62. pass
  63. return files
  64. def _judge_fn(req):
  65. from agentpaas.engine.pipeline import default_judge
  66. return default_judge(req)
  67. def _load_tasks(paths):
  68. import yaml
  69. tasks = []
  70. for p in paths:
  71. spec = yaml.safe_load(open(p, encoding="utf-8"))
  72. tmpl = spec.get("agent_template", "")
  73. for t in spec.get("tasks", []):
  74. tasks.append(EvalTask(
  75. id=t["id"], agent_template=tmpl, agent_id=t.get("agent_id", ""),
  76. input=t.get("input", ""), must_produce=t.get("must_produce", []),
  77. rubric=t.get("rubric", ""), threshold=float(t.get("threshold", 0.6))))
  78. return tasks
  79. def main():
  80. if not KEY:
  81. print("✗ 需要 AGENTPAAS_API_KEY 环境变量(同前端用的那把)。"); return 2
  82. paths = sys.argv[1:] or sorted(glob.glob(os.path.join(os.path.dirname(__file__), "golden", "*.yaml")))
  83. tasks = _load_tasks(paths)
  84. print(f"跑 {len(tasks)} 个 golden 任务 @ {BASE}\n")
  85. results = []
  86. for t in tasks:
  87. t0 = time.time()
  88. r = run_eval_task(t, run_fn=_run_fn, collect_artifacts_fn=_collect_fn, judge_fn=_judge_fn)
  89. results.append(r)
  90. flag = "✅" if r.passed else "❌"
  91. print(f"{flag} {t.id:24} files_ok={r.files_ok} judge={r.judge_score:.2f} "
  92. f"steps={r.steps} ${r.cost_usd:.3f} {int(time.time()-t0)}s"
  93. + (f" 缺:{r.missing_files}" if r.missing_files else "")
  94. + (f" err:{r.error[:60]}" if r.error else ""))
  95. s = summarize(results)
  96. print("\n=== 汇总 ===")
  97. print(json.dumps(s, ensure_ascii=False, indent=2))
  98. out = os.path.join(os.path.dirname(__file__), "last_report.json")
  99. json.dump({"summary": s, "results": [r.__dict__ for r in results]},
  100. open(out, "w", encoding="utf-8"), ensure_ascii=False, indent=2)
  101. print(f"\n报告已落:{out}")
  102. return 0 if s["passed"] == s["total"] else 1
  103. if __name__ == "__main__":
  104. sys.exit(main())