mirror of
https://github.com/NekoMonci12/RakunNakun-AI.git
synced 2025-12-19 14:59:15 +00:00
Initial commit
This commit is contained in:
15
.env.example
Normal file
15
.env.example
Normal file
@@ -0,0 +1,15 @@
|
||||
DISCORD_TOKEN=DISCORD_BOT_TOKEN
|
||||
CLIENT_ID=DISCORD_BOT_CLIENT_ID
|
||||
OPENAI_API_KEY=sk-***********************************
|
||||
OPENAI_BASE_URL=https://api.deepseek.com/chat/completions
|
||||
|
||||
MONGO_URL=mongodb://username:password@localhost:27000
|
||||
MONGO_DB_NAME=RakunNakun
|
||||
REDIS_URL=redis://localhost:6424
|
||||
PASTEBIN_DEV_KEY=PASTEBIN_DEVELOPER_KEY
|
||||
|
||||
MYSQL_HOST=localhost
|
||||
MYSQL_PORT=3306
|
||||
MYSQL_USER=root
|
||||
MYSQL_PASSWORD=
|
||||
MYSQL_DATABASE=database
|
||||
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
130
.gitignore
vendored
Normal file
130
.gitignore
vendored
Normal file
@@ -0,0 +1,130 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
74
cacheManagerRedis.js
Normal file
74
cacheManagerRedis.js
Normal file
@@ -0,0 +1,74 @@
|
||||
const { createClient } = require('redis');
|
||||
|
||||
class CacheManagerRedis {
|
||||
/**
|
||||
* @param {object} options - Options for the Redis client.
|
||||
* Example: { url: 'redis://localhost:6379' }
|
||||
*/
|
||||
constructor(options) {
|
||||
this.client = createClient(options);
|
||||
this.client.on('error', (err) => console.error('Redis Client Error', err));
|
||||
this.client.connect();
|
||||
}
|
||||
|
||||
// Normalize input for consistent comparison
|
||||
normalize(input) {
|
||||
return input.trim().toLowerCase();
|
||||
}
|
||||
|
||||
// Compute the Levenshtein distance between two strings
|
||||
levenshtein(a, b) {
|
||||
const matrix = [];
|
||||
for (let i = 0; i <= b.length; i++) {
|
||||
matrix[i] = [i];
|
||||
}
|
||||
for (let j = 0; j <= a.length; j++) {
|
||||
matrix[0][j] = j;
|
||||
}
|
||||
for (let i = 1; i <= b.length; i++) {
|
||||
for (let j = 1; j <= a.length; j++) {
|
||||
if (b.charAt(i - 1) === a.charAt(j - 1)) {
|
||||
matrix[i][j] = matrix[i - 1][j - 1];
|
||||
} else {
|
||||
matrix[i][j] = Math.min(
|
||||
matrix[i - 1][j - 1] + 1,
|
||||
matrix[i][j - 1] + 1,
|
||||
matrix[i - 1][j] + 1
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return matrix[b.length][a.length];
|
||||
}
|
||||
|
||||
// Calculate similarity between two strings (1 means identical, 0 means completely different)
|
||||
similarity(a, b) {
|
||||
const distance = this.levenshtein(a, b);
|
||||
const maxLen = Math.max(a.length, b.length);
|
||||
if (maxLen === 0) return 1;
|
||||
return 1 - distance / maxLen;
|
||||
}
|
||||
|
||||
// Check the cache for a result that is at least 80% similar to the new input.
|
||||
async getCachedResult(input) {
|
||||
const normalizedInput = this.normalize(input);
|
||||
const keys = await this.client.keys('cache:*');
|
||||
for (const key of keys) {
|
||||
const storedNormalizedInput = key.slice(6); // remove "cache:" prefix
|
||||
const sim = this.similarity(normalizedInput, storedNormalizedInput);
|
||||
if (sim >= 0.8) {
|
||||
const cachedOutput = await this.client.get(key);
|
||||
return cachedOutput;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Store the result in cache with key as normalized input
|
||||
async setCache(input, output) {
|
||||
const normalizedInput = this.normalize(input);
|
||||
await this.client.set(`cache:${normalizedInput}`, output, { EX: 3600 });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CacheManagerRedis;
|
||||
76
database.js
Normal file
76
database.js
Normal file
@@ -0,0 +1,76 @@
|
||||
// database.js
|
||||
|
||||
const mysql = require('mysql2/promise');
|
||||
require('dotenv').config();
|
||||
|
||||
class Database {
|
||||
constructor() {
|
||||
this.pool = mysql.createPool({
|
||||
host: process.env.MYSQL_HOST || 'localhost',
|
||||
port: process.env.MYSQL_PORT || 3306,
|
||||
user: process.env.MYSQL_USER || 'root',
|
||||
password: process.env.MYSQL_PASSWORD || '',
|
||||
database: process.env.MYSQL_DATABASE || 'discord_bot',
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0
|
||||
});
|
||||
}
|
||||
|
||||
// Initializes the database by creating the Models and Guilds tables
|
||||
async init() {
|
||||
// SQL query to create the "Models" table with columns MODEL_ID, TOKEN_INPUT, TOKEN_OUTPUT, and API_KEY
|
||||
const createModelsQuery = `
|
||||
CREATE TABLE IF NOT EXISTS Models (
|
||||
MODEL_ID VARCHAR(255) NOT NULL PRIMARY KEY,
|
||||
TOKEN_INPUT DECIMAL(10,2),
|
||||
TOKEN_OUTPUT DECIMAL(10,2),
|
||||
TOKEN_CACHED_INPUT DECIMAL(10,2),
|
||||
TOKEN_CACHED_OUPUT DECIMAL(10,2),
|
||||
API_KEY VARCHAR(255),
|
||||
API_URL VARCHAR(255)
|
||||
);
|
||||
`;
|
||||
|
||||
// SQL query to create the "Guilds" table with a foreign key on CHAT_MODELS referencing Models.MODEL_ID
|
||||
const createGuildsQuery = `
|
||||
CREATE TABLE IF NOT EXISTS Guilds (
|
||||
GUILD_ID VARCHAR(255) NOT NULL PRIMARY KEY,
|
||||
GUILD_OWNER_ID VARCHAR(255) NOT NULL,
|
||||
GUILD_USERS_ID TEXT,
|
||||
CHAT_TOKENS TEXT,
|
||||
CHAT_MODELS VARCHAR(255),
|
||||
CHAT_PERSONA TEXT NOT NULL DEFAULT 'You are a helpful assistant.',
|
||||
STATUS TINYINT(1) DEFAULT 1,
|
||||
DEBUG TINYINT(1) DEFAULT 0,
|
||||
CONSTRAINT fk_chat_models FOREIGN KEY (CHAT_MODELS) REFERENCES Models(MODEL_ID)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE SET NULL
|
||||
);
|
||||
`;
|
||||
// Create ChatLogs table
|
||||
const createChatLogsQuery = `
|
||||
CREATE TABLE IF NOT EXISTS ChatLogs (
|
||||
GUILD_ID VARCHAR(255) NOT NULL,
|
||||
GUILD_USERS_ID TEXT,
|
||||
MESSAGE_INPUT TEXT NOT NULL,
|
||||
MESSAGE_OUTPUT TEXT NOT NULL,
|
||||
CACHED TINYINT(1) NOT NULL DEFAULT 0,
|
||||
TIMESTAMP TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`;
|
||||
|
||||
try {
|
||||
await this.pool.query(createModelsQuery);
|
||||
console.log('Models table is ready or already exists.');
|
||||
await this.pool.query(createGuildsQuery);
|
||||
console.log('Guilds table is ready or already exists.');
|
||||
await this.pool.query(createChatLogsQuery);
|
||||
console.log('ChatLogs table is ready or already exists.');
|
||||
} catch (error) {
|
||||
console.error('Error creating tables:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Database;
|
||||
33
deploy-commands.js
Normal file
33
deploy-commands.js
Normal file
@@ -0,0 +1,33 @@
|
||||
// deployCommands.js
|
||||
|
||||
const { REST, Routes } = require('discord.js');
|
||||
require('dotenv').config();
|
||||
|
||||
class CommandDeployer {
|
||||
/**
|
||||
* @param {Array} commands - Array of command definitions (JSON).
|
||||
* @param {string} clientId - Your Discord application client ID.
|
||||
* @param {string} token - Your Discord bot token.
|
||||
*/
|
||||
constructor(commands, clientId, token) {
|
||||
this.commands = commands;
|
||||
this.clientId = clientId;
|
||||
this.token = token;
|
||||
this.rest = new REST({ version: '10' }).setToken(token);
|
||||
}
|
||||
|
||||
async deploy() {
|
||||
try {
|
||||
console.log('Started refreshing application (/) commands.');
|
||||
await this.rest.put(
|
||||
Routes.applicationCommands(this.clientId),
|
||||
{ body: this.commands }
|
||||
);
|
||||
console.log('Successfully reloaded application (/) commands.');
|
||||
} catch (error) {
|
||||
console.error('Error deploying commands:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CommandDeployer;
|
||||
42
hybridCacheManager.js
Normal file
42
hybridCacheManager.js
Normal file
@@ -0,0 +1,42 @@
|
||||
const CacheManagerRedis = require('./cacheManagerRedis');
|
||||
const MongoCacheManager = require('./mongoCacheManager');
|
||||
|
||||
class HybridCacheManager {
|
||||
/**
|
||||
* @param {object} redisOptions - Options for Redis.
|
||||
* @param {string} mongoUrl - Connection URL for MongoDB.
|
||||
* @param {string} dbName - MongoDB database name.
|
||||
* @param {string} collectionName - MongoDB collection name for cache.
|
||||
*/
|
||||
constructor(redisOptions = {}, mongoUrl, dbName, collectionName = 'cache') {
|
||||
this.redisCache = new CacheManagerRedis(redisOptions);
|
||||
this.mongoCache = new MongoCacheManager(mongoUrl, dbName, collectionName);
|
||||
}
|
||||
|
||||
async getCachedResult(input) {
|
||||
// Try Redis first.
|
||||
let result = await this.redisCache.getCachedResult(input);
|
||||
if (result) {
|
||||
console.log("Hybrid Cache: Found result in Redis.");
|
||||
return result;
|
||||
}
|
||||
// If not in Redis, try MongoDB.
|
||||
result = await this.mongoCache.getCachedResult(input);
|
||||
if (result) {
|
||||
console.log("Hybrid Cache: Found result in MongoDB.");
|
||||
// Optionally, refresh Redis cache.
|
||||
await this.redisCache.setCache(input, result);
|
||||
return result;
|
||||
}
|
||||
console.log("Hybrid Cache: No cached result found.");
|
||||
return null;
|
||||
}
|
||||
|
||||
async setCache(input, value) {
|
||||
await this.redisCache.setCache(input, value);
|
||||
await this.mongoCache.setCache(input, value);
|
||||
console.log("Hybrid Cache: Stored value in both caches for key:", input);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HybridCacheManager;
|
||||
319
index.js
Normal file
319
index.js
Normal file
@@ -0,0 +1,319 @@
|
||||
const { Client, GatewayIntentBits, SlashCommandBuilder } = require('discord.js');
|
||||
const Database = require('./database');
|
||||
const CommandDeployer = require('./deploy-commands');
|
||||
const axios = require('axios');
|
||||
const HybridCacheManager = require('./hybridCacheManager');
|
||||
const MessageSplitter = require('./messageSplitter');
|
||||
const PastebinClient = require('./pastebinClient');
|
||||
require('dotenv').config();
|
||||
|
||||
// Simple logging helpers
|
||||
const logInfo = (msg, ...args) => console.log(`[INFO] ${msg}`, ...args);
|
||||
const logDebug = (msg, ...args) => console.log(`[DEBUG] ${msg}`, ...args);
|
||||
const logError = (msg, ...args) => console.error(`[ERROR] ${msg}`, ...args);
|
||||
|
||||
// Initialize PasteBin (pastebin method is left unchanged)
|
||||
const pastebinClient = new PastebinClient(process.env.PASTEBIN_DEV_KEY);
|
||||
|
||||
async function safeDeferReply(interaction) {
|
||||
try {
|
||||
if (!interaction.deferred && !interaction.replied) {
|
||||
await interaction.deferReply();
|
||||
}
|
||||
} catch (error) {
|
||||
logError("Error deferring reply:", error);
|
||||
}
|
||||
}
|
||||
|
||||
(async () => {
|
||||
// Initialize database (create tables if needed)
|
||||
const db = new Database();
|
||||
await db.init();
|
||||
logInfo("Database initialization complete.");
|
||||
|
||||
// Instantiate Hybrid Cache Manager and message splitter
|
||||
const redisOptions = { url: process.env.REDIS_URL || 'redis://localhost:6379' };
|
||||
const mongoUrl = process.env.MONGO_URL || 'mongodb://localhost:27017';
|
||||
const dbName = process.env.MONGO_DB_NAME || 'discordCache';
|
||||
const cache = new HybridCacheManager(redisOptions, mongoUrl, dbName);
|
||||
const splitter = new MessageSplitter(2000);
|
||||
|
||||
// Specify the default model ID and persona to use if none is set for a guild
|
||||
const defaultModelId = 'deepseek-chat';
|
||||
const defaultPersona = 'You are a helpful assistant.';
|
||||
const defaultApiUrl = process.env.OPENAI_BASE_URL;
|
||||
|
||||
// --- Define Slash Command ---
|
||||
const commands = [
|
||||
new SlashCommandBuilder()
|
||||
.setName('chat')
|
||||
.setDescription('Chat with the RakunNakun')
|
||||
.addStringOption(option =>
|
||||
option.setName('message')
|
||||
.setDescription('Your question to RakunNakun')
|
||||
.setRequired(true)
|
||||
)
|
||||
].map(command => command.toJSON());
|
||||
|
||||
// --- Deploy Commands using CommandDeployer class ---
|
||||
const deployer = new CommandDeployer(commands, process.env.CLIENT_ID, process.env.DISCORD_TOKEN);
|
||||
await deployer.deploy();
|
||||
logInfo("Slash commands deployed.");
|
||||
|
||||
// Create a new Discord client with the required intents
|
||||
const client = new Client({
|
||||
intents: [GatewayIntentBits.Guilds]
|
||||
});
|
||||
|
||||
client.once('ready', () => {
|
||||
logInfo(`Logged in as ${client.user.tag}`);
|
||||
});
|
||||
|
||||
// Listen for slash command interactions
|
||||
client.on('interactionCreate', async (interaction) => {
|
||||
if (!interaction.isChatInputCommand()) return;
|
||||
if (interaction.commandName !== 'chat') return;
|
||||
|
||||
const userMessage = interaction.options.getString('message');
|
||||
logInfo(`Received message: "${userMessage}" from ${interaction.user.tag}`);
|
||||
|
||||
// Set default factors and values
|
||||
let tokenInputFactor = 0.6;
|
||||
let tokenOutputFactor = 0.9;
|
||||
let tokenCachedInputFactor = 0.6;
|
||||
let tokenCachedOutputFactor = 0.9;
|
||||
let modelApiKey = process.env.OPENAI_API_KEY;
|
||||
let modelBaseUrl = process.env.OPENAI_BASE_URL;
|
||||
let modelIdToUse = defaultModelId;
|
||||
let personaToUse = defaultPersona;
|
||||
let requiredRoleId = null;
|
||||
let debugMode = false;
|
||||
let currentTokens = 0;
|
||||
let pasteUrl;
|
||||
|
||||
try {
|
||||
// Fetch guild settings including model, token balance, persona, etc.
|
||||
if (interaction.guild && interaction.guild.id) {
|
||||
const [guildRows] = await db.pool.query(
|
||||
'SELECT CHAT_MODELS, GUILD_USERS_ID, GUILD_OWNER_ID, DEBUG, CHAT_TOKENS, CHAT_PERSONA FROM Guilds WHERE GUILD_ID = ?',
|
||||
[interaction.guild.id]
|
||||
);
|
||||
if (guildRows.length === 0) {
|
||||
logError("Guild not registered:", interaction.guild.id);
|
||||
await interaction.reply(`Your guild is not registered in our system. Please contact the guild owner <@${interaction.guild.ownerId}>.`);
|
||||
return;
|
||||
}
|
||||
currentTokens = Number(guildRows[0].CHAT_TOKENS) || 0;
|
||||
logDebug(`Current token balance: ${currentTokens}`);
|
||||
if (currentTokens < 2000) {
|
||||
logInfo(`Token balance (${currentTokens}) is below threshold for guild ${interaction.guild.id}.`);
|
||||
await interaction.reply("Insufficient tokens in your guild. Please recharge tokens.");
|
||||
return;
|
||||
}
|
||||
if (guildRows[0].CHAT_MODELS) {
|
||||
modelIdToUse = guildRows[0].CHAT_MODELS;
|
||||
logInfo(`Guild ${interaction.guild.id} is using model ${modelIdToUse}`);
|
||||
} else {
|
||||
logInfo(`Guild ${interaction.guild.id} has no custom model. Using default model.`);
|
||||
}
|
||||
if (guildRows[0].GUILD_USERS_ID) {
|
||||
requiredRoleId = guildRows[0].GUILD_USERS_ID;
|
||||
}
|
||||
if (guildRows[0].CHAT_PERSONA) {
|
||||
personaToUse = guildRows[0].CHAT_PERSONA;
|
||||
}
|
||||
if (guildRows[0].DEBUG && Number(guildRows[0].DEBUG) === 1) {
|
||||
debugMode = true;
|
||||
logDebug("Debug mode enabled for this guild.");
|
||||
}
|
||||
}
|
||||
|
||||
// Check required role if defined.
|
||||
if (requiredRoleId && !interaction.member.roles.cache.has(requiredRoleId)) {
|
||||
logInfo(`User ${interaction.user.tag} lacks the required role (${requiredRoleId}).`);
|
||||
await interaction.reply("You don't have the required role to use this command.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Query the Models table for the model parameters using modelIdToUse
|
||||
const [modelRows] = await db.pool.query(
|
||||
'SELECT TOKEN_INPUT, TOKEN_OUTPUT, TOKEN_CACHED_INPUT, TOKEN_CACHED_OUTPUT, API_KEY, API_URL FROM Models WHERE MODEL_ID = ?',
|
||||
[modelIdToUse]
|
||||
);
|
||||
if (modelRows.length > 0) {
|
||||
tokenInputFactor = modelRows[0].TOKEN_INPUT || tokenInputFactor;
|
||||
tokenOutputFactor = modelRows[0].TOKEN_OUTPUT || tokenOutputFactor;
|
||||
tokenCachedInputFactor = modelRows[0].TOKEN_CACHED_INPUT || tokenInputFactor;
|
||||
tokenCachedOutputFactor = modelRows[0].TOKEN_CACHED_OUTPUT || tokenOutputFactor;
|
||||
if (modelRows[0].API_KEY) {
|
||||
modelApiKey = modelRows[0].API_KEY;
|
||||
}
|
||||
if (modelRows[0].API_URL) {
|
||||
modelBaseUrl = modelRows[0].API_URL;
|
||||
}
|
||||
if (debugMode) {
|
||||
logDebug("Model row:", modelRows[0]);
|
||||
}
|
||||
} else {
|
||||
logError(`No record found in Models for ${modelIdToUse}. Using default parameters.`);
|
||||
}
|
||||
} catch (error) {
|
||||
logError('Error fetching model parameters:', error);
|
||||
}
|
||||
|
||||
// --- Helper Functions to Count Tokens using dynamic factors ---
|
||||
const calculateInputTokens = (text, factor = tokenInputFactor) => Math.ceil(text.length * factor);
|
||||
|
||||
// Check cache before making an API call
|
||||
const cachedOutput = await cache.getCachedResult(userMessage);
|
||||
if (cachedOutput) {
|
||||
logDebug("Cache hit: Using cached result for input:", userMessage);
|
||||
const cachedInputTokens = Math.ceil(userMessage.length * tokenCachedInputFactor);
|
||||
const cachedOutputTokens = Math.ceil(cachedOutput.length * tokenCachedOutputFactor);
|
||||
let finalReply = cachedOutput;
|
||||
if (debugMode) {
|
||||
finalReply += `\n\n**Token Usage (Cached):**\n- Input Tokens: ${cachedInputTokens}\n- Output Tokens: ${cachedOutputTokens}`;
|
||||
}
|
||||
try {
|
||||
pasteUrl = await pastebinClient.createPaste(finalReply, '1M', 'Chat Log (Cached)');
|
||||
if (debugMode) {
|
||||
logInfo(`Chat logged on Pastebin: ${pasteUrl}`);
|
||||
}
|
||||
} catch (pasteError) {
|
||||
if (debugMode) {
|
||||
logError('Failed to create Pastebin paste:', pasteError);
|
||||
}
|
||||
pasteUrl = "FAILED ERROR CODE";
|
||||
}
|
||||
await db.pool.query(
|
||||
'INSERT INTO ChatLogs (GUILD_ID, GUILD_USERS_ID, MESSAGE_INPUT, MESSAGE_OUTPUT, CACHED) VALUES (?, ?, ?, ?, 1)',
|
||||
[interaction.guild.id, interaction.user.id, userMessage, pasteUrl]
|
||||
);
|
||||
const parts = splitter.split(finalReply);
|
||||
if (parts.length === 1) {
|
||||
await interaction.reply(parts[0]);
|
||||
} else {
|
||||
await interaction.reply(parts[0]);
|
||||
for (let i = 1; i < parts.length; i++) {
|
||||
await interaction.followUp(parts[i]);
|
||||
}
|
||||
}
|
||||
const tokensUsed = cachedInputTokens + cachedOutputTokens;
|
||||
await db.pool.query(
|
||||
'UPDATE Guilds SET CHAT_TOKENS = CHAT_TOKENS - ? WHERE GUILD_ID = ?',
|
||||
[tokensUsed, interaction.guild.id]
|
||||
);
|
||||
logInfo(`Deducted ${tokensUsed} tokens (cached). New balance: ${currentTokens - tokensUsed}`);
|
||||
return;
|
||||
} else {
|
||||
logDebug("Cache miss: No cached result for input:", userMessage);
|
||||
}
|
||||
|
||||
// Defer reply to allow time for API processing
|
||||
await safeDeferReply(interaction);
|
||||
|
||||
try {
|
||||
logDebug("Sending API request with model:", modelIdToUse);
|
||||
let axiosResponse;
|
||||
try {
|
||||
axiosResponse = await axios.post(
|
||||
modelBaseUrl,
|
||||
{
|
||||
model: modelIdToUse,
|
||||
messages: [
|
||||
{ role: 'system', content: personaToUse },
|
||||
{ role: 'user', content: userMessage }
|
||||
],
|
||||
stream: false
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${modelApiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
if (
|
||||
error.response &&
|
||||
error.response.data &&
|
||||
error.response.data.error &&
|
||||
error.response.data.error.message &&
|
||||
error.response.data.error.message.includes('Service is too busy')
|
||||
) {
|
||||
logInfo("Primary API is too busy. Falling back to default model and default API.");
|
||||
axiosResponse = await axios.post(
|
||||
defaultApiUrl,
|
||||
{
|
||||
model: defaultModelId,
|
||||
messages: [
|
||||
{ role: 'system', content: personaToUse },
|
||||
{ role: 'user', content: userMessage }
|
||||
],
|
||||
stream: false
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
logDebug("API response received.");
|
||||
const reply = axiosResponse.data.choices[0].message.content.trim();
|
||||
const outputTokens = Math.ceil(reply.length * tokenOutputFactor);
|
||||
const apiInputTokens = calculateInputTokens(userMessage);
|
||||
await cache.setCache(userMessage, reply);
|
||||
logDebug("Caching API response for input:", userMessage);
|
||||
|
||||
let finalReply = reply;
|
||||
if (debugMode) {
|
||||
finalReply += `\n\n**Token Usage:**\n- Model-ID: \`${modelIdToUse}\`\n- Input Tokens: ${apiInputTokens}\n- Output Tokens: ${outputTokens}`;
|
||||
}
|
||||
|
||||
try {
|
||||
pasteUrl = await pastebinClient.createPaste(finalReply, '1M', 'Chat Log');
|
||||
if (debugMode) {
|
||||
logInfo(`Chat logged on Pastebin: ${pasteUrl}`);
|
||||
}
|
||||
} catch (pasteError) {
|
||||
if (debugMode) {
|
||||
logError('Failed to create Pastebin paste:', pasteError);
|
||||
}
|
||||
pasteUrl = "FAILED ERROR CODE";
|
||||
}
|
||||
|
||||
await db.pool.query(
|
||||
'INSERT INTO ChatLogs (GUILD_ID, GUILD_USERS_ID, MESSAGE_INPUT, MESSAGE_OUTPUT, CACHED) VALUES (?, ?, ?, ?, 0)',
|
||||
[interaction.guild.id, interaction.user.id, userMessage, pasteUrl]
|
||||
);
|
||||
|
||||
const parts = splitter.split(finalReply);
|
||||
if (parts.length === 1) {
|
||||
await interaction.editReply(parts[0]);
|
||||
} else {
|
||||
await interaction.editReply(parts[0]);
|
||||
for (let i = 1; i < parts.length; i++) {
|
||||
await interaction.followUp(parts[i]);
|
||||
}
|
||||
}
|
||||
|
||||
const tokensUsed = apiInputTokens + outputTokens;
|
||||
await db.pool.query(
|
||||
'UPDATE Guilds SET CHAT_TOKENS = CHAT_TOKENS - ? WHERE GUILD_ID = ?',
|
||||
[tokensUsed, interaction.guild.id]
|
||||
);
|
||||
logInfo(`Deducted ${tokensUsed} tokens (API). New balance: ${currentTokens - tokensUsed}`);
|
||||
} catch (error) {
|
||||
logError('Error with API:', error.response ? error.response.data : error.message);
|
||||
await interaction.editReply('There was an error processing your request.');
|
||||
}
|
||||
});
|
||||
|
||||
client.login(process.env.DISCORD_TOKEN);
|
||||
})();
|
||||
43
messageSplitter.js
Normal file
43
messageSplitter.js
Normal file
@@ -0,0 +1,43 @@
|
||||
// messageSplitter.js
|
||||
|
||||
class MessageSplitter {
|
||||
/**
|
||||
* Create a new MessageSplitter instance.
|
||||
* @param {number} maxLength - Maximum allowed length per message. Defaults to 2000.
|
||||
*/
|
||||
constructor(maxLength = 2000) {
|
||||
this.maxLength = maxLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits the given message into an array of parts that do not exceed maxLength,
|
||||
* without cutting off words.
|
||||
* @param {string} message - The message to split.
|
||||
* @returns {string[]} - An array of message parts.
|
||||
*/
|
||||
split(message) {
|
||||
if (message.length <= this.maxLength) return [message];
|
||||
|
||||
const parts = [];
|
||||
// Split message by spaces.
|
||||
const words = message.split(' ');
|
||||
let currentPart = '';
|
||||
|
||||
for (const word of words) {
|
||||
// If adding the next word would exceed maxLength, push the current part.
|
||||
if (currentPart.length + word.length + 1 > this.maxLength) {
|
||||
parts.push(currentPart.trim());
|
||||
currentPart = word + ' ';
|
||||
} else {
|
||||
currentPart += word + ' ';
|
||||
}
|
||||
}
|
||||
// Push any remaining text.
|
||||
if (currentPart.length > 0) {
|
||||
parts.push(currentPart.trim());
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MessageSplitter;
|
||||
74
mongoCacheManager.js
Normal file
74
mongoCacheManager.js
Normal file
@@ -0,0 +1,74 @@
|
||||
const { MongoClient } = require('mongodb');
|
||||
|
||||
class MongoCacheManager {
|
||||
/**
|
||||
* @param {string} mongoUrl - Connection URL for MongoDB.
|
||||
* @param {string} dbName - Database name.
|
||||
* @param {string} collectionName - Collection name for caching.
|
||||
*/
|
||||
constructor(mongoUrl, dbName, collectionName = 'cache') {
|
||||
this.mongoUrl = mongoUrl;
|
||||
this.dbName = dbName;
|
||||
this.collectionName = collectionName;
|
||||
this.client = new MongoClient(mongoUrl, { useUnifiedTopology: true });
|
||||
this.connected = false;
|
||||
}
|
||||
|
||||
async connect() {
|
||||
if (!this.connected) {
|
||||
try {
|
||||
await this.client.connect();
|
||||
this.collection = this.client.db(this.dbName).collection(this.collectionName);
|
||||
this.connected = true;
|
||||
console.log("[MongoCache] Connected to MongoDB for caching.");
|
||||
} catch (error) {
|
||||
console.error("[MongoCache] MongoDB connection error:", error);
|
||||
this.connected = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
normalize(input) {
|
||||
return input.trim().toLowerCase();
|
||||
}
|
||||
|
||||
async getCachedResult(input) {
|
||||
try {
|
||||
await this.connect();
|
||||
if (!this.connected || !this.collection) {
|
||||
console.error("[MongoCache] Not connected to MongoDB, skipping getCachedResult.");
|
||||
return null;
|
||||
}
|
||||
const key = this.normalize(input);
|
||||
const doc = await this.collection.findOne({ key });
|
||||
if (doc) {
|
||||
console.log("[MongoCache] Found cached result for key:", key);
|
||||
return doc.value;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[MongoCache] Error retrieving cache for key:", this.normalize(input), error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async setCache(input, value) {
|
||||
try {
|
||||
await this.connect();
|
||||
if (!this.connected || !this.collection) {
|
||||
console.error("[MongoCache] Not connected to MongoDB, skipping setCache.");
|
||||
return;
|
||||
}
|
||||
const key = this.normalize(input);
|
||||
const result = await this.collection.updateOne(
|
||||
{ key },
|
||||
{ $set: { value, updatedAt: new Date() } },
|
||||
{ upsert: true }
|
||||
);
|
||||
console.log("[MongoCache] Stored value for key:", key, "Update result:", result.result);
|
||||
} catch (error) {
|
||||
console.error("[MongoCache] Error storing cache for key:", this.normalize(input), error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MongoCacheManager;
|
||||
1157
package-lock.json
generated
Normal file
1157
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
12
package.json
Normal file
12
package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"discord.js": "^14.11.0",
|
||||
"dotenv": "^16.0.0",
|
||||
"mysql2": "^3.2.0",
|
||||
"openai": "^3.2.1",
|
||||
"axios": "^1.4.0",
|
||||
"qs": "^6.11.2",
|
||||
"redis": "^4.6.7",
|
||||
"mongodb": "^5.7.0"
|
||||
}
|
||||
}
|
||||
54
pastebinClient.js
Normal file
54
pastebinClient.js
Normal file
@@ -0,0 +1,54 @@
|
||||
const axios = require('axios');
|
||||
const qs = require('qs'); // for form URL-encoded data
|
||||
|
||||
class PastebinClient {
|
||||
/**
|
||||
* Create a new PastebinClient instance.
|
||||
* @param {string} devKey - Your Pastebin developer key.
|
||||
*/
|
||||
constructor(devKey) {
|
||||
this.devKey = devKey;
|
||||
this.apiUrl = 'https://pastebin.com/api/api_post.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new paste on Pastebin.
|
||||
* @param {string} text - The text to paste.
|
||||
* @param {string} expireDate - Expiration for the paste (e.g. '1M' for 1 month).
|
||||
* @param {string} pasteName - Optional paste title.
|
||||
* @param {string} pasteFormat - Optional paste format.
|
||||
* @param {number} pastePrivate - 0=public, 1=unlisted, 2=private.
|
||||
* @returns {Promise<string>} - The URL of the created paste.
|
||||
*/
|
||||
async createPaste(text, expireDate = '1M', pasteName = 'Chat Log', pasteFormat = '', pastePrivate = 1) {
|
||||
// Prepare the payload as form URL encoded data.
|
||||
const payload = {
|
||||
api_dev_key: this.devKey,
|
||||
api_option: 'paste',
|
||||
api_paste_code: text,
|
||||
api_paste_expire_date: expireDate, // "1M" for 1 month
|
||||
api_paste_name: pasteName,
|
||||
api_paste_format: pasteFormat,
|
||||
api_paste_private: pastePrivate,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await axios.post(this.apiUrl, qs.stringify(payload), {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
});
|
||||
// If Pastebin returns a URL, then paste creation succeeded.
|
||||
// If not, it might return an error message.
|
||||
if (response.data.startsWith('http')) {
|
||||
return response.data;
|
||||
} else {
|
||||
throw new Error(`Pastebin error: ${response.data}`);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Pastebin API request failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PastebinClient;
|
||||
Reference in New Issue
Block a user