docsif 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 (86) hide show
  1. docsif-0.1.0.dist-info/METADATA +393 -0
  2. docsif-0.1.0.dist-info/RECORD +86 -0
  3. docsif-0.1.0.dist-info/WHEEL +4 -0
  4. docsif-0.1.0.dist-info/entry_points.txt +3 -0
  5. docsif-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. sif/__init__.py +24 -0
  7. sif/_version.py +3 -0
  8. sif/cli/__init__.py +11 -0
  9. sif/cli/commands/__init__.py +18 -0
  10. sif/cli/commands/bench.py +148 -0
  11. sif/cli/commands/collection.py +360 -0
  12. sif/cli/commands/context.py +154 -0
  13. sif/cli/commands/get.py +200 -0
  14. sif/cli/commands/index.py +294 -0
  15. sif/cli/commands/ls.py +67 -0
  16. sif/cli/commands/mcp.py +93 -0
  17. sif/cli/commands/pull.py +111 -0
  18. sif/cli/commands/search.py +574 -0
  19. sif/cli/config.py +206 -0
  20. sif/cli/formatters.py +269 -0
  21. sif/cli/main.py +205 -0
  22. sif/config/__init__.py +30 -0
  23. sif/config/constants.py +52 -0
  24. sif/config/settings.py +217 -0
  25. sif/core/__init__.py +21 -0
  26. sif/core/collection.py +161 -0
  27. sif/core/context.py +167 -0
  28. sif/core/document.py +125 -0
  29. sif/core/models.py +298 -0
  30. sif/database/__init__.py +23 -0
  31. sif/database/connection.py +139 -0
  32. sif/database/database.py +136 -0
  33. sif/database/migrations.py +187 -0
  34. sif/database/repositories.py +500 -0
  35. sif/database/repository.py +171 -0
  36. sif/database/schema.py +362 -0
  37. sif/embedding/__init__.py +22 -0
  38. sif/embedding/cache.py +196 -0
  39. sif/embedding/embedder.py +328 -0
  40. sif/embedding/factory.py +92 -0
  41. sif/embedding/manager.py +200 -0
  42. sif/embedding/model.py +126 -0
  43. sif/indexing/__init__.py +21 -0
  44. sif/indexing/chunker.py +226 -0
  45. sif/indexing/indexer.py +226 -0
  46. sif/indexing/parser.py +192 -0
  47. sif/indexing/scanner.py +148 -0
  48. sif/indexing/watcher.py +156 -0
  49. sif/mcp/README.md +258 -0
  50. sif/mcp/__init__.py +9 -0
  51. sif/mcp/claude_desktop_config.json +16 -0
  52. sif/mcp/cli.py +92 -0
  53. sif/mcp/protocol.py +336 -0
  54. sif/mcp/server.py +433 -0
  55. sif/mcp/server_http.py +54 -0
  56. sif/mcp/test_mcp.py +108 -0
  57. sif/mcp/tools.py +497 -0
  58. sif/mcp/transport_http.py +424 -0
  59. sif/mcp/transport_stdio.py +292 -0
  60. sif/mcp_server/__init__.py +26 -0
  61. sif/mcp_server/handlers.py +68 -0
  62. sif/mcp_server/server.py +58 -0
  63. sif/mcp_server/tools.py +216 -0
  64. sif/mcp_server/transport.py +221 -0
  65. sif/models/__init__.py +70 -0
  66. sif/models/collection.py +51 -0
  67. sif/models/context.py +45 -0
  68. sif/models/document.py +76 -0
  69. sif/models/download.py +208 -0
  70. sif/models/embedding.py +71 -0
  71. sif/models/search.py +92 -0
  72. sif/search/__init__.py +22 -0
  73. sif/search/benchmark.py +110 -0
  74. sif/search/bm25.py +216 -0
  75. sif/search/expansion.py +157 -0
  76. sif/search/hybrid.py +323 -0
  77. sif/search/rerank.py +390 -0
  78. sif/search/rrf.py +186 -0
  79. sif/search/snippets.py +130 -0
  80. sif/search/strategy.py +97 -0
  81. sif/search/vector.py +177 -0
  82. sif/utils/__init__.py +26 -0
  83. sif/utils/logging.py +143 -0
  84. sif/utils/paths.py +82 -0
  85. sif/utils/progress.py +83 -0
  86. sif/utils/text.py +128 -0
