File size: 16,824 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
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
"""Karşılaştırmalı başarım raporu (tek dosya HTML, dış bağımlılık yok)."""
from __future__ import annotations

import html
import json
from datetime import date
from pathlib import Path

from .errors import CATALOG

ROOT = Path(__file__).resolve().parent.parent

CSS = """
:root{--bg:#f7f7f5;--surface:#fcfcfb;--ink:#0b0b0b;--ink2:#52514e;--muted:#8a8984;--line:#e4e3de;
--s1:#2a78d6;--s2:#eb6834;--track:#ecebe6;--good:#1c7c4a;--bad:#b42318}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){--bg:#121211;--surface:#1a1a19;--ink:#fff;
--ink2:#c3c2b7;--muted:#8f8e86;--line:#2e2e2b;--s1:#3987e5;--s2:#d95926;--track:#2a2a27;--good:#4cc38a;--bad:#f97066}}
:root[data-theme="dark"]{--bg:#121211;--surface:#1a1a19;--ink:#fff;--ink2:#c3c2b7;--muted:#8f8e86;--line:#2e2e2b;
--s1:#3987e5;--s2:#d95926;--track:#2a2a27;--good:#4cc38a;--bad:#f97066}
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif}
main{max-width:1060px;margin:0 auto;padding:32px 16px 64px}h1{font-size:28px;margin:0 0 4px;letter-spacing:-.01em}
h2{font-size:19px;margin:40px 0 10px;padding-top:8px;border-top:1px solid var(--line)}h3{font-size:15px;margin:20px 0 8px}
.sub{color:var(--ink2);margin:0 0 24px}.tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px}
.tile{background:var(--surface);border:1px solid var(--line);border-radius:10px;padding:14px 16px}
.tile .k{font-size:12px;color:var(--ink2);text-transform:uppercase;letter-spacing:.04em}.tile .v{font-size:30px;font-weight:650;margin-top:2px}
.tile .n{font-size:13px;color:var(--muted)}
table{border-collapse:collapse;width:100%;background:var(--surface);border:1px solid var(--line);border-radius:10px;overflow:hidden;font-size:13.5px}
th,td{padding:7px 10px;border-bottom:1px solid var(--line);text-align:left;vertical-align:top}th{color:var(--ink2);font-weight:600;background:var(--bg)}
td.num{text-align:right;font-variant-numeric:tabular-nums}.wrap{overflow-x:auto}
.bars{background:var(--surface);border:1px solid var(--line);border-radius:10px;padding:12px 14px}
.bar{display:grid;grid-template-columns:minmax(120px,240px) 1fr 56px;gap:10px;align-items:center;margin:5px 0;font-size:13.5px}
.track{background:var(--track);height:14px;border-radius:4px;position:relative}
.fill{height:14px;border-radius:0 4px 4px 0;background:var(--s1)}.fill.alt{background:var(--s2)}
.bar .val{text-align:right;font-variant-numeric:tabular-nums;color:var(--ink)}
.note{color:var(--ink2);font-size:13.5px}.ok{color:var(--good);font-weight:600}.no{color:var(--bad);font-weight:600}
code{font:12.5px ui-monospace,Menlo,monospace;background:var(--track);padding:1px 4px;border-radius:4px;word-break:break-word}
.legend{display:flex;gap:16px;font-size:13px;color:var(--ink2);margin:4px 0 8px}.sw{display:inline-block;width:10px;height:10px;border-radius:2px;margin-right:6px;vertical-align:middle}
@media (max-width:600px){.bar{grid-template-columns:110px 1fr 48px}}
"""


def e(x) -> str:
    return html.escape(str(x))


def pct(x) -> str:
    return "—" if x is None else f"%{str(round(x, 1)).replace('.', ',')}"


