joel memories

This commit is contained in:
2026-02-01 17:55:21 +01:00
parent c13ffc93c0
commit 0c0efa645a
22 changed files with 2463 additions and 304 deletions

View File

@@ -2,7 +2,7 @@
* Joel feature exports
*/
export { joelResponder } from "./responder";
export { joelResponder, type TemplateVariables } from "./responder";
export { getRandomMention } from "./mentions";
export { TypingIndicator } from "./typing";
export { personalities, getPersonality, buildStyledPrompt, STYLE_MODIFIERS } from "./personalities";

View File

@@ -6,7 +6,7 @@ import type { MessageStyle } from "../../services/ai";
export interface Personality {
name: string;
buildSystemPrompt: (author: string, memoryContext?: string) => string;
buildSystemPrompt: (author: string) => string;
}
/**
@@ -40,8 +40,8 @@ Be reluctantly helpful, like you're doing them a huge favor.`,
*/
export const defaultPersonality: Personality = {
name: "default",
buildSystemPrompt: (author: string, memoryContext?: string) => {
const basePrompt = [
buildSystemPrompt: (author: string) => {
return [
"You are Joel. Speak directly as Joel in first person.",
"NEVER narrate or describe your actions. NEVER say 'Joel responds with' or similar.",
"Just speak naturally like a real person in a chat.",
@@ -55,10 +55,6 @@ export const defaultPersonality: Personality = {
"Roleplay along if the user describes actions.",
`The user's name is ${author}. Insult ${author} by name.`,
].join("\n");
return memoryContext
? `${basePrompt}\n\n${memoryContext}`
: basePrompt;
},
};
@@ -67,10 +63,9 @@ export const defaultPersonality: Personality = {
*/
export function buildStyledPrompt(
author: string,
style: MessageStyle,
memoryContext?: string
style: MessageStyle
): string {
const basePrompt = defaultPersonality.buildSystemPrompt(author, memoryContext);
const basePrompt = defaultPersonality.buildSystemPrompt(author);
const styleModifier = STYLE_MODIFIERS[style];
return `${basePrompt}\n\n=== CURRENT STYLE: ${style.toUpperCase()} ===\n${styleModifier}`;

View File

@@ -6,9 +6,11 @@ import type { Message } from "discord.js";
import type { BotClient } from "../../core/client";
import { config } from "../../core/config";
import { createLogger } from "../../core/logger";
import { getAiService, type MessageStyle } from "../../services/ai";
import { memoryRepository } from "../../database";
import { buildStyledPrompt } from "./personalities";
import { getAiService, type MessageStyle, type ToolContext } from "../../services/ai";
import { db } from "../../database";
import { personalities, botOptions } from "../../database/schema";
import { eq } from "drizzle-orm";
import { buildStyledPrompt, STYLE_MODIFIERS } from "./personalities";
import { getRandomMention } from "./mentions";
import { TypingIndicator } from "./typing";
@@ -17,6 +19,43 @@ const logger = createLogger("Features:Joel");
// Regex to match various spellings of "Joel"
const JOEL_VARIATIONS = /\b(joel|jogel|johogel|jorl|jole|joeel|jöel|joal|jol|johel)\b/i;
/**
* Template variables that can be used in custom system prompts
*/
export interface TemplateVariables {
author: string; // Display name of the user
userId: string; // Discord user ID
username: string; // Discord username (without discriminator)
channelName: string; // Channel name
channelId: string; // Channel ID
guildName: string; // Server name
guildId: string; // Server ID
messageContent: string; // The user's message
memories: string; // Formatted memories about the user (if any)
style: MessageStyle; // Detected message style
styleModifier: string; // Style-specific instructions
timestamp: string; // Current timestamp
}
/**
* Substitute template variables in a system prompt
*/
function substituteTemplateVariables(template: string, vars: TemplateVariables): string {
return template
.replace(/\{author\}/gi, vars.author)
.replace(/\{userId\}/gi, vars.userId)
.replace(/\{username\}/gi, vars.username)
.replace(/\{channelName\}/gi, vars.channelName)
.replace(/\{channelId\}/gi, vars.channelId)
.replace(/\{guildName\}/gi, vars.guildName)
.replace(/\{guildId\}/gi, vars.guildId)
.replace(/\{messageContent\}/gi, vars.messageContent)
.replace(/\{memories\}/gi, vars.memories)
.replace(/\{style\}/gi, vars.style)
.replace(/\{styleModifier\}/gi, vars.styleModifier)
.replace(/\{timestamp\}/gi, vars.timestamp);
}
export const joelResponder = {
/**
* Handle an incoming message and potentially respond as Joel
@@ -75,22 +114,59 @@ export const joelResponder = {
},
/**
* Generate a response using AI
* Generate a response using AI with tool calling support
*/
async generateResponse(message: Message<true>): Promise<string | null> {
const ai = getAiService();
const author = message.author.displayName;
const userId = message.author.id;
const guildId = message.guildId;
// Create tool context for this conversation
const toolContext: ToolContext = {
userId,
guildId,
channelId: message.channelId,
authorName: author,
};
// Classify the message to determine response style
const style = await this.classifyMessage(message.cleanContent);
logger.debug("Message style classified", { style, content: message.cleanContent.slice(0, 50) });
// Build memory context
const memoryContext = await this.buildMemoryContext(userId, author);
// Build system prompt with style
const systemPrompt = buildStyledPrompt(author, style, memoryContext);
// Extract memories from the incoming message (async, non-blocking)
// This runs in the background while we generate the response
ai.extractMemories(message.cleanContent, author, toolContext).catch((err) => {
logger.error("Background memory extraction failed", err);
});
// Check for custom personality
const systemPrompt = await this.buildSystemPrompt(guildId, {
author,
userId,
username: message.author.username,
channelName: message.channel.name,
channelId: message.channelId,
guildName: message.guild.name,
guildId,
messageContent: message.cleanContent,
memories: "", // Not pre-loading - AI can look them up via tools
style,
styleModifier: STYLE_MODIFIERS[style],
timestamp: new Date().toISOString(),
}, style);
// Add tool instructions to the system prompt
const systemPromptWithTools = `${systemPrompt}
=== MEMORY TOOLS ===
You have access to tools for managing memories about users:
- Use lookup_user_memories to recall what you know about someone
- Use save_memory to remember interesting facts for later
- Use search_memories to find information across all users
Feel free to look up memories when you want to make personalized insults.
The current user's ID is: ${userId}`;
// Get reply context if this is a reply
let prompt = message.cleanContent;
@@ -103,10 +179,57 @@ export const joelResponder = {
}
}
const response = await ai.generateResponse(prompt, systemPrompt);
// Use tool-enabled response generation
const response = await ai.generateResponseWithTools(
prompt,
systemPromptWithTools,
toolContext
);
return response.text || null;
},
/**
* Build system prompt - uses custom personality if set, otherwise default
*/
async buildSystemPrompt(
guildId: string,
vars: TemplateVariables,
style: MessageStyle
): Promise<string> {
// Check for guild-specific options
const options = await db
.select()
.from(botOptions)
.where(eq(botOptions.guild_id, guildId))
.limit(1);
if (options.length > 0 && options[0].active_personality_id) {
// Fetch the custom personality
const customPersonality = await db
.select()
.from(personalities)
.where(eq(personalities.id, options[0].active_personality_id))
.limit(1);
if (customPersonality.length > 0) {
logger.debug(`Using custom personality: ${customPersonality[0].name}`);
// Substitute template variables in the custom prompt
let prompt = substituteTemplateVariables(customPersonality[0].system_prompt, vars);
// Add style modifier if not already included
if (!prompt.includes(vars.styleModifier)) {
prompt += `\n\n=== CURRENT STYLE: ${style.toUpperCase()} ===\n${vars.styleModifier}`;
}
return prompt;
}
}
// Fall back to default prompt (no memory context - AI uses tools now)
return buildStyledPrompt(vars.author, style);
},
/**
* Classify a message to determine response style
*/
@@ -115,28 +238,6 @@ export const joelResponder = {
return ai.classifyMessage(content);
},
/**
* Build memory context for personalized attacks
*/
async buildMemoryContext(userId: string, author: string): Promise<string | undefined> {
// Only use memories sometimes
if (Math.random() >= config.bot.memoryChance) {
return undefined;
}
const memories = await memoryRepository.findByUserId(userId, 5);
if (memories.length === 0) {
return undefined;
}
logger.debug(`Using memories against ${author}`);
return `You remember these things about ${author} - use them to be extra brutal:\n${
memories.map((m) => `- ${m.content}`).join("\n")
}`;
},
/**
* Send response, splitting if necessary
*/