Compare commits

..

4 Commits

Author SHA1 Message Date
Khalid Abdi f1c9f8af55 chore(release): v0.2.3
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 19:37:23 +03:00
Khalid Abdi 1c65f72ccf backend: render patient card from searchPatients on a unique match
"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>
2026-06-29 19:36:49 +03:00
Khalid Abdi f12285b8c6 chore(release): v0.2.2
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 20:53:13 +03:00
Khalid Abdi 75910f7fb0 backend: fix AI chat cards not rendering on Gemini (empty tool schemas)
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>
2026-06-28 20:52:22 +03:00
6 changed files with 84 additions and 18 deletions
+24
View File
@@ -7,6 +7,30 @@ for how releases are cut and published.
## [Unreleased]
## [0.2.3] — 2026-06-29
### Fixed
- **AI chat patient record cards** now render on Google Gemini for name
lookups. "Show me <name>'s medical record" relied on the model chaining
`searchPatients``getPatient`, but Gemini often called `searchPatients`
and then emitted only a canned closing line ("Here's the record.") without
the second tool call — so no card was ever drawn. `searchPatients` now
displays the record card directly when exactly one patient matches, so the
flow no longer depends on a follow-up tool call. (The previous Gemini fix in
0.2.2 only covered the empty-schema list tools.)
## [0.2.2] — 2026-06-28
### Fixed
- **AI chat record cards** now render on Google Gemini. The card-emitting
chat tools (`listAppointments`, `listTasks`, `listPrescriptions`,
`getClinicInfo`, `getAnalytics`, `listInventory`) used an empty parameter
schema; Gemini can't emit a function call for a schema with no properties,
so it printed the call as `tool_code` text instead of invoking the tool —
leaving replies as plain text (e.g. "Show today's schedule" leaked a raw
`<tool_code>` block) with no cards. The tools now share a non-empty schema
so Gemini calls them; other providers are unaffected.
## [0.2.1] — 2026-06-27
### Fixed
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "temetro-backend",
"version": "0.2.1",
"version": "0.2.3",
"private": true,
"type": "module",
"description": "temetro backend — Express + Postgres API with Better Auth (email/password, organizations) and org-scoped patient records.",
+4 -1
View File
@@ -107,7 +107,10 @@ function systemPrompt(
"",
"Display tools (read-only):",
"- getPatient: when asked about a specific patient by file number / MRN.",
"- searchPatients: when given a name; then getPatient on the match.",
"- searchPatients: when given a name. If exactly one patient matches it",
" already shows that patient's record card — don't call getPatient again,",
" just confirm. Only call getPatient yourself for a direct file number / MRN,",
" or to pick one of several matches it returns.",
"- getPatientLabs: when asked about labs/results/trends.",
"- listAppointments: when asked to see the schedule / upcoming visits.",
"- listTasks: when asked to see open tasks / to-dos.",
+53 -14
View File
@@ -40,6 +40,18 @@ export type ToolContext = {
writer: UIMessageStreamWriter;
};
// Shared schema for tools that take no real arguments. Google Gemini cannot
// emit a function call when a tool's parameter schema has no properties — it
// prints the call as `tool_code` text instead of invoking it (see the
// previewImport note below). One optional, ignored field keeps the schema
// non-empty so Gemini calls the tool; other providers ignore the extra field.
const emptyToolArgs = z.object({
filter: z
.string()
.optional()
.describe("Optional free-text filter (ignored — the full list is shown)"),
});
// Compact, model-facing projection of a patient (Veil-redacted upstream). Keeps
// clinical signal, drops bulky arrays the model rarely needs verbatim.
function forModel(p: Patient) {
@@ -177,10 +189,12 @@ export function createChatTools(ctx: ToolContext) {
},
}),
// Search the clinic's patients by name or file number.
// Search the clinic's patients by name or file number. On a unique match this
// also displays that patient's record card directly (see below), so the common
// "show me <name>'s record" flow doesn't depend on a follow-up getPatient call.
searchPatients: tool({
description:
"Search the clinic's patients by name fragment. Returns matches with file numbers so you can then call getPatient.",
"Search the clinic's patients by name fragment. If EXACTLY ONE patient matches, this already displays their full record card — do NOT call getPatient again; just confirm in one sentence. If multiple match, it returns the matches (with file numbers) so you can disambiguate or call getPatient on the right one.",
inputSchema: z.object({
query: z.string().describe("Name or file-number fragment to match"),
}),
@@ -192,17 +206,42 @@ export function createChatTools(ctx: ToolContext) {
scopeProviderId,
);
const q = query.trim().toLowerCase();
const matches = all
// Keep the full Patient objects so a unique match can render a card.
const matched = all
.filter(
(p) =>
p.name.toLowerCase().includes(q) ||
p.fileNumber.toLowerCase().includes(q),
)
.slice(0, 10)
.map((p) => {
const r = veil.redactPatient(p);
return { fileNumber: r.fileNumber, name: r.name, status: p.status };
});
.slice(0, 10);
// Unique match → show the record card right here. Gemini often skips the
// search→getPatient hand-off and just emits a canned sentence, leaving the
// clinician with no card; rendering it directly removes that dependency.
if (matched.length === 1) {
const patient = matched[0]!;
step(`Looking up patient ${patient.fileNumber}`);
writer.write(
mode === "graph"
? { type: "data-recordGraph", data: patient }
: { type: "data-patientCard", data: patient },
);
const sourceId = addSource(
`${patient.name} · MRN ${patient.fileNumber}`,
"patient",
);
return {
count: 1,
shownCard: true,
sourceId,
patient: forModel(veil.redactPatient(patient)),
};
}
const matches = matched.map((p) => {
const r = veil.redactPatient(p);
return { fileNumber: r.fileNumber, name: r.name, status: p.status };
});
step(`Found ${matches.length} match(es)`);
return { count: matches.length, matches };
},
@@ -213,7 +252,7 @@ export function createChatTools(ctx: ToolContext) {
listAppointments: tool({
description:
"Display the clinic's appointments. Use when the clinician asks to see the schedule, upcoming visits, or today's appointments.",
inputSchema: z.object({}),
inputSchema: emptyToolArgs,
execute: async () => {
step("Loading appointments");
const all = await appointments.listAppointments(orgId);
@@ -235,7 +274,7 @@ export function createChatTools(ctx: ToolContext) {
listTasks: tool({
description:
"Display the care-team task list. Use when the clinician asks to see open tasks, to-dos, or what's assigned.",
inputSchema: z.object({}),
inputSchema: emptyToolArgs,
execute: async () => {
step("Loading tasks");
const all = await tasks.listTasks(orgId, {
@@ -258,7 +297,7 @@ export function createChatTools(ctx: ToolContext) {
listPrescriptions: tool({
description:
"Display prescriptions for the clinic. Use when the clinician asks to see prescriptions or medications prescribed.",
inputSchema: z.object({}),
inputSchema: emptyToolArgs,
execute: async () => {
if (demographicsOnly) {
return { found: false as const, reason: "not_authorized" as const };
@@ -454,7 +493,7 @@ export function createChatTools(ctx: ToolContext) {
getClinicInfo: tool({
description:
"Get the clinic's name and basic info. Use when the clinician asks about their clinic/organization (e.g. 'what's my clinic called?').",
inputSchema: z.object({}),
inputSchema: emptyToolArgs,
execute: async () => {
step("Loading clinic info");
const [org] = await db
@@ -479,7 +518,7 @@ export function createChatTools(ctx: ToolContext) {
getAnalytics: tool({
description:
"Retrieve the clinic's analytics AND earnings — patient/appointment/prescription/task counts plus money billed, paid, and outstanding (from invoices), with a by-month earnings trend. Use for KPIs, earnings, revenue, or performance questions.",
inputSchema: z.object({}),
inputSchema: emptyToolArgs,
execute: async () => {
step("Loading clinic analytics");
const data = await analytics.getAnalytics(orgId);
@@ -492,7 +531,7 @@ export function createChatTools(ctx: ToolContext) {
listInventory: tool({
description:
"List the clinic's inventory (medications/supplies, stock levels, reorder thresholds). Use for stock, low-stock, or reorder questions.",
inputSchema: z.object({}),
inputSchema: emptyToolArgs,
execute: async () => {
step("Loading inventory");
const items = await inventory.listInventory(orgId);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "frontend",
"version": "0.2.1",
"version": "0.2.3",
"private": true,
"scripts": {
"dev": "next dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "temetro",
"version": "0.2.1",
"version": "0.2.3",
"private": true,
"devDependencies": {
"shadcn": "^4.11.0"