Skip to content

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:

Code Status Meaning
missing_key 401 No bearer token; WWW-Authenticate: Bearer is set
invalid_key 401 Token not recognized
insufficient_scope 403 Key lacks the required scope
rate_limited 429 Over the per-key limit; honor Retry-After
invalid_request 422 Body failed validation
document_not_found 404 Unknown document id
llm_unavailable 503 No LLM credentials configured server-side
generation_failed 502 The model call failed
http_error / internal_error varies Everything else, with the request id for the logs

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, or auto.
  • Per-layer overrides: include_graph, include_community, include_hyde, include_rerank.
  • license_classes and sources allowlists. Omit them for all; an empty license list returns nothing by design.
  • The metadata filters below.
  • include_content: false for 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 (scope retrieval: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.