23 KiB
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_atomcu component='claims' acoperă TOATE claim-urile din analiza acelui content (rezultatul agregat) - SAU: continuăm pe schema veche
verification_cacheper claim individual + unanalysis_atomagregator - Recomandare: păstrăm
verification_cacheper-claim cum e (atomar, granular) + adăugămanalysis_atomca 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_atomtabel + 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.tsculookupAnalysisAtom,writeAnalysisAtomAsync,patchAnalysisAtomGold - Modificare
techniques/executor.tscu lookup + write back - Modificare
ai-tampered/executor.tsla fel - Feature flag:
BRAIN_ATOMS_ENABLED=falsedefault. 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.tsresolve handler să chemepatchAnalysisAtomGold - UI badge "Verified by analyst" în extensie + dashboard pentru sesiuni cu atom gold
Faza 4 — Bootstrap (1 zi)
- Script
scripts/bootstrap-brain-from-history.tspentru 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
- Repo brain location: unde e fizic codul brain? Pe ce mașină rulează? Cum se face deploy? (necesar pentru Faza 1)
- Brain stack: e FastAPI + Python? Folosește SQLAlchemy/asyncpg? Pgvector deja instalat? (necesar pentru migration)
- Embedding model exact: Qwen embed local sau alt model? Dimensiune exactă vector? (necesar pentru
vector(N)în schema) - Cross-encoder reranking: brain face deja reranking pe
/v1/search. Aplicăm același reranking și pe/v1/analysis_atom/lookupsemantic match? Sau e overkill (lookup-ul trebuie să fie fast)? - 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}— counterbrain_lookup_latency_ms{component, hit}— histogrambrain_atoms_count{component, tier, cache_tier}— gauge (din /v1/analysis_atom/stats)brain_writes_total{component, cache_tier}— counterbrain_promotions_total{from, to}— counteranalyses_with_cache_hit_ratio— derived: hit_total / lookup_totalanalyses_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.