| |
| |
| |
| |
| |
|
|
| import { createMemory } from "./store"; |
| import { MemoryType } from "./types"; |
|
|
| |
|
|
| |
| const PREFERENCE_PATTERNS: RegExp[] = [ |
| /\bI\s+(?:really\s+)?prefer\s+(.+?)(?:\.|,|$)/gi, |
| /\bI\s+(?:really\s+)?like\s+(.+?)(?:\.|,|$)/gi, |
| /\bmy\s+(?:favorite|favourite)\s+(?:is|are)\s+(.+?)(?:\.|,|$)/gi, |
| /\bI\s+(?:don'?t|do\s+not)\s+like\s+(.+?)(?:\.|,|$)/gi, |
| /\bI\s+(?:hate|dislike|avoid)\s+(.+?)(?:\.|,|$)/gi, |
| /\bI\s+enjoy\s+(.+?)(?:\.|,|$)/gi, |
| /\bI\s+love\s+(.+?)(?:\.|,|$)/gi, |
| ]; |
|
|
| |
| const DECISION_PATTERNS: RegExp[] = [ |
| /\bI'?(?:ll|will)\s+use\s+(.+?)(?:\.|,|$)/gi, |
| /\bI\s+chose\s+(.+?)(?:\.|,|$)/gi, |
| /\bI\s+(?:have\s+)?decided\s+(?:to\s+)?(.+?)(?:\.|,|$)/gi, |
| /\bI'?m\s+going\s+(?:to\s+)?(?:use|with|adopt)\s+(.+?)(?:\.|,|$)/gi, |
| /\bI\s+selected\s+(.+?)(?:\.|,|$)/gi, |
| /\bI\s+picked\s+(.+?)(?:\.|,|$)/gi, |
| /\bI\s+went\s+with\s+(.+?)(?:\.|,|$)/gi, |
| ]; |
|
|
| |
| const PATTERN_PATTERNS: RegExp[] = [ |
| /\bI\s+usually\s+(.+?)(?:\.|,|$)/gi, |
| /\bI\s+always\s+(.+?)(?:\.|,|$)/gi, |
| /\bI\s+never\s+(.+?)(?:\.|,|$)/gi, |
| /\bI\s+typically\s+(.+?)(?:\.|,|$)/gi, |
| /\bI\s+tend\s+to\s+(.+?)(?:\.|,|$)/gi, |
| /\bI\s+(?:often|frequently|regularly)\s+(.+?)(?:\.|,|$)/gi, |
| ]; |
|
|
| |
| const MAX_FACT_LENGTH = 500; |
| |
| const MIN_FACT_LENGTH = 3; |
|
|
| |
|
|
| export interface ExtractedFact { |
| key: string; |
| content: string; |
| type: MemoryType; |
| category: "preference" | "decision" | "pattern"; |
| } |
|
|
| |
|
|
| |
| |
| |
| function sanitizeMatch(raw: string): string { |
| return raw.trim().replace(/\s+/g, " ").slice(0, MAX_FACT_LENGTH); |
| } |
|
|
| |
| |
| |
| function factKey(category: string, content: string): string { |
| const slug = content |
| .toLowerCase() |
| .replace(/[^a-z0-9]+/g, "_") |
| .slice(0, 40) |
| .replace(/_+$/, ""); |
| return `${category}:${slug}`; |
| } |
|
|
| |
| |
| |
| |
| function runPatterns( |
| text: string, |
| patterns: RegExp[], |
| category: "preference" | "decision" | "pattern", |
| memoryType: MemoryType, |
| seen: Set<string> |
| ): ExtractedFact[] { |
| const facts: ExtractedFact[] = []; |
|
|
| for (const pattern of patterns) { |
| |
| pattern.lastIndex = 0; |
|
|
| let match: RegExpExecArray | null; |
| while ((match = pattern.exec(text)) !== null) { |
| const raw = match[1]; |
| if (!raw) continue; |
|
|
| const content = sanitizeMatch(raw); |
| if (content.length < MIN_FACT_LENGTH) continue; |
|
|
| const key = factKey(category, content); |
| if (seen.has(key)) continue; |
| seen.add(key); |
|
|
| facts.push({ key, content, type: memoryType, category }); |
| } |
|
|
| |
| pattern.lastIndex = 0; |
| } |
|
|
| return facts; |
| } |
|
|
| |
| |
| |
| |
| |
| export function extractFactsFromText(text: string): ExtractedFact[] { |
| if (!text || typeof text !== "string") return []; |
|
|
| const seen = new Set<string>(); |
| const facts: ExtractedFact[] = []; |
|
|
| |
| facts.push(...runPatterns(text, PREFERENCE_PATTERNS, "preference", MemoryType.FACTUAL, seen)); |
|
|
| |
| facts.push(...runPatterns(text, DECISION_PATTERNS, "decision", MemoryType.EPISODIC, seen)); |
|
|
| |
| facts.push(...runPatterns(text, PATTERN_PATTERNS, "pattern", MemoryType.FACTUAL, seen)); |
|
|
| return facts; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function extractFacts(response: string, apiKeyId: string, sessionId: string): void { |
| if (!response || !apiKeyId || !sessionId) return; |
|
|
| |
| setImmediate(() => { |
| const facts = extractFactsFromText(response); |
| if (facts.length === 0) return; |
|
|
| |
| for (const fact of facts) { |
| createMemory({ |
| apiKeyId, |
| sessionId, |
| type: fact.type, |
| key: fact.key, |
| content: fact.content, |
| metadata: { |
| category: fact.category, |
| extractedAt: new Date().toISOString(), |
| source: "llm_response", |
| }, |
| expiresAt: null, |
| }).catch((err) => { |
| |
| if (process.env.NODE_ENV !== "test") { |
| console.warn("[memory:extraction] Failed to store fact:", err?.message); |
| } |
| }); |
| } |
| }); |
| } |
|
|