Spaces:
Sleeping
Sleeping
File size: 4,330 Bytes
70e641d bf9f7d1 70e641d 6ab7946 70e641d 6ab7946 70e641d 6ab7946 70e641d 6ab7946 bf9f7d1 70e641d 6ab7946 70e641d 6ab7946 70e641d bf9f7d1 70e641d bf9f7d1 70e641d bf9f7d1 70e641d a5d73ff 70e641d 6ab7946 70e641d 6ab7946 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | """Cliente medGemma auto-alojado (ruta privada por defecto).
Habla con Ollama por su API nativa /api/chat usando SALIDA ESTRUCTURADA: se pasa el
JSON Schema de InterpretacionClinica en el campo `format`, de modo que el modelo emite
JSON que valida contra Pydantic. Esto sustituye la inyecci贸n del token <unused95> y toda
la limpieza por regex de limpiarRespuesta.
Nota: se usa la plantilla de chat propia de Ollama (rol system/user), no concatenaci贸n
manual de tokens de control.
"""
from __future__ import annotations
import re
import httpx
from ..config import obtener_config
from ..schemas import InterpretacionClinica, esquema_estructurado
from .base import ErrorModelo
from .prosa import interpretacion_desde_prosa
_DATA_URL = re.compile(r"^data:image/(?:jpeg|png|gif|webp);base64,(.+)$", re.DOTALL)
def _base64_imagenes(imagenes: list[str]) -> list[str]:
salida = []
for img in imagenes:
m = _DATA_URL.match(img)
if m:
salida.append(m.group(1))
return salida
class MedGemmaClient:
"""Cliente de Ollama. `modelo` lo puede fijar el usuario desde la UI, pero S脫LO con un
nombre ya validado contra la lista blanca (`PeticionInterpretacion.modelo_local`); la
base_url nunca sale de la configuraci贸n del servidor.
`prosa` desactiva la decodificaci贸n restringida para los modelos que la aceptan pero la
rellenan en hueco (ver `config.modelos_locales`). El nombre distinto NO es cosm茅tico: el
servicio decide por `prosa` si usa el system prompt de prosa, si exige campos
estructurados y si suple `requiere_derivacion`.
"""
def __init__(self, modelo: str | None = None, *, prosa: bool = False) -> None:
cfg = obtener_config()
self._url = cfg.medgemma_base_url.rstrip("/")
self.modelo = modelo or cfg.medgemma_model
self.prosa = prosa
self.nombre = "medgemma-prosa" if prosa else "medgemma"
self._timeout = cfg.medgemma_timeout_s
self._esquema = esquema_estructurado()
async def interpretar(
self, sistema: str, mensaje_usuario: str, imagenes: list[str]
) -> InterpretacionClinica:
mensaje_user: dict = {"role": "user", "content": mensaje_usuario}
b64 = _base64_imagenes(imagenes)
if b64:
mensaje_user["images"] = b64
payload = {
"model": self.modelo,
"messages": [{"role": "system", "content": sistema}, mensaje_user],
"stream": False,
"think": False,
"options": {"temperature": 0.2, "num_predict": 1500},
}
if not self.prosa:
payload["format"] = self._esquema # salida estructurada nativa de Ollama
try:
async with httpx.AsyncClient(timeout=self._timeout) as cliente:
resp = await cliente.post(f"{self._url}/api/chat", json=payload)
except httpx.TimeoutException as exc:
# Se distingue del fallo de conexi贸n a prop贸sito: varias excepciones de httpx se
# convierten a cadena vac铆a, as铆 que "No se pudo conectar: " mandaba a revisar la
# red cuando lo que pasaba era que Ollama estaba cargando el modelo.
raise ErrorModelo(
f"medGemma no respondi贸 en {self._timeout}s ({type(exc).__name__}). Si el modelo "
"acaba de cambiar, la primera petici贸n carga pesos y tarda: sube "
"MORPHOS_MEDGEMMA_TIMEOUT_S o precarga el modelo.",
) from exc
except httpx.HTTPError as exc:
raise ErrorModelo(
f"No se pudo conectar con medGemma en {self._url}: {type(exc).__name__} {exc}"
) from exc
if resp.status_code >= 400:
raise ErrorModelo(
f"medGemma devolvi贸 HTTP {resp.status_code}: {resp.text[:200]}",
reintentable=resp.status_code >= 500,
saturado=resp.status_code == 429,
)
contenido = resp.json().get("message", {}).get("content", "")
if self.prosa:
return interpretacion_desde_prosa(contenido)
try:
return InterpretacionClinica.model_validate_json(contenido)
except Exception as exc: # noqa: BLE001
raise ErrorModelo(f"Salida de {self.modelo} no valida el esquema: {exc}") from exc
|