Text-to-SQL / t2s /report.py
SevdanurGenc's picture
Upload 466 files
a72d1cf verified
Raw History Blame Contribute Delete
16.8 kB
"""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