diff --git a/src/main.ts b/src/main.ts index 941fdc2..3c1281f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -25,6 +25,7 @@ import { selectRandomEmojis, isLLMRefusal, shouldContinue, + processConversationHistory, } from "./util.js"; import { analyzeInteraction, @@ -94,6 +95,8 @@ const generateOllamaRequest = async ( const userMessage = notification.status.pleroma.content["text/plain"]; let conversationHistory: PostAncestorsForModel[] = []; + let processedContext = ""; + if (replyWithContext) { const contextPosts = await getStatusContext(notification.status.id); if (!contextPosts?.ancestors || !contextPosts) { @@ -107,6 +110,9 @@ const generateOllamaRequest = async ( plaintext_content: ancestor.pleroma.content["text/plain"], }; }); + + // Process context - summarize if too long + processedContext = await processConversationHistory(conversationHistory); } const formattedUserMessage = `${userFqn} says: ${userMessage}`; @@ -125,16 +131,7 @@ const generateOllamaRequest = async ( let systemContent = ollamaSystemPrompt + memoryContext + availableEmojis; if (replyWithContext) { - systemContent = `${ollamaSystemPrompt}${memoryContext}\n\nPrevious conversation context:\n${conversationHistory - .map( - (post) => - `${post.account_fqn} (to ${post.mentions.join(", ")}): ${ - post.plaintext_content - }` - ) - .join( - "\n" - )}\nReply as if you are a party to the conversation. If '@nice-ai' is mentioned, respond directly. Prefix usernames with '@' when addressing them.${availableEmojis}`; + systemContent = `${ollamaSystemPrompt}${memoryContext}\n\nPrevious conversation context:\n${processedContext}\nReply as if you are a party to the conversation. If '@nice-ai' is mentioned, respond directly. Prefix usernames with '@' when addressing them.${availableEmojis}`; } // Use different seeds for retry attempts @@ -176,7 +173,6 @@ const generateOllamaRequest = async ( } }; - /** * Analyze interaction and update user memory (runs asynchronously) */ diff --git a/src/memory.ts b/src/memory.ts index 86453c6..163bed0 100644 --- a/src/memory.ts +++ b/src/memory.ts @@ -1,3 +1,42 @@ +/** + * ADAPTIVE MEMORY SYSTEM FOR FEDIVERSE CHATBOT + * + * This system maintains persistent, evolving user profiles to enable personalized + * interactions across chat sessions. It uses LLM-based analysis to extract and + * categorize user traits, then builds context for future conversations. + * + * ARCHITECTURE: + * - UserMemory: Core profile (personality, gags, relationships, interests, backstory) + * - InteractionLog: Historical conversation snapshots with sentiment analysis + * - JSON string arrays in SQLite for flexible data storage + * + * WORKFLOW: + * 1. Each user message + bot response gets analyzed by Ollama + * 2. Extract personality traits, running gags, relationship dynamics, etc. + * 3. Merge new insights with existing profile (deduplication) + * 4. Generate memory context string for next conversation's system prompt + * 5. Log interaction with sentiment and notable quotes + * + * MEMORY CATEGORIES: + * - personalityTraits: User characteristics (sarcastic, protective, etc.) + * - runningGags: Recurring jokes, memes, fake claims between user and bot + * - relationships: How user treats bot (mean, protective, flirty) + * - interests: Hobbies, topics user cares about + * - backstory: Biographical info, "lore" (real or fabricated) + * + * CURRENT LIMITATIONS: + * - No memory aging/decay - old info persists indefinitely + * - Simple deduplication - similar but not identical entries accumulate + * - No relevance scoring - stale assumptions carry same weight as recent ones + * - Fixed array limits may truncate important long-term patterns + * + * RECOMMENDED IMPROVEMENTS: + * - Add timestamp-based relevance weighting + * - Implement semantic similarity checks for better deduplication + * - Add contradiction detection to update outdated assumptions + * - Consider LRU-style eviction instead of simple truncation + */ + // Updated memory.ts with JSON string handling for SQLite import { prisma } from "./main.js"; import { envConfig } from "./main.js"; diff --git a/src/util.ts b/src/util.ts index f82874b..cd8db69 100644 --- a/src/util.ts +++ b/src/util.ts @@ -2,6 +2,8 @@ import striptags from "striptags"; import { prisma } from "./main.js"; import { envConfig } from "./main.js"; import { Notification } from "../types.js"; +import { OllamaChatRequest, OllamaChatResponse, PostAncestorsForModel } from "../types.js"; + const trimInputData = (input: string): string => { const strippedInput = striptags(input); @@ -134,6 +136,122 @@ const isLLMRefusal = (response: string): boolean => { return refusalPatterns.some(pattern => pattern.test(normalizedResponse)); }; +/** + * Summarize a long conversation thread to reduce context length + */ +const summarizeConversationHistory = async ( + conversationHistory: PostAncestorsForModel[] +): Promise => { + const { ollamaUrl, ollamaModel } = envConfig; + + if (conversationHistory.length === 0) return ""; + + // Create a concise thread representation + const threadText = conversationHistory + .map(post => `${post.account_fqn}: ${post.plaintext_content}`) + .join('\n'); + + const summarizePrompt = `Summarize this conversation thread in 2-3 sentences, focusing on the main topics discussed and the overall tone/mood. Keep it brief but capture the essential context: + +${threadText} + +Summary:`; + + try { + const summarizeRequest: OllamaChatRequest = { + model: ollamaModel, + messages: [ + { + role: "system", + content: "You are excellent at creating concise, informative summaries. Keep summaries under 150 words and focus on key topics and relationships between participants." + }, + { role: "user", content: summarizePrompt } + ], + stream: false, + options: { + temperature: 0.2, // Low temperature for consistent summaries + num_predict: 200, + num_ctx: 4096, // Smaller context for summarization + } + }; + + const response = await fetch(`${ollamaUrl}/api/chat`, { + method: "POST", + body: JSON.stringify(summarizeRequest), + }); + + if (!response.ok) { + console.error(`Summary request failed: ${response.statusText}`); + return `Previous conversation with ${conversationHistory.length} messages about various topics.`; + } + + const summaryResponse: OllamaChatResponse = await response.json(); + return summaryResponse.message.content.trim(); + + } catch (error: any) { + console.error(`Error summarizing conversation: ${error.message}`); + return `Previous conversation with ${conversationHistory.length} messages.`; + } +}; + +/** + * Decide whether to summarize based on thread length and complexity + */ +const shouldSummarizeThread = (conversationHistory: PostAncestorsForModel[]): boolean => { + const SUMMARY_THRESHOLD = 15; + + if (conversationHistory.length < SUMMARY_THRESHOLD) return false; + + // Additional heuristics could be added here: + // - Total character count + // - Average message length + // - Time span of conversation + + return true; +}; + +/** + * Process conversation history - either use full context or summarized version + */ +const processConversationHistory = async ( + conversationHistory: PostAncestorsForModel[] +): Promise => { + if (!shouldSummarizeThread(conversationHistory)) { + // Use full context for short threads + return conversationHistory + .map(post => + `${post.account_fqn} (to ${post.mentions.join(", ")}): ${post.plaintext_content}` + ) + .join('\n'); + } + + // Keep the last few messages in full detail + summary of earlier messages + const KEEP_RECENT_COUNT = 5; + const recentMessages = conversationHistory.slice(-KEEP_RECENT_COUNT); + const olderMessages = conversationHistory.slice(0, -KEEP_RECENT_COUNT); + + let contextString = ""; + + if (olderMessages.length > 0) { + const summary = await summarizeConversationHistory(olderMessages); + contextString += `Earlier conversation summary: ${summary}\n\n`; + } + + if (recentMessages.length > 0) { + contextString += "Recent messages:\n"; + contextString += recentMessages + .map(post => + `${post.account_fqn} (to ${post.mentions.join(", ")}): ${post.plaintext_content}` + ) + .join('\n'); + } + + return contextString; +}; + + + + export { alreadyRespondedTo, selectRandomEmoji, @@ -143,4 +261,7 @@ export { recordPendingResponse, isFromWhitelistedDomain, shouldContinue, + summarizeConversationHistory, + shouldSummarizeThread, + processConversationHistory, };