I built a chatbot that uses a Large Language Model (LLM) to answer questions about pool availability in Montréal.

Allowing an LLM to operate unchecked and make decisions can be risky. For example, Air Canada had to pay compensation because of a misleading response from an LLM: Washington Post article.

The problem

I started swimming a couple of weeks ago, and fortunately, Montréal has many indoor swimming pools. The city’s website lists all the indoor pools along with their availability: Indoor swimming pools.

The downside is that you can’t search for an available pool at a specific time, as each pool has its own schedule, which varies significantly. Some pools have their schedules embedded as a table on the website, like this one: Bain Émard.

However, for other pools, you have to leave the city website and visit a Facebook page to see the pool’s schedule: Complexe Sportif de Saint-Laurent.

Since I didn’t have a fixed time to go to the pool, I found myself constantly navigating through pool websites to find the next available slot.

Large language models

I decided to use an LLM like ChatGPT to address this problem. LLMs excel at understanding complex text, such as pool schedules, and grasping concepts related to time and space, like pool opening and closing times, or pools nearest to a particular address.

One challenge is that pool schedules change frequently, and recent schedules weren’t included in the LLMs’ training data.

Here are three possible solutions:

  • Fine-tune the model using recent data. Fine-tuning involves updating the model’s weights by training it with a new dataset. This is effective if you have a large, “static” dataset that was not available during the initial training of the LLM. By static, I mean data that doesn’t change over time. However, this approach can be costly and resource-intensive.
  • Provide the entire database alongside the user’s query. It works well if the dataset is relatively small. Current LLMs support a large context window, but if the dataset is too large, this approach becomes impractical because the LLM can only process a limited portion of the prompt and will only consider the most recent part.
  • Use retrieval-augmented generation. RAG integrates LLMs with specific knowledge databases, without the need for retraining the model. This approach is cost-effective and helps keep the LLM’s output relevant and accurate. RAG works by first searching through a large database to find relevant information related to a query. Then, it uses this information to generate a response.
Diagram of a basic RAG pipeline, from documents through indexing and retrieval to the language model
A basic RAG pipeline. Source: DeepLearning.AI — Building and Evaluating Advanced RAG.

The design and implementation

The pipeline has two halves that run at different times. Everything on the left happens once, offline, when the schedules are collected. Everything on the right happens per question:

OFFLINE · ONCEMontréal pool pagesHTML tables · FacebookOne text file per poolschedule · address · closuresEmbedding modeltext → vectorsVector storeLlamaIndexPER QUESTIONUser questionChainLit chat UISame embedding modelsemantic searchPrompt = context + questionretrieved schedulesLLM answer+ link to the source

Prepare the dataset

We need to collect data on all the indoor pools. I tried to automate this process as much as possible, but there is room for improvement to handle more complex websites.

Using some Python packages, I created a text file with the schedule of each pool:

Directory listing of one text file per Montréal indoor pool
Montréal indoor pools dataset: one file per pool.

To achieve this, we need to extract all tables and relevant information from each pool’s HTML page, such as the pool’s address and any exceptional closures.

There are tools that can help automate this process, such as LlamaIndex’s web readers. See data connectors and the web reader package.

Each schedule looks something like this:

Extracted pool schedule as plain text, with days, times and activity types
Example of a pool schedule after extraction.

Create the embeddings

To find relevant information in the database, we can use various search methods, such as semantic search or keyword search.

Semantic search finds data based on the intent and contextual meaning of a query, rather than an exact match on query words.

So, once the dataset of text files is ready, we need to represent each word or phrase with a vector in a high-dimensional vector space, where words with similar meanings will be close to each other.

I used LlamaIndex to handle data preprocessing.

Diagram of text being converted into vectors in a high-dimensional embedding space
Text becomes vectors.

To create an embedding, we need an embedding model, which is an LLM used to convert text into vectors. It’s important to use the same model for both the user query and the database embeddings.

Prepare the prompt

With the embeddings ready, we need to obtain the user query, create an embedding for it, search the database for relevant documents, and prepare the prompt for the LLM. Tools like ChainLit can be used to create the chatbot, handling server creation, message history, themes, parsing, and more.

Demo

Here’s the chatbot running:

The chatbot answering a question about which pool is open next, and linking to the pool's page on the city website
ChatGPT used in a RAG system to answer questions about Montréal pools.

The lesson

  • As a software developer, I always aim to find exact solutions that are deterministic and proven through mathematics, physics, and science. Working with LLMs made me uncomfortable because I couldn’t fully trust them, even though the responses were accurate in my case. As far as I know, there’s no guarantee that the model will consistently perform well. This is why in my demo I provide the URL to the pool website as a source.
  • Due to the AI boom, there are countless tools available, with many attempting to achieve similar objectives. Standards and interfaces are not yet well defined, and code that works today may become obsolete in a few months.
  • The whole thing took an afternoon, with an LLM as a coding partner, to build an app that answers questions with an LLM.
  • I was occasionally banned from accessing pool websites due to intensive data collection for the database. My apologies to Ville de Montréal for the load. :)

Sources