mirror of
https://github.com/cassoule/flopobot_v2.git
synced 2026-03-18 21:40:27 +01:00
AI prompt upgrade
This commit is contained in:
@@ -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) {
|
||||
|
||||
121
src/utils/ai.js
121
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];
|
||||
}
|
||||
Reference in New Issue
Block a user