lorebase-inference 0.2.3__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.
- lorebase_inference/__init__.py +70 -0
- lorebase_inference/__main__.py +6 -0
- lorebase_inference/chat.py +447 -0
- lorebase_inference/cli.py +214 -0
- lorebase_inference/errors.py +54 -0
- lorebase_inference/fake.py +317 -0
- lorebase_inference/interfaces.py +151 -0
- lorebase_inference/local_embeddings.py +58 -0
- lorebase_inference/local_relations.py +157 -0
- lorebase_inference/local_spans.py +93 -0
- lorebase_inference/registry.py +313 -0
- lorebase_inference/remote_embeddings.py +100 -0
- lorebase_inference/settings.py +338 -0
- lorebase_inference/transport.py +233 -0
- lorebase_inference-0.2.3.dist-info/METADATA +13 -0
- lorebase_inference-0.2.3.dist-info/RECORD +18 -0
- lorebase_inference-0.2.3.dist-info/WHEEL +4 -0
- lorebase_inference-0.2.3.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""Provider selection from configuration: the only place that names providers.
|
|
2
|
+
|
|
3
|
+
Chat providers: `openai_compatible`, `fake`. Embedding providers: `fastembed`,
|
|
4
|
+
`openai_compatible`, `fake`. Span extractor providers: `gliner`, `fake`. Relation extractor
|
|
5
|
+
providers: `glirel`, `cooccurrence`, `fake`.
|
|
6
|
+
|
|
7
|
+
The local model libraries are optional extras (`fastembed`, `local-extractors`). They are
|
|
8
|
+
imported lazily, only when a provider that needs them is created, and the small backend
|
|
9
|
+
functions below are the only code that calls them; everything else in the package works with
|
|
10
|
+
plain callables. Missing extras fail with an install hint, at `validate_providers(...,
|
|
11
|
+
require_installed=True)` or at creation.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import importlib
|
|
17
|
+
import importlib.util
|
|
18
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from lorebase_inference.chat import EndpointChatModel
|
|
23
|
+
from lorebase_inference.errors import ProviderUnavailableError
|
|
24
|
+
from lorebase_inference.fake import FakeChatModel, FakeEmbeddingModel, FakeSpanExtractor
|
|
25
|
+
from lorebase_inference.interfaces import (
|
|
26
|
+
ChatModel,
|
|
27
|
+
EmbeddingModel,
|
|
28
|
+
RelationExtractor,
|
|
29
|
+
SpanExtractor,
|
|
30
|
+
)
|
|
31
|
+
from lorebase_inference.local_embeddings import EmbedBackend, LocalEmbeddingModel
|
|
32
|
+
from lorebase_inference.local_relations import (
|
|
33
|
+
CooccurrenceRelationExtractor,
|
|
34
|
+
LocalRelationExtractor,
|
|
35
|
+
RelationBackend,
|
|
36
|
+
)
|
|
37
|
+
from lorebase_inference.local_spans import LocalSpanExtractor, SpanBackend
|
|
38
|
+
from lorebase_inference.remote_embeddings import EndpointEmbeddingModel
|
|
39
|
+
from lorebase_inference.settings import (
|
|
40
|
+
EmbeddingSettings,
|
|
41
|
+
ExtractorSettings,
|
|
42
|
+
InferenceSettings,
|
|
43
|
+
LlmSettings,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
ChatFactory = Callable[[InferenceSettings, LlmSettings], ChatModel]
|
|
47
|
+
EmbeddingFactory = Callable[[InferenceSettings], EmbeddingModel]
|
|
48
|
+
SpanFactory = Callable[[InferenceSettings], SpanExtractor]
|
|
49
|
+
RelationFactory = Callable[[InferenceSettings], RelationExtractor]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class UnknownProviderError(ValueError):
|
|
53
|
+
"""The configuration names a provider that is not registered."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True, slots=True)
|
|
57
|
+
class Requirement:
|
|
58
|
+
"""Optional Python modules a provider imports, and the extra that installs them."""
|
|
59
|
+
|
|
60
|
+
modules: tuple[str, ...]
|
|
61
|
+
extra: str
|
|
62
|
+
|
|
63
|
+
def missing(self) -> list[str]:
|
|
64
|
+
"""Modules that cannot be imported."""
|
|
65
|
+
return [m for m in self.modules if importlib.util.find_spec(m) is None]
|
|
66
|
+
|
|
67
|
+
def ensure(self, provider: str) -> None:
|
|
68
|
+
"""Raise `ProviderUnavailableError` with an install hint when a module is missing."""
|
|
69
|
+
missing = self.missing()
|
|
70
|
+
if missing:
|
|
71
|
+
raise ProviderUnavailableError(
|
|
72
|
+
f"provider {provider!r} needs {', '.join(missing)}, which is not installed; "
|
|
73
|
+
f"install it with: pip install 'lorebase-inference[{self.extra}]'"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True, slots=True)
|
|
78
|
+
class _Entry[F]:
|
|
79
|
+
factory: F
|
|
80
|
+
requires: Requirement | None = None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class ProviderRegistry:
|
|
84
|
+
"""Maps provider names to factories for each model kind."""
|
|
85
|
+
|
|
86
|
+
def __init__(self) -> None:
|
|
87
|
+
self._chat: dict[str, _Entry[ChatFactory]] = {}
|
|
88
|
+
self._embedding: dict[str, _Entry[EmbeddingFactory]] = {}
|
|
89
|
+
self._span: dict[str, _Entry[SpanFactory]] = {}
|
|
90
|
+
self._relation: dict[str, _Entry[RelationFactory]] = {}
|
|
91
|
+
|
|
92
|
+
def register_chat(
|
|
93
|
+
self, name: str, factory: ChatFactory, requires: Requirement | None = None
|
|
94
|
+
) -> None:
|
|
95
|
+
self._chat[name] = _Entry(factory, requires)
|
|
96
|
+
|
|
97
|
+
def register_embedding(
|
|
98
|
+
self, name: str, factory: EmbeddingFactory, requires: Requirement | None = None
|
|
99
|
+
) -> None:
|
|
100
|
+
self._embedding[name] = _Entry(factory, requires)
|
|
101
|
+
|
|
102
|
+
def register_span_extractor(
|
|
103
|
+
self, name: str, factory: SpanFactory, requires: Requirement | None = None
|
|
104
|
+
) -> None:
|
|
105
|
+
self._span[name] = _Entry(factory, requires)
|
|
106
|
+
|
|
107
|
+
def register_relation_extractor(
|
|
108
|
+
self, name: str, factory: RelationFactory, requires: Requirement | None = None
|
|
109
|
+
) -> None:
|
|
110
|
+
self._relation[name] = _Entry(factory, requires)
|
|
111
|
+
|
|
112
|
+
def validate(self, settings: InferenceSettings, *, require_installed: bool = False) -> None:
|
|
113
|
+
"""Fail if any configured provider, including per-task ones, is not registered.
|
|
114
|
+
|
|
115
|
+
With `require_installed`, also fail when a selected provider's optional dependency is
|
|
116
|
+
missing (use at service start); without it the check is on names only.
|
|
117
|
+
"""
|
|
118
|
+
llm = settings.llm
|
|
119
|
+
selected: list[tuple[str, _Entry[Any]]] = []
|
|
120
|
+
for task in (None, *llm.tasks):
|
|
121
|
+
chat = llm.for_task(task).provider
|
|
122
|
+
selected.append((chat, _lookup(self._chat, "chat", chat)))
|
|
123
|
+
embedding = settings.embeddings.provider
|
|
124
|
+
selected.append((embedding, _lookup(self._embedding, "embedding", embedding)))
|
|
125
|
+
extractors = settings.local_extractors
|
|
126
|
+
span = extractors.provider
|
|
127
|
+
selected.append((span, _lookup(self._span, "span extractor", span)))
|
|
128
|
+
relation = extractors.relation_provider
|
|
129
|
+
selected.append((relation, _lookup(self._relation, "relation extractor", relation)))
|
|
130
|
+
if require_installed:
|
|
131
|
+
problems: dict[str, None] = {}
|
|
132
|
+
for provider, entry in selected:
|
|
133
|
+
try:
|
|
134
|
+
_ensure(entry, provider)
|
|
135
|
+
except ProviderUnavailableError as exc:
|
|
136
|
+
problems[str(exc)] = None
|
|
137
|
+
if problems:
|
|
138
|
+
raise ProviderUnavailableError("; ".join(problems))
|
|
139
|
+
|
|
140
|
+
def create_chat_model(self, settings: InferenceSettings, task: str | None = None) -> ChatModel:
|
|
141
|
+
"""Chat model for `task` (that task's overrides applied), or the default one.
|
|
142
|
+
|
|
143
|
+
The default model routes `ChatOptions.task` to the matching override by itself.
|
|
144
|
+
"""
|
|
145
|
+
llm = settings.llm.for_task(task) if task else settings.llm
|
|
146
|
+
entry = _lookup(self._chat, "chat", llm.provider)
|
|
147
|
+
_ensure(entry, llm.provider)
|
|
148
|
+
return entry.factory(settings, llm)
|
|
149
|
+
|
|
150
|
+
def create_embedding_model(self, settings: InferenceSettings) -> EmbeddingModel:
|
|
151
|
+
"""Embedding model of the configured provider."""
|
|
152
|
+
name = settings.embeddings.provider
|
|
153
|
+
entry = _lookup(self._embedding, "embedding", name)
|
|
154
|
+
_ensure(entry, name)
|
|
155
|
+
return entry.factory(settings)
|
|
156
|
+
|
|
157
|
+
def create_span_extractor(self, settings: InferenceSettings) -> SpanExtractor:
|
|
158
|
+
"""Tier-1 span extractor of the configured provider."""
|
|
159
|
+
name = settings.local_extractors.provider
|
|
160
|
+
entry = _lookup(self._span, "span extractor", name)
|
|
161
|
+
_ensure(entry, name)
|
|
162
|
+
return entry.factory(settings)
|
|
163
|
+
|
|
164
|
+
def create_relation_extractor(self, settings: InferenceSettings) -> RelationExtractor:
|
|
165
|
+
"""Tier-1 relation extractor of the configured provider."""
|
|
166
|
+
name = settings.local_extractors.relation_provider
|
|
167
|
+
entry = _lookup(self._relation, "relation extractor", name)
|
|
168
|
+
_ensure(entry, name)
|
|
169
|
+
return entry.factory(settings)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _lookup[F](table: dict[str, _Entry[F]], kind: str, name: str) -> _Entry[F]:
|
|
173
|
+
try:
|
|
174
|
+
return table[name]
|
|
175
|
+
except KeyError:
|
|
176
|
+
known = ", ".join(sorted(table)) or "none"
|
|
177
|
+
raise UnknownProviderError(
|
|
178
|
+
f"unknown {kind} provider {name!r}; available: {known}"
|
|
179
|
+
) from None
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _ensure(entry: _Entry[Any], name: str) -> None:
|
|
183
|
+
if entry.requires:
|
|
184
|
+
entry.requires.ensure(name)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# ---- local model backends: the only code that calls the optional libraries -----------------
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _import(module: str) -> Any:
|
|
191
|
+
return importlib.import_module(module)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _fastembed_backend(embeddings: EmbeddingSettings) -> Callable[[], EmbedBackend]:
|
|
195
|
+
"""Factory of a CPU ONNX embedding backend; the model is loaded when first needed."""
|
|
196
|
+
|
|
197
|
+
def build() -> EmbedBackend:
|
|
198
|
+
model = _import("fastembed").TextEmbedding(
|
|
199
|
+
model_name=embeddings.model,
|
|
200
|
+
cache_dir=str(embeddings.cache_dir) if embeddings.cache_dir else None,
|
|
201
|
+
threads=embeddings.threads,
|
|
202
|
+
providers=["CPUExecutionProvider"],
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
def embed(texts: Sequence[str]) -> list[list[float]]:
|
|
206
|
+
return [vector.tolist() for vector in model.embed(list(texts), batch_size=len(texts))]
|
|
207
|
+
|
|
208
|
+
return embed
|
|
209
|
+
|
|
210
|
+
return build
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _gliner_backend(extractor: ExtractorSettings) -> Callable[[], SpanBackend]:
|
|
214
|
+
"""Factory of a CPU zero-shot entity backend; the checkpoint is loaded when first needed."""
|
|
215
|
+
|
|
216
|
+
def build() -> SpanBackend:
|
|
217
|
+
model = _import("gliner").GLiNER.from_pretrained(
|
|
218
|
+
extractor.model,
|
|
219
|
+
cache_dir=str(extractor.cache_dir) if extractor.cache_dir else None,
|
|
220
|
+
map_location="cpu",
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
def predict(text: str, labels: list[str], threshold: float) -> list[Mapping[str, Any]]:
|
|
224
|
+
entities: list[Mapping[str, Any]] = model.predict_entities(
|
|
225
|
+
text, labels, threshold=threshold
|
|
226
|
+
)
|
|
227
|
+
return entities
|
|
228
|
+
|
|
229
|
+
return predict
|
|
230
|
+
|
|
231
|
+
return build
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _glirel_backend(extractor: ExtractorSettings) -> Callable[[], RelationBackend]:
|
|
235
|
+
"""Factory of a CPU zero-shot relation backend; loaded when first needed."""
|
|
236
|
+
|
|
237
|
+
def build() -> RelationBackend:
|
|
238
|
+
glirel = _import("glirel").GLiREL
|
|
239
|
+
cache = str(extractor.cache_dir) if extractor.cache_dir else None
|
|
240
|
+
try:
|
|
241
|
+
model = glirel.from_pretrained(extractor.model, cache_dir=cache)
|
|
242
|
+
except TypeError:
|
|
243
|
+
# glirel 1.2 with huggingface_hub 1.x: the hub mixin no longer passes two arguments
|
|
244
|
+
# that the library's loader requires. Call the loader directly with their old values.
|
|
245
|
+
model = glirel._from_pretrained(
|
|
246
|
+
model_id=extractor.model,
|
|
247
|
+
revision=None,
|
|
248
|
+
cache_dir=cache,
|
|
249
|
+
force_download=False,
|
|
250
|
+
proxies=None,
|
|
251
|
+
resume_download=False,
|
|
252
|
+
local_files_only=False,
|
|
253
|
+
token=None,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
def predict(
|
|
257
|
+
tokens: list[str], entities: list[list[Any]], labels: list[str], threshold: float
|
|
258
|
+
) -> list[Mapping[str, Any]]:
|
|
259
|
+
relations: list[Mapping[str, Any]] = model.predict_relations(
|
|
260
|
+
tokens, labels, threshold=threshold, ner=entities, top_k=-1
|
|
261
|
+
)
|
|
262
|
+
return relations
|
|
263
|
+
|
|
264
|
+
return predict
|
|
265
|
+
|
|
266
|
+
return build
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
EMBEDDINGS_EXTRA = Requirement(("fastembed",), "fastembed")
|
|
270
|
+
LOCAL_EXTRACTORS_EXTRA = Requirement(("gliner",), "local-extractors")
|
|
271
|
+
RELATION_EXTRA = Requirement(("glirel",), "local-extractors")
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def build_default_registry() -> ProviderRegistry:
|
|
275
|
+
"""Registry with every provider this package ships."""
|
|
276
|
+
registry = ProviderRegistry()
|
|
277
|
+
registry.register_chat("fake", lambda s, llm: FakeChatModel(llm, s.fake))
|
|
278
|
+
registry.register_chat("openai_compatible", lambda _s, llm: EndpointChatModel(llm))
|
|
279
|
+
registry.register_embedding("fake", lambda s: FakeEmbeddingModel(s.embeddings))
|
|
280
|
+
registry.register_embedding("openai_compatible", lambda s: EndpointEmbeddingModel(s.embeddings))
|
|
281
|
+
registry.register_embedding(
|
|
282
|
+
"fastembed",
|
|
283
|
+
lambda s: LocalEmbeddingModel(s.embeddings, _fastembed_backend(s.embeddings)),
|
|
284
|
+
EMBEDDINGS_EXTRA,
|
|
285
|
+
)
|
|
286
|
+
registry.register_span_extractor("fake", lambda s: FakeSpanExtractor(s.fake.dictionary))
|
|
287
|
+
registry.register_span_extractor(
|
|
288
|
+
"gliner",
|
|
289
|
+
lambda s: LocalSpanExtractor(
|
|
290
|
+
s.local_extractors.gliner, _gliner_backend(s.local_extractors.gliner)
|
|
291
|
+
),
|
|
292
|
+
LOCAL_EXTRACTORS_EXTRA,
|
|
293
|
+
)
|
|
294
|
+
registry.register_relation_extractor(
|
|
295
|
+
"glirel",
|
|
296
|
+
lambda s: LocalRelationExtractor(
|
|
297
|
+
s.local_extractors.glirel, _glirel_backend(s.local_extractors.glirel)
|
|
298
|
+
),
|
|
299
|
+
RELATION_EXTRA,
|
|
300
|
+
)
|
|
301
|
+
for name in ("cooccurrence", "fake"):
|
|
302
|
+
registry.register_relation_extractor(
|
|
303
|
+
name, lambda s: CooccurrenceRelationExtractor(s.local_extractors.cooccurrence)
|
|
304
|
+
)
|
|
305
|
+
return registry
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
default_registry = build_default_registry()
|
|
309
|
+
create_chat_model = default_registry.create_chat_model
|
|
310
|
+
create_embedding_model = default_registry.create_embedding_model
|
|
311
|
+
create_span_extractor = default_registry.create_span_extractor
|
|
312
|
+
create_relation_extractor = default_registry.create_relation_extractor
|
|
313
|
+
validate_providers = default_registry.validate
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Embeddings from a compatible HTTP endpoint: `POST {base_url}/embeddings`.
|
|
2
|
+
|
|
3
|
+
Works with any local server, proxy or hosted endpoint that implements the embeddings route
|
|
4
|
+
(docs/inference-providers.md lists the tested ones). Texts are sent in batches of `batch_size`;
|
|
5
|
+
the answer is checked for count and dimension so that a model swap cannot silently corrupt a
|
|
6
|
+
vector index.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import math
|
|
12
|
+
from collections.abc import Sequence
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
|
|
17
|
+
from lorebase_inference.errors import DimensionMismatchError, EndpointError
|
|
18
|
+
from lorebase_inference.interfaces import Usage
|
|
19
|
+
from lorebase_inference.settings import EmbeddingSettings
|
|
20
|
+
from lorebase_inference.transport import EndpointClient, Sleep
|
|
21
|
+
|
|
22
|
+
_CHARS_PER_TOKEN = 4
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class EndpointEmbeddingModel:
|
|
26
|
+
"""`EmbeddingModel` for an HTTP embeddings endpoint."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
embeddings: EmbeddingSettings,
|
|
31
|
+
*,
|
|
32
|
+
max_concurrency: int = 2,
|
|
33
|
+
transport: httpx.AsyncBaseTransport | None = None,
|
|
34
|
+
sleep: Sleep | None = None,
|
|
35
|
+
) -> None:
|
|
36
|
+
if not embeddings.base_url:
|
|
37
|
+
raise ValueError("embeddings.base_url is required for this provider")
|
|
38
|
+
self.name = embeddings.model
|
|
39
|
+
self.dims = embeddings.dims
|
|
40
|
+
self.usage = Usage()
|
|
41
|
+
"""Tokens sent so far, summed over calls (estimated when the server omits counts)."""
|
|
42
|
+
self._batch_size = embeddings.batch_size
|
|
43
|
+
self._client = EndpointClient(
|
|
44
|
+
base_url=embeddings.base_url,
|
|
45
|
+
api_key=embeddings.api_key,
|
|
46
|
+
timeout_s=embeddings.timeout_s,
|
|
47
|
+
retry=embeddings.retry,
|
|
48
|
+
trust_env=embeddings.trust_env,
|
|
49
|
+
max_concurrency=max_concurrency,
|
|
50
|
+
transport=transport,
|
|
51
|
+
sleep=sleep,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
async def embed(self, texts: Sequence[str]) -> list[list[float]]:
|
|
55
|
+
"""One vector of `dims` floats per text, in order."""
|
|
56
|
+
vectors: list[list[float]] = []
|
|
57
|
+
for start in range(0, len(texts), self._batch_size):
|
|
58
|
+
vectors.extend(await self._embed_batch(list(texts[start : start + self._batch_size])))
|
|
59
|
+
return vectors
|
|
60
|
+
|
|
61
|
+
async def aclose(self) -> None:
|
|
62
|
+
"""Release connections."""
|
|
63
|
+
await self._client.aclose()
|
|
64
|
+
|
|
65
|
+
async def _embed_batch(self, batch: list[str]) -> list[list[float]]:
|
|
66
|
+
body = await self._client.post("/embeddings", {"model": self.name, "input": batch})
|
|
67
|
+
data = body.get("data")
|
|
68
|
+
if not isinstance(data, list) or len(data) != len(batch):
|
|
69
|
+
raise EndpointError(
|
|
70
|
+
f"/embeddings: expected {len(batch)} vectors, got "
|
|
71
|
+
f"{len(data) if isinstance(data, list) else 'none'}"
|
|
72
|
+
)
|
|
73
|
+
ordered = sorted(
|
|
74
|
+
data, key=lambda item: item.get("index", 0) if isinstance(item, dict) else 0
|
|
75
|
+
)
|
|
76
|
+
vectors = [_vector(item) for item in ordered]
|
|
77
|
+
for vector in vectors:
|
|
78
|
+
if len(vector) != self.dims:
|
|
79
|
+
raise DimensionMismatchError(
|
|
80
|
+
f"model {self.name!r} returned {len(vector)} dimensions, "
|
|
81
|
+
f"embeddings.dims is {self.dims}"
|
|
82
|
+
)
|
|
83
|
+
raw = body.get("usage")
|
|
84
|
+
reported = raw.get("prompt_tokens") if isinstance(raw, dict) else None
|
|
85
|
+
tokens = (
|
|
86
|
+
reported
|
|
87
|
+
if isinstance(reported, int) and not isinstance(reported, bool)
|
|
88
|
+
else math.ceil(sum(len(t) for t in batch) / _CHARS_PER_TOKEN)
|
|
89
|
+
)
|
|
90
|
+
self.usage = Usage(self.usage.input_tokens + tokens, 0)
|
|
91
|
+
return vectors
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _vector(item: Any) -> list[float]:
|
|
95
|
+
embedding = item.get("embedding") if isinstance(item, dict) else None
|
|
96
|
+
if not isinstance(embedding, list) or not all(
|
|
97
|
+
isinstance(v, int | float) and not isinstance(v, bool) for v in embedding
|
|
98
|
+
):
|
|
99
|
+
raise EndpointError("/embeddings: entry without a numeric vector")
|
|
100
|
+
return [float(v) for v in embedding]
|