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.
Files changed (164) hide show
  1. atif_analytics/__init__.py +9 -0
  2. atif_analytics/application/__init__.py +3 -0
  3. atif_analytics/application/analyze.py +158 -0
  4. atif_analytics/application/prompts.py +750 -0
  5. atif_analytics/application/use_cases/__init__.py +3 -0
  6. atif_analytics/application/use_cases/_shared.py +264 -0
  7. atif_analytics/application/use_cases/classify.py +430 -0
  8. atif_analytics/application/use_cases/conflicts.py +448 -0
  9. atif_analytics/application/use_cases/friction.py +634 -0
  10. atif_analytics/application/use_cases/perceived.py +451 -0
  11. atif_analytics/domain/__init__.py +3 -0
  12. atif_analytics/domain/authorship.py +166 -0
  13. atif_analytics/domain/config.py +30 -0
  14. atif_analytics/domain/costs.py +50 -0
  15. atif_analytics/domain/friction.py +117 -0
  16. atif_analytics/domain/layout.py +94 -0
  17. atif_analytics/domain/models.py +385 -0
  18. atif_analytics/domain/ports.py +100 -0
  19. atif_analytics/domain/transcript.py +359 -0
  20. atif_analytics/infrastructure/__init__.py +3 -0
  21. atif_analytics/infrastructure/corpus_reader.py +546 -0
  22. atif_analytics/infrastructure/parquet_cache.py +186 -0
  23. atif_analytics/infrastructure/settings.py +114 -0
  24. atif_analytics/infrastructure/sqlite_state/__init__.py +3 -0
  25. atif_analytics/infrastructure/sqlite_state/checkpointer.py +326 -0
  26. atif_analytics/infrastructure/sqlite_state/retry_queue.py +296 -0
  27. atif_analytics/py.typed +0 -0
  28. atif_cli/__init__.py +3 -0
  29. atif_cli/__main__.py +8 -0
  30. atif_cli/app.py +2459 -0
  31. atif_cli/converter_adapter.py +151 -0
  32. atif_cli/corpus.py +409 -0
  33. atif_cli/cron.py +311 -0
  34. atif_cli/duck_errors.py +97 -0
  35. atif_cli/embed_lake.py +101 -0
  36. atif_cli/errors.py +89 -0
  37. atif_cli/lake.py +373 -0
  38. atif_cli/lake_sessions.py +270 -0
  39. atif_cli/output.py +297 -0
  40. atif_cli/py.typed +0 -0
  41. atif_converter/__init__.py +3 -0
  42. atif_converter/application/__init__.py +3 -0
  43. atif_converter/application/convert_and_audit.py +294 -0
  44. atif_converter/application/convert_codex.py +210 -0
  45. atif_converter/domain/__init__.py +9 -0
  46. atif_converter/domain/agents.py +40 -0
  47. atif_converter/domain/atif/__init__.py +55 -0
  48. atif_converter/domain/atif/agent.py +37 -0
  49. atif_converter/domain/atif/content.py +161 -0
  50. atif_converter/domain/atif/final_metrics.py +45 -0
  51. atif_converter/domain/atif/metrics.py +49 -0
  52. atif_converter/domain/atif/observation.py +21 -0
  53. atif_converter/domain/atif/observation_result.py +47 -0
  54. atif_converter/domain/atif/step.py +144 -0
  55. atif_converter/domain/atif/subagent_trajectory_ref.py +90 -0
  56. atif_converter/domain/atif/tool_call.py +36 -0
  57. atif_converter/domain/atif/trajectory.py +211 -0
  58. atif_converter/domain/atif/trajectory_validator.py +299 -0
  59. atif_converter/domain/blobs.py +510 -0
  60. atif_converter/domain/claude_code_conversion.py +1077 -0
  61. atif_converter/domain/codex_conversion.py +1036 -0
  62. atif_converter/domain/codex_edges.py +192 -0
  63. atif_converter/domain/codex_enrichment.py +419 -0
  64. atif_converter/domain/codex_fidelity.py +124 -0
  65. atif_converter/domain/codex_nested_calls.py +165 -0
  66. atif_converter/domain/edges.py +113 -0
  67. atif_converter/domain/enrichment.py +412 -0
  68. atif_converter/domain/errors.py +60 -0
  69. atif_converter/domain/fidelity.py +171 -0
  70. atif_converter/domain/model_prices.json +8538 -0
  71. atif_converter/domain/pricing.py +790 -0
  72. atif_converter/domain/result_signals.py +774 -0
  73. atif_converter/domain/schema_version.py +90 -0
  74. atif_converter/domain/session_events.py +408 -0
  75. atif_converter/infrastructure/__init__.py +3 -0
  76. atif_converter/infrastructure/census.py +89 -0
  77. atif_converter/infrastructure/claude_code_converter.py +127 -0
  78. atif_converter/infrastructure/codex_adapter.py +132 -0
  79. atif_converter/infrastructure/codex_census.py +100 -0
  80. atif_converter/infrastructure/codex_converter.py +82 -0
  81. atif_converter/infrastructure/harbor_adapter.py +187 -0
  82. atif_converter/infrastructure/raw_records.py +426 -0
  83. atif_converter/infrastructure/source_archive.py +154 -0
  84. atif_converter/py.typed +0 -0
  85. atif_corpus/__init__.py +13 -0
  86. atif_corpus/application/__init__.py +7 -0
  87. atif_corpus/application/materialize.py +1826 -0
  88. atif_corpus/domain/__init__.py +9 -0
  89. atif_corpus/domain/agents.py +33 -0
  90. atif_corpus/domain/generation.py +34 -0
  91. atif_corpus/domain/layout.py +168 -0
  92. atif_corpus/domain/ports.py +175 -0
  93. atif_corpus/domain/session_id.py +68 -0
  94. atif_corpus/domain/sessions.py +228 -0
  95. atif_corpus/domain/slug.py +72 -0
  96. atif_corpus/domain/source_layout.py +143 -0
  97. atif_corpus/domain/watermark.py +76 -0
  98. atif_corpus/infrastructure/__init__.py +7 -0
  99. atif_corpus/infrastructure/atomic.py +266 -0
  100. atif_corpus/infrastructure/compress_artifacts.py +161 -0
  101. atif_corpus/infrastructure/fake_converter.py +174 -0
  102. atif_corpus/infrastructure/scanner.py +316 -0
  103. atif_corpus/infrastructure/settings.py +130 -0
  104. atif_corpus/infrastructure/source_archive.py +202 -0
  105. atif_corpus/py.typed +0 -0
  106. atif_duck/__init__.py +43 -0
  107. atif_duck/domain/__init__.py +32 -0
  108. atif_duck/domain/artifacts.py +66 -0
  109. atif_duck/domain/authorship.py +165 -0
  110. atif_duck/domain/catalog.py +626 -0
  111. atif_duck/domain/columnar.py +149 -0
  112. atif_duck/domain/embedding_guard.py +88 -0
  113. atif_duck/domain/examples.py +265 -0
  114. atif_duck/domain/lake.py +427 -0
  115. atif_duck/domain/lake_sessions.py +97 -0
  116. atif_duck/domain/raw_readers.py +140 -0
  117. atif_duck/domain/session_id.py +69 -0
  118. atif_duck/domain/sql_literal.py +50 -0
  119. atif_duck/infrastructure/__init__.py +34 -0
  120. atif_duck/infrastructure/analytics.py +469 -0
  121. atif_duck/infrastructure/authorship.py +139 -0
  122. atif_duck/infrastructure/columnar.py +641 -0
  123. atif_duck/infrastructure/lake.py +1383 -0
  124. atif_duck/infrastructure/lake_sessions.py +194 -0
  125. atif_duck/infrastructure/lake_settings.py +63 -0
  126. atif_duck/infrastructure/lake_steps.py +225 -0
  127. atif_duck/infrastructure/projections.py +221 -0
  128. atif_duck/infrastructure/registry.py +2194 -0
  129. atif_duck/infrastructure/stored_artifacts.py +115 -0
  130. atif_duck/py.typed +0 -0
  131. atif_embed/__init__.py +12 -0
  132. atif_embed/application/__init__.py +3 -0
  133. atif_embed/application/embed.py +293 -0
  134. atif_embed/application/prune.py +80 -0
  135. atif_embed/domain/__init__.py +3 -0
  136. atif_embed/domain/discovery.py +70 -0
  137. atif_embed/domain/embedding_guard.py +83 -0
  138. atif_embed/domain/errors.py +94 -0
  139. atif_embed/domain/ports.py +223 -0
  140. atif_embed/domain/sql_literal.py +49 -0
  141. atif_embed/domain/text_stamp.py +71 -0
  142. atif_embed/infrastructure/__init__.py +3 -0
  143. atif_embed/infrastructure/cohere_bedrock.py +433 -0
  144. atif_embed/infrastructure/corpus_text_rows.py +273 -0
  145. atif_embed/infrastructure/gemma_local.py +355 -0
  146. atif_embed/infrastructure/lake_text_rows.py +203 -0
  147. atif_embed/infrastructure/lance_store.py +440 -0
  148. atif_embed/infrastructure/providers.py +30 -0
  149. atif_embed/infrastructure/settings.py +154 -0
  150. atif_embed/py.typed +0 -0
  151. atif_models/__init__.py +14 -0
  152. atif_models/domain/__init__.py +9 -0
  153. atif_models/domain/ports.py +143 -0
  154. atif_models/domain/registry.py +151 -0
  155. atif_models/domain/schema.py +109 -0
  156. atif_models/infrastructure/__init__.py +8 -0
  157. atif_models/infrastructure/openai_bedrock.py +314 -0
  158. atif_models/infrastructure/settings.py +85 -0
  159. atif_models/py.typed +0 -0
  160. atif_sql-0.2.0.dist-info/METADATA +272 -0
  161. atif_sql-0.2.0.dist-info/RECORD +164 -0
  162. atif_sql-0.2.0.dist-info/WHEEL +4 -0
  163. atif_sql-0.2.0.dist-info/entry_points.txt +2 -0
  164. 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
+ ]