diff --git a/backend-server/groq-backend.js b/backend-server/groq-backend.js new file mode 100644 index 0000000..a39429f --- /dev/null +++ b/backend-server/groq-backend.js @@ -0,0 +1,241 @@ +import 'dotenv/config'; +import express from 'express'; +import bodyParser from 'body-parser'; +import cmd from 'cmd-promise'; +import cors from 'cors'; +import cheerio from 'cheerio'; +import llamaTokenizer from 'llama-tokenizer-js'; +import googleIt from 'google-it'; +import Groq from 'groq-sdk'; + +// Constants and initialization +const app = express(); +const port = 3000; +const prompt = process.env.PROMPT; + +const groq = new Groq({ apiKey: process.env.GROQ }); + +app.use(cors({ + origin: '*', + allowedHeaders: ['Content-Type', 'x-forwarded-for-id', 'x-forwarded-for-name'] +})); +app.use(bodyParser.json()); + +let isProcessing = false; +let conversationHistory = {}; + +// Helper function to get current timestamp +const getTimestamp = () => { + const now = new Date(); + const date = now.toLocaleDateString('en-US'); + const time = now.toLocaleTimeString('en-US'); + return `${date} [${time}]`; +}; + +// Middleware to track conversation history by CF-Connecting-IP +app.use((req, res, next) => { + const ip = req.headers['x-forwarded-for-id'] || req.headers['cf-connecting-ip'] || req.headers['x-forwarded-for'] || req.headers['x-real-ip'] || req.ip; + const name = req.headers['x-forwarded-for-name']; + const guild = req.headers['x-forwarded-for-guild']; + + req.clientIp = ip; // Store the IP in a request property + + if (!conversationHistory[req.clientIp]) { + console.log(`${getTimestamp()} [INFO] Initializing conversation history for: ${req.clientIp}`); + conversationHistory[req.clientIp] = [{ + role: 'system', + content: `My name is: ${name || 'Unknown'}, my Discord ID is: ${req.clientIp}.` + (guild ? ` We are chatting inside ${guild} a Discord Server.` : '') + prompt + }]; + } + + next(); +}); + +function countLlamaTokens(messages) { + let totalTokens = 0; + for (const message of messages) { + if (message.role === 'user' || message.role === 'assistant') { + const encodedTokens = llamaTokenizer.encode(message.content); + totalTokens += encodedTokens.length; + } + } + return totalTokens; +} + +function trimConversationHistory(ip, maxLength = 14000, tolerance = 25) { + const messages = conversationHistory[ip]; + let totalTokens = countLlamaTokens(messages); + + while (totalTokens > maxLength + tolerance && messages.length > 1) { + messages.shift(); // Remove the oldest messages first + totalTokens = countLlamaTokens(messages); + } +} + +// Function to scrape web page +async function scrapeWebPage(url, length = 2000) { + try { + const res = await fetch(url); + const html = await res.text(); + const $ = cheerio.load(html); + + const pageTitle = $('head title').text().trim(); + const pageDescription = $('head meta[name="description"]').attr('content'); + let plainTextContent = $('body').text().trim().replace(/[\r\n\t]+/g, ' '); + + if (plainTextContent.length > length) { + plainTextContent = plainTextContent.substring(0, length) + '...'; + } + + return `Title: ${pageTitle}\nDescription: ${pageDescription || 'N/A'}\nContent: ${plainTextContent}\nURL: ${url}`; + } catch (err) { + console.error(`${getTimestamp()} [ERROR] Failed to scrape URL: ${url}`, err); + return null; + } +} + +// Function to handle IP plugin +async function handleIPPlugin(ipAddr, ip, conversationHistory) { + try { + const url = new URL('https://api.abuseipdb.com/api/v2/check'); + url.searchParams.append('ipAddress', ipAddr); + url.searchParams.append('maxAgeInDays', '90'); + url.searchParams.append('verbose', ''); + + const options = { + method: 'GET', + headers: { + 'Key': process.env.ABUSE_KEY, + 'Accept': 'application/json' + } + }; + + const response = await fetch(url, options); + const data = await response.json(); + + let abuseResponse = `IP: ${ipAddr}\n`; + abuseResponse += `Abuse Score: ${data.data.abuseConfidenceScore}\n`; + abuseResponse += `Country: ${data.data.countryCode}\n`; + abuseResponse += `Usage Type: ${data.data.usageType}\n`; + abuseResponse += `ISP: ${data.data.isp}\n`; + abuseResponse += `Domain: ${data.data.domain}\n`; + + if (data.data.totalReports) { + abuseResponse += `Total Reports: ${data.data.totalReports}\n`; + abuseResponse += `Last Reported: ${data.data.lastReportedAt}\n`; + } + + const lastMessageIndex = conversationHistory[ip].length - 1; + if (lastMessageIndex >= 0) { + conversationHistory[ip][lastMessageIndex].content += "\n" + abuseResponse; + console.log(`${getTimestamp()} [INFO] Processed IP address: ${ipAddr}, response: ${abuseResponse}`); + } else { + console.error(`${getTimestamp()} [ERROR] Conversation history is unexpectedly empty for: ${ip}`); + } + } catch (err) { + console.error(`${getTimestamp()} [ERROR] Failed to process IP address: ${ipAddr}`, err); + } +} + +// Main chat handler +app.post('/api/v1/chat', async (req, res) => { + const startTime = Date.now(); + const ip = req.clientIp; + +// if (isProcessing) { +// return res.status(429).json({ message: "System is busy processing another request, try again later" }); +// } + + isProcessing = true; + + try { + const userMessage = req.body.message + `\nDate/Time: ${getTimestamp()}`; + conversationHistory[ip].push({ role: 'user', content: userMessage }); + trimConversationHistory(ip); + + const pluginTasks = []; + + // Check for IPs in user message and process them + const ipRegex = /(\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b)/g; + const ipAddresses = userMessage.match(ipRegex); + if (ipAddresses) { + for (const ipAddr of ipAddresses) { + pluginTasks.push(handleIPPlugin(ipAddr, ip, conversationHistory)); + } + } + + // Check for URLs and scrape them + const urls = userMessage.match(/(https?:\/\/[^\s]+)/g); + if (urls) { + for (const url of urls) { + pluginTasks.push(scrapeWebPage(url).then(content => { + if (content) { + conversationHistory[ip].push({ role: 'assistant', content }); + } + })); + } + } + + await Promise.all(pluginTasks); + + const completion = await groq.chat.completions.create({ + messages: conversationHistory[ip], + //model: "llama3-8b-8192" + model: "llama-3.2-3b-preview" + }); + + const assistantMessage = completion.choices[0].message.content; + conversationHistory[ip].push({ role: 'assistant', content: assistantMessage }); + + res.json(assistantMessage); + } catch (error) { + console.error(`${getTimestamp()} [ERROR] An error occurred`, error); + res.status(500).json({ message: "An error occurred", error: error.message }); + } finally { + isProcessing = false; + const endTime = Date.now(); + const processingTime = ((endTime - startTime) / 1000).toFixed(2); + console.log(`${getTimestamp()} [STATS] Processing Time: ${processingTime} seconds`); + } +}); + +// Endpoint to restart core service +app.post('/api/v1/restart-core', (req, res) => { + console.log(`${getTimestamp()} [INFO] Restarting core service`); + cmd(`docker restart llama-gpu-server`).then(out => { + console.log(`${getTimestamp()} [INFO] Core service restarted`); + res.json(out.stdout); + }).catch(err => { + console.error(`${getTimestamp()} [ERROR] Failed to restart core service`, err); + res.status(500).json({ + message: "An error occurred while restarting the core service", + error: err.message + }); + }); +}); + +// Endpoint to reset conversation history +app.post('/api/v1/reset-conversation', (req, res) => { + const ip = req.clientIp; + console.log(`${getTimestamp()} [INFO] Resetting conversation history for: ${ip}`); + + conversationHistory[ip] = [{ + role: 'system', + content: prompt + }]; + + console.log(`${getTimestamp()} [INFO] Conversation history reset for: ${ip}`); + res.json({ message: "Conversation history reset for: " + ip }); +}); + +// Get conversation history for debugging purposes +app.get('/api/v1/conversation-history', (req, res) => { + const ip = req.clientIp; + console.log(`${getTimestamp()} [INFO] Fetching conversation history for: ${ip}`); + res.json(conversationHistory[ip]); +}); + +// Start server +app.listen(port, () => { + console.log(`${getTimestamp()} [INFO] Server running at http://localhost:${port}`); +}); diff --git a/backend-server/package.json b/backend-server/package.json index 81c7217..326f271 100644 --- a/backend-server/package.json +++ b/backend-server/package.json @@ -17,6 +17,7 @@ "dotenv": "^16.4.5", "express": "^4.19.2", "google-it": "^1.6.4", + "groq-sdk": "^0.7.0", "llama-tokenizer-js": "^1.2.2" } } diff --git a/bot/discord-bot-groq.js b/bot/discord-bot-groq.js new file mode 100644 index 0000000..d139b2b --- /dev/null +++ b/bot/discord-bot-groq.js @@ -0,0 +1,200 @@ +const { Client, GatewayIntentBits, EmbedBuilder } = require('discord.js'); +const axios = require('axios'); +const he = require('he'); +const fetch = (...args) => import('node-fetch').then(({ default: fetch }) => fetch(...args)); +const { userResetMessages } = require('./assets/messages.js'); +const cheerio = require('cheerio'); + +require('dotenv').config(); + +const channelIDs = process.env.CHANNEL_IDS.split(','); + +const client = new Client({ + intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent] +}); + +const MAX_CONTENT_LENGTH = process.env.MAX_CONTENT_LENGTH || 8000; + +client.on('ready', () => { + console.log(`Logged in as ${client.user.tag}!`); +}); + +client.on('messageCreate', async message => { + if (message.guildId == "1192181551526584380") return + if (message.content.includes("[LOG]")) return + console.log(`Received message: ${message.content}, from ${message.author.tag}, in channel: ${message.channel.name}`); + console.log(message.guildId) + + // Function to send a random message from any array + async function sendRand(array) { + const arrayChoice = array[Math.floor(Math.random() * array.length)]; + console.log(`Sending random response: ${arrayChoice}`); + await message.channel.send(arrayChoice); // give a notification of reset using a human-like response. + } + + if (message.author.bot) return; + + // Only respond in the specified channels + if (!channelIDs.includes(message.channel.id)) { + console.log(`Ignoring message from channel: ${message.channel.id}, not in the specified channels.`); + return; + } + + const content = message.content.trim(); + let additionalContent = ''; + + if (content === '!r' || content === '!reset') { + console.log("Reset command received."); + await resetConversation(message); + // Handle conversation reset + return await sendRand(userResetMessages); + } + if (content === '!restartCore') { + console.log("Restart core command received."); + // Handle core restart + return await restartCore(message); + } + + console.log(`Handling user message: ${content}`); + await handleUserMessage(message, content, additionalContent); +}); + + +async function handleUserMessage(message, content, additionalContent) { + const encodedMessage = he.encode(content + additionalContent); + console.log(`Encoded message: ${encodedMessage}`); + + const typingInterval = setInterval(() => { + message.channel.sendTyping(); + }, 9000); + message.channel.sendTyping(); // Initial typing indicator + + try { + console.log(`Sending message to API at: http://${process.env.ROOT_IP}:${process.env.ROOT_PORT}/api/v1/chat`); + const response = await axios.post(`http://${process.env.ROOT_IP}:${process.env.ROOT_PORT}/api/v1/chat`, { + message: encodedMessage, + max_tokens: Number(process.env.MAX_TOKENS), + repeat_penalty: Number(process.env.REPEAT_PENALTY) + }, { + headers: { + 'Content-Type': 'application/json', + 'x-forwarded-for-id': message.author.id, + 'x-forwarded-for-name': message.author.username, + 'x-forwarded-for-guild': message.guild.name + } + }); + + console.log("API response received:", response.data); + + // If response.data is a string (no 'content' property), use it directly + const responseText = typeof response.data === 'string' ? response.data : response.data.content; + + if (!responseText) { + console.error("No content found in API response."); + return message.channel.send("Oops, something went wrong. No response content."); + } + + clearInterval(typingInterval); // Stop typing indicator + await sendLongMessage(message, responseText); + } catch (error) { + console.error("Error during API call:", error.message); + clearInterval(typingInterval); // Stop typing indicator + if (error.response && error.response.status === 429) { + console.log("API rate limited, trying to send DM."); + try { + await message.author.send('I am currently busy. Please try again later.'); + } catch (dmError) { + console.error('Failed to send DM:', dmError); + message.reply('I am currently busy. Please try again later.'); + } + } else { + message.reply('Error: ' + error.message); + } + } +} + +async function resetConversation(message) { + try { + console.log("Resetting conversation..."); + const response = await axios.post( + `${process.env.API_PATH}/reset-conversation`, {}, { + headers: { + 'Content-Type': 'application/json', + 'x-forwarded-for-id': message.author.id, + 'x-forwarded-for-name': message.author.username, + 'x-forwarded-for-guild': message.guild.name + } + } + ); + console.log(`Reset conversation status: ${response.status}`); + } catch (error) { + console.error("Error during reset conversation:", error); + } +} + +async function restartCore(message) { + try { + console.log("Restarting core..."); + const response = await axios.post(`${process.env.API_PATH}/restart-core`); + console.log(`Core restart status: ${response.status}`); + } catch (error) { + console.error("Error during core restart:", error); + } +} + +async function sendLongMessage(message, responseText) { + console.log(`Preparing to send response. Length: ${responseText ? responseText.length : 'undefined'}`); + + const limit = 8096; + + if (!responseText) { + console.error("Response text is undefined or null."); + return message.channel.send("Oops, I didn't get any response. Please try again."); + } + + if (responseText.length > limit) { + console.log("Response too long, splitting into chunks..."); + const lines = responseText.split('\n'); + const chunks = []; + let currentChunk = ''; + + for (const line of lines) { + if (currentChunk.length + line.length > limit) { + chunks.push(currentChunk); + currentChunk = ''; + } + currentChunk += line + '\n'; + } + + if (currentChunk.trim() !== '') { + chunks.push(currentChunk.trim()); + } + + if (chunks.length >= 80) return await message.channel.send("Response chunks too large. Try again"); + + for (let i = 0; i < chunks.length; i++) { + const chunk = chunks[i]; + const embed = new EmbedBuilder() + .setDescription(chunk) // Wraps the chunk in a code block + .setColor("#3498DB") + .setTimestamp(); + console.log(`Sending chunk ${i + 1}/${chunks.length}`); + setTimeout(() => { + message.channel.send({ + embeds: [embed] + }); + }, i * (process.env.OVERFLOW_DELAY || 3) * 1000); + } + } else { + console.log("Response fits within limit, sending single message."); + const embed = new EmbedBuilder() + .setDescription(responseText) // Wraps the response in a code block + .setColor("#3498DB") + .setTimestamp(); + message.channel.send({ + embeds: [embed] + }); + } +} + +client.login(process.env.THE_TOKEN); diff --git a/bot/installableApp-groq.js b/bot/installableApp-groq.js new file mode 100644 index 0000000..d7a22ee --- /dev/null +++ b/bot/installableApp-groq.js @@ -0,0 +1,302 @@ +const { Client, GatewayIntentBits, REST, Routes, EmbedBuilder, SlashCommandBuilder, AttachmentBuilder } = require('discord.js'); +const axios = require('axios'); +const he = require('he'); +const fs = require('fs'); +const PDFDocument = require('pdfkit'); +require('dotenv').config(); +const markdownIt = require('markdown-it'); +const puppeteer = require('puppeteer'); +const highlight = require('highlight.js'); + +const md = new markdownIt({ + highlight: function (str, lang) { + if (lang && highlight.getLanguage(lang)) { + try { + return highlight.highlight(str, { language: lang }).value; + } catch (__) {} + } + return ''; + } +}); + +const client = new Client({ + intents: [GatewayIntentBits.Guilds] +}); + +const userPrivacyFilePath = './userPrivacySettings.json'; +let userPrivacySettings = {}; +if (fs.existsSync(userPrivacyFilePath)) { + userPrivacySettings = JSON.parse(fs.readFileSync(userPrivacyFilePath)); +} + +function saveUserPrivacySettings() { + fs.writeFileSync(userPrivacyFilePath, JSON.stringify(userPrivacySettings, null, 2)); +} + +// Check if the user's responses should be ephemeral +function isEphemeral(userId) { + return userPrivacySettings[userId] || false; // Default to false (standard response) if not set +} + +let userLastInteraction = {}; +let conversationHistories = {}; + +async function resetConversationIfExpired(userId) { + const now = Date.now(); + if (userLastInteraction[userId] && now - userLastInteraction[userId] >= 3600000) { + try { + const response = await axios.post(`${process.env.API_PATH}/reset-conversation`, {}, { + headers: { + 'Content-Type': 'application/json', + 'x-forwarded-for-id': userId, + } + }); + console.log(`Conversation history reset for user ${userId} due to inactivity. Status: ${response.status}`); + } catch (error) { + console.error(`Failed to reset conversation for user ${userId}: ${error.message}`); + } + } +} + +function updateUserInteractionTime(userId) { + userLastInteraction[userId] = Date.now(); +} + +// Create the commands +const commands = [ + new SlashCommandBuilder().setName('reset').setDescription('Reset the conversation'), + new SlashCommandBuilder().setName('restartcore').setDescription('Restart the core service'), + new SlashCommandBuilder().setName('chat').setDescription('Send a chat message') + .addStringOption(option => + option.setName('message') + .setDescription('Message to send') + .setRequired(true)), + new SlashCommandBuilder().setName('privacy').setDescription('Toggle between ephemeral and standard responses'), + new SlashCommandBuilder().setName('pdf').setDescription('Download conversation history as a PDF') +]; + +// Add integration types and contexts to each command +const commandData = commands.map(command => { + const commandJSON = command.toJSON(); + const extras = { + "integration_types": [0, 1], // 0 for guilds, 1 for user apps + "contexts": [0, 1, 2] // 0 for guilds, 1 for app DMs, 2 for group DMs + }; + + // Merge extras into command JSON + Object.assign(commandJSON, extras); + return commandJSON; +}); + +const rest = new REST({ version: '10' }).setToken(process.env.THE_TOKEN_2); + +client.once('ready', async () => { + try { + console.log(`Logged in as ${client.user.tag}!`); + + // Register the commands with Discord + const data = await rest.put( + Routes.applicationCommands(process.env.DISCORD_CLIENT_ID), + { body: commandData } // Send the array of commands with integration types and contexts + ); + + console.log('Successfully registered application commands.'); + } catch (error) { + console.error('Error registering commands: ', error); + } +}); + +client.on('interactionCreate', async interaction => { + if (!interaction.isCommand()) return; + + const { commandName, options } = interaction; + const userId = interaction.user.id; + + updateUserInteractionTime(userId); + await resetConversationIfExpired(userId); + + if (commandName === 'reset') { + await resetConversation(interaction); + } else if (commandName === 'restartcore') { + await restartCore(interaction); + } else if (commandName === 'chat') { + const content = options.getString('message'); + await handleUserMessage(interaction, content); + } else if (commandName === 'privacy') { + await togglePrivacy(interaction); + } else if (commandName === 'pdf') { + try { + await interaction.deferReply({ ephemeral: isEphemeral(interaction.user.id) }); + await generatePDF(interaction); + } catch (error) { + console.error('Failed to generate PDF:', error); + await interaction.editReply({ content: 'Failed to generate PDF.' }); + } + } +}); + +async function handleUserMessage(interaction, content) { + const encodedMessage = he.encode(content); + + // Start typing indicator + await interaction.deferReply({ ephemeral: isEphemeral(interaction.user.id) }); + + try { + // Making the API call and expecting the plain text response + const response = await axios.post(`http://${process.env.ROOT_IP}:${process.env.ROOT_PORT}/api/v1/chat`, { + message: encodedMessage, + max_tokens: Number(process.env.MAX_TOKENS), + repeat_penalty: Number(process.env.REPEAT_PENALTY) + }, { + headers: { + 'Content-Type': 'application/json', + 'x-forwarded-for-id': interaction.user.id, + 'x-forwarded-for-name': interaction.user.username + } + }); + + // Since response is expected to be plain text + const responseText = response.data || 'Oops, something went wrong. No content received.'; + + await sendLongMessage(interaction, responseText); + } catch (error) { + if (error.response && error.response.status === 429) { + try { + await interaction.editReply({ content: 'I am currently busy. Please try again later.', ephemeral: isEphemeral(interaction.user.id) }); + } catch (dmError) { + console.error('Failed to send DM:', dmError); + interaction.editReply({ content: 'I am currently busy. Please try again later.', ephemeral: isEphemeral(interaction.user.id) }); + } + } else { + interaction.editReply({ content: 'Error: ' + error.message, ephemeral: isEphemeral(interaction.user.id) }); + } + } +} + +async function sendLongMessage(interaction, responseText) { + const limit = 8096; + + if (responseText.length > limit) { + const lines = responseText.split('\n'); + const chunks = []; + let currentChunk = ''; + + for (const line of lines) { + if (currentChunk.length + line.length > limit) { + chunks.push(currentChunk); + currentChunk = ''; + } + currentChunk += line + '\n'; + } + + if (currentChunk.trim() !== '') { + chunks.push(currentChunk.trim()); + } + + if (chunks.length >= 80) return await interaction.reply({ content: "Response chunks too large. Try again", ephemeral: isEphemeral(interaction.user.id) }); + + for (let i = 0; i < chunks.length; i++) { + const chunk = chunks[i]; + const embed = new EmbedBuilder() + .setDescription(chunk) + .setColor("#3498DB") + .setTimestamp(); + + setTimeout(() => { + interaction.followUp({ + embeds: [embed], + ephemeral: isEphemeral(interaction.user.id) + }); + }, i * (process.env.OVERFLOW_DELAY || 3) * 1000); + } + } else { + const embed = new EmbedBuilder() + .setDescription(responseText) + .setColor("#3498DB") + .setTimestamp(); + + interaction.editReply({ + embeds: [embed], + ephemeral: isEphemeral(interaction.user.id) + }); + } +} + +async function resetConversation(interaction) { + try { + const userId = interaction.user.id; + conversationHistories[userId] = []; // Reset conversation history for the user + const response = await axios.post(`${process.env.API_PATH}/reset-conversation`, {}, { + headers: { + 'Content-Type': 'application/json', + 'x-forwarded-for-id': interaction.user.id, + 'x-forwarded-for-name': interaction.user.username + } + }); + console.log(response.status); + interaction.reply({ content: 'Conversation reset successfully.', ephemeral: isEphemeral(interaction.user.id) }); + } catch (error) { + console.log(error); + interaction.reply({ content: 'Failed to reset the conversation.', ephemeral: isEphemeral(interaction.user.id) }); + } +} + +async function generatePDF(interaction) { + try { + const response = await axios.get(`http://${process.env.ROOT_IP}:${process.env.ROOT_PORT}/api/v1/conversation-history`, { + headers: { + 'x-forwarded-for-id': interaction.user.id + } + }); + + const conversationHistory = response.data; + + const filteredHistory = conversationHistory.filter(message => message.role !== 'system'); + + let htmlContent = ` + + + + + +

Conversation History

`; + + filteredHistory.forEach(message => { + const role = message.role.charAt(0).toUpperCase() + message.role.slice(1); + const content = he.decode(message.content); + const renderedMarkdown = md.render(content); + + htmlContent += `

${role}:

`; + htmlContent += `
${renderedMarkdown}
`; + }); + + htmlContent += ``; + + const browser = await puppeteer.launch({ + args: ['--no-sandbox', '--disable-setuid-sandbox'] + }); + const page = await browser.newPage(); + await page.setContent(htmlContent); + const pdfPath = `./conversation_${interaction.user.id}.pdf`; + await page.pdf({ path: pdfPath, format: 'A4' }); + + await browser.close(); + + const attachment = new AttachmentBuilder(pdfPath); + await interaction.editReply({ + content: 'Here is your conversation history in PDF format:', + files: [attachment], + ephemeral: isEphemeral(interaction.user.id) + }); + + fs.unlinkSync(pdfPath); + } catch (error) { + console.error('Failed to generate PDF:', error); + await interaction.reply({ content: 'Failed to generate PDF.', ephemeral: isEphemeral(interaction.user.id) }); + } +} + +client.login(process.env.THE_TOKEN_2);