SAP-ERP-AI-Agent / src /evaluation /eval_text2sql.py
daisysooyeon's picture
deploy: SAP ERP AI Agent (HF Spaces docker)
50efdc6
Raw
History Blame Contribute Delete
24.1 kB
"""
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,
)