68 lines
3.2 KiB
Python
68 lines
3.2 KiB
Python
"""Optional local Ollama embeddings for semantic knowledge retrieval."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
import os
|
|
from typing import Sequence
|
|
import httpx
|
|
|
|
|
|
class EmbeddingUnavailable(RuntimeError):
|
|
"""The configured local embedding model is missing or cannot embed text."""
|
|
|
|
|
|
def embedding_model_name() -> str | None:
|
|
return os.getenv("KNOWLEDGE_EMBEDDING_MODEL", "granite-embedding:278m").strip() or None
|
|
|
|
|
|
def _ollama_base_url() -> str:
|
|
configured = os.getenv("LOCAL_LLM_BASE_URL", "http://127.0.0.1:11434/v1").rstrip("/")
|
|
if configured.endswith("/v1"):
|
|
configured = configured[:-3]
|
|
return configured
|
|
|
|
|
|
async def embed_texts(texts: Sequence[str], *, model: str | None = None) -> list[list[float]]:
|
|
"""Generate bounded-size embedding batches through the local Ollama API."""
|
|
if not texts:
|
|
return []
|
|
configured_model = model if model is not None else embedding_model_name()
|
|
model_name = configured_model.strip() if configured_model else ""
|
|
if not model_name:
|
|
raise EmbeddingUnavailable("لم يُضبط نموذج التضمين المحلي.")
|
|
endpoint = f"{_ollama_base_url()}/api/embed"
|
|
results: list[list[float]] = []
|
|
try:
|
|
async with httpx.AsyncClient(timeout=httpx.Timeout(180.0, connect=5.0)) as client:
|
|
for offset in range(0, len(texts), 32):
|
|
batch = [str(text)[:4_000] for text in texts[offset : offset + 32]]
|
|
response = await client.post(
|
|
endpoint,
|
|
json={"model": model_name, "input": batch, "keep_alive": "5m"},
|
|
)
|
|
response.raise_for_status()
|
|
payload = response.json()
|
|
vectors = payload.get("embeddings") if isinstance(payload, dict) else None
|
|
if not isinstance(vectors, list) or len(vectors) != len(batch):
|
|
raise EmbeddingUnavailable("أعاد نموذج التضمين عدد متجهات غير متوقع.")
|
|
dimension = None
|
|
for vector in vectors:
|
|
if not isinstance(vector, list) or not vector:
|
|
raise EmbeddingUnavailable("أعاد نموذج التضمين متجهًا فارغًا.")
|
|
if dimension is None:
|
|
dimension = len(vector)
|
|
if len(vector) != dimension:
|
|
raise EmbeddingUnavailable("أبعاد متجهات التضمين غير متطابقة.")
|
|
numeric = [float(value) for value in vector]
|
|
if not all(math.isfinite(value) for value in numeric):
|
|
raise EmbeddingUnavailable("أعاد نموذج التضمين قيمًا غير صالحة.")
|
|
if not any(value != 0 for value in numeric):
|
|
raise EmbeddingUnavailable("أعاد نموذج التضمين متجهًا صفريًا.")
|
|
results.append(numeric)
|
|
except EmbeddingUnavailable:
|
|
raise
|
|
except (httpx.HTTPError, ValueError, TypeError, KeyError) as exc:
|
|
raise EmbeddingUnavailable("نموذج التضمين المحلي غير متاح؛ سيبقى البحث النصي مستخدمًا.") from exc
|
|
return results
|