初始化提交

This commit is contained in:
chenxin committed 2026-08-21 16:00:13 +08:00
commit 887c1f449e
963 files changed
+166869

No files matched your search

@@ -0,0 +1,135 @@
"""重排序服务 — Reranker 适配器外观。
用法::
from app.ai_provider.reranker import RerankerService
service = RerankerService(manager)
result = await service.rerank("查询", ["文档1", "文档2"])
"""
from __future__ import annotations
from app.ai_provider.reranker.base import (
RerankerProtocol,
RerankerServiceError,
RerankRequest,
RerankResponse,
RerankResult,
)
from app.ai_provider.types.errors import AIProviderError
class RerankerService:
"""外观类,负责解析 Reranker 模型并委托给 ProviderManager。
接受 ProviderManager 或实现了以下接口的适配器:
- ``.model_registry`` → 具有 ``list_by_capability(capability)`` 方法
- ``.create_reranker(name)`` → 返回 RerankerProtocol 实例
"""
def __init__(self, manager) -> None:
self._manager = manager
self._adapter: RerankerProtocol | None = None
# ── Registry 兼容适配 ─────────────────────────────────────────
@property
def _registry(self):
mgr = self._manager
if hasattr(mgr, "model_registry"):
return mgr.model_registry
return mgr
@property
def _factory(self):
mgr = self._manager
if hasattr(mgr, "create_reranker"):
return mgr
return None
# ── Reranker ───────────────────────────────────────────────────
@property
def adapter(self) -> RerankerProtocol:
"""解析并缓存 Reranker 适配器。"""
if self._adapter is None:
self._adapter = self._resolve()
return self._adapter
async def rerank(
self,
query: str,
documents: list[str],
model_hint: str | None = None,
top_n: int | None = None,
) -> RerankResponse:
"""快捷方法:构造请求并调用 Reranker 适配器。"""
model = self._resolve_model_name(model_hint)
request = RerankRequest(
query=query,
documents=documents,
model=model,
top_n=top_n,
)
return await self.adapter.rerank(request)
# ── 解析 ───────────────────────────────────────────────────────
def _resolve(self) -> RerankerProtocol:
"""查找第一个活跃的 Reranker 模型并通过工厂创建。"""
models = self._list_capability_models("reranker")
if not models:
raise AIProviderError(
status_code=503,
code="reranker_no_model",
message="No reranker model configured in the model registry.",
)
factory = self._factory
if factory is None:
raise AIProviderError(
status_code=501,
code="reranker_no_factory",
message="Reranker adapter factory not available (provider manager missing create_reranker).",
)
return factory.create_reranker(models[0].name)
def _resolve_model_name(self, model_hint: str | None) -> str:
models = self._list_capability_models("reranker")
if model_hint:
return model_hint
if models:
return models[0].name
return ""
def _list_capability_models(self, tag: str):
reg = self._registry
if hasattr(reg, "list_capability_models"):
return reg.list_capability_models(tag)
if hasattr(reg, "list_by_capability"):
return reg.list_by_capability(tag)
return []
# ── 生命周期 ────────────────────────────────────────────────────
async def close(self) -> None:
if self._adapter is not None:
close = getattr(self._adapter, "close", None)
if close is not None:
await close()
async def __aenter__(self) -> RerankerService:
return self
async def __aexit__(self, *exc_info: object) -> None:
await self.close()
__all__ = [
"RerankerProtocol",
"RerankRequest",
"RerankResponse",
"RerankResult",
"RerankerService",
"RerankerServiceError",
]
+147
View File
@@ -0,0 +1,147 @@
"""重排序服务类型 — 请求/响应模型和协议定义。
Reranker 适配器通过 HTTP 调用重排序端点,经由 ``RerankerService``
或直接从 ``ProviderManager`` 消费。
用法::
service = RerankerService(manager)
result = await service.rerank("查询", ["文档1", "文档2"])
# 或通过工厂直接调用:
adapter = manager.create_reranker("rerank-model")
result = await adapter.rerank(RerankRequest(query="...", documents=["..."]))
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
from app.ai_provider.types.errors import AIProviderError
if TYPE_CHECKING:
from app.ai_provider.config.models import ModelConfig, ModelEndpointConfig
class RerankerServiceError(AIProviderError):
"""Reranker API 调用失败的基础错误。"""
# ── 请求 / 响应 ────────────────────────────────────────────────────────
@dataclass
class RerankRequest:
"""重排序请求。
属性:
query: 查询文本。
documents: 待重排序的文档列表。
model: 模型标识符。
top_n: 返回的最相关文档数量(nil 表示全部)。
max_chunks_per_doc: 每个文档最多切分为多少块。
return_documents: 返回结果中是否包含文档文本。
"""
query: str
documents: list[str]
model: str = ""
top_n: int | None = None
max_chunks_per_doc: int | None = None
return_documents: bool = False
@dataclass
class RerankResult:
"""单条重排序结果。"""
index: int
relevance_score: float
text: str | None = None
@dataclass
class RerankResponse:
"""重排序 API 的完整响应。"""
results: list[RerankResult]
model: str = ""
usage: dict[str, int] | None = None
# ── Protocol ────────────────────────────────────────────────────────────
@runtime_checkable
class RerankerProtocol(Protocol):
"""Reranker 适配器的协议。"""
async def rerank(self, request: RerankRequest) -> RerankResponse:
"""对文档进行重排序。
异常:
RerankerServiceError: API 调用失败或响应异常时抛出。
"""
...
async def close(self) -> None:
"""释放适配器持有的 HTTP 连接。"""
...
# ── 共享工厂基类 ──────────────────────────────────────────────────────
class RerankerAdapterBase:
"""Reranker 适配器基类,提供共享的工厂方法。
子类应在 ``__init__`` 中接受 ``base_url``、``api_key``、``model``、
``request_timeout_seconds`` 参数。
"""
_adapter_type: str = "reranker"
_default_timeout: int = 60
@classmethod
def from_endpoint(
cls,
endpoint: ModelEndpointConfig,
*,
timeout_seconds: int | None = None,
) -> Any:
"""从已解析的端点配置构建适配器。"""
base_url = endpoint.base_url
api_key = endpoint.api_key
if not base_url:
raise RerankerServiceError(
status_code=500,
code="reranker_endpoint_invalid",
message="Reranker endpoint has no resolved base_url.",
details={"endpoint": endpoint.name},
)
return cls(
base_url=base_url,
api_key=api_key,
model=endpoint.model,
request_timeout_seconds=timeout_seconds
or endpoint.request_timeout_seconds
or cls._default_timeout,
)
@classmethod
def from_model_config(
cls,
config: ModelConfig,
*,
timeout_seconds: int | None = None,
) -> Any:
"""从逻辑模型配置构建适配器(使用第一个活跃端点)。"""
for ep in config.endpoints:
if ep.status == "active":
return cls.from_endpoint(ep, timeout_seconds=timeout_seconds)
raise RerankerServiceError(
status_code=500,
code="reranker_no_endpoint",
message=f"Reranker model '{config.name}' has no active endpoint.",
)
@@ -0,0 +1,186 @@
"""Cohere 兼容格式的 Reranker 适配器 — ``POST /v1/rerank``。
覆盖 Cohere、mixedbread、Jina AI 等采用 Cohere 兼容重排序端点的提供商。
请求::
POST {base_url}/v1/rerank
Authorization: Bearer {api_key}
Content-Type: application/json
{
"model": "rerank-model",
"query": "查询文本",
"documents": ["文档1", "文档2"],
"top_n": 3,
"return_documents": false
}
响应::
{
"results": [
{"index": 0, "relevance_score": 0.99, "document": {"text": "文档..."}},
{"index": 1, "relevance_score": 0.85, "document": {"text": "文档..."}}
],
"meta": {"billed_units": {"search_units": 2}}
}
另见:
- Cohere Rerank API: https://docs.cohere.com/reference/rerank
- Jina Reranker: https://api.jina.ai/redoc#tag/rerank
"""
from __future__ import annotations
from typing import Any
import httpx
from app.ai_provider.reranker.base import (
RerankerAdapterBase,
RerankerProtocol,
RerankerServiceError,
RerankRequest,
RerankResponse,
RerankResult,
)
from app.core.logging import logger
class CohereFormatRerankerAdapter(RerankerAdapterBase, RerankerProtocol):
"""Cohere 兼容重排序适配器,覆盖多数重排序服务提供商。"""
_adapter_type = "reranker"
_default_timeout = 60
def __init__(
self,
*,
base_url: str,
api_key: str,
model: str = "rerank-model",
request_timeout_seconds: int = 60,
) -> None:
self._base_url = base_url.rstrip("/")
self._api_key = api_key
self._model = model
self._timeout = request_timeout_seconds
self._client: httpx.AsyncClient | None = None
# ── 客户端 ───────────────────────────────────────────────────────────
@property
def _http(self) -> httpx.AsyncClient:
if self._client is None or self._client.is_closed:
self._client = httpx.AsyncClient(timeout=httpx.Timeout(self._timeout))
return self._client
async def close(self) -> None:
if self._client is not None and not self._client.is_closed:
await self._client.aclose()
# ── RerankerProtocol ────────────────────────────────────────────────
async def rerank(self, request: RerankRequest) -> RerankResponse:
"""通过 ``POST /v1/rerank`` 对文档进行重排序。"""
url = f"{self._base_url}/v1/rerank"
payload: dict[str, Any] = {
"model": request.model or self._model,
"query": request.query,
"documents": request.documents,
"return_documents": request.return_documents,
}
if request.top_n is not None:
payload["top_n"] = request.top_n
if request.max_chunks_per_doc is not None:
payload["max_chunks_per_doc"] = request.max_chunks_per_doc
headers = {
"Authorization": f"Bearer {self._api_key}",
"Content-Type": "application/json",
}
logger.info(
"reranker.request",
model=payload["model"],
query_length=len(request.query),
document_count=len(request.documents),
top_n=request.top_n,
url=url,
)
try:
response = await self._http.post(url, json=payload, headers=headers)
except httpx.TimeoutException as exc:
raise RerankerServiceError(
status_code=504,
code="reranker_timeout",
message="Reranker request timed out.",
) from exc
except httpx.RequestError as exc:
raise RerankerServiceError(
status_code=503,
code="reranker_connection_error",
message=f"Reranker connection error: {exc}",
) from exc
if response.status_code != 200:
body = response.text[:500]
raise RerankerServiceError(
status_code=response.status_code,
code="reranker_api_error",
message=f"Reranker API returned {response.status_code}: {body}",
)
result = self._parse_response(response.json())
logger.info(
"reranker.success",
model=payload["model"],
total_results=len(result.results),
)
return result
# ── 辅助方法 ──────────────────────────────────────────────────────────
@staticmethod
def _parse_response(body: dict) -> RerankResponse:
"""从 Cohere 格式的重排序响应中提取结果。"""
try:
raw_results = body.get("results", [])
results = []
for item in raw_results:
results.append(
RerankResult(
index=item.get("index", 0),
relevance_score=float(item.get("relevance_score", 0.0)),
text=item.get("document", {}).get("text")
if isinstance(item.get("document"), dict)
else None,
)
)
# 按相关性分数降序排列
results.sort(key=lambda r: r.relevance_score, reverse=True)
# 提取用量信息(不同提供商格式各异)
usage: dict[str, int] | None = None
meta = body.get("meta")
if isinstance(meta, dict):
billed = meta.get("billed_units")
if isinstance(billed, dict):
usage = {k: int(v) for k, v in billed.items() if isinstance(v, (int, float))}
return RerankResponse(
results=results,
model=body.get("model") or body.get("id", ""),
usage=usage,
)
except Exception as exc:
raise RerankerServiceError(
status_code=500,
code="reranker_response_parse_error",
message=f"Failed to parse reranker response: {exc}",
) from exc