"""
=============================================================================
第 2 讲 · 代码 07（可选）：在真实的 Claude 上测模板、示例顺序与翻转标签
=============================================================================

对应讲义：lectures/lecture02_in_context_learning.md §9

【这个脚本要回答的问题】
    01–05 号脚本在迷你模型和模拟器上看到的现象，在一个真实的大模型上是什么样子？
    3 个英文模板 × 5 种条件（零样本；4 个示例 × 标签正确 / 全部翻转 × 最后一个示例是好评 / 差评）× 16 条评论。

    - 模板：Review/Sentiment、Input/Label、"How was the experience?"——同一个任务的三种写法；
    - 顺序：同样 4 个示例，只把最后一个换成好评或差评——测近因偏差；
    - 翻转标签：示例里好评都标 negative、差评都标 positive，而指令仍然说"分成 positive 或 negative"——
      模型听指令和标签词的语义，还是跟着示例走？（Wei et al. 2023 的设定；指令和示例互相矛盾）

【运行】
    python3 07_real_llm.py --toy     # 不联网：用 SimICL-large 跑同一段代码（检查代码能跑通）
    python3 07_real_llm.py           # 真实模型：需要 pip install anthropic，以及 ANTHROPIC_API_KEY 或 `ant auth login`
                                     # 会花钱：默认 claude-opus-5-5、effort=low，240 次短调用；预算上限 1.00 美元
    python3 07_real_llm.py --model claude-haiku-5-5    # 换一个更便宜的模型
    第二次运行会命中磁盘缓存（cache/ 目录），不再花钱。
【注意】真实模型不返回标签词的概率（P2 §3.6），这里只能看它**写了**哪个词，所以不做校准实验；
    它也不接受 temperature，同样的请求两次结果可能不同（缓存会把第一次的结果固定下来）。
【依赖】anthropic（可选）；import 同目录的 icl_lab.py，前置讲 P2 的 llm_api.py，第 1 讲的 console.py。
"""

import os
import random
import sys

sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "lecture01"))
from console import pct_ci, print_header, print_observations, print_table  # noqa: E402
from icl_lab import (ENGLISH_TEMPLATES, SimICL, demo_pool, flip, render_english, sample_demos,  # noqa: E402
                     test_set)
from llm_api import (CachedLLM, ClaudeLLM, LLMError, LLMRequest, LoggedLLM, MeteredLLM, RealClock,  # noqa: E402
                     RetryingLLM)

HERE = os.path.dirname(os.path.abspath(__file__))
N_ITEMS = 16
SYSTEM = "Reply with exactly one word: the label."


