livrare lot 2
This commit is contained in:
commit
8ecc78e729
763 changed files with 164593 additions and 0 deletions
|
|
@ -0,0 +1,421 @@
|
|||
# HIL Moderation System — Design Doc
|
||||
|
||||
Status: DRAFT 2026-04-30
|
||||
Owner: tehnic@finesynergy.eu
|
||||
Companion doc: BRAIN_V2_DESIGN.md (atomi knowledge — separat)
|
||||
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Adăugăm un layer Human-in-the-Loop peste pipeline-ul existent: 1-2 moderatori validează/corectează 10-20 sesiuni/zi (cele cu confidence scăzut, în topicuri sensibile sau flagged de useri). Verdictul final livrat userului e instant; corecțiile vin post-fapt și se reflectă în extensie/dashboard cu badge "Verified by analyst". Corecțiile validate alimentează brain v2 (knowledge cache).
|
||||
|
||||
**Out of scope** pentru acest doc: tot ce ține de brain (atomi, embeddings, cache propagation). Vezi BRAIN_V2_DESIGN.md.
|
||||
|
||||
---
|
||||
|
||||
## Decizii agreate
|
||||
|
||||
| # | Decizie |
|
||||
|---|---|
|
||||
| 1 | SLA = instant cu corecție post-fapt. User vede verdictul în 5s; corecțiile sunt async |
|
||||
| 2 | Triage v1 strict (10-20 sesiuni/zi pentru 1-2 moderatori). Auto-tunable |
|
||||
| 3 | Operational data (coadă, status, audit) în DIDI PG schema `bos_analysis` — NU în brain |
|
||||
| 4 | Brain primește atomi gold doar la trigger din moderation (`POST /v1/analysis_atom` PATCH cu `human_validated=true`) |
|
||||
| 5 | Modificări UI într-un modul nou `admin-dashboard/src/components/Moderation/`, NU subtab |
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Pipeline existent (neschimbat) │
|
||||
│ POST /api/v3/pipeline/analyze │
|
||||
│ → ComponentRunner.runAll() → VerdictCalculator → persist │
|
||||
│ → response 200 cu AnalysisSession (instant, ≤5s) │
|
||||
└────────────────────────────┬────────────────────────────────────┘
|
||||
│ după persist
|
||||
▼
|
||||
┌────────────────────────────────────┐
|
||||
│ triage.shouldEnqueueForReview() │ ← nou
|
||||
│ (apelat din pipeline/executor.ts) │
|
||||
└────────────────┬───────────────────┘
|
||||
needs_review?
|
||||
┌────┴────┐
|
||||
yes no
|
||||
│ │
|
||||
▼ (nimic — sesiunea e finală)
|
||||
INSERT INTO moderation_queue
|
||||
(priority, session_id, status='pending')
|
||||
|
||||
┌─────────────────────────────────────┐
|
||||
│ Moderator UI (/moderation) │
|
||||
│ - listează queue │
|
||||
│ - opens detail │
|
||||
│ - approve / edit / reject │
|
||||
└────────────┬────────────────────────┘
|
||||
│
|
||||
┌───────────────────────┴──────────────────────┐
|
||||
│ PUT /api/v3/moderation/queue/:id/resolve │
|
||||
│ body: { action, corrections, notes } │
|
||||
└─────────────────────┬────────────────────────┘
|
||||
│
|
||||
┌─────────────┼──────────────┐
|
||||
▼ ▼ ▼
|
||||
UPDATE analysis_session UPDATE POST brain
|
||||
(human_corrected=true, moderation_ /v1/analysis_atom
|
||||
human_corrections={...}, queue (PATCH gold)
|
||||
verified_by, verified_at) (status= (vezi doc B)
|
||||
'resolved')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Model
|
||||
|
||||
### Schema modifications — `bos_analysis`
|
||||
|
||||
```sql
|
||||
-- Migration: agent-v3/sql/migrations/010_add_moderation.sql
|
||||
|
||||
-- 1. Coloane noi pe analysis_session pentru a trace human review
|
||||
ALTER TABLE bos_analysis.analysis_session
|
||||
ADD COLUMN review_status TEXT DEFAULT 'none'
|
||||
CHECK (review_status IN ('none', 'pending', 'in_review', 'resolved', 'declined')),
|
||||
ADD COLUMN human_corrected BOOLEAN DEFAULT false,
|
||||
ADD COLUMN human_corrections JSONB NULL,
|
||||
ADD COLUMN verified_by TEXT NULL, -- keycloak_id moderator
|
||||
ADD COLUMN verified_at TIMESTAMP NULL,
|
||||
ADD COLUMN review_notes TEXT NULL;
|
||||
|
||||
CREATE INDEX idx_analysis_session_review_status
|
||||
ON bos_analysis.analysis_session(review_status)
|
||||
WHERE review_status != 'none';
|
||||
|
||||
-- 2. Tabel nou: queue moderare
|
||||
CREATE TABLE bos_analysis.moderation_queue (
|
||||
queue_id BIGSERIAL PRIMARY KEY,
|
||||
session_id TEXT NOT NULL REFERENCES bos_analysis.analysis_session(session_id) ON DELETE CASCADE,
|
||||
priority INTEGER NOT NULL DEFAULT 5, -- 1=highest (user_flagged), 5=lowest (low_confidence)
|
||||
enqueue_reason TEXT NOT NULL, -- 'low_confidence', 'flagged', 'sensitive_topic', 'mixed'
|
||||
enqueue_meta JSONB NULL, -- raw scores, topic detected, flag reason
|
||||
status TEXT NOT NULL DEFAULT 'pending'
|
||||
CHECK (status IN ('pending', 'in_review', 'resolved', 'declined', 'auto_closed')),
|
||||
assigned_to TEXT NULL, -- keycloak_id moderator
|
||||
assigned_at TIMESTAMP NULL,
|
||||
resolved_at TIMESTAMP NULL,
|
||||
resolved_by TEXT NULL,
|
||||
resolution_action TEXT NULL -- 'approved' (no change), 'corrected', 'rejected'
|
||||
CHECK (resolution_action IS NULL OR resolution_action IN ('approved', 'corrected', 'rejected')),
|
||||
time_in_queue_ms INTEGER NULL, -- enqueue → start review
|
||||
time_in_review_ms INTEGER NULL, -- start review → resolved
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_moderation_queue_status_priority
|
||||
ON bos_analysis.moderation_queue(status, priority, created_at)
|
||||
WHERE status IN ('pending', 'in_review');
|
||||
|
||||
CREATE INDEX idx_moderation_queue_session
|
||||
ON bos_analysis.moderation_queue(session_id);
|
||||
|
||||
CREATE INDEX idx_moderation_queue_assigned
|
||||
ON bos_analysis.moderation_queue(assigned_to)
|
||||
WHERE status = 'in_review';
|
||||
```
|
||||
|
||||
### `human_corrections` JSONB shape
|
||||
|
||||
Diff-style — doar ce s-a schimbat, NU întreaga sesiune:
|
||||
|
||||
```json
|
||||
{
|
||||
"verdict": {
|
||||
"risk_score": { "from": 67, "to": 45 },
|
||||
"risk_category": { "from": "QUESTIONABLE", "to": "MIXED" },
|
||||
"severity": { "from": "MEDIUM", "to": "LOW" }
|
||||
},
|
||||
"techniques": {
|
||||
"removed": ["false_dilemma_42"],
|
||||
"added": [],
|
||||
"score_override": { "from": 78, "to": 55 }
|
||||
},
|
||||
"ai_tampered": null,
|
||||
"claims": {
|
||||
"status_changes": [
|
||||
{ "claim_id": "c1", "from": "UNVERIFIED", "to": "VERIFIED_TRUE" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Permite UI să arate "ce a corectat moderatorul" + permite brain să primească diff pentru gold atom.
|
||||
|
||||
---
|
||||
|
||||
## Triage Logic
|
||||
|
||||
Locație: `agent-v3/src/components/moderation/triage.ts` (modul nou)
|
||||
|
||||
```typescript
|
||||
// Pseudo-cod, nu pentru implementare directă
|
||||
|
||||
interface TriageInput {
|
||||
session: AnalysisSession;
|
||||
userFlagged: boolean; // din input request, opțional
|
||||
}
|
||||
|
||||
interface TriageOutput {
|
||||
needsReview: boolean;
|
||||
priority: 1 | 2 | 3 | 4 | 5;
|
||||
reason: 'flagged' | 'low_confidence' | 'sensitive_topic' | 'mixed' | 'none';
|
||||
meta: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// Reguli (citite din Redis pentru a putea ajusta fără rebuild)
|
||||
const SENSITIVE_TOPICS = ['elections', 'health', 'war', 'covid']; // configurable
|
||||
const CONFIDENCE_LOW_THRESHOLD = 50; // configurable
|
||||
const RISK_GREY_ZONE = [45, 60]; // configurable
|
||||
const QUEUE_AUTO_RELAX_THRESHOLD = 50; // dacă pending > 50, drop topic filter
|
||||
const QUEUE_AUTO_STRICT_THRESHOLD = 5; // dacă pending < 5, ridica confidence threshold
|
||||
|
||||
function shouldEnqueueForReview(input: TriageInput): TriageOutput {
|
||||
// 1. Flagged de user — priority maxim, întotdeauna
|
||||
if (input.userFlagged) {
|
||||
return { needsReview: true, priority: 1, reason: 'flagged', meta: {...} };
|
||||
}
|
||||
|
||||
// 2. Confidence foarte scăzut — priority mediu
|
||||
if (input.session.confidence < CONFIDENCE_LOW_THRESHOLD) {
|
||||
return { needsReview: true, priority: 3, reason: 'low_confidence', meta: { confidence: input.session.confidence } };
|
||||
}
|
||||
|
||||
// 3. Risk în zona gri + topic sensibil — priority scăzut (dar nenul)
|
||||
const inGreyZone = RISK_GREY_ZONE[0] <= input.session.risk_score && input.session.risk_score <= RISK_GREY_ZONE[1];
|
||||
const isSensitive = SENSITIVE_TOPICS.includes(input.session.topic_applied);
|
||||
if (inGreyZone && isSensitive) {
|
||||
return { needsReview: true, priority: 4, reason: 'sensitive_topic', meta: {...} };
|
||||
}
|
||||
|
||||
// 4. Default — nu intră în review
|
||||
return { needsReview: false, priority: 5, reason: 'none', meta: {} };
|
||||
}
|
||||
|
||||
// Auto-tuning daily cron (apelat din didi-framework sau scheduler nou)
|
||||
async function autoTuneTriageThresholds() {
|
||||
const pending = await query("SELECT COUNT(*) FROM moderation_queue WHERE status='pending'");
|
||||
if (pending > QUEUE_AUTO_RELAX_THRESHOLD) {
|
||||
// Drop SENSITIVE_TOPICS filter — doar low_confidence + flagged se mai pun în coadă
|
||||
} else if (pending < QUEUE_AUTO_STRICT_THRESHOLD) {
|
||||
// Lower CONFIDENCE_LOW_THRESHOLD la 65 — mai multe sesiuni intră
|
||||
}
|
||||
// Salvează în Redis: didi:config:moderation:v1:thresholds
|
||||
}
|
||||
```
|
||||
|
||||
Triage e apelat din `pipeline/executor.ts` DUPĂ ce sesiunea e persistată (sync trec, async după aggregator finalizează).
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints (agent-v3)
|
||||
|
||||
Locație: `agent-v3/src/api/moderation-routes.ts` (modul nou)
|
||||
|
||||
```
|
||||
GET /api/v3/moderation/queue
|
||||
?status=pending&priority=1,2,3&assigned_to=me&limit=20&offset=0
|
||||
→ { items: [{ queue_id, session_id, priority, reason, created_at, age_minutes }], total }
|
||||
|
||||
GET /api/v3/moderation/queue/:queueId
|
||||
→ { queue_entry, session: AnalysisSession (full) }
|
||||
|
||||
POST /api/v3/moderation/queue/:queueId/claim
|
||||
→ { success, assigned_to, assigned_at }
|
||||
(atomic claim: UPDATE ... WHERE status='pending' RETURNING ...)
|
||||
|
||||
PUT /api/v3/moderation/queue/:queueId/resolve
|
||||
body: {
|
||||
action: 'approved' | 'corrected' | 'rejected',
|
||||
corrections?: HumanCorrectionsDiff, // doar dacă action='corrected'
|
||||
notes?: string,
|
||||
trigger_brain_write?: boolean // default true pe 'corrected', false pe 'approved'
|
||||
}
|
||||
→ { success, session_updated, brain_atom_written }
|
||||
|
||||
POST /api/v3/moderation/flag
|
||||
body: { session_id, user_id, reason: 'wrong_verdict' | 'missing_techniques' | 'other', notes? }
|
||||
→ { success, queue_id }
|
||||
(apelat de extensia browser când user dă click pe "report")
|
||||
|
||||
GET /api/v3/moderation/stats
|
||||
?period=7d
|
||||
→ {
|
||||
pending_count,
|
||||
resolved_today: { count, avg_time_ms },
|
||||
resolved_period: { count, by_action: { approved, corrected, rejected } },
|
||||
avg_corrections_per_session,
|
||||
brain_writes_period: { gold, silver }
|
||||
}
|
||||
```
|
||||
|
||||
Auth: toate rutele necesită JWT cu rol `moderator` sau `senior_moderator`. Excepție `/flag` — necesită doar JWT user normal.
|
||||
|
||||
---
|
||||
|
||||
## Keycloak
|
||||
|
||||
Realm `didi-clients` modificare:
|
||||
|
||||
```
|
||||
Roluri noi:
|
||||
- moderator (poate face claim/resolve pe queue)
|
||||
- senior_moderator (poate face escalate, override decisions)
|
||||
|
||||
Grup nou:
|
||||
- moderators-team (atribuit roluri: moderator)
|
||||
- senior-moderators-team (atribuit roluri: moderator + senior_moderator)
|
||||
```
|
||||
|
||||
Verificare middleware în `moderation-routes.ts`: read JWT, check `realm_access.roles.includes('moderator')`. Pe orice ruta nu-moderator → 403.
|
||||
|
||||
---
|
||||
|
||||
## UI — Admin Dashboard
|
||||
|
||||
Locație: `admin-dashboard/src/components/Moderation/` (modul nou)
|
||||
|
||||
```
|
||||
Moderation/
|
||||
├── ModerationQueue.tsx # /moderation
|
||||
│ - Listă paginată cu filtre (priority, reason, age, assigned_to=me|all)
|
||||
│ - Click pe row → ModerationDetail
|
||||
│ - Auto-refresh la 30s
|
||||
│
|
||||
├── ModerationDetail.tsx # /moderation/:queueId
|
||||
│ - Side-by-side:
|
||||
│ * Stânga: input (text/url/media), metadata sesiune
|
||||
│ * Dreapta: tab-uri Verdict / Techniques / AI / Claims
|
||||
│ - Pe fiecare tab: fields editabile (toggle technique on/off, change claim status)
|
||||
│ - Buton "Claim review" → POST /claim
|
||||
│ - Butoane finale: "Approve as is" | "Save corrections" | "Reject (low quality input)"
|
||||
│
|
||||
├── ModerationStats.tsx # /moderation/stats
|
||||
│ - Cards: pending count, resolved today, avg time, brain writes
|
||||
│ - Chart: trend 7d (recharts)
|
||||
│
|
||||
├── api.ts # fetch helpers
|
||||
└── index.ts # exports
|
||||
```
|
||||
|
||||
App.tsx adaugă rută `/moderation` cu `<ProtectedRoute requiredRole="moderator">`.
|
||||
|
||||
ServicesDashboard.tsx (sidebar): adaugă link "Moderation" dacă userul are rol moderator.
|
||||
|
||||
---
|
||||
|
||||
## Modificări în agent-v3 (existing files)
|
||||
|
||||
### 1. `pipeline/executor.ts` — apel triage post-persist
|
||||
|
||||
```typescript
|
||||
// Diff conceptual
|
||||
|
||||
import { shouldEnqueueForReview } from '../moderation/triage';
|
||||
import { enqueueForReview } from '../moderation/queue-manager';
|
||||
|
||||
// În execute(), după PersistService.persist():
|
||||
const triage = shouldEnqueueForReview({ session, userFlagged: input.userFlagged ?? false });
|
||||
if (triage.needsReview) {
|
||||
await enqueueForReview({ session_id: session.session_id, ...triage });
|
||||
}
|
||||
return session;
|
||||
```
|
||||
|
||||
### 2. `api/pipeline-routes.ts` — primește user_flagged opțional
|
||||
|
||||
```typescript
|
||||
// Body request /api/v3/pipeline/analyze
|
||||
{ text, user_id, media_type, user_flagged?: boolean }
|
||||
```
|
||||
|
||||
### 3. `queue/aggregator.ts` (pentru flow async) — apel triage post-aggregation
|
||||
|
||||
Identic cu pipeline/executor.ts dar în path-ul async. Triage se apelează DUPĂ ce verdict-ul e calculat și persistat, indiferent dacă e sync sau async.
|
||||
|
||||
---
|
||||
|
||||
## Rollout Plan
|
||||
|
||||
### Faza 1 — Foundation (1-2 zile)
|
||||
- [ ] Migration SQL `010_add_moderation.sql` aplicat manual pe cluster
|
||||
- [ ] Rol `moderator` + grup `moderators-team` în Keycloak (realm import)
|
||||
- [ ] Modul nou `agent-v3/src/components/moderation/` (triage + queue-manager) — fără triage activ încă
|
||||
- [ ] Modul nou `agent-v3/src/api/moderation-routes.ts` — endpoint-uri minimale (queue list, detail, claim, resolve fără brain write)
|
||||
- [ ] Test endpoint-uri cu Postman/curl
|
||||
|
||||
### Faza 2 — UI (2-3 zile)
|
||||
- [ ] `admin-dashboard/src/components/Moderation/` — Queue + Detail + Stats
|
||||
- [ ] Rută `/moderation` cu role guard
|
||||
- [ ] Sidebar link
|
||||
- [ ] Test E2E pe staging cu user moderator dummy
|
||||
|
||||
### Faza 3 — Triage activation (1 zi)
|
||||
- [ ] Apel `shouldEnqueueForReview()` din `pipeline/executor.ts` și `aggregator.ts`
|
||||
- [ ] Configurabilitate threshold-uri prin Redis (`didi:config:moderation:v1:thresholds`)
|
||||
- [ ] CRUD UI pentru threshold-uri în admin dashboard (tab nou în `/llm-components` sau pagina nouă)
|
||||
- [ ] Auto-tuning cron (zilnic 03:00 UTC)
|
||||
|
||||
### Faza 4 — Brain integration (vezi Doc B)
|
||||
- [ ] Brain v2 deployment + `/v1/analysis_atom` endpoint
|
||||
- [ ] Modificare `resolve` endpoint să cheme brain PATCH cu human_validated=true
|
||||
- [ ] Test end-to-end: corecție mod → atom gold în brain → analiză repetată → return cached gold
|
||||
|
||||
### Faza 5 — User flag în extensie
|
||||
- [ ] Endpoint `/api/v3/moderation/flag` activat
|
||||
- [ ] Modificare extensie browser (separate repo) să adauge buton "Report"
|
||||
- [ ] Rate limiting: max 5 flag-uri/zi/user pentru a preveni abuz
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Cine devine primul moderator?** Email-ul lui tehnic@finesynergy.eu? Trebuie creat user separat cu rol moderator?
|
||||
2. **Notificări**: când nou enqueue → email/Slack către moderatori? (opțional, putem face polling UI auto-refresh inițial)
|
||||
3. **Escalation logic**: dacă mod simplu marchează "uncertain", sesiunea trece la senior? (faza 2 sau mai târziu)
|
||||
4. **Soft delete vs hard delete pe `rejected`**: dacă moderator marchează "low quality input" (ex: text gibberish), sesiunea rămâne dar marcată rejected sau dispare din istoricul user-ului?
|
||||
5. **Privacy**: moderatorul vede textul original integral. Dacă e date personale (nume, adrese) — trebuie redactare? Nu acum, dar în roadmap.
|
||||
|
||||
---
|
||||
|
||||
## Metrice de monitorizat
|
||||
|
||||
- `moderation_queue_pending` (gauge) — alertă peste 50
|
||||
- `moderation_queue_avg_age_minutes` (gauge) — alertă peste 1440 (24h lag)
|
||||
- `moderation_resolutions_per_day` (counter, by action)
|
||||
- `moderation_corrections_per_session_avg` — semnal cât de bine se descurcă AI-ul
|
||||
- `triage_enqueue_rate` — % din sesiuni care intră în coadă (target 1-5%)
|
||||
|
||||
Toate scrise în Prometheus prin endpoint `/metrics` din agent-v3 (sau via PG query în Grafana).
|
||||
|
||||
---
|
||||
|
||||
## Risk register
|
||||
|
||||
| Risc | Probabilitate | Impact | Mitigare |
|
||||
|---|---|---|---|
|
||||
| Coada explodează (1-2 mod insuficient) | Medie | Mare | Auto-tune triage descrescător + alert pending>50 |
|
||||
| Mod corectează inconsistent → bad gold atoms | Medie | Mediu | Faza inițială: toate corecțiile cer al 2-lea acord (slowness ok pe bootstrap); după 200 atoms gold colectate, 1-mod e suficient |
|
||||
| User abuz pe `/flag` | Mică | Mic | Rate limit 5/zi/user + auto-decline pe sesiuni cu >3 flag-uri rezolvate ca approved (mod a zis e ok) |
|
||||
| Schema PG migration fail pe cluster | Mică | Mare | Migration testat pe staging, rollback script pregătit |
|
||||
| Race condition pe queue claim | Mică | Mic | UPDATE ... WHERE status='pending' RETURNING ... e atomic în PG |
|
||||
|
||||
---
|
||||
|
||||
## Dependencies cu Doc B (Brain v2)
|
||||
|
||||
- `resolve` endpoint apelează brain PATCH `/v1/analysis_atom` — depinde de Brain v2 deploy
|
||||
- Faza 1-3 din rollout NU depind de brain (queue funcționează standalone)
|
||||
- Faza 4 e gating pentru beneficii cost-saving + cached results
|
||||
|
||||
Brain v2 poate fi deployed în paralel cu Faza 1-3.
|
||||
Loading…
Add table
Add a link
Reference in a new issue