AI Recommendation Engine Validation
Validation & migration plan for the AI recommendation engine.
Date: 2026-05-25 Author: Claude (validation session) Scope: 5-domain LLM-powered clinical recommendation engine Status: All 4 phases (A-D) implemented — code shipped, pending Supabase migration apply for Phase C MVs
1. The Vision
A unified AI recommendation engine that uses on-premise Ollama to surface 5 domains of suggestions while a clinician is working an encounter:
| # | Domain | Input | Output |
|---|---|---|---|
| 1 | Diagnosis | Chief complaint + vitals + patient context | Ranked ICD-10 / SNOMED differentials |
| 2 | Order Set | Confirmed diagnoses + encounter class | Suggested bundle (meds + labs + imaging + procedures) |
| 3 | Order recommendations | In-flight order + existing orders | “You usually add X with this” — cross-type associations |
| 4 | Medical coding | Completed encounter | ICD-10/9 + reimbursement-success-weighted ranking |
| 5 | Department quick-picks | Department + doctor + time of day | Top-N most-used orders (no LLM, pure SQL frequency) |
All driven by a 3-tier ranking signal (your history > department pattern > hospital pattern) with LLM enrichment for clinical reasoning (DDI, dose adjustment, allergy filtering, completeness).
2. Executive Summary
You don’t need to design a new architecture — there’s already a reference implementation at services/vision/.../medicationPlanner/medicationPlanner.controller.mixin.ts that does exactly this for medications:
POST /api/v2/vision/medication-planner/suggest
→ get_prescription_suggestions RPC (3-tier ranking)
→ fetchPatientContext (allergies, meds)
→ filterAllergicDrugs
→ LLM enrichment (DDI, dose, alternatives)
→ log to medication_ai_suggestions_log
POST /api/v2/vision/medication-planner/feedback
→ update RLHF columns
However, almost none of the supporting infrastructure is actually deployed on PH demo. Direct queries against https://hynsmfrevlsegbmjnoiy.supabase.co confirm:
| Component | Code | Migration | Deployed | Working |
|---|---|---|---|---|
prescription_patterns MV |
✅ | ✅ | ❌ | ❌ |
get_prescription_suggestions() RPC |
✅ | ✅ | ❌ | ❌ |
medication_ai_suggestions_log table |
✅ | ✅ | ❌ | ❌ |
encounter_journey_cache.diagnoses |
reads only | none | ❌ column missing | ❌ |
medication_requests table |
✅ | ✅ | ✅ 17 rows (seed) | ⚠️ |
coding_ai_suggestion_log table |
✅ | ✅ | ✅ 0 rows | scaffold |
vision service (NestJS) |
✅ | n/a | ✅ pid 1112789 | ⚠️ no calls hit it |
| Ollama (mistral:7b) | n/a | n/a | ✅ localhost:11434 | ✅ loaded |
OLLAMA_URL / OLLAMA_MODEL env |
n/a | n/a | ✅ set | ✅ |
VISION_PLANNER_ENABLED env |
n/a | n/a | ❌ unset | ❌ refuses to run |
LLM_PROVIDER / LLM_BASE_URL / LLM_MODEL env |
n/a | n/a | ❌ unset | ❌ llmClient would error |
3. The Three Stacked Gaps
Gap 1 (DATA SOURCE): medication_requests has 17 seed rows. prescriber_doctor_id
is NULL in 100% of rows — but only because the SEED INSERT
omits the column. Real prescribing via handleRxPrescribed.ts
would set it correctly. No real Rx is currently projecting
to Supabase though (no production data yet).
Gap 2 (DATA ENRICHMENT): encounter_journey_cache has NO diagnoses field — neither
column nor nested jsonb. Structured diagnoses live in
MongoDB (Encounter.diagnoses[] → Condition.code) but ZERO
code projects them to Supabase. Three different paths
expected by different consumers — none populated.
Gap 3 (AGGREGATION): Migration 20260524d (prescription_patterns MV + RPC +
audit log table) never applied. Easiest gap to close.
Gap 1 details — prescriber_doctor_id NULL
Root cause: Seed file infrastructure/medbase/migrations/20260518i_ipd_rx_test_seed.sql (lines 219-294) uses a direct INSERT INTO medication_requests with a hardcoded column list that omits prescriber_doctor_id. The seed pre-dates the column existing.
Real production prescribing is wired correctly:
infrastructure/medbase/functions/inpatient-handlers/handleRxPrescribed.ts:198— passesprescriberDoctorIdto the RPC- RPC
upsert_medication_requestacceptsp_prescriber_doctor_idand writes it (defined in20260518h_ipd_medication_orchestrator.sql+20260519a_medication_requests_emar_columns.sql)
Fix options:
- (a) Patch the seed — add
prescriber_doctor_idto the INSERT column list with a sandbox UUID - (b) Generate real prescriptions — order something via the IPD demo to validate the live path
- © Both — patch seed for reproducibility, generate live data for confidence
Gap 2 details — diagnoses not projected to Supabase
MongoDB shape:
packages/platform-api-schema/src/administration/encounter/entity/Encounter.ts—diagnoses?: EncounterDiagnosis[]EncounterDiagnosis.ts—{ conditionRef, use, rank, recordedAt, recordedBy, note }Condition.ts—code?: CodeableConcept(holds ICD-10 / SNOMED)
No projection layer:
- Clinical service emits no
DIAGNOSIS_ADDED/manifest.diagnosis.recordedevents infrastructure/medbase/functions/encounter-orchestrator/index.tshas no diagnosis handler (handles vitals, labs, meds, device readings, alerts — not diagnoses)
Triple-location confusion among readers:
| Consumer | Expects diagnoses at |
|---|---|
prescription_patterns MV (migration 20260524d) |
encounter_journey_cache.diagnoses (top-level column) |
Frontend web/src/services/ai/voice-order/diagnosis-context.ts:125 |
encounter_journey_cache.clinical_summary.diagnoses |
| RCM rule engine | clinical_context.diagnoses (nested) |
Recommended canonical path: encounter_journey_cache.clinical_context.diagnoses (nested in existing jsonb — no schema migration, fits accumulated-fact pattern of clinical_context, easy to update the 3 consumers).
Smallest fix:
- Add an
emitDiagnosisRecordedmixin inservices/clinical/.../encounterthat firesmanifest.diagnosis.recordedtohospital_eventswhenEncounter.diagnosesis mutated. - Add
handleDiagnosisRecordedininfrastructure/medbase/functions/encounter-orchestrator/handlers/that resolvesconditionRef → Condition.code, formats as[{code, icd10, name, use, rank}], and merges intoclinical_context.diagnosesvia jsonb_set. - Patch the prescription_patterns view extractor to read from
clinical_context->'diagnoses'->0->>'icd10'instead ofdiagnoses->0->>'icd10'. - Patch the frontend to read from the same canonical path.
Gap 3 details — migration not applied
Migration infrastructure/medbase/migrations/20260524d_prescription_patterns.sql was written but never applied. Per CLAUDE.md:
Supabase migrations: SQL editor in dashboard, OR
psql -fwith DB password ❌ manual (CLIdb pushblocked by drifted history table)
Two ways to apply: Supabase SQL editor (paste-and-run) or psql -f if you have the DB password.
4. What’s Already in Place (Reference Material)
4.1 Ollama (live on PH demo)
/usr/local/bin/ollama serverunning as pid 1379293mistral:7bmodel loaded (4.4 GB Q4_K_M)- Reachable at
localhost:11434/api/tags - Env vars set in
/opt/medos/medOS-ultra/env-files/ever/.env:OLLAMA_URL=http://localhost:11434OLLAMA_MODEL=mistral:7b
4.2 Vision service (running, planner code loaded but disabled)
- Process:
pid 1112789on PH demo EC2 - Modules: bloodBagScan, bloodBankVerify, deviceReader, medicationPlanner, medicationSafety, medicationVerify, nutritionAnalysis, pharmacyVerify, specimenQa, sterilizationQa, surgicalCount, woundAssess, wristbandId
- Shared LLM client at
services/vision/src/api/vision/modules/_shared/llmClient.tssupports both OpenAI-compatible and Ollama viaconfig.llmProvider - Planner refuses to run without
VISION_PLANNER_ENABLED=true
4.3 Voice Order Proxy (separate Ollama path — also live)
services/gateway/src/ai/voiceOrderProxy.ts- Route:
POST /api/v2/ai/voice-order - Uses Ollama via
/api/generate(JSON mode native) — different code path from llmClient’s/api/chat - 12 tool mappings (createLabRequest, createMedicationOrder, etc.)
- Standardization decision needed: unify on
/api/chat(OpenAI-compatible) for all modules, or keep two paths
4.4 What’s deployed in Supabase today
medication_requeststable (17 rows of seed)coding_ai_suggestion_logtable (0 rows, scaffold ready)encounter_journey_cachetable (229 rows, no diagnoses anywhere)safety_snapshotpopulated in 5.7% of encounters (13/229)order_bustable (real-time order event stream — excludes medications, only covers pathology/blood_bank/imaging/or_request/labour_room/admission/er_request)
5. Migration & Deployment Plan
Goal: Get the existing medication planner working end-to-end before extending to the other 4 domains.
Phase A — Unblock the medication planner (1-2 hours)
| Step | Action | Where | Risk |
|---|---|---|---|
| A1 | Apply migration 20260524d_prescription_patterns.sql |
Supabase SQL editor | Low |
| A2 | Patch seed 20260518i_ipd_rx_test_seed.sql to include prescriber_doctor_id |
Migration file + re-run seed | Low |
| A3 | Add env vars on PH backend | /opt/medos/medOS-ultra/env-files/ever/.env |
Low |
| A4 | Restart vision service via GH Actions workflow_dispatch |
GH Actions UI | Low |
| A5 | Smoke test POST /api/v2/vision/medication-planner/suggest with seed doctor_id |
curl from local | Low |
Expected outcome after Phase A: Medication planner returns rankings — but with icd10_code='unspecified' for every pattern (no Dx context yet). Proves the LLM + RPC + audit log pipeline is wired.
Env vars to add:
# Vision AI planner
VISION_PLANNER_ENABLED=true
LLM_PROVIDER=ollama
LLM_BASE_URL=http://localhost:11434
LLM_MODEL=mistral:7b
Phase B — Project diagnoses to Supabase (4-6 hours)
| Step | Action | Where | Risk |
|---|---|---|---|
| B1 | Add emitDiagnosisRecorded mixin to clinical encounter service |
services/clinical/src/api/clinical/modules/encounter/ |
Medium (touches MongoDB write path) |
| B2 | Add handleDiagnosisRecorded orchestrator handler |
infrastructure/medbase/functions/encounter-orchestrator/handlers/ |
Low (additive) |
| B3 | supabase functions deploy encounter-orchestrator |
Manual CLI | Low |
| B4 | Patch prescription_patterns MV to read clinical_context->'diagnoses'->0->>'icd10' |
New migration 20260526a_fix_rx_patterns_dx_path.sql |
Low |
| B5 | Refresh MV + verify icd10_code != 'unspecified' for new prescriptions |
Supabase SQL editor | Low |
| B6 | Backfill diagnoses for existing encounters (optional) | Standalone script reading MongoDB → upserting clinical_context.diagnoses |
Medium |
Expected outcome after Phase B: Real ICD-10 codes flow into prescription_patterns. The 3-tier ranking becomes meaningful. Medication planner returns Dx-specific suggestions.
Phase C — Extend to 4 additional domains (1-2 weeks)
Each new domain follows the medicationPlanner template. Per domain, create:
- Materialized view (template:
prescription_patterns) - RPC (template:
get_prescription_suggestions) - Vision service module (template:
vision/medicationPlanner/) - Audit log table (template:
medication_ai_suggestions_log— or rename tovision_ai_suggestions_logwith asuggestion_typecolumn to unify the 5 domains)
| Domain | MV | RPC | Vision Module | Notes |
|---|---|---|---|---|
| 2. Order Set | order_set_patterns |
suggest_order_set() |
vision.orderSetPlanner |
Returns bundle (meds + labs + imaging) per Dx |
| 3. Order recommendations | order_patterns (covers labs/imaging/procedures from order_bus union medication_requests) |
get_order_suggestions() |
vision.orderPlanner |
Per-type suggestions + cross-type associations |
| 1. Diagnosis | dx_complaint_patterns (chief_complaint text → final Dx pairs) |
suggest_diagnoses() |
vision.diagnosisSuggester |
Heavy LLM use — Ollama reasoning over symptoms |
| 4. Coding | extend coding_ai_suggestion_log with claim_outcome column |
suggest_coding() |
promote existing edge fn to vision.codingAssistant |
RLHF on reimbursement success |
| 5. Dept Quick-Picks | department_quick_picks (union over order_bus + medication_requests) |
get_dept_quick_picks() |
vision.departmentQuickPicks |
No LLM — pure SQL frequency |
Phase D — Unify the LLM client path (1 day)
- Decision: standardize on
/api/chat(OpenAI-compatible) for all vision modules - Migrate
voiceOrderProxyto usevision.voiceOrderaction that calls the same_shared/llmClient - Drop
services/gateway/src/ai/voiceOrderProxy.tsonce parity confirmed
6. Open Decisions Before Building
-
Where do diagnoses live in
encounter_journey_cache?- (a) New top-level column
diagnoses jsonb(cleanest; needs migration) - (b) Nested in
clinical_context.diagnoses(no migration; matches existing pattern) ← recommended - Either way, frontend
voice-order/diagnosis-context.tsand the MV both need updating to the chosen path.
- (a) New top-level column
-
Unified audit log or per-domain?
- (a) Keep
medication_ai_suggestions_log+ add 4 more parallel tables - (b) Rename to
vision_ai_suggestions_logwithsuggestion_type text NOT NULLcolumn ← recommended
- (a) Keep
-
voiceOrderProxyfuture?- (a) Keep separate JSON-mode path (faster ~30-40s)
- (b) Migrate to shared
llmClient/api/chat (slower but unified) ← decide after Phase A latency measurements
-
Seed real prescriptions vs patch the seed?
- (a) Patch the seed to add
prescriber_doctor_id(quick — unblocks smoke test) - (b) Generate real prescriptions through the demo UI (slow — but tests the live path)
- © Both ← recommended
- (a) Patch the seed to add
-
Frontend recommendation surface?
- Where in the order UI does the recommendation chip / drawer mount?
- Existing candidate:
OrderSystemPanelinweb/packages/miniapps/central-order-inspector/ - Out of scope for backend validation — defer to frontend planning session.
7. Evidence Log
Direct PostgREST queries against https://hynsmfrevlsegbmjnoiy.supabase.co (anon key from web/.env):
# Confirms prescription_patterns MV does NOT exist
GET /rest/v1/prescription_patterns → PGRST205
"Could not find the table 'public.prescription_patterns' in the schema cache"
# Confirms get_prescription_suggestions RPC does NOT exist
POST /rest/v1/rpc/get_prescription_suggestions → PGRST202
"Could not find the function public.get_prescription_suggestions"
# Confirms medication_ai_suggestions_log does NOT exist
GET /rest/v1/medication_ai_suggestions_log → PGRST205
# Confirms encounter_journey_cache.diagnoses column does NOT exist
GET /rest/v1/encounter_journey_cache?select=diagnoses → 42703
"column encounter_journey_cache.diagnoses does not exist"
# Confirms 17 rows in medication_requests, all encounter_class=IMP,
# all ward_id=SANDBOX-WARD-4A, all prescriber_doctor_id=NULL
GET /rest/v1/medication_requests?select=count → 17
GET /rest/v1/medication_requests?select=count&prescriber_doctor_id=not.is.null → 0
# Confirms clinical_context populated in 96.5% (221/229), but contains
# only chiefComplaint (free text) — no structured diagnoses
GET /rest/v1/encounter_journey_cache?select=encounter_id,clinical_context&limit=10
→ top-level keys observed: activity_tracker, admission_context,
department_progress, encounter_context, esi_level, order_ack_summary,
patient_context, screened_at, vitals_recorded
→ recursive search across 10 rows: 0 keys matching diag/icd/condition/problem
→ 1 row had admission_context.chiefComplaint = "Community-acquired pneumonia..."
PH demo EC2 (ssh ph-demo):
# Ollama running, mistral:7b loaded
pgrep -af ollama → 1379293 /usr/local/bin/ollama serve
curl -s localhost:11434/api/tags
→ {"models":[{"name":"mistral:7b","size":4372824384,...}]}
# Vision service running
pgrep -af moleculer-runner | grep vision → pid 1112789
# Env vars: Ollama set, planner/LLM unset
grep -iE 'PLANNER|OLLAMA|LLM_' /opt/medos/medOS-ultra/env-files/ever/.env
→ OLLAMA_URL=http://localhost:11434
→ OLLAMA_MODEL=mistral:7b
→ (no PLANNER_ENABLED, no LLM_PROVIDER, no LLM_BASE_URL, no LLM_MODEL)
8. Related Documents
docs/architecture/medication-ai-planner.md— original design for the medication planner (the reference implementation)docs/architecture/smart-diagnosis-unified-pipeline.md— frontend smart-diagnosis runner (already shipped asweb/src/services/ai/smart-diagnosis/)services/vision/src/api/vision/modules/medicationPlanner/medicationPlanner.controller.mixin.ts— code reference
9. Recommendation
Start with Phase A (1-2 hours, low risk). It unblocks the existing medication planner and proves the architecture is sound end-to-end with real Ollama + real Supabase. Once Phase A passes smoke test, Phases B-D become predictable mechanical work.
Do NOT design new modules until Phase A is green. Anything you add now would inherit the same hidden failure mode (silently returning [] from a missing RPC).