"""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 = ['
") 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"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
") # ---- ö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("Not: Bu çalıştırmada LLM arka ucu kullanılmadı (API anahtarı yoktu). "
"python scripts/run_all.py --claude, --openai veya --ollama "
"ile LLM sistemleri aynı tabloya eklenir.
| Sistem | Sayı kipi | EX | " "Geliştirme | Ayrılmış test | İlk deneme EX | " "Cevapsız → doğru red | Yanlış red | Ort. deneme | " "Ort. süre |
|---|---|---|---|---|---|---|---|---|---|
| {e(sys_label(s['sistem']))} | {e(s['sayi_kipi'])} | {pct(s['EX'])} | " f"{pct(s['bolum'].get('gelistirme'))} | {pct(s['bolum'].get('test'))} | " f"{pct(s['EX_ilk_deneme'])} | {pct(s['cevapsiz_dogru_red'])} | " f"{pct(s['yanlis_red_orani'])} | {str(s['ort_deneme']).replace('.', ',')} | " f"{str(s['ort_sure_sn']).replace('.', ',')} sn |
Aynı belgeler üç farklı sayı ayrıştırma kipiyle veritabanına aktarıldı. "
"tr: doğru Türkçe yorum. en: yaygın naif yaklaşım — nokta ondalık, virgül binlik sanılır "
"('1.346' → 1,346; '4,5' → 45; '258.107.640,00' ayrıştırılamaz → sütun metne düşer). "
"text: hiç ayrıştırma yok; SQLite SUM('1.234,56') 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ü.
| Kip | Altın SQL EX | Bozulan cevap | " "Sayısal sütun | Metin sütun | Ayrıştırılamayan hücre |
|---|---|---|---|---|---|
| {e(k)} | {pct(r['EX'])} | {len(r['bozulan'])} / {r['n']} | " f"{s2.get('sayisal_sutun', '—')} | {s2.get('metin_sutun', '—')} | " f"{s2.get('ayristirilamayan_hucre', '—')} |
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.
") # ---- kategori P.append("| Sistem | " + "".join(f"{z} | " for z in ("kolay", "orta", "zor")) + "
|---|---|
| {e(sys_label(s['sistem']))} | " + "".join( f"{pct(s['zorluk'].get(z))} | " for z in ("kolay", "orta", "zor")) + "
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.
") types = list(CATALOG.keys()) P.append("| Hata tipi | Tanım | " + "".join(f"{e(sys_label(s['sistem']).split(' · ')[-1])} | " for s in S) + "
|---|---|---|
{e(CATALOG[t]['ad'])}{t} | {e(CATALOG[t]['tanim'])}"
f" Örnek: {e(CATALOG[t]['ornek'])} · Önlem: {e(CATALOG[t]['cozum'])} | " +
"".join(f"{c} | " for c in counts) + "
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.
") # ---- çıkarım P.append("| Tablo | " + "".join(f"{e(m)} | " for m in ex) + "{pct(100 * r['hucre_dogrulugu']) if r['bulundu'] else 'bulunamadı'}{mark} | ") P.append(f"
|---|---|
| {e(t)} | {''.join(cells)}|
| Ortalama hücre doğruluğu | " + "".join( f"{pct(100 * sum(r['hucre_dogrulugu'] for r in ex[m]) / len(ex[m]))} | " for m in ex) + "
• = çok satırlı/birleşik başlık yapısı doğru çözülemedi. " "auto: pdfplumber (çizgili tablolar, hücre kutularından birleşim çözümleme) → bulamazsa Camelot-stream " "(çizgisiz tablolar). HTML tablolarında rowspan/colspan doğrudan okunur.
") # ---- hatalı örnekler recs = _load_records(main_s["sistem"]) bad = [r for r in recs if not r["dogru"]] P.append(f"| ID | Soru | Hata tipi | Tahmin | Altın SQL |
|---|---|---|---|---|
| {e(r['id'])} | {e(r['soru'])} | {e(r['hata_tipi'])} | " f"{e(pred)} | {e(r['altin_sql'] or 'CEVAPSIZ')} |