didi-lot2-backend/backend/services/orchestration-layer/agent-v3/BRAIN_V2_DESIGN.md
2026-07-10 03:39:53 -07:00

23 KiB
Raw Blame History

Brain v2 Extension — Design Doc

Status: DRAFT 2026-04-30 Owner: tehnic@finesynergy.eu Companion doc: HIL_MODERATION_DESIGN.md (queue + UI — separat)


Goal

Extindem didi-brain să cache-uiască rezultate de inferență pentru techniques și ai_tampered (la fel ca face deja pentru claims), cu 3 tier-uri de încredere (gold/silver/bronze) și invalidare la schimbare prompt/framework. Atomi gold provin din corecții moderator (vezi HIL doc); silver din LLM cache regular; bronze NU se servește.

Out of scope: workflow moderare, UI, queue management — vezi HIL_MODERATION_DESIGN.md.


Decizii agreate

# Decizie
1 1 atom per (componentă × content_hash) — techniques separat de ai_tampered separat de claims
2 Embedding model = ce folosește brain deja (NU adăugăm infra nouă). Brain are deja semantic search + cross-encoder reranking
3 3 tier-uri: gold (human_validated), silver (LLM cache neverificat), bronze (pending review, nu se servește)
4 Write trigger: silver pe LLM run (fire-and-forget); gold pe PATCH cu human_validated=true; bronze pe LLM run cu confidence < 60
5 Confidence threshold pe write — sub 60, NU se scrie silver (lasă miss → re-try cu poate model premium)
6 Prompt versioning: gold supraviețuiește la schimbare prompt; silver e invalidat

Architecture

┌──────────────────────────────────────────────────────────────────┐
│  agent-v3 (techniques/ai-tampered/claims executors)              │
│                                                                   │
│  1. Pre-LLM:                                                      │
│     POST http://10.11.10.13:8090/v1/analysis_atom/lookup          │
│     body: { content_hash, content?, component, tier,              │
│             prompt_hash, framework_version }                      │
│                                                                   │
│  2. Brain răspunde:                                               │
│     {                                                             │
│       hit: true | false,                                          │
│       atom: { tier, result_processed, ... } | null,               │
│       staleness: 'fresh' | 'stale_prompt' | 'stale_framework',    │
│       hit_count_incremented: true                                 │
│     }                                                             │
│                                                                   │
│  3. Branch logic:                                                 │
│     - tier=gold + fresh        → return cached, skip LLM          │
│     - tier=silver + fresh      → return cached, skip LLM          │
│     - tier=silver + stale_prompt → run LLM (prompt changed)       │
│     - miss / bronze            → run LLM normal                   │
│                                                                   │
│  4. Post-LLM (miss path, fire-and-forget):                        │
│     POST /v1/analysis_atom                                        │
│     body: { ..., tier: 'silver' | 'bronze' (if conf<60) }         │
│                                                                   │
│  5. Post-moderation (HIL flow):                                   │
│     PATCH /v1/analysis_atom/:atom_id                              │
│     body: { tier: 'gold', human_validated: true,                  │
│             human_corrections: {...}, validator_user_id }         │
└──────────────────────────────────────────────────────────────────┘

Data Model (în PG-ul brain, NU în DIDI cluster)

Tabel nou analysis_atom

-- Migration brain-side: analysis_atom_v1.sql

CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE analysis_atom (
  atom_id              BIGSERIAL PRIMARY KEY,
  content_hash         TEXT NOT NULL,
  content_embedding    vector(1024),  -- ajustat la dimensiunea modelului brain (poate fi 768/1024/3072)
  content_preview      TEXT,           -- first 200 chars pentru debug, NU full text
  component            TEXT NOT NULL CHECK (component IN ('techniques', 'ai_tampered', 'claims')),
  tier                 TEXT NOT NULL CHECK (tier IN ('free', 'premium')),
  prompt_hash          TEXT NOT NULL,
  framework_version    TEXT NOT NULL,
  model_used           TEXT,                  -- ex: 'qwen35:Qwen3.5-397B-A17B' sau 'openrouter:google/gemini-3-flash-preview'
  result_processed     JSONB NOT NULL,        -- mapped la canonical types din agent-v3 (TechniquesResult, AITamperedResult etc.)
  result_raw           JSONB,                 -- raw LLM output (pentru re-mapping ulterior dacă schimba schema)
  cache_tier           TEXT NOT NULL DEFAULT 'silver'
                          CHECK (cache_tier IN ('gold', 'silver', 'bronze')),
  human_validated      BOOLEAN NOT NULL DEFAULT false,
  human_corrections    JSONB,                 -- diff din moderation (vezi HIL doc)
  validator_user_id    TEXT,
  validated_at         TIMESTAMP,
  hit_count            INTEGER NOT NULL DEFAULT 0,
  last_hit_at          TIMESTAMP,
  llm_confidence       NUMERIC,               -- 0-100, folosit pentru bronze decision
  created_at           TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at           TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  expires_at           TIMESTAMP,             -- TTL 90 zile pentru silver/bronze, NULL pentru gold
  
  UNIQUE (content_hash, component, tier, prompt_hash)
);

-- Indexuri
CREATE INDEX idx_analysis_atom_lookup
  ON analysis_atom(content_hash, component, tier);

CREATE INDEX idx_analysis_atom_embedding
  ON analysis_atom USING ivfflat (content_embedding vector_cosine_ops)
  WITH (lists = 100);

CREATE INDEX idx_analysis_atom_gold
  ON analysis_atom(component, cache_tier)
  WHERE cache_tier = 'gold';

CREATE INDEX idx_analysis_atom_expires
  ON analysis_atom(expires_at)
  WHERE expires_at IS NOT NULL;

-- Trigger pentru updated_at
CREATE TRIGGER trigger_analysis_atom_updated_at
  BEFORE UPDATE ON analysis_atom
  FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();

-- TTL job (pg_cron sau external cleanup zilnic)
-- DELETE FROM analysis_atom WHERE expires_at < NOW() AND cache_tier != 'gold';

result_processed shapes — exemple per component

Pentru component='techniques' (schema TechniquesResult din agent-v3):

{
  "manipulation_score": 65,
  "total_severity": 23.4,
  "dimensions_affected": ["emotional_appeal", "loaded_language"],
  "techniques_count": 4,
  "techniques_detected": [
    { "code": "loaded_language", "intensity": 3, "evidence": "...", "severity": 7.2 }
  ],
  "coupling_context": null,
  "screening_duration_ms": 1200,
  "deep_analysis_duration_ms": 3400
}

Pentru component='ai_tampered':

{
  "ai_probability": 78,
  "verdict": "LIKELY_AI",
  "risk_score": 65,
  "categories_affected": ["text_patterns"],
  "indicators_count": 6,
  "disclosure_detected": false,
  "indicators_detected": [...]
}

Pentru component='claims' (păstrăm compatibilitatea cu schema existentă verification_cache):

  • Minimal: 1 row în analysis_atom cu component='claims' acoperă TOATE claim-urile din analiza acelui content (rezultatul agregat)
  • SAU: continuăm pe schema veche verification_cache per claim individual + un analysis_atom agregator
  • Recomandare: păstrăm verification_cache per-claim cum e (atomar, granular) + adăugăm analysis_atom ca agregator opțional pentru "claims output" (rezultat final post-LLM-extraction)

Cache Tier Logic

Write rules

Trigger: post LLM run în executor
Input: { result, component, tier, content, prompt_hash, framework_version, llm_confidence }

if (component === 'claims') {
  // Claims continuă să folosească verification_cache existent (nu schimbăm)
  // Plus adăugăm un atom agregator pentru "claims output as a whole"
}

confidence = result.confidence ?? estimateConfidence(result);

if (confidence < 60) {
  cache_tier = 'bronze';        // se scrie pentru audit/debug, NU se servește
} else {
  cache_tier = 'silver';        // se scrie și se servește
}

