Harden OpenPets memory capture and retrieval #1
3 changed files with 37 additions and 13 deletions
|
|
@ -4,7 +4,7 @@ import { dirname, join } from "node:path";
|
|||
|
||||
import { defaultOpenApiChatEndpoint, getAppStateSnapshot, normalizeOpenApiChatEndpoint } from "./app-state.js";
|
||||
import { applyExternalPetReaction, applyInternalPetMessage, showDefaultPet } from "./default-pet-controller.js";
|
||||
import { info, warn } from "./logger.js";
|
||||
import { error, info, warn } from "./logger.js";
|
||||
import type { OpenPetsReaction } from "./local-ipc-protocol.js";
|
||||
import { buildRelevantMemoryContext, capturePromptMemories } from "./openpets-memory.js";
|
||||
import { getMcpChatClientManager, type McpChatToolDefinition } from "./mcp-chat-client.js";
|
||||
|
|
@ -274,7 +274,14 @@ export async function sendOpenApiChatPrompt(prompt: string): Promise<OpenApiChat
|
|||
throw new Error(`Prompt is too long. Keep it under ${maxPromptChars} characters.`);
|
||||
}
|
||||
|
||||
capturePromptMemories(trimmedPrompt);
|
||||
try {
|
||||
const captured = capturePromptMemories(trimmedPrompt);
|
||||
if (captured.length > 0) {
|
||||
info("app", "memory captured", { component: "openapi-chat", count: captured.length, kinds: captured.map((e) => e.kind).join(",") });
|
||||
}
|
||||
} catch (err) {
|
||||
error("app", "memory capture failed", { component: "openapi-chat", error: err instanceof Error ? err.message : String(err) });
|
||||
}
|
||||
appendTranscriptEntry("user", trimmedPrompt);
|
||||
|
||||
if (isVanillaChatMcpEnabled()) {
|
||||
|
|
|
|||
|
|
@ -234,39 +234,54 @@ export interface ChatHistorySearchEntry {
|
|||
readonly createdAt: number;
|
||||
}
|
||||
|
||||
const recallIntentPattern = /(^|[^a-z0-9])(remember|recall|what do you know|who am i|what is my name|what's my name|tell me about me|about myself|myself|do you remember)($|[^a-z0-9])/i;
|
||||
|
||||
export function buildRelevantMemoryContext(prompt: string, chatHistory?: readonly ChatHistorySearchEntry[]): string | undefined {
|
||||
const memoryHits = searchOpenPetsMemories(prompt, maxRelevantMemoryItems);
|
||||
const chatHits = chatHistory && chatHistory.length > 0
|
||||
? searchChatHistory(prompt, chatHistory, maxRelevantMemoryItems)
|
||||
: [];
|
||||
|
||||
if (memoryHits.length === 0 && chatHits.length === 0) {
|
||||
const hasRecallIntent = recallIntentPattern.test(normalizeMemoryComparisonValue(prompt));
|
||||
const fallbackMemories = memoryHits.length === 0 || hasRecallIntent
|
||||
? listOpenPetsMemories(3).filter((entry) => !memoryHits.some((hit) => hit.entry.id === entry.id))
|
||||
: [];
|
||||
|
||||
if (memoryHits.length === 0 && chatHits.length === 0 && fallbackMemories.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const lines: string[] = [];
|
||||
let usedChars = 0;
|
||||
|
||||
// Include memory hits first
|
||||
for (const hit of memoryHits) {
|
||||
const tagSuffix = hit.entry.tags.length > 0 ? ` tags=${hit.entry.tags.join(",")}` : "";
|
||||
const line = `- [${hit.entry.kind} importance=${hit.entry.importance}${tagSuffix}] ${hit.entry.text}`;
|
||||
const addLine = (line: string): boolean => {
|
||||
if (usedChars > 0 && usedChars + line.length + 1 > maxRelevantMemoryChars) {
|
||||
break;
|
||||
return false;
|
||||
}
|
||||
lines.push(line);
|
||||
usedChars += line.length + 1;
|
||||
return true;
|
||||
};
|
||||
|
||||
// Include scored memory hits first
|
||||
for (const hit of memoryHits) {
|
||||
const tagSuffix = hit.entry.tags.length > 0 ? ` tags=${hit.entry.tags.join(",")}` : "";
|
||||
const line = `- [${hit.entry.kind} importance=${hit.entry.importance}${tagSuffix}] ${hit.entry.text}`;
|
||||
if (!addLine(line)) break;
|
||||
}
|
||||
|
||||
// Add recent memories as a fallback when the user is asking about themselves or no scored hits exist
|
||||
for (const entry of fallbackMemories) {
|
||||
const tagSuffix = entry.tags.length > 0 ? ` tags=${entry.tags.join(",")}` : "";
|
||||
const line = `- [${entry.kind} importance=${entry.importance}${tagSuffix}] ${entry.text}`;
|
||||
if (!addLine(line)) break;
|
||||
}
|
||||
|
||||
// Include relevant chat history excerpts
|
||||
for (const hit of chatHits) {
|
||||
const prefix = hit.entry.role === "user" ? "User said" : "Assistant said";
|
||||
const line = `- [chat history] ${prefix}: ${hit.entry.text}`;
|
||||
if (usedChars > 0 && usedChars + line.length + 1 > maxRelevantMemoryChars) {
|
||||
break;
|
||||
}
|
||||
lines.push(line);
|
||||
usedChars += line.length + 1;
|
||||
if (!addLine(line)) break;
|
||||
}
|
||||
|
||||
if (lines.length === 0) {
|
||||
|
|
|
|||
|
|
@ -1226,6 +1226,8 @@ function SettingsView({ onThemeModeChange }: { onThemeModeChange: (mode: ThemeMo
|
|||
|
||||
useEffect(() => { void loadSettings().catch((err) => setError(String(err?.message ?? err))); }, []);
|
||||
|
||||
useEffect(() => { void loadMemories().catch((err) => setError(String(err?.message ?? err))); }, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!message) return;
|
||||
const timeout = window.setTimeout(() => setMessage(""), 2200);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue