Skip to main content
services/api/main.py is a second, separate FastAPI application — not a superset of product_app.py, and not imported by it. It hosts the historical research workbench that predates the tenant product: causal-graph schema exploration, dataset import and clustering experimentation, artifact/lineage inspection, the strict MVP1 250-issue analysis pipeline, and a file-backed client onboarding API that predates the real tenant-schema onboarding pipeline documented under Client onboarding pipeline. It is large — as of this writing its FastAPI() app registers 279 distinct paths (334 method+path operations), against the product API’s 44.
This page documents research/local-only surface area. Nothing here is reachable at api.causeloop.ai, is exercised by the tenant console, or should be treated as a stable contract — it changes freely as research work continues, with no allowlist and no product_app.py-style import-time enforcement holding it to a fixed shape.

Why it isn’t deployed

Three structural reasons, not just “it’s old code”:
  1. It’s a different process boundary entirely. The repo’s Dockerfile starts uvicorn services.api.product_app:app; nothing in the deployed container ever imports services.api.main. product_app.py’s own module docstring states the reasoning directly: importing main’s app “creates stores and exposes operations the customer/staff product does not need.”
  2. No tenant isolation. Its routes operate on process-global, dataset-id-keyed state (causegraph.storage.memory.CauseLoopState) rather than the per-tenant, RLS-isolated Postgres schemas the product uses. The remediation router’s own docstring calls out the specific example: the legacy /mvp1/caps / /mvp1/reviews system this app hosts “has no tenant isolation at all (dataset_id-keyed, process-global),” which is exactly why services/api/remediation_api.py was built as a real, tenant-scoped replacement rather than extended in place.
  3. It does real work on request threads. Parts of this surface (services/api/clients_api.py’s legacy onboarding flow, included here alongside the shared product routers) spawn threading.Thread(..., daemon=True) to run background work — a pattern the product API’s routers never use, since a daemon thread’s work is lost if the process restarts mid-request. See System overview for why that distinction matters operationally.
main.py does include the same tenant-facing routers the product uses (auth_router, onboarding_router, sources_router, insights_router, members_router, remediation_router, metrics_router) alongside its own — so in local basic/mvp1/relational modes you can reach both the tenant/staff product routes and the research surface on the same port, unlike production where only the allowlisted 44 operations exist at all.

Exploring it locally

The research surface has three local modes, from lightest to heaviest (docs/LOCAL_SETUP.md); see Research modes for the full walkthrough of each.
1

Install dependencies

The doctor is local and non-mutating — it never calls OpenAI or a remote service.
2

Start basic mode

Needs neither Docker nor an API key:
or two terminals:
3

Open it

  • Web UI (the Vite workbench, apps/web): http://127.0.0.1:5173
  • Swagger UI: http://127.0.0.1:8000/docs
  • Live schema: http://127.0.0.1:8000/openapi.json
A fresh clone has the tracked synthetic banking dataset available in this mode, but no pre-generated strict MVP1 report — report-read endpoints correctly 404 until you run an analysis.
Two heavier modes build on basic, both opt-in and both requiring explicit user approval before any external call: Neither of these is ever something a code agent should trigger without the user’s explicit, in-the-moment approval of the OpenAI cost and data transfer — that approval can’t be inferred from a task description. See Research modes and MVP1 pipeline for the full contract.

A grouped tour of the surface

The routes below are organized by domain, not registration order, to make the ~280 paths navigable. None of these paths are on the product allowlist (see API reference overview) — treat every one as local/research-only.

Graph schema and registry

