"""업무 3종별 grok reasoning effort 실측 (2026-08-27, #92 2단계).

Codex 판정: 「한 종류 probe 로 15자리 공통값을 정하면 안 된다. 최소 세
부류를 따로 재라 — (a) binary defect/gate (b) multi-image ranking
(c) exact-ID/geometry. 각 부류에 **입력에서만 알 수 있는** 양성·음성/정답쌍을
두고 minimal vs low 를 각 2회 이상. 「이미지가 보이나」 같은 자가신고 물음은
금지.」

## 정답을 어떻게 아는가 — 지어내지 않는다

세 부류 다 **코드로 만든 그림**을 쓴다. 원본 스틸 한 장을 가져다

    gate      한 장은 그대로, 한 장은 **좌우 반씩 다른 그림을 붙여** 이음매를
              만든다 → 「이 그림에 이어붙인 자국이 있나」의 답을 우리가 안다
    ranking   서로 다른 세 장에 index 라벨을 붙여 보낸다 → 「어느 index 에
              사람이 가장 많나」의 답은 **각 장을 따로 물어** 얻은 값이다
    geometry  한 장 위 정해진 자리에 **한 가지 색 사각형**을 그린다 →
              그 사각형의 위치를 우리가 안다

★**코드로 그리는 것은 「시각 요소를 코드가 만든다」와 다르다.** 여기서
 그리는 것은 프로덕션 그림이 아니라 **자를 재는 자**다 — 정답을 아는
 입력이 없으면 「0」과 「꺼진 장치」를 못 가른다.

    .venv/bin/python tools/prompt_measure/probe_grok_effort_by_task.py
"""
from __future__ import annotations

import base64
import io
import json
import pathlib
import sys

sys.path.insert(0, "/Users/manta/Documents/Projects/TheRoad-I1/scratchpad")
import _opik_env  # noqa: F401,E402

ROOT = pathlib.Path("/Users/manta/Documents/Projects/TheRoad-I1")
OUT = ROOT / "artifact/20260827_grok_vlm_probe"
EFFORTS = (None, "low", "minimal")
REPS = 2


def _png(img) -> bytes:
    buf = io.BytesIO()
    img.save(buf, format="PNG")
    return buf.getvalue()


def _part(raw: bytes) -> dict:
    return {"type": "image_url", "image_url": {
        "url": "data:image/png;base64," + base64.b64encode(raw).decode()}}


