tribalmemory 0.1.0__py3-none-any.whl

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 (51) hide show
  1. tribalmemory/__init__.py +3 -0
  2. tribalmemory/a21/__init__.py +38 -0
  3. tribalmemory/a21/config/__init__.py +20 -0
  4. tribalmemory/a21/config/providers.py +104 -0
  5. tribalmemory/a21/config/system.py +184 -0
  6. tribalmemory/a21/container/__init__.py +8 -0
  7. tribalmemory/a21/container/container.py +212 -0
  8. tribalmemory/a21/providers/__init__.py +32 -0
  9. tribalmemory/a21/providers/base.py +241 -0
  10. tribalmemory/a21/providers/deduplication.py +99 -0
  11. tribalmemory/a21/providers/lancedb.py +232 -0
  12. tribalmemory/a21/providers/memory.py +128 -0
  13. tribalmemory/a21/providers/mock.py +54 -0
  14. tribalmemory/a21/providers/openai.py +151 -0
  15. tribalmemory/a21/providers/timestamp.py +88 -0
  16. tribalmemory/a21/system.py +293 -0
  17. tribalmemory/cli.py +298 -0
  18. tribalmemory/interfaces.py +306 -0
  19. tribalmemory/mcp/__init__.py +9 -0
  20. tribalmemory/mcp/__main__.py +6 -0
  21. tribalmemory/mcp/server.py +484 -0
  22. tribalmemory/performance/__init__.py +1 -0
  23. tribalmemory/performance/benchmarks.py +285 -0
  24. tribalmemory/performance/corpus_generator.py +171 -0
  25. tribalmemory/portability/__init__.py +1 -0
  26. tribalmemory/portability/embedding_metadata.py +320 -0
  27. tribalmemory/server/__init__.py +9 -0
  28. tribalmemory/server/__main__.py +6 -0
  29. tribalmemory/server/app.py +187 -0
  30. tribalmemory/server/config.py +115 -0
  31. tribalmemory/server/models.py +206 -0
  32. tribalmemory/server/routes.py +378 -0
  33. tribalmemory/services/__init__.py +15 -0
  34. tribalmemory/services/deduplication.py +115 -0
  35. tribalmemory/services/embeddings.py +273 -0
  36. tribalmemory/services/import_export.py +506 -0
  37. tribalmemory/services/memory.py +275 -0
  38. tribalmemory/services/vector_store.py +360 -0
  39. tribalmemory/testing/__init__.py +22 -0
  40. tribalmemory/testing/embedding_utils.py +110 -0
  41. tribalmemory/testing/fixtures.py +123 -0
  42. tribalmemory/testing/metrics.py +256 -0
  43. tribalmemory/testing/mocks.py +560 -0
  44. tribalmemory/testing/semantic_expansions.py +91 -0
  45. tribalmemory/utils.py +23 -0
  46. tribalmemory-0.1.0.dist-info/METADATA +275 -0
  47. tribalmemory-0.1.0.dist-info/RECORD +51 -0
  48. tribalmemory-0.1.0.dist-info/WHEEL +5 -0
  49. tribalmemory-0.1.0.dist-info/entry_points.txt +3 -0
  50. tribalmemory-0.1.0.dist-info/licenses/LICENSE +190 -0
  51. tribalmemory-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,285 @@