@@ -0,0 +1,328 @@
1
+ """Embedding model implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ from pathlib import Path
7
+
8
+ import numpy as np
9
+
10
+ from sif.core.models import Embedder
11
+ from sif.models.download import ModelDownloader
12
+ from sif.utils.logging import get_logger, is_quiet, suppress_output
13
+
14
+
15
+ logger = get_logger(__name__)
16
+
17
+
18
+ class SentenceTransformerEmbedder(Embedder):
19
+ """Embedder using sentence-transformers."""
20
+
21
+ def __init__(
22
+ self,
23
+ model_name: str = "all-MiniLM-L6-v2",
24
+ device: str | None = None,
25
+ cache_dir: str | None = None,
26
+ ) -> None:
27
+ """Initialize sentence transformer embedder.
28
+
29
+ Args:
30
+ model_name: HuggingFace model name
31
+ device: Device to use (cpu, cuda, etc.)
32
+ cache_dir: Cache directory for models
33
+ """
34
+ try:
35
+ from sentence_transformers import SentenceTransformer # noqa: PLC0415
36
+ except ImportError:
37
+ logger.error(
38
+ "sentence-transformers not installed. "
39
+ "Install with: pip install sentence-transformers"
40
+ )
41
+ raise
42
+
43
+ self.model_name = model_name
44
+
45
+ if device is None:
46
+ import torch # noqa: PLC0415
47
+
48
+ if torch.cuda.is_available():
49
+ device = "cuda"
50
+ elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
51
+ device = "mps"
52
+ else:
53
+ device = "cpu"
54
+
55
+ logger.info(f"Loading embedding model: {model_name} on {device}")
56
+
57
+ if is_quiet():
58
+ with suppress_output():
59
+ self.model = SentenceTransformer(
60
+ model_name,
61
+ device=device,
62
+ cache_folder=cache_dir,
63
+ )
64
+ else:
65
+ self.model = SentenceTransformer(
66
+ model_name,
67
+ device=device,
68
+ cache_folder=cache_dir,
69
+ )
70
+
71
+ self._dimension = self.model.get_sentence_embedding_dimension()
72
+ logger.info(f"Embedding dimension: {self._dimension}")
73
+
74
+ def embed(self, text: str) -> list[float]:
75
+ """Embed a single text.
76
+
77
+ Args:
78
+ text: Text to embed
79
+
80
+ Returns:
81
+ Embedding vector
82
+ """
83
+ embedding = self.model.encode(text, normalize_embeddings=True)
84
+ return embedding.tolist()
85
+
86
+ def embed_batch(self, texts: list[str]) -> list[list[float]]:
87
+ """Embed multiple texts.
88
+
89
+ Args:
90
+ texts: List of texts to embed
91
+
92
+ Returns:
93
+ List of embedding vectors
94
+ """
95
+ embeddings = self.model.encode(
96
+ texts,
97
+ normalize_embeddings=True,
98
+ show_progress_bar=len(texts) > 100,
99
+ )
100
+ return embeddings.tolist()
101
+
102
+ @property
103
+ def dimension(self) -> int:
104
+ """Get embedding dimension."""
105
+ return self._dimension
106
+
107
+
108
+ class LlamaCppEmbedder(Embedder):
109
+ """Embedder using llama-cpp-python."""
110
+
111
+ def __init__(
112
+ self,
113
+ model_path: str | Path,
114
+ n_ctx: int = 8192,
115
+ n_threads: int | None = None,
116
+ verbose: bool = False,
117
+ ) -> None:
118
+ """Initialize llama.cpp embedder.
119
+
120
+ Args:
121
+ model_path: Path to GGUF model file
122
+ n_ctx: Context size
123
+ n_threads: Number of threads (auto if None)
124
+ verbose: Enable verbose output
125
+ """
126
+ try:
127
+ from llama_cpp import Llama # noqa: PLC0415
128
+ except ImportError:
129
+ logger.error(
130
+ "llama-cpp-python not installed. Install with: pip install llama-cpp-python"
131
+ )
132
+ raise
133
+
134
+ self.model_path = Path(model_path)
135
+
136
+ if n_threads is None:
137
+ import os # noqa: PLC0415
138
+
139
+ n_threads = os.cpu_count() or 4
140
+
141
+ logger.info(f"Loading llama.cpp model: {self.model_path}")
142
+
143
+ self.model = Llama(
144
+ model_path=str(self.model_path),
145
+ n_ctx=n_ctx,
146
+ n_threads=n_threads,
147
+ embedding=True,
148
+ verbose=verbose,
149
+ )
150
+
151
+ self._dimension = self.model.n_embd()
152
+ logger.info(f"Embedding dimension: {self._dimension}")
153
+
154
+ def embed(self, text: str) -> list[float]:
155
+ """Embed a single text."""
156
+ embedding = self.model.embed(text)
157
+ # Normalize
158
+ norm = np.linalg.norm(embedding)
159
+ if norm > 0:
160
+ embedding = embedding / norm
161
+ return embedding.tolist()
162
+
163
+ def embed_batch(self, texts: list[str]) -> list[list[float]]:
164
+ """Embed multiple texts."""
165
+ embeddings = []
166
+ for text in texts:
167
+ embedding = self.embed(text)
168
+ embeddings.append(embedding)
169
+ return embeddings
170
+
171
+ @property
172
+ def dimension(self) -> int:
173
+ """Get embedding dimension."""
174
+ return self._dimension
175
+
176
+
177
+ class ModelScopeEmbedder(Embedder):
178
+ """Embedder that downloads from ModelScope and uses sentence-transformers."""
179
+
180
+ def __init__(
181
+ self,
182
+ model_id: str = "iic/gte_Qwen2-7B-instruct",
183
+ device: str | None = None,
184
+ cache_dir: str | None = None,
185
+ force_download: bool = False,
186
+ ) -> None:
187
+ """Initialize ModelScope embedder.
188
+
189
+ Args:
190
+ model_id: ModelScope model ID
191
+ device: Device to use
192
+ cache_dir: Cache directory
193
+ force_download: Force re-download
194
+ """
195
+ # Download model from ModelScope
196
+ downloader = ModelDownloader(cache_dir)
197
+ model_path = downloader.download(model_id, force=force_download)
198
+
199
+ # Find the actual model directory (snapshot_download creates subdirectories)
200
+ model_dirs = list(model_path.glob("*"))
201
+ if model_dirs:
202
+ # Use the first subdirectory if exists
203
+ actual_model_path = model_dirs[0] if model_dirs[0].is_dir() else model_path
204
+ else:
205
+ actual_model_path = model_path
206
+
207
+ # Initialize with sentence-transformers
208
+ try:
209
+ from sentence_transformers import SentenceTransformer # noqa: PLC0415
210
+ except ImportError:
211
+ logger.error("sentence-transformers not installed")
212
+ raise
213
+
214
+ if device is None:
215
+ import torch # noqa: PLC0415
216
+
217
+ if torch.cuda.is_available():
218
+ device = "cuda"
219
+ elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
220
+ device = "mps"
221
+ else:
222
+ device = "cpu"
223
+
224
+ logger.info(f"Loading ModelScope model: {model_id} on {device}")
225
+
226
+ if is_quiet():
227
+ with suppress_output():
228
+ self.model = SentenceTransformer(
229
+ str(actual_model_path),
230
+ device=device,
231
+ )
232
+ else:
233
+ self.model = SentenceTransformer(
234
+ str(actual_model_path),
235
+ device=device,
236
+ )
237
+
238
+ self._dimension = self.model.get_sentence_embedding_dimension()
239
+ self.model_id = model_id
240
+ logger.info(f"Embedding dimension: {self._dimension}")
241
+
242
+ def embed(self, text: str) -> list[float]:
243
+ """Embed a single text."""
244
+ embedding = self.model.encode(text, normalize_embeddings=True)
245
+ return embedding.tolist()
246
+
247
+ def embed_batch(self, texts: list[str]) -> list[list[float]]:
248
+ """Embed multiple texts."""
249
+ embeddings = self.model.encode(
250
+ texts,
251
+ normalize_embeddings=True,
252
+ show_progress_bar=len(texts) > 100,
253
+ )
254
+ return embeddings.tolist()
255
+
256
+ @property
257
+ def dimension(self) -> int:
258
+ """Get embedding dimension."""
259
+ return self._dimension
260
+
261
+
262
+ class SimpleEmbedder(Embedder):
263
+ """Simple embedder using TF-IDF (fallback when no ML models available)."""
264
+
265
+ def __init__(self, dimension: int = 384) -> None:
266
+ """Initialize simple TF-IDF embedder.
267
+
268
+ Args:
269
+ dimension: Embedding dimension
270
+ """
271
+ self._dimension = dimension
272
+ self.vocabulary: dict = {}
273
+ self._doc_count = 0
274
+
275
+ def embed(self, text: str) -> list[float]:
276
+ """Embed text using simple hashing."""
277
+ # Simple hash-based embedding
278
+
279
+ # Create a deterministic embedding based on text hash
280
+ hash_bytes = hashlib.sha256(text.encode()).digest()
281
+
282
+ # Expand to desired dimension
283
+ embedding = []
284
+ for i in range(self._dimension):
285
+ # Use different parts of hash
286
+ idx = i % len(hash_bytes)
287
+ val = (hash_bytes[idx] / 255.0) * 2 - 1 # Scale to [-1, 1]
288
+ embedding.append(val)
289
+
290
+ # Normalize
291
+ norm = np.linalg.norm(embedding)
292
+ if norm > 0:
293
+ embedding = [v / norm for v in embedding]
294
+
295
+ return embedding
296
+
297
+ def embed_batch(self, texts: list[str]) -> list[list[float]]:
298
+ """Embed multiple texts."""
299
+ return [self.embed(text) for text in texts]
300
+
301
+ @property
302
+ def dimension(self) -> int:
303
+ """Get embedding dimension."""
304
+ return self._dimension
305
+
306
+
307
+ def create_embedder(
308
+ embedder_type: str = "sentence_transformer",
309
+ **kwargs,
310
+ ) -> Embedder:
311
+ """Factory function to create embedders.
312
+
313
+ Args:
314
+ embedder_type: Type of embedder to create
315
+ **kwargs: Additional arguments for the embedder
316
+
317
+ Returns:
318
+ Embedder instance
319
+ """
320
+ if embedder_type == "sentence_transformer":
321
+ return SentenceTransformerEmbedder(**kwargs)
322
+ if embedder_type == "llama_cpp":
323
+ return LlamaCppEmbedder(**kwargs)
324
+ if embedder_type == "modelscope":
325
+ return ModelScopeEmbedder(**kwargs)
326
+ if embedder_type == "simple":
327
+ return SimpleEmbedder(**kwargs)
328
+ raise ValueError(f"Unknown embedder type: {embedder_type}")
@@ -0,0 +1,92 @@
1
+ """Embedding model factory."""
2
+
3
+ from sif.embedding.embedder import (
4
+ LlamaCppEmbedder,
5
+ ModelScopeEmbedder,
6
+ SentenceTransformerEmbedder,
7
+ )
8
+ from sif.embedding.model import EmbeddingModel, ModelType
9
+ from sif.utils.logging import get_logger
10
+
11
+
12
+ logger = get_logger(__name__)
13
+
14
+
15
+ class EmbeddingModelFactory:
16
+ """Factory for creating embedding model instances."""
17
+
18
+ def create_model(
19
+ self,
20
+ model_type: ModelType,
21
+ model_path: str | None,
22
+ model_name: str,
23
+ **kwargs: dict[str, any],
24
+ ) -> EmbeddingModel:
25
+ """Create an embedding model instance."""
26
+ if model_type == ModelType.SENTENCE_TRANSFORMERS:
27
+ return self._create_sentence_transformers_model(model_name, **kwargs)
28
+ if model_type == ModelType.GGUF:
29
+ return self._create_gguf_model(model_path, **kwargs)
30
+ if model_type == ModelType.OPENAI:
31
+ return self._create_openai_model(model_name, **kwargs)
32
+ if model_type == ModelType.HUGGINGFACE:
33
+ return self._create_huggingface_model(model_name, **kwargs)
34
+ if model_type == ModelType.MODELSCOPE:
35
+ return self._create_modelscope_model(model_name, **kwargs)
36
+ raise ValueError(f"Unsupported model type: {model_type}")
37
+
38
+ def _create_gguf_model(
39
+ self,
40
+ model_path: str | None,
41
+ **kwargs: dict[str, any],
42
+ ) -> EmbeddingModel:
43
+ """Create a GGUF model using llama-cpp-python."""
44
+ if not model_path:
45
+ raise ValueError("model_path is required for GGUF models")
46
+
47
+ return LlamaCppEmbedder(
48
+ model_path=model_path,
49
+ n_ctx=kwargs.get("n_ctx", 2048),
50
+ n_threads=kwargs.get("n_threads"),
51
+ verbose=kwargs.get("verbose", False),
52
+ )
53
+
54
+ def _create_sentence_transformers_model(
55
+ self,
56
+ model_name: str,
57
+ **kwargs: dict[str, any],
58
+ ) -> EmbeddingModel:
59
+ """Create a Sentence Transformers model."""
60
+ return SentenceTransformerEmbedder(
61
+ model_name=model_name,
62
+ device=kwargs.get("device"),
63
+ cache_dir=kwargs.get("cache_dir"),
64
+ )
65
+
66
+ def _create_openai_model(
67
+ self,
68
+ model_name: str,
69
+ **kwargs: dict[str, any],
70
+ ) -> EmbeddingModel:
71
+ """Create an OpenAI API model."""
72
+ raise NotImplementedError("OpenAI models not yet implemented")
73
+
74
+ def _create_huggingface_model(
75
+ self,
76
+ model_name: str,
77
+ **kwargs: dict[str, any],
78
+ ) -> EmbeddingModel:
79
+ """Create a HuggingFace Transformers model."""
80
+ raise NotImplementedError("HuggingFace models not yet implemented")
81
+
82
+ def _create_modelscope_model(
83
+ self,
84
+ model_name: str,
85
+ **kwargs: dict[str, any],
86
+ ) -> EmbeddingModel:
87
+ """Create a ModelScope embedder."""
88
+ return ModelScopeEmbedder(
89
+ model_id=model_name,
90
+ device=kwargs.get("device"),
91
+ cache_dir=kwargs.get("cache_dir"),
92
+ )
@@ -0,0 +1,200 @@
1
+ """Embedding manager for model lifecycle and embedding generation."""
2
+
3
+ import time
4
+
5
+ from sif.config.settings import Settings
6
+ from sif.core.models import Embedder
7
+ from sif.embedding.cache import EmbeddingCache
8
+ from sif.embedding.factory import EmbeddingModelFactory
9
+ from sif.models.embedding import EmbeddingConfig, EmbeddingResponse, ModelType
10
+ from sif.utils.logging import get_logger
11
+
12
+
13
+ logger = get_logger(__name__)
14
+
15
+
16
+ class EmbeddingManager:
17
+ """Manager for embedding model lifecycle and operations.
18
+
19
+ This class provides a high-level interface for:
20
+ - Loading and unloading embedding models
21
+ - Generating embeddings for text
22
+ - Caching embeddings for reuse
23
+ - Managing model configuration
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ config: EmbeddingConfig | None = None,
29
+ factory: EmbeddingModelFactory | None = None,
30
+ cache: EmbeddingCache | None = None,
31
+ ) -> None:
32
+ """Initialize embedding manager.
33
+
34
+ Args:
35
+ config: Embedding configuration
36
+ factory: Model factory (uses default if not provided)
37
+ cache: Embedding cache (creates default if not provided)
38
+ """
39
+ self._config = config or EmbeddingConfig()
40
+ self._factory = factory or EmbeddingModelFactory()
41
+ self._cache = cache
42
+ self._model: Embedder | None = None
43
+
44
+ @classmethod
45
+ def from_settings(cls, settings: Settings) -> "EmbeddingManager":
46
+ """Create an embedding manager from settings.
47
+
48
+ Args:
49
+ settings: Application settings
50
+
51
+ Returns:
52
+ Configured embedding manager
53
+ """
54
+ config = EmbeddingConfig(
55
+ model_type=ModelType(settings.model_type),
56
+ model_name=settings.model_name,
57
+ model_path=str(settings.model_path) if settings.model_path else None,
58
+ embedding_dim=settings.embedding_dim,
59
+ max_tokens=settings.max_tokens,
60
+ batch_size=settings.batch_size,
61
+ n_gpu_layers=settings.n_gpu_layers,
62
+ api_key=settings.api_key,
63
+ api_base=settings.api_base,
64
+ cache_embeddings=settings.cache_embeddings,
65
+ cache_dir=str(settings.get_cache_dir()) if settings.cache_embeddings else None,
66
+ )
67
+
68
+ cache = None
69
+ if config.cache_embeddings and config.cache_dir:
70
+ cache = EmbeddingCache(config.cache_dir)
71
+
72
+ return cls(config=config, cache=cache)
73
+
74
+ def load_model(self) -> None:
75
+ """Load the embedding model."""
76
+ if self._model is not None:
77
+ return
78
+
79
+ logger.info(f"Loading embedding model: {self._config.model_name}")
80
+
81
+ self._model = self._factory.create_model(
82
+ model_type=self._config.model_type,
83
+ model_path=self._config.model_path,
84
+ model_name=self._config.model_name,
85
+ embedding_dim=self._config.embedding_dim,
86
+ max_tokens=self._config.max_tokens,
87
+ n_gpu_layers=self._config.n_gpu_layers,
88
+ n_ctx=self._config.n_ctx,
89
+ api_key=self._config.api_key,
90
+ api_base=self._config.api_base,
91
+ cache_dir=self._config.cache_dir,
92
+ )
93
+
94
+ logger.info("Embedding model loaded successfully")
95
+
96
+ def unload_model(self) -> None:
97
+ """Unload the embedding model to free memory."""
98
+ if self._model is not None:
99
+ self._model = None
100
+ logger.info("Embedding model unloaded")
101
+
102
+ def embed(
103
+ self,
104
+ texts: list[str],
105
+ normalize: bool = True, # noqa: ARG002
106
+ use_cache: bool = True,
107
+ ) -> EmbeddingResponse:
108
+ """Generate embeddings for texts.
109
+
110
+ Args:
111
+ texts: Texts to embed
112
+ normalize: Whether to normalize embeddings
113
+ use_cache: Whether to use caching
114
+
115
+ Returns:
116
+ Embedding response with embeddings and metadata
117
+ """
118
+ self.load_model()
119
+
120
+ start_time = time.time()
121
+
122
+ # Check cache for cached embeddings
123
+ embeddings: list[list[float] | None] = [None] * len(texts)
124
+ texts_to_embed: list[tuple[int, str]] = []
125
+
126
+ if use_cache and self._cache:
127
+ for i, text in enumerate(texts):
128
+ cached = self._cache.get(text)
129
+ if cached is not None:
130
+ embeddings[i] = cached
131
+ else:
132
+ texts_to_embed.append((i, text))
133
+ else:
134
+ texts_to_embed = list(enumerate(texts))
135
+
136
+ # Generate embeddings for uncached texts
137
+ if texts_to_embed and self._model:
138
+ indices, to_embed = zip(*texts_to_embed)
139
+ new_embeddings = self._model.embed_batch(list(to_embed))
140
+
141
+ # Store in cache
142
+ if use_cache and self._cache:
143
+ for _idx, text, emb in zip(indices, to_embed, new_embeddings):
144
+ self._cache.set(text, emb)
145
+
146
+ # Fill in results
147
+ for idx, emb in zip(indices, new_embeddings):
148
+ embeddings[idx] = emb
149
+
150
+ processing_time = (time.time() - start_time) * 1000
151
+
152
+ # Count tokens (rough estimate)
153
+ total_tokens = sum(len(t) // 4 for t in texts)
154
+
155
+ actual_dim = self._model.dimension if self._model else self._config.embedding_dim
156
+ return EmbeddingResponse(
157
+ embeddings=[e for e in embeddings if e is not None],
158
+ model_id=self._config.model_name,
159
+ dimensions=actual_dim,
160
+ total_tokens=total_tokens,
161
+ processing_time_ms=processing_time,
162
+ )
163
+
164
+ def embed_single(
165
+ self,
166
+ text: str,
167
+ normalize: bool = True,
168
+ use_cache: bool = True,
169
+ ) -> list[float]:
170
+ """Generate embedding for a single text.
171
+
172
+ Args:
173
+ text: Text to embed
174
+ normalize: Whether to normalize the embedding
175
+ use_cache: Whether to use caching
176
+
177
+ Returns:
178
+ Embedding vector
179
+ """
180
+ response = self.embed([text], normalize=normalize, use_cache=use_cache)
181
+ return response.embeddings[0]
182
+
183
+ def get_model_info(self) -> dict:
184
+ """Get information about the loaded model.
185
+
186
+ Returns:
187
+ Model information dictionary
188
+ """
189
+ if not self._model:
190
+ return {
191
+ "loaded": False,
192
+ "model_name": self._config.model_name,
193
+ }
194
+
195
+ return {
196
+ "loaded": True,
197
+ "model_id": self._config.model_name,
198
+ "embedding_dim": self._model.dimension,
199
+ "max_tokens": self._config.max_tokens,
200
+ }