didi-lot1-ai/ai_platform/modules/didi_brain/INDEX.md

19 KiB

DidiBrain - Index

Knowledge atom storage + claim verification cache + analysis atom cache pentru platforma DIDI. FastAPI service care vorbeste contractul web-gathering 1:1 (drop-in replacement / cache layer pentru /v1/gather) si in plus expune doua cache-uri proprii: brain_verification_cache (claims, din 2026-04-23) si brain_analysis_atom (techniques + ai_tampered + claims, NEW v2 din 2026-05-01).

Stack: Python 3.11+, FastAPI, Pydantic v2, asyncpg, BGE-M3 1024-dim embeddings (vLLM), BGE-reranker-v2-m3 (vLLM cross-encoder), Qwen3.5-397B-A17B (llama.cpp via OpenAI-compat router) Storage: Atomic (Rust knowledge graph) on Postgres 16 + pgvector. Brain owns its own tables prefixed brain_* alongside Atomic's tables. URL: http://10.11.10.12:8090 (production-local, since brain cutover 2026-05-01 — anterior pe 10.11.10.13:8090) Container: didibrain-api (alongside didibrain-atomic and didibrain-postgres)


Ce face

DidiBrain sta intre Didi backend (agent-v3) si modulele live de web-gathering. Vorbeste acelasi contract HTTP pentru /v1/gather, raspunzand din knowledge atoms pre-ingerate + semantic search + cross-encoder reranking + NLI stance vs query. Tipic 1.5-6s in loc de 20+s pentru un crawl live. Plus detine 2 cache-uri proprii pentru rezultate LLM ale didi-backend: brain_verification_cache (per-claim verification, since 2026-04-23) si brain_analysis_atom (rezultate full-component pe techniques + ai_tampered + claims, since 2026-05-01 v2).


API endpoints

Method Path Ce face
GET /health Liveness probe
POST /v1/search Flat list of doc-level search results
POST /v1/fetch Look up atoms by URL → return extracted text
POST /v1/gather Full claim → ranked evidence pipeline (with optional verification cache lookup)
POST /v1/image-search Stub, always returns empty list
POST /v1/ingest Populate brain from web-module output
POST /v1/verification_cache Write claim verification result (by didi-backend after LLM run)
POST /v1/analysis_atom/lookup NEW v2 — find cached analysis result by (content_hash, component, prompt_hash). Tier-agnostic. Bronze never served.
POST /v1/analysis_atom NEW v2 — write atom (rejects tier=free). Auto cache_tier silver/bronze din llm_confidence (>=60 silver). Gold preservation in upsert.
PATCH /v1/analysis_atom/{atom_id} NEW v2 — promote to gold (after moderator review). human_validated=true, expires_at=NULL.
GET /v1/analysis_atom/stats NEW v2 — counts per tier/component, hit rate 24h

Plus FastAPI auto: /docs (Swagger UI), /redoc, /openapi.json.


Structura fisiere

brain_api/
  app.py                  # FastAPI app + lifespan + 11 endpoints
  db.py                   # asyncpg pool + _SCHEMA_SQL (creates brain_verification_cache + brain_analysis_atom tables idempotent)
  schemas.py              # Pydantic v2 models (request/response for all endpoints)
  deps.py                 # AppState (atomic, embed, llm, resolver) singleton
  run.py                  # CLI entry (uvicorn launcher)
  Dockerfile
  requirements.txt
  prompts/                # LLM prompts for verification + extraction (nli_v1.md etc.)
  services/
    fetch.py              # /v1/fetch
    gather.py             # /v1/gather (most complex — semantic search + reranking + NLI + verification cache integration)
    ingest.py             # /v1/ingest
    search.py             # /v1/search
    verification_cache.py # claims cache (since 2026-04-23 v2 schema)
    analysis_atom.py      # NEW 2026-05-01 — techniques+ai_tampered+claims cache, 3 tiers (gold/silver/bronze)
    nli.py                # NLI stance classification
    mapping.py            # taxonomy / tag mapping helpers (atom → EvidenceItem / FetchedPage)

shared/                   # (one level up, used by brain_api)
  atomic_api.py           # AtomicClient (typed REST client for atomic-server)
  embedding_client.py     # BGE-M3 embed + reranker
  llm_client.py           # async OpenAI-compat wrapper
  config.py               # Pydantic Settings, LlmRole, model routing
  taxonomy.py             # canonical TAXONOMY tree + TagResolver
  logging.py              # structlog setup

extractor/                # claim extraction layer (separate package)
  extract.py              # single-doc extraction + substring quote validation
  push.py                 # claim atom creation
  batch.py                # batch orchestrator with state file
  prompts/

scripts/                  # operator CLI tools (numbered 01-11 + bootstrap_deploy.sh)
infra/                    # docker-compose.yml + deploy
lint/                     # custom lint pass (cross-corpus contradiction detection)
reports/                  # generated audit reports

Brain-owned PG schema

Doua tabele alaturi de cele ale Atomic, in acelasi Postgres (prefix brain_* pentru izolare).

brain_verification_cache (since 2026-04-23 v2)

Cache pentru verificare claims (one row per (claim_hash, tier), last-wins UPSERT). evidence_urls + evidence_hash stocate ca METADATA (NU in unique key) pentru ca backend sa poata calcula overlap. TTL 30 zile default.

Coloana Tip Scop
id uuid PK gen_random_uuid()
claim_hash text NOT NULL sha256(normalized claim)
tier text CHECK (free|premium)
evidence_hash text NOT NULL sha256(canonical urls), metadata only
evidence_urls jsonb NOT NULL metadata only
model text NULL LLM used
prompt_hash text NOT NULL sha256(system+user_template)[:12]
framework_version text NULL sha256 of relevant Redis configs
schema_name text default 'didi-v1'
verification_raw jsonb NULL raw LLM output
verification_processed jsonb NOT NULL mapped to canonical
created_at timestamptz default now()
updated_at timestamptz default now()
expires_at timestamptz NOT NULL TTL 30d default

UNIQUE: (claim_hash, tier). Indexes: idx_bvc_lookup, idx_bvc_expires, idx_bvc_prompt.

Migration v1 → v2: vechiul UNIQUE (claim_hash, evidence_hash, tier) facea cache-ul practic nereachable (URL-urile la read difera de cele la write). v2 muta evidence_hash afara din unique key. Migrarea e idempotenta (rulata la fiecare connect).

brain_analysis_atom (NEW 2026-05-01)

Cache pentru rezultate LLM full-component (techniques, ai_tampered, claims-aggregator). 3-tier system gold/silver/bronze.

Coloana Tip Scop
atom_id bigserial PK
content_hash text NOT NULL sha256(normalized text)
content_preview text NULL first 200 chars for debug
component text CHECK (techniques|ai_tampered|claims)
tier text CHECK (free|premium) informational only on row
prompt_hash text NOT NULL
framework_version text NULL
model_used text NULL
result_processed jsonb NOT NULL canonical mapped output
result_raw jsonb NULL
llm_confidence numeric NULL 0-100, drives silver vs bronze
cache_tier text CHECK (gold|silver|bronze) default 'silver'
human_validated bool default false true after moderator review
human_corrections jsonb NULL diff applied by moderator
validator_user_id text NULL keycloak_id
validated_at timestamptz NULL
hit_count integer default 0 incremented on lookup hit
last_hit_at timestamptz NULL
created_at, updated_at timestamptz
expires_at timestamptz NULL NULL=never (gold). Silver=90d, Bronze=30d

UNIQUE: (content_hash, component, prompt_hash)tier OUT of unique key (write only premium, read tier-agnostic). Indexes: idx_baa_lookup, idx_baa_gold (partial WHERE cache_tier='gold'), idx_baa_expires, idx_baa_prompt.

Cache tier rules:

  • gold: human_validated=true. Set by didi-backend moderation flow (PATCH endpoint). Survives prompt change. Forever (expires_at=NULL).
  • silver: LLM result, llm_confidence >= 60. Default fresh write. TTL 90d.
  • bronze: LLM result, llm_confidence < 60. Stored for audit but NEVER served on lookup. TTL 30d.

