mirror of
https://github.com/temetro/temetro.git
synced 2026-07-27 04:08:56 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f1c9f8af55 | |||
| 1c65f72ccf | |||
| f12285b8c6 | |||
| 75910f7fb0 |
@@ -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,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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.2.1",
|
||||
"version": "0.2.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "temetro",
|
||||
"version": "0.2.1",
|
||||
"version": "0.2.3",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"shadcn": "^4.11.0"
|
||||
|
||||
Reference in New Issue
Block a user