import { eq, desc, sql, and } from "drizzle-orm"; import { getDatabase } from "../../shared/database/drizzle.js"; import { messagesTable, channelCulturesTable, } from "../../shared/database/schema.js"; import { config } from "../../shared/config/config.js"; import { createChildLogger } from "@bete/shared/logger"; import { llmChat } from "./llmClient.js"; import { updateChannelCulture } from "./channelCultureStore.js"; const CULTURE_LEARNING_INTERVAL = 1000 * 60 * 60 * 12; // 12 hours const log = createChildLogger("cultureLearner"); async function learnChannelCulture(channelId: string, guildId: string): Promise { const db = getDatabase(); // Get recent clean messages for this channel const recentMessages = await db .select({ content: messagesTable.content, username: messagesTable.username, }) .from(messagesTable) .where( and( eq(messagesTable.channel_id, channelId), eq(messagesTable.ai_status, "clean") ) ) .orderBy(desc(messagesTable.created_at)) .limit(100); if (recentMessages.length < 10) { log.debug({ channelId }, "Not enough messages to learn culture"); return; } const messagesText = recentMessages .reverse() .map(m => `${m.username}: ${m.content}`) .join("\n"); const prompt = `Anda adalah AI ahli perilaku sosiologis dan budaya online. Tugas Anda adalah merangkum budaya (culture) dari sebuah channel chat berdasarkan riwayat pesan-pesan yang dianggap bersih (clean/tidak melanggar). Pesan-pesan terakhir: ${messagesText} Berdasarkan pesan-pesan di atas, buatlah ringkasan singkat (maksimal 3 paragraf) mengenai gaya bahasa, topik obrolan, dan norma sosial di channel ini. Ringkasan ini akan digunakan oleh sistem AI moderasi untuk memahami konteks dan "inside jokes" yang wajar di channel ini. Jangan menambahkan teks basa-basi, langsung berikan ringkasannya.`; try { const completion = await llmChat({ messages: [{ role: "user", content: prompt }], max_tokens: 500, temperature: 0.7, // Higher temp for summarization retries: 2, }); if (!completion) throw new Error("Empty response from LLM"); const text = completion.choices[0]?.message?.content?.trim(); if (!text) throw new Error("Empty response from LLM"); await updateChannelCulture(channelId, guildId, text); log.info({ channelId, guildId }, "Successfully learned and updated channel culture"); } catch (error) { log.error({ channelId, error }, "Failed to learn channel culture"); } } export async function runCultureLearningCycle(): Promise { const db = getDatabase(); log.info("Starting culture learning cycle"); try { // Find channels that haven't been analyzed recently // We do a simple distinct channel_id query with a left join to see if it's stale const staleChannels = await db.execute(sql` SELECT m.channel_id, m.guild_id FROM (SELECT DISTINCT channel_id, guild_id FROM messages) m LEFT JOIN channel_cultures c ON m.channel_id = c.channel_id WHERE c.last_analyzed_at IS NULL OR c.last_analyzed_at < ${Date.now() - CULTURE_LEARNING_INTERVAL} LIMIT 50 `); for (const row of staleChannels.rows || staleChannels) { // Cast the row because execute() returns untyped Record[] const channelId = String(row.channel_id); const guildId = String(row.guild_id); await learnChannelCulture(channelId, guildId); } } catch (error) { log.error({ error }, "Error in culture learning cycle"); } } let cultureInterval: NodeJS.Timeout | null = null; export function startCultureLearnerWorker(): void { if (!config.AI_ANALYSIS_ENABLED) return; if (cultureInterval) return; // Run once on startup after 1 minute, then every 1 hour setTimeout(() => { runCultureLearningCycle().catch(e => log.error(e)); }, 60000); cultureInterval = setInterval(() => { runCultureLearningCycle().catch(e => log.error(e)); }, 1000 * 60 * 60); // Check every hour for channels that reached 12h expiry log.info("Started background culture learner worker"); }