Livrare LOT 1 - Didi

This commit is contained in:
Dezvoltari Evotech 2026-06-25 14:13:25 -07:00
commit 5380c3fc63
990 changed files with 133308 additions and 0 deletions

View file

@ -0,0 +1,398 @@
# 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:**
```json
{
"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-397B-A17B",
"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:**
```json
{
"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, evidence_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ă):
```json
{
"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:
```json
{
"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-397B-A17B",
"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:
```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`
```python
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`
```python
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, evidence_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:
```javascript
// 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
```typescript
// Î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)
```bash
# 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.