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

13 KiB

Verification Cache — Contract didi-brain ↔ didi-backend

Status: IMPLEMENTAT v2 și live la http://10.11.10.13:8090. Backend-side trebuie să implementeze apelurile descrise mai jos.

Schimbare majoră față de v1 (2026-04-23): cheia de cache NU mai include evidence_hash. Cheia e acum (claim_hash, tier). URL-urile evidence rămân stocate ca metadata și sunt returnate în response ca verification_evidence_urls — backend decide overlap cu evidence-ul curent.

De ce: URL-urile pe care le avea backend-ul la WRITE-time rareori coincid cu URL-urile pe care brain le întoarce la READ-time (brain face semantic search în corpus-ul propriu, backend avea URL-uri din M17/web sau din altă rulare). Cu evidence_hash în cheie, cache-ul era practic nereachable.


Principiu

  • Backend deține prompt-ul + modelul + logica de status/thresholds.
  • Brain stochează rezultatul LLM ca blob opac și îl returnează 1:1.
  • Zero sync de prompt între backend și brain. Zero duplicare de lucru LLM.

Ciclul complet per claim

┌─ gather (cu verification cache check) ─────────────────────────────┐
│                                                                    │
│  backend → POST http://10.11.10.13:8090/v1/gather                  │
│    body: { claim, max_evidence, include_verification: true,        │
│            tier, prompt_hash, framework_version }                  │
│                                                                    │
│  brain → returnează evidence + brain_meta cu:                      │
│    ├─ verification_staleness: fresh | stale_framework |            │
│    │                           stale_prompt | miss                 │
│    └─ verification: { ... } sau null                                │
│                                                                    │
│  backend decide:                                                   │
│    fresh          → folosește verification DIRECT la UI             │
│    stale_framework → ia raw, RECOMPUTĂ status local (zero LLM)     │
│    stale_prompt   → LLM call + POST /v1/verification_cache         │
│    miss           → LLM call + POST /v1/verification_cache         │
│                                                                    │
└────────────────────────────────────────────────────────────────────┘

Endpoints

1. POST /v1/verification_cache (WRITE)

URL: http://10.11.10.13:8090/v1/verification_cache

Request body:

{
  "claim": "Romania a câștigat 9 medalii olimpice la Paris 2024.",
  "evidence_urls": [
    "https://adevarul.ro/articol1",
    "https://somes-tisa.rowater.ro/pdf2"
  ],
  "tier": "free",
  "model": "qwen35:qwen3.5",
  "prompt_hash": "a3f2b1c9d4e7",
  "framework_version": "f9e1d2c3b4a5",
  "schema_name": "didi-v1",

  "verification_raw": {
    "sources_analysis": [
      {"url": "...", "stance": "SUPPORTS", "reliability": "news", "relevant_quote": "..."}
    ],
    "agreement_score": 60,
    "confidence": 75,
    "status": "LT",
    "reasoning": "..."
  },

  "verification_processed": {
    "id": "claim_1",
    "text": "...",
    "status": "LT",
    "status_name_ro": "Probabil Adevărat",
    "confidence": 75,
    "agreement_score": 100,
    "sources": [...],
    "reasoning": "..."
  }
}

Required: claim, evidence_urls (non-empty), tier, prompt_hash (≥4 chars), verification_processed.

Optional: model, framework_version, schema_name (default "didi-v1"), verification_raw.

Response 200:

{
  "cached": true,
  "claim_hash":    "9b769fa8caba883...",
  "evidence_hash": "d04318eec4bfc01...",
  "tier": "free",
  "created_at": "2026-04-23T11:01:22.607213Z",
  "updated_at": "2026-04-23T11:01:22.607213Z",
  "expires_at": "2026-05-23T11:01:22.598217Z"
}

Errors:

  • 413 Payload Too Large — body > 64 KB
  • 422 Unprocessable Entity — validare pydantic (tier != free/premium, prompt_hash < 4 chars, etc.)
  • 503 Service Unavailable — PG indisponibil, retry mai târziu
  • 500 Internal Server Error — orice alt eșec

Idempotență: UPSERT pe cheia (claim_hash, tier). Same-tuple, second write → update verification_processed/verification_raw/updated_at/expires_at. Last-writer-wins.


2. POST /v1/gather (READ extins)

URL: http://10.11.10.13:8090/v1/gather

