Build a RAG bot on the Tock documentation
In this tutorial, you will build a bot that answers questions about Tock, using the Tock documentation itself as its knowledge base. The bot relies on RAG (Retrieval-Augmented Generation): for each question, Tock searches the documentation for the most relevant passages, then asks an LLM to write an answer from them, with links to the sources.
Everything runs on your machine: the Tock platform with Docker, and the models with Ollama.
What you will create
- A local Tock platform with the Gen AI orchestrator and a PGVector vector store
- A vector index of the Tock documentation
- A bot that answers questions such as "Which vector stores are supported?", with links to the documentation pages
Prerequisites
- About 30 minutes
- Git, Python 3 (no extra package is needed), and recent versions of Docker and Docker Compose
- Ollama, and about 16 GB of RAM to run the platform and a 7B model
- Tock 26.9.0 or later: earlier versions of the indexing tool fail at startup, and their PostgreSQL schema does not support the hybrid search used below
GPU or not? With a GPU (or an Apple Silicon Mac), an answer takes from a few seconds to about thirty seconds (20 to 35 seconds on an M4 Mac with 16 GB of RAM). On a CPU only, a 7B model needs several minutes per answer: the platform must then be started with longer timeouts, see Running on a CPU. You can also use a hosted LLM, see Using OpenAI instead of Ollama.
On macOS
- Install the Ollama application, not a Docker image: Docker containers cannot use the GPU of the Mac.
- Docker can be provided by Docker Desktop
or by Colima. With Colima:
- the virtual machine only has 2 GB of memory by default, whereas the platform uses almost 3 GB:
start it with more resources, for instance
colima start --cpu 4 --memory 6(the new values are kept for the next starts); - the
docker composecommand needs the Docker Compose plugin:brew install docker-compose, then add its folder tocliPluginsExtraDirsin~/.docker/config.json, as explained at the end of the installation; - clone the repositories in your home folder: by default, Colima only shares this one with the virtual machine.
Elsewhere (for instance in
/tmp), the files mounted by Docker Compose are empty in the containers: neither the MongoDB replica set nor the PostgreSQL schema is initialized.
- the virtual machine only has 2 GB of memory by default, whereas the platform uses almost 3 GB:
start it with more resources, for instance
Prepare the models
The bot uses two models:
- an embedding model,
mxbai-embed-large, which turns texts into vectors to search the documentation, - an LLM,
qwen2.5:7b, which writes the answers.
ollama pull mxbai-embed-large
ollama pull qwen2.5:7b
The RAG prompt of Tock, with the retrieved documents, is longer than the default context window of Ollama (4096 tokens): the prompt would be truncated, and the LLM would not answer in the expected JSON format. Create a variant of the model with a larger context window:
printf 'FROM qwen2.5:7b\nPARAMETER num_ctx 16384\n' > Modelfile
ollama create qwen2.5-16k -f Modelfile
The Tock containers reach Ollama through the host.docker.internal address
(the Docker Compose files of Tock map it to the host, also on Linux).
On Linux, Ollama only listens on 127.0.0.1 by default: make it listen on all interfaces, for instance
when it runs as a systemd service:
sudo systemctl edit ollama
# add the following lines, then save:
# [Service]
# Environment="OLLAMA_HOST=0.0.0.0"
sudo systemctl restart ollama
On macOS (Docker Desktop or Colima) and Windows,
host.docker.internalreaches Ollama without any configuration: this step is not needed.
Start the platform
The Tock Docker repository provides a Docker Compose file with everything needed for RAG: Tock Studio, Bot API, the Gen AI orchestrator, and a PostgreSQL database with the PGVector extension.
git clone https://github.com/theopenconversationkit/tock-docker.git && cd tock-docker
chmod +x scripts/setup.sh
docker compose -f docker-compose-rag-pgvector.yml up -d
The Tock version is set by the
TAGvariable of the.envfile.
At its first start, the PostgreSQL database is initialized with the schema of the Gen AI orchestrator, including the full-text search column used by the hybrid search.
If the
pgvector-postgres-vlvolume was created by an earlier version of Tock Docker, this schema is missing and the hybrid search fails (the bot answers "Technical error"): remove the volume withdocker compose -f docker-compose-rag-pgvector.yml down -v(this also deletes the Tock data), or applyscripts/pgvector/init.sqlto the database.
After a minute or so, Tock Studio is available at http://localhost.
Log in with admin@app.com / password.
Create the application
When you first log in, a wizard invites you to create an application:
- Choose your language: select English, then Next
- Select a first Channel: select web, then Next
- Create your Assistant: Create
The wizard creates an application named new_assistant, in the app namespace (the namespace of the default users
of the Docker platform). The documents are indexed for a given bot: the indexing tool needs this namespace
and this application name.
Convert the documentation into a CSV file
The indexing tool reads documents from a CSV file with the columns title, source and text.
The Tock documentation is a set of Markdown files, in the docs/docs folder of the Tock repository:
the following script turns each section of each page into a row of the CSV file, with the URL of the section
on doc.tock.ai as its source.
Get the documentation, next to the tock-docker folder, and create a working folder:
cd ..
git clone --depth 1 https://github.com/theopenconversationkit/tock.git
mkdir -p tock-rag && cd tock-rag
Create a docs_to_csv.py file in the tock-rag folder:
"""Converts the Tock documentation (Markdown) into a CSV file for the Tock indexing tools.
Usage: python3 docs_to_csv.py <docs folder> <output csv> [<base url>]
Each section (## heading) of each page becomes a row: title | source | text.
"""
import csv
import re
import sys
import unicodedata
from pathlib import Path
docs_dir = Path(sys.argv[1])
output = Path(sys.argv[2])
base_url = sys.argv[3] if len(sys.argv) > 3 else 'https://doc.tock.ai/tock/master/'
# Pages that are not published on doc.tock.ai (exclude_docs in mkdocs.yml)
excluded_pages = 'prompt-example-type-*'
def slugify(heading):
# Same anchors as the MkDocs table of contents
custom_id = re.search(r'\{#([\w-]+)\}$', heading)
if custom_id:
return custom_id.group(1)
text = re.sub(r'\[([^\]]*)\]\([^)]*\)', r'\1', heading) # links: keep the label
text = re.sub(r'[`*]|\b_|_\b', '', text) # code and emphasis markers, not underscores inside words
text = unicodedata.normalize('NFKD', text).encode('ascii', 'ignore').decode()
text = re.sub(r'[^\w\s-]', '', text).strip().lower()
return re.sub(r'[-\s]+', '-', text)
def clean(text):
text = re.sub(r'<!--.*?-->', '', text, flags=re.DOTALL) # comments
text = re.sub(r'!\[[^\]]*\]\([^)]*\)', '', text) # images
text = re.sub(r'\[([^\]]*)\]\([^)]*\)', r'\1', text) # links: keep the label
text = re.sub(r'-{4,}', '---', text) # table separators: fewer tokens
return re.sub(r'\n{3,}', '\n\n', text).strip()
rows = []
for page in sorted(docs_dir.rglob('*.md')):
if page.match(excluded_pages):
continue
content = page.read_text(encoding='utf-8')
page_title = page.stem
front_matter = re.match(r'^---\n(.*?)\n---\n', content, re.DOTALL)
if front_matter:
content = content[front_matter.end():]
title = re.search(r'^title:\s*(.+)$', front_matter.group(1), re.MULTILINE)
if title:
page_title = title.group(1).strip().strip('"\'')
url = base_url + page.relative_to(docs_dir).with_suffix('.html').as_posix()
# Split the page on level 2 headings, outside code blocks
sections, heading, lines, in_code = [], None, [], False
for line in content.splitlines():
if line.lstrip().startswith('```'):
in_code = not in_code
if not in_code and line.startswith('## '):
sections.append((heading, lines))
heading, lines = line[3:].strip(), []
else:
lines.append(line)
sections.append((heading, lines))
for heading, lines in sections:
text = clean('\n'.join(lines))
if len(text) < 50:
continue
if heading:
label = re.sub(r'\s*\{#[\w-]+\}$', '', heading) # without the custom anchor
title, source = f'{page_title} - {label}', f'{url}#{slugify(heading)}'
else:
title, source = page_title, url
rows.append({'title': title, 'source': source, 'text': text})
output.parent.mkdir(parents=True, exist_ok=True)
with output.open('w', encoding='utf-8', newline='') as f:
writer = csv.DictWriter(f, fieldnames=['title', 'source', 'text'], delimiter='|', quotechar='"')
writer.writeheader()
writer.writerows(rows)
print(f'{len(rows)} sections written to {output}')
Run it on the English documentation. The CSV file is written where the indexing tool expects it:
<base folder>/<namespace>-<application name>/input/.
python3 docs_to_csv.py ../tock/docs/docs/en ingestion/app-new_assistant/input/tock-doc.csv
405 sections written to ingestion/app-new_assistant/input/tock-doc.csv
Index the documentation
Create the configuration file of the indexing tool, ingestion/config.json:
{
"bot": {
"namespace": "app",
"bot_id": "new_assistant",
"file_location": "/ingestion"
},
"em_setting": {
"provider": "Ollama",
"model": "mxbai-embed-large",
"base_url": "http://host.docker.internal:11434"
},
"vector_store_setting": {
"provider": "PGVector",
"host": "pgvector_postgres",
"port": 5432,
"username": "postgres",
"password": {
"type": "Raw",
"secret": "ChangeMe"
},
"database": "postgres"
},
"data_csv_file": "tock-doc.csv",
"document_index_name": null,
"chunk_size": 1000,
"embedding_bulk_size": 20,
"embedding_max_chunks": null,
"ignore_source": false,
"append_doc_title_and_chunk": true
}
bot: the namespace and the application name of the bot.file_locationis the folder containing the CSV files, as seen from the container.em_setting: the embedding model. The same model must be selected in the RAG settings of the bot.chunk_size: the maximum size of a chunk, in characters.mxbai-embed-largereads at most 512 tokens: larger chunks make the indexing fail with "the input length exceeds the context length".vector_store_setting: the PostgreSQL database started by Docker Compose.document_index_name: leave itnull, so that the index is named after the namespace, the bot and the indexing session, as Tock Studio expects.append_doc_title_and_chunk: prepends the title (page and section) to each chunk, which improves the search.
Run the indexing tool, available as the tock/llm-indexing-tools Docker image, on the network of the platform.
The -v option displays the progress and the summary of the indexing:
docker run --rm \
--network tock-docker_default \
--add-host host.docker.internal:host-gateway \
-v "$PWD/ingestion:/ingestion" \
tock/llm-indexing-tools:26.9.0 \
python tock-llm-indexing-tools/scripts/indexing/vectorisation/run_vectorisation.py \
--json-config-file=/ingestion/config.json -v
The network name is
<folder of the Docker Compose file>_default:tock-docker_defaultif you cloned the repository with its default name.docker network lslists the networks.
Embedding the whole documentation takes less than a minute with a GPU or an Apple Silicon Mac, and a few minutes on a CPU. At the end, the tool displays a summary:
------------------------------ RUN VECTORISATION OUTPUT ------------------------------
Index name : ns_app_bot_new_assistant_session_7fab9630_3a85_403d_ad43_233ec15fd7e7
Index session ID : 7fab9630-3a85-403d-ad43-233ec15fd7e7
Documents extracted : 405 (Docs)
Documents chunked : 737 (Chunks)
Duration : 44.01 seconds
...
Status : COMPLETED
Write down the Index session ID: it tells the bot which documents to use.
Configure the RAG
In Tock Studio, go to Gen AI > Rag settings:
- Question condensing and Question answering, Configuration:
- Provider: Ollama
- BaseUrl:
http://host.docker.internal:11434 - Model:
qwen2.5-16k - Temperature:
0(a small local model follows the expected answer format more reliably)
- Embedding, Configuration:
- Provider: Ollama
- BaseUrl:
http://host.docker.internal:11434 - Model:
mxbai-embed-large
- Indexing session:
- Indexing session id: the Index session ID displayed by the indexing tool
- Search type: Hybrid search (combines the vector search with a keyword search)
- Enable Rag activated and Dialogs debug
- Save
Once saved, reload the page: the Vector database index name field shows the name of the index created by the indexing tool.
There is no need to configure the vector store in Gen AI > Vector DB settings: by default, the orchestrator uses the PostgreSQL database of the platform (set by environment variables in the Docker Compose file).
Test the bot
Go to Test > Test and ask a question about Tock, for instance:
- Which vector stores are supported?
- How do I deploy Tock with Docker?
- How can I connect my bot to WhatsApp?
The bot has no intent yet: every question is qualified as unknown and handled by the RAG. The answer comes with the links to the documentation sections it is based on, for instance:
The vector stores supported by Tock include PGVector (PostgreSQL) and OpenSearch.
Sources: Vector store providers
The bot can also be called from outside Tock Studio, with the web connector created by the wizard:
curl -X POST http://localhost:8080/io/app/new_assistant/web \
-H 'Content-Type: application/json' \
-d '{"query": "Which vector stores are supported?", "userId": "my-user", "locale": "en"}'
With Dialogs debug enabled, Analytics > Dialogs shows, for each answer, the condensed question and the documentation chunks sent to the LLM.
Running on a CPU
Without a GPU, an answer of the 7B model takes two to three minutes, whereas the platform gives up after one minute
(the bot then answers "Technical error" or "technical error :( timeout"). Extend the timeouts with a
docker-compose-slow-llm.yml file, in the tock-docker folder:
services:
bot_api:
environment:
# Timeout of the calls to the Gen AI orchestrator
- tock_gen_ai_orchestrator_client_request_timeout_ms=600000
# Delay before the bot closes the answer channel
- tock_cleanup_delay_seconds=600
admin_web:
environment:
# Timeout of the Test screen of Tock Studio
- tock_bot_rest_client_request_timeout_ms=600000
Then restart the platform with both files:
docker compose -f docker-compose-rag-pgvector.yml -f docker-compose-slow-llm.yml up -d
Smaller models (3B) are faster, but they do not follow the JSON answer format required by the RAG prompt reliably.
Using OpenAI instead of Ollama
To use OpenAI, choose the OpenAI provider with your API key in the RAG settings, for instance with the gpt-4o-mini LLM
and the text-embedding-3-small embedding model (which accepts larger chunks, for instance "chunk_size": 2000), and replace the em_setting of the indexing configuration with:
"em_setting": {
"provider": "OpenAI",
"api_key": {
"type": "Raw",
"secret": "sk-..."
},
"model": "text-embedding-3-small",
"base_url": "https://api.openai.com/v1"
}
The documentation must be indexed again with this model, and the new indexing session ID entered in the RAG settings: the embedding model of the RAG settings must always be the one used for the indexing.
Going further
- Handle the journeys that must not be left to the LLM with stories: Create your first bot with Tock Studio.
- Improve the answers with the RAG prompt and the Rag prompt context (covered and excluded topics, business lexicon).
- Understand why a documentation section is (or is not) used with the retrieval diagnostic.
- Measure the quality of the answers with datasets and evaluations.
- Trace the LLM calls with an observability provider.
- Embed the bot in a web page with the Web connector.