Tutorial 01 · Stream 2 — The Laboratory · DECISION-LAYER-2026-10
System-1 Clinical Decision Layers with Laya
A first-principles engineering guide to replacing slow, uncalibrated autoregressive verification stages with a
calibrated, single-pass bidirectional decision layer (Laya: ModernBERT-large +
[MASK]-gather head) evaluated against the OpenMRS ChartSearchAI validation harness.
00 · Problem Formulation
Why Clinical Agent Pipelines Need a Calibrated System-1 Layer
In multi-stage clinical retrieval and verification pipelines like OpenMRS ChartSearchAI, a generative language model ("System 2") is asked not only to synthesize a clinical answer, but also to filter candidate patient records for topic relevance, verify whether each generated sentence is grounded in cited observations, and review drafts for unsupported claims.
Inspecting the extracted telemetry in artifacts/decision-layer/phase0/phase0b_stage_timings.json
across 6,635 captured Med-Agent-Hub traces reveals two structural bottlenecks:
-
Latency Roofline Bottleneck: The autoregressive
reviewstage alone consumes 25.08% of total turn time (mean24.68s, median18.34s, p9052.33s), whileground_verdictsadds another2.07smean. Generating hundreds of Chain-of-Thought tokens just to emit a binary or 3-way verification verdict is memory-bandwidth bound on sequential KV-cache reads. -
Calibration & Precision Bottleneck: Off-the-shelf zero-shot NLI cross-encoders failed
our L6 clinical precision gate (
med-agent-hub#48 → #50) because general-domain entailment heads do not align with clinical chart conventions (abbreviated lab panels, temporal ranges, and implicit renal/HIV groupings) and output uncalibrated probabilities.
notebooks/laya_decision_layer.py—implementing each mathematical component from scratch via
scaffolded # TODO cells—and graduate the verified code into
ai-lab/experiments/laya/ to pass Gate G0 (Phase 0a LOPO) and
Gate G1 (Phase 0b shadow calibration).
Module 01 · Probabilistic Foundations
Proper Scoring Rules, Brier Decomposition & Temperature Scaling
1.1 Strictly Proper Scoring Rules vs. Expected Calibration Error
Let $Y \in \{0, 1\}$ denote the ground-truth clinical verdict (e.g., $Y=1$ if a chart record is relevant to the queried topic, or if a clinical claim is supported by cited evidence) with true conditional probability $q(x) = \Pr(Y = 1 \mid X = x)$. Suppose our model outputs a predicted probability $p(x) \in (0, 1)$.
A scoring rule $\mathcal{L}(p, y)$ is proper if the expected loss under the true distribution $Y \sim \text{Bernoulli}(q)$ is minimized when the model reports the true probability $p = q$, and strictly proper if $p = q$ is the unique minimizer:
Both Negative Log-Likelihood (Log Loss) and the Brier Score are strictly proper scoring rules:
- Brier Score: $\mathcal{L}_{\text{Brier}}(p, y) = (p - y)^2$. Taking the expectation yields $\mathbb{E}[(p - Y)^2] = q(1 - p)^2 + (1 - q)p^2 = (p - q)^2 + q(1 - q)$. Differentiating with respect to $p$ gives $\frac{\partial}{\partial p}\mathbb{E}[\mathcal{L}_{\text{Brier}}] = 2(p - q) = 0 \iff p^* = q$.
- Negative Log-Likelihood (NLL): $\mathcal{L}_{\text{NLL}}(p, y) = -y \log p - (1 - y)\log(1 - p)$. Its expectation equals $H(q) + D_{\text{KL}}(\text{Bern}(q) \parallel \text{Bern}(p))$, where Gibbs' inequality guarantees $D_{\text{KL}} \ge 0$ with equality if and only if $p^* = q$.
In contrast, Expected Calibration Error (ECE) partitions predictions into $M$ confidence bins $B_1, \dots, B_M$ and computes the weighted gap between empirical bin accuracy $\bar{y}_m$ and mean predicted confidence $\bar{p}_m$:
Why ECE alone is not a proper scoring rule: ECE measures only marginal bin reliability, not discriminative sharpness. A degenerate classifier that ignores $x$ and outputs the constant marginal prevalence $p(x) \equiv \bar{y} = 0.0116$ places all examples into a single bin whose empirical accuracy equals $0.0116$, achieving $\text{ECE} = 0$ with zero discriminative power ($\text{AUROC} = 0.50$).
1.2 Murphy's (1973) Three-Component Decomposition of the Brier Score
Murphy showed that if predictions are grouped into $M$ discrete bins $B_1, \dots, B_M$ with bin count $n_m$, bin mean probability $\bar{p}_m$, bin empirical frequency $\bar{y}_m$, and overall dataset base rate $\bar{y} = \frac{1}{N}\sum_{i=1}^N y_i$, the Brier score decomposes into three interpretable quantities:
- Reliability ($\downarrow$ better): The squared L2 analogue of ECE. Zero when every bin's empirical accuracy matches its predicted confidence.
- Resolution ($\uparrow$ better): Measures how far conditional bin accuracies $\bar{y}_m$ separate from the marginal base rate $\bar{y}$. A constant predictor has $\text{Resolution} = 0$.
- Uncertainty (fixed): The variance $\bar{y}(1 - \bar{y})$ of the Bernoulli ground truth, independent of the model.
1.3 Post-Hoc Temperature Scaling & Rank Invariance
When a neural encoder like Laya is fine-tuned with cross-entropy loss, weight magnitudes grow once training
examples are classified correctly, pushing raw logits $z_i$ far from zero. In the Laya technical report,
task fine-tuning raises accuracy from 0.362 → 0.766 (`0.857` on binary `noul`), but leaves raw
probabilities severely overconfident with mean ECE = 0.466.
Temperature Scaling (Guo et al., 2017) divides logits by a single positive scalar $T > 0$ fit by minimizing NLL on a held-out calibration split:
Because $z \mapsto \sigma(z / T)$ is strictly monotonically increasing for any $T > 0$, for every pair of
examples $(i, j)$ we have $z_i > z_j \iff \sigma(z_i / T) > \sigma(z_j / T)$. Consequently,
Temperature Scaling leaves AUROC, AUPRC, and top-1 accuracy strictly unchanged while collapsing
Laya's mean ECE from 0.466 to 0.081.
Reliability Diagram, Brier Decomposition & Temperature Scaling
1.4 Marimo Implementation Blueprint (Cell 1)
Copy this scaffold into notebooks/laya_decision_layer.py and implement the four functions without
overflowing on large logits ($\pm 500$). The self-check assertions at the bottom of the cell verify your
implementation against exact analytical identities.
@app.cell
def _(np):
import scipy.optimize as opt
def stable_sigmoid(logits: np.ndarray) -> np.ndarray:
"""Numerically stable elementwise logistic sigmoid σ(z) = 1 / (1 + exp(-z)).
Contract:
- Must not overflow or emit RuntimeWarning for logits in [-1000, +1000].
- Hint: branch on (logits >= 0) using np.where; use exp(-z) for z >= 0 and exp(z) for z < 0.
"""
# TODO: implement branch-stable sigmoid
raise NotImplementedError("Implement stable_sigmoid")
def binary_nll_from_logits(logits: np.ndarray, labels: np.ndarray) -> float:
"""Mean binary cross-entropy loss directly from raw logits z and binary labels y in {0, 1}.
Formula per item:
max(z, 0) - z * y + log(1 + exp(-|z|))
"""
# TODO: compute mean stable binary NLL using np.maximum and np.log1p
raise NotImplementedError("Implement binary_nll_from_logits")
def brier_decomposition(probs: np.ndarray, labels: np.ndarray, n_bins: int = 10) -> dict[str, float]:
"""Compute Brier score, 10-bin ECE, and Murphy's (REL, RES, UNC) decomposition.
Bins:
- Partition [0.0, 1.0] into `n_bins` equal-width bins via np.linspace(0.0, 1.0, n_bins + 1).
- Include the right endpoint 1.0 in the last bin.
"""
# TODO: compute brier, ece, reliability, resolution, uncertainty
raise NotImplementedError("Implement brier_decomposition")
def fit_temperature(val_logits: np.ndarray, val_labels: np.ndarray) -> float:
"""Find T* > 0 minimizing binary_nll_from_logits(val_logits / T, val_labels).
Hint:
- Optimize over log_T in [-2.5, 2.5] using scipy.optimize.minimize_scalar(..., method="bounded")
so T = exp(log_T) is guaranteed strictly positive.
"""
# TODO: fit and return optimal temperature T* > 0
raise NotImplementedError("Implement fit_temperature")
# --- Self-Check Assertions (uncomment once implemented) ---
# _z = np.array([-800.0, 0.0, 800.0])
# assert np.allclose(stable_sigmoid(_z), [0.0, 0.5, 1.0])
# _rng = np.random.default_rng(0)
# _true_z = _rng.normal(0, 1.5, size=400)
# _y = (_rng.random(400) < stable_sigmoid(_true_z)).astype(np.float64)
# _overconf_z = _true_z * 3.0
# _t_star = fit_temperature(_overconf_z, _y)
# assert 2.0 < _t_star < 4.0, f"Expected T* near 3.0, got {_t_star:.3f}"
# assert binary_nll_from_logits(_overconf_z / _t_star, _y) < binary_nll_from_logits(_overconf_z, _y)
return binary_nll_from_logits, brier_decomposition, fit_temperature, stable_sigmoid
Module 02 · Representation & Systems Roofline
Bi-Encoders vs. Cross-Encoders vs. Single-Pass Multi-Option Packing
2.1 The Rank Bottleneck of Dual Encoders vs. Token-Level Cross-Attention
Consider scoring whether a patient chart record $d \in \mathcal{D}$ is relevant to clinical topic query $q \in \mathcal{Q}$, or whether a generated claim $q$ is entailed by evidence $d$. Three architectural families trade off representational expressivity against compute latency:
- Bi-Encoder (Dual Encoder): Computes independent embeddings $u = f(q) \in \mathbb{R}^r$ and $v = g(d) \in \mathbb{R}^r$ and scores via inner product $S(q, d) = \langle f(q), g(d) \rangle$. Across any grid of $|\mathcal{Q}| \times |\mathcal{D}|$ pairs, the score matrix $S$ is constrained to $\text{rank}(S) \le r$. More importantly for clinical verification, $g(d)$ must compress a lab record into a single vector without knowing which date, numeric threshold, or negation scope $q$ will ask about.
-
Standard Cross-Encoder: Concatenates $[q; d]$ and applies all-pairs self-attention
$A_{ij} = \text{softmax}(h_i W_Q W_K^\top h_j / \sqrt{d_k})$ across every layer. Token-level cross-attention
binds specific values (e.g., matching
"Serum Creatinine 1.8 mg/dL"in Patient 39's chart to thekidneytopic or detecting"denies chest pain"). However, evaluating $K$ candidate decision options requires either $K$ separate forward passes or a fixed task-specific linear classifier head. -
Laya Single-Pass Multi-Option Packing: Packs the evidence context, question, and all $K$
candidate options into one sequence using XML tags and
[MASK]markers:<a>Context</a><q>Question?[MASK]Option_1[MASK]Option_2...</q>. A single bidirectional encoder pass allows every[MASK]token to attend simultaneously to the evidence, the question, and the competing options.
2.2 Hardware Roofline: Autoregressive Decode vs. Single Encoder Prefill
Autoregressive LLM verification (`review`) generates $T_{\text{out}}$ reasoning tokens sequentially. Each generated token reads the full model weights and KV cache from memory, making decode strictly memory-bandwidth bound with latency $O(T_{\text{out}} \cdot \text{bytes}_{\text{model}} / \text{BW})$. In contrast, Laya processes a batch of $N$ verification items in a single parallel prefill pass ($T_{\text{out}} = 0$), operating in the compute-bound matrix-multiply regime on Apple Silicon MPS or GPU Tensor Cores.
Verification Latency Roofline (Calibrated to Phase 0b Timings)
2.3 Marimo Implementation Blueprint (Cell 2 — Phase 0a & Patient 39 Data Inspection)
In Phase 0a, Patient 39 (dd5558ed-1691-11df-97a5-7038c432aabf) had 3 serum creatinine records
dropped during retrieval under the kidney topic. Write the data loader and verify the base rate
and Patient 39's gold records.
@app.cell
def _(Path, json):
PHASE0_DIR = Path("artifacts/decision-layer/phase0")
PATIENT_39_UUID = "dd5558ed-1691-11df-97a5-7038c432aabf"
PATIENT_39_RENAL_IDS = {
"c1aede5e-1691-11df-97a5-7038c432aabf",
"c4c1e455-1691-11df-97a5-7038c432aabf",
"ccac7083-1691-11df-97a5-7038c432aabf",
}
def load_phase0a_pairs(phase0_dir: Path = PHASE0_DIR) -> list[dict]:
"""Load all 7,840 relevance pairs from phase0a_relevance_pairs.jsonl."""
# TODO: read and parse JSONL rows from phase0_dir / "phase0a_relevance_pairs.jsonl"
raise NotImplementedError("Implement load_phase0a_pairs")
def summarize_phase0a(rows: list[dict]) -> dict:
"""Compute total pairs, positive count, positive base rate, and Patient 39 kidney renal records.
Expected keys:
- total_pairs (int): should be 7840
- positive_pairs (int): should be 91
- positive_rate (float): should be ~0.011607
- p39_kidney_renal_records (list[dict]): the 3 records matching PATIENT_39_UUID,
topic == "kidney", and record_id in PATIENT_39_RENAL_IDS.
"""
# TODO: compute summary dictionary over `rows`
raise NotImplementedError("Implement summarize_phase0a")
# --- Self-Check Assertions (uncomment once implemented) ---
# phase0a_rows = load_phase0a_pairs()
# summary0a = summarize_phase0a(phase0a_rows)
# assert summary0a["total_pairs"] == 7840
# assert summary0a["positive_pairs"] == 91
# assert len(summary0a["p39_kidney_renal_records"]) == 3
return PATIENT_39_RENAL_IDS, PATIENT_39_UUID, PHASE0_DIR, load_phase0a_pairs, summarize_phase0a
Module 03 · Model Architecture
Inside Laya: ModernBERT-large + [MASK] Gather Decision Head
3.1 Anatomical Breakdown of convaiinnovations/laya
Laya pairs a 28-layer ModernBERT-large bidirectional encoder ($d = 1024$, 16 attention heads,
rotary position embeddings, alternating global and 128-token local sliding-window attention) with a
custom LayaHead that converts contextualized [MASK] token embeddings
into option logits and an epistemic confidence score:
Here $\text{task\_type} \in \{0, 1, 2\}$ indexes a learned embedding table (`0 = choice`, `1 = score`,
`2 = noul` binary classification). In addition to the option logits $z \in \mathbb{R}^{B \times K}$, Laya
computes 4 distributional summary statistics from the option probability distribution $p = \text{softmax}(z)$—the
top-1 probability $p_{(1)}$, the top-1 vs. top-2 margin $p_{(1)} - p_{(2)}$, the normalized Shannon entropy
$H(p) / \log K$, and the normalized option count $K / 255$—concatenates them with the sequence
[CLS] representation $\mathbf{X}_{:, 0, :} \in \mathbb{R}^{B \times 1024}$, and passes the
$1028$-dimensional vector through act_head.
Step-by-Step Tensor Shape & torch.gather Tracer
3.2 Marimo Implementation Blueprint (Cell 3 — LayaDecisionHead in PyTorch)
Implement the sequence packer and the vectorized LayaDecisionHead module in
notebooks/laya_decision_layer.py. Pay particular attention to how marker_pos is
clamped and expanded to `[B, K, D]` before calling torch.gather(x, dim=1, index=idx).
@app.cell
def _(torch):
import torch.nn as nn
def format_laya_prompt(context: str, question: str, options: list[str], mask_token: str = "[MASK]") -> str:
"""Format a clinical context, question, and K options into Laya's single-pass XML template.
Template:
"{context}{question}{mask_token}{opt_0}{mask_token}{opt_1}...
"
"""
# TODO: construct and return the XML-packed prompt string
raise NotImplementedError("Implement format_laya_prompt")
class LayaDecisionHead(nn.Module):
"""Vectorized [MASK]-gather decision head matching convaiinnovations/laya."""
def __init__(self, d_model: int = 1024, n_heads: int = 16, n_layers: int = 2):
super().__init__()
# TODO: initialize:
# 1. self.type_emb = nn.Embedding(3, d_model)
# 2. self.transformer = 2-layer nn.TransformerEncoder (d_model, n_heads, dim_feedforward=4*d_model,
# activation="gelu", batch_first=True, norm_first=True)
# 3. self.norm = nn.LayerNorm(d_model)
# 4. self.scorer = 2-layer MLP: Linear(d_model, d_model // 2) -> GELU -> Linear(d_model // 2, 1)
# 5. self.act_head = 2-layer MLP: Linear(d_model + 4, d_model // 4) -> GELU -> Linear(d_model // 4, 1)
raise NotImplementedError("Initialize LayaDecisionHead submodules")
def forward(
self,
hidden: torch.Tensor, # [B, L, D] backbone hidden states
attention_mask: torch.Tensor, # [B, L] 1 for valid tokens, 0 for pad
marker_pos: torch.Tensor, # [B, K] sequence indices of [MASK] tokens (-1 for unused slots)
marker_mask: torch.Tensor, # [B, K] bool True for active options, False for padded slots
task_type: torch.Tensor, # [B] int64 in {0=choice, 1=score, 2=noul}
) -> tuple[torch.Tensor, torch.Tensor]:
"""Return (option_logits [B, K], act_logit [B, 1]).
Steps:
1. Add broadcast task-type embedding self.type_emb(task_type).unsqueeze(1) to `hidden`.
2. Pass through self.transformer (with src_key_padding_mask=~attention_mask.bool()) and self.norm.
3. Clamp marker_pos to >= 0, expand to [B, K, D], and torch.gather along dim=1 to get opt_h [B, K, D].
4. Score opt_h with self.scorer -> squeeze(-1) -> mask ~marker_mask with -1e9 -> logits [B, K].
5. Compute top1_prob, margin (top1 - top2), normalized_entropy, and K_valid / 255.0;
concatenate with x[:, 0, :] and pass through self.act_head -> act_logit [B, 1].
"""
# TODO: implement vectorized forward pass
raise NotImplementedError("Implement LayaDecisionHead.forward")
# --- Self-Check Assertions (uncomment once implemented) ---
# _head = LayaDecisionHead(d_model=64, n_heads=4, n_layers=2)
# _H = torch.randn(3, 20, 64)
# _amask = torch.ones(3, 20, dtype=torch.long)
# _mpos = torch.tensor([[5, 9, -1], [4, 8, 12], [6, 11, -1]])
# _mmask = _mpos >= 0
# _ttype = torch.tensor([2, 0, 2])
# _logits, _act = _head(_H, _amask, _mpos, _mmask, _ttype)
# assert _logits.shape == (3, 3) and _act.shape == (3, 1)
# assert _logits[0, 2].item() <= -1e8, "Unused option slot must be masked to -1e9"
return LayaDecisionHead, format_laya_prompt, nn
Module 04 · Phase 0a Relevance Filtering & Gate G0
Extreme Class Imbalance (1.16%), Prior Shift & 5-Fold LOPO
4.1 Bayes Logit Shift Under Importance-Weighted Cross-Entropy (Elkan, 2001)
In phase0a_relevance_pairs.jsonl, only 91 out of 7,840 patient-record/topic pairs
(`1.16%`) are positive. Training a classifier with unweighted binary cross-entropy on a $1.16\%$ base
rate causes early gradient updates to push all logits negative ($p \approx 0.01$), starving minority-class recall.
To prevent minority collapse, we up-weight positive examples by $w_+ > 1$ in the training loss. Let $q(x) = \Pr(Y=1 \mid x)$ denote the true conditional probability on the unweighted clinical population. For a single input $x$, the pointwise expected weighted cross-entropy loss with respect to logit $z$ is:
Differentiating with respect to $z$ and setting $\frac{\partial \mathcal{L}_{w_+}}{\partial z} = 0$ using $\frac{d}{dz}\log\sigma(z) = 1 - \sigma(z)$ and $\frac{d}{dz}\log(1-\sigma(z)) = -\sigma(z)$:
4.2 Leave-One-Patient-Out (LOPO) Cross-Validation & Gate G0
Because records within the same patient chart share longitudinal boilerplate, random pair-level splits leak patient-specific vocabulary across train and test. Phase 0a therefore uses a 5-fold Leave-One-Patient-Out (LOPO) design across the 5 benchmark patients. To pass Gate G0, the out-of-fold predictions must satisfy all three criteria simultaneously:
- Out-of-fold Recall $\ge 0.95$ across the 91 gold-relevant pairs.
- Irrelevant Record Pruning Ratio $\ge 0.40$ (at least 40% of negative records filtered out).
- Patient 39 Renal Rescue ($3/3$): All 3 of Patient 39's creatinine records (
c1aede5e,c4c1e455,ccac7083) must be admitted under thekidneytopic when Patient 39 is the held-out test patient.
1.16% Prevalence Prior-Shift & Gate G0 Explorer
4.3 Marimo Implementation Blueprint (Cell 4 — Prior-Shift Correction & 5-Fold LOPO Gate G0)
@app.cell
def _(PATIENT_39_RENAL_IDS, PATIENT_39_UUID, np, stable_sigmoid):
def correct_prior_shift_logits(
raw_logits: np.ndarray,
pos_weight: float,
temperature: float = 1.0,
) -> np.ndarray:
"""Remove the +log(pos_weight) Bayes logit shift and apply temperature scaling.
Formula:
z_cal = (raw_logits - log(pos_weight)) / temperature
"""
# TODO: compute and return calibrated logits z_cal
raise NotImplementedError("Implement correct_prior_shift_logits")
def evaluate_gate_g0(
records: list[dict],
oof_probs: np.ndarray,
threshold: float,
) -> dict:
"""Evaluate 5-fold LOPO out-of-fold probabilities against Gate G0 criteria.
Required return keys:
- recall (float): TP / (TP + FN) over gold_relevant == 1
- prune_ratio (float): TN / (TN + FP) fraction of irrelevant records filtered out (pred == 0)
- patient_39_renal_admitted (int): count of PATIENT_39_RENAL_IDS under topic=='kidney' with pred == 1
- gate_g0_pass (bool): recall >= 0.95 and prune_ratio >= 0.40 and patient_39_renal_admitted == 3
"""
# TODO: implement Gate G0 evaluation metrics
raise NotImplementedError("Implement evaluate_gate_g0")
# --- Self-Check Assertions (uncomment once implemented) ---
# _z = np.array([2.0, 0.0])
# _z_cal = correct_prior_shift_logits(_z, pos_weight=np.e, temperature=2.0)
# assert np.allclose(_z_cal, [0.5, -0.5])
return correct_prior_shift_logits, evaluate_gate_g0
Module 05 · Phase 0b Grounding, Review & Gate G1
Selective Classification, Risk-Coverage Curves & 3-Band Routing
5.1 Selective Risk-Coverage Trade-Off (El-Yaniv & Wiener, 2010)
In Phase 0b (`4,703` decision items extracted from `6,635` Hub traces across `answer_grounding`, `answer_review_error`, `indepth_grounding`, and `indepth_review`), Laya acts as a fast System-1 filter in front of the slow System-2 LLM reviewer. Rather than forcing a binary choice on ambiguous clinical claims, we define a selective classifier $(f, g)$ governed by two calibrated probability thresholds $0 < \tau_{\text{low}} < \tau_{\text{high}} < 1$:
For any threshold pair $(\tau_{\text{low}}, \tau_{\text{high}})$, performance is characterized by:
- System-1 Coverage $\Phi(f, g)$: The fraction of items resolved directly by Laya without invoking the LLM: $\Phi = \Pr(g(X) = 1) = \Pr(\hat{p}_{\text{cal}}(X) \le \tau_{\text{low}} \cup \hat{p}_{\text{cal}}(X) \ge \tau_{\text{high}})$.
- Selective Risk $R(f, g)$: The error rate conditioned on Laya making a definitive decision: $R = \Pr(\hat{Y} \neq Y \mid g(X) = 1)$.
- Expected Turn Review Latency: Because only the uncertain Yellow band ($1 - \Phi$) escalates to the `24.68s` mean LLM review stage, expected latency drops to $L(\Phi) = \Phi \cdot L_{\text{Laya}} + (1 - \Phi) \cdot L_{\text{LLM}}$.
Phase 0b 3-Band Thresholds, Selective Risk & Turn Latency Savings
5.2 Marimo Implementation Blueprint (Cell 5 — 3-Band Selective Router & Optimal Threshold Search)
@app.cell
def _(np):
def route_three_band(
probs: np.ndarray,
tau_low: float = 0.20,
tau_high: float = 0.82,
) -> list[str]:
"""Map calibrated probabilities to ['unsupported', 'uncertain', 'verified'].
Contract:
- prob <= tau_low -> 'unsupported'
- prob >= tau_high -> 'verified'
- otherwise -> 'uncertain'
"""
# TODO: validate 0 < tau_low < tau_high < 1 and return band labels
raise NotImplementedError("Implement route_three_band")
def selective_risk_coverage(
probs: np.ndarray,
labels: np.ndarray,
tau_low: float,
tau_high: float,
llm_mean_sec: float = 24.68,
laya_mean_sec: float = 0.02,
) -> dict[str, float]:
"""Compute coverage, selective_risk, escalation_rate, and expected_latency_sec."""
# TODO: compute selective classification metrics on decided mask ((probs <= tau_low) | (probs >= tau_high))
raise NotImplementedError("Implement selective_risk_coverage")
def select_thresholds_for_risk_budget(
val_probs: np.ndarray,
val_labels: np.ndarray,
max_selective_risk: float = 0.02,
) -> tuple[float, float]:
"""Grid-search (tau_low, tau_high) to maximize coverage subject to selective_risk <= max_selective_risk."""
# TODO: search tau_low in np.linspace(0.05, 0.45, 41) and tau_high in np.linspace(0.55, 0.95, 41)
raise NotImplementedError("Implement select_thresholds_for_risk_budget")
return route_three_band, select_thresholds_for_risk_budget, selective_risk_coverage
Module 06 · Production Wiring
FastAPI Serving & ChartSearchAI Harness Integration (med-agent-hub#49)
Once your calibrated Laya checkpoint passes Gate G0 and Gate G1 in Marimo and pytest, it wires into
/Users/pmanko/code/openclinai.org/work/decision-layer/targets/med-agent-hub (`src/services/laya.ts`)
over a local HTTP contract. Click through the three task families below to inspect the exact JSON schemas
consumed by Med-Agent-Hub and the validation harness replay script (`decision-replay.py`).