File size: 11,810 Bytes
a72d1cf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
"""LLM deneyi: makaledeki hakem eleştirilerini kapatmak için tek komutluk, tekrarlanabilir protokol.

Çalıştırılan koşullar (aynı TR-TabloSQL soruları, aynı veritabanları, sıcaklık 0, hibrit geri düşme kapalı):
  <etiket>_tr           Türkçe normalizasyon + statik denetim + geri beslemeli döngü (K = 3)      -> ana sonuç
  <etiket>_tr_K1        aynı, döngü kapalı (tek deneme)                                          -> döngünün katkısı
  <etiket>_tr_statiksiz aynı, statik denetim kapalı (yalnızca yürütme/sonuç geri beslemesi)     -> statik denetimin katkısı
  <etiket>_metin        sayılar metin olarak saklanmış veritabanı                               -> LLM metni kurtarabilir mi?
  <etiket>_en           İngilizce yerel ayar varsayımlı veritabanı                              -> geri döndürülemez kayıp
  stres testi           pilot/belgeler (görülmemiş düzenler), cevap düzeyinde değerlendirme

Kullanım (model adını sabit sürüm olarak verin; 'auto' yalnızca --auto-izin ile):
  python scripts/llm_deneyi.py --saglayici claude --model claude-sonnet-4-5-20250929 --kuru     # maliyet tahmini
  python scripts/llm_deneyi.py --saglayici claude --model claude-sonnet-4-5-20250929
  python scripts/llm_deneyi.py --saglayici openai --model gpt-4o-2024-08-06
  python scripts/llm_deneyi.py --saglayici ollama --model qwen2.5-coder:7b                       # yerel, ücretsiz

Anahtarlar .env dosyasından ya da ortam değişkenlerinden okunur (ANTHROPIC_API_KEY, OPENAI_API_KEY).
Çıktılar: outputs/llm/<etiket>/ozet.json, rapor.md, sonuclar_*.jsonl, stres/pilot_sonuclari.json
"""
from __future__ import annotations

import argparse
import json
import re
import subprocess
import sys
import time
import warnings
from math import comb
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
warnings.filterwarnings("ignore")

from t2s.errors import ErrorLog  # noqa: E402
from t2s.evaluate import SystemSpec, evaluate_system, load_benchmark  # noqa: E402
from t2s.llm import USAGE, load_dotenv, make_backend  # noqa: E402
from t2s.pipeline import build_database, build_reference_db  # noqa: E402

DB = ROOT / "outputs" / "db"


def wilson(k, n, z=1.96):
    if not n:
        return None
    p = k / n
    d = 1 + z * z / n
    c = (p + z * z / (2 * n)) / d
    h = z * ((p * (1 - p) / n + z * z / (4 * n * n)) ** 0.5) / d
    return [round(100 * (c - h), 1), round(100 * (c + h), 1)]


def mcnemar(b, c):
    n = b + c
    if n == 0:
        return 1.0
    k = min(b, c)
    return min(1.0, 2 * sum(comb(n, i) for i in range(k + 1)) / 2 ** n)


def load_recs(path: Path) -> dict:
    return {r["id"]: r for r in (json.loads(l) for l in path.read_text(encoding="utf-8").splitlines() if l.strip())}


def compare(a: dict, b: dict):
    ids = sorted(set(a) & set(b))
    bb = sum(1 for i in ids if a[i]["dogru"] and not b[i]["dogru"])
    cc = sum(1 for i in ids if b[i]["dogru"] and not a[i]["dogru"])
    return bb, cc, mcnemar(bb, cc)


