tribalmemory 0.1.1__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/PKG-INFO +1 -1
  2. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/pyproject.toml +1 -1
  3. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/cli.py +147 -4
  4. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/interfaces.py +66 -3
  5. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/mcp/server.py +272 -14
  6. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/server/app.py +53 -2
  7. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/server/config.py +41 -0
  8. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/server/models.py +65 -0
  9. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/server/routes.py +68 -0
  10. tribalmemory-0.3.0/src/tribalmemory/services/fts_store.py +255 -0
  11. tribalmemory-0.3.0/src/tribalmemory/services/graph_store.py +627 -0
  12. tribalmemory-0.3.0/src/tribalmemory/services/memory.py +709 -0
  13. tribalmemory-0.3.0/src/tribalmemory/services/reranker.py +267 -0
  14. tribalmemory-0.3.0/src/tribalmemory/services/session_store.py +412 -0
  15. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/services/vector_store.py +86 -1
  16. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory.egg-info/PKG-INFO +1 -1
  17. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory.egg-info/SOURCES.txt +10 -0
  18. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/tests/test_a21_config.py +2 -2
  19. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/tests/test_a21_container.py +8 -8
  20. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/tests/test_a21_providers.py +10 -11
  21. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/tests/test_a21_system.py +4 -4
  22. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/tests/test_benchmarks.py +1 -1
  23. tribalmemory-0.3.0/tests/test_cli.py +428 -0
  24. tribalmemory-0.3.0/tests/test_graph_aware_recall.py +375 -0
  25. tribalmemory-0.3.0/tests/test_graph_memory_integration.py +216 -0
  26. tribalmemory-0.3.0/tests/test_graph_store.py +263 -0
  27. tribalmemory-0.3.0/tests/test_hybrid_search.py +323 -0
  28. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/tests/test_mcp_server.py +9 -5
  29. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/tests/test_negative_security.py +3 -3
  30. tribalmemory-0.3.0/tests/test_reranking.py +392 -0
  31. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/tests/test_services.py +7 -7
  32. tribalmemory-0.3.0/tests/test_session_store.py +429 -0
  33. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/tests/test_tier1_functional.py +3 -3
  34. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/tests/test_tier2_capability.py +1 -1
  35. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/tests/test_tier3_emergence.py +1 -1
  36. tribalmemory-0.1.1/src/tribalmemory/services/memory.py +0 -275
  37. tribalmemory-0.1.1/tests/test_cli.py +0 -207
  38. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/LICENSE +0 -0
  39. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/README.md +0 -0
  40. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/setup.cfg +0 -0
  41. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/__init__.py +0 -0
  42. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/a21/__init__.py +0 -0
  43. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/a21/config/__init__.py +0 -0
  44. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/a21/config/providers.py +0 -0
  45. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/a21/config/system.py +0 -0
  46. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/a21/container/__init__.py +0 -0
  47. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/a21/container/container.py +0 -0
  48. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/a21/providers/__init__.py +0 -0
  49. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/a21/providers/base.py +0 -0
  50. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/a21/providers/deduplication.py +0 -0
  51. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/a21/providers/lancedb.py +0 -0
  52. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/a21/providers/memory.py +0 -0
  53. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/a21/providers/mock.py +0 -0
  54. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/a21/providers/openai.py +0 -0
  55. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/a21/providers/timestamp.py +0 -0
  56. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/a21/system.py +0 -0
  57. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/mcp/__init__.py +0 -0
  58. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/mcp/__main__.py +0 -0
  59. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/performance/__init__.py +0 -0
  60. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/performance/benchmarks.py +0 -0
  61. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/performance/corpus_generator.py +0 -0
  62. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/portability/__init__.py +0 -0
  63. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/portability/embedding_metadata.py +0 -0
  64. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/server/__init__.py +0 -0
  65. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/server/__main__.py +0 -0
  66. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/services/__init__.py +0 -0
  67. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/services/deduplication.py +0 -0
  68. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/services/embeddings.py +0 -0
  69. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/services/import_export.py +0 -0
  70. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/testing/__init__.py +0 -0
  71. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/testing/embedding_utils.py +0 -0
  72. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/testing/fixtures.py +0 -0
  73. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/testing/metrics.py +0 -0
  74. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/testing/mocks.py +0 -0
  75. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/testing/semantic_expansions.py +0 -0
  76. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory/utils.py +0 -0
  77. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory.egg-info/dependency_links.txt +0 -0
  78. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory.egg-info/entry_points.txt +0 -0
  79. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory.egg-info/requires.txt +0 -0
  80. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/src/tribalmemory.egg-info/top_level.txt +0 -0
  81. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/tests/test_embedding_portability.py +0 -0
  82. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/tests/test_import_export.py +0 -0
  83. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/tests/test_local_embeddings.py +0 -0
  84. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/tests/test_mcp_integration.py +0 -0
  85. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/tests/test_memory_harness.py +0 -0
  86. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/tests/test_performance.py +0 -0
  87. {tribalmemory-0.1.1 → tribalmemory-0.3.0}/tests/test_server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tribalmemory
