contextual-engine 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 (60) hide show
  1. contextual/__init__.py +18 -0
  2. contextual/__main__.py +11 -0
  3. contextual/cli.py +339 -0
  4. contextual/cli_docs.py +685 -0
  5. contextual/config.py +7 -0
  6. contextual/core/__init__.py +11 -0
  7. contextual/core/errors.py +470 -0
  8. contextual/core/models.py +590 -0
  9. contextual/docs/__init__.py +66 -0
  10. contextual/docs/chunker.py +550 -0
  11. contextual/docs/pipeline.py +513 -0
  12. contextual/docs/retrieval.py +654 -0
  13. contextual/docs/watcher.py +265 -0
  14. contextual/embedding/__init__.py +87 -0
  15. contextual/embedding/cache.py +455 -0
  16. contextual/embedding/embedder.py +414 -0
  17. contextual/embedding/helpers.py +252 -0
  18. contextual/git/__init__.py +22 -0
  19. contextual/git/blame.py +334 -0
  20. contextual/indexing/__init__.py +20 -0
  21. contextual/indexing/bug_sweep.py +119 -0
  22. contextual/indexing/chunker.py +691 -0
  23. contextual/indexing/embedder.py +271 -0
  24. contextual/indexing/file_watcher.py +154 -0
  25. contextual/indexing/incremental.py +260 -0
  26. contextual/indexing/index_writer.py +442 -0
  27. contextual/indexing/pipeline.py +438 -0
  28. contextual/indexing/processor.py +436 -0
  29. contextual/indexing/queries/readme.md +22 -0
  30. contextual/indexing/symbol_extractor.py +426 -0
  31. contextual/indexing/tokenizer.py +203 -0
  32. contextual/integrations/__init__.py +10 -0
  33. contextual/mcp/__init__.py +15 -0
  34. contextual/mcp/__main__.py +24 -0
  35. contextual/mcp/docs_tools.py +286 -0
  36. contextual/mcp/server.py +118 -0
  37. contextual/mcp/tools.py +443 -0
  38. contextual/observability/__init__.py +21 -0
  39. contextual/observability/logging.py +115 -0
  40. contextual/py.typed +0 -0
  41. contextual/retrieval/__init__.py +24 -0
  42. contextual/retrieval/context_assembler.py +372 -0
  43. contextual/retrieval/ranker.py +193 -0
  44. contextual/retrieval/search.py +548 -0
  45. contextual/security/__init__.py +52 -0
  46. contextual/security/paths.py +347 -0
  47. contextual/security/sanitize.py +349 -0
  48. contextual/security/workspace.py +348 -0
  49. contextual/storage/__init__.py +36 -0
  50. contextual/storage/fts_manager.py +273 -0
  51. contextual/storage/migration_v2.py +289 -0
  52. contextual/storage/migrations.py +316 -0
  53. contextual/storage/schema.py +210 -0
  54. contextual/storage/sqlite_pool.py +468 -0
  55. contextual/storage/vec0_manager.py +421 -0
  56. contextual_engine-0.1.0.dist-info/METADATA +297 -0
  57. contextual_engine-0.1.0.dist-info/RECORD +60 -0
  58. contextual_engine-0.1.0.dist-info/WHEEL +4 -0
  59. contextual_engine-0.1.0.dist-info/entry_points.txt +2 -0
  60. contextual_engine-0.1.0.dist-info/licenses/LICENSE +111 -0
