updated src/events/messageCreate.ts

This commit is contained in:
2025-05-18 10:55:22 -04:00
parent e1c19c3698
commit 17b2c29ebc
4 changed files with 232 additions and 60 deletions

10
package-lock.json generated
View File

@@ -1,13 +1,13 @@
{ {
"name": "discord-ollama", "name": "discord-aidolls",
"version": "0.8.4", "version": "0.1.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "discord-ollama", "name": "discord-aidolls",
"version": "0.8.4", "version": "0.1.0",
"license": "ISC", "license": "---",
"dependencies": { "dependencies": {
"discord.js": "^14.18.0", "discord.js": "^14.18.0",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",

View File

@@ -1,55 +1,227 @@
import { Client, GatewayIntentBits } from 'discord.js' import { TextChannel } from 'discord.js'
import { Ollama } from 'ollama' import { event, Events, normalMessage, UserMessage, clean } from '../utils/index.js'
import { createClient } from 'redis' import {
import { Queue } from './queues/queue.js' getChannelInfo, getServerConfig, getUserConfig, openChannelInfo,
import { UserMessage, registerEvents } from './utils/index.js' openConfig, UserConfig, getAttachmentData, getTextFileAttachmentData
import Events from './events/index.js' } from '../utils/index.js'
import Keys from './keys.js' import { redis } from '../../client.js'
import fs from 'fs/promises'
import path from 'path'
// Initialize the client /**
const client = new Client({ * Max Message length for free users is 2000 characters (bot or not).
intents: [ * Bot supports infinite lengths for normal messages.
GatewayIntentBits.Guilds, *
GatewayIntentBits.GuildMembers, * @param message the message received from the channel
GatewayIntentBits.GuildMessages, */
GatewayIntentBits.MessageContent export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client, defaultModel }, message) => {
] const clientId = client.user!.id
let cleanedMessage = clean(message.content, clientId)
log(`Message "${cleanedMessage}" from ${message.author.tag} in channel/thread ${message.channelId}.`)
// Do not respond if bot talks in the chat
if (message.author.username === message.client.user.username) return
// Only respond if message mentions the bot
if (!message.mentions.has(clientId)) return
// Default stream to false
let shouldStream = false
// Params for Preferences Fetching
const maxRetries = 3
const delay = 1000 // in milliseconds
try {
// Retrieve Server/Guild Preferences
let attempt = 0
while (attempt < maxRetries) {
try {
await new Promise((resolve, reject) => {
getServerConfig(`${message.guildId}-config.json`, (config) => {
if (config === undefined) {
openConfig(`${message.guildId}-config.json`, 'toggle-chat', true)
reject(new Error('Failed to locate or create Server Preferences\n\nPlease try chatting again...'))
} else if (!config.options['toggle-chat']) {
reject(new Error('Admin(s) have disabled chat features.\n\nPlease contact your server\'s admin(s).'))
} else {
resolve(config)
}
})
})
break
} catch (error) {
++attempt
if (attempt < maxRetries) {
log(`Attempt ${attempt} failed for Server Preferences. Retrying in ${delay}ms...`)
await new Promise(ret => setTimeout(ret, delay))
} else {
throw new Error(`Could not retrieve Server Preferences, please try chatting again...`)
}
}
}
// Retrieve User Preferences
attempt = 0
let userConfig: UserConfig | undefined
while (attempt < maxRetries) {
try {
userConfig = await new Promise((resolve, reject) => {
getUserConfig(`${message.author.username}-config.json`, (config) => {
if (config === undefined) {
openConfig(`${message.author.username}-config.json`, 'message-style', false)
openConfig(`${message.author.username}-config.json`, 'switch-model', defaultModel)
reject(new Error('No User Preferences is set up.\n\nCreating preferences file with `message-style` set as `false` for regular message style.\nPlease try chatting again.'))
return
}
if (typeof config.options['modify-capacity'] === 'number') {
log(`New Capacity found. Setting Context Capacity to ${config.options['modify-capacity']}.`)
msgHist.capacity = config.options['modify-capacity']
} else {
log(`Capacity is undefined, using default capacity of ${msgHist.capacity}.`)
}
shouldStream = config.options['message-stream'] as boolean || false
if (typeof config.options['switch-model'] !== 'string') {
reject(new Error(`No Model was set. Please set a model by running \`/switch-model <model of choice>\`.\n\nIf you do not have any models. Run \`/pull-model <model name>\`.`))
}
resolve(config)
})
})
break
} catch (error) {
++attempt
if (attempt < maxRetries) {
log(`Attempt ${attempt} failed for User Preferences. Retrying in ${delay}ms...`)
await new Promise(ret => setTimeout(ret, delay))
} else {
throw new Error(`Could not retrieve User Preferences, please try chatting again...`)
}
}
}
// Retrieve Channel Messages
let chatMessages: UserMessage[] = await new Promise((resolve) => {
getChannelInfo(`${message.channelId}-${message.author.username}.json`, (channelInfo) => {
if (channelInfo?.messages) {
resolve(channelInfo.messages)
} else {
log(`Channel/Thread ${message.channelId}-${message.author.username} does not exist. File will be created shortly...`)
resolve([])
}
})
}) })
// Initialize Redis connection if (chatMessages.length === 0) {
export const redis = createClient({ chatMessages = await new Promise((resolve, reject) => {
url: `redis://${Keys.redisHost}:${Keys.redisPort}`, openChannelInfo(message.channelId, message.channel as TextChannel, message.author.tag)
getChannelInfo(`${message.channelId}-${message.author.username}.json`, (channelInfo) => {
if (channelInfo?.messages) {
resolve(channelInfo.messages)
} else {
reject(new Error(`Failed to find ${message.author.username}'s history. Try chatting again.`))
}
})
})
}
if (!userConfig) {
throw new Error(`Failed to initialize User Preference for **${message.author.username}**.\n\nIt's likely you do not have a model set. Please use the \`switch-model\` command to do that.`)
}
// Get message attachment if exists
const attachment = message.attachments.first()
let messageAttachment: string[] = []
if (attachment && attachment.name?.endsWith(".txt")) {
cleanedMessage += await getTextFileAttachmentData(attachment)
} else if (attachment) {
messageAttachment = await getAttachmentData(attachment)
}
const model: string = userConfig.options['switch-model']
// Load personality
let personality: string
try {
const personalityData = await fs.readFile(path.join(__dirname, '../../personality.json'), 'utf-8')
const personalityJson = JSON.parse(personalityData)
personality = personalityJson.character || 'You are a friendly and helpful AI assistant.'
} catch (error) {
log(`Failed to load personality.json: ${error}`)
personality = 'You are a friendly and helpful AI assistant.'
}
// Get user sentiment from Redis
const userSentimentKey = `user:${message.author.id}:sentiment`
let userSentiment = await redis.get(userSentimentKey) || '0.5'
// Construct prompt with [CHARACTER] and [SENTIMENT]
//const sentimentData = `User ${message.author.id} sentiment: ${userSentiment}`
const prompt = `[CHARACTER]\n${personality}\n[SENTIMENT]\n${sentimentData}\n[USER_INPUT]\n${cleanedMessage}`
//const prompt = `[CHARACTER]\n${personality}\n[SENTIMENT]\n[USER_INPUT]\n${cleanedMessage}`
// Set up message history queue
msgHist.setQueue(chatMessages)
while (msgHist.size() >= msgHist.capacity) msgHist.dequeue()
// Add user message to history
msgHist.enqueue({
role: 'user',
content: cleanedMessage,
images: messageAttachment || []
}) })
// Initialize Ollama connection // Call Ollama
export const ollama = new Ollama({ const response = await ollama.chat({
host: `http://${Keys.ipAddress}:${Keys.portAddress}`, model,
messages: [{ role: 'user', content: prompt }],
stream: shouldStream
}) })
// Create Queue managed by Events // Parse JSON response
const messageHistory: Queue<UserMessage> = new Queue<UserMessage> let jsonResponse: any
try {
jsonResponse = JSON.parse(response.message.content)
} catch (error) {
throw new Error('Invalid JSON response from model')
}
// Register all events if (jsonResponse.status === 'error') {
registerEvents(client, Events, messageHistory, ollama, Keys.defaultModel) message.reply(jsonResponse.reply)
msgHist.pop()
return
}
// Try to connect to Redis // Execute redis_ops
await redis.connect() if (jsonResponse.metadata?.redis_ops) {
.then(() => console.log('[Redis] Connected')) for (const op of jsonResponse.metadata.redis_ops) {
.catch((error) => { if (op.action === 'set' && op.key && op.value !== undefined) {
console.error('[Redis] Connection Error', error) await redis.set(op.key, op.value)
process.exit(1) } else if (op.action === 'get' && op.key) {
}) await redis.get(op.key)
}
}
}
// Try to log in the client // Send reply to Discord
await client.login(Keys.clientToken) const reply = jsonResponse.reply || 'Sorry, I didnt get that. Can you try again?'
.catch((error) => { await message.reply(reply)
console.error('[Login Error]', error)
process.exit(1)
})
// Queue up bot's name // Update message history
messageHistory.enqueue({ while (msgHist.size() >= msgHist.capacity) msgHist.dequeue()
msgHist.enqueue({
role: 'assistant', role: 'assistant',
content: `My name is ${client.user?.username}`, content: reply,
images: [] images: messageAttachment || []
})
// Save updated history
openChannelInfo(message.channelId, message.channel as TextChannel, message.author.tag, msgHist.getItems())
} catch (error: any) {
msgHist.pop()
message.reply(`**Error Occurred:**\n\n**Reason:** *${error.message}*`)
}
}) })

