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 { 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
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user