Documentation menu

Guide

Knowledge

How a knowledge base is built from Q&A entries, how retrieval works, and how to grow and curate a base well — by hand, by import, or by AI extraction.

The data model

A knowledge base holds entries. Each entry is one fact expressed as a question and a self-contained answer, filed under a short subject. Entries are what retrieval returns and what a flow's model reads — never the original document — so an answer must stand on its own.

An entry
{
  "id": "…", "subject": "Warranty",
  "question": "What is the warranty on the X200?",
  "altQuestions": ["X200 warranty length", "How long is the X200 covered?"],
  "answer": "Two years from the date of purchase, parts and labour.",
  "keywords": ["X200", "warranty"],
  "sourceExcerpt": "…the X200 carries a two-year warranty…", "page": 4,
  "status": "active", "conflictOfId": null, "validUntil": null,
  "hitCount": 3, "lastHitAt": "2026-08-29T07:30:00Z"
}
FieldRules
subject1–60 chars, Title Case, one or two words. Reuse what GET …/subjects returns rather than inventing near-duplicates. Part of the embedding, so changing it re-indexes.
questionOne clear question a customer would actually ask, ≤ 500 chars. Exact duplicates (case-insensitive) are rejected.
altQuestionsUp to 5 paraphrases — different wordings or languages. This is how you cover variants without duplicate entries.
answer≤ 2000 chars, complete. The model sees only matched entries.
keywordsUp to 10 product codes, names, numbers that keyword search should hit.
statusactive (searchable) · draft (hidden until approved) · archived (retired).
validUntilFor promotions and seasonal facts. Listed under status=expiring 30 days before.
hitCountHow often retrieval returned it. sort=unused finds dead weight.

A source is one piece of submitted material (an uploaded file or pasted text); entries point back to it with sourceId,page and sourceExcerpt. Deleting a source deletes its entries.

Retrieving: search vs ask

Retrieval is hybrid: vector similarity on the entry's embedding plus Postgres full-text on the words, fused into one ranking. Both endpoints take the same body — query, optional topK and minScore — and both count as read.

Free · no model call

POST …/search

Returns ranked entries with vectorScore (cosine, 0–1) and a fused score. Use it to iterate, to check for an existing entry before you add one, and whenever you want to compose the answer yourself.

Credits · one model answer

POST …/ask

Runs the search, then answers only from the hits (“I don't have that information.” when they don't cover it). This is exactly what a flow's Knowledge tool grounds on — the best way to test how the base behaves in production.

Reading the scores: vectorScore ≥ 0.6 is a strong match, 0.35–0.6 is related, below is noise (dropped unless full-text also matched). Every hit bumps the entry's hitCount; a query that clears nothing is logged under unanswered.

Growing a base

Three ways in, by what you have:

You have…UseCost
one factPOST …/entries — stored verbatim, indexed immediatelyembedding only
many ready pairsPOST …/import — up to 2000 rows, stored verbatimembeddings only
raw text or a documentPOST …/submissions — the AI reads it and extracts pairsextraction + embeddings

Ready-made Q&A does not go through submissions

Submissions run model extraction over your text: it costs credits and may reword or split your pairs. Entries and import store them exactly as sent.

AI extraction from text and files

Send clean text — headings and paragraphs, boilerplate stripped. 5–40 k characters per call gives the best pairs; up to 400 k is accepted and processed in parallel windows.

Seed a base from a policy text
curl -X POST https://api.simplynice.ai/api/ai/knowledge/bases/$KB_ID/submissions -H "Authorization: Bearer $DJC_TOKEN" -H "Content-Type: application/json" \
  -d '{
    "text": "## Returns policy\nItems can be returned within 30 days of delivery in original packaging. Refunds are issued to the original payment method within 5 working days of receipt. Sale items are exchange-only.\n\n## Shipping\nWest Malaysia: RM 8 flat, 2–4 working days. East Malaysia: RM 15, 4–7 working days. Free shipping above RM 150."
  }'
Response (returned when extraction finishes)
{
  "status": "success",
  "title": "Returns & Shipping Policy",
  "summary": "Return window, refund timing, sale-item rule and domestic shipping rates.",
  "entries": [
    { "id": "…", "subject": "Returns",  "question": "How long do I have to return an item?", "answer": "30 days from delivery, in original packaging.", "status": "active" },
    { "id": "…", "subject": "Shipping", "question": "How much is shipping to East Malaysia?", "answer": "RM 15, 4–7 working days; free above RM 150.", "status": "active" }
  ],
  "extraction": { "status": "done", "windows": 1, "pairsExtracted": 7, "pairsInserted": 7, "duplicates": 0, "conflicts": 0, "credits": 0.02 }
}

For a document, upload it first together with its text — the API stores the original but does not parse PDFs or Office files — then submit with the source attached. Mark page boundaries in the text with lines --- page N --- so entries get page numbers.

