updated src/events/messageCreate.ts
This commit is contained in:
10
package-lock.json
generated
10
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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}.`)
|
||||||
// Initialize Redis connection
|
|
||||||
export const redis = createClient({
|
// Do not respond if bot talks in the chat
|
||||||
url: `redis://${Keys.redisHost}:${Keys.redisPort}`,
|
if (message.author.username === message.client.user.username) return
|
||||||
})
|
|
||||||
|
// Only respond if message mentions the bot
|
||||||
// Initialize Ollama connection
|
if (!message.mentions.has(clientId)) return
|
||||||
export const ollama = new Ollama({
|
|
||||||
host: `http://${Keys.ipAddress}:${Keys.portAddress}`,
|
// Default stream to false
|
||||||
})
|
let shouldStream = false
|
||||||
|
|
||||||
// Create Queue managed by Events
|
// Params for Preferences Fetching
|
||||||
const messageHistory: Queue<UserMessage> = new Queue<UserMessage>
|
const maxRetries = 3
|
||||||
|
const delay = 1000 // in milliseconds
|
||||||
// Register all events
|
|
||||||
registerEvents(client, Events, messageHistory, ollama, Keys.defaultModel)
|
try {
|
||||||
|
// Retrieve Server/Guild Preferences
|
||||||
// Try to connect to Redis
|
let attempt = 0
|
||||||
await redis.connect()
|
while (attempt < maxRetries) {
|
||||||
.then(() => console.log('[Redis] Connected'))
|
try {
|
||||||
.catch((error) => {
|
await new Promise((resolve, reject) => {
|
||||||
console.error('[Redis] Connection Error', error)
|
getServerConfig(`${message.guildId}-config.json`, (config) => {
|
||||||
process.exit(1)
|
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...'))
|
||||||
// Try to log in the client
|
} else if (!config.options['toggle-chat']) {
|
||||||
await client.login(Keys.clientToken)
|
reject(new Error('Admin(s) have disabled chat features.\n\nPlease contact your server\'s admin(s).'))
|
||||||
.catch((error) => {
|
} else {
|
||||||
console.error('[Login Error]', error)
|
resolve(config)
|
||||||
process.exit(1)
|
}
|
||||||
})
|
})
|
||||||
|
})
|
||||||
// Queue up bot's name
|
break
|
||||||
messageHistory.enqueue({
|
} catch (error) {
|
||||||
role: 'assistant',
|
++attempt
|
||||||
content: `My name is ${client.user?.username}`,
|
if (attempt < maxRetries) {
|
||||||
images: []
|
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([])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (chatMessages.length === 0) {
|
||||||
|
chatMessages = await new Promise((resolve, reject) => {
|
||||||
|
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 || []
|
||||||
|
})
|
||||||
|
|
||||||
|
// Call Ollama
|
||||||
|
const response = await ollama.chat({
|
||||||
|
model,
|
||||||
|
messages: [{ role: 'user', content: prompt }],
|
||||||
|
stream: shouldStream
|
||||||
|
})
|
||||||
|
|
||||||
|
// Parse JSON response
|
||||||
|
let jsonResponse: any
|
||||||
|
try {
|
||||||
|
jsonResponse = JSON.parse(response.message.content)
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error('Invalid JSON response from model')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonResponse.status === 'error') {
|
||||||
|
message.reply(jsonResponse.reply)
|
||||||
|
msgHist.pop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute redis_ops
|
||||||
|
if (jsonResponse.metadata?.redis_ops) {
|
||||||
|
for (const op of jsonResponse.metadata.redis_ops) {
|
||||||
|
if (op.action === 'set' && op.key && op.value !== undefined) {
|
||||||
|
await redis.set(op.key, op.value)
|
||||||
|
} else if (op.action === 'get' && op.key) {
|
||||||
|
await redis.get(op.key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send reply to Discord
|
||||||
|
const reply = jsonResponse.reply || 'Sorry, I didn’t get that. Can you try again?'
|
||||||
|
await message.reply(reply)
|
||||||
|
|
||||||
|
// Update message history
|
||||||
|
while (msgHist.size() >= msgHist.capacity) msgHist.dequeue()
|
||||||
|
msgHist.enqueue({
|
||||||
|
role: 'assistant',
|
||||||
|
content: reply,
|
||||||
|
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}*`)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
import('./client.js')
|
import('./client.js')
|
||||||
|
|||||||
@@ -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."
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user