120 lines
4.5 KiB
Python
120 lines
4.5 KiB
Python
"""
|
|
app/services/embeddings_client.py — Text→Embedding Service
|
|
|
|
Zweck:
|
|
Liefert Embeddings für Textqueries.
|
|
- Legacy Mode (Sync): Nutzt lokal Sentence-Transformers (CPU).
|
|
- Modern Mode (Async/Class): Nutzt Ollama API (HTTP) für Non-Blocking Operations (WP-11).
|
|
|
|
Kompatibilität:
|
|
Python 3.12+, sentence-transformers 5.x, httpx
|
|
Version:
|
|
0.2.0 (Erweitert um Async EmbeddingsClient)
|
|
Stand:
|
|
2025-12-11
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
import os
|
|
import logging
|
|
import httpx
|
|
from typing import List, Optional
|
|
from functools import lru_cache
|
|
from app.config import get_settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# ==============================================================================
|
|
# TEIL 1: NEUE ASYNC KLASSE (Für Ingestion API / WP-11)
|
|
# ==============================================================================
|
|
|
|
class EmbeddingsClient:
|
|
"""
|
|
Async Client für Embeddings via Ollama (oder kompatible APIs).
|
|
Verhindert das Blockieren des Event-Loops bei schweren Berechnungen.
|
|
"""
|
|
def __init__(self):
|
|
self.settings = get_settings()
|
|
# Fallback auf Environment Variablen, falls Settings nicht geladen
|
|
self.base_url = os.getenv("MINDNET_OLLAMA_URL", "http://127.0.0.1:11434")
|
|
# Nutze explizites Embedding Modell oder Fallback auf LLM Modell
|
|
self.model = os.getenv("MINDNET_EMBEDDING_MODEL", os.getenv("MINDNET_LLM_MODEL", "phi3:mini"))
|
|
|
|
async def embed_query(self, text: str) -> List[float]:
|
|
"""Erzeugt Embedding für einen einzelnen Text."""
|
|
return await self._request_embedding(text)
|
|
|
|
async def embed_documents(self, texts: List[str]) -> List[List[float]]:
|
|
"""
|
|
Erzeugt Embeddings für eine Liste von Texten.
|
|
Nutzt eine Session für effizientere Requests.
|
|
"""
|
|
vectors = []
|
|
async with httpx.AsyncClient(timeout=60.0) as client:
|
|
for text in texts:
|
|
vec = await self._request_embedding_with_client(client, text)
|
|
vectors.append(vec)
|
|
return vectors
|
|
|
|
async def _request_embedding(self, text: str) -> List[float]:
|
|
"""Interne Hilfsmethode für Single-Request (One-off Client)."""
|
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
return await self._request_embedding_with_client(client, text)
|
|
|
|
async def _request_embedding_with_client(self, client: httpx.AsyncClient, text: str) -> List[float]:
|
|
"""Führt den eigentlichen Request gegen Ollama aus."""
|
|
if not text or not text.strip():
|
|
return []
|
|
|
|
url = f"{self.base_url}/api/embeddings"
|
|
try:
|
|
response = await client.post(
|
|
url,
|
|
json={
|
|
"model": self.model,
|
|
"prompt": text
|
|
}
|
|
)
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
return data.get("embedding", [])
|
|
except Exception as e:
|
|
logger.error(f"Embedding error (Ollama) for model {self.model}: {e}")
|
|
# Fallback: Leere Liste, damit der Prozess nicht crasht (wird vom Caller gefiltert)
|
|
return []
|
|
|
|
|
|
# ==============================================================================
|
|
# TEIL 2: LEGACY FUNKTIONEN (Für bestehende Sync-Module / CLI)
|
|
# ==============================================================================
|
|
|
|
# Lazy import, damit Testläufe ohne Modell-Laden schnell sind
|
|
def _load_model():
|
|
# Performance-Warnung loggen, da dies viel RAM braucht
|
|
logger.info("Loading local SentenceTransformer model (Legacy Mode)...")
|
|
from sentence_transformers import SentenceTransformer # import hier, nicht top-level
|
|
s = get_settings()
|
|
return SentenceTransformer(s.MODEL_NAME, device="cpu")
|
|
|
|
@lru_cache(maxsize=1)
|
|
def _cached_model():
|
|
return _load_model()
|
|
|
|
def embed_text(text: str) -> List[float]:
|
|
"""
|
|
LEGACY: Erzeugt einen Vektor synchron via Sentence-Transformers.
|
|
Wird u.a. vom Retriever oder alten CLI-Skripten genutzt.
|
|
"""
|
|
if not text or not text.strip():
|
|
# Um Konsistenz mit neuer Klasse zu wahren, loggen wir Warnung statt Error
|
|
# raise ValueError("embed_text: leerer Text") -> Veraltet
|
|
logger.warning("embed_text called with empty string")
|
|
return []
|
|
|
|
try:
|
|
model = _cached_model()
|
|
vec = model.encode([text], normalize_embeddings=True)[0]
|
|
return vec.astype(float).tolist()
|
|
except Exception as e:
|
|
logger.error(f"Legacy embed_text failed: {e}")
|
|
return [] |