Navigation Next-Best-Action
Navigation recommender: telemetry table, n-gram pattern MV, abstain-first suggestion RPC, feedback log, fail-open frontend hook.
Date: 2026-05-29 Author: Claude (design session) Scope: Behavior-driven “next step / next tab” nudges in patient-profile and unified-action surfaces, with a future form-autofill tier Status: Design only — not built. Extends the existing AI recommendation engine; adds one genuinely new substrate (UI-interaction capture).
1. The Idea (and the key reframe)
While a clinician works an encounter, watch how they actually move through the UI — open a tab → scroll → finish → move on — and at the natural seams offer a quiet nudge: “you usually open Nursing Assessment next.” Later, extend the same engine to pre-fill form fields from RAG’d patterns. Every suggestion is recommend-only, human-confirms, dismissible.
This is not a new architecture. The repo already has a 5-domain AI recommendation engine (docs/architecture/ai-recommendation-engine-validation.md, reference impl services/vision/.../medicationPlanner). It already gives you: the 3-tier ranking signal (your history > department pattern > hospital pattern), the MV → RPC → Ollama-enrich → audit-log template, on-prem Ollama, and — crucially — Domain 5 (“Department Quick-Picks”) which is pure SQL frequency, no LLM. Navigation is just Domain 6, built from that same template.
What’s genuinely new: the existing engine mines clinical content (orders, diagnoses, prescriptions). Navigation mines UI behavior (tab opens, scroll, dwell, completion). That stream doesn’t exist yet — capturing it is the one new substrate this doc adds.
2. Reuse vs. New
| Concern | Source | Reuse or New |
|---|---|---|
| 3-tier ranking (you > dept > hospital) | get_prescription_suggestions RPC pattern |
Reuse (re-parameterize for nav) |
| MV → RPC → LLM-enrich → audit-log skeleton | medicationPlanner template |
Reuse |
| No-LLM pure-frequency tier | Domain 5 “Dept Quick-Picks” | Reuse (this IS the safe Tier-1 nudge) |
On-prem Ollama (mistral:7b) |
live on PH demo | Reuse |
| Config-as-data thresholds | policy_gates / cds_rules / detection_rules JSON-predicate shape |
Reuse |
| Coarse behavioral events | RUDS user_action_events (server-side audit) |
Partial — too coarse for nav |
| Fine-grained UI capture (tab/scroll/dwell/dismiss) | — | NEW (ui_interaction_events) |
| Encounter-aware context key + backoff | — | NEW (this doc, §6) |
| Abstain-first gate ladder + prompt | — | NEW (this doc, §8–9) |
| Form-autofill tier | unified-clinical-assistant.md FORM-FILL class + forms_registry |
Reuse (Tier 2, §12) |
3. The Two Hard Requirements (and how they reconcile)
R1 — Each encounter type yields different navigation. A nurse in ER trauma navigates nothing like the same nurse in an OPD follow-up, an IPD admission, a delivery-room case, or an NM appointment. So encounter_class/subtype is a first-class context dimension, and prediction is fundamentally a sequence problem (“given the last k things you did in this kind of encounter, what’s next”).
R2 — Don’t be annoying when there isn’t enough data. Silence is the default. A nudge must clear an explicit, configurable evidence bar before it shows.
These pull against each other: the more you specialize the key for R1, the fewer samples per exact key → everything goes silent (curse of dimensionality). The reconciliation is n-gram-style backoff (§6): try the specific key, fall back to coarser keys until one clears the support threshold; if even the coarsest fails, stay silent. Collect wide, mine narrow with backoff.
4. Data Capture — “Collect All Data”
4.1 Why a new table (not just user_action_events)
user_action_events is server-side audit grade — API calls, logins, resource mutations. UI micro-events (tab focus, scroll depth, dwell, dismiss) are client-side, far higher volume, and a different lifecycle. Folding them into the security audit stream would drown RUDS in noise.
Decision: a sibling hypertable ui_interaction_events that follows the same envelope conventions, with aggressive retention/downsampling, feeding the miner. Session-summary facts (e.g. “completed encounter workspace in 4 tabs, 6m”) may optionally roll up into user_action_events for the RUDS/security view. This honors “collect all data” without polluting the audit silo.
4.2 Schema (mirrors user_action_events conventions)
CREATE TABLE IF NOT EXISTS ui_interaction_events (
event_id UUID NOT NULL DEFAULT gen_random_uuid(),
occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
user_id UUID,
session_id UUID,
role TEXT, -- denormalized for fast mining
-- ── context dimensions (the R1 axes), denormalized at capture time ──
tenant_id UUID,
department_id TEXT,
dept_type TEXT, -- opd / ipd / er / or / lab / pharmacy / delivery / nm ...
encounter_id TEXT,
encounter_class TEXT, -- OPD / IMP / EMER / SS ...
encounter_subtype TEXT, -- trauma / anc / dialysis / delivery / ...
journey_state TEXT, -- workflow node from encounter_journey_cache
acuity TEXT, -- esi_level / priority bucket
insurance_scheme TEXT, -- nhso / sso / philhealth / kaigo / cash (optional)
shift TEXT, -- day / evening / night (off-hours navigate differently)
-- ── the interaction itself ──
surface TEXT NOT NULL, -- 'patient-profile' | 'medical-worklist' | 'order-system' ...
element TEXT, -- tab/module id, e.g. 'modules.IpdNursingAssessment'
interaction TEXT NOT NULL, -- tab_open | tab_finish | scroll_depth | dwell | form_open
-- | form_save | action_invoke | nudge_show | nudge_accept | nudge_dismiss
value NUMERIC, -- scroll %, dwell ms, etc.
prev_element TEXT, -- for sequence/n-gram mining
metadata JSONB DEFAULT '{}'::jsonb,
PRIMARY KEY (event_id, occurred_at)
) PARTITION BY RANGE (occurred_at);
PHI stance: capture identifiers and structure, never clinical free-text or values.
elementis a module id, not a note body.encounter_subtype/acuityare coarse buckets. No patient name, no lab value, no narrative ever enters this table — which also keeps it safe to feed a prompt later (§9).
4.3 The emitter (frontend)
A thin useInteractionTelemetry() hook + a small dispatcher (batched, navigator.sendBeacon, debounced scroll/dwell). Context dimensions resolve from the already-loaded encounter_journey_cache row + auth/role context — no extra fetches. Mount points: DynamicContentRenderer (tab open/finish/dwell), MainPatientTabs/WorkflowBasedTabs (tab switches), form components (open/save), action handlers (invoke). Capture is fire-and-forget and must never block or error the UI (fail-open).
5. Event Vocabulary (the “all data” we collect)
interaction |
Fires when | Used for |
|---|---|---|
tab_open / tab_finish |
module/tab gains focus / is left after meaningful dwell | sequence mining (the core signal) |
scroll_depth |
debounced scroll milestone (25/50/75/100%) | “did they actually read it” / completion proxy |
dwell |
time spent on a surface before leaving | weak weight; distinguishes glance vs work |
form_open / form_save |
a form is opened / persisted | completion-boundary trigger (Gate 0) |
action_invoke |
a workflow action / button is run | cross-links nav patterns to clinical actions |
nudge_show / nudge_accept / nudge_dismiss |
the recommender surfaced / was taken / rejected | the feedback loop (Gate 3 + RLHF) |
nudge_dismiss is the most valuable row we collect — it’s how the engine learns to shut up.
6. The Context Key + Backoff Hierarchy (the spine)
The prediction target: P(next_element | context, recent_sequence). Context is the high-dimensional tuple from §4.2; recent_sequence is the last k elements (the n-gram prefix).
Because the full key is sparse, the runtime walks a backoff ladder, most-specific → coarsest, and uses the first level that clears support ≥ S_min:
L0 (role, dept_type, encounter_class, encounter_subtype, journey_state, ngram k=2) ← most specific
L1 (role, dept_type, encounter_class, journey_state, ngram k=2)
L2 (role, dept_type, encounter_class, journey_state, ngram k=1)
L3 (role, encounter_class, journey_state)
L4 (role, journey_state) ← coarsest (hospital-wide)
↓
if even L4 has support < S_min → STAY SILENT
This is exactly Stupid-Backoff for navigation:
- R1 satisfied — a well-trafficked encounter type (ER trauma at a busy hospital) resolves at L0/L1, giving encounter-specific predictions.
- R2 satisfied — a rare encounter type, a new deployment, or a new role finds nothing at L0–L4 and produces no candidates → no nudge. Cold-start handles itself; we never guess from thin air.
Each level blends the 3-tier signal (reused from the existing engine): personal history (this user) over the level’s cohort, with a recency-weighted personalization weight λ that decays toward the cohort when the user has few samples at that level.
7. The Pattern Miner (reuse the template)
Follow the prescription_patterns MV → get_prescription_suggestions RPC → audit-log shape, re-parameterized:
navigation_patternsMV — aggregatesui_interaction_eventsinto(context_key_level, ngram_prefix, next_element)rows with counts, refreshed by the existing two-tier RUDS cadence (nightly batch + lighter inline rollup). The batch job is where “not enough data” is decided: rows belowS_minare simply not materialized, so the inline path has nothing to show.get_navigation_suggestions(user, context, recent_seq)RPC — walks the backoff ladder, returns ranked candidates with their scores. Pure SQL. This alone powers the Tier-1 nudge with zero LLM.- Scoring per candidate (association-rule math, computed in the MV):
- support(context) — gate input.
- confidence = P(next | context) — need top
≥ C_min. - margin = top − 2nd — ambiguous (many-way tie) ⇒ suppress.
- lift = P(next | context) / P(next) —
≥ lift_minkills “globally obvious” picks that aren’t actually contextual.
navigation_ai_suggestions_log— every show/accept/dismiss (unify under the existingvision_ai_suggestions_log+suggestion_type='navigation'if Open Decision #2 in the validation doc lands that way).
8. The Gate Ladder (cheap → expensive)
The prompt is the second line of defense. The first is not calling the LLM until a deterministic gate passes.
| Gate | Where | Rule | Why |
|---|---|---|---|
| 0 — boundary | client | only evaluate on tab_finish / form_save / dwell-past-threshold; never mid-scroll |
half of “annoying” is timing |
| 1 — sufficiency | RPC (SQL, no tokens) | backoff finds a level with support ≥ S_min, top confidence ≥ C_min, margin ≥ M_min, lift ≥ lift_min |
the cold-start killer; LLM never runs when sparse |
| 2 — abstain-first prompt | Ollama (only if Gate 1 passed) | model re-ranks/phrases pre-scored candidates; may return abstain:true; output confidence < floor ⇒ suppress |
model can’t invent; silence is modeled as correct |
| 3 — behavioral memory | RPC + client | per-session frequency cap; dismissed (context, element) ⇒ cooldown/penalty; 2 dismissals ⇒ stop |
learns to shut up |
For Tier-1, Gate 2 is optional — pure SQL (Gates 0/1/3) already gives a safe, useful nudge. The LLM is added only for tie-breaking + natural-language rationale.
9. The Prompt Contract (Tier-1 re-rank)
The model is never the pattern miner — it receives pre-scored candidates and either picks one or abstains.
- System framing: “You are a workflow-completion assistant. You will receive the clinician’s role, the current encounter context, the recent action sequence, and a ranked list of candidate next actions WITH their historical support/confidence/lift. Recommend exactly ONE, or set
abstain:true. Prefer abstaining when no candidate clearly beats the others or evidence is weak. Never propose an action not in the candidate list.” - Few-shot: include at least one ABSTAIN exemplar so silence is learned, not penalized.
- Structured output:
{ recommend: bool, element_id: string|null, confidence: 0..1, rationale: string }— trivially gateable;confidence < floor⇒ suppress. - No PHI: the payload is the §4.2 structured context + candidate ids/counts. No patient identifiers, no clinical free-text, no values — which is why §4.2 forbids them at capture time.
10. Config-as-Data (thresholds are rule rows, not constants)
S_min, C_min, M_min, lift_min, LLM confidence_floor, per-session cap, dismissal cooldown, personalization λ, and the backoff ladder itself live as editable rows in the same JSON-predicate shape as policy_gates / cds_rules / detection_rules — scoped by tenant / role / dept_type / encounter_class. A chatty clinic can be dialed conservative without a code change; a new market tunes per region. Default seed = conservative (nudges rarely).
11. Surfacing UX
A small, non-modal NextStepNudge (chip/popover near the active tab rail, or a slot in the existing WidgetRail), shown only when all gates pass. Affordances: Accept (dispatches the existing actionModalResolver/openModal — same path real tabs use, zero new action plumbing), Dismiss (emits nudge_dismiss), auto-hide on next interaction. Never steals focus, never blocks. Behind a per-tenant feature flag + role gate.
12. Tier 2 — Form Autofill (future)
The same engine, one rung harder. This is the FORM-FILL tool class from unified-clinical-assistant.md, whose linchpin is forms_registry (web/src/services/forms/formRegistry.service.ts + schema_json): one populated FormFieldDefinition[] per form feeds both the autofill suggester and a human “what can I prefill” palette. RAG over the patient’s prior structured forms + cohort patterns proposes field values; the clinician confirms every field. Gated behind the same recommender-only + human-confirm contract, plus the stricter PHI posture (structured-only payload, regional endpoint, per-inference audit) the e-Kardex/RUDS docs already establish. Depends on the agent runner (runAgentLoop) from unified-clinical-assistant.md, which is still a design, not code — so Tier 2 is explicitly after the runner ships.
13. Safety Invariants
- AI recommends, never acts. A nudge proposes; the human invokes via the existing action path.
- AI never gates. It cannot block, hide, or reorder real workflow controls.
- Abstain is first-class. Silence is the default and the correct answer under weak evidence.
- No PHI in the stream or the prompt — structured ids/counts only, enforced at capture (§4.2).
- Full audit — every show/accept/dismiss logged (
navigation_ai_suggestions_log). - Per-tenant flag + role gate + kill switch.
- Fail-open — capture and recommendation are best-effort; their failure never degrades the clinical UI.
- Cold-start = silence — empty candidates produce no nudge, by construction (§6/§7).
14. Rollout
| Phase | Deliverable | LLM? | Demoable |
|---|---|---|---|
| P0 | ui_interaction_events table + useInteractionTelemetry emitter wired at the §4.3 mount points; data starts accumulating |
no | data only |
| P1 | navigation_patterns MV + get_navigation_suggestions RPC (backoff) + NextStepNudge + Gates 0/1/3 + threshold rule rows |
no — pure SQL | ✅ safe, cheap, demoable |
| P2 | Ollama re-rank (Gate 2, abstain-first prompt) + navigation_ai_suggestions_log RLHF |
yes | ✅ |
| P3 | Personalization λ tuning + per-market threshold seeds + observability (nudge accept/dismiss rates per context) |
yes | ✅ |
| P4 | Tier-2 form autofill (after runAgentLoop ships) |
yes | future |
Start at P0→P1. P1 is the whole user-visible feature with zero LLM — frequency + backoff + gates. Add the model in P2 only for tie-breaking and phrasing.
15. Open Decisions
- Retention/downsampling for
ui_interaction_events— raw events are high-volume. Keep raw N days, then roll into the MV and drop? (recommend: 30d raw, MV is the durable artifact.) ngramdepthk— start k≤2; measure whether k=3 improves accuracy enough to justify the extra sparsity.- Unified vs per-domain audit log — fold into
vision_ai_suggestions_log(Open Decision #2 of the validation doc) vs standalone. - Backoff ladder per dept_type — should ER/IPD/OPD have different ladders (e.g. ER weights acuity higher)? (recommend: default ladder + per-dept_type override rows.)
- Surface placement —
WidgetRailslot vs inline chip on the tab rail. (recommend: prototype both in sandbox.)
16. Related Documents
docs/architecture/ai-recommendation-engine-validation.md— the 5-domain engine this extends (3-tier signal, MV→RPC→Ollama template, the “don’t design new architecture” rule)docs/architecture/medication-ai-planner.md— the reference implementation to template fromdocs/architecture/ai-training-corpus.md— the separate de-identified training/eval data dump fed from the §4 capture streams (medallion ML-corpus, versioned immutable snapshots, RAG / fine-tune / seq-model outputs)docs/architecture/rogue-user-detection-system.md—user_action_events, two-tier scorer, recommender-only AI stance this mirrorsdocs/architecture/unified-clinical-assistant.md— the FORM-FILL tool class +forms_registrylinchpin for Tier 2docs/architecture/encounter-orchestrator-triggers.md—encounter_journey_cache(source of the context dimensions)docs/architecture/widget-rail-surface-system.md— candidate surface for the nudge