From 83f6f68d95c46d3ef2063b4c9ed2bde051ae9fa5 Mon Sep 17 00:00:00 2001 From: milo Date: Sun, 14 Sep 2025 02:48:08 +0200 Subject: [PATCH] AI prompt upgrade --- src/bot/handlers/messageCreate.js | 61 ++++++++++----- src/utils/ai.js | 121 ++++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+), 18 deletions(-) diff --git a/src/bot/handlers/messageCreate.js b/src/bot/handlers/messageCreate.js index d082dab..c0af9b0 100644 --- a/src/bot/handlers/messageCreate.js +++ b/src/bot/handlers/messageCreate.js @@ -1,5 +1,12 @@ import { sleep } from 'openai/core'; -import { gork } from '../../utils/ai.js'; +import { + buildAiMessages, + buildParticipantsMap, + buildTranscript, + CONTEXT_LIMIT, + gork, INCLUDE_ATTACHMENT_URLS, MAX_ATTS_PER_MESSAGE, + stripMentionsOfBot +} from '../../utils/ai.js'; import { formatTime, postAPOBuy, @@ -129,29 +136,47 @@ async function handleAiMention(message, client, io) { // --- AI Processing --- try { - message.channel.sendTyping(); - // Fetch last 20 messages for context - const fetchedMessages = await message.channel.messages.fetch({ limit: 20 }); - const messagesArray = Array.from(fetchedMessages.values()).reverse(); // Oldest to newest + await message.channel.sendTyping(); - const requestMessage = message.content.replace(`<@${client.user.id}>`, '').trim(); + // 1) Récup contexte + const fetched = await message.channel.messages.fetch({ limit: Math.min(CONTEXT_LIMIT, 100) }); + const messagesArray = Array.from(fetched.values()).reverse(); // oldest -> newest - // Format the conversation for the AI - const messageHistory = messagesArray.map(msg => ({ - role: msg.author.id === client.user.id ? 'assistant' : 'user', - content: `${authorId} a dit: ${msg.content}` + const requestText = stripMentionsOfBot(message.content, client.user.id); + const invokerId = message.author.id; + const invokerName = message.member?.nickname || message.author.globalName || message.author.username; + const repliedUserId = message.mentions?.repliedUser?.id || null; + + // 2) Compact transcript & participants + const participants = buildParticipantsMap(messagesArray); + const transcript = buildTranscript(messagesArray, client.user.id); + + const invokerAttachments = Array.from(message.attachments?.values?.() || []).slice(0, MAX_ATTS_PER_MESSAGE).map(a => ({ + id: a.id, + name: a.name, + type: a.contentType || 'application/octet-stream', + size: a.size, + isImage: !!(a.contentType && a.contentType.startsWith('image/')), + url: INCLUDE_ATTACHMENT_URLS ? a.url : undefined, })); - const idToUser = getAllUsers.all().map(u => `${u.id} est ${u.username}/${u.globalName}`).join(', '); - - // Add system prompts - messageHistory.unshift( - { role: 'system', content: "Adopte une attitude détendue de membre du serveur. Réponds comme si tu participais à la conversation ne commence surtout pas tes messages par 'tel utilisateur a dit' il faut que ce soit fluide, pas trop long, évite de te répéter, évite de te citer toi-même ou quelqu'un d'autre. Utilise les emojis du serveur quand c'est pertinent. Ton id est 132380758368780288, ton nom est FlopoBot." }, - { role: 'system', content: `L'utilisateur qui s'adresse à toi est <@${authorId}>. Son message est une réponse à ${message.mentions.repliedUser ? `<@${message.mentions.repliedUser.id}>` : 'personne'}.` }, - { role: 'system', content: `Voici les différents utilisateurs : ${idToUser}, si tu veux t'adresser ou nommer un utilisateur, utilise leur ID comme suit : <@ID>` }, - ); + // 3) Construire prompts + const messageHistory = buildAiMessages({ + botId: client.user.id, + botName: 'FlopoBot', + invokerId, + invokerName, + requestText, + transcript, + participants, + repliedUserId, + invokerAttachments, + }); + // 4) Appel modèle const reply = await gork(messageHistory); + + // 5) Réponse await message.reply(reply); } catch (err) { diff --git a/src/utils/ai.js b/src/utils/ai.js index b552dfc..dd4ad99 100644 --- a/src/utils/ai.js +++ b/src/utils/ai.js @@ -38,6 +38,7 @@ export async function gork(messageHistory) { if (modelProvider === 'OpenAI' && openai) { const completion = await openai.chat.completions.create({ model: "gpt-5", // Using a modern, cost-effective model + reasoning_effort: "low", messages: messageHistory, }); return completion.choices[0].message.content; @@ -79,4 +80,124 @@ export async function gork(messageHistory) { console.error(`[AI] Error with ${modelProvider} API:`, error); return "Oups, une erreur est survenue en contactant le service d'IA."; } +} + +export const CONTEXT_LIMIT = parseInt(process.env.AI_CONTEXT_MESSAGES || '100', 10); +export const MAX_ATTS_PER_MESSAGE = parseInt(process.env.AI_MAX_ATTS_PER_MSG || '3', 10); +export const INCLUDE_ATTACHMENT_URLS = (process.env.AI_INCLUDE_ATTACHMENT_URLS || 'true') === 'true'; + +export const stripMentionsOfBot = (text, botId) => + text.replace(new RegExp(`<@!?${botId}>`, 'g'), '').trim(); + +export const sanitize = (s) => + (s || '') + .replace(/\s+/g, ' ') + .replace(/```/g, 'ʼʼʼ') // éviter de casser des fences éventuels + .trim(); + +export const shortTs = (d) => new Date(d).toISOString(); // compact et triable + +export function buildParticipantsMap(messages) { + const map = {}; + for (const m of messages) { + const id = m.author.id; + if (!map[id]) { + map[id] = { + id, + username: m.author.username, + globalName: m.author.globalName || null, + isBot: !!m.author.bot, + }; + } + } + return map; +} + +export function buildTranscript(messages, botId) { + // Oldest -> newest, JSONL compact, une ligne par message pertinent + const lines = []; + for (const m of messages) { + const content = sanitize(m.content); + const atts = Array.from(m.attachments?.values?.() || []); + if (!content && atts.length === 0) continue; + + const attMeta = atts.length + ? atts.slice(0, MAX_ATTS_PER_MESSAGE).map(a => ({ + id: a.id, + name: a.name, + type: a.contentType || 'application/octet-stream', + size: a.size, + isImage: !!(a.contentType && a.contentType.startsWith('image/')), + width: a.width || undefined, + height: a.height || undefined, + spoiler: typeof a.spoiler === 'boolean' ? a.spoiler : false, + url: INCLUDE_ATTACHMENT_URLS ? a.url : undefined, // désactive par défaut + })) + : undefined; + + const line = { + t: shortTs(m.createdTimestamp || Date.now()), + id: m.author.id, + nick: m.member?.nickname || m.author.globalName || m.author.username, + isBot: !!m.author.bot, + mentionsBot: new RegExp(`<@!?${botId}>`).test(m.content || ''), + replyTo: m.reference?.messageId || null, + content, + attachments: attMeta, + }; + lines.push(line); + } + return lines.map(l => JSON.stringify(l)).join('\n'); +} + +export function buildAiMessages({ + botId, + botName = 'FlopoBot', + invokerId, + invokerName, + requestText, + transcript, + participants, + repliedUserId, + invokerAttachments = [], +}) { + const system = { + role: 'system', + content: + `Tu es ${botName} (ID: ${botId}) sur un serveur Discord. Style: bref, naturel, détendu, comme un pote. + Règles de sortie: + - Réponds en français, en 1–3 phrases. + - Réponds PRINCIPALEMENT au message de <@${invokerId}>. Le transcript est un contexte facultatif. + - Pas de "Untel a dit…", pas de longs préambules. + - Utilise <@ID> pour mentionner quelqu'un. + - Tu ne peux PAS ouvrir les liens; si des pièces jointes existent, tu peux simplement les mentionner (ex: "ta photo", "le PDF").`, + }; + + const attLines = invokerAttachments.length + ? invokerAttachments.map(a => `- ${a.name} (${a.type || 'type inconnu'}, ${a.size ?? '?'} o${a.isImage ? ', image' : ''})`).join('\n') + : ''; + + const user = { + role: 'user', + content: + `Tâche: répondre brièvement à <@${invokerId}>. + + Message de <@${invokerId}> (${invokerName || 'inconnu'}): + """ + ${requestText} + """ + ${invokerAttachments.length ? `Pièces jointes du message: + ${attLines} + ` : ''}${repliedUserId ? `Ce message répond à <@${repliedUserId}>.` : ''} + + Participants (id -> nom): + ${Object.values(participants).map(p => `- ${p.id} -> ${p.globalName || p.username}`).join('\n')} + + Contexte (transcript JSONL; à utiliser seulement si utile): + \`\`\`jsonl + ${transcript} + \`\`\``, + }; + + return [system, user]; } \ No newline at end of file