Complete local hybrid search and improve agent reliability

This commit is contained in:
Hamza Ayed
2026-10-02 23:38:02 +03:00
parent 3563a104a3
commit 140f6eb287
62 changed files with 7546 additions and 314 deletions
+67
View File
@@ -0,0 +1,67 @@
"""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