Repository navigation
refactor(intrinsics): guardian.py IOContract subclasses + Adapter constants (Epic #929 Phase 1 follow-up) #1332
Description
Parent epic: #929
Phase: 1 (Wave 3 follow-up — guardian.py)
Deferred from: #1139 (guardian.py whole-file migration — PR #1323)
Blocked by: #1139 must merge; #1137 must merge (adds io_contract= to call_intrinsic in _util.py)
Independent of: #1138 (requirement_check), #1140 (Phase 2)
Blocks: nothing directly; Phase 2 (#1140) can start once all Phase 1 work is done
Background
#1139 (PR #1323) completed the guardian.py behavioural migration: Google-style docstrings, documents= keyword-only on the factuality functions, model_options= on all four helpers, and the policy_guardrails XOR fix. One acceptance-criteria item was explicitly deferred:
"Construct via new types" — create
IOContractsubclasses and module-levelAdapterconstants for each guardian helper, following the per-module migration pattern established in #1137 (rag.py).
The deferral was necessary because the call_intrinsic(io_contract=…) parameter that wires IOContract into the call path is being added to _util.py by #1137. Using it before #1137 merges would add a mid-wave cross-PR dependency.
What this issue covers
For each of the four helpers in mellea/stdlib/components/intrinsic/guardian.py:
| Helper | Adapter name | Expected output key(s) |
|---|---|---|
policy_guardrails |
policy-guardrails |
label xor score |
guardian_check |
guardian-core |
guardian.score |
factuality_detection |
factuality-detection |
score |
factuality_correction |
factuality-correction |
correction |
1. IOContract subclasses
Create a private _*Contract class for each helper that extends IOContract (from mellea.backends.adapters). Each class implements parse(result_json) to:
- Validate required keys are present
- Raise
AdapterSchemaMismatchErroron schema violations (replacing the current implicitKeyError) - Return the typed parsed value
Follow the pattern established by the _DictContract / _ListContract classes in rag.py (PR #1321).
2. Module-level Adapter constants
Add one Adapter constant per helper (e.g. _POLICY_GUARDRAILS_ADAPTER, _GUARDIAN_CHECK_ADAPTER, etc.) that couples the adapter name to its IOContract. Pass the constant into the call_intrinsic call via io_contract=.
3. Wire into existing call sites
Update the four call_intrinsic(...) calls in guardian.py to pass io_contract= using the new constants. Remove any inline key-access that is now handled by the contract's parse() method.
Success criteria
- One
IOContractsubclass per helper, matching the output schema documented in the HF README for each adapter -
AdapterSchemaMismatchErrorraised (notKeyError) when the adapter returns an unexpected schema - Module-level
Adapterconstants wired intocall_intrinsiccalls - All 19 existing unit tests in
test_guardian_documents.pystill pass unchanged - Existing qualitative tests in
test_guardian.pystill pass -
ruff,mypyclean; docstring quality gate passes
What to look at first
rag.pyimplementation in PR refactor(intrinsics): migrate rag.py to new Adapter types (Epic #929 Phase 1 Wave 3) #1321 — the canonical reference for how_*Contractclasses andAdapterconstants should lookmellea/backends/adapters/adapter.py—IOContract,Adapter,AdapterSchemaMismatchErrordefinitionsmellea/stdlib/components/intrinsic/_util.py—call_intrinsicsignature (extended in refactor(intrinsics): rag.py whole-file migration to new Adapter types (Epic #929 Phase 1) #1137 to acceptio_contract=)- HF READMEs for each guardian adapter (see AGENTS.md §14 for URL pattern)
Activity
psschwei commentedon Jun 25, 2026
Follow-up note from PR #1320 (#1138), relevant to the
IOContract.parse()design here.The inline
requirement-checkvalidation that #1320 added reusesAdapterSchemaMismatchErrorfor value violations (NaN / ±Inf / score outside[0.0, 1.0]), not just key/shape mismatches. In those cases the key is present and well-typed — only the value is out of contract — soexpected_keys=frozenset({"score"})is approximate and the error name reads slightly wrong.When the guardian (and eventual
requirement-check)IOContractsubclasses land, consider giving them a distinct value-error type (e.g.AdapterValueError) separate fromAdapterSchemaMismatchError, so "wrong shape" and "out-of-range value" surface as different failures.guardian.scoreandfactuality_detection'sscorein the table above have the same range/finiteness concern asrequirement_check.This would also let the two currently-duplicated inline blocks in #1320 (
core.py::requirement_checkandrequirement.py::requirement_check_to_bool) consolidate onto one contract.