improve joels memory

This commit is contained in:
eric
2026-02-23 15:00:55 +01:00
parent 283802ae55
commit a4650c14ae
2 changed files with 112 additions and 2 deletions

View File

@@ -2,7 +2,7 @@
* Message repository - handles all message-related database operations * Message repository - handles all message-related database operations
*/ */
import { and, eq } from "drizzle-orm"; import { and, desc, eq } from "drizzle-orm";
import { db } from "../connection"; import { db } from "../connection";
import { messages, users, type InsertMessage, type Message } from "../schema"; import { messages, users, type InsertMessage, type Message } from "../schema";
@@ -28,4 +28,57 @@ export const messageRepository = {
userName: r.users?.name ?? null, userName: r.users?.name ?? null,
})); }));
}, },
async findRecentContext(
guildId: string,
channelId: string,
options?: {
maxMessages?: number;
withinMinutes?: number;
excludeMessageId?: string;
}
): Promise<Array<{ message: Message; userName: string | null }>> {
const maxMessages = options?.maxMessages ?? 5;
const withinMinutes = options?.withinMinutes ?? 20;
const scanLimit = Math.max(20, maxMessages * 5);
const results = await db
.select()
.from(messages)
.where(
and(eq(messages.guild_id, guildId), eq(messages.channel_id, channelId))
)
.leftJoin(users, eq(users.id, messages.user_id))
.orderBy(desc(messages.timestamp))
.limit(scanLimit);
const now = Date.now();
const cutoff = now - withinMinutes * 60 * 1000;
const filtered = results
.filter((r) => {
if (options?.excludeMessageId && r.messages.id === options.excludeMessageId) {
return false;
}
const timestamp = r.messages.timestamp;
if (!timestamp) {
return true;
}
const parsedTs = Date.parse(timestamp);
if (Number.isNaN(parsedTs)) {
return true;
}
return parsedTs >= cutoff;
})
.slice(0, maxMessages)
.reverse();
return filtered.map((r) => ({
message: r.messages,
userName: r.users?.name ?? null,
}));
},
}; };

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 } from "../../database"; import { db, messageRepository } 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";
@@ -20,6 +20,8 @@ const logger = createLogger("Features:Joel");
const DIRECTED_CLASSIFICATION_LIMIT_PER_HOUR = 20; const DIRECTED_CLASSIFICATION_LIMIT_PER_HOUR = 20;
const DIRECTED_CLASSIFICATION_WINDOW_MS = 60 * 60 * 1000; 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_WINDOW_MINUTES = 20;
type ResponseTrigger = "free-will" | "summoned" | "classifier" | "none"; type ResponseTrigger = "free-will" | "summoned" | "classifier" | "none";
@@ -348,6 +350,12 @@ The image URL will appear in your response for the user to see.`;
} }
} }
// Add recent conversation context so Joel can follow ongoing chat
const conversationContext = await this.buildConversationContext(message);
if (conversationContext) {
prompt = `${conversationContext}\n\nCurrent message:\n${prompt}`;
}
// Analyze attachments if present (images, etc.) // Analyze attachments if present (images, etc.)
const attachmentDescriptions = await this.analyzeAttachments(message); const attachmentDescriptions = await this.analyzeAttachments(message);
if (attachmentDescriptions) { if (attachmentDescriptions) {
@@ -364,6 +372,55 @@ The image URL will appear in your response for the user to see.`;
return response.text || null; return response.text || null;
}, },
/**
* Build a compact conversation context from recent messages in this channel.
*/
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,
}
);
if (recent.length === 0) {
return null;
}
const contextLines = recent
.map(({ message: recentMessage, userName }) => {
const author = userName || recentMessage.user_id || "Unknown";
const content = (recentMessage.content || "")
.replace(/\s+/g, " ")
.trim()
.slice(0, 220);
if (!content) {
return null;
}
return `- ${author}: ${content}`;
})
.filter((line): line is string => Boolean(line));
if (contextLines.length === 0) {
return null;
}
return [
`Recent channel context (last ${CONVERSATION_CONTEXT_MAX_MESSAGES} messages within ${CONVERSATION_CONTEXT_WINDOW_MINUTES} minutes):`,
...contextLines,
].join("\n");
} catch (error) {
logger.error("Failed to build conversation context", error);
return null;
}
},
/** /**
* Extract and analyze attachments from a message using vision AI * Extract and analyze attachments from a message using vision AI
*/ */