POST /v1/analysis_atom { ..., cache_tier, human_validated: false }

Lookup rules (priority order)

1. Match exact pe (content_hash, component, tier, prompt_hash)
   → if found: check tier
     - gold + fresh → SERVE
     - silver + fresh → SERVE
     - bronze → IGNORE (treat as miss)
     - silver + stale (prompt_hash diferă față de current) → IGNORE pe stale_prompt

2. Match semantic pe content_embedding cu cosine_distance < 0.08
   AND component=X AND tier=Y AND cache_tier='gold'
   AND human_validated=true
   → if found: SERVE (gold supraviețuiește prompt change pentru că răspunsul corect nu depinde de prompt)

3. Match semantic pe embedding < 0.08 AND cache_tier='silver' AND prompt_hash matches
   → if found: SERVE (silver semantic, dar doar pe prompt actual)

4. Miss → return { hit: false }

Promotion rules

silver → gold: când moderator face PATCH cu human_validated=true
                + human_corrections=null (mod a aprobat fără modificări)
              SAU human_corrections!=null (mod a corectat → result_processed actualizat)

bronze → silver: dacă același content e re-analizat și produce confidence ≥ 60
                 → atom-ul vechi bronze e șters, se scrie silver nou

gold → expires never: gold rămâne forever (sau până prompt schimbă fundamental schema, atunci marcaj manual)

API Endpoints (brain side)

POST /v1/analysis_atom/lookup

Request:

{
  "content": "text full optional pentru semantic search",
  "content_hash": "sha256:abc123...",
  "component": "techniques",
  "tier": "free",
  "prompt_hash": "p_v3_2026_03_15",
  "framework_version": "fw_v1_2026_03",
  "allow_semantic_match": true,
  "semantic_threshold": 0.08
}

Response:

{
  "hit": true,
  "atom": {
    "atom_id": 4242,
    "cache_tier": "gold",
    "human_validated": true,
    "result_processed": { ... },
    "model_used": "qwen35:Qwen3.5-397B-A17B",
    "validator_user_id": "admin-123",
    "validated_at": "2026-04-15T12:34:56Z",
    "hit_count": 17
  },
  "staleness": "fresh",
  "match_type": "exact"  // sau "semantic"
}

Sau pe miss:

{ "hit": false, "atom": null, "staleness": null }

POST /v1/analysis_atom

Request:

{
  "content": "full text pentru embedding compute",
  "content_hash": "sha256:abc123...",
  "content_preview": "first 200 chars...",
  "component": "techniques",
  "tier": "free",
  "prompt_hash": "p_v3_2026_03_15",
  "framework_version": "fw_v1_2026_03",
  "model_used": "qwen35:Qwen3.5-397B-A17B",
  "result_processed": { ... },
  "result_raw": { ... },
  "llm_confidence": 78,
  "cache_tier": "silver"   // calculat client-side din confidence
}

Response:

{ "success": true, "atom_id": 4243, "cache_tier": "silver" }

Idempotent: dacă există atom pentru același (content_hash, component, tier, prompt_hash), face UPDATE (sau ignoră dacă cache_tier=gold — nu suprascriu gold cu silver).

PATCH /v1/analysis_atom/:atom_id

Request:

{
  "human_validated": true,
  "human_corrections": { "verdict": {...}, "techniques": {...} },
  "validator_user_id": "admin-123",
  "result_processed": { ... corrected version ... },
  "cache_tier": "gold"
}

Response:

{ "success": true, "atom_id": 4242, "cache_tier": "gold", "expires_at": null }

GET /v1/analysis_atom/stats

Response:

{
  "total_atoms": 12345,
  "by_tier": { "gold": 234, "silver": 11000, "bronze": 1111 },
  "by_component": { "techniques": 4500, "ai_tampered": 4200, "claims": 3645 },
  "hit_rate_24h": 0.42,
  "writes_24h": 850,
  "promotions_24h": { "silver_to_gold": 12, "bronze_to_silver": 3 }
}

