From 04f2862abc6cfa42bafa240ce61f99fc91a00709 Mon Sep 17 00:00:00 2001 From: MythEclipse Date: Sun, 21 Jun 2026 20:07:51 +0700 Subject: [PATCH] feat(ai-moderation): profile learner ai_status filter + channel-aware + simple fallback memory - userProfileLearner.ts: filter query to only clean messages (eq ai_status='clean') to prevent profile contamination from flagged content. Also select channel_id to group messages by channel in prompt, enabling channel-aware personality summarization (user may behave differently across channels). - llmModerationClient.ts (runSimpleTextFallback): inject user profile into both the classify prompt and the reason prompt, so even the last-resort fallback path has personality/memory context instead of being blind. --- .../ai-moderation/llmModerationClient.ts | 15 ++++++-- .../ai-moderation/userProfileLearner.ts | 35 +++++++++++++++---- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/services/discord-gateway/src/modules/ai-moderation/llmModerationClient.ts b/services/discord-gateway/src/modules/ai-moderation/llmModerationClient.ts index b3bf6be..c18ee68 100644 --- a/services/discord-gateway/src/modules/ai-moderation/llmModerationClient.ts +++ b/services/discord-gateway/src/modules/ai-moderation/llmModerationClient.ts @@ -1516,6 +1516,17 @@ export async function runSimpleTextFallback( ? content.slice(0, MAX_CONTENT_CHARS) + "..." : content; + // ── Inject user profile for personality-aware fallback ── + let userProfileCtx = ""; + try { + const profile = await getUserProfile(message.user_id); + if (profile?.profile_summary) { + userProfileCtx = `\n\nProfil pengirim pesan:\n${profile.profile_summary}\n`; + } + } catch { + // Profile fetch failure is non-fatal — proceed without context + } + // ── Step 1: Single-word classification ── const classifyPrompt = `Pesan berikut perlu diklasifikasikan sebagai: clean, warn, atau flagged. @@ -1529,7 +1540,7 @@ PENTING (False Positive Prevention): - Konten coding/programming (kode, log error, SQL, command line, error message, stack trace, nama library) = clean. JANGAN flag hanya karena ada kata "error" atau "crash" dalam konteks teknis. - Nama proyek, tools, framework (IMPHNEN, Bete, Cursor, Claude, React, Discord) = clean. - Percakapan multilingual (campuran Indonesia-Inggris) = clean. - +${userProfileCtx} Pesan: "${truncatedContent}" Jawab HANYA dengan satu kata: clean, warn, atau flagged`; @@ -1582,7 +1593,7 @@ Jawab HANYA dengan satu kata: clean, warn, atau flagged`; const categoryOptions = status === "flagged" ? "harassment, gambling, atau sara" : "spam"; const reasonPrompt = `Pesan berikut telah diklasifikasikan sebagai "${status}". - +${userProfileCtx} Pesan: "${truncatedContent}" Jelaskan dalam 1-2 kalimat Bahasa Indonesia: APA yang melanggar dan KENAPA. Jangan gunakan kata "mungkin" atau "sepertinya". Jangan tulis ulang pesan. Langsung ke alasan. diff --git a/services/discord-gateway/src/modules/ai-moderation/userProfileLearner.ts b/services/discord-gateway/src/modules/ai-moderation/userProfileLearner.ts index 004bf5d..78c8c00 100644 --- a/services/discord-gateway/src/modules/ai-moderation/userProfileLearner.ts +++ b/services/discord-gateway/src/modules/ai-moderation/userProfileLearner.ts @@ -18,29 +18,48 @@ async function learnUserProfile( ): Promise { const db = getDatabase(); - // Get recent messages for this user + // Get recent CLEAN messages for this user (avoid profiling from flagged content) const recentMessages = await db .select({ content: messagesTable.content, + channelId: messagesTable.channel_id, }) .from(messagesTable) .where( and( eq(messagesTable.user_id, userId), eq(messagesTable.guild_id, guildId), + eq(messagesTable.ai_status, "clean"), ), ) .orderBy(desc(messagesTable.created_at)) .limit(100); if (recentMessages.length < 10) { - log.debug({ userId }, "Not enough messages to learn user profile"); + log.debug({ userId }, "Not enough clean messages to learn user profile"); return; } + // Group messages by channel for channel-aware profiling + const channelGroups = new Map(); + for (const msg of recentMessages) { + const ch = msg.channelId ?? "unknown"; + if (!channelGroups.has(ch)) channelGroups.set(ch, []); + channelGroups.get(ch)!.push(msg); + } + + // Build messages text with channel context const messagesText = recentMessages .reverse() - .map((m) => m.content) + .map((m) => { + const chLabel = m.channelId ? `[#channel:${m.channelId}]` : ""; + return `${chLabel} ${m.content}`; + }) + .join("\n"); + + // Build channel activity summary + const channelSummary = [...channelGroups.entries()] + .map(([ch, msgs]) => ` - #channel ${ch}: ${msgs.length} pesan`) .join("\n"); const prompt = `Anda adalah AI ahli psikologi, analisis perilaku online, dan pembaca karakter. @@ -48,11 +67,14 @@ Tugas Anda adalah merangkum profil kepribadian SEORANG PRIBADI — bukan sekadar gaya bicara — berdasarkan riwayat pesan-pesan mereka di server Discord. Buatlah ringkasan yang KAYA AKAN PERSONALITAS sehingga pembaca merasa "mengenal" orang ini. -Pesan-pesan terakhir dari user "${userId}": +Pesan-pesan terakhir dari user "${userId}" (hanya pesan bersih/clean): ${messagesText} +Distribusi aktivitas user per channel: +${channelSummary} + Berdasarkan pesan-pesan di atas, buatlah ringkasan singkat (maksimal 4 paragraf) mengenai: @@ -60,8 +82,9 @@ mengenai: Apakah orang ini suka pake singkatan, emot, reaksi berlebihan ("WKWKWK"), atau nada datar? Bagaimana mereka memulai dan mengakhiri pembicaraan? -2. **Topik-topik yang sering dibahas** — apa PASSION mereka? Coding, gaming, musik, debat? - Apakah mereka inisiator topik atau lebih suka merespon? +2. **Topik-topik yang sering dibahas & channel favorit** — apa PASSION mereka? Coding, gaming, musik, debat? + Apakah mereka inisiator topik atau lebih suka merespon? Di channel mana mereka paling aktif? + Apakah perilaku mereka berbeda tergantung channel (misal: profesional di #coding vs santai di #general)? 3. **Kepribadian dan karakter yang terpancar** — Apakah mereka ramah dan hangat? Kritis dan analitis? Easy going? Gampang marah? Humoris? Supportif? Suka memprovokasi? Suka membantu?