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