Home Playground Docs hub

Guide G-11 · Applied Intelligence

Grounding answers in your documents

Verified against local · 2026-09-23 · rag-service (workspace HEAD), evergreen-ai-service 3.0.1 (workspace HEAD)
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

subdomain).

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.

and .xlsx are also accepted.

Concepts (only what this journey needs)

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.

POST /v3/ingest (raw bytes) adds a document; GET /v3/documents lists a collection; DELETE /v3/documents/{id} removes one.

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).

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:

bash
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}"
python
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))
javascript
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:

json
{ "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):

bash
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
  }'
python
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:

json
{
  "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:

bash
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:

json
{ "documents": [ { "id": "onboarding_docs-runbook.txt", "collection_id": "onboarding_docs", "file_name": "runbook.txt", "chunk_count": 0 } ], "total": 1 }

Delete by the derived id:

bash
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):

bash
#!/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):

bash
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:

json
{ "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

Agent prompt — copy and hand to your agent
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