Guardrails¶
A guardrail always returns a GuardrailResult; it never raises for a failed
check. Whether a BLOCK halts a pipeline is the caller's policy. Import
openbtk.guardrails to register them all.
General¶
openbtk.guardrails.phi_leakage.PHILeakageGuardrail ¶
PHILeakageGuardrail(*, recognizers: tuple[str, ...] = ('rule',))
Bases: BaseGuardrail
Flags PHI-shaped spans in a payload.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
recognizers
|
tuple[str, ...]
|
Forwarded to the wrapped |
('rule',)
|
Example
import contextlib, io with contextlib.redirect_stdout(io.StringIO()): # registry logging ... guardrail = PHILeakageGuardrail() ... result = guardrail.check("Patient SSN: 123-45-6789.") ... clean = guardrail.check("No identifiers here.") result.passed False result.severity.value 'block' clean.passed True
Source code in src/openbtk/guardrails/phi_leakage.py
58 59 | |
openbtk.guardrails.terminology_validity.TerminologyValidityGuardrail ¶
TerminologyValidityGuardrail(*, terminology: BaseTerminologyService | None = None)
Bases: BaseGuardrail
Checks that every code found in a payload validates against its declared terminology system.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
terminology
|
BaseTerminologyService | None
|
The service to validate codes against. Defaults to
:class: |
None
|
A code the backend cannot confirm (TerminologyError -- an
unlicensed or unavailable backend) is reported as a WARNING, not
silently treated as valid or as a BLOCK-worthy invalid code:
docs/03_ARCHITECTURE.md section 8.2 is explicit that a terminology
backend "does not silently return valid," and conflating "could not
check" with "is invalid" would be its own kind of silent wrong answer.
Example
from openbtk.core.schemas import CodeSystem guardrail = TerminologyValidityGuardrail() guardrail.check(("E11.9", CodeSystem.ICD10CM)).passed True guardrail.check(("Z99.999", CodeSystem.ICD10CM)).passed False
Source code in src/openbtk/guardrails/terminology_validity.py
105 106 107 108 | |
openbtk.guardrails.groundedness.GroundednessGuardrail ¶
GroundednessGuardrail(*, support_threshold: float = 0.5, decompose_claims: Callable[[str], list[str]] = _default_decompose_claims, is_supported: Callable[[str, list[str]], bool] | None = None)
Bases: BaseGuardrail
Checks whether every claim in a generated answer is supported by its retrieved context, returning unsupported spans.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
support_threshold
|
float
|
Fraction of a claim's significant words that
must appear in the context for the default |
0.5
|
decompose_claims
|
Callable[[str], list[str]]
|
|
_default_decompose_claims
|
is_supported
|
Callable[[str, list[str]], bool] | None
|
|
None
|
check() only understands a :class:GroundednessCheckInput
payload (it needs both the answer AND its context together, which no
other schema in this project bundles) -- any other payload shape
passes with an INFO result, the same "not my concern, not a failure"
treatment TerminologyValidityGuardrail gives an unrelated payload.
Example
guardrail = GroundednessGuardrail() grounded = GroundednessCheckInput( ... answer="The patient has type 2 diabetes.", ... context=["Assessment: type 2 diabetes mellitus, stable."], ... ) guardrail.check(grounded).passed True ungrounded = GroundednessCheckInput( ... answer="The patient has a fractured femur.", ... context=["Assessment: type 2 diabetes mellitus, stable."], ... ) result = guardrail.check(ungrounded) result.passed False len(result.spans) 1
Source code in src/openbtk/guardrails/groundedness.py
157 158 159 160 161 162 163 164 165 166 167 168 169 | |
openbtk.guardrails.groundedness.GroundednessCheckInput ¶
Bases: BaseModel
The payload this guardrail's check() understands: a generated
answer plus the source text it was supposed to be grounded in.
Example
GroundednessCheckInput(answer="The sky is blue.").context []
openbtk.guardrails.dose.DosePlausibilityGuardrail ¶
DosePlausibilityGuardrail(*, reference: str | None = None, limits: Sequence[DoseLimit | Mapping[str, object]] | None = None)
Bases: BaseGuardrail
Flags a dose, daily total or route that breaks a limit in a reference table.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
reference
|
str | None
|
Path to a JSON file holding a list of :class: |
None
|
limits
|
Sequence[DoseLimit | Mapping[str, object]] | None
|
The same limits given directly, instead of (or as well as) a file. |
None
|
Example
limits = [DoseLimit( ... drug="examplamine", unit="mg", max_single=100, max_daily=300, ... source="illustrative values for documentation only", ... )] guardrail = DosePlausibilityGuardrail(limits=limits) guardrail.check("Give examplamine 500 mg PO BID.").passed False guardrail.check("Give examplamine 50 mg PO BID.").passed True
Source code in src/openbtk/guardrails/dose.py
254 255 256 257 258 259 260 261 262 263 264 | |
provenance ¶
provenance() -> ComponentProvenance
Records the number of limits and a hash of the table, not its content.
Source code in src/openbtk/guardrails/dose.py
303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 | |
openbtk.guardrails.dose.DoseLimit ¶
Bases: BaseModel
One drug's reference limits, with the source they came from.
Example
limit = DoseLimit( ... drug="examplamine", unit="mg", max_single=100, ... source="illustrative values for documentation only", ... ) limit.max_daily is None True
EHR¶
openbtk.guardrails.ehr.EHRCodeValidityGuardrail ¶
EHRCodeValidityGuardrail(*, terminology: BaseTerminologyService | None = None)
Bases: TerminologyValidityGuardrail
Every code in a PatientRecord (conditions/medications/
procedures/observations) exists in its declared system.
Genuinely the identical check :class:TerminologyValidityGuardrail
already performs -- PatientRecord was already one of its
supported payload shapes. Registered under this separate, modality-
scoped key because docs/05_DATA_MODALITY_SPEC.md section 2.5 names
one explicitly, and a real subclass (not
TERMINOLOGY_REGISTRY.register_alias, whose own docstring scopes
it to a deprecation transition for the same class under a renamed
key, not two permanent, independent semantic roles) keeps
registry_key/provenance honest about which key actually
constructed a given instance.
Source code in src/openbtk/guardrails/terminology_validity.py
105 106 107 108 | |
openbtk.guardrails.ehr.ReferentialIntegrityGuardrail ¶
Bases: BaseGuardrail
Every event's encounter_ref resolves to a real encounter, and
its timestamp (when known) falls within that encounter's window.
Example
import contextlib, io from datetime import datetime, UTC from openbtk.core.schemas import CodeSystem with contextlib.redirect_stdout(io.StringIO()): ... from openbtk.data.ehr.schemas import ( ... CodedEvent, Demographics, Encounter, PatientRecord, ... ) record = PatientRecord( ... patient_id="pt-1", ... demographics=Demographics(), ... encounters=[Encounter( ... encounter_id="enc-1", ... start=datetime(2024, 3, 14, tzinfo=UTC), ... end=datetime(2024, 3, 15, tzinfo=UTC), ... )], ... conditions=[CodedEvent( ... code="x", system=CodeSystem.SNOMED, encounter_ref="enc-1", ... timestamp=datetime(2024, 3, 20, tzinfo=UTC), ... )], ... source_system="fhir-r4", ... ) ReferentialIntegrityGuardrail().check(record).passed False
openbtk.guardrails.ehr.UnitPlausibilityGuardrail ¶
Bases: BaseGuardrail
Every observation's unit is a recognised UCUM string, and its value falls within a wide, illustrative physiological range for known LOINC codes.
A data-quality check, not a clinical decision support tool -- see
this module's own _PHYSIOLOGICAL_RANGES docstring comment for the
real, disclosed scope.
Example
from openbtk.data.ehr.schemas import ( ... Demographics, Measurement, PatientRecord, ... ) record = PatientRecord( ... patient_id="pt-1", ... demographics=Demographics(), ... observations=[Measurement(code="6690-2", value=400000, unit="10*3/uL")], ... source_system="fhir-r4", ... ) UnitPlausibilityGuardrail().check(record).passed False
openbtk.guardrails.ehr.CohortKAnonymityGuardrail ¶
CohortKAnonymityGuardrail(*, k: int = 5, quasi_identifiers: Sequence[str] = ('birth_year', 'gender', 'race', 'ethnicity'), as_of: date | str | None = None)
Bases: BaseGuardrail
A cohort is k-anonymous over its quasi-identifiers, or the check warns.
Give it the cohort (any iterable of PatientRecord, consumed once) and it counts
how many patients share each combination of the chosen attributes. If the smallest
group is under k the result is a WARNING (per docs/06_SECURITY_COMPLIANCE.md
section 3.10) saying how many patients are in a too-small group and how many are
unique. The result carries counts only, never a value.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
k
|
int
|
The smallest acceptable group. |
5
|
quasi_identifiers
|
Sequence[str]
|
Attributes to combine; see
|
('birth_year', 'gender', 'race', 'ethnicity')
|
as_of
|
date | str | None
|
The date |
None
|
It reaches "satisfied" only over the attributes you name, and says nothing about
what a group has in common. See openbtk.deid.kanonymity for what k-anonymity
does and does not give you, and anonymise_to_k to generalise a table until it
passes.
Example
from openbtk.data.ehr.schemas import Demographics, PatientRecord cohort = [ ... PatientRecord( ... patient_id=f"pt-{i}", ... demographics=Demographics(gender="female"), ... source_system="fhir-r4", ... ) ... for i in range(6) ... ] guardrail = CohortKAnonymityGuardrail(k=5, quasi_identifiers=["gender"]) guardrail.check(cohort).passed True guardrail.check(cohort[:2]).passed False
Source code in src/openbtk/guardrails/ehr.py
304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 | |
provenance ¶
provenance() -> ComponentProvenance
Records the threshold and the attributes used.
Source code in src/openbtk/guardrails/ehr.py
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 | |
Composing guardrails¶
openbtk.guardrails.pipeline.GuardrailPipeline ¶
GuardrailPipeline(guardrails: Sequence[BaseGuardrail | str], *, policy: PolicyConfig | None = None, short_circuit: bool = True)
Run an ordered list of guardrails over one payload.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
guardrails
|
Sequence[BaseGuardrail | str]
|
Each entry is either a real |
required |
policy
|
PolicyConfig | None
|
Forwarded to |
None
|
short_circuit
|
bool
|
When True (the default), stop running further guardrails once one returns a failed BLOCK-severity result. When False, every guardrail always runs, for a complete diagnostic report rather than a fail-fast check. |
True
|
Example
import contextlib, io with contextlib.redirect_stdout(io.StringIO()): ... from openbtk.guardrails.phi_leakage import PHILeakageGuardrail ... pipeline = GuardrailPipeline([PHILeakageGuardrail()]) ... result = pipeline.run("Patient SSN: 123-45-6789.") result.passed False len(result.results) 1
Source code in src/openbtk/guardrails/pipeline.py
123 124 125 126 127 128 129 130 131 | |
openbtk.guardrails.pipeline.GuardrailPipelineResult ¶
Bases: BaseModel
The outcome of one :meth:GuardrailPipeline.run call.
Example
result = GuardrailPipelineResult( ... passed=True, ... results=[GuardrailResult( ... passed=True, severity=GuardrailSeverity.INFO, ... guardrail_key="guardrail.general.phi_leakage", ... message="No PHI detected.", ... )], ... ) result.short_circuited False
to_guardrail_outcomes ¶
to_guardrail_outcomes(at: str) -> list[GuardrailOutcome]
Reshape each result into a GuardrailOutcome (checked_count=1
each, since this pipeline checks one payload, not a whole
streamed run) for attaching to a RunManifest.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
at
|
str
|
An attachment-point-shaped label the caller chooses -- this pipeline has no step id of its own, unlike the executor's own guardrail attachment. |
required |
Source code in src/openbtk/guardrails/pipeline.py
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | |