Spaces:
Sleeping
Sleeping
File size: 4,604 Bytes
70e641d bf9f7d1 70e641d bf9f7d1 70e641d 6ab7946 70e641d a5d73ff 70e641d 6ab7946 70e641d a5d73ff 70e641d a5d73ff 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 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 105 106 107 108 109 110 111 112 113 | """Cliente Claude (ruta h铆brida opcional y juez de evals).
Usa salida estructurada v铆a 'tool use': se define una herramienta cuyo input_schema es
el JSON Schema de InterpretacionClinica y se fuerza su uso, de modo que el modelo
devuelve directamente un objeto que valida contra Pydantic. Sin regex de limpieza.
"""
from __future__ import annotations
import json
import re
from ..config import obtener_config
from ..schemas import InterpretacionClinica, esquema_estructurado
from .base import ErrorModelo
_HERRAMIENTA = {
"name": "emitir_interpretacion",
"description": "Emite la interpretaci贸n cl铆nica veterinaria en formato estructurado.",
"input_schema": esquema_estructurado(),
}
_DATA_URL = re.compile(r"^data:(image/(?:jpeg|png|gif|webp));base64,(.+)$", re.DOTALL)
def _bloques_imagen(imagenes: list[str]) -> list[dict]:
bloques = []
for img in imagenes:
m = _DATA_URL.match(img)
if not m:
continue
bloques.append(
{
"type": "image",
"source": {"type": "base64", "media_type": m.group(1), "data": m.group(2)},
}
)
return bloques
class ClaudeClient:
nombre = "claude"
prosa = False
def __init__(self) -> None:
cfg = obtener_config()
if not cfg.anthropic_api_key:
raise ErrorModelo(
"ANTHROPIC_API_KEY no configurada para la ruta Claude.", reintentable=False
)
# Import perezoso para no exigir el SDK cuando s贸lo se usa medGemma.
from anthropic import AsyncAnthropic
self._cliente = AsyncAnthropic(api_key=cfg.anthropic_api_key)
self._modelo = cfg.claude_model
self.modelo = cfg.claude_model
async def interpretar(
self, sistema: str, mensaje_usuario: str, imagenes: list[str]
) -> InterpretacionClinica:
contenido: list[dict] = [*_bloques_imagen(imagenes), {"type": "text", "text": mensaje_usuario}]
try:
resp = await self._cliente.messages.create(
model=self._modelo,
max_tokens=1500,
system=sistema,
messages=[{"role": "user", "content": contenido}],
tools=[_HERRAMIENTA],
tool_choice={"type": "tool", "name": "emitir_interpretacion"},
)
except Exception as exc: # noqa: BLE001
raise ErrorModelo(f"Fallo llamando a Claude: {exc}") from exc
# Los clasificadores de seguridad pueden rechazar la petici贸n: llega un HTTP 200 con
# `stop_reason="refusal"` y `content` vac铆o o parcial. Sin esta comprobaci贸n el bucle de
# abajo no encuentra el bloque tool_use y el usuario recibe un error enga帽oso.
if resp.stop_reason == "refusal":
categoria = getattr(getattr(resp, "stop_details", None), "category", None)
raise ErrorModelo(
"El modelo rechaz贸 la petici贸n por sus filtros de seguridad"
+ (f" (categor铆a: {categoria})" if categoria else "")
+ ". Reformula el caso o usa la ruta medGemma.",
reintentable=False,
)
for bloque in resp.content:
if getattr(bloque, "type", None) == "tool_use":
try:
return InterpretacionClinica.model_validate(bloque.input)
except Exception as exc: # noqa: BLE001
raise ErrorModelo(f"Salida de Claude no valida el esquema: {exc}") from exc
raise ErrorModelo("Claude no devolvi贸 el bloque tool_use esperado.")
async def juzgar(self, sistema: str, mensaje: str) -> dict:
"""Utilidad para el juez de evals: devuelve JSON arbitrario del modelo."""
resp = await self._cliente.messages.create(
model=self._modelo,
max_tokens=1200,
system=sistema,
messages=[{"role": "user", "content": mensaje}],
)
if resp.stop_reason == "refusal":
raise ErrorModelo(
"El juez rechaz贸 el caso por sus filtros de seguridad.", reintentable=False
)
texto = "".join(b.text for b in resp.content if getattr(b, "type", None) == "text")
try:
return json.loads(texto)
except json.JSONDecodeError as exc:
# Un juez que devuelve prosa en vez de JSON debe fallar con un error tipado, no con
# un JSONDecodeError crudo que el arn茅s de evals no distingue de un fallo de red.
raise ErrorModelo(f"El juez no devolvi贸 JSON v谩lido: {exc}") from exc
|