3
- Version: 0.1.1
3
+ Version: 0.3.0
4
4
  Summary: Shared memory infrastructure for multi-instance AI agents
5
5
  Author-email: Joe <joe@example.com>
6
6
  License: Apache-2.0
@@ -7,7 +7,7 @@ where = ["src"]
7
7
 
8
8
  [project]
9
9
  name = "tribalmemory"
10
- version = "0.1.1"
10
+ version = "0.3.0"
11
11
  description = "Shared memory infrastructure for multi-instance AI agents"
12
12
  readme = "README.md"
13
13
  license = {text = "Apache-2.0"}
@@ -9,6 +9,7 @@ Usage:
9
9
  import argparse
10
10
  import json
11
11
  import os
12
+ import shutil
12
13
  import sys
13
14
  from pathlib import Path
14
15
 
@@ -26,6 +27,36 @@ TRIBAL_DIR = Path.home() / ".tribal-memory"
26
27
  CONFIG_FILE = TRIBAL_DIR / "config.yaml"
27
28
  DEFAULT_INSTANCE_ID = "default"
28
29
 
30
+ # Paths to global instructions files (relative to home)
31
+ CLAUDE_INSTRUCTIONS_FILE = Path(".claude") / "CLAUDE.md"
32
+ CODEX_INSTRUCTIONS_FILE = Path(".codex") / "AGENTS.md"
33
+
34
+ # Section marker used to detect if auto-capture instructions already exist
35
+ _AUTO_CAPTURE_MARKER = "## Tribal Memory — Auto-Capture"
36
+
37
+ AUTO_CAPTURE_INSTRUCTIONS = f"""\
38
+
39
+ {_AUTO_CAPTURE_MARKER}
40
+
41
+ You have access to Tribal Memory — a shared memory system for AI agents.
42
+ Use it proactively:
43
+
44
+ **After completing tasks, making decisions, or learning something:**
45
+ - Call `tribal_remember` to store the insight with relevant tags
46
+ - Good memories: architectural decisions, bug fixes, "gotchas", user preferences,
47
+ project conventions, API quirks, things that took a while to figure out
48
+ - Bad memories: trivial actions ("I ran the tests"), obvious facts, temporary state
49
+
50
+ **Before answering questions about prior work:**
51
+ - Call `tribal_recall` to search for relevant context first
52
+ - Search by topic, not exact phrases — it's semantic search
53
+
54
+ **Tips:**
55
+ - Use tags to organize: `["python", "debugging"]`, `["api", "auth"]`
56
+ - One clear insight per memory is better than a wall of text
57
+ - If you're unsure whether to remember something, remember it — recall is cheap
58
+ """
59
+
29
60
  # MCP config for Claude Code CLI and Claude Desktop
