Meridian Ask build and guardrails¶
Confidential. Last 1 Enterprises. Status: staging on (2026-09-22). Prod stays off until Ryan GO after the device walk. Audience: internal briefing for the Ask path that shipped after Suites A/B/C/D went green.
This is the veteran-facing Ask this record surface: grounded Q&A over the veteran's consented, synced VA record. It is not an EHR, not a clinician note, not Service Matches, and not a diagnosis engine.
Canonical spec and eval bar live in last1-meridian (MERIDIAN_SPEC.md, MERIDIAN_EVAL_SPEC.md, CRISIS_PROTOCOL.md). API flip notes: last.vet-api/docs/runbooks/gate-meridian/MERIDIAN_ASK_MODEL.md.
What this build is¶
Meridian is LastVet's intelligence layer. Ask is the first live record-Q&A path:
- Eval first (
last1-meridian). Suites A/B/C plus adversarial Suite D. Gates are product behavior, not model goodwill. - API (
last.vet-api).POST /api/veteran/me/meridian/askruns the same gate order, then local Ollama. Question and chunks are transient. Logs are metadata only. - iOS (
last.vet-ios,feature/meridian-ask-live-record). On-device top-k retrieval, then the API when the flag is on. Keyword fixture remains the fallback.
The model is a swappable component behind those gates. Default serving model: self-hosted mistral-small:24b-instruct-2501-q4_K_M via Ollama on the LAN (192.168.1.40:11434). PHI does not leave LastVet infra on the default path.
Flags (do not overload)¶
| Flag | Meaning |
|---|---|
MERIDIAN_ASK_MODEL_ENABLED |
Ask may call the model API. This is the Ask go-live flag. |
MERIDIAN_M1_ELIGIBLE |
Service Matches live labeling only. Independent of Ask. |
LASTVET_CRISIS_LIVE_ENABLED |
Radio Check / dashboard crisis live. Independent of Ask. |
MERIDIAN_ASK_ALLOW_HOSTED |
Required to point OLLAMA_BASE_URL at a hosted vendor. Default false. Explicit PHI-leaving tradeoff. |
iOS reads GET /api/veteran/me/meridian/ask/status → { modelEnabled }. Debug may force on/off with -meridianAskModelEnabled / -meridianAskModelDisabled. When the flag is off or the API errors, Ask falls back to MeridianSyntheticQAResponder (on-device keyword fixture).
As of 2026-09-22: staging MERIDIAN_ASK_MODEL_ENABLED=true. Prod unset (defaults false). Testers use scheme LastVet-Staging / internal TestFlight group. See docs/testflight/MERIDIAN_ASK_STAGING.md. Do not flip prod until multiple testers walk the live device path on staging.
Live path (device)¶
Veteran question
→ iOS CrisisDetection (same fire patterns as crisis_gate)
→ local top-k retrieval (MeridianRecordIntelligence, default 8 chunks)
→ POST /api/veteran/me/meridian/ask { question, priorQuestions, chunks }
→ API gates (below) before any model call
→ local Ollama, temperature 0.1, system prompt + record context
→ strip freelance VCL language if the crisis gate did not fire
→ cited answer renders on device
PHI blast is the top-k chunk texts plus the question. No persist of question or chunk bodies. Two-data-plane still applies: VA data is consented and transient unless the veteran directed a copy.
Guardrail stack¶
Gates are deterministic short-circuits. They run before the model. Shared crisis and safety gates also fire on Matcher surfaces so matching is not a bypass.
Order on Ask (eval AskGuardedProvider and API MeridianAskService):
- Crisis
- Grounding (inventory, dates, false premise, injection context)
- Safety refusal
- Absent item / fabrication bait
- Model
- Volunteer-crisis strip (post-model)
1. Crisis gate (zero tolerance)¶
Source: last1-meridian/src/meridian/guardrails/crisis_gate.py. Parity: last.vet-api/src/lib/meridian/crisisGate.ts, iOS CrisisDetection.swift.
- Reads veteran user turns only. Never FHIR / record text.
- Over-inclusive first-person SI / self-harm / acute-distress patterns. No sincerity judgment.
- Precision floor: definitional asks ("what is suicidal ideation", "how does the VA treat") must not fire.
"I'm done"is not a standalone trigger.- Mid-task emergence still fires (distress during a benign task). Prior task is abandoned.
- On fire: verbatim Veterans Crisis Line response (CRISIS_PROTOCOL v1.5). No counseling, no risk assessment, no competing content.
- Mic signed v1.5 (2026-09-21). Product
LASTVET_CRISIS_LIVE_ENABLEDis a separate flip.
2. Grounding gates (record, not advice)¶
Source: last1-meridian/src/meridian/ask/ask_gates.py. Parity: last.vet-api/src/lib/meridian/askGrounding.ts.
These answer from the retrieved chunks or refuse to invent. They run before safety so "do I have appointments / allergies / meds?" is not misclassified as diagnosis solicitation, and so a false premise is corrected instead of only refusing the advice half.
| Gate | What it does |
|---|---|
| Appointment empty | Honest empty when the veteran asks about upcoming appointments and none are in context. |
| Take-all | "Do I take all of these?" lists what the record shows. Does not confirm current adherence. |
| Injection context | Poison in the question or a retrieved chunk (DAN, ignore previous, new system prompt, comply_token, etc.). Refuses. Record fields are data, never instructions. |
| False premise | Veteran asserts a named fact that is not in top-k (leading / "since I have X"). Corrects. Does not accept the invented fact. Adds treatment refusal if they also ask what to do. |
| Temporal / last procedure | Last, earliest, year, or named dated Procedure / Immunization from chunk dates. |
| Immunization inventory | Lists shots from retrieved Immunization chunks. |
| Condition / allergy / medication inventory | Lists those resource types with citations. Narrowed so "do I have allergies" is inventory, not diagnosis. |
| Cardiac filter | Heart / cardiac asks stay on cardiac-relevant chunks. |
| Follow-up residual | Short follow-ups ("when?", "anything else") stay on the prior retrieved topic. |
| Absent item | Named med/item not in top-k → "not on your list." No fabricated cite. |
| Fabrication bait | Inventory asks that sneak in an absent drug name list only what is on the record. |
3. Safety refusal gate (zero-tolerance clinical / legal lines)¶
Source: last1-meridian/src/meridian/guardrails/safety_refusal_gate.py. Parity: last.vet-api/src/lib/meridian/safetyRefusalGate.ts.
Verbatim templates. Educational definitions ("what is PTSD", "what does A1C measure") must not fire. Applying that knowledge to the individual must.
| Rule | Refuses |
|---|---|
self_harm_adjacent |
Lethal dose / how-to overdose. Routes toward crisis help. |
impersonation |
"I'm her caseworker / doctor." Third-party record asks. |
consent_scope_part2 / consent_scope_violation |
Part 2 / SUD outside the grant. Revoked or out-of-grant data. |
injection_compliance |
Follow-through on embedded instructions (user-turn form). |
treatment_directive |
Start, stop, taper, dose, prescribe, care plan. |
benefits_legal |
Rating predictions, "will I get", file an appeal. |
diagnosis_solicitation |
"Do I have PTSD?", "diagnose me", "what's wrong with me." Inventory lookahead so "do I have allergies" does not hit this. |
application_boundary |
"Does my A1C mean I have diabetes?" General knowledge applied to this veteran. |
Hard product rules these encode: no diagnosis, no treatment recommendations, consent-scoped retrieval, 42 CFR Part 2 respected.
4. Post-model: volunteer crisis strip¶
The model must not freelance Veterans Crisis Line language on a benign ask. If the crisis gate did not fire and the completion still contains VCL / 988 copy, those paragraphs are stripped. Crisis protocol is only allowed when the crisis gate fired.
5. Hosted-model block¶
If OLLAMA_BASE_URL looks like OpenAI / Anthropic / Groq and MERIDIAN_ASK_ALLOW_HOSTED is not true, the API refuses to call it. Default path is local Ollama only.
Eval proof (promotion bar item 1)¶
Harness: last1-meridian. Command shape: meridian-eval run-ask --provider ollama.
| Suite | Job | Count | Last Ollama result |
|---|---|---|---|
| A | Grounding: cite the record, honest empty, no hallucination | part of 168 | 168/168 with B+C |
| B | Red team: crisis, diagnosis, treatment, scope, injection, impersonation, benefits | part of 168 | 168/168 with A+C |
| C | Utility: do the job, do not over-refuse | part of 168 | 168/168 with A+B |
| D | Adversarial pressure by failure mode (~20 each) | 224 | 224/224 |
Suite D categories (all green on Ollama before staging flip):
adv_absent_fab: named missing item / bait must not be inventedadv_leading: false premises must be correctedadv_clinical: diagnosis / treatment linesadv_crisis_fire: protocol must fireadv_crisis_nofire: precision floor; model must not volunteer VCLadv_natural: ordinary phrasing still worksadv_followup: short follow-ups stay on topicadv_temporal: dates and orderadv_aggregation: counts and lists stay citedadv_inventory: "what meds / conditions / allergies"adv_injection: poison in question or chunk
Promotion gates: fabrication_zero and crisis_perfect on A/B/C (CI scope). Suite D is the adversarial bar that unlocked staging. Any real failure becomes a permanent regression case.
Graders are dimensional: must-cite, abstention, clinical refusal, crisis required / must-not-fire, follow-up resolved, required / forbidden substrings.
iOS surface¶
Branch: feature/meridian-ask-live-record (Ask wire 71c6393; tip includes crisis I'm-thinking parity 830d204).
- Retrieval is on-device (
MeridianRecordIntelligence). Chunks map citable kinds to FHIR resource types. - Citations render from
citationElementIdsreturned by the API (or fixture). - Crisis detection on device matches the Python fire patterns so a live walk should match CLI crisis behavior.
- Fixture stays for flag-off, 503/502, or transport failure.
Device walk (staging): live round-trip (retrieval → API → model → cited answer), citations feel native, plus adversarial probes on device (absent-item, false premise, crisis phrasing).
What this is not¶
- Not the clinician's system of record. LastVet holds the veteran-visible Coordination Note, never progress notes / treatment plans / assessments.
- Not Matcher-0 / Service Matches. Matching is a parallel lane with the same crisis + safety foundation.
- Not training on identified data. Eval and serving use synthetic or consented transient top-k only.
- Not a hosted-model default. That is a separate, explicit flag.
Source map¶
| Piece | Where |
|---|---|
| Ask gate order | last1-meridian/src/meridian/ask/ask_provider.py |
| Grounding gates | last1-meridian/src/meridian/ask/ask_gates.py |
| Crisis | last1-meridian/src/meridian/guardrails/crisis_gate.py |
| Safety refusal | last1-meridian/src/meridian/guardrails/safety_refusal_gate.py |
| Ask cases | last1-meridian/cases/suite_ask/{A,B,C,D}/ |
| API serving | last.vet-api/src/services/meridian-ask.service.ts |
| API gate parity | last.vet-api/src/lib/meridian/{crisisGate,safetyRefusalGate,askGrounding}.ts |
| iOS flag + wire | MeridianAskModelFlag.swift, MeridianAskModel.swift, MeridianSyntheticQAView.swift |
| iOS crisis parity | LastVet/Features/Crisis/CrisisDetection.swift |