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:
@@ -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?
|
||||||
|
|||||||
Reference in New Issue
Block a user