From 820d46ae358b43a875234a1a5ea25c66c6bd573b Mon Sep 17 00:00:00 2001 From: Izan Gil <66965250+SrIzan10@users.noreply.github.com> Date: Fri, 25 Jul 2025 23:50:40 +0200 Subject: [PATCH] feat: ai messaging --- TODO.md | 8 ---- bun.lock | 5 ++- package.json | 7 +-- src/config.ts | 2 +- src/events/ai/message.ts | 11 +++++ src/index.ts | 6 ++- src/utils/aiHandle.ts | 92 ++++++++++++++++++++++++++++++++++++++++ src/utils/openai.ts | 6 +++ 8 files changed, 123 insertions(+), 14 deletions(-) create mode 100644 src/events/ai/message.ts create mode 100644 src/utils/aiHandle.ts create mode 100644 src/utils/openai.ts diff --git a/TODO.md b/TODO.md index b5634bd..1b4392b 100644 --- a/TODO.md +++ b/TODO.md @@ -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 \ No newline at end of file diff --git a/bun.lock b/bun.lock index 4a39205..a02e69c 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], diff --git a/package.json b/package.json index f9bd3bf..f4606fd 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/src/config.ts b/src/config.ts index 7808662..bdf33bd 100644 --- a/src/config.ts +++ b/src/config.ts @@ -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' diff --git a/src/events/ai/message.ts b/src/events/ai/message.ts new file mode 100644 index 0000000..b79f652 --- /dev/null +++ b/src/events/ai/message.ts @@ -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); + } +}); diff --git a/src/index.ts b/src/index.ts index bc4aa79..4a45d0f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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, ], }); diff --git a/src/utils/aiHandle.ts b/src/utils/aiHandle.ts new file mode 100644 index 0000000..02fcfb5 --- /dev/null +++ b/src/utils/aiHandle.ts @@ -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>, isThread = false) { + if (msg.author.bot) return; + const threadCh = msg.channel as PublicThreadChannel; + 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 }[], + }); + } +} diff --git a/src/utils/openai.ts b/src/utils/openai.ts new file mode 100644 index 0000000..681d104 --- /dev/null +++ b/src/utils/openai.ts @@ -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" +}); \ No newline at end of file