Livrare LOT 1 - Didi
This commit is contained in:
commit
5380c3fc63
990 changed files with 133308 additions and 0 deletions
398
ai_platform/modules/didi_brain/CONTRACT_VERIFICATION_CACHE.md
Normal file
398
ai_platform/modules/didi_brain/CONTRACT_VERIFICATION_CACHE.md
Normal 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue