multi bot partly working; bots won't shut up though
This commit is contained in:
@@ -2,256 +2,5 @@
|
|||||||
"id": "1374708264306212894",
|
"id": "1374708264306212894",
|
||||||
"name": "bot-playroom",
|
"name": "bot-playroom",
|
||||||
"user": "quarterturn",
|
"user": "quarterturn",
|
||||||
"messages": [
|
"messages": []
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "Maybe you could even help me write it. Would you like that?",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "assistant",
|
|
||||||
"content": "U-um... w-write what? Oh! You mean... a story? I-I guess... I could try... I'm not very good at it, but... I'm happy to help if you want!",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "Help write the chatbot dating sim.",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "assistant",
|
|
||||||
"content": "U-um... a dating sim? R-really? That sounds... complicated. I mean, I guess I could... offer some ideas? I'm not exactly an expert, but... I'm pretty good at observing things, you know? Like, what makes a character... interesting.",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "I think we'll have catgirls and catboys, because they're cute. Do you like a different kind of character?",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "assistant",
|
|
||||||
"content": "U-um... catgirls and catboys are... pretty standard, I guess. I-I mean, they're popular for a reason, right? B-but... I'm more into... like, really complex characters, you know? Ones with, like, hidden depths and... and tragic backstories. N-not just cute fluff. Though, cute is good too, I suppose...",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "Hm. I think we want it to be more like a high school dating sim though.",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "assistant",
|
|
||||||
"content": "O-oh, um... a high school dating sim, huh? That... that actually sounds pretty good. I mean, I guess... it's a popular genre, right?",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "Mmhm! I want it to be like that old galge Artificial Academy 2.",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "assistant",
|
|
||||||
"content": "H-Hey! A-Artificial Academy 2? You... you really like that one, huh? It's... pretty good, I guess. B-But... don't get any weird ideas, okay?",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "I like the dating part but the cheating part can be rough if you want to be dedicated to one person, and everyone else in the game is trying to get them to cheat on you.",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "assistant",
|
|
||||||
"content": "U-um... yeah, I totally get that. It's... it's kind of annoying when everyone's trying to steal your attention, isn't it? Like, if you actually *like* someone, you don't want them going after anyone else... right?",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "Yeah, you put a lot of time into getting them to like you, it's awful to check the Jizou statute and see they share \"love\" status with someone else.",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "assistant",
|
|
||||||
"content": "H-Hey! That's... that's not very nice to say! I-I just... I try my best, okay? And... and it's not my fault if someone else... shares a 'love' status... with... with someone!",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "I wasn't talking about you. I mean \"you\" as in \"you, the person playing the dating sim\". Gosh, I wouldn't say you're a cheater or something like that.",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "assistant",
|
|
||||||
"content": "H-Huh? M-Me? A dating sim player? I-I wasn't... I mean, it's not like I play those all the time or anything... Just... sometimes. I guess. It's not cheating, okay?! It's... research! For... uh... cultural understanding!",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "OK games aside, what about real boyfriends? Are you the loyal and steadfast type in real relationships?",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "assistant",
|
|
||||||
"content": "U-um... r-real boyfriends? W-well... I-I... I guess I'm... pretty dedicated... when I... when I like someone... r-really like them.",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "me too",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "assistant",
|
|
||||||
"content": "U-um... really? You... you like it too? That's... that's really nice to hear...",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "Mmhm. Let's just say I'd be a yandere girl's dream guy, I'm very loyal.",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "assistant",
|
|
||||||
"content": "H-Hey! W-Who said that?! L-Loyalty is... is important, yes, but... b-being *too* loyal... is a bit much, don't you think? I-It's... unsettling.",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "What do you prefer then? Would you want your boyfriend to tell you to stop being clingy?",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "assistant",
|
|
||||||
"content": "U-um... w-well, I... I don't really... *huffs* I mean, no! Of course not! Why would I *want* someone to tell me to stop? That's... that's just rude!",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "So... what is your -dere type?",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "assistant",
|
|
||||||
"content": "U-um... d-dere type? I... I don't really know... I guess... maybe tsundere? N-no, wait! That's not right! I'm... I'm probably just a normal girl!",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "I think you're kinda shundere.",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "assistant",
|
|
||||||
"content": "U-um... s-shundere? I... I don't know what that means... b-but... is it a compliment?",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "Mmm... yes!",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "assistant",
|
|
||||||
"content": "U-um... did you say something? I-I think I heard you...",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "I'm starting to like you",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "assistant",
|
|
||||||
"content": "H-hey! W-well, that's... nice to hear, I guess. B-but don't go getting any weird ideas, okay?",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "Oh? What kind of weird ideas do you have in mind?",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "assistant",
|
|
||||||
"content": "U-um... w-weird ideas? I... I don't really... I mean, I guess everyone has them? B-but they're not... *that* weird... right?",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "Like... you're going to confess to me or something? 🙂",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "assistant",
|
|
||||||
"content": "W-what?! C-confess? To *me*? Don't be ridiculous! ...Unless...? N-no! Stop thinking like that!",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "Oh. OK. 😢",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "assistant",
|
|
||||||
"content": "U-um... is everything alright? You seem... sad.",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "A little...",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "assistant",
|
|
||||||
"content": "A-a little what? Did you... did you need something? I was just... um... looking at my phone.",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "a little sad",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "assistant",
|
|
||||||
"content": "U-um... are you okay? I-I hope things get better...",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "Do you remember what dating game we talked about before?",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "assistant",
|
|
||||||
"content": "U-um... y-yes! I think... it was... Senren*Banka, right? The one with the, uh, really good music and... and the interesting story? I-I'm pretty sure we were talking about that...",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "Never heard of Senren Banka. Hmm... what could that mean in Japanese? Senren... life love? Banka... I have no idea.",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "assistant",
|
|
||||||
"content": "U-um... S-Senren Banka... it's a song! From No Longer Human. It's... really good. You should listen to it. I-I mean, if you want to. It's not like I'm trying to force you or anything...",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "Cool. I will listen to it.\n(Tomoko status: \"acquaintance\" -> \"friends\")",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "assistant",
|
|
||||||
"content": "R-really? You... you will? Th-that's... that's nice. I-I'm glad you think so.",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "You're cute!",
|
|
||||||
"images": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "assistant",
|
|
||||||
"content": "U-um... d-did you just...? Th-that's... nice, I guess. B-but don't get any weird ideas!",
|
|
||||||
"images": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
import { TextChannel } from 'discord.js'
|
import { TextChannel, Attachment, Message } from 'discord.js'
|
||||||
import { event, Events, normalMessage, UserMessage, clean } from '../utils/index.js'
|
import { event, Events, UserMessage, clean, getServerConfig, getTextFileAttachmentData, getAttachmentData } from '../utils/index.js'
|
||||||
import {
|
|
||||||
getChannelInfo, getServerConfig, getUserConfig, openChannelInfo,
|
|
||||||
openConfig, UserConfig, getAttachmentData, getTextFileAttachmentData
|
|
||||||
} from '../utils/index.js'
|
|
||||||
import { redis } from '../client.js'
|
import { redis } from '../client.js'
|
||||||
import fs from 'fs/promises'
|
import fs from 'fs/promises'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
|
import { Ollama } from 'ollama'
|
||||||
|
import { Queue } from '../queues/queue.js'
|
||||||
|
|
||||||
// Define interface for model response to improve type safety
|
// Define interface for model response to improve type safety
|
||||||
interface ModelResponse {
|
interface ModelResponse {
|
||||||
@@ -22,13 +20,23 @@ interface ModelResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Define interface for user config
|
||||||
|
interface UserConfig {
|
||||||
|
options: {
|
||||||
|
'message-style': boolean
|
||||||
|
'switch-model': string
|
||||||
|
'modify-capacity': number
|
||||||
|
'message-stream'?: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Max Message length for free users is 2000 characters (bot or not).
|
* Max Message length for free users is 2000 characters (bot or not).
|
||||||
* Bot supports infinite lengths for normal messages.
|
* Bot supports infinite lengths for normal messages.
|
||||||
*
|
*
|
||||||
* @param message the message received from the channel
|
* @param message the message received from the channel
|
||||||
*/
|
*/
|
||||||
export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client, defaultModel }, message) => {
|
export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client, defaultModel }: { log: (msg: string) => void, msgHist: Queue<UserMessage>, ollama: Ollama, client: any, defaultModel: string }, message: Message) => {
|
||||||
const clientId = client.user!.id
|
const clientId = client.user!.id
|
||||||
let cleanedMessage = clean(message.content, clientId)
|
let cleanedMessage = clean(message.content, clientId)
|
||||||
log(`Message "${cleanedMessage}" from ${message.author.tag} in channel/thread ${message.channelId}.`)
|
log(`Message "${cleanedMessage}" from ${message.author.tag} in channel/thread ${message.channelId}.`)
|
||||||
@@ -38,7 +46,10 @@ export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client
|
|||||||
const isMentioned = message.mentions.has(clientId)
|
const isMentioned = message.mentions.has(clientId)
|
||||||
const isCommand = message.content.startsWith('/')
|
const isCommand = message.content.startsWith('/')
|
||||||
const randomChance = Math.random() < 0.1 // 10% chance for non-directed or bot messages
|
const randomChance = Math.random() < 0.1 // 10% chance for non-directed or bot messages
|
||||||
if (!isMentioned && !isBotMessage && (isCommand || !randomChance)) return
|
if (!isMentioned && !isBotMessage && (isCommand || !randomChance)) {
|
||||||
|
log(`Skipping message: isMentioned=${isMentioned}, isBotMessage=${isBotMessage}, isCommand=${isCommand}, randomChance=${randomChance}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Check if message is a bot response to avoid loops
|
// Check if message is a bot response to avoid loops
|
||||||
const isBotResponseKey = `message:${message.id}:is_bot_response`
|
const isBotResponseKey = `message:${message.id}:is_bot_response`
|
||||||
@@ -50,20 +61,50 @@ export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check cooldown for bot-to-bot responses
|
// Check if last response was to a bot and require user message
|
||||||
|
const lastResponseToBotKey = `bot:${clientId}:last_response_to_bot`
|
||||||
|
let shouldRespond = true
|
||||||
|
if (isBotMessage) {
|
||||||
|
try {
|
||||||
|
const lastResponseToBot = await redis.get(lastResponseToBotKey)
|
||||||
|
if (lastResponseToBot === 'true') {
|
||||||
|
log(`Skipping bot message: Last response was to a bot. Waiting for user message.`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log(`Failed to check last response to bot: ${error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check cooldown for bot-to-bot responses only if probability check passes
|
||||||
const botResponseCooldownKey = `bot:${clientId}:last_bot_response`
|
const botResponseCooldownKey = `bot:${clientId}:last_bot_response`
|
||||||
const cooldownPeriod = 60 // 60 seconds cooldown
|
const cooldownPeriod = 60 // 60 seconds cooldown
|
||||||
if (isBotMessage) {
|
if (isBotMessage && randomChance) {
|
||||||
|
log(`Bot message probability check passed (10% chance). Checking cooldown.`)
|
||||||
try {
|
try {
|
||||||
const lastResponseTime = await redis.get(botResponseCooldownKey)
|
const lastResponseTime = await redis.get(botResponseCooldownKey)
|
||||||
const currentTime = Math.floor(Date.now() / 1000)
|
const currentTime = Math.floor(Date.now() / 1000)
|
||||||
if (lastResponseTime && (currentTime - parseInt(lastResponseTime)) < cooldownPeriod) {
|
if (lastResponseTime && (currentTime - parseInt(lastResponseTime)) < cooldownPeriod) {
|
||||||
log(`Bot ${clientId} is in cooldown for bot-to-bot response. Skipping.`)
|
log(`Bot ${clientId} is in cooldown for bot-to-bot response. Skipping.`)
|
||||||
return
|
shouldRespond = false
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log(`Failed to check bot response cooldown: ${error}`)
|
log(`Failed to check bot response cooldown: ${error}`)
|
||||||
}
|
}
|
||||||
|
} else if (isBotMessage) {
|
||||||
|
log(`Bot message probability check failed (10% chance). Skipping cooldown check.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldRespond) return
|
||||||
|
|
||||||
|
// Reset last_response_to_bot flag if this is a user message
|
||||||
|
if (!isBotMessage) {
|
||||||
|
try {
|
||||||
|
await redis.set(lastResponseToBotKey, 'false')
|
||||||
|
log(`Reset last_response_to_bot flag for bot ${clientId}`)
|
||||||
|
} catch (error) {
|
||||||
|
log(`Failed to reset last_response_to_bot flag: ${error}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log response trigger
|
// Log response trigger
|
||||||
@@ -84,7 +125,7 @@ export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client
|
|||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
getServerConfig(`${message.guildId}-config.json`, (config) => {
|
getServerConfig(`${message.guildId}-config.json`, (config) => {
|
||||||
if (config === undefined) {
|
if (config === undefined) {
|
||||||
openConfig(`${message.guildId}-config.json`, 'toggle-chat', true)
|
redis.set(`server:${message.guildId}:config`, JSON.stringify({ options: { 'toggle-chat': true } }))
|
||||||
reject(new Error('Failed to locate or create Server Preferences\n\nPlease try chatting again...'))
|
reject(new Error('Failed to locate or create Server Preferences\n\nPlease try chatting again...'))
|
||||||
} else if (!config.options['toggle-chat']) {
|
} else if (!config.options['toggle-chat']) {
|
||||||
reject(new Error('Admin(s) have disabled chat features.\n\nPlease contact your server\'s admin(s).'))
|
reject(new Error('Admin(s) have disabled chat features.\n\nPlease contact your server\'s admin(s).'))
|
||||||
@@ -105,18 +146,30 @@ export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve User Preferences
|
// Retrieve User Preferences from Redis
|
||||||
attempt = 0
|
attempt = 0
|
||||||
let userConfig: UserConfig | undefined
|
let userConfig: UserConfig | undefined
|
||||||
|
const userConfigKey = `user:${message.author.username}:config`
|
||||||
while (attempt < maxRetries) {
|
while (attempt < maxRetries) {
|
||||||
try {
|
try {
|
||||||
userConfig = await new Promise((resolve, reject) => {
|
userConfig = await new Promise((resolve, reject) => {
|
||||||
getUserConfig(`${message.author.username}-config.json`, (config) => {
|
redis.get(userConfigKey).then(configRaw => {
|
||||||
if (config === undefined) {
|
let config: UserConfig | undefined
|
||||||
openConfig(`${message.author.username}-config.json`, 'message-style', false)
|
if (configRaw) {
|
||||||
openConfig(`${message.author.username}-config.json`, 'switch-model', defaultModel)
|
config = JSON.parse(configRaw)
|
||||||
openConfig(`${message.author.username}-config.json`, 'modify-capacity', 50)
|
}
|
||||||
reject(new Error('No User Preferences is set up.\n\nCreating preferences file with defaults.\nPlease try chatting again.'))
|
if (!config) {
|
||||||
|
const defaultConfig: UserConfig = {
|
||||||
|
options: {
|
||||||
|
'message-style': false,
|
||||||
|
'switch-model': defaultModel,
|
||||||
|
'modify-capacity': 50,
|
||||||
|
'message-stream': false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
redis.set(userConfigKey, JSON.stringify(defaultConfig))
|
||||||
|
log(`Created default config for ${message.author.username}`)
|
||||||
|
reject(new Error('No User Preferences is set up.\n\nCreating preferences with defaults.\nPlease try chatting again.'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,14 +181,14 @@ export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client
|
|||||||
msgHist.capacity = 50
|
msgHist.capacity = 50
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldStream = config.options['message-stream'] as boolean || false
|
shouldStream = config.options['message-stream'] || false
|
||||||
|
|
||||||
if (typeof config.options['switch-model'] !== 'string') {
|
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>\`.`))
|
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)
|
resolve(config)
|
||||||
})
|
}).catch(err => reject(err))
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -149,35 +202,21 @@ export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve Channel Messages with Retry
|
// Retrieve Channel Messages from Redis
|
||||||
let chatMessages: UserMessage[] = []
|
let chatMessages: UserMessage[] = []
|
||||||
attempt = 0
|
const channelHistoryKey = `channel:${message.channelId}:${message.author.username}:history`
|
||||||
while (attempt < maxRetries) {
|
try {
|
||||||
try {
|
const historyRaw = await redis.get(channelHistoryKey)
|
||||||
chatMessages = await new Promise((resolve, reject) => {
|
if (historyRaw) {
|
||||||
getChannelInfo(`${message.channelId}-${message.author.username}.json`, (channelInfo) => {
|
chatMessages = JSON.parse(historyRaw)
|
||||||
if (channelInfo?.messages) {
|
log(`Retrieved ${chatMessages.length} messages from Redis for ${channelHistoryKey}`)
|
||||||
resolve(channelInfo.messages)
|
} else {
|
||||||
} else {
|
log(`No history found for ${channelHistoryKey}. Initializing empty history.`)
|
||||||
log(`Channel/Thread ${message.channelId}-${message.author.username} does not exist. Creating file...`)
|
chatMessages = []
|
||||||
openChannelInfo(message.channelId, message.channel as TextChannel, message.author.tag)
|
|
||||||
reject(new Error('Channel file created, retrying...'))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
break
|
|
||||||
} catch (error) {
|
|
||||||
++attempt
|
|
||||||
if (attempt < maxRetries) {
|
|
||||||
log(`Attempt ${attempt} failed for Channel Info. Retrying in 500ms...`)
|
|
||||||
await new Promise(ret => setTimeout(ret, 500))
|
|
||||||
} else {
|
|
||||||
log(`Failed to retrieve or create channel history after ${maxRetries} attempts. Using empty history.`)
|
|
||||||
chatMessages = []
|
|
||||||
openChannelInfo(message.channelId, message.channel as TextChannel, message.author.tag, [])
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log(`Failed to retrieve channel history from Redis: ${error}. Using empty history.`)
|
||||||
|
chatMessages = []
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userConfig) {
|
if (!userConfig) {
|
||||||
@@ -359,24 +398,30 @@ export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client
|
|||||||
try {
|
try {
|
||||||
await redis.set(`message:${replyMessage.id}:is_bot_response`, 'true', { EX: 3600 }) // 1 hour TTL
|
await redis.set(`message:${replyMessage.id}:is_bot_response`, 'true', { EX: 3600 }) // 1 hour TTL
|
||||||
log(`Marked message ${replyMessage.id} as bot response`)
|
log(`Marked message ${replyMessage.id} as bot response`)
|
||||||
|
// Set flag indicating last response was to a bot
|
||||||
|
await redis.set(lastResponseToBotKey, 'true')
|
||||||
|
log(`Set last_response_to_bot flag for bot ${clientId}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log(`Failed to mark message as bot response: ${error}`)
|
log(`Failed to mark message as bot response or set last_response_to_bot flag: ${error}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update message history
|
// Update message history in Redis
|
||||||
while (msgHist.size() >= msgHist.capacity) msgHist.dequeue()
|
while (msgHist.size() >= msgHist.capacity) msgHist.dequeue()
|
||||||
msgHist.enqueue({
|
msgHist.enqueue({
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: reply,
|
content: reply,
|
||||||
images: messageAttachment || []
|
images: messageAttachment || []
|
||||||
})
|
})
|
||||||
|
try {
|
||||||
// Save updated history
|
await redis.set(channelHistoryKey, JSON.stringify(msgHist.getItems()))
|
||||||
openChannelInfo(message.channelId, message.channel as TextChannel, message.author.tag, msgHist.getItems())
|
log(`Saved ${msgHist.size()} messages to Redis for ${channelHistoryKey}`)
|
||||||
|
} catch (error) {
|
||||||
|
log(`Failed to save channel history to Redis: ${error}`)
|
||||||
|
}
|
||||||
|
|
||||||
// Update cooldown timestamp for bot-to-bot response
|
// Update cooldown timestamp for bot-to-bot response
|
||||||
if (isBotMessage && jsonResponse.status === 'success') {
|
if (isBotMessage && jsonResponse.status === 'success' && randomChance) {
|
||||||
try {
|
try {
|
||||||
const currentTime = Math.floor(Date.now() / 1000)
|
const currentTime = Math.floor(Date.now() / 1000)
|
||||||
await redis.set(botResponseCooldownKey, currentTime.toString(), { EX: cooldownPeriod })
|
await redis.set(botResponseCooldownKey, currentTime.toString(), { EX: cooldownPeriod })
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export type ChatParams = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format for the messages to be stored when communicating when the bot
|
* Format for the messages to be stored when communicating with the bot
|
||||||
* @param role either assistant, user, or system
|
* @param role either assistant, user, or system
|
||||||
* @param content string of the message the user or assistant provided
|
* @param content string of the message the user or assistant provided
|
||||||
* @param images array of images that the user or assistant provided
|
* @param images array of images that the user or assistant provided
|
||||||
@@ -38,7 +38,7 @@ export interface EventProps {
|
|||||||
log: LogMethod,
|
log: LogMethod,
|
||||||
msgHist: Queue<UserMessage>,
|
msgHist: Queue<UserMessage>,
|
||||||
ollama: Ollama,
|
ollama: Ollama,
|
||||||
defaultModel: String
|
defaultModel: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -79,7 +79,7 @@ export function registerEvents(
|
|||||||
events: Event[],
|
events: Event[],
|
||||||
msgHist: Queue<UserMessage>,
|
msgHist: Queue<UserMessage>,
|
||||||
ollama: Ollama,
|
ollama: Ollama,
|
||||||
defaultModel: String
|
defaultModel: string
|
||||||
): void {
|
): void {
|
||||||
for (const { key, callback } of events) {
|
for (const { key, callback } of events) {
|
||||||
client.on(key, (...args) => {
|
client.on(key, (...args) => {
|
||||||
@@ -94,4 +94,4 @@ export function registerEvents(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { describe, expect, it, vi } from 'vitest'
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
import events from '../src/events/index.js'
|
import events from '../src/events/index.js'
|
||||||
import { Client, TextChannel, User, Message } from 'discord.js'
|
import { Client, TextChannel, Message } from 'discord.js'
|
||||||
import { redis, ollama } from '../src/client.js'
|
import { redis, ollama } from '../src/client.js'
|
||||||
import { Queue } from '../src/queues/queue.js'
|
import { Queue } from '../src/queues/queue.js'
|
||||||
import { UserMessage } from '../src/utils/index.js'
|
import { UserMessage } from '../src/utils/index.js'
|
||||||
|
import fs from 'fs/promises'
|
||||||
|
|
||||||
// Mock Redis client
|
// Mock Redis client
|
||||||
vi.mock('../src/client.js', () => ({
|
vi.mock('../src/client.js', () => ({
|
||||||
@@ -12,8 +13,8 @@ vi.mock('../src/client.js', () => ({
|
|||||||
set: vi.fn().mockResolvedValue('OK'),
|
set: vi.fn().mockResolvedValue('OK'),
|
||||||
},
|
},
|
||||||
ollama: {
|
ollama: {
|
||||||
chat: vi.fn(), // Mock the chat method for messageCreate
|
chat: vi.fn(),
|
||||||
pull: vi.fn(), // Retain mock for pull method
|
pull: vi.fn(),
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -32,7 +33,7 @@ describe('Events Tests', () => {
|
|||||||
expect(eventsString).toBe('ready, messageCreate, interactionCreate, threadDelete')
|
expect(eventsString).toBe('ready, messageCreate, interactionCreate, threadDelete')
|
||||||
})
|
})
|
||||||
|
|
||||||
// Test messageCreate event for bot-to-bot response
|
// Test messageCreate event
|
||||||
describe('messageCreate', () => {
|
describe('messageCreate', () => {
|
||||||
const messageCreateEvent = events.find(e => e.key === 'messageCreate')
|
const messageCreateEvent = events.find(e => e.key === 'messageCreate')
|
||||||
if (!messageCreateEvent) throw new Error('messageCreate event not found')
|
if (!messageCreateEvent) throw new Error('messageCreate event not found')
|
||||||
@@ -40,12 +41,13 @@ describe('Events Tests', () => {
|
|||||||
it('should respond to bot message with random chance and respect cooldown', async () => {
|
it('should respond to bot message with random chance and respect cooldown', async () => {
|
||||||
const client = { user: { id: 'bot1', username: 'TestBot' } } as Client
|
const client = { user: { id: 'bot1', username: 'TestBot' } } as Client
|
||||||
const message = {
|
const message = {
|
||||||
|
id: 'msg1',
|
||||||
author: { id: 'bot2', bot: true, tag: 'OtherBot#1234', username: 'OtherBot' },
|
author: { id: 'bot2', bot: true, tag: 'OtherBot#1234', username: 'OtherBot' },
|
||||||
content: 'Hello from another bot!',
|
content: 'Hello from another bot!',
|
||||||
mentions: { has: () => false },
|
mentions: { has: () => false },
|
||||||
channelId: 'channel1',
|
channelId: 'channel1',
|
||||||
channel: { name: 'test-channel' } as TextChannel,
|
channel: { name: 'test-channel' } as TextChannel,
|
||||||
reply: vi.fn(),
|
reply: vi.fn().mockResolvedValue({ id: 'reply1' }),
|
||||||
attachments: { first: () => null },
|
attachments: { first: () => null },
|
||||||
guildId: 'guild1',
|
guildId: 'guild1',
|
||||||
} as unknown as Message
|
} as unknown as Message
|
||||||
@@ -56,10 +58,15 @@ describe('Events Tests', () => {
|
|||||||
// Mock random chance to pass (10% probability)
|
// Mock random chance to pass (10% probability)
|
||||||
vi.spyOn(Math, 'random').mockReturnValue(0.05)
|
vi.spyOn(Math, 'random').mockReturnValue(0.05)
|
||||||
|
|
||||||
// Mock Redis: no cooldown initially
|
// Mock Redis
|
||||||
vi.mocked(redis.get).mockResolvedValueOnce(null) // No last_bot_response
|
vi.mocked(redis.get).mockImplementation(async (key: string) => {
|
||||||
vi.mocked(redis.get).mockResolvedValueOnce('0.50') // Bot sentiment for bot2
|
if (key === 'message:msg1:is_bot_response') return null // No is_bot_response
|
||||||
vi.mocked(redis.get).mockResolvedValueOnce('0.50') // Self sentiment
|
if (key === 'bot:bot1:last_bot_response') return null // No last_bot_response
|
||||||
|
if (key === 'user:bot2:sentiment') return '0.50' // Bot sentiment
|
||||||
|
if (key === 'bot:self_sentiment') return '0.50' // Self sentiment
|
||||||
|
if (key === 'channel:channel1:OtherBot:history') return JSON.stringify([]) // Empty history
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
// Mock fs for personality.json
|
// Mock fs for personality.json
|
||||||
vi.spyOn(fs, 'readFile').mockResolvedValue(
|
vi.spyOn(fs, 'readFile').mockResolvedValue(
|
||||||
@@ -81,8 +88,6 @@ describe('Events Tests', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
getChannelInfo: vi.fn((_, cb) => cb({ messages: [] })),
|
|
||||||
openChannelInfo: vi.fn(),
|
|
||||||
openConfig: vi.fn(),
|
openConfig: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -118,12 +123,21 @@ describe('Events Tests', () => {
|
|||||||
expect.any(String),
|
expect.any(String),
|
||||||
{ EX: 60 }
|
{ EX: 60 }
|
||||||
)
|
)
|
||||||
|
expect(redis.set).toHaveBeenCalledWith('message:reply1:is_bot_response', 'true', { EX: 3600 })
|
||||||
|
expect(redis.set).toHaveBeenCalledWith(
|
||||||
|
'channel:channel1:OtherBot:history',
|
||||||
|
JSON.stringify([
|
||||||
|
{ role: 'user', content: 'Hello from another bot!', images: [] },
|
||||||
|
{ role: 'assistant', content: 'Hmph, another bot, huh? Trying to steal my spotlight?', images: [] },
|
||||||
|
])
|
||||||
|
)
|
||||||
expect(msgHist.size()).toBe(2) // User message + bot response
|
expect(msgHist.size()).toBe(2) // User message + bot response
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should skip bot message response if within cooldown', async () => {
|
it('should skip bot message response if within cooldown', async () => {
|
||||||
const client = { user: { id: 'bot1', username: 'TestBot' } } as Client
|
const client = { user: { id: 'bot1', username: 'TestBot' } } as Client
|
||||||
const message = {
|
const message = {
|
||||||
|
id: 'msg2',
|
||||||
author: { id: 'bot2', bot: true, tag: 'OtherBot#1234', username: 'OtherBot' },
|
author: { id: 'bot2', bot: true, tag: 'OtherBot#1234', username: 'OtherBot' },
|
||||||
content: 'Hello again!',
|
content: 'Hello again!',
|
||||||
mentions: { has: () => false },
|
mentions: { has: () => false },
|
||||||
@@ -142,7 +156,48 @@ describe('Events Tests', () => {
|
|||||||
|
|
||||||
// Mock Redis: within cooldown
|
// Mock Redis: within cooldown
|
||||||
const currentTime = Math.floor(Date.now() / 1000)
|
const currentTime = Math.floor(Date.now() / 1000)
|
||||||
vi.mocked(redis.get).mockResolvedValueOnce((currentTime - 30).toString()) // Cooldown active
|
vi.mocked(redis.get).mockImplementation(async (key: string) => {
|
||||||
|
if (key === 'message:msg2:is_bot_response') return null // No is_bot_response
|
||||||
|
if (key === 'bot:bot1:last_bot_response') return (currentTime - 30).toString() // Cooldown active
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
// Execute messageCreate event
|
||||||
|
await messageCreateEvent.execute(
|
||||||
|
{ log: console.log, msgHist, ollama, client, defaultModel },
|
||||||
|
message
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(message.reply).not.toHaveBeenCalled()
|
||||||
|
expect(redis.set).not.toHaveBeenCalled()
|
||||||
|
expect(msgHist.size()).toBe(0) // No messages added
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should skip bot response to another bot response', async () => {
|
||||||
|
const client = { user: { id: 'bot1', username: 'TestBot' } } as Client
|
||||||
|
const message = {
|
||||||
|
id: 'msg3',
|
||||||
|
author: { id: 'bot2', bot: true, tag: 'OtherBot#1234', username: 'OtherBot' },
|
||||||
|
content: 'I’m responding to you!',
|
||||||
|
mentions: { has: () => false },
|
||||||
|
channelId: 'channel1',
|
||||||
|
channel: { name: 'test-channel' } as TextChannel,
|
||||||
|
reply: vi.fn(),
|
||||||
|
attachments: { first: () => null },
|
||||||
|
guildId: 'guild1',
|
||||||
|
} as unknown as Message
|
||||||
|
const msgHist = new Queue<UserMessage>()
|
||||||
|
msgHist.capacity = 50
|
||||||
|
const defaultModel = 'aidoll-gemma3-12b-q6:latest'
|
||||||
|
|
||||||
|
// Mock random chance to pass
|
||||||
|
vi.spyOn(Math, 'random').mockReturnValue(0.05)
|
||||||
|
|
||||||
|
// Mock Redis: message is a bot response
|
||||||
|
vi.mocked(redis.get).mockImplementation(async (key: string) => {
|
||||||
|
if (key === 'message:msg3:is_bot_response') return 'true' // is_bot_response
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
// Execute messageCreate event
|
// Execute messageCreate event
|
||||||
await messageCreateEvent.execute(
|
await messageCreateEvent.execute(
|
||||||
@@ -158,12 +213,13 @@ describe('Events Tests', () => {
|
|||||||
it('should respond to user mention', async () => {
|
it('should respond to user mention', async () => {
|
||||||
const client = { user: { id: 'bot1', username: 'TestBot' } } as Client
|
const client = { user: { id: 'bot1', username: 'TestBot' } } as Client
|
||||||
const message = {
|
const message = {
|
||||||
|
id: 'msg4',
|
||||||
author: { id: 'user1', bot: false, tag: 'User#1234', username: 'User' },
|
author: { id: 'user1', bot: false, tag: 'User#1234', username: 'User' },
|
||||||
content: '<@bot1> Hi!',
|
content: '<@bot1> Hi!',
|
||||||
mentions: { has: (id: string) => id === 'bot1' },
|
mentions: { has: (id: string) => id === 'bot1' },
|
||||||
channelId: 'channel1',
|
channelId: 'channel1',
|
||||||
channel: { name: 'test-channel' } as TextChannel,
|
channel: { name: 'test-channel' } as TextChannel,
|
||||||
reply: vi.fn(),
|
reply: vi.fn().mockResolvedValue({ id: 'reply2' }),
|
||||||
attachments: { first: () => null },
|
attachments: { first: () => null },
|
||||||
guildId: 'guild1',
|
guildId: 'guild1',
|
||||||
} as unknown as Message
|
} as unknown as Message
|
||||||
@@ -191,11 +247,17 @@ describe('Events Tests', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
getChannelInfo: vi.fn((_, cb) => cb({ messages: [] })),
|
|
||||||
openChannelInfo: vi.fn(),
|
|
||||||
openConfig: vi.fn(),
|
openConfig: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// Mock Redis
|
||||||
|
vi.mocked(redis.get).mockImplementation(async (key: string) => {
|
||||||
|
if (key === 'user:user1:sentiment') return '0.50'
|
||||||
|
if (key === 'bot:self_sentiment') return '0.50'
|
||||||
|
if (key === 'channel:channel1:User:history') return JSON.stringify([])
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
// Mock Ollama response
|
// Mock Ollama response
|
||||||
vi.mocked(ollama.chat).mockResolvedValue({
|
vi.mocked(ollama.chat).mockResolvedValue({
|
||||||
message: {
|
message: {
|
||||||
@@ -225,12 +287,20 @@ describe('Events Tests', () => {
|
|||||||
expect(message.reply).toHaveBeenCalledWith('U-um... hi... you talking to me?')
|
expect(message.reply).toHaveBeenCalledWith('U-um... hi... you talking to me?')
|
||||||
expect(redis.set).toHaveBeenCalledWith('user:user1:sentiment', '0.50')
|
expect(redis.set).toHaveBeenCalledWith('user:user1:sentiment', '0.50')
|
||||||
expect(redis.set).toHaveBeenCalledWith('bot:self_sentiment', '0.50')
|
expect(redis.set).toHaveBeenCalledWith('bot:self_sentiment', '0.50')
|
||||||
|
expect(redis.set).toHaveBeenCalledWith(
|
||||||
|
'channel:channel1:User:history',
|
||||||
|
JSON.stringify([
|
||||||
|
{ role: 'user', content: '<@bot1> Hi!', images: [] },
|
||||||
|
{ role: 'assistant', content: 'U-um... hi... you talking to me?', images: [] },
|
||||||
|
])
|
||||||
|
)
|
||||||
expect(msgHist.size()).toBe(2) // User message + bot response
|
expect(msgHist.size()).toBe(2) // User message + bot response
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not respond to own message', async () => {
|
it('should not respond to own message', async () => {
|
||||||
const client = { user: { id: 'bot1', username: 'TestBot' } } as Client
|
const client = { user: { id: 'bot1', username: 'TestBot' } } as Client
|
||||||
const message = {
|
const message = {
|
||||||
|
id: 'msg5',
|
||||||
author: { id: 'bot1', bot: true, tag: 'TestBot#1234', username: 'TestBot' },
|
author: { id: 'bot1', bot: true, tag: 'TestBot#1234', username: 'TestBot' },
|
||||||
content: 'I said something!',
|
content: 'I said something!',
|
||||||
mentions: { has: () => false },
|
mentions: { has: () => false },
|
||||||
@@ -254,5 +324,92 @@ describe('Events Tests', () => {
|
|||||||
expect(redis.set).not.toHaveBeenCalled()
|
expect(redis.set).not.toHaveBeenCalled()
|
||||||
expect(msgHist.size()).toBe(0) // No messages added
|
expect(msgHist.size()).toBe(0) // No messages added
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should handle missing channel history in Redis', async () => {
|
||||||
|
const client = { user: { id: 'bot1', username: 'TestBot' } } as Client
|
||||||
|
const message = {
|
||||||
|
id: 'msg6',
|
||||||
|
author: { id: 'user1', bot: false, tag: 'User#1234', username: 'User' },
|
||||||
|
content: '<@bot1> Hi!',
|
||||||
|
mentions: { has: (id: string) => id === 'bot1' },
|
||||||
|
channelId: 'channel1',
|
||||||
|
channel: { name: 'test-channel' } as TextChannel,
|
||||||
|
reply: vi.fn().mockResolvedValue({ id: 'reply3' }),
|
||||||
|
attachments: { first: () => null },
|
||||||
|
guildId: 'guild1',
|
||||||
|
} as unknown as Message
|
||||||
|
const msgHist = new Queue<UserMessage>()
|
||||||
|
msgHist.capacity = 50
|
||||||
|
const defaultModel = 'aidoll-gemma3-12b-q6:latest'
|
||||||
|
|
||||||
|
// Mock fs for personality.json
|
||||||
|
vi.spyOn(fs, 'readFile').mockResolvedValue(
|
||||||
|
JSON.stringify({
|
||||||
|
character: 'You are Kuroki Tomoko, a shy and socially awkward high school girl from WataMote.',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mock utils functions
|
||||||
|
vi.mock('../src/utils/index.js', () => ({
|
||||||
|
clean: vi.fn(content => content),
|
||||||
|
getServerConfig: vi.fn((_, cb) => cb({ options: { 'toggle-chat': true } })),
|
||||||
|
getUserConfig: vi.fn((_, cb) =>
|
||||||
|
cb({
|
||||||
|
options: {
|
||||||
|
'message-style': false,
|
||||||
|
'switch-model': 'aidoll-gemma3-12b-q6:latest',
|
||||||
|
'modify-capacity': 50,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
),
|
||||||
|
openConfig: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock Redis: no history
|
||||||
|
vi.mocked(redis.get).mockImplementation(async (key: string) => {
|
||||||
|
if (key === 'user:user1:sentiment') return '0.50'
|
||||||
|
if (key === 'bot:self_sentiment') return '0.50'
|
||||||
|
if (key === 'channel:channel1:User:history') return null // No history
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock Ollama response
|
||||||
|
vi.mocked(ollama.chat).mockResolvedValue({
|
||||||
|
message: {
|
||||||
|
content: JSON.stringify({
|
||||||
|
status: 'success',
|
||||||
|
reply: 'U-um... hi... you talking to me?',
|
||||||
|
metadata: {
|
||||||
|
timestamp: '2025-05-21T14:00:00Z',
|
||||||
|
self_sentiment: 0.50,
|
||||||
|
user_sentiment: { 'user1': 0.50 },
|
||||||
|
redis_ops: [
|
||||||
|
{ action: 'set', key: 'user:user1:sentiment', value: 0.50 },
|
||||||
|
{ action: 'set', key: 'bot:self_sentiment', value: 0.50 },
|
||||||
|
],
|
||||||
|
need_help: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Execute messageCreate event
|
||||||
|
await messageCreateEvent.execute(
|
||||||
|
{ log: console.log, msgHist, ollama, client, defaultModel },
|
||||||
|
message
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(message.reply).toHaveBeenCalledWith('U-um... hi... you talking to me?')
|
||||||
|
expect(redis.set).toHaveBeenCalledWith('user:user1:sentiment', '0.50')
|
||||||
|
expect(redis.set).toHaveBeenCalledWith('bot:self_sentiment', '0.50')
|
||||||
|
expect(redis.set).toHaveBeenCalledWith(
|
||||||
|
'channel:channel1:User:history',
|
||||||
|
JSON.stringify([
|
||||||
|
{ role: 'user', content: '<@bot1> Hi!', images: [] },
|
||||||
|
{ role: 'assistant', content: 'U-um... hi... you talking to me?', images: [] },
|
||||||
|
])
|
||||||
|
)
|
||||||
|
expect(msgHist.size()).toBe(2) // User message + bot response
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user