Gold preservation: ON CONFLICT upsert preserves gold (won't downgrade to silver if a fresh write comes in — result_processed, cache_tier, llm_confidence, expires_at all CASE-WHEN protected).

Lookup logic (services/analysis_atom.py::lookup): SELECT WHERE content_hash=$1 AND component=$2 AND cache_tier IN ('gold','silver') AND (expires_at IS NULL OR expires_at > now()) ORDER BY (cache_tier='gold') DESC, updated_at DESC LIMIT 1. Apoi decide_freshness() pe baza prompt_hash + framework_version. Gold = always fresh.


How didi-backend (agent-v3) uses brain

3 puncte de integrare:

  1. Claims verification cache — claims executor cheama gatherFromBrain() cu include_verification=true. Brain returneaza evidence + cached verification (if any) + staleness. Backend decide fresh / stale_framework / stale_prompt / miss path. Backend scrie via fire-and-forget POST /v1/verification_cache. Detalii contract: CONTRACT_VERIFICATION_CACHE.md. Helper centralizat: agent-v3/src/shared/brain/client.ts.

  2. Analysis atom cache (NEW 2026-05-01) — techniques si ai_tampered executors cheama lookupAnalysisAtom() inainte de LLM. Pe hit (gold sau silver+fresh), skip LLM, return cached. Dupa LLM run, fire-and-forget writeAnalysisAtomAsync() (only tier=premium). Dupa ce moderator rezolva sesiunea cu corectii, patchAnalysisAtomGold() promoveaza silver → gold.

  3. Gather pipeline — agent-v3 inca foloseste /v1/gather pentru live web-augmented retrieval (pre-ingerat + semantic search + reranker + NLI). Branch-ul pe brain_meta.cache_status = HIT|PARTIAL|MISS decide fallback la web module.


Configuration

  • Settings via shared/config.py cu pydantic-settings
  • Env vars: postgres_dsn, atomic_url, llm_router_url, embedding_url, reranker_url, verification_cache_ttl_days (default 30), verification_cache_max_payload_kb (default 64)
  • Internal Atomic URL: http://didibrain-atomic:8080 (Docker DNS)
  • Upstream LLM/embed/rerank pe VPN 10.11.10.x (10.11.10.17:14011 router, 10.11.10.15:8200 BGE-M3, 10.11.10.15:8100 reranker)
  • No auth in v1 (binds to internal Docker network)

Deployment

  • Docker compose la infra/docker-compose.yml
  • 3 containere: didibrain-api (FastAPI), didibrain-atomic (Rust KG), didibrain-postgres (PG 16 + pgvector)
  • Schema bootstrap on startup (idempotent — _SCHEMA_SQL ruleaza la fiecare connect, all DDL is IF NOT EXISTS + ALTER guards)
  • TTL cleanup: cron job in scripts/ sterge expired silver/bronze atoms
  • Resource footprint: ~200 MB RAM total cross 3 containere; brain-api idle ~6% CPU, spike ~20% during /v1/gather cu NLI

Rebuild:

cd /home/admin365/didi_mono/ai_platform/modules/didi_brain
docker compose -f infra/docker-compose.yml --env-file .env up -d --build

Bootstrap & lint

  • scripts/01_sanity_full.py — upstream stack validator (LLM + embed + rerank, 8 checks)
  • scripts/02_bootstrap_atomic.py — claim atomic instance, configure BGE-M3 provider
  • scripts/03_sanity_atomic.py — brain end-to-end (create → embed → search → cleanup)
  • scripts/04_seed_taxonomy.py — seed canonical tag taxonomy (79 tags, 7 namespaces)
  • scripts/05_import_wikipedia_seed.py — seed-list Wikipedia import
  • scripts/06_validate_queries.py — smoke test doc-level retrieval
  • scripts/07_run_extraction.py — claim extraction batch (Qwen 397B)
  • scripts/08_validate_claims.py — smoke test claim-level retrieval
  • scripts/09_brain_api_demo.py — brain_api contract test (all endpoints)
  • scripts/10_run_lint.py — Lint pass runner (contradiction detection)
  • scripts/11_show_contradictions.py — render top contradictions
  • scripts/bootstrap_deploy.sh — fresh-server deploy orchestrator
  • lint/ — custom lint pass (cross-corpus contradiction detection via NLI)

Ce NU face

  • No image storage (image-search endpoint always returns empty)
  • No auth in v1 (intern only, binds to Docker internal network)
  • No multi-tenant (single brain instance, multiple consumers)
  • No long-term archive — atoms expire on TTL (gold = forever, silver = 90d, bronze = 30d)
  • No live web crawling (delegata la modulele M17 / web — brain serveste din corpus pre-ingest)

  • README.md — project landing, quick start, architecture diagram
  • STATUS.md — end-of-session snapshot + troubleshooting + session log
  • CONTRACT_VERIFICATION_CACHE.md — contract backend ↔ brain pentru verification cache (v2)
  • CHANGES_2026-04-23.md — session changelog cu integrare web-api + verification cache v2
  • ARCHITECTURE.md — diagrame + pipeline detail
  • AUDIT.md — initial upstream Atomic audit

Cache Freshness Defense (2026-05-04 → 2026-05-05) — 11/11 piloni live

Whole-system response to "what happens if a cached verdict goes stale because the world changed?". Strict additive — zero breaking changes on the contracts agent-v3 already calls.

New services in brain_api/services/

Module Pilon Role
classifier.py 1 LLM single-call classify of a claim → {volatility, topic_codes, entity_bindings, estimated_validity_hours}. Caps clamped per tier (volatile≤48h, evolving≤720h, stable≤26280h). Conservative defaults on LLM failure.
cache_judge.py 2 NLI judge run on cache hits — given a claim, cached truth direction (TRUE/FALSE/MIXED), and current top-3 evidence, decides KEEP_CACHE / INVALIDATE / NEEDS_FULL_RECHECK. Cheap-path skip for stable+young. Audit-pass bonus raises invalidate threshold for atoms that survived multiple audits.
fact_status.py 11 Versioned (subject, predicate, object) knowledge layer. register_facts_from_bindings, assert_fact_truth (opens new fact_version when truth flips), lock_fact/unlock_fact for moderator overrides, check_fact_validity for gather-time lookup. Plus admin: list_facts_admin, list_audit_log.
canonicalizer.py 7 LLM rewrites a claim with relative time markers (azi, alegerile) anchored to the current date. Same surface text at different times produces different cache keys. Passthrough on failure.
invalidation.py 8 InvalidateFilter (topic_codes / entity_canonicals / claim_pattern / since / invalidate_gold / dry_run) → bulk UPDATE on both cache tables. Audit log row written per non-dry-run execution.
topic_volatility.py D1 HTTP polls didiFramework /api/sensitive-topics?active=true (cache 60s) for admin-configured per-topic TTL/recency. reconcile_with_classifier picks min(LLM_estimate, admin_TTL).

DB schema (alongside Atomic in same Postgres, prefix brain_*)

ALTER brain_analysis_atom + brain_verification_cache
  ADD volatility ('volatile'|'evolving'|'stable')
  ADD topic_codes text[]
  ADD entity_bindings jsonb         -- list of {subject, predicate, object, confidence}
  ADD ttl_hours_used integer
  ADD last_audited_at timestamptz
  ADD audit_history jsonb           -- last-50 JudgeVerdict snapshots
  ADD consecutive_audit_passes integer

CREATE brain_fact_status               -- current truth per (subject, predicate, object)
CREATE brain_fact_version              -- temporal versioning with valid_from/valid_to
CREATE brain_audit_log                 -- mass invalidations + judge decisions + truth changes

GIN indexes on topic_codes. Partial indexes on cache_tier IN ('gold','silver') AND volatility != 'stable' for the auditor's fast candidate query.

New endpoints (brain_api/app.py)

POST /v1/canonicalize                — Pilon 7 temporal disambiguation
POST /v1/cache/invalidate            — Pilon 8 mass invalidation (with dry_run)
GET  /v1/cache/audit_log             — paginated audit browser
GET  /v1/fact_status/list            — paginated fact browser (admin)
GET  /v1/fact_status/{fact_id}       — single fact detail
GET  /v1/fact_status/{fact_id}/versions  — full timeline
PATCH /v1/fact_status/{fact_id}      — moderator override (set_truth + lock + notes)

Existing upsert paths (POST /v1/analysis_atom, POST /v1/verification_cache) now run the classifier internally and persist volatility/topic_codes/entity_bindings on every write. Lookup paths (/v1/gather with include_verification=true) consult brain_fact_status and demote staleness to stale_evidence when bound facts have flipped.

gather.py recency reranking (Pilon 3+4)

_apply_recency blends rerank score with age decay using volatility-driven profiles:

Volatility w_recency half_life_days hard cut
volatile 0.50 3 7 days
evolving 0.30 30 (none)
stable 0.00 (none)
(none / hint absent) 0.15 30 (none)

combined = (1 - w_recency) * rerank + w_recency * exp(-age_days/half_life). Volatile evidence older than recency_window_days is hard-dropped.

didibrain-scheduler container (scheduler/)

Single image, four asyncio tasks under _supervised (auto-restart with exponential backoff):

  • feeder — RSS pull at volatility-aware intervals (15 min volatile / 6 h evolving / 24 h stable). Default feeds in config.py:DEFAULT_*_FEEDS; override via FEED_VOLATILE_URLS/FEED_EVOLVING_URLS/FEED_STABLE_URLS/FEED_BREAKING_URLS env. Feeds POST to /v1/ingest with run_extraction=true.
  • auditor — daily sweep of brain_analysis_atom rows with cache_tier IN ('gold','silver') AND volatility != 'stable' AND last_audited_at < now()-1d. For each: confidence-decay cheap path → judge → apply_judge_verdict. Sequential (not parallel) to avoid LLM router saturation.
  • watcher — 5-min poll on the breaking-news feeds. Per item: classify with LLM → _build_invalidation_targetsPOST /v1/cache/invalidate with topic_codes + entity_canonicals. In-process LRU of seen URLs (cap 2000). End-to-end target latency: <30s from RSS publish to cache invalidation.
  • heartbeat — touches /tmp/scheduler.healthy every 30s for the docker healthcheck.

Compose entry in infra/docker-compose.yml builds via scheduler/Dockerfile. Imports brain_api.* directly so the auditor uses the same DB pool + apply_judge_verdict helper as the API container (separate process, separate pool to the same Postgres).

Live state at write-time

47 articles ingested in first cycle. Watcher invalidated 4 breaking stories live (Iran/Hormuz, Nigerian elections, Ukraine drones, etc.) within seconds of feed publication. Auditor scheduled 24h after first start.

Memory pointer

~/.claude/projects/-home-admin365/memory/MEMORY.md § "Cache Freshness Defense" — one-liner status + counts.