"Show me <name>'s medical record" relied on the model chaining
searchPatients → getPatient, but Gemini Flash often calls searchPatients
then emits the canned closing line ("Here's the record.") without the
second tool call, so no data-patientCard part is ever written and no card
renders. (The prior Gemini fix only covered the empty-schema list tools.)
searchPatients now writes the record card (data-patientCard, or
data-recordGraph in graph mode) and a source directly when EXACTLY ONE
patient matches — mirroring getPatient — so the common name-lookup flow no
longer depends on a second tool call. Multiple/zero matches keep returning
the disambiguation list. The tool description and system prompt tell the
model the card is already shown on a unique match so it doesn't double-render.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The card-emitting chat tools (listAppointments, listTasks,
listPrescriptions, getClinicInfo, getAnalytics, listInventory) used an
empty `z.object({})` parameter schema. Google Gemini can't emit a
function call for a tool whose JSON schema has no properties — it prints
the call as `tool_code` text instead of invoking it — so the tool never
ran and the frontend never received the data part to render a card.
Give those tools a shared `emptyToolArgs` schema with one optional,
ignored field so the schema is non-empty and Gemini calls them. execute
bodies are unchanged; other providers ignore the extra field. This is the
same fix already documented for previewImport.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Give previewImport a concrete object schema (was z.array(z.unknown())) so
Google Gemini can emit a real function call — an array-of-unknown serializes
to an empty JSON schema, which made Gemini print the call as a `tool_code`
text block instead of invoking the tool. Validation stays lenient in
execute() via patientInputSchema. Also strengthen the system prompt: never
print tool calls/JSON or re-list record fields; keep prose to one sentence.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Retrieval tools now register a PHI-free sourceId per record/list and stream a
data-source part with the real clinician-facing title; the system prompt asks
the model to cite facts inline as [[src:id]]. The chat renders those markers as
numbered inline citation chips (PreviewCard hover) via a Streamdown link
override, with a Sources footer fallback when the model omits markers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In Graph mode, a natural-language request ("graph for John Smith") went
through the getPatient tool, which always emitted record cards — so the
clinician got Summary/Vitals cards instead of a graph (the mode prompt
even wrongly claimed the graph rendered automatically). Thread the
composer mode into the chat tools and have getPatient emit
data-recordGraph in graph mode (matching the client-side /patient
fast-path), with the cards kept for chat/analysis modes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The import preview only showed counts + skipped-row errors with no way to
fix anything. Now every parsed record is editable before import:
- backend: shared validatePatientImport() (used by the previewImport tool)
+ POST /api/ai/import/validate for dry-run re-validation; the import
preview payload now carries the original records (and the source record on
each invalid row) so the UI can edit them.
- frontend: the import card gets a "Review & edit" dialog listing every
record with a ready / needs-fixing badge; opening one reuses the full
PatientFormDialog in a new non-persisting review mode (editable file
number) and re-validates on save, so skipped rows can be fixed and
included.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Chat fixes (#1/#6/#7):
- chat-input passes raw File[] up instead of inlining file text
- chat-panel sends files as FileUIPart and renders them with the
ai-elements Attachments component (no more raw text dumps)
- backend extracts text-like file content for the model in routes/chat
- Chain-of-Thought now defaults to collapsed
- file upload works regardless of whether the input has text
AI add-to-inventory (#2):
- new proposeInventory tool (validates items, streams an approval card)
- system prompt distinguishes stocking inventory vs. billing a patient
- ActionPreviewCard commits inventory via POST /api/inventory
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
real Live card, persistent chat history
Analytics & earnings:
- Analytics now carries real money computed from invoices (billed/paid/
outstanding + by-month); new Earnings section on the Analysis page drawn with
the project's Bklit chart components (shared EarningsChart).
AI agent reaches the whole clinic:
- new read tools getClinicInfo / getAnalytics / listInventory render clinic,
analytics (with a Bklit earnings chart) and inventory cards in chat
- proposeInvoice turns an uploaded purchase/medication list into an invoice
(new "invoice" action-preview kind → createInvoice); invoices/appointments
auto-create/link a patient (ensurePatient) so they hit the Patients page
Live card:
- plots real data — patients checked in today — via GET /api/analytics/live
(polled); value pill clamped and margins widened so nothing spills the card
Persistent AI chat history (Claude-style):
- ai_chat_threads + ai_chat_messages (migration 0016); per-user, org-scoped
thread CRUD under /api/chat/threads
- chat panel owns a thread id, loads /?thread=<id>, and auto-saves after each
exchange; sidebar lists past chats (open/delete), "New chat" starts fresh
Verified with backend typecheck + frontend tsc + next build.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stop blocking AI imports/proposals on missing non-critical fields. Records
the chat agent drafts now save with safe placeholders, auto-generated file
numbers, and a source="ai" marker that surfaces an "Added by AI" badge so a
clinician can review/edit them later.
Backend:
- add `source` (manual|ai) column to patients/appointments/prescriptions
(migration 0014) + canonical types, services, validation schemas
- relax patient/appointment validation: empty file number allowed, demographic
+ type/provider/initials fall back to placeholders (initials derived from name)
- patients.generateFileNumber() auto-assigns an MRN when one is missing
- proposeAppointment accepts a name when no file number resolves; AI commits +
/api/ai/import stamp source="ai"
Frontend:
- `source` on Appointment/Patient/Prescription types; AI commits send source="ai"
- reusable <AiBadge> shown on the Patients table/detail and prescriptions list
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make the chat feel alive and let the agent act on the clinic — safely.
UX (frontend):
- Stream `data-step` parts from each tool into an inline Chain-of-Thought
trace, plus a "Thinking…" shimmer while a request is in flight (works even
on the non-streamed external+Veil path).
- Replace the modal Veil consent Dialog with an inline, once-per-session
"Veil" confirmation above the input (with a "Use local model" option).
- Render new list + action-preview cards; chat-input now uses COSS tokens.
Agent (backend):
- Add display tools (listAppointments / listTasks / listPrescriptions) and
propose tools (proposeAppointment / proposeTask / proposePrescription) that
validate as a dry run and stream an approval card — nothing is written until
the clinician approves, via the existing RBAC-gated create endpoints.
- previewImport now also covers single-patient add + migration.
- System prompt: display + add only, never edit/delete or alter the schema;
stronger migration guidance. ToolContext carries the viewer for task scoping.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Real LLM chat replacing the mock, backend-centric per the plan:
- Multi-provider API-key mode (OpenAI / Anthropic / Gemini via the AI SDK) plus
local Ollama (OpenAI-compatible endpoint). Provider is derived from the
picked model id; the matching stored key is used. New user_ai_settings table
holds per-user config with provider API keys encrypted at rest (AES-256-GCM,
src/lib/crypto.ts, keyed by AI_CREDENTIALS_KEY).
- POST /api/ai/config (get/put, secrets never returned), POST /api/ai/test
(Ollama ping / key presence), POST /api/ai/import (approved migration commit,
re-validated server-side, reuses the audited patient service).
- POST /api/chat: streamText agent with tools (getPatient, getPatientLabs,
searchPatients, previewImport). Real record data streams to the clinician as
custom data parts (cards) while the model sees only Veil-redacted results.
- Veil (src/services/ai/veil.ts): de-identifies patient identifiers to tokens
before external calls, resolves tokens on tool args, and rehydrates the final
answer. Bypassed for local Ollama. External mode runs non-streamed so the
rehydrated text is correct. Every call is audited (provider + Veil level).
- Shared role-scoping helpers extracted to src/lib/role-scope.ts (reused by the
patient routes and chat tools so visibility rules match).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>