def bars(rows: list[tuple[str, float | None]], alt_rows: list[float | None] | None = None,
         labels: tuple[str, str] | None = None) -> str:
    out = ['<div class="bars">']
    if labels:
        out.append(f'<div class="legend"><span><i class="sw" style="background:var(--s1)"></i>{e(labels[0])}</span>'
                   f'<span><i class="sw" style="background:var(--s2)"></i>{e(labels[1])}</span></div>')
    for i, (name, v) in enumerate(rows):
        w = 0 if v is None else max(0.0, min(100.0, v))
        out.append(f'<div class="bar" title="{e(name)}: {pct(v)}"><span>{e(name)}</span>'
                   f'<div class="track"><div class="fill" style="width:{w}%"></div></div><span class="val">{pct(v)}</span></div>')
        if alt_rows is not None:
            a = alt_rows[i]
            wa = 0 if a is None else max(0.0, min(100.0, a))
            out.append(f'<div class="bar" title="{e(name)} ({e(labels[1] if labels else "")}): {pct(a)}"><span></span>'
                       f'<div class="track"><div class="fill alt" style="width:{wa}%"></div></div><span class="val">{pct(a)}</span></div>')
    out.append("</div>")
    return "\n".join(out)


SYS_LABEL = {
    "kural_tr": "Kural tabanlı · TR normalizasyon · kendini düzeltme",
    "kural_tr_duzeltmesiz": "Kural tabanlı · TR normalizasyon · tek deneme",
    "kural_en_naif": "Kural tabanlı · naif (İngilizce yerel ayar)",
    "kural_metin_naif": "Kural tabanlı · naif (sayılar metin)",
}


def sys_label(name: str) -> str:
    if name in SYS_LABEL:
        return SYS_LABEL[name]
    for k, v in (("_tr_duzeltmesiz", " · TR · tek deneme"), ("_tr", " · TR · kendini düzeltme"),
                 ("_en_naif", " · naif (İngilizce yerel ayar)"), ("_metin_naif", " · naif (sayılar metin)")):
        if name.endswith(k):
            return name[: -len(k)].capitalize() + v
    return name


def _load_records(name: str) -> list[dict]:
    p = ROOT / "outputs" / "sonuclar" / f"sonuclar_{name}.jsonl"
    if not p.exists():
        return []
    return [json.loads(l) for l in p.read_text(encoding="utf-8").splitlines() if l.strip()]


