@@ -14,6 +14,8 @@ import {
1414import { dispatchWebhookEvent } from "@/lib/webhook-dispatcher" ;
1515import { calculateDose } from "@/lib/dosing" ;
1616import { 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+
462528export const AGENT_TOOLS : AgentTool [ ] = [
463529 findClient ,
464530 getPatientSummary ,
465531 listAppointments ,
532+ findOpenSlotsTool ,
466533 bookAppointment ,
467534 listOverdueVaccinations ,
468535 calculateDrugDose ,
0 commit comments