RouteKitAI 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 (64) hide show
  1. routekitai/__init__.py +53 -0
  2. routekitai/cli/__init__.py +18 -0
  3. routekitai/cli/main.py +40 -0
  4. routekitai/cli/replay.py +80 -0
  5. routekitai/cli/run.py +95 -0
  6. routekitai/cli/serve.py +966 -0
  7. routekitai/cli/test_agent.py +178 -0
  8. routekitai/cli/trace.py +209 -0
  9. routekitai/cli/trace_analyze.py +120 -0
  10. routekitai/cli/trace_search.py +126 -0
  11. routekitai/core/__init__.py +58 -0
  12. routekitai/core/agent.py +325 -0
  13. routekitai/core/errors.py +49 -0
  14. routekitai/core/hooks.py +174 -0
  15. routekitai/core/memory.py +54 -0
  16. routekitai/core/message.py +132 -0
  17. routekitai/core/model.py +91 -0
  18. routekitai/core/policies.py +373 -0
  19. routekitai/core/policy.py +85 -0
  20. routekitai/core/policy_adapter.py +133 -0
  21. routekitai/core/runtime.py +1403 -0
  22. routekitai/core/tool.py +148 -0
  23. routekitai/core/tools.py +180 -0
  24. routekitai/evals/__init__.py +13 -0
  25. routekitai/evals/dataset.py +75 -0
  26. routekitai/evals/metrics.py +101 -0
  27. routekitai/evals/runner.py +184 -0
  28. routekitai/graphs/__init__.py +12 -0
  29. routekitai/graphs/executors.py +457 -0
  30. routekitai/graphs/graph.py +164 -0
  31. routekitai/memory/__init__.py +13 -0
  32. routekitai/memory/episodic.py +242 -0
  33. routekitai/memory/kv.py +34 -0
  34. routekitai/memory/retrieval.py +192 -0
  35. routekitai/memory/vector.py +700 -0
  36. routekitai/memory/working.py +66 -0
  37. routekitai/message.py +29 -0
  38. routekitai/model.py +48 -0
  39. routekitai/observability/__init__.py +21 -0
  40. routekitai/observability/analyzer.py +314 -0
  41. routekitai/observability/exporters/__init__.py +10 -0
  42. routekitai/observability/exporters/base.py +30 -0
  43. routekitai/observability/exporters/jsonl.py +81 -0
  44. routekitai/observability/exporters/otel.py +119 -0
  45. routekitai/observability/spans.py +111 -0
  46. routekitai/observability/streaming.py +117 -0
  47. routekitai/observability/trace.py +144 -0
  48. routekitai/providers/__init__.py +9 -0
  49. routekitai/providers/anthropic.py +227 -0
  50. routekitai/providers/azure_openai.py +243 -0
  51. routekitai/providers/local.py +196 -0
  52. routekitai/providers/openai.py +321 -0
  53. routekitai/py.typed +0 -0
  54. routekitai/sandbox/__init__.py +12 -0
  55. routekitai/sandbox/filesystem.py +131 -0
  56. routekitai/sandbox/network.py +142 -0
  57. routekitai/sandbox/permissions.py +70 -0
  58. routekitai/tool.py +33 -0
  59. routekitai-0.1.0.dist-info/METADATA +328 -0
  60. routekitai-0.1.0.dist-info/RECORD +64 -0
  61. routekitai-0.1.0.dist-info/WHEEL +5 -0
  62. routekitai-0.1.0.dist-info/entry_points.txt +2 -0
  63. routekitai-0.1.0.dist-info/licenses/LICENSE +21 -0
  64. routekitai-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,700 @@
