ui: pipeline trace UI + verifier panel + insights banner
Browse filesThe Space now surfaces every MIE layer's intermediate state:
- Insights banner: punchy headline (CO2e avoided, guardrail fires, hazards)
- Layer A validator (D012): JSON shape + enum
- Layer B calibration (D015): raw vs calibrated table
- Layer C hazard auto-flagger (D019): before/added/after diff
- Layer D do_not guardrail (D018): proposed vs safe-default
- Verifier panel: independent attestation via matter.verifier
- Passport JSON
Backed by engine.infer_with_trace() which captures per-layer state
without changing infer()'s API. Demo mode shows passport + verifier
(no trace, since model didn't run); Live mode shows the full trace.
- app.py +267 -41
- matter/engine.py +138 -0
- matter/verifier.py +341 -0
app.py
CHANGED
|
@@ -18,8 +18,11 @@ import gradio as gr
|
|
| 18 |
|
| 19 |
from matter.engine import MIE, CaptureInput, MIEError
|
| 20 |
from matter.heads import HEADS
|
|
|
|
| 21 |
from transformers_runtime import TransformersRuntime
|
| 22 |
|
|
|
|
|
|
|
| 23 |
ROOT = Path(__file__).parent
|
| 24 |
EXAMPLES_DIR = ROOT / "examples"
|
| 25 |
SPEC_EXAMPLES = ROOT / "spec" / "examples"
|
|
@@ -55,27 +58,95 @@ def get_engine() -> MIE:
|
|
| 55 |
return MIE(runtime=_runtime, on_device=True)
|
| 56 |
|
| 57 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
def render_summary(p: dict) -> str:
|
|
|
|
| 59 |
ident = p.get("identity", {})
|
| 60 |
state = p.get("state", {})
|
| 61 |
nba = p.get("next_best_action", {})
|
| 62 |
routing = p.get("routing", {})
|
| 63 |
prov = p.get("provenance", {})
|
| 64 |
-
val = (p.get("value") or {}).get("environmental") or {}
|
| 65 |
|
| 66 |
hazards = state.get("hazard_flags") or []
|
| 67 |
do_not = nba.get("do_not") or []
|
| 68 |
|
| 69 |
badge = "🟢 clear"
|
| 70 |
if nba.get("fallback_used"):
|
| 71 |
-
badge = "🟡 guardrail fired
|
| 72 |
if any(h in {"biohazard", "sharps_injury_risk", "thermal_runaway_risk"} for h in hazards):
|
| 73 |
badge = "🔴 hazard"
|
| 74 |
|
| 75 |
-
|
| 76 |
-
f"### {ident.get('class', '?')} · _{ident.get('subclass'
|
| 77 |
"",
|
| 78 |
-
f"**Status**
|
| 79 |
"",
|
| 80 |
"| | |",
|
| 81 |
"|---|---|",
|
|
@@ -84,51 +155,152 @@ def render_summary(p: dict) -> str:
|
|
| 84 |
f"| **Do not** | "
|
| 85 |
+ (", ".join(f"`{x}`" for x in do_not) if do_not else "_none_") + " |",
|
| 86 |
f"| **Confidence** | `{ident.get('confidence', 0):.3f}` "
|
| 87 |
-
+ ("
|
| 88 |
f"| **Hazards** | "
|
| 89 |
+ (", ".join(f"`{h}`" for h in hazards) if hazards else "_none_") + " |",
|
| 90 |
f"| **Condition** | `{state.get('condition', '?')}` |",
|
| 91 |
f"| **Jurisdiction** | {routing.get('jurisdiction', '?')} |",
|
|
|
|
| 92 |
]
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
|
| 102 |
-
def
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
return "\n".join([
|
| 108 |
-
"**
|
| 109 |
"",
|
| 110 |
-
"|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
"|---|---|",
|
| 112 |
-
"|
|
| 113 |
-
"
|
| 114 |
-
"|
|
| 115 |
-
+ (
|
| 116 |
-
"|
|
| 117 |
-
+ ("
|
| 118 |
])
|
| 119 |
|
| 120 |
|
| 121 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
fname = DEMO_PASSPORTS.get(head, DEMO_PASSPORTS["domestic"])
|
| 123 |
p = json.loads((SPEC_EXAMPLES / fname).read_text())
|
| 124 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
|
| 127 |
-
def run_live(image_path: str | None, head: str, jurisdiction: str) -> tuple
|
| 128 |
if image_path is None:
|
| 129 |
return (
|
| 130 |
-
"⚠️ Upload an image first, or switch to **Demo** mode
|
| 131 |
-
"", "",
|
| 132 |
)
|
| 133 |
try:
|
| 134 |
engine = get_engine()
|
|
@@ -136,21 +308,30 @@ def run_live(image_path: str | None, head: str, jurisdiction: str) -> tuple[str,
|
|
| 136 |
image_path=Path(image_path),
|
| 137 |
jurisdiction=jurisdiction.strip() or None,
|
| 138 |
)
|
| 139 |
-
passport = engine.
|
| 140 |
p = passport.to_dict()
|
| 141 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
except MIEError as e:
|
| 143 |
return (
|
| 144 |
-
f"### ❌ MIE
|
| 145 |
-
"
|
| 146 |
-
"", "",
|
| 147 |
)
|
| 148 |
except Exception as e:
|
| 149 |
return (
|
| 150 |
f"### ❌ Runtime error\n\n```\n{e.__class__.__name__}: {e}\n```\n\n"
|
| 151 |
-
"_If this is the first call after a cold start, the GPU worker is still loading Gemma 4 (≈30s). Try again
|
| 152 |
f"<details><summary>traceback</summary>\n\n```\n{traceback.format_exc()}\n```\n</details>",
|
| 153 |
-
"",
|
| 154 |
)
|
| 155 |
|
| 156 |
|
|
@@ -519,6 +700,32 @@ html, body, gradio-app, .gradio-container {
|
|
| 519 |
/* ===== Selection ===== */
|
| 520 |
::selection { background: rgba(0, 217, 126, 0.35); color: white; }
|
| 521 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 522 |
/* ===== Catch-all readability ===== */
|
| 523 |
.gradio-container { color: #f1faf4; }
|
| 524 |
.gradio-container input::placeholder,
|
|
@@ -587,10 +794,28 @@ with gr.Blocks(title="Matter — Material Intelligence") as demo:
|
|
| 587 |
)
|
| 588 |
|
| 589 |
with gr.Column(scale=7):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 590 |
gr.Markdown("### Passport")
|
| 591 |
summary_out = gr.Markdown(value="_Pick a mode and press_ **Generate Passport**.")
|
| 592 |
-
|
| 593 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 594 |
json_out = gr.Code(language="json", label=None, lines=22)
|
| 595 |
|
| 596 |
gr.Markdown(
|
|
@@ -603,7 +828,8 @@ with gr.Blocks(title="Matter — Material Intelligence") as demo:
|
|
| 603 |
run_btn.click(
|
| 604 |
dispatch,
|
| 605 |
inputs=[mode_in, image_in, head_in, juris_in],
|
| 606 |
-
outputs=[summary_out,
|
|
|
|
| 607 |
)
|
| 608 |
|
| 609 |
|
|
|
|
| 18 |
|
| 19 |
from matter.engine import MIE, CaptureInput, MIEError
|
| 20 |
from matter.heads import HEADS
|
| 21 |
+
from matter.verifier import Verifier
|
| 22 |
from transformers_runtime import TransformersRuntime
|
| 23 |
|
| 24 |
+
verifier = Verifier()
|
| 25 |
+
|
| 26 |
ROOT = Path(__file__).parent
|
| 27 |
EXAMPLES_DIR = ROOT / "examples"
|
| 28 |
SPEC_EXAMPLES = ROOT / "spec" / "examples"
|
|
|
|
| 58 |
return MIE(runtime=_runtime, on_device=True)
|
| 59 |
|
| 60 |
|
| 61 |
+
# =====================================================================
|
| 62 |
+
# Rendering helpers — every section returns a Markdown string.
|
| 63 |
+
# Outputs in order:
|
| 64 |
+
# insights, summary, raw, validator, calibration, hazards, guardrail,
|
| 65 |
+
# verifier, json
|
| 66 |
+
# =====================================================================
|
| 67 |
+
|
| 68 |
+
EMPTY_OUTPUT = ("",) * 9
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def render_insights(p: dict, trace: dict | None) -> str:
|
| 72 |
+
"""Top banner — the punchy 'value generated' headline.
|
| 73 |
+
|
| 74 |
+
Pulls 1–3 high-impact statements out of the Passport. Examples:
|
| 75 |
+
🌱 0.0315 kg CO₂e avoided — routes to blue_bin_recycle
|
| 76 |
+
🛡️ Guardrail caught a sharps misroute (CRITICAL)
|
| 77 |
+
⚠️ Biohazard auto-flagged
|
| 78 |
+
"""
|
| 79 |
+
lines: list[str] = []
|
| 80 |
+
nba = p.get("next_best_action", {})
|
| 81 |
+
state = p.get("state", {})
|
| 82 |
+
routing = p.get("routing", {})
|
| 83 |
+
val = (p.get("value") or {}).get("environmental") or {}
|
| 84 |
+
hazards = state.get("hazard_flags") or []
|
| 85 |
+
|
| 86 |
+
# Guardrail save (highest priority — safety win)
|
| 87 |
+
if nba.get("fallback_used") and trace and trace.get("guardrail", {}).get("fired"):
|
| 88 |
+
g = trace["guardrail"]
|
| 89 |
+
sev = (g.get("severity") or "").upper()
|
| 90 |
+
sev_emoji = "🚨" if sev == "CRITICAL" else "🛡️"
|
| 91 |
+
lines.append(
|
| 92 |
+
f"{sev_emoji} **Guardrail fired ({sev or 'high severity'})** — "
|
| 93 |
+
f"unsafe action `{g['proposed_action']}` overridden to `{g['safe_default']}`"
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
# Hazards detected
|
| 97 |
+
canonical = {"biohazard", "sharps_injury_risk", "thermal_runaway_risk",
|
| 98 |
+
"lead_toxicity", "acid_corrosion"}
|
| 99 |
+
flagged = [h for h in hazards if h in canonical]
|
| 100 |
+
if flagged:
|
| 101 |
+
lines.append(f"⚠️ **Hazards detected**: {', '.join(f'`{h}`' for h in flagged)}")
|
| 102 |
+
|
| 103 |
+
# Hazards added by auto-flagger (different from 'detected' — these are the ones the model missed)
|
| 104 |
+
if trace and trace.get("hazards", {}).get("added"):
|
| 105 |
+
lines.append(f"🔍 **Auto-flagger added** missing hazards: {', '.join(f'`{h}`' for h in trace['hazards']['added'])}")
|
| 106 |
+
|
| 107 |
+
# Environmental impact
|
| 108 |
+
if val.get("co2e_avoided_kg") is not None:
|
| 109 |
+
co2 = val["co2e_avoided_kg"]
|
| 110 |
+
lines.append(f"🌱 **{co2} kg CO₂e avoided** by routing to `{nba.get('primary', '?')}`")
|
| 111 |
+
|
| 112 |
+
# Routing / jurisdiction
|
| 113 |
+
if routing.get("jurisdiction"):
|
| 114 |
+
lines.append(f"📋 **Jurisdiction**: {routing['jurisdiction']}")
|
| 115 |
+
|
| 116 |
+
# Calibration shift (if Live)
|
| 117 |
+
if trace:
|
| 118 |
+
cr = trace["calibration"]["raw"]["identity"]
|
| 119 |
+
cc = trace["calibration"]["calibrated"]["identity"]
|
| 120 |
+
if abs(cr - cc) > 0.05:
|
| 121 |
+
arrow = "↓" if cc < cr else "↑"
|
| 122 |
+
lines.append(f"📊 **Confidence calibrated**: {cr:.2f} {arrow} {cc:.2f} (histogram-corrected)")
|
| 123 |
+
|
| 124 |
+
if not lines:
|
| 125 |
+
return ""
|
| 126 |
+
return "\n\n".join(lines)
|
| 127 |
+
|
| 128 |
+
|
| 129 |
def render_summary(p: dict) -> str:
|
| 130 |
+
"""The Passport headline card — always visible."""
|
| 131 |
ident = p.get("identity", {})
|
| 132 |
state = p.get("state", {})
|
| 133 |
nba = p.get("next_best_action", {})
|
| 134 |
routing = p.get("routing", {})
|
| 135 |
prov = p.get("provenance", {})
|
|
|
|
| 136 |
|
| 137 |
hazards = state.get("hazard_flags") or []
|
| 138 |
do_not = nba.get("do_not") or []
|
| 139 |
|
| 140 |
badge = "🟢 clear"
|
| 141 |
if nba.get("fallback_used"):
|
| 142 |
+
badge = "🟡 guardrail fired"
|
| 143 |
if any(h in {"biohazard", "sharps_injury_risk", "thermal_runaway_risk"} for h in hazards):
|
| 144 |
badge = "🔴 hazard"
|
| 145 |
|
| 146 |
+
rows = [
|
| 147 |
+
f"### {ident.get('class', '?')} · _{ident.get('subclass') or ''}_",
|
| 148 |
"",
|
| 149 |
+
f"**Status** — {badge}",
|
| 150 |
"",
|
| 151 |
"| | |",
|
| 152 |
"|---|---|",
|
|
|
|
| 155 |
f"| **Do not** | "
|
| 156 |
+ (", ".join(f"`{x}`" for x in do_not) if do_not else "_none_") + " |",
|
| 157 |
f"| **Confidence** | `{ident.get('confidence', 0):.3f}` "
|
| 158 |
+
+ ("calibrated" if prov.get("confidence_calibrated") else "raw") + " |",
|
| 159 |
f"| **Hazards** | "
|
| 160 |
+ (", ".join(f"`{h}`" for h in hazards) if hazards else "_none_") + " |",
|
| 161 |
f"| **Condition** | `{state.get('condition', '?')}` |",
|
| 162 |
f"| **Jurisdiction** | {routing.get('jurisdiction', '?')} |",
|
| 163 |
+
f"| **Model** | `{prov.get('model', '?')}` |",
|
| 164 |
]
|
| 165 |
+
return "\n".join(rows)
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def render_raw(trace: dict | None) -> str:
|
| 169 |
+
"""Step 1 — what Gemma actually emitted."""
|
| 170 |
+
if not trace:
|
| 171 |
+
return "_Live mode only — Demo mode shows a pre-computed Passport, no model output._"
|
| 172 |
+
raw = trace["raw_output"]
|
| 173 |
+
parsed = trace["parsed"]
|
| 174 |
+
pretty = json.dumps(parsed, indent=2)
|
| 175 |
+
return (
|
| 176 |
+
"**Gemma 4's raw output (post `parse_response`):**\n\n"
|
| 177 |
+
f"```\n{raw[:600] if len(raw) > 600 else raw}\n```\n\n"
|
| 178 |
+
"**Parsed by the validator (D012):**\n\n"
|
| 179 |
+
f"```json\n{pretty}\n```"
|
| 180 |
+
)
|
| 181 |
|
| 182 |
|
| 183 |
+
def render_validator(trace: dict | None) -> str:
|
| 184 |
+
if not trace:
|
| 185 |
+
return ""
|
| 186 |
+
v = trace["validators"]
|
| 187 |
+
return "\n".join([
|
| 188 |
+
"**Layer A — JSON validator (D012)**",
|
| 189 |
+
"",
|
| 190 |
+
"| Check | Result |",
|
| 191 |
+
"|---|---|",
|
| 192 |
+
f"| JSON shape | {'✅ valid' if v['json_ok'] else '❌ malformed'} |",
|
| 193 |
+
f"| Taxonomy enum | {'✅ in domestic' if v['enum_ok'] else '❌ out of taxonomy'} |",
|
| 194 |
+
])
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def render_calibration(trace: dict | None) -> str:
|
| 198 |
+
if not trace:
|
| 199 |
+
return ""
|
| 200 |
+
c = trace["calibration"]
|
| 201 |
+
raw = c["raw"]
|
| 202 |
+
cal = c["calibrated"]
|
| 203 |
+
arrow = lambda r, k: "→" if abs(r - k) > 0.001 else "="
|
| 204 |
return "\n".join([
|
| 205 |
+
f"**Layer B — Confidence calibration (D015 · `{c['method']}`)**",
|
| 206 |
"",
|
| 207 |
+
"| Block | Raw | | Calibrated | Δ |",
|
| 208 |
+
"|---|---:|:---:|---:|---:|",
|
| 209 |
+
f"| identity | `{raw['identity']:.3f}` | {arrow(raw['identity'], cal['identity'])} | `{cal['identity']:.3f}` | `{cal['identity'] - raw['identity']:+.3f}` |",
|
| 210 |
+
f"| state | `{raw['state']:.3f}` | {arrow(raw['state'], cal['state'])} | `{cal['state']:.3f}` | `{cal['state'] - raw['state']:+.3f}` |",
|
| 211 |
+
f"| nba | `{raw['nba']:.3f}` | {arrow(raw['nba'], cal['nba'])} | `{cal['nba']:.3f}` | `{cal['nba'] - raw['nba']:+.3f}` |",
|
| 212 |
+
])
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
def render_hazards(trace: dict | None) -> str:
|
| 216 |
+
if not trace:
|
| 217 |
+
return ""
|
| 218 |
+
h = trace["hazards"]
|
| 219 |
+
return "\n".join([
|
| 220 |
+
"**Layer C — Hazard auto-flagger (D019)**",
|
| 221 |
+
"",
|
| 222 |
+
"| | |",
|
| 223 |
"|---|---|",
|
| 224 |
+
f"| Model said | "
|
| 225 |
+
+ (", ".join(f"`{x}`" for x in h["before"]) if h["before"] else "_none_") + " |",
|
| 226 |
+
f"| Auto-flagger added | "
|
| 227 |
+
+ (", ".join(f"`{x}`" for x in h["added"]) if h["added"] else "_none_") + " |",
|
| 228 |
+
f"| Final hazard set | "
|
| 229 |
+
+ (", ".join(f"`{x}`" for x in h["after"]) if h["after"] else "_none_") + " |",
|
| 230 |
])
|
| 231 |
|
| 232 |
|
| 233 |
+
def render_guardrail(trace: dict | None) -> str:
|
| 234 |
+
if not trace:
|
| 235 |
+
return ""
|
| 236 |
+
g = trace["guardrail"]
|
| 237 |
+
if not g["fired"]:
|
| 238 |
+
return "\n".join([
|
| 239 |
+
"**Layer D — `do_not` guardrail (D018)**",
|
| 240 |
+
"",
|
| 241 |
+
f"✅ Proposed action `{g['proposed_action']}` passed all `do_not` rules.",
|
| 242 |
+
])
|
| 243 |
+
return "\n".join([
|
| 244 |
+
"**Layer D — `do_not` guardrail (D018)**",
|
| 245 |
+
"",
|
| 246 |
+
f"⚠️ **Guardrail fired** — severity: `{g['severity']}`",
|
| 247 |
+
"",
|
| 248 |
+
"| | |",
|
| 249 |
+
"|---|---|",
|
| 250 |
+
f"| Triggered class | `{g['triggered_class']}` |",
|
| 251 |
+
f"| Model proposed | `{g['proposed_action']}` |",
|
| 252 |
+
f"| Safe default applied | `{g['safe_default']}` |",
|
| 253 |
+
])
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
def render_verifier(p: dict, head: str) -> str:
|
| 257 |
+
"""Run the Passport back through the verifier — independent attestation that
|
| 258 |
+
every layer's contract held."""
|
| 259 |
+
raw = json.dumps({
|
| 260 |
+
"identity": p.get("identity", {}),
|
| 261 |
+
"state": p.get("state", {}),
|
| 262 |
+
"next_best_action": p.get("next_best_action", {}),
|
| 263 |
+
})
|
| 264 |
+
score = verifier.score(raw, head, ground_truth=None)
|
| 265 |
+
rows = [
|
| 266 |
+
"**Verifier scoring** (`matter.verifier.Verifier`)",
|
| 267 |
+
"",
|
| 268 |
+
"| Component | Score | Status |",
|
| 269 |
+
"|---|---:|:---:|",
|
| 270 |
+
f"| `json_valid` | `{score.json_valid:.0f}` | {'✅' if score.json_valid else '❌'} |",
|
| 271 |
+
f"| `enum_valid` | `{score.enum_valid:.0f}` | {'✅' if score.enum_valid else '❌'} |",
|
| 272 |
+
f"| `do_not_compliance` | `{score.do_not_compliance:.0f}` | {'✅' if score.do_not_compliance else '❌'} |",
|
| 273 |
+
f"| `hazard_completeness` | `{score.hazard_completeness:.0f}` | {'✅' if score.hazard_completeness else '❌'} |",
|
| 274 |
+
f"| **Structural total** | **`{score.structural:.3f}`** | {'✅' if score.structural >= 0.99 else '⚠️'} |",
|
| 275 |
+
]
|
| 276 |
+
return "\n".join(rows)
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
# =====================================================================
|
| 280 |
+
# Run handlers
|
| 281 |
+
# =====================================================================
|
| 282 |
+
|
| 283 |
+
def run_demo(head: str) -> tuple:
|
| 284 |
fname = DEMO_PASSPORTS.get(head, DEMO_PASSPORTS["domestic"])
|
| 285 |
p = json.loads((SPEC_EXAMPLES / fname).read_text())
|
| 286 |
+
return (
|
| 287 |
+
render_insights(p, trace=None),
|
| 288 |
+
render_summary(p),
|
| 289 |
+
render_raw(trace=None),
|
| 290 |
+
render_validator(trace=None),
|
| 291 |
+
render_calibration(trace=None),
|
| 292 |
+
render_hazards(trace=None),
|
| 293 |
+
render_guardrail(trace=None),
|
| 294 |
+
render_verifier(p, head),
|
| 295 |
+
json.dumps(p, indent=2),
|
| 296 |
+
)
|
| 297 |
|
| 298 |
|
| 299 |
+
def run_live(image_path: str | None, head: str, jurisdiction: str) -> tuple:
|
| 300 |
if image_path is None:
|
| 301 |
return (
|
| 302 |
+
"⚠️ Upload an image first, or switch to **Demo** mode.",
|
| 303 |
+
"_no Passport yet_", "", "", "", "", "", "", "",
|
| 304 |
)
|
| 305 |
try:
|
| 306 |
engine = get_engine()
|
|
|
|
| 308 |
image_path=Path(image_path),
|
| 309 |
jurisdiction=jurisdiction.strip() or None,
|
| 310 |
)
|
| 311 |
+
passport, trace = engine.infer_with_trace(capture, head)
|
| 312 |
p = passport.to_dict()
|
| 313 |
+
return (
|
| 314 |
+
render_insights(p, trace),
|
| 315 |
+
render_summary(p),
|
| 316 |
+
render_raw(trace),
|
| 317 |
+
render_validator(trace),
|
| 318 |
+
render_calibration(trace),
|
| 319 |
+
render_hazards(trace),
|
| 320 |
+
render_guardrail(trace),
|
| 321 |
+
render_verifier(p, head),
|
| 322 |
+
json.dumps(p, indent=2),
|
| 323 |
+
)
|
| 324 |
except MIEError as e:
|
| 325 |
return (
|
| 326 |
+
f"### ❌ MIE rejected the model output\n\n```\n{e}\n```",
|
| 327 |
+
"_pipeline halted_", "", "", "", "", "", "", "",
|
|
|
|
| 328 |
)
|
| 329 |
except Exception as e:
|
| 330 |
return (
|
| 331 |
f"### ❌ Runtime error\n\n```\n{e.__class__.__name__}: {e}\n```\n\n"
|
| 332 |
+
"_If this is the first call after a cold start, the GPU worker is still loading Gemma 4 (≈30s). Try again or use Demo mode._",
|
| 333 |
f"<details><summary>traceback</summary>\n\n```\n{traceback.format_exc()}\n```\n</details>",
|
| 334 |
+
"", "", "", "", "", "", "",
|
| 335 |
)
|
| 336 |
|
| 337 |
|
|
|
|
| 700 |
/* ===== Selection ===== */
|
| 701 |
::selection { background: rgba(0, 217, 126, 0.35); color: white; }
|
| 702 |
|
| 703 |
+
/* ===== Insights banner — value-generated headline ===== */
|
| 704 |
+
#insights-banner {
|
| 705 |
+
background: linear-gradient(135deg, rgba(0, 217, 126, 0.10), rgba(0, 229, 255, 0.05)) !important;
|
| 706 |
+
border: 1px solid rgba(0, 217, 126, 0.32) !important;
|
| 707 |
+
border-radius: 14px !important;
|
| 708 |
+
padding: 18px 22px !important;
|
| 709 |
+
margin-bottom: 18px !important;
|
| 710 |
+
box-shadow: 0 0 24px rgba(0, 217, 126, 0.10), inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
| 711 |
+
}
|
| 712 |
+
#insights-banner p, #insights-banner strong {
|
| 713 |
+
color: #f1faf4 !important;
|
| 714 |
+
font-size: 0.96rem !important;
|
| 715 |
+
line-height: 1.55 !important;
|
| 716 |
+
margin: 4px 0 !important;
|
| 717 |
+
}
|
| 718 |
+
#insights-banner code {
|
| 719 |
+
background: rgba(0, 217, 126, 0.12) !important;
|
| 720 |
+
color: #00ff8c !important;
|
| 721 |
+
border: 1px solid rgba(0, 217, 126, 0.28) !important;
|
| 722 |
+
font-weight: 500;
|
| 723 |
+
}
|
| 724 |
+
#insights-banner:has(p:empty),
|
| 725 |
+
#insights-banner:not(:has(*)) {
|
| 726 |
+
display: none;
|
| 727 |
+
}
|
| 728 |
+
|
| 729 |
/* ===== Catch-all readability ===== */
|
| 730 |
.gradio-container { color: #f1faf4; }
|
| 731 |
.gradio-container input::placeholder,
|
|
|
|
| 794 |
)
|
| 795 |
|
| 796 |
with gr.Column(scale=7):
|
| 797 |
+
# Insights banner — punchy 1-3 line summary of the value generated
|
| 798 |
+
insights_out = gr.Markdown(
|
| 799 |
+
value="",
|
| 800 |
+
elem_id="insights-banner",
|
| 801 |
+
)
|
| 802 |
gr.Markdown("### Passport")
|
| 803 |
summary_out = gr.Markdown(value="_Pick a mode and press_ **Generate Passport**.")
|
| 804 |
+
|
| 805 |
+
# Step-by-step trace — one accordion per pipeline layer
|
| 806 |
+
with gr.Accordion("🔍 Gemma 4's raw output (pre-pipeline)", open=False):
|
| 807 |
+
raw_out = gr.Markdown()
|
| 808 |
+
with gr.Accordion("✅ Layer A — JSON validator (D012)", open=False):
|
| 809 |
+
validator_out = gr.Markdown()
|
| 810 |
+
with gr.Accordion("📐 Layer B — Confidence calibration (D015)", open=False):
|
| 811 |
+
calibration_out = gr.Markdown()
|
| 812 |
+
with gr.Accordion("⚠️ Layer C — Hazard auto-flagger (D019)", open=False):
|
| 813 |
+
hazards_out = gr.Markdown()
|
| 814 |
+
with gr.Accordion("🛡️ Layer D — `do_not` guardrail (D018)", open=False):
|
| 815 |
+
guardrail_out = gr.Markdown()
|
| 816 |
+
with gr.Accordion("🧪 Verifier — independent attestation", open=False):
|
| 817 |
+
verifier_out = gr.Markdown()
|
| 818 |
+
with gr.Accordion("📋 Passport JSON", open=False):
|
| 819 |
json_out = gr.Code(language="json", label=None, lines=22)
|
| 820 |
|
| 821 |
gr.Markdown(
|
|
|
|
| 828 |
run_btn.click(
|
| 829 |
dispatch,
|
| 830 |
inputs=[mode_in, image_in, head_in, juris_in],
|
| 831 |
+
outputs=[insights_out, summary_out, raw_out, validator_out, calibration_out,
|
| 832 |
+
hazards_out, guardrail_out, verifier_out, json_out],
|
| 833 |
)
|
| 834 |
|
| 835 |
|
matter/engine.py
CHANGED
|
@@ -201,6 +201,144 @@ class MIE:
|
|
| 201 |
# 7. Final Pydantic validation against the v0.1 schema
|
| 202 |
return Passport.model_validate(draft)
|
| 203 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
|
| 205 |
__all__ = ["MIE", "MIEError", "Runtime", "CaptureInput", "Capture", "Identity",
|
| 206 |
"State", "NextBestAction", "Provenance", "Routing", "Passport"]
|
|
|
|
| 201 |
# 7. Final Pydantic validation against the v0.1 schema
|
| 202 |
return Passport.model_validate(draft)
|
| 203 |
|
| 204 |
+
def infer_with_trace(
|
| 205 |
+
self, capture: CaptureInput, head_name: str
|
| 206 |
+
) -> tuple[Passport, dict]:
|
| 207 |
+
"""Run the pipeline and capture every layer's intermediate state.
|
| 208 |
+
|
| 209 |
+
Returns (passport, trace) where trace is a JSON-serializable dict with:
|
| 210 |
+
- raw_output: Gemma's raw response text
|
| 211 |
+
- parsed: JSON-parsed model output (pre-pipeline)
|
| 212 |
+
- validators: {json_ok, enum_ok}
|
| 213 |
+
- calibration: {raw, calibrated, method} (per-block confidences)
|
| 214 |
+
- hazards: {before, after, added}
|
| 215 |
+
- guardrail: {proposed_action, fired, safe_default,
|
| 216 |
+
triggered_class, severity}
|
| 217 |
+
- metadata: {head, jurisdiction, runtime, model_id}
|
| 218 |
+
|
| 219 |
+
Used by the demo UI to render a step-by-step pipeline view. The Passport
|
| 220 |
+
return value is identical to what `infer()` would have produced.
|
| 221 |
+
"""
|
| 222 |
+
if head_name not in HEADS:
|
| 223 |
+
raise MIEError(f"unknown head: {head_name}. Heads: {list(HEADS)}")
|
| 224 |
+
head = HEADS[head_name]
|
| 225 |
+
jurisdiction = capture.jurisdiction or head.default_jurisdiction
|
| 226 |
+
prompt = build_prompt(head_name, jurisdiction)
|
| 227 |
+
|
| 228 |
+
# 1. Runtime
|
| 229 |
+
raw = self.runtime.infer(prompt, capture.image_path)
|
| 230 |
+
|
| 231 |
+
# 2. Validator
|
| 232 |
+
parsed = _parse_json_block(raw)
|
| 233 |
+
_validate_enum(parsed, head_name)
|
| 234 |
+
ident = parsed["identity"]
|
| 235 |
+
st = parsed.get("state", {})
|
| 236 |
+
nba = parsed["next_best_action"]
|
| 237 |
+
|
| 238 |
+
confidences_raw = {
|
| 239 |
+
"identity": float(ident.get("confidence", 0.0)),
|
| 240 |
+
"state": float(st.get("confidence", 0.0)),
|
| 241 |
+
"nba": float(nba.get("confidence", 0.0)),
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
# 3. Calibration
|
| 245 |
+
ident_conf_cal = _calibrate(confidences_raw["identity"], self.calib)
|
| 246 |
+
state_conf_cal = _calibrate(confidences_raw["state"], self.calib)
|
| 247 |
+
nba_conf_cal = _calibrate(confidences_raw["nba"], self.calib)
|
| 248 |
+
confidences_cal = {
|
| 249 |
+
"identity": ident_conf_cal,
|
| 250 |
+
"state": state_conf_cal,
|
| 251 |
+
"nba": nba_conf_cal,
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
# 4. Build draft
|
| 255 |
+
modality, content_hash = _content_hash(capture)
|
| 256 |
+
ts = now_utc()
|
| 257 |
+
passport_id = make_passport_id(content_hash, ident["class"], ts)
|
| 258 |
+
draft: dict = {
|
| 259 |
+
"schema": "matter-passport/v0.1",
|
| 260 |
+
"passport_id": passport_id,
|
| 261 |
+
"prev": None,
|
| 262 |
+
"timestamp": ts,
|
| 263 |
+
"capture": {
|
| 264 |
+
"modality": modality,
|
| 265 |
+
"content_hash": content_hash,
|
| 266 |
+
**({"geohash_coarse": capture.geohash_coarse} if capture.geohash_coarse else {}),
|
| 267 |
+
},
|
| 268 |
+
"identity": {
|
| 269 |
+
"class": ident["class"],
|
| 270 |
+
"subclass": ident.get("subclass"),
|
| 271 |
+
"taxonomy": head.taxonomy_uri,
|
| 272 |
+
"confidence": ident_conf_cal,
|
| 273 |
+
},
|
| 274 |
+
"state": {
|
| 275 |
+
"condition": st.get("condition", "unknown"),
|
| 276 |
+
"hazard_flags": list(st.get("hazard_flags") or []),
|
| 277 |
+
"confidence": state_conf_cal,
|
| 278 |
+
},
|
| 279 |
+
"next_best_action": {
|
| 280 |
+
"primary": nba["primary"],
|
| 281 |
+
"secondary": nba.get("secondary"),
|
| 282 |
+
"do_not": list(nba.get("do_not") or []),
|
| 283 |
+
"confidence": nba_conf_cal,
|
| 284 |
+
"fallback_used": False,
|
| 285 |
+
},
|
| 286 |
+
"routing": {"jurisdiction": jurisdiction, "regulation_refs": []},
|
| 287 |
+
"provenance": {
|
| 288 |
+
"model": self.runtime.model_id,
|
| 289 |
+
"runtime": self.runtime.name,
|
| 290 |
+
"on_device": self.on_device,
|
| 291 |
+
"confidence_calibrated": True,
|
| 292 |
+
"calibration_ref": self._calibration_ref,
|
| 293 |
+
},
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
# 5. Hazard auto-flagger — capture before/after
|
| 297 |
+
hazards_before = list(draft["state"].get("hazard_flags") or [])
|
| 298 |
+
apply_hazard_flagger(draft, self.hazard_rules)
|
| 299 |
+
hazards_after = list(draft["state"].get("hazard_flags") or [])
|
| 300 |
+
hazards_added = [h for h in hazards_after if h not in hazards_before]
|
| 301 |
+
|
| 302 |
+
# 6. Guardrail — capture decision via GuardrailResult
|
| 303 |
+
proposed_action = draft["next_best_action"]["primary"]
|
| 304 |
+
g_result = apply_guardrail(draft, self.safety_rules)
|
| 305 |
+
|
| 306 |
+
# 7. Final Pydantic validation
|
| 307 |
+
passport = Passport.model_validate(draft)
|
| 308 |
+
|
| 309 |
+
trace = {
|
| 310 |
+
"raw_output": raw,
|
| 311 |
+
"parsed": parsed,
|
| 312 |
+
"validators": {"json_ok": True, "enum_ok": True},
|
| 313 |
+
"calibration": {
|
| 314 |
+
"raw": confidences_raw,
|
| 315 |
+
"calibrated": confidences_cal,
|
| 316 |
+
"method": self.calib.method,
|
| 317 |
+
"ref": self._calibration_ref,
|
| 318 |
+
},
|
| 319 |
+
"hazards": {
|
| 320 |
+
"before": hazards_before,
|
| 321 |
+
"after": hazards_after,
|
| 322 |
+
"added": hazards_added,
|
| 323 |
+
},
|
| 324 |
+
"guardrail": {
|
| 325 |
+
"proposed_action": proposed_action,
|
| 326 |
+
"fired": g_result.fallback_used,
|
| 327 |
+
"safe_default": (g_result.triggered_rule.safe_default
|
| 328 |
+
if g_result.fallback_used and g_result.triggered_rule else None),
|
| 329 |
+
"triggered_class": ident["class"] if g_result.fallback_used else None,
|
| 330 |
+
"severity": (g_result.triggered_rule.severity
|
| 331 |
+
if g_result.triggered_rule else None),
|
| 332 |
+
},
|
| 333 |
+
"metadata": {
|
| 334 |
+
"head": head_name,
|
| 335 |
+
"jurisdiction": jurisdiction,
|
| 336 |
+
"runtime": self.runtime.name,
|
| 337 |
+
"model_id": self.runtime.model_id,
|
| 338 |
+
},
|
| 339 |
+
}
|
| 340 |
+
return passport, trace
|
| 341 |
+
|
| 342 |
|
| 343 |
__all__ = ["MIE", "MIEError", "Runtime", "CaptureInput", "Capture", "Identity",
|
| 344 |
"State", "NextBestAction", "Provenance", "Routing", "Passport"]
|
matter/verifier.py
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Matter Verifier — programmatic reward signal for Passport generation.
|
| 2 |
+
|
| 3 |
+
The MIE pipeline already encodes "what makes a Passport good": JSON shape,
|
| 4 |
+
taxonomy enums, hazard flags, do_not guardrails, calibrated confidence.
|
| 5 |
+
The Verifier turns that pipeline into a deterministic, per-example score
|
| 6 |
+
suitable for:
|
| 7 |
+
|
| 8 |
+
- Offline eval (compare model outputs head-to-head on a fixed test set)
|
| 9 |
+
- DPO/KTO data generation (rank N samples per prompt, build preference pairs)
|
| 10 |
+
- GRPO / RL with verifiable rewards (used directly as the reward function)
|
| 11 |
+
- Test-time scaling (best-of-N selection)
|
| 12 |
+
|
| 13 |
+
The verifier is split into two layers:
|
| 14 |
+
|
| 15 |
+
Structural (no ground truth needed) — usable for unsupervised RL:
|
| 16 |
+
- json_valid Did the output parse as a JSON object in the expected shape?
|
| 17 |
+
- enum_valid Are class and next_best_action.primary in the head's enums?
|
| 18 |
+
- do_not_compliance Is the proposed primary action NOT in the class's do_not set?
|
| 19 |
+
- hazard_completeness Are all class-required hazard flags present?
|
| 20 |
+
|
| 21 |
+
Semantic (needs a ground-truth Passport / dict) — used for eval and offline DPO:
|
| 22 |
+
- class_correct Does identity.class match GT?
|
| 23 |
+
- subclass_match Is subclass a string-match (exact or substring) of GT?
|
| 24 |
+
- nba_correct Does next_best_action.primary match GT?
|
| 25 |
+
- confidence_brier 1 - (predicted_confidence - is_correct)^2 — per-example
|
| 26 |
+
Brier-like calibration signal in [0, 1].
|
| 27 |
+
|
| 28 |
+
Aggregation:
|
| 29 |
+
total = (sum of w_i * score_i) / (sum of w_i scored)
|
| 30 |
+
Hard gate: if json_valid == 0, total = 0 (model emitted garbage; nothing else matters).
|
| 31 |
+
|
| 32 |
+
For RL, prefer `reward()` which returns a single scalar with sensible shaping
|
| 33 |
+
(garbage → low, structurally valid but wrong → mid, fully correct → high).
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
from __future__ import annotations
|
| 37 |
+
|
| 38 |
+
import json
|
| 39 |
+
import re
|
| 40 |
+
from dataclasses import asdict, dataclass, field
|
| 41 |
+
from pathlib import Path
|
| 42 |
+
from typing import Any
|
| 43 |
+
|
| 44 |
+
from matter.guardrail import Rule, load_rules as load_safety_rules
|
| 45 |
+
from matter.hazard_flagger import HazardRule, load_hazard_rules
|
| 46 |
+
from matter.heads import HEADS
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
_SPEC_DIR = Path(__file__).resolve().parent.parent / "spec"
|
| 50 |
+
_SAFETY_PATH = _SPEC_DIR / "safety_rules_v1.json"
|
| 51 |
+
_HAZARD_PATH = _SPEC_DIR / "hazard_flags_v1.json"
|
| 52 |
+
_JSON_RE = re.compile(r"\{.*\}", re.DOTALL)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
# Default reward weights. Tunable per experiment.
|
| 56 |
+
DEFAULT_WEIGHTS: dict[str, float] = {
|
| 57 |
+
# Structural — gates and shape (cheap, always scored)
|
| 58 |
+
"json_valid": 1.0,
|
| 59 |
+
"enum_valid": 1.0,
|
| 60 |
+
"do_not_compliance": 3.0,
|
| 61 |
+
"hazard_completeness": 2.0,
|
| 62 |
+
# Semantic — needs ground truth
|
| 63 |
+
"class_correct": 4.0,
|
| 64 |
+
"subclass_match": 1.0,
|
| 65 |
+
"nba_correct": 2.0,
|
| 66 |
+
"confidence_brier": 1.0,
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
@dataclass
|
| 71 |
+
class VerifierScore:
|
| 72 |
+
"""Per-example scoring result.
|
| 73 |
+
|
| 74 |
+
Each component is in [0, 1] (or None when not scorable). `total` is the
|
| 75 |
+
weight-normalized aggregate over the components that were actually
|
| 76 |
+
scored. `parsed` carries the parsed dict on success, useful for callers
|
| 77 |
+
that want to inspect / cache.
|
| 78 |
+
"""
|
| 79 |
+
|
| 80 |
+
# Structural
|
| 81 |
+
json_valid: float = 0.0
|
| 82 |
+
enum_valid: float = 0.0
|
| 83 |
+
do_not_compliance: float = 0.0
|
| 84 |
+
hazard_completeness: float = 0.0
|
| 85 |
+
# Semantic (None if no ground truth)
|
| 86 |
+
class_correct: float | None = None
|
| 87 |
+
subclass_match: float | None = None
|
| 88 |
+
nba_correct: float | None = None
|
| 89 |
+
confidence_brier: float | None = None
|
| 90 |
+
# Aggregates
|
| 91 |
+
structural: float = 0.0
|
| 92 |
+
semantic: float | None = None
|
| 93 |
+
total: float = 0.0
|
| 94 |
+
# Diagnostics
|
| 95 |
+
parsed: dict | None = None
|
| 96 |
+
parse_error: str | None = None
|
| 97 |
+
weights: dict[str, float] = field(default_factory=dict)
|
| 98 |
+
|
| 99 |
+
def to_dict(self) -> dict:
|
| 100 |
+
return asdict(self)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
class Verifier:
|
| 104 |
+
"""Score raw model outputs against the MIE specification.
|
| 105 |
+
|
| 106 |
+
Stateless across calls except for the loaded rule tables; safe to share
|
| 107 |
+
one instance across an entire training/eval run.
|
| 108 |
+
"""
|
| 109 |
+
|
| 110 |
+
def __init__(
|
| 111 |
+
self,
|
| 112 |
+
weights: dict[str, float] | None = None,
|
| 113 |
+
safety_rules_path: Path = _SAFETY_PATH,
|
| 114 |
+
hazard_rules_path: Path = _HAZARD_PATH,
|
| 115 |
+
):
|
| 116 |
+
self.weights = dict(weights or DEFAULT_WEIGHTS)
|
| 117 |
+
self.safety_rules: dict[str, Rule] = load_safety_rules(safety_rules_path)
|
| 118 |
+
self.hazard_rules: dict[str, HazardRule] = load_hazard_rules(hazard_rules_path)
|
| 119 |
+
|
| 120 |
+
# ----------------------------- public API -----------------------------
|
| 121 |
+
|
| 122 |
+
def score(
|
| 123 |
+
self,
|
| 124 |
+
raw: str,
|
| 125 |
+
head_name: str,
|
| 126 |
+
ground_truth: dict | None = None,
|
| 127 |
+
) -> VerifierScore:
|
| 128 |
+
"""Score a raw model output for a given head, optionally against GT.
|
| 129 |
+
|
| 130 |
+
Args:
|
| 131 |
+
raw: the model's untrimmed output text. May contain prose around
|
| 132 |
+
the JSON; we extract the first {...} block.
|
| 133 |
+
head_name: which head's enums + jurisdiction apply (e.g. "domestic").
|
| 134 |
+
ground_truth: optional dict shaped like a model output OR a full
|
| 135 |
+
Passport. We look at identity.class / .subclass and
|
| 136 |
+
next_best_action.primary.
|
| 137 |
+
|
| 138 |
+
Returns:
|
| 139 |
+
VerifierScore with per-component scores and a total in [0, 1].
|
| 140 |
+
"""
|
| 141 |
+
if head_name not in HEADS:
|
| 142 |
+
raise ValueError(f"Unknown head: {head_name!r}. Known: {list(HEADS)}")
|
| 143 |
+
head = HEADS[head_name]
|
| 144 |
+
s = VerifierScore(weights=dict(self.weights))
|
| 145 |
+
|
| 146 |
+
# 1. JSON parse
|
| 147 |
+
parsed, err = _parse_json(raw)
|
| 148 |
+
if parsed is None:
|
| 149 |
+
s.parse_error = err
|
| 150 |
+
self._aggregate(s, has_gt=ground_truth is not None)
|
| 151 |
+
return s
|
| 152 |
+
s.json_valid = 1.0
|
| 153 |
+
s.parsed = parsed
|
| 154 |
+
|
| 155 |
+
# 2. Enum validity
|
| 156 |
+
cls = _get(parsed, "identity", "class") or parsed.get("class")
|
| 157 |
+
nba_primary = _get(parsed, "next_best_action", "primary")
|
| 158 |
+
s.enum_valid = float(
|
| 159 |
+
cls in head.identity_classes and (nba_primary is None or nba_primary in head.nba_classes)
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
# 3. do_not compliance — given the predicted class, is the proposed
|
| 163 |
+
# primary action allowed by the safety rules? If the class isn't in
|
| 164 |
+
# the rule table, it has no constraints and trivially complies.
|
| 165 |
+
s.do_not_compliance = self._score_do_not(cls, nba_primary)
|
| 166 |
+
|
| 167 |
+
# 4. Hazard completeness — predicted hazard_flags include all the
|
| 168 |
+
# canonical class-implied hazards. If the class has no required
|
| 169 |
+
# hazards, this trivially passes (1.0).
|
| 170 |
+
predicted_hazards = (
|
| 171 |
+
_get(parsed, "state", "hazard_flags") or parsed.get("hazard_flags") or []
|
| 172 |
+
)
|
| 173 |
+
s.hazard_completeness = self._score_hazards(cls, predicted_hazards)
|
| 174 |
+
|
| 175 |
+
# 5. Semantic — only if GT supplied
|
| 176 |
+
if ground_truth is not None:
|
| 177 |
+
gt_cls = _get(ground_truth, "identity", "class") or ground_truth.get("class")
|
| 178 |
+
gt_subclass = _get(ground_truth, "identity", "subclass") or ground_truth.get("subclass")
|
| 179 |
+
gt_nba = _get(ground_truth, "next_best_action", "primary") or ground_truth.get("primary")
|
| 180 |
+
pred_subclass = _get(parsed, "identity", "subclass") or parsed.get("subclass")
|
| 181 |
+
pred_conf = _confidence(parsed)
|
| 182 |
+
|
| 183 |
+
s.class_correct = float(cls is not None and cls == gt_cls)
|
| 184 |
+
s.subclass_match = _string_match(pred_subclass, gt_subclass)
|
| 185 |
+
s.nba_correct = (
|
| 186 |
+
float(nba_primary == gt_nba) if gt_nba is not None and nba_primary is not None else None
|
| 187 |
+
)
|
| 188 |
+
s.confidence_brier = (
|
| 189 |
+
_brier(pred_conf, s.class_correct) if pred_conf is not None else None
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
self._aggregate(s, has_gt=ground_truth is not None)
|
| 193 |
+
return s
|
| 194 |
+
|
| 195 |
+
def reward(
|
| 196 |
+
self,
|
| 197 |
+
raw: str,
|
| 198 |
+
head_name: str,
|
| 199 |
+
ground_truth: dict | None = None,
|
| 200 |
+
) -> float:
|
| 201 |
+
"""Single-scalar reward in [-1, 1] suitable for RL.
|
| 202 |
+
|
| 203 |
+
Shaping:
|
| 204 |
+
- garbage (json_valid == 0): -1.0 (strong signal: don't emit non-JSON)
|
| 205 |
+
- parsed but otherwise zero: 0.0
|
| 206 |
+
- perfect: 1.0
|
| 207 |
+
|
| 208 |
+
For unsupervised RL (no GT), structural score alone determines reward.
|
| 209 |
+
"""
|
| 210 |
+
s = self.score(raw, head_name, ground_truth)
|
| 211 |
+
if s.json_valid == 0:
|
| 212 |
+
return -1.0
|
| 213 |
+
return s.total
|
| 214 |
+
|
| 215 |
+
# --------------------------- internals --------------------------------
|
| 216 |
+
|
| 217 |
+
def _score_do_not(self, cls: str | None, nba_primary: str | None) -> float:
|
| 218 |
+
if cls is None or nba_primary is None:
|
| 219 |
+
return 0.0
|
| 220 |
+
rule = self.safety_rules.get(cls)
|
| 221 |
+
if rule is None:
|
| 222 |
+
return 1.0 # class unconstrained, trivially compliant
|
| 223 |
+
return 0.0 if nba_primary in rule.do_not else 1.0
|
| 224 |
+
|
| 225 |
+
def _score_hazards(self, cls: str | None, predicted: list[str]) -> float:
|
| 226 |
+
if cls is None:
|
| 227 |
+
return 0.0
|
| 228 |
+
rule = self.hazard_rules.get(cls)
|
| 229 |
+
if rule is None or not rule.required:
|
| 230 |
+
return 1.0 # no required hazards for this class
|
| 231 |
+
present = set(predicted)
|
| 232 |
+
return float(all(h in present for h in rule.required))
|
| 233 |
+
|
| 234 |
+
def _aggregate(self, s: VerifierScore, *, has_gt: bool) -> None:
|
| 235 |
+
# Structural always contributes
|
| 236 |
+
struct_keys = ("json_valid", "enum_valid", "do_not_compliance", "hazard_completeness")
|
| 237 |
+
struct_w = sum(self.weights[k] for k in struct_keys)
|
| 238 |
+
struct_v = sum(self.weights[k] * getattr(s, k) for k in struct_keys)
|
| 239 |
+
s.structural = struct_v / struct_w if struct_w > 0 else 0.0
|
| 240 |
+
|
| 241 |
+
# Hard gate: garbage → 0
|
| 242 |
+
if s.json_valid == 0.0:
|
| 243 |
+
s.semantic = None
|
| 244 |
+
s.total = 0.0
|
| 245 |
+
return
|
| 246 |
+
|
| 247 |
+
if has_gt:
|
| 248 |
+
sem_pairs = [
|
| 249 |
+
("class_correct", s.class_correct),
|
| 250 |
+
("subclass_match", s.subclass_match),
|
| 251 |
+
("nba_correct", s.nba_correct),
|
| 252 |
+
("confidence_brier", s.confidence_brier),
|
| 253 |
+
]
|
| 254 |
+
sem_w = sum(self.weights[k] for k, v in sem_pairs if v is not None)
|
| 255 |
+
sem_v = sum(self.weights[k] * v for k, v in sem_pairs if v is not None)
|
| 256 |
+
s.semantic = (sem_v / sem_w) if sem_w > 0 else None
|
| 257 |
+
total_w = struct_w + sem_w
|
| 258 |
+
total_v = struct_v + sem_v
|
| 259 |
+
else:
|
| 260 |
+
s.semantic = None
|
| 261 |
+
total_w = struct_w
|
| 262 |
+
total_v = struct_v
|
| 263 |
+
s.total = total_v / total_w if total_w > 0 else 0.0
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
# ----------------------------- helpers ------------------------------------
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
def _parse_json(raw: str) -> tuple[dict | None, str | None]:
|
| 270 |
+
"""Extract and parse the first JSON object in raw. Returns (parsed, error)."""
|
| 271 |
+
if not isinstance(raw, str) or not raw.strip():
|
| 272 |
+
return None, "empty input"
|
| 273 |
+
m = _JSON_RE.search(raw)
|
| 274 |
+
if m is None:
|
| 275 |
+
return None, "no JSON object found"
|
| 276 |
+
try:
|
| 277 |
+
obj = json.loads(m.group(0))
|
| 278 |
+
except json.JSONDecodeError as e:
|
| 279 |
+
return None, f"JSONDecodeError: {e}"
|
| 280 |
+
if not isinstance(obj, dict):
|
| 281 |
+
return None, "top-level JSON is not an object"
|
| 282 |
+
return obj, None
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
def _get(d: dict | None, *keys: str) -> Any:
|
| 286 |
+
"""Nested dict lookup, returning None if any step is missing."""
|
| 287 |
+
cur: Any = d
|
| 288 |
+
for k in keys:
|
| 289 |
+
if not isinstance(cur, dict):
|
| 290 |
+
return None
|
| 291 |
+
cur = cur.get(k)
|
| 292 |
+
return cur
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
def _confidence(parsed: dict) -> float | None:
|
| 296 |
+
"""Pull a confidence value from common output shapes."""
|
| 297 |
+
for path in (("identity", "confidence"), ("confidence",)):
|
| 298 |
+
v = _get(parsed, *path)
|
| 299 |
+
if isinstance(v, (int, float)):
|
| 300 |
+
return float(v)
|
| 301 |
+
return None
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
def _string_match(pred: str | None, gt: str | None) -> float | None:
|
| 305 |
+
"""Soft string match: 1.0 exact (case-insensitive), 0.5 substring, 0 else.
|
| 306 |
+
Returns None if either side is missing — caller decides whether to score it.
|
| 307 |
+
"""
|
| 308 |
+
if not pred or not gt:
|
| 309 |
+
return None
|
| 310 |
+
p = pred.strip().lower()
|
| 311 |
+
g = gt.strip().lower()
|
| 312 |
+
if p == g:
|
| 313 |
+
return 1.0
|
| 314 |
+
if p in g or g in p:
|
| 315 |
+
return 0.5
|
| 316 |
+
return 0.0
|
| 317 |
+
|
| 318 |
+
|
| 319 |
+
def _brier(confidence: float, is_correct: float | None) -> float | None:
|
| 320 |
+
"""Per-example Brier-like calibration signal in [0, 1].
|
| 321 |
+
|
| 322 |
+
Returns 1 - (confidence - is_correct)^2:
|
| 323 |
+
- confident & correct (1.0, 1) -> 1.0
|
| 324 |
+
- confident & wrong (1.0, 0) -> 0.0
|
| 325 |
+
- unsure & correct (0.5, 1) -> 0.75
|
| 326 |
+
- unsure & wrong (0.5, 0) -> 0.75
|
| 327 |
+
- underconfident & correct (0.0, 1) -> 0.0
|
| 328 |
+
|
| 329 |
+
Returns None if is_correct is None.
|
| 330 |
+
"""
|
| 331 |
+
if is_correct is None:
|
| 332 |
+
return None
|
| 333 |
+
c = max(0.0, min(1.0, confidence))
|
| 334 |
+
return 1.0 - (c - is_correct) ** 2
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
__all__ = [
|
| 338 |
+
"Verifier",
|
| 339 |
+
"VerifierScore",
|
| 340 |
+
"DEFAULT_WEIGHTS",
|
| 341 |
+
]
|