File size: 7,818 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 | """Hata enjeksiyonu deneyi: hata kataloğunun tespit, tip ve öneri doğruluğu.
Altın SQL sorgularına, LLM tabanlı Text-to-SQL sistemlerinde sık görülen hata tipleri
kontrollü olarak enjekte edilir. Her bozuk sorgu ajanın denetim zincirinden
(statik denetim → yürütme → sonuç denetimi) geçirilir ve şunlar ölçülür:
sessiz : denetim olmasaydı sorgu hatasız çalışıp YANLIŞ sonuç döndürür müydü?
tespit : zincir en az bir hata/uyarı üretti mi?
tip : ilk olayın tipi beklenen katalog tipiyle aynı mı?
oneri : geri besleme metni doğru onarımı (doğru sütun adı / değer / ISO tarih /
sayı / toplam_satiri koşulu) içeriyor mu?
Çalıştırma: python scripts/hata_enjeksiyonu.py
"""
from __future__ import annotations
import json
import re
import sys
from collections import defaultdict
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
from t2s.errors import check_result, classify_exception, static_checks # noqa: E402
from t2s.evaluate import load_benchmark, results_match # noqa: E402
from t2s.schema import connect # noqa: E402
from t2s.trnum import AYLAR, fold # noqa: E402
AY_ADI = {v: k for k, v in AYLAR.items() if len(k) > 3}
AY_TR = {1: "Ocak", 2: "Şubat", 3: "Mart", 4: "Nisan", 5: "Mayıs", 6: "Haziran", 7: "Temmuz",
8: "Ağustos", 9: "Eylül", 10: "Ekim", 11: "Kasım", 12: "Aralık"}
def tr_thousands(n: float) -> str:
s = f"{n:,.0f}" if float(n).is_integer() else f"{n:,.2f}"
return s.replace(",", "§").replace(".", ",").replace("§", ".")
def perturbations(sql: str, cols: set[str]):
"""(tip, bozuk_sql, onarım_belirteci) üçlüleri üretir."""
out = []
# 1) Şema eşleme: sütun adındaki alt çizgiyi kaldır ya da yıl önekini taşı
used = [c for c in sorted(cols, key=len, reverse=True) if re.search(rf"\b{re.escape(c)}\b", sql) and "_" in c
and c != "toplam_satiri"]
if used:
c = used[0]
out.append(("SEMA_ESLEME", re.sub(rf"\b{re.escape(c)}\b", c.replace("_", ""), sql, count=1), c))
# 2) Değer eşleme: metin literalini Türkçe dışı büyük harfe çevir (İzmir -> IZMIR)
m = re.search(r"=\s*'([^'%]+)'", sql)
if m and not re.match(r"^\d{4}-\d{2}", m.group(1)):
v = m.group(1)
bad = fold(v).upper()
if bad != v:
out.append(("DEGER_ESLEME", sql[:m.start(1)] + bad + sql[m.end(1):], v))
# 3) Tarih biçimi: ISO -> Türkçe yazım
m = re.search(r"'(\d{4})-(\d{2})-(\d{2})'", sql)
if m:
y, mo, d = m.groups()
out.append(("TARIH_BICIMI", sql[:m.start()] + f"'{d}.{mo}.{y}'" + sql[m.end():], f"'{y}-{mo}-{d}'"))
else:
m = re.search(r"'(\d{4})-(\d{2})'", sql)
if m:
y, mo = m.groups()
out.append(("TARIH_BICIMI", sql[:m.start()] + f"'{AY_TR[int(mo)]} {y}'" + sql[m.end():], f"'{y}-{mo}'"))
# 4) Sayı biçimi: WHERE eşiğini Türkçe biçimli metin olarak yaz (> 2000 -> > '2.000,00')
m = re.search(r"([<>]=?)\s*(\d{3,})(?![\d.])", sql)
if m:
n = int(m.group(2))
lit = tr_thousands(n) + ",00"
out.append(("SAYI_BICIMI", sql[:m.start(2)] + f"'{lit}'" + sql[m.end(2):], str(n)))
if n >= 1000: # tırnaksız binlik ayraçlı yazım (SQLite bunu ondalık sayı olarak okur)
out.append(("SAYI_BICIMI*", sql[:m.start(2)] + tr_thousands(n) + sql[m.end(2):], str(n)))
# 5) Sayısal akıl yürütme: toplam satırı koşulunu kaldır
if re.search(r"toplam_satiri\s*=\s*0", sql):
bad = re.sub(r"\s+AND\s+toplam_satiri\s*=\s*0|toplam_satiri\s*=\s*0\s+AND\s+", " ", sql)
bad = re.sub(r"\s+WHERE\s+toplam_satiri\s*=\s*0", "", bad)
if bad != sql:
out.append(("SAYISAL_AKIL", bad, "toplam_satiri"))
# 6) Sözdizimi: başka bir SQL lehçesinden alışkanlık (LIMIT 1 -> TOP 1)
m = re.search(r"^SELECT\s+(.*)\s+LIMIT 1$", sql, re.S)
if m:
out.append(("SOZDIZIMI", "SELECT TOP 1 " + m.group(1), "LIMIT"))
return out
def run_chain(sql: str, con):
events = static_checks(sql, con)
rows, exc = None, None
if not any(e.tip == "SOZDIZIMI" for e in events):
try:
rows = [tuple(r) for r in con.execute(sql).fetchall()]
events += check_result(sql, rows, con)
except Exception as e: # noqa: BLE001
exc = e
events.append(classify_exception(e, sql, con))
return events, rows, exc
def main():
sysdb = connect(ROOT / "outputs" / "db" / "sistem_tr.db", readonly=True)
ref = connect(ROOT / "outputs" / "db" / "referans.db", readonly=True)
cols = {c for (c,) in sysdb.execute("SELECT DISTINCT sutun FROM _sutunlar")}
items = [it for it in load_benchmark() if it["altin_sql"]]
stats = defaultdict(lambda: defaultdict(int))
examples = {}
for it in items:
gold = [tuple(r) for r in ref.execute(it["altin_sql"]).fetchall()]
gold_tips = {e.tip for e in run_chain(it["altin_sql"], sysdb)[0]} # bozulmamış sorguda da çıkan uyarılar
for tip, bad, fix in perturbations(it["altin_sql"], cols):
ev, rows, exc = run_chain(bad, sysdb)
s = stats[tip]
s["n"] += 1
silent = exc is None and rows is not None and not results_match(rows, gold) and \
not any(e.tip == "SOZDIZIMI" for e in ev)
s["sessiz"] += silent
s["sessiz_tespit"] += silent and bool(ev) # yalnızca sessiz (zararlı) hatalar üzerinde tespit
s["tespit"] += bool(ev)
exp = tip.rstrip("*")
s["tip"] += bool(ev) and ev[0].tip == exp
s["tespit_dogru_tip"] += any(e.tip == exp for e in ev) # beklenen tipte en az bir olay
s["tespit_ozgun"] += any(e.tip not in gold_tips for e in ev) # altın sorguda da çıkan uyarılar hariç
fb = " ".join(e.feedback() for e in ev)
s["oneri"] += bool(ev) and fix.lower() in fb.lower()
examples.setdefault(tip, {"soru": it["soru"], "bozuk_sql": bad,
"geri_besleme": ev[0].feedback() if ev else "(tespit edilmedi)"})
# yanlış alarm: bozulmamış altın sorgularda denetim zinciri olay üretiyor mu?
fa = []
for it in items:
ev, rows, exc = run_chain(it["altin_sql"], sysdb)
if ev:
fa.append({"id": it["id"], "tipler": [e.tip for e in ev]})
out = {k: dict(v) for k, v in stats.items()}
tot = defaultdict(int)
for k, v in out.items():
for kk, vv in v.items():
tot[kk] += vv
out["TOPLAM"] = dict(tot)
print(f"{'tip':14} {'n':>4} {'sessiz':>7} {'tespit':>7} {'tip':>7} {'öneri':>7}")
for k, v in out.items():
n = v["n"]
print(f"{k:14} {n:4d} {100*v['sessiz']/n:6.1f}% {100*v['tespit']/n:6.1f}% {100*v['tip']/n:6.1f}% "
f"{100*v['oneri']/n:6.1f}%")
ci = {k: {m: wilson(v[m], v["n"]) for m in ("sessiz", "tespit", "tespit_ozgun", "tespit_dogru_tip", "tip", "oneri")}
for k, v in out.items()}
print(f"yanlış alarm: {len(fa)}/{len(items)} altın sorgu")
(ROOT / "outputs" / "hata_enjeksiyonu.json").write_text(
json.dumps({"ozet": out, "wilson_ga95": ci, "altin_sorgu": len(items),
"yanlis_alarm": {"n": len(fa), "toplam": len(items), "ornekler": fa},
"ornekler": examples}, ensure_ascii=False, indent=1), encoding="utf-8")
def wilson(k: int, n: int, z: float = 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)]
if __name__ == "__main__":
main()
|