Skip to content
4 min read · 738 words

Tutorial: RAG End to End

Goal: see every stage of the RAG pipeline with your own eyes — chunks in SQLite, retrieval scores, the evaluator's verdict on both an answerable and an unanswerable question, and tenant isolation. 20 minutes.

Prereq: Your First Agent. Concepts: RAG Overview.

1 · Create a knowledge agent and a corpus

bash
curl -X POST http://localhost:8001/agents/ -H "Content-Type: application/json" -d '{
  "name": "KB Agent", "type": "Customer Support",
  "system_prompt": "You answer questions from the company knowledge base.",
  "enabled_tools": "document_search"
}'

Assume id = 4. Build a test document big enough to chunk (>800 chars):

bash
python - <<'EOF'
faq = """Support tiers. Premium customers receive a 4-hour response SLA on all tickets.
Standard customers receive a 24-hour response SLA. Enterprise plans include a dedicated
technical account manager and quarterly reviews. """ * 3
policy = """Refund policy. Annual subscriptions may be refunded pro-rata within 60 days
of purchase. Monthly subscriptions are non-refundable once the billing period starts.
Refunds are processed to the original payment method within 5 business days. """ * 3
open("kb.txt", "w", encoding="utf-8").write(faq + policy)
EOF

2 · Ingest and inspect the artifacts

bash
curl -X POST http://localhost:8001/agents/4/documents/upload -F "file=@kb.txt" | jq

The response reports chunks_count (expect 2–3 for ~1.5 KB at 800/100 chunking). Now inspect both stores' records:

bash
# SQLite: the chunks with their Chroma vector IDs
curl http://localhost:8001/agents/4/chunks | jq \
  '.[] | {chunk_index, first_words: .chunk_text[0:50]}'
sqlite3 backend/app/agentops.db \
  "SELECT chunk_index, vector_id FROM document_chunks WHERE agent_id=4;"

Note the deterministic IDs (agent_4_document_N_chunk_i) — the SQLite↔Chroma bridge. Look for a chunk boundary mid-sentence: that's why the 100-char overlap exists.

3 · Raw retrieval — see what the evaluator will see

bash
curl -X POST http://localhost:8001/agents/4/documents/search \
  -H "Content-Type: application/json" \
  -d '{"query": "how fast do premium customers get answers", "top_k": 2}' | jq \
  '.results[] | {score, chunk_index, preview: .chunk_text[0:60]}'

Two things to internalize: the query shares no keywords with "4-hour response SLA" yet matches — that's semantic retrieval; and score is a distance (lower = better), not a percentage (Retrieval).

4 · The graph answers…

bash
curl -X POST http://localhost:8001/agents/4/documents/ask \
  -H "Content-Type: application/json" \
  -d '{"question": "What is the refund window for annual subscriptions?", "top_k": 3}' | jq

Inspect all three response layers: answer (grounded — "60 days… pro-rata"), sources (which chunks), retrieval_evaluation (has_answer: true + the evaluator's reason). The path taken: retrieve → evaluate → answer (Corrective RAG).

5 · …and refuses

bash
curl -X POST http://localhost:8001/agents/4/documents/ask \
  -H "Content-Type: application/json" \
  -d '{"question": "What is the company vacation policy?", "top_k": 3}' | jq

The answer is the fixed refusal string; sources still lists the nearest (irrelevant) chunks and retrieval_evaluation.reason explains why they weren't enough. This is the point of corrective RAG: related-but-insufficient context gets refused, not hallucinated — and the refusal was cheaper than an answer (evaluator only, no generation — cost profile).

6 · Prove tenant isolation

Ask the same refund question as a different agent (your agent 2 or 3 — grant it document_search if needed):

bash
curl -X POST http://localhost:8001/agents/3/documents/ask \
  -H "Content-Type: application/json" \
  -d '{"question": "What is the refund window for annual subscriptions?", "top_k": 3}' | jq \
  '{answer, sources}'

Refusal with empty (or foreign-corpus) sources — agent 3 cannot see agent 4's vectors because every query filters where={"agent_id": ...} (the isolation primitive).

7 · Check the telemetry trail

bash
curl http://localhost:8001/agents/4/tool-calls | jq \
  '.[] | {tool_name, success, latency_ms}'

You'll see document_search and langgraph_corrective_rag entries — but notice no ToolCall for the upload and no cost fields on any of them: two documented observability gaps you now recognize on sight (coverage map).

What you learned

Chunking artifacts and the dual-store design · semantic retrieval and distance scores · both graph paths and their costs · tenant isolation by metadata filter · where RAG telemetry is thinner than the rest of the platform.

Next: Stage 4 of the Learning Guide — the frontend — or start shipping with the Guides.