Upload a document, then extract with an instruction
# 1. upload the file together with its text (the API does not parse PDFs — send the text you extracted)
curl -X POST https://api.simplynice.ai/api/ai/knowledge/bases/$KB_ID/sources/upload -H "Authorization: Bearer $DJC_TOKEN" \
  -F "file=@datasheet.pdf" -F "content=<datasheet.txt" -F "pageCount=12"
# → 201 { "id": "b7e1…", "kind": "file", "title": "datasheet", … }

# 2. extract from it — "text" is now an instruction, not material
curl -X POST https://api.simplynice.ai/api/ai/knowledge/bases/$KB_ID/submissions -H "Authorization: Bearer $DJC_TOKEN" -H "Content-Type: application/json" \
  -d '{ "text": "Focus on specifications, prices and warranty. Skip marketing copy.",
        "attachments": [ { "sourceId": "b7e1…" } ] }'

If the base has review mode on (reviewBeforeIndex), extracted pairs land as drafts; approve the good ones withPOST …/entries/bulk { action: "approve" }. Add ?stream=1 to a submission to receive progress events as NDJSON instead of waiting for the whole response.

Importing ready pairs

curl -X POST https://api.simplynice.ai/api/ai/knowledge/bases/$KB_ID/import -H "Authorization: Bearer $DJC_TOKEN" -H "Content-Type: application/json" \
  -d '{ "rows": [
    { "subject": "Shipping", "question": "Do you ship to Singapore?", "answer": "Not yet — Malaysia only.", "altQuestions": ["International shipping"] },
    { "question": "What are your office hours?", "answer": "Mon–Fri 9am–6pm." }
  ] }'
# → { "inserted": 2, "duplicates": 0, "conflicts": 0 }
  • Exact-duplicate questions (against the base and within the batch, case-insensitive) are skipped and counted in duplicates.
  • Rows ≥ 92 % similar to an existing entry are stored as draft conflicts — see below.
  • Everything else lands active, even in review mode. Missing subject becomes General.

Curating

After extraction, read the returned entries and tidy: fix wording with PATCH, merge near-duplicates, and make subjects consistent with a bulk setSubject. Text edits re-embed automatically.

Duplicates and conflicts

Whenever a new entry is ≥ 0.92 cosine-similar to an existing one, it is stored as a draft with conflictOfId pointing at the older entry, and it stays out of search until you decide:

curl "https://api.simplynice.ai/api/ai/knowledge/bases/$KB_ID/entries?status=conflicts" -H "Authorization: Bearer $DJC_TOKEN"
# each item has conflictOfId → the older entry it resembles

curl -X PATCH https://api.simplynice.ai/api/ai/knowledge/bases/$KB_ID/entries/$NEW_ID -H "Authorization: Bearer $DJC_TOKEN" -H "Content-Type: application/json" -d '{ "resolveConflict": "replace" }'   # archive the old one, activate this
curl -X PATCH https://api.simplynice.ai/api/ai/knowledge/bases/$KB_ID/entries/$NEW_ID -H "Authorization: Bearer $DJC_TOKEN" -H "Content-Type: application/json" -d '{ "resolveConflict": "keep" }'      # both stay, flag cleared

Closing the gaps

GET …/unanswered lists real questions retrieval failed on — from flows, from the app's Test drawer and from this API. Write entries for them, then clear the list:

curl https://api.simplynice.ai/api/ai/knowledge/bases/$KB_ID/unanswered -H "Authorization: Bearer $DJC_TOKEN"
# [ { "query": "do you ship to singapore", "count": 4, "lastAt": "…" } ]

curl -X POST https://api.simplynice.ai/api/ai/knowledge/bases/$KB_ID/entries -H "Authorization: Bearer $DJC_TOKEN" -H "Content-Type: application/json" \
  -d '{ "subject": "Shipping", "question": "Do you ship to Singapore?", "answer": "Not yet — Malaysia only.", "altQuestions": ["International shipping"] }'

curl -X DELETE "https://api.simplynice.ai/api/ai/knowledge/bases/$KB_ID/unanswered?query=do%20you%20ship%20to%20singapore" -H "Authorization: Bearer $DJC_TOKEN"

Playbook

  1. Inspect first. GET /bases, then GET …/subjects and GET …/entries?limit=50. Know the size and taxonomy before changing anything.
  2. Search before you add. If a near-identical entry exists, PATCH it. Two answers to one question make retrieval worse.
  3. Bulk material → submissions; ready pairs → import. Never paste a whole document into one answer.
  4. One fact per entry; wordings go in altQuestions.
  5. Test like a flow does with ask; iterate for free with search.
  6. Keep description accurate — the model uses it to decide when to search this base.
  7. Don't invent subjects that exist under another spelling, archive or delete entries you haven't read, or change a description the user didn't ask for. Bases can only be deleted in the app.

Every endpoint, with parameters and samples: Knowledge reference.