Spaces:
Running
Running
File size: 24,088 Bytes
50efdc6 | 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 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 | """
src/evaluation/eval_text2sql.py
Text-to-SQL μ±κ³΅λ₯ νκ°
νμ κΈ°μ€:
[P1] SQL ꡬ문 μ€ν κ°λ₯ μ¬λΆ (syntax / runtime error β FAIL)
[P2] μλ΅ μκ° 10μ΄ μ΄λ΄ (timeout β FAIL)
[P3] νμ alias μ»¬λΌ 4κ° ν¬ν¨ μ¬λΆ (quantity, delivery_status, delivery_date, available_stock)
[P4] expected_values μΌμΉ μ¬λΆ (expected_values=Noneμ SQL μ μ μ€νμ΄λ©΄ PASS;
κ°μ΄ λͺ
μλλλ° rowκ° μμΌλ©΄ FAIL β μ΄μ μ skipλμ΄ κ±°μ§ PASSμμ)
[P5] μμ± κΈ°μ€(criteria) μ€μ μ¬λΆ (LLMμ΄ μμ€ν
ν둬ννΈμ ν΅μ¬ κ·μΉμ μ§μΌ°λμ§ μ μ κ²μ¬;
μ€ν κ²°κ³Όκ° μλ "μ΄λ€ κΈ°μ€μΌλ‘ SQLμ λ§λ€μλμ§" νμΈμ©)
λͺ©ν: P1+P2+P3+P4 κΈ°μ€ 95% μ΄μ (P4λ expected_values=Noneμ΄λ©΄ μλ PASS) / P5(criteria) λ³λ μ§κ³
β μ΄μ μ P1+P2+P3λ§ λ΄€λλ κ±°μ§ 100%κ° λμμ. P4κ° μ€μ μ νλλ₯Ό λ΄λ³΄νλ ν΅μ¬.
μ€ν:
python -m src.evaluation.eval_text2sql
python -m src.evaluation.eval_text2sql --report reports/worker_a_eval_result.json
"""
from __future__ import annotations
import argparse
import json
import logging
import re
import sqlite3
import sys
import threading
import time
from dataclasses import dataclass, field, asdict
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from src.tools.text2sql import build_validation_query, _hardcoded_query
# Windows cp949 νκ²½μμ μ λμ½λ λ‘κ·Έ μΆλ ₯ μμ ν
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
# ---------------------------------------------------------------------------
# λ‘κΉ
μ€μ
# ---------------------------------------------------------------------------
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
stream=sys.stdout,
)
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# κ²½λ‘ μμ
# ---------------------------------------------------------------------------
DB_PATH = "data/sap_erp.db"
TEST_CASES_PATH = "data/eval/text2sql_test_cases.json"
REQUIRED_ALIASES = {"quantity", "delivery_status", "delivery_date", "available_stock"}
TIMEOUT_SECS = 10 # μ‘°μ κ°λ₯
# ---------------------------------------------------------------------------
# λ°μ΄ν° ν΄λμ€
# ---------------------------------------------------------------------------
@dataclass
class CaseResult:
id: str
description: str
order_id: str
item_no: int
passed: bool # P1+P2+P3+P4 λͺ¨λ ν΅κ³Ό (P4=Noneμ΄λ©΄ P1+P2+P3λ§μΌλ‘ νμ )
p1_syntax: bool = False # SQL μ€ν κ°λ₯
p2_timeout: bool = False # 10μ΄ μ΄λ΄
p3_columns: bool = False # νμ alias μ‘΄μ¬
p4_values: bool | None = None # κΈ°λκ° μΌμΉ (None=κ²μ¦ μλ΅)
p5_criteria: bool = False # μμ± κΈ°μ€ λͺ¨λ μΆ©μ‘± (score == total)
elapsed_ms: float = 0.0
generated_sql: str = ""
actual_result: Any = None
error: str = ""
sql_strategy: str = "" # "primary" | "fallback" | "hardcoded"
p4_mismatches: list = field(default_factory=list) # [{column, expected, actual}, ...]
criteria_checks: dict = field(default_factory=dict) # {rule_name: bool, ...}
criteria_score: int = 0 # ν΅κ³Όν rule μ
criteria_total: int = 0 # κ²μ¬ν rule μ
# ββ Fallback μΆμ (μ΄μ λ‘κΉ
μ© β evalμμλ κ°μ μκ·Έλμ λ³Έλ€) βββββββββββββ
fallback_triggered: bool = False # μ΄μμμ hardcoded fallbackμ΄ (νΉμ νΈλ¦¬κ±°λμ) μΌμ΄μ€
fallback_reason: str = "" # "llm_fail" | "primary_zero_rows" | ""
hardcoded_recovered: bool | None = None # primary_zero_rowsμΌ λ hardcodedκ° rowλ₯Ό μ‘μλμ§
@dataclass
class EvalReport:
generated_at: str = ""
model_primary: str = ""
total: int = 0
p1_pass: int = 0 # syntax OK
p2_pass: int = 0 # timeout OK
p3_pass: int = 0 # columns OK
p4_pass: int = 0 # values OK
p4_total: int = 0 # p4 κ²μ¦ λμ μ
p5_pass: int = 0 # criteria μ κ·μΉ ν΅κ³Ό
overall_pass: int = 0 # P1+P2+P3+P4 λͺ¨λ ν΅κ³Ό (P4=Noneμ PASSλ‘ κ°μ£Ό)
success_rate: float = 0.0 # overall_pass / total
p4_accuracy: float = 0.0 # p4_pass / p4_total
p5_compliance: float = 0.0 # p5_pass / total (μ κ·μΉ λ§μ‘± λΉμ¨)
criteria_rule_pass: dict = field(default_factory=dict) # {rule_name: ν΅κ³Ό μΌμ΄μ€ μ}
criteria_avg_score: float = 0.0 # μΌμ΄μ€λΉ νκ· μ μ (score/total)
strategy_primary: int = 0 # primary LLMμΌλ‘ μμ±λ μΌμ΄μ€ μ (build μμ )
strategy_hardcoded: int = 0 # build λ¨κ³μμ hardcodedλ‘ ν΄λ°±λ μ (LLM νΈμΆ/νμ± μ€ν¨)
fallback_zero_rows: int = 0 # LLM SQLμ΄ μ€νμ λμ§λ§ 0건 λ°ν β μ΄μμμλ hardcodedλ‘ μ¬μλλλ μΌμ΄μ€
fallback_zero_rows_recoverable: int = 0 # κ·Έ μ€ hardcodedλ‘ μ¬μλνλ©΄ rowκ° μ‘ν LLM μλͺ»μ΄ μ
μ¦λλ μ
p4_mismatch_cases: list[dict] = field(default_factory=list) # P4 λΆμΌμΉ μΌμ΄μ€ μμΈ
fallback_cases: list[dict] = field(default_factory=list) # fallbackμ΄ μΌμ΄λ¬κ±°λ νΈλ¦¬κ±°λμ μΌμ΄μ€ μμΈ
results: list[dict] = field(default_factory=list)
# ---------------------------------------------------------------------------
# P5: SQL μμ± κΈ°μ€(criteria) μ μ κ²μ¬
# ---------------------------------------------------------------------------
#
# SQL λ¬Έλ²/μ€νμ P1μ΄ λ³΄κ³ , μ¬κΈ°μλ "LLMμ΄ μμ€ν
ν둬ννΈμ ν΅μ¬ κ·μΉμ λ°λλκ°"λ₯Ό
# μ μ μ κ·μμΌλ‘ λ³Έλ€. rowκ° λΉμ΄λ β μ¦ κ²°κ³Όμ μΌλ‘ 0κ±΄μ΄ λμλ β κ·μΉμ μ΄λμ
# μ΄κ²Όλμ§ λ³΄κ³ νκΈ° μν¨μ΄λ€.
#
# κ·μΉ (text2sql.py _SYSTEM_PROMPT μ°Έμ‘°):
# r1 cast_vbeln : VBELNμ CAST(... AS INTEGER)λ‘ μ κ·ν (rule 4)
# r2 cast_posnr : POSNRμ CAST(... AS INTEGER)λ‘ μ κ·ν (rule 4)
# r3 mard_sum_aggregation: μ μ₯μμΉλ³ λ€ν β SUM(LABST) μ§κ³ (rule 3a)
# r4 makt_lang_handling : μΈμ΄λ³ λ€ν β SPRAS κΈ°μ€ μ°μ μμ (rule 3b)
# r5 min_edatu : μΌμ λΌμΈ κ°μ₯ μ΄λ₯Έ λ μ§ β MIN(EDATU) (rule 3c)
# r6 coalesce_zero : NULL stock β 0 μΌλ‘ 보μ COALESCE(..., 0) (rule 3a)
_CRITERIA_PATTERNS = {
"cast_vbeln": r"CAST\s*\(\s*[\w.]*VBELN\s+AS\s+INTEGER\s*\)",
"cast_posnr": r"CAST\s*\(\s*[\w.]*POSNR\s+AS\s+INTEGER\s*\)",
"mard_sum_aggregation": r"SUM\s*\(\s*[\w.]*LABST\s*\)",
"makt_lang_handling": r"\bSPRAS\b",
"min_edatu": r"MIN\s*\(\s*[\w.]*EDATU\s*\)",
"coalesce_zero": r"COALESCE\s*\([^()]*,\s*0\s*\)",
}
def _check_criteria(sql: str) -> tuple[dict, int, int]:
"""LLMμ΄ μμ€ν
ν둬ννΈμ μ΄λ€ κ·μΉμ μ§μΌ°λμ§ μ μ κ²μ¬.
Returns:
(checks, score, total) β checksλ {rule_name: bool}, score/totalμ ν΅κ³Ό/μ 체 κ·μΉ μ
"""
s = sql.upper()
checks = {name: bool(re.search(pat, s, re.IGNORECASE)) for name, pat in _CRITERIA_PATTERNS.items()}
score = sum(checks.values())
total = len(checks)
return checks, score, total
# ---------------------------------------------------------------------------
# μ€ν ν¬νΌ
# ---------------------------------------------------------------------------
def _run_with_timeout(fn, timeout: int):
"""fn()μ λ³λ μ€λ λλ‘ μ€ν, timeout μ΄ μ΄κ³Ό μ TimeoutError"""
result_box: list = [None]
error_box: list = [None]
def target():
try:
result_box[0] = fn()
except Exception as e:
error_box[0] = e
t = threading.Thread(target=target, daemon=True)
t.start()
t.join(timeout=timeout)
if t.is_alive():
raise TimeoutError(f"SQL execution exceeded {timeout}s")
if error_box[0] is not None:
raise error_box[0]
return result_box[0]
def _execute_sql(sql: str, db_path: str = DB_PATH) -> tuple[list[dict], list[str]]:
"""SQL μ€ν β (rows, column_names) νν λ°ν. rowκ° μμ΄λ 컨λΌλͺ
νμΈ κ°λ₯."""
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
try:
cursor = conn.execute(sql)
rows = [dict(row) for row in cursor.fetchall()]
col_names = [d[0] for d in cursor.description] if cursor.description else []
return rows, col_names
finally:
conn.close()
# ---------------------------------------------------------------------------
# μΌμ΄μ€ λ¨μ νκ°
# ---------------------------------------------------------------------------
def _evaluate_case(tc: dict, db_path: str = DB_PATH) -> CaseResult:
order_id = str(tc["order_id"])
item_no = int(tc["item_no"])
expected = tc.get("expected_values") # None or dict
cr = CaseResult(
id=tc["id"],
description=tc["description"],
order_id=order_id,
item_no=item_no,
passed=False,
)
# ββ SQL μμ± βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
try:
sql, strategy = build_validation_query(order_id, str(item_no))
cr.generated_sql = sql
cr.sql_strategy = strategy
except Exception as e:
cr.error = f"SQL generation failed: {e}"
logger.warning("[%s] SQL μμ± μ€ν¨: %s", tc["id"], e)
return cr
# ββ P5: μμ± κΈ°μ€(criteria) μ μ κ²μ¬ βββββββββββββββββββββββββββββββββββββ
# SQLμ΄ μ€νλλ μ λλ , μ΄λ€ κ·μΉμ μ§μΌ°λμ§ νμ κΈ°λ‘νλ€.
checks, score, total = _check_criteria(sql)
cr.criteria_checks = checks
cr.criteria_score = score
cr.criteria_total = total
cr.p5_criteria = (score == total)
# ββ P1: ꡬ문 μ€ν + P2: Timeout ββββββββββββββββββββββββββββββββββββββββββ
t0 = time.perf_counter()
try:
rows, col_names = _run_with_timeout(lambda: _execute_sql(sql, db_path), TIMEOUT_SECS)
cr.elapsed_ms = (time.perf_counter() - t0) * 1000
cr.p1_syntax = True
cr.p2_timeout = True
cr.actual_result = rows[0] if rows else None
except TimeoutError as e:
cr.elapsed_ms = TIMEOUT_SECS * 1000
cr.error = str(e)
cr.p1_syntax = True # μ€νμ μμλμΌλ―λ‘ λ¬Έλ² OK
cr.p2_timeout = False
logger.warning("[%s] timeout: %s", tc["id"], e)
return cr
except Exception as e:
cr.elapsed_ms = (time.perf_counter() - t0) * 1000
cr.error = str(e)
cr.p1_syntax = False
cr.p2_timeout = False
logger.warning("[%s] SQL execution failed: %s", tc["id"], e)
return cr
# ββ P3: νμ alias μ»¬λΌ ν¬ν¨ (row μ 무 μκ΄μμ΄ cursor.descriptionμΌλ‘ κ²μ¦) ββββ
actual_col_set = set(col_names)
cr.p3_columns = REQUIRED_ALIASES.issubset(actual_col_set)
if not cr.p3_columns:
missing = REQUIRED_ALIASES - actual_col_set
cr.error = f"Missing columns: {missing}"
logger.warning("[%s] Missing columns: %s", tc["id"], missing)
# ββ Fallback μΆμ (μ΄μμμ hardcoded ν΄λ°±μ΄ μ€μ μΌμ΄λ¬κ±°λ, μΌμ΄λ¬μ μΌμ΄μ€) βββββ
# evalμ build_validation_queryλ§ νΈμΆνλ―λ‘ μ΄μ μ½λ(run_validation_query)μ
# "primary 0건 β hardcoded μ¬μλ" κ²½λ‘λ μλμΌλ‘ νμ§ μλλ€.
# κ°μ μκ·Έλμ μ¬ννκΈ° μν΄ μ¬κΈ°μ λμΌ μ‘°κ±΄μ μ§μ νμΈνκ³ hardcoded κ²°κ³ΌκΉμ§ λΉκ΅.
if strategy == "hardcoded_llm_fail":
cr.fallback_triggered = True
cr.fallback_reason = "llm_fail"
logger.warning(
"[%s] FALLBACK(llm_fail) | LLM νΈμΆ μ€ν¨ β hardcoded μΏΌλ¦¬λ‘ νκ°λ¨",
tc["id"],
)
elif strategy == "primary" and cr.actual_result is None:
cr.fallback_triggered = True
cr.fallback_reason = "primary_zero_rows"
# hardcoded μΏΌλ¦¬λ‘ μ¬μ€νν΄μ LLM μλͺ»μΈμ§ / μ€λ°μ΄ν° μμμΈμ§ κ°λ₯Έλ€
try:
hc_rows, _ = _execute_sql(_hardcoded_query(order_id, str(item_no)), db_path)
cr.hardcoded_recovered = len(hc_rows) > 0
logger.warning(
"[%s] FALLBACK(primary_zero_rows) | LLM SQL 0건, hardcoded μ¬μλ β %s",
tc["id"],
"row λ°κ²¬ (LLM 쿼리 μ€λ₯ κ°ν μ νΈ)" if cr.hardcoded_recovered
else "μ¬μ ν 0건 (μ€λ°μ΄ν° λΆμ¬ κ°λ₯)",
)
except Exception as e:
logger.warning("[%s] hardcoded μ¬μλ μ€ν¨: %s", tc["id"], e)
cr.hardcoded_recovered = None
# ββ P4: κΈ°λκ° μΌμΉ βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if expected is not None:
if cr.actual_result is None:
# μ΄μ λ²κ·Έ: μ¬κΈ°μ p4_values=NoneμΌλ‘ λλ©΄ νκ° μ§κ³μμ ν΅μ§Έλ‘ λΉ μ Έ
# "κ±°μ§ 100% PASS"κ° λμλ€. κΈ°λκ°μ΄ μλλ° rowκ° 0건μ΄λ©΄ λͺ
λ°±ν FAIL.
cr.p4_values = False
cr.p4_mismatches = [{
"column": "<row>",
"expected": "row exists matching expected_values",
"actual": "no row returned (SQL ran but matched 0 rows)",
}]
cr.error = (cr.error + " | " if cr.error else "") + \
"P4 FAIL: expected_values set but SQL returned 0 rows"
else:
mismatches = []
for col, exp_val in expected.items():
act_val = cr.actual_result.get(col)
if act_val != exp_val:
mismatches.append({"column": col, "expected": exp_val, "actual": act_val})
cr.p4_mismatches = mismatches
cr.p4_values = (len(mismatches) == 0)
if mismatches:
mismatch_strs = [
f"{m['column']}: expected={m['expected']!r}, actual={m['actual']!r}"
for m in mismatches
]
cr.error += " | Value mismatches: " + "; ".join(mismatch_strs)
elif expected is None:
# expected_values=null -> rowκ° μμ΄μΌ μ μ OR 컬λΌμ΄ λ§μΌλ©΄ OK
cr.p4_values = True # no-row μΌμ΄μ€λ SQLλ§ μ μμ΄λ©΄ PASS
# ββ μ΅μ’
ν΅κ³Ό νμ ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# P4κ° None(=κ²μ¦ λμ μλ)μ΄λ©΄ κΈ°μ ν΅κ³Ό(P1+P2+P3)λ§μΌλ‘ PASS,
# P4κ° True/Falseλ©΄ κ·Έκ²κΉμ§ ν¬ν¨ν΄ PASS κ²°μ . Falseλ©΄ μ¦μ FAIL.
p4_ok = cr.p4_values is not False
cr.passed = cr.p1_syntax and cr.p2_timeout and cr.p3_columns and p4_ok
status = "PASS" if cr.passed else "FAIL"
logger.info(
"[%s] %s | elapsed=%.0fms | p1=%s p2=%s p3=%s p4=%s p5=%d/%d strategy=%s",
tc["id"], status, cr.elapsed_ms,
cr.p1_syntax, cr.p2_timeout, cr.p3_columns, cr.p4_values,
cr.criteria_score, cr.criteria_total, cr.sql_strategy,
)
return cr
# ---------------------------------------------------------------------------
# λ©μΈ νκ° λ£¨ν
# ---------------------------------------------------------------------------
def run_eval(
test_cases_path: str = TEST_CASES_PATH,
db_path: str = DB_PATH,
report_path: str | None = None,
) -> EvalReport:
from src.config import get_config
cfg = get_config()
test_cases = json.loads(Path(test_cases_path).read_text(encoding="utf-8"))
logger.info("=== Text-to-SQL νκ° μμ | μΌμ΄μ€ μ: %d ===", len(test_cases))
report = EvalReport(
generated_at = datetime.now(timezone.utc).isoformat(),
model_primary = cfg.models.worker_a_sql.name,
total = len(test_cases),
)
# P5 ruleλ³ λμ μΉ΄μ΄ν° β μ΄λ€ κ·μΉμ΄ μμ£Ό λλ½λλμ§ λ³΄κΈ° μν¨
rule_pass_counter: dict[str, int] = {name: 0 for name in _CRITERIA_PATTERNS.keys()}
criteria_score_sum = 0
for tc in test_cases:
cr = _evaluate_case(tc, db_path)
report.results.append(asdict(cr))
if cr.p1_syntax: report.p1_pass += 1
if cr.p2_timeout: report.p2_pass += 1
if cr.p3_columns: report.p3_pass += 1
if cr.p5_criteria: report.p5_pass += 1
if cr.passed: report.overall_pass += 1
if cr.sql_strategy == "primary": report.strategy_primary += 1
elif cr.sql_strategy == "hardcoded_llm_fail": report.strategy_hardcoded += 1
if cr.fallback_triggered:
if cr.fallback_reason == "primary_zero_rows":
report.fallback_zero_rows += 1
if cr.hardcoded_recovered:
report.fallback_zero_rows_recoverable += 1
report.fallback_cases.append({
"id": cr.id,
"description": cr.description,
"order_id": cr.order_id,
"item_no": cr.item_no,
"reason": cr.fallback_reason,
"hardcoded_recovered": cr.hardcoded_recovered,
"sql_strategy": cr.sql_strategy,
})
# P5: ruleλ³ ν΅κ³Ό μΉ΄μ΄νΈ
for rule_name, ok in cr.criteria_checks.items():
if ok:
rule_pass_counter[rule_name] = rule_pass_counter.get(rule_name, 0) + 1
criteria_score_sum += cr.criteria_score
if cr.p4_values is not None:
report.p4_total += 1
if cr.p4_values:
report.p4_pass += 1
else:
report.p4_mismatch_cases.append({
"id": cr.id,
"description": cr.description,
"order_id": cr.order_id,
"item_no": cr.item_no,
"sql_strategy": cr.sql_strategy,
"actual_result": cr.actual_result,
"mismatches": cr.p4_mismatches,
})
n = report.total
report.success_rate = round(report.overall_pass / n, 4) if n else 0.0
report.p4_accuracy = round(report.p4_pass / report.p4_total, 4) if report.p4_total else 0.0
report.p5_compliance = round(report.p5_pass / n, 4) if n else 0.0
report.criteria_rule_pass = rule_pass_counter
total_rules = len(_CRITERIA_PATTERNS)
report.criteria_avg_score = round(criteria_score_sum / (n * total_rules), 4) if n else 0.0
# ββ μμ½ μΆλ ₯ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
print("\n" + "=" * 60)
print(" Text-to-SQL νκ° κ²°κ³Ό")
print("=" * 60)
print(f" Primary model : {report.model_primary}")
print(f" Total cases : {n}")
print(f" P1 Syntax OK : {report.p1_pass}/{n} ({report.p1_pass/n*100:.1f}%)")
print(f" P2 Timeout OK : {report.p2_pass}/{n} ({report.p2_pass/n*100:.1f}%)")
print(f" P3 Columns OK : {report.p3_pass}/{n} ({report.p3_pass/n*100:.1f}%)")
print(f" Overall PASS : {report.overall_pass}/{n} ({report.success_rate*100:.1f}%) [Target >= 95%]")
if report.p4_total:
print(f" P4 Value Match : {report.p4_pass}/{report.p4_total} ({report.p4_accuracy*100:.1f}%)")
print(f" P5 Criteria : {report.p5_pass}/{n} ({report.p5_compliance*100:.1f}%) "
f"[avg score {report.criteria_avg_score*100:.1f}%]")
target_met = report.success_rate >= 0.95
print(f" Target Met : {'YES' if target_met else 'NO'}")
print("-" * 60)
print(f" SQL Strategy : primary={report.strategy_primary} "
f"hardcoded(llm_fail)={report.strategy_hardcoded}")
print(f" Fallback (μ΄μ μ hardcoded μ¬μλκ° νΈλ¦¬κ±°λμ μΌμ΄μ€):")
print(f" primary_zero_rows : {report.fallback_zero_rows}/{n} "
f"(LLM SQLμ΄ 0건 β hardcoded μ¬μλ λ°μ)")
print(f" ββ hardcodedλ‘ λ³΅κ΅¬ κ°λ₯ : {report.fallback_zero_rows_recoverable}/{report.fallback_zero_rows or 1} "
f"(LLM μΏΌλ¦¬κ° μ€μ λ‘ μλͺ»λλ€λ μ νΈ)")
print("-" * 60)
print(" P5 Rule-level pass rate (μ΄λ€ κΈ°μ€μΌλ‘ SQLμ λ§λ€μλμ§):")
for rule_name, cnt in report.criteria_rule_pass.items():
pct = (cnt / n * 100) if n else 0.0
print(f" {rule_name:<22}: {cnt}/{n} ({pct:.1f}%)")
print("=" * 60)
# μ€ν¨ μΌμ΄μ€ μμΈ μΆλ ₯
failed = [r for r in report.results if not r["passed"]]
if failed:
print(f"\n Failed cases ({len(failed)}):")
for r in failed:
print(f" [{r['id']}] {r['description']}")
print(f" Error: {r['error']}")
# Fallback μΌμ΄μ€ μμΈ μΆλ ₯ (LLMμ΄ μ΄λμ μ€ν¨ν΄ hardcodedλ‘ λ¨μ΄μ‘λμ§)
if report.fallback_cases:
print(f"\n Fallback cases ({len(report.fallback_cases)}):")
for c in report.fallback_cases:
recov = c.get("hardcoded_recovered")
recov_str = (
"hardcodedλ‘ row νμ (LLM SQL μ€λ₯)" if recov is True
else "hardcodedλ 0건" if recov is False
else "n/a"
)
print(f" [{c['id']}] reason={c['reason']} strategy={c['sql_strategy']} β {recov_str}")
print(f" order_id={c['order_id']} item_no={c['item_no']} β {c['description']}")
# P4 λΆμΌμΉ μΌμ΄μ€ μμΈ μΆλ ₯
if report.p4_mismatch_cases:
print(f"\n P4 Value Mismatch cases ({len(report.p4_mismatch_cases)}):")
for c in report.p4_mismatch_cases:
print(f" [{c['id']}] {c['description']} (strategy={c['sql_strategy']})")
print(f" actual_result: {c['actual_result']}")
for m in c["mismatches"]:
print(f" {m['column']}: expected={m['expected']!r} actual={m['actual']!r}")
# ββ 리ν¬νΈ μ μ₯ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if report_path:
out = Path(report_path)
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(json.dumps(asdict(report), indent=2, ensure_ascii=False), encoding="utf-8")
print(f"\n Report saved: {report_path}")
return report
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Text-to-SQL μ±κ³΅λ₯ νκ°")
parser.add_argument(
"--test-cases", default=TEST_CASES_PATH,
help=f"ν
μ€νΈ μΌμ΄μ€ JSON κ²½λ‘ (default: {TEST_CASES_PATH})"
)
parser.add_argument(
"--db", default=DB_PATH,
help=f"SQLite DB κ²½λ‘ (default: {DB_PATH})"
)
parser.add_argument(
"--report", default=None,
help="리ν¬νΈ μ μ₯ κ²½λ‘ (μ: reports/worker_a_eval_result.json)"
)
args = parser.parse_args()
run_eval(
test_cases_path=args.test_cases,
db_path=args.db,
report_path=args.report,
)
|