GET /graph/schema returns the full causal-graph schema (causegraph.graph_schema.full_graph_schema()) — node/edge type definitions for the provenance graph model this whole research pipeline is built around. GET /lifecycle/graph and the /artifacts/registry, /artifacts/validation family expose the artifact-kind registry and its internal-consistency checks. GET /artifacts/models/* covers the separate model-training-artifact registry (trainers, targets, a “model × data” coverage matrix, checkpoint registration/retirement) — distinct from the product’s model_checkpoints table entirely; this is research-pipeline model bookkeeping.

Datasets

/datasets/import, /datasets/status, /datasets/public/sources, /datasets/narrative/adapters, /datasets/narrative/import, /datasets/imported, /datasets/{dataset_id}/summary load and inspect datasets into the process-global CauseLoopState — including the built-in public dataset acquisition workers (workers/ingest/acquire_public_datasets.py) that seed the tracked synthetic banking dataset used by basic mode.

Runs, artifacts, and lineage

/artifacts/object-storage/* is a large sub-family covering export/read/write, promotion between primary/secondary storage, and lifecycle validation for the research pipeline’s own object-storage abstraction — a separate concern from the product’s services/storage/object_store.py. /artifacts/lineage and /artifacts/{artifact_id}/lineage expose OpenLineage-style provenance for a given artifact; /runs, /tasks/runs/recent, /tasks/runs/{artifact_id} list and inspect individual pipeline executions. /mvp1/runs, /mvp1/runs/{artifact_id}, and /mvp1/runs/{artifact_id}/lineage are the equivalent read surface scoped specifically to strict MVP1 runs (see below).

Themes and clustering

/clusters/run and /clusters/algorithms execute and enumerate the clustering algorithms available to the research pipeline — a materially larger set than the product’s fixed hdbscan default (TrainingConfigRequest.clusterer in onboarding_api.py), including kmeans, leiden, bm25, tfidf and several hybrid combinations. /themes, /themes/{theme_id}/analysis, /themes/{theme_id}/issues, /themes/{theme_id}/fishbone, and /themes/llm-summaries* cover theme-level exploration and LLM-generated summaries/fishbone diagrams once a clustering run has produced themes. /loops/* is an earlier, process-global causal-loop remediation surface (algorithms, review, CAPA drafting) — the direct conceptual predecessor of the tenant-scoped Remediation system, kept here for research continuity rather than removed outright.

Strict MVP1 reports

A stable, explicitly-versioned sub-contract defined separately in services/api/mvp1_contract.py (MVP1_ENDPOINTS, all under /mvp1/*) rather than inline in main.py, specifically so its transport shape can’t silently drift from its own analysis logic. Each entry is tagged product or support audience and a minimum permission (read/review/operate) — product routes are the ones the Vite workbench’s browser client actually calls; support routes exist for imports, diagnostics, and stage-by-stage execution but aren’t duplicated in that client. Highlights: GET /mvp1/algorithms, GET /mvp1/datasets, POST /mvp1/issue-intelligence/run (the governed run itself, gated on external_embedding_transfer_approved/external_llm_transfer_approved flags in its request body), GET /mvp1/issue-intelligence/latest and its .../export.xlsx sibling, GET /mvp1/caps / POST /mvp1/caps / PATCH /mvp1/caps/{cap_id} (the process-global CAP system referenced above), GET /mvp1/reviews / POST /mvp1/reviews/{review_id}/decision, GET /mvp1/timeline, and the public-evidence pair GET /mvp1/public-evidence/latest / POST /mvp1/public-evidence/run. Every Mvp1Request subclass sets model_config = ConfigDict(extra="forbid"), so an unexpected field is a 422 rather than a silently-ignored typo — useful when frontend and backend drift during active research work. See MVP1 pipeline for the full analysis contract.

File-based /clients onboarding API

services/api/clients_api.py (mounted into main.py, not a standalone module) is the client onboarding flow that predates the real tenant-schema pipeline in onboarding_api.py. Its state is a flat directory tree, not Postgres: ClientAssetStore persists one manifest.json per client under data/clients/<client_id>/ (path overridable with CAUSELOOP_CLIENTS_ROOT) — no schema-per-tenant, no row-level security, no migrations. Its shape mirrors the real pipeline closely enough to be a useful reference: POST /clients (create), GET /clients / GET /clients/{client_id}, POST /clients/{client_id}/sources (register a connector) and .../sources/{source_id}/activate, .../sources/{source_id}/upload, POST /clients/{client_id}/ingest, PUT /clients/{client_id}/training-config, POST /clients/{client_id}/train (202) with GET /clients/{client_id}/train/status, POST /clients/{client_id}/activate, POST /clients/{client_id}/go-live, and GET /clients/{client_id}/insights/{collection}. Its connector registry (GET /connectors, causegraph.onboarding.connectors.CONNECTORS) is the source of the documented 409 not_enabled stub behavior: only file_upload is enabled = True; postgres, s3_dump, and kafka_pubsub are registered (visible in the schema, listable, schema-validated) but raise ConnectorNotEnabledError — surfaced as 409 {"detail": {"status": "not_enabled", "message": "..."}} — the moment you try to activate one. See Errors and conventions for the exact response shape.

Product code must not depend on this surface

The dependency direction is one-way and enforced structurally, not just by convention: product_app.py imports auth_api, onboarding_api, insights_api, members_api, metrics_api, remediation_api, and sources_api — never services.api.main, never clients_api, never mvp1_contract, and never causegraph.storage.memory.CauseLoopState. If you’re working on a product feature and find yourself reaching for something defined only in main.py or clients_api.py, that’s a signal the product-side equivalent either already exists elsewhere (check onboarding_api.py, remediation_api.py, sources_api.py first) or genuinely needs to be built there — not imported from the research app.