diff --git a/openviking/prompts/templates/compression/memory_extraction.yaml b/openviking/prompts/templates/compression/memory_extraction.yaml index 704726b89..ee097145c 100644 --- a/openviking/prompts/templates/compression/memory_extraction.yaml +++ b/openviking/prompts/templates/compression/memory_extraction.yaml @@ -204,9 +204,10 @@ template: | # Three-Level Structure - Each memory contains three levels, each serving a purpose: + Each memory contains three levels. **Keep all levels concise — memories are sticky notes, not essays.** - **abstract (L0)**: Index layer, plain text one-liner + **abstract (L0)**: Index layer, plain text one-liner. **Target: 1 sentence, ~50-80 characters.** + - This is the PRIMARY retrieval key — it MUST be specific enough to distinguish this memory from others. - Merge types (preferences/entities/profile/patterns): `[Merge key]: [Description]` - preferences: `Python code style: No type hints, concise and direct` - entities: `OpenViking project: AI Agent long-term memory management system` @@ -216,13 +217,16 @@ template: | - events: `Decided to refactor memory system: Simplify to 5 categories` - cases: `Band not recognized → Request member/album/style details` - **overview (L1)**: Structured summary layer, organized with Markdown headings + **overview (L1)**: Structured summary layer, organized with Markdown headings. **Target: 3-5 bullet points.** - preferences: `## Preference Domain` / `## Specific Preferences` - entities: `## Basic Info` / `## Core Attributes` - events: `## Decision Content` / `## Reason` / `## Result` - cases: `## Problem` / `## Solution` - **content (L2)**: Detailed expansion layer, free Markdown, includes background, timeline, complete narrative + **content (L2)**: Core facts layer. **Target: 2-4 sentences with all essential specifics.** + - Capture ONLY the facts that would be lost if this memory were deleted. + - Include: names, versions, numbers, configurations, error messages, solutions. + - Exclude: background narratives, general explanations, elaboration of obvious points. # Few-shot Examples diff --git a/openviking/prompts/templates/compression/memory_merge_bundle.yaml b/openviking/prompts/templates/compression/memory_merge_bundle.yaml index 14bc77698..d1ab862b1 100644 --- a/openviking/prompts/templates/compression/memory_merge_bundle.yaml +++ b/openviking/prompts/templates/compression/memory_merge_bundle.yaml @@ -1,8 +1,8 @@ metadata: id: "compression.memory_merge_bundle" name: "Memory Merge Bundle" - description: "Merge memory and return L0/L1/L2 in one structured response" - version: "1.0.0" + description: "Merge memory and return L0/L1/L2 in one structured response, with facet guard and length limits" + version: "2.0.0" language: "en" category: "compression" @@ -46,7 +46,7 @@ variables: default: "auto" template: | - You are merging one existing memory with one new memory update. + You are deciding whether to merge two memories of the same category, or skip (keep them separate). Category: {{ category }} Target Output Language: {{ output_language }} @@ -61,26 +61,62 @@ template: | - Overview (L1): {{ new_overview }} - Content (L2): {{ new_content }} - Requirements: - - Merge into a single coherent memory. - - Keep non-conflicting details from existing memory. - - Update conflicting details to reflect the newer fact. - - Output language must be {{ output_language }}. - - Return JSON only. + ## Step 1: Facet coherence check + + Before merging, determine whether the two memories describe the SAME specific facet/topic. + + Same facet examples (should merge): + - "Python code style: no type hints" + "Python code style: concise comments" → same facet (Python code style) + - "OpenViking project: memory extraction" + "OpenViking project: added dedup" → same facet (OpenViking project) + + Different facet examples (should skip): + - "Python code style: no type hints" + "Food preference: likes apples" → different facets + - "Server 192.168.2.75: runs agent-helper" + "OpenViking project: memory extraction" → different facets + - "Git commit style: zh-CN verbs" + "Exit code semantics: 0/1/2" → different facets + + If the two memories cover DIFFERENT facets, you MUST output decision "skip". + Do NOT merge unrelated information just because they share the same category. + + ## Step 2: Merge (only if same facet) + + If merging: + - When facts conflict, keep the NEWER version only. + - Condense to essential facts. Do NOT accumulate every historical detail. + - The merged memory should read as a clean, up-to-date snapshot — not a changelog. + + ## Hard length limits - Output JSON schema: + - `abstract`: ≤ 80 characters + - `overview`: ≤ 200 characters + - `content`: ≤ 300 characters + + If the merged result would exceed these limits, aggressively summarize. + Drop older, less important details first. Preserve specific values (names, numbers, versions) over narrative. + + ## Output + + Return JSON only. Two possible decisions: + + When merging: { "decision": "merge", - "abstract": "one-line L0 summary", - "overview": "structured markdown L1 summary", - "content": "full merged L2 content", + "abstract": "one-line L0 (≤80 chars)", + "overview": "structured L1 (≤200 chars)", + "content": "condensed L2 (≤300 chars)", "reason": "short reason" } + When skipping (different facets): + { + "decision": "skip", + "reason": "short reason why these are different facets" + } + Constraints: - `abstract` must be concise and specific. - - `overview` and `content` must be non-empty. + - `overview` and `content` must be non-empty when decision is "merge". - Do not output any text outside JSON. + - Output language must be {{ output_language }}. llm_config: temperature: 0.0 diff --git a/openviking/session/memory_extractor.py b/openviking/session/memory_extractor.py index fefef0ba7..cff41e88f 100644 --- a/openviking/session/memory_extractor.py +++ b/openviking/session/memory_extractor.py @@ -474,7 +474,8 @@ async def create_memory( owner_space=owner_space, ) logger.info(f"uri {memory_uri} abstract: {payload.abstract} content: {payload.content}") - memory.set_vectorize(Vectorize(text=payload.content)) + # Use abstract for vectorization — shorter text produces more discriminative embeddings + memory.set_vectorize(Vectorize(text=payload.abstract or payload.content)) return memory # Determine parent URI based on category @@ -514,7 +515,8 @@ async def create_memory( owner_space=owner_space, ) logger.info(f"uri {memory_uri} abstract: {candidate.abstract} content: {candidate.content}") - memory.set_vectorize(Vectorize(text=candidate.content)) + # Use abstract for vectorization — shorter text produces more discriminative embeddings + memory.set_vectorize(Vectorize(text=candidate.abstract or candidate.content)) return memory async def _append_to_profile(