feat: joel can read images now
This commit is contained in:
@@ -7,7 +7,7 @@ import type { BotClient } from "../../core/client";
|
||||
import { config } from "../../core/config";
|
||||
import { createLogger } from "../../core/logger";
|
||||
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 { eq } from "drizzle-orm";
|
||||
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 CONVERSATION_CONTEXT_MAX_MESSAGES = 5;
|
||||
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";
|
||||
|
||||
@@ -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> {
|
||||
try {
|
||||
const recent = await messageRepository.findRecentContext(
|
||||
message.guildId,
|
||||
message.channelId,
|
||||
{
|
||||
maxMessages: CONVERSATION_CONTEXT_MAX_MESSAGES,
|
||||
withinMinutes: CONVERSATION_CONTEXT_WINDOW_MINUTES,
|
||||
excludeMessageId: message.id,
|
||||
}
|
||||
);
|
||||
const fetched = await message.channel.messages.fetch({
|
||||
limit: CONVERSATION_CONTEXT_SCAN_LIMIT,
|
||||
});
|
||||
|
||||
const cutoffTimestamp = Date.now() - CONVERSATION_CONTEXT_WINDOW_MINUTES * 60 * 1000;
|
||||
const recent = Array.from(fetched.values())
|
||||
.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) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const contextLines = recent
|
||||
.map(({ message: recentMessage, userName }) => {
|
||||
const author = userName || recentMessage.user_id || "Unknown";
|
||||
const content = (recentMessage.content || "")
|
||||
.map((recentMessage) => {
|
||||
const author = recentMessage.member?.displayName || recentMessage.author.username || "Unknown";
|
||||
const content = (recentMessage.cleanContent || "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
.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) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `- ${author}: ${content}`;
|
||||
return `- ${author}: ${content}${linksSuffix}`;
|
||||
})
|
||||
.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 [
|
||||
const sections = [
|
||||
`Recent channel context (last ${CONVERSATION_CONTEXT_MAX_MESSAGES} messages within ${CONVERSATION_CONTEXT_WINDOW_MINUTES} minutes):`,
|
||||
...contextLines,
|
||||
].join("\n");
|
||||
];
|
||||
|
||||
if (mediaContext) {
|
||||
sections.push("", mediaContext);
|
||||
}
|
||||
|
||||
return sections.join("\n");
|
||||
} catch (error) {
|
||||
logger.error("Failed to build conversation context", error);
|
||||
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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user