Folosit de admin dashboard pentru tracking.


Modificări în agent-v3

1. shared/brain/client.ts (extindere)

// Pseudo-cod, nu pentru implementare directă

// Existent: gatherFromBrain, writeVerificationCacheAsync (pentru claims)
// NOU:

export interface AnalysisAtomLookupResult {
  hit: boolean;
  atom: {
    atom_id: number;
    cache_tier: 'gold' | 'silver' | 'bronze';
    human_validated: boolean;
    result_processed: any;
    hit_count: number;
  } | null;
  staleness: 'fresh' | 'stale_prompt' | 'stale_framework' | null;
}

export async function lookupAnalysisAtom(params: {
  content: string;
  contentHash: string;
  component: 'techniques' | 'ai_tampered' | 'claims';
  tier: 'free' | 'premium';
  promptHash: string;
  frameworkVersion: string;
}): Promise<AnalysisAtomLookupResult | null> {
  if (!process.env.DIDI_BRAIN_URL) return null;
  try {
    const res = await fetch(`${process.env.DIDI_BRAIN_URL}/v1/analysis_atom/lookup`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({...}),
      signal: AbortSignal.timeout(2000),  // brain MUST respond fast
    });
    return await res.json();
  } catch {
    return null;  // fail open — fallback la LLM
  }
}

export function writeAnalysisAtomAsync(params: {
  content: string;
  contentHash: string;
  component: 'techniques' | 'ai_tampered' | 'claims';
  tier: 'free' | 'premium';
  promptHash: string;
  frameworkVersion: string;
  modelUsed: string;
  resultProcessed: any;
  resultRaw: any;
  llmConfidence: number;
}) {
  // Fire-and-forget, nu așteaptă răspuns
  const cacheTier = params.llmConfidence < 60 ? 'bronze' : 'silver';
  fetch(`${process.env.DIDI_BRAIN_URL}/v1/analysis_atom`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ ...params, cacheTier }),
    signal: AbortSignal.timeout(5000),
  }).catch(() => {});  // log warn, ignore
}

export async function patchAnalysisAtomGold(params: {
  atomId: number;
  validatorUserId: string;
  humanCorrections: any | null;
  resultProcessed: any;
}) {
  // Apelat din moderation/queue-manager.ts după resolve
  ...
}

2. components/techniques/executor.ts (modificare)

// La începutul execute():
const promptHash = computePromptHash(systemPrompt, userTemplate);
const frameworkVersion = computeFrameworkVersion();
const contentHash = sha256(input.text).slice(0, 16);

const lookup = await lookupAnalysisAtom({
  content: input.text,
  contentHash,
  component: 'techniques',
  tier: searchTier,
  promptHash,
  frameworkVersion,
});

if (lookup?.hit && lookup.atom) {
  if (lookup.atom.cache_tier === 'gold' || (lookup.atom.cache_tier === 'silver' && lookup.staleness === 'fresh')) {
    // Return cached, skip LLM
    return {
      ...lookup.atom.result_processed,
      _cache_hit: true,
      _cache_tier: lookup.atom.cache_tier,
    };
  }
}

// Miss path — run LLM normal:
const result = await runLLMScreeningAndDeepAnalysis(...);

// Fire-and-forget write back
writeAnalysisAtomAsync({
  content: input.text,
  contentHash,
  component: 'techniques',
  tier: searchTier,
  promptHash,
  frameworkVersion,
  modelUsed: result.llm_screening,
  resultProcessed: result,
  resultRaw: result._raw,
  llmConfidence: estimateConfidence(result),
});

return result;

3. components/ai-tampered/executor.ts (modificare)

Identic cu techniques. Schimbă component: 'ai_tampered'.

4. components/claims/executor.ts (NU modifica)

Claims continuă pe verification_cache existent (granular per-claim). Eventual adăugăm analysis_atom agregator în Faza 2 doar dacă măsurăm și hit rate scăzut pe claims output ca whole.

5. api/moderation-routes.ts resolve handler (din Doc A, dar dependent de brain)