def write_report(ozet: dict, path: Path) -> Path:
    S = ozet["sistemler"]
    by = {s["sistem"]: s for s in S}
    main_s = by.get("kural_tr") or S[0]
    llm_main = next((s for s in S if s["sistem"].endswith("_tr") and not s["sistem"].startswith("kural")), None)
    naive = {r["kip"]: r for r in ozet["sayi_ablasyonu"]}
    P = []
    P.append(f"<h1>TR-TabloSQL Başarım Raporu</h1><p class='sub'>Dokümanlardaki tabloların yapılandırılmış bilgi tabanına "
             f"dönüştürülmesi ve Türkçe doğal dille sorgulanması · {date.today().strftime('%d.%m.%Y')} · "
             f"{ozet['soru_sayisi']} soru</p>")

    # ---- özet kutuları
    tiles = [("En iyi EX (TR kipi)", pct(max(s["EX"] for s in S if s["sayi_kipi"] == "tr")),
              "tüm sorular, kendini düzeltme açık"),
             ("Ayrılmış test EX", pct(main_s["bolum"].get("test")), f"{sys_label(main_s['sistem']).split(' ·')[0]}, "
              "geliştirmede kullanılmayan sorular")]
    if "en" in naive:
        tiles.append(("Sayı biçimi kaybı", pct(100 - naive["en"]["EX"]),
                      "doğru SQL bile naif ayrıştırmada yanlış cevap veriyor"))
    tiles.append(("Cevapsız sorularda doğru red", pct(main_s["cevapsiz_dogru_red"]), "uydurma yerine açık 'cevap yok'"))
    P.append("<div class='tiles'>" + "".join(
        f"<div class='tile'><div class='k'>{e(k)}</div><div class='v'>{v}</div><div class='n'>{e(n)}</div></div>"
        for k, v, n in tiles) + "</div>")
    if llm_main is None:
        P.append("<p class='note'>Not: Bu çalıştırmada LLM arka ucu kullanılmadı (API anahtarı yoktu). "
                 "<code>python scripts/run_all.py --claude</code>, <code>--openai</code> veya <code>--ollama</code> "
                 "ile LLM sistemleri aynı tabloya eklenir.</p>")

    # ---- sistem karşılaştırması
    P.append("<h2>1. Sistem karşılaştırması (yürütme doğruluğu)</h2>")
    P.append("<div class='wrap'><table><tr><th>Sistem</th><th>Sayı kipi</th><th class='num'>EX</th>"
             "<th class='num'>Geliştirme</th><th class='num'>Ayrılmış test</th><th class='num'>İlk deneme EX</th>"
             "<th class='num'>Cevapsız → doğru red</th><th class='num'>Yanlış red</th><th class='num'>Ort. deneme</th>"
             "<th class='num'>Ort. süre</th></tr>")
    for s in S:
        P.append(f"<tr><td>{e(sys_label(s['sistem']))}</td><td>{e(s['sayi_kipi'])}</td><td class='num'><b>{pct(s['EX'])}</b></td>"
                 f"<td class='num'>{pct(s['bolum'].get('gelistirme'))}</td><td class='num'>{pct(s['bolum'].get('test'))}</td>"
                 f"<td class='num'>{pct(s['EX_ilk_deneme'])}</td><td class='num'>{pct(s['cevapsiz_dogru_red'])}</td>"
                 f"<td class='num'>{pct(s['yanlis_red_orani'])}</td><td class='num'>{str(s['ort_deneme']).replace('.', ',')}</td>"
                 f"<td class='num'>{str(s['ort_sure_sn']).replace('.', ',')} sn</td></tr>")
    P.append("</table></div>")
    P.append(bars([(sys_label(s["sistem"]), s["EX"]) for s in S]))

    # ---- sayı biçimi
    P.append("<h2>2. Türkçe sayı biçiminin etkisi ('1.234,56')</h2>")
    P.append("<p class='note'>Aynı belgeler üç farklı sayı ayrıştırma kipiyle veritabanına aktarıldı. "
             "<b>tr</b>: doğru Türkçe yorum. <b>en</b>: yaygın naif yaklaşım — nokta ondalık, virgül binlik sanılır "
             "(<code>'1.346'</code> → 1,346; <code>'4,5'</code> → 45; <code>'258.107.640,00'</code> ayrıştırılamaz → sütun metne düşer). "
             "<b>text</b>: hiç ayrıştırma yok; SQLite <code>SUM('1.234,56')</code> değerini 1,234 olarak toplar. "
             "Altın SQL sorguları bu veritabanlarında çalıştırılınca, sorgu doğru olsa bile kaç cevabın bozulduğu ölçüldü.</p>")
    P.append("<div class='wrap'><table><tr><th>Kip</th><th class='num'>Altın SQL EX</th><th class='num'>Bozulan cevap</th>"
             "<th class='num'>Sayısal sütun</th><th class='num'>Metin sütun</th><th class='num'>Ayrıştırılamayan hücre</th></tr>")
    st = {r["kip"]: r for r in ozet.get("sayi_istatistik", [])}
    for k, r in naive.items():
        s2 = st.get(k, {})
        P.append(f"<tr><td>{e(k)}</td><td class='num'>{pct(r['EX'])}</td><td class='num'>{len(r['bozulan'])} / {r['n']}</td>"
                 f"<td class='num'>{s2.get('sayisal_sutun', '—')}</td><td class='num'>{s2.get('metin_sutun', '—')}</td>"
                 f"<td class='num'>{s2.get('ayristirilamayan_hucre', '—')}</td></tr>")
    P.append("</table></div>")
    kip_sys = [(f"{sys_label(s['sistem'])}", s["EX"]) for s in S if s["sistem"].startswith("kural") and s["kendini_duzeltme"]]
    P.append("<h3>Uçtan uca sistem, kip başına</h3>" + bars(kip_sys))
    tr_cat = main_s["kategori"].get("tr_sayi")
    P.append(f"<p class='note'>TR kipinde, Türkçe biçimli sayı içeren sorularda (tr_sayi, n={main_s['kategori_n'].get('tr_sayi')}) "
             f"EX {pct(tr_cat)}; naif kiplerdeki aynı alt kümedeki düşüş tabloda kategori satırlarında görülür.</p>")

    # ---- kategori
    P.append("<h2>3. Soru kategorisine göre EX</h2>")
    cats = sorted(main_s["kategori"].items(), key=lambda kv: (kv[1] is None, -(kv[1] or 0)))
    comp = by.get("kural_en_naif")
    P.append(bars([(f"{k} (n={main_s['kategori_n'][k]})", v) for k, v in cats],
                  [comp["kategori"].get(k) for k, _ in cats] if comp else None,
                  (sys_label(main_s["sistem"]), sys_label(comp["sistem"])) if comp else None))
    P.append("<h3>Zorluk düzeyi</h3><div class='wrap'><table><tr><th>Sistem</th>" +
             "".join(f"<th class='num'>{z}</th>" for z in ("kolay", "orta", "zor")) + "</tr>" +
             "".join(f"<tr><td>{e(sys_label(s['sistem']))}</td>" + "".join(
                 f"<td class='num'>{pct(s['zorluk'].get(z))}</td>" for z in ("kolay", "orta", "zor")) + "</tr>" for s in S)
             + "</table></div>")

    # ---- hata kataloğu
    P.append("<h2>4. Hata kataloğu</h2>")
    P.append("<p class='note'>Başarısız örnekler aşağıdaki tiplere otomatik atandı (değerlendirme anında, altın sonuçla "
             "karşılaştırılarak). Çalışma zamanında ajan aynı tipleri statik denetim, yürütme ve sonuç denetimi "
             "adımlarında yakalayıp geri besleme olarak kullanır.</p>")
    types = list(CATALOG.keys())
    P.append("<div class='wrap'><table><tr><th>Hata tipi</th><th>Tanım</th>" +
             "".join(f"<th class='num'>{e(sys_label(s['sistem']).split(' · ')[-1])}</th>" for s in S) + "</tr>")
    for t in types:
        counts = [s["hata_tipleri"].get(t, 0) for s in S]
        if not any(counts) and t in ("YANLIS_SONUC", "YURUTME"):
            continue
        P.append(f"<tr><td><b>{e(CATALOG[t]['ad'])}</b><br><code>{t}</code></td><td>{e(CATALOG[t]['tanim'])}"
                 f"<br><span class='note'>Örnek: {e(CATALOG[t]['ornek'])} · Önlem: {e(CATALOG[t]['cozum'])}</span></td>" +
                 "".join(f"<td class='num'>{c}</td>" for c in counts) + "</tr>")
    P.append("</table></div>")

    # ---- kendini düzeltme
    P.append("<h2>5. Kendini düzeltme döngüsü</h2>")
    sc = [s for s in S if s["kendini_duzeltme"] and s["sayi_kipi"] == "tr"]
    P.append(bars([(sys_label(s["sistem"]), s["EX"]) for s in sc], [s["EX_ilk_deneme"] for s in sc],
                  ("Son deneme", "İlk deneme")))
    P.append("<p class='note'>Kural tabanlı üreticide 'düzeltme', hata/boş sonuç sonrası sıradaki aday sorguya geçmektir; "
             "LLM arka uçlarında hata tipi, mesaj ve öneriler (ör. en yakın sütun adları, tablodaki gerçek değerler, ISO tarih "
             "dönüşümü) bir sonraki isteme eklenir.</p>")

    # ---- çıkarım
    P.append("<h2>6. Tablo çıkarma doğruluğu</h2>")
    ex = ozet["cikarim"]
    tabs = [r["tablo"] for r in next(iter(ex.values()))]
    P.append("<div class='wrap'><table><tr><th>Tablo</th>" + "".join(f"<th class='num'>{e(m)}</th>" for m in ex) + "</tr>")
    for i, t in enumerate(tabs):
        cells = []
        for m in ex:
            r = ex[m][i]
            mark = "" if r["baslik_dogru"] else " <span class='no' title='başlık yapısı hatalı'>•</span>"
            cells.append(f"<td class='num'>{pct(100 * r['hucre_dogrulugu']) if r['bulundu'] else '<span class=no>bulunamadı</span>'}{mark}</td>")
        P.append(f"<tr><td>{e(t)}</td>{''.join(cells)}</tr>")
    P.append("<tr><td><b>Ortalama hücre doğruluğu</b></td>" + "".join(
        f"<td class='num'><b>{pct(100 * sum(r['hucre_dogrulugu'] for r in ex[m]) / len(ex[m]))}</b></td>" for m in ex) + "</tr>")
    P.append("</table></div><p class='note'>• = çok satırlı/birleşik başlık yapısı doğru çözülemedi. "
             "<b>auto</b>: pdfplumber (çizgili tablolar, hücre kutularından birleşim çözümleme) → bulamazsa Camelot-stream "
             "(çizgisiz tablolar). HTML tablolarında rowspan/colspan doğrudan okunur.</p>")

    # ---- hatalı örnekler
    recs = _load_records(main_s["sistem"])
    bad = [r for r in recs if not r["dogru"]]
    P.append(f"<h2>7. {e(sys_label(main_s['sistem']))}: başarısız örnekler ({len(bad)})</h2>")
    if bad:
        P.append("<div class='wrap'><table><tr><th>ID</th><th>Soru</th><th>Hata tipi</th><th>Tahmin</th><th>Altın SQL</th></tr>")
        for r in bad:
            pred = r["tahmin_sql"] or r["cevap"]
            P.append(f"<tr><td>{e(r['id'])}</td><td>{e(r['soru'])}</td><td>{e(r['hata_tipi'])}</td>"
                     f"<td><code>{e(pred)}</code></td><td><code>{e(r['altin_sql'] or 'CEVAPSIZ')}</code></td></tr>")
        P.append("</table></div>")

    # ---- yöntem
    P.append("<h2>8. Yöntem ve sınırlılıklar</h2><ul class='note'>"
             "<li><b>Veri:</b> Belgeler sentetiktir (gerçek kurum verisi değildir) ama Türk raporlarının yapısal zorluklarını "
             "taşır: birleşik başlıklar, rowspan'li gruplar, '1.234,56', parantezli negatifler, '%4,5', 'Ocak 2024', "
             "'Toplam' satırları, çizgisiz tablo.</li>"
             "<li><b>Test kümesi:</b> Spider (seçim, filtre, toplama, gruplama, sıralama, iç içe, çoklu tablo) ve BIRD "
             "(kanıt/formül, değer eşleme, birim) soru tiplerinden uyarlanmıştır; cevapsız sorular dahildir.</li>"
             "<li><b>EX:</b> Sistem SQL'i kendi çıkardığı veritabanında, altın SQL ise elle doğrulanmış referans veritabanında "
             "çalışır; böylece çıkarım hataları da ölçüme yansır. Sayısal tolerans ±(0,01 + %0,1).</li>"
             "<li><b>Önemli:</b> Kural tabanlı sistemin kuralları geliştirme bölümündeki sorular üzerinde ayarlandı; bu nedenle "
             "geliştirme skoru iyimserdir. Genellemeyi gösteren sayı <b>ayrılmış test</b> skorudur.</li>"
             "<li>Kural tabanlı sistem çoklu tablo ve alan bilgisi gerektiren formül sorularında (ör. brüt kâr marjı) zayıftır; "
             "bu sorular LLM arka uçlarının katkısını ölçmek için kümede tutulmuştur.</li></ul>")

    page = (f"<!doctype html><html lang='tr'><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'>"
            f"<title>TR-TabloSQL Raporu</title><style>{CSS}</style></head><body><main>{''.join(P)}</main></body></html>")
    path = Path(path)
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(page, encoding="utf-8")
    return path