88 lines
3.2 KiB
Python
88 lines
3.2 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Embedding providers — con thermal gate e keep_alive adattivo."""
|
|
|
|
import logging
|
|
import os
|
|
import time
|
|
from typing import Optional
|
|
|
|
import httpx
|
|
|
|
from . import thermal
|
|
|
|
LOGGER = logging.getLogger("loogle_mcp.embeddings")
|
|
|
|
|
|
def embed_texts(texts: list[str]) -> list[list[float]]:
|
|
if not texts:
|
|
return []
|
|
ollama_url = os.environ.get("OLLAMA_URL", "").strip()
|
|
if ollama_url:
|
|
try:
|
|
return _embed_ollama(texts, ollama_url)
|
|
except Exception as exc:
|
|
LOGGER.warning("Ollama embedding failed: %s", exc)
|
|
openai_key = os.environ.get("OPENAI_API_KEY", "").strip()
|
|
if openai_key:
|
|
return _embed_openai(texts, openai_key)
|
|
raise RuntimeError("Nessun provider embedding configurato (OLLAMA_URL o OPENAI_API_KEY)")
|
|
|
|
|
|
def _embed_ollama(texts: list[str], base_url: str) -> list[list[float]]:
|
|
model = os.environ.get("OLLAMA_EMBED_MODEL", "nomic-embed-text")
|
|
vectors = []
|
|
timeout = httpx.Timeout(connect=30.0, read=300.0, write=30.0, pool=30.0)
|
|
with httpx.Client(timeout=timeout) as client:
|
|
for i, text in enumerate(texts):
|
|
status = thermal.wait_for_headroom(context=f"embed:{i+1}/{len(texts)}")
|
|
keep_alive = thermal.suggested_keep_alive(status)
|
|
delay = thermal.suggested_delay_s(status)
|
|
|
|
payload = {"model": model, "prompt": text, "keep_alive": keep_alive}
|
|
# options.num_thread limita i thread CPU lato Ollama (se supportato)
|
|
num_thread = os.environ.get("OLLAMA_NUM_THREAD", "").strip()
|
|
if num_thread:
|
|
try:
|
|
payload["options"] = {"num_thread": int(num_thread)}
|
|
except ValueError:
|
|
pass
|
|
|
|
resp = client.post(f"{base_url.rstrip('/')}/api/embeddings", json=payload)
|
|
if resp.status_code >= 400:
|
|
LOGGER.warning(
|
|
"Ollama embeddings HTTP %s: %s — payload keys=%s",
|
|
resp.status_code,
|
|
resp.text[:300],
|
|
list(payload.keys()),
|
|
)
|
|
resp.raise_for_status()
|
|
vectors.append(resp.json()["embedding"])
|
|
|
|
if delay > 0 and i + 1 < len(texts):
|
|
time.sleep(delay)
|
|
|
|
# Unload solo se esplicitamente richiesto (zona HARD) — evita spike da reload
|
|
if keep_alive == 0:
|
|
try:
|
|
client.post(
|
|
f"{base_url.rstrip('/')}/api/generate",
|
|
json={"model": model, "keep_alive": 0},
|
|
timeout=30.0,
|
|
)
|
|
except Exception:
|
|
pass
|
|
return vectors
|
|
|
|
|
|
def _embed_openai(texts: list[str], api_key: str) -> list[list[float]]:
|
|
model = os.environ.get("OPENAI_EMBED_MODEL", "text-embedding-3-small")
|
|
with httpx.Client(timeout=120.0) as client:
|
|
resp = client.post(
|
|
"https://api.openai.com/v1/embeddings",
|
|
headers={"Authorization": f"Bearer {api_key}"},
|
|
json={"model": model, "input": texts},
|
|
)
|
|
resp.raise_for_status()
|
|
data = resp.json()["data"]
|
|
return [item["embedding"] for item in sorted(data, key=lambda x: x["index"])]
|