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.
This commit is contained in:
MythEclipse
2026-06-21 20:07:51 +07:00
parent e5fa2b3f30
commit 04f2862abc
2 changed files with 42 additions and 8 deletions
@@ -1516,6 +1516,17 @@ export async function runSimpleTextFallback(
? content.slice(0, MAX_CONTENT_CHARS) + "..." ? content.slice(0, MAX_CONTENT_CHARS) + "..."
: content; : 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 ── // ── Step 1: Single-word classification ──
const classifyPrompt = `Pesan berikut perlu diklasifikasikan sebagai: clean, warn, atau flagged. 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. - 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. - Nama proyek, tools, framework (IMPHNEN, Bete, Cursor, Claude, React, Discord) = clean.
- Percakapan multilingual (campuran Indonesia-Inggris) = clean. - Percakapan multilingual (campuran Indonesia-Inggris) = clean.
${userProfileCtx}
Pesan: "${truncatedContent}" Pesan: "${truncatedContent}"
Jawab HANYA dengan satu kata: clean, warn, atau flagged`; Jawab HANYA dengan satu kata: clean, warn, atau flagged`;
@@ -1582,7 +1593,7 @@ Jawab HANYA dengan satu kata: clean, warn, atau flagged`;
const categoryOptions = const categoryOptions =
status === "flagged" ? "harassment, gambling, atau sara" : "spam"; status === "flagged" ? "harassment, gambling, atau sara" : "spam";
const reasonPrompt = `Pesan berikut telah diklasifikasikan sebagai "${status}". const reasonPrompt = `Pesan berikut telah diklasifikasikan sebagai "${status}".
${userProfileCtx}
Pesan: "${truncatedContent}" 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. 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.
@@ -18,29 +18,48 @@ async function learnUserProfile(
): Promise<void> { ): Promise<void> {
const db = getDatabase(); 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 const recentMessages = await db
.select({ .select({
content: messagesTable.content, content: messagesTable.content,
channelId: messagesTable.channel_id,
}) })
.from(messagesTable) .from(messagesTable)
.where( .where(
and( and(
eq(messagesTable.user_id, userId), eq(messagesTable.user_id, userId),
eq(messagesTable.guild_id, guildId), eq(messagesTable.guild_id, guildId),
eq(messagesTable.ai_status, "clean"),
), ),
) )
.orderBy(desc(messagesTable.created_at)) .orderBy(desc(messagesTable.created_at))
.limit(100); .limit(100);
if (recentMessages.length < 10) { 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; return;
} }
// Group messages by channel for channel-aware profiling
const channelGroups = new Map<string, { content: string; channelId: string }[]>();
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 const messagesText = recentMessages
.reverse() .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"); .join("\n");
const prompt = `Anda adalah AI ahli psikologi, analisis perilaku online, dan pembaca karakter. 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. gaya bicara — berdasarkan riwayat pesan-pesan mereka di server Discord.
Buatlah ringkasan yang KAYA AKAN PERSONALITAS sehingga pembaca merasa "mengenal" orang ini. 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):
<messages> <messages>
${messagesText} ${messagesText}
</messages> </messages>
Distribusi aktivitas user per channel:
${channelSummary}
Berdasarkan pesan-pesan di atas, buatlah ringkasan singkat (maksimal 4 paragraf) Berdasarkan pesan-pesan di atas, buatlah ringkasan singkat (maksimal 4 paragraf)
mengenai: mengenai:
@@ -60,8 +82,9 @@ mengenai:
Apakah orang ini suka pake singkatan, emot, reaksi berlebihan ("WKWKWK"), atau nada datar? Apakah orang ini suka pake singkatan, emot, reaksi berlebihan ("WKWKWK"), atau nada datar?
Bagaimana mereka memulai dan mengakhiri pembicaraan? Bagaimana mereka memulai dan mengakhiri pembicaraan?
2. **Topik-topik yang sering dibahas** — apa PASSION mereka? Coding, gaming, musik, debat? 2. **Topik-topik yang sering dibahas & channel favorit** — apa PASSION mereka? Coding, gaming, musik, debat?
Apakah mereka inisiator topik atau lebih suka merespon? 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? 3. **Kepribadian dan karakter yang terpancar** — Apakah mereka ramah dan hangat? Kritis dan analitis?
Easy going? Gampang marah? Humoris? Supportif? Suka memprovokasi? Suka membantu? Easy going? Gampang marah? Humoris? Supportif? Suka memprovokasi? Suka membantu?