Documentation chatbot
Answer questions about your own product docs, with citations back to the page.
Answer questions about your own product documentation, with footnotes that link back to the page and position the answer came from.
Architecture
Same shape as the help centre — one shared workspace, proxied through your server — but it uses response.parts instead of citations, because a docs answer reads better with per-sentence footnotes than a list at the bottom.
- Upload each docs page as its own resource, titled with its slug.
- Ask through your own route, passing a visitor id you already have.
- Turn each part's source back into a URL using the title and metadata.
Setup
Keep your own slug in the resource title. It is the only field that survives into a citation, so it is what lets you rebuild a link without a second lookup.
The code
Note the filter: parts that carry no source are ordinary prose the model wrote to connect the cited spans, and they should not become footnotes.
const BASE = "https://api.daneshyar.info/api/v1";
const KEY = process.env.DANESHYAR_API_KEY;
const WORKSPACE = "8f14e45f-ceea-467a-9f4c-1a2b3c4d5e6f";
/**
* Answer questions about your own docs, and link each source back to the page
* it came from. The trick is keeping your own slug in the resource title, so a
* citation can be turned back into a URL without a second lookup.
*/
export async function answerDocsQuestion({ visitorId, question }) {
const res = await fetch(`${BASE}/workspaces/${WORKSPACE}/chat/`, {
method: "POST",
headers: { "X-API-Key": KEY, "Content-Type": "application/json" },
body: JSON.stringify({
external_user_id: visitorId,
message: question,
}),
});
const { data } = await res.json();
const reply = data.assistant_message;
// response.parts attaches each source to the span it supports — use it when
// you want footnotes rather than a flat list under the answer.
const footnotes = reply.response.parts
.filter((part) => part.source)
.map((part) => ({
text: part.text,
title: part.source.resource_title,
// metadata carries the position: page for a PDF, start_time for audio.
page: part.source.metadata?.page ?? null,
href: `/docs/${slugFromTitle(part.source.resource_title)}`,
}));
return { answer: reply.content, footnotes };
}
function slugFromTitle(title) {
return title.replace(/\.[^.]+$/, "").toLowerCase().replace(/\s+/g, "-");
}What you get
Readers get an answer where each claim carries a link to the exact page and position behind it — which is the difference between a chatbot people trust and one they double-check elsewhere.