Spaces:
Running
Running
| """ | |
| 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 # μ‘°μ κ°λ₯ | |
| # --------------------------------------------------------------------------- | |
| # λ°μ΄ν° ν΄λμ€ | |
| # --------------------------------------------------------------------------- | |
| 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λ₯Ό μ‘μλμ§ | |
| 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, | |
| ) | |