# 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; } // 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 ``. 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.