Add initial project setup with environment variables, server logic, and memory handling
This commit is contained in:
@@ -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`;
|
||||
|
||||
@@ -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.
|
||||
</system_response>
|
||||
|
||||
|
||||
@@ -146,34 +171,34 @@
|
||||
<!-- This prompt is used for memory fetching-->
|
||||
<memory_query>
|
||||
<memory_query>
|
||||
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.
|
||||
</memory_query>
|
||||
</memory_query>
|
||||
</prompts>
|
||||
Reference in New Issue
Block a user