30
61
  CLAUDE_CODE_MCP_CONFIG = {
31
62
  "mcpServers": {
@@ -55,7 +86,7 @@ db:
55
86
  server:
56
87
  host: 127.0.0.1
57
88
  port: 18790
58
- """
89
+ {auto_capture_line}"""
59
90
 
60
91
  LOCAL_CONFIG_TEMPLATE = """\
61
92
  # Tribal Memory Configuration — Local Mode (Zero Cloud)
@@ -78,7 +109,7 @@ db:
78
109
  server:
79
110
  host: 127.0.0.1
80
111
  port: 18790
81
- """
112
+ {auto_capture_line}"""
82
113
 
83
114
 
84
115
  def cmd_init(args: argparse.Namespace) -> int:
@@ -89,16 +120,23 @@ def cmd_init(args: argparse.Namespace) -> int:
89
120
  # Create config directory
90
121
  TRIBAL_DIR.mkdir(parents=True, exist_ok=True)
91
122
 
123
+ # Auto-capture config line (only included when flag is set)
124
+ auto_capture_line = ""
125
+ if args.auto_capture:
126
+ auto_capture_line = "\nauto_capture: true\n"
127
+
92
128
  # Choose template
93
129
  if args.local:
94
130
  config_content = LOCAL_CONFIG_TEMPLATE.format(
95
131
  instance_id=instance_id,
96
132
  db_path=db_path,
133
+ auto_capture_line=auto_capture_line,
97
134
  )
98
135
  else:
99
136
  config_content = OPENAI_CONFIG_TEMPLATE.format(
100
137
  instance_id=instance_id,
101
138
  db_path=db_path,
139
+ auto_capture_line=auto_capture_line,
102
140
  )
103
141
 
104
142
  # Write config
@@ -124,16 +162,74 @@ def cmd_init(args: argparse.Namespace) -> int:
124
162
  if args.codex:
125
163
  _setup_codex_mcp(args.local)
126
164
 
165
+ # Set up auto-capture instructions
166
+ if args.auto_capture:
167
+ _setup_auto_capture(
168
+ claude_code=args.claude_code,
169
+ codex=args.codex,
170
+ )
171
+
127
172
  print()
128
173
  print("🚀 Start the server:")
129
174
  print(" tribalmemory serve")
130
175
  print()
131
176
  print("🧠 Or use with Claude Code (MCP):")
132
177
  print(" tribalmemory-mcp")
178
+
179
+ if not args.auto_capture:
180
+ print()
181
+ print("💡 Want your agents to remember things automatically?")
182
+ print(" tribalmemory init --auto-capture --force")
133
183
 
134
184
  return 0
135
185
 
136
186
 
187
+ def _setup_auto_capture(claude_code: bool = False, codex: bool = False) -> None:
188
+ """Write auto-capture instructions to agent instruction files.
189
+
190
+ Appends memory usage instructions so agents proactively use
191
+ tribal_remember and tribal_recall without being explicitly asked.
192
+
193
+ Writes to:
194
+ - ~/.claude/CLAUDE.md (Claude Code) — when --claude-code is set
195
+ - ~/.codex/AGENTS.md (Codex CLI) — when --codex is set
196
+ - Both files if neither flag is set (covers the common case)
197
+
198
+ Skips if instructions are already present (idempotent).
199
+ """
200
+ # If no specific flag, write to both (default behavior)
201
+ if not claude_code and not codex:
202
+ claude_code = codex = True
203
+
204
+ targets = []
205
+ if claude_code:
206
+ targets.append(("Claude Code", Path.home() / CLAUDE_INSTRUCTIONS_FILE))
207
+ if codex:
208
+ targets.append(("Codex CLI", Path.home() / CODEX_INSTRUCTIONS_FILE))
209
+
210
+ for label, instructions_path in targets:
211
+ _write_instructions_file(instructions_path, label)
212
+
213
+
214
+ def _write_instructions_file(instructions_path: Path, label: str) -> None:
215
+ """Write auto-capture instructions to a single instructions file."""
216
+ instructions_path.parent.mkdir(parents=True, exist_ok=True)
217
+
218
+ if instructions_path.exists():
219
+ existing = instructions_path.read_text()
220
+ if _AUTO_CAPTURE_MARKER in existing:
221
+ print(f"✅ Auto-capture already present in {label}: {instructions_path}")
222
+ return
223
+ # Append to existing file
224
+ if not existing.endswith("\n"):
225
+ existing += "\n"
226
+ instructions_path.write_text(existing + AUTO_CAPTURE_INSTRUCTIONS)
227
+ else:
228
+ instructions_path.write_text(AUTO_CAPTURE_INSTRUCTIONS.lstrip("\n"))
229
+
230
+ print(f"✅ Auto-capture instructions written for {label}: {instructions_path}")
231
+
232
+
137
233
  def _setup_claude_code_mcp(is_local: bool) -> None:
138
234
  """Add Tribal Memory to Claude Code's MCP configuration.
139
235
 
@@ -151,8 +247,13 @@ def _setup_claude_code_mcp(is_local: bool) -> None:
151
247
  Path.home() / ".claude" / "claude_desktop_config.json", # Legacy / Linux
152
248
  ]
153
249
 
250
+ # Resolve full path to tribalmemory-mcp binary.
251
+ # Claude Desktop doesn't inherit the user's shell PATH (e.g. ~/.local/bin),
252
+ # so we need the absolute path for it to find the command.
253
+ mcp_command = _resolve_mcp_command()
254
+
154
255
  mcp_entry = {
155
- "command": "tribalmemory-mcp",
256
+ "command": mcp_command,
156
257
  "env": {},
157
258
  }
158
259
 
@@ -169,6 +270,42 @@ def _setup_claude_code_mcp(is_local: bool) -> None:
169
270
  print(f"✅ Claude Desktop config updated: {desktop_path}")
170
271
 
171
272
 
273
+ def _resolve_mcp_command() -> str:
274
+ """Resolve the full path to the tribalmemory-mcp binary.
275
+
276
+ Claude Desktop doesn't inherit the user's shell PATH (e.g. ~/.local/bin
277
+ from uv/pipx installs), so bare command names like "tribalmemory-mcp"
278
+ fail with "No such file or directory". We resolve the absolute path at
279
+ init time so the config works regardless of the app's PATH.
280
+
281
+ Falls back to the bare command name if not found on PATH (e.g. user
282
+ hasn't installed yet and will do so later).
283
+ """
284
+ resolved = shutil.which("tribalmemory-mcp")
285
+ if resolved:
286
+ return resolved
287
+
288
+ # Check common tool install locations that might not be on PATH
289
+ base_name = "tribalmemory-mcp"
290
+ search_dirs = [
291
+ Path.home() / ".local" / "bin", # uv/pipx (Linux/macOS)
292
+ Path.home() / ".cargo" / "bin", # unlikely but possible
293
+ ]
294
+ # On Windows, executables may have .exe/.cmd extensions
295
+ suffixes = [""]
296
+ if sys.platform == "win32":
297
+ suffixes = [".exe", ".cmd", ""]
298
+
299
+ for search_dir in search_dirs:
300
+ for suffix in suffixes:
301
+ candidate = search_dir / (base_name + suffix)
302
+ if candidate.exists() and os.access(candidate, os.X_OK):
303
+ return str(candidate)
304
+
305
+ # Fall back to bare command — will work if PATH is set correctly
306
+ return "tribalmemory-mcp"
307
+
308
+
172
309
  def _get_claude_desktop_config_path() -> Path:
173
310
  """Get the platform-appropriate Claude Desktop config path."""
174
311
  if sys.platform == "darwin":
@@ -211,6 +348,10 @@ def _setup_codex_mcp(is_local: bool) -> None:
211
348
  codex_config_path = Path.home() / ".codex" / "config.toml"
212
349
  codex_config_path.parent.mkdir(parents=True, exist_ok=True)
213
350
 
351
+ # Resolve full path (same reason as Claude Desktop — Codex may not
352
+ # inherit the user's full shell PATH)
353
+ mcp_command = _resolve_mcp_command()
354
+
214
355
  # Build the TOML section manually (avoid tomli_w dependency)
215
356
  # Codex uses [mcp_servers.name] sections in config.toml
216
357
  section_marker = "[mcp_servers.tribal-memory]"
@@ -219,7 +360,7 @@ def _setup_codex_mcp(is_local: bool) -> None:
219
360
  "",
220
361
  "# Tribal Memory — shared memory for AI agents",
221
362
  section_marker,
222
- 'command = "tribalmemory-mcp"',
363
+ f'command = "{mcp_command}"',
223
364
  ]
