API reference¶
One server, two front doors, one service behind both. Start it with
sci-rag serve; interactive OpenAPI docs live at /docs.
Authentication¶
Send Authorization: Bearer <key>. Keys are configured by the operator
as a JSON map in SCI_RAG_API_KEYS:
{"team-key": {"scopes": ["retrieval:query", "retrieval:answer", "corpus:read"],
"rate_limit_per_minute": 120},
"agent-key": {"scopes": ["retrieval:query", "retrieval:answer", "byo_llm"]}}
| Scope | Grants |
|---|---|
retrieval:query |
POST /v1/query, and the MCP mount |
retrieval:answer |
POST /v1/answer (spends LLM tokens) |
corpus:read |
document catalog and /v1/status |
byo_llm |
may supply llm_api_key per request |
With no keys configured the server runs open and warns loudly at
startup; that is for localhost development only. GET /health and
GET /v1/corpus-manifest never require auth.
Errors¶
Every error is RFC 9457 application/problem+json with a stable code
and the request id:
{"type": ".../docs/api.md#invalid_key", "title": "Unknown API key",
"status": 401, "code": "invalid_key", "detail": "", "request_id": "9f2c..."}
Codes you can branch on:
REST endpoints¶
POST /v1/query¶
Retrieval only: ranked evidence with full transparency, no generation.
curl -s -X POST localhost:8000/v1/query \
-H 'Authorization: Bearer team-key' -H 'Content-Type: application/json' \
-d '{"query": "biogas yield of pretreated rice straw",
"top_k": 5, "profile": "deep",
"license_classes": ["public", "open_commercial"]}'
Request fields:
query, required.top_k, 1 to 50, default 8.profile:interactive(the default),deep, orauto.- Per-layer overrides:
include_graph,include_community,include_hyde,include_rerank. license_classesandsourcesallowlists. Omit them for all; an empty license list returns nothing by design.- The metadata filters below.
include_content: falsefor lean responses.
Metadata filters. year_min, year_max, authors, journals, and
exclude_dois narrow by publication metadata:
curl -s -X POST localhost:8000/v1/query \
-H 'Authorization: Bearer team-key' -H 'Content-Type: application/json' \
-d '{"query": "pretreatment of rice straw", "top_k": 5,
"year_min": 2020, "journals": ["Biomass and Bioenergy"],
"exclude_dois": ["10.1016/j.biombioe.2019.00000"]}'
They are enforced inside every layer's SQL, before ranking, exactly like
the license scope: an out-of-range document can never crowd an eligible
one out of a bounded candidate pool. authors and journals match
whole stored strings, not substrings. One consequence to expect: any
filter disables the community layer. A stored community summary aggregates
evidence across documents before your scope is known, so nothing can
filter it after the fact. The community trace reads skipped when that
happens. journal can come directly from your manifest or be
refreshed from explicit Crossref metadata with sci-rag corpus enrich.
The response carries three things: items, traces, and
degraded_stages. Each item has a title, section path, citation, license
class, fused score, and a layers field naming the layers that found it.
Each trace has a per-stage status and timing. If something timed out, you
will know exactly what.
POST /v1/answer¶
A grounded, cited answer. Two modes in one endpoint:
Streaming (default). "stream": true returns Server-Sent Events:
event: retrieval_started data: {"profile": "deep"}
event: retrieval_done data: {"item_count": 8, "degraded_stages": [], "traces": [...]}
event: compression_done data: {"enabled": true, "prompt_tokens_before": 1408, "prompt_tokens_after": 462, ...}
event: generation_started data: {"model": "gemini-2.5-flash"}
event: delta data: {"text": "Given its ash content, ..."} (repeats)
event: citations data: {"citations": [{"index": 1, "title": "...", "cited": true, ...}]}
event: done data: {"finish_reason": "stop"}
An error event (code, message) replaces the tail on failure.
JSON. "stream": false returns one body: answer, model,
citations (with a cited flag per source), traces,
degraded_stages, measured prompt_tokens_before and
prompt_tokens_after, plus compression failure and dropped-source counts.
Set include_compression to true or false to override the domain's
compression.enabled setting. Compression relevance-scores ordinary chunks
and summarizes them before prompt assembly. The shipped demo enables it after
the paired judged-answer gate documented in Evaluation; a new
domain should run the same gate before enabling its default. Failed, malformed,
empty, duplicate, or over-budget model output falls back to the complete chunk
and increments compression_failure_count; it never silently removes the
evidence. Citations always retain the original document and chunk identity.
Known retracted documents are excluded from answers by default. The flag
comes from explicit Crossref metadata written by sci-rag corpus enrich;
missing enrichment is not guessed to mean retracted. Raw /v1/query
retrieval keeps its previous behavior so evaluation and inspection remain
deliberate. The CLI has sci-rag answer --include-retracted for the rare,
explicit case where an operator needs retracted evidence in an answer.
Bring your own key. A request may include llm_api_key (an AI
Studio key) if its API key holds the byo_llm scope; the operator can
also bind an LLM key to an API key server-side. Either way the
credential is used for that call only and never stored or logged.
GET /v1/documents and GET /v1/documents/{id}¶
The corpus catalog (scope corpus:read): paginated summaries with
title, source, license class, authors, and counts; filters search
(title substring), source, license_class. The detail view adds the
formatted citation, source ref, and chunk previews.
GET /v1/status, GET /health, GET /v1/corpus-manifest¶
status (scope corpus:read): counts of everything plus breakdowns by
license, source, and embedding version. health (no auth): liveness
plus a database check, Cloud Run friendly. corpus-manifest (no auth,
deliberately): the machine-readable descriptor of this knowledge base:
domain, counts, embedding model and dimension, retrieval layers and
fusion weights, endpoint URLs, feature flags. A router in front of
several sci-rag deployments (the "switchboard" pattern) reads this to
decide which knowledge base fits a query.
MCP¶
Same capabilities as tools for agents. Two transports:
- stdio (local agents):
sci-rag mcp, or one-line registration:claude mcp add my-corpus -- uv run --directory /path/to/repo sci-rag mcp - Streamable HTTP (remote agents):
POST /mcp/on the running server, guarded by the same bearer keys (scoperetrieval:query).
The eight tools:
| Tool | Use it to |
|---|---|
search_corpus(query, top_k, deep, license_classes, year_min, year_max, journals) |
get ranked evidence chunks to reason over yourself |
answer_question(query, top_k, license_classes) |
get a grounded answer with numbered citations |
get_document(document_id) |
inspect a cited source: metadata, license, chunk previews |
get_citations(document_id) |
follow its references and the corpus documents that cite it |
search_entities(name_contains, entity_type, limit) |
find knowledge-graph entities by name |
get_entity_relationships(entity_name) |
see every stated relationship of an entity, with evidence quotes |
list_sources() |
learn the corpus's source buckets and license mix |
corpus_stats() |
size and shape; a fast "is this corpus worth querying" check |
Plus two resources: corpus://manifest (same payload as the REST
manifest) and corpus://methodology (how retrieval works here, one
page). Tool descriptions are written for the agent reading them; if you
add tools, hold that bar.
Python API¶
The CLI and server are thin wrappers over importable pieces, so the same
capabilities work in notebooks and your own applications. The package
ships type information (py.typed).
from sci_rag import Retriever, AnswerEngine, RetrievalScope
retriever = Retriever() # settings from the environment, domain from domain/
result = await retriever.retrieve(
"rice straw availability",
profile="deep",
limit=5,
scope=RetrievalScope(
license_classes=("public", "open_commercial"),
year_min=2020,
journals=("Biomass and Bioenergy",),
),
)
for item in result.items:
print(item.title, item.layers, item.score)
engine = AnswerEngine(retriever=retriever)
answer = await engine.answer("What biogas yield should I expect?")
print(answer.text, [s.citation for s in answer.cited_sources])
Ingestion, graph building, and evaluation are importable the same way
(ingest_entries, extract_graph, build_communities,
run_retrieval_eval); see examples/library_quickstart.py for a
complete, runnable walkthrough.