medOS ultra

AI Recommendation Engine Validation

Validation & migration plan for the AI recommendation engine.

11 min read diagramsUpdated 2026-05-25docs/architecture/ai-recommendation-engine-validation.md

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 — passes prescriberDoctorId to the RPC
  • RPC upsert_medication_request accepts p_prescriber_doctor_id and writes it (defined in 20260518h_ipd_medication_orchestrator.sql + 20260519a_medication_requests_emar_columns.sql)

Fix options:

  • (a) Patch the seed — add prescriber_doctor_id to 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.tsdiagnoses?: EncounterDiagnosis[]
  • EncounterDiagnosis.ts{ conditionRef, use, rank, recordedAt, recordedBy, note }
  • Condition.tscode?: CodeableConcept (holds ICD-10 / SNOMED)

No projection layer:

  • Clinical service emits no DIAGNOSIS_ADDED / manifest.diagnosis.recorded events
  • infrastructure/medbase/functions/encounter-orchestrator/index.ts has 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:

  1. Add an emitDiagnosisRecorded mixin in services/clinical/.../encounter that fires manifest.diagnosis.recorded to hospital_events when Encounter.diagnoses is mutated.
  2. Add handleDiagnosisRecorded in infrastructure/medbase/functions/encounter-orchestrator/handlers/ that resolves conditionRef → Condition.code, formats as [{code, icd10, name, use, rank}], and merges into clinical_context.diagnoses via jsonb_set.
  3. Patch the prescription_patterns view extractor to read from clinical_context->'diagnoses'->0->>'icd10' instead of diagnoses->0->>'icd10'.
  4. 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 -f with DB password ❌ manual (CLI db push blocked 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 serve running as pid 1379293
  • mistral:7b model 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:11434
    • OLLAMA_MODEL=mistral:7b

4.2 Vision service (running, planner code loaded but disabled)

  • Process: pid 1112789 on 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.ts supports both OpenAI-compatible and Ollama via config.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_requests table (17 rows of seed)
  • coding_ai_suggestion_log table (0 rows, scaffold ready)
  • encounter_journey_cache table (229 rows, no diagnoses anywhere)
  • safety_snapshot populated in 5.7% of encounters (13/229)
  • order_bus table (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:

  1. Materialized view (template: prescription_patterns)
  2. RPC (template: get_prescription_suggestions)
  3. Vision service module (template: vision/medicationPlanner/)
  4. Audit log table (template: medication_ai_suggestions_log — or rename to vision_ai_suggestions_log with a suggestion_type column 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 voiceOrderProxy to use vision.voiceOrder action that calls the same _shared/llmClient
  • Drop services/gateway/src/ai/voiceOrderProxy.ts once parity confirmed

6. Open Decisions Before Building

  1. 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.ts and the MV both need updating to the chosen path.
  2. Unified audit log or per-domain?

    • (a) Keep medication_ai_suggestions_log + add 4 more parallel tables
    • (b) Rename to vision_ai_suggestions_log with suggestion_type text NOT NULL column ← recommended
  3. voiceOrderProxy future?

    • (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
  4. 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
  5. Frontend recommendation surface?

    • Where in the order UI does the recommendation chip / drawer mount?
    • Existing candidate: OrderSystemPanel in web/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)

  • 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 as web/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).

Ask Anything