def dry_run(items, n_systems):
    from t2s.llm import SYSTEM_PROMPT, build_user_prompt
    from t2s.schema import connect, describe_schema
    con = connect(DB / "sistem_tr.db", readonly=True)
    schema = describe_schema(con)
    chars = sum(len(SYSTEM_PROMPT) + len(build_user_prompt(it["soru"], schema, it.get("kanit"))) for it in items)
    con.close()
    tok_in = chars / 3.2 * 1.35 * n_systems   # ~3,2 karakter/token (Türkçe), ortalama 1,35 deneme
    tok_out = len(items) * 220 * 1.35 * n_systems
    return int(tok_in), int(tok_out)


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--saglayici", required=True, choices=["claude", "openai", "ollama"])
    ap.add_argument("--model", required=True, help="sabit model sürümü (ör. claude-sonnet-4-5-20250929)")
    ap.add_argument("--anahtar", default=None)
    ap.add_argument("--taban-url", default=None)
    ap.add_argument("--etiket", default=None, help="çıktı klasörü adı (varsayılan: sağlayıcı_model)")
    ap.add_argument("--yalniz", choices=["hepsi", "ana", "stres"], default="hepsi")
    ap.add_argument("--kuru", action="store_true", help="çağrı yapmadan token/maliyet tahmini")
    ap.add_argument("--auto-izin", action="store_true", help="model adı 'auto' olabilir (önerilmez)")
    args = ap.parse_args()
    load_dotenv(ROOT / ".env")

    if args.model == "auto" and not args.auto_izin:
        raise SystemExit("Tekrarlanabilirlik için sabit bir model sürümü verin (ör. claude-sonnet-4-5-20250929).")
    etiket = args.etiket or re.sub(r"[^A-Za-z0-9._-]+", "_", f"{args.saglayici}_{args.model}")
    out = ROOT / "outputs" / "llm" / etiket

    items = load_benchmark()
    for m in ("tr", "en", "text"):
        if not (DB / f"sistem_{m}.db").exists():
            build_database(DB / f"sistem_{m}.db", mode=m)
    if not (DB / "referans.db").exists():
        build_reference_db(DB / "referans.db")
    db_paths = {m: DB / f"sistem_{m}.db" for m in ("tr", "en", "text")}

    p = args.saglayici
    specs_def = [(f"{p}_tr", "tr", True, True), (f"{p}_tr_K1", "tr", False, True),
                 (f"{p}_tr_statiksiz", "tr", True, False), (f"{p}_metin", "text", True, True),
                 (f"{p}_en", "en", True, True)]

    if args.kuru:
        ti, to = dry_run(items, len(specs_def))
        pilot_q = len(json.loads((ROOT / "pilot" / "sorular.json").read_text(encoding="utf-8"))["sorular"])
        print(f"Tahmini çağrı sayısı : ~{int(len(items) * 1.35 * len(specs_def)) + int(pilot_q * 1.35)}")
        print(f"Tahmini giriş tokenı : ~{ti:,}  (+ stres testi ~{int(ti / len(specs_def) * pilot_q / len(items)):,})")
        print(f"Tahmini çıkış tokenı : ~{to:,}")
        print("Ek cevap düzeyinde kümeler: stres kümesi (307 soru) ve ağır küme hızlı alt kümesi (200 soru) için")
        print("  yaklaşık 2,5 milyon giriş tokenı daha (tablo seçimi ve düzeltme denemeleriyle değişir).")
        print("Maliyet = giriş × (sağlayıcının giriş fiyatı) + çıkış × (çıkış fiyatı); Ollama ücretsizdir.")
        return

    out.mkdir(parents=True, exist_ok=True)
    # anahtar/adres verilmezse uygulamanın ayar dosyasından (Model bağlantıları) ya da .env'den okunur
    from t2s.settings import Settings
    cfg = Settings()
    key = args.anahtar or cfg.key(p) or None
    base = args.taban_url or cfg.base(p)
    backend = make_backend(p, args.model, key, base,
                           device=cfg.conn("ollama").get("cihaz") if p == "ollama" else None)
    print("Bağlantı denemesi…", end=" ", flush=True)
    backend.chat("Kısa yanıt ver.", [{"role": "user", "content": "ping"}], max_tokens=5)
    print("tamam")

    meta = {"saglayici": p, "model": args.model, "sicaklik": 0.0, "K": 3, "hibrit_geri_dusme": False,
            "baslangic": time.strftime("%Y-%m-%dT%H:%M:%S"), "kosullar": {}}
    if args.yalniz in ("hepsi", "ana"):
        elog = ErrorLog(out / "hata_gunlugu.jsonl")
        for name, mode, sc, st in specs_def:
            t0 = time.time()
            spec = SystemSpec(name, backend, mode, self_correct=sc, static_check=st)
            s = evaluate_system(spec, items, db_paths, DB / "referans.db", out, elog,
                                progress=lambda k, n, r: print(f"\r  {name}: {k}/{n}", end="", flush=True))
            n = s["n"]
            k = round(s["EX"] * n / 100)
            s["EX_ga95"] = wilson(k, n)
            s["sure_dk"] = round((time.time() - t0) / 60, 1)
            meta["kosullar"][name] = s
            print(f"\r  {name:24} EX=%{s['EX']}  test=%{s['bolum'].get('test')}  ilk=%{s['EX_ilk_deneme']}  "
                  f"red=%{s['cevapsiz_dogru_red']}  yanlış red=%{s['yanlis_red_orani']}  ({s['sure_dk']} dk)")

        rule = ROOT / "outputs" / "sonuclar" / "sonuclar_kural_tr.jsonl"
        R = {name: load_recs(out / f"sonuclar_{name}.jsonl") for name, *_ in specs_def}
        pairs = [(f"{p}_tr", f"{p}_tr_K1", "geri beslemeli döngünün katkısı"),
                 (f"{p}_tr", f"{p}_tr_statiksiz", "statik denetimin katkısı"),
                 (f"{p}_tr", f"{p}_metin", "Türkçe normalizasyonun katkısı (LLM, metin kipine karşı)"),
                 (f"{p}_tr", f"{p}_en", "Türkçe normalizasyonun katkısı (LLM, en kipine karşı)")]
        if rule.exists():
            R["kural_tr"] = load_recs(rule)
            pairs.append((f"{p}_tr", "kural_tr", "LLM ve kural tabanlı üretici"))
        meta["mcnemar"] = [{"a": a, "b": b, "anlam": t, **dict(zip(("a_dogru_b_yanlis", "b_dogru_a_yanlis", "p"),
                                                                    compare(R[a], R[b])))} for a, b, t in pairs]

    if args.yalniz in ("hepsi", "stres"):
        flag = {"claude": "--claude", "openai": "--openai", "ollama": "--ollama"}[p]
        extra = (["--anahtar", args.anahtar] if args.anahtar else []) + \
                (["--taban-url", args.taban_url] if args.taban_url else [])
        for kume, ad in (("pilot", "stres"), ("stres", "stres_kumesi"), ("agir_hizli", "agir_hizli")):
            cmd = [sys.executable, str(ROOT / "pilot" / "pilot_calistir.py"), "--kume", kume, flag, args.model,
                   "--cikti", str(out / ad)] + extra
            print(f"Cevap düzeyinde küme: {kume}…")
            subprocess.run(cmd, check=True)
            meta[ad] = json.loads((out / ad / "sonuclar.json").read_text(encoding="utf-8"))["ozet"]
            meta[ad].pop("cikarim", None)

    meta["kullanim"] = {"cagri": USAGE.calls, "hata": USAGE.errors, "giris_token": USAGE.input_tokens,
                        "cikis_token": USAGE.output_tokens}
    meta["bitis"] = time.strftime("%Y-%m-%dT%H:%M:%S")
    (out / "ozet.json").write_text(json.dumps(meta, ensure_ascii=False, indent=1, default=str), encoding="utf-8")

    L = [f"# LLM deneyi: {p} / {args.model}", "", f"Sıcaklık 0 · K = 3 · hibrit geri düşme kapalı · {meta['baslangic']}", ""]
    if meta["kosullar"]:
        L += ["| Koşul | EX (%95 GA) | Ayrılmış test | İlk deneme | Cevapsızda red | Yanlış red | Ort. deneme |",
              "|---|---|---|---|---|---|---|"]
        for n_, s in meta["kosullar"].items():
            L.append(f"| {n_} | {s['EX']} ({s['EX_ga95'][0]}–{s['EX_ga95'][1]}) | {s['bolum'].get('test')} | "
                     f"{s['EX_ilk_deneme']} | {s['cevapsiz_dogru_red']} | {s['yanlis_red_orani']} | {s['ort_deneme']} |")
        L += ["", "| Karşılaştırma | A doğru B yanlış | B doğru A yanlış | McNemar p |", "|---|---|---|---|"]
        for m in meta["mcnemar"]:
            L.append(f"| {m['a']} – {m['b']} ({m['anlam']}) | {m['a_dogru_b_yanlis']} | {m['b_dogru_a_yanlis']} | "
                     f"{m['p']:.3g} |")
    for ad, baslik in (("stres", "Pilot (3 belge)"), ("stres_kumesi", "Stres kümesi (33 belge)"),
                       ("agir_hizli", "Ağır küme, hızlı alt küme (tek veritabanı)")):
        if ad in meta:
            st = meta[ad]
            lv = st.get("seviyeye_gore") or {}
            lvs = " · ".join(f"{k} %{v['basari']}" for k, v in sorted(lv.items()))
            L += ["", f"**{baslik}:** genel %{st['basari']}, cevaplanabilir %{st['cevaplanabilir_basari']}, "
                      f"cevapsızda red %{st['cevapsiz_dogru_red']}, yanlış red %{st['yanlis_red']}, "
                      f"sessiz yanlış %{st.get('sessiz_yanlis')}" + (f" ({lvs})" if lvs else "")]
    u = meta["kullanim"]
    L += ["", f"Kullanım: {u['cagri']} çağrı, {u['giris_token']:,} giriş / {u['cikis_token']:,} çıkış tokenı, "
              f"{u['hata']} hata."]
    (out / "rapor.md").write_text("\n".join(L) + "\n", encoding="utf-8")
    print("\n".join(L))
    print(f"\nÇıktılar: {out}")


if __name__ == "__main__":
    main()