| .. | ||
| brain_api | ||
| extractor | ||
| infra | ||
| lint | ||
| scheduler | ||
| scripts | ||
| shared | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| ARCHITECTURE.md | ||
| AUDIT.md | ||
| CONTRACT_VERIFICATION_CACHE.md | ||
| INDEX.md | ||
| pyproject.toml | ||
| README.md | ||
| STATUS.md | ||
DidiBrain
A claim-verification knowledge cache for Didi, a disinformation analysis application. DidiBrain sits between Didi's backend and Didi's live web-gathering module and speaks the same HTTP contract, answering from pre-ingested atoms + multilingual semantic retrieval + LLM stance classification — typically in 1.5-6 seconds instead of 20+ seconds for a live web crawl.
Built on top of Atomic, a Rust knowledge-graph backend. DidiBrain adds the scraping, claim-level extraction, reranker integration, NLI stance layer, HTTP contract, and the contradiction audit job on top.
What it is, in one paragraph
Didi's backend currently verifies claims by calling a live
POST /v1/gather endpoint that does web search + fetch + score. That's
slow (~23 s) and expensive on premium engines. DidiBrain exposes the same
HTTP contract but serves responses from a locally-grown knowledge graph:
- Atoms (documents and atomic claims) are stored in a Postgres + pgvector backend with multilingual embeddings (BGE-M3, 1024-dim).
- Claim extraction runs Qwen3.5 over every ingested document to pull out verifiable atomic claims with source quotes and stance.
- Retrieval uses vector kNN + BGE-reranker-v2-m3 cross-encoder for precision.
- NLI stance vs query classifies each piece of evidence as SUPPORTS, CONTRADICTS, or NEUTRAL relative to the input claim — giving Didi the signal it really needs for a disinfo verdict.
brain_meta.cache_statustells Didi HIT / PARTIAL / MISS so the backend can cleanly fall back to the expensive web module when the brain doesn't have relevant knowledge yet.
The brain is also self-populating: whenever Didi falls back to the web
module and gets fresh evidence, it can POST the result to /v1/ingest
and DidiBrain will create new atoms + queue claim extraction in the
background. Next time a similar claim arrives, it's a HIT.
Tech stack
| Layer | Tech |
|---|---|
| Brain storage | Atomic (Rust) on Postgres 16 + pgvector |
| Embeddings | BAAI/bge-m3 via vLLM OpenAI-compat (1024 dim, 8K ctx, multilingual) |
| Reranker | BAAI/bge-reranker-v2-m3 cross-encoder via vllm-rerank-api |
| LLM (reasoning) | qwen3.5 via vLLM through an OpenAI-compat router (MODEL_FAST enabled) |
| Service | Python 3.12, FastAPI, Uvicorn, Pydantic v2, httpx, structlog, tenacity |
| Deployment | Docker + docker-compose (4 services: api, atomic, postgres, scheduler) |
Quick start — local dev
Assumes Docker Desktop running and .env filled with reachable upstream
endpoints.
cd didibrain
# 1. Copy env template and fill upstream URLs
cp .env.example .env
# Edit LLM_ROUTER_URL, EMBEDDING_URL, RERANKER_URL to reachable addresses
# 2. Bring up the stack (atomic + postgres + brain_api)
docker compose -f infra/docker-compose.yml --env-file .env up -d --build
# 3. Create a Python venv for operator scripts
python3 -m venv .venv
.venv/bin/pip install httpx pydantic pydantic-settings structlog \
python-dotenv tenacity rich
# 4. Bootstrap Atomic (claim instance, configure BGE-M3 provider)
.venv/bin/python scripts/02_bootstrap_atomic.py
# 5. Seed the canonical tag taxonomy
.venv/bin/python scripts/04_seed_taxonomy.py
# 6. (Optional) Import seed Wikipedia corpus — first topic: vaccines
.venv/bin/python scripts/05_import_wikipedia_seed.py
# 7. (Optional) Extract atomic claims from the imported documents
.venv/bin/python scripts/07_run_extraction.py
# 8. Verify it works
curl -fsS http://localhost:8090/health
.venv/bin/python scripts/09_brain_api_demo.py
Open the interactive Swagger UI at http://localhost:8090/docs to poke the full route set live (~30 routes).
Production deploy — fresh Linux server
A single bash script does the whole thing:
scp -r didibrain/ user@server:~/
ssh user@server
cd ~/didibrain
cp .env.example .env
vim .env # set LLM/BGE/reranker URLs for this network
./scripts/bootstrap_deploy.sh # preflight → build → up → seed → smoke test
The script is idempotent — re-run it any time to pick up from wherever it
was interrupted. See scripts/bootstrap_deploy.sh for the exact flow and
optional env flags (BRAIN_IMPORT_CORPUS=0 to skip the seed import, etc.).
Alternatively, run the numbered scripts/XX_*.py files in order by hand —
they are all idempotent and explicit.
HTTP API — the contract
Brain-API speaks Didi's existing web-gathering module contract, 1:1 at
the response-shape level, with additive brain_meta fields that older
backends safely ignore.
| Endpoint | Purpose | Typical latency |
|---|---|---|
GET /health |
liveness | <10 ms |
GET /docs |
interactive Swagger UI | — |
GET /redoc |
read-only API docs | — |
GET /openapi.json |
machine-readable schema | — |
POST /v1/search |
flat list of doc-level search results | 200-400 ms |
POST /v1/fetch |
look up atoms by URL → extracted text | 30-100 ms |
POST /v1/gather |
claim → ranked evidence with NLI stance | 5-6 s (1.5 s without NLI) |
POST /v1/image-search |
stub (always empty list) | <5 ms |
POST /v1/ingest |
populate brain from web-module output | variable (async extraction) |
POST /v1/verification_cache |
write claim verification result (claims cache) | <50 ms |
POST /v1/analysis_atom/lookup |
read cached analysis result (techniques / ai_tampered / claims) | <50 ms |
POST /v1/analysis_atom |
write analysis atom (silver/bronze by confidence) | <50 ms |
PATCH /v1/analysis_atom/{id} |
promote atom to gold (moderator review) | <50 ms |
GET /v1/analysis_atom/stats |
per-tier/component counts + 24h hit rate | <20 ms |
POST /v1/canonicalize |
temporal claim disambiguation (Pilon 7) | LLM-bound |
POST /v1/cache/invalidate |
mass invalidation with dry_run (Pilon 8) |
variable |
GET /v1/cache/audit_log |
paginated audit browser | <50 ms |
GET/PATCH /v1/fact_status/* |
versioned fact-status layer (list/detail/versions/override) | <50 ms |
This is the full set served by brain_api (~30 routes including the FastAPI
auto docs); the /v1/gather, /v1/search, /v1/fetch, /v1/ingest,
/v1/image-search group is the web-gathering contract, the rest are the
brain-owned cache + freshness-defense layers. See INDEX.md for the
canonical route list.
The response from /v1/gather matches the existing web-module shape
exactly plus an additive brain_meta object on the top level and inside
each evidence[].provenance. The key signal to branch on:
resp = requests.post(f"{BRAIN_URL}/v1/gather", json={"claim": text}).json()
if resp["brain_meta"]["cache_status"] == "MISS":
resp = requests.post(f"{WEB_MODULE_URL}/v1/gather", json={"claim": text}).json()
# optional self-populating:
requests.post(f"{BRAIN_URL}/v1/ingest",
json={"claim": text, "evidence": resp["evidence"]})
See STATUS.md for the full field reference.
Architecture
Didi backend
│
│ POST /v1/gather {claim}
▼
┌────────────────────────────────────────────────────┐
│ brain_api :8090 (FastAPI, Uvicorn) │
│ │
│ stage context → language detection │
│ stage retrieval → Atomic semantic search (top 50) │
│ stage rerank → BGE cross-encoder (top 15) │
│ stage nli → Qwen3.5 stance vs query │
│ stage evidence → group by parent doc, shape │
└──────┬──────────────────────────┬──────────────────┘
│ │
│ HTTP (docker DNS) │ HTTP (docker DNS)
▼ ▼
┌────────────────┐ ┌───────────────────────┐
│ atomic-server │ │ BGE-M3 embeddings │
│ :8080 (8088) │◀──SQL──│ & BGE reranker │
└──────┬─────────┘ │ (vLLM) │
│ └───────────────────────┘
▼
┌────────────────┐ ┌───────────────────────┐
│ postgres │ │ qwen3.5 │
│ + pgvector │ │ via LLM router │
│ :5432 (5434) │ │ (vLLM) │
└────────────────┘ └───────────────────────┘
Project structure
didibrain/
├── infra/docker-compose.yml # 4-service stack (api, atomic, postgres, scheduler)
├── brain_api/ # FastAPI service (the main deliverable)
├── shared/ # config, clients, taxonomy (reused everywhere)
├── extractor/ # claim extraction (host jobs + /v1/ingest)
├── lint/ # cross-corpus contradiction detection
├── scripts/ # operator CLI tools (numbered 01-11)
│ └── bootstrap_deploy.sh # fresh-server bootstrap script
├── STATUS.md # end-of-session snapshot + troubleshooting
└── README.md # this file
Detailed file-by-file rundown is in STATUS.md.
Status
- Upstream stack validation (LLM + BGE + reranker sanity gate)
- Atomic running on Postgres + pgvector in compose
- Canonical tag taxonomy (79 tags, 7 root namespaces)
- First Wikipedia import (19 documents, EN+RO, vaccines topic)
- Claim extraction (513 atoms, 1.2% hallucination filter)
- Document-level retrieval validated (cross-lingual cosine 0.88-0.94)
- Claim-level retrieval validated
- brain_api HTTP service with Didi contract (full route set, ~30 routes)
- brain_api dockerized (self-sufficient, taxonomy auto-refresh)
- NLI stance vs query in
/v1/gather - Lint pass contradiction detection (code + smoke test)
- Deployment bootstrap script
- Corpus expansion — more topics, more sources (next session)
- Full Lint run on diverse corpus
- Bearer auth on brain_api (when exposing beyond loopback)
- Optional
atomic-webfrontend service
Operational notes
- Resource footprint: ~200 MB RAM total across the 4 containers; brain-api
idles at ~6% CPU, spikes to ~20% during a
/v1/gatherwith NLI. - Image size: brain-api Docker image is ~253 MB (Python 3.12-slim base).
- Upstream connectivity: the LLM router, BGE-M3 embeddings, and the BGE
reranker are reached over Docker DNS as
didiAI-llm-api:14011,didiAI-embeddings-api:14100, anddidiAI-rerank-api:14200on the shareddidi-network. If those upstreams are unreachable,/v1/gatherreturns 500 because Atomic cannot embed the query. Confirm upstream reachability before debugging anything else when search starts failing. - Startup: brain_api refreshes the
TagResolverfrom Atomic at startup; in additionshared/_tag_ids.jsonis mounted into the container (compose volume) so background extraction — which instantiatesTagResolverdirectly, bypassing the lifespan refresh — can still resolve canonical tag UUIDs. - Idempotency: every operator script (taxonomy seeder, Wikipedia importer, claim extractor, Lint pass) is idempotent via state files or URL-based dedup. Re-running is always safe.
License & upstream
DidiBrain itself is private (not open source). It builds on top of
Atomic (MIT). The Atomic
upstream clone at D:\didi_brain\atomic\ is untouched — git pull
from upstream is always safe and doesn't conflict with anything in
this repo.