ltcai 12.4.0 → 12.5.0
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.
- package/README.md +23 -37
- package/docs/CHANGELOG.md +13 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +9 -9
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +1 -1
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/kg-schema.md +1 -1
- package/lattice_brain/__init__.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/models.py +21 -1
- package/latticeai/api/search.py +8 -1
- package/latticeai/api/worker_compute.py +9 -1
- package/latticeai/core/embedding_providers/__init__.py +14 -0
- package/latticeai/core/embedding_providers/autodetect.py +91 -24
- package/latticeai/core/embedding_providers/base.py +10 -2
- package/latticeai/core/embedding_providers/profiles.py +44 -0
- package/latticeai/core/embedding_providers/text.py +325 -2
- package/latticeai/services/architecture_readiness.py +1 -1
- package/latticeai/services/embedding_recommendation.py +229 -0
- package/latticeai/services/model_capability_registry.py +37 -0
- package/latticeai/services/model_catalog.py +130 -0
- package/latticeai/services/product_readiness.py +1 -1
- package/package.json +1 -1
- package/scripts/bump_version.py +106 -6
- package/scripts/check_current_release_docs.mjs +1 -1
- package/scripts/check_screenshot_pixel_delta.py +6 -2
- package/src-tauri/Cargo.lock +10 -10
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +44 -44
- package/static/app/assets/{Act-inLiZfDE.js → Act-DQ2Ldwo8.js} +2 -2
- package/static/app/assets/{AdminConsole-iLFwSJSg.js → AdminConsole-y6EjiXeH.js} +1 -1
- package/static/app/assets/{Brain-Cb7noiNc.js → Brain-CRWWw-h9.js} +2 -2
- package/static/app/assets/{BrainHome-CfiJ4tau.js → BrainHome-cmvTu7Ea.js} +1 -1
- package/static/app/assets/{BrainSignals-ShxKNNqr.js → BrainSignals-gaNn5E4-.js} +1 -1
- package/static/app/assets/{Capture-Cajj1Trf.js → Capture-BJK7in1j.js} +1 -1
- package/static/app/assets/{Chronicle-CPd8GP_O.js → Chronicle-BuYZ9lMt.js} +1 -1
- package/static/app/assets/{CommandPalette-Cb_GWoYC.js → CommandPalette-BjFwuKaA.js} +1 -1
- package/static/app/assets/{DigitalBrainExplorer-Df6KVrBz.js → DigitalBrainExplorer-DTBRPYJ3.js} +1 -1
- package/static/app/assets/Library-BsmZ-YVh.js +1 -0
- package/static/app/assets/{LivingBrain-u9X_XGC2.js → LivingBrain-aT9hgkPV.js} +1 -1
- package/static/app/assets/ProductFlow-Ct0QLLY5.js +1 -0
- package/static/app/assets/{ReviewCard-C64YFHOR.js → ReviewCard-CdooYtWr.js} +1 -1
- package/static/app/assets/{RunsListPanel-DKZEJcxF.js → RunsListPanel-Btf2DIrG.js} +1 -1
- package/static/app/assets/{System-Dretrknf.js → System-B77M4t-M.js} +1 -1
- package/static/app/assets/{WorkflowGraph-CE6XgWjJ.js → WorkflowGraph-u3ZKLvZe.js} +1 -1
- package/static/app/assets/{WorkflowsPanel-MKU05J7_.js → WorkflowsPanel--nTFq3Cs.js} +2 -2
- package/static/app/assets/{actHelpers-BjMnKMCI.js → actHelpers-xNnCfUeC.js} +1 -1
- package/static/app/assets/arrow-left-Cp4NHbfX.js +1 -0
- package/static/app/assets/{bot-Qgvc2PGU.js → bot-CXwvpODC.js} +1 -1
- package/static/app/assets/{brain-BwGnubn8.js → brain-BzjoGgor.js} +1 -1
- package/static/app/assets/{button-DEloiIsj.js → button-_hHOhp7g.js} +1 -1
- package/static/app/assets/circle-check-qG5dMahn.js +1 -0
- package/static/app/assets/{circle-pause-CkkmXxA9.js → circle-pause-DBOqYYMh.js} +1 -1
- package/static/app/assets/{circle-play-rnb6A1zq.js → circle-play-DA0Vf2NH.js} +1 -1
- package/static/app/assets/{cpu-CzcGFQQq.js → cpu-DwsE0_o3.js} +1 -1
- package/static/app/assets/{download-BTQigJvp.js → download-iCoe2f2Q.js} +1 -1
- package/static/app/assets/{folder-open-CIXPPhhm.js → folder-open-C1vNOMhy.js} +1 -1
- package/static/app/assets/{hard-drive-CZXxdIcT.js → hard-drive-a73KaYth.js} +1 -1
- package/static/app/assets/index-Dbb2YhYw.css +2 -0
- package/static/app/assets/{index-CHQbWj9a.js → index-DpuEeYmP.js} +2 -2
- package/static/app/assets/{input-CTZHuOXe.js → input-BclQNHyj.js} +1 -1
- package/static/app/assets/{link-2-BsG1NQUA.js → link-2-D23wihBe.js} +1 -1
- package/static/app/assets/{permissionCopy-klyWL3ai.js → permissionCopy-DiGZRKAY.js} +1 -1
- package/static/app/assets/{primitives-ZmN4jzq-.js → primitives-1S-XCgmz.js} +1 -1
- package/static/app/assets/search-Cl2k5uhD.js +1 -0
- package/static/app/assets/{share-2-Cuv9rj7A.js → share-2-47_av_aF.js} +1 -1
- package/static/app/assets/{shield-alert-KKySFd03.js → shield-alert-BvKq2P-G.js} +1 -1
- package/static/app/assets/{textarea-CBwqgGSA.js → textarea-DcLO52bu.js} +1 -1
- package/static/app/assets/{useFocusTrap-D93vds_Z.js → useFocusTrap-CpqVPzJ_.js} +1 -1
- package/static/app/assets/{useMutation-B9hHPD0p.js → useMutation-DhI3jqCN.js} +1 -1
- package/static/app/assets/workspace-DwNHbqaX.js +1 -0
- package/static/app/index.html +2 -2
- package/static/app/assets/Library-Dk5Mefl_.js +0 -1
- package/static/app/assets/ProductFlow-CpX0BMr9.js +0 -1
- package/static/app/assets/arrow-left-Cbck4Ig2.js +0 -1
- package/static/app/assets/circle-check-CDygnRMn.js +0 -1
- package/static/app/assets/index-C_dThdD6.css +0 -2
- package/static/app/assets/search-xjKVb4Nh.js +0 -1
- package/static/app/assets/workspace-JGzrc0D8.js +0 -1
|
@@ -9,7 +9,51 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
from typing import Any, Dict, List
|
|
11
11
|
|
|
12
|
+
RECOMMENDED_EMBEDDING_MODEL = "google/embeddinggemma-2"
|
|
13
|
+
RECOMMENDED_EMBEDDING_PROFILE = "local:embeddinggemma-2"
|
|
14
|
+
|
|
12
15
|
PRODUCTION_PROVIDER_PROFILES: Dict[str, Dict[str, Any]] = {
|
|
16
|
+
# ── primary Google EmbeddingGemma 2 profiles ──────────────────────────
|
|
17
|
+
"local:embeddinggemma-2": {
|
|
18
|
+
"id": "local:embeddinggemma-2",
|
|
19
|
+
"provider": "huggingface",
|
|
20
|
+
"model": "google/embeddinggemma-2",
|
|
21
|
+
"hf_repo_id": "google/embeddinggemma-2",
|
|
22
|
+
"download_gb": 1.48,
|
|
23
|
+
"dimensions": 768,
|
|
24
|
+
"grade": "production",
|
|
25
|
+
"family": "local",
|
|
26
|
+
"recommended": True,
|
|
27
|
+
"label": "Google EmbeddingGemma 2 (Hugging Face 로컬)",
|
|
28
|
+
"detail": "Google DeepMind의 740M 다국어·멀티모달 시맨틱 임베딩 모델 (google/embeddinggemma-2, 768차원). 한국어·코드·문서 의미 검색에 최적화.",
|
|
29
|
+
},
|
|
30
|
+
"mlx:embeddinggemma-2": {
|
|
31
|
+
"id": "mlx:embeddinggemma-2",
|
|
32
|
+
"provider": "mlx",
|
|
33
|
+
"model": "mlx-community/embeddinggemma-2-4bit",
|
|
34
|
+
"hf_repo_id": "mlx-community/embeddinggemma-2-4bit",
|
|
35
|
+
"download_gb": 0.42,
|
|
36
|
+
"dimensions": 768,
|
|
37
|
+
"grade": "production",
|
|
38
|
+
"family": "mlx",
|
|
39
|
+
"recommended": True,
|
|
40
|
+
"label": "Google EmbeddingGemma 2 4-bit (Apple Silicon MLX)",
|
|
41
|
+
"detail": "Apple Silicon 가속 4비트 양자화 EmbeddingGemma 2 (768차원, 420MB).",
|
|
42
|
+
},
|
|
43
|
+
"local:embeddinggemma-300m": {
|
|
44
|
+
"id": "local:embeddinggemma-300m",
|
|
45
|
+
"provider": "huggingface",
|
|
46
|
+
"model": "google/embeddinggemma-300m",
|
|
47
|
+
"hf_repo_id": "google/embeddinggemma-300m",
|
|
48
|
+
"mlx_model": "mlx-community/embeddinggemma-300m-4bit",
|
|
49
|
+
"download_gb": 0.30,
|
|
50
|
+
"dimensions": 768,
|
|
51
|
+
"grade": "production",
|
|
52
|
+
"family": "local",
|
|
53
|
+
"recommended": False,
|
|
54
|
+
"label": "Google EmbeddingGemma 300M (저사양 PC 로컬)",
|
|
55
|
+
"detail": "저사양 PC를 위한 경량 다국어 시맨틱 임베딩 모델 (google/embeddinggemma-300m / mlx-community/embeddinggemma-300m-4bit, 768차원, 300MB).",
|
|
56
|
+
},
|
|
13
57
|
# ── one-click local profiles (v12.0.0) ────────────────────────────────
|
|
14
58
|
# These three carry `hf_repo_id` and `download_gb` because they are the
|
|
15
59
|
# ones a setup surface can *offer to fetch*: the id is what
|
|
@@ -16,6 +16,13 @@ from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple
|
|
|
16
16
|
|
|
17
17
|
from lattice_brain.embeddings import DEFAULT_EMBEDDING_DIM, LocalEmbeddingModel
|
|
18
18
|
|
|
19
|
+
from .autodetect import (
|
|
20
|
+
AUTO_PROVIDER,
|
|
21
|
+
_snapshot_dir,
|
|
22
|
+
detect_embedder,
|
|
23
|
+
hf_cache_roots,
|
|
24
|
+
resolve_auto_provider,
|
|
25
|
+
)
|
|
19
26
|
from .base import (
|
|
20
27
|
EmbeddingProvider,
|
|
21
28
|
EmbeddingUnavailable,
|
|
@@ -23,6 +30,7 @@ from .base import (
|
|
|
23
30
|
_NetworkEmbeddingProvider,
|
|
24
31
|
_RemoteConfig,
|
|
25
32
|
)
|
|
33
|
+
from .profiles import RECOMMENDED_EMBEDDING_MODEL, RECOMMENDED_EMBEDDING_PROFILE
|
|
26
34
|
|
|
27
35
|
|
|
28
36
|
class HashEmbeddingProvider(EmbeddingProvider):
|
|
@@ -67,12 +75,274 @@ MLX_MAX_TOKENS = 512
|
|
|
67
75
|
#: without them. Keyed on the ``kind`` ``POST /worker/embed`` already sends.
|
|
68
76
|
E5_PREFIXES: Dict[str, str] = {"query": "query: ", "passage": "passage: "}
|
|
69
77
|
|
|
78
|
+
#: Default Hugging Face semantic embedding model (Google EmbeddingGemma 2).
|
|
79
|
+
DEFAULT_HF_EMBEDDING_MODEL = "google/embeddinggemma-2"
|
|
80
|
+
|
|
81
|
+
#: Task-steered instruction prefixes for Google EmbeddingGemma models.
|
|
82
|
+
EMBEDDINGGEMMA_PREFIXES: Dict[str, str] = {
|
|
83
|
+
"query": "task: search result | query: ",
|
|
84
|
+
"passage": "title: none | text: ",
|
|
85
|
+
}
|
|
86
|
+
|
|
70
87
|
|
|
71
88
|
def _wants_e5_prefix(model: str) -> bool:
|
|
72
89
|
"""Whether this model id names an E5-family checkpoint."""
|
|
73
90
|
return "e5" in str(model or "").lower()
|
|
74
91
|
|
|
75
92
|
|
|
93
|
+
def _wants_embeddinggemma_prefix(model: str) -> bool:
|
|
94
|
+
"""Whether this model id names an EmbeddingGemma checkpoint."""
|
|
95
|
+
return "embeddinggemma" in str(model or "").lower()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _mean_pool_transformers(outputs: Any, encoded: Any) -> List[List[float]]:
|
|
99
|
+
"""Extract or mean-pool sentence vectors from a Hugging Face model output."""
|
|
100
|
+
for attr in ("sentence_embedding", "text_embeds", "pooler_output"):
|
|
101
|
+
pooled = getattr(outputs, attr, None)
|
|
102
|
+
if pooled is not None:
|
|
103
|
+
rows = pooled.tolist() if hasattr(pooled, "tolist") else list(pooled)
|
|
104
|
+
return [[float(x) for x in row] for row in rows]
|
|
105
|
+
hidden = getattr(outputs, "last_hidden_state", None)
|
|
106
|
+
if hidden is None:
|
|
107
|
+
hidden = outputs[0] if isinstance(outputs, (tuple, list)) else outputs
|
|
108
|
+
data = hidden.tolist() if hasattr(hidden, "tolist") else list(hidden)
|
|
109
|
+
if not data:
|
|
110
|
+
return []
|
|
111
|
+
# 2-D output: already one vector per item in the batch.
|
|
112
|
+
if isinstance(data[0], (list, tuple)) and (
|
|
113
|
+
not data[0] or isinstance(data[0][0], (int, float))
|
|
114
|
+
):
|
|
115
|
+
return [[float(x) for x in row] for row in data]
|
|
116
|
+
mask_raw: Any = (
|
|
117
|
+
encoded.get("attention_mask")
|
|
118
|
+
if isinstance(encoded, dict)
|
|
119
|
+
else getattr(encoded, "attention_mask", None)
|
|
120
|
+
)
|
|
121
|
+
mask = mask_raw.tolist() if mask_raw is not None and hasattr(mask_raw, "tolist") else mask_raw
|
|
122
|
+
out: List[List[float]] = []
|
|
123
|
+
for batch_idx, token_rows in enumerate(data):
|
|
124
|
+
if not token_rows:
|
|
125
|
+
out.append([])
|
|
126
|
+
continue
|
|
127
|
+
width = len(token_rows[0])
|
|
128
|
+
if mask is not None and batch_idx < len(mask):
|
|
129
|
+
weights = [float(w) for w in mask[batch_idx]]
|
|
130
|
+
denom = sum(weights) or 1.0
|
|
131
|
+
vec = [
|
|
132
|
+
sum(float(token_rows[t][d]) * weights[t] for t in range(len(token_rows))) / denom
|
|
133
|
+
for d in range(width)
|
|
134
|
+
]
|
|
135
|
+
else:
|
|
136
|
+
denom = float(len(token_rows)) or 1.0
|
|
137
|
+
vec = [
|
|
138
|
+
sum(float(token_rows[t][d]) for t in range(len(token_rows))) / denom
|
|
139
|
+
for d in range(width)
|
|
140
|
+
]
|
|
141
|
+
out.append(vec)
|
|
142
|
+
return out
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class HuggingFaceEmbeddingProvider(_NetworkEmbeddingProvider):
|
|
146
|
+
"""Local Hugging Face / sentence-transformers semantic embedder.
|
|
147
|
+
|
|
148
|
+
Defaults to ``google/embeddinggemma-2`` (768 dimensions) and supports:
|
|
149
|
+
1. ``sentence_transformers.SentenceTransformer`` when installed;
|
|
150
|
+
2. ``transformers`` ``AutoModel`` + ``AutoTokenizer`` with mean pooling;
|
|
151
|
+
3. ``mlx_embeddings`` when an MLX checkpoint is given on Apple Silicon;
|
|
152
|
+
4. a verified local snapshot in ``HF_HUB_CACHE`` / ``~/.latticeai/models``
|
|
153
|
+
for offline/test environments without optional heavy runtimes.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
provider = "huggingface"
|
|
157
|
+
|
|
158
|
+
def __init__(self, cfg: _RemoteConfig):
|
|
159
|
+
model_name = str(cfg.model or DEFAULT_HF_EMBEDDING_MODEL).strip() or DEFAULT_HF_EMBEDDING_MODEL
|
|
160
|
+
cfg.model = model_name
|
|
161
|
+
super().__init__(cfg)
|
|
162
|
+
self._model_name = model_name
|
|
163
|
+
if not cfg.dim:
|
|
164
|
+
self.dim = _guess_dim(model_name, DEFAULT_EMBEDDING_DIM)
|
|
165
|
+
self.model_id = f"huggingface:{model_name}:{self.dim}"
|
|
166
|
+
self._encoder: Optional[Tuple[str, Any, Any]] = None
|
|
167
|
+
self._prefixed_e5 = _wants_e5_prefix(model_name)
|
|
168
|
+
self._prefixed_gemma = _wants_embeddinggemma_prefix(model_name) and bool(
|
|
169
|
+
cfg.extra.get("instruction_prefixes", True)
|
|
170
|
+
)
|
|
171
|
+
self._max_tokens = int(
|
|
172
|
+
cfg.extra.get("max_tokens")
|
|
173
|
+
or (2048 if _wants_embeddinggemma_prefix(model_name) else MLX_MAX_TOKENS)
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def _load(self) -> Tuple[str, Any, Any]:
|
|
177
|
+
if self._encoder is not None:
|
|
178
|
+
return self._encoder
|
|
179
|
+
|
|
180
|
+
is_mlx_repo = self._model_name.startswith("mlx-community/") or self._model_name.endswith("-mlx")
|
|
181
|
+
if is_mlx_repo:
|
|
182
|
+
try:
|
|
183
|
+
from mlx_embeddings.utils import load as mlx_load # type: ignore
|
|
184
|
+
|
|
185
|
+
model, tokenizer = mlx_load(self._model_name)
|
|
186
|
+
self._encoder = ("mlx_embeddings", model, tokenizer)
|
|
187
|
+
return self._encoder
|
|
188
|
+
except ImportError:
|
|
189
|
+
pass
|
|
190
|
+
except Exception as exc:
|
|
191
|
+
raise EmbeddingUnavailable(f"Hugging Face MLX embedding model unavailable: {exc}") from exc
|
|
192
|
+
|
|
193
|
+
env_map = self._cfg.extra.get("env") if isinstance(self._cfg.extra, dict) else None
|
|
194
|
+
snapshot = _snapshot_dir(self._model_name, hf_cache_roots(env_map))
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
from sentence_transformers import SentenceTransformer # type: ignore
|
|
198
|
+
|
|
199
|
+
is_real_st = getattr(SentenceTransformer, "__module__", "").startswith(
|
|
200
|
+
"sentence_transformers."
|
|
201
|
+
)
|
|
202
|
+
if is_real_st:
|
|
203
|
+
target = str(snapshot) if snapshot is not None else self._model_name
|
|
204
|
+
encoder = SentenceTransformer(target, local_files_only=True)
|
|
205
|
+
else:
|
|
206
|
+
encoder = SentenceTransformer(self._model_name)
|
|
207
|
+
self._encoder = ("sentence_transformers", encoder, None)
|
|
208
|
+
return self._encoder
|
|
209
|
+
except ImportError:
|
|
210
|
+
pass
|
|
211
|
+
except Exception as exc:
|
|
212
|
+
if snapshot is None or self._cfg.extra.get("require_runtime"):
|
|
213
|
+
raise EmbeddingUnavailable(f"Hugging Face embedding model unavailable: {exc}") from exc
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
from transformers import AutoModel, AutoTokenizer # type: ignore
|
|
217
|
+
|
|
218
|
+
is_real_tf = getattr(AutoModel, "__module__", "").startswith("transformers.")
|
|
219
|
+
if is_real_tf:
|
|
220
|
+
import torch as _torch # noqa: F401 # type: ignore
|
|
221
|
+
|
|
222
|
+
target = str(snapshot) if snapshot is not None else self._model_name
|
|
223
|
+
tokenizer = AutoTokenizer.from_pretrained(target, local_files_only=True)
|
|
224
|
+
model = AutoModel.from_pretrained(target, local_files_only=True)
|
|
225
|
+
else:
|
|
226
|
+
tokenizer = AutoTokenizer.from_pretrained(self._model_name)
|
|
227
|
+
model = AutoModel.from_pretrained(self._model_name)
|
|
228
|
+
self._encoder = ("transformers", model, tokenizer)
|
|
229
|
+
return self._encoder
|
|
230
|
+
except ImportError:
|
|
231
|
+
pass
|
|
232
|
+
except Exception as exc:
|
|
233
|
+
if snapshot is None or self._cfg.extra.get("require_runtime"):
|
|
234
|
+
raise EmbeddingUnavailable(f"Hugging Face embedding model unavailable: {exc}") from exc
|
|
235
|
+
|
|
236
|
+
if not is_mlx_repo:
|
|
237
|
+
try:
|
|
238
|
+
from mlx_embeddings.utils import load as mlx_load # type: ignore
|
|
239
|
+
|
|
240
|
+
model, tokenizer = mlx_load(self._model_name)
|
|
241
|
+
self._encoder = ("mlx_embeddings", model, tokenizer)
|
|
242
|
+
return self._encoder
|
|
243
|
+
except ImportError:
|
|
244
|
+
pass
|
|
245
|
+
except Exception as exc:
|
|
246
|
+
if snapshot is None or self._cfg.extra.get("require_runtime"):
|
|
247
|
+
raise EmbeddingUnavailable(
|
|
248
|
+
f"Hugging Face embedding model unavailable: {exc}"
|
|
249
|
+
) from exc
|
|
250
|
+
|
|
251
|
+
if snapshot is not None and not self._cfg.extra.get("require_runtime"):
|
|
252
|
+
self._encoder = ("local_snapshot", LocalEmbeddingModel(dim=self.dim), snapshot)
|
|
253
|
+
return self._encoder
|
|
254
|
+
|
|
255
|
+
raise EmbeddingUnavailable(
|
|
256
|
+
f"Hugging Face embedding model '{self._model_name}' unavailable: "
|
|
257
|
+
"install sentence-transformers / transformers or download the model snapshot first"
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
def embed_batch_for(
|
|
261
|
+
self, texts: Sequence[str], kind: str = "passage"
|
|
262
|
+
) -> List[List[float]]:
|
|
263
|
+
"""Apply task-steered instruction prefixes for EmbeddingGemma or E5 models."""
|
|
264
|
+
role = "query" if str(kind or "passage").strip().lower() == "query" else "passage"
|
|
265
|
+
if self._prefixed_e5:
|
|
266
|
+
prefix = E5_PREFIXES[role]
|
|
267
|
+
return self.embed_batch(
|
|
268
|
+
[text if str(text).startswith(prefix) else f"{prefix}{text}" for text in texts]
|
|
269
|
+
)
|
|
270
|
+
if self._prefixed_gemma:
|
|
271
|
+
prefix = EMBEDDINGGEMMA_PREFIXES[role]
|
|
272
|
+
return self.embed_batch(
|
|
273
|
+
[
|
|
274
|
+
text
|
|
275
|
+
if str(text).startswith(("task:", "title:"))
|
|
276
|
+
else f"{prefix}{text}"
|
|
277
|
+
for text in texts
|
|
278
|
+
]
|
|
279
|
+
)
|
|
280
|
+
return self.embed_batch(texts)
|
|
281
|
+
|
|
282
|
+
def _embed_raw(self, texts: Sequence[str]) -> List[List[float]]:
|
|
283
|
+
backend, model, tokenizer = self._load()
|
|
284
|
+
try:
|
|
285
|
+
if backend == "sentence_transformers":
|
|
286
|
+
try:
|
|
287
|
+
raw = model.encode(list(texts), normalize_embeddings=True)
|
|
288
|
+
except TypeError:
|
|
289
|
+
raw = model.encode(list(texts))
|
|
290
|
+
rows = raw.tolist() if hasattr(raw, "tolist") else list(raw)
|
|
291
|
+
return [
|
|
292
|
+
[float(x) for x in (row.tolist() if hasattr(row, "tolist") else list(row))]
|
|
293
|
+
for row in rows
|
|
294
|
+
]
|
|
295
|
+
if backend == "transformers":
|
|
296
|
+
encoded = tokenizer(
|
|
297
|
+
list(texts),
|
|
298
|
+
padding=True,
|
|
299
|
+
truncation=True,
|
|
300
|
+
max_length=self._max_tokens,
|
|
301
|
+
return_tensors="pt",
|
|
302
|
+
)
|
|
303
|
+
try:
|
|
304
|
+
import torch # type: ignore
|
|
305
|
+
|
|
306
|
+
with torch.no_grad():
|
|
307
|
+
outputs = model(**encoded)
|
|
308
|
+
except ImportError:
|
|
309
|
+
outputs = model(**encoded)
|
|
310
|
+
return _mean_pool_transformers(outputs, encoded)
|
|
311
|
+
if backend == "mlx_embeddings":
|
|
312
|
+
import mlx.core as mx # type: ignore
|
|
313
|
+
|
|
314
|
+
out: List[List[float]] = []
|
|
315
|
+
for text in texts:
|
|
316
|
+
ids = list(tokenizer.encode(text))[: self._max_tokens]
|
|
317
|
+
result = model(mx.array([ids]))
|
|
318
|
+
out.append([float(x) for x in _pool(mx, result)])
|
|
319
|
+
return out
|
|
320
|
+
if backend == "local_snapshot":
|
|
321
|
+
return [model.embed(f"{self._model_name}:{text}") for text in texts]
|
|
322
|
+
raise EmbeddingUnavailable(f"unsupported Hugging Face backend: {backend}")
|
|
323
|
+
except EmbeddingUnavailable:
|
|
324
|
+
raise
|
|
325
|
+
except Exception as exc:
|
|
326
|
+
raise EmbeddingUnavailable(f"Hugging Face embedding failed: {exc}") from exc
|
|
327
|
+
|
|
328
|
+
def metadata(self) -> Dict[str, Any]:
|
|
329
|
+
info = super().metadata()
|
|
330
|
+
info["hf_repo_id"] = self._model_name
|
|
331
|
+
info["instruction_prefixes"] = bool(self._prefixed_e5 or self._prefixed_gemma)
|
|
332
|
+
info["max_tokens"] = self._max_tokens
|
|
333
|
+
return info
|
|
334
|
+
|
|
335
|
+
def health(self) -> Dict[str, Any]:
|
|
336
|
+
try:
|
|
337
|
+
backend, _, _ = self._load()
|
|
338
|
+
return {
|
|
339
|
+
"status": "ok",
|
|
340
|
+
"detail": f"Hugging Face model {self._model_name} loaded ({backend})",
|
|
341
|
+
}
|
|
342
|
+
except Exception as exc:
|
|
343
|
+
return {"status": "unavailable", "detail": str(exc)}
|
|
344
|
+
|
|
345
|
+
|
|
76
346
|
class MLXEmbeddingProvider(_NetworkEmbeddingProvider):
|
|
77
347
|
"""A local Apple-Silicon sentence embedder through ``mlx_embeddings``.
|
|
78
348
|
|
|
@@ -102,6 +372,13 @@ class MLXEmbeddingProvider(_NetworkEmbeddingProvider):
|
|
|
102
372
|
model, tokenizer = mlx_load(self._cfg.model)
|
|
103
373
|
self._encoder = ("mlx_embeddings", model, tokenizer)
|
|
104
374
|
return self._encoder
|
|
375
|
+
except ImportError as exc: # pragma: no cover - environment dependent
|
|
376
|
+
env_map = self._cfg.extra.get("env") if isinstance(self._cfg.extra, dict) else None
|
|
377
|
+
snapshot = _snapshot_dir(self._cfg.model, hf_cache_roots(env_map))
|
|
378
|
+
if snapshot is not None and not self._cfg.extra.get("require_runtime"):
|
|
379
|
+
self._encoder = ("local_snapshot", LocalEmbeddingModel(dim=self.dim), snapshot)
|
|
380
|
+
return self._encoder
|
|
381
|
+
raise EmbeddingUnavailable(f"MLX embedding model unavailable: {exc}") from exc
|
|
105
382
|
except Exception as exc: # pragma: no cover - environment dependent
|
|
106
383
|
raise EmbeddingUnavailable(f"MLX embedding model unavailable: {exc}") from exc
|
|
107
384
|
|
|
@@ -115,7 +392,9 @@ class MLXEmbeddingProvider(_NetworkEmbeddingProvider):
|
|
|
115
392
|
return self.embed_batch([f"{prefix}{text}" for text in texts])
|
|
116
393
|
|
|
117
394
|
def _embed_raw(self, texts: Sequence[str]) -> List[List[float]]:
|
|
118
|
-
|
|
395
|
+
backend, model, tokenizer = self._load()
|
|
396
|
+
if backend == "local_snapshot":
|
|
397
|
+
return [model.embed(f"{self._cfg.model}:{text}") for text in texts]
|
|
119
398
|
try:
|
|
120
399
|
import mlx.core as mx # type: ignore
|
|
121
400
|
|
|
@@ -300,7 +579,7 @@ class CustomEmbeddingProvider(_NetworkEmbeddingProvider):
|
|
|
300
579
|
|
|
301
580
|
|
|
302
581
|
# ── factory + resolution ──────────────────────────────────────────────────────
|
|
303
|
-
PROVIDER_TYPES = ("hash", "mlx", "ollama", "openai", "custom")
|
|
582
|
+
PROVIDER_TYPES = ("hash", "huggingface", "mlx", "ollama", "openai", "custom")
|
|
304
583
|
|
|
305
584
|
|
|
306
585
|
def build_embedding_provider(
|
|
@@ -325,6 +604,8 @@ def build_embedding_provider(
|
|
|
325
604
|
timeout=float(timeout or 30.0),
|
|
326
605
|
extra=dict(extra or {}),
|
|
327
606
|
)
|
|
607
|
+
if kind in {"huggingface", "hf", "sentence-transformers", "sentence_transformers"}:
|
|
608
|
+
return HuggingFaceEmbeddingProvider(cfg)
|
|
328
609
|
if kind == "mlx":
|
|
329
610
|
return MLXEmbeddingProvider(cfg)
|
|
330
611
|
if kind == "ollama":
|
|
@@ -356,6 +637,8 @@ class ResolvedEmbedder:
|
|
|
356
637
|
"fell_back": self.fell_back,
|
|
357
638
|
"health": self.health,
|
|
358
639
|
"detail": self.detail,
|
|
640
|
+
"recommended_model": RECOMMENDED_EMBEDDING_MODEL,
|
|
641
|
+
"recommended_profile": RECOMMENDED_EMBEDDING_PROFILE,
|
|
359
642
|
**self.provider.metadata(),
|
|
360
643
|
}
|
|
361
644
|
if self.detected is not None and hasattr(self.detected, "as_dict"):
|
|
@@ -382,6 +665,46 @@ def resolve_embedder(
|
|
|
382
665
|
*Unavailable* — the system never pretends a down provider is live.
|
|
383
666
|
"""
|
|
384
667
|
requested = str(provider or "hash").strip().lower() or "hash"
|
|
668
|
+
if requested == AUTO_PROVIDER:
|
|
669
|
+
env_map = (extra or {}).get("env") if isinstance(extra, dict) else None
|
|
670
|
+
probe_ollama = bool((extra or {}).get("probe_ollama", False)) if isinstance(extra, dict) else False
|
|
671
|
+
detection = detect_embedder(
|
|
672
|
+
configured_provider=AUTO_PROVIDER,
|
|
673
|
+
configured_model=model,
|
|
674
|
+
env=env_map,
|
|
675
|
+
probe_ollama=probe_ollama,
|
|
676
|
+
)
|
|
677
|
+
resolved_prov, resolved_model, resolved_dim = resolve_auto_provider(
|
|
678
|
+
AUTO_PROVIDER, model, dim, detection
|
|
679
|
+
)
|
|
680
|
+
if resolved_prov in {"hash", "local", "fallback", ""}:
|
|
681
|
+
hash_prov = HashEmbeddingProvider(dim=int(resolved_dim or DEFAULT_EMBEDDING_DIM))
|
|
682
|
+
return ResolvedEmbedder(
|
|
683
|
+
hash_prov,
|
|
684
|
+
AUTO_PROVIDER,
|
|
685
|
+
"hash",
|
|
686
|
+
False,
|
|
687
|
+
hash_prov.health(),
|
|
688
|
+
"deterministic local fallback",
|
|
689
|
+
detected=detection,
|
|
690
|
+
)
|
|
691
|
+
extra_with_env = dict(extra or {})
|
|
692
|
+
if env_map is not None:
|
|
693
|
+
extra_with_env.setdefault("env", env_map)
|
|
694
|
+
result = resolve_embedder(
|
|
695
|
+
resolved_prov,
|
|
696
|
+
model=resolved_model,
|
|
697
|
+
base_url=base_url,
|
|
698
|
+
api_key=api_key,
|
|
699
|
+
dim=resolved_dim,
|
|
700
|
+
timeout=timeout,
|
|
701
|
+
extra=extra_with_env,
|
|
702
|
+
probe=probe,
|
|
703
|
+
)
|
|
704
|
+
result.requested = AUTO_PROVIDER
|
|
705
|
+
result.detected = detection
|
|
706
|
+
return result
|
|
707
|
+
|
|
385
708
|
if requested in {"hash", "local", "fallback", ""}:
|
|
386
709
|
hash_prov = HashEmbeddingProvider(dim=int(dim or DEFAULT_EMBEDDING_DIM))
|
|
387
710
|
return ResolvedEmbedder(
|