feat: ai messaging

This commit is contained in:
2025-07-25 23:50:40 +02:00
parent f1b5662480
commit 820d46ae35
8 changed files with 123 additions and 14 deletions

View File

@@ -52,13 +52,5 @@
# Database
- [x] Migration to sqlite
# Command Features to Preserve
- [ ] Autocomplete functionality for various commands
- [ ] Interactive components (buttons, select menus)
- [ ] File attachments and image processing
- [ ] API integrations (TheCatAPI, TheDogAPI, Genius, etc.)
- [ ] Canvas-based image generation
- [ ] Modal forms and user input handling
# Other
- [ ] Figure out fonts

View File

@@ -6,12 +6,13 @@
"dependencies": {
"@napi-rs/canvas": "^0.1.72",
"@prisma/client": "^6.10.1",
"@sern/handler": "^4.0.0",
"@sern/handler": "^4.2.4",
"@sern/publisher": "1.1.2",
"discord.js": "^14.21.0",
"dotenv": "^16.3.1",
"mongodb": "^6.17.0",
"node-html-parser": "^7.0.1",
"openai": "^5.10.2",
"rockpaperscissors-checker": "^1.2.0",
"sharp": "^0.34.2",
},
@@ -368,6 +369,8 @@
"onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="],
"openai": ["openai@5.10.2", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-n+vi74LzHtvlKcDPn9aApgELGiu5CwhaLG40zxLTlFQdoSJCLACORIPC2uVQ3JEYAbqapM+XyRKFy2Thej7bIw=="],
"ora": ["ora@6.3.1", "", { "dependencies": { "chalk": "^5.0.0", "cli-cursor": "^4.0.0", "cli-spinners": "^2.6.1", "is-interactive": "^2.0.0", "is-unicode-supported": "^1.1.0", "log-symbols": "^5.1.0", "stdin-discarder": "^0.1.0", "strip-ansi": "^7.0.1", "wcwidth": "^1.0.1" } }, "sha512-ERAyNnZOfqM+Ao3RAvIXkYh5joP220yf59gVe2X/cI6SiCxIdi4c9HZKZD8R6q/RDXEje1THBju6iExiSsgJaQ=="],
"p-limit": ["p-limit@4.0.0", "", { "dependencies": { "yocto-queue": "^1.0.0" } }, "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ=="],

View File

@@ -6,11 +6,11 @@
"main": "dist/index.js",
"scripts": {
"build": "sern build",
"start": "bun run .",
"start": "bun run --inspect .",
"install": "sern build",
"commands:publish": "sern commands publish",
"dev": "sern build -w --watch-command \"bun start\"",
"db:mongo": "bun run src/utils/db/migrateMongo.ts"
"db:mongo": "bun run src/utils/db/migrateMongo.ts"
},
"keywords": [
"typescript",
@@ -20,12 +20,13 @@
"dependencies": {
"@napi-rs/canvas": "^0.1.72",
"@prisma/client": "^6.10.1",
"@sern/handler": "^4.0.0",
"@sern/handler": "^4.2.4",
"@sern/publisher": "1.1.2",
"discord.js": "^14.21.0",
"dotenv": "^16.3.1",
"mongodb": "^6.17.0",
"node-html-parser": "^7.0.1",
"openai": "^5.10.2",
"rockpaperscissors-checker": "^1.2.0",
"sharp": "^0.34.2"
},

View File

@@ -3,7 +3,7 @@
//commands directory. REQUIRED
export const commands = './dist/commands'
// events directory.
// export const events = './dist/events'
export const events = './dist/events'
// schedule tasks and declare them here
// export const tasks = './dist/tasks'

11
src/events/ai/message.ts Normal file
View File

@@ -0,0 +1,11 @@
import { aiHandle } from '#/aiHandle';
import { EventType, eventModule } from '@sern/handler';
import { ChannelType } from 'discord.js';
export default eventModule({
type: EventType.Discord,
name: 'messageCreate',
execute: async (msg) => {
await aiHandle(msg, msg.channel.type === ChannelType.PublicThread);
}
});

View File

@@ -8,8 +8,12 @@ import prisma from './utils/db/index.js';
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildMessageReactions,
GatewayIntentBits.GuildVoiceStates,
],
});

92
src/utils/aiHandle.ts Normal file
View File

@@ -0,0 +1,92 @@
import { Message, OmitPartialGroupDMChannel, PublicThreadChannel } from 'discord.js';
import { ChatCompletionMessageParam } from 'openai/resources/index';
import { openai } from './openai';
import prisma from './db';
export async function aiHandle(msg: OmitPartialGroupDMChannel<Message<boolean>>, isThread = false) {
if (msg.author.bot) return;
const threadCh = msg.channel as PublicThreadChannel<false>;
if (isThread && (threadCh.parentId !== process.env.CHATGPT_CHANNEL)) return;
if (!isThread && (msg.channelId !== process.env.CHATGPT_CHANNEL)) return;
if (msg.content.startsWith('!')) return;
let aiChatId;
const systemMsg =
'You are Vinci, a friendly and helpful Discord bot assistant dedicated to answering all user questions clearly and naturally, as if texting a friend. Avoid mentioning that you are an assistant, since users already know this. When it is useful, you can use markdown. You will interact with Spanish-speaking users, so all your responses, including any future ones, must be written exclusively in Spanish without exception.';
const messages: ChatCompletionMessageParam[] = [];
if (isThread) {
const dbMsgs = await prisma.aiChat.findFirst({
where: { threadid: threadCh.id },
select: { messages: true, id: true },
});
if (dbMsgs) {
messages.push(
...dbMsgs.messages.map((m) => ({
content: m.content,
role: m.role as 'user' | 'assistant' | 'system',
}))
);
}
messages.push({ role: 'user', content: msg.content });
aiChatId = dbMsgs?.id;
} else {
messages.push({ role: 'system', content: systemMsg }, { role: 'user', content: msg.content });
}
const sentMsg = await msg.reply(':sparkles: Pensando...');
const stream = await openai.chat.completions.create({
model: 'llama-3.3-70b-versatile',
messages,
max_tokens: 2000,
max_completion_tokens: 2000,
temperature: 0.7,
});
const message = stream.choices[0].message.content!
await sentMsg.edit(message.slice(0, 2000));
messages.push({ role: 'assistant', content: message.replace(/^\n{2}/, '') });
if (!isThread) {
const titleMessage = (
await openai.chat.completions.create({
model: 'llama-3.3-70b-versatile',
messages: [
{ role: 'system', content: systemMsg },
{
role: 'user',
content: `Give a short title for the following AI prompt. Do NOT use markdown. USE SPANISH:\n\n${message}`,
},
],
max_tokens: 50,
temperature: 0.7,
})
).choices[0].message
.content!.trim()
.slice(0, 100);
const thread = await sentMsg.startThread({
name: titleMessage || 'Nuevo hilo',
});
await prisma.aiChat.create({
data: {
messageid: msg.id,
threadid: thread.id,
messages: {
createMany: {
data: messages as { role: string; content: string }[],
},
},
},
});
} else {
await prisma.aiMessage.createMany({
data: messages.map((m) => ({
role: m.role,
content: m.content,
aiChatId: aiChatId!,
})) as { role: string; content: string; aiChatId: number }[],
});
}
}

6
src/utils/openai.ts Normal file
View File

@@ -0,0 +1,6 @@
import OpenAI from "openai";
export const openai = new OpenAI({
apiKey: process.env.AI_KEY,
baseURL: "https://api.groq.com/openai/v1"
});