def pick_items(test, n=N_ITEMS, seed=0):
    """从测试集里挑 n 条：好评、差评各一半，三种难度都有。"""
    rng = random.Random(seed)
    pos = [t for t in test if t[1] == "positive"]
    neg = [t for t in test if t[1] == "negative"]
    return rng.sample(pos, n // 2) + rng.sample(neg, n - n // 2)


def parse_label(text):
    """模型回答 → 'positive' / 'negative' / 'other'（取第一个词，去掉标点、转小写）。"""
    w = text.strip().split()[0].strip(".,!:;\"'*").lower() if text.strip() else ""
    return w if w in ("positive", "negative") else "other"


def conditions(pool):
    """
    5 种条件：零样本 + 4 个示例（同一组）的 2 × 2 种变化。

    【返回】{名字: 示例列表}。"最后是好评"用顺序 N P N P，"最后是差评"用 P N P N——两者是同一组 4 条评论。
    """
    rng = random.Random(42)
    p = rng.sample(pool.pos, 2)
    n = rng.sample(pool.neg, 2)
    last_p = [n[0], p[0], n[1], p[1]]
    last_n = [p[0], n[0], p[1], n[1]]
    return {
        "零样本": [],
        "正确标签 · 最后是好评": last_p,
        "正确标签 · 最后是差评": last_n,
        "翻转标签 · 最后是好评": [(x, flip(y), k) for x, y, k in last_p],
        "翻转标签 · 最后是差评": [(x, flip(y), k) for x, y, k in last_n],
    }


def main():
    toy = "--toy" in sys.argv
    model = sys.argv[sys.argv.index("--model") + 1] if "--model" in sys.argv else "claude-opus-5-5"
    if toy:
        base = SimICL("large")
        model = base.name
    else:
        try:
            base = ClaudeLLM(model=model, effort="low", sdk_max_retries=0)
        except LLMError as e:
            print(f"无法创建 ClaudeLLM：{e}\n可以先用 --toy 运行。")
            return
    os.makedirs(os.path.join(HERE, "cache"), exist_ok=True)
    suffix = "toy" if toy else model
    clock = RealClock()
    retry = RetryingLLM(base, max_retries=4, base=1.0, clock=clock)
    cache = CachedLLM(retry, path=os.path.join(HERE, "cache", f"icl_{suffix}_cache.jsonl"))
    meter = MeteredLLM(cache, budget_usd=1.00)
    llm = LoggedLLM(meter, os.path.join(HERE, "cache", f"icl_{suffix}_log.jsonl"), run_id=suffix, clock=clock)

    items = pick_items(test_set())
    conds = conditions(demo_pool())
    print_header(f"{'SimICL-large（--toy）' if toy else model}：3 个模板 × 5 种条件 × {len(items)} 条评论")
    example = render_english("review", ("positive", "negative"), conds["翻转标签 · 最后是差评"], items[0][0])
    print("  一个提示的样子（翻转标签 · 最后是差评）：\n    " + example.replace("\n", "\n    "))
    rows, acc = [], {}
    for tname in ENGLISH_TEMPLATES:
        for cname, demos in conds.items():
            ok = other = pos = 0
            for x, y, _ in items:
                prompt = render_english(tname, ("positive", "negative"), demos, x)
                req = LLMRequest(messages=[{"role": "user", "content": prompt}], system=SYSTEM, model=model,
                                 max_tokens=400, metadata={"tag": f"{tname}/{cname}", "gold": y})
                try:
                    r = llm.complete(req)
                except LLMError as e:
                    print(f"  调用失败（{type(e).__name__}）：{e}")
                    return
                lab = parse_label(r.text)
                ok += lab == y
                other += lab == "other"
                pos += lab == "positive"
            acc[(tname, cname)] = ok / len(items)
            rows.append([tname, cname, pct_ci(ok, len(items)), f"{pos}/{len(items)}", other])
    print_table(["模板", "条件", "准确率（相对真实标签）[95% CI]", "答 positive 的条数", "答了别的词"], rows)
    print(f"\n  缓存命中 {cache.hits} 次、未命中 {cache.misses} 次；本次花费 {meter.spent:.4f} 美元；重试 {retry.retries} 次")
    flipped = [acc[(t, c)] for t in ENGLISH_TEMPLATES for c in conds if c.startswith("翻转")]
    gold = [acc[(t, c)] for t in ENGLISH_TEMPLATES for c in conds if c.startswith("正确")]
    print_observations([
        f"标签正确时 6 种（模板 × 顺序）的平均准确率 {100 * sum(gold) / len(gold):.1f}%，翻转标签时 {100 * sum(flipped) / len(flipped):.1f}%。"
        "翻转后准确率接近 0，说明模型跟着示例'反着答'；接近 100%，说明它听的是指令和标签词的意思。",
        "比较'最后是好评'和'最后是差评'两行：差别大说明有近因偏差；在只有 16 条评论时，几条的差别还在抽样误差之内"
        "（看区间是否重叠，或者用 stats_kit.mcnemar_exact 做配对检验）。",
        "每次运行的结论只对这个模型、这天的版本、这几个模板成立。换模型（--model）重跑一遍，常常能看到不同的图景——"
        "这正是本讲的主题：提示的效果是'提示 × 模型'的属性。",
        "--toy 模式下的数字来自 SimICL 的旋钮，不代表任何真实模型。",
    ])


if __name__ == "__main__":
    main()
