Ruby Copilot — WhatsApp staff-assist redesign
Date: 2026-06-03 Status: Approved (design), pending implementation planProblem
Ruby currently auto-sends WhatsApp replies to patients (autopilot). This produced inconsistent behaviour: transient model/DB hiccups latched conversations off permanently, Meta re-deliveries caused doubles, and staff couldn’t predict or control what Ruby said. Even after reliability fixes, an autopilot that texts patients directly is hard to trust.Decision
Pivot Ruby from autopilot (auto-sends to patient) to copilot (suggests to staff, staff approve + send). A wrong/late/duplicate message becomes structurally impossible in the default mode because Ruby never sends — staff do.Goals
- Ruby produces a suggested reply visible to staff only, never to the patient.
- One click puts the suggestion into the composer, ready to edit/send. No auto-send.
- Suggestions are on-demand (staff click a button) — cost-conscious, no DeepSeek call per inbound.
- A conversation can be marked Ruby on/off.
- Authorized roles: reception, admin, doctor.
Non-goals
- No change to the Ruby knowledge assembler or the
askRubyWhatsAppagent contract. - No auto-suggest-on-inbound (explicitly on-demand only).
- No removal of the autopilot code path — it stays, gated behind opt-in.
Design
1. Clinic mode
Replace thewhatsapp_assistant module’s boolean enabled with:
off— Ruby disabled clinic-wide.copilot(default) — suggestions only; Ruby never sends.autopilot— legacy auto-send path may fire (opt-in).
enabled: true → copilot; enabled: false → off.
Stored in the same clinic_modules.config JSON (key mode); parseAssistantConfig
back-fills mode from the old enabled boolean for any unmigrated row (no DB
migration required — JSON config). This silences the current auto-sends immediately,
including on the ssh & Associates test tenant.
2. On-demand suggest endpoint
POST /whatsapp/conversations/:id/suggest
- Auth: staff with WhatsApp access (reception/admin/doctor); not patients.
- Loads the conversation transcript (last ~8 messages, same window the dispatcher
uses) + clinic knowledge block, calls
askRubyWhatsApp, returns{ reply, intent }. Never callssendTextMessage. - Reuses the logic already proven in
POST /assistant/preview(which “calls Ruby agent, never sends”), generalised to take a conversation id and be reachable by the three staff roles rather than admin-only. - Returns 409/empty gracefully if
mode === 'off'.
3. Suggestion UI
- Trigger: a Ruby “Suggest reply” button in the composer toolbar (visible to
reception/admin/doctor when clinic
mode !== 'off'). - Render: on success, a staff-only card directly above the composer input, clearly labelled “Ruby suggestion — only your team can see this” with the Ruby branding. Buttons: Use and Dismiss. Loading + error states inline.
- Use: dispatches a new
chat-v2:suggest-usewindow event carrying the text; the Composer listens and callssetText(text)(mirrors the existingchat-v2:attach/chat-v2:toggle-internal-noteevent pattern). Staff edit and send manually. The card dismisses on Use. - The card is purely client-side ephemeral state — suggestions are not persisted as
messages and never enter the
messagestable.
4. Per-conversation Ruby on/off
- A toggle in the thread header, default OFF.
- Backed by a single conversation label
ruby-on: present = on, absent = off (so default-off = no label, which is the natural state of every existing thread). Toggling on addsruby-on; toggling off (and any staff manual send — durable takeover) removes it. This retires the oldruby-pausedlabel entirely; theruby-resumeendpoint folds into the toggle’s “on” direction. - Meaningful only in
autopilotmode, where the auto-send dispatch fires for a thread only whenruby-onis present. Incopilotmode there is no automation, so the toggle is hidden and the on-demand Suggest button is always available.
5. Auto-send gating
runRubyForInbound (the inline dispatch) gains a single early guard: run only when
clinic mode === 'autopilot' AND the conversation has the ruby-on label. In
copilot (default) it returns immediately at the top. All existing reliability fixes
(no-latch-on-transient-error, wamid dedup, send retry) remain for the autopilot path.
Data model
- No schema migration.
modelives inclinic_modules.configJSON. Per-conversation state stays inconversations.labels(text[]).
API changes
POST /whatsapp/conversations/:id/suggest— new (suggest, never send).GET/PUT /whatsapp/assistant/config—modefield replacesenabled(with back-compat parsing).POST /whatsapp/conversations/:id/ruby-toggle— body{ on: boolean }, adds/removes theruby-onlabel (subsumes the oldruby-resumeendpoint).- Update
docs/api-reference.mdper the API-documentation rule.
Files (anticipated)
server/src/lib/whatsapp-assistant-config.ts—modetype + back-compat parse.server/src/routes/whatsapp.ts— suggest endpoint, toggle endpoint, config mode.server/src/lib/whatsapp-assistant-dispatch.ts— mode/toggle guard at top ofrunRubyForInbound.server/src/lib/whatsapp-assistant-state.ts—ruby-offlabel helpers.ui/src/components/chat-v2/composer/Composer.tsx— Suggest button +chat-v2:suggest-uselistener.ui/src/components/chat-v2/thread/— suggestion card component + header Ruby toggle.ui/src/lib/serverComm.ts—suggestRubyReply,setRubyConversationModeclients.- Superadmin parity: surface clinic
modein the superadmin WhatsApp view (per the superadmin-parity rule).
Roles / permissions
Suggest button + toggle visible to reception, admin, doctor (existing WhatsApp inbox access). Patients never see suggestions. Clinicmode editing stays admin/superadmin.
Testing
- Unit:
parseAssistantConfigback-compat (enabled→mode); the dispatch guard (copilot → no send; autopilot + ruby-off → no send; autopilot + on → sends). - The suggest endpoint returns text and never writes a message row.
- Manual/visual: button → card → Use → composer populated; no patient-visible send.
Rollout
Defaultcopilot takes effect on deploy → auto-sends stop immediately. Autopilot is
re-enabled per clinic by an admin choosing the mode. No data migration to run.
