Chatbot
A deployable, embeddable "chat with your documents" surface.
What it is
The Chatbot turns the Knowledge Base into a deployable, embeddable "chat with your documents" experience. It is composed entirely from existing workflow blocks — ingestion (documents and URLs) plus a RAG query path — so customers customise it by editing the workflow. It ships an authenticated chat page, a sidebar chat, multi-tenant chatbot management, and an embeddable widget that runs on any external website.
When to use it
Use it to expose a knowledge base to end users — internal staff (authenticated /chat) or
external website visitors (embeddable widget with a per-chatbot API key). For backend
programmatic Q&A from your own services, call the public chat API directly. Each chatbot
queries its owner's documents (optionally scoped to one collection), so one tenant's
content never leaks into another's answers.
How it works
Process flow — the three composing workflow templates (chatbot-ingestion,
chatbot-url-ingestion, chatbot-query):
Call flow — a public widget query and the checks DMS runs before it ever reaches RAG:
Sub-features
- Authenticated chat (
/chat) — three-panel page: sessions, message thread, sources. - Sidebar chat — floating button on every authenticated page (
ChatWindow). - Chatbot management — named chatbots, per-chatbot API key (
cb_prefix), branding, welcome message, allowed origins, rate limit, active toggle. - Embeddable widget — Shadow DOM isolation,
localStoragesessions, source-citation badges, "Powered by TurfAI" footer. - KB Assembly view — table of all sources (documents + URLs) with status and reindex.
- Settings (5 tabs) — General, Branding, Security, Embed, Knowledge Base.
- Auto-create — instantiating a chatbot template creates a Chatbot record + endpoint.
Configuration parameters
Reconciled against dms/src/api/chatbot/content-types/chatbot/schema.json.
| Parameter | Type | Default | Description |
|---|---|---|---|
name | string (required) | — | Display name |
slug | string (unique, required) | — | URL-safe id used in the embed code + public API. Lowercase alphanumeric + hyphens |
active | boolean | true | Enable / disable; an inactive chatbot is invisible to config and chat |
welcome_message | text | "Hi! How can I help you today?" | Shown when the widget opens |
branding | JSON | { "primary_color": "#2563eb" } | Holds primary_color and optional logo_url (there are no standalone primary_color / logo_url columns) |
allowed_origins | JSON array | [] (empty = all origins) | CORS allowlist of domains permitted to embed the widget |
rate_limit | integer | 60 (min 1, max 1000) | Requests per minute, per chatbot |
query_count | integer | 0 | Total queries processed (incremented per successful chat) |
last_query_at | datetime | — | Timestamp of the most recent query |
api_key_rotated_at | datetime | set on create | Last key rotation; NULL on legacy rows surfaces as "never rotated" |
transcripts_enabled | boolean | true | When true, each query+answer is persisted for owner review; set false for GDPR-sensitive deployments |
collection | relation → Collection | — | Optional scope: queries only search this collection's indexed documents |
owner | relation → User | (caller) | Whose documents are queried and who controls settings |
The API key is never stored. Only api_key_hash (SHA-256, marked private) lives on the
row. The plaintext cb_… key is returned once — at create and at regenerate-key — in the
response body and shown in a one-time-reveal dialog. There is no api_key column, no
rate_limit_per_minute, no knowledge_base_url, and no last_queried_at; scoping is done via
the collection relation, and the rate field is rate_limit / the timestamp is last_query_at.
Tips & best practices
- Knowledge-base hygiene: keep one chatbot pointed at one
collectionso unrelated documents can't leak into answers. Confirm sources areIndexedin KB Assembly before going live — a chatbot with a collection but no indexed docs returns "No indexed documents found…" by design rather than hallucinating. - Branding: set
branding.primary_colorto your brand colour andbranding.logo_urlto a hosted logo; both are served by the publicconfigendpoint and applied by the widget before the first message. - CORS /
allowed_origins: lock the widget to the exact domains that should embed it (e.g.["https://www.acme.com"]). Leave it empty only for local testing — an empty array allows every origin. - Rate limiting: the default
60/min is per chatbot, enforced in-memory. Raise it for high-traffic sites (max1000), but keep it tight on public widgets to blunt scraping and abuse. Clients should honour theretryAfter: 60hint on a429. - Key rotation: rotate via regenerate-key on a schedule and whenever a key may have leaked — rotation invalidates the old key immediately and stamps
api_key_rotated_at. A "never rotated" badge (NULL timestamp) is your cue to rotate.
Concrete examples
1. Embed widget — declarative
<script src="https://app.turfai.com/widget.js"
data-chatbot="my-kb"
data-api="https://api.turfai.com"></script>Programmatic init (when you need to mount it yourself):
<script src="https://app.turfai.com/widget.js"></script>
<script>
window.TurfAIChat.init({
chatbotSlug: 'my-kb',
apiUrl: 'https://api.turfai.com'
});
</script>The widget mounts a floating button (bottom-right), opens a Shadow-DOM-isolated chat overlay,
pulls branding from GET /api/chatbots/my-kb/config, and persists the session in
localStorage keyed by slug.
2. Public chat request + response with sources
POST /api/chatbots/my-kb/chat
Headers: { "X-Chatbot-Key": "cb_abc123...", "Content-Type": "application/json" }
{ "query": "What is the remote work policy?", "session_id": "optional-for-multi-turn" }{
"answer": "Based on the company handbook, remote work is allowed up to 3 days a week [1].",
"sources": [
{
"document_id": 42,
"document_title": "Company Handbook 2025",
"page_number": 15,
"similarity_score": 0.92,
"signed_url": "https://storage.googleapis.com/...?X-Goog-Expires=3600...",
"file_url": "gs://bucket/documents/handbook.pdf"
}
],
"confidence": 0.82,
"session_id": "sess_abc123"
}Reuse the returned session_id on the next call to keep multi-turn context.
3. The three composing workflows (seed templates, all built from existing task types):
| Template | Flow | Task types |
|---|---|---|
chatbot-ingestion | Webhook → Decision (OCR?) → OCR → RAG Enable → Email | webhook_trigger, decision_task, ocr, rag_enable_task, email_send_task |
chatbot-url-ingestion | Webhook → Scrape → LLM cleanup → RAG Enable → Email | webhook_trigger, metadata_scraping, llm_task, rag_enable_task, email_send_task |
chatbot-query | Webhook → RAG Query → LLM format → REST response | webhook_trigger, rag_query_task, llm_task, rest_api_task |
To customise behaviour (classification routing, email escalation, multilingual support), edit these workflows rather than the chatbot record.
Limitations
- The public chat path is RAG-only; Data Shield does not tokenise it in v0.5 — do not expect PII masking on public chat answers.
- Queries run as the chatbot owner's context — the owner's documents (optionally one collection) are searched, never the visitor's.
- Rate limiting is in-memory per DMS instance, so limits are approximate behind multiple replicas.
- A chatbot whose
api_key_hashisNULL(legacy row) cannot authenticate — the operator must regenerate the key.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Widget chat fails / browser blocks the request | Host origin not in allowed_origins | Add the exact origin (scheme + host) to allowed_origins, or empty the array for local testing |
429 Rate limit exceeded (retryAfter: 60) | More than rate_limit requests/min for this chatbot | Back off for 60s; raise rate_limit (max 1000) if legitimately high-traffic |
| Answer is "No indexed documents found…" | Assigned collection has no docs with rag_enabled + completed status | Index docs (KB Assembly → reindex) and confirm status is Indexed before querying |
401 Invalid or missing API key | Wrong / rotated key, or api_key_hash is NULL on a legacy row | Send the current cb_… key; if the row was never rotated, regenerate the key |
404 Chatbot not found or inactive | Wrong slug, or active: false | Check the slug; toggle active: true in settings |
| Widget never appears | widget.js not loaded, or data-chatbot slug missing/typo | Verify the script URL resolves; confirm data-chatbot matches an active slug; check the browser console |
How to test
End-to-end happy path:
- Create a chatbot —
POST /api/chatbotswithname+slug(or instantiate a chatbot template, which auto-creates the record). Capture the one-timecb_…key from the response. - Index a document — ingest a PDF via the ingestion webhook; poll until it shows
Indexedin KB Assembly (chunk_count > 0). - Public chat via API —
curl -H "X-Chatbot-Key: <key>" -H "Content-Type: application/json" -d '{"query":"<a question answered by the doc>"}' /api/chatbots/<slug>/chat; confirm a non-emptyanswerand asources[]entry with the rightdocument_title,page_number,similarity_score, and a workingsigned_url. - Embed the widget — drop the
<script src=".../widget.js" data-chatbot="<slug>" data-api="...">snippet on a test HTML page served from an allowed origin; confirm the floating button opens the chat. - Verify branding + sources — the widget header shows your
primary_color/logo_urlandwelcome_message(from the publicconfigendpoint), and assistant messages render source-citation badges. - Negative checks — open the widget from an origin not in
allowed_origins(request blocked); fire more thanrate_limitrequests in a minute (429withretryAfter).
Cross-links & roadmap
- Knowledge Base / RAG — the indexing + query layer this surface is built on.
- RAG concept — chunking, embeddings, and retrieval explained.
- Data Shield — note: public chat is NOT tokenised by Data Shield in v0.5.
- Coming soon: Data Shield tokenisation of the chat path; transcript review UI (
transcripts_enabled).
Dependencies
- DMS chatbot controller/service — CRUD, public chat, config, rate limiting, key hashing.
- RAG query service — answers and citations (called with the owner's JWT).
- Ingestion workflows — OCR, web scrape, LLM cleanup, RAG Enable, email (see Node catalog).
- GCS — source documents and 1-hour signed URLs on citations.
APIs used
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET / POST | /api/chatbots | JWT | List / create chatbots (create returns the one-time api_key) |
GET / PUT / DELETE | /api/chatbots/:id | JWT | Get / update / delete a chatbot |
POST | /api/chatbots/:id/regenerate-key | JWT | Rotate API key (returns new one-time api_key) |
GET | /api/chatbots/:slug/config | none | Public branding / welcome message |
POST | /api/chatbots/:slug/chat | X-Chatbot-Key | Public chat query |
OPTIONS | /api/chatbots/:slug/chat | none | CORS preflight |
See the synced feature reference for the full chatbot surface.