from __future__ import annotations import hashlib import math from typing import Any from rag.ports import QueryEmbeddingUnavailable BEDROCK_RUNTIME_SERVICE = "bedrock-runtime" COHERE_EMBED_V4 = "cohere.embed-v4:0" # Cohere embeds corpus records and queries into different subspaces. Sending # the corpus value here raises no error — recall just drops silently — so this # constant exists to make the asymmetry visible rather than incidental. COHERE_QUERY_INPUT_TYPE = "search_query" def _provider_error_types() -> tuple[type[BaseException], ...]: """botocore's error classes, or none when botocore is absent. Resolved lazily and tolerantly so a test injecting a stub client — and a deployment that never enables a cloud provider — neither imports botocore nor depends on it being installed. """ try: from botocore.exceptions import BotoCoreError, ClientError except ImportError: return () return (BotoCoreError, ClientError) class BedrockCohereQueryEmbedder: """Embeds a query with the same model the collection was built from. A collection loaded with Cohere vectors and queried with any other embedder still returns results and still raises nothing — the hits are simply meaningless. That failure is silent, which is why the corpus manifest records `model_id` and why this adapter names the model explicitly. The request shape is duplicated from `ingestion/embed/bedrock_cohere.py` rather than imported: `ingestion` is a separate deployable and importing it here would couple the API to the batch pipeline. The duplication is one JSON body and is deliberate. """ def __init__( self, dimensions: int, region: str = "us-east-1", client: Any | None = None, model_id: str = COHERE_EMBED_V4, ) -> None: if dimensions <= 0: raise ValueError("dimensions must be positive") self._dimensions = dimensions self._region = region self._client = client self._model_id = model_id @property def dimensions(self) -> int: return self._dimensions @property def model_id(self) -> str: return self._model_id def _runtime(self) -> Any: if self._client is None: import boto3 from botocore.config import Config self._client = boto3.client( BEDROCK_RUNTIME_SERVICE, region_name=self._region, config=Config( connect_timeout=10, read_timeout=30, retries={"max_attempts": 3, "mode": "standard"}, ), ) return self._client def embed_query(self, text: str) -> list[float]: import json try: response = self._runtime().invoke_model( modelId=self._model_id, body=json.dumps( { "texts": [text], "input_type": COHERE_QUERY_INPUT_TYPE, "embedding_types": ["float"], "output_dimension": self._dimensions, } ), accept="*/*", contentType="application/json", ) except _provider_error_types() as error: raise QueryEmbeddingUnavailable( f"{self._model_id} could not be invoked: {type(error).__name__}" ) from error body = json.loads(response["body"].read()) embeddings = body.get("embeddings") if isinstance(embeddings, dict): rows = embeddings.get("float") else: rows = embeddings if not rows: raise ValueError( f"{self._model_id} returned no float embedding; " f"response keys were {sorted(body)}" ) values = list(rows[0]) if len(values) != self._dimensions: raise ValueError( f"{self._model_id} returned {len(values)} dimensions, " f"expected {self._dimensions}" ) return values class SectionOnlyQueryEmbedder: """Refuses to embed, confining retrieval to the route that is verified. The section route resolves the question's attribute to a `section_key` and filters on it; no vector is involved, and it measured 16/16 on the human-written golden questions on 2026-08-04. Similarity measured 0.544 and its provider is currently revoked. Declining locally and immediately is better than the two alternatives it replaces: a `cohere-v4` round-trip spends the boto3 retry budget before failing, and `LocalHashQueryEmbedder` searches a SHA-256 vector against a Cohere collection, which returns confident and meaningless hits. """ def __init__(self, dimensions: int) -> None: if dimensions <= 0: raise ValueError("dimensions must be positive") self._dimensions = dimensions @property def dimensions(self) -> int: return self._dimensions def embed_query(self, text: str) -> list[float]: # noqa: ARG002 # The text is irrelevant: this embedder exists to refuse, not to embed. raise QueryEmbeddingUnavailable( "no query embedding provider is enabled; set EMBEDDING_PROVIDER to " "use the similarity fallback" ) class LocalHashQueryEmbedder: """Deterministic local plumbing probe; not a semantic retrieval model.""" def __init__(self, dimensions: int) -> None: if dimensions <= 0: raise ValueError("dimensions must be positive") self._dimensions = dimensions @property def dimensions(self) -> int: return self._dimensions def embed_query(self, text: str) -> list[float]: vector = [0.0] * self._dimensions for token in text.casefold().split(): digest = hashlib.sha256(token.encode("utf-8")).digest() index = int.from_bytes(digest[:4], "big") % self._dimensions sign = 1.0 if digest[4] & 1 else -1.0 vector[index] += sign norm = math.sqrt(sum(value * value for value in vector)) if norm == 0: return vector return [value / norm for value in vector]