Request body: exact ca azi, plus 4 câmpuri noi (toate optional; default behavior nu se schimbă):

{
  "claim": "...",
  "max_evidence": 5,
  "run_nli": false,

  "include_verification": true,     // NEW — opt-in, default false
  "tier": "free",                    // REQUIRED când include_verification=true
  "prompt_hash": "a3f2b1c9d4e7",    // optional, dar recomandat
  "framework_version": "f9e1d2c3b4a5"  // optional
}

Response body: exact ca azi, plus brain_meta îmbogățit:

{
  "claim": "...",
  "evidence": [ ... ],                 // URL-uri din corpus-ul brain — pot diferi
  "brain_meta": {                      // de cele din verification_evidence_urls
    "cache_status": "HIT",
    "evidence_sources": 5,

    "verification_staleness": "fresh",
    "verification": { ... payload stocat ... },
    "verification_model": "qwen35:qwen3.5",
    "verification_tier": "free",
    "verification_prompt_hash": "a3f2b1c9d4e7",
    "verification_framework_version": "f9e1d2c3b4a5",
    "verification_cached_at": "2026-04-23T11:01:22.607213Z",
    "verification_expires_at": "2026-05-23T11:01:22.598217Z",

    "verification_evidence_urls": [     // URL-urile pe care a fost rulat LLM-ul
      "https://adevarul.ro/articol1",   // când a fost scris cache-ul.
      "https://somes-tisa.rowater.ro/pdf2"  // Backend poate compara cu
    ],                                  // evidence[].url și decide dacă cache-ul
    "verification_evidence_hash": "d04318eec4bfc01..."  // mai e relevant.
  }
}

Important: verificare overlap evidence (backend-side)

URL-urile din evidence[] (ce returnează gather din corpus) pot diferi de brain_meta.verification_evidence_urls (ce a primit LLM-ul la WRITE). Backend decide ce face:

  • Overlap >= 60% (recomandat): folosește verification direct; sources din verification acoperă cele mai multe evidence din evidence[].
  • Overlap mic/zero: corpusul brain s-a schimbat; poți trata ca miss și să rulezi LLM din nou. Brain returnează staleness=fresh din perspectiva prompt/framework, dar tu evaluezi semantic dacă sursele mai sunt relevante.

Exemplu TypeScript:

function evidenceOverlap(a: string[], b: string[]): number {
  const norm = (u: string) => u.trim().toLowerCase().replace(/\/$/, '');
  const setA = new Set(a.map(norm));
  const setB = new Set(b.map(norm));
  const inter = [...setA].filter(x => setB.has(x)).length;
  return inter / Math.max(setA.size, setB.size);
}

const gatherEvidence = data.evidence.map(e => e.url);
const cachedFor = data.brain_meta.verification_evidence_urls || [];
if (evidenceOverlap(gatherEvidence, cachedFor) >= 0.6) {
  // use cached verification
} else {
  // treat as miss despite staleness=fresh
}

Semantica verification_staleness

Staleness Când apare verification conține Ce face backend
fresh prompt + framework identice cu cache verification_processed folosește direct la UI
stale_framework prompt identic, framework diferit verification_raw recomputează status local din raw (zero LLM)
stale_prompt prompt diferit null LLM call + POST cache
miss nu există entry sau a expirat null LLM call + POST cache

Reguli de hashing (toate calculate server-side de brain)

Backend trimite plain claim și evidence_urls. Brain normalizează și hash-uiește. Backend nu trebuie să computeze nimic.

Normalizare claim

def normalize_claim(s):
    s = unicodedata.normalize("NFKD", s)                   # decompunere diacritice
    s = "".join(c for c in s if not unicodedata.combining(c))  # strip diacritice
    s = s.lower()                                           # lowercase
    s = " ".join(s.split())                                # collapse whitespace
    s = s.rstrip(".?!")                                    # trim trailing punctuation
    return s

Aceleași hash:

  • "România a câștigat 9 medalii."
  • "romania a castigat 9 medalii"
  • "Romania a câștigat 9 medalii!"

Hash-uri diferite (corect):

  • "Vaccinul e sigur" vs "Vaccinul nu e sigur" (negație contează)
  • "X are 10 mașini" vs "X are 11 mașini" (numere contează)

Normalizare evidence_urls

