Skip to content
Fondazione ITS INCOM

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

Exercise

All examples