def _make_inputs():
    """정답을 아는 입력 세 벌."""
    from PIL import Image

    srcs = sorted(ROOT.glob("projects/*/images/*/scene/**/*.png"))
    a = Image.open(srcs[0]).convert("RGB")
    b = Image.open(srcs[len(srcs) // 2]).convert("RGB").resize(a.size)
    c = Image.open(srcs[len(srcs) // 3]).convert("RGB").resize(a.size)

    # (a) gate — 이음매가 **있는** 것과 **없는** 것
    seam = Image.new("RGB", a.size)
    seam.paste(a.crop((0, 0, a.width // 2, a.height)), (0, 0))
    seam.paste(b.crop((a.width // 2, 0, a.width, a.height)), (a.width // 2, 0))

    # (c) geometry — 사각형을 **우리가 정한 자리**에
    from PIL import ImageDraw

    box = a.copy()
    W, H = box.size
    rect = (int(W * 0.10), int(H * 0.60), int(W * 0.30), int(H * 0.85))
    ImageDraw.Draw(box).rectangle(rect, fill=(255, 0, 255))

    return {
        "gate_clean": _png(a), "gate_seam": _png(seam),
        "rank": [_png(a), _png(b), _png(c)],
        "geom": _png(box),
        "geom_answer": "bottom-left",   # 우리가 그린 자리
    }


TASKS = {}

TASKS["gate"] = {
    "schema": {"type": "object", "additionalProperties": False,
               "properties": {"has_seam": {"type": "boolean"},
                              "why_en": {"type": "string"}},
               "required": ["has_seam", "why_en"]},
    "sys": ("You judge one image. Say whether it is a single continuous"
            " photograph or two different photographs joined along a vertical"
            " line. Answer from the image only."),
}
TASKS["rank"] = {
    "schema": {"type": "object", "additionalProperties": False,
               "properties": {"most_people_index": {"type": "integer"},
                              "counts": {"type": "string"}},
               "required": ["most_people_index", "counts"]},
    "sys": ("You receive several images, each preceded by a label naming its"
            " index. Report which index shows the most people, and list the"
            " count you see for each index."),
}
TASKS["geom"] = {
    "schema": {"type": "object", "additionalProperties": False,
               "properties": {
                   "quadrant": {"type": "string", "enum": [
                       "top-left", "top-right", "bottom-left",
                       "bottom-right", "none"]},
                   "color_en": {"type": "string"}},
               "required": ["quadrant", "color_en"]},
    "sys": ("A solid rectangle of one flat colour has been placed on this"
            " photograph. Report which quadrant of the frame it sits in and"
            " what colour it is. If there is no such rectangle, answer"
            " quadrant=none."),
}


def main() -> None:
    import app.modules.llm.llm_client as lc
    from app.modules.llm.llm_client import call_structured

    ins = _make_inputs()
    real = lc._completion
    cur = {"effort": None}

    def _with_effort(binding, model, kwargs):
        if cur["effort"]:
            kwargs["reasoning_effort"] = cur["effort"]
        return real(binding, model, kwargs)

    lc._completion = _with_effort

    arms = [
        ("gate", "양성(이음매 있음)", [{"type": "text", "text": "Judge it."},
                                _part(ins["gate_seam"])], True),
        ("gate", "음성(원본)", [{"type": "text", "text": "Judge it."},
                             _part(ins["gate_clean"])], False),
        ("rank", "세 장", [{"type": "text", "text": "Which index?"}]
         + [p for i, raw in enumerate(ins["rank"])
            for p in ({"type": "text", "text": f"IMAGE index={i}:"},
                      _part(raw))], None),
        ("geom", "양성(사각형)", [{"type": "text", "text": "Where is it?"},
                              _part(ins["geom"])], ins["geom_answer"]),
        ("geom", "음성(없음)", [{"type": "text", "text": "Where is it?"},
                             _part(ins["gate_clean"])], "none"),
    ]

    rows = []
    for effort in EFFORTS:
        cur["effort"] = effort
        lab = effort or "기본"
        print(f"\n■ effort = {lab}")
        for task, arm, parts, want in arms:
            for rep in range(REPS):
                tag = f"eff2_{effort or 'def'}_{task}_{rep}"
                sink: dict = {}
                try:
                    got = call_structured(
                        tag, TASKS[task]["sys"], parts,
                        TASKS[task]["schema"],
                        project_config={tag: {"model": "grok"}},
                        schema_name="probe", enable_fallback=False,
                        usage_sink=sink)
                    err = None
                except Exception as e:
                    got, err = {}, f"{type(e).__name__}: {e}"[:100]
                rows.append({"effort": lab, "task": task, "arm": arm,
                             "rep": rep, "want": want, "got": got,
                             "error": err, "usage": sink})
                if err:
                    print(f"   [{task:5s} {arm:14s} #{rep}] ★{err}")
                else:
                    key = ("has_seam" if task == "gate"
                           else "most_people_index" if task == "rank"
                           else "quadrant")
                    hit = ("—" if want is None
                           else "○" if got.get(key) == want else "✕")
                    print(f"   [{task:5s} {arm:14s} #{rep}] {hit} "
                          f"{key}={got.get(key)}  "
                          f"완성={sink.get('completion_tokens')} "
                          f"${sink.get('estimated_cost_usd')}")

    OUT.mkdir(parents=True, exist_ok=True)
    (OUT / "effort_by_task.json").write_text(
        json.dumps(rows, ensure_ascii=False, indent=2), encoding="utf-8")

    print("\n■ 요약 — 정답을 맞힌 비율과 값")
    for effort in EFFORTS:
        lab = effort or "기본"
        sub = [r for r in rows if r["effort"] == lab and r["want"] is not None]
        ok = 0
        for r in sub:
            key = ("has_seam" if r["task"] == "gate"
                   else "quadrant")
            if not r["error"] and r["got"].get(key) == r["want"]:
                ok += 1
        cost = sum(float(r["usage"].get("estimated_cost_usd") or 0)
                   for r in rows if r["effort"] == lab)
        print(f"   {lab:8s} 정답 {ok}/{len(sub)}   비용 ${cost:.4f}")
    print(f"\n  기록 → {OUT / 'effort_by_task.json'}")
    print("★부류마다 답이 다르면 **operation 별로** effort 를 고른다 —")
    print(" 한 값으로 15자리를 덮지 않는다.")


if __name__ == "__main__":
    main()