def hash_evidence_urls(urls):
    canonical = sorted({u.strip().lower().rstrip("/") for u in urls if u})
    return sha256("\n".join(canonical)).hexdigest()
  • Ordinea URL-urilor nu contează (sortate).
  • Case nu contează (HTTPS://X.COM == https://x.com).
  • Trailing / nu contează.
  • Duplicate sunt eliminate.

Isolation pe tier

Cache pe free și premium sunt complet separate. Un user free nu vede ce a scris un user premium (și invers). Este intenționat: modele diferite = nuanțe diferite la stance.

Cheia unică în DB: (claim_hash, tier).


TTL

Default 30 zile per entry. Reîmprospătat la fiecare UPSERT. După expirare, rândul există încă în DB dar e invizibil la lookup (filtrat cu expires_at > now()). Pruning ulterior — nu e critic pentru corectitudine.


Cum calculezi prompt_hash și framework_version backend-side

Recomandarea ta, pe care o respect în brain:

// prompt_hash — schimbă la orice editare a prompt-ului (auto-invalidation)
const promptData = await redis.get('didi:config:claims:v1:prompts:verification');
const { system, user_template } = JSON.parse(promptData);
const promptHash = sha256(system + '|' + user_template).slice(0, 12);

// framework_version — hash peste thresholds + scoring config
const frameworkData = await redis.get('didi:framework:claims');
const scoringData = await redis.get('didi:config:claims:v1:scoring_config');
const frameworkVersion = sha256(frameworkData + '|' + scoringData).slice(0, 12);

Calculat în loadConfigs() și cached în memorie până la următorul reload.


Exemplu complet flow backend-side

// În searchEvidence / verifySingleClaim:

async function verifyClaim(claim, evidence, tier) {
  const evidenceUrls = evidence.map(e => e.url);

  // 1. Check brain cache via gather extension
  const brainResp = await fetch(`${DIDI_BRAIN_URL}/v1/gather`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      claim,
      max_evidence: evidence.length,
      run_nli: false,
      include_verification: true,
      tier,
      prompt_hash: await getPromptHash(),
      framework_version: await getFrameworkVersion(),
    }),
    signal: AbortSignal.timeout(5000),
  });
  const data = await brainResp.json();
  const meta = data.brain_meta || {};

  if (meta.verification_staleness === 'fresh') {
    // Direct la UI — zero LLM, zero overhead
    return meta.verification;
  }

  if (meta.verification_staleness === 'stale_framework') {
    // Backend recomputează din raw (thresholds noi)
    const raw = meta.verification;  // verification_raw
    return {
      ...buildProcessedFromRaw(raw),  // aplică calculateStatusFromSources
      reasoning: raw.reasoning,
    };
  }

  // 2. miss sau stale_prompt — LLM call propriu
  const llmResponse = await callVerificationLLM(claim, evidence, tier);
  const processed = buildProcessedFromRaw(llmResponse);

  // 3. Fire-and-forget push la brain
  fetch(`${DIDI_BRAIN_URL}/v1/verification_cache`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      claim,
      evidence_urls: evidenceUrls,
      tier,
      model: modelUsed,
      prompt_hash: await getPromptHash(),
      framework_version: await getFrameworkVersion(),
      verification_raw: llmResponse,
      verification_processed: processed,
    }),
    signal: AbortSignal.timeout(3000),
  }).catch(() => {});  // silent failure

  return processed;
}

Smoke tests (poți rula oricând)

# Write test
curl -sf -X POST http://10.11.10.13:8090/v1/verification_cache \
  -H 'Content-Type: application/json' \
  -d '{
    "claim": "test claim",
    "evidence_urls": ["https://example.com/a"],
    "tier": "free",
    "prompt_hash": "test_abc_12",
    "verification_processed": {"status": "LT", "confidence": 80}
  }' | jq

# Read test (should be fresh)
curl -sf -X POST http://10.11.10.13:8090/v1/gather \
  -H 'Content-Type: application/json' \
  -d '{
    "claim": "test claim",
    "include_verification": true,
    "tier": "free",
    "prompt_hash": "test_abc_12"
  }' | jq '.brain_meta.verification_staleness, .brain_meta.verification'

Config brain-side

Azi:

  • VERIFICATION_CACHE_TTL_DAYS=30
  • VERIFICATION_CACHE_MAX_PAYLOAD_KB=64

Ajustabil via .env dacă e nevoie.