synapse-memory-engine 1.3.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.
@@ -0,0 +1,123 @@
1
+ import math
2
+ import time
3
+ from typing import Any, Dict, Optional
4
+
5
+
6
+ class DecayEngine:
7
+ """
8
+ Robust Ebbinghaus-style decay and reinforcement engine.
9
+
10
+ Formula:
11
+ Score = Confidence * exp(-(now - last_accessed) / Strength)
12
+ Strength = BaseStrength * (1 + (access_count * factor) * multiplier)
13
+ """
14
+ def __init__(
15
+ self,
16
+ category_configs: Optional[Dict[str, Dict[str, float]]] = None
17
+ ):
18
+ self.category_configs = category_configs or {}
19
+ self.default_config = {
20
+ 'base_strength': 86400.0,
21
+ 'reinforcement_factor': 0.45
22
+ }
23
+ self.decay_floor = 0.10
24
+
25
+ def _get_config(self, category: str) -> Dict[str, float]:
26
+ return self.category_configs.get(category, self.default_config)
27
+
28
+ def calculate_relevance(
29
+ self,
30
+ memory: Dict[str, Any],
31
+ now: Optional[float] = None
32
+ ) -> float:
33
+ """Calculates current relevance score for a memory node."""
34
+ if now is None:
35
+ now = time.time()
36
+
37
+ last_accessed = memory.get("last_accessed_at", memory["created_at"])
38
+ elapsed = max(0.0, now - last_accessed)
39
+
40
+ config = self._get_config(memory.get("category", "interaction"))
41
+
42
+ access_count = memory.get("access_count", 0)
43
+ feedback_multiplier = memory.get("feedback_multiplier", 1.0)
44
+
45
+ strength = config['base_strength'] * (
46
+ 1 + (access_count * config['reinforcement_factor']) * feedback_multiplier
47
+ )
48
+
49
+ confidence = float(memory.get("confidence", 0.5))
50
+ return max(self.decay_floor, confidence * math.exp(-elapsed / max(1.0, strength)))
51
+
52
+ def temporal_multiplier(
53
+ self,
54
+ memory: Dict[str, Any],
55
+ now: Optional[float] = None,
56
+ ) -> float:
57
+ """Temporal-decay multiplier D(m,t) in [decay_floor, 1.0].
58
+
59
+ Separated from retrieval relevance R(m,q) so query relevance is
60
+ never erased: combined score S(m,q,t) = R(m,q) * D(m,t).
61
+ """
62
+ if now is None:
63
+ now = time.time()
64
+ last_accessed = memory.get("last_accessed_at", memory["created_at"])
65
+ elapsed = max(0.0, now - last_accessed)
66
+ config = self._get_config(memory.get("category", "interaction"))
67
+ access_count = memory.get("access_count", 0)
68
+ feedback_multiplier = memory.get("feedback_multiplier", 1.0)
69
+ strength = config['base_strength'] * (
70
+ 1 + (access_count * config['reinforcement_factor']) * feedback_multiplier
71
+ )
72
+ return max(self.decay_floor, math.exp(-elapsed / max(1.0, strength)))
73
+
74
+ def combine_scores(self, retrieval_score: float, memory: Dict[str, Any], now: Optional[float] = None) -> float:
75
+ """S(m,q,t) = R(m,q) * D(m,t)."""
76
+ return float(retrieval_score) * self.temporal_multiplier(memory, now=now)
77
+
78
+ def calculate_retention(
79
+ self,
80
+ base_relevance: float,
81
+ created_epoch: float,
82
+ access_count: int = 0,
83
+ now: Optional[float] = None
84
+ ) -> float:
85
+ """
86
+ Calculates Ebbinghaus retention for a memory based on age and access count.
87
+ """
88
+ if now is None:
89
+ now = time.time()
90
+ elapsed = max(0.0, now - created_epoch)
91
+ effective_half_life = 86400.0 * 2.0 * (1.0 + 0.5 * access_count)
92
+ decay = math.exp(-elapsed / effective_half_life)
93
+ return max(self.decay_floor, base_relevance * decay)
94
+
95
+ def compute_feedback_boost(
96
+ self,
97
+ current_confidence: float,
98
+ feedback_type: str
99
+ ) -> float:
100
+ """Helper to adjust confidence based on feedback."""
101
+ boost = 0.05 if feedback_type == "positive" else -0.1
102
+ return max(0.1, min(1.0, current_confidence + boost))
103
+
104
+ def reinforce(
105
+ self,
106
+ memory: Dict[str, Any],
107
+ feedback_value: float = 1.0
108
+ ) -> Dict[str, Any]:
109
+ """
110
+ Applies reinforcement to a memory node.
111
+
112
+ feedback_value > 0: Positive reinforcement
113
+ feedback_value < 0: Negative reinforcement (penalty)
114
+ """
115
+ memory["access_count"] = memory.get("access_count", 0) + 1
116
+ memory["last_accessed_at"] = time.time()
117
+
118
+ current_multiplier = memory.get("feedback_multiplier", 1.0)
119
+ memory["feedback_multiplier"] = max(
120
+ 0.1,
121
+ current_multiplier + (feedback_value * 0.1)
122
+ )
123
+ return memory
@@ -0,0 +1,98 @@
1
+ from dataclasses import dataclass
2
+ from enum import Enum
3
+ import logging
4
+ import math
5
+ from typing import Any, Dict, List, Optional
6
+
7
+
8
+ logger = logging.getLogger("Deduplicator")
9
+
10
+
11
+ class Action(Enum):
12
+ CREATE = "CREATE"
13
+ MERGE = "MERGE"
14
+ REJECT = "REJECT"
15
+
16
+
17
+ @dataclass
18
+ class MergeResult:
19
+ action: Action
20
+ target_id: Optional[str]
21
+ reason: str
22
+ provenance: Dict[str, Any]
23
+
24
+
25
+ # Alias for backwards compatibility
26
+ DeduplicationResult = MergeResult
27
+
28
+
29
+ class Deduplicator:
30
+ def __init__(self, category_thresholds: Dict[str, float], default_threshold: float = 0.85):
31
+ self.category_thresholds = category_thresholds
32
+ self.default_threshold = default_threshold
33
+
34
+ def _get_threshold(self, category: str) -> float:
35
+ return self.category_thresholds.get(category, self.default_threshold)
36
+
37
+ def _jaccard_similarity(self, content_a: str, content_b: str) -> float:
38
+ set_a = set(content_a.lower().split())
39
+ set_b = set(content_b.lower().split())
40
+ intersection = len(set_a.intersection(set_b))
41
+ union = len(set_a.union(set_b))
42
+ return intersection / union if union > 0 else 0.0
43
+
44
+ def _cosine_similarity(self, vec_a: List[float], vec_b: List[float]) -> float:
45
+ if not vec_a or not vec_b or len(vec_a) != len(vec_b):
46
+ return 0.0
47
+ dot_product = sum(a * b for a, b in zip(vec_a, vec_b))
48
+ norm_a = math.sqrt(sum(a * a for a in vec_a))
49
+ norm_b = math.sqrt(sum(b * b for b in vec_b))
50
+ if norm_a == 0.0 or norm_b == 0.0:
51
+ return 0.0
52
+ return dot_product / (norm_a * norm_b)
53
+
54
+ def check_duplicate(self, new_mem: Dict[str, Any], existing_mems: List[Dict[str, Any]]) -> MergeResult:
55
+ threshold = self._get_threshold(new_mem.get("category", "interaction"))
56
+
57
+ for existing in existing_mems:
58
+ # 1. Exact Match Check (e.g., hash of content or specific ID)
59
+ if new_mem.get("id") == existing.get("id"):
60
+ return MergeResult(Action.REJECT, existing["id"], "Exact ID match", {"type": "exact"})
61
+
62
+ # 2. Lexical Similarity (Jaccard)
63
+ lexical_sim = self._jaccard_similarity(new_mem["content"], existing["content"])
64
+
65
+ # 3. Semantic Similarity (Cosine)
66
+ semantic_sim = self._cosine_similarity(new_mem.get("embedding", []), existing.get("embedding", []))
67
+
68
+ # 4. Conflict & Supersession Detection (Production-Grade Heuristic)
69
+ if semantic_sim > threshold:
70
+ # If high semantic similarity, compare metadata to decide supersession
71
+ new_conf = new_mem.get("confidence", 0.5)
72
+ old_conf = existing.get("confidence", 0.5)
73
+
74
+ # If new memory has significantly higher confidence, suggest supersession (merge)
75
+ if new_conf > old_conf + 0.1:
76
+ return MergeResult(
77
+ Action.MERGE,
78
+ existing["id"],
79
+ "Higher confidence supersession",
80
+ {"new_conf": new_conf, "old_conf": old_conf}
81
+ )
82
+
83
+ # Check for explicit contradiction in content
84
+ content_new = new_mem["content"].lower()
85
+ content_old = existing["content"].lower()
86
+ if ("true" in content_new and "false" in content_old) or ("false" in content_new and "true" in content_old):
87
+ return MergeResult(Action.REJECT, None, "Direct contradiction detected", {"type": "conflict"})
88
+
89
+ # 5. Threshold Decision
90
+ if semantic_sim >= threshold or (lexical_sim > 0.9 and semantic_sim > 0.7):
91
+ return MergeResult(
92
+ Action.MERGE,
93
+ existing["id"],
94
+ "Semantic/Lexical similarity threshold exceeded",
95
+ {"semantic_sim": semantic_sim, "lexical_sim": lexical_sim}
96
+ )
97
+
98
+ return MergeResult(Action.CREATE, None, "No duplicates found", {})
@@ -0,0 +1,119 @@
1
+ import logging
2
+ import os
3
+ from typing import List, Optional
4
+ from synapse_memory.core.embedding_provider import (
5
+ EmbeddingManager,
6
+ LocalEmbeddingProvider,
7
+ GeminiEmbeddingProvider,
8
+ OpenAIEmbeddingProvider,
9
+ SentenceTransformerEmbeddingProvider
10
+ )
11
+
12
+ logger = logging.getLogger("SynapseEmbedder")
13
+
14
+
15
+ class SynapseEmbedder:
16
+ """
17
+ Multi-model embedding client. Provides seamless failover between Google Gemini API,
18
+ OpenAI API, SentenceTransformers, and deterministic hash vector providers.
19
+ """
20
+
21
+ def __init__(self, provider: Optional[str] = None, allow_fallback: Optional[bool] = None):
22
+ env_provider = os.getenv("SYNAPSE_EMBEDDING_PROVIDER")
23
+ self.provider_name = (provider or env_provider or "deterministic").lower()
24
+ self.model_name = os.getenv("SYNAPSE_EMBEDDING_MODEL", "all-MiniLM-L6-v2")
25
+ self.gemini_key = os.getenv("GEMINI_API_KEY", "")
26
+ self.openai_key = os.getenv("OPENAI_API_KEY", "")
27
+
28
+ is_production = os.getenv("NODE_ENV", "").lower() == "production"
29
+ if allow_fallback is None:
30
+ allow_fallback = not is_production
31
+
32
+ provider_instance = self._get_provider(allow_fallback=allow_fallback)
33
+ self.manager = EmbeddingManager(provider_instance)
34
+
35
+ # Validate configured dimension if explicitly set
36
+ configured_dim = os.getenv("SYNAPSE_EMBEDDING_DIMENSION")
37
+ if configured_dim:
38
+ expected = int(configured_dim)
39
+ actual = self.manager.provider.dimension
40
+ if actual != expected:
41
+ raise RuntimeError(
42
+ f"Embedding dimension mismatch: provider '{self.provider_name}' produces {actual}-d vectors, "
43
+ f"but SYNAPSE_EMBEDDING_DIMENSION is configured as {expected}-d."
44
+ )
45
+
46
+ @property
47
+ def dimension(self) -> int:
48
+ return self.manager.provider.dimension
49
+
50
+ @property
51
+ def is_semantic(self) -> bool:
52
+ return self.manager.provider.is_semantic
53
+
54
+ def _get_provider(self, allow_fallback: bool = False):
55
+ if self.provider_name in ("deterministic", "hash", "local"):
56
+ # Deterministic hash provider: always available, no external deps,
57
+ # but NOT a real semantic embedding model. Safe for offline/dev use;
58
+ # production must not silently run on it.
59
+ is_production = os.getenv("NODE_ENV", "").lower() == "production"
60
+ allow_hash = os.getenv("SYNAPSE_ALLOW_DETERMINISTIC_EMBEDDING", "false").lower() == "true"
61
+ if is_production and not allow_hash:
62
+ raise RuntimeError(
63
+ "Refusing to start in production with the deterministic hash "
64
+ "embedding provider (it is not a semantic model). "
65
+ "Set SYNAPSE_EMBEDDING_PROVIDER to a real semantic provider "
66
+ "(gemini, openai, sentence-transformers) or opt in with "
67
+ "SYNAPSE_ALLOW_DETERMINISTIC_EMBEDDING=true."
68
+ )
69
+ dim = int(os.getenv("SYNAPSE_EMBEDDING_DIMENSION", "128"))
70
+ return LocalEmbeddingProvider(dim=dim)
71
+ elif self.provider_name == "gemini":
72
+ if not self.gemini_key:
73
+ if allow_fallback:
74
+ logger.warning(
75
+ "GEMINI_API_KEY not set; falling back to deterministic hash provider."
76
+ )
77
+ dim = int(os.getenv("SYNAPSE_EMBEDDING_DIMENSION", "128"))
78
+ return LocalEmbeddingProvider(dim=dim)
79
+ raise RuntimeError(
80
+ "SYNAPSE_EMBEDDING_PROVIDER=gemini but GEMINI_API_KEY is not set. "
81
+ "Set GEMINI_API_KEY or switch to provider=deterministic."
82
+ )
83
+ return GeminiEmbeddingProvider(self.gemini_key)
84
+ elif self.provider_name == "openai":
85
+ if not self.openai_key:
86
+ if allow_fallback:
87
+ logger.warning(
88
+ "OPENAI_API_KEY not set; falling back to deterministic hash provider."
89
+ )
90
+ dim = int(os.getenv("SYNAPSE_EMBEDDING_DIMENSION", "128"))
91
+ return LocalEmbeddingProvider(dim=dim)
92
+ raise RuntimeError(
93
+ "SYNAPSE_EMBEDDING_PROVIDER=openai but OPENAI_API_KEY is not set. "
94
+ "Set OPENAI_API_KEY or switch to provider=deterministic."
95
+ )
96
+ return OpenAIEmbeddingProvider(self.openai_key)
97
+ elif self.provider_name == "sentence-transformers":
98
+ # Explicit opt-in: requires [local-models] extra and model availability.
99
+ return SentenceTransformerEmbeddingProvider(
100
+ model_name=self.model_name,
101
+ allow_fallback=allow_fallback
102
+ )
103
+ # Unknown provider: fail closed in production, fall back in dev.
104
+ if allow_fallback:
105
+ logger.warning(
106
+ f"Unknown embedding provider '{self.provider_name}'; falling back to deterministic hash provider."
107
+ )
108
+ dim = int(os.getenv("SYNAPSE_EMBEDDING_DIMENSION", "128"))
109
+ return LocalEmbeddingProvider(dim=dim)
110
+ raise RuntimeError(
111
+ f"Unknown embedding provider '{self.provider_name}'. "
112
+ "Use one of: deterministic, gemini, openai, sentence-transformers."
113
+ )
114
+
115
+ def embed_query(self, text: str) -> List[float]:
116
+ """Generates a dense float vector representation for text strings."""
117
+ if not text.strip():
118
+ return [0.0] * self.manager.provider.dimension
119
+ return self.manager.get_embedding(text)
@@ -0,0 +1,241 @@
1
+ import hashlib
2
+ import json
3
+ import logging
4
+ import math
5
+ import re
6
+ import urllib.request
7
+ from abc import ABC, abstractmethod
8
+ from functools import lru_cache
9
+ from typing import List, Optional
10
+
11
+ from synapse_memory.core.observability import instrument_operation
12
+
13
+ logger = logging.getLogger("EmbeddingProvider")
14
+
15
+
16
+ class EmbeddingProvider(ABC):
17
+ """Base class for all embedding providers."""
18
+
19
+ @abstractmethod
20
+ def embed(self, text: str) -> List[float]:
21
+ pass
22
+
23
+ @abstractmethod
24
+ def embed_batch(self, texts: List[str]) -> List[List[float]]:
25
+ pass
26
+
27
+ @property
28
+ @abstractmethod
29
+ def dimension(self) -> int:
30
+ pass
31
+
32
+ @property
33
+ def is_semantic(self) -> bool:
34
+ return True
35
+
36
+
37
+ class DeterministicHashEmbeddingProvider(EmbeddingProvider):
38
+ """
39
+ Local deterministic hash provider for offline unit testing and development.
40
+ NOTE: This generates deterministic word-frequency hash projections, NOT neural semantic embeddings.
41
+ """
42
+ def __init__(self, dim: int = 128):
43
+ self._dim = dim
44
+
45
+ @property
46
+ def dimension(self) -> int:
47
+ return self._dim
48
+
49
+ @property
50
+ def is_semantic(self) -> bool:
51
+ return False
52
+
53
+ def embed(self, text: str) -> List[float]:
54
+ # Normalize text (lowercase and strip non-alphanumeric characters)
55
+ normalized = re.sub(r"[^\w\s]", "", text.lower()).strip()
56
+ words = normalized.split()
57
+ if not words:
58
+ return [0.0] * self._dim
59
+
60
+ vector = [0.0] * self._dim
61
+ for i, word in enumerate(words):
62
+ # SHA-256 digest instead of the built-in hash(): Python string hashing
63
+ # is salted per interpreter (PYTHONHASHSEED), so hash(word) is stable
64
+ # only within a single process and changes across invocations -- it must
65
+ # never back a stored index or a repeatable offline test.
66
+ digest = hashlib.sha256(word.encode("utf-8")).digest()
67
+ h = int.from_bytes(digest[:8], "big", signed=True)
68
+ idx = abs(h) % self._dim
69
+ sign = 1.0 if h >= 0 else -1.0
70
+ vector[idx] += sign * (1.0 / (1.0 + 0.05 * i))
71
+
72
+ # L2 normalization for accurate cosine similarities
73
+ norm = math.sqrt(sum(x * x for x in vector))
74
+ if norm > 0.0:
75
+ vector = [x / norm for x in vector]
76
+ return vector
77
+
78
+ def embed_batch(self, texts: List[str]) -> List[List[float]]:
79
+ return [self.embed(t) for t in texts]
80
+
81
+
82
+ # Alias for backward compatibility
83
+ LocalEmbeddingProvider = DeterministicHashEmbeddingProvider
84
+
85
+
86
+ class SentenceTransformerEmbeddingProvider(EmbeddingProvider):
87
+ """
88
+ Production-grade local neural semantic embedding provider using sentence-transformers.
89
+ Requires: sentence-transformers (pip install 'synapse-memory-engine[local-models]')
90
+ """
91
+ def __init__(self, model_name: str = "all-MiniLM-L6-v2", allow_fallback: bool = False):
92
+ self.model_name = model_name
93
+ self._model = None
94
+ self._fallback: Optional[DeterministicHashEmbeddingProvider] = None
95
+ self._is_using_real_model = False
96
+
97
+ try:
98
+ from sentence_transformers import SentenceTransformer
99
+ self._model = SentenceTransformer(model_name)
100
+ self._dim = int(self._model.get_sentence_embedding_dimension())
101
+ self._is_using_real_model = True
102
+ logger.info(f"Loaded neural embedding model '{model_name}' (dim: {self._dim}).")
103
+ except Exception as err:
104
+ if not allow_fallback:
105
+ raise ImportError(
106
+ f"Failed to load semantic embedding model '{model_name}': {err}. "
107
+ "For production neural embeddings, install sentence-transformers or set provider='deterministic'."
108
+ ) from err
109
+ logger.warning(
110
+ f"[SynapseEmbedder Warning] sentence-transformers not installed ({err}). "
111
+ "Falling back to DeterministicHashEmbeddingProvider for non-production development. "
112
+ "For production neural embeddings, install with: pip install sentence-transformers"
113
+ )
114
+ self._fallback = DeterministicHashEmbeddingProvider()
115
+ self._dim = self._fallback.dimension
116
+
117
+ @property
118
+ def dimension(self) -> int:
119
+ return self._dim
120
+
121
+ @property
122
+ def is_semantic(self) -> bool:
123
+ return self._is_using_real_model
124
+
125
+ @instrument_operation("st_embed")
126
+ def embed(self, text: str) -> List[float]:
127
+ if self._model is not None:
128
+ return self._model.encode(text).tolist()
129
+ if self._fallback is None:
130
+ self._fallback = DeterministicHashEmbeddingProvider()
131
+ return self._fallback.embed(text)
132
+
133
+ def embed_batch(self, texts: List[str]) -> List[List[float]]:
134
+ if self._model is not None:
135
+ return self._model.encode(texts).tolist()
136
+ if self._fallback is None:
137
+ self._fallback = DeterministicHashEmbeddingProvider()
138
+ return self._fallback.embed_batch(texts)
139
+
140
+
141
+ class GeminiEmbeddingProvider(EmbeddingProvider):
142
+ """
143
+ Google Gemini text embedding provider.
144
+
145
+ NOTE: text-embedding-004 was shut down on 2026-01-14 and is retired.
146
+ This provider now targets gemini-embedding-001 (3072 dimensions by default),
147
+ the supported text-only replacement. Embeddings from the two model families
148
+ are incompatible -- existing text-embedding-004 vectors must be re-embedded
149
+ with `reembed_all_memories()` (or the equivalent migration job) before
150
+ querying, since changing the column definition alone does not recalculate
151
+ stored vectors. See `docs/production-hardening-baseline.md` and
152
+ `docs/backup-recovery.md` for the batched re-embed/key-rotation sweep
153
+ pattern.
154
+ """
155
+ def __init__(self, api_key: str, dim: int = 3072):
156
+ self.api_key = api_key
157
+ self._dim = dim
158
+
159
+ @property
160
+ def dimension(self) -> int:
161
+ return self._dim
162
+
163
+ @property
164
+ def is_semantic(self) -> bool:
165
+ return True
166
+
167
+ @instrument_operation("gemini_embed")
168
+ def embed(self, text: str) -> List[float]:
169
+ url = (
170
+ "https://generativelanguage.googleapis.com/v1beta/models/"
171
+ f"gemini-embedding-001:embedContent?key={self.api_key}"
172
+ )
173
+ headers = {"Content-Type": "application/json"}
174
+ payload = {"content": {"parts": [{"text": text}]}}
175
+
176
+ req = urllib.request.Request(
177
+ url,
178
+ data=json.dumps(payload).encode("utf-8"),
179
+ headers=headers,
180
+ method="POST"
181
+ )
182
+ with urllib.request.urlopen(req, timeout=5) as response:
183
+ res_data = json.loads(response.read().decode("utf-8"))
184
+ return res_data["embedding"]["values"]
185
+
186
+ def embed_batch(self, texts: List[str]) -> List[List[float]]:
187
+ return [self.embed(t) for t in texts]
188
+
189
+
190
+ class OpenAIEmbeddingProvider(EmbeddingProvider):
191
+ def __init__(self, api_key: str, model: str = "text-embedding-3-small", dim: int = 1536):
192
+ self.api_key = api_key
193
+ self.model = model
194
+ self._dim = dim
195
+
196
+ @property
197
+ def dimension(self) -> int:
198
+ return self._dim
199
+
200
+ @property
201
+ def is_semantic(self) -> bool:
202
+ return True
203
+
204
+ @instrument_operation("openai_embed")
205
+ def embed(self, text: str) -> List[float]:
206
+ url = "https://api.openai.com/v1/embeddings"
207
+ headers = {
208
+ "Content-Type": "application/json",
209
+ "Authorization": f"Bearer {self.api_key}"
210
+ }
211
+ payload = {
212
+ "input": text,
213
+ "model": self.model
214
+ }
215
+ req = urllib.request.Request(
216
+ url,
217
+ data=json.dumps(payload).encode("utf-8"),
218
+ headers=headers,
219
+ method="POST"
220
+ )
221
+ with urllib.request.urlopen(req, timeout=5) as response:
222
+ res_data = json.loads(response.read().decode("utf-8"))
223
+ return res_data["data"][0]["embedding"]
224
+
225
+ def embed_batch(self, texts: List[str]) -> List[List[float]]:
226
+ return [self.embed(t) for t in texts]
227
+
228
+
229
+ class EmbeddingManager:
230
+ """
231
+ Manages embedding provider operations with caching and observability.
232
+ """
233
+ def __init__(self, provider: EmbeddingProvider):
234
+ self.provider = provider
235
+
236
+ @lru_cache(maxsize=1024)
237
+ def get_embedding(self, text: str) -> List[float]:
238
+ return self.provider.embed(text)
239
+
240
+ def get_batch_embeddings(self, texts: List[str]) -> List[List[float]]:
241
+ return self.provider.embed_batch(texts)