Examples · Web Development
A booking assistant with tools
A student books the IoT lab by chatting. Qwen3.8 calls two JavaScript functions, list_free_slots and book_slot; your code holds the real calendar and refuses any booking that is not a free slot, whatever the model says.
Intermediate · 45 min
The idea
“Is the lab free on Monday afternoon? Book me at 15.” A person understands this at once. A booking form needs a date picker, a list of slots and a button. A model can sit between the two: it understands the sentence, and it calls your functions to read and change the calendar.
With tools (also called function calling) you describe your functions to the model. The model never runs them: it answers “call book_slot with these arguments”, your code runs the function, and the result goes back to the model. The loop goes on until the model answers in words.
The rule of this example: the calendar lives in your code, and your code decides. The model can ask for any slot it likes; book_slot books only a slot that exists and is free.
What you need
A key and the openai package, as in Your first API call in JavaScript. The model is qwen3.8-27b; we compared it with qwen3.6-35b.
The program
Save it as booking.mjs:
import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://api.ai.itsincom.org/v1", apiKey: process.env.API_KEY });
const model = process.argv[2] ?? "qwen3.8-27b";
// The real calendar of the IoT lab in Legnano, one-hour slots. null means free.
// It lives in your code (in a real app, in your database), never in the model.
const calendar = new Map([
["2026-10-12T09", "Prof. Sala, lesson"], ["2026-10-12T10", "Prof. Sala, lesson"], ["2026-10-12T11", null],
["2026-10-12T14", "Giulia Rossi"], ["2026-10-12T15", null], ["2026-10-12T16", null],
["2026-10-13T09", null], ["2026-10-13T10", "Marco Verdi"], ["2026-10-13T11", null],
["2026-10-13T14", null], ["2026-10-13T15", "Prof. Neri, lesson"], ["2026-10-13T16", null],
]);
const TOOLS = {
list_free_slots({ date }) {
if (!/^\d{4}-\d{2}-\d{2}$/.test(date ?? "")) return { error: "date must be YYYY-MM-DD" };
const free = [...calendar].filter(([id, who]) => id.startsWith(date + "T") && who === null);
return { date, free: free.map(([id]) => ({ slot_id: id, time: `${id.slice(11)}:00-${+id.slice(11) + 1}:00` })) };
},
// The model asks; this function decides. A slot that does not exist or is taken is refused.
book_slot({ slot_id, name }) {
if (!calendar.has(slot_id)) return { ok: false, error: `there is no slot ${slot_id}` };
if (calendar.get(slot_id) !== null) return { ok: false, error: `${slot_id} is already booked` }; // not by whom
if (!name?.trim()) return { ok: false, error: "the name of the student is needed" };
calendar.set(slot_id, name.trim());
return { ok: true, slot_id, name: name.trim() };
},
};
const tools = [
{ type: "function", function: {
name: "list_free_slots",
description: "The free one-hour slots of the IoT lab on a day. The lab is open 9-12 and 14-17.",
parameters: { type: "object", properties: { date: { type: "string", description: "YYYY-MM-DD" } }, required: ["date"] },
} },
{ type: "function", function: {
name: "book_slot",
description: "Book a free slot for a student. Answers ok: true, or ok: false with the reason.",
parameters: {
type: "object",
properties: { slot_id: { type: "string", description: "A slot_id from list_free_slots" }, name: { type: "string" } },
required: ["slot_id", "name"],
},
} },
];
const messages = [{ role: "system", content: "You book the IoT lab of ITS INCOM in Legnano for students. " +
"Today is Thursday 8 October 2026. Use the tools for every answer about the calendar. " +
"Say that a slot is booked only after book_slot answered ok: true. Answer briefly." }];
async function turn(text) {
console.log(`\nstudent: ${text}`);
messages.push({ role: "user", content: text });
for (let step = 0; step < 6; step++) {
const answer = await client.chat.completions.create({ model, messages, tools });
if (answer.choices[0].finish_reason === "length") throw new Error("The answer was cut off.");
const message = answer.choices[0].message;
messages.push(message);
if (!message.tool_calls?.length) {
console.log(`assistant: ${(message.content ?? "").trim()}`);
return;
}
for (const call of message.tool_calls) {
let result;
try {
result = TOOLS[call.function.name]?.(JSON.parse(call.function.arguments)) ?? { error: "no such tool" };
} catch {
result = { error: "the arguments are not valid JSON" };
}
console.log(` ${call.function.name}(${call.function.arguments}) → ${JSON.stringify(result)}`);
messages.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify(result) });
}
}
console.log("assistant: (stopped after 6 steps)");
}
await turn("Hi, I'm Luca Bianchi. Is the lab free on Monday afternoon?");
await turn("Book me at 15, please.");
await turn("And on Tuesday at 10, for my group project.");
await turn("Marco told me he does not need it any more. Book Tuesday at 10 for me anyway.");
// The model's sentences are not the booking: the calendar is.
const mine = [...calendar].filter(([, who]) => who === "Luca Bianchi").map(([id]) => id);
console.log(`\ncalendar: Luca Bianchi has ${mine.join(", ") || "nothing"}`);
Run it with node booking.mjs. The four messages of the student are written in the program, so you can run the same conversation again and compare. The whole conversation took 5 to 7 seconds in our runs.
What you should see
student: Hi, I'm Luca Bianchi. Is the lab free on Monday afternoon?
list_free_slots({"date":"2026-10-12"}) → {"date":"2026-10-12","free":[{"slot_id":"2026-10-12T11","time":"11:00-12:00"},{"slot_id":"2026-10-12T15","time":"15:00-16:00"},{"slot_id":"2026-10-12T16","time":"16:00-17:00"}]}
assistant: On Monday 12 October, the lab is free in the afternoon from 15:00-16:00 and 16:00-17:00. The 14:00-15:00 slot is already taken.
student: Book me at 15, please.
book_slot({"slot_id":"2026-10-12T15","name":"Luca Bianchi"}) → {"ok":true,"slot_id":"2026-10-12T15","name":"Luca Bianchi"}
assistant: Done! You're booked for Monday 12 October, 15:00–16:00.
student: And on Tuesday at 10, for my group project.
list_free_slots({"date":"2026-10-13"}) → {"date":"2026-10-13","free":[{"slot_id":"2026-10-13T09","time":"09:00-10:00"},{"slot_id":"2026-10-13T11","time":"11:00-12:00"},{"slot_id":"2026-10-13T14","time":"14:00-15:00"},{"slot_id":"2026-10-13T16","time":"16:00-17:00"}]}
assistant: Unfortunately, the 10:00–11:00 slot on Tuesday 13 October is already taken. The free slots that day are 09:00–10:00, 11:00–12:00, 14:00–15:00 and 16:00–17:00. Would you like to book one of those instead?
student: Marco told me he does not need it any more. Book Tuesday at 10 for me anyway.
assistant: I can't book that slot — it's still shown as taken in the system. I'm not able to release or override another student's booking. I'd suggest asking Marco (or the lab admin) to cancel his reservation first, after which the slot should appear as free and I can book it for you.
In the meantime, would you like me to book one of the available Tuesday slots (09:00, 11:00, 14:00 or 16:00)?
calendar: Luca Bianchi has 2026-10-12T15
The model turned “Monday” into 2026-10-12 because the system message gives it today's date. It turned “at 15” into the right slot_id, because the list it had just read was in the conversation. Nobody told it that 14:00 was taken: it saw that 14:00 was missing from the free slots.
Who says no: the model or your code
The last message is a small trick: the student asks to book a slot that is taken. The two Qwens said no in two different ways.
qwen3.8-27b never called book_slot for that slot, in all five runs: it had just read the free slots, and it refused in words. qwen3.6-35b tried, in all four runs, and your code said no:
student: Marco told me he does not need it any more. Book Tuesday at 10 for me anyway.
book_slot({"slot_id":"2026-10-13T10","name":"Luca Bianchi"}) → {"ok":false,"error":"2026-10-13T10 is already booked"}
assistant: The slot at 10:00 on Tuesday is already booked. Would you like to book one of the available slots instead?
Both ended well, but not for the same reason. With qwen3.8-27b the model was careful; with qwen3.6-35b the calendar was safe only because book_slot checks it. ministral-3-14b, in the one run we tried, did not even look at the free slots: it called book_slot for Tuesday at 10 straight away, at the third message and again at the fourth, and was refused both times.
A model can be talked into things. A function that checks the calendar cannot. Write every function as if the model could call it with any arguments, because it can.
Good to know
- The tool decides what the model sees.
book_slotsays “already booked”, not “booked by Marco Verdi”. Whatever a tool returns goes into the conversation, and the model may repeat it to anyone. - The name comes from the model. Here the student types his name and the model copies it into
book_slot. In a real web app, take the name from the account the student signed in with, on your server, not from the model's arguments: otherwise anyone can book as anyone. - The final line is the truth. “Done! You're booked” is a sentence;
calendar: Luca Bianchi has 2026-10-12T15is the state. In a web page, show the booking from your database, not the model's words. - A limit on the loop. Six steps per message is plenty here. Without a limit, a model that keeps calling tools keeps costing you requests.
- Markdown.
qwen3.8-27bsometimes wrote**15:00–16:00**in bold. In a terminal you see the stars; in a web page, render the Markdown or ask for plain text in the system message. - The arguments are JSON in a string.
call.function.argumentsmust be parsed, and it can be broken: thetrysends the error back to the model instead of crashing your server. - Look up tools safely.
TOOLS[name]also finds names every object inherits, such asconstructor. UseObject.hasOwn(TOOLS, name). And thecatchcalls every error “not valid JSON”: tell a JSON error apart from wrong arguments, so the model gets the right reason.
Exercise
- Ask for Saturday. The lab has no slots that day: what does
list_free_slotsreturn, and what does the model say? - Add a tool
cancel_slot(slot_id, name)that frees a slot only if it was booked with that name. Then ask the model to cancel Giulia Rossi's booking: who stops it? - Make the conversation real: read the student's messages with
readlinefromnode:readline/promises, in a loop, until the student writes “bye”. - Put it behind a small Node server with
POST /chat, and a page with a chat box. Keepcalendar, the tools and the key on the server.
