64 KiB
Implementation Plan — HIL Moderation + Brain v2 Integration
Status: NOT STARTED Started: — Last Updated: 2026-04-30 Owner: tehnic@finesynergy.eu
Companion documents
HIL_MODERATION_DESIGN.md— design queue + UI moderareBRAIN_V2_DESIGN.md— design atomi brain + tier-uri (cu corecție: tier NU în UNIQUE, write doar premium)
How to resume from this file
If a new chat session starts, the new assistant should:
- Read this file FIRST, completely
- Read
HIL_MODERATION_DESIGN.mdandBRAIN_V2_DESIGN.md - Look at the "Status & Progress Tracking" section below — find first unchecked
[ ]task in current phase - Verify the actual codebase state matches what's marked completed (DON'T trust this file blindly —
git log, file contents, DB schema) - Continue from first unchecked task; update
[ ]→[x]as work progresses
If a task description seems stale (e.g., file structure changed since written), the new assistant should:
- Read the current file structure with
Read/Bashtools - Update the task description in this file BEFORE implementing
- Note "✏️ Updated 2026-XX-XX: reason" inline
⚠️ CRITICAL — DO NOT BREAK
These are existing flows that MUST keep working at every step. Any change that breaks them is a stop-the-world bug.
Existing endpoints that MUST keep working
POST /api/v3/pipeline/analyze— sync analyze, returnsAnalysisSessionflatPOST /api/v3/pipeline/analyze-async— returns 202 + poll URLPOST /api/v3/{techniques,ai-tampered,claims}/analyze— individual componentsGET /api/v3/pipeline/{sessionId}/status— pollingGET /api/v3/pipeline/{sessionId}/result— final resultGET /api/v3/pipeline/history— user historyGET /api/v3/pipeline/history/admin— admin history- All
/agent-v3/*endpoints called from admin dashboard
Existing data flows that MUST keep working
- agent-v3 reads framework config from Redis keys
didi:framework:*anddidi:config:* - agent-v3 writes session state to Redis
didi:pipeline:*(TTL 7d) and PGbos_analysis.* - didiFramework
POST /api/sync-rediswrites Redis from PG - Claims component continues to use
verification_cachein brain (existing path, not modified) - All worker queues (techniques, ai_tampered, claims, domain, media-preprocess, aggregator)
Feature flags as safety net
EVERY new code path must check a feature flag from Redis. If flag is OFF, code path is bypassed and the OLD behavior runs. Flags are managed by bos_parammgmt.moderation_config table:
triage_enabled(default false → start OFF, activate after testing)brain_enabled(default false → start OFF)brain_per_component.{techniques,ai_tampered,claims}(per-component toggle)
Rollback strategy per phase
Each phase ends with a deployment checkpoint. If the next phase breaks something, we roll back BY:
- Setting feature flag = false in
moderation_config(no code revert needed, just SQL UPDATE) - If schema change breaks queries: pre-prepared rollback migration
010_rollback.sql - If new code breaks startup:
git revertlast commit, redeploy previous image
Architecture overview (1-paragraph recap)
User → web app → agent-v3 → 4 component workers in parallel. Each worker BEFORE running LLM does brain lookup. On hit → return cached, skip LLM. On miss → run LLM + fire-and-forget write atom. After verdict aggregator persists, triage decides if session needs review. If yes, INSERT moderation_queue. Moderator opens admin dashboard /moderation, claims session, edits if needed, resolves. On resolve with corrections, brain atom is PATCHed to gold tier with human_validated=true. Next user analyzing same content → hit gold atom → return verified-by-analyst result instantly.
ZERO config hardcoded — everything in PG bos_parammgmt.moderation_config + sensitive_topic + moderation_role, synced to Redis. Editable via admin dashboard tab "Moderation Settings".
Separation: didi side (HIL, queue, triage, brain client) is implemented HERE. Brain side (atomi storage, lookup endpoints, brain admin UI) is implemented in AI platform repo (separate). Contracts in BRAIN_V2_DESIGN.md.
Status & Progress Tracking
Format: [ ] = not started, [~] = in progress, [x] = completed, [!] = blocked, [s] = skipped (not needed)
Update format: when changing status, also append → YYYY-MM-DD note if useful for resume context.
TRACK 1 — Didi Side (independent of brain)
This track delivers a working HIL moderation system without brain integration. Even with brain OFF, queue + UI work standalone.
PHASE 1.1 — Database foundation (estimated 0.5 day) — ✅ DONE 2026-05-01
Goal: All schema changes applied to cluster PG. New tables seeded with defaults. Old tables get new columns. Rollback migration ready.
RESULT: Migration 011_add_moderation.sql (NOTE: 011 not 010 — 010 was already taken by add_user_storage_quota). Applied to cluster 10.11.50.167:5000 DB DIDI. Dry-run confirmed clean before commit. Smoke tests on existing endpoints pass.
Task 1.1.1 — Write migration 011_add_moderation.sql
- File:
didiFramework/sql/migrations/011_add_moderation.sql(NEW, 14.5KB)- Note: lives in
didiFramework/sql/migrations/, NOTagent-v3/sql/migrations/— that's where existing migrations are.
- Note: lives in
- Contents per design doc:
- 6 columns added to
bos_analysis.analysis_session(review_status, human_corrected, human_corrections, verified_by, verified_at, review_notes) with CHECK constraint on review_status bos_analysis.moderation_queuetable — FK toanalysis_session(session_id)(UUID type, important: not TEXT)- 3 indexes on moderation_queue (status_priority, session, assigned)
bos_parammgmt.moderation_configsingle-row table with CHECK(config_id=1) — 13 fields covering triage + brain client + auditbos_parammgmt.sensitive_topicwith 5 seed rows (elections, health, war, covid, climate)bos_parammgmt.moderation_rolewith 2 seed rows (moderator, senior_moderator)- Triggers
set_updated_aton all 3 config tables
- 6 columns added to
- Companion file:
011_rollback.sql(NEW, 2.5KB) — drops everything in reverse order - Dry-run validated (BEGIN+ROLLBACK trick) before commit
- Applied to cluster, verified all rows seeded correctly
- Smoke tested existing endpoints (
/api/admin/users,/api/verdicts/runtime-config) — both still respond OK
Resume context:
- Both migration files exist on disk and have been applied to cluster.
moderation_config.triage_enabled=falseandbrain_enabled=false— feature flags OFF as planned.- DB schema ready for Phase 1.2 (CRUD endpoints in didiFramework).
Gotcha discovered: analysis_session.session_id is UUID type, not TEXT. FK on moderation_queue.session_id MUST be UUID NOT NULL REFERENCES. Initial design assumed TEXT — corrected before apply.
Validation:
# Apply on cluster — manual, never automated
docker exec didi-framework node -e "
const fs=require('fs'); const {Pool}=require('pg');
const pool=new Pool({host:'10.11.50.167',port:5000,user:'bos_interface',password:'interface',database:'DIDI'});
pool.query(fs.readFileSync('/path/to/010_add_moderation.sql','utf8')).then(r=>{console.log('OK');pool.end();}).catch(e=>{console.error(e);pool.end();});
"
Validation checks:
\d+ bos_analysis.analysis_sessionshows 6 new columnsSELECT * FROM bos_parammgmt.moderation_configreturns 1 row with defaultsSELECT COUNT(*) FROM bos_parammgmt.sensitive_topic WHERE is_active=truereturns 5SELECT * FROM bos_parammgmt.moderation_rolereturns 2 rows
Rollback:
- Apply
010_rollback.sql. All changes are additive — no data loss on rollback (review_status drops to default 'none' on old sessions).
Risk: LOW. Pure schema additions, no destructive ops.
PHASE 1.2 — didiFramework: config CRUD endpoints (estimated 0.5 day) — ✅ DONE 2026-05-01
Goal: Admin can read/write moderation config via REST API. Sync-redis extended to push new keys to Redis.
RESULT: 3 routes files created + mounted + sync-redis extended. All 4 endpoints tested live (GET/PUT/POST/DELETE all working). Validation tested (rejects bad input). Sync-redis now writes 3 new keys (config_keys count rose from 48 to 51). Existing endpoints still respond.
Task 1.2.1 — Create routes/moderation-config.ts
- File created.
UPDATABLE_FIELDSwhitelist (14 fields), validates body is object, returns 400 if no allowed fields, dynamic SET clause builder, auditupdated_byfrom headerx-user-id. PUT response includes "Sync to Redis to apply" message.
Task 1.2.2 — Create routes/sensitive-topics.ts
- File created. Full CRUD with
?active=true|false|allfilter, regex validation on topic_code ([a-z0-9_]+), 409 conflict on duplicate (PG code 23505), 404 on missing id, soft delete (is_active=false).
Task 1.2.3 — Create routes/moderation-roles.ts
- File created. GET list, PUT only on
:code(immutable PK). Toggle fields whitelist + label validation. role_code IS immutable.
Task 1.2.4 — Mount routes in server.ts
- 3 imports + 3 mounts under "Category 14: HIL Moderation Config". Existing routes untouched.
Task 1.2.5 — Extend sync-redis
- Block added BEFORE
pipeline.exec()— wrapped in try/catch (skips silently if migration 011 not applied). Writes:didi:config:moderation:v1:settings(full row from moderation_config)didi:config:moderation:v1:sensitive_topics(active topics only)didi:config:moderation:v1:roles(all roles with permissions)
Live test results:
GET /api/moderation-config→ 200 with full rowGET /api/sensitive-topics→ 200, 5 active topicsGET /api/moderation-roles→ 200, 2 rolesPUT /api/moderation-config→ 200, updates persistPOST /api/sensitive-topics→ 201, validation worksPUT /api/moderation-roles/:code→ 200, toggles workDELETE /api/sensitive-topics/:id→ 200, soft delete- Validation rejects: empty body, bad regex codes, non-boolean toggles
POST /api/sync-redis→keys_written: 59, config_keys: 51(was 48 before)- All existing endpoints (
/api/admin/users,/api/verdicts/runtime-config) still respond OK
Resume context:
- All 3 endpoints respond at port 3005.
- Redis cluster has the 3 new keys after sync.
- Container
didi-frameworkrunning healthy. - Ready for Phase 1.3 (admin dashboard UI tab "Moderation Settings").
Validation:
# After PUT
curl -s http://localhost:3005/api/moderation-config | jq '.data.confidence_low'
# Should match what was PUT
curl -s -X POST http://localhost:3005/api/sync-redis
# Then check Redis:
redis-cli -h 10.11.50.100 -p 16379 -a <pass> --no-auth-warning GET didi:config:moderation:v1:settings | jq
Validation checks:
- GET returns full config including all defaults
- PUT with valid body updates DB
- PUT with invalid body returns 400 with error message
- Sync-redis includes new keys in response manifest
- Redis key contains JSON with all settings after sync
Risk: LOW. New routes, no modification to existing behavior.
PHASE 1.3 — Admin dashboard: "Moderation Settings" tab (estimated 1 day) — ✅ DONE 2026-05-01
Goal: User can edit all moderation config from UI. Triage rules, brain client params (CLIENT only — server config lives in AI platform dashboard), sensitive topics list, roles permissions.
RESULT: Module components/ModerationSettings/ (6 files) mounted as 6th toggle button in /llm-components. Auto-sync to Redis after every save. Dashboard rebuilt + container didi-admin recreated. All endpoints respond via nginx proxy.
Task 1.3.1 — Create components/ModerationSettings/
- Folder created (NEW), 6 files:
index.tsx— main panel: Refresh + Sync buttons, 4 cards stacked vertically, Snackbar for successTriageCard.tsx— toggle, slider for confidence_low, TextFields for risk_grey min/max + queue thresholds, auto-tune toggleBrainClientCard.tsx— toggle, URL TextField, timeouts (lookup/write), sliders (confidence_min_silver, semantic_threshold), per-component checkboxes. Includes Alert: "These are CLIENT settings — brain server config lives in AI platform dashboard"SensitiveTopicsCard.tsx— chip list with delete icon, add form (code+label), show inactive toggle, soft delete + reactivateRolesCard.tsx— table with 4 toggle columns (can_resolve, can_escalate, can_force_gold_brain, is_active)api.ts— fetch helpers + types matching backend
- Auto-sync after save: every save handler calls
handleSync()automatically — user sees "Config saved. Syncing to Redis…" snackbar without manual button press - Manual sync button still available top-right for explicit re-sync
Task 1.3.2 — Mount in /llm-components
LLMComponentsConfig/index.tsxmodified:- Added imports:
ModerationIcon(Shield) andModerationSettingscomponent - Extended
ComponentTypeunion with'moderation' - Added entry in
COMPONENT_CONFIGS['moderation'](color #0288d1, ShieldIcon) - Added
'moderation': ''inSAMPLE_TEXTS(not used, has own component) - Added
'moderation'toanalysisComponentsarray → 6th toggle button rendered - Added render branch:
selectedComponent === 'moderation' ? <ModerationSettings /> : ...
- Added imports:
- Existing 5 tabs (Techniques/AI/Claims/Source/Verdict) unchanged
Validation results (all ✅):
- TypeScript clean (preexisting warnings only, not from my changes)
- Image built
didi-admin:latest, container recreated, healthy https://10.11.10.12:3000/admin/→ 200https://10.11.10.12:3000/framework/api/moderation-config→ 200 (proxy)https://10.11.10.12:3000/framework/api/sensitive-topics→ 200 (proxy)https://10.11.10.12:3000/framework/api/moderation-roles→ 200 (proxy)
Resume context:
- Open
/admin/llm-components, click "Moderation" (last toggle). - 4 cards visible. All editable. Saves auto-sync.
- Feature flags
triage_enabledandbrain_enabledare still false — agent-v3 behavior unchanged. - Ready for Phase 1.4 (triage logic + queue manager in agent-v3).
Risk: LOW (UI only, no breaking changes).
PHASE 1.4 — agent-v3: triage + queue manager (estimated 1 day) — ✅ DONE 2026-05-01
Goal: After every analysis finalizes (sync or async), triage decides if session enters moderation queue. All thresholds in Redis (no hardcode). Triage must NEVER block analysis on failure.
RESULT: 2 new modules + 4 files modified. Live E2E test verified: with triage_enabled=true, confidence_low=95, an analysis with confidence=42 was correctly enqueued (queue_id=1, reason=low_confidence, priority=3, status=pending). With triage_enabled=false, no enqueue occurs. Existing endpoints unchanged.
Files created:
agent-v3/src/components/moderation/triage.ts— pure decision function with Redis config + 60s TTL cache. Returns{needsReview, priority, reason, meta}. Disabled flag → always returnsneedsReview=false. Refresh helperinvalidateTriageCache()for admin.agent-v3/src/components/moderation/queue-manager.ts— PG ops:enqueueForReview,listQueue,getQueueEntry,claimQueueEntry,resolveQueueEntry,getQueueStats. Idempotent enqueue (checks for existing pending/in_review row before INSERT). Updatesanalysis_session.review_statusin same transaction.
Files modified:
pipeline/executor.ts— calls triage AFTERpersistService.persist(session). Wrapped in try/catch — triage failure logs warning and continues.queue/aggregator.ts— same wire-up in async path after persist.queue/types.ts— addeduserFlagged?: booleantoSessionState.components/pipeline/types.ts— addeduserFlagged?: booleantoPipelineInput.api/pipeline-routes.ts— acceptsuser_flaggedin body, propagates toPipelineInput.userFlagged.
E2E Test results:
- Triage OFF → analyze runs → 0 queue rows ✅
- Triage ON, confidence_low=95 → analyze runs → 1 queue row with priority=3, reason=low_confidence ✅
analysis_session.review_statusupdated to 'pending' ✅- Triage timing: ~10ms (Redis read cached after first call)
- Total pipeline time unchanged (~71s for full LLM analysis), triage adds <50ms
Resume context:
- Modules in place, wired, tested.
- Feature flag
triage_enabled=falseagain (default safe). - Migration 011 applied, 4 endpoints + sync-redis working, UI tab live.
- Ready for Phase 1.5 (REST API moderation in agent-v3 — endpoints
/api/v3/moderation/*).
Risk: MEDIUM (touches pipeline hot path). Mitigation: try/catch wrapper, feature flag, Redis cache fallback to no-op. Verified live.
Goal: After every analysis, triage logic decides if session enters moderation queue. Logic reads thresholds from Redis (no hardcode).
Task 1.4.1 — Create components/moderation/triage.ts
- File:
agent-v3/src/components/moderation/triage.ts(NEW) - Function
shouldEnqueueForReview(session, userFlagged): TriageOutput- Reads config from Redis key
didi:config:moderation:v1:settings(cached in memory with 60s TTL) - Reads sensitive topics from
didi:config:moderation:v1:sensitive_topics - Returns
{ needsReview, priority, reason, meta }
- Reads config from Redis key
- Function
loadTriageConfigFromRedis()— refresh cache - Tests: 5 unit tests covering each branch (flagged, low_confidence, sensitive_topic, none, disabled)
- If
triage_enabled=falsein config, returns{needsReview: false}always
Task 1.4.2 — Create components/moderation/queue-manager.ts
- File:
agent-v3/src/components/moderation/queue-manager.ts(NEW) - Functions:
enqueueForReview(sessionId, priority, reason, meta)— INSERT INTO moderation_queue + UPDATE analysis_session.review_status='pending'claimQueueEntry(queueId, userId)— atomic UPDATE with WHERE status='pending' RETURNINGresolveQueueEntry(queueId, action, corrections, notes, userId)— UPDATE queue + analysis_sessiongetQueueStats()— counts pending/in_review by priority
- All functions take a PG pool from existing
pg-pool.ts(don't create new connection)
Task 1.4.3 — Wire triage into pipeline executor (sync flow)
- File:
agent-v3/src/components/pipeline/executor.ts(MODIFY) - After
PersistService.persist(...), call:const triage = await shouldEnqueueForReview({ session, userFlagged: input.userFlagged }); if (triage.needsReview) await enqueueForReview(session.session_id, ...); - WRAP in try/catch — triage failure must NOT fail the analysis
- If catch fires, log warning + continue (degraded mode)
Task 1.4.4 — Wire triage into aggregator (async flow)
- File:
agent-v3/src/queue/aggregator.ts(MODIFY) - After persist, identical try/catch pattern as 1.4.3
Task 1.4.5 — Accept userFlagged in request body
- File:
agent-v3/src/api/pipeline-routes.ts(MODIFY) - Accept optional
user_flagged: booleanin body of/analyzeand/analyze-async - Default false. Pass through to PipelineInput / dispatcher.
- Document in API_GUIDE.md if exists
Validation:
# Make moderation_config triage_enabled=false in DB
# Run analyze → check moderation_queue is empty
# Set triage_enabled=true, sync-redis
# Run analyze with text designed to be low-confidence → check queue has 1 row
Validation checks:
- With triage_enabled=false: zero queue entries created
- With triage_enabled=true: queue gets entries for low-confidence sessions
- Triage failure (e.g., Redis down) does NOT fail analysis
- user_flagged=true in body → priority=1 entry in queue
Risk: MEDIUM. New code path in critical pipeline. Mitigation: try/catch around triage, feature flag.
PHASE 1.5 — agent-v3: moderation REST API (estimated 0.5 day) — ✅ DONE 2026-05-01
Goal: REST endpoints for queue list/detail/claim/resolve/flag/stats. Used by admin UI (Phase 1.6) and browser extension (Phase 5).
RESULT: New api/moderation-routes.ts with 6 endpoints. JWT roles middleware extended. All endpoints tested live with realistic flow (insert → get → claim → resolve). Validation tested. Existing endpoints still respond.
Files created:
agent-v3/src/api/moderation-routes.ts— 6 endpoints, soft role check (permits in staging without JWT, enforces in prod with JWT roles)
Files modified:
agent-v3/src/index.ts— addedjwtRoles?: string[]to Request interface, populated frompayload.realm_access.roles. Mounted/api/v3/moderation.
Endpoints live:
GET /api/v3/moderation/queue— paginated, filters: status, priority (CSV), assigned_to (or 'me'), limit/offsetGET /api/v3/moderation/queue/:queueId— entry + full session (with all component LEFT JOINs)POST /api/v3/moderation/queue/:queueId/claim— atomic claim (UPDATE pending→in_review)PUT /api/v3/moderation/queue/:queueId/resolve— body:{action, corrections?, notes?, user_id?}POST /api/v3/moderation/flag— any authenticated user (extension report); validates session exists; idempotent (won't double-enqueue)GET /api/v3/moderation/stats— pending, in_review, resolved_24h, by_priority, avg_time_in_queue_ms
E2E live test results:
GET /queueempty list → 200GET /statszeroed → 200- Inserted fake queue entry, then:
GET /queue/3→ 200 with full detailPOST /queue/3/claim→ 200, status=in_review, assigned_to=mod-testPUT /queue/3/resolveaction=approved → 200, status=resolved
POST /flag→ 200, returns queue_id=4- Validations:
action='bogus'→ 400 with clear messageaction='corrected'withoutcorrections→ 400flagreason='spam' → 400 (not in allowed list)
- Final smoke:
/api/v3/health→ 200 (no regression)
Resume context:
- 6 endpoints respond on port 24803 + via nginx proxy at
/agent-v3/api/v3/moderation/* - Role check is permissive in staging (works without JWT roles) and strict in prod (Kong sets jwtRoles)
correctionsJSONB diff is just stored — applied to brain only in Phase 1.8- Ready for Phase 1.6 (admin dashboard
/moderationpage).
Risk: LOW (new routes, no modification to existing pipeline behavior).
Goal: Endpoints for queue list, detail, claim, resolve, stats. Used by admin UI.
Task 1.5.1 — Create api/moderation-routes.ts
- File:
agent-v3/src/api/moderation-routes.ts(NEW) - Endpoints (per
HIL_MODERATION_DESIGN.mdAPI section):GET /api/v3/moderation/queue— paginated list with filtersGET /api/v3/moderation/queue/:queueId— detail (queue entry + full session)POST /api/v3/moderation/queue/:queueId/claim— assign to current userPUT /api/v3/moderation/queue/:queueId/resolve— body: action, corrections, notes, trigger_brain_writePOST /api/v3/moderation/flag— for browser extension user reportsGET /api/v3/moderation/stats— counts + averages
- Auth middleware: extract JWT roles, check includes
moderator(orsenior_moderatorfor escalate). Reject 403 otherwise. - For
/flagendpoint: any authenticated user, NOT moderator-only - In resolve handler:
- On
action='approved'and corrections=null: nothing to brain (yet — phase 4) - On
action='corrected': save corrections to analysis_session.human_corrections (already wired); brain PATCH happens in phase 4
- On
Task 1.5.2 — Mount in index.ts
- File:
agent-v3/src/index.ts(MODIFY) - Add import +
app.use('/api/v3/moderation', moderationRoutes) - DO NOT touch existing mounts
Validation:
- curl with bad JWT → 401
- curl with user JWT (no moderator role) → 403
- curl with moderator JWT → 200, gets queue
- Claim then resolve flow works end-to-end on a test session
Risk: LOW (new routes).
PHASE 1.6 — Admin dashboard: /moderation page (estimated 2 days) — ✅ DONE 2026-05-01
Goal: Moderator opens /moderation, sees pending queue, opens session detail, claims, resolves with action+corrections+notes. Stats dashboard at /moderation/stats.
RESULT: 4 files created in new Moderation/ folder, 2 modifications (App.tsx + AdminLayout.tsx). MVP delivered: queue list with filters/pagination, detail page with side-by-side input+verdict + Resolve dialog (action, corrections JSON, notes), stats page with cards + by-priority breakdown. JSON corrections panel is intentionally minimal (free-form JSON edit) — bogát Edit panels per component (techniques toggle, claims status changer) are deferred to follow-up MR if needed.
Files created:
admin-dashboard/src/components/Moderation/api.ts— fetch helpers + types + UI helpers (priorityLabel, priorityColor, statusColor, ageMinutes)admin-dashboard/src/components/Moderation/ModerationQueue.tsx— paginated table, filters (status: pending/in_review/resolved/all + priority CSV), auto-refresh 30s on pending tab, click row → detailadmin-dashboard/src/components/Moderation/ModerationDetail.tsx— back button, action buttons (Claim / Approve / Resolve with corrections / Reject), side-by-side cards (Input + AI Verdict), Queue metadata card, Resolve dialog with action toggle + JSON corrections + notesadmin-dashboard/src/components/Moderation/ModerationStats.tsx— 3 stat cards (pending, in_review, resolved_24h) + by-priority chips + avg time in queue, auto-refresh 30s
Files modified:
admin-dashboard/src/App.tsx— 3 new imports + 3 routes inside ProtectedRoute/AdminLayout (/moderation,/moderation/stats,/moderation/:queueId)admin-dashboard/src/components/layout/AdminLayout.tsx— addedShield as ModerationIconimport + sidebarListItemButtonunder "Management" group
Live test results:
https://10.11.10.12:3000/admin/→ 200https://10.11.10.12:3000/admin/moderation→ 200https://10.11.10.12:3000/admin/moderation/stats→ 200/agent-v3/api/v3/moderation/queuevia proxy → 200/agent-v3/api/v3/moderation/statsvia proxy → 200- E2E: inserted fake queue entry → appears in list → detail loads with full JSON
- Cleanup: queue empty after test
Resume context:
- 4 routes wired, sidebar link visible always (not role-gated yet — Phase 1.7 adds Keycloak role enforcement)
- All test paths working through nginx → didi-framework / agent-v3
- Resolve form supports approved + corrected (JSON) + rejected
- Ready for Phase 1.7 (Keycloak roles setup) — admin/moderator user, role-gated sidebar visibility
Risk: LOW (UI only, no breaking changes to existing routes).
Goal: Moderator opens /moderation, sees pending queue, opens session, edits, resolves.
Task 1.6.1 — Create components/Moderation/ folder
- Folder:
admin-dashboard/src/components/Moderation/(NEW) - Files:
ModerationQueue.tsx— paginated table with filtersModerationDetail.tsx— split view (input | verdict editable)ModerationStats.tsx— cards + chartsEditVerdictPanel.tsx— sub-component for verdict overridesEditTechniquesPanel.tsx— toggle techniques on/offEditAITamperedPanel.tsx— verdict + indicators toggleEditClaimsPanel.tsx— per-claim status changerapi.ts— fetch helperstypes.ts— TypeScript types matching backend
Task 1.6.2 — Add routes in App.tsx
- File:
admin-dashboard/src/App.tsx(MODIFY) - Add 3 routes:
/moderation→ ModerationQueue/moderation/:queueId→ ModerationDetail/moderation/stats→ ModerationStats
- Wrap each in
<ProtectedRoute requiredRole="moderator">
Task 1.6.3 — Add sidebar link
- File:
admin-dashboard/src/components/dashboard/ServicesDashboard.tsx(MODIFY) or wherever sidebar lives - Add link "Moderation" with badge showing pending count
- Show only if user has role
moderator - Auto-refresh badge count every 30s (poll
/api/v3/moderation/stats)
Task 1.6.4 — Detail page edit logic
- When moderator changes a field (toggle technique off, change claim status, override risk_score), local state captures the diff
- On "Save corrections" click, build
human_correctionsJSONB diff - PUT
/api/v3/moderation/queue/:id/resolvewithaction='corrected', corrections=diff, trigger_brain_write=true - Show loading state, then redirect to queue list with success toast
Validation:
- Moderator login → sees Moderation in sidebar
- Click Moderation → list of pending sessions
- Click a session → detail view loads
- Edit a technique toggle → state changes locally, dirty flag shows
- Click Save → resolve API call, queue refreshes, that entry now has status=resolved
Risk: MEDIUM (complex UI). Mitigation: ship in 2 sub-PRs (queue list first, then detail page).
PHASE 1.7 — Keycloak roles setup (estimated 0.25 day) — ✅ DONE 2026-05-01
Goal: Realm didi-clients on SSO cluster (<sso-extern> public / <sso-extern-admin> admin) has 2 new roles + 2 groups + 1 test user assigned. Sidebar link "Moderation" gated by role in admin dashboard.
RESULT: 2 realm roles created via Admin API, 2 groups created with proper role mappings, admin@didi.local user got both moderator + senior_moderator roles. Sidebar gating wired in AdminLayout.tsx. In staging mode (REACT_APP_STAGING_MODE=true), hasRole() always returns true → link visible to all users for testing. In prod, only users with moderator|senior_moderator|admin see the link.
SSO cluster credentials discovered:
- Public URL:
https://<sso-extern>/realms/didi-clients - Admin URL:
https://<sso-extern-admin>(needs internal DNS, resolves to 10.11.10.171) - Master credentials:
admin / admin123(frombackend/production/.env) - Realm:
didi-clients
Operations performed via Admin API:
- Created realm role
moderator(id=6552fc4e-...) - Created realm role
senior_moderator(id=e0b18224-...) - Created group
moderators-team(id=8a692f87-...) → has[moderator] - Created group
senior-moderators-team(id=0e3bae14-...) → has[moderator, senior_moderator] - Assigned both roles to user
admin@didi.local(id=2c0f074b-97f1-4779-91a5-6b5b5bc1da8c) — same user used in earlier tests
Files modified:
admin-dashboard/src/components/layout/AdminLayout.tsx:- Added
Shield as ModerationIconimport const canModerate = hasRole('moderator') || hasRole('senior_moderator') || isAdmin- Wrapped Moderation
<ListItemButton>in{canModerate && (...)}
- Added
Verification (live):
- API:
GET /admin/realms/didi-clients/roles→ 12 roles total (was 10), includes moderator + senior_moderator - API:
GET /admin/realms/didi-clients/groups→ 6 groups total (was 4) - User admin@didi.local → realm roles:
[moderator, viewer, analyst, api_user, admin, enterprise_tier, senior_moderator] - Admin dashboard rebuilt, container recreated and healthy
/admin/moderation→ 200 (visible because staging mode)
NOT modified:
realm-import/didi-clients-realm.json(local container artifact, no longer used since 2026-04-29 migration to SSO cluster — already deprecated)- agent-v3 routes — already use
req.jwtRolesarray populated from JWT (Phase 1.5 setup)
Resume context:
- Roles are LIVE on SSO cluster, will persist across SSO restarts
- admin@didi.local already had admin role; now also has moderator + senior_moderator (additive, non-destructive)
- For prod role gating to work, need to ensure JWT issued to admin user includes the new roles in
realm_access.rolesarray — Keycloak does this automatically on next token refresh
Risk: VERY LOW. Additive operations only. Did not modify any existing role/group/user.
Goal: Roles moderator and senior_moderator exist in Keycloak realm. Test user assigned.
- Realm
didi-clients— Add 2 roles via admin console or realm-import:moderatorsenior_moderator
- Add 2 groups:
moderators-team,senior-moderators-team - Assign 1 test user to
moderators-team(e.g., test@didi.local) - Verify JWT contains role on next login: decode JWT, check
realm_access.roles - Document: who is real moderator? Add row in
moderation_roletable for each Keycloak role
Validation:
- Login as moderator → frontend gets JWT with role
- Login as normal user → frontend gets JWT without role
- /moderation route 200 for moderator, 403 for normal
Risk: LOW.
PHASE 1 — END-OF-TRACK CHECKPOINT
After phases 1.1-1.7, the deliverable is:
- ✅ Triage runs after every analysis (configurable, default OFF until tested)
- ✅ Queue auto-fills based on rules in PG (editable from UI)
- ✅ Moderator opens UI, claims, edits verdict, resolves
- ✅ Resolved corrections saved to
analysis_session.human_corrections(PG) - ❌ Brain integration NOT yet — that's Phase 1.8
At this checkpoint, system has full HIL workflow but doesn't propagate to brain yet. Operationally useful already (analysts can correct verdicts internally).
Deployment validation before moving to Phase 1.8:
- Run 50 test analyses through pipeline
- Verify: ones matching triage rules end up in queue
- Verify: moderator can resolve all of them
- Verify: NO existing tests fail
- Verify: latency on
/analyzenot increased (triage adds <50ms)
PHASE 1.8 — Brain client integration in agent-v3 (estimated 1 day) — ✅ DONE 2026-05-01
Goal: Wire brain client into techniques/ai_tampered executors + moderation resolve handler. Feature flag controls activation.
Files modified:
agent-v3/src/shared/brain/client.ts— addedcomputeContentHash,loadBrainConfig,lookupAnalysisAtom,writeAnalysisAtomAsync,patchAnalysisAtomGold, typesAtomComponent,AtomCacheTier,AtomStaleness, etc. Config read from Redis (60s cache).agent-v3/src/components/techniques/executor.ts— brain lookup at start ofexecute(), write atom on both happy path AND early-exit path.agent-v3/src/components/ai-tampered/executor.ts— same pattern.agent-v3/src/api/moderation-routes.ts—promoteAtomsToGold()helper called from resolve handler. Iterates components, finds matching atom, PATCHes to gold.
Bug found + fixed during E2E: writeAnalysisAtomAsync was called only on the happy path (after buildFinalResult). Early-exit branches (buildEmptyResult for both techniques and ai_tampered) returned BEFORE the write call → no atoms written for non-manipulation content. Fixed by duplicating write call in early-exit branches.
Resume context:
- Brain config in
moderation_config.brain_urlset tohttp://10.11.10.12:8090(local brain for testing); change to production brain when ready. - Test user
2c0f074b-...switched to plan_type=4 (premium) for E2E testing — needs to be reverted post-test to plan_type=1. - Atoms written tier-isolated by content (read tier-agnostic, write only tier=premium).
Risk: MEDIUM. Touches hot path (executors). Mitigation: try/catch on lookup, fire-and-forget on write, feature flag, 2s lookup timeout, fail-open to LLM.
E2E test results (full HIL → gold cycle on local stack):
| Test | Result |
|---|---|
| Run 1 cold (no cache) | 30.5s end-to-end, 2 atoms written silver (techniques + ai_tampered) |
| Run 2 warm (same content) | 18.3s end-to-end (-40%), atom hit_count incremented to 1 |
| Insert moderation queue entry | 200 |
| Claim queue entry | 200 |
| Resolve as 'corrected' with diff | 200 |
| Brain atoms after resolve | Both promoted to gold, human_validated=true, validator_user_id=mod-tester, human_corrections stored as JSONB |
| Cleanup (revert plan, disable brain, truncate atoms) | All systems back to safe defaults |
Final state after Track 1 + Track 2:
- All feature flags (
triage_enabled,brain_enabled) →false(safe) - Test user plan reverted to Freemium
- Brain atom DB cleaned
- All 4 service containers healthy:
didi-agent-v3,didi-framework,didi-admin,didibrain-api
Goal: agent-v3 has client code ready to call brain. Feature flag OFF by default. When brain becomes available (Track 2), flip flag.
Task 1.8.1 — Extend shared/brain/client.ts
- File:
agent-v3/src/shared/brain/client.ts(MODIFY — already has gatherFromBrain etc.) - Add:
lookupAnalysisAtom(params)— POST /v1/analysis_atom/lookup with timeout from Redis configwriteAnalysisAtomAsync(params)— fire-and-forget POST /v1/analysis_atom (only if tier='premium')patchAnalysisAtomGold(params)— PATCH /v1/analysis_atom/:id with human_validated=true
- Read
brain_enabledflag from Redis. If false, return null/skip immediately - Read
brain_url,brain_lookup_timeout_ms,brain_per_componentfrom Redis - Compute
prompt_hash(sha256 of system_prompt + user_template) andframework_version(sha256 of relevant Redis configs) - All errors caught silently, return null. Brain failure must NOT fail analysis.
Task 1.8.2 — Integrate in techniques/executor.ts
- File:
agent-v3/src/components/techniques/executor.ts(MODIFY) - At start of execute():
if (brain_enabled AND brain_per_component.techniques) { const lookup = await lookupAnalysisAtom(...); if (lookup?.hit) { if (lookup.atom.cache_tier === 'gold' || (lookup.atom.cache_tier === 'silver' AND lookup.staleness === 'fresh')) { return { ...lookup.atom.result_processed, _cache_tier: lookup.atom.cache_tier }; } } } // Else continue normal LLM run - After LLM run (miss path):
if (tier === 'premium' AND brain_enabled AND llmConfidence >= brain_confidence_min_silver) { writeAnalysisAtomAsync(...); // fire-and-forget } - DO NOT change return signature. UI just passes
_cache_tierthrough verdict to extension UI.
Task 1.8.3 — Integrate in ai-tampered/executor.ts
- Same pattern as 1.8.2 with component='ai_tampered'
Task 1.8.4 — Update moderation resolve handler
- File:
agent-v3/src/api/moderation-routes.ts(MODIFY) - In resolve handler, after saving corrections to PG:
if (action === 'corrected' AND trigger_brain_write !== false) { for (const component of ['techniques', 'ai_tampered', 'claims']) { if (corrections[component]) { const lookup = await lookupAnalysisAtom(...); if (lookup?.atom) { await patchAnalysisAtomGold({ atomId: lookup.atom.atom_id, validatorUserId: jwt.sub, humanCorrections: corrections[component], resultProcessed: applyCorrections(lookup.atom.result_processed, corrections[component]), }); } } } } if (action === 'approved') { // Same lookup, PATCH with human_corrections=null but human_validated=true (silver→gold without changes) } - applyCorrections() helper: takes a result and a diff, returns corrected result. Pure function, unit-testable.
- Brain failure here does NOT fail resolve. Log warning, continue. Resolve still saves to PG.
Validation (only with brain available — see Track 2):
- With brain_enabled=false in PG: brain not called, no log lines, normal flow
- With brain_enabled=true but brain unreachable: 2s timeout, fallback to LLM, no failure
- With brain reachable, hit path: latency drops from ~5s to ~200ms on cache hit
- Resolve corrected → brain has new gold atom
Risk: MEDIUM. Touches hot path (executors). Mitigation: feature flag, try/catch, timeout, brain optional.
TRACK 2 — Brain Side — ✅ DONE 2026-05-01 (Phases 2.1-2.4)
Status: All 4 endpoints live and tested E2E on local brain instance (10.11.10.12:8090). Production brain on 10.11.10.13 still on old version — to be deployed when integration is final.
Files created/modified in ai_platform/modules/didi_brain/brain_api/:
db.py— extended_SCHEMA_SQLwithbrain_analysis_atomtable + 4 indexes (idx_baa_lookup, idx_baa_gold, idx_baa_expires, idx_baa_prompt). Schema applied automatically on app startup (existing migration mechanism).schemas.py— added 6 Pydantic models:AnalysisAtomLookupRequest/Response,AnalysisAtomWriteRequest/Response,AnalysisAtomPatchRequest,AnalysisAtomStatsResponse, plusAnalysisAtomDatashared type.services/analysis_atom.py(NEW) — 4 functions:lookup,upsert,patch_to_gold,get_stats. Tier decision logic (silver vs bronze based on confidence threshold). Gold-preservation in upsert ON CONFLICT (gold never downgraded). Hit count tracking.app.py— added 4 endpoints + 7 imports. Existing endpoints UNCHANGED.
Endpoints live (port 8090):
POST /v1/analysis_atom/lookup— tier-agnostic match by (content_hash, component, prompt_hash). Returns hit/staleness. Bronze never served.POST /v1/analysis_atom— upsert with gold preservation. Rejects tier=free (skipped_reason="tier=free (premium-only ingest)").PATCH /v1/analysis_atom/{atom_id}— promote to gold (sets human_validated, applies corrections, cache_tier=gold, expires_at=NULL).GET /v1/analysis_atom/stats— counts by tier/component, hit rate 24h, writes 24h.
E2E test results (8/8 pass):
| # | Test | Result |
|---|---|---|
| 1 | Write conf=78 → silver | ✅ |
| 2 | Write conf=40 → bronze | ✅ |
| 3 | Write tier=free → rejected | ✅ |
| 4 | Lookup tier=free on premium silver atom → HIT | ✅ tier-agnostic |
| 5 | Lookup bronze atom → MISS (filtered) | ✅ |
| 6 | PATCH silver → gold + corrections | ✅ |
| 7 | Lookup gold with different prompt → HIT (survives prompt change) | ✅ |
| 8 | Stats reflects state | ✅ |
Bug fixed during testing: cache_tier had Pydantic default "silver" which preempted the _decide_cache_tier() logic from llm_confidence. Changed to cache_tier: AtomCacheTier | None = None so server-side decision applies when caller doesn't override.
Resume context for next run:
- Brain v2 endpoints live on
http://10.11.10.12:8090(this dev machine). - Production brain on
10.11.10.13:8090is UNTOUCHED — still on old version. - agent-v3
moderation_config.brain_urldefaults to10.11.10.13— when ready to test integration on this machine, set tohttp://didibrain-api:8090(Docker internal hostname) orhttp://10.11.10.12:8090. - DB cleaned post-test (atom table empty).
Risk: LOW. New endpoints, additive schema. Existing /v1/verification_cache and /v1/gather flows untouched.
TRACK 2 — Original detailed plan (preserved for reference)
RESOLVED 2026-05-01: Brain code lives at /home/admin365/didi_mono/ai_platform/modules/didi_brain/. Same monorepo, separate Python service. Pattern to follow: existing verification_cache.py (claims-side cache).
Brain stack reference
| Layer | Tech | Where |
|---|---|---|
| FastAPI app | Python 3.11+, Uvicorn, Pydantic v2 | brain_api/app.py |
| DB pool | asyncpg via brain_api/db.py (brain_db.connect()) |
Direct PG |
| Schemas (Pydantic) | brain_api/schemas.py |
Add new models for analysis_atom |
| Services (logic) | brain_api/services/<name>.py |
Add analysis_atom.py |
| Storage | PG 16 + pgvector. Tables prefixed brain_* (alongside Atomic) |
New: brain_analysis_atom |
| Embeddings | BGE-M3 (1024-dim) via vLLM OpenAI-compat | shared/embedding_client.py |
| Reranker | BGE-reranker-v2-m3 (only on /search & /gather, NOT on lookup) | shared/reranker_client.py (if exists) |
PHASE 2.1 — Brain schema migration
- File:
ai_platform/modules/didi_brain/sql/migrations/brain_analysis_atom_v1.sql(NEW or follow brain's existing migration convention) - CREATE TABLE
brain_analysis_atomperBRAIN_V2_DESIGN.mdschema, with corrected UNIQUE(content_hash, component, prompt_hash)— tier OUT of unique key - Validate pgvector extension installed (it is — verification_cache uses it via Atomic)
- Index ivfflat on content_embedding (lists=100)
- TTL cleanup: cron job in
scripts/deletes expired silver/bronze
PHASE 2.2 — Brain Pydantic schemas
- File:
ai_platform/modules/didi_brain/brain_api/schemas.py(MODIFY — add new models) - Add:
AnalysisAtomLookupRequest,AnalysisAtomLookupResponse,AnalysisAtomWriteRequest,AnalysisAtomWriteResponse,AnalysisAtomPatchRequest,AnalysisAtomStatsResponse - Match field types to
BRAIN_V2_DESIGN.mdexactly
PHASE 2.3 — Brain service logic
- File:
ai_platform/modules/didi_brain/brain_api/services/analysis_atom.py(NEW) - Functions:
lookup_atom(req, db, embed)— exact match by SHA256, fallback semantic if allow_semantic_matchwrite_atom(req, db, embed)— INSERT or UPDATE if same key. Reject silently if tier='free' (per decision)patch_atom_gold(atom_id, req, db)— UPDATE with human_validated=true, cache_tier='gold', expires_at=NULLget_stats(db)— aggregates per tier/component
PHASE 2.4 — Brain endpoints in app.py
- File:
ai_platform/modules/didi_brain/brain_api/app.py(MODIFY — add 4 routes) POST /v1/analysis_atom/lookup→lookup_atomPOST /v1/analysis_atom→write_atomPATCH /v1/analysis_atom/{atom_id}→patch_atom_goldGET /v1/analysis_atom/stats→get_stats- Errors handled like existing endpoints (HTTPException with structured detail)
PHASE 2.5 — Brain admin UI (in ai_platform/modules/dashboard)
- File:
ai_platform/modules/dashboard/...(location TBD — check this dashboard's structure) - Page: atom stats, browse atoms (paginated), force gold/demote silver/delete buttons
- Existing AI platform dashboard already runs at port 51300 (per ai_platform/README.md)
PHASE 2.6 — Bootstrap script (optional, for hit rate jump-start)
- File:
ai_platform/modules/didi_brain/scripts/bootstrap_from_didi_history.py(NEW) - Connects to DIDI cluster PG, reads
bos_analysis.analysis_sessionolder than 90d - For each row, computes content_hash + posts to
/v1/analysis_atom(silver, tier='premium' only) - Skip if
confidence < 60
Track 2 deployment: brain Docker image rebuilt + redeployed on 10.11.10.13. Existing verification_cache endpoints UNTOUCHED.
TRACK 3 — Production cutover & role enforcement — ✅ Faza A+B DONE 2026-05-01
After Track 1 + Track 2 worked end-to-end on staging, the user decided to flip
brain local (10.11.10.12:8090) into production and freeze the old brain on
10.11.10.13 as idle fallback. Plus enforce role-based access on admin dashboard
so end-users (clients with viewer/free_tier/etc) cannot reach /admin/*.
Faza A — Brain cutover 10.11.10.13 → 10.11.10.12 — ✅ DONE 2026-05-01
Goal: agent-v3 talks to local brain (with new analysis_atom endpoints), prod
brain on .13 runs idle as fallback. No deprovisioning yet.
Files modified:
agent-v3/docker-compose.yml—DIDI_BRAIN_URLdefault changed fromhttp://10.11.10.13:8090→http://10.11.10.12:8090(env var used by claimsverification_cacheflow)didiFramework/sql/migrations/011_add_moderation.sql—brain_urlDEFAULT changed fromhttp://10.11.10.13:8090→http://10.11.10.12:8090(so new fresh deploys point local)- Live PG
bos_parammgmt.moderation_config.brain_url→ updated to10.11.10.12:8090via API + sync to Redis
Live state:
brain_url: http://10.11.10.12:8090
brain_enabled: true
triage_enabled: false (default safe — activate from UI when ready to use queue)
Verified:
- agent-v3 recreated with new env (
docker exec didi-agent-v3 printenv DIDI_BRAIN_URL→ 10.11.10.12) - E2E analyze through pipeline → brain local hit verification_cache (claims=1)
- Brain prod 10.11.10.13 still up but idle (no requests routed)
GET http://10.11.10.13:8090/v1/analysis_atom/stats→{"detail":"Not Found"}confirms old version, NEW endpoints only on local
Resume context:
- Brain prod on
.13is left idle as 1-2 week fallback. Decommission deferred to Faza E. - All agent-v3 brain calls (gather, verification_cache, analysis_atom) → 10.11.10.12.
triage_enabled=falsekeeps queue empty until activated explicitly.
Faza B — Role guards on admin dashboard — ✅ DONE 2026-05-01
Goal: end-users (clients with viewer role) cannot reach /admin/*. Get a clear 403 page with link to public app. Admin dashboard sidebar shows only relevant items per role.
Decision log update: viewer (default Keycloak role on signup) = end-user / client. Should NEVER access admin dashboard. Admin dashboard is for staff only: admin, moderator, senior_moderator.
Files modified:
admin-dashboard/src/components/auth/ProtectedRoute.tsx— extended withrequiredAnyRole?: string[](any-of gate). Redirects to/unauthorizedon fail.admin-dashboard/src/components/auth/Unauthorized.tsx(NEW) — 403 page with role list + "Go to Public App" + "Log out" buttonsadmin-dashboard/src/App.tsx:- Added
/unauthorizedroute - Wrapped AdminLayout in
ProtectedRoute requiredAnyRole={['admin','moderator','senior_moderator']} - Per-route nested guards:
/framework,/users,/providers,/llm-components→requiredRole="admin";/history,/moderation/*→requiredAnyRole=[admin,moderator,senior_moderator]
- Added
admin-dashboard/src/components/layout/AdminLayout.tsx— sidebar restructured:- Configuration section (Framework, LLM Components, Providers) → admin-only (existing logic kept)
- Management section split: Users → admin only; Analysis History → admin OR moderator; Moderation → moderator only
Permission matrix (effective):
| Page | viewer / paid_tier / etc. | moderator | senior_moderator | admin |
|---|---|---|---|---|
/admin/* (any) |
❌ 403 → Unauthorized | ✅ Dashboard + History + Moderation | ✅ same + force_gold_brain | ✅ everything |
/users, /framework, /llm-components, /providers |
❌ | ❌ | ❌ | ✅ |
/history |
❌ | ✅ | ✅ | ✅ |
/moderation/* |
❌ | ✅ | ✅ | ✅ |
Staging mode behavior: REACT_APP_STAGING_MODE=true makes hasRole() always return true → all guards are no-ops locally for testing. In production with Keycloak, JWT roles enforce strictly.
Verified:
- TypeScript clean (
npx tsc --noEmitexit 0) didi-admincontainer rebuilt + recreated, healthyhttps://10.11.10.12:3000/admin/→ 200https://10.11.10.12:3000/admin/unauthorized→ 200 (renders 403 page)https://10.11.10.12:3000/admin/moderation→ 200 (in staging, guards bypassed)
Risk: LOW. New routes + UI gates only; no breaking changes to existing protected routes.
Faza C — AI Platform dashboard reskin (React + Keycloak SSO) — ✅ DONE 2026-05-02
RESULT: Full reskin shipped. AI platform admin dashboard now React 19 + MUI 7 + TanStack Query + keycloak-js, side-by-side with original Jinja UI at /v2/. All 9 sub-phases (C.0–C.8) complete. Detailed log lives in /home/admin365/didi_mono/ai_platform/modules/dashboard/AI_PLATFORM_RESKIN_PLAN.md (~700 lines).
Highlights:
- 98 config keys CRUD-able via schema-driven
<ModuleConfigForm>across 8 modules + brain settings - 8 new brain admin endpoints (atom browse with filters, force-gold flow, taxonomy, extended stats) + matching React DataGrid pages
- 5 React pages porting all 7 existing Jinja routes (Overview, History, Cost, Providers, Archive, AuditLog) with parity + drawer details
- Each AI module (llm, embeddings, rerank, audio, video, catalog) gained a
RuntimeConfigClientthat polls dashboard/api/configevery 30s and applies log-level + rate-limit changes live - Keycloak SSO: new client
ai-platform-dashboardon<sso-extern>realmdidi-clients, distinct roleai_platform_admin. Hybrid backend auth (JWT first, legacy bearer fallback). DIDI admin-dashboard's existing Keycloak setup (Faza B) untouched.
Pre-requisite for production cutover (see C.8 in plan doc):
- Manual Keycloak setup: create client + role + assign to user
- Set
DASHBOARD_KEYCLOAK_URL=https://<sso-extern>,DASHBOARD_STAGING_MODE=false,VITE_STAGING_MODE=false - Rebuild with build-args + redeploy
Pages to port (current AI platform dashboard):
overview.html— services + usage statsarchive.html+archive_detail.html— knowledge graph atoms browsingaudit.html— audit log of actionsconfig.html— runtime config (knowledge atom config, ingest defaults)cost.html— LLM cost per periodhistory.html+history_detail.html— ingest historyproviders.html— search providers (Brave, Tavily, M17 SearXNG) + keys
Endpoints (FastAPI on dashboard/src/dashboard/api/routes/):
archive.py,config.py,health.py,history.py,ingest.py,pages.py,stats.py
Auth model: Replace auth.py (bearer token in PG) with Keycloak JWT middleware. Existing User table can map keycloak_id → role (still enforced in DB for audit trail, but JWT is source of truth).
Decision needed before starting:
- Where to host the React SPA? Same container or separate? Recommend same nginx serving SPA + reverse proxying API.
- Brain admin UI (atom browse, force gold) — add as new section in this dashboard? Recommend yes.
Faza D — Backend role guards (agent-v3) — ⏳ PENDING
Currently agent-v3 moderation routes use soft role check (permits if no JWT roles array). For production, must be strict: every /api/v3/moderation/* endpoint MUST verify jwtRoles.includes('moderator') or return 403. Soft mode only when an explicit STAGING_MODE=true env var is present.
Single file change: agent-v3/src/api/moderation-routes.ts — requireRole() helper tightened.
Faza E — Decommission 10.11.10.13 brain — ⏳ PENDING (defer ~2 weeks)
After local brain proves stable in production for 1-2 weeks:
- Stop
didibrain-apicontainer on.13 - Optionally archive its PG data (verification_cache atoms can be useful as bootstrap source)
- Update DEPLOYMENT.md / ENDPOINTS.md to remove
.13references
INTEGRATION PHASE — End-to-end (estimated 1 day)
After Track 1 phases 1.1-1.8 done AND Track 2 phases 2.1-2.2 done:
Task INT.1 — Activate brain on staging
- In moderation_config, set
brain_enabled=true,brain_url=<staging brain URL> - Sync-redis
- Run 100 analyses with same text → verify hit rate increases
- Verify: cached responses come back faster (<500ms)
Task INT.2 — Test full HIL → brain flow
- Run an analysis (gets to queue via triage)
- Moderator resolves with correction
- Verify atom in brain became gold (
GET /v1/analysis_atom/:idshowscache_tier='gold', human_validated=true) - Run same analysis again → verify response includes
_cache_tier='gold'and frontend shows badge
Task INT.3 — Performance test
- Run 1000 concurrent analyses (mix of cached + new content)
- Measure: P50/P95/P99 latency, brain lookup latency, brain write latency
- Verify: P95 brain lookup <200ms, P99 <500ms
- Verify: no analysis failures due to brain
Task INT.4 — Production cutover
- Apply migration on prod cluster (off-hours)
- Deploy didi backend with feature flags OFF
- Deploy admin dashboard
- Set up moderator user on Keycloak prod
- Activate triage_enabled=true
- Monitor queue depth for 24h
- If stable, activate brain_enabled=true
- Monitor cost reduction + hit rate
Task INT.5 — Bootstrap brain from prod history (optional)
- Run script on prod historical sessions older than 90 days
- Verify hit rate jump on subsequent traffic
OPEN QUESTIONS (need answers before starting)
These block parts of the plan. Get answers before phases that depend on them.
| # | Question | Blocks | Default if unresolved |
|---|---|---|---|
| 1 | /home/admin365/didi_mono/ai_platform/modules/didi_brain/. Stack: Python 3.11+, FastAPI, Pydantic v2, Postgres 16 + pgvector + Atomic (Rust KG). Embeddings BGE-M3 1024-dim. Service runs at 10.11.10.13:8090. Existing services pattern in brain_api/services/ (e.g., verification_cache.py). Follow same pattern for analysis_atom. |
Resolved | |
| 2 | First moderator user identity (existing user or new)? | Phase 1.7 | Use admin@didi.local |
| 3 | Notification channel for new queue entries (email/Slack)? | Out of scope for v1 | Skip — UI polling is enough |
| 4 | Senior moderator escalation flow specifics? | Phase 2 / future | Faza 2: senior=admin override only |
| 5 | Soft vs hard delete on rejected sessions? | Phase 1.5 | Soft — keep for audit trail |
| 6 | Privacy redaction for moderator (PII in input text)? | Phase 1.6 | Skip v1, document as known issue |
| 7 | Brain embedding model exact dimensions? | Phase 2.1 | Block — query brain at start of Track 2 |
| 8 | Cross-encoder reranking on atom lookup or only on /v1/search? | Phase 2.2 | Default: only on /v1/search; lookup is fast-path |
| 9 | Atom TTL: 90 days silver default OK? | Phase 2.1 | Yes, configurable from UI later |
DECISIONS LOG (already agreed, do not re-litigate)
- 2026-04-30: SLA = instant with post-hoc correction. User sees verdict in 5s; corrections async.
- 2026-04-30: Triage v1 strict — 1-2 moderators max, 10-20 reviews/day target.
- 2026-04-30: Operational data (queue, status) in DIDI PG. Knowledge (atoms) in brain PG. Split.
- 2026-04-30: 1 atom per (component × content) — techniques separate from ai_tampered separate from claims.
- 2026-04-30: 3 tiers — gold (human_validated), silver (LLM cache), bronze (pending review, not served).
- 2026-04-30: Confidence < 60 → bronze, not silver.
- 2026-04-30: Brain write ONLY from premium tier. Read tier-agnostic (free benefits from premium cache).
- 2026-04-30: Schema correction — UNIQUE (content_hash, component, prompt_hash) — tier OUT of unique key.
- 2026-04-30: Zero hardcoded config. All in PG
bos_parammgmt.moderation_config+ sensitive_topic + moderation_role tables. Synced to Redis. - 2026-04-30: Brain admin UI lives in AI platform repo, NOT in didi admin dashboard.
- 2026-04-30: Feature flags everywhere. Default OFF on first deploy. Activate progressively after validation.
- 2026-05-01:
viewerrole (default Keycloak signup role) = end-user / client. NEVER reaches admin dashboard. Admin dashboard = staff-only (admin / moderator / senior_moderator). - 2026-05-01: Brain cutover — local
10.11.10.12:8090is the production brain; old10.11.10.13:8090runs idle as fallback for 1-2 weeks before decommission. - 2026-05-01: AI platform dashboard reskin = full React 19 + MUI 7 + Keycloak SSO (same stack as admin dashboard). Admin-only access. Postponed to next session due to size.
- 2026-05-01: Permission matrix locked: admin sees all, moderator + senior_moderator see Dashboard + History + Moderation, viewer/paid/free_tier see 403 → public app.
- 2026-05-01: AI platform brain admin UI (browse atoms, force gold, stats) goes inside the reskinned AI platform dashboard, not a new app.
FILE INVENTORY
What gets created vs modified across the entire plan.
New files (didi side)
agent-v3/sql/migrations/
├── 010_add_moderation.sql NEW
└── 010_rollback.sql NEW
agent-v3/src/api/
└── moderation-routes.ts NEW
agent-v3/src/components/moderation/
├── triage.ts NEW
└── queue-manager.ts NEW
didiFramework/src/routes/
├── moderation-config.ts NEW
├── sensitive-topics.ts NEW
└── moderation-roles.ts NEW
admin-dashboard/src/components/ModerationSettings/
├── index.tsx NEW
├── TriageCard.tsx NEW
├── BrainClientCard.tsx NEW
├── SensitiveTopicsCard.tsx NEW
├── RolesCard.tsx NEW
└── api.ts NEW
admin-dashboard/src/components/Moderation/
├── ModerationQueue.tsx NEW
├── ModerationDetail.tsx NEW
├── ModerationStats.tsx NEW
├── EditVerdictPanel.tsx NEW
├── EditTechniquesPanel.tsx NEW
├── EditAITamperedPanel.tsx NEW
├── EditClaimsPanel.tsx NEW
├── api.ts NEW
└── types.ts NEW
Modified files (didi side, surgical edits only)
agent-v3/src/
├── index.ts MODIFY (mount /api/v3/moderation)
├── api/pipeline-routes.ts MODIFY (accept user_flagged in body)
├── components/pipeline/executor.ts MODIFY (call triage after persist, with try/catch)
├── components/techniques/executor.ts MODIFY (brain lookup + write — phase 1.8)
├── components/ai-tampered/executor.ts MODIFY (brain lookup + write — phase 1.8)
├── queue/aggregator.ts MODIFY (call triage after persist, with try/catch)
└── shared/brain/client.ts MODIFY (add lookupAnalysisAtom, writeAnalysisAtomAsync, patchAnalysisAtomGold)
didiFramework/src/
├── server.ts MODIFY (mount 3 new route modules)
└── routes/sync-redis.ts MODIFY (add 3 new Redis keys)
admin-dashboard/src/
├── App.tsx MODIFY (add 3 routes for /moderation/*)
└── components/
├── LLMComponentsConfig/index.tsx MODIFY (add Moderation toggle to top-level selector)
└── dashboard/ServicesDashboard.tsx MODIFY (add sidebar link for moderators)
NOT touched (existing flows preserved)
agent-v3/src/
├── components/claims/executor.ts UNTOUCHED (uses verification_cache pattern, working)
├── components/source-assessment/executor.ts UNTOUCHED
├── components/pipeline/verdict-calculator.ts UNTOUCHED
├── components/pipeline/verdict-explanation.ts UNTOUCHED
└── shared/persistence/* UNTOUCHED
didiFramework/src/routes/
├── verdicts.ts UNTOUCHED (already extended with runtime-config in prev session)
├── input-profiles.ts UNTOUCHED
└── (everything else) UNTOUCHED
admin-dashboard/src/components/
├── LLMComponentsConfig/VerdictConfig.tsx UNTOUCHED (already extended in prev session)
└── (everything else) UNTOUCHED
RUNBOOK FOR FRESH SESSION RESUME
If you (the new assistant) are reading this in a fresh session:
-
Read all 3 docs (this + HIL_MODERATION_DESIGN.md + BRAIN_V2_DESIGN.md). Do not start work without context.
-
Verify state of codebase:
ls /home/admin365/didi_mono/backend/services/orchestration-layer/agent-v3/sql/migrations/ # Look for 010_*.sql — if exists, Phase 1.1 done ls /home/admin365/didi_mono/backend/services/orchestration-layer/didiFramework/src/routes/ | grep moderation # If files exist, Phase 1.2 partly done ls /home/admin365/didi_mono/backend/admin-dashboard/src/components/Moderation* 2>/dev/null # If exists, Phase 1.3 or 1.6 partly done -
Verify DB state:
docker exec didi-framework node -e " const {Pool} = require('pg'); const p = new Pool({host:'10.11.50.167',port:5000,user:'bos_interface',password:'interface',database:'DIDI'}); p.query(\"SELECT table_name FROM information_schema.tables WHERE table_schema='bos_parammgmt' AND table_name IN ('moderation_config','sensitive_topic','moderation_role')\").then(r => {console.log(r.rows); p.end();}); " # If 3 rows → Phase 1.1 done -
Find first unchecked task in the order: 1.1 → 1.2 → 1.3 → 1.4 → 1.5 → 1.6 → 1.7 → 1.8 → INT.1+
-
Update this file as you go: change
[ ]→[x]immediately after completing each subtask. Add→ YYYY-MM-DD noteif useful. -
Critical rule: NEVER ship code without:
- Type-check passing (
npx tsc --noEmit) - Existing endpoints still responding (curl smoke test on
/api/v3/health,/api/v3/pipeline/analyzewith simple text) - Feature flag default OFF for new path
- Type-check passing (
-
If stuck or in doubt: prefer asking the user over guessing. Don't introduce hardcoded values. Don't break existing flows.
-
Update Decisions Log if user makes new decisions during the session. Append, don't overwrite.
ESTIMATED TIMELINE
Track 1 didi side: ~5-6 working days for one person, given parallel work.
- Phase 1.1: 0.5 day
- Phase 1.2: 0.5 day
- Phase 1.3: 1 day
- Phase 1.4: 1 day
- Phase 1.5: 0.5 day
- Phase 1.6: 2 days
- Phase 1.7: 0.25 day
- Phase 1.8: 1 day (mostly idle waiting for brain)
Track 2 brain side: AI platform team estimate (separate)
Integration: 1 day end-to-end test + cutover
Total didi side: ~6 days. Brain side parallel. Integration adds 1 day.