feat: joel can read images now

This commit is contained in:
eric
2026-02-23 17:01:31 +01:00
parent e26d665bdf
commit 724666ea36

View File

@@ -7,7 +7,7 @@ import type { BotClient } from "../../core/client";
import { config } from "../../core/config"; import { config } from "../../core/config";
import { createLogger } from "../../core/logger"; import { createLogger } from "../../core/logger";
import { getAiService, getVisionService, type MessageStyle, type ToolContext, type Attachment } from "../../services/ai"; import { getAiService, getVisionService, type MessageStyle, type ToolContext, type Attachment } from "../../services/ai";
import { db, messageRepository } from "../../database"; import { db } from "../../database";
import { personalities, botOptions } from "../../database/schema"; import { personalities, botOptions } from "../../database/schema";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { buildStyledPrompt, STYLE_MODIFIERS } from "./personalities"; import { buildStyledPrompt, STYLE_MODIFIERS } from "./personalities";
@@ -22,6 +22,11 @@ const DIRECTED_CLASSIFICATION_WINDOW_MS = 60 * 60 * 1000;
const directedClassificationBudget = new Map<string, { windowStart: number; count: number }>(); const directedClassificationBudget = new Map<string, { windowStart: number; count: number }>();
const CONVERSATION_CONTEXT_MAX_MESSAGES = 5; const CONVERSATION_CONTEXT_MAX_MESSAGES = 5;
const CONVERSATION_CONTEXT_WINDOW_MINUTES = 20; const CONVERSATION_CONTEXT_WINDOW_MINUTES = 20;
const CONVERSATION_CONTEXT_SCAN_LIMIT = Math.max(20, CONVERSATION_CONTEXT_MAX_MESSAGES * 5);
const CONVERSATION_CONTEXT_MAX_LINKS_PER_MESSAGE = 2;
const CONVERSATION_CONTEXT_MAX_MEDIA_MESSAGES = 2;
const CONVERSATION_CONTEXT_MAX_MEDIA_ATTACHMENTS = 3;
const URL_REGEX = /https?:\/\/[^\s<>()]+/gi;
type ResponseTrigger = "free-will" | "summoned" | "classifier" | "none"; type ResponseTrigger = "free-will" | "summoned" | "classifier" | "none";
@@ -377,50 +382,133 @@ The image URL will appear in your response for the user to see.`;
*/ */
async buildConversationContext(message: Message<true>): Promise<string | null> { async buildConversationContext(message: Message<true>): Promise<string | null> {
try { try {
const recent = await messageRepository.findRecentContext( const fetched = await message.channel.messages.fetch({
message.guildId, limit: CONVERSATION_CONTEXT_SCAN_LIMIT,
message.channelId, });
{
maxMessages: CONVERSATION_CONTEXT_MAX_MESSAGES, const cutoffTimestamp = Date.now() - CONVERSATION_CONTEXT_WINDOW_MINUTES * 60 * 1000;
withinMinutes: CONVERSATION_CONTEXT_WINDOW_MINUTES, const recent = Array.from(fetched.values())
excludeMessageId: message.id, .filter((recentMessage) => recentMessage.id !== message.id)
} .filter((recentMessage) => recentMessage.createdTimestamp >= cutoffTimestamp)
); .sort((a, b) => a.createdTimestamp - b.createdTimestamp)
.slice(-CONVERSATION_CONTEXT_MAX_MESSAGES);
if (recent.length === 0) { if (recent.length === 0) {
return null; return null;
} }
const contextLines = recent const contextLines = recent
.map(({ message: recentMessage, userName }) => { .map((recentMessage) => {
const author = userName || recentMessage.user_id || "Unknown"; const author = recentMessage.member?.displayName || recentMessage.author.username || "Unknown";
const content = (recentMessage.content || "") const content = (recentMessage.cleanContent || "")
.replace(/\s+/g, " ") .replace(/\s+/g, " ")
.trim() .trim()
.slice(0, 220); .slice(0, 220);
const links = this.extractLinks(recentMessage.content);
const linkPreview = links.slice(0, CONVERSATION_CONTEXT_MAX_LINKS_PER_MESSAGE).join(", ");
const linksSuffix = links.length > 0
? ` [links: ${linkPreview}${links.length > CONVERSATION_CONTEXT_MAX_LINKS_PER_MESSAGE ? ", ..." : ""}]`
: "";
if (!content && links.length > 0) {
return `- ${author}: [shared links] ${linkPreview}${links.length > CONVERSATION_CONTEXT_MAX_LINKS_PER_MESSAGE ? ", ..." : ""}`;
}
if (!content && recentMessage.attachments.size > 0) {
const attachmentWord = recentMessage.attachments.size === 1 ? "attachment" : "attachments";
return `- ${author}: [shared ${recentMessage.attachments.size} ${attachmentWord}]`;
}
if (!content) { if (!content) {
return null; return null;
} }
return `- ${author}: ${content}`; return `- ${author}: ${content}${linksSuffix}`;
}) })
.filter((line): line is string => Boolean(line)); .filter((line): line is string => Boolean(line));
if (contextLines.length === 0) { const mediaContext = await this.buildRecentMediaContext(recent);
if (contextLines.length === 0 && !mediaContext) {
return null; return null;
} }
return [ const sections = [
`Recent channel context (last ${CONVERSATION_CONTEXT_MAX_MESSAGES} messages within ${CONVERSATION_CONTEXT_WINDOW_MINUTES} minutes):`, `Recent channel context (last ${CONVERSATION_CONTEXT_MAX_MESSAGES} messages within ${CONVERSATION_CONTEXT_WINDOW_MINUTES} minutes):`,
...contextLines, ...contextLines,
].join("\n"); ];
if (mediaContext) {
sections.push("", mediaContext);
}
return sections.join("\n");
} catch (error) { } catch (error) {
logger.error("Failed to build conversation context", error); logger.error("Failed to build conversation context", error);
return null; return null;
} }
}, },
extractLinks(content: string): string[] {
const matches = content.match(URL_REGEX) ?? [];
const cleaned = matches.map((url) => url.replace(/[),.!?]+$/, ""));
return Array.from(new Set(cleaned));
},
async buildRecentMediaContext(recentMessages: Message[]): Promise<string | null> {
const mediaMessages = recentMessages
.filter((recentMessage) => recentMessage.attachments.size > 0)
.slice(-CONVERSATION_CONTEXT_MAX_MEDIA_MESSAGES);
if (mediaMessages.length === 0) {
return null;
}
const vision = getVisionService();
const mediaLines: string[] = [];
let remainingAttachments = CONVERSATION_CONTEXT_MAX_MEDIA_ATTACHMENTS;
for (const mediaMessage of mediaMessages) {
if (remainingAttachments <= 0) {
break;
}
const attachments: Attachment[] = mediaMessage.attachments
.map((att) => ({
url: att.url,
name: att.name,
contentType: att.contentType,
size: att.size,
}))
.slice(0, remainingAttachments);
if (attachments.length === 0) {
continue;
}
const analyses = await vision.analyzeAttachments(attachments, mediaMessage.cleanContent);
if (analyses.length === 0) {
continue;
}
remainingAttachments -= analyses.length;
const author = mediaMessage.member?.displayName || mediaMessage.author.username || "Unknown";
const attachmentSummaries = analyses
.map((analysis) => `${analysis.attachmentName}: ${analysis.description}`)
.join(" | ");
mediaLines.push(`- ${author}: ${attachmentSummaries}`);
}
if (mediaLines.length === 0) {
return null;
}
return ["Recent shared media:", ...mediaLines].join("\n");
},
/** /**
* Extract and analyze attachments from a message using vision AI * Extract and analyze attachments from a message using vision AI
*/ */