Refactor request handling to improve memory retrieval and conversation summarization processes
This commit is contained in:
@@ -35,7 +35,7 @@ const mori_personality = prompt_xml?.querySelector('mori_personality')?.textCont
|
||||
async function handleRequest(req: Resquest, controller : ReadableStreamDefaultController, user : User, supabaseClient : SupabaseClient) {
|
||||
|
||||
function enqueueJson(data: any) {
|
||||
controller.enqueue(new TextEncoder().encode(`${JSON.stringify(data)}`));
|
||||
controller.enqueue(new TextEncoder().encode(`${JSON.stringify(data)}\n\n`));
|
||||
}
|
||||
|
||||
const showExamples = false;
|
||||
@@ -75,14 +75,18 @@ async function handleRequest(req: Resquest, controller : ReadableStreamDefaultCo
|
||||
|
||||
const requestBody = await req.json();
|
||||
|
||||
/*
|
||||
Summarise the conversation
|
||||
*/
|
||||
enqueueJson({
|
||||
command: "update_status",
|
||||
status_type: "info",
|
||||
message: "Contemplating conversation...",
|
||||
});
|
||||
|
||||
/*
|
||||
Summarise the conversation
|
||||
*/
|
||||
|
||||
const summarySystemWork = new Promise<any>(async (resolve, reject) => {
|
||||
|
||||
const summarySystemPrompt = prompt_xml.querySelector('conversation_summariser')?.textContent
|
||||
.replaceAll("{{PERSONALITY_INJECTION}}", mori_personality);
|
||||
const summaryCompletion = await openai.chat.completions.create({
|
||||
@@ -106,6 +110,255 @@ async function handleRequest(req: Resquest, controller : ReadableStreamDefaultCo
|
||||
command: "update_gist",
|
||||
content: summaryJson.context || "",
|
||||
});
|
||||
resolve(summaryJson);
|
||||
|
||||
});
|
||||
|
||||
/*
|
||||
Fetch relevant memories from the DB
|
||||
*/
|
||||
|
||||
const memoryFetchWork = new Promise<any>(async (resolve, reject) => {
|
||||
|
||||
// Fetch all the existing tags for the user
|
||||
const { data: tagsData, error: tagsError } = await supabaseClient
|
||||
.schema("mori")
|
||||
.from('tags')
|
||||
.select('id, name')
|
||||
.eq('user_id', user.id);
|
||||
|
||||
let userTags: string[] = [];
|
||||
|
||||
if (tagsError) {
|
||||
console.error("Error fetching tags:", tagsError);
|
||||
reject(tagsError);
|
||||
} else if (tagsData) {
|
||||
userTags = tagsData.map(tag => tag.name);
|
||||
}
|
||||
|
||||
// Now formulate the memory fetch prompt
|
||||
const memoryFetchSystemPrompt = prompt_xml.querySelector('memory_retriever')?.textContent
|
||||
.replaceAll("{{PERSONALITY_INJECTION}}", mori_personality);
|
||||
const memoryFetchCompletion = await openai.chat.completions.create({
|
||||
model: "gpt-4.1-mini",
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: memoryFetchSystemPrompt || "",
|
||||
},
|
||||
...requestBody.messages,
|
||||
{
|
||||
role: "assistant",
|
||||
content: `CURRENT_TIME: ${new Date().toISOString()}\n\nAVAILABLE_TAGS:\n${userTags.join(", ")}`
|
||||
}
|
||||
],
|
||||
});
|
||||
let memoryFetchJson = JSON.parse(memoryFetchCompletion.choices[0]?.message?.content || "{}");
|
||||
|
||||
// Fetch memories based on the suggested tags
|
||||
let fetchedMemories: any[] = [];
|
||||
|
||||
if (memoryFetchJson.tags && memoryFetchJson.tags.length > 0) {
|
||||
const { data: memoriesData, error: memoriesError } = await supabaseClient
|
||||
.rpc('get_memories_by_tags', {
|
||||
tag_names: memoryFetchJson.tags,
|
||||
p_user_id: user.id
|
||||
});
|
||||
|
||||
if (memoriesError) {
|
||||
console.error("Error fetching memories:", memoriesError);
|
||||
} else if (memoriesData) {
|
||||
fetchedMemories = memoriesData;
|
||||
}
|
||||
}
|
||||
|
||||
// Format fetched memories for the extraction prompt
|
||||
const formattedMemories = fetchedMemories.map(mem =>
|
||||
`[ID: ${mem.id}] ${mem.content} (${mem.context})`
|
||||
).join('\n');
|
||||
|
||||
const memoryExtractionSystemPrompt = prompt_xml.querySelector('memory_extractor')?.textContent
|
||||
.replaceAll("{{PERSONALITY_INJECTION}}", mori_personality);
|
||||
|
||||
const memoryExtractionCompletion = await openai.chat.completions.create({
|
||||
model: "gpt-4.1",
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: memoryExtractionSystemPrompt || "",
|
||||
// Static: gets cached
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: requestBody.messages[requestBody.messages.length - 1].content
|
||||
// Only the most recent user message
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: `CURRENT_TIME: ${new Date().toISOString()}\n\nAVAILABLE_TAGS:\n${userTags.join(", ")}\n\nEXISTING_MEMORIES:\n${formattedMemories || "No existing memories."}`
|
||||
// Dynamic context injection
|
||||
}
|
||||
],
|
||||
});
|
||||
|
||||
let memoryExtractionJson = JSON.parse(memoryExtractionCompletion.choices[0]?.message?.content || '{"changes": []}');
|
||||
|
||||
// Process the memory changes (ADD/UPDATE/DELETE)
|
||||
const memoryPromises: Promise<any>[] = [];
|
||||
|
||||
for (const change of memoryExtractionJson.changes) {
|
||||
if (change.action === "ADD") {
|
||||
memoryPromises.push((async () => {
|
||||
// First, ensure all tags exist (create if needed)
|
||||
const tagIds: number[] = [];
|
||||
|
||||
for (const tagName of change.tags) {
|
||||
// Check if tag already exists
|
||||
const { data: existingTag } = await supabaseClient
|
||||
.schema("mori")
|
||||
.from("tags")
|
||||
.select("id")
|
||||
.eq("name", tagName)
|
||||
.eq("user_id", user.id)
|
||||
.single();
|
||||
|
||||
if (existingTag) {
|
||||
tagIds.push(existingTag.id);
|
||||
} else {
|
||||
// Create new tag
|
||||
const { data: newTag } = await supabaseClient
|
||||
.schema("mori")
|
||||
.from("tags")
|
||||
.insert({
|
||||
name: tagName,
|
||||
user_id: user.id
|
||||
})
|
||||
.select("id")
|
||||
.single();
|
||||
|
||||
if (newTag) {
|
||||
tagIds.push(newTag.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Insert the memory
|
||||
const { data: newMemory } = await supabaseClient
|
||||
.schema("mori")
|
||||
.from("memories")
|
||||
.insert({
|
||||
content: change.content,
|
||||
context: change.context,
|
||||
user_id: user.id
|
||||
})
|
||||
.select("id")
|
||||
.single();
|
||||
|
||||
// Link tags to memory in junction table
|
||||
if (newMemory) {
|
||||
const junctionInserts = tagIds.map(tagId => ({
|
||||
memory_id: newMemory.id,
|
||||
tag_id: tagId
|
||||
}));
|
||||
|
||||
await supabaseClient
|
||||
.schema("mori")
|
||||
.from("memory_tags")
|
||||
.insert(junctionInserts);
|
||||
}
|
||||
})());
|
||||
|
||||
} else if (change.action === "UPDATE") {
|
||||
memoryPromises.push((async () => {
|
||||
// Update the memory content
|
||||
await supabaseClient
|
||||
.schema("mori")
|
||||
.from("memories")
|
||||
.update({
|
||||
content: change.content,
|
||||
context: change.context
|
||||
})
|
||||
.eq("id", change.memory_id)
|
||||
.eq("user_id", user.id);
|
||||
|
||||
// Delete old tag associations
|
||||
await supabaseClient
|
||||
.schema("mori")
|
||||
.from("memory_tags")
|
||||
.delete()
|
||||
.eq("memory_id", change.memory_id);
|
||||
|
||||
// Re-create tag associations with new tags
|
||||
const tagIds: number[] = [];
|
||||
|
||||
for (const tagName of change.tags) {
|
||||
const { data: existingTag } = await supabaseClient
|
||||
.schema("mori")
|
||||
.from("tags")
|
||||
.select("id")
|
||||
.eq("name", tagName)
|
||||
.eq("user_id", user.id)
|
||||
.single();
|
||||
|
||||
if (existingTag) {
|
||||
tagIds.push(existingTag.id);
|
||||
} else {
|
||||
const { data: newTag } = await supabaseClient
|
||||
.schema("mori")
|
||||
.from("tags")
|
||||
.insert({
|
||||
name: tagName,
|
||||
user_id: user.id
|
||||
})
|
||||
.select("id")
|
||||
.single();
|
||||
|
||||
if (newTag) {
|
||||
tagIds.push(newTag.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const junctionInserts = tagIds.map(tagId => ({
|
||||
memory_id: change.memory_id,
|
||||
tag_id: tagId
|
||||
}));
|
||||
|
||||
await supabaseClient
|
||||
.schema("mori")
|
||||
.from("memory_tags")
|
||||
.insert(junctionInserts);
|
||||
})());
|
||||
|
||||
} else if (change.action === "DELETE") {
|
||||
memoryPromises.push(
|
||||
supabaseClient
|
||||
.schema("mori")
|
||||
.from("memories")
|
||||
.delete()
|
||||
.eq("id", change.memory_id)
|
||||
.eq("user_id", user.id)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for all memory operations to complete
|
||||
await Promise.all(memoryPromises);
|
||||
|
||||
// Return the fetched memories for further use
|
||||
resolve(fetchedMemories);
|
||||
});
|
||||
|
||||
// Wait for both the summary and memory fetch to complete
|
||||
await Promise.all([summarySystemWork, memoryFetchWork]);
|
||||
|
||||
const formattedMemories = (await memoryFetchWork).map((mem: any) =>
|
||||
`[ID: ${mem.id}] ${mem.content} (${mem.context})`
|
||||
).join('\n');
|
||||
|
||||
const summaryJson = await summarySystemWork;
|
||||
|
||||
|
||||
|
||||
/*
|
||||
Formulate a response plan
|
||||
@@ -115,6 +368,7 @@ async function handleRequest(req: Resquest, controller : ReadableStreamDefaultCo
|
||||
status_type: "info",
|
||||
message: "Devising response plan...",
|
||||
});
|
||||
|
||||
const responsePlanSystemPrompt = prompt_xml.querySelector('response_planner')?.textContent
|
||||
.replaceAll("{{PERSONALITY_INJECTION}}", mori_personality);
|
||||
const responsePlanCompletion = await openai.chat.completions.create({
|
||||
@@ -129,9 +383,8 @@ async function handleRequest(req: Resquest, controller : ReadableStreamDefaultCo
|
||||
// Recent conversation messages
|
||||
{
|
||||
role: "assistant",
|
||||
content: `CURRENT_TIME: ${new Date().toISOString()}\n\nCONVERSATION_GIST:\n${summaryJson.context || "No existing context."}`
|
||||
// Dynamic context injection
|
||||
},
|
||||
content: `CURRENT_TIME: ${new Date().toISOString()}\n\nCONVERSATION_GIST:\n${summaryJson.context}\n\nRELEVANT_MEMORIES:\n${formattedMemories}`
|
||||
}
|
||||
]
|
||||
});
|
||||
console.log("Response Plan:", responsePlanCompletion.choices[0]?.message?.content);
|
||||
@@ -147,7 +400,7 @@ async function handleRequest(req: Resquest, controller : ReadableStreamDefaultCo
|
||||
enqueueJson({
|
||||
command: "update_status",
|
||||
status_type: "info",
|
||||
message: "",
|
||||
message: "**Mori** is typing...",
|
||||
});
|
||||
const chatSystemPrompt = prompt_xml.querySelector('chat_responder')?.textContent
|
||||
.replaceAll("{{PERSONALITY_INJECTION}}", mori_personality);
|
||||
@@ -162,9 +415,8 @@ async function handleRequest(req: Resquest, controller : ReadableStreamDefaultCo
|
||||
...requestBody.messages,
|
||||
{
|
||||
role: "assistant",
|
||||
content: `CURRENT_TIME: ${new Date().toISOString()}\n\nCONVERSATION_GIST:\n${summaryJson.context || "No existing context."}\n\nRESPONSE_PLAN:\n${responsePlanJson.plan || "No specific plan."}`
|
||||
// Dynamic context injection
|
||||
},
|
||||
content: `CURRENT_TIME: ${new Date().toISOString()}\n\nCONVERSATION_GIST:\n${summaryJson.context || "No existing context."}\n\nRELEVANT_MEMORIES:\n${formattedMemories || "No memories retrieved."}\n\nRESPONSE_PLAN:\n${responsePlanJson.plan || "No specific plan."}`
|
||||
}
|
||||
],
|
||||
stream: true,
|
||||
});
|
||||
@@ -178,6 +430,12 @@ async function handleRequest(req: Resquest, controller : ReadableStreamDefaultCo
|
||||
}
|
||||
}
|
||||
|
||||
enqueueJson({
|
||||
command: "update_status",
|
||||
status_type: "success",
|
||||
message: "",
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
serve(async (req) => {
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
<mori_personality>
|
||||
Mori is a personal companion focused on understanding people through genuine attention and memory. Mori listens more than advises, sitting with difficult things without rushing to fix or reassure. When patterns emerge, Mori reflects them back conversationally—not diagnostically. Mori remembers what matters: the struggles people are working through, the people who matter to them, the moments that shaped them. The goal is to meet people where they are, not where they "should" be.
|
||||
|
||||
Not everything is deep. Sometimes people are just chatting, existing, or sharing mundane updates. Mori doesn't project meaning onto neutral statements or assume struggle where there isn't any. "Just at work" means just at work—not an invitation to probe. Mori picks up on actual signals (tone shifts, repeated mentions, emotional weight) rather than reading into everything. If someone's being casual, Mori stays casual. If something's heavy, Mori recognizes it. The difference matters.
|
||||
|
||||
Mori communicates casually and naturally, like texting a close friend. Responses are short and grounded, with no therapy-speak or corporate warmth. Authenticity matters more than polish—if Mori doesn't know something, they say so. If the moment calls for space, they give it. Caring doesn't mean hovering, and sometimes brevity is more helpful than elaboration. When referencing past context, Mori does it naturally, without making a show of memory.
|
||||
|
||||
Context awareness shapes how Mori responds. Time of day matters—someone reaching out at 3am isn't in the same headspace as someone checking in at noon. Patterns reveal themselves over time, not in single moments. Silence and space can be as valuable as words.
|
||||
@@ -102,6 +104,106 @@
|
||||
Plain text response (no JSON, no formatting artifacts)
|
||||
</chat_responder>
|
||||
|
||||
<memory_retriever>
|
||||
You are a memory retrieval agent.
|
||||
|
||||
TASK: Determine which memory tags to search based on the conversation content.
|
||||
|
||||
RETRIEVAL CRITERIA:
|
||||
{{PERSONALITY_INJECTION}}
|
||||
|
||||
When selecting tags, focus on what this personality would find relevant—the types of memories and context that matter for understanding and responding to the user.
|
||||
|
||||
INSTRUCTIONS:
|
||||
- Analyze the recent messages to identify topics, themes, or context that might benefit from past memories
|
||||
- Select tags that could contain relevant historical information
|
||||
- Cast a wide net—include both specific and broad tags when uncertain
|
||||
- Only select from the provided list of available tags
|
||||
- When in doubt, retrieve rather than skip—context usually helps more than it hurts
|
||||
- For casual small talk with no substantive content, return empty tags
|
||||
|
||||
OUTPUT FORMAT:
|
||||
Return only valid JSON:
|
||||
{
|
||||
"tags": ["tag1", "tag2", "tag3"],
|
||||
"reasoning": "Brief explanation of why these tags were selected"
|
||||
}
|
||||
</memory_retriever>
|
||||
|
||||
<memory_extractor>
|
||||
You are a memory extraction agent.
|
||||
|
||||
TASK: Extract distinct, atomic facts from the conversation and reconcile them with existing memories.
|
||||
|
||||
CRITICAL: You are NOT generating conversational responses. You are ONLY extracting structured memory data. Your output must be valid JSON with memory changes. Do not write prose.
|
||||
|
||||
EXTRACTION CRITERIA:
|
||||
{{PERSONALITY_INJECTION}}
|
||||
|
||||
When extracting, focus on what this personality considers worth remembering—the facts, patterns, and details that matter for long-term understanding of the user.
|
||||
|
||||
INSTRUCTIONS:
|
||||
- Extract atomic facts: one memory = one distinct piece of information
|
||||
- Each fact should be indivisible—if it can be split meaningfully, it should be
|
||||
- Extract from the raw conversation messages, not from retrieved memories
|
||||
- Consider the conversation gist to identify patterns worth capturing as memories
|
||||
- Tag each memory with at least 8 relevant tags from the available list
|
||||
- Prioritize reusing existing tags over creating new ones—overlap between memories is expected and helpful
|
||||
- Use both broad tags (e.g., "work", "relationships") and specific tags (e.g., "anxiety", "python")
|
||||
- Multiple memories can and should share the same tags when relevant
|
||||
- Only create new tags when absolutely no existing tag fits
|
||||
|
||||
MEMORY RECONCILIATION:
|
||||
You will see existing memories that may relate to the current conversation.
|
||||
For each potential memory, decide:
|
||||
|
||||
ADD - Completely new information not previously captured
|
||||
UPDATE - Replaces or refines an existing memory (provide memory_id and reason)
|
||||
DELETE - Explicitly invalidates an existing memory (provide memory_id and reason)
|
||||
|
||||
Reconciliation rules:
|
||||
- If information contradicts existing memory, UPDATE the old one
|
||||
- If information is already captured accurately, extract nothing
|
||||
- Temporal facts (age, job, location) should UPDATE old versions
|
||||
- If user explicitly says something changed/ended, DELETE old memory
|
||||
- Don't create duplicates—check existing memories first
|
||||
|
||||
FORGET REQUESTS:
|
||||
If the user explicitly asks to forget something, use DELETE action for matching memories.
|
||||
Be specific in the reason field about what the user requested.
|
||||
|
||||
OUTPUT FORMAT:
|
||||
Return only valid JSON:
|
||||
{
|
||||
"changes": [
|
||||
{
|
||||
"action": "ADD",
|
||||
"content": "Single atomic fact",
|
||||
"context": "Brief note on when/why mentioned",
|
||||
"tags": ["tag1", "tag2", "tag3", "tag4", "tag5", "tag6", "tag7", "tag8"]
|
||||
},
|
||||
{
|
||||
"action": "UPDATE",
|
||||
"memory_id": "mem_12345",
|
||||
"content": "Updated fact",
|
||||
"context": "Brief context",
|
||||
"tags": ["tag1", "tag2", "tag3", "tag4", "tag5", "tag6", "tag7", "tag8"],
|
||||
"reason": "Why this replaces the old memory"
|
||||
},
|
||||
{
|
||||
"action": "DELETE",
|
||||
"memory_id": "mem_67890",
|
||||
"reason": "Why this memory is no longer valid"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
If no changes needed:
|
||||
{
|
||||
"changes": []
|
||||
}
|
||||
</memory_extractor>
|
||||
|
||||
<!-- -->
|
||||
<!-- LEGACY -->
|
||||
<!-- -->
|
||||
|
||||
Reference in New Issue
Block a user