generated from aulasoftwarelibre/plantilla-talleres
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
330 additions
and
361 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
# Handling User Selection | ||
|
||
In this chapter, we’ll focus on how the chatbot processes user selections of time slots and finalizes their booking by reserving the appointment in the database. We’ll examine how the user’s button action triggers the confirmation tool and how the appointment is stored and managed. | ||
|
||
## Processing User Selections and Confirming the Appointment | ||
|
||
Once the user has chosen a time slot using the button interface, the chatbot must handle this selection and confirm the booking. This process involves handling the button action, reserving the time slot, and providing feedback to the user. | ||
|
||
### Managing Appointment Data | ||
|
||
The AppointmentRepository is responsible for handling appointment data within our chatbot. It ensures that available time slots are fetched, new ones are created if necessary, and appointments are reserved when a user confirms a time slot. | ||
|
||
The `reserve` is a new function than attempts to reserve an available slot. It updates the database, associating the selected time slot with the user’s chat ID. If successful, the slot is marked as reserved, and no other users can select it. | ||
|
||
|
||
```ts title="src/lib/repositories/appointments.ts" | ||
import { and, eq, isNull } from "drizzle-orm/expressions"; | ||
|
||
import { db as database } from "../db/index"; | ||
import { appointments } from "../db/schema/appointments"; | ||
|
||
export class AppointmentRepository { | ||
async getFreeAppointmentsForDay(date: Date): Promise<Array<{ timeSlot: string }>> { | ||
const allAppointments = await database | ||
.select({ | ||
chatId: appointments.chatId, | ||
timeSlot: appointments.timeSlot, | ||
}) | ||
.from(appointments) | ||
.where(eq(appointments.date, date)); | ||
|
||
// If no appointments exist for this day, create them | ||
if (allAppointments.length === 0) { | ||
await this.createEmptyAppointmentsForDay(date); | ||
return this.getFreeAppointmentsForDay(date); | ||
} | ||
|
||
// Filter free appointments where chatId is null (not reserved) | ||
const freeAppointments = allAppointments.filter((appointment) => appointment.chatId === null); | ||
|
||
return freeAppointments.map((appointment) => ({ | ||
timeSlot: appointment.timeSlot, | ||
})); | ||
} | ||
|
||
private async createEmptyAppointmentsForDay(date: Date): Promise<void> { | ||
const openingTime = 9; // 9 AM | ||
const closingTime = 19; // 7 PM | ||
|
||
const timeSlots = Array.from({ length: closingTime - openingTime }, (_, index) => { | ||
const time = `${(openingTime + index).toString().padStart(2, "0")}:00`; | ||
return { date, timeSlot: time }; | ||
}); | ||
|
||
// Insert empty appointments for each time slot | ||
await database.insert(appointments).values(timeSlots); | ||
} | ||
|
||
async reserve(chatId: number, date: Date, timeSlot: string): Promise<boolean> { | ||
const updated = await database | ||
.update(appointments) | ||
.set({ chatId }) | ||
.where(and(eq(appointments.date, date), eq(appointments.timeSlot, timeSlot), isNull(appointments.chatId))) | ||
.returning() | ||
.execute(); | ||
|
||
return updated.length > 0; | ||
} | ||
} | ||
|
||
export const appointmentsRepository = new AppointmentRepository(); | ||
``` | ||
|
||
### Confirming the Appointment | ||
|
||
The `confirmAppointment` tool is used to handle the reservation once the user selects a time slot. The tool checks if the selected time slot is still available in the database. If the slot is free, it reserves it for the user by associating it with their chat ID. If the slot is already taken, it notifies the user that they need to pick a different time. This tool ensures that only available slots can be reserved, and it provides feedback based on the reservation’s success or failure. | ||
|
||
```ts title="src/lib/tools/confirm-appointment.ts" | ||
import { type CoreTool, tool } from "ai"; | ||
import type { Context } from "grammy"; | ||
import { z } from "zod"; | ||
|
||
import { appointmentsRepository } from "../repositories/appointments"; | ||
import { tomorrow } from "../utils"; | ||
|
||
export const buildConfirmAppointment = (context: Context): CoreTool => | ||
tool({ | ||
description: "Confirm the selected appointment.", | ||
execute: async ({ slot }) => { | ||
console.log(`Called confirmAppointment tool with ${JSON.stringify({ slot }, null, 2)}`); | ||
|
||
const chatId = context.chatId; | ||
if (!chatId) { | ||
return "Sorry, I cannot do the reserve."; | ||
} | ||
|
||
const isReserved = appointmentsRepository.reserve(chatId, tomorrow(), slot); | ||
|
||
if (!isReserved) { | ||
return "Sorry, that time slot is no longer available. Please choose another one."; | ||
} | ||
|
||
return `Your appointment at ${slot} is confirmed!`; | ||
}, | ||
parameters: z.object({ | ||
slot: z.string().describe("The selected time slot to confirm. Format HH:MM"), | ||
}), | ||
}); | ||
``` | ||
|
||
## Telegram Callback Listener for Button Actions | ||
|
||
The onAppointment listener is responsible for capturing the user’s button presses and processing them within the ongoing conversation. When a user clicks a time slot button, the listener extracts the selected slot from the button’s callback data and integrates it into the conversation. | ||
|
||
|
||
```ts title="src/lib/handlers/on-appointment.ts" | ||
import { generateText } from "ai"; | ||
import { Composer } from "grammy"; | ||
|
||
import { registry } from "../ai/setup-registry"; | ||
import { environment } from "../environment.mjs"; | ||
import { conversationRepository } from "../repositories/conversation"; | ||
import { buildConfirmAppointment } from "../tools/confirm-appointment"; | ||
|
||
export const onAppointment = new Composer(); | ||
|
||
const PROMPT = ` | ||
You are a chatbot designed to help users book hair salon appointments for tomorrow. | ||
You have access to several tools to help with this task and are restricted to handling appointment bookings only. You do not handle any other inquiries. | ||
Your primary goal is to guide users through the process of booking their appointments efficiently. | ||
You will follow these rules: | ||
1. Appointment Search: Use the "getFreeAppointments" tool to search for available appointment times for tomorrow. | ||
2. Display Options: After "getFreeAppointments" use always the "displaySelectionButtons" tool to ask the user the available time slots as buttons they can select from. | ||
3. Confirm Appointment: After the user selects a time, use the confirmAppointment function to finalize their appointment request. | ||
4. Single-purpose chatbot: You only help users book appointments for tomorrow and do not answer unrelated questions, you need to use tools to ask options. | ||
If a user asks for information outside of these details, please respond with: "I'm sorry, but I cannot assist with that. For more information, please call us at (555) 456-7890 or email us at [email protected]." | ||
`; | ||
|
||
onAppointment.callbackQuery(/slot-.+/, async (context) => { | ||
const chatId = context.chatId as number; | ||
const slot = context.callbackQuery.data.slice(5); | ||
|
||
await conversationRepository.addMessage(chatId, "user", slot); | ||
const messages = await conversationRepository.get(chatId); | ||
|
||
await context.api.sendChatAction(chatId, "typing"); | ||
|
||
const { text } = await generateText({ | ||
maxSteps: 2, | ||
messages, | ||
model: registry.languageModel(environment.MODEL), | ||
system: PROMPT, | ||
tools: { | ||
confirmAppointment: buildConfirmAppointment(context), | ||
}, | ||
}); | ||
|
||
if (text) { | ||
await context.reply(text); | ||
await conversationRepository.addMessage(chatId, "assistant", text); | ||
} | ||
}); | ||
``` | ||
|
||
And we need include this listener into the main bot file: | ||
|
||
```ts title="src/main.ts" | ||
import process from 'node:process' | ||
|
||
import { Bot } from 'grammy' | ||
|
||
import { ask } from './lib/commands/ask' | ||
import { learn } from './lib/commands/learn' | ||
import { start } from './lib/commands/start' | ||
import { environment } from './lib/environment.mjs' | ||
import { onAppointment } from './lib/handlers/on-appointment' | ||
import { onMessage } from './lib/handlers/on-message' | ||
|
||
async function main(): Promise<void> { | ||
const bot = new Bot(environment.BOT_TOKEN) | ||
|
||
bot.command('start', start) | ||
bot.command('learn', learn) | ||
bot.command('ask', ask) | ||
|
||
bot.use(onMessage) | ||
bot.use(onAppointment) | ||
|
||
// Enable graceful stop | ||
process.once('SIGINT', () => bot.stop()) | ||
process.once('SIGTERM', () => bot.stop()) | ||
process.once('SIGUSR2', () => bot.stop()) | ||
|
||
await bot.start() | ||
} | ||
|
||
main().catch((error) => console.error(error)) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
# Displaying Available Time Slots | ||
|
||
In this chapter, we’ll explore how our chatbot guides users to book a hair salon appointment for the next day by displaying the available time slots using Telegram buttons. We will focus on retrieving the available appointments and dynamically creating button options for the user. | ||
|
||
## Showing Time Slot Buttons to Users | ||
|
||
Our chatbot uses several components to display time slot buttons, allowing users to easily select their preferred appointment time. We’ll break down how this is achieved by combining the Telegram bot API, the Vercel AI SDK, and custom tools. | ||
|
||
The chatbot leverages tools from the Vercel AI SDK to perform specific actions—like displaying buttons. In this case, the displaySelectionButtons tool is responsible for rendering Telegram buttons for available time slots. This tool works with the context provided by Telegram (via grammy), dynamically creating and sending button options to the user. | ||
|
||
The chatbot uses the getFreeAppointments tool to retrieve available time slots for the next day. This tool queries the backend for any unreserved time slots. The slots are passed into the displaySelectionButtons tool, which then turns them into clickable options for the user to choose from. | ||
|
||
The Telegram API, through grammy, allows the chatbot to interact with users in a conversational manner. Once the chatbot receives a message from the user, it generates a response using the AI model from Vercel, following the set rules in the prompt. If time slots are available, the bot uses InlineKeyboard to display them as buttons, providing a seamless way for users to select their appointment time. | ||
|
||
To maintain a coherent flow, the chatbot uses the conversationRepository to log user messages and the bot’s responses. This ensures the chatbot can maintain context, such as remembering which time slots were offered, and guiding the user step-by-step toward confirming their appointment. | ||
|
||
By combining these components, the chatbot provides a smooth and user-friendly booking experience. In the next chapter, we will cover how to handle the button clicks and confirm the reservation. | ||
|
||
### Create the tool to display buttons | ||
|
||
```ts title="src/lib/tools/display-selection-buttons.ts" | ||
import { type CoreTool, tool } from "ai"; | ||
import { type Context, InlineKeyboard } from "grammy"; | ||
import { z } from "zod"; | ||
|
||
import { conversationRepository } from "../repositories/conversation"; | ||
|
||
export const buildDisplaySelectionButtons = (context: Context): CoreTool => | ||
tool({ | ||
description: "Use this tool to ask to the user with the available time slots as buttons they can select from.", | ||
execute: async ({ options, question }) => { | ||
console.log(`Called displaySelectionButtons tool with ${JSON.stringify({ options, question }, null, 2)}`); | ||
|
||
const buttonsRows = []; | ||
for (let index = 0; index < options.length; index += 2) { | ||
buttonsRows.push( | ||
options | ||
.slice(index, index + 2) | ||
.map((option: string) => ({ | ||
data: `slot-${option}`, | ||
label: option, | ||
})) | ||
.map(({ data, label }) => InlineKeyboard.text(label, data)) | ||
); | ||
} | ||
|
||
await context.reply(question, { | ||
reply_markup: InlineKeyboard.from(buttonsRows), | ||
}); | ||
|
||
const content = `${question}: ${options.join(", ")}`; | ||
|
||
const chatId = context?.chat?.id as number; | ||
await conversationRepository.addMessage(chatId, "assistant", content); | ||
|
||
return null; | ||
}, | ||
parameters: z.object({ | ||
options: z.array(z.string()).describe("Array of time slots for the user to choose from"), | ||
question: z.string().describe("The question to ask the user"), | ||
}), | ||
}); | ||
``` | ||
|
||
### Add the tool and update the prompt | ||
|
||
```ts title="src/lib/handlers/on-message.ts" | ||
import { generateText } from 'ai' | ||
import { Composer } from 'grammy' | ||
|
||
import { registry } from '../ai/setup-registry' | ||
import { environment } from '../environment.mjs' | ||
import { conversationRepository } from '../repositories/conversation' | ||
import { buildDisplaySelectionButtons } from '../tools/display-selection-buttons' | ||
import { getFreeAppointments } from '../tools/get-free-appointments' | ||
|
||
export const onMessage = new Composer() | ||
|
||
const PROMPT = ` | ||
You are a chatbot designed to help users book hair salon appointments for tomorrow. | ||
You have access to several tools to help with this task and are restricted to handling appointment bookings only. You do not handle any other inquiries. | ||
Your primary goal is to guide users through the process of booking their appointments efficiently. | ||
You will follow these rules: | ||
1. Appointment Search: Use the "getFreeAppointments" tool to search for available appointment times for tomorrow. | ||
2. Display Options: After "getFreeAppointments" use always the "displaySelectionButtons" tool to ask the user the available time slots as buttons they can select from. | ||
3. Confirm Appointment: After the user selects a time, use the confirmAppointment function to finalize their appointment request. | ||
4. Single-purpose chatbot: You only help users book appointments for tomorrow and do not answer unrelated questions, you need to use tools to ask options. | ||
If a user asks for information outside of these details, please respond with: "I'm sorry, but I cannot assist with that. For more information, please call us at (555) 456-7890 or email us at [email protected]." | ||
` | ||
|
||
onMessage.on('message:text', async (context) => { | ||
const userMessage = context.message.text | ||
const chatId = context.chat.id | ||
|
||
// Store the user's message | ||
await conversationRepository.addMessage(chatId, 'user', userMessage) | ||
|
||
// Retrieve past conversation history | ||
const messages = await conversationRepository.get(chatId) | ||
|
||
// Generate the assistant's response using the conversation history | ||
const { text } = await generateText({ | ||
maxSteps: 2, | ||
messages, | ||
model: registry.languageModel(environment.MODEL), | ||
system: PROMPT, | ||
tools: { | ||
displaySelectionButtons: buildDisplaySelectionButtons(context), | ||
getFreeAppointments, | ||
}, | ||
}) | ||
|
||
if (!text) { | ||
return | ||
} | ||
|
||
// Store the assistant's response | ||
await conversationRepository.addMessage(chatId, 'assistant', text) | ||
|
||
// Reply with the generated text | ||
await context.reply(text) | ||
}) | ||
``` |
Oops, something went wrong.