224
365
 
225
366
  if is_local:
@@ -286,6 +427,8 @@ def main() -> None:
286
427
  help="Configure Claude Code MCP integration")
287
428
  init_parser.add_argument("--codex", action="store_true",
288
429
  help="Configure Codex CLI MCP integration")
430
+ init_parser.add_argument("--auto-capture", action="store_true",
431
+ help="Enable auto-capture (writes instructions to agent config files)")
289
432
  init_parser.add_argument("--instance-id", type=str, default=None,
290
433
  help="Instance identifier (default: 'default')")
291
434
  init_parser.add_argument("--force", action="store_true",
@@ -7,9 +7,12 @@ from abc import ABC, abstractmethod
7
7
  from dataclasses import dataclass, field
8
8
  from datetime import datetime
9
9
  from enum import Enum
10
- from typing import Optional
10
+ from typing import Literal, Optional
11
11
  import uuid
12
12
 
13
+ # Valid retrieval methods for RecallResult
14
+ RetrievalMethod = Literal["vector", "graph", "hybrid", "entity"]
15
+
13
16
 
14
17
  class MemorySource(Enum):
15
18
  """Source of a memory entry."""
@@ -69,13 +72,21 @@ class MemoryEntry:
69
72
 
70
73
  @dataclass
71
74
  class RecallResult:
72
- """Result of a memory recall query."""
75
+ """Result of a memory recall query.
76
+
77
+ Attributes:
78
+ memory: The recalled memory entry.
79
+ similarity_score: Relevance score (0.0-1.0 for vector, 1.0 for exact entity match).
80
+ retrieval_time_ms: Time taken for retrieval.
81
+ retrieval_method: How this result was found (see RetrievalMethod type).
82
+ """
73
83
  memory: MemoryEntry
74
84
  similarity_score: float
75
85
  retrieval_time_ms: float
86
+ retrieval_method: RetrievalMethod = "vector"
76
87
 
77
88
  def __repr__(self) -> str:
78
- return f"RecallResult(score={self.similarity_score:.3f}, memory_id={self.memory.id[:8]}...)"
89
+ return f"RecallResult(score={self.similarity_score:.3f}, method={self.retrieval_method}, memory_id={self.memory.id[:8]}...)"
79
90
 
80
91
 
81
92
  @dataclass
@@ -174,6 +185,50 @@ class IVectorStore(ABC):
174
185
  """Count memories matching filters."""
175
186
  pass
176
187
 
188
+ async def get_stats(self) -> dict:
189
+ """Compute aggregate statistics over all memories.
190
+
191
+ Returns dict with keys:
192
+ total_memories, by_source_type, by_tag, by_instance, corrections
193
+
194
+ Default implementation iterates in pages of 500. Subclasses
195
+ should override with native queries (SQL GROUP BY, etc.) for
196
+ stores with >10k entries.
197
+ """
198
+ page_size = 500
199
+ total = 0
200
+ corrections = 0
201
+ by_source: dict[str, int] = {}
202
+ by_instance: dict[str, int] = {}
203
+ by_tag: dict[str, int] = {}
204
+
205
+ offset = 0
206
+ while True:
207
+ page = await self.list(limit=page_size, offset=offset)
208
+ if not page:
209
+ break
210
+ total += len(page)
211
+ for m in page:
212
+ src = m.source_type.value
213
+ by_source[src] = by_source.get(src, 0) + 1
214
+ inst = m.source_instance
215
+ by_instance[inst] = by_instance.get(inst, 0) + 1
216
+ for tag in m.tags:
217
+ by_tag[tag] = by_tag.get(tag, 0) + 1
218
+ if m.supersedes:
219
+ corrections += 1
220
+ if len(page) < page_size:
221
+ break
222
+ offset += page_size
223
+
224
+ return {
225
+ "total_memories": total,
226
+ "by_source_type": by_source,
227
+ "by_tag": by_tag,
228
+ "by_instance": by_instance,
229
+ "corrections": corrections,
230
+ }
231
+
177
232
 
178
233
  class IDeduplicationService(ABC):
179
234
  """Interface for detecting duplicate memories."""
@@ -271,6 +326,7 @@ class IMemoryService(ABC):
271
326
  limit: int = 5,
272
327
  min_relevance: float = 0.7,
273
328
  tags: Optional[list[str]] = None,
329
+ graph_expansion: bool = True,
274
330
  ) -> list[RecallResult]:
275
331
  """Recall relevant memories for a query.
276
332
 
@@ -279,6 +335,13 @@ class IMemoryService(ABC):
279
335
  limit: Maximum results
280
336
  min_relevance: Minimum similarity score
281
337
  tags: Filter by tags (e.g., ["work", "preferences"])
338
+ graph_expansion: Expand candidates via entity graph (default True)
339
+
340
+ Returns:
341
+ List of RecallResult objects with retrieval_method indicating source:
342
+ - "vector": Vector similarity search
343
+ - "hybrid": Vector + BM25 merge
344
+ - "graph": Entity graph traversal
282
345
  """
283
346
  pass
284
347