From f849a87f2f73d991eb82818fd3ace278e9ff7d5d Mon Sep 17 00:00:00 2001 From: asepharyana Date: Wed, 12 Aug 2026 20:48:26 +0700 Subject: [PATCH] fix: remove user history injection to prevent false positive moderation - Removed getUserRecentInfractions usage in textBatchProcessor.ts and visionAnalyzer.ts - Removed buildUserHistoryXml import and calls - Messages are now evaluated standalone, not influenced by past violations in other channels - Updated moderation prompts with clearer instructions about user_history usage - Fixes issue where benign messages like 'tubuh manusia vs gravitasi' were incorrectly flagged due to carryover from previous drone weapons discussion The user history context was causing the LLM to interpret unrelated current messages as threats because it conflated them with past violations. Now each message is judged on its own merit with only channel-specific context. --- .../modules/ai-moderation/prompts/output.ts | 2 +- .../modules/ai-moderation/prompts/rules.ts | 10 ++++--- .../ai-moderation/textBatchProcessor.ts | 28 ++----------------- .../modules/ai-moderation/visionAnalyzer.ts | 28 ++----------------- 4 files changed, 12 insertions(+), 56 deletions(-) diff --git a/services/discord-gateway/src/modules/ai-moderation/prompts/output.ts b/services/discord-gateway/src/modules/ai-moderation/prompts/output.ts index a261eea..a697206 100644 --- a/services/discord-gateway/src/modules/ai-moderation/prompts/output.ts +++ b/services/discord-gateway/src/modules/ai-moderation/prompts/output.ts @@ -40,7 +40,7 @@ Data konteks tersedia: (peta ringkasan kepribadian, di pesan USE Gunakan untuk personalisasi analysis, tapi: - Profil adalah KONTEKS, bukan bukti. Profil mencurigakan ≠ flag; profil bersih ≠ loloskan pelanggaran. - Perubahan perilaku mencolok (biasanya teknis tiba-tiba provokatif) layak dicatat di analysis. -- (kutipan pesan yang pernah di-flag) = pola pelanggaran lama. Gunakan untuk mendeteksi PENGULANGAN (mis. spam link yang sama, provokasi berulang), tapi JANGAN memflag pesan bersih hanya karena riwayat. +- (kutipan pesan yang pernah di-flag) = pola pelanggaran lama. Gunakan untuk mendeteksi PENGULANGAN KEKONSISTEN (spam link yang SAMA, provokasi yang MENGULANG KONTEN NYATA YANG SAMA). JANGAN pernah gunakan history untuk "menginterpretasi ulang" pesan bersih yang TERPISAH DARI riwayat. Setiap pesan BARU dinilai TERSAMBUNG (standalone). Jika tidak ada pola pengulangan yang jelas → CLEAN. Contoh: Jika sebelumnya ada pesan dengan link scam.example.com yang di-flag, dan pesan baru juga ada link scam.example.com → FLAG. Tapi jika pesan baru tentang "energi kinetik dari jatuh" tanpa link yang sama → CLEAN walaupun ada history lain. - JANGAN paksa referensi profil jika tidak relevan — analysis natural lebih baik. - Channel culture coding/teknis → pesan teknis lebih wajar; channel santai → slang lebih wajar. Jangan dipakai mengabaikan pelanggaran nyata. diff --git a/services/discord-gateway/src/modules/ai-moderation/prompts/rules.ts b/services/discord-gateway/src/modules/ai-moderation/prompts/rules.ts index d4b1636..4a9f2e7 100644 --- a/services/discord-gateway/src/modules/ai-moderation/prompts/rules.ts +++ b/services/discord-gateway/src/modules/ai-moderation/prompts/rules.ts @@ -35,10 +35,12 @@ export const SYSTEM_RULES = `Kamu adalah asisten moderasi konten untuk server Di Kata alat kelamin/anatomi seksual (kontol, memek, titten, tit, dick) atau istilah seksual eksplisit WAJIB di-flag sebagai vulgar_language/sexual_content — TANPA pengecualian bercanda, slang, atau "santai". ## Nilai Server — Diskriminasi -- Seksisme ("dasar perempuan", "logika cewek") → hate_speech (umum) / harassment (terarah). -- Ageisme ("dasar bocil", "tau aja lo tua") → hate_speech / harassment. -- Diskriminasi fisik ("gendut", "iteman", "cungkring") → harassment jika terarah. -- Serangan personal, penghinaan, merendahkan = tidak ditoleransi. Perbedaan pendapat wajar. +-Ketika sesuatu yang melanggar terjadi di channel, flag jika relevan. Setiap pesan dinilai BERDASARKAN ISINYA SENDIRI, bukan sekadar histori pengguna. +-Seksisme ("dasar perempuan", "logika cewek") → hate_speech (umum) / harassment (terarah). +-Ageisme ("dasar bocil", "tau aja lo tua") → hate_speech / harassment. +-Diskriminasi fisik ("gendut", "iteman", "cungkring") → harassment jika terarah. +-Serangan personal, penghinaan, merendahkan = tidak ditoleransi. Perbedaan pendapat wajar. ++**PESAN DINILAI SECARA STANDALONE:** Setiap pesan baru dinilai BERDASARKAN ISINYA SENDIRI. (jika ada) HANYA untuk mendeteksi POLA PENGULANGAN dengan JAMAK (spam link yang SAMA, provokasi berulang yang MENGANDALKAN KONTEN YANG SAMA). JANGAN gunakan history untuk "menginterpretasi ulang" pesan bersih yang TERPISAH DARI riwayat pelanggaran sebelumnya. Jika pesan tidak mengandung unsur yang BERPANDUAN PADA riwayat → tetap CLEAN. ## LARANGAN BERAT (ZERO TOLERANCE) - **LGBT:** Segala promosi, diskusi, pengakuan orientasi, coming out, atau curhat personal tentang LGBT WAJIB di-flag "sexual_deviation". Tidak ada pengecualian. diff --git a/services/discord-gateway/src/modules/ai-moderation/textBatchProcessor.ts b/services/discord-gateway/src/modules/ai-moderation/textBatchProcessor.ts index d455391..03f79bd 100644 --- a/services/discord-gateway/src/modules/ai-moderation/textBatchProcessor.ts +++ b/services/discord-gateway/src/modules/ai-moderation/textBatchProcessor.ts @@ -19,7 +19,6 @@ import { callModerationLLM } from "./llmCaller.js"; import { analyzeSingleMediaImage } from "./mediaAnalysisClient.js"; import { buildReferenceXml, - buildUserHistoryXml, buildUserProfileRef, buildUserProfilesBlock, escapeXml, @@ -41,10 +40,7 @@ import { buildTermGlossaryBlock } from "./termGlossary.js"; import { getRecentCorrectedModerations } from "./textCacheStore.js"; import { extractUrlsFromText, fetchUrlSafely } from "./urlFetcher.js"; import { getUserProfile } from "./userProfileStore.js"; -import { - getUserRecentInfractions, - initializeUserReputation, -} from "./userReputationStore.js"; +import { initializeUserReputation } from "./userReputationStore.js"; import type { MessageImagePart } from "./visionAnalyzer.js"; const log = createChildLogger("textBatchProcessor"); @@ -218,27 +214,7 @@ export async function runTextOnlyBatch( if (!userContexts.has(msg.user_id)) { const rep = await initializeUserReputation(msg.user_id, msg.guild_id); const repAttrs = formatReputationAttrs(rep); - let repXml = ``; - // Repeat offenders get their last flagged messages as - // so the LLM can recognize PATTERNS (same scam link, repeated - // provocation) — history is reference, never proof. Best-effort. - if (rep.total_infractions > 0) { - try { - const history = await getUserRecentInfractions(msg.user_id, 2); - const historyXml = buildUserHistoryXml( - history.map((h) => ({ - content: h.content ?? "", - severity: h.severity, - created_at: h.created_at, - })), - ); - if (historyXml) { - repXml = `\n${historyXml}\n`; - } - } catch { - // history is a bonus — fall back to attrs-only reputation - } - } + const repXml = ``; userContexts.set(msg.user_id, repXml); } if (!userProfiles.has(msg.user_id)) { diff --git a/services/discord-gateway/src/modules/ai-moderation/visionAnalyzer.ts b/services/discord-gateway/src/modules/ai-moderation/visionAnalyzer.ts index e1b5aee..c1b1963 100644 --- a/services/discord-gateway/src/modules/ai-moderation/visionAnalyzer.ts +++ b/services/discord-gateway/src/modules/ai-moderation/visionAnalyzer.ts @@ -66,7 +66,6 @@ import { } from "./mediaDownloader.js"; import { buildReferenceXml, - buildUserHistoryXml, buildUserProfileRef, escapeXml, formatReputationAttrs, @@ -90,10 +89,7 @@ import { import { buildTermGlossaryBlock } from "./termGlossary.js"; import { extractUrlsFromText } from "./urlFetcher.js"; import { getUserProfile } from "./userProfileStore.js"; -import { - getUserRecentInfractions, - initializeUserReputation, -} from "./userReputationStore.js"; +import { initializeUserReputation } from "./userReputationStore.js"; // --------------------------------------------------------------------------- // Types @@ -459,27 +455,9 @@ export async function prepareMediaMessage( ? buildUserProfileRef(target.user_id) : ""; - // Rich reputation — same shape as the text path: attrs + optional - // with the last flagged messages for repeat offenders. + // Rich reputation — attrs only, no user history injection (per channel context preference) const repAttrs = formatReputationAttrs(rep); - let repXml = ``; - if (rep.total_infractions > 0) { - try { - const history = await getUserRecentInfractions(target.user_id, 2); - const historyXml = buildUserHistoryXml( - history.map((h) => ({ - content: h.content ?? "", - severity: h.severity, - created_at: h.created_at, - })), - ); - if (historyXml) { - repXml = `\n${historyXml}\n`; - } - } catch { - // history is a bonus — fall back to attrs-only reputation - } - } + const repXml = ``; const isBot = resolveIsBot(target); const isEdited = resolveIsEdited(target);