1
+ """Vector memory for semantic search with efficient similarity search.
2
+
3
+ This module provides a production-ready vector memory implementation with:
4
+ - Support for multiple embedding backends (sentence-transformers, OpenAI, custom)
5
+ - Efficient similarity search using cosine similarity with optional FAISS indexing
6
+ - Batch operations for performance
7
+ - Metadata filtering and hybrid search
8
+ - Persistence support
9
+ """
10
+
11
+ import math
12
+ import pickle
13
+ import uuid
14
+ from collections import defaultdict
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ from pydantic import BaseModel, Field
19
+
20
+ from routekitai.core.errors import RuntimeError as RouteKitRuntimeError
21
+
22
+
23
+ class VectorMemoryError(RouteKitRuntimeError):
24
+ """Error raised by vector memory operations."""
25
+
26
+ pass
27
+
28
+
29
+ class EmbeddingBackend:
30
+ """Abstract embedding backend interface."""
31
+
32
+ def embed(self, text: str) -> list[float]:
33
+ """Generate embedding for a single text.
34
+
35
+ Args:
36
+ text: Input text
37
+
38
+ Returns:
39
+ Embedding vector as list of floats
40
+ """
41
+ raise NotImplementedError
42
+
43
+ def embed_batch(self, texts: list[str]) -> list[list[float]]:
44
+ """Generate embeddings for multiple texts (optional optimization).
45
+
46
+ Args:
47
+ texts: List of input texts
48
+
49
+ Returns:
50
+ List of embedding vectors
51
+ """
52
+ return [self.embed(text) for text in texts]
53
+
54
+ @property
55
+ def dimension(self) -> int:
56
+ """Return the embedding dimension."""
57
+ raise NotImplementedError
58
+
59
+
60
+ class SimpleEmbeddingBackend(EmbeddingBackend):
61
+ """Simple TF-IDF-like embedding backend for MVP (no external dependencies).
62
+
63
+ Uses character-level n-grams and frequency-based weighting.
64
+ For production, use SentenceTransformerBackend or OpenAIBackend.
65
+ """
66
+
67
+ def __init__(self, dimension: int = 384, ngram_size: int = 3) -> None:
68
+ """Initialize simple embedding backend.
69
+
70
+ Args:
71
+ dimension: Embedding dimension
72
+ ngram_size: N-gram size for character-level features
73
+ """
74
+ self._dimension = dimension
75
+ self.ngram_size = ngram_size
76
+ self._vocab: dict[str, int] = {}
77
+ self._idf: dict[str, float] = {}
78
+ self._document_count = 0
79
+ self._document_ngrams: list[set[str]] = []
80
+
81
+ def _extract_ngrams(self, text: str) -> list[str]:
82
+ """Extract character n-grams from text."""
83
+ text_lower = text.lower()
84
+ ngrams = []
85
+ for i in range(len(text_lower) - self.ngram_size + 1):
86
+ ngram = text_lower[i : i + self.ngram_size]
87
+ ngrams.append(ngram)
88
+ return ngrams
89
+
90
+ def _build_vocab(self, texts: list[str]) -> None:
91
+ """Build vocabulary from texts."""
92
+ ngram_counts: dict[str, int] = defaultdict(int)
93
+ for text in texts:
94
+ ngrams = set(self._extract_ngrams(text))
95
+ self._document_ngrams.append(ngrams)
96
+ for ngram in ngrams:
97
+ ngram_counts[ngram] += 1
98
+
99
+ # Build vocab with most frequent n-grams
100
+ sorted_ngrams = sorted(ngram_counts.items(), key=lambda x: x[1], reverse=True)
101
+ self._vocab = {
102
+ ngram: idx for idx, (ngram, _) in enumerate(sorted_ngrams[: self._dimension])
103
+ }
104
+
105
+ # Calculate IDF
106
+ self._document_count = len(texts)
107
+ for ngram in self._vocab.keys():
108
+ doc_freq = sum(1 for doc_ngrams in self._document_ngrams if ngram in doc_ngrams)
109
+ self._idf[ngram] = (
110
+ math.log((self._document_count + 1) / (doc_freq + 1)) if doc_freq > 0 else 0.0
111
+ )
112
+
113
+ def embed(self, text: str) -> list[float]:
114
+ """Generate embedding using TF-IDF on character n-grams."""
115
+ if not self._vocab:
116
+ # Initialize with empty vocab if not trained
117
+ return [0.0] * self._dimension
118
+
119
+ ngrams = self._extract_ngrams(text)
120
+ ngram_counts: dict[str, int] = defaultdict(int)
121
+ for ngram in ngrams:
122
+ if ngram in self._vocab:
123
+ ngram_counts[ngram] += 1
124
+
125
+ # Build TF-IDF vector
126
+ vector = [0.0] * self._dimension
127
+ total_ngrams = len(ngrams) if ngrams else 1
128
+
129
+ for ngram, count in ngram_counts.items():
130
+ if ngram in self._vocab:
131
+ idx = self._vocab[ngram]
132
+ tf = count / total_ngrams
133
+ idf = self._idf.get(ngram, 0.0)
134
+ vector[idx] = tf * idf
135
+
136
+ # L2 normalization (important for cosine similarity)
137
+ norm = math.sqrt(sum(x * x for x in vector))
138
+ if norm > 0:
139
+ vector = [x / norm for x in vector]
140
+ else:
141
+ # If all zeros, return a small uniform vector to avoid division issues
142
+ vector = [1.0 / math.sqrt(self._dimension)] * self._dimension
143
+
144
+ return vector
145
+
146
+ def embed_batch(self, texts: list[str]) -> list[list[float]]:
147
+ """Generate embeddings for batch with vocabulary building."""
148
+ if not self._vocab and texts:
149
+ # Build vocab from batch
150
+ self._build_vocab(texts)
151
+
152
+ return [self.embed(text) for text in texts]
153
+
154
+ @property
155
+ def dimension(self) -> int:
156
+ """Return embedding dimension."""
157
+ return self._dimension
158
+
159
+
160
+ class VectorMemory(BaseModel):
161
+ """Production-ready vector memory with efficient similarity search.
162
+
163
+ Features:
164
+ - Multiple embedding backends (simple, sentence-transformers, OpenAI)
165
+ - Efficient cosine similarity search
166
+ - Optional FAISS indexing for large-scale search
167
+ - Metadata filtering
168
+ - Batch operations
169
+ - Persistence support
170
+ """
171
+
172
+ dimension: int = Field(default=384, description="Vector dimension")
173
+ backend: str = Field(
174
+ default="simple", description="Embedding backend (simple, sentence-transformers, openai)"
175
+ )
176
+ use_faiss: bool = Field(default=False, description="Use FAISS for large-scale indexing")
177
+ persist_path: Path | None = Field(default=None, description="Path to persist vectors")
178
+ similarity_threshold: float = Field(
179
+ default=0.0, description="Minimum similarity threshold for search"
180
+ )
181
+
182
+ def __init__(self, **data: Any) -> None:
183
+ """Initialize vector memory."""
184
+ super().__init__(**data)
185
+ self._vectors: dict[str, dict[str, Any]] = {}
186
+ self._embeddings: dict[str, list[float]] = {}
187
+ self._embedding_backend: EmbeddingBackend | None = None
188
+ self._faiss_index: Any = None # FAISS index if available
189
+ self._id_to_vector_id: dict[int, str] = {} # Map FAISS index to vector ID
190
+
191
+ def _get_embedding_backend(self) -> EmbeddingBackend:
192
+ """Get or create embedding backend."""
193
+ if self._embedding_backend is None:
194
+ if self.backend == "simple":
195
+ self._embedding_backend = SimpleEmbeddingBackend(dimension=self.dimension)
196
+ elif self.backend == "sentence-transformers":
197
+ try:
198
+ from sentence_transformers import SentenceTransformer
199
+
200
+ model = SentenceTransformer("all-MiniLM-L6-v2")
201
+ self._embedding_backend = SentenceTransformerBackend(model)
202
+ except ImportError:
203
+ raise VectorMemoryError(
204
+ "sentence-transformers not installed. Install with: pip install sentence-transformers",
205
+ context={"backend": self.backend},
206
+ ) from None
207
+ elif self.backend == "openai":
208
+ try:
209
+ import os
210
+
211
+ api_key = os.getenv("OPENAI_API_KEY")
212
+ if not api_key:
213
+ raise VectorMemoryError(
214
+ "OPENAI_API_KEY environment variable not set",
215
+ context={"backend": self.backend},
216
+ )
217
+ self._embedding_backend = OpenAIBackend(
218
+ api_key=api_key, dimension=self.dimension
219
+ )
220
+ except ImportError:
221
+ raise VectorMemoryError(
222
+ "openai package not installed. Install with: pip install openai",
223
+ context={"backend": self.backend},
224
+ ) from None
225
+ else:
226
+ raise VectorMemoryError(
227
+ f"Unknown embedding backend: {self.backend}",
228
+ context={
229
+ "backend": self.backend,
230
+ "available": ["simple", "sentence-transformers", "openai"],
231
+ },
232
+ )
233
+ return self._embedding_backend
234
+
235
+ def _cosine_similarity(self, vec1: list[float], vec2: list[float]) -> float:
236
+ """Calculate cosine similarity between two vectors.
237
+
238
+ Args:
239
+ vec1: First vector
240
+ vec2: Second vector
241
+
242
+ Returns:
243
+ Cosine similarity score between -1 and 1
244
+ """
245
+ if len(vec1) != len(vec2):
246
+ return 0.0
247
+
248
+ dot_product = sum(a * b for a, b in zip(vec1, vec2, strict=True))
249
+ norm1 = math.sqrt(sum(a * a for a in vec1))
250
+ norm2 = math.sqrt(sum(a * a for a in vec2))
251
+
252
+ if norm1 == 0 or norm2 == 0:
253
+ return 0.0
254
+
255
+ return dot_product / (norm1 * norm2)
256
+
257
+ def _initialize_faiss(self) -> None:
258
+ """Initialize FAISS index if available and enabled."""
259
+ if not self.use_faiss:
260
+ return
261
+
262
+ try:
263
+ import faiss # noqa: F401
264
+ import numpy as np # noqa: F401
265
+ except ImportError as e:
266
+ # FAISS or numpy not available, fall back to linear search
267
+ self.use_faiss = False
268
+ import logging
269
+
270
+ logger = logging.getLogger(__name__)
271
+ logger.warning(
272
+ f"FAISS not available ({e}). Falling back to linear search. "
273
+ "Install with: pip install faiss-cpu numpy"
274
+ )
275
+ return
276
+
277
+ # Use L2 distance index (we'll convert to cosine similarity)
278
+ self._faiss_index = faiss.IndexFlatL2(self.dimension)
279
+
280
+ async def add(self, text: str, metadata: dict[str, Any] | None = None) -> str:
281
+ """Add text to vector memory with automatic embedding.
282
+
283
+ Args:
284
+ text: Text to embed and store
285
+ metadata: Optional metadata dictionary
286
+
287
+ Returns:
288
+ ID of stored vector
289
+ """
290
+ if not text.strip():
291
+ raise VectorMemoryError("Cannot add empty text to vector memory")
292
+
293
+ vector_id = str(uuid.uuid4())
294
+ backend = self._get_embedding_backend()
295
+
296
+ # For simple backend, rebuild vocab with all texts (existing + new)
297
+ if isinstance(backend, SimpleEmbeddingBackend):
298
+ existing_texts = [v["text"] for v in self._vectors.values()]
299
+ all_texts = existing_texts + [text]
300
+ backend._build_vocab(all_texts)
301
+
302
+ # Generate embedding
303
+ embedding = backend.embed(text)
304
+
305
+ # Store vector data
306
+ self._vectors[vector_id] = {
307
+ "text": text,
308
+ "metadata": metadata or {},
309
+ "id": vector_id,
310
+ "embedding_dim": len(embedding),
311
+ }
312
+
313
+ # Store embedding
314
+ self._embeddings[vector_id] = embedding
315
+
316
+ # Add to FAISS index if enabled
317
+ if self.use_faiss:
318
+ if self._faiss_index is None:
319
+ self._initialize_faiss()
320
+
321
+ if self._faiss_index is not None:
322
+ try:
323
+ import faiss # noqa: F401
324
+ import numpy as np
325
+ except ImportError as e:
326
+ raise VectorMemoryError(
327
+ f"FAISS dependencies not available: {e}. Install with: pip install faiss-cpu numpy",
328
+ context={"operation": "add", "use_faiss": True},
329
+ ) from e
330
+
331
+ embedding_array = np.array([embedding], dtype=np.float32)
332
+ idx = self._faiss_index.ntotal
333
+ self._faiss_index.add(embedding_array)
334
+ self._id_to_vector_id[idx] = vector_id
335
+
336
+ return vector_id
337
+
338
+ async def add_batch(
339
+ self, texts: list[str], metadata_list: list[dict[str, Any]] | None = None
340
+ ) -> list[str]:
341
+ """Add multiple texts to vector memory efficiently.
342
+
343
+ Args:
344
+ texts: List of texts to embed and store
345
+ metadata_list: Optional list of metadata dictionaries
346
+
347
+ Returns:
348
+ List of vector IDs
349
+ """
350
+ if not texts:
351
+ return []
352
+
353
+ if metadata_list and len(metadata_list) != len(texts):
354
+ raise VectorMemoryError("metadata_list length must match texts length")
355
+
356
+ backend = self._get_embedding_backend()
357
+
358
+ # For simple backend, rebuild vocab with all texts
359
+ if isinstance(backend, SimpleEmbeddingBackend):
360
+ existing_texts = [v["text"] for v in self._vectors.values()]
361
+ all_texts = existing_texts + texts
362
+ backend._build_vocab(all_texts)
363
+
364
+ # Batch embed
365
+ embeddings = backend.embed_batch(texts)
366
+
367
+ vector_ids = []
368
+ for idx, (text, embedding) in enumerate(zip(texts, embeddings, strict=True)):
369
+ vector_id = str(uuid.uuid4())
370
+ metadata = metadata_list[idx] if metadata_list else None
371
+
372
+ self._vectors[vector_id] = {
373
+ "text": text,
374
+ "metadata": metadata or {},
375
+ "id": vector_id,
376
+ "embedding_dim": len(embedding),
377
+ }
378
+ self._embeddings[vector_id] = embedding
379
+ vector_ids.append(vector_id)
380
+
381
+ # Batch add to FAISS if enabled
382
+ if self.use_faiss and embeddings:
383
+ if self._faiss_index is None:
384
+ self._initialize_faiss()
385
+
386
+ if self._faiss_index is not None:
387
+ try:
388
+ import faiss # noqa: F401
389
+ import numpy as np
390
+ except ImportError as e:
391
+ raise VectorMemoryError(
392
+ f"FAISS dependencies not available: {e}. Install with: pip install faiss-cpu numpy",
393
+ context={"operation": "add_batch", "use_faiss": True},
394
+ ) from e
395
+
396
+ embedding_array = np.array(embeddings, dtype=np.float32)
397
+ start_idx = self._faiss_index.ntotal
398
+ self._faiss_index.add(embedding_array)
399
+
400
+ for i, vector_id in enumerate(vector_ids):
401
+ self._id_to_vector_id[start_idx + i] = vector_id
402
+
403
+ return vector_ids
404
+
405
+ async def search(
406
+ self,
407
+ query: str,
408
+ top_k: int = 5,
409
+ filter_metadata: dict[str, Any] | None = None,
410
+ min_similarity: float | None = None,
411
+ ) -> list[dict[str, Any]]:
412
+ """Search for similar vectors using efficient similarity search.
413
+
414
+ Args:
415
+ query: Search query text
416
+ top_k: Number of results to return
417
+ filter_metadata: Optional metadata filter (exact match on keys)
418
+ min_similarity: Minimum similarity threshold (overrides instance default)
419
+
420
+ Returns:
421
+ List of similar vectors with metadata, scores, and distances
422
+ """
423
+ if not query.strip():
424
+ return []
425
+
426
+ if not self._vectors:
427
+ return []
428
+
429
+ backend = self._get_embedding_backend()
430
+
431
+ # For simple backend, ensure vocab is built from existing vectors
432
+ if isinstance(backend, SimpleEmbeddingBackend) and not backend._vocab:
433
+ existing_texts = [v["text"] for v in self._vectors.values()]
434
+ if existing_texts:
435
+ backend._build_vocab(existing_texts)
436
+ else:
437
+ # No vectors yet, return empty
438
+ return []
439
+
440
+ query_embedding = backend.embed(query)
441
+
442
+ threshold = min_similarity if min_similarity is not None else self.similarity_threshold
443
+
444
+ results = []
445
+
446
+ if self.use_faiss and self._faiss_index is not None:
447
+ # Use FAISS for efficient search
448
+ try:
449
+ import faiss # noqa: F401
450
+ import numpy as np
451
+ except ImportError as e:
452
+ raise VectorMemoryError(
453
+ f"FAISS dependencies not available: {e}. Install with: pip install faiss-cpu numpy",
454
+ context={"operation": "search", "use_faiss": True},
455
+ ) from e
456
+
457
+ query_array = np.array([query_embedding], dtype=np.float32)
458
+ k = min(top_k * 2, self._faiss_index.ntotal) # Get more candidates for filtering
459
+
460
+ if k > 0:
461
+ distances, indices = self._faiss_index.search(query_array, k)
462
+
463
+ for dist, idx in zip(distances[0], indices[0], strict=True):
464
+ vector_id = self._id_to_vector_id.get(idx)
465
+ if vector_id is None:
466
+ continue
467
+
468
+ vector_data = self._vectors[vector_id]
469
+ vector_embedding = self._embeddings[vector_id]
470
+
471
+ # Convert L2 distance to cosine similarity
472
+ # L2_dist^2 = 2 * (1 - cos_sim)
473
+ # cos_sim = 1 - (L2_dist^2 / 2)
474
+ similarity = 1.0 - (dist * dist / 2.0)
475
+
476
+ if similarity >= threshold:
477
+ # Apply metadata filter
478
+ if filter_metadata:
479
+ if not all(
480
+ vector_data.get("metadata", {}).get(k) == v
481
+ for k, v in filter_metadata.items()
482
+ ):
483
+ continue
484
+
485
+ results.append(
486
+ {
487
+ "id": vector_id,
488
+ "text": vector_data["text"],
489
+ "metadata": vector_data["metadata"],
490
+ "score": similarity,
491
+ "distance": dist,
492
+ }
493
+ )
494
+ else:
495
+ # Linear search with cosine similarity
496
+ for vector_id, vector_data in self._vectors.items():
497
+ vector_embedding = self._embeddings[vector_id]
498
+
499
+ # Apply metadata filter first (early exit)
500
+ if filter_metadata:
501
+ if not all(
502
+ vector_data.get("metadata", {}).get(k) == v
503
+ for k, v in filter_metadata.items()
504
+ ):
505
+ continue
506
+
507
+ # Calculate cosine similarity
508
+ similarity = self._cosine_similarity(query_embedding, vector_embedding)
509
+
510
+ if similarity >= threshold:
511
+ results.append(
512
+ {
513
+ "id": vector_id,
514
+ "text": vector_data["text"],
515
+ "metadata": vector_data["metadata"],
516
+ "score": similarity,
517
+ "distance": 1.0 - similarity, # Convert similarity to distance
518
+ }
519
+ )
520
+
521
+ # Sort by score descending and return top_k
522
+ results.sort(key=lambda x: x["score"], reverse=True)
523
+ return results[:top_k]
524
+
525
+ async def delete(self, vector_id: str) -> bool:
526
+ """Delete a vector from memory.
527
+
528
+ Args:
529
+ vector_id: ID of vector to delete
530
+
531
+ Returns:
532
+ True if deleted, False if not found
533
+ """
534
+ if vector_id not in self._vectors:
535
+ return False
536
+
537
+ del self._vectors[vector_id]
538
+ del self._embeddings[vector_id]
539
+
540
+ # Note: FAISS doesn't support deletion efficiently, so we'd need to rebuild
541
+ # For now, we'll just mark it as needing rebuild
542
+ if self.use_faiss:
543
+ # In production, you'd want to rebuild the index periodically
544
+ pass
545
+
546
+ return True
547
+
548
+ async def get(self, vector_id: str) -> dict[str, Any] | None:
549
+ """Get vector data by ID.
550
+
551
+ Args:
552
+ vector_id: Vector ID
553
+
554
+ Returns:
555
+ Vector data or None if not found
556
+ """
557
+ return self._vectors.get(vector_id)
558
+
559
+ def save(self, path: Path | str) -> None:
560
+ """Save vector memory to disk.
561
+
562
+ Args:
563
+ path: Path to save to
564
+ """
565
+ path = Path(path)
566
+ path.parent.mkdir(parents=True, exist_ok=True)
567
+
568
+ data = {
569
+ "vectors": self._vectors,
570
+ "embeddings": self._embeddings,
571
+ "dimension": self.dimension,
572
+ "backend": self.backend,
573
+ "id_to_vector_id": self._id_to_vector_id,
574
+ }
575
+
576
+ with open(path, "wb") as f:
577
+ pickle.dump(data, f)
578
+
579
+ @classmethod
580
+ def load(cls, path: Path | str, **kwargs: Any) -> "VectorMemory":
581
+ """Load vector memory from disk.
582
+
583
+ Args:
584
+ path: Path to load from
585
+ **kwargs: Additional initialization parameters
586
+
587
+ Returns:
588
+ Loaded VectorMemory instance
589
+ """
590
+ path = Path(path)
591
+ if not path.exists():
592
+ raise VectorMemoryError(f"Vector memory file not found: {path}")
593
+
594
+ with open(path, "rb") as f:
595
+ data = pickle.load(f)
596
+
597
+ instance = cls(
598
+ dimension=data.get("dimension", 384), backend=data.get("backend", "simple"), **kwargs
599
+ )
600
+
601
+ instance._vectors = data.get("vectors", {})
602
+ instance._embeddings = data.get("embeddings", {})
603
+ instance._id_to_vector_id = data.get("id_to_vector_id", {})
604
+
605
+ # Rebuild vocabulary for SimpleEmbeddingBackend if needed
606
+ if instance.backend == "simple" and instance._vectors:
607
+ backend = instance._get_embedding_backend()
608
+ if isinstance(backend, SimpleEmbeddingBackend):
609
+ existing_texts = [v["text"] for v in instance._vectors.values()]
610
+ if existing_texts:
611
+ backend._build_vocab(existing_texts)
612
+
613
+ # Rebuild FAISS index if needed
614
+ if instance.use_faiss and instance._embeddings:
615
+ instance._initialize_faiss()
616
+ if instance._faiss_index is not None:
617
+ try:
618
+ import faiss # noqa: F401
619
+ import numpy as np
620
+ except ImportError as e:
621
+ raise VectorMemoryError(
622
+ f"FAISS dependencies not available: {e}. Install with: pip install faiss-cpu numpy",
623
+ context={"operation": "load", "use_faiss": True},
624
+ ) from e
625
+
626
+ embeddings_list = [instance._embeddings[vid] for vid in instance._vectors.keys()]
627
+ if embeddings_list:
628
+ embedding_array = np.array(embeddings_list, dtype=np.float32)
629
+ instance._faiss_index.add(embedding_array)
630
+
631
+ return instance
632
+
633
+
634
+ class SentenceTransformerBackend(EmbeddingBackend):
635
+ """Sentence transformers embedding backend."""
636
+
637
+ def __init__(self, model: Any) -> None:
638
+ """Initialize with sentence transformer model.
639
+
640
+ Args:
641
+ model: SentenceTransformer model instance
642
+ """
643
+ self.model = model
644
+
645
+ def embed(self, text: str) -> list[float]:
646
+ """Generate embedding using sentence transformer."""
647
+ embedding = self.model.encode(text, convert_to_numpy=True)
648
+ result = embedding.tolist()
649
+ return [float(x) for x in result]
650
+
651
+ def embed_batch(self, texts: list[str]) -> list[list[float]]:
652
+ """Batch embed using sentence transformer."""
653
+ embeddings = self.model.encode(texts, convert_to_numpy=True)
654
+ return [[float(x) for x in e.tolist()] for e in embeddings]
655
+
656
+ @property
657
+ def dimension(self) -> int:
658
+ """Return embedding dimension."""
659
+ return int(self.model.get_sentence_embedding_dimension())
660
+
661
+
662
+ class OpenAIBackend(EmbeddingBackend):
663
+ """OpenAI embedding backend."""
664
+
665
+ def __init__(
666
+ self, api_key: str, dimension: int = 1536, model: str = "text-embedding-3-small"
667
+ ) -> None:
668
+ """Initialize OpenAI embedding backend.
669
+
670
+ Args:
671
+ api_key: OpenAI API key
672
+ dimension: Expected embedding dimension
673
+ model: OpenAI embedding model name
674
+ """
675
+ try:
676
+ from openai import OpenAI
677
+ except ImportError:
678
+ raise VectorMemoryError(
679
+ "openai package not installed. Install with: pip install openai",
680
+ context={"backend": "openai"},
681
+ ) from None
682
+
683
+ self.client = OpenAI(api_key=api_key)
684
+ self.model = model
685
+ self._dimension = dimension
686
+
687
+ def embed(self, text: str) -> list[float]:
688
+ """Generate embedding using OpenAI API."""
689
+ response = self.client.embeddings.create(model=self.model, input=text)
690
+ return list(response.data[0].embedding)
691
+
692
+ def embed_batch(self, texts: list[str]) -> list[list[float]]:
693
+ """Batch embed using OpenAI API."""
694
+ response = self.client.embeddings.create(model=self.model, input=texts)
695
+ return [list(item.embedding) for item in response.data]
696
+
697
+ @property
698
+ def dimension(self) -> int:
699
+ """Return embedding dimension."""
700
+ return self._dimension