Spaces:
Sleeping
Sleeping
File size: 4,260 Bytes
bf9f7d1 | 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 | """Transporte del juez servido por el CLI de Claude Code (`claude -p`).
Es la tercera vía entre las dos que ya había, y la que mejor encaja en este proyecto:
- El juez Ollama es gratis y corre headless, pero un 7B discrimina poco.
- El juez por SDK necesita ANTHROPIC_API_KEY y saldo de API — el requisito que dejó la capa
de juez sin cablear durante toda la fase 3.
- Este usa la sesión de Claude Code que el desarrollador ya tiene abierta: sin clave de API,
con un modelo grande detrás.
A cambio: sólo existe donde está el binario y hay sesión iniciada (es decir, en local, NO en
un runner de GitHub Actions), consume los límites de uso de la suscripción, y no expone
temperatura, así que es algo menos reproducible que la ruta Ollama. Por eso `crear_juez`
degrada solo a Ollama cuando el binario no está.
"""
from __future__ import annotations
import json
import os
import shutil
import subprocess
from . import ErrorJuez
# Sonnet como juez por defecto: la rúbrica es una tarea de clasificación corta y no justifica
# el coste de Opus en cada caso del dataset. Para una auditoría puntual, MORPHOS_JUEZ_CLI_MODELO=opus.
MODELO_DEFECTO = "sonnet"
TIMEOUT_S = 180
class ErrorJuezCLI(ErrorJuez):
"""Fallo al invocar el CLI o al interpretar su salida."""
def modelo_cli() -> str:
return os.environ.get("MORPHOS_JUEZ_CLI_MODELO", MODELO_DEFECTO)
def disponible() -> tuple[bool, str]:
"""(disponible, motivo). Sólo comprueba el binario: verificar la sesión exigiría gastar
una petición real, así que un fallo de autenticación se reporta en la primera llamada."""
if shutil.which("claude") is None:
return False, "el CLI `claude` no está en el PATH"
return True, ""
def preguntar_json(sistema: str, mensaje: str) -> dict:
"""Una llamada de juicio. Lanza ErrorJuezCLI ante cualquier fallo."""
orden = [
"claude",
"-p",
"--system-prompt", sistema,
"--output-format", "json",
"--model", modelo_cli(),
# El juez no necesita herramientas ni servidores MCP, y no debe arrastrar la
# configuración del repo donde se ejecuta: sin MCP, sin sesión persistida y con un
# solo turno. Las herramientas quedan denegadas por defecto en modo -p.
"--strict-mcp-config",
"--no-session-persistence",
"--max-turns", "1",
]
try:
# El prompt va por stdin, no por argv: los casos con muchos hallazgos generan
# mensajes de varios KB y argv tiene un límite de tamaño.
proc = subprocess.run(
orden, input=mensaje, capture_output=True, text=True, timeout=TIMEOUT_S
)
except FileNotFoundError as exc:
raise ErrorJuezCLI("El CLI `claude` no está instalado.") from exc
except subprocess.TimeoutExpired as exc:
raise ErrorJuezCLI(f"El juez CLI no respondió en {TIMEOUT_S}s.") from exc
if proc.returncode != 0:
detalle = (proc.stderr or proc.stdout or "").strip()[:300]
raise ErrorJuezCLI(f"`claude -p` falló (código {proc.returncode}): {detalle}")
try:
sobre = json.loads(proc.stdout)
except json.JSONDecodeError as exc:
raise ErrorJuezCLI(f"El CLI no devolvió el sobre JSON esperado: {exc}") from exc
if sobre.get("is_error"):
raise ErrorJuezCLI(f"El juez CLI devolvió error: {str(sobre.get('result'))[:300]}")
return _extraer_json(str(sobre.get("result", "")))
def _extraer_json(texto: str) -> dict:
"""El modelo devuelve el JSON pedido, pero a veces envuelto en una valla de código.
No hay `format` que forzar como en Ollama, así que se desenvuelve aquí."""
limpio = texto.strip()
if limpio.startswith("```"):
limpio = limpio.split("\n", 1)[-1].rsplit("```", 1)[0].strip()
try:
return json.loads(limpio)
except json.JSONDecodeError as exc:
error = exc
# Último recurso: el primer objeto JSON del texto.
inicio, fin = limpio.find("{"), limpio.rfind("}")
if inicio != -1 and fin > inicio:
try:
return json.loads(limpio[inicio : fin + 1])
except json.JSONDecodeError as exc:
error = exc
raise ErrorJuezCLI(f"El juez no devolvió JSON válido: {error}") from error
|