Watch the walkthrough: Agentic RAG — Architectural Patterns for Knowledge-Driven AI Systems (video) — the patterns this platform implements, presented by the author.
ATLAS is a self-correcting research analyst that answers questions about Aurora Station, a fictional six-person Mars research outpost. It combines three classic Agentic RAG patterns in one LangGraph loop:
- Routing + query decomposition — a structured router picks the right knowledge collection(s); multi-collection questions are decomposed into standalone sub-questions.
- Corrective RAG (CRAG, arXiv:2401.15884) — every retrieved chunk is graded for relevance; weak evidence triggers an anchored query rewrite and escalation to an archive index (plus optional Tavily web search).
- Self-RAG groundedness reflection (arXiv:2310.11511) — the draft answer is checked against the context; ungrounded drafts send the agent back for another retrieval round, under a hard retry budget enforced in code.
This repository is the industrial re-implementation of the academic notebook in
../atlas-agentic-rag/: same agent behavior, production engineering — config-driven
settings, persistent vector stores, a REST + SSE API, containerization, structured
logging, an eval harness, and a deterministic fake-LLM mode that runs the whole
system (including the test suite) with zero API keys.
The two design frames this implementation is built on (from Agentic RAG — Architectural Patterns for Knowledge-Driven AI Systems):
Anatomy of the knowledge agent — query understanding, retrieval, preprocessing and ranking, reasoning and generation, with provenance collected continuously across every stage. In ATLAS that rail is the bracketed source citations plus the run trace.
Retrieval as a control loop — perceive, plan, act (retrieve), evaluate, adapt, synthesize. ATLAS implements the loop concretely: routing and decomposition, CRAG grading, anchored rewriting with archive escalation, and groundedness reflection under a hard retry budget.
┌──────────┐ route=multiple ┌────────────┐
question ──────▶│ router │───────────────────▶│ decompose │──┐
└──────────┘ └────────────┘ │
│ route=ops|science|crew ▼
│ ┌──────────┐
└───────────────────────────────────▶│ retrieve │
└──────────┘
│
▼
┌──────────────────────────────────────────── ┌────────────┐
│ enough relevant chunks │ grade │
▼ └────────────┘
┌───────────┐ grounded=yes │ weak,
│ generate │──▶ ┌────────────────┐ END │ budget left
└───────────┘ │ groundedness │──────▶ (answer) ▼
└────────────────┘ ┌──────────────┐
│ no, budget left │ rewrite_query│
└─────────────────────────▶│ (anchored) │
└──────────────┘
│
▼
┌───────────────────┐
│ retrieve_archive │──▶ grade
│ (+ optional web) │ (loop)
└───────────────────┘
Rendered directly from the compiled graph (atlas graph):
graph TD;
__start__ --> router;
router -.-> decompose;
router -.-> retrieve;
decompose --> retrieve;
retrieve --> grade_documents;
grade_documents -.-> generate;
grade_documents -.-> rewrite_query;
rewrite_query --> retrieve_archive;
retrieve_archive --> grade_documents;
generate --> check_groundedness;
check_groundedness -.-> rewrite_query;
check_groundedness -.-> __end__;
| Academic notebook cell | Industrial module |
|---|---|
Inline Document lists |
data/corpus/<collection>/*.md + atlas.corpus loader |
InMemoryVectorStore globals |
atlas.stores: protocol + Chroma (persistent, default), JSON file, in-memory adapters |
getpass key loading |
atlas.config.Settings (pydantic-settings, ATLAS_ env prefix, .env) |
MAX_RETRIES / MIN_RELEVANT constants |
ATLAS_MAX_RETRIES / ATLAS_MIN_RELEVANT settings, enforced in graph edges |
| Pydantic schemas + prompts (verbatim) | atlas.schemas, atlas.prompts |
Chain globals (router_llm, grader, ...) |
atlas.chains.build_chains() |
| Notebook node functions | atlas.nodes.NodeFactory (injected chains/stores/settings, per-node latency) |
builder.compile() |
atlas.graph.build_graph() |
ask() print helper |
atlas.service.AtlasService.query() / query_stream() with run_id + QueryResult |
| Gradio cell | atlas.ui rebuilt on the service (same streaming UX) |
| Report-card cell (pandas) | evals/report_card.py → console table + machine-readable evals/results.json |
| — (not in notebook) | FastAPI app (/v1/query, SSE stream, health/readiness), Dockerfile, CI |
| — (not in notebook) | Deterministic fake LLM/embeddings for tests and CI |
atlas-industrial/
├── data/corpus/{ops,science,crew,archive}/*.md # 17 seed documents, verbatim
├── src/atlas/ # the package (src layout, `pip install -e .`)
│ ├── config.py logging.py schemas.py prompts.py llm.py chains.py
│ ├── state.py nodes.py graph.py corpus.py ingest.py service.py
│ ├── stores/{base,memory,jsonstore,chroma}.py
│ ├── api/{models,app}.py
│ ├── cli.py # atlas ask|serve|ingest|eval|seed|graph
│ └── ui.py # Gradio chat UI on AtlasService
├── evals/{questions.json,report_card.py}
└── tests/ # pytest, all runnable with zero API keys
make setup # python3 -m venv .venv && pip install -e ".[dev]"
export OPENAI_API_KEY=sk-... # or copy .env.example to .env
# Zero-key smoke run first (deterministic fake provider):
ATLAS_LLM_PROVIDER=fake .venv/bin/atlas seed
ATLAS_LLM_PROVIDER=fake .venv/bin/atlas ask \
"Who leads the greenhouse experiment, and what did its most recent harvest produce?"
# Real run:
.venv/bin/atlas seed
.venv/bin/atlas ask "What actually happened on Sol 188?"
.venv/bin/atlas serve # FastAPI on :8000
.venv/bin/atlas ui (python -m atlas.ui) # Gradio chat UIdocker compose up --build # API on http://localhost:8000, chroma data on a volume| Endpoint | Description |
|---|---|
POST /v1/query |
Full agent run → structured QueryResult (answer, sources, trace, retries, run_id, latency) |
GET /v1/query/stream?question=... |
Same run as Server-Sent Events: one trace event per node, then a final result event |
GET /health |
Liveness probe |
GET /ready |
Readiness probe (503 until every collection is non-empty) |
GET /v1/collections |
Collection names + document counts |
GET /v1/metrics |
Process counters: requests, errors, avg latency, tokens, est. cost |
GET /graph |
Agent graph as Mermaid |
curl -s localhost:8000/v1/query -H 'content-type: application/json' \
-d '{"question": "How long does a message to Earth take?"}' | jq .result.answer
curl -N "localhost:8000/v1/query/stream?question=What%20happened%20on%20Sol%20188%3F"
# event: trace
# data: {"run_id":"...","node":"router","lines":["[router] route = ops -- ..."],...}
# ...
# event: result
# data: {"answer":"...","sources":["arc-02"],"retries":1,...}Errors are structured: {"error": {"code": "...", "message": "...", "run_id": "..."}}.
All settings are environment variables with the ATLAS_ prefix (see .env.example):
| Variable | Default | Meaning |
|---|---|---|
ATLAS_LLM_PROVIDER |
openai |
openai or fake (deterministic, no keys) |
ATLAS_OPENAI_MODEL |
gpt-4o-mini |
Chat model |
ATLAS_EMBEDDING_MODEL |
text-embedding-3-small |
Embedding model |
ATLAS_TEMPERATURE |
0.0 |
LLM temperature |
ATLAS_MAX_RETRIES |
2 |
Hard corrective-loop budget (enforced in code) |
ATLAS_MIN_RELEVANT |
1 |
Chunks that must pass grading before generation |
ATLAS_RETRIEVE_K / ATLAS_ARCHIVE_K |
2 / 3 |
Retrieval fan-out per index / archive |
ATLAS_STORE_BACKEND |
chroma |
chroma (persistent), json (file), memory |
ATLAS_PERSIST_DIR |
data/vectorstore |
Where chroma/json stores persist |
ATLAS_CORPUS_DIR |
data/corpus |
Seed corpus location |
ATLAS_CHUNK_SIZE / ATLAS_CHUNK_OVERLAP |
1000 / 100 |
Ingestion chunking |
ATLAS_USE_WEB_SEARCH |
false |
Tavily escalation in retrieve_archive (needs TAVILY_API_KEY) |
ATLAS_LOG_FORMAT / ATLAS_LOG_LEVEL |
text / INFO |
json for structured logs |
ATLAS_API_KEYS |
(empty) | Comma-separated API keys; empty = auth disabled |
ATLAS_RATE_LIMIT_RPM |
0 |
Requests/min per key or IP; 0 = disabled |
ATLAS_INPUT_PRICE_PER_1M / ATLAS_OUTPUT_PRICE_PER_1M |
0.15 / 0.60 |
USD per 1M tokens (gpt-4o-mini defaults) |
ATLAS_TRACE_DIR |
(unset) | When set, appends run records to runs.jsonl |
ATLAS_LANGSMITH_PROJECT |
(unset) | Exported as LANGSMITH_PROJECT |
ATLAS_STORE_DIR |
(unset) | Alias overriding ATLAS_PERSIST_DIR |
ATLAS_API_HOST / ATLAS_API_PORT |
0.0.0.0 / 8000 |
Uvicorn bind |
ATLAS_CORS_ORIGINS |
* |
Comma-separated CORS allowlist |
Both features are off by default — the zero-config dev flow stays open.
API-key authentication (src/atlas/api/auth.py): set ATLAS_API_KEYS to a
comma-separated list and every /v1/* endpoint requires Authorization: Bearer <key> or X-API-Key: <key>. /health, /ready and /graph stay public.
Comparison is constant-time; failures return a structured 401 and are logged
without ever logging the key.
ATLAS_API_KEYS=alice-key,bob-key atlas serve
curl -s -o /dev/null -w '%{http_code}\n' localhost:8000/v1/metrics # 401
curl -s localhost:8000/v1/metrics -H 'X-API-Key: alice-key' # 200Rate limiting (src/atlas/api/ratelimit.py): set ATLAS_RATE_LIMIT_RPM to a
positive integer to apply a sliding-window limit per API key (or per client IP
when auth is off). Over-limit requests get a structured 429 with a
Retry-After header. In-memory, stdlib-only, thread- and async-safe; state is
per-process.
- Structured logs:
ATLAS_LOG_FORMAT=jsonemits one JSON object per record withrun_id, node, and latency fields. - Per-run usage: every
QueryResultcarries ausageblock (prompt / completion / total tokens,estimated_cost_usd) built from OpenAIusage_metadataby a LangChain callback handler. Prices are configurable viaATLAS_INPUT_PRICE_PER_1M/ATLAS_OUTPUT_PRICE_PER_1M. In fake mode (or when a model reports no usage)usageis null. - Process metrics:
GET /v1/metricsreturns cumulative counters — requests total, errors, average latency, tokens, estimated cost. - LangSmith (zero code): set
LANGSMITH_TRACING=trueandLANGSMITH_API_KEYand every LLM call is traced by LangChain natively.ATLAS_LANGSMITH_PROJECTis exported asLANGSMITH_PROJECTwhen set. - Built-in trace file (no external service): set
ATLAS_TRACE_DIRand every run appends one JSON line to<trace_dir>/runs.jsonl— run_id, question (truncated), route, retries, per-node latencies, usage, timestamp.
In addition to the built-in ISUSE report card (make eval), a RAGAS harness is
available as an optional extra (kept out of the core dependencies because it is
heavy):
pip install 'atlas-rag[ragas]'
export OPENAI_API_KEY=sk-...
make ragas # python evals/ragas_eval.pyIt runs the report-card questions through the service and scores faithfulness
and answer relevancy, writing evals/ragas_results.json. Without the extra or a
key it prints a clear message and exits 2 (CI-safe skip).
atlas ingest --dir /path/to/docs --collection ops.md/.txt files are chunked with RecursiveCharacterTextSplitter and upserted
with deterministic ids (<source>:<chunk>), so re-ingesting is idempotent. PDFs
work with the optional extra: pip install ".[pdf]".
.venv/bin/python evals/report_card.py --fake # deterministic smoke, no keys
.venv/bin/python evals/report_card.py # real ISUSE judging (needs OPENAI_API_KEY)Runs the six report-card questions from evals/questions.json through the full
agent, scores each answer with the Self-RAG usefulness judge (ISUSE, 1–5), prints a
console table and writes evals/results.json (mean usefulness, grounded count,
per-question route/loops/abstention vs. expectation) — machine-readable for CI.
ATLAS_LLM_PROVIDER=fake swaps in FakeChatModel + FakeEmbeddings
(atlas/llm.py). The fake model implements with_structured_output and answers
each chain by keyword rules on the rendered prompt: routing scores topic word-sets,
the grader uses stemmed token overlap plus the incident-specificity rule from the
prompt ("Sol 188" questions reject chunks that never mention 188), the generator
quotes the best-scoring surviving chunk (its bracketed source id is the citation)
and abstains verbatim when the context is empty, and the groundedness/usefulness
judges apply matching heuristics. An optional script (AtlasService(settings, llm_script={"grounded": ["no", "yes"]})) forces specific verdicts in order, which
is how tests exercise the reflection loop and the retry budget deterministically.
FakeEmbeddings is an MD5-hashed bag-of-words embedder, so retrieval has real
lexical behavior and is stable across runs and processes. One known approximation:
the report-card question "What movies does the crew watch on their rest sols?"
lexically matches the movie-nights policy chunk, so fake mode quotes it instead of
abstaining — the abstention path is covered by the Wi-Fi question and the
retry-budget tests.
make test # pytest — 36 tests, all fake-mode, zero API keys
make lint # ruff check src tests evalsGitHub Actions (.github/workflows/ci.yml) runs the same ruff + pytest on every
push. The Dockerfile builds a non-root python:3.12-slim image whose entrypoint is
uvicorn atlas.api.app:create_app --factory.

