Ruby for Reception — Cockpit Design
Date: 2026-05-23 Status: Spec Author: ssh Reference tenant: Dental Square (dfffc93f-82eb-47fd-a276-b79c24dccb80)
1. Problem
Dental Square reception handles 20–40 appointments/day (ramping toward 100+) on one receptionist with every automation switch off. Diagnostic snapshot (last 90 days):| Signal | Value | What it means |
|---|---|---|
| Appointments | 101 | Volume real, ramping |
missed status | 40 (39.6%) | Massive no-show drag |
completed status | 0 | Reception never closes visit loop |
| Doctor assigned | 1 of 101 | Slots booked without doctor binding |
| Room assigned | 0 of 101 | Operatory tracking unused |
doctor_schedules rows | 0 | No availability windows defined |
| WhatsApp automations enabled | 0 | No auto-confirm, no reminders, no recall |
patient_recalls rows | 0 | No recall pipeline |
saved_replies rows | 0 | No message templating |
bulk_messages rows | 0 | No batch outreach |
| Operating hours | 13:00–21:30 Mon–Sat | Evening clinic, late peak |
whatsapp_automations, patient_recalls, doctor_schedules, saved_replies, clinic_modules.ai_insights = true). The gap is leverage: the receptionist has no surface that pulls the data into a single action queue, and there’s no Ruby narration layer making it readable on a busy floor.
WhatsApp send is not yet integrated for Dental Square — surfaces that depend on WA send are Phase 2.
2. Architecture
Hard rule: deterministic SQL decides who/what/when. Ruby only writes the words.- Deterministic SQL is fast, cheap, free, and fully predictable — perfect for “who needs follow-up” decisions.
- Ruby is good at narration, tone, classification, summarization — bad at deciding who to message.
- Ruby never reads PHI without a deterministic gate (consistent with existing
decryptPatientPHIpattern inappointment-nudges.ts).
3. Model & Cost
Lock:deepseek-v4-flash (replaces current deepseek-chat / V3.2 default).
Why:
- 1M context (was 128K) — full patient history + clinic config fits with room
- 0.0028/M cache hit** (50× cheaper)
- 0.40)
- Native JSON mode + tool calls
response_format: { type: 'json_object' }supported as today
server/src/lib/ai/client.ts line 64 from 'deepseek-chat' to 'deepseek-v4-flash'. DEEPSEEK_MODEL env var continues to override per-environment.
DICOM is untouched. server/src/lib/ai/agents/dicom-analysis.ts uses gpt-5.4-mini via OpenAI and is out of scope.
Cost guardrail (40-appt/day clinic, all 4 Phase-1 surfaces):
- Morning brief: 1 call/day, ~2K input + ~500 output = $0.0004
- Prep hints: 40 calls/day, mostly cache hits on system prompt (~3K stable + ~200 variable input) + ~80 output ≈ $0.001 total
- Patient snapshot: ~20 lookups/day, ~1K input + ~150 output = $0.003
- EOD reconciliation: 1 call/day, ~2.5K input + ~400 output = $0.0005
- Total: < $0.01/day per clinic at steady state. Pennies.
- System prompt stays byte-identical across calls of the same surface (managed by Langfuse, fetched once per worker isolate via 300s TTL cache)
- User message starts with a stable schema-reminder header, then the per-call JSON payload
- Track
prompt_cache_hit_tokens/prompt_cache_miss_tokensin Langfuse metadata for cost monitoring
4. Prompt Management — Langfuse
No hardcoded prompts. Every Ruby surface fetches its system prompt from Langfuse viagetPrompt(name, undefined, { cacheTtlSeconds: 300 }). Fallback strings in prompts.ts stay as last-resort failsafe only (existing behavior).
| Surface | Langfuse name | Prompt key (PROMPT_NAMES) | New? |
|---|---|---|---|
| Morning Brief | reception-morning-brief | receptionMorningBrief | ✅ NEW |
| Today’s Queue prep hint | reception-prep-hint | receptionPrepHint | ✅ NEW |
| Patient Snapshot | reception-patient-snapshot | receptionPatientSnapshot | ✅ NEW |
| EOD Reconciliation | reception-eod-reconcile | receptionEodReconcile | ✅ NEW |
| Follow-Up Drafter (Phase 2) | reception-followup-drafter | receptionFollowupDrafter | ⏳ deferred |
| Triage Classifier (Phase 2) | reception-triage-classifier | receptionTriageClassifier | ⏳ deferred |
appointment-nudges— reused by Today’s Queue badge logicpayment-reminder-msg— used when reception clicks “draft balance reminder”patient-recall-msg— used by EOD Reconciliation when a recall is overduedaily-clinic-brief— owner-facing daily brief; distinct from reception morning brief
- Add new prompts to
server/src/lib/ai/prompts.ts(PROMPTS+PROMPT_NAMES) - Run
npx tsx server/src/scripts/sync-prompts-to-langfuse.ts(existing script) - Verify with
--dry-runfirst, then push for real - Spot-check in Langfuse dashboard: each prompt should be on label
production, version bumped
- Three-tier structure: System role + JSON schema + Rules (matches existing house style)
- The word “json” must appear in the system prompt (DeepSeek JSON-mode requirement)
- Concrete JSON example baked in
- Ruby brand, never “AI” or “Action Items”
- PKR with commas
- No emojis in patient-facing output
- Empty array / empty string preferred over fabrication
- Stable text — never embed per-call data in the prompt template; that comes via user message
5. Phase 1 surfaces (ship now)
5.1 Morning Brief
Surface: Top of Reception dashboard, replaces the generic “Good morning” greeting card. When: Once per day per clinic, on first dashboard load after 05:00 PKT. Cached intenant_daily_briefs table for the rest of the day.
Deterministic input shape (user message JSON):
5.2 Today’s Queue prep hints
Surface: Inline badge under each appointment row in today’s list. Hover/tap reveals 1-sentence prep hint. When: On dashboard load. Batch all today’s appts in a single Ruby call (cache-hit friendly — same system prompt, just a different per-call user payload). Deterministic input (user message JSON):tone: "ok" | "info" | "warn" | "urgent" (drives badge color).
5.3 Patient Snapshot
Surface: Side drawer on patient row click. 4 lines, 15-second read. Reuses existingpatient-brief-summary prompt at first — but with a reception-specific user payload that emphasizes today’s relevance (balance owed, what’s due today, allergies that matter for today’s procedure type). No new prompt needed if existing one works; if reception variant needed, branch as reception-patient-snapshot.
Decision: Start with existing patient-brief-summary. Re-evaluate after 2 weeks; only fork to a new prompt if reception explicitly asks for a different framing.
→ Phase 1 only authors 3 new prompts (reception-morning-brief, reception-prep-hint, reception-eod-reconcile). Snapshot uses existing.
5.4 EOD Reconciliation
Surface: Modal triggered at clinic close time (configurable; default 30 min beforeoperating_hours.end) or via “Wrap up day” button.
When: Once per day. Stored in tenant_daily_briefs table or a new reception_eod_logs row (TBD in plan).
Deterministic input:
6. Phase 2 surfaces (when WA send lands)
- Follow-Up Drafter (
reception-followup-drafter): given a list of patients flagged by deterministic SQL (missed today, unconfirmed tomorrow, recall due, outstanding balance), Ruby drafts per-patient WA message with reschedule slot suggestions. Deterministic layer provides the slots. - Triage Inbox (
reception-triage-classifier): inbound WA messages classified (booking-request / question / complaint / no-action) with suggested 1-click replies. Confidence threshold below 0.7 → human review. - D from prior brainstorm = subset of Follow-Up Drafter (the “missed today” cohort).
PROMPT_NAMES now.
7. Data model changes
None required for Phase 1. All needed surfaces query existing tables:appointments(clinic_id + appointment_date indexed)invoices(balance + status)patients(last_visit derived)tenant_daily_briefs(already exists for daily brief caching)clinic_operating_hours/clinics.operating_hours
reception_eod_logs if we want to store the EOD reconciliation history. Not blocking.
8. API surface (Phase 1)
New Hono routes underserver/src/routes/reception.ts (new file):
| Method | Path | Purpose |
|---|---|---|
GET | /reception/morning-brief | Returns cached or freshly-generated brief for today |
POST | /reception/morning-brief/refresh | Force regenerate (rate-limited 1/hour per clinic) |
POST | /reception/prep-hints | Body: { appointmentIds: string[] } — returns hint per id |
GET | /reception/eod | Returns EOD reconciliation payload |
POST | /reception/eod/close-confirmed | Batch mark confirmed→completed |
- Require
receptionist,admin, ordoctorrole - Clinic scope enforced via
getClinicIdFromContext - PHI decrypted server-side, never sent raw to Ruby (deterministic aggregator already handles this in
appointment-nudges.ts— copy the pattern)
9. UI surface (Phase 1)
Extend, don’t replaceui/src/components/receptionist/ReceptionistOverview.tsx:
- MorningBriefCard — new component at top, above existing KPI cards. Collapsible. Shows greeting + headline + priority list with deep links.
- Today’s appointments table — add a new
Prepcolumn rendering the badge + hover tooltip with the hint. Existing rows untouched. - EOD Reconciliation button — new “Wrap up day” CTA in the header. Opens a modal with the Ruby summary + batch action buttons.
- Patient drawer — keep using existing patient brief endpoint (no change in Phase 1).
MetricSkeleton and a Ruby-specific shimmer.
10. Eval & observability
Langfuse:- All Ruby calls already traced via
client.ts— extend metadata to includesurface(“morning-brief” | “prep-hint” | “eod”) andcacheHitRatio(computed fromprompt_cache_hit_tokens) - Per-surface dashboards in Langfuse for cost, latency, p99 token count
- Eval dataset: collect 50 Dental Square brief renders over 1 week, human-rate on a 1-5 scale, set up Langfuse experiments to compare prompt versions
- DeepSeek returns empty content (documented failure) → retry once; if still empty, show deterministic-only fallback (“38 appointments today, 26 unconfirmed” — no narration)
- Langfuse
getPromptthrows → fall through to hardcoded prompt inprompts.ts(existing pattern) - Ruby JSON parse error → log to
worker_logs, render deterministic fallback
worker_logs row tagged ruby_fallback so we know when Ruby is sad.
11. Migration plan
- Push 3 new prompts to Langfuse (
reception-morning-brief,reception-prep-hint,reception-eod-reconcile) - Switch model default in
client.tstodeepseek-v4-flash - Re-push existing prompts to ensure they’re current on the new model context (no edits, just version bump):
daily-clinic-brief,patient-brief-summary,appointment-nudges,payment-reminder-msg,patient-recall-msg,churn-reengagement-msg,treatment-followup-msg,revenue-forecast-insight,monthly-performance-summary,clinical-notes-soap,note-rewrite,note-grammar-check,note-shorten,note-expand,treatment-plan-presentation,superadmin-tenant-daily-brief. Skip DICOM — uses GPT, not DeepSeek. - Smoke test on Dental Square: trigger morning brief, render today’s queue, run EOD reconciliation. Confirm Langfuse traces show
prompt_cache_hit_tokens > 0after second call. - Build endpoints + UI per Section 8–9.
- Ship behind feature flag
reception_cockpit_v1onclinic_modules. Roll out to Dental Square first; observe 1 week; then enable for other reception-using clinics.
12. Scope this spec covers
- 3 new Langfuse prompts (authored + pushed)
- 16 existing prompts re-pushed (untouched content, version bump)
- Model default flip to
deepseek-v4-flash - Spec for Phase 1 surfaces (Morning Brief, Prep Hints, EOD Reconciliation; Patient Snapshot reuses existing)
13. Scope this spec does NOT cover
- DICOM prompt or model (GPT-based, untouched)
- WhatsApp send integration
- Follow-Up Drafter and Triage Inbox prompts (Phase 2)
- Reception cockpit UI build-out (separate plan via writing-plans)
- API route implementation (separate plan via writing-plans)
- Smart booking (C from prior brainstorm — explicitly out of scope per user)
14. Risks
| Risk | Likelihood | Mitigation |
|---|---|---|
deepseek-v4-flash not yet on our DeepSeek account | Medium | Smoke test before merging the default swap; keep deepseek-chat env override available |
| Cache hit rate stays low (system prompts drift) | Low | Lint check that Langfuse prompts don’t contain dates/IDs |
| Ruby fabricates patient names | Low | Deterministic layer is the single source of truth; Ruby reads decrypted names only after aggregator decides who to mention |
| Receptionist trusts Ruby blindly | Medium | Every Ruby card has the Ruby logo + a “deterministic count: N” footnote so the source is visible |
| Cost overruns | Very low | Cost ceiling is pennies/day; Langfuse dashboards alert at $1/day per clinic |