1
+ """Performance benchmarks for Tribal Memory.
2
+
3
+ Provides functions to measure retrieval latency, embedding throughput,
4
+ and cache effectiveness using mock services for CI-friendly execution.
5
+ """
6
+
7
+ import random
8
+ import statistics
9
+ import time
10
+ from dataclasses import dataclass
11
+
12
+ from ..testing.mocks import MockEmbeddingService, MockVectorStore
13
+ from .corpus_generator import CorpusConfig, generate_corpus
14
+
15
+
16
+ @dataclass
17
+ class LatencyStats:
18
+ """Latency percentile statistics in milliseconds."""
19
+ p50: float
20
+ p95: float
21
+ p99: float
22
+ mean: float
23
+ min: float
24
+ max: float
25
+
26
+
27
+ @dataclass
28
+ class BenchmarkResult:
29
+ """Result of a retrieval latency benchmark."""
30
+ corpus_size: int
31
+ num_queries: int
32
+ stats: LatencyStats
33
+
34
+
35
+ @dataclass
36
+ class ThroughputResult:
37
+ """Result of an embedding throughput benchmark."""
38
+ total_embeddings: int
39
+ total_time_ms: float
40
+ embeddings_per_second: float
41
+ batch_size: int
42
+
43
+
44
+ @dataclass
45
+ class CacheResult:
46
+ """Result of a cache effectiveness benchmark.
47
+
48
+ 'Cache hits' here means queries that were repeats of previously
49
+ seen queries (simulating a query cache). 'Cache misses' are
50
+ first-time queries that would require full embedding + retrieval.
51
+ """
52
+ total_queries: int
53
+ cache_hits: int # Repeated queries (would be served from cache)
54
+ cache_misses: int # First-seen queries (require full retrieval)
55
+ hit_rate: float
56
+ avg_hit_latency_ms: float
57
+ avg_miss_latency_ms: float
58
+
59
+
60
+ async def benchmark_retrieval_latency(
61
+ corpus_size: int = 1000,
62
+ num_queries: int = 50,
63
+ seed: int = 42,
64
+ ) -> BenchmarkResult:
65
+ """Benchmark retrieval latency at a given corpus size.
66
+
67
+ Populates a mock store with `corpus_size` entries, then
68
+ measures latency of `num_queries` random recall operations.
69
+
70
+ Args:
71
+ corpus_size: Number of memories to populate.
72
+ num_queries: Number of recall queries to measure.
73
+ seed: Random seed for reproducibility.
74
+
75
+ Returns:
76
+ BenchmarkResult with p50/p95/p99 latency stats.
77
+ """
78
+ rng = random.Random(seed)
79
+
80
+ # Use small embedding dimension for benchmark speed (the dimension
81
+ # doesn't affect retrieval algorithm complexity, just constant factors)
82
+ embedding_dim = 64
83
+ embedding_service = MockEmbeddingService(
84
+ embedding_dim=embedding_dim, skip_latency=True
85
+ )
86
+ vector_store = MockVectorStore(embedding_service)
87
+
88
+ # Populate corpus
89
+ corpus = generate_corpus(CorpusConfig(size=corpus_size, seed=seed))
90
+ for entry in corpus:
91
+ entry.embedding = await embedding_service.embed(entry.content)
92
+ await vector_store.store(entry)
93
+
94
+ # Generate query texts
95
+ queries = [
96
+ rng.choice(corpus).content[:50] # Use prefix of random entry
97
+ for _ in range(num_queries)
98
+ ]
99
+
100
+ # Pre-compute query embeddings (not part of latency measurement)
101
+ query_embeddings = [
102
+ await embedding_service.embed(q) for q in queries
103
+ ]
104
+
105
+ # Measure retrieval latencies (vector store only — the core path)
106
+ latencies: list[float] = []
107
+ for qe in query_embeddings:
108
+ start = time.perf_counter()
109
+ await vector_store.recall(qe, limit=5, min_similarity=0.1)
110
+ elapsed_ms = (time.perf_counter() - start) * 1000
111
+ latencies.append(elapsed_ms)
112
+
113
+ latencies.sort()
114
+ stats = LatencyStats(
115
+ p50=_percentile(latencies, 50),
116
+ p95=_percentile(latencies, 95),
117
+ p99=_percentile(latencies, 99),
118
+ mean=statistics.mean(latencies),
119
+ min=min(latencies),
120
+ max=max(latencies),
121
+ )
122
+
123
+ return BenchmarkResult(
124
+ corpus_size=corpus_size,
125
+ num_queries=num_queries,
126
+ stats=stats,
127
+ )
128
+
129
+
130
+ async def benchmark_batch_embedding_throughput(
131
+ num_texts: int = 500,
132
+ batch_size: int = 50,
133
+ seed: int = 42,
134
+ ) -> ThroughputResult:
135
+ """Benchmark embedding generation throughput.
136
+
137
+ Measures how many embeddings per second the service can produce,
138
+ comparing single vs batch modes.
139
+
140
+ Args:
141
+ num_texts: Total number of texts to embed.
142
+ batch_size: Size of each batch (1 for single mode).
143
+ seed: Random seed for reproducibility.
144
+
145
+ Returns:
146
+ ThroughputResult with throughput metrics.
147
+ """
148
+ service = MockEmbeddingService(embedding_dim=64, skip_latency=True)
149
+
150
+ # Generate texts
151
+ corpus = generate_corpus(CorpusConfig(size=num_texts, seed=seed))
152
+ texts = [entry.content for entry in corpus]
153
+
154
+ # Embed in batches
155
+ start = time.perf_counter()
156
+ for i in range(0, len(texts), batch_size):
157
+ batch = texts[i:i + batch_size]
158
+ await service.embed_batch(batch)
159
+ total_ms = (time.perf_counter() - start) * 1000
160
+
161
+ return ThroughputResult(
162
+ total_embeddings=num_texts,
163
+ total_time_ms=total_ms,
164
+ embeddings_per_second=num_texts / (total_ms / 1000) if total_ms > 0 else 0,
165
+ batch_size=batch_size,
166
+ )
167
+
168
+
169
+ async def benchmark_cache_effectiveness(
170
+ corpus_size: int = 500,
171
+ num_queries: int = 100,
172
+ repeat_ratio: float = 0.5,
173
+ seed: int = 42,
174
+ ) -> CacheResult:
175
+ """Benchmark query cache effectiveness.
176
+
177
+ Simulates a realistic workload with a mix of repeated and unique
178
+ queries, measuring cache hit rates and latency impact.
179
+
180
+ Args:
181
+ corpus_size: Number of memories in the store.
182
+ num_queries: Total number of queries to run.
183
+ repeat_ratio: Fraction of queries that are repeats (0.0-1.0).
184
+ seed: Random seed for reproducibility.
185
+
186
+ Returns:
187
+ CacheResult with hit rate and latency metrics.
188
+ """
189
+ rng = random.Random(seed)
190
+
191
+ # Use small embedding dimension for benchmark speed
192
+ embedding_dim = 64
193
+ embedding_service = MockEmbeddingService(
194
+ embedding_dim=embedding_dim, skip_latency=True
195
+ )
196
+ vector_store = MockVectorStore(embedding_service)
197
+
198
+ # Populate corpus
199
+ corpus = generate_corpus(CorpusConfig(size=corpus_size, seed=seed))
200
+ for entry in corpus:
201
+ entry.embedding = await embedding_service.embed(entry.content)
202
+ await vector_store.store(entry)
203
+
204
+ # Generate a pool of truly unique queries (sample without replacement)
205
+ pool_size = min(len(corpus), num_queries * 2)
206
+ unique_pool = rng.sample(corpus, k=pool_size)
207
+ unique_queries = list(dict.fromkeys(
208
+ entry.content[:50] for entry in unique_pool
209
+ )) # Deduplicate while preserving order
210
+
211
+ seen_queries: list[str] = [] # Ordered list for repeat selection
212
+ seen_set: set[str] = set()
213
+ cache_hits = 0
214
+ cache_misses = 0
215
+ hit_latencies: list[float] = []
216
+ miss_latencies: list[float] = []
217
+ unique_idx = 0 # Track position in unique pool (no replacement)
218
+
219
+ # Cache of embeddings to simulate cache behavior
220
+ embedding_cache: dict[str, list[float]] = {}
221
+
222
+ for i in range(num_queries):
223
+ if rng.random() < repeat_ratio and seen_queries:
224
+ # Pick a previously seen query (repeat)
225
+ query = rng.choice(seen_queries)
226
+ is_repeat = True
227
+ else:
228
+ # Pick next unique query (sequential, no replacement)
229
+ if unique_idx < len(unique_queries):
230
+ query = unique_queries[unique_idx]
231
+ unique_idx += 1
232
+ else:
233
+ # Exhausted unique pool, fall back to random
234
+ query = rng.choice(unique_queries)
235
+ is_repeat = query in seen_set
236
+
237
+ # Simulate cache: reuse embedding if seen before
238
+ if query in embedding_cache:
239
+ query_embedding = embedding_cache[query]
240
+ else:
241
+ query_embedding = await embedding_service.embed(query)
242
+ embedding_cache[query] = query_embedding
243
+
244
+ start = time.perf_counter()
245
+ await vector_store.recall(query_embedding, limit=5, min_similarity=0.3)
246
+ elapsed_ms = (time.perf_counter() - start) * 1000
247
+
248
+ if is_repeat:
249
+ cache_hits += 1
250
+ hit_latencies.append(elapsed_ms)
251
+ else:
252
+ cache_misses += 1
253
+ miss_latencies.append(elapsed_ms)
254
+
255
+ if query not in seen_set:
256
+ seen_queries.append(query)
257
+ seen_set.add(query)
258
+
259
+ hit_rate = cache_hits / num_queries if num_queries > 0 else 0.0
260
+
261
+ return CacheResult(
262
+ total_queries=num_queries,
263
+ cache_hits=cache_hits,
264
+ cache_misses=cache_misses,
265
+ hit_rate=hit_rate,
266
+ avg_hit_latency_ms=(
267
+ statistics.mean(hit_latencies) if hit_latencies else 0.0
268
+ ),
269
+ avg_miss_latency_ms=(
270
+ statistics.mean(miss_latencies) if miss_latencies else 0.0
271
+ ),
272
+ )
273
+
274
+
275
+ def _percentile(sorted_data: list[float], pct: int) -> float:
276
+ """Calculate percentile from sorted data using nearest-rank method.
277
+
278
+ Uses simple index-based lookup without interpolation. Sufficient
279
+ for benchmark reporting where exact percentile precision isn't critical.
280
+ """
281
+ if not sorted_data:
282
+ return 0.0
283
+ idx = int(len(sorted_data) * pct / 100)
284
+ idx = min(idx, len(sorted_data) - 1)
285
+ return sorted_data[idx]
@@ -0,0 +1,171 @@
1
+ """Synthetic corpus generator for scale/performance testing.
2
+
3
+ Generates realistic memory entries with varied content, tags,
4
+ and source types for benchmarking retrieval and storage.
5
+ """
6
+
7
+ import random
8
+ from dataclasses import dataclass
9
+ from typing import Optional
10
+
11
+ from ..interfaces import MemoryEntry, MemorySource
12
+
13
+
14
+ @dataclass
15
+ class CorpusConfig:
16
+ """Configuration for corpus generation."""
17
+ size: int = 1000
18
+ seed: Optional[int] = None
19
+ min_content_words: int = 5
20
+ max_content_words: int = 30
21
+
22
+
23
+ # Realistic memory content templates
24
+ _TEMPLATES = [
25
+ "User prefers {preference} for {domain}",
26
+ "Meeting with {person} scheduled for {time}",
27
+ "{person} mentioned they like {preference}",
28
+ "Project {project} uses {technology} for {purpose}",
29
+ "Important: {fact} about {topic}",
30
+ "User's {attribute} is {value}",
31
+ "{person} works at {company} on {project}",
32
+ "Reminder: {task} is due {time}",
33
+ "The {tool} configuration uses {setting}",
34
+ "Conversation about {topic} with {person}",
35
+ "{person} prefers {preference} over {alternative}",
36
+ "Bug in {project}: {description}",
37
+ "Decision: use {technology} for {purpose}",
38
+ "User asked about {topic} in the context of {domain}",
39
+ "Note: {fact} regarding {topic}",
40
+ ]
41
+
42
+ _PERSONS = [
43
+ "Joe", "Alice", "Bob", "Charlie", "Diana", "Eve",
44
+ "Frank", "Grace", "Hank", "Iris", "Jake", "Karen",
45
+ ]
46
+ _PREFERENCES = [
47
+ "dark mode", "TypeScript", "Python", "concise responses",
48
+ "morning meetings", "async communication", "vim", "VS Code",
49
+ "functional programming", "microservices", "monorepos",
50
+ "test-driven development", "pair programming", "remote work",
51
+ ]
52
+ _DOMAINS = [
53
+ "web development", "machine learning", "DevOps", "UI design",
54
+ "backend services", "data engineering", "mobile apps",
55
+ "cloud infrastructure", "security", "performance optimization",
56
+ ]
57
+ _PROJECTS = [
58
+ "Wally", "TribalMemory", "OpenClaw", "Dashboard",
59
+ "API Gateway", "Auth Service", "Analytics", "Notifications",
60
+ ]
61
+ _TECHNOLOGIES = [
62
+ "React", "FastAPI", "PostgreSQL", "Redis", "Docker",
63
+ "Kubernetes", "LanceDB", "OpenAI", "Tailscale", "Synapse",
64
+ ]
65
+ _TOPICS = [
66
+ "embedding models", "vector search", "memory portability",
67
+ "performance tuning", "caching strategies", "deduplication",
68
+ "schema migrations", "API versioning", "error handling",
69
+ "security best practices", "testing strategies", "CI/CD",
70
+ ]
71
+ _TAGS_POOL = [
72
+ "preferences", "meetings", "projects", "technical",
73
+ "personal", "work", "urgent", "low-priority",
74
+ "architecture", "bugs", "decisions", "reminders",
75
+ ]
76
+ _SOURCES = [
77
+ MemorySource.USER_EXPLICIT,
78
+ MemorySource.AUTO_CAPTURE,
79
+ MemorySource.CROSS_INSTANCE,
80
+ ]
81
+
82
+
83
+ def generate_corpus(config: Optional[CorpusConfig] = None) -> list[MemoryEntry]:
84
+ """Generate a synthetic corpus of memory entries.
85
+
86
+ Args:
87
+ config: Corpus generation configuration. Uses defaults if None.
88
+
89
+ Returns:
90
+ List of MemoryEntry objects with varied content and metadata.
91
+ """
92
+ config = config or CorpusConfig()
93
+ rng = random.Random(config.seed)
94
+
95
+ entries: list[MemoryEntry] = []
96
+ for _ in range(config.size):
97
+ template = rng.choice(_TEMPLATES)
98
+ content = _fill_template(template, rng)
99
+
100
+ tags = rng.sample(_TAGS_POOL, k=rng.randint(1, 3))
101
+ source = rng.choice(_SOURCES)
102
+
103
+ entry = MemoryEntry(
104
+ content=content,
105
+ tags=tags,
106
+ source_type=source,
107
+ source_instance=f"instance-{rng.randint(1, 5)}",
108
+ )
109
+ entries.append(entry)
110
+
111
+ return entries
112
+
113
+
114
+ def _fill_template(template: str, rng: random.Random) -> str:
115
+ """Fill a template with random realistic values."""
116
+ replacements = {
117
+ "{preference}": rng.choice(_PREFERENCES),
118
+ "{domain}": rng.choice(_DOMAINS),
119
+ "{person}": rng.choice(_PERSONS),
120
+ "{time}": rng.choice([
121
+ "next Monday", "tomorrow", "Friday afternoon",
122
+ "end of sprint", "Q2", "next week",
123
+ ]),
124
+ "{project}": rng.choice(_PROJECTS),
125
+ "{technology}": rng.choice(_TECHNOLOGIES),
126
+ "{purpose}": rng.choice([
127
+ "the backend", "testing", "deployment", "monitoring",
128
+ "data storage", "real-time updates", "authentication",
129
+ ]),
130
+ "{topic}": rng.choice(_TOPICS),
131
+ "{attribute}": rng.choice([
132
+ "timezone", "favorite language", "team", "role",
133
+ "preferred editor", "working hours",
134
+ ]),
135
+ "{value}": rng.choice([
136
+ "Mountain Time", "Python", "engineering", "senior dev",
137
+ "VS Code", "9am-5pm", "night owl hours",
138
+ ]),
139
+ "{company}": rng.choice([
140
+ "Google", "a startup", "Anthropic", "OpenAI",
141
+ "Meta", "a consulting firm",
142
+ ]),
143
+ "{tool}": rng.choice([
144
+ "Docker", "Kubernetes", "Nginx", "Redis",
145
+ "PostgreSQL", "LanceDB",
146
+ ]),
147
+ "{setting}": rng.choice([
148
+ "port 8080", "max_connections=100", "debug=false",
149
+ "cache_ttl=3600", "workers=4",
150
+ ]),
151
+ "{fact}": rng.choice([
152
+ "embeddings need normalization", "cache invalidation is hard",
153
+ "deadline was moved", "requirements changed",
154
+ "API rate limit is 100/min", "tests must pass before merge",
155
+ ]),
156
+ "{task}": rng.choice([
157
+ "code review", "deploy to staging", "update docs",
158
+ "run benchmarks", "fix flaky test", "merge PR",
159
+ ]),
160
+ "{alternative}": rng.choice(_PREFERENCES),
161
+ "{description}": rng.choice([
162
+ "query timeout under load", "missing error handler",
163
+ "incorrect cache key", "race condition in startup",
164
+ "memory leak in long sessions",
165
+ ]),
166
+ }
167
+
168
+ result = template
169
+ for key, value in replacements.items():
170
+ result = result.replace(key, value)
171
+ return result
@@ -0,0 +1 @@
1
+ """Portability module for embedding model metadata and bundle import/export."""