"""
=============================================================================
CS486 · 第 11 讲 · 代码 03：怎样可靠地评测一个智能体——pass^k、方差、题目数、成本
=============================================================================

【这个脚本要回答的问题】（讲义 §6）
    - τ-bench（Yao et al. 2024）：智能体是随机的；同一个任务跑 k 次全部成功的概率 pass^k 远低于 pass@1。
    - "AI Agents That Matter"（Kapoor et al. 2024）：很多智能体论文不控制成本；在 HumanEval 上，简单的"失败就重试、
      逐步提高温度"能达到复杂智能体的准确率，成本低得多；评测缺少留出集、可复现性差。
    - 第 1、2 讲：题目少、只跑一次时，置信区间很宽，两个系统的排名容易翻转。

    我们让代码 02 的智能体变成随机的（采样温度 0.7），在 100 个任务（3 块积木、4 个子目标）上各跑 16 次。

    演示 1：每个任务 16 次里成功几次？pass@k 和 pass^k。
    演示 2：同一个智能体，换一个随机种子、只跑一次，成功率差多少？
    演示 3：脚手架和成本——记忆、重试次数、贪心还是采样：成功率和成本；脚手架之间会互相影响。
    演示 4：比较两个差距不大的智能体：只有 10–20 道题、各跑一次时，排名有多大概率是反的？

【运行】
    python3 03_evaluating_agents.py           # 约 2 秒（需要第 8 讲的提议者缓存）
    python3 03_evaluating_agents.py --quick   # 冒烟测试

【依赖】PyTorch + 本目录的 agent_lab.py。
"""

import argparse
import os
import random
import sys
import time

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(HERE, "..", "lecture01"))

import agent_lab as ag                                                   # noqa: E402
from console import pct, print_header, print_observations, print_table  # noqa: E402

V1 = ("采样", ag.AgentConfig(illegal="reject", temp=0.7))
V2 = ("采样 + 检查点重试 3 次", ag.AgentConfig(illegal="reject", retries=3, temp=0.7, retry_temp=1.0))


def runs(pol, tasks, cfg, n_runs):
    """每个任务跑 n_runs 次（种子 0 … n_runs−1）。【返回】runs[r][i] = 第 r 次运行、第 i 个任务的结果字典"""
    return [ag.run_agent(pol, tasks, cfg, seed=r) for r in range(n_runs)]


