FastAPI app in backend/main.py. All product routes are
under the /api prefix. Interactive OpenAPI docs are served at
http://localhost:8000/docs.
- CORS: any
http://localhost:*/http://127.0.0.1:*origin. - Auth: product routes under
/apirequire a logged-in user, except the/api/auth/*entry points. The browser uses HttpOnly cookies; Bearer access tokens are also accepted for programmatic clients. - Rate limiting: slowapi is wired up (
Limiterkeyed on authenticated user when available, otherwise remote address); theRateLimitExceededhandler returns 429. - Request IDs: every response carries
X-Request-ID(generated if absent), bound into the structlog context for the request. - Quota: any
DailyQuotaExhaustedraised anywhere becomes 503 withRetry-After: 86400.
Pydantic request/response shapes live in
models/schemas.py.
Create an account and auto-login. Body: {email, password} (password min
length 8). Sets kgre_access and kgre_refresh HttpOnly cookies. 409 if the
email is already registered; 403 if REGISTRATION_ENABLED=false.
Login with {email, password}. Sets both auth cookies and returns
{id, email, created_at}. Unknown email and wrong password both return the same
generic 401.
Rotates the refresh token, issues fresh cookies, and returns the user. Reuse of a
revoked refresh token revokes the remaining token family and returns 401.
Revokes the presented refresh token, clears both cookies, and returns 204.
Returns the current user from the access token cookie or Bearer token.
Ask a question (blocking; returns the full answer).
Body (QuestionRequest): { "question": str, "workspace_id": str = "arxiv_seed" }
Response (QuestionResponse):
The answer is also persisted as a Report with version = (count of prior
visible reports for this user+question+workspace_id) + 1.
Server-Sent Events variant. Query params: question (required),
workspace_id (default arxiv_seed). Emits progress, routing, done,
error events (see Query pipeline → Streaming).
The done event's payload is the full QuestionResponse plus id/version/
created_at. The report is saved before done is emitted.
List saved reports for a workspace (newest first, max 50). Query:
workspace_id (default arxiv_seed). Returns ReportSummary[]
({id, question, answer, retrieval_type, version, created_at}). In the public
arxiv_seed workspace, users see their own reports plus legacy pre-auth rows.
Full saved report rehydrated into a QuestionResponse (graph records, chunks,
entities, insights, cypher all restored from sources_used).
Delete one report. 204, or 404 if not found or not visible. A user can
delete their own report, or any report in a workspace they own.
Readable workspaces (oldest first): public NULL-owner demo workspaces plus the
current user's own workspaces. Returns WorkspaceResponse[].
Create. Body (WorkspaceCreate): {name, domain, description?}. The new
workspace is owned by the current user.
Update any of {name?, domain?, description?} (WorkspaceUpdate). Requires an
owned workspace; the public demo workspace returns 404.
Delete the workspace and cascade its sources, their ingestion jobs, and its
reports. 204. (Note: this removes Postgres rows; orphaned graph/vector data is
reclaimed by the cleanup sweep.)
Requires an owned workspace.
Auto-discover sources from the workspace description. Gemini
(source_discovery.py) maps the
description to 2–4 ArXiv category slugs; each new category becomes an
arxiv_feed source and is enqueued. Returns the created SourceResponse[].
Requires an owned workspace; 400 if the workspace has no description; 422 if
no categories could be inferred.
List sources (newest first). Also auto-resets any first-time source stuck in
running past 15 minutes to error (safety net for crashed workers without a
backend restart). Returns SourceResponse[].
Add a source and enqueue ingestion (30-min job timeout). Body
(SourceCreate): {type, url} where type ∈ {arxiv_feed, rss, web_url, pdf_upload}. For arxiv_feed, url may be a category, paper ID, arxiv URL, or
free-text query (see Ingestion → Fetch).
Requires an owned workspace.
Upload a PDF (multipart file). Saved under uploads/ with a UUID-prefixed
name; a pdf_upload source is created and enqueued. Returns SourceResponse.
Requires an owned workspace.
Per-source ingestion-job summary. Query: limit (default 50). Returns
SourceJobsResponse: {total, success, failed, running, jobs: IngestionJobBrief[]}.
Re-queue a source (sets pending, clears last_error). Skips already-processed
documents via the checkpoint. 404 if not found.
Requires an owned workspace.
Force a full re-ingest (force=True) — re-extracts and re-embeds even
already-processed documents (use after a pipeline change). Safe to replay
(MERGE + upsert).
Requires an owned workspace.
Force re-ingest of every source in the workspace. 404 if none.
Requires an owned workspace.
Delete a source and precisely detach its contribution: drops its source_id
from graph nodes/edges (deleting only what's now orphaned), deletes its vector
chunks (by source_id and URL), removes the PDF file, clears the checkpoint,
deletes its job rows, and invalidates workspace caches. Returns
{status: "deleted", graph: {nodes_removed, edges_removed}}. Shared concepts
that other live sources still assert are preserved.
Requires an owned workspace.
Reclaim stale data left by deleted sources and reset stuck-running sources past
15 min. Returns
{stale_vector_sources_removed, stale_graph_papers_removed, orphaned_jobs_removed, stuck_sources_reset}. Safe to call anytime — only removes data with no active
source.
Requires an owned workspace.
Subgraph for visualization. Query: workspace_id (default arxiv_seed),
limit (default 150, max 500). Picks the highest-degree hub nodes and returns
every edge incident to those hubs (so hubs don't look disconnected from the
periphery). Returns GraphResponse:
{nodes: [{name, type, degree}], edges: [{source, target, type, confidence, conflict}]}.
See graph_explorer.py.
RQ worker and queue health (drives the Sources UI worker indicator). Returns
{worker_count, workers: [{name, state, queues, current_job_id}], queues: {ingestion, ingestion_bulk, ingestion_dlq: {queued, started, failed}}}. See
system.py.
Distributed-worker coordinator status. Requires any authenticated user.
Returns a ready-to-paste local MCP stdio configuration for a readable workspace. Requires auth; configs are only emitted for workspaces the current user can see.
Operational endpoints (not under /api, hidden from schema)
| Endpoint | Purpose |
|---|---|
GET /health/live |
Liveness — process is up ({status: "ok"}). |
GET /health/ready |
Readiness — pings Postgres, Redis, Neo4j; 200 if all ok, else 503 with per-store status. |
GET /metrics |
Prometheus exposition (see Observability). |
GET /docs, GET /openapi.json |
FastAPI interactive docs + schema. |
Continue to Distributed worker pool.
{ "id": "uuid", "question": "…", "answer": "markdown…", "retrieval_type": "graph|vector|hybrid", "reasoning": "router's one-sentence justification", "cypher": "MATCH … | null", "graph_records": [ … ], "vector_chunks": [ {text, source_title, source_url, distance}, … ], "key_entities": [ {name, type, role}, … ], "insights": [ {type: "stat_grid|bar_chart|flow_path|comparison_table|timeline", …}, … ], "version": 1, "cached": false, "created_at": "ISO-8601" }