tribalmemory 0.1.1__tar.gz → 0.2.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.
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/PKG-INFO +1 -1
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/pyproject.toml +1 -1
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/cli.py +147 -4
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/interfaces.py +44 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/mcp/server.py +160 -14
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/server/app.py +53 -2
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/server/config.py +41 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/server/models.py +65 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/server/routes.py +68 -0
- tribalmemory-0.2.0/src/tribalmemory/services/fts_store.py +255 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/services/memory.py +193 -33
- tribalmemory-0.2.0/src/tribalmemory/services/reranker.py +267 -0
- tribalmemory-0.2.0/src/tribalmemory/services/session_store.py +412 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/services/vector_store.py +86 -1
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory.egg-info/PKG-INFO +1 -1
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory.egg-info/SOURCES.txt +6 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/tests/test_a21_config.py +2 -2
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/tests/test_a21_container.py +8 -8
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/tests/test_a21_providers.py +10 -11
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/tests/test_a21_system.py +4 -4
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/tests/test_benchmarks.py +1 -1
- tribalmemory-0.2.0/tests/test_cli.py +428 -0
- tribalmemory-0.2.0/tests/test_hybrid_search.py +323 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/tests/test_mcp_server.py +9 -5
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/tests/test_negative_security.py +3 -3
- tribalmemory-0.2.0/tests/test_reranking.py +392 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/tests/test_services.py +7 -7
- tribalmemory-0.2.0/tests/test_session_store.py +429 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/tests/test_tier1_functional.py +3 -3
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/tests/test_tier2_capability.py +1 -1
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/tests/test_tier3_emergence.py +1 -1
- tribalmemory-0.1.1/tests/test_cli.py +0 -207
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/LICENSE +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/README.md +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/setup.cfg +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/__init__.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/a21/__init__.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/a21/config/__init__.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/a21/config/providers.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/a21/config/system.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/a21/container/__init__.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/a21/container/container.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/a21/providers/__init__.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/a21/providers/base.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/a21/providers/deduplication.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/a21/providers/lancedb.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/a21/providers/memory.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/a21/providers/mock.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/a21/providers/openai.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/a21/providers/timestamp.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/a21/system.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/mcp/__init__.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/mcp/__main__.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/performance/__init__.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/performance/benchmarks.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/performance/corpus_generator.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/portability/__init__.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/portability/embedding_metadata.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/server/__init__.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/server/__main__.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/services/__init__.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/services/deduplication.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/services/embeddings.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/services/import_export.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/testing/__init__.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/testing/embedding_utils.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/testing/fixtures.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/testing/metrics.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/testing/mocks.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/testing/semantic_expansions.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory/utils.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory.egg-info/dependency_links.txt +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory.egg-info/entry_points.txt +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory.egg-info/requires.txt +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/src/tribalmemory.egg-info/top_level.txt +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/tests/test_embedding_portability.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/tests/test_import_export.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/tests/test_local_embeddings.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/tests/test_mcp_integration.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/tests/test_memory_harness.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/tests/test_performance.py +0 -0
- {tribalmemory-0.1.1 → tribalmemory-0.2.0}/tests/test_server.py +0 -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":
|
|
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 = "
|
|
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",
|
|
@@ -174,6 +174,50 @@ class IVectorStore(ABC):
|
|
|
174
174
|
"""Count memories matching filters."""
|
|
175
175
|
pass
|
|
176
176
|
|
|
177
|
+
async def get_stats(self) -> dict:
|
|
178
|
+
"""Compute aggregate statistics over all memories.
|
|
179
|
+
|
|
180
|
+
Returns dict with keys:
|
|
181
|
+
total_memories, by_source_type, by_tag, by_instance, corrections
|
|
182
|
+
|
|
183
|
+
Default implementation iterates in pages of 500. Subclasses
|
|
184
|
+
should override with native queries (SQL GROUP BY, etc.) for
|
|
185
|
+
stores with >10k entries.
|
|
186
|
+
"""
|
|
187
|
+
page_size = 500
|
|
188
|
+
total = 0
|
|
189
|
+
corrections = 0
|
|
190
|
+
by_source: dict[str, int] = {}
|
|
191
|
+
by_instance: dict[str, int] = {}
|
|
192
|
+
by_tag: dict[str, int] = {}
|
|
193
|
+
|
|
194
|
+
offset = 0
|
|
195
|
+
while True:
|
|
196
|
+
page = await self.list(limit=page_size, offset=offset)
|
|
197
|
+
if not page:
|
|
198
|
+
break
|
|
199
|
+
total += len(page)
|
|
200
|
+
for m in page:
|
|
201
|
+
src = m.source_type.value
|
|
202
|
+
by_source[src] = by_source.get(src, 0) + 1
|
|
203
|
+
inst = m.source_instance
|
|
204
|
+
by_instance[inst] = by_instance.get(inst, 0) + 1
|
|
205
|
+
for tag in m.tags:
|
|
206
|
+
by_tag[tag] = by_tag.get(tag, 0) + 1
|
|
207
|
+
if m.supersedes:
|
|
208
|
+
corrections += 1
|
|
209
|
+
if len(page) < page_size:
|
|
210
|
+
break
|
|
211
|
+
offset += page_size
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
"total_memories": total,
|
|
215
|
+
"by_source_type": by_source,
|
|
216
|
+
"by_tag": by_tag,
|
|
217
|
+
"by_instance": by_instance,
|
|
218
|
+
"corrections": corrections,
|
|
219
|
+
}
|
|
220
|
+
|
|
177
221
|
|
|
178
222
|
class IDeduplicationService(ABC):
|
|
179
223
|
"""Interface for detecting duplicate memories."""
|
|
@@ -16,11 +16,13 @@ from mcp.server.fastmcp import FastMCP
|
|
|
16
16
|
from ..interfaces import MemorySource
|
|
17
17
|
from ..server.config import TribalMemoryConfig
|
|
18
18
|
from ..services import create_memory_service, TribalMemoryService
|
|
19
|
+
from ..services.session_store import SessionStore, SessionMessage
|
|
19
20
|
|
|
20
21
|
logger = logging.getLogger(__name__)
|
|
21
22
|
|
|
22
23
|
# Global service instance (initialized on first use)
|
|
23
24
|
_memory_service: Optional[TribalMemoryService] = None
|
|
25
|
+
_session_store: Optional[SessionStore] = None
|
|
24
26
|
_service_lock = asyncio.Lock()
|
|
25
27
|
|
|
26
28
|
|
|
@@ -60,6 +62,32 @@ async def get_memory_service() -> TribalMemoryService:
|
|
|
60
62
|
return _memory_service
|
|
61
63
|
|
|
62
64
|
|
|
65
|
+
async def get_session_store() -> SessionStore:
|
|
66
|
+
"""Get or create the session store singleton (thread-safe)."""
|
|
67
|
+
global _session_store
|
|
68
|
+
|
|
69
|
+
if _session_store is not None:
|
|
70
|
+
return _session_store
|
|
71
|
+
|
|
72
|
+
memory_service = await get_memory_service()
|
|
73
|
+
|
|
74
|
+
async with _service_lock:
|
|
75
|
+
if _session_store is not None:
|
|
76
|
+
return _session_store
|
|
77
|
+
|
|
78
|
+
config = TribalMemoryConfig.from_env()
|
|
79
|
+
instance_id = os.environ.get("TRIBAL_MEMORY_INSTANCE_ID", "mcp-claude-code")
|
|
80
|
+
|
|
81
|
+
_session_store = SessionStore(
|
|
82
|
+
instance_id=instance_id,
|
|
83
|
+
embedding_service=memory_service.embedding_service,
|
|
84
|
+
vector_store=memory_service.vector_store,
|
|
85
|
+
)
|
|
86
|
+
logger.info("Session store initialized")
|
|
87
|
+
|
|
88
|
+
return _session_store
|
|
89
|
+
|
|
90
|
+
|
|
63
91
|
def create_server() -> FastMCP:
|
|
64
92
|
"""Create and configure the MCP server with all tools."""
|
|
65
93
|
mcp = FastMCP("tribal-memory")
|
|
@@ -127,17 +155,19 @@ def create_server() -> FastMCP:
|
|
|
127
155
|
limit: int = 5,
|
|
128
156
|
min_relevance: float = 0.3,
|
|
129
157
|
tags: Optional[list[str]] = None,
|
|
158
|
+
sources: str = "memories",
|
|
130
159
|
) -> str:
|
|
131
|
-
"""Search memories by semantic similarity.
|
|
160
|
+
"""Search memories and/or session transcripts by semantic similarity.
|
|
132
161
|
|
|
133
162
|
Args:
|
|
134
163
|
query: Natural language search query (required)
|
|
135
164
|
limit: Maximum number of results (1-50, default 5)
|
|
136
165
|
min_relevance: Minimum similarity score (0.0-1.0, default 0.3)
|
|
137
166
|
tags: Filter results to only memories with these tags
|
|
167
|
+
sources: What to search - "memories" (default), "sessions", or "all"
|
|
138
168
|
|
|
139
169
|
Returns:
|
|
140
|
-
JSON with: results (list of memories with similarity scores), query, count
|
|
170
|
+
JSON with: results (list of memories/chunks with similarity scores), query, count
|
|
141
171
|
"""
|
|
142
172
|
# Input validation
|
|
143
173
|
if not query or not query.strip():
|
|
@@ -148,22 +178,33 @@ def create_server() -> FastMCP:
|
|
|
148
178
|
"error": "Query cannot be empty",
|
|
149
179
|
})
|
|
150
180
|
|
|
151
|
-
|
|
181
|
+
valid_sources = {"memories", "sessions", "all"}
|
|
182
|
+
if sources not in valid_sources:
|
|
183
|
+
return json.dumps({
|
|
184
|
+
"results": [],
|
|
185
|
+
"query": query,
|
|
186
|
+
"count": 0,
|
|
187
|
+
"error": f"Invalid sources: {sources}. Valid options: {', '.join(sorted(valid_sources))}",
|
|
188
|
+
})
|
|
152
189
|
|
|
153
190
|
# Clamp limit to valid range
|
|
154
191
|
limit = max(1, min(50, limit))
|
|
155
192
|
min_relevance = max(0.0, min(1.0, min_relevance))
|
|
156
193
|
|
|
157
|
-
|
|
158
|
-
query=query,
|
|
159
|
-
limit=limit,
|
|
160
|
-
min_relevance=min_relevance,
|
|
161
|
-
tags=tags,
|
|
162
|
-
)
|
|
194
|
+
all_results = []
|
|
163
195
|
|
|
164
|
-
|
|
165
|
-
|
|
196
|
+
# Search memories
|
|
197
|
+
if sources in ("memories", "all"):
|
|
198
|
+
service = await get_memory_service()
|
|
199
|
+
memory_results = await service.recall(
|
|
200
|
+
query=query,
|
|
201
|
+
limit=limit,
|
|
202
|
+
min_relevance=min_relevance,
|
|
203
|
+
tags=tags,
|
|
204
|
+
)
|
|
205
|
+
all_results.extend([
|
|
166
206
|
{
|
|
207
|
+
"type": "memory",
|
|
167
208
|
"memory_id": r.memory.id,
|
|
168
209
|
"content": r.memory.content,
|
|
169
210
|
"similarity_score": round(r.similarity_score, 4),
|
|
@@ -173,12 +214,117 @@ def create_server() -> FastMCP:
|
|
|
173
214
|
"created_at": r.memory.created_at.isoformat(),
|
|
174
215
|
"context": r.memory.context,
|
|
175
216
|
}
|
|
176
|
-
for r in
|
|
177
|
-
]
|
|
217
|
+
for r in memory_results
|
|
218
|
+
])
|
|
219
|
+
|
|
220
|
+
# Search sessions
|
|
221
|
+
if sources in ("sessions", "all"):
|
|
222
|
+
session_store = await get_session_store()
|
|
223
|
+
session_results = await session_store.search(
|
|
224
|
+
query=query,
|
|
225
|
+
limit=limit,
|
|
226
|
+
min_relevance=min_relevance,
|
|
227
|
+
)
|
|
228
|
+
all_results.extend([
|
|
229
|
+
{
|
|
230
|
+
"type": "session",
|
|
231
|
+
"chunk_id": r["chunk_id"],
|
|
232
|
+
"session_id": r["session_id"],
|
|
233
|
+
"instance_id": r["instance_id"],
|
|
234
|
+
"content": r["content"],
|
|
235
|
+
"similarity_score": round(r["similarity_score"], 4),
|
|
236
|
+
"start_time": r["start_time"].isoformat() if hasattr(r["start_time"], "isoformat") else str(r["start_time"]),
|
|
237
|
+
"end_time": r["end_time"].isoformat() if hasattr(r["end_time"], "isoformat") else str(r["end_time"]),
|
|
238
|
+
"chunk_index": r["chunk_index"],
|
|
239
|
+
}
|
|
240
|
+
for r in session_results
|
|
241
|
+
])
|
|
242
|
+
|
|
243
|
+
# Sort combined results by score, take top limit
|
|
244
|
+
all_results.sort(key=lambda x: x["similarity_score"], reverse=True)
|
|
245
|
+
all_results = all_results[:limit]
|
|
246
|
+
|
|
247
|
+
return json.dumps({
|
|
248
|
+
"results": all_results,
|
|
178
249
|
"query": query,
|
|
179
|
-
"count": len(
|
|
250
|
+
"count": len(all_results),
|
|
251
|
+
"sources": sources,
|
|
180
252
|
})
|
|
181
253
|
|
|
254
|
+
@mcp.tool()
|
|
255
|
+
async def tribal_sessions_ingest(
|
|
256
|
+
session_id: str,
|
|
257
|
+
messages: str,
|
|
258
|
+
instance_id: Optional[str] = None,
|
|
259
|
+
) -> str:
|
|
260
|
+
"""Ingest a session transcript for indexing.
|
|
261
|
+
|
|
262
|
+
Chunks conversation messages into ~400 token windows and indexes them
|
|
263
|
+
for semantic search. Supports delta ingestion — only new messages
|
|
264
|
+
since last ingest are processed.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
session_id: Unique identifier for the session (required)
|
|
268
|
+
messages: JSON array of messages, each with "role", "content",
|
|
269
|
+
and optional "timestamp" (ISO 8601). Example:
|
|
270
|
+
[{"role": "user", "content": "What is Docker?"},
|
|
271
|
+
{"role": "assistant", "content": "Docker is a container platform"}]
|
|
272
|
+
instance_id: Override the agent instance ID (optional)
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
JSON with: success, chunks_created, messages_processed
|
|
276
|
+
"""
|
|
277
|
+
if not session_id or not session_id.strip():
|
|
278
|
+
return json.dumps({
|
|
279
|
+
"success": False,
|
|
280
|
+
"error": "session_id cannot be empty",
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
try:
|
|
284
|
+
raw_messages = json.loads(messages)
|
|
285
|
+
except (json.JSONDecodeError, TypeError) as e:
|
|
286
|
+
return json.dumps({
|
|
287
|
+
"success": False,
|
|
288
|
+
"error": f"Invalid messages JSON: {e}",
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
if not isinstance(raw_messages, list):
|
|
292
|
+
return json.dumps({
|
|
293
|
+
"success": False,
|
|
294
|
+
"error": "messages must be a JSON array",
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
from datetime import datetime, timezone
|
|
298
|
+
parsed_messages = []
|
|
299
|
+
for i, msg in enumerate(raw_messages):
|
|
300
|
+
if not isinstance(msg, dict) or "role" not in msg or "content" not in msg:
|
|
301
|
+
return json.dumps({
|
|
302
|
+
"success": False,
|
|
303
|
+
"error": f"Message {i} must have 'role' and 'content' fields",
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
ts = datetime.now(timezone.utc)
|
|
307
|
+
if "timestamp" in msg:
|
|
308
|
+
try:
|
|
309
|
+
ts = datetime.fromisoformat(msg["timestamp"])
|
|
310
|
+
except (ValueError, TypeError):
|
|
311
|
+
pass # Use current time if timestamp is invalid
|
|
312
|
+
|
|
313
|
+
parsed_messages.append(SessionMessage(
|
|
314
|
+
role=msg["role"],
|
|
315
|
+
content=msg["content"],
|
|
316
|
+
timestamp=ts,
|
|
317
|
+
))
|
|
318
|
+
|
|
319
|
+
session_store = await get_session_store()
|
|
320
|
+
result = await session_store.ingest(
|
|
321
|
+
session_id=session_id,
|
|
322
|
+
messages=parsed_messages,
|
|
323
|
+
instance_id=instance_id,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
return json.dumps(result)
|
|
327
|
+
|
|
182
328
|
@mcp.tool()
|
|
183
329
|
async def tribal_correct(
|
|
184
330
|
original_id: str,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""FastAPI application for tribal-memory service."""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
import logging
|
|
4
5
|
from contextlib import asynccontextmanager
|
|
5
6
|
from pathlib import Path
|
|
@@ -10,11 +11,13 @@ from fastapi import FastAPI
|
|
|
10
11
|
from fastapi.middleware.cors import CORSMiddleware
|
|
11
12
|
|
|
12
13
|
from ..services import create_memory_service, TribalMemoryService
|
|
14
|
+
from ..services.session_store import SessionStore
|
|
13
15
|
from .config import TribalMemoryConfig
|
|
14
16
|
from .routes import router
|
|
15
17
|
|
|
16
18
|
# Global service instance (set during lifespan)
|
|
17
19
|
_memory_service: Optional[TribalMemoryService] = None
|
|
20
|
+
_session_store: Optional[SessionStore] = None
|
|
18
21
|
_instance_id: Optional[str] = None
|
|
19
22
|
|
|
20
23
|
logger = logging.getLogger("tribalmemory.server")
|
|
@@ -23,7 +26,7 @@ logger = logging.getLogger("tribalmemory.server")
|
|
|
23
26
|
@asynccontextmanager
|
|
24
27
|
async def lifespan(app: FastAPI):
|
|
25
28
|
"""Application lifespan manager."""
|
|
26
|
-
global _memory_service, _instance_id
|
|
29
|
+
global _memory_service, _session_store, _instance_id
|
|
27
30
|
|
|
28
31
|
config: TribalMemoryConfig = app.state.config
|
|
29
32
|
|
|
@@ -43,18 +46,66 @@ async def lifespan(app: FastAPI):
|
|
|
43
46
|
api_base=config.embedding.api_base,
|
|
44
47
|
embedding_model=config.embedding.model,
|
|
45
48
|
embedding_dimensions=config.embedding.dimensions,
|
|
49
|
+
hybrid_search=config.search.hybrid_enabled,
|
|
50
|
+
hybrid_vector_weight=config.search.vector_weight,
|
|
51
|
+
hybrid_text_weight=config.search.text_weight,
|
|
52
|
+
hybrid_candidate_multiplier=config.search.candidate_multiplier,
|
|
46
53
|
)
|
|
47
54
|
|
|
48
|
-
|
|
55
|
+
# Create session store (shares embedding service and vector store)
|
|
56
|
+
_session_store = SessionStore(
|
|
57
|
+
instance_id=config.instance_id,
|
|
58
|
+
embedding_service=_memory_service.embedding_service,
|
|
59
|
+
vector_store=_memory_service.vector_store,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
search_mode = "hybrid (vector + BM25)" if config.search.hybrid_enabled else "vector-only"
|
|
63
|
+
logger.info(f"Memory service initialized (db: {config.db.path}, search: {search_mode})")
|
|
64
|
+
logger.info(f"Session store initialized (retention: {config.server.session_retention_days} days)")
|
|
65
|
+
|
|
66
|
+
# Start background session cleanup task
|
|
67
|
+
cleanup_task = asyncio.create_task(
|
|
68
|
+
_session_cleanup_loop(
|
|
69
|
+
_session_store,
|
|
70
|
+
config.server.session_retention_days,
|
|
71
|
+
)
|
|
72
|
+
)
|
|
49
73
|
|
|
50
74
|
yield
|
|
51
75
|
|
|
52
76
|
# Cleanup
|
|
77
|
+
cleanup_task.cancel()
|
|
78
|
+
try:
|
|
79
|
+
await cleanup_task
|
|
80
|
+
except asyncio.CancelledError:
|
|
81
|
+
pass
|
|
53
82
|
logger.info("Shutting down tribal-memory service")
|
|
54
83
|
_memory_service = None
|
|
84
|
+
_session_store = None
|
|
55
85
|
_instance_id = None
|
|
56
86
|
|
|
57
87
|
|
|
88
|
+
async def _session_cleanup_loop(
|
|
89
|
+
session_store: SessionStore,
|
|
90
|
+
retention_days: int,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Background task that periodically cleans up expired session chunks.
|
|
93
|
+
|
|
94
|
+
Runs every 6 hours. Deletes session chunks older than retention_days.
|
|
95
|
+
"""
|
|
96
|
+
cleanup_interval = 6 * 60 * 60 # 6 hours in seconds
|
|
97
|
+
while True:
|
|
98
|
+
try:
|
|
99
|
+
await asyncio.sleep(cleanup_interval)
|
|
100
|
+
deleted = await session_store.cleanup(retention_days=retention_days)
|
|
101
|
+
if deleted > 0:
|
|
102
|
+
logger.info(f"Session cleanup: deleted {deleted} expired chunks (retention: {retention_days} days)")
|
|
103
|
+
except asyncio.CancelledError:
|
|
104
|
+
raise
|
|
105
|
+
except Exception:
|
|
106
|
+
logger.exception("Session cleanup failed")
|
|
107
|
+
|
|
108
|
+
|
|
58
109
|
def create_app(config: Optional[TribalMemoryConfig] = None) -> FastAPI:
|
|
59
110
|
"""Create FastAPI application.
|
|
60
111
|
|