Skip to content

Commit

Permalink
feat: add agent section
Browse files Browse the repository at this point in the history
  • Loading branch information
sgomez committed Oct 13, 2024
1 parent c371fc2 commit 021fa04
Show file tree
Hide file tree
Showing 5 changed files with 330 additions and 361 deletions.
201 changes: 201 additions & 0 deletions docs/agents/using-tools-2.md
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))
```
126 changes: 126 additions & 0 deletions docs/agents/using-tools.md
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)
})
```
Loading

0 comments on commit 021fa04

Please sign in to comment.