"""
=============================================================================
前置讲 P2 · 代码 06（可选）：用真实的 Claude 跑同一套调用栈
=============================================================================

对应讲义：lectures/prereq2_llm_as_a_service.md §7

【这个脚本要回答的问题】
    05 号脚本的调用栈（日志 → 记账与预算 → 缓存 → 重试 → 模型）换成真实的模型，一行都不用改吗？
    真实模型和 SimLLM 有哪些不一样？

【任务】真实的 Claude 不可能知道第 1 讲的虚构世界，所以这里把相关国家的"资料卡"放进提示里，
    让模型**根据资料**回答（这也是第 7 讲 RAG 的雏形）。2 个提示模板 × 12 道题 = 24 次调用。

【运行】
    python3 06_real_llm.py --toy     # 不联网：用 SimLLM 跑同一段代码（检查代码能跑通；SimLLM 不读资料卡，靠自己的"记忆"回答）
    python3 06_real_llm.py           # 真实模型：需要 pip install anthropic，以及 ANTHROPIC_API_KEY 或 `ant auth login`
                                     # 会花钱：claude-opus-5-5、effort=low，24 次短调用，通常只要几美分；预算上限 0.50 美元
    第二次运行会命中磁盘缓存（cache/real_llm_cache.jsonl），不再花钱；删掉这个文件就会重新调用。
【依赖】anthropic（可选）；其余只用标准库。
"""

import os
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 llm_api import (CachedLLM, ClaudeLLM, LLMError, LLMRequest, LoggedLLM, MeteredLLM, RealClock,  # noqa: E402
                     RetryingLLM, SimLLM)
from pretrain_world import RELATIONS, question_text  # noqa: E402

HERE = os.path.dirname(os.path.abspath(__file__))
TEMPLATES = {
    "A 直接问": "Use only the fact sheet provided by the user.",
    "B 只答一个词": "Use only the fact sheet provided by the user. Answer with only the answer word.",
}


def fact_sheet(c):
    """一个国家的资料卡（英文，几句话）。"""
    return (f"Fact sheet: {c.name.capitalize()} is a country in the {c.region}. Its capital is {c.capital}. "
            f"People there speak {c.language}. It is known for its {c.export}. It is led by {c.leader}.")


def main():
    toy = "--toy" in sys.argv
    sim = SimLLM()
    world = sim.world
    if toy:
        base, model = sim, sim.name
    else:
        try:
            base = ClaudeLLM(effort="low", sdk_max_retries=0)     # 外面有我们自己的 RetryingLLM，SDK 就不再重试
        except LLMError as e:
            print(f"无法创建 ClaudeLLM：{e}\n可以先用 --toy 运行。")
            return
        model = base.name
    os.makedirs(os.path.join(HERE, "cache"), exist_ok=True)
    suffix = "toy" if toy else "real"
    clock = RealClock()
    retry = RetryingLLM(base, max_retries=4, base=1.0, clock=clock)
    cache = CachedLLM(retry, path=os.path.join(HERE, "cache", f"{suffix}_llm_cache.jsonl"))
    meter = MeteredLLM(cache, budget_usd=0.50)
    llm = LoggedLLM(meter, os.path.join(HERE, "cache", f"{suffix}_llm_log.jsonl"), run_id=suffix, clock=clock)

    print_header(f"用 {'SimLLM（--toy）' if toy else model} 跑 2 个模板 × 12 道题")
    countries = world.countries[:3]
    probes = [(c, rel) for c in countries for rel in RELATIONS]
    rows, examples = [], []
    for t, system in TEMPLATES.items():
        ok = n = 0
        for c, rel in probes:
            q = question_text(c.name, rel).replace(" ?", "?")
            req = LLMRequest(messages=[{"role": "user", "content": f"{fact_sheet(c)}\n\nQuestion: {q}"}],
                             system=system, model=model, max_tokens=200, metadata={"tag": t, "gold": c.attr(rel)})
            r = llm.complete(req)
            hit = c.attr(rel) in r.text.lower().replace(".", " ").split()
            ok, n = ok + hit, n + 1
            if len(examples) < 4 and (rel == "capital"):
                examples.append([t, q, repr(r.text[:60]), r.stop_reason, r.usage.output_tokens])
        tag = meter.by_tag[t]
        rows.append([t, pct_ci(ok, n), tag[1], tag[2], f"{tag[3]:.5f}"])
    print_table(["模板", "答对", "输入 token（合计，不含缓存命中）", "输出 token（合计）", "费用（美元）"], rows)
    print()
    print_table(["模板", "问题", "回答（前 60 个字符）", "stop_reason", "输出 token"], examples, aligns=["<"] * 5)
    print(f"\n  缓存命中 {cache.hits} 次、未命中 {cache.misses} 次；本次花费 {meter.spent:.5f} 美元；重试 {retry.retries} 次")
    print_observations([
        "调用栈的代码和 05 号脚本完全一样，只换了最底层的后端——这就是统一接口的意义：实验代码不依赖某一家的 API。",
        "真实模型的不同之处：它不接受 temperature / seed（ClaudeLLM 会提醒），所以无法像 SimLLM 那样精确复现；"
        "它会先在内部思考，思考的 token 计入输出用量，所以输出 token 往往比你看到的回答多；"
        "它的回答格式由提示决定，'只答一个词'的模板通常真的只写一个词。",
        "第二次运行会全部命中磁盘缓存、不再花钱。日志在 cache/ 目录下，可以用 llm_api.read_log 读回来分析。",
    ])


if __name__ == "__main__":
    main()
