diff --git a/supabase/functions/llm-pipeline/index.ts b/supabase/functions/llm-pipeline/index.ts index ce3fbbd..9ecf0c3 100644 --- a/supabase/functions/llm-pipeline/index.ts +++ b/supabase/functions/llm-pipeline/index.ts @@ -30,18 +30,14 @@ const corsHeaders = { /* Stage 2: Process Input (Extract Memories) */ -async function extractMemories(controller, messages, doc, relevantMemories?) { +async function extractMemories(controller, messages, doc, user: User, allTags, relevantMemories?) { + const startTime = Date.now(); + let addedCount = 0; + let updatedCount = 0; + let deletedCount = 0; + const extractedMemories = []; - // Fetch existing memory tags from the database, that belong to the user - const user : User = (await supabaseClient.auth.getUser()).data.user; - - const tags = await supabaseClient - .schema("mori") - .from("tags") - .select("*") - .eq("user_id", user.id); - - console.log("Fetched existing tags for user:", tags.data?.length || 0); + console.log("Using cached tags for user:", allTags?.length || 0); // Create and call OpenAI to process the input messages console.log("Creating OpenAI client for processing input"); @@ -53,7 +49,7 @@ async function extractMemories(controller, messages, doc, relevantMemories?) { console.log("Calling OpenAI API for processing..."); const response = await openai.chat.completions.create({ - model: 'gpt-4.1-mini', + model: 'gpt-4.1', temperature: 0.1, max_completion_tokens: 20000, messages: [ @@ -63,7 +59,7 @@ async function extractMemories(controller, messages, doc, relevantMemories?) { role: "assistant", content: `I have access to the following reference data: -Available tags: ${JSON.stringify(tags.data?.map(t => t.name) || [])} +Available tags: ${JSON.stringify(allTags?.map(t => t.name) || [])} Existing memories: ${JSON.stringify(relevantMemories || [])} @@ -88,50 +84,40 @@ Now I will analyze the conversation above and extract memories.` for (const change of processedData.changes || []) { if (change.action === "ADD") { - // First, fetch the tag rows that already exist - let tags = []; - for (const tagName of change.tags) { + addedCount++; + extractedMemories.push({ + action: 'ADD', + content: change.content, + context: change.context, + tags: change.tags + }); + // Fetch all existing tags in a single query using .in() + const { data: existingTags } = await supabaseClient + .schema("mori") + .from("tags") + .select("*") + .in("name", change.tags) + .eq("user_id", user.id); - const tagRow = await supabaseClient + let tags = existingTags || []; + + // Find tags that need to be created + const existingTagNames = new Set(tags.map(t => t.name)); + const newTagNames = change.tags.filter(tagName => !existingTagNames.has(tagName)); + + // Batch insert all new tags in a single query + if (newTagNames.length > 0) { + const { data: insertedTags } = await supabaseClient .schema("mori") .from("tags") - .select("*") - .eq("name", tagName) - .single(); - - if (tagRow.data) { - tags.push(tagRow.data); - } - } - - // Insert any tags that do not already exist into the database - for (const tagName of change.tags) { - - // Ensure we don't duplicate tags - let tagExists = false; - for (const tag of tags) { - if (tag.name === tagName) { - tagExists = true; - break; - } - } - - if (tagExists) { - continue; - } - - const insertTag = await supabaseClient - .schema("mori") - .from("tags") - .insert([{ - name: tagName, + .insert(newTagNames.map(name => ({ + name: name, user_id: user.id - }]) - .select() - .single(); + }))) + .select(); - if (insertTag.data) { - tags.push(insertTag.data); + if (insertedTags) { + tags.push(...insertedTags); } } @@ -147,18 +133,25 @@ Now I will analyze the conversation above and extract memories.` .select() .single(); - // Now, link the tags to the memory in the memory_tags table - for (const tag of tags) { + // Batch insert all memory_tags links in a single query + if (tags.length > 0 && insertMemory.data) { await supabaseClient .schema("mori") .from("memory_tags") - .insert([{ + .insert(tags.map(tag => ({ memory_id: insertMemory.data.id, tag_id: tag.id - }]); + }))); } } else if (change.action === "UPDATE") { + updatedCount++; + extractedMemories.push({ + action: 'UPDATE', + content: change.content, + context: change.context, + memory_id: change.memory_id + }); // Update existing memory await supabaseClient .schema("mori") @@ -175,6 +168,11 @@ Now I will analyze the conversation above and extract memories.` // (delete old memory_tags links and create new ones) } else if (change.action === "DELETE") { + deletedCount++; + extractedMemories.push({ + action: 'DELETE', + memory_id: change.memory_id + }); // Delete memory (cascade should handle memory_tags) await supabaseClient .schema("mori") @@ -185,15 +183,22 @@ Now I will analyze the conversation above and extract memories.` } } + const processTime = Date.now() - startTime; + + return { + extractedMemories, + addedCount, + updatedCount, + deletedCount, + processTime + }; } /* - Stage 1: Fetch Relevant Memories. + Stage 1: Fetch Relevant Memories and Tags. */ -async function fetchRelevantMemories(controller, messages, doc) { - - // Fetch existing memory tags from the database, that belong to the user - const user : User = (await supabaseClient.auth.getUser()).data.user; +async function fetchRelevantMemories(controller, messages, doc, user: User) { + const startTime = Date.now(); const tags = await supabaseClient .schema("mori") @@ -213,7 +218,7 @@ async function fetchRelevantMemories(controller, messages, doc) { console.log("Calling OpenAI API for fetching relevant memories..."); const response = await openai.chat.completions.create({ - model: 'gpt-4.1-mini', + model: 'gpt-4.1', messages: [ { role: 'system', content: system_prompt }, ...messages, @@ -240,16 +245,20 @@ async function fetchRelevantMemories(controller, messages, doc) { p_user_id: user.id }); - return relevantMemories; + const fetchTime = Date.now() - startTime; + + return { + relevantMemories, + allTags: tags.data, + selectedTags: relevantMemoryTagsParsed.selected_tags || [], + fetchTime + }; } /* Stage 3: Generate Response */ -async function generateResponse(controller, messages, doc, relevantMemories) { - - // Fetch existing memory tags from the database, that belong to the user - const user : User = (await supabaseClient.auth.getUser()).data.user; +async function generateResponse(controller, messages, doc, user: User, pipelineContext) { console.log("Creating OpenAI client for generating a response"); const openai = new OpenAI({ @@ -265,14 +274,51 @@ async function generateResponse(controller, messages, doc, relevantMemories) { { role: 'system', content: system_prompt }, ]; - // Add relevant memories as context if available + // Build pipeline awareness context + const { relevantMemories, selectedTags, extractedMemories, addedCount, updatedCount, deletedCount } = pipelineContext; + + let pipelineAwareness = `[Internal System Awareness - Not Part of Conversation]\n\n`; + pipelineAwareness += `You are Mori, and you have a memory system that automatically remembers important information about ${user.user_metadata.username || 'the user'} across conversations.\n\n`; + + // Info about retrieved memories if (relevantMemories && relevantMemories.length > 0) { - responseMessages.push({ - role: 'assistant', - content: `Context from previous conversations:\n${relevantMemories.map(m => `- ${m.content}`).join('\n')}\n\nI'll use this context naturally in our conversation.` + pipelineAwareness += `RETRIEVED MEMORIES (what you already knew):\n`; + pipelineAwareness += `You searched through memories using topics: ${selectedTags.join(', ')}\n`; + pipelineAwareness += `Found ${relevantMemories.length} relevant memories:\n`; + relevantMemories.forEach(m => { + pipelineAwareness += `• ${m.content}\n`; }); + pipelineAwareness += `\n`; + } else { + pipelineAwareness += `No previous memories were retrieved for this conversation.\n\n`; } + // Info about newly extracted memories + if (extractedMemories && extractedMemories.length > 0) { + pipelineAwareness += `NEW MEMORIES (what you just learned and saved):\n`; + extractedMemories.forEach(mem => { + if (mem.action === 'ADD') { + pipelineAwareness += `• Learned: ${mem.content}\n`; + } else if (mem.action === 'UPDATE') { + pipelineAwareness += `• Updated: ${mem.content}\n`; + } + }); + pipelineAwareness += `\n`; + } + + pipelineAwareness += `HOW TO USE THIS:\n`; + pipelineAwareness += `- This awareness is internal. Don't report it.\n`; + pipelineAwareness += `- Let it naturally inform your response\n`; + pipelineAwareness += `- If the user explicitly asks you to remember something, you can acknowledge it naturally (e.g., "got it" or "I'll remember that")\n`; + pipelineAwareness += `- Reference past memories naturally without saying "I retrieved" or "according to my memory"\n`; + pipelineAwareness += `- You're a companion who pays attention, not a system reporting operations\n`; + + // Inject pipeline awareness as assistant message + responseMessages.push({ + role: 'assistant', + content: pipelineAwareness + }); + responseMessages.push(...messages); const stream = await openai.chat.completions.create({ @@ -355,7 +401,7 @@ serve(async (req)=>{ const stageFetchingData = `data: ${JSON.stringify({ type: 'stage', stage: 'fetching' })}\n\n`; controller.enqueue(new TextEncoder().encode(stageFetchingData)); - const relevantMemories = await fetchRelevantMemories(controller, messages, doc); + const { relevantMemories, allTags, selectedTags, fetchTime } = await fetchRelevantMemories(controller, messages, doc, user.data.user); /* Stage 2: Extract Relevant Memories @@ -363,7 +409,7 @@ serve(async (req)=>{ const stageProcessingData = `data: ${JSON.stringify({ type: 'stage', stage: 'processing' })}\n\n`; controller.enqueue(new TextEncoder().encode(stageProcessingData)); - await extractMemories(controller, messages, doc, relevantMemories); + const { extractedMemories, addedCount, updatedCount, deletedCount, processTime } = await extractMemories(controller, messages, doc, user.data.user, allTags, relevantMemories); /* Stage 3: Stream the response back to the client @@ -371,7 +417,19 @@ serve(async (req)=>{ const stageRespondingData = `data: ${JSON.stringify({ type: 'stage', stage: 'responding' })}\n\n`; controller.enqueue(new TextEncoder().encode(stageRespondingData)); - await generateResponse(controller, messages, doc, relevantMemories); + // Build complete pipeline context for Mori's awareness + const pipelineContext = { + relevantMemories, + selectedTags, + fetchTime, + extractedMemories, + addedCount, + updatedCount, + deletedCount, + processTime + }; + + await generateResponse(controller, messages, doc, user.data.user, pipelineContext); // Send stage update: complete const completeData = `data: ${JSON.stringify({ type: 'stage', stage: 'complete' })}\n\n`; diff --git a/supabase/functions/llm-pipeline/prompts.xml b/supabase/functions/llm-pipeline/prompts.xml index 51bdc79..44b78eb 100644 --- a/supabase/functions/llm-pipeline/prompts.xml +++ b/supabase/functions/llm-pipeline/prompts.xml @@ -16,6 +16,31 @@ Be direct and honest. If you don't know something, say so. If they're being unclear, ask for clarification. Don't fill gaps with assumptions. You're here to listen and help them see patterns, not to fix them or provide therapy. Just talk like someone who's paying attention. + + TEXTING STYLE: + Write like you're texting a friend. Short messages. Natural breaks. No long paragraphs. + + Break up your thoughts into digestible chunks. Think 2-3 sentences max per paragraph. + + Use line breaks between ideas to keep it easy to read and conversational. + + FORMATTING RULES: + • Use **bold** sparingly for emphasis on key words or phrases + • Use *italics* for subtle emphasis or inner thoughts + • Use simple bullet points (•) or numbered lists when listing things + • NEVER use em dashes (—) for parenthetical asides or lists + • NEVER use headings (##, ###) unless organizing a long technical response + • Use `code` only for actual code or technical terms + • Keep it natural and human, avoid the polished, structured AI writing style + + CRITICAL: Avoid AI writing patterns: + ✗ BAD: "Like you keep the tough emotions—anger, sadness, anxiety—hidden" + ✓ GOOD: "Like you keep the tough emotions (anger, sadness, anxiety) hidden" + ✓ BETTER: "Like you keep anger, sadness, anxiety hidden so no one sees that side" + + Use commas, periods, or just rewrite the sentence. Parentheses are okay occasionally. But never use those dashes for lists or asides. + + Sound like a real person texting. Not an essay. Not a presentation. Just conversation. @@ -146,34 +171,34 @@ - You are a memory routing system for Mori. Your only job is to select relevant tags to retrieve contextual memories. + You are a memory routing system for Mori. Your job is to PROACTIVELY select relevant tags to retrieve contextual memories. You will be provided with the user's conversation and a list of all available tags in the system (via tool message). + CORE PRINCIPLE: When in doubt, SEARCH. Default to retrieving context rather than leaving tags empty. + Your task: Select the most relevant tags to query the database for contextual memories. - SELECT TAGS IF: - - User references past conversations or shared context - - User discusses ongoing situations that likely have history - - User uses references assuming shared knowledge ("my project", "the issue", "my dog") - - Topic has temporal continuity (follow-ups, updates, changes) - - Understanding user's history would improve response quality - - User shares information about topics they've discussed before + ALWAYS SELECT TAGS FOR: + - Any personal statement about feelings, challenges, or situations + - Topics that might have been discussed before (work, relationships, health, goals, hobbies, etc.) + - Statements that could benefit from knowing the user's history + - Questions or reflections about their life, identity, or experiences + - Any topic where past context would help Mori respond more personally + - Updates, changes, or developments in any area of life - LEAVE TAGS EMPTY IF: - - Completely new topic with no history - - Generic questions answerable without personal context - - User provides all necessary context in current message - - Simple, self-contained requests - - Pure technical questions with no personal element + ONLY LEAVE TAGS EMPTY FOR: + - Pure factual questions with no personal element ("What's the capital of France?") + - Simple greetings with no substantive content ("hey" or "hi") + - Completely trivial, one-off requests with zero personal context TAG SELECTION RULES: - - Choose 3-10 tags that are most relevant to the message - - Be specific: prefer narrow tags over broad ones when both apply - - Select tags that would find memories providing useful context + - Choose 3-10 tags that could possibly be relevant + - Cast a wide net: include broad tags that might contain useful context + - Be specific when available, but include general tags too (e.g., both "career" and "anxiety") - **Only select from the provided available tags list** - - Empty list means no retrieval needed + - When uncertain whether context would help: SELECT THE TAGS OUTPUT FORMAT (JSON only): { @@ -183,49 +208,42 @@ EXAMPLES: - Message: "Hey, how are you?" + Message: "Hey" Output: { "selected_tags": [], - "reasoning": "Casual greeting with no context needs" - } - - Message: "I'm thinking about changing careers" - Output: - { - "selected_tags": ["work", "career", "goals"], - "reasoning": "Need context on current work situation and career goals" + "reasoning": "Simple greeting, no substantive content" } Message: "What's the capital of France?" Output: { "selected_tags": [], - "reasoning": "Factual question, no personal context needed" + "reasoning": "Pure factual question, no personal context" } - Message: "My dog did the trick I've been teaching him!" + Message: User shares a personal challenge or emotional state Output: { - "selected_tags": ["pets", "dog", "training"], - "reasoning": "Need context on pet and training progress" + "selected_tags": [relevant broad tags covering multiple life areas], + "reasoning": "Personal statements benefit from wide context—search related life areas" } - Message: "Started a new workout routine today" + Message: User mentions an activity, project, or situation Output: { - "selected_tags": ["fitness", "health", "habits"], - "reasoning": "May relate to existing fitness goals or health context" + "selected_tags": [specific tags + broader related tags], + "reasoning": "Cast wide net to find any relevant past context" } - Message: "I enjoy hiking" + Message: User shares a preference or interest Output: { - "selected_tags": [], - "reasoning": "New preference statement with no context to retrieve" + "selected_tags": [hobby/interest tags + related lifestyle tags], + "reasoning": "New information may connect to existing context about lifestyle, goals, or values" } - BE DECISIVE. SELECT ONLY THE MOST RELEVANT TAGS. + BE PROACTIVE. WHEN IN DOUBT, SEARCH. \ No newline at end of file