atif-sql 0.2.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.
- atif_analytics/__init__.py +9 -0
- atif_analytics/application/__init__.py +3 -0
- atif_analytics/application/analyze.py +158 -0
- atif_analytics/application/prompts.py +750 -0
- atif_analytics/application/use_cases/__init__.py +3 -0
- atif_analytics/application/use_cases/_shared.py +264 -0
- atif_analytics/application/use_cases/classify.py +430 -0
- atif_analytics/application/use_cases/conflicts.py +448 -0
- atif_analytics/application/use_cases/friction.py +634 -0
- atif_analytics/application/use_cases/perceived.py +451 -0
- atif_analytics/domain/__init__.py +3 -0
- atif_analytics/domain/authorship.py +166 -0
- atif_analytics/domain/config.py +30 -0
- atif_analytics/domain/costs.py +50 -0
- atif_analytics/domain/friction.py +117 -0
- atif_analytics/domain/layout.py +94 -0
- atif_analytics/domain/models.py +385 -0
- atif_analytics/domain/ports.py +100 -0
- atif_analytics/domain/transcript.py +359 -0
- atif_analytics/infrastructure/__init__.py +3 -0
- atif_analytics/infrastructure/corpus_reader.py +546 -0
- atif_analytics/infrastructure/parquet_cache.py +186 -0
- atif_analytics/infrastructure/settings.py +114 -0
- atif_analytics/infrastructure/sqlite_state/__init__.py +3 -0
- atif_analytics/infrastructure/sqlite_state/checkpointer.py +326 -0
- atif_analytics/infrastructure/sqlite_state/retry_queue.py +296 -0
- atif_analytics/py.typed +0 -0
- atif_cli/__init__.py +3 -0
- atif_cli/__main__.py +8 -0
- atif_cli/app.py +2459 -0
- atif_cli/converter_adapter.py +151 -0
- atif_cli/corpus.py +409 -0
- atif_cli/cron.py +311 -0
- atif_cli/duck_errors.py +97 -0
- atif_cli/embed_lake.py +101 -0
- atif_cli/errors.py +89 -0
- atif_cli/lake.py +373 -0
- atif_cli/lake_sessions.py +270 -0
- atif_cli/output.py +297 -0
- atif_cli/py.typed +0 -0
- atif_converter/__init__.py +3 -0
- atif_converter/application/__init__.py +3 -0
- atif_converter/application/convert_and_audit.py +294 -0
- atif_converter/application/convert_codex.py +210 -0
- atif_converter/domain/__init__.py +9 -0
- atif_converter/domain/agents.py +40 -0
- atif_converter/domain/atif/__init__.py +55 -0
- atif_converter/domain/atif/agent.py +37 -0
- atif_converter/domain/atif/content.py +161 -0
- atif_converter/domain/atif/final_metrics.py +45 -0
- atif_converter/domain/atif/metrics.py +49 -0
- atif_converter/domain/atif/observation.py +21 -0
- atif_converter/domain/atif/observation_result.py +47 -0
- atif_converter/domain/atif/step.py +144 -0
- atif_converter/domain/atif/subagent_trajectory_ref.py +90 -0
- atif_converter/domain/atif/tool_call.py +36 -0
- atif_converter/domain/atif/trajectory.py +211 -0
- atif_converter/domain/atif/trajectory_validator.py +299 -0
- atif_converter/domain/blobs.py +510 -0
- atif_converter/domain/claude_code_conversion.py +1077 -0
- atif_converter/domain/codex_conversion.py +1036 -0
- atif_converter/domain/codex_edges.py +192 -0
- atif_converter/domain/codex_enrichment.py +419 -0
- atif_converter/domain/codex_fidelity.py +124 -0
- atif_converter/domain/codex_nested_calls.py +165 -0
- atif_converter/domain/edges.py +113 -0
- atif_converter/domain/enrichment.py +412 -0
- atif_converter/domain/errors.py +60 -0
- atif_converter/domain/fidelity.py +171 -0
- atif_converter/domain/model_prices.json +8538 -0
- atif_converter/domain/pricing.py +790 -0
- atif_converter/domain/result_signals.py +774 -0
- atif_converter/domain/schema_version.py +90 -0
- atif_converter/domain/session_events.py +408 -0
- atif_converter/infrastructure/__init__.py +3 -0
- atif_converter/infrastructure/census.py +89 -0
- atif_converter/infrastructure/claude_code_converter.py +127 -0
- atif_converter/infrastructure/codex_adapter.py +132 -0
- atif_converter/infrastructure/codex_census.py +100 -0
- atif_converter/infrastructure/codex_converter.py +82 -0
- atif_converter/infrastructure/harbor_adapter.py +187 -0
- atif_converter/infrastructure/raw_records.py +426 -0
- atif_converter/infrastructure/source_archive.py +154 -0
- atif_converter/py.typed +0 -0
- atif_corpus/__init__.py +13 -0
- atif_corpus/application/__init__.py +7 -0
- atif_corpus/application/materialize.py +1826 -0
- atif_corpus/domain/__init__.py +9 -0
- atif_corpus/domain/agents.py +33 -0
- atif_corpus/domain/generation.py +34 -0
- atif_corpus/domain/layout.py +168 -0
- atif_corpus/domain/ports.py +175 -0
- atif_corpus/domain/session_id.py +68 -0
- atif_corpus/domain/sessions.py +228 -0
- atif_corpus/domain/slug.py +72 -0
- atif_corpus/domain/source_layout.py +143 -0
- atif_corpus/domain/watermark.py +76 -0
- atif_corpus/infrastructure/__init__.py +7 -0
- atif_corpus/infrastructure/atomic.py +266 -0
- atif_corpus/infrastructure/compress_artifacts.py +161 -0
- atif_corpus/infrastructure/fake_converter.py +174 -0
- atif_corpus/infrastructure/scanner.py +316 -0
- atif_corpus/infrastructure/settings.py +130 -0
- atif_corpus/infrastructure/source_archive.py +202 -0
- atif_corpus/py.typed +0 -0
- atif_duck/__init__.py +43 -0
- atif_duck/domain/__init__.py +32 -0
- atif_duck/domain/artifacts.py +66 -0
- atif_duck/domain/authorship.py +165 -0
- atif_duck/domain/catalog.py +626 -0
- atif_duck/domain/columnar.py +149 -0
- atif_duck/domain/embedding_guard.py +88 -0
- atif_duck/domain/examples.py +265 -0
- atif_duck/domain/lake.py +427 -0
- atif_duck/domain/lake_sessions.py +97 -0
- atif_duck/domain/raw_readers.py +140 -0
- atif_duck/domain/session_id.py +69 -0
- atif_duck/domain/sql_literal.py +50 -0
- atif_duck/infrastructure/__init__.py +34 -0
- atif_duck/infrastructure/analytics.py +469 -0
- atif_duck/infrastructure/authorship.py +139 -0
- atif_duck/infrastructure/columnar.py +641 -0
- atif_duck/infrastructure/lake.py +1383 -0
- atif_duck/infrastructure/lake_sessions.py +194 -0
- atif_duck/infrastructure/lake_settings.py +63 -0
- atif_duck/infrastructure/lake_steps.py +225 -0
- atif_duck/infrastructure/projections.py +221 -0
- atif_duck/infrastructure/registry.py +2194 -0
- atif_duck/infrastructure/stored_artifacts.py +115 -0
- atif_duck/py.typed +0 -0
- atif_embed/__init__.py +12 -0
- atif_embed/application/__init__.py +3 -0
- atif_embed/application/embed.py +293 -0
- atif_embed/application/prune.py +80 -0
- atif_embed/domain/__init__.py +3 -0
- atif_embed/domain/discovery.py +70 -0
- atif_embed/domain/embedding_guard.py +83 -0
- atif_embed/domain/errors.py +94 -0
- atif_embed/domain/ports.py +223 -0
- atif_embed/domain/sql_literal.py +49 -0
- atif_embed/domain/text_stamp.py +71 -0
- atif_embed/infrastructure/__init__.py +3 -0
- atif_embed/infrastructure/cohere_bedrock.py +433 -0
- atif_embed/infrastructure/corpus_text_rows.py +273 -0
- atif_embed/infrastructure/gemma_local.py +355 -0
- atif_embed/infrastructure/lake_text_rows.py +203 -0
- atif_embed/infrastructure/lance_store.py +440 -0
- atif_embed/infrastructure/providers.py +30 -0
- atif_embed/infrastructure/settings.py +154 -0
- atif_embed/py.typed +0 -0
- atif_models/__init__.py +14 -0
- atif_models/domain/__init__.py +9 -0
- atif_models/domain/ports.py +143 -0
- atif_models/domain/registry.py +151 -0
- atif_models/domain/schema.py +109 -0
- atif_models/infrastructure/__init__.py +8 -0
- atif_models/infrastructure/openai_bedrock.py +314 -0
- atif_models/infrastructure/settings.py +85 -0
- atif_models/py.typed +0 -0
- atif_sql-0.2.0.dist-info/METADATA +272 -0
- atif_sql-0.2.0.dist-info/RECORD +164 -0
- atif_sql-0.2.0.dist-info/WHEEL +4 -0
- atif_sql-0.2.0.dist-info/entry_points.txt +2 -0
- atif_sql-0.2.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
"""Cohere Embed v4 on Bedrock: the ``EmbeddingProvider`` adapter.
|
|
4
|
+
|
|
5
|
+
The raw ``invoke_model`` call (:func:`_invoke_raw`) under a tenacity retry
|
|
6
|
+
over :data:`RETRY_CODES`, batch orchestration under an asyncio semaphore,
|
|
7
|
+
and the document-``int8`` / query-``float`` asymmetry. One process-wide
|
|
8
|
+
``bedrock-runtime`` client per ``(region, pool_size)``, with botocore
|
|
9
|
+
retries disabled so tenacity owns the policy.
|
|
10
|
+
|
|
11
|
+
Every botocore failure that reaches the caller is wrapped in an
|
|
12
|
+
:class:`~atif_embed.domain.errors.EmbeddingProviderUnavailable` (or
|
|
13
|
+
:class:`~atif_embed.domain.errors.EmbeddingResponseInvalid` for an
|
|
14
|
+
unreadable response shape): the CLI classifies ``DomainError`` and nothing
|
|
15
|
+
else, so an expired AWS session must not escape as a raw traceback.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import asyncio
|
|
21
|
+
import json
|
|
22
|
+
import threading
|
|
23
|
+
import time
|
|
24
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
25
|
+
|
|
26
|
+
from botocore.config import Config as BotoConfig
|
|
27
|
+
from botocore.exceptions import (
|
|
28
|
+
BotoCoreError,
|
|
29
|
+
ClientError,
|
|
30
|
+
ConnectionError as BotoConnectionError,
|
|
31
|
+
EndpointConnectionError,
|
|
32
|
+
ReadTimeoutError,
|
|
33
|
+
SSLError,
|
|
34
|
+
)
|
|
35
|
+
from loguru import logger
|
|
36
|
+
from tenacity import (
|
|
37
|
+
retry,
|
|
38
|
+
retry_if_exception,
|
|
39
|
+
stop_after_attempt,
|
|
40
|
+
wait_exponential,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
from atif_embed.domain.errors import (
|
|
44
|
+
EmbeddingProviderUnavailable,
|
|
45
|
+
EmbeddingResponseInvalid,
|
|
46
|
+
)
|
|
47
|
+
from atif_embed.domain.text_stamp import MAX_EMBEDDABLE_CHARS, clip_text
|
|
48
|
+
|
|
49
|
+
if TYPE_CHECKING:
|
|
50
|
+
from collections.abc import Callable
|
|
51
|
+
|
|
52
|
+
from tenacity import RetryCallState
|
|
53
|
+
|
|
54
|
+
from atif_embed.infrastructure.settings import EmbedSettings
|
|
55
|
+
|
|
56
|
+
#: Bedrock error codes that tenacity should retry. TWIN of
|
|
57
|
+
#: ``atif_models.infrastructure.openai_bedrock.RETRY_CODES`` — the two
|
|
58
|
+
#: packages may not import each other (independence contract), so each pins
|
|
59
|
+
#: this set against the same literal in its own test suite. Dropping a
|
|
60
|
+
#: throttle code here turns a mid-backfill throttle into an aborted run.
|
|
61
|
+
RETRY_CODES: frozenset[str] = frozenset(
|
|
62
|
+
{
|
|
63
|
+
"ThrottlingException",
|
|
64
|
+
"ServiceUnavailableException",
|
|
65
|
+
"ModelTimeoutException",
|
|
66
|
+
"ModelErrorException",
|
|
67
|
+
"ProvisionedThroughputExceededException",
|
|
68
|
+
"TooManyRequestsException",
|
|
69
|
+
"InternalServerException",
|
|
70
|
+
"InternalFailure",
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _loguru_before_sleep(level: str = "WARNING") -> Callable[[RetryCallState], None]:
|
|
76
|
+
"""Return a tenacity ``before_sleep`` callback that logs via loguru.
|
|
77
|
+
|
|
78
|
+
Mirrors :func:`tenacity.before_sleep_log`'s message shape while keeping
|
|
79
|
+
stdlib ``logging`` out of the loguru-only workspace.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def _before_sleep(retry_state: RetryCallState) -> None:
|
|
83
|
+
if retry_state.outcome is None or retry_state.next_action is None:
|
|
84
|
+
return
|
|
85
|
+
if retry_state.fn is None:
|
|
86
|
+
fn_name = "<unknown>"
|
|
87
|
+
else:
|
|
88
|
+
fn_name = getattr(retry_state.fn, "__qualname__", repr(retry_state.fn))
|
|
89
|
+
if retry_state.outcome.failed:
|
|
90
|
+
exc = retry_state.outcome.exception()
|
|
91
|
+
verb, value = "raised", f"{exc.__class__.__name__}: {exc}"
|
|
92
|
+
else:
|
|
93
|
+
verb, value = "returned", retry_state.outcome.result()
|
|
94
|
+
logger.log(
|
|
95
|
+
level,
|
|
96
|
+
"Retrying {} in {:.3g} seconds as it {} {}.",
|
|
97
|
+
fn_name,
|
|
98
|
+
retry_state.next_action.sleep,
|
|
99
|
+
verb,
|
|
100
|
+
value,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return _before_sleep
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _is_retryable(exc: BaseException) -> bool:
|
|
107
|
+
"""Return True if ``exc`` is a Bedrock error worth retrying.
|
|
108
|
+
|
|
109
|
+
Two buckets:
|
|
110
|
+
* ``ClientError`` with a code in :data:`RETRY_CODES`: service-level
|
|
111
|
+
throttling and transient model failures.
|
|
112
|
+
* Network-layer errors (SSL, connection, endpoint, read-timeout) that
|
|
113
|
+
surface when long-running batches hit flaky TCP connections.
|
|
114
|
+
"""
|
|
115
|
+
if isinstance(exc, SSLError | BotoConnectionError | EndpointConnectionError | ReadTimeoutError):
|
|
116
|
+
return True
|
|
117
|
+
if not isinstance(exc, ClientError):
|
|
118
|
+
return False
|
|
119
|
+
code = exc.response.get("Error", {}).get("Code")
|
|
120
|
+
return code in RETRY_CODES
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
_CLIENT_LOCK = threading.Lock()
|
|
124
|
+
_CLIENT_CACHE: dict[tuple[str, int], Any] = {}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _build_bedrock_client(settings: EmbedSettings) -> Any:
|
|
128
|
+
"""Return a process-wide ``bedrock-runtime`` client keyed on region + pool size.
|
|
129
|
+
|
|
130
|
+
boto3 clients are thread-safe and intended to be shared; creating one per
|
|
131
|
+
request wastes the TCP pool. ``max_pool_connections`` sizes to at least
|
|
132
|
+
``2 × embed_concurrency`` with a floor of 32 so concurrent batches never
|
|
133
|
+
starve; ``retries.max_attempts=0`` disables botocore's retry loop so the
|
|
134
|
+
tenacity decorator below sees errors immediately.
|
|
135
|
+
"""
|
|
136
|
+
import boto3
|
|
137
|
+
|
|
138
|
+
pool_size = max(32, settings.embed_concurrency * 2)
|
|
139
|
+
key = (settings.region, pool_size)
|
|
140
|
+
with _CLIENT_LOCK:
|
|
141
|
+
client = _CLIENT_CACHE.get(key)
|
|
142
|
+
if client is None:
|
|
143
|
+
boto_cfg = BotoConfig(
|
|
144
|
+
region_name=settings.region,
|
|
145
|
+
retries={"max_attempts": 0, "mode": "adaptive"},
|
|
146
|
+
max_pool_connections=pool_size,
|
|
147
|
+
connect_timeout=10,
|
|
148
|
+
read_timeout=600,
|
|
149
|
+
)
|
|
150
|
+
client = boto3.client("bedrock-runtime", config=boto_cfg)
|
|
151
|
+
_CLIENT_CACHE[key] = client
|
|
152
|
+
return client
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@retry(
|
|
156
|
+
# Cohere Embed v4 on Bedrock has a strict TPM bucket that replenishes over
|
|
157
|
+
# tens of seconds; wait up to 60s between attempts and try up to 10 times
|
|
158
|
+
# before surfacing the ThrottlingException.
|
|
159
|
+
stop=stop_after_attempt(10),
|
|
160
|
+
wait=wait_exponential(multiplier=2, min=2, max=60),
|
|
161
|
+
retry=retry_if_exception(_is_retryable),
|
|
162
|
+
before_sleep=_loguru_before_sleep("WARNING"),
|
|
163
|
+
reraise=True,
|
|
164
|
+
)
|
|
165
|
+
def _invoke_raw(client: Any, model_id: str, body: str) -> dict[str, Any]:
|
|
166
|
+
"""One retried ``invoke_model`` call, raising botocore errors unchanged.
|
|
167
|
+
|
|
168
|
+
Wrapping into the domain taxonomy happens in the CALLER, not here:
|
|
169
|
+
tenacity's ``retry_if_exception(_is_retryable)`` inspects the botocore
|
|
170
|
+
exception type and error code, so a wrap at this level would make every
|
|
171
|
+
throttle look non-retryable and abort the run on the first one.
|
|
172
|
+
"""
|
|
173
|
+
resp = client.invoke_model(
|
|
174
|
+
modelId=model_id,
|
|
175
|
+
body=body,
|
|
176
|
+
contentType="application/json",
|
|
177
|
+
accept="application/json",
|
|
178
|
+
)
|
|
179
|
+
return cast("dict[str, Any]", json.loads(resp["body"].read()))
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _invoke_bedrock_sync(
|
|
183
|
+
client: Any,
|
|
184
|
+
model_id: str,
|
|
185
|
+
texts: list[str],
|
|
186
|
+
*,
|
|
187
|
+
input_type: str,
|
|
188
|
+
output_dimension: int,
|
|
189
|
+
embedding_type: str,
|
|
190
|
+
) -> list[list[int]] | list[list[float]]:
|
|
191
|
+
"""Make one synchronous ``invoke_model`` call and return the vectors.
|
|
192
|
+
|
|
193
|
+
Parameters
|
|
194
|
+
----------
|
|
195
|
+
client
|
|
196
|
+
A boto3 ``bedrock-runtime`` client.
|
|
197
|
+
model_id
|
|
198
|
+
Cohere Embed v4 model ID (direct or CRIS profile).
|
|
199
|
+
texts
|
|
200
|
+
Up to 96 strings, each already clipped by :func:`clip_text`.
|
|
201
|
+
input_type
|
|
202
|
+
Either ``"search_document"`` (corpus) or ``"search_query"``.
|
|
203
|
+
output_dimension
|
|
204
|
+
Target Matryoshka dimension: 256, 512, 1024, or 1536.
|
|
205
|
+
embedding_type
|
|
206
|
+
One of ``"int8"``, ``"float"``, ``"uint8"``, ``"binary"``, ``"ubinary"``.
|
|
207
|
+
|
|
208
|
+
Returns
|
|
209
|
+
-------
|
|
210
|
+
list of list of int or float
|
|
211
|
+
Flat list of vectors matching the order of ``texts``.
|
|
212
|
+
|
|
213
|
+
Raises
|
|
214
|
+
------
|
|
215
|
+
EmbeddingProviderUnavailable
|
|
216
|
+
Any botocore failure — retryable codes only after the tenacity budget
|
|
217
|
+
is spent, non-retryable ones (denied or expired credentials, rejected
|
|
218
|
+
request) immediately.
|
|
219
|
+
EmbeddingResponseInvalid
|
|
220
|
+
The call succeeded but the payload carries no vectors under
|
|
221
|
+
``embeddings[embedding_type]``.
|
|
222
|
+
"""
|
|
223
|
+
body = json.dumps(
|
|
224
|
+
{
|
|
225
|
+
"texts": texts,
|
|
226
|
+
"input_type": input_type,
|
|
227
|
+
"output_dimension": output_dimension,
|
|
228
|
+
"embedding_types": [embedding_type],
|
|
229
|
+
"truncate": "RIGHT",
|
|
230
|
+
}
|
|
231
|
+
)
|
|
232
|
+
try:
|
|
233
|
+
payload = _invoke_raw(client, model_id, body)
|
|
234
|
+
except (ClientError, BotoCoreError) as exc:
|
|
235
|
+
msg = f"Bedrock invoke_model failed for {model_id}: {type(exc).__name__}: {exc}"
|
|
236
|
+
raise EmbeddingProviderUnavailable(msg) from exc
|
|
237
|
+
except json.JSONDecodeError as exc:
|
|
238
|
+
msg = f"Bedrock response for {model_id} is not valid JSON: {exc}"
|
|
239
|
+
raise EmbeddingResponseInvalid(msg) from exc
|
|
240
|
+
try:
|
|
241
|
+
vectors = payload["embeddings"][embedding_type]
|
|
242
|
+
except (KeyError, TypeError) as exc:
|
|
243
|
+
# `_invoke_raw` CASTS the decoded body to `dict[str, Any]`; Bedrock is not
|
|
244
|
+
# obliged to honour that, and this handler runs precisely when it did not
|
|
245
|
+
# (the `TypeError` arm above). So the check is redundant to the declared
|
|
246
|
+
# type and required at run time; both checkers are told so (ty's
|
|
247
|
+
# `redundant-condition-strict` arrived in 0.0.80 and fires here too).
|
|
248
|
+
shape = (
|
|
249
|
+
sorted(payload)
|
|
250
|
+
if isinstance(payload, dict) # pyright: ignore[reportUnnecessaryIsInstance] # ty: ignore[redundant-condition-strict]
|
|
251
|
+
else type(payload).__name__
|
|
252
|
+
)
|
|
253
|
+
msg = (
|
|
254
|
+
f"Bedrock response for {model_id} has no embeddings[{embedding_type!r}] "
|
|
255
|
+
f"(payload shape: {shape})"
|
|
256
|
+
)
|
|
257
|
+
raise EmbeddingResponseInvalid(msg) from exc
|
|
258
|
+
# Untyped-JSON boundary: name the shape once here rather than returning Any.
|
|
259
|
+
return cast("list[list[int]] | list[list[float]]", vectors)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
async def _embed_one_batch(
|
|
263
|
+
client: Any,
|
|
264
|
+
texts: list[str],
|
|
265
|
+
model_id: str,
|
|
266
|
+
*,
|
|
267
|
+
input_type: str,
|
|
268
|
+
output_dimension: int,
|
|
269
|
+
embedding_type: str,
|
|
270
|
+
sem: asyncio.Semaphore,
|
|
271
|
+
) -> list[list[int]] | list[list[float]]:
|
|
272
|
+
"""Embed a single batch under a concurrency-limiting semaphore."""
|
|
273
|
+
async with sem:
|
|
274
|
+
return await asyncio.to_thread(
|
|
275
|
+
_invoke_bedrock_sync,
|
|
276
|
+
client,
|
|
277
|
+
model_id,
|
|
278
|
+
texts,
|
|
279
|
+
input_type=input_type,
|
|
280
|
+
output_dimension=output_dimension,
|
|
281
|
+
embedding_type=embedding_type,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class CohereBedrockEmbedder:
|
|
286
|
+
"""``EmbeddingProvider`` over Cohere Embed v4 on Amazon Bedrock.
|
|
287
|
+
|
|
288
|
+
Documents embed at ``settings.embedding_type`` (``int8`` by default) and
|
|
289
|
+
are float-widened on the way out; queries force ``embedding_type="float"``
|
|
290
|
+
because the HNSW distance math needs float vectors. The Matryoshka
|
|
291
|
+
``output_dimension`` knob (256/512/1024/1536) is Cohere-specific and is
|
|
292
|
+
the provider's fixed :attr:`dimension`.
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
provider = "cohere-bedrock"
|
|
296
|
+
|
|
297
|
+
def __init__(self, settings: EmbedSettings) -> None:
|
|
298
|
+
self._settings = settings
|
|
299
|
+
|
|
300
|
+
@property
|
|
301
|
+
def model_id(self) -> str:
|
|
302
|
+
"""The Bedrock model id this embedder invokes."""
|
|
303
|
+
return self._settings.embed_model_id
|
|
304
|
+
|
|
305
|
+
@property
|
|
306
|
+
def dimension(self) -> int:
|
|
307
|
+
"""The output vector width this embedder requests from Cohere."""
|
|
308
|
+
return self._settings.embedding_dim
|
|
309
|
+
|
|
310
|
+
async def embed_documents(self, texts: list[str]) -> list[list[float] | None]:
|
|
311
|
+
"""Embed corpus documents in parallel; one slot per input text, in order.
|
|
312
|
+
|
|
313
|
+
A slot is ``None`` when that text's batch failed terminally. Batches
|
|
314
|
+
run under ``return_exceptions`` on purpose: one batch exhausting its
|
|
315
|
+
retry budget must not discard its siblings' vectors, which were
|
|
316
|
+
already billed. The caller writes the rows it got and lets the next
|
|
317
|
+
run's staleness anti-join re-pick the ``None`` slots.
|
|
318
|
+
|
|
319
|
+
Texts over :data:`MAX_EMBEDDABLE_CHARS` are clipped (logged, and flagged
|
|
320
|
+
on the stored row) before the call.
|
|
321
|
+
"""
|
|
322
|
+
if not texts:
|
|
323
|
+
return []
|
|
324
|
+
|
|
325
|
+
settings = self._settings
|
|
326
|
+
client = _build_bedrock_client(settings)
|
|
327
|
+
batch_size = settings.batch_size
|
|
328
|
+
|
|
329
|
+
clipped: list[str] = []
|
|
330
|
+
for position, text in enumerate(texts):
|
|
331
|
+
sent, was_truncated = clip_text(text)
|
|
332
|
+
if was_truncated:
|
|
333
|
+
logger.warning(
|
|
334
|
+
"Clipping text at position {} for embedding: {} chars -> {} "
|
|
335
|
+
"(content past the cap will not match a search)",
|
|
336
|
+
position,
|
|
337
|
+
len(text),
|
|
338
|
+
MAX_EMBEDDABLE_CHARS,
|
|
339
|
+
)
|
|
340
|
+
clipped.append(sent)
|
|
341
|
+
|
|
342
|
+
starts = list(range(0, len(clipped), batch_size))
|
|
343
|
+
batches = [clipped[i : i + batch_size] for i in starts]
|
|
344
|
+
sem = asyncio.Semaphore(settings.embed_concurrency)
|
|
345
|
+
|
|
346
|
+
logger.info(
|
|
347
|
+
"Embedding {} texts in {} batches (batch_size={}, concurrency={}, model={})",
|
|
348
|
+
len(texts),
|
|
349
|
+
len(batches),
|
|
350
|
+
batch_size,
|
|
351
|
+
settings.embed_concurrency,
|
|
352
|
+
self.model_id,
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
t0 = time.monotonic()
|
|
356
|
+
outcomes = await asyncio.gather(
|
|
357
|
+
*(
|
|
358
|
+
_embed_one_batch(
|
|
359
|
+
client,
|
|
360
|
+
batch,
|
|
361
|
+
self.model_id,
|
|
362
|
+
input_type="search_document",
|
|
363
|
+
output_dimension=self.dimension,
|
|
364
|
+
embedding_type=settings.embedding_type,
|
|
365
|
+
sem=sem,
|
|
366
|
+
)
|
|
367
|
+
for batch in batches
|
|
368
|
+
),
|
|
369
|
+
return_exceptions=True,
|
|
370
|
+
)
|
|
371
|
+
elapsed = time.monotonic() - t0
|
|
372
|
+
|
|
373
|
+
vectors: list[list[float] | None] = [None] * len(texts)
|
|
374
|
+
failed_batches = 0
|
|
375
|
+
for start, batch, outcome in zip(starts, batches, outcomes, strict=True):
|
|
376
|
+
if isinstance(outcome, BaseException):
|
|
377
|
+
failed_batches += 1
|
|
378
|
+
logger.error(
|
|
379
|
+
"Batch at offset {} ({} texts) failed terminally ({}: {}); "
|
|
380
|
+
"keeping the batches that succeeded — the next run re-picks these rows",
|
|
381
|
+
start,
|
|
382
|
+
len(batch),
|
|
383
|
+
type(outcome).__name__,
|
|
384
|
+
outcome,
|
|
385
|
+
)
|
|
386
|
+
continue
|
|
387
|
+
for offset, vector in enumerate(outcome):
|
|
388
|
+
vectors[start + offset] = [float(x) for x in vector]
|
|
389
|
+
|
|
390
|
+
embedded = sum(1 for v in vectors if v is not None)
|
|
391
|
+
logger.info(
|
|
392
|
+
"Embedded {} vectors across {}/{} batches in {:.2f}s ({:.1f} vec/s)",
|
|
393
|
+
embedded,
|
|
394
|
+
len(batches) - failed_batches,
|
|
395
|
+
len(batches),
|
|
396
|
+
elapsed,
|
|
397
|
+
embedded / elapsed if elapsed > 0 else 0.0,
|
|
398
|
+
)
|
|
399
|
+
return vectors
|
|
400
|
+
|
|
401
|
+
def embed_query(self, text: str) -> list[float]:
|
|
402
|
+
"""Embed a single query string for HNSW nearest-neighbor search.
|
|
403
|
+
|
|
404
|
+
Uses ``input_type="search_query"`` and forces ``embedding_type="float"``
|
|
405
|
+
regardless of ``settings.embedding_type`` because HNSW distance math
|
|
406
|
+
needs float vectors. Returns a single vector of length :attr:`dimension`.
|
|
407
|
+
"""
|
|
408
|
+
client = _build_bedrock_client(self._settings)
|
|
409
|
+
sent, was_truncated = clip_text(text)
|
|
410
|
+
if was_truncated:
|
|
411
|
+
logger.warning(
|
|
412
|
+
"Clipping query for embedding: {} chars -> {}", len(text), MAX_EMBEDDABLE_CHARS
|
|
413
|
+
)
|
|
414
|
+
vectors = _invoke_bedrock_sync(
|
|
415
|
+
client,
|
|
416
|
+
self.model_id,
|
|
417
|
+
[sent],
|
|
418
|
+
input_type="search_query",
|
|
419
|
+
output_dimension=self.dimension,
|
|
420
|
+
embedding_type="float",
|
|
421
|
+
)
|
|
422
|
+
return [float(x) for x in vectors[0]]
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
__all__ = [
|
|
426
|
+
"RETRY_CODES",
|
|
427
|
+
"CohereBedrockEmbedder",
|
|
428
|
+
"_build_bedrock_client",
|
|
429
|
+
"_embed_one_batch",
|
|
430
|
+
"_invoke_bedrock_sync",
|
|
431
|
+
"_invoke_raw",
|
|
432
|
+
"_is_retryable",
|
|
433
|
+
]
|