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/__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/exocortex/domain/services/curiosity.py b/exocortex/domain/services/curiosity.py index 52c9a9f..a2fce8c 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,245 @@ 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 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 +788,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 +820,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", 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/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" diff --git a/tests/unit/test_curiosity_engine.py b/tests/unit/test_curiosity_engine.py index 02e9019..9133441 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 = "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 = "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