Files
PlantAI-rag-new/vstore/embed.py
T
jerryandClaude Fable 5 5db3ead73e feat(docgen): doc_writer 能力全链——vstore 独立服务 + BFF docgen 路由 + 前端文档工作台
- vstore 独立安装包(8094):解析/嵌入/加密 vault/faiss 检索,存储路径支持本地 file 与 http(s):// URL,启动握手校验,集群化预留(embed 独立模块)
- BFF docgen 路由:上传入库/generate/编辑/导出/下载/主库管理,意图护栏(与前端同词典),vstore 失败兜底,quota 507 与 TTL 惰性清理
- 前端:自然触发意图路由、拖拽上传 chips、能力徽标、右侧文档 TAB(WYSIWYG 编辑/prompt 修改/PDF+DOCX 导出下载)、右栏拖宽+手柄仿左栏
- 批量导入工具 docgen_import.py(并发 8、导入即加密+删源明文)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-03 21:13:37 +08:00

92 lines
3.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""embed.py —— ollama 嵌入调用封装(httpx,并发 ≤8,bge-m3 1024 维)。
- embed_batch(texts):同步函数,分批并发调 POST {url}/api/embed
- 空文本返回零向量;维度不符抛 EmbedError
- 单元测试注入假 embed(monkeypatch),本模块不依赖真 ollama
"""
from __future__ import annotations
import logging
from concurrent.futures import ThreadPoolExecutor
import httpx
from .settings import (
DEFAULT_EMBED_MODEL,
DEFAULT_EMBED_URL,
EMBED_CONCURRENCY,
EMBED_DIM,
EMBED_TIMEOUT,
get_paths,
)
logger = logging.getLogger(__name__)
class EmbedError(Exception):
"""嵌入调用失败(服务不可达/维度不符/响应异常)。"""
def _zero_vector() -> list[float]:
return [0.0] * EMBED_DIM
def _request_one(client: httpx.Client, url: str, model: str, text: str) -> list[float]:
"""单条嵌入请求。空文本返回零向量(不打上游)。"""
if not text or not text.strip():
return _zero_vector()
try:
resp = client.post(
f"{url.rstrip('/')}/api/embed",
json={"model": model, "input": text},
timeout=EMBED_TIMEOUT,
)
resp.raise_for_status()
payload = resp.json()
embeddings = payload.get("embeddings")
if not embeddings or not isinstance(embeddings[0], list):
raise EmbedError(f"嵌入响应形态异常:{str(payload)[:200]}")
vector = [float(v) for v in embeddings[0]]
if len(vector) != EMBED_DIM:
raise EmbedError(f"嵌入维度不符:期望 {EMBED_DIM},实际 {len(vector)}")
return vector
except EmbedError:
raise
except (httpx.HTTPError, ValueError) as exc:
raise EmbedError(f"嵌入服务调用失败:{exc}") from exc
def embed_batch(
texts: list[str],
url: str | None = None,
model: str | None = None,
) -> list[list[float]]:
"""批量嵌入:线程池并发 ≤ EMBED_CONCURRENCY,保持输入顺序。
url/model 缺省取 settings(VSTORE_EMBED_URL / VSTORE_EMBED_MODEL)。
"""
if not texts:
return []
paths = get_paths()
base_url = url or paths.embed_url or DEFAULT_EMBED_URL
model_name = model or paths.embed_model or DEFAULT_EMBED_MODEL
results: list[list[float] | None] = [None] * len(texts)
with httpx.Client() as client:
with ThreadPoolExecutor(max_workers=EMBED_CONCURRENCY) as pool:
futures = {
pool.submit(_request_one, client, base_url, model_name, text): idx
for idx, text in enumerate(texts)
}
for future in futures:
idx = futures[future]
try:
results[idx] = future.result()
except EmbedError as exc:
logger.warning("embed: 第 %d 条嵌入失败:%s", idx, exc)
results[idx] = None
except Exception as exc: # 线程池其他异常也降级为零向量
logger.warning("embed: 第 %d 条嵌入异常:%s", idx, exc)
results[idx] = None
return [v if v is not None else _zero_vector() for v in results]