Make the first RAG slice executable without risking production data
Some checks failed
verify / verify (push) Has been cancelled
Some checks failed
verify / verify (push) Has been cancelled
The Stage 1 foundation now proves provider contracts with mocks and validates PostgreSQL/pgvector ingestion, approval binding, retrieval, reranking, and idempotency using only synthetic data. Live Bailian validation remains gated on rotating the exposed key. Constraint: The key shown in chat is compromised and cannot be used or committed Rejected: Mark Stage 1 complete from mock and offline results | real three-model smoke is still required Confidence: high Scope-risk: moderate Reversibility: clean Directive: Do not enable real-data ingestion until Stage 3 cloud approval and outbound manifest controls are enforced end to end Tested: make verify; 41 pytest tests; strict mypy; Ruff; Compose config; pinned image build; empty-volume migration; role denial; two idempotent 20-vector seeds; database restart persistence Not-tested: Live Bailian calls require a newly rotated key; React product UI is not implemented
This commit is contained in:
208
backend/app/adapters/bailian/rerank.py
Normal file
208
backend/app/adapters/bailian/rerank.py
Normal file
@@ -0,0 +1,208 @@
|
||||
"""Alibaba Cloud Model Studio compatible rerank adapter."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from app.adapters.bailian._base import (
|
||||
BailianHttpAdapter,
|
||||
conservative_utf8_token_count,
|
||||
count_tokens,
|
||||
extract_request_id,
|
||||
invalid_request,
|
||||
invalid_response,
|
||||
parse_usage,
|
||||
response_model,
|
||||
)
|
||||
from app.ports.model_providers import RankedItem, RerankResult, TokenCounter
|
||||
|
||||
RERANK_MAX_DOCUMENTS = 500
|
||||
RERANK_MAX_TOKENS_PER_TEXT = 4_000
|
||||
RERANK_MAX_TOKENS_PER_REQUEST = 120_000
|
||||
|
||||
|
||||
class BailianRerankerAdapter(BailianHttpAdapter):
|
||||
"""Call ``compatible-api/v1/reranks`` and map indices locally."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
api_key: str,
|
||||
base_url: str,
|
||||
model: str = "qwen3-rerank",
|
||||
token_counter: TokenCounter = conservative_utf8_token_count,
|
||||
http_client: httpx.AsyncClient | None = None,
|
||||
timeout_seconds: float = 30.0,
|
||||
max_retries: int = 0,
|
||||
retry_base_seconds: float = 0.5,
|
||||
) -> None:
|
||||
if not model or model != model.strip():
|
||||
raise invalid_request("rerank.configuration", "invalid_model")
|
||||
if not callable(token_counter):
|
||||
raise invalid_request("rerank.configuration", "invalid_token_counter")
|
||||
|
||||
super().__init__(
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
expected_path="/compatible-api/v1",
|
||||
http_client=http_client,
|
||||
timeout_seconds=timeout_seconds,
|
||||
max_retries=max_retries,
|
||||
retry_base_seconds=retry_base_seconds,
|
||||
)
|
||||
self._model = model
|
||||
self._token_counter = token_counter
|
||||
|
||||
async def rerank(
|
||||
self,
|
||||
query: str,
|
||||
documents: Sequence[str],
|
||||
*,
|
||||
top_n: int,
|
||||
instruct: str | None = None,
|
||||
) -> RerankResult:
|
||||
operation = "rerank.create"
|
||||
validated_documents = self._validate_request(
|
||||
query=query,
|
||||
documents=documents,
|
||||
top_n=top_n,
|
||||
instruct=instruct,
|
||||
operation=operation,
|
||||
)
|
||||
payload: dict[str, Any] = {
|
||||
"model": self._model,
|
||||
"query": query,
|
||||
"documents": list(validated_documents),
|
||||
"top_n": top_n,
|
||||
}
|
||||
if instruct is not None:
|
||||
payload["instruct"] = instruct
|
||||
|
||||
sensitive_values = (
|
||||
query,
|
||||
*validated_documents,
|
||||
*((instruct,) if instruct is not None else ()),
|
||||
)
|
||||
response = await self._post_json(
|
||||
operation=operation,
|
||||
path="reranks",
|
||||
payload=payload,
|
||||
sensitive_values=sensitive_values,
|
||||
)
|
||||
items = self._parse_results(
|
||||
response.body,
|
||||
documents=validated_documents,
|
||||
top_n=top_n,
|
||||
operation=operation,
|
||||
)
|
||||
return RerankResult(
|
||||
items=items,
|
||||
model=response_model(
|
||||
response.body,
|
||||
self._model,
|
||||
sensitive_values=(*sensitive_values, self._api_key),
|
||||
),
|
||||
request_id=extract_request_id(
|
||||
response.body,
|
||||
sensitive_values=(*sensitive_values, self._api_key),
|
||||
),
|
||||
usage=parse_usage(response.body.get("usage")),
|
||||
elapsed_ms=response.elapsed_ms,
|
||||
)
|
||||
|
||||
def _validate_request(
|
||||
self,
|
||||
*,
|
||||
query: str,
|
||||
documents: Sequence[str],
|
||||
top_n: int,
|
||||
instruct: str | None,
|
||||
operation: str,
|
||||
) -> tuple[str, ...]:
|
||||
if not isinstance(query, str) or not query:
|
||||
raise invalid_request(operation, "invalid_query")
|
||||
if isinstance(documents, (str, bytes)) or not isinstance(documents, Sequence):
|
||||
raise invalid_request(operation, "invalid_document_collection")
|
||||
validated_documents = tuple(documents)
|
||||
if not validated_documents:
|
||||
raise invalid_request(operation, "empty_documents")
|
||||
if len(validated_documents) > RERANK_MAX_DOCUMENTS:
|
||||
raise invalid_request(operation, "document_count_exceeded")
|
||||
if isinstance(top_n, bool) or not isinstance(top_n, int) or top_n <= 0:
|
||||
raise invalid_request(operation, "invalid_top_n")
|
||||
if instruct is not None and (not isinstance(instruct, str) or not instruct):
|
||||
raise invalid_request(operation, "invalid_instruct")
|
||||
|
||||
query_tokens = count_tokens(
|
||||
self._token_counter,
|
||||
query,
|
||||
operation=operation,
|
||||
)
|
||||
if query_tokens > RERANK_MAX_TOKENS_PER_TEXT:
|
||||
raise invalid_request(operation, "query_token_limit_exceeded")
|
||||
|
||||
document_tokens_total = 0
|
||||
for document in validated_documents:
|
||||
if not isinstance(document, str) or not document:
|
||||
raise invalid_request(operation, "invalid_document")
|
||||
document_tokens = count_tokens(
|
||||
self._token_counter,
|
||||
document,
|
||||
operation=operation,
|
||||
)
|
||||
if document_tokens > RERANK_MAX_TOKENS_PER_TEXT:
|
||||
raise invalid_request(operation, "document_token_limit_exceeded")
|
||||
document_tokens_total += document_tokens
|
||||
|
||||
request_tokens = query_tokens * len(validated_documents) + document_tokens_total
|
||||
if request_tokens > RERANK_MAX_TOKENS_PER_REQUEST:
|
||||
raise invalid_request(operation, "request_token_limit_exceeded")
|
||||
return validated_documents
|
||||
|
||||
def _parse_results(
|
||||
self,
|
||||
body: Mapping[str, Any],
|
||||
*,
|
||||
documents: tuple[str, ...],
|
||||
top_n: int,
|
||||
operation: str,
|
||||
) -> tuple[RankedItem, ...]:
|
||||
results = body.get("results")
|
||||
if not isinstance(results, list) or len(results) > min(top_n, len(documents)):
|
||||
raise invalid_response(operation, "invalid_rerank_count")
|
||||
|
||||
seen_indices: set[int] = set()
|
||||
parsed: list[RankedItem] = []
|
||||
previous_score = math.inf
|
||||
for item in results:
|
||||
if not isinstance(item, Mapping):
|
||||
raise invalid_response(operation, "invalid_rerank_item")
|
||||
index = item.get("index")
|
||||
if (
|
||||
isinstance(index, bool)
|
||||
or not isinstance(index, int)
|
||||
or not 0 <= index < len(documents)
|
||||
or index in seen_indices
|
||||
):
|
||||
raise invalid_response(operation, "invalid_rerank_index")
|
||||
raw_score = item.get("relevance_score")
|
||||
if isinstance(raw_score, bool) or not isinstance(raw_score, (int, float)):
|
||||
raise invalid_response(operation, "invalid_rerank_score")
|
||||
score = float(raw_score)
|
||||
if not math.isfinite(score) or not 0 <= score <= 1 or score > previous_score:
|
||||
raise invalid_response(operation, "invalid_rerank_score")
|
||||
|
||||
seen_indices.add(index)
|
||||
previous_score = score
|
||||
parsed.append(
|
||||
RankedItem(
|
||||
index=index,
|
||||
relevance_score=score,
|
||||
document=documents[index],
|
||||
)
|
||||
)
|
||||
return tuple(parsed)
|
||||
Reference in New Issue
Block a user