// În handler-ul PUT /api/v3/moderation/queue/:queueId/resolve:

if (action === 'corrected') {
  // ... salvăm corecții în analysis_session ...
  
  // Pentru fiecare componentă afectată în human_corrections, găsim atom-ul corespunzător și PATCH gold
  for (const component of ['techniques', 'ai_tampered', 'claims']) {
    if (corrections[component]) {
      const atomLookup = await lookupAnalysisAtom({...});
      if (atomLookup?.atom) {
        await patchAnalysisAtomGold({
          atomId: atomLookup.atom.atom_id,
          validatorUserId: userId,
          humanCorrections: corrections[component],
          resultProcessed: applyCorrections(atomLookup.atom.result_processed, corrections[component]),
        });
      }
    }
  }
}

if (action === 'approved') {
  // Userul a aprobat fără modificări → ridicăm silver la gold
  // PATCH cu human_corrections=null
  ...
}

Bootstrap Strategy

Brain pornește gol pe atomi. Pentru a accelera hit rate:

Opțiunea A — re-rulare istoric (recomandată)

Avem ~thousands de sesiuni completate în bos_analysis.analysis_session. Putem face un script one-time:

1. SELECT session_id, input_text, techniques (JSONB), ai_tampered (JSONB)
   FROM analysis_session WHERE status='completed' AND created_at > '2026-01-01'
2. Pentru fiecare row, calculează content_hash + apel POST /v1/analysis_atom
   cu cache_tier='silver', human_validated=false
3. Skip dacă confidence < 60 (sau marcaj bronze)

Hit rate jump: 0% → ~25-35% peste noapte.

Opțiunea B — start gol, learn organic

Hit rate creste lent (5% săpt 1 → 25% săpt 4). Mai puțin risc de a propaga erori vechi.

Recomandare: Opțiunea A doar pentru sesiuni completate ≥ 90 zile (probabilitate mare că nu au erori), iar pentru ultimele 90 zile — start gol și lasă organic.


Embedding Model

Brain folosește deja embedding pentru semantic search (vezi /v1/search cu cross-encoder reranking). NU schimbăm modelul. Dimensiunea vector field în PG (vector(1024) în schema) trebuie aliniată cu modelul actual al brain.

Action item: Înainte de migration, query brain SELECT vector_dims(embedding) FROM atom LIMIT 1 (sau echivalent) ca să confirmăm dimensiunea exactă.


Cost & Latency Impact

Cost

Cu hit rate 50% după 3 luni, cost LLM scade ~40-50%. Pentru analiză text simplă premium:

  • Înainte: $0.05/analiză (techniques + ai + claims + verdict)
  • După (50% hits): $0.025-0.030/analiză amortizat

Brain operating cost neglijabil — postgres + pgvector pe mașina existentă (10.11.10.13). Singura cost suplimentar: storage atomi (~ 5KB per atom, 20K atomi/lună = 100MB).

Latency

Scenariu Înainte După
Cache HIT exact n/a ~80-150ms (brain lookup + serialize)
Cache HIT semantic n/a ~120-250ms (vector search adaugat)
Cache MISS 5-15s 5-15s + 100ms (lookup overhead pe miss)

Lookup overhead pe miss path = ~100ms cost net. Acceptabil dacă hit rate ≥ 30%.


Rollout Plan

Faza 1 — Brain schema + endpoints (2 zile)

  • Migration brain-side (analysis_atom tabel + indexuri)
  • Endpoint-uri /v1/analysis_atom/lookup, POST, PATCH, GET stats
  • Test cu Postman: write silver, read back, write gold, lookup match exact + semantic
  • Deploy brain v2 pe staging-ul brain (separate environment dacă există)

Faza 2 — agent-v3 client integration (2 zile)

  • Extindere shared/brain/client.ts cu lookupAnalysisAtom, writeAnalysisAtomAsync, patchAnalysisAtomGold
  • Modificare techniques/executor.ts cu lookup + write back
  • Modificare ai-tampered/executor.ts la fel
  • Feature flag: BRAIN_ATOMS_ENABLED=false default. Activate per env var pentru testing
  • Test E2E: rulează 2x aceeași analiză → a doua vine din cache silver

