18 KiB
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
-- 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:
{
"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)
// 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
// 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
// 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.sqlaplicat manual pe cluster - Rol
moderator+ grupmoderators-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ă
/moderationcu role guard - Sidebar link
- Test E2E pe staging cu user moderator dummy
Faza 3 — Triage activation (1 zi)
- Apel
shouldEnqueueForReview()dinpipeline/executor.tsșiaggregator.ts - Configurabilitate threshold-uri prin Redis (
didi:config:moderation:v1:thresholds) - CRUD UI pentru threshold-uri în admin dashboard (tab nou în
/llm-componentssau pagina nouă) - Auto-tuning cron (zilnic 03:00 UTC)
Faza 4 — Brain integration (vezi Doc B)
- Brain v2 deployment +
/v1/analysis_atomendpoint - Modificare
resolveendpoint 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/flagactivat - Modificare extensie browser (separate repo) să adauge buton "Report"
- Rate limiting: max 5 flag-uri/zi/user pentru a preveni abuz
Open Questions
- Cine devine primul moderator? Email-ul lui tehnic@finesynergy.eu? Trebuie creat user separat cu rol moderator?
- Notificări: când nou enqueue → email/Slack către moderatori? (opțional, putem face polling UI auto-refresh inițial)
- Escalation logic: dacă mod simplu marchează "uncertain", sesiunea trece la senior? (faza 2 sau mai târziu)
- 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? - 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 50moderation_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-ultriage_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)
resolveendpoint 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.