From 9f9ebb24287bbba2a44766a2fca74fd6d3781d6f Mon Sep 17 00:00:00 2001 From: fuwasegu Date: Fri, 12 Dec 2025 13:17:31 +0900 Subject: [PATCH 1/5] feat: add suggested links detection to curiosity scan - Add SuggestedLink dataclass for link recommendations - Implement 3 link detection strategies: - Tag sharing (2+ shared tags) - Context sharing (same context + type) - Semantic similarity (>70% similarity) - Update CuriosityReport with suggested_links field - Add next_actions for link creation in exo_curiosity_scan --- exocortex/domain/services/curiosity.py | 280 ++++++++++++++++++++++++- exocortex/server.py | 28 ++- 2 files changed, 303 insertions(+), 5 deletions(-) diff --git a/exocortex/domain/services/curiosity.py b/exocortex/domain/services/curiosity.py index 52c9a9f..e9af9ad 100644 --- a/exocortex/domain/services/curiosity.py +++ b/exocortex/domain/services/curiosity.py @@ -62,6 +62,20 @@ class KnowledgeGap: suggestion: str +@dataclass +class SuggestedLink: + """A suggested link between two memories.""" + + source_id: str + source_summary: str + target_id: str + target_summary: str + reason: str + link_type: str # "tag_shared", "context_shared", "semantic_similar" + confidence: float + suggested_relation: str = "related" # Default relation type to use + + @dataclass class CuriosityReport: """Report from the Curiosity Engine's scan.""" @@ -69,6 +83,7 @@ class CuriosityReport: contradictions: list[Contradiction] = field(default_factory=list) outdated_knowledge: list[OutdatedKnowledge] = field(default_factory=list) knowledge_gaps: list[KnowledgeGap] = field(default_factory=list) + suggested_links: list[SuggestedLink] = field(default_factory=list) questions: list[str] = field(default_factory=list) scan_summary: str = "" @@ -106,6 +121,19 @@ def to_dict(self) -> dict: } for g in self.knowledge_gaps ], + "suggested_links": [ + { + "source_id": s.source_id, + "source_summary": s.source_summary, + "target_id": s.target_id, + "target_summary": s.target_summary, + "reason": s.reason, + "link_type": s.link_type, + "confidence": s.confidence, + "suggested_relation": s.suggested_relation, + } + for s in self.suggested_links + ], "questions": self.questions, "scan_summary": self.scan_summary, } @@ -245,6 +273,12 @@ def scan( outdated = self._find_outdated_knowledge(context_filter, max_findings) report.outdated_knowledge = outdated + # Find suggested links (unlinked but related memories) + suggested_links = self._find_suggested_links( + context_filter, tag_filter, max_findings + ) + report.suggested_links = suggested_links + # Generate questions based on findings report.questions = self._generate_questions(report) @@ -481,6 +515,235 @@ def _find_outdated_knowledge( return outdated + def _find_suggested_links( + self, + context_filter: str | None, + tag_filter: list[str] | None, + max_findings: int, + ) -> list[SuggestedLink]: + """Find unlinked memories that should be connected. + + Uses three strategies: + 1. Tag sharing - memories with same tags + 2. Context sharing - memories in same project/context + 3. Semantic similarity - high similarity (>0.7) memories + + Only suggests links for memories that aren't already linked. + """ + suggested: list[SuggestedLink] = [] + checked_pairs: set[tuple[str, str]] = set() + existing_links = self._get_existing_link_pairs() + + # Get memories to analyze + memories, _, _ = self._repo.list_memories( + limit=100, + context_filter=context_filter, + tag_filter=tag_filter, + ) + + if len(memories) < 2: + return suggested + + # Strategy 1: Tag sharing (high confidence) + tag_suggestions = self._find_tag_shared_links( + memories, existing_links, checked_pairs, max_findings + ) + suggested.extend(tag_suggestions) + + # Strategy 2: Context sharing (medium confidence) + context_suggestions = self._find_context_shared_links( + memories, existing_links, checked_pairs, max_findings - len(suggested) + ) + suggested.extend(context_suggestions) + + # Strategy 3: Semantic similarity (high confidence for >0.7) + semantic_suggestions = self._find_semantic_links( + memories, existing_links, checked_pairs, max_findings - len(suggested) + ) + suggested.extend(semantic_suggestions) + + return suggested[:max_findings] + + def _get_existing_link_pairs(self) -> set[tuple[str, str]]: + """Get all existing link pairs to avoid duplicate suggestions.""" + pairs: set[tuple[str, str]] = set() + try: + # Get all memories with links + memories, _, _ = self._repo.list_memories(limit=500) + for mem in memories: + links = self._repo.get_links(mem.id) + for link in links: + pair = tuple(sorted([mem.id, link.target_id])) + pairs.add(pair) + except Exception as e: + logger.warning(f"Error getting existing links: {e}") + return pairs + + def _find_tag_shared_links( + self, + memories: list, + existing_links: set[tuple[str, str]], + checked_pairs: set[tuple[str, str]], + max_findings: int, + ) -> list[SuggestedLink]: + """Find memories that share multiple tags but aren't linked.""" + suggested: list[SuggestedLink] = [] + + # Group memories by tags + tag_to_memories: dict[str, list] = {} + for mem in memories: + for tag in (mem.tags or []): + if tag not in tag_to_memories: + tag_to_memories[tag] = [] + tag_to_memories[tag].append(mem) + + # Find pairs with multiple shared tags + for i, mem_a in enumerate(memories): + if len(suggested) >= max_findings: + break + + for mem_b in memories[i + 1:]: + if len(suggested) >= max_findings: + break + + pair = tuple(sorted([mem_a.id, mem_b.id])) + if pair in checked_pairs or pair in existing_links: + continue + + shared_tags = set(mem_a.tags or []) & set(mem_b.tags or []) + if len(shared_tags) >= 2: # At least 2 shared tags + checked_pairs.add(pair) + confidence = min(0.5 + len(shared_tags) * 0.1, 0.9) + suggested.append( + SuggestedLink( + source_id=mem_a.id, + source_summary=mem_a.summary[:80] if mem_a.summary else "", + target_id=mem_b.id, + target_summary=mem_b.summary[:80] if mem_b.summary else "", + reason=f"Share {len(shared_tags)} tags: {', '.join(list(shared_tags)[:3])}", + link_type="tag_shared", + confidence=confidence, + suggested_relation="related", + ) + ) + + return suggested + + def _find_context_shared_links( + self, + memories: list, + existing_links: set[tuple[str, str]], + checked_pairs: set[tuple[str, str]], + max_findings: int, + ) -> list[SuggestedLink]: + """Find memories in same context with same type but aren't linked.""" + suggested: list[SuggestedLink] = [] + + # Group by context + context_to_memories: dict[str, list] = {} + for mem in memories: + ctx = mem.context_name or "unknown" + if ctx not in context_to_memories: + context_to_memories[ctx] = [] + context_to_memories[ctx].append(mem) + + # Find pairs in same context with same type + for ctx, ctx_memories in context_to_memories.items(): + if len(suggested) >= max_findings: + break + + for i, mem_a in enumerate(ctx_memories): + if len(suggested) >= max_findings: + break + + for mem_b in ctx_memories[i + 1:]: + if len(suggested) >= max_findings: + break + + pair = tuple(sorted([mem_a.id, mem_b.id])) + if pair in checked_pairs or pair in existing_links: + continue + + # Same context + same type = likely related + type_a = str(mem_a.memory_type).lower() if mem_a.memory_type else "" + type_b = str(mem_b.memory_type).lower() if mem_b.memory_type else "" + + if type_a == type_b and type_a in ["insight", "decision", "success"]: + checked_pairs.add(pair) + suggested.append( + SuggestedLink( + source_id=mem_a.id, + source_summary=mem_a.summary[:80] if mem_a.summary else "", + target_id=mem_b.id, + target_summary=mem_b.summary[:80] if mem_b.summary else "", + reason=f"Same context '{ctx}' and type '{type_a}'", + link_type="context_shared", + confidence=0.6, + suggested_relation="related", + ) + ) + + return suggested + + def _find_semantic_links( + self, + memories: list, + existing_links: set[tuple[str, str]], + checked_pairs: set[tuple[str, str]], + max_findings: int, + ) -> list[SuggestedLink]: + """Find memories with high semantic similarity that aren't linked.""" + suggested: list[SuggestedLink] = [] + similarity_threshold = 0.70 # High similarity threshold + + # Sample memories for semantic search (avoid too many queries) + sample_size = min(20, len(memories)) + sample_memories = memories[:sample_size] + + for mem in sample_memories: + if len(suggested) >= max_findings: + break + + # Search for similar memories + try: + similar_memories, _ = self._repo.search_by_similarity( + query=mem.content or mem.summary or "", + limit=5, + ) + except Exception as e: + logger.warning(f"Error in semantic search: {e}") + continue + + for similar in similar_memories: + if len(suggested) >= max_findings: + break + + # Skip self + if similar.id == mem.id: + continue + + pair = tuple(sorted([mem.id, similar.id])) + if pair in checked_pairs or pair in existing_links: + continue + + similarity = similar.similarity or 0.0 + if similarity >= similarity_threshold: + checked_pairs.add(pair) + suggested.append( + SuggestedLink( + source_id=mem.id, + source_summary=mem.summary[:80] if mem.summary else "", + target_id=similar.id, + target_summary=similar.summary[:80] if similar.summary else "", + reason=f"High semantic similarity ({similarity:.0%})", + link_type="semantic_similar", + confidence=similarity, + suggested_relation="related", + ) + ) + + return suggested + def _check_if_superseded(self, memory_id: str) -> bool: """Check if a memory has been superseded by another memory. @@ -515,7 +778,17 @@ def _generate_questions(self, report: CuriosityReport) -> list[str]: "まだ有効ですか?新しい情報で更新が必要では?" ) - if not report.contradictions and not report.outdated_knowledge: + if report.suggested_links: + questions.append( + "🔗 リンクされていない関連メモリが見つかりました。" + "これらをリンクして知識グラフを強化しませんか?" + ) + + if ( + not report.contradictions + and not report.outdated_knowledge + and not report.suggested_links + ): questions.append( "✨ 知識ベースは一貫しています!" "引き続き知見を記録して、より強いパターンを構築しましょう。" @@ -537,6 +810,11 @@ def _create_summary(self, report: CuriosityReport) -> str: f"Found {len(report.outdated_knowledge)} potentially stale item(s) needing review" ) + if report.suggested_links: + parts.append( + f"Found {len(report.suggested_links)} suggested link(s) to strengthen the knowledge graph" + ) + if not parts: return "No notable findings. Your knowledge base appears consistent." diff --git a/exocortex/server.py b/exocortex/server.py index 4a2c479..02e4794 100644 --- a/exocortex/server.py +++ b/exocortex/server.py @@ -1116,13 +1116,14 @@ def curiosity_scan( tag_filter: list[str] | None = None, max_findings: int = 10, ) -> dict[str, Any]: - """Scan the knowledge base with curiosity - find contradictions and questions. + """Scan the knowledge base with curiosity - find contradictions, links, and questions. The Curiosity Engine is like a curious human that notices inconsistencies and asks questions. It looks for: 🤔 **Contradictions**: "Wait, these two memories seem to contradict each other..." 📅 **Outdated Knowledge**: "This knowledge has been superseded, is it still valid?" + 🔗 **Suggested Links**: Unlinked memories that should be connected (by tag, context, or similarity) ❓ **Questions**: Human-like questions about your knowledge base This tool helps you maintain a consistent and up-to-date knowledge base @@ -1134,12 +1135,12 @@ def curiosity_scan( max_findings: Maximum findings per category (default: 10). Returns: - CuriosityReport with contradictions, outdated knowledge, and questions. + CuriosityReport with contradictions, outdated knowledge, suggested links, and questions. Example usage: - "Scan my knowledge base for contradictions" - "What inconsistencies exist in my architecture decisions?" - - "Question my assumptions about the database design" + - "Find unlinked memories that should be connected" """ container = get_container() report = container.memory_service.curiosity_scan( @@ -1184,8 +1185,27 @@ def curiosity_scan( } ) + # Medium priority: create suggested links + for link in report.suggested_links[:5]: + next_actions.append( + { + "action": "create_link", + "priority": "medium", + "description": f"Link: {link.reason[:50]}", + "details": { + "call": "exo_link_memories", + "args": { + "source_id": link.source_id, + "target_id": link.target_id, + "relation_type": link.suggested_relation, + "reason": link.reason, + }, + }, + } + ) + # If issues found, suggest analyze_knowledge - if report.contradictions or report.outdated_knowledge: + if report.contradictions or report.outdated_knowledge or report.suggested_links: next_actions.append( { "action": "full_analysis", From f1078e5e1e9855cae6b186c800b7fcefbf4ccdc1 Mon Sep 17 00:00:00 2001 From: fuwasegu Date: Fri, 12 Dec 2025 13:34:36 +0900 Subject: [PATCH 2/5] refactorig --- README.ja.md | 50 +++++++- README.md | 55 +++++++- exocortex/domain/services/curiosity.py | 24 ++-- exocortex/worker/dream.py | 167 ++++++++++++++++++++++++- manuals/usage-guide.ja.md | 49 +++++++- manuals/usage-guide.md | 49 +++++++- tests/unit/test_curiosity_engine.py | 127 +++++++++++++++++++ tests/unit/test_sleep_mechanism.py | 167 +++++++++++++++++++++++++ 8 files changed, 658 insertions(+), 30 deletions(-) diff --git a/README.ja.md b/README.ja.md index e954865..3268d2f 100644 --- a/README.ja.md +++ b/README.ja.md @@ -194,7 +194,7 @@ uv run --directory /path/to/exocortex exocortex --transport sse --port 8765 | `exo_trace_lineage` | 🕰️ 記憶の系譜・進化を追跡(時系列推論) | | `exo_curiosity_scan` | 🤔 矛盾・古い情報・知識のギャップをスキャン | | `exo_analyze_knowledge` | 知識ベースの健全性分析と改善提案 | -| `exo_sleep` | バックグラウンド整理(重複検出、孤立記憶のレスキュー)を起動 | +| `exo_sleep` | バックグラウンド整理(重複検出、孤立記憶のレスキュー、自動リンク)を起動 | | `exo_consolidate` | 記憶クラスタから抽象パターンを抽出 | ### 🤖 知識の自律的改善(Knowledge Autonomy) @@ -338,16 +338,25 @@ AI: exo_trace_lineage(memory_id="現在の判断", direction="backward") ### Curiosity Engine `exo_curiosity_scan` -Curiosity Engineは、好奇心旺盛な人間のように**知識ベースに疑問を投げかけます**。矛盾や不整合をスキャンし、知識の質を向上させるための質問を生成します。 +Curiosity Engineは、好奇心旺盛な人間のように**知識ベースに疑問を投げかけます**。矛盾や不整合をスキャンし、リンク候補を提案し、知識の質を向上させるための質問を生成します。 **検出する内容:** | カテゴリ | 説明 | 例 | |---------|------|-----| | 🔴 **矛盾** | 互いに矛盾する記憶 | 同じトピックで成功 vs 失敗 | +| 🔗 **リンク候補** | リンクすべき未接続の記憶 | タグ・コンテキスト・類似度で検出 | | 📅 **古い情報** | 見直しが必要な古い知識 | supersededされたが未リンク | | ❓ **質問** | 知識に関する人間的な質問 | 「これはまだ有効?」 | +**リンク候補の検出戦略:** + +| 戦略 | 信頼度 | 説明 | +|------|--------|------| +| **タグ共有** | 高 (0.7+) | 2つ以上のタグを共有する記憶 | +| **コンテキスト共有** | 中 (0.6) | 同じプロジェクト+同じタイプ | +| **セマンティック類似度** | 高 (0.7+) | 70%以上の類似度を持つ記憶 | + **出力例:** ```json @@ -360,9 +369,29 @@ Curiosity Engineは、好奇心旺盛な人間のように**知識ベースに "confidence": 0.85 } ], + "suggested_links": [ + { + "source_summary": "DB最適化テクニック", + "target_summary": "クエリパフォーマンス改善", + "reason": "3つのタグを共有: database, performance, optimization", + "link_type": "tag_shared", + "confidence": 0.8 + } + ], "outdated_knowledge": [], "questions": [ - "🤔 これらの記憶は矛盾しているようです。両方とも有効ですか?" + "🤔 これらの記憶は矛盾しているようです。両方とも有効ですか?", + "🔗 リンクされていない関連記憶が見つかりました。リンクしてグラフを強化しませんか?" + ], + "next_actions": [ + { + "action": "create_link", + "priority": "medium", + "details": { + "call": "exo_link_memories", + "args": { "source_id": "...", "target_id": "...", "relation_type": "related" } + } + } ] } ``` @@ -371,11 +400,16 @@ Curiosity Engineは、好奇心旺盛な人間のように**知識ベースに ``` AI: exo_curiosity_scan(context_filter="my-project") ↓ -結果: 潜在的な問題と調査すべき質問のレポート +結果: 問題・リンク候補・質問のレポート + ↓ +AI: next_actions を実行してリンクを作成 + ↓ +結果: 知識グラフがより豊かに! ``` **ユースケース:** - 🔍 **知識の監査**: 「知識に矛盾はない?」 +- 🔗 **グラフ強化**: 「リンクすべき未接続の記憶を見つけて」 - 🧹 **品質メンテナンス**: 「何をクリーンアップすべき?」 - 💡 **発見**: 「知識について何を問うべき?」 @@ -553,13 +587,17 @@ exo_store_memory( │ ┌──────────────────────────────────────────────────────┐ │ │ │ 1. 重複検出 │ │ │ │ - 類似度 >= 95% の記憶を検出 │ │ -│ │ - 新しい方 → 古い方に 'supersedes' リンクを作成 │ │ +│ │ - 新しい方 → 古い方に 'related' リンクを作成 │ │ │ ├──────────────────────────────────────────────────────┤ │ │ │ 2. 孤立記憶のレスキュー │ │ │ │ - タグもリンクもない記憶を検出 │ │ │ │ - 最も類似した記憶に 'related' リンクを作成 │ │ │ ├──────────────────────────────────────────────────────┤ │ -│ │ 3. パターンマイニング(Phase 2) │ │ +│ │ 3. 自動リンク(高信頼度のみ) │ │ +│ │ - タグ共有: 3つ以上 → 'related' │ │ +│ │ - セマンティック: 80%以上 → 'related' │ │ +│ ├──────────────────────────────────────────────────────┤ │ +│ │ 4. パターンマイニング(Phase 2) │ │ │ │ - 記憶クラスタから共通パターンを抽出 │ │ │ └──────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ diff --git a/README.md b/README.md index 5b18151..3208aca 100644 --- a/README.md +++ b/README.md @@ -194,7 +194,7 @@ uv run --directory /path/to/exocortex exocortex --transport sse --port 8765 | `exo_trace_lineage` | 🕰️ Trace the evolution/lineage of a memory (temporal reasoning) | | `exo_curiosity_scan` | 🤔 Scan for contradictions, outdated info, and knowledge gaps | | `exo_analyze_knowledge` | Analyze knowledge base health and get improvement suggestions | -| `exo_sleep` | Trigger background consolidation (deduplication, orphan rescue) | +| `exo_sleep` | Trigger background consolidation (deduplication, orphan rescue, auto-linking) | | `exo_consolidate` | Extract abstract patterns from memory clusters | ### 🤖 Knowledge Autonomy @@ -338,7 +338,7 @@ Result: Shows the evolution chain of how the current decision came to be ### Curiosity Engine with `exo_curiosity_scan` -The Curiosity Engine actively **questions your knowledge base** like a curious human would. It scans for inconsistencies and generates questions to improve knowledge quality. +The Curiosity Engine actively **questions your knowledge base** like a curious human would. It scans for inconsistencies, finds unlinked memories, and generates questions to improve knowledge quality. **What it detects:** @@ -346,8 +346,17 @@ The Curiosity Engine actively **questions your knowledge base** like a curious h |----------|-------------|---------| | 🔴 **Contradictions** | Memories that conflict with each other | Success vs Failure on same topic | | 📅 **Outdated Info** | Old knowledge that may need review | Memories superseded but not linked | +| 🔗 **Suggested Links** | Unlinked memories that should be connected | Memories sharing tags, context, or high similarity | | ❓ **Questions** | Human-like questions about your knowledge | "Is this still valid?" | +**Suggested Links Detection Strategies:** + +| Strategy | Confidence | Description | +|----------|------------|-------------| +| **Tag Sharing** | High (0.7+) | Memories sharing 2+ tags are likely related | +| **Context Sharing** | Medium (0.6) | Same project + same type (insight/decision) | +| **Semantic Similarity** | High (0.7+) | High vector similarity (>70%) but not linked | + **Example Output:** ```json @@ -360,9 +369,34 @@ The Curiosity Engine actively **questions your knowledge base** like a curious h "confidence": 0.85 } ], + "suggested_links": [ + { + "source_summary": "Database optimization technique", + "target_summary": "Query performance improvement", + "reason": "Share 3 tags: database, performance, optimization", + "link_type": "tag_shared", + "confidence": 0.8, + "suggested_relation": "related" + } + ], "outdated_knowledge": [], "questions": [ - "🤔 These memories seem to contradict. Are both still valid?" + "🤔 These memories seem to contradict. Are both still valid?", + "🔗 Found unlinked related memories. Link them to strengthen the graph?" + ], + "next_actions": [ + { + "action": "create_link", + "priority": "medium", + "details": { + "call": "exo_link_memories", + "args": { + "source_id": "...", + "target_id": "...", + "relation_type": "related" + } + } + } ] } ``` @@ -371,11 +405,16 @@ The Curiosity Engine actively **questions your knowledge base** like a curious h ``` AI: exo_curiosity_scan(context_filter="my-project") ↓ -Result: Report of potential issues and questions to investigate +Result: Report of issues, suggested links, and questions + ↓ +AI: Executes next_actions to create links + ↓ +Result: Knowledge graph becomes richer and more connected! ``` **Use Cases:** - 🔍 **Knowledge audit**: "Are there any contradictions in my knowledge?" +- 🔗 **Graph enrichment**: "Find unlinked memories that should be connected" - 🧹 **Quality maintenance**: "What needs to be cleaned up?" - 💡 **Discovery**: "What questions should I be asking about my knowledge?" @@ -553,13 +592,17 @@ Like human sleep consolidates memories, Exocortex has a **background consolidati │ ┌──────────────────────────────────────────────────────┐ │ │ │ 1. Deduplication │ │ │ │ - Find memories with similarity >= 95% │ │ -│ │ - Link newer → older with 'supersedes' relation │ │ +│ │ - Link newer → older with 'related' relation │ │ │ ├──────────────────────────────────────────────────────┤ │ │ │ 2. Orphan Rescue │ │ │ │ - Find memories with no tags and no links │ │ │ │ - Link to most similar memory with 'related' │ │ │ ├──────────────────────────────────────────────────────┤ │ -│ │ 3. Pattern Mining (Phase 2) │ │ +│ │ 3. Auto-linking (High Confidence Only) │ │ +│ │ - Tag sharing: 3+ shared tags → 'related' │ │ +│ │ - Semantic: 80%+ similarity → 'related' │ │ +│ ├──────────────────────────────────────────────────────┤ │ +│ │ 4. Pattern Mining (Phase 2) │ │ │ │ - Extract common patterns from memory clusters │ │ │ └──────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ diff --git a/exocortex/domain/services/curiosity.py b/exocortex/domain/services/curiosity.py index e9af9ad..1786ba8 100644 --- a/exocortex/domain/services/curiosity.py +++ b/exocortex/domain/services/curiosity.py @@ -592,7 +592,7 @@ def _find_tag_shared_links( # Group memories by tags tag_to_memories: dict[str, list] = {} for mem in memories: - for tag in (mem.tags or []): + for tag in mem.tags or []: if tag not in tag_to_memories: tag_to_memories[tag] = [] tag_to_memories[tag].append(mem) @@ -602,7 +602,7 @@ def _find_tag_shared_links( if len(suggested) >= max_findings: break - for mem_b in memories[i + 1:]: + for mem_b in memories[i + 1 :]: if len(suggested) >= max_findings: break @@ -656,7 +656,7 @@ def _find_context_shared_links( if len(suggested) >= max_findings: break - for mem_b in ctx_memories[i + 1:]: + for mem_b in ctx_memories[i + 1 :]: if len(suggested) >= max_findings: break @@ -668,14 +668,22 @@ def _find_context_shared_links( type_a = str(mem_a.memory_type).lower() if mem_a.memory_type else "" type_b = str(mem_b.memory_type).lower() if mem_b.memory_type else "" - if type_a == type_b and type_a in ["insight", "decision", "success"]: + if type_a == type_b and type_a in [ + "insight", + "decision", + "success", + ]: checked_pairs.add(pair) suggested.append( SuggestedLink( source_id=mem_a.id, - source_summary=mem_a.summary[:80] if mem_a.summary else "", + source_summary=mem_a.summary[:80] + if mem_a.summary + else "", target_id=mem_b.id, - target_summary=mem_b.summary[:80] if mem_b.summary else "", + target_summary=mem_b.summary[:80] + if mem_b.summary + else "", reason=f"Same context '{ctx}' and type '{type_a}'", link_type="context_shared", confidence=0.6, @@ -734,7 +742,9 @@ def _find_semantic_links( source_id=mem.id, source_summary=mem.summary[:80] if mem.summary else "", target_id=similar.id, - target_summary=similar.summary[:80] if similar.summary else "", + target_summary=similar.summary[:80] + if similar.summary + else "", reason=f"High semantic similarity ({similarity:.0%})", link_type="semantic_similar", confidence=similarity, diff --git a/exocortex/worker/dream.py b/exocortex/worker/dream.py index b734c1d..9991566 100644 --- a/exocortex/worker/dream.py +++ b/exocortex/worker/dream.py @@ -5,7 +5,8 @@ 1. Deduplication: Detect and link highly similar memories 2. Orphan Rescue: Find and link isolated memories -3. Pattern Mining: Extract patterns from frequently accessed topics (Phase 2) +3. Auto-linking: Connect unlinked but related memories (high confidence only) +4. Pattern Mining: Extract patterns from frequently accessed topics (Phase 2) The worker is designed to run as a detached process, acquiring a file lock to ensure exclusive database access when the user is not actively working. @@ -270,8 +271,15 @@ def _run_consolidation_tasks(self) -> None: if not self._running: return - # Task 3: Pattern Mining (Phase 2 - placeholder) - logger.info("Task 3: Pattern mining (placeholder for Phase 2)...") + # Task 3: Auto-linking (high confidence only) + logger.info("Task 3: Auto-linking related memories...") + self._task_auto_linking(container) + + if not self._running: + return + + # Task 4: Pattern Mining (Phase 2 - placeholder) + logger.info("Task 4: Pattern mining (placeholder for Phase 2)...") # self._task_pattern_mining(container) def _task_deduplication(self, container: Container) -> None: @@ -413,6 +421,159 @@ def _task_orphan_rescue(self, container: Container) -> None: except Exception as e: logger.warning(f"Orphan rescue task error: {e}") + def _task_auto_linking(self, container: Container) -> None: + """Automatically link related memories with high confidence. + + Uses two strategies (high confidence only to avoid noise): + 1. Tag sharing: Memories with 3+ shared tags + 2. Semantic similarity: Memories with >= 80% similarity + + Only creates links between memories that aren't already linked. + """ + repo = container.repository + service = container.memory_service + + try: + # Get all memories + memories, total, _ = repo.list_memories(limit=500) + logger.info(f"Checking {total} memories for auto-linking opportunities...") + + # Get existing links to avoid duplicates + existing_links: set[tuple[str, str]] = set() + for mem in memories: + links = repo.get_links(mem.id) + for link in links: + pair = tuple(sorted([mem.id, link.target_id])) + existing_links.add(pair) + + links_created = 0 + processed_pairs: set[tuple[str, str]] = set() + + # Strategy 1: Tag sharing (3+ shared tags) + logger.info("Strategy 1: Checking tag sharing...") + tag_links = self._find_tag_shared_pairs( + memories, existing_links, processed_pairs + ) + for source_id, target_id, shared_tags in tag_links: + if not self._running: + break + try: + service.link_memories( + source_id=source_id, + target_id=target_id, + relation_type=RelationType.RELATED, + reason=f"Auto-linked: Share {len(shared_tags)} tags ({', '.join(list(shared_tags)[:3])})", + ) + links_created += 1 + logger.info( + f"Linked (tags): {source_id[:8]}... ↔ {target_id[:8]}... " + f"(shared: {', '.join(list(shared_tags)[:3])})" + ) + except Exception as e: + logger.debug(f"Could not create tag-based link: {e}") + + # Strategy 2: Semantic similarity (80%+) + if self._running: + logger.info("Strategy 2: Checking semantic similarity...") + semantic_links = self._find_semantic_pairs( + memories, existing_links, processed_pairs, repo + ) + for source_id, target_id, similarity in semantic_links: + if not self._running: + break + try: + service.link_memories( + source_id=source_id, + target_id=target_id, + relation_type=RelationType.RELATED, + reason=f"Auto-linked: High semantic similarity ({similarity:.0%})", + ) + links_created += 1 + logger.info( + f"Linked (semantic): {source_id[:8]}... ↔ {target_id[:8]}... " + f"(similarity: {similarity:.0%})" + ) + except Exception as e: + logger.debug(f"Could not create semantic link: {e}") + + logger.info(f"Auto-linking complete: {links_created} links created") + + except Exception as e: + logger.warning(f"Auto-linking task error: {e}") + + def _find_tag_shared_pairs( + self, + memories: list, + existing_links: set[tuple[str, str]], + processed_pairs: set[tuple[str, str]], + min_shared_tags: int = 3, + ) -> list[tuple[str, str, set[str]]]: + """Find memory pairs with 3+ shared tags. + + Returns list of (source_id, target_id, shared_tags) tuples. + """ + results: list[tuple[str, str, set[str]]] = [] + + for i, mem_a in enumerate(memories): + if not self._running: + break + + for mem_b in memories[i + 1 :]: + pair = tuple(sorted([mem_a.id, mem_b.id])) + if pair in existing_links or pair in processed_pairs: + continue + + shared_tags = set(mem_a.tags or []) & set(mem_b.tags or []) + if len(shared_tags) >= min_shared_tags: + processed_pairs.add(pair) + results.append((mem_a.id, mem_b.id, shared_tags)) + + return results + + def _find_semantic_pairs( + self, + memories: list, + existing_links: set[tuple[str, str]], + processed_pairs: set[tuple[str, str]], + repo, + min_similarity: float = 0.80, + sample_size: int = 50, + ) -> list[tuple[str, str, float]]: + """Find memory pairs with high semantic similarity. + + Returns list of (source_id, target_id, similarity) tuples. + """ + results: list[tuple[str, str, float]] = [] + + # Sample memories to avoid too many embedding queries + sample = memories[:sample_size] + + for mem in sample: + if not self._running: + break + + try: + similar = repo.search_similar_by_embedding( + embedding=repo._embedding_engine.embed(mem.content), + limit=5, + exclude_id=mem.id, + ) + except Exception: + continue + + for other_id, _, similarity, _, _ in similar: + if similarity < min_similarity: + continue + + pair = tuple(sorted([mem.id, other_id])) + if pair in existing_links or pair in processed_pairs: + continue + + processed_pairs.add(pair) + results.append((mem.id, other_id, similarity)) + + return results + def _cleanup(self) -> None: """Clean up resources.""" if self._container is not None: diff --git a/manuals/usage-guide.ja.md b/manuals/usage-guide.ja.md index 800b6c8..45e4a4e 100644 --- a/manuals/usage-guide.ja.md +++ b/manuals/usage-guide.ja.md @@ -164,7 +164,7 @@ AIエージェントのための「第二の脳」Exocortex の実践的な使 ### 🤔 好奇心スキャン (`exo_curiosity_scan`) -知識ベースの矛盾・古い情報をスキャンし、質問を生成します。 +知識ベースの矛盾・リンク候補・古い情報をスキャンし、質問を生成します。 | パラメータ | 説明 | 例 | |-----------|------|-----| @@ -174,14 +174,44 @@ AIエージェントのための「第二の脳」Exocortex の実践的な使 **検出する内容:** - 🔴 **矛盾**: 同じトピックで成功 vs 失敗 +- 🔗 **リンク候補**: リンクすべき未接続の記憶 - 📅 **古い情報**: supersededされていない古い知識 - ❓ **質問**: 知識に関する人間的な質問 +**リンク候補の検出戦略:** + +| 戦略 | 説明 | +|------|------| +| **タグ共有** | 2つ以上のタグを共有する記憶(高信頼度) | +| **コンテキスト共有** | 同じプロジェクト+同じタイプ(中信頼度) | +| **セマンティック類似度** | 70%以上の類似度(高信頼度) | + **プロンプト例:** - 「知識に矛盾はない?」 +- 「リンクされていない関連記憶を見つけて」 - 「DBの設計について疑問を投げかけて」 - 「プロジェクトの不整合をスキャンして」 +**自動リンク作成:** + +レスポンスには `next_actions` が含まれ、`exo_link_memories` の呼び出しが提案されます: + +```json +{ + "suggested_links": [...], + "next_actions": [ + { + "action": "create_link", + "priority": "medium", + "details": { + "call": "exo_link_memories", + "args": { "source_id": "...", "target_id": "...", "relation_type": "related" } + } + } + ] +} +``` + **🤖 Optional: BERTベースのセンチメント分析** より高精度な判定のため、`exocortex[sentiment]` をインストールできます: @@ -323,10 +353,21 @@ pip install exocortex[sentiment] ``` 💬 「知識ベースに問題はない?」 -1. exo_curiosity_scan で矛盾・古い情報を検出 +1. exo_curiosity_scan で矛盾・リンク候補・古い情報を検出 2. 生成された質問を確認 -3. 矛盾する記憶を evolved_from や supersedes でリンク -4. 古い記憶を superseded としてマーク +3. next_actions を実行してリンク候補を接続 +4. 矛盾する記憶を evolved_from や supersedes でリンク +5. 古い記憶を superseded としてマーク +``` + +### 🔗 グラフ強化フロー + +``` +💬 「リンクされていない記憶を繋いで」 + +1. exo_curiosity_scan → suggested_links を取得 +2. AI が next_actions を実行(exo_link_memories 呼び出し) +3. 知識グラフがより豊かに、より繋がりのあるものに! ``` **例:** diff --git a/manuals/usage-guide.md b/manuals/usage-guide.md index c8228aa..32d2dd4 100644 --- a/manuals/usage-guide.md +++ b/manuals/usage-guide.md @@ -164,7 +164,7 @@ Trace the **evolution and history** of a memory. Understand how decisions evolve ### 🤔 Curiosity Scan (`exo_curiosity_scan`) -Scan your knowledge base for contradictions, outdated info, and generate questions. +Scan your knowledge base for contradictions, suggested links, outdated info, and generate questions. | Parameter | Description | Example | |-----------|-------------|---------| @@ -174,14 +174,44 @@ Scan your knowledge base for contradictions, outdated info, and generate questio **What it detects:** - 🔴 **Contradictions**: Success vs Failure on same topic +- 🔗 **Suggested Links**: Unlinked memories that should be connected - 📅 **Outdated Info**: Old knowledge not marked as superseded - ❓ **Questions**: Human-like questions about your knowledge +**Suggested Link Detection Strategies:** + +| Strategy | Description | +|----------|-------------| +| **Tag Sharing** | Memories sharing 2+ tags (high confidence) | +| **Context Sharing** | Same project + same type (medium confidence) | +| **Semantic Similarity** | High vector similarity >70% (high confidence) | + **Example Prompts:** - "Are there any contradictions in my knowledge?" +- "Find unlinked memories that should be connected" - "Question my assumptions about the database design" - "Scan for inconsistencies in my project" +**Automated Link Creation:** + +The response includes `next_actions` with suggested `exo_link_memories` calls: + +```json +{ + "suggested_links": [...], + "next_actions": [ + { + "action": "create_link", + "priority": "medium", + "details": { + "call": "exo_link_memories", + "args": { "source_id": "...", "target_id": "...", "relation_type": "related" } + } + } + ] +} +``` + **🤖 Optional: BERT-based Sentiment Analysis** For higher accuracy, install `exocortex[sentiment]` to enable BERT model: @@ -323,10 +353,21 @@ Diagnose knowledge base health. ``` 💬 "Are there any issues with my knowledge base?" -1. exo_curiosity_scan to detect contradictions and outdated info +1. exo_curiosity_scan to detect contradictions, suggested links, and outdated info 2. Review the generated questions -3. Link contradicting memories with evolved_from or supersedes -4. Mark outdated memories as superseded +3. Execute next_actions to create suggested links +4. Link contradicting memories with evolved_from or supersedes +5. Mark outdated memories as superseded +``` + +### 🔗 Graph Enrichment Flow + +``` +💬 "Find unlinked memories and connect them" + +1. exo_curiosity_scan → Returns suggested_links +2. AI executes next_actions (exo_link_memories calls) +3. Knowledge graph becomes richer and more interconnected ``` **Example:** diff --git a/tests/unit/test_curiosity_engine.py b/tests/unit/test_curiosity_engine.py index 02e9019..5fbb99b 100644 --- a/tests/unit/test_curiosity_engine.py +++ b/tests/unit/test_curiosity_engine.py @@ -16,6 +16,7 @@ CuriosityEngine, CuriosityReport, OutdatedKnowledge, + SuggestedLink, ) @@ -258,6 +259,117 @@ def test_generates_positive_message_when_clean(self, engine): assert "一貫" in questions[0] +class TestCuriosityEngineSuggestedLinks: + """Tests for suggested link detection.""" + + @pytest.fixture + def engine(self): + """Create engine with mock repository.""" + mock_repo = MagicMock() + mock_repo.get_links.return_value = [] + return CuriosityEngine(repository=mock_repo) + + @pytest.fixture + def mock_memories_shared_tags(self): + """Create memories with shared tags.""" + mem_a = MagicMock() + mem_a.id = "mem-a" + mem_a.summary = "Database optimization technique" + mem_a.content = "Use connection pooling for better performance" + mem_a.memory_type = "insight" + mem_a.context_name = "project-a" + mem_a.tags = ["database", "performance", "optimization"] + + mem_b = MagicMock() + mem_b.id = "mem-b" + mem_b.summary = "Query optimization approach" + mem_b.content = "Index frequently queried columns" + mem_b.memory_type = "insight" + mem_b.context_name = "project-b" + mem_b.tags = ["database", "performance", "sql"] + + return [mem_a, mem_b] + + @pytest.fixture + def mock_memories_same_context(self): + """Create memories in same context with same type.""" + mem_a = MagicMock() + mem_a.id = "mem-c" + mem_a.summary = "API design decision" + mem_a.content = "Use REST for public API" + mem_a.memory_type = "decision" + mem_a.context_name = "backend-project" + mem_a.tags = ["api"] + + mem_b = MagicMock() + mem_b.id = "mem-d" + mem_b.summary = "Authentication decision" + mem_b.content = "Use JWT tokens" + mem_b.memory_type = "decision" + mem_b.context_name = "backend-project" + mem_b.tags = ["auth"] + + return [mem_a, mem_b] + + def test_finds_tag_shared_links(self, engine, mock_memories_shared_tags): + """Memories with 2+ shared tags should be suggested as links.""" + engine._repo.list_memories.return_value = (mock_memories_shared_tags, 2, False) + + suggestions = engine._find_tag_shared_links( + mock_memories_shared_tags, set(), set(), 10 + ) + + assert len(suggestions) == 1 + assert suggestions[0].link_type == "tag_shared" + assert ( + "database" in suggestions[0].reason + or "performance" in suggestions[0].reason + ) + assert suggestions[0].confidence >= 0.5 + + def test_finds_context_shared_links(self, engine, mock_memories_same_context): + """Memories in same context with same type should be suggested.""" + suggestions = engine._find_context_shared_links( + mock_memories_same_context, set(), set(), 10 + ) + + assert len(suggestions) == 1 + assert suggestions[0].link_type == "context_shared" + assert "backend-project" in suggestions[0].reason + assert "decision" in suggestions[0].reason + + def test_skips_already_linked_memories(self, engine, mock_memories_shared_tags): + """Should not suggest links for already linked memories.""" + existing_links = {("mem-a", "mem-b")} + + suggestions = engine._find_tag_shared_links( + mock_memories_shared_tags, existing_links, set(), 10 + ) + + assert len(suggestions) == 0 + + def test_generates_link_question(self, engine): + """Should generate question when suggested links found.""" + report = CuriosityReport( + suggested_links=[ + SuggestedLink( + source_id="a", + source_summary="A", + target_id="b", + target_summary="B", + reason="Share 2 tags", + link_type="tag_shared", + confidence=0.7, + ) + ] + ) + + questions = engine._generate_questions(report) + + assert len(questions) >= 1 + assert "リンク" in questions[0] + + class TestCuriosityReportSerialization: """Tests for CuriosityReport serialization.""" @@ -269,6 +381,7 @@ def test_to_dict_empty_report(self): assert result["contradictions"] == [] assert result["outdated_knowledge"] == [] assert result["knowledge_gaps"] == [] + assert result["suggested_links"] == [] assert result["questions"] == [] def test_to_dict_with_data(self): @@ -285,6 +398,17 @@ def test_to_dict_with_data(self): confidence=0.75, ) ], + suggested_links=[ + SuggestedLink( + source_id="x", + source_summary="Source", + target_id="y", + target_summary="Target", + reason="Share tags", + link_type="tag_shared", + confidence=0.8, + ) + ], questions=["Is this correct?"], scan_summary="Found 1 issue", ) @@ -294,5 +418,8 @@ def test_to_dict_with_data(self): assert len(result["contradictions"]) == 1 assert result["contradictions"][0]["memory_a_id"] == "a" assert result["contradictions"][0]["confidence"] == 0.75 + assert len(result["suggested_links"]) == 1 + assert result["suggested_links"][0]["source_id"] == "x" + assert result["suggested_links"][0]["link_type"] == "tag_shared" assert result["questions"] == ["Is this correct?"] assert result["scan_summary"] == "Found 1 issue" diff --git a/tests/unit/test_sleep_mechanism.py b/tests/unit/test_sleep_mechanism.py index a3efe81..d965765 100644 --- a/tests/unit/test_sleep_mechanism.py +++ b/tests/unit/test_sleep_mechanism.py @@ -401,3 +401,170 @@ def test_backup_returns_true_when_no_database(self): result = worker._backup_database() assert result is True + + +class TestDreamWorkerAutoLinking: + """Tests for auto-linking task.""" + + def test_find_tag_shared_pairs_with_enough_shared_tags(self): + """Memories with 3+ shared tags should be paired.""" + from exocortex.config import Config + from exocortex.worker.dream import DreamWorker + + with tempfile.TemporaryDirectory() as tmpdir: + config = Config(data_dir=Path(tmpdir)) + worker = DreamWorker(config=config) + worker._running = True + + # Create mock memories with shared tags + mem_a = MagicMock() + mem_a.id = "mem-a" + mem_a.tags = ["database", "performance", "optimization", "sql"] + + mem_b = MagicMock() + mem_b.id = "mem-b" + mem_b.tags = ["database", "performance", "optimization", "indexing"] + + memories = [mem_a, mem_b] + existing_links: set[tuple[str, str]] = set() + processed_pairs: set[tuple[str, str]] = set() + + results = worker._find_tag_shared_pairs( + memories, existing_links, processed_pairs, min_shared_tags=3 + ) + + assert len(results) == 1 + assert results[0][0] == "mem-a" + assert results[0][1] == "mem-b" + assert "database" in results[0][2] + assert "performance" in results[0][2] + assert "optimization" in results[0][2] + + def test_find_tag_shared_pairs_skips_insufficient_tags(self): + """Memories with < 3 shared tags should not be paired.""" + from exocortex.config import Config + from exocortex.worker.dream import DreamWorker + + with tempfile.TemporaryDirectory() as tmpdir: + config = Config(data_dir=Path(tmpdir)) + worker = DreamWorker(config=config) + worker._running = True + + mem_a = MagicMock() + mem_a.id = "mem-a" + mem_a.tags = ["database", "performance"] + + mem_b = MagicMock() + mem_b.id = "mem-b" + mem_b.tags = ["database", "frontend"] + + memories = [mem_a, mem_b] + existing_links: set[tuple[str, str]] = set() + processed_pairs: set[tuple[str, str]] = set() + + results = worker._find_tag_shared_pairs( + memories, existing_links, processed_pairs, min_shared_tags=3 + ) + + assert len(results) == 0 + + def test_find_tag_shared_pairs_skips_existing_links(self): + """Already linked memories should not be paired again.""" + from exocortex.config import Config + from exocortex.worker.dream import DreamWorker + + with tempfile.TemporaryDirectory() as tmpdir: + config = Config(data_dir=Path(tmpdir)) + worker = DreamWorker(config=config) + worker._running = True + + mem_a = MagicMock() + mem_a.id = "mem-a" + mem_a.tags = ["a", "b", "c", "d"] + + mem_b = MagicMock() + mem_b.id = "mem-b" + mem_b.tags = ["a", "b", "c", "e"] + + memories = [mem_a, mem_b] + existing_links = {("mem-a", "mem-b")} # Already linked + processed_pairs: set[tuple[str, str]] = set() + + results = worker._find_tag_shared_pairs( + memories, existing_links, processed_pairs, min_shared_tags=3 + ) + + assert len(results) == 0 + + def test_find_semantic_pairs_with_high_similarity(self): + """Memories with 80%+ similarity should be paired.""" + from exocortex.config import Config + from exocortex.worker.dream import DreamWorker + + with tempfile.TemporaryDirectory() as tmpdir: + config = Config(data_dir=Path(tmpdir)) + worker = DreamWorker(config=config) + worker._running = True + + mem_a = MagicMock() + mem_a.id = "mem-a" + mem_a.content = "Test content" + + # Mock repo with search results + mock_repo = MagicMock() + mock_repo._embedding_engine.embed.return_value = [0.1, 0.2, 0.3] + # Returns: (id, summary, similarity, tags, type) + mock_repo.search_similar_by_embedding.return_value = [ + ("mem-b", "Summary B", 0.85, ["tag"], "insight"), + ] + + memories = [mem_a] + existing_links: set[tuple[str, str]] = set() + processed_pairs: set[tuple[str, str]] = set() + + results = worker._find_semantic_pairs( + memories, + existing_links, + processed_pairs, + mock_repo, + min_similarity=0.80, + ) + + assert len(results) == 1 + assert results[0][0] == "mem-a" + assert results[0][1] == "mem-b" + assert results[0][2] == 0.85 + + def test_find_semantic_pairs_skips_low_similarity(self): + """Memories with < 80% similarity should not be paired.""" + from exocortex.config import Config + from exocortex.worker.dream import DreamWorker + + with tempfile.TemporaryDirectory() as tmpdir: + config = Config(data_dir=Path(tmpdir)) + worker = DreamWorker(config=config) + worker._running = True + + mem_a = MagicMock() + mem_a.id = "mem-a" + mem_a.content = "Test content" + + mock_repo = MagicMock() + mock_repo._embedding_engine.embed.return_value = [0.1, 0.2, 0.3] + mock_repo.search_similar_by_embedding.return_value = [ + ("mem-b", "Summary B", 0.75, ["tag"], "insight"), # Below threshold + ] + + memories = [mem_a] + existing_links: set[tuple[str, str]] = set() + processed_pairs: set[tuple[str, str]] = set() + + results = worker._find_semantic_pairs( + memories, + existing_links, + processed_pairs, + mock_repo, + min_similarity=0.80, + ) + + assert len(results) == 0 From 6ea36931cbb672776ed4f47ceba0689797fdba35 Mon Sep 17 00:00:00 2001 From: fuwasegu Date: Fri, 12 Dec 2025 13:36:52 +0900 Subject: [PATCH 3/5] version --- exocortex/__init__.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/exocortex/__init__.py b/exocortex/__init__.py index 1179cf4..1cbba01 100644 --- a/exocortex/__init__.py +++ b/exocortex/__init__.py @@ -4,4 +4,4 @@ storing and retrieving development insights across projects. """ -__version__ = "0.9.1" +__version__ = "1.0.0" diff --git a/pyproject.toml b/pyproject.toml index f99965a..c34f037 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "exocortex" -version = "0.9.1" +version = "1.0.0" description = "Local MCP Server acting as your external brain for development insights." readme = "README.md" requires-python = ">=3.10" From 73662cdde94d22cc5a9d8f6ca26efa18b40c6c2a Mon Sep 17 00:00:00 2001 From: fuwasegu Date: Fri, 12 Dec 2025 13:45:46 +0900 Subject: [PATCH 4/5] fix --- exocortex/domain/services/curiosity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exocortex/domain/services/curiosity.py b/exocortex/domain/services/curiosity.py index 1786ba8..a2fce8c 100644 --- a/exocortex/domain/services/curiosity.py +++ b/exocortex/domain/services/curiosity.py @@ -642,7 +642,7 @@ def _find_context_shared_links( # Group by context context_to_memories: dict[str, list] = {} for mem in memories: - ctx = mem.context_name or "unknown" + ctx = mem.context or "unknown" if ctx not in context_to_memories: context_to_memories[ctx] = [] context_to_memories[ctx].append(mem) From a80ad9bba2d6e88e743fdc3446df32bbf3cd4932 Mon Sep 17 00:00:00 2001 From: fuwasegu Date: Fri, 12 Dec 2025 13:50:03 +0900 Subject: [PATCH 5/5] fix --- tests/unit/test_curiosity_engine.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_curiosity_engine.py b/tests/unit/test_curiosity_engine.py index 5fbb99b..9133441 100644 --- a/tests/unit/test_curiosity_engine.py +++ b/tests/unit/test_curiosity_engine.py @@ -298,7 +298,7 @@ def mock_memories_same_context(self): mem_a.summary = "API design decision" mem_a.content = "Use REST for public API" mem_a.memory_type = "decision" - mem_a.context_name = "backend-project" + mem_a.context = "backend-project" mem_a.tags = ["api"] mem_b = MagicMock() @@ -306,7 +306,7 @@ def mock_memories_same_context(self): mem_b.summary = "Authentication decision" mem_b.content = "Use JWT tokens" mem_b.memory_type = "decision" - mem_b.context_name = "backend-project" + mem_b.context = "backend-project" mem_b.tags = ["auth"] return [mem_a, mem_b]