Faza 3 — HIL integration (depinde de Doc A)

  • Modificare moderation-routes.ts resolve handler să cheme patchAnalysisAtomGold
  • UI badge "Verified by analyst" în extensie + dashboard pentru sesiuni cu atom gold

Faza 4 — Bootstrap (1 zi)

  • Script scripts/bootstrap-brain-from-history.ts pentru re-import sesiuni vechi
  • Run pe sesiuni completate înainte de now() - 90 days
  • Monitor hit rate creste

Faza 5 — Monitoring + tuning (continuous)

  • Dashboard Grafana: hit rate per component, gold/silver ratio, drift detection
  • Alertă: hit rate scade brusc → cineva a schimbat prompt
  • Alertă: bronze ratio crește → modelul produce confidence scăzut, problemă de calitate

Open Questions

  1. Repo brain location: unde e fizic codul brain? Pe ce mașină rulează? Cum se face deploy? (necesar pentru Faza 1)
  2. Brain stack: e FastAPI + Python? Folosește SQLAlchemy/asyncpg? Pgvector deja instalat? (necesar pentru migration)
  3. Embedding model exact: Qwen embed local sau alt model? Dimensiune exactă vector? (necesar pentru vector(N) în schema)
  4. Cross-encoder reranking: brain face deja reranking pe /v1/search. Aplicăm același reranking și pe /v1/analysis_atom/lookup semantic match? Sau e overkill (lookup-ul trebuie să fie fast)?
  5. Atom expiration: TTL 90 zile pentru silver/bronze. Gold = forever. Acceptabil sau prea generos? Putem pune 30 zile silver pentru a forța regenerare cu modele mai noi.

Risk Register

Risc Probabilitate Impact Mitigare
Brain endpoint slow (>500ms) → analize globale lente Medie Mare AbortSignal.timeout(2000) pe lookup. Dacă brain nu răspunde în 2s, fail open la LLM
Embedding drift (model schimbat în brain) Mică Mare Versionare strict embedding_model_id în atom; la schimbare → invalidate tot
Storage atomi explodează Mică Mediu TTL 90 zile + cleanup zilnic; gold păstrat forever (volum mic)
Race condition write atom (2 agenți scriu același content_hash) Medie Mic UPSERT pe UNIQUE constraint (content_hash, component, tier, prompt_hash)
Mock human_corrections invalid (mod scrie diff care nu se aplică) Mică Mare Validare schema diff în resolve handler; dacă invalid, aprobă fără gold write
Brain v2 endpoint break compat cu brain v1 (claims) Mică Mare NU modificăm verification_cache table sau /v1/gather endpoint. analysis_atom e separat

Metrice cheie

  • brain_lookup_total{component, tier, hit} — counter
  • brain_lookup_latency_ms{component, hit} — histogram
  • brain_atoms_count{component, tier, cache_tier} — gauge (din /v1/analysis_atom/stats)
  • brain_writes_total{component, cache_tier} — counter
  • brain_promotions_total{from, to} — counter
  • analyses_with_cache_hit_ratio — derived: hit_total / lookup_total
  • analyses_with_gold_ratio — derived: gold_hits / hit_total

Toate scrise via /metrics endpoint pe agent-v3 + brain.


Dependencies cu Doc A

Brain v2 funcționează STANDALONE pe path-ul silver (LLM cache) FĂRĂ HIL. Beneficiile inițiale (cost reduction 30%) vin de aici, nu din gold.

Gold path (HIL → brain) e benefic doar după ce moderatorii produc atomi gold (~200/lună la 1-2 mod). Beneficiul gold = quality lift + badge "Verified", nu cost reduction.

Concluzie: putem deploy Brain v2 înainte de a avea HIL complet. HIL doar adaugă layer gold deasupra.