Medication AI Planner
Premium add-on: pattern-learning prescription intelligence.
Premium add-on. Feature-flagged behind
medos_premium_medication_planner. Learns from the hospital’s own prescribing history, then layers LLM reasoning for dose optimization, DDI resolution, and regimen completeness.
Design Philosophy
Learn first, suggest second. Generic drug databases are useful but miss how this hospital’s doctors actually prescribe. A cardiologist at Siriraj prescribes differently than one at Chula — different formulary preferences, different dosing conventions, different insurance constraints. The AI should learn from the patterns already in the data before adding textbook reasoning on top.
Architecture
┌────────────────────────────────────────────────────────────────────────┐
│ Layer 0 — Pattern Mining (Supabase materialized view + RPC) │
│ │
│ Source: medication_requests + encounter_journey_cache + order_requests │
│ Aggregation dimensions: │
│ • department (ward_id) │
│ • prescriber (prescriber_doctor_id) │
│ • diagnosis (ICD-10 from encounter or order_request_item) │
│ • encounter class (OPD / IPD / ER) │
│ │
│ Output: prescription_patterns (materialized view) │
│ { drug_key, drug_name, dose, dose_uom, frequency, route, duration, │
│ department_id, doctor_id, icd10_code, encounter_class, │
│ rx_count, last_prescribed_at, avg_dose_numeric, │
│ rank_in_department, rank_for_doctor } │
└────────────────────┬───────────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────────┐
│ Layer 1 — Suggestion Engine (vision service mixin / Supabase RPC) │
│ │
│ Input: │
│ • current_doctor_id │
│ • current_department_id │
│ • patient diagnoses (ICD-10[]) │
│ • patient_context (allergies, active_meds, weight, renal, hepatic) │
│ │
│ Processing: │
│ 1. Query prescription_patterns for matching (dept, doctor, ICD) │
│ 2. Rank by: doctor's own history > department average > hospital │
│ 3. Filter out drugs the patient is allergic to │
│ 4. Merge with existing order_favorite_items for the doctor │
│ 5. Return top-N suggestions with provenance labels │
│ │
│ Output: RankedSuggestion[] │
│ { drug_name, dose, frequency, route, duration, │
│ source: 'your_history'|'department_pattern'|'hospital_pattern', │
│ rx_count, confidence, provenance_detail } │
└────────────────────┬───────────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────────┐
│ Layer 2 — LLM Clinical Enrichment (existing llmClient) │
│ │
│ Input: │
│ • top suggestions from Layer 1 │
│ • patient_context (full clinical snapshot) │
│ • active_medications (for DDI check) │
│ │
│ Processing (one LLM call for the batch): │
│ 1. Dose optimization — adjust for weight/renal/hepatic/age │
│ 2. DDI screening — check each suggestion against active_meds │
│ 3. If DDI detected → suggest alternatives from Layer 1 pool │
│ 4. Regimen completeness — flag missing prophylaxis, monitoring │
│ 5. Duration reasonableness — flag unusually short/long courses │
│ │
│ Output: EnrichedSuggestion[] │
│ { ...RankedSuggestion, │
│ adjustedDose?, adjustedFrequency?, │
│ ddiWarnings[], alternatives[], │
│ missingItems[], durationNote?, │
│ llmReasoning } │
└────────────────────┬───────────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────────┐
│ Layer 3 — Frontend Surface │
│ │
│ MedicationAiPlanner component mounted in the prescription dialog: │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 🧠 AI Suggestions for J18.9 (Pneumonia) │ │
│ │ │ │
│ │ YOUR HISTORY (Dr. Somchai) │ │
│ │ ┌──────────────────────────────────────────────────────────┐ │ │
│ │ │ Amoxicillin/Clav 1g BID × 7d (PO) — 23 times + │ │ │
│ │ │ Azithromycin 500mg OD × 3d (PO) — 8 times + │ │ │
│ │ └──────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ DEPARTMENT PATTERN (Internal Medicine) │ │
│ │ ┌──────────────────────────────────────────────────────────┐ │ │
│ │ │ Levofloxacin 750mg OD × 5d (PO) — 67 dept rxs + │ │ │
│ │ │ ⚠ DDI: Warfarin (active) — ↑ INR risk │ │ │
│ │ │ → Alt: Moxifloxacin 400mg OD (no warfarin DDI) + │ │ │
│ │ └──────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ AI ADJUSTMENTS │ │
│ │ ┌──────────────────────────────────────────────────────────┐ │ │
│ │ │ 💊 Dose: Amox/Clav adjusted 875mg → 500mg (eGFR 35) │ │ │
│ │ │ ⚕ Missing: consider PPI prophylaxis with fluoroquinolone│ │ │
│ │ └──────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ [+ Add to prescription] [Dismiss] │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────┘
Data Model
Supabase: prescription_patterns (materialized view)
CREATE MATERIALIZED VIEW prescription_patterns AS
SELECT
md5(concat_ws('|',
mr.ward_id,
mr.prescriber_doctor_id,
COALESCE(ejc.icd10_primary, ''),
mr.medication_name,
mr.dose,
mr.dose_uom,
mr.frequency,
mr.route
)) AS pattern_id,
-- Dimensions
mr.ward_id AS department_id,
mr.prescriber_doctor_id AS doctor_id,
mr.prescriber_name AS doctor_name,
COALESCE(ejc.icd10_primary, 'unspecified') AS icd10_code,
mr.encounter_class,
-- Drug details
mr.medication_name AS drug_name,
mr.dose,
mr.dose_uom,
mr.frequency,
mr.route,
-- Aggregates
COUNT(*) AS rx_count,
MAX(mr.created_at) AS last_prescribed_at,
AVG(NULLIF(regexp_replace(mr.dose, '[^0-9.]', '', 'g'), '')::numeric)
AS avg_dose_numeric,
-- Ranking
ROW_NUMBER() OVER (
PARTITION BY mr.ward_id, COALESCE(ejc.icd10_primary, 'unspecified')
ORDER BY COUNT(*) DESC
) AS rank_in_department,
ROW_NUMBER() OVER (
PARTITION BY mr.prescriber_doctor_id, COALESCE(ejc.icd10_primary, 'unspecified')
ORDER BY COUNT(*) DESC
) AS rank_for_doctor
FROM medication_requests mr
LEFT JOIN encounter_journey_cache ejc
ON ejc.encounter_ref = mr.encounter_id
WHERE mr.status NOT IN ('cancelled', 'entered-in-error')
AND mr.created_at > NOW() - INTERVAL '12 months'
GROUP BY
mr.ward_id, mr.prescriber_doctor_id, mr.prescriber_name,
COALESCE(ejc.icd10_primary, 'unspecified'),
mr.encounter_class,
mr.medication_name, mr.dose, mr.dose_uom, mr.frequency, mr.route;
CREATE UNIQUE INDEX ON prescription_patterns (pattern_id);
CREATE INDEX ON prescription_patterns (doctor_id, icd10_code);
CREATE INDEX ON prescription_patterns (department_id, icd10_code);
Refresh schedule: SELECT cron.schedule('refresh-rx-patterns', '0 3 * * *', 'REFRESH MATERIALIZED VIEW CONCURRENTLY prescription_patterns');
Supabase: medication_ai_suggestions_log (audit)
CREATE TABLE medication_ai_suggestions_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
encounter_id TEXT,
patient_id TEXT,
doctor_id TEXT,
department_id TEXT,
icd10_codes JSONB DEFAULT '[]',
patient_context JSONB DEFAULT '{}',
-- What the AI suggested
suggestions JSONB NOT NULL, -- EnrichedSuggestion[]
llm_reasoning TEXT,
llm_model TEXT,
llm_latency_ms INT,
-- What the doctor actually did
accepted_items JSONB DEFAULT '[]', -- which suggestions were added
rejected_items JSONB DEFAULT '[]', -- explicitly dismissed
modified_items JSONB DEFAULT '[]', -- accepted but changed dose/freq
tenant_id UUID,
created_at TIMESTAMPTZ DEFAULT NOW(),
resolved_at TIMESTAMPTZ
);
This table is the learning feedback loop — by tracking which suggestions
doctors accept vs reject, future iterations can weight the ranking:
confidence = rx_count * (1 + accept_rate) * recency_decay
Supabase RPC: get_prescription_suggestions
CREATE OR REPLACE FUNCTION get_prescription_suggestions(
p_doctor_id TEXT,
p_department_id TEXT,
p_icd10_codes TEXT[],
p_encounter_class TEXT DEFAULT 'AMB',
p_limit INT DEFAULT 20
)
RETURNS TABLE (
drug_name TEXT,
dose TEXT,
dose_uom TEXT,
frequency TEXT,
route TEXT,
source TEXT, -- 'your_history', 'department_pattern', 'hospital_pattern'
rx_count BIGINT,
last_prescribed_at TIMESTAMPTZ,
avg_dose_numeric NUMERIC,
rank_score INT
) LANGUAGE plpgsql STABLE AS $$
BEGIN
RETURN QUERY
WITH patterns AS (
SELECT
pp.*,
CASE
WHEN pp.doctor_id = p_doctor_id THEN 'your_history'
WHEN pp.department_id = p_department_id THEN 'department_pattern'
ELSE 'hospital_pattern'
END AS source_label,
CASE
WHEN pp.doctor_id = p_doctor_id THEN 3
WHEN pp.department_id = p_department_id THEN 2
ELSE 1
END AS source_weight
FROM prescription_patterns pp
WHERE pp.icd10_code = ANY(p_icd10_codes)
AND (pp.encounter_class = p_encounter_class OR pp.encounter_class IS NULL)
AND (pp.doctor_id = p_doctor_id
OR pp.department_id = p_department_id
OR TRUE) -- hospital-wide fallback
)
SELECT
p.drug_name,
p.dose,
p.dose_uom,
p.frequency,
p.route,
p.source_label AS source,
p.rx_count,
p.last_prescribed_at,
p.avg_dose_numeric,
(p.source_weight * 1000 + p.rx_count::int) AS rank_score
FROM patterns p
ORDER BY rank_score DESC, p.last_prescribed_at DESC
LIMIT p_limit;
END;
$$;
Implementation Plan
Phase 1 — Pattern Mining Infrastructure
| File | Purpose |
|---|---|
infrastructure/medbase/migrations/20260524d_prescription_patterns.sql |
Materialized view + RPC + audit table + cron refresh |
Phase 2 — Backend Suggestion Engine
| File | Purpose |
|---|---|
services/vision/modules/medicationPlanner/medicationPlanner.controller.mixin.ts |
vision.medicationPlanner.suggest — fetches patterns via RPC, applies allergy filter, calls LLM for enrichment |
services/vision/modules/medicationPlanner/llmPlannerPrompt.ts |
Prompt template for dose optimization + DDI resolution + completeness check |
Phase 3 — Frontend Component
| File | Purpose |
|---|---|
web/packages/miniapps/e-mar/components/MedicationAiPlanner.tsx |
Suggestion panel for the prescription dialog — 3-tier display (your history / dept pattern / AI adjustments), “+ Add” per suggestion |
web/src/services/vision-ai.service.ts |
getMedicationSuggestions() client function |
Phase 4 — Feedback Loop
| File | Purpose |
|---|---|
| Accept/reject tracking in the audit log | When doctor adds a suggested item → log accepted_items; when dismissed → log rejected_items |
| Recency + acceptance weighting in the RPC | confidence = rx_count * (1 + historical_accept_rate) * exp(-days_since_last/90) |
LLM Prompt Template
You are a clinical pharmacist AI. A doctor is prescribing medications for a
patient. Based on the hospital's prescribing patterns and the patient's
clinical context, enrich the suggestions below.
## Prescribing Suggestions (from hospital pattern data)
{{suggestions | json}}
## Patient Context
Age: {{patient.age}} Sex: {{patient.sex}} Weight: {{patient.weight_kg}}kg
eGFR: {{patient.renal.eGFR}} Creatinine: {{patient.renal.creatinine}}
ALT: {{patient.hepatic.alt}} Bilirubin: {{patient.hepatic.bilirubin}}
Allergies: {{patient.allergies | join(', ')}}
Active medications: {{patient.active_meds | join(', ')}}
Diagnoses: {{patient.diagnoses | join(', ')}}
## For each suggestion, return:
1. adjustedDose — if the dose should change for this patient (renal/hepatic/weight), provide the adjusted value + reason
2. ddiWarnings — if this drug interacts with any active_med, describe the interaction + severity
3. alternatives — if a DDI is moderate/severe, suggest 1-2 alternatives from the suggestion pool
4. missingItems — prophylaxis or monitoring the doctor might want to add (e.g. PPI with NSAID, INR monitoring with warfarin)
5. durationNote — if the typical duration seems too short/long for this diagnosis
Return JSON array matching the input suggestions order:
[
{
"drugName": "...",
"adjustedDose": { "value": "500mg", "reason": "eGFR 35 — reduce from 875mg" } | null,
"ddiWarnings": [{ "interactsWith": "Warfarin", "severity": "major", "detail": "..." }],
"alternatives": [{ "drugName": "...", "dose": "...", "reason": "..." }],
"missingItems": ["Consider PPI prophylaxis"],
"durationNote": "7 days is standard for uncomplicated CAP" | null
}
]
Ranking Algorithm
score(suggestion) =
source_weight # 3=your_history, 2=department, 1=hospital
× rx_count # raw prescribing frequency
× (1 + historical_accept_rate) # from audit log (0.0–1.0, default 0.5)
× recency_decay # exp(-(days_since_last_rx) / 90)
× allergy_filter # 0 if patient allergic, else 1
× ddi_penalty # 0.3 if major DDI, 0.7 if moderate, 1.0 if none
Suggestions with score < threshold are excluded. Threshold is configurable
per department at /admin/medication-planner-config.
Feature Flags
| Flag | Layer | Default | Purpose |
|---|---|---|---|
VISION_PLANNER_ENABLED |
Backend | false |
Kill switch |
medos_premium_medication_planner |
Frontend (localStorage) | not set | Feature gate |
Integration Points
- Prescription dialog —
MedicationAiPlannermounts below the drug search field. When a diagnosis is selected or already present on the encounter, it auto-fetches suggestions. - Order favorites — existing
order_favorite_item/order_favorite_order_setare included as “Your Favorites” tier (rank above department patterns). - CDS alerts — when the LLM flags a critical DDI, it emits
manifest.clinical.alertvia the existing CDS alert surface. - Audit — every suggestion session is logged. Acceptance/rejection feedback improves future rankings.