| from __future__ import annotations |
| """ |
| 리ν©ν λ§ νκ· μ€λ
μ· νλμ€ (Step A). |
| |
| config-driven μλ ΄ 리ν©ν λ§(Phase 3~4)μ μμ λ§. |
| "리ν©ν λ§ μ == ν"λ₯Ό κΈ°κ³μ μΌλ‘ μ¦λͺ
νλ€ β 3μλλ¦¬μ€ μ 체 intent score + feature dict 1:1 λΉκ΅. |
| |
| λ μ€λ
μ· ν¨λ°λ¦¬: |
| 1. scores : seed_datasetμ (μ€λ³΅ μ κ±°λ) survey_answers β infer_batch β {intent_id: score}. |
| build_batch_features(Index/Score) + rule_predict + model_predict μ 체 κ²½λ‘ μ»€λ². |
| 2. features : κ³ μ ν©μ± μ΄λ²€νΈ μνμ€ β engine.pattern_features/event_features dict. |
| pattern/event μΆμΆκΈ°(μν°ν°βκ·Έλ£ΉΒ·νλκ·Έ λ§΅) 컀λ². (νμμ€ν¬νλ₯ νλλ λΉκ΅ μ μΈ) |
| |
| μ¬μ©λ²: |
| python scripts/regression_snapshot.py --save # νμ¬ λμμ baselineμΌλ‘ μ μ₯ |
| python scripts/regression_snapshot.py --check # νμ¬ λμ vs baseline (λΆμΌμΉ μ exit 1) |
| |
| baseline: .documents/_snapshots/{scenario_id}.json (gitignore κ²½λ‘) |
| """ |
| import argparse |
| import json |
| import sys |
| from pathlib import Path |
|
|
| sys.path.insert(0, str(Path(__file__).parent.parent)) |
|
|
| from core.engines import available_scenarios, get_engine, config |
| from core.extractor import get_extractor |
| from core.inference import infer_batch |
|
|
| _SNAPSHOT_DIR = Path(__file__).parent.parent / ".documents" / "_snapshots" |
|
|
| |
| _VOLATILE_KEYS = {"last_event_at"} |
|
|
| _ROUND = 6 |
|
|
|
|
| |
| def _norm(v: object) -> object: |
| """λΉκ΅ μμ ν: floatλ _ROUND μ리 λ°μ¬λ¦Ό, κ·Έ μΈ(bool ν¬ν¨)λ κ·Έλλ‘.""" |
| if isinstance(v, float): |
| return round(v, _ROUND) |
| if isinstance(v, bool): |
| return v |
| return v |
|
|
|
|
| def _norm_dict(d: dict) -> dict: |
| """dictλ₯Ό ν€ μ λ ¬Β·κ° μ κ·ννκ³ νλ°μ± ν€(_VOLATILE_KEYS)λ μ μΈ.""" |
| return {k: _norm(v) for k, v in sorted(d.items()) if k not in _VOLATILE_KEYS} |
|
|
|
|
| |
| def _unique_answers(scenario_id: str) -> list[dict]: |
| """seed_datasetμ survey_answersλ₯Ό μ€λ³΅ μ κ±°νμ¬ κ²°μ μ μμλ‘ λ°ν.""" |
| path = Path(__file__).parent.parent / "scenarios" / scenario_id / "seed_dataset.json" |
| data = json.loads(path.read_text(encoding="utf-8")) |
| seen: dict[tuple, dict] = {} |
| for s in data["samples"]: |
| ans = s["survey_answers"] |
| key = tuple(sorted(ans.items())) |
| seen.setdefault(key, ans) |
| |
| return [seen[k] for k in sorted(seen.keys())] |
|
|
|
|
| def _scores_snapshot(scenario_id: str) -> list[dict]: |
| """μ€λ³΅ μ κ±°λ survey_answersλ§λ€ infer_batch β {answers, intentλ³ final_score}.""" |
| out = [] |
| for ans in _unique_answers(scenario_id): |
| _, scores = infer_batch(ans, scenario_id) |
| out.append({ |
| "answers": {k: ans[k] for k in sorted(ans)}, |
| "scores": {s.intent_id: round(s.final_score, _ROUND) for s in scores}, |
| }) |
| return out |
|
|
|
|
| |
| def _synthetic_sequence(scenario_id: str) -> list[tuple[str, str]]: |
| """behavior_signalsμ entity μ 체λ₯Ό (click, entity) μ΄λ²€νΈλ‘ β λ§€ν ν
μ΄λΈ μ μ 컀λ². |
| λ§μ§λ§ entityλ₯Ό ν λ² λ λ°λ³΅ν΄ repeated/dominant μ§κ³λ μκ·Ή.""" |
| entities = sorted(config.get_behavior_signals(scenario_id).keys()) |
| seq = [("click", e) for e in entities] |
| if entities: |
| seq.append(("click", entities[0])) |
| return seq |
|
|
|
|
| def _features_snapshot(scenario_id: str) -> dict: |
| """ν©μ± μ΄λ²€νΈ μνμ€λ₯Ό μ£Όμ
ν΄ empty/pattern/event Feature dictλ₯Ό μ€λ
μ·(νμμ€ν¬ν μ μΈ).""" |
| engine = get_engine(scenario_id) |
| ext = get_extractor() |
| session = f"__snapshot__{scenario_id}" |
| ext.reset(session) |
| for event_type, entity in _synthetic_sequence(scenario_id): |
| ext.add_event(session, event_type, entity) |
|
|
| snap = { |
| "empty_pattern": _norm_dict(engine.empty_pattern_features()), |
| "empty_event": _norm_dict(engine.empty_event_features()), |
| "pattern": _norm_dict(engine.pattern_features(session)), |
| "event": _norm_dict(engine.event_features(session)), |
| } |
| ext.reset(session) |
| return snap |
|
|
|
|
| |
| def _build(scenario_id: str) -> dict: |
| """ν μλ리μ€μ μ 체 μ€λ
μ·(scores + features) μμ±.""" |
| return { |
| "scenario_id": scenario_id, |
| "scores": _scores_snapshot(scenario_id), |
| "features": _features_snapshot(scenario_id), |
| } |
|
|
|
|
| |
| def _diff(old: dict, new: dict, scenario_id: str) -> list[str]: |
| """baseline(old) vs νμ¬(new) μ€λ
μ· λΉκ΅ β λΆμΌμΉ λ©μμ§ λ¦¬μ€νΈ(λΉ λ¦¬μ€νΈλ©΄ 무μμ).""" |
| errs: list[str] = [] |
|
|
| |
| for fam in ("empty_pattern", "empty_event", "pattern", "event"): |
| o, n = old["features"].get(fam, {}), new["features"].get(fam, {}) |
| for k in sorted(set(o) | set(n)): |
| if o.get(k) != n.get(k): |
| errs.append(f"[{scenario_id}] features.{fam}.{k}: {o.get(k)} β {n.get(k)}") |
|
|
| |
| o_cases, n_cases = old["scores"], new["scores"] |
| if len(o_cases) != len(n_cases): |
| errs.append(f"[{scenario_id}] scores μΌμ΄μ€ μ: {len(o_cases)} β {len(n_cases)}") |
| for i, (oc, nc) in enumerate(zip(o_cases, n_cases)): |
| if oc["answers"] != nc["answers"]: |
| errs.append(f"[{scenario_id}] scores[{i}] answers λΆμΌμΉ") |
| continue |
| os_, ns_ = oc["scores"], nc["scores"] |
| for iid in sorted(set(os_) | set(ns_)): |
| if os_.get(iid) != ns_.get(iid): |
| errs.append(f"[{scenario_id}] scores[{i}].{iid}: {os_.get(iid)} β {ns_.get(iid)}") |
| return errs |
|
|
|
|
| |
| def main() -> None: |
| """--save: baseline μ μ₯ / --check: baseline λλΉ κ²μ¦(λΆμΌμΉ μ exit 1).""" |
| ap = argparse.ArgumentParser() |
| g = ap.add_mutually_exclusive_group(required=True) |
| g.add_argument("--save", action="store_true", help="baseline μ μ₯") |
| g.add_argument("--check", action="store_true", help="baseline λλΉ κ²μ¦") |
| ap.add_argument("--scenarios", nargs="*", default=None, help="λμ μλ리μ€(κΈ°λ³Έ: μ 체)") |
| args = ap.parse_args() |
|
|
| _SNAPSHOT_DIR.mkdir(parents=True, exist_ok=True) |
| scenarios = args.scenarios or available_scenarios() |
|
|
| if args.save: |
| for sid in scenarios: |
| snap = _build(sid) |
| path = _SNAPSHOT_DIR / f"{sid}.json" |
| path.write_text(json.dumps(snap, ensure_ascii=False, indent=2, sort_keys=True), encoding="utf-8") |
| print(f"saved {path} (scores={len(snap['scores'])} cases)") |
| return |
|
|
| |
| all_errs: list[str] = [] |
| for sid in scenarios: |
| path = _SNAPSHOT_DIR / f"{sid}.json" |
| if not path.exists(): |
| print(f"β baseline μμ: {path} (λ¨Όμ --save)") |
| sys.exit(2) |
| old = json.loads(path.read_text(encoding="utf-8")) |
| new = _build(sid) |
| errs = _diff(old, new, sid) |
| if errs: |
| all_errs.extend(errs) |
| print(f"{'β' if errs else 'β
'} {sid}: {len(errs)} diff (scores={len(new['scores'])} cases)") |
|
|
| if all_errs: |
| print("\nββ λΆμΌμΉ μμΈ (μ΅λ 50건) ββ") |
| for e in all_errs[:50]: |
| print(" " + e) |
| print(f"\nμ΄ {len(all_errs)}건 λΆμΌμΉ β νκ· λ°μ") |
| sys.exit(1) |
| print("\nβ
무μμ β μ μλλ¦¬μ€ scoreΒ·feature 1:1 μΌμΉ") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|