AACSearch / guide
Assistants (RAG)
Create an assistant that answers from your collections, ask it from a server or a browser, stream the answer, continue conversations and keep the cost in check.
An assistant answers questions from one or more of your collections. On every turn it retrieves matching documents, hands them to the model and returns the answer together with the documents it was based on. It is retrieval-augmented generation (RAG) you configure rather than build.
From your app, or the widget
Search the assistant's collections
Reads the documents, writes an answer
Every claim cites a document
Because the answer is grounded in retrieved documents, it is only as good as what retrieval found — the returned sources are the first thing to check when an answer looks wrong.
Creating and changing assistants needs a full key. Asking one also works with a search key from a browser (see From a browser). For query understanding inside a normal search request instead of a chat — parsing plain language into filters, or a refinement loop with your own model — see natural-language search and conversation models.
Create an assistant
createAssistant — POST /assistants:
const assistant = await client.assistants.createAssistant({
name: "Store help",
system_prompt: "You help shoppers choose tools. Answer only from the sources.",
collections: [
{
name: "products",
query_by: "name,description",
filter_by: "in_stock:true",
k: 5,
include_fields: "id,name,price,url",
},
],
max_sources: 6,
retrieval_mode: "single",
});
// assistant.id — use it for every chat call| Field | Meaning |
|---|---|
name | required |
collections[] | required, at least one: name, query_by (add an auto-embedding field for hybrid retrieval), filter_by, k (documents from this collection, default 5, max 20), include_fields (what the model sees and sources return), exclude_fields (vectors, internal ids) |
system_prompt | instructions prepended to every conversation |
max_sources | upper bound on documents per answer across collections — default 5, 1–40 |
retrieval_mode | single (default): rewrite the question once and search. agentic: may run one extra keyword search when the first sources look incomplete — higher latency and credit use |
memory_ttl_seconds | how long a conversation can be continued — default 86400, minimum 60 |
model | provider (workers_ai), name (a model the platform's catalog offers for chat; default @cf/meta/llama-3.3-70b-instruct-fp8-fast), temperature (0–2), max_output_tokens (up to 4096) |
listAssistants, getAssistant and deleteAssistant do what their names say.
updateAssistant (PUT /assistants/{assistantId}) takes the same body as
create, so send the whole configuration.
Ask
createChatCompletion — POST /assistants/{assistantId}/chat/completions:
curl -X POST "https://api.aacsearch.com/v1/assistants/$ASSISTANT_ID/chat/completions" \
-H "X-NOVA-API-KEY: $NOVA_API_KEY" -H "Content-Type: application/json" \
-d '{"messages":[{"role":"user","content":"Which drill is best for concrete?"}]}'The request:
| Field | Meaning |
|---|---|
messages | required — the new messages for this turn (role: system, user or assistant; content) |
conversation_id | continue an existing conversation; earlier turns are loaded from it |
filter_by | extra filter for this turn's retrieval, combined with each collection's own — e.g. the category the shopper is browsing |
max_sources, retrieval_mode | override the assistant's settings for this turn |
user_id | your id of the end user, for analytics |
The answer:
{
"id": "…",
"conversation_id": "…",
"message": { "role": "assistant", "content": "For concrete, pick a hammer drill [1] …" },
"sources": [
{ "collection": "products", "document": { "id": "42", "name": "Hammer drill, 18V" }, "score": 0.91, "snippet": "…" }
],
"usage": { "input_tokens": 1830, "output_tokens": 164 },
"model": "@cf/meta/llama-3.3-70b-instruct-fp8-fast"
}sources are in the order the model saw them, so [1], [2] in the answer
point at sources[0], sources[1]. Render them as links: they are what makes
an answer checkable.
Stream the answer
streamChatCompletion — POST /assistants/{assistantId}/chat/completions/stream
— takes the same body and answers with Server-Sent Events. Each event is a
ChatChunk with a type:
type | Carries |
|---|---|
sources | the documents, plus conversation_id — render them before the text arrives |
delta | the next fragment of the answer |
usage | input_tokens, output_tokens |
done | conversation_id |
error | message |
const stream = await client.assistants.streamChatCompletion({
assistantId: assistant.id,
body: { messages: [{ role: "user", content: question }] },
});
let conversationId: string | undefined;
for await (const chunk of stream) {
if (chunk.type === "delta") process.stdout.write(chunk.delta ?? "");
if (chunk.type === "done") conversationId = chunk.conversation_id;
}Conversations
Send the conversation_id from the previous answer (or the done chunk) with
the next turn, and only the new message — the assistant loads the history
itself. A conversation can be continued for memory_ttl_seconds after it was
last used.
getConversation—GET /assistants/{assistantId}/conversations/{conversationId}returns the history.deleteConversation— the same path withDELETE, for example when the shopper clicks "start over".
From a browser
createChatCompletion, streamChatCompletion, getConversation and
deleteConversation are search-scoped, so a storefront calls them directly with
a search key or a short-lived browser key minted by your server (see
the widget's keys). With a search key:
X-NOVA-SESSION-IDis required (400without it) — one id per browser tab.- A conversation belongs to the session that started it; another session gets
403. - AI calls are limited per visitor session; past the allowance the API answers
429, withRetry-Afterwhen it knows how long to wait.
The aacsearch-ui support chat and the light widget send the session header for
you.
Cost
Every turn is paid in credits from the organization's wallet by tokens in and
out, whichever key made the request (see
credits). When the wallet cannot cover
a turn the API answers 402 with message, available and requested. To
spend less per turn, lower max_sources or model.max_output_tokens, trim
include_fields to what the answer needs, and keep retrieval_mode: "single".
Programmatic SEO pages
Turn a facet into indexable landing pages with templated metatags, generate them from the live index, audit what was generated and export a sitemap.
Natural-language search and conversation models
Register the models that power nl_query (parsing plain language into filters) and conversation (multi-turn memory on top of a search) — the engine's own query understanding, not the assistants RAG.