Spaces:
Running
Running
| """ | |
| src/evaluation/eval_worker_a.py | |
| Worker A νμ΄νλΌμΈ νκ° | |
| νκ° νλͺ©: | |
| [E1] Extraction accuracy β LLMμ΄ μ¬λ°λ₯Έ action_typeμ μΆμΆνλκ° | |
| [E2] Validation accuracy β μ΄μ κ²½λ‘(run_validation_query, LLM text2sql)κ° νμ λ°ννκ³ , | |
| κ·Έ 5κ° νλ κ°μ΄ κ²°μ λ‘ μ 골λ (_hardcoded_query)κ³Ό μΌμΉνλκ°. | |
| λΉμ¦λμ€ λ£°μ΄ μ μ½λ material_name/delivery_date μ€λ₯κΉμ§ μ‘λλ€. | |
| [E3] Status accuracy β erp_action_statusκ° expectedμ μΌμΉνλκ° | |
| ν μ€νΈ μΌμ΄μ€ μμ€: | |
| data/eval/router_test_cases_gen.json (ACTION_ONLY + BOTH μΌμ΄μ€λ§ μ¬μ©) | |
| - expected_action_type : CHANGE_QTY | CHANGE_DATE | CANCEL_ITEM | OTHER | |
| - expected_erp_action_status : PENDING_APPROVAL | BLOCKED_NO_STOCK | BLOCKED_SHIPPED | β¦ | |
| μ€ν: | |
| python -m src.evaluation.eval_worker_a | |
| python -m src.evaluation.eval_worker_a --report reports/worker_a_eval_result.json | |
| python -m src.evaluation.eval_worker_a --dry-run # 첫 5κ°λ§ λΉ λ₯΄κ² ν μ€νΈ | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import logging | |
| import sys | |
| import time | |
| from dataclasses import dataclass, field, asdict | |
| from datetime import datetime, timezone | |
| from pathlib import Path | |
| from dotenv import load_dotenv | |
| load_dotenv() | |
| from src.config import get_config | |
| from src.graph.worker_a import ( | |
| extract_erp_action, | |
| resolve_quantity, | |
| check_business_rules, | |
| _build_request_context, | |
| ) | |
| from src.tools.text2sql import run_validation_query, _hardcoded_query, DB_PATH as _T2S_DB_PATH | |
| 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__) | |
| _ROOT = Path(__file__).resolve().parent.parent.parent | |
| DEFAULT_TEST_CASES = str(_ROOT / "data" / "eval" / "router_test_cases_gen.json") | |
| DEFAULT_DB = str(_ROOT / "data" / "sap_erp.db") | |
| DEFAULT_REPORT = str(_ROOT / "reports" / "worker_a_eval_result.json") | |
| # E2 κ° μΌμΉ κ²μ¬ λμ β text2sqlμ΄ λ³΄μ₯ν΄μΌ νλ 5κ° alias μ»¬λΌ | |
| REQUIRED_FIELDS = ["material_name", "quantity", "delivery_status", "delivery_date", "available_stock"] | |
| def _execute_golden(order_id: str, item_no: int) -> dict | None: | |
| """κ²°μ λ‘ μ λ νΌλ°μ€(_hardcoded_query)λ₯Ό μ€νν΄ μ λ΅ νμ λ§λ λ€. | |
| μ΄μ κ²½λ‘(run_validation_query)μ κ°μ DBλ₯Ό μ°λλ‘ text2sql.DB_PATHλ‘ μ°κ²°νλ€.""" | |
| import sqlite3 | |
| conn = sqlite3.connect(_T2S_DB_PATH) | |
| conn.row_factory = sqlite3.Row | |
| try: | |
| row = conn.execute(_hardcoded_query(str(order_id), str(item_no))).fetchone() | |
| return dict(row) if row else None | |
| finally: | |
| conn.close() | |
| def _values_equal(a, b) -> bool: | |
| """μ«μλ λΆλμμ νμ©μ€μ°¨λ‘, κ·Έ μΈλ λλ± λΉκ΅.""" | |
| if a is None and b is None: | |
| return True | |
| if isinstance(a, (int, float)) and isinstance(b, (int, float)): | |
| return abs(float(a) - float(b)) < 1e-6 | |
| return a == b | |
| def _digits_equal(a, b) -> bool: | |
| """order_id/item_noλ₯Ό μ μλ‘ μ κ·νν΄ λΉκ΅ β ν¨λ©('0000017343')Β·νμ μ°¨μ΄λ₯Ό 무μνλ€. | |
| μ΄λ νμͺ½μ΄λΌλ μ«μλ‘ ν΄μ λΆκ°(λΉκ°/None)λ©΄ False.""" | |
| try: | |
| na = int("".join(ch for ch in str(a) if ch.isdigit()) or "x") | |
| nb = int("".join(ch for ch in str(b) if ch.isdigit()) or "x") | |
| return na == nb | |
| except ValueError: | |
| return False | |
| def _compare_to_golden(actual: dict | None, golden: dict | None) -> list[dict]: | |
| """μ΄μ κ²½λ‘ κ²°κ³Ό(actual)μ μ λ΅(golden)μ 5κ° νλλ₯Ό λΉκ΅, λΆμΌμΉ λͺ©λ‘ λ°ν. | |
| golden μμ²΄κ° μμΌλ©΄(μ‘΄μ¬νμ§ μλ μ£Όλ¬Έ λ±) λΉκ΅ λμμ΄ μλλ―λ‘ λΉ λ¦¬μ€νΈ.""" | |
| if golden is None: | |
| return [] | |
| if actual is None: | |
| return [{"column": "<row>", "expected": "row matching golden", "actual": "no row returned"}] | |
| mismatches: list[dict] = [] | |
| for col in REQUIRED_FIELDS: | |
| exp, act = golden.get(col), actual.get(col) | |
| if not _values_equal(act, exp): | |
| mismatches.append({"column": col, "expected": exp, "actual": act}) | |
| return mismatches | |
| # --------------------------------------------------------------------------- | |
| # Data classes | |
| # --------------------------------------------------------------------------- | |
| class CaseResult: | |
| id: str | |
| label: str # ACTION_ONLY | BOTH | |
| user_input: str | |
| expected_action_type: str # ground truth | |
| expected_status: str # ground truth | |
| # E1 β Extraction | |
| e1_pass: bool = False # action_type μ λ΅ μΌμΉ | |
| e1_order_match: bool = False # μΆμΆ order_idκ° ground-truthμ μΌμΉ | |
| e1_item_match: bool = False # μΆμΆ item_noκ° ground-truthμ μΌμΉ | |
| extracted_action_type: str = "" # what LLM extracted | |
| extracted_order_id: str = "" | |
| extracted_item_no: str = "" | |
| extraction_error: str = "" | |
| # E2 β Validation query (real production path + value match vs golden) | |
| e2_pass: bool = False # ν λ°ν AND 5κ° νλ λͺ¨λ 골λ κ³Ό μΌμΉ | |
| e2_row_returned: bool = False # μ΄μ κ²½λ‘κ° νμ λ°ννλκ° (coverage) | |
| e2_values_match: bool = False # λ°νλ 5κ° νλ κ°μ΄ 골λ κ³Ό μΌμΉνλκ° | |
| validation_result: dict | None = None # μ΄μ κ²½λ‘(run_validation_query) κ²°κ³Ό | |
| golden_result: dict | None = None # κ²°μ λ‘ μ _hardcoded_query μ λ΅ | |
| validation_mismatches: list = field(default_factory=list) | |
| validation_error: str = "" | |
| # E3 β Status judgment | |
| e3_pass: bool = False | |
| actual_status: str = "" | |
| elapsed_ms: float = 0.0 | |
| class EvalReport: | |
| generated_at: str = "" | |
| model_worker_a: str = "" | |
| model_sql: str = "" | |
| total: int = 0 | |
| # E1 β Extraction | |
| e1_pass: int = 0 # action_type μ λ΅ | |
| e1_rate: float = 0.0 | |
| e1_order_pass: int = 0 # order_id μΆμΆ μΌμΉ | |
| e1_order_rate: float = 0.0 | |
| e1_item_pass: int = 0 # item_no μΆμΆ μΌμΉ | |
| e1_item_rate: float = 0.0 | |
| # action_type/order_id/item_noκ° λͺ¨λ λ§μ μΌμ΄μ€ (μΆμΆ μμ μ λ΅) | |
| e1_full_pass: int = 0 | |
| e1_full_rate: float = 0.0 | |
| # μΆμΆ idκ° νλ¦° μΌμ΄μ€ μμΈ | |
| e1_id_mismatch_cases: list = field(default_factory=list) | |
| # E2 β Validation (row returned AND values match golden) | |
| e2_pass: int = 0 # ν λ°ν AND κ° μΌμΉ (μ΅μ’ E2 ν΅κ³Ό) | |
| e2_rate: float = 0.0 | |
| e2_row_pass: int = 0 # ν λ°ν (coverage) | |
| e2_row_rate: float = 0.0 | |
| e2_values_pass: int = 0 # κ° μΌμΉ | |
| e2_values_rate: float = 0.0 | |
| # 골λ κ³Ό κ°μ΄ μ΄κΈλ μΌμ΄μ€ μμΈ (text2sqlμ΄ νμ 쀬μ§λ§ κ°μ΄ νλ¦° κ²½μ°) | |
| e2_mismatch_cases: list = field(default_factory=list) | |
| # E3 β Status | |
| e3_pass: int = 0 | |
| e3_rate: float = 0.0 | |
| # Action type breakdown | |
| action_type_breakdown: dict = field(default_factory=dict) | |
| # Status breakdown | |
| status_breakdown: dict = field(default_factory=dict) | |
| results: list[dict] = field(default_factory=list) | |
| # --------------------------------------------------------------------------- | |
| # Per-case evaluator | |
| # --------------------------------------------------------------------------- | |
| def _evaluate_case(tc: dict) -> CaseResult: | |
| """Run the full Worker A pipeline on one test case and score it. | |
| DB κ²½λ‘λ μ΄μ μ½λ(text2sql.DB_PATH)κ° κ²°μ νλ―λ‘ λ³λ μΈμλ₯Ό λ°μ§ μλλ€ β | |
| E2μ μ΄μ κ²½λ‘(run_validation_query)μ 골λ (_execute_golden)μ΄ κ°μ DBλ₯Ό μ΄λ€.""" | |
| label = tc.get("label", "") | |
| user_input = tc.get("user_input", "") | |
| expected_action_type = tc.get("expected_action_type", "") | |
| expected_status = tc.get("expected_erp_action_status", "") | |
| cr = CaseResult( | |
| id=tc.get("id", ""), | |
| label=label, | |
| user_input=user_input[:120], | |
| expected_action_type=expected_action_type, | |
| expected_status=expected_status, | |
| ) | |
| t0 = time.perf_counter() | |
| # erp_evidence ground-truth β E1μ order_id/item_no μΆμΆ κ²μ¦κ³Ό E2 μ λ ₯μ κ³΅μ© μ¬μ© | |
| ev = tc.get("erp_evidence") or {} | |
| gt_order = str(ev.get("order_id", "")).strip() | |
| gt_item_raw = ev.get("item_no") | |
| gt_item_int: int | None = None | |
| if gt_item_raw is not None: | |
| try: | |
| gt_item_int = int(str(gt_item_raw).strip().lstrip("0") or "0") | |
| except ValueError: | |
| gt_item_int = None | |
| # ββ E1: Parameter extraction βββββββββββββββββββββββββββββββββββββββββββββ | |
| # μ΄μ κ²½λ‘μ λμΌνκ² μ§μ μ μ΄λ©μΌ μ μ²λ¦¬ κ²°κ³Όλ₯Ό hintλ‘ κ°μ΄ λκΈ΄λ€. | |
| # (μ μ²λ¦¬ μ€ν¨ μ email_context.preprocess_ok=False, λ€μ΄μ€νΈλ¦Όμ΄ μμμ 무μ) | |
| from src.preprocess import preprocess_email | |
| email_ctx = preprocess_email(user_input).model_dump() | |
| logger.info("[%s] E1 β extracting ERP action from email β¦", cr.id) | |
| try: | |
| action = extract_erp_action(user_input, email_context=email_ctx) | |
| if action is None: | |
| cr.extraction_error = "extract_erp_action returned None" | |
| cr.extracted_action_type = "" | |
| cr.e1_pass = False | |
| else: | |
| cr.extracted_action_type = action.action_type | |
| cr.extracted_order_id = action.order_id | |
| cr.extracted_item_no = action.item_no | |
| cr.e1_pass = (action.action_type == expected_action_type) | |
| # order_id/item_noλ μ¬λ°λ₯΄κ² μΆμΆνλμ§ κ²μ¦ (ν¨λ© μ°¨μ΄λ 무μνκ³ μ«μλ‘ λΉκ΅) | |
| cr.e1_order_match = _digits_equal(action.order_id, gt_order) | |
| cr.e1_item_match = _digits_equal(action.item_no, gt_item_raw) | |
| except Exception as e: | |
| cr.extraction_error = str(e) | |
| cr.e1_pass = False | |
| logger.error("[%s] Extraction exception: %s", cr.id, e) | |
| # ββ E2: Validation query β REAL production path + value match vs golden ββ | |
| # μ΄μκ³Ό λμΌνκ² run_validation_query(LLM text2sql β 0건μ΄λ©΄ hardcoded μ¬μλ)λ₯Ό νμ | |
| # νκ°κ° νλ‘λμ μ κ·Έλλ‘ λ°μνκ² νλ€. μ λ΅(golden)μ κ²°μ λ‘ μ _hardcoded_query κ²°κ³Ό. | |
| # λ¨μ "νμ΄ λμλ"λ₯Ό λμ΄ 5κ° νλ κ° μΌμΉκΉμ§ 보λ―λ‘, λΉμ¦λμ€ λ£°μ΄ μ½μ§ μλ | |
| # material_name/delivery_date μ€λ₯λ μ¬κΈ°μ μ‘νλ€. | |
| # μΆμΆ(E1)κ³Ό λΆλ¦¬νκΈ° μν΄ μ λ ₯ order_id/item_noλ erp_evidenceμ ground-truthλ₯Ό μ΄λ€. | |
| # (gt_order/gt_item_intλ ν¨μ μλ¨μμ 1ν κ³μ°ν΄ E1 κ²μ¦κ³Ό 곡μ νλ€.) | |
| if gt_order and gt_item_int is not None: # λ κ°μ 무쑰건 μμ΄μΌ ν¨ | |
| try: | |
| logger.info("[%s] E2 β running validation query order=%s item=%s β¦", | |
| cr.id, gt_order, gt_item_int) | |
| # μ΄μ κ²½λ‘: LLMμ΄ SQLμ μμ±Β·μ€ν (0건μ΄λ©΄ λ΄λΆμ μΌλ‘ hardcoded μ¬μλ) | |
| # μ΄μκ³Ό λμΌνκ² μμ² μ»¨ν μ€νΈλ ν¨κ» λκΈ΄λ€(μΆμΆλ actionμ΄ μμ λ). | |
| req_ctx = _build_request_context(action) if action is not None else None | |
| val_result = run_validation_query(gt_order, str(gt_item_int), request_context=req_ctx) | |
| cr.validation_result = val_result | |
| cr.e2_row_returned = val_result is not None | |
| # μ λ΅: κ²°μ λ‘ μ νλμ½λ© 쿼리 κ²°κ³Ό | |
| golden = _execute_golden(gt_order, gt_item_int) | |
| cr.golden_result = golden | |
| # 5κ° νλ κ° μΌμΉ κ²μ¬ | |
| cr.validation_mismatches = _compare_to_golden(val_result, golden) | |
| cr.e2_values_match = cr.e2_row_returned and not cr.validation_mismatches | |
| cr.e2_pass = cr.e2_row_returned and cr.e2_values_match | |
| except Exception as e: | |
| cr.validation_error = str(e) | |
| cr.e2_pass = False | |
| logger.error("[%s] Validation exception: %s", cr.id, e) | |
| else: | |
| cr.validation_error = "Missing order_id/item_no in erp_evidence" | |
| cr.e2_pass = False | |
| # ββ E3: Status judgment ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Use extracted action (if available) + ground-truth validation result | |
| # so E3 measures business-rule logic, not extraction quality. | |
| if action is not None or cr.extracted_action_type: | |
| eval_action = action | |
| else: | |
| eval_action = None | |
| if eval_action is not None: | |
| try: | |
| # μ΄μ(worker_a_node)κ³Ό λμΌνκ² μλμλμ λ¨Όμ μ λκ°μΌλ‘ νμ°ν λ€ λ£° κ²μ¬. | |
| # μ΄κ² λΉ μ§λ©΄ "reduce by 50" κ°μ μλμλ μΌμ΄μ€μ μ¬κ³ /INVALID_QTY λ£°μ΄ λλ½λλ€. | |
| if eval_action.action_type == "OTHER": | |
| # worker_a_nodeμ λμΌ: OTHERλ μλ μ²λ¦¬ λΆκ° β λ£° κ²μ¬ μ μ MANUAL_REQUIRED. | |
| cr.actual_status = "MANUAL_REQUIRED" | |
| else: | |
| block_reason = resolve_quantity(eval_action, cr.validation_result) \ | |
| or check_business_rules(eval_action, cr.validation_result) | |
| cr.actual_status = block_reason if block_reason else "PENDING_APPROVAL" | |
| except Exception as e: | |
| cr.actual_status = "ERROR" | |
| logger.error("[%s] Business rule check exception: %s", cr.id, e) | |
| else: | |
| cr.actual_status = "BLOCKED_EXTRACTION_FAILED" | |
| cr.e3_pass = (cr.actual_status == expected_status) | |
| cr.elapsed_ms = (time.perf_counter() - t0) * 1000 | |
| logger.info( | |
| "[%s] done | E1=%s E2=%s E3=%s | %.0fms", | |
| cr.id, | |
| "β" if cr.e1_pass else "β", | |
| "β" if cr.e2_pass else "β", | |
| "β" if cr.e3_pass else "β", | |
| cr.elapsed_ms, | |
| ) | |
| return cr | |
| # --------------------------------------------------------------------------- | |
| # Main runner | |
| # --------------------------------------------------------------------------- | |
| def run_eval( | |
| test_cases_path: str = DEFAULT_TEST_CASES, | |
| db_path: str = DEFAULT_DB, | |
| report_path: str | None = None, | |
| dry_run: bool = False, | |
| ) -> EvalReport: | |
| cfg = get_config() | |
| all_cases: list[dict] = json.loads( | |
| Path(test_cases_path).read_text(encoding="utf-8") | |
| ) | |
| # Filter to only ACTION_ONLY and BOTH (QA_ONLY has no ERP action) | |
| cases = [c for c in all_cases if c.get("label") in ("ACTION_ONLY", "BOTH")] | |
| logger.info( | |
| "Loaded %d total cases β %d ACTION/BOTH cases selected", | |
| len(all_cases), len(cases), | |
| ) | |
| if dry_run: | |
| cases = cases[:5] | |
| logger.info("Dry-run mode: using first %d cases only", len(cases)) | |
| report = EvalReport( | |
| generated_at = datetime.now(timezone.utc).isoformat(), | |
| model_worker_a = cfg.models.worker_a.name, | |
| model_sql = cfg.models.worker_a_sql.name, | |
| total = len(cases), | |
| ) | |
| for tc in cases: | |
| cr = _evaluate_case(tc) | |
| report.results.append(asdict(cr)) | |
| if cr.e1_pass: report.e1_pass += 1 | |
| if cr.e1_order_match: report.e1_order_pass += 1 | |
| if cr.e1_item_match: report.e1_item_pass += 1 | |
| if cr.e1_pass and cr.e1_order_match and cr.e1_item_match: | |
| report.e1_full_pass += 1 | |
| if cr.e2_pass: report.e2_pass += 1 | |
| if cr.e2_row_returned: report.e2_row_pass += 1 | |
| if cr.e2_values_match: report.e2_values_pass += 1 | |
| if cr.e3_pass: report.e3_pass += 1 | |
| # μΆμΆ id(order_id/item_no)κ° ground-truthμ μ΄κΈλ μΌμ΄μ€ κΈ°λ‘ | |
| if cr.extracted_action_type and (not cr.e1_order_match or not cr.e1_item_match): | |
| gt_ev = tc.get("erp_evidence") or {} | |
| report.e1_id_mismatch_cases.append({ | |
| "id": cr.id, | |
| "expected_order": gt_ev.get("order_id"), | |
| "extracted_order": cr.extracted_order_id, | |
| "order_match": cr.e1_order_match, | |
| "expected_item": gt_ev.get("item_no"), | |
| "extracted_item": cr.extracted_item_no, | |
| "item_match": cr.e1_item_match, | |
| }) | |
| # text2sqlμ΄ νμ λ°ννμ§λ§ κ°μ΄ 골λ κ³Ό μ΄κΈλ μΌμ΄μ€ κΈ°λ‘ | |
| if cr.e2_row_returned and cr.validation_mismatches: | |
| report.e2_mismatch_cases.append({ | |
| "id": cr.id, | |
| "order_id": cr.extracted_order_id or (tc.get("erp_evidence") or {}).get("order_id"), | |
| "item_no": cr.extracted_item_no, | |
| "actual_result": cr.validation_result, | |
| "golden_result": cr.golden_result, | |
| "mismatches": cr.validation_mismatches, | |
| }) | |
| # Action type breakdown | |
| at = cr.expected_action_type or "UNKNOWN" | |
| bp = report.action_type_breakdown.setdefault(at, {"total": 0, "e1": 0, "e3": 0}) | |
| bp["total"] += 1 | |
| if cr.e1_pass: bp["e1"] += 1 | |
| if cr.e3_pass: bp["e3"] += 1 | |
| # Status breakdown | |
| es = cr.expected_status or "UNKNOWN" | |
| sb = report.status_breakdown.setdefault(es, {"total": 0, "e3": 0}) | |
| sb["total"] += 1 | |
| if cr.e3_pass: sb["e3"] += 1 | |
| n = report.total | |
| report.e1_rate = round(report.e1_pass / n, 4) if n else 0.0 | |
| report.e1_order_rate = round(report.e1_order_pass / n, 4) if n else 0.0 | |
| report.e1_item_rate = round(report.e1_item_pass / n, 4) if n else 0.0 | |
| report.e1_full_rate = round(report.e1_full_pass / n, 4) if n else 0.0 | |
| report.e2_rate = round(report.e2_pass / n, 4) if n else 0.0 | |
| report.e2_row_rate = round(report.e2_row_pass / n, 4) if n else 0.0 | |
| report.e2_values_rate = round(report.e2_values_pass / n, 4) if n else 0.0 | |
| report.e3_rate = round(report.e3_pass / n, 4) if n else 0.0 | |
| # ββ Summary printout βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| print("\n" + "=" * 65) | |
| print(" Worker A Evaluation Results") | |
| print("=" * 65) | |
| print(f" Worker A model : {report.model_worker_a}") | |
| print(f" SQL model : {report.model_sql}") | |
| print(f" Total cases : {n} (ACTION_ONLY + BOTH only)") | |
| print("-" * 65) | |
| print(f" E1 Extraction : {report.e1_pass}/{n} ({report.e1_rate*100:.1f}%) " | |
| f"[action_type correct]") | |
| print(f" ββ order_id : {report.e1_order_pass}/{n} ({report.e1_order_rate*100:.1f}%)") | |
| print(f" ββ item_no : {report.e1_item_pass}/{n} ({report.e1_item_rate*100:.1f}%)") | |
| print(f" ββ all 3 β : {report.e1_full_pass}/{n} ({report.e1_full_rate*100:.1f}%) " | |
| f"[action_type + order_id + item_no]") | |
| print(f" E2 Validation : {report.e2_pass}/{n} ({report.e2_rate*100:.1f}%) " | |
| f"[real text2sql: row + values match golden]") | |
| print(f" ββ row returned : {report.e2_row_pass}/{n} ({report.e2_row_rate*100:.1f}%)") | |
| print(f" ββ values match : {report.e2_values_pass}/{n} ({report.e2_values_rate*100:.1f}%)") | |
| print(f" E3 Status : {report.e3_pass}/{n} ({report.e3_rate*100:.1f}%) " | |
| f"[erp_action_status correct]") | |
| print("=" * 65) | |
| # Action type breakdown | |
| print("\n E1 breakdown by action_type:") | |
| for at, bp in sorted(report.action_type_breakdown.items()): | |
| rate = bp["e1"] / bp["total"] * 100 if bp["total"] else 0 | |
| print(f" {at:<15}: {bp['e1']}/{bp['total']} ({rate:.0f}%)") | |
| # Status breakdown | |
| print("\n E3 breakdown by expected_status:") | |
| for es, sb in sorted(report.status_breakdown.items()): | |
| rate = sb["e3"] / sb["total"] * 100 if sb["total"] else 0 | |
| print(f" {es:<30}: {sb['e3']}/{sb['total']} ({rate:.0f}%)") | |
| # Failure details | |
| failed_e1 = [r for r in report.results if not r["e1_pass"]] | |
| failed_e3 = [r for r in report.results if not r["e3_pass"]] | |
| if failed_e1: | |
| print(f"\n E1 action_type failures ({len(failed_e1)}):") | |
| for r in failed_e1[:10]: | |
| print(f" [{r['id']}] expected={r['expected_action_type']!r} " | |
| f"got={r['extracted_action_type']!r} " | |
| f"err={r['extraction_error'][:60]}") | |
| # μΆμΆ id(order_id/item_no) λΆμΌμΉ μμΈ | |
| if report.e1_id_mismatch_cases: | |
| print(f"\n E1 id mismatches ({len(report.e1_id_mismatch_cases)}) " | |
| f"[order_id/item_no extracted β ground-truth]:") | |
| for c in report.e1_id_mismatch_cases[:10]: | |
| ord_flag = "" if c["order_match"] else " βorder" | |
| item_flag = "" if c["item_match"] else " βitem" | |
| print(f" [{c['id']}] order: expected={c['expected_order']!r} got={c['extracted_order']!r}" | |
| f"{ord_flag} | item: expected={c['expected_item']!r} got={c['extracted_item']!r}{item_flag}") | |
| if failed_e3: | |
| print(f"\n E3 failures ({len(failed_e3)}):") | |
| for r in failed_e3[:10]: | |
| print(f" [{r['id']}] expected={r['expected_status']!r} " | |
| f"got={r['actual_status']!r}") | |
| # text2sqlμ΄ νμ 쀬μ§λ§ κ°μ΄ 골λ κ³Ό μ΄κΈλ μΌμ΄μ€ (silent error β LLM SQLμ΄ νλ¦° μ νΈ) | |
| if report.e2_mismatch_cases: | |
| print(f"\n E2 value mismatches ({len(report.e2_mismatch_cases)}) " | |
| f"[text2sql returned a row but values differ from golden]:") | |
| for c in report.e2_mismatch_cases[:10]: | |
| print(f" [{c['id']}] order={c['order_id']} item={c['item_no']}") | |
| for m in c["mismatches"]: | |
| print(f" {m['column']}: expected={m['expected']!r} actual={m['actual']!r}") | |
| # ββ Save report ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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 | |
| # --------------------------------------------------------------------------- | |
| # CLI | |
| # --------------------------------------------------------------------------- | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser(description="Worker A pipeline evaluation") | |
| parser.add_argument( | |
| "--test-cases", default=DEFAULT_TEST_CASES, | |
| help=f"Test cases JSON path (default: {DEFAULT_TEST_CASES})" | |
| ) | |
| parser.add_argument( | |
| "--db", default=DEFAULT_DB, | |
| help=f"SQLite DB path (default: {DEFAULT_DB})" | |
| ) | |
| parser.add_argument( | |
| "--report", default=DEFAULT_REPORT, | |
| help=f"Report output path (default: {DEFAULT_REPORT})" | |
| ) | |
| parser.add_argument( | |
| "--dry-run", action="store_true", | |
| help="Run only first 5 cases for quick sanity check" | |
| ) | |
| args = parser.parse_args() | |
| run_eval( | |
| test_cases_path=args.test_cases, | |
| db_path=args.db, | |
| report_path=args.report, | |
| dry_run=args.dry_run, | |
| ) | |