@@ -0,0 +1,414 @@
1
+ """Production embedder using fastembed with jina-embeddings-v2-base-code.
2
+
3
+ This module provides a thread-safe, cached embedder singleton for code chunks.
4
+ Uses ONNX int8 quantization for efficient CPU inference.
5
+
6
+ Key features:
7
+ - Model: jina-embeddings-v2-base-code (768-dim)
8
+ - Batching: Up to 64 documents per batch
9
+ - Caching: Content-hash based deduplication
10
+ - Thread-safe: Singleton with lock protection
11
+ - Fast: ~18 docs/sec on modern CPU
12
+
13
+ Performance characteristics (from TECHNICAL_SPEC):
14
+ - Model size: ~165MB on disk
15
+ - Context window: 8,192 tokens
16
+ - Batch size: 64 @ 512 tokens
17
+ - Throughput: ~18 docs/sec (int8, 128 seq len)
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import hashlib
23
+ import threading
24
+ from typing import TYPE_CHECKING
25
+
26
+ from fastembed import TextEmbedding
27
+
28
+ from contextual.core.errors import ErrorCode, IndexingError, indexing_context
29
+ from contextual.observability.logging import get_logger
30
+
31
+ if TYPE_CHECKING:
32
+ pass
33
+
34
+
35
+ logger = get_logger(__name__)
36
+
37
+ # ============================================================================
38
+ # CONSTANTS (from TECHNICAL_SPEC §4)
39
+ # ============================================================================
40
+
41
+ MODEL_NAME = "jinaai/jina-embeddings-v2-base-code"
42
+ """jina-embeddings-v2-base-code - optimized for code understanding."""
43
+
44
+ DOCS_MODEL_NAME = "nomic-ai/nomic-embed-text-v1.5"
45
+ """nomic-embed-text-v1.5 - optimized for natural language text (docs)."""
46
+
47
+ EMBEDDING_DIM = 768
48
+ """Output dimensionality."""
49
+
50
+ BATCH_SIZE = 64
51
+ """Maximum documents per batch for optimal throughput."""
52
+
53
+ MAX_CONTEXT_LENGTH = 8192
54
+ """Maximum token context window."""
55
+
56
+
57
+ # ============================================================================
58
+ # EMBEDDER CLASS
59
+ # ============================================================================
60
+
61
+
62
+ class CodeEmbedder:
63
+ """Thread-safe code embedder using jina-v2-base-code.
64
+
65
+ Wraps fastembed's TextEmbedding with:
66
+ - Singleton pattern for model reuse
67
+ - Batched inference for efficiency
68
+ - Content-hash caching integration
69
+ - Thread-safe operations
70
+
71
+ The model is loaded once and reused across all embedding operations.
72
+ Model weights are cached by fastembed in ~/.cache/fastembed.
73
+
74
+ Attributes:
75
+ model: fastembed TextEmbedding instance
76
+ _lock: Threading lock for model access
77
+ """
78
+
79
+ def __init__(self) -> None:
80
+ """Initialize embedder and load model.
81
+
82
+ Downloads model on first use (~165MB).
83
+ Subsequent runs load from cache.
84
+
85
+ Raises:
86
+ IndexingError: If model loading fails.
87
+ """
88
+ self._lock = threading.Lock()
89
+
90
+ logger.info(
91
+ "Initializing code embedder",
92
+ model=MODEL_NAME,
93
+ dimensions=EMBEDDING_DIM,
94
+ )
95
+
96
+ try:
97
+ # Initialize fastembed TextEmbedding
98
+ # This downloads the model on first use
99
+ self.model = TextEmbedding(
100
+ model_name=MODEL_NAME,
101
+ max_length=MAX_CONTEXT_LENGTH,
102
+ )
103
+
104
+ logger.info(
105
+ "Code embedder initialized successfully",
106
+ model=MODEL_NAME,
107
+ )
108
+
109
+ except Exception as e:
110
+ msg = f"Failed to initialize embedder: {e}"
111
+ raise IndexingError(
112
+ code=ErrorCode.INDEXING_EMBEDDING_MODEL_LOAD_FAILED,
113
+ message=msg,
114
+ context=indexing_context(),
115
+ ) from e
116
+
117
+ def embed(self, texts: list[str]) -> list[list[float]]:
118
+ """Generate embeddings for a batch of texts.
119
+
120
+ This is the main entry point for embedding generation.
121
+ Handles batching automatically if input exceeds BATCH_SIZE.
122
+
123
+ Args:
124
+ texts: List of text strings to embed (code chunks, queries, etc.)
125
+
126
+ Returns:
127
+ List of 768-dimensional embedding vectors.
128
+
129
+ Raises:
130
+ IndexingError: If embedding generation fails.
131
+
132
+ Example:
133
+ >>> embedder = get_embedder()
134
+ >>> chunks = ["def parse():", "class Config:", "import json"]
135
+ >>> embeddings = embedder.embed(chunks)
136
+ >>> len(embeddings), len(embeddings[0])
137
+ (3, 768)
138
+ """
139
+ if not texts:
140
+ return []
141
+
142
+ try:
143
+ # Process in batches if necessary
144
+ all_embeddings = []
145
+
146
+ for i in range(0, len(texts), BATCH_SIZE):
147
+ batch = texts[i : i + BATCH_SIZE]
148
+
149
+ logger.debug(
150
+ "Embedding batch",
151
+ batch_size=len(batch),
152
+ batch_index=i // BATCH_SIZE,
153
+ )
154
+
155
+ # fastembed returns a generator - convert to list
156
+ with self._lock:
157
+ batch_embeddings = list(self.model.embed(batch))
158
+
159
+ # Validate dimensions
160
+ for idx, emb in enumerate(batch_embeddings):
161
+ if len(emb) != EMBEDDING_DIM:
162
+ msg = (
163
+ f"Unexpected embedding dimension: "
164
+ f"expected {EMBEDDING_DIM}, got {len(emb)}"
165
+ )
166
+ raise IndexingError(
167
+ code=ErrorCode.INDEXING_EMBEDDING_INFERENCE_FAILED,
168
+ message=msg,
169
+ context=indexing_context(),
170
+ )
171
+
172
+ all_embeddings.extend(batch_embeddings)
173
+
174
+ logger.info(
175
+ "Embeddings generated",
176
+ count=len(texts),
177
+ batches=len(range(0, len(texts), BATCH_SIZE)),
178
+ )
179
+
180
+ return [list(emb) for emb in all_embeddings]
181
+
182
+ except IndexingError:
183
+ # Re-raise our own errors
184
+ raise
185
+ except Exception as e:
186
+ msg = f"Embedding inference failed: {e}"
187
+ raise IndexingError(
188
+ code=ErrorCode.INDEXING_EMBEDDING_INFERENCE_FAILED,
189
+ message=msg,
190
+ context=indexing_context(),
191
+ ) from e
192
+
193
+ def embed_query(self, query: str) -> list[float]:
194
+ """Embed a single query string.
195
+
196
+ Convenience method for embedding search queries.
197
+ jina-v2-code is symmetric, so no special query prefix needed.
198
+
199
+ Args:
200
+ query: Query string to embed.
201
+
202
+ Returns:
203
+ 768-dimensional embedding vector.
204
+
205
+ Example:
206
+ >>> embedder = get_embedder()
207
+ >>> query_vec = embedder.embed_query("parse JSON configuration")
208
+ >>> len(query_vec)
209
+ 768
210
+ """
211
+ return self.embed([query])[0]
212
+
213
+ def compute_content_hash(self, text: str) -> str:
214
+ """Compute SHA-256 hash of text for caching.
215
+
216
+ Used to deduplicate embeddings - identical content gets same hash.
217
+
218
+ Args:
219
+ text: Text content to hash.
220
+
221
+ Returns:
222
+ Hex-encoded SHA-256 hash (64 chars).
223
+ """
224
+ return hashlib.sha256(text.encode("utf-8")).hexdigest()
225
+
226
+
227
+ class DocsEmbedder:
228
+ """Thread-safe docs embedder using nomic-embed-text-v1.5.
229
+
230
+ Identical to CodeEmbedder in behavior (batching, locking, dimension checks).
231
+ Only difference is the underlying model name.
232
+ """
233
+
234
+ def __init__(self) -> None:
235
+ self._lock = threading.Lock()
236
+
237
+ logger.info(
238
+ "Initializing docs embedder",
239
+ model=DOCS_MODEL_NAME,
240
+ dimensions=EMBEDDING_DIM,
241
+ )
242
+
243
+ try:
244
+ self.model = TextEmbedding(
245
+ model_name=DOCS_MODEL_NAME,
246
+ max_length=MAX_CONTEXT_LENGTH,
247
+ )
248
+
249
+ logger.info(
250
+ "Docs embedder initialized successfully",
251
+ model=DOCS_MODEL_NAME,
252
+ )
253
+
254
+ except Exception as e:
255
+ msg = f"Failed to initialize embedder: {e}"
256
+ raise IndexingError(
257
+ code=ErrorCode.INDEXING_EMBEDDING_MODEL_LOAD_FAILED,
258
+ message=msg,
259
+ context=indexing_context(),
260
+ ) from e
261
+
262
+ def embed(self, texts: list[str]) -> list[list[float]]:
263
+ if not texts:
264
+ return []
265
+
266
+ try:
267
+ all_embeddings = []
268
+
269
+ for i in range(0, len(texts), BATCH_SIZE):
270
+ batch = texts[i : i + BATCH_SIZE]
271
+
272
+ logger.debug(
273
+ "Embedding batch",
274
+ batch_size=len(batch),
275
+ batch_index=i // BATCH_SIZE,
276
+ )
277
+
278
+ with self._lock:
279
+ batch_embeddings = list(self.model.embed(batch))
280
+
281
+ for emb in batch_embeddings:
282
+ if len(emb) != EMBEDDING_DIM:
283
+ msg = (
284
+ f"Unexpected embedding dimension: "
285
+ f"expected {EMBEDDING_DIM}, got {len(emb)}"
286
+ )
287
+ raise IndexingError(
288
+ code=ErrorCode.INDEXING_EMBEDDING_INFERENCE_FAILED,
289
+ message=msg,
290
+ context=indexing_context(),
291
+ )
292
+
293
+ all_embeddings.extend(batch_embeddings)
294
+
295
+ logger.info(
296
+ "Embeddings generated",
297
+ count=len(texts),
298
+ batches=len(range(0, len(texts), BATCH_SIZE)),
299
+ )
300
+
301
+ return [list(emb) for emb in all_embeddings]
302
+
303
+ except IndexingError:
304
+ raise
305
+ except Exception as e:
306
+ msg = f"Embedding inference failed: {e}"
307
+ raise IndexingError(
308
+ code=ErrorCode.INDEXING_EMBEDDING_INFERENCE_FAILED,
309
+ message=msg,
310
+ context=indexing_context(),
311
+ ) from e
312
+
313
+ def embed_query(self, query: str) -> list[float]:
314
+ return self.embed([query])[0]
315
+
316
+ def compute_content_hash(self, text: str) -> str:
317
+ return hashlib.sha256(text.encode("utf-8")).hexdigest()
318
+
319
+
320
+ # ============================================================================
321
+ # SINGLETON PATTERN
322
+ # ============================================================================
323
+
324
+ _embedder: CodeEmbedder | None = None
325
+ _embedder_lock = threading.Lock()
326
+
327
+ _docs_embedder: DocsEmbedder | None = None
328
+ _docs_embedder_lock = threading.Lock()
329
+
330
+
331
+ def get_embedder() -> CodeEmbedder:
332
+ """Get or create the global embedder singleton.
333
+
334
+ Thread-safe lazy initialization. The embedder is created once
335
+ on first call and reused for all subsequent calls.
336
+
337
+ Returns:
338
+ Shared CodeEmbedder instance.
339
+
340
+ Example:
341
+ >>> embedder1 = get_embedder()
342
+ >>> embedder2 = get_embedder()
343
+ >>> embedder1 is embedder2
344
+ True
345
+ """
346
+ global _embedder
347
+
348
+ if _embedder is None:
349
+ with _embedder_lock:
350
+ # Double-check pattern
351
+ if _embedder is None:
352
+ _embedder = CodeEmbedder()
353
+
354
+ return _embedder
355
+
356
+
357
+ def get_docs_embedder() -> DocsEmbedder:
358
+ """Get or create the global docs embedder singleton."""
359
+ global _docs_embedder
360
+
361
+ if _docs_embedder is None:
362
+ with _docs_embedder_lock:
363
+ if _docs_embedder is None:
364
+ _docs_embedder = DocsEmbedder()
365
+
366
+ return _docs_embedder
367
+
368
+
369
+ def reset_embedder() -> None:
370
+ """Reset the global embedder singleton.
371
+
372
+ Used for testing or to force model reload.
373
+ Should NOT be called in production code.
374
+ """
375
+ global _embedder
376
+ with _embedder_lock:
377
+ _embedder = None
378
+ logger.warning("Embedder singleton reset")
379
+
380
+
381
+ # ============================================================================
382
+ # BATCH EMBEDDING HELPERS
383
+ # ============================================================================
384
+
385
+
386
+ def embed_with_hashes(
387
+ texts: list[str],
388
+ ) -> list[tuple[str, list[float]]]:
389
+ """Embed texts and return (content_hash, embedding) pairs.
390
+
391
+ Useful for populating embedding cache or detecting duplicates.
392
+
393
+ Args:
394
+ texts: Texts to embed.
395
+
396
+ Returns:
397
+ List of (content_hash, embedding) tuples.
398
+
399
+ Example:
400
+ >>> embedder = get_embedder()
401
+ >>> results = embed_with_hashes(["def foo():", "class Bar:"])
402
+ >>> hash1, emb1 = results[0]
403
+ >>> len(hash1), len(emb1)
404
+ (64, 768)
405
+ """
406
+ embedder = get_embedder()
407
+
408
+ # Generate embeddings
409
+ embeddings = embedder.embed(texts)
410
+
411
+ # Compute hashes
412
+ hashes = [embedder.compute_content_hash(text) for text in texts]
413
+
414
+ return list(zip(hashes, embeddings, strict=False))
@@ -0,0 +1,252 @@
1
+ """High-level embedding API with caching integration.
2
+
3
+ Provides convenience functions that combine embedder + cache for common workflows:
4
+ - embed_chunks_with_cache: Batch embed with cache lookup/write
5
+ - embed_query: Simple query embedding (no cache)
6
+ - warm_cache: Pre-populate cache from database
7
+
8
+ These are the recommended entry points for indexing and search operations.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import TYPE_CHECKING
14
+
15
+ from contextual.embedding.cache import (
16
+ get_cached_embeddings_batch,
17
+ put_cached_embeddings_batch,
18
+ )
19
+ from contextual.embedding.embedder import MODEL_NAME, get_embedder
20
+ from contextual.observability.logging import get_logger
21
+
22
+ if TYPE_CHECKING:
23
+ from sqlite3 import Connection
24
+
25
+
26
+ logger = get_logger(__name__)
27
+
28
+
29
+ # ============================================================================
30
+ # CACHED EMBEDDING WORKFLOW
31
+ # ============================================================================
32
+
33
+
34
+ def embed_chunks(
35
+ conn: Connection,
36
+ chunks: list[tuple[str, str]],
37
+ ) -> list[tuple[str, list[float]]]:
38
+ """Embed chunks with cache lookup and write.
39
+
40
+ This is the main entry point for indexing pipelines.
41
+
42
+ Workflow:
43
+ 1. Extract content hashes from chunks
44
+ 2. Lookup cached embeddings in batch
45
+ 3. Generate embeddings only for cache misses
46
+ 4. Write new embeddings to cache
47
+ 5. Return all embeddings in original order
48
+
49
+ Args:
50
+ conn: SQLite connection (for cache operations).
51
+ chunks: List of (content_hash, text) tuples.
52
+
53
+ Returns:
54
+ List of (content_hash, embedding) tuples in original order.
55
+
56
+ Example:
57
+ >>> chunks = [
58
+ ... ("abc123", "def parse_json():"),
59
+ ... ("def456", "class Config:"),
60
+ ... ]
61
+ >>> results = embed_chunks(conn, chunks)
62
+ >>> len(results)
63
+ 2
64
+ >>> hash1, emb1 = results[0]
65
+ >>> len(emb1)
66
+ 768
67
+ """
68
+ if not chunks:
69
+ return []
70
+
71
+ embedder = get_embedder()
72
+
73
+ # Extract hashes and texts
74
+ hashes = [h for h, _ in chunks]
75
+ texts = [t for _, t in chunks]
76
+
77
+ # Batch cache lookup
78
+ cached = get_cached_embeddings_batch(conn, hashes, MODEL_NAME)
79
+
80
+ # Identify cache misses
81
+ miss_indices = [i for i, h in enumerate(hashes) if h not in cached]
82
+ miss_texts = [texts[i] for i in miss_indices]
83
+ miss_hashes = [hashes[i] for i in miss_indices]
84
+
85
+ logger.info(
86
+ "Embedding with cache",
87
+ total=len(chunks),
88
+ cached=len(cached),
89
+ misses=len(miss_indices),
90
+ hit_rate=len(cached) / len(chunks) if chunks else 0,
91
+ )
92
+
93
+ # Generate embeddings for misses
94
+ new_embeddings = {}
95
+ if miss_texts:
96
+ logger.debug("Generating embeddings for cache misses", count=len(miss_texts))
97
+
98
+ fresh_embeddings = embedder.embed(miss_texts)
99
+
100
+ # Build miss hash -> embedding map
101
+ for hash_val, embedding in zip(miss_hashes, fresh_embeddings, strict=False):
102
+ new_embeddings[hash_val] = embedding
103
+
104
+ # Write to cache
105
+ cache_entries = list(zip(miss_hashes, fresh_embeddings, strict=False))
106
+ put_cached_embeddings_batch(conn, cache_entries, MODEL_NAME)
107
+
108
+ # Combine cached + new in original order
109
+ results = []
110
+ for hash_val in hashes:
111
+ if hash_val in cached:
112
+ embedding = cached[hash_val]
113
+ else:
114
+ embedding = new_embeddings[hash_val]
115
+
116
+ results.append((hash_val, embedding))
117
+
118
+ return results
119
+
120
+
121
+ def embed_query_simple(query: str) -> list[float]:
122
+ """Embed a search query (no caching).
123
+
124
+ Queries are not cached because:
125
+ 1. They are unique and rarely repeated
126
+ 2. Cache lookup overhead > embedding time for single item
127
+ 3. Query embedding is already fast (~55ms)
128
+
129
+ Args:
130
+ query: Query string.
131
+
132
+ Returns:
133
+ 768-dim embedding vector.
134
+
135
+ Example:
136
+ >>> embedding = embed_query_simple("parse JSON configuration")
137
+ >>> len(embedding)
138
+ 768
139
+ """
140
+ embedder = get_embedder()
141
+ return embedder.embed_query(query)
142
+
143
+
144
+ # ============================================================================
145
+ # CACHE WARMING
146
+ # ============================================================================
147
+
148
+
149
+ def warm_cache_from_chunks(
150
+ conn: Connection,
151
+ chunk_ids: list[int] | None = None,
152
+ ) -> int:
153
+ """Pre-populate embedding cache from existing chunks.
154
+
155
+ Useful after:
156
+ - Fresh database creation
157
+ - Cache cleared
158
+ - Model upgrade
159
+
160
+ Args:
161
+ conn: SQLite connection.
162
+ chunk_ids: Specific chunk IDs to warm, or None for all.
163
+
164
+ Returns:
165
+ Number of embeddings cached.
166
+
167
+ Example:
168
+ >>> count = warm_cache_from_chunks(conn)
169
+ >>> print(f"Cached {count} embeddings")
170
+ """
171
+ try:
172
+ cursor = conn.cursor()
173
+
174
+ # Fetch chunks
175
+ if chunk_ids:
176
+ placeholders = ",".join("?" * len(chunk_ids))
177
+ query = f"""
178
+ SELECT content_hash, content
179
+ FROM chunks
180
+ WHERE id IN ({placeholders})
181
+ AND content_hash IS NOT NULL
182
+ """
183
+ cursor.execute(query, chunk_ids)
184
+ else:
185
+ query = """
186
+ SELECT content_hash, content
187
+ FROM chunks
188
+ WHERE content_hash IS NOT NULL
189
+ """
190
+ cursor.execute(query)
191
+
192
+ rows = cursor.fetchall()
193
+ chunks = [(h, c) for h, c in rows]
194
+
195
+ logger.info("Warming cache from chunks", chunk_count=len(chunks))
196
+
197
+ # Use cached embedding workflow
198
+ # (will skip already-cached entries automatically)
199
+ results = embed_chunks(conn, chunks)
200
+
201
+ return len(results)
202
+
203
+ except Exception as e:
204
+ logger.error("Cache warming failed", error=str(e))
205
+ return 0
206
+
207
+
208
+ # ============================================================================
209
+ # STATISTICS
210
+ # ============================================================================
211
+
212
+
213
+ def get_embedding_stats(conn: Connection) -> dict[str, int | float]:
214
+ """Get embedding and cache statistics.
215
+
216
+ Returns:
217
+ Dict with cache stats and efficiency metrics.
218
+
219
+ Example:
220
+ >>> stats = get_embedding_stats(conn)
221
+ >>> stats
222
+ {
223
+ 'cache_entries': 10000,
224
+ 'cache_hits': 45000,
225
+ 'cache_hit_rate': 0.82,
226
+ 'estimated_time_saved_hours': 12.5
227
+ }
228
+ """
229
+ from contextual.embedding.cache import get_cache_stats
230
+
231
+ cache_stats = get_cache_stats(conn)
232
+
233
+ # Calculate efficiency metrics
234
+ total_entries = cache_stats["total_entries"]
235
+ total_hits = cache_stats["total_hits"]
236
+
237
+ # Assume ~55ms per embedding generation
238
+ # Cache hit saves ~55ms, lookup costs ~0.1ms
239
+ time_saved_ms = total_hits * 55
240
+ time_saved_hours = time_saved_ms / 1000 / 60 / 60
241
+
242
+ cache_hit_rate = 0.0
243
+ if total_entries + total_hits > 0:
244
+ cache_hit_rate = total_hits / (total_entries + total_hits)
245
+
246
+ return {
247
+ "cache_entries": total_entries,
248
+ "cache_hits": total_hits,
249
+ "cache_hit_rate": round(cache_hit_rate, 3),
250
+ "estimated_time_saved_hours": round(time_saved_hours, 2),
251
+ "models": cache_stats["models"],
252
+ }
@@ -0,0 +1,22 @@
1
+ """Git integration module for temporal code attribution.
2
+
3
+ Public API:
4
+ - GitRepository: Wrapper for git operations
5
+ - BlameInfo: Line range attribution
6
+ - ChunkAttribution: Aggregated chunk metadata
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from contextual.git.blame import (
12
+ BlameInfo,
13
+ ChunkAttribution,
14
+ GitRepository,
15
+ )
16
+
17
+
18
+ __all__ = [
19
+ "BlameInfo",
20
+ "ChunkAttribution",
21
+ "GitRepository",
22
+ ]