Skip to content

Commit 50deae8

Browse files
evangauerclaude
andcommitted
Add find_open_slots agent tool
Closes the booking loop for the agent: it can now find free times on a date (optionally per doctor/room) via the availability engine, then book one with book_appointment. Read-only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent f12904f commit 50deae8

1 file changed

Lines changed: 67 additions & 0 deletions

File tree

apps/web/lib/agent/tools.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import {
1414
import { dispatchWebhookEvent } from "@/lib/webhook-dispatcher";
1515
import { calculateDose } from "@/lib/dosing";
1616
import { summarizePlanProgress, type PlanItemStatus } from "@/lib/treatment-plans/progress";
17+
import { findOpenSlots } from "@/lib/scheduling/availability";
18+
import { not, gt } from "drizzle-orm";
1719

1820
/**
1921
* The agent's "hands": typed tools that operate the practice's data, always
@@ -459,10 +461,75 @@ const recordVitalSigns: AgentTool = {
459461
},
460462
};
461463

464+
const findOpenSlotsTool: AgentTool = {
465+
name: "find_open_slots",
466+
description:
467+
"Find open appointment times on a date (optionally for a specific doctor or room). Use before book_appointment to pick a free time.",
468+
inputSchema: {
469+
type: "object",
470+
properties: {
471+
date: { type: "string", description: "YYYY-MM-DD" },
472+
durationMinutes: { type: "number" },
473+
doctorId: { type: "string" },
474+
roomId: { type: "string" },
475+
},
476+
required: ["date"],
477+
},
478+
zod: z.object({
479+
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
480+
durationMinutes: z.number().int().min(10).max(240).optional(),
481+
doctorId: z.string().uuid().optional(),
482+
roomId: z.string().uuid().optional(),
483+
}),
484+
readOnly: true,
485+
async execute(args, ctx) {
486+
const input = this.zod.parse(args) as {
487+
date: string;
488+
durationMinutes?: number;
489+
doctorId?: string;
490+
roomId?: string;
491+
};
492+
const dayStart = new Date(`${input.date}T08:00:00`);
493+
const dayEnd = new Date(`${input.date}T18:00:00`);
494+
495+
const rows = await ctx.db
496+
.select({
497+
startTime: appointments.startTime,
498+
endTime: appointments.endTime,
499+
doctorId: appointments.doctorId,
500+
roomId: appointments.roomId,
501+
})
502+
.from(appointments)
503+
.where(
504+
and(
505+
eq(appointments.practiceId, ctx.practiceId),
506+
isNull(appointments.deletedAt),
507+
not(inArray(appointments.status, ["cancelled", "no_show"])),
508+
lt(appointments.startTime, dayEnd),
509+
gt(appointments.endTime, dayStart)
510+
)
511+
);
512+
513+
const busy = rows.filter((r) => {
514+
if (input.doctorId && r.doctorId === input.doctorId) return true;
515+
if (input.roomId && r.roomId === input.roomId) return true;
516+
return !input.doctorId && !input.roomId;
517+
});
518+
519+
return findOpenSlots({
520+
dayStart,
521+
dayEnd,
522+
slotMinutes: input.durationMinutes ?? 30,
523+
busy,
524+
}).map((s) => ({ start: s.start.toISOString(), end: s.end.toISOString() }));
525+
},
526+
};
527+
462528
export const AGENT_TOOLS: AgentTool[] = [
463529
findClient,
464530
getPatientSummary,
465531
listAppointments,
532+
findOpenSlotsTool,
466533
bookAppointment,
467534
listOverdueVaccinations,
468535
calculateDrugDose,

0 commit comments

Comments
 (0)