初始化提交
This commit is contained in:
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",
|
||||
]
|
||||
@@ -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
|
||||
Reference in new issue
Block a user