Files
PlantAI-rag-new/vstore/backend.py
T

124 lines
4.6 KiB
Python
Raw Normal View History

"""backend.py —— 存储后端解析与握手校验(本地磁盘 / 远程 HTTP)。
- parse_backend(spec) → LocalBackend | RemoteBackend
含 :// 且 scheme http/https → RemoteBackend(vault/index 操作转 HTTP 指向远程根);
其余(含 file://)→ LocalBackend(本机磁盘)
- 启动握手校验:/health + 空检索;失败日志告警并抛异常(拒绝启动)
"""
from __future__ import annotations
import logging
from pathlib import Path
from typing import Any
from urllib.parse import urlparse
import httpx
logger = logging.getLogger(__name__)
class BackendError(Exception):
"""后端不可用/握手失败(启动拒绝)。"""
def _parse_scheme(spec: str) -> str:
"""提取 scheme(http/https 才视为远程)。"""
return urlparse(spec).scheme.lower()
def is_remote_spec(spec: str) -> bool:
"""含 :// 且 scheme http/https → 远程。"""
return "://" in spec and _parse_scheme(spec) in ("http", "https")
class LocalBackend:
"""本机磁盘后端:vault/index 均为本地目录。"""
kind = "local"
def __init__(self, vault_path: str, index_path: str) -> None:
self.vault_path = Path(vault_path)
self.index_path = Path(index_path)
def health(self) -> dict[str, Any]:
"""本地健康:目录可写即 ok。"""
try:
self.vault_path.mkdir(parents=True, exist_ok=True)
self.index_path.mkdir(parents=True, exist_ok=True)
return {"ok": True, "backend": self.kind}
except OSError as exc:
raise BackendError(f"本地目录不可写:{exc}") from exc
def search_empty(self) -> dict[str, Any]:
"""空检索握手:本地恒可用。"""
return {"ok": True, "mode": "local", "items": []}
def close(self) -> None: # pragma: no cover - 无资源可释放
pass
class RemoteBackend:
"""远程 HTTP 后端:vault/index 操作指向远程 vstore 根。"""
kind = "remote"
def __init__(self, vault_url: str, index_url: str) -> None:
self.vault_url = vault_url.rstrip("/")
self.index_url = index_url.rstrip("/")
def health(self) -> dict[str, Any]:
"""远程健康:GET /health(超时 5s)。"""
try:
resp = httpx.get(f"{self.index_url}/health", timeout=5.0)
resp.raise_for_status()
return resp.json()
except (httpx.HTTPError, ValueError) as exc:
raise BackendError(f"远程 vstore 健康检查失败:{exc}") from exc
def search_empty(self) -> dict[str, Any]:
"""空检索握手:GET /api/vstore/search?q=。"""
try:
resp = httpx.get(
f"{self.index_url}/api/vstore/search", params={"q": "", "top_k": 1}, timeout=5.0
)
resp.raise_for_status()
return resp.json()
except (httpx.HTTPError, ValueError) as exc:
raise BackendError(f"远程 vstore 空检索握手失败:{exc}") from exc
def close(self) -> None: # pragma: no cover - 无连接可释放
pass
def parse_backend(vault_spec: str, index_spec: str) -> LocalBackend | RemoteBackend:
"""解析存储后端。
- 两个 spec 都是本地(含 file://)→ LocalBackend(file:// 剥掉前缀)
- 两个 spec 都是 http(s):// → RemoteBackend
- 混合形态不支持,抛 BackendError
"""
vault_remote = is_remote_spec(vault_spec)
index_remote = is_remote_spec(index_spec)
if vault_remote != index_remote:
raise BackendError(
f"vault/index 后端形态不一致(vault={'远程' if vault_remote else '本地'},"
f"index={'远程' if index_remote else '本地'}),不支持混合部署"
)
if vault_remote:
return RemoteBackend(vault_url=vault_spec, index_url=index_spec)
# 本地路径(file:// 前缀剥掉)
vault_path = vault_spec[len("file://"):] if vault_spec.startswith("file://") else vault_spec
index_path = index_spec[len("file://"):] if index_spec.startswith("file://") else index_spec
return LocalBackend(vault_path=vault_path, index_path=index_path)
def handshake(backend: LocalBackend | RemoteBackend) -> None:
"""启动握手:health + 空检索。失败日志告警并抛 BackendError(拒绝启动)。"""
health = backend.health()
if not health.get("ok"):
raise BackendError(f"后端健康检查未通过:{health}")
search = backend.search_empty()
if not search.get("ok"):
raise BackendError(f"后端空检索握手未通过:{search}")
logger.info("backend: %s 握手通过(health+空检索)", backend.kind)