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.
- docsif-0.1.0.dist-info/METADATA +393 -0
- docsif-0.1.0.dist-info/RECORD +86 -0
- docsif-0.1.0.dist-info/WHEEL +4 -0
- docsif-0.1.0.dist-info/entry_points.txt +3 -0
- docsif-0.1.0.dist-info/licenses/LICENSE +21 -0
- sif/__init__.py +24 -0
- sif/_version.py +3 -0
- sif/cli/__init__.py +11 -0
- sif/cli/commands/__init__.py +18 -0
- sif/cli/commands/bench.py +148 -0
- sif/cli/commands/collection.py +360 -0
- sif/cli/commands/context.py +154 -0
- sif/cli/commands/get.py +200 -0
- sif/cli/commands/index.py +294 -0
- sif/cli/commands/ls.py +67 -0
- sif/cli/commands/mcp.py +93 -0
- sif/cli/commands/pull.py +111 -0
- sif/cli/commands/search.py +574 -0
- sif/cli/config.py +206 -0
- sif/cli/formatters.py +269 -0
- sif/cli/main.py +205 -0
- sif/config/__init__.py +30 -0
- sif/config/constants.py +52 -0
- sif/config/settings.py +217 -0
- sif/core/__init__.py +21 -0
- sif/core/collection.py +161 -0
- sif/core/context.py +167 -0
- sif/core/document.py +125 -0
- sif/core/models.py +298 -0
- sif/database/__init__.py +23 -0
- sif/database/connection.py +139 -0
- sif/database/database.py +136 -0
- sif/database/migrations.py +187 -0
- sif/database/repositories.py +500 -0
- sif/database/repository.py +171 -0
- sif/database/schema.py +362 -0
- sif/embedding/__init__.py +22 -0
- sif/embedding/cache.py +196 -0
- sif/embedding/embedder.py +328 -0
- sif/embedding/factory.py +92 -0
- sif/embedding/manager.py +200 -0
- sif/embedding/model.py +126 -0
- sif/indexing/__init__.py +21 -0
- sif/indexing/chunker.py +226 -0
- sif/indexing/indexer.py +226 -0
- sif/indexing/parser.py +192 -0
- sif/indexing/scanner.py +148 -0
- sif/indexing/watcher.py +156 -0
- sif/mcp/README.md +258 -0
- sif/mcp/__init__.py +9 -0
- sif/mcp/claude_desktop_config.json +16 -0
- sif/mcp/cli.py +92 -0
- sif/mcp/protocol.py +336 -0
- sif/mcp/server.py +433 -0
- sif/mcp/server_http.py +54 -0
- sif/mcp/test_mcp.py +108 -0
- sif/mcp/tools.py +497 -0
- sif/mcp/transport_http.py +424 -0
- sif/mcp/transport_stdio.py +292 -0
- sif/mcp_server/__init__.py +26 -0
- sif/mcp_server/handlers.py +68 -0
- sif/mcp_server/server.py +58 -0
- sif/mcp_server/tools.py +216 -0
- sif/mcp_server/transport.py +221 -0
- sif/models/__init__.py +70 -0
- sif/models/collection.py +51 -0
- sif/models/context.py +45 -0
- sif/models/document.py +76 -0
- sif/models/download.py +208 -0
- sif/models/embedding.py +71 -0
- sif/models/search.py +92 -0
- sif/search/__init__.py +22 -0
- sif/search/benchmark.py +110 -0
- sif/search/bm25.py +216 -0
- sif/search/expansion.py +157 -0
- sif/search/hybrid.py +323 -0
- sif/search/rerank.py +390 -0
- sif/search/rrf.py +186 -0
- sif/search/snippets.py +130 -0
- sif/search/strategy.py +97 -0
- sif/search/vector.py +177 -0
- sif/utils/__init__.py +26 -0
- sif/utils/logging.py +143 -0
- sif/utils/paths.py +82 -0
- sif/utils/progress.py +83 -0
- 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}")
|
sif/embedding/factory.py
ADDED
|
@@ -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
|
+
)
|
sif/embedding/manager.py
ADDED
|
@@ -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
|
+
}
|