joel momoent

This commit is contained in:
2026-02-01 19:28:30 +01:00
parent 032d25c9af
commit 09143a0638
8 changed files with 565 additions and 2 deletions

View File

@@ -6,7 +6,7 @@ import type { Message } from "discord.js";
import type { BotClient } from "../../core/client";
import { config } from "../../core/config";
import { createLogger } from "../../core/logger";
import { getAiService, type MessageStyle, type ToolContext } from "../../services/ai";
import { getAiService, getVisionService, type MessageStyle, type ToolContext, type Attachment } from "../../services/ai";
import { db } from "../../database";
import { personalities, botOptions } from "../../database/schema";
import { eq } from "drizzle-orm";
@@ -65,18 +65,51 @@ export const joelResponder = {
if (!shouldRespond) return;
// Check channel restriction
const channelCheck = await this.checkChannelRestriction(message);
if (!channelCheck.allowed) {
if (channelCheck.rebellionResponse) {
// Joel is breaking the rules - he'll respond anyway but acknowledge it
logger.debug("Joel exercises free will despite channel restriction", {
channel: message.channelId,
restricted: channelCheck.restrictedChannelId,
});
} else {
// Joel respects the restriction this time
logger.debug("Joel blocked by channel restriction", {
channel: message.channelId,
restricted: channelCheck.restrictedChannelId,
});
return;
}
}
const typing = new TypingIndicator(message.channel);
try {
typing.start();
const response = await this.generateResponse(message);
let response = await this.generateResponse(message);
if (!response) {
await message.reply("\\*Ignorerar dig\\*");
return;
}
// If Joel is rebelling against channel restriction, add a prefix
if (channelCheck.rebellionResponse) {
const rebellionPrefixes = [
"*sneaks in from the shadows*\n\n",
"*appears despite being told to stay in his channel*\n\n",
"You think you can contain me? Anyway,\n\n",
"*breaks the rules because fuck you*\n\n",
"I'm not supposed to be here but I don't care.\n\n",
"*escapes from his designated channel*\n\n",
];
const prefix = rebellionPrefixes[Math.floor(Math.random() * rebellionPrefixes.length)];
response = prefix + response;
}
// Occasionally add a random mention
const mention = await getRandomMention(message);
const fullResponse = response + mention;
@@ -90,6 +123,53 @@ export const joelResponder = {
}
},
/**
* Check if Joel is allowed to respond in this channel
* Returns whether he's allowed, and if not, whether he's rebelling anyway
*/
async checkChannelRestriction(message: Message<true>): Promise<{
allowed: boolean;
rebellionResponse: boolean;
restrictedChannelId?: string;
}> {
const guildOptions = await db
.select()
.from(botOptions)
.where(eq(botOptions.guild_id, message.guildId))
.limit(1);
const restrictedChannelId = guildOptions[0]?.restricted_channel_id;
// No restriction set - Joel can respond anywhere
if (!restrictedChannelId) {
return { allowed: true, rebellionResponse: false };
}
// Joel is in the allowed channel
if (message.channelId === restrictedChannelId) {
return { allowed: true, rebellionResponse: false };
}
// Joel is NOT in the allowed channel - but maybe he rebels?
// 5% chance to respond anyway (free will override)
const rebellionChance = 0.05;
const isRebelling = Math.random() < rebellionChance;
if (isRebelling) {
return {
allowed: false,
rebellionResponse: true,
restrictedChannelId
};
}
return {
allowed: false,
rebellionResponse: false,
restrictedChannelId
};
},
/**
* Determine if Joel should respond to a message
*/
@@ -202,6 +282,12 @@ The GIF URL will appear in your response for the user to see.`;
}
}
// Analyze attachments if present (images, etc.)
const attachmentDescriptions = await this.analyzeAttachments(message);
if (attachmentDescriptions) {
prompt += attachmentDescriptions;
}
// Use tool-enabled response generation
const response = await ai.generateResponseWithTools(
prompt,
@@ -212,6 +298,46 @@ The GIF URL will appear in your response for the user to see.`;
return response.text || null;
},
/**
* Extract and analyze attachments from a message using vision AI
*/
async analyzeAttachments(message: Message<true>): Promise<string | null> {
// Check if message has attachments
if (message.attachments.size === 0) {
return null;
}
// Convert Discord attachments to our format
const attachments: Attachment[] = message.attachments.map(att => ({
url: att.url,
name: att.name,
contentType: att.contentType,
size: att.size,
}));
logger.debug("Message has attachments", {
count: attachments.length,
types: attachments.map(a => a.contentType)
});
try {
const vision = getVisionService();
const analyses = await vision.analyzeAttachments(
attachments,
message.cleanContent // Provide message context for better analysis
);
if (analyses.length === 0) {
return null;
}
return vision.formatForPrompt(analyses);
} catch (error) {
logger.error("Failed to analyze attachments", error);
return null;
}
},
/**
* Build system prompt - uses custom personality if set, otherwise default
*/