File size: 2,538 Bytes
70e641d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a5d73ff
 
 
 
 
 
 
 
 
 
bf9f7d1
 
 
 
 
a5d73ff
 
bf9f7d1
 
 
 
 
 
 
 
a5d73ff
 
 
bf9f7d1
70e641d
 
 
 
6ab7946
 
 
 
 
 
 
70e641d
 
 
 
 
 
 
 
 
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
"""Interfaz común de los clientes de modelo.

Abstrae la ruta híbrida: medGemma auto-alojado (privado por defecto) y Claude (opcional,
mayor precisión). Ambos deben devolver una InterpretacionClinica validada; la validación
de esquema vive en cada cliente para poder reintentar ante salida malformada.
"""

from __future__ import annotations

from typing import Protocol

from ..schemas import InterpretacionClinica


class ErrorModelo(Exception):
    """Fallo al invocar un modelo o validar su salida.

    `reintentable` decide si el servicio vuelve a muestrear. La distinción importa de verdad:
    el reintento existe para la salida malformada de medGemma (razonamiento filtrado o bucle de
    repetición), donde volver a muestrear suele funcionar. Reintentar un 429 hace lo contrario —
    duplica la presión sobre la cuota de ZeroGPU, que es justo el recurso agotado— y reintentar
    un rechazo por seguridad o una clave mal configurada no puede cambiar nada.

    `saturado` marca los casos de límite de tasa/cuota, para poder responder 503 + Retry-After
    en vez de un 502 genérico.

    `truncado` marca la respuesta cortada a mitad de frase. Es reintentable, pero repetir la
    misma petición no sirve: se ha medido que el corte es determinista y lo provoca el
    contexto RAG comiéndose el presupuesto de generación del Space. El servicio lo usa para
    reintentar con MENOS literatura en vez de con el mismo prompt.
    """

    def __init__(
        self,
        mensaje: str,
        *,
        reintentable: bool = True,
        saturado: bool = False,
        truncado: bool = False,
    ) -> None:
        super().__init__(mensaje)
        self.reintentable = reintentable
        self.saturado = saturado
        self.truncado = truncado


class ClienteModelo(Protocol):
    nombre: str
    modelo: str
    # True si el cliente devuelve TEXTO libre envuelto en `interpretacion` en vez de rellenar
    # los campos estructurados. El servicio lo consulta para elegir el system prompt, para no
    # exigir campos que esta ruta no puede rellenar y para suplir `requiere_derivacion` desde
    # el motor determinista. Antes se deducía del nombre del cliente, que dejó de bastar en
    # cuanto la prosa pudo venir también de un modelo local.
    prosa: bool

    async def interpretar(
        self,
        sistema: str,
        mensaje_usuario: str,
        imagenes: list[str],
    ) -> InterpretacionClinica:
        """Devuelve una interpretación validada o lanza ErrorModelo."""
        ...