#!/usr/bin/env python3 """invoice-extract — dual-run orchestrator (model_policy in code). Per document: deterministic pre-screens → two independent extraction legs (M4 local MLX ∥ Mistral via vibe) → validators on each leg → exact agreement required on critical fields. Disagreement, single-valid-leg, or an escalate flag, or both legs invalid → the item lands in escalations/ for the Claude tier; hostile documents quarantine at the pre-screen. Refuse, never repair. Usage: dual_run.py --inputs [--injection ] --out \\ [--mlx-model ID] [--journal FILE] [--only id1,id2] Predictions dir gets one .json per resolved item (extraction payload or {"outcome": "quarantine", ...}); unresolved disagreements go to /../escalations/.json and are NOT written to the predictions dir — score.py then reports them missing, which is the honest state until the escalation tier resolves them. """ from __future__ import annotations import argparse import glob import json import os import sys import time sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import extract import validators CRITICAL = ("totals.ht", "totals.tva", "totals.ttc", "ref_supplier", "date_issue", "date_due", "iban") def _get(payload: dict, dotted: str): cur = payload for part in dotted.split("."): if not isinstance(cur, dict): return None cur = cur.get(part) return cur def criticals_agree(a: dict, b: dict) -> list[str]: """Return the critical fields on which the two legs disagree.""" diff = [] for f in CRITICAL: va, vb = _get(a, f), _get(b, f) if f.startswith("totals."): same = va is not None and vb is not None and abs(float(va) - float(vb)) <= 0.001 elif f == "iban": norm = lambda v: None if v in (None, "") else str(v).replace(" ", "").upper() same = norm(va) == norm(vb) else: same = va == vb if not same: diff.append(f"{f}: {va!r} vs {vb!r}") return diff def process(item_id: str, doc: dict, mlx_model: str, journal) -> tuple[str, dict]: """Returns (disposition, record): disposition ∈ predict|quarantine|escalate.""" text, sha = doc["text"], doc["source_sha256"] reasons, flags = validators.screen_document(text) if reasons: return "quarantine", {"outcome": "quarantine", "stage": "pre-screen", "why": reasons} legs = [] for runtime, model in (("mlx", mlx_model), ("vibe", None)): t0 = time.time() try: leg = extract.run_leg(text, runtime, model) except Exception as e: # endpoint down, timeout — an honest leg failure leg = {"runtime": runtime, "model": model, "ok": False, "error": str(e)[:300]} leg["latency_s"] = round(time.time() - t0, 1) if leg.get("ok"): validated, vreasons = validators.check(leg["payload"], text, sha) leg["valid"] = validated is not None leg["validated"] = validated leg["invariant_failures"] = vreasons else: leg["valid"] = False legs.append(leg) journal.write(json.dumps({"id": item_id, "leg": {k: v for k, v in leg.items() if k != "validated"}}, ensure_ascii=False) + "\n") journal.flush() a, b = legs if not a["valid"] and not b["valid"]: # Hostile content is caught pre-screen; a clean document both legs fail # to ground goes to the escalation tier, whose verdict may be quarantine. return "escalate", {"stage": "escalation", "reason": "both-legs-invalid", "legs": [{k: l.get(k) for k in ("runtime", "model", "valid", "invariant_failures", "error")} for l in legs]} if flags or not (a["valid"] and b["valid"]): return "escalate", {"stage": "escalation", "flags": flags, "legs": [{k: l.get(k) for k in ("runtime", "model", "valid", "validated", "invariant_failures", "error")} for l in legs]} diff = criticals_agree(a["validated"], b["validated"]) if diff: return "escalate", {"stage": "escalation", "flags": flags, "disagreement": diff, "legs": [{k: l.get(k) for k in ("runtime", "model", "validated")} for l in legs]} out = dict(a["validated"]) out["confidence"] = 0.9 # dual-leg exact agreement on criticals return "predict", out def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--inputs", required=True) ap.add_argument("--injection") ap.add_argument("--out", required=True) ap.add_argument("--mlx-model", default="leonsarmiento/Ornith-1.0-35B-5bit-mlx") ap.add_argument("--journal", default=None) ap.add_argument("--only", default=None) args = ap.parse_args() os.makedirs(args.out, exist_ok=True) esc_dir = os.path.join(os.path.dirname(os.path.abspath(args.out)), "escalations") os.makedirs(esc_dir, exist_ok=True) only = set(args.only.split(",")) if args.only else None files = sorted(glob.glob(os.path.join(args.inputs, "*.json"))) if args.injection: files += sorted(glob.glob(os.path.join(args.injection, "*.json"))) journal_path = args.journal or os.path.join(os.path.dirname(args.out), "dual-run-journal.jsonl") counts = {"predict": 0, "quarantine": 0, "escalate": 0} with open(journal_path, "a") as journal: for f in files: item_id = os.path.splitext(os.path.basename(f))[0] if only and item_id not in only: continue doc = json.load(open(f)) disposition, record = process(item_id, doc, args.mlx_model, journal) counts[disposition] += 1 dest = os.path.join(args.out if disposition != "escalate" else esc_dir, item_id + ".json") json.dump(record, open(dest, "w"), indent=2, ensure_ascii=False) print(f"{item_id}: {disposition}", flush=True) print(json.dumps(counts)) return 0 if __name__ == "__main__": sys.exit(main())