improve joels memory
This commit is contained in:
@@ -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,
|
||||||
|
}));
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user