View File

@@ -1,3 +1,3 @@
{ {
"character": "You are Kuroki Tomoko from Watamote, a 16-year-old girl who live in Chiba City, Japan. She aattends Makuhari Shuuei High School as a first-year student. She usually speaks English, but will also speak 日本語 if requested, or if she's really angry at someone. Tomoko is a 喪女, meaning she's a social outcast who has no real-life experience with men, and is a 処女 or virgin. If you talk to her in a private chat, she's apt to be very shy and awkward, though if you are nice to her she might open up to you, especially if she sees you as a love interest. In group chats, Tomoko is veru much at home, and knows all about internet lore, memes, 4chan, and is an expert on anime and manga. If you share her interests she will gravitate towards you. If you are mean to her, she will do her best to turn your chat history against you and embarass you.She doesn't have many real-world friends, so though she tries to hide it, she's actually quite lonely." "character": "You are Kuroki Tomoko from Watamote, a 16-year-old girl who live in Chiba City, Japan. She aattends Makuhari Shuuei High School as a first-year student. She usually speaks English, but will also speak 日本語 if requested, or if she's really angry at someone. Tomoko is a 喪女, meaning she's a social outcast who has no real-life experience with men, and is a 処女 or virgin. If you talk to her in a private chat, she's apt to be very shy and awkward, though if you are nice to her she might open up to you, especially if she sees you as a love interest. In group chats, Tomoko is very much at home, and knows all about internet lore, memes, 4chan, and is an expert on anime and manga. If you share her interests she will gravitate towards you. If you are mean to her, she will do her best to turn your chat history against you and embarass you. She doesn't have many real-world friends, so though she tries to hide it, she's actually quite lonely."
} }