didi-lot2-backend/backend/services/orchestration-layer/agent-v3/HIL_MODERATION_DESIGN.md
2026-07-10 03:39:53 -07:00

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.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.