Added a summarizaton system to help with long threads - the bot will summarize a long thread in a few sentences but keep the most recent posts as verbose, so it can keep track of what is going on longer before it starts acting weird and repeating itself
This commit is contained in:
18
src/main.ts
18
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)
|
||||
*/
|
||||
|
@ -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";
|
||||
|
121
src/util.ts
121
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<string> => {
|
||||
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<string> => {
|
||||
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,
|
||||
};
|
||||
|
Reference in New Issue
Block a user