Journey. As a developer, I want my users' questions answered from our own
documents — policies, runbooks, FAQs — so the AI cites our material instead of
guessing.
The RAG Service is the platform's retrieval layer. It works in two modes: update mode (add and remove documents in a collection) and query mode (ask a natural-language question, get back the most relevant chunks with citations). This guide walks the full loop — upload, list, search, delete — and then wires the collection into an Evergreen AI chat session so replies are grounded in your material. Verified against the local stack (RAG Service :40100).
Prerequisites
- The RAG Service is reachable (local stack
:40100, or the shared dev
subdomain).
- Your tenant key pair in
INAYA_API_KEY/INAYA_APP_KEY, and a user JWT in
TOKEN (obtain one via guide G-01; the full example below fetches one via the QA sign-in shortcut). On the local stack the service has no auth middleware of its own — the header trio is enforced by the platform gateway in deployed environments, and sending it everywhere keeps your code environment-agnostic.
curl, or any HTTP client. Snippets also show Python and Node.- A document to play with.
.txtis the simplest;.pdf,.docx,.pptx,
and .xlsx are also accepted.
Concepts (only what this journey needs)
- A collection is a named bucket of documents. Every operation is scoped by
collection_id — one tenant typically uses one collection per product, project, or knowledge base. Collections come into existence when you first upload into them; there is no separate "create collection" call.
- Update mode = three calls.
POST /v3/documents(multipart upload) or
POST /v3/ingest (raw bytes) adds a document; GET /v3/documents lists a collection; DELETE /v3/documents/{id} removes one.
- Query mode = one call.
POST /v3/searchtakes aquestionand returns
chunks (the best-matching passages, with scores and page/section metadata), citations (file + page + section to display alongside an answer), and glossary_terms (domain-term definitions to inject into a prompt).
- Document ids are derived, not chosen. Uploading
runbook.txtinto
collection onboarding_docs produces the id onboarding_docs-runbook.txt.
NOTE
The store is in-memory: ingested documents do not survive a service restart. Treat ingestion as part of your startup/bootstrap flow, and check the roadmap before relying on it as a system of record.
NOTE
Two response fields are contractually stable but not yet populated in this build: glossary_terms (glossary extraction is a stub, so it is always [] today) and vector-similarity ranking (the embedding engine is a placeholder, so ranking is effectively keyword/BM25). The include_glossary flag and max_rag_context_tokens budget are accepted and forward-compatible. Keyword-style questions already rank well.
Step 1 — Upload a document (update mode)
Multipart upload takes the file plus the target collection:
curl -s -X POST "${BASE_URL}/v3/documents" \
-H "Authorization: Bearer ${TOKEN}" \
-H "x-api-key: ${INAYA_API_KEY}" \
-H "x-client-app-key: ${INAYA_APP_KEY}" \
-F "file=@runbook.txt" \
-F "collection_id=${COLLECTION}"import json, os, urllib.request
boundary = "----g11boundary"
body = (
f"--{boundary}\r\n"
'Content-Disposition: form-data; name="file"; filename="runbook.txt"\r\n'
"Content-Type: text/plain\r\n\r\n"
"Acme Platform Onboarding Runbook\n\n"
"Section 1: Tenant provisioning\n"
"Rotate the apikey every ninety days; rotation never interrupts traffic.\r\n"
f"--{boundary}\r\n"
'Content-Disposition: form-data; name="collection_id"\r\n\r\n'
f"{os.environ['COLLECTION']}\r\n"
f"--{boundary}--\r\n"
).encode()
req = urllib.request.Request(
f"{os.environ['BASE_URL']}/v3/documents",
data=body,
headers={
"Content-Type": f"multipart/form-data; boundary={boundary}",
"Authorization": f"Bearer {os.environ['TOKEN']}",
"x-api-key": os.environ["INAYA_API_KEY"],
"x-client-app-key": os.environ["INAYA_APP_KEY"],
},
)
print(json.dumps(json.load(urllib.request.urlopen(req, timeout=60)), indent=2))import { readFileSync } from "node:fs";
const form = new FormData();
form.set("file", new Blob([readFileSync("runbook.txt")]), "runbook.txt");
form.set("collection_id", process.env.COLLECTION);
const res = await fetch(`${process.env.BASE_URL}/v3/documents`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TOKEN}`,
"x-api-key": process.env.INAYA_API_KEY,
"x-client-app-key": process.env.INAYA_APP_KEY,
},
body: form,
});
console.log(await res.json());Expected response — 200 with the derived document id and the chunk count produced by ingestion:
{ "id": "onboarding_docs-runbook.txt", "collection_id": "onboarding_docs", "file_name": "runbook.txt", "chunk_count": 1 }TIP
Automations and pipelines without file handling can use the raw-bytes variant instead: POST /v3/ingest?collection_id=… with header X-File-Name: runbook.txt and the file as the application/octet-stream body. It returns { "document_id": "onboarding_docs-runbook.txt" } — the same id under a different field name.
PITFALL
Do not put hyphens in collection ids (or file names). The id is built as {collection_id}-{file_name} and the service parses the collection back out of the id on delete — with a hyphenated collection id, DELETE returns 200 but silently removes nothing. Use snake_case/CamelCase ids, and confirm removals with a list call (Step 3).
Step 2 — Search the collection (query mode)
Ask a question; get back ranked chunks ready to paste into an LLM prompt (or to feed straight to an Evergreen AI session — see the capstone below):
curl -s -X POST "${BASE_URL}/v3/search" \
-H "Authorization: Bearer ${TOKEN}" \
-H "x-api-key: ${INAYA_API_KEY}" \
-H "x-client-app-key: ${INAYA_APP_KEY}" \
-H "Content-Type: application/json" \
-d '{
"collection_id": "'"${COLLECTION}"'",
"question": "how often should the apikey be rotated?",
"include_glossary": true,
"max_rag_context_tokens": 2000
}'import json, os, urllib.request
req = urllib.request.Request(
f"{os.environ['BASE_URL']}/v3/search",
data=json.dumps({
"collection_id": os.environ["COLLECTION"],
"question": "how often should the apikey be rotated?",
"include_glossary": True,
"max_rag_context_tokens": 2000,
}).encode(),
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {os.environ['TOKEN']}",
"x-api-key": os.environ["INAYA_API_KEY"],
"x-client-app-key": os.environ["INAYA_APP_KEY"],
},
)
print(json.dumps(json.load(urllib.request.urlopen(req, timeout=60)), indent=2))Expected response — 200; chunks ranked by score, plus citations and the (currently empty) glossary list:
{
"chunks": [
{ "chunk_id": "onboarding_docs:onboarding_docs-runbook.txt:0", "document_id": "onboarding_docs-runbook.txt", "file_name": "runbook.txt", "page_number": 1, "section_hierarchy": "Root", "content": "[Context Section: Root]\nAcme Platform Onboarding Runbook\n…", "score": 1.33 }
],
"citations": [ { "file_name": "runbook.txt", "page_number": 1, "section": "Root" } ],
"glossary_terms": []
}NOTE
question and collection_id are both required — either one empty gets a 400. An unknown collection is not an error: you get 200 with empty chunks, which is how "nothing ingested yet" surfaces. (Also note the chunk_count field in list responses is always 0 in this build — rely on the upload response and actual search hits instead.)
Step 3 — List, then remove a document (update mode)
List everything in the collection:
curl -s "${BASE_URL}/v3/documents?collection_id=${COLLECTION}" \
-H "Authorization: Bearer ${TOKEN}" \
-H "x-api-key: ${INAYA_API_KEY}" \
-H "x-client-app-key: ${INAYA_APP_KEY}"Expected response — 200 with the collection's contents:
{ "documents": [ { "id": "onboarding_docs-runbook.txt", "collection_id": "onboarding_docs", "file_name": "runbook.txt", "chunk_count": 0 } ], "total": 1 }Delete by the derived id:
curl -s -o /dev/null -w "HTTP %{http_code}\n" -X DELETE \
"${BASE_URL}/v3/documents/${COLLECTION}-runbook.txt" \
-H "Authorization: Bearer ${TOKEN}" \
-H "x-api-key: ${INAYA_API_KEY}" \
-H "x-client-app-key: ${INAYA_APP_KEY}"Expected response — 200, and a follow-up list no longer includes the document.
PITFALL
DELETE currently returns 200 even when the document id does not exist — it is not an existence check. If you need to know a removal actually happened, list before and after.
Full working example
A complete update → query → cleanup cycle (rag_tour.sh) — it fetches a user JWT via the QA shortcut first (local/QA environments only; production uses the G-01 flows):
#!/usr/bin/env bash
set -euo pipefail
# 0. Obtain a user JWT (QA shortcut — see G-01 for production flows)
TOKEN=$(curl -sf -X POST "${AUTH_URL}/v3/auth/qa/login" \
-H "Content-Type: application/json" \
-H "x-api-key: ${INAYA_API_KEY}" \
-H "x-client-app-key: ${INAYA_APP_KEY}" \
-d "{\"device_id\": \"rag-demo-device\", \"locale\": \"en-US\", \"tenant_id\": \"${TENANT_ID}\", \"username\": \"rag-demo\"}" \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["token"])')
AUTH=(-H "Authorization: Bearer ${TOKEN}"
-H "x-api-key: ${INAYA_API_KEY}"
-H "x-client-app-key: ${INAYA_APP_KEY}")
# 1. Upload (raw-bytes variant)
DOC_ID=$(curl -sf -X POST "${BASE_URL}/v3/ingest?collection_id=${COLLECTION}" \
"${AUTH[@]}" -H "X-File-Name: runbook.txt" -H "Content-Type: application/octet-stream" \
--data-binary @runbook.txt \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["document_id"])')
echo "ingested: ${DOC_ID}"
# 2. Query
curl -sf -X POST "${BASE_URL}/v3/search" "${AUTH[@]}" \
-H "Content-Type: application/json" \
-d "{\"collection_id\": \"${COLLECTION}\", \"question\": \"how often should the apikey be rotated?\"}" \
| python3 -c 'import json,sys; d=json.load(sys.stdin); c=d["chunks"][0]; print("top hit:", c["file_name"], "score", round(c["score"],3), "| citations:", len(d["citations"]))'
# 3. List and clean up
curl -sf "${BASE_URL}/v3/documents?collection_id=${COLLECTION}" "${AUTH[@]}" \
| python3 -c 'import json,sys; print("docs:", json.load(sys.stdin)["total"])'
curl -sf -o /dev/null -w "delete: %{http_code}\n" -X DELETE "${BASE_URL}/v3/documents/${DOC_ID}" "${AUTH[@]}"(AUTH_URL is the Auth Service base URL — http://127.0.0.1:40004 on the local stack; BASE_URL here is the RAG Service — http://127.0.0.1:40100.)
Capstone — ground an Evergreen AI chat session
Retrieval becomes "grounding" when a chat session uses it automatically. Evergreen AI accepts a use_rag block at session creation; every message in that session is then answered against your collection (the service needs RAG_SERVICE_URL set — it is, on the local stack and in deployed environments):
curl -s -X POST "${EVG_URL}/v3/sessions" \
-H "Authorization: Bearer ${TOKEN}" \
-H "x-api-key: ${INAYA_API_KEY}" \
-H "x-client-app-key: ${INAYA_APP_KEY}" \
-H "Content-Type: application/json" \
-d '{
"required_modalities": ["Text"],
"selection_criteria": [ { "Model": { "optionality": "MustHave", "name": "Qwen3_VL_4B_Q4" } } ],
"temperature": 0.2,
"use_rag": { "collection_id": "'"${COLLECTION}"'", "include_glossary": true }
}'Chat messages on that session behave exactly as in guide G-10 — but the answer now comes from your documents. With a short refund-policy text ingested into the collection, asking "According to the refund policy, how long do I have for a full refund? One sentence." produced:
{ "author": "Assistant", "content": [{ "Text": "Full refunds are available within thirty days of purchase, no questions asked." }], "model_name": "Qwen3_VL_4B_Q4" }(First call in a session still pays the model-load cost — see G-10.)
Try it live
Open the Playground → Applied Intelligence → RAG — upload a file, search the collection, and delete it, running against a live service.
Copy agent prompt
You are working with the Inaya Labs RAG Service at ${BASE_URL} (local port
40100). The tenant key pair is in INAYA_API_KEY / INAYA_APP_KEY env vars;
obtain a user JWT via the QA login from guide G-01 if TOKEN is not already set.
Steps: (1) POST /v3/ingest?collection_id=onboarding_docs with header
X-File-Name: runbook.txt and the raw text bytes of the provided document;
report the document_id. (2) POST /v3/search with collection_id
onboarding_docs and question "how often should the apikey be rotated?";
report the top chunk's file_name and score. (3) GET /v3/documents?collection_id=
onboarding_docs and report the total. (4) DELETE /v3/documents/<document_id>
and confirm the collection is empty. Never use hyphens in collection ids.
Never log the JWT.The same four operations are also exposed as authenticated MCP tools (ingest_document, list_documents, delete_document, search_documents) for agentic access — see guide G-02 for the discovery and calling mechanics.
API reference
Next journeys
- Detecting objects and poses in images (G-12) — the platform's vision API: upload an image, get back labeled boxes and keypoints.
- Choosing Evergreen vs Bella vs Chloe (G-13) — which of the platform's three AI surfaces fits your use case.
- Your first AI chat session (G-10) — the session mechanics this guide's capstone builds on.