def main():
    ap = argparse.ArgumentParser(description="评测智能体")
    ap.add_argument("--quick", action="store_true", help="冒烟测试")
    args = ap.parse_args()
    t0 = time.perf_counter()
    count = 20 if args.quick else 100
    R = 4 if args.quick else 16

    import plan_lab as pl
    print_header(f"准备：随机智能体（温度 0.7）；{count} 个任务（3 块积木、4 个子目标），每个任务跑 {R} 次")
    pol = ag.RawPolicy(pl.train_proposer(quick=args.quick))
    tasks = ag.task_set(3, 4, count, seed=7)
    res = {name: runs(pol, tasks, cfg, R) for name, cfg in (V1, V2)}

    # ------------------------------------------------------------------
    print_header(f"演示 1：每个任务 {R} 次里成功几次？pass@k 与 pass^k")
    succ = {name: [sum(res[name][r][i]["ok"] for r in range(R)) for i in range(count)] for name in res}
    bins = [(0, 0), (1, R // 4), (R // 4 + 1, R // 2), (R // 2 + 1, 3 * R // 4), (3 * R // 4 + 1, R - 1), (R, R)]
    rows = []
    for name in res:
        rows.append([name, *[sum(lo <= c <= hi for c in succ[name]) for lo, hi in bins]])
    print_table(["智能体", *[f"{lo}–{hi} 次" if lo != hi else f"{lo} 次" for lo, hi in bins]], rows, [26] + [9] * len(bins))
    ks = [k for k in (1, 2, 4, 8, 16) if k <= R]
    rows, d1 = [], {}
    for name in res:
        at = [sum(ag.pass_at_k(R, c, k) for c in succ[name]) / count for k in ks]
        hat = [sum(ag.pass_hat_k(R, c, k) for c in succ[name]) / count for k in ks]
        d1[name] = (at, hat)
        rows.append([name, "pass@k", *[pct(x, 0) for x in at]])
        rows.append(["", "pass^k", *[pct(x, 0) for x in hat]])
    print_table(["智能体", "", *[f"k={k}" for k in ks]], rows, [26, 8] + [8] * len(ks))

    # ------------------------------------------------------------------
    print_header(f"演示 2：同一个智能体、同样 {count} 个任务，换一个随机种子只跑一次——成功率是多少？")
    rows, d2 = [], {}
    for name in res:
        per_seed = [sum(res[name][r][i]["ok"] for i in range(count)) / count for r in range(R)]
        d2[name] = (min(per_seed), max(per_seed))
        rows.append([name, pct(min(per_seed), 0), pct(max(per_seed), 0), pct(sum(per_seed) / R, 1)])
    print_table(["智能体", f"{R} 个种子里最低", "最高", "平均"], rows, [26, 16, 8, 8])

    # ------------------------------------------------------------------
    half = max(2, R // 2)
    print_header(f"演示 3：脚手架和成本——贪心 / 采样（温度 0.7）× 有没有记忆 × 最多重试几次（重试时温度 1.0）；采样的每题跑 {half} 次")
    rows, d3, cost_runs = [], {}, {}
    settings = [(0.0, False, 0), (0.0, True, 0)] + [(0.7, m, r) for m in (False, True) for r in (0, 1, 3, 7)]
    for temp, memory, retries in settings:
        cfg = ag.AgentConfig(illegal="reject", memory=memory, retries=retries, temp=temp, retry_temp=1.0)
        rr = runs(pol, tasks, cfg, 1 if temp == 0 else half)
        cost_runs[(temp, memory, retries)] = rr
        ok = sum(r["ok"] for run in rr for r in run) / (count * len(rr))
        env = sum(r["env"] for run in rr for r in run) / (count * len(rr))
        stuck = sum(r["reason"] == "stuck" for run in rr for r in run) / (count * len(rr))
        d3[(temp, memory, retries)] = (ok, env, stuck)
        rows.append(["贪心" if temp == 0 else "采样", "有" if memory else "无", retries, pct(ok, 1), pct(stuck, 1), f"{env:.1f}"])
    print_table(["决策", "记忆", "最多重试", "成功率", "因'无路可走'失败", "平均和环境交互的次数"], rows, [6, 6, 10, 10, 18, 22])

    # ------------------------------------------------------------------
    A_, B_ = (0.7, False, 3), (0.7, True, 3)
    okA = [sum(cost_runs[A_][r][i]["ok"] for r in range(half)) / half for i in range(count)]
    okB = [sum(cost_runs[B_][r][i]["ok"] for r in range(half)) / half for i in range(count)]
    mA, mB = sum(okA) / count, sum(okB) / count
    print_header("演示 4：比较两个差距不大的智能体（演示 3 里重试 3 次的'无记忆'和'有记忆'）——随机抽 n 道题、各跑一次")
    print(f"  （用全部 {count} 个任务 × {half} 次的平均：无记忆 {pct(mA)}，有记忆 {pct(mB)}）")
    rng = random.Random(0)
    rows, d4 = [], {}
    trials = 200 if args.quick else 2000
    for n_tasks in (10, 20, 50, 100):
        flips = 0
        for _ in range(trials):
            idx = [rng.randrange(count) for _ in range(n_tasks)]
            a = sum(cost_runs[A_][rng.randrange(half)][i]["ok"] for i in idx)
            b = sum(cost_runs[B_][rng.randrange(half)][i]["ok"] for i in idx)
            flips += (a >= b) if mB > mA else (b >= a)
        d4[n_tasks] = flips / trials
        rows.append([n_tasks, pct(flips / trials, 0)])
    print_table(["题目数", "排名反了或打平的概率"], rows, [8, 22])
    diffs = [b - a for a, b in zip(okA, okB)]
    lo, hi = ag.bootstrap_ci(diffs, lambda v: sum(v) / len(v), random.Random(1), reps=500 if args.quick else 2000)
    one = [cost_runs[B_][0][i]["ok"] - cost_runs[A_][0][i]["ok"] for i in range(count)]
    lo1, hi1 = ag.bootstrap_ci(one, lambda v: sum(v) / len(v), random.Random(1), reps=500 if args.quick else 2000)
    print(f"  差别（有记忆 − 无记忆）的 95% 自助法区间（{count} 道题）：每题跑 {half} 次 [{100 * lo:+.1f}, {100 * hi:+.1f}] 个百分点；"
          f"每题只跑 1 次 [{100 * lo1:+.1f}, {100 * hi1:+.1f}]")

    if args.quick:
        print("\n（--quick 冒烟测试：数字没有意义，不打印观察。）")
        print(f"\n⏱ 总用时 {time.perf_counter() - t0:.0f} 秒")
        return
    n1, n2 = V1[0], V2[0]
    k8 = ks.index(8)
    always = sum(c == R for c in succ[n1])
    never = sum(c == 0 for c in succ[n1])
    g0, g1 = d3[(0.0, False, 0)], d3[(0.0, True, 0)]
    s0, s1 = d3[(0.7, False, 0)], d3[(0.7, True, 0)]
    print_observations([
        f"pass^k 掉得很快（演示 1）：'{n1}' pass@1 {pct(d1[n1][0][0], 0)}，pass^8 只有 {pct(d1[n1][1][k8], 0)}；pass@8 却有 "
        f"{pct(d1[n1][0][k8], 0)}。{count} 个任务里，{R} 次全成功的 {always} 个、全失败的 {never} 个，其余都是'有时成功'。"
        f"加上检查点重试，pass^8 从 {pct(d1[n1][1][k8], 0)} 提到 {pct(d1[n2][1][k8], 0)}。pass@k 回答'多试几次能不能做到'"
        "（有验证器挑选时有意义），pass^k 回答'每次都能做到吗'（直接面对用户时才有意义）——两者在同一个智能体上可以差几十个百分点。",
        f"同一个智能体、同样的题，只换随机种子（演示 2）：'{n1}' 的成功率在 {pct(d2[n1][0], 0)} 到 {pct(d2[n1][1], 0)} 之间。"
        "一篇论文只报告一次运行的结果，这个范围就是它没告诉你的误差（第 2 讲的种子方差）。",
        f"脚手架之间会互相影响（演示 3）：贪心决策时，记忆有帮助（{pct(g0[0])} → {pct(g1[0])}：它打破了确定性的来回横跳）；"
        f"采样、不重试时，记忆反而有害（{pct(s0[0])} → {pct(s1[0])}），因'无路可走'失败的比例从 {pct(s0[2])} 升到 {pct(s1[2])}——"
        "随机性本身已经能打破循环，记忆又把通向走过的状态的动作全部禁止，堵死了需要绕回去的路；"
        f"而加上重试之后，记忆又变得有帮助（重试 3 次：{pct(d3[(0.7, False, 3)][0])} → {pct(d3[(0.7, True, 3)][0])}——退回检查点时记忆被清空，死路不再致命）。"
        "'加一个模块总不会更差'是错的；每个组件的价值要在完整的系统里、和其他组件一起测（第 2 讲的交互效应）。",
        f"简单的'失败就重试'很强（演示 3）：无记忆、最多重试 7 次，成功率 {pct(d3[(0.7, False, 7)][0])}，平均和环境交互 "
        f"{d3[(0.7, False, 7)][1]:.1f} 次；有记忆、重试 7 次 {pct(d3[(0.7, True, 7)][0])}（{d3[(0.7, True, 7)][1]:.1f} 次）。"
        "比较'更复杂的智能体'时，要和同样成本的简单基线比（Kapoor et al.：HumanEval 上逐步提高温度的重试达到了复杂智能体的准确率，"
        "成本低得多）——最好画出成功率–成本曲线，而不是只报告一个成功率。",
        f"差距不大时，小基准上的比较很不可靠（演示 4）：两个智能体的真实差距是 {pct(mB - mA)}（{pct(mA)} vs {pct(mB)}），"
        f"只抽 10 道题、各跑一次时，有 {pct(d4[10], 0)} 的概率排名反了或打平；20 道题 {pct(d4[20], 0)}，100 道题 {pct(d4[100], 0)}。"
        f"同样 {count} 道题，每题跑 {half} 次时差别的 95% 区间是 [{100 * lo:+.1f}, {100 * hi:+.1f}]，只跑 1 次时是 "
        f"[{100 * lo1:+.1f}, {100 * hi1:+.1f}] 个百分点。真实的智能体基准任务更少（τ-bench 零售领域 115 个、OSWorld 369 个）、"
        "每次运行更贵、环境本身也有随机性——可靠的报告至少包括：多次运行的均值和区间、pass^k、成本、和同成本的简单基线的比较。",
    ])
    print(f"\n⏱ 总用时 {time.perf_counter() - t0:.0f} 秒")


if __name__ == "__main__":
    main()
