Initial commit: THE FACTORY - Iterative Image Generation
Tasks: - image_generate: Generate image from prompt - image_variant: Generate variant of existing image - image_upscale: Increase resolution Models: SDXL, Flux, SDXL-Turbo RunPod Serverless Handler
This commit is contained in:
233
evaluator.py
Normal file
233
evaluator.py
Normal file
@@ -0,0 +1,233 @@
|
||||
"""
|
||||
=============================================================================
|
||||
THE FACTORY - Evaluator
|
||||
=============================================================================
|
||||
Evalúa artefactos generados vs el objetivo.
|
||||
Proporciona:
|
||||
- confidence: 0.0 a 1.0
|
||||
- feedback: sugerencias de mejora
|
||||
=============================================================================
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
import anthropic
|
||||
|
||||
from config import FactoryConfig, FunctionType
|
||||
|
||||
logger = logging.getLogger("factory.evaluator")
|
||||
|
||||
|
||||
class Evaluator:
|
||||
"""
|
||||
El Evaluator evalúa artefactos contra el objetivo.
|
||||
"""
|
||||
|
||||
def __init__(self, config: FactoryConfig):
|
||||
self.config = config
|
||||
|
||||
if config.anthropic_api_key:
|
||||
self.anthropic = anthropic.Anthropic(api_key=config.anthropic_api_key)
|
||||
else:
|
||||
self.anthropic = None
|
||||
logger.warning("ANTHROPIC_API_KEY no configurada para Evaluator")
|
||||
|
||||
def evaluate(
|
||||
self,
|
||||
artifact: Any,
|
||||
objective: str,
|
||||
function: FunctionType
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Evalúa un artefacto.
|
||||
|
||||
Args:
|
||||
artifact: El artefacto generado
|
||||
objective: El objetivo a cumplir
|
||||
function: Tipo de función
|
||||
|
||||
Returns:
|
||||
{
|
||||
"confidence": 0.0-1.0,
|
||||
"feedback": "sugerencias de mejora",
|
||||
"strengths": ["punto fuerte 1", ...],
|
||||
"weaknesses": ["punto débil 1", ...],
|
||||
"cost_usd": coste de evaluación
|
||||
}
|
||||
"""
|
||||
|
||||
if not self.anthropic:
|
||||
# Fallback: evaluación básica sin LLM
|
||||
return self._evaluate_basic(artifact, objective, function)
|
||||
|
||||
return self._evaluate_with_llm(artifact, objective, function)
|
||||
|
||||
def _evaluate_with_llm(
|
||||
self,
|
||||
artifact: Any,
|
||||
objective: str,
|
||||
function: FunctionType
|
||||
) -> Dict[str, Any]:
|
||||
"""Evalúa usando Claude Haiku."""
|
||||
|
||||
model = self.config.get_model(self.config.evaluator_model)
|
||||
|
||||
# Preparar artefacto para evaluación
|
||||
artifact_str = self._prepare_artifact_for_eval(artifact, function)
|
||||
|
||||
prompt = f"""Evalúa el siguiente artefacto contra el objetivo especificado.
|
||||
|
||||
OBJETIVO:
|
||||
{objective}
|
||||
|
||||
ARTEFACTO A EVALUAR:
|
||||
{artifact_str}
|
||||
|
||||
Proporciona tu evaluación en formato JSON con esta estructura exacta:
|
||||
{{
|
||||
"confidence": <número entre 0.0 y 1.0>,
|
||||
"feedback": "<sugerencias específicas de mejora>",
|
||||
"strengths": ["<punto fuerte 1>", "<punto fuerte 2>"],
|
||||
"weaknesses": ["<punto débil 1>", "<punto débil 2>"],
|
||||
"meets_objective": <true/false>
|
||||
}}
|
||||
|
||||
Criterios de evaluación:
|
||||
- 0.9-1.0: Excelente, cumple completamente el objetivo
|
||||
- 0.7-0.9: Bueno, cumple mayormente con mejoras menores posibles
|
||||
- 0.5-0.7: Aceptable, cumple parcialmente
|
||||
- 0.3-0.5: Deficiente, necesita mejoras significativas
|
||||
- 0.0-0.3: Inaceptable, no cumple el objetivo
|
||||
|
||||
Sé específico en el feedback para que el siguiente intento pueda mejorar."""
|
||||
|
||||
try:
|
||||
response = self.anthropic.messages.create(
|
||||
model=model.name,
|
||||
max_tokens=1024,
|
||||
messages=[{"role": "user", "content": prompt}]
|
||||
)
|
||||
|
||||
# Extraer texto
|
||||
response_text = ""
|
||||
for block in response.content:
|
||||
if hasattr(block, "text"):
|
||||
response_text += block.text
|
||||
|
||||
# Parsear JSON
|
||||
eval_result = self._parse_eval_response(response_text)
|
||||
|
||||
# Calcular coste
|
||||
input_tokens = response.usage.input_tokens
|
||||
output_tokens = response.usage.output_tokens
|
||||
cost = (
|
||||
(input_tokens / 1000) * model.cost_per_1k_input +
|
||||
(output_tokens / 1000) * model.cost_per_1k_output
|
||||
)
|
||||
|
||||
eval_result["cost_usd"] = cost
|
||||
eval_result["tokens_used"] = input_tokens + output_tokens
|
||||
|
||||
logger.info(f"Evaluación: confidence={eval_result['confidence']:.2f}")
|
||||
|
||||
return eval_result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error en evaluación LLM: {e}")
|
||||
return self._evaluate_basic(artifact, objective, function)
|
||||
|
||||
def _parse_eval_response(self, response: str) -> Dict[str, Any]:
|
||||
"""Parsea respuesta de evaluación."""
|
||||
|
||||
# Intentar extraer JSON
|
||||
try:
|
||||
# Buscar JSON en la respuesta
|
||||
start = response.find("{")
|
||||
end = response.rfind("}") + 1
|
||||
|
||||
if start >= 0 and end > start:
|
||||
json_str = response[start:end]
|
||||
result = json.loads(json_str)
|
||||
|
||||
# Validar campos requeridos
|
||||
confidence = float(result.get("confidence", 0.5))
|
||||
confidence = max(0.0, min(1.0, confidence)) # Clamp 0-1
|
||||
|
||||
return {
|
||||
"confidence": confidence,
|
||||
"feedback": result.get("feedback", "Sin feedback específico"),
|
||||
"strengths": result.get("strengths", []),
|
||||
"weaknesses": result.get("weaknesses", []),
|
||||
"meets_objective": result.get("meets_objective", confidence >= 0.7)
|
||||
}
|
||||
|
||||
except (json.JSONDecodeError, ValueError) as e:
|
||||
logger.warning(f"Error parseando evaluación: {e}")
|
||||
|
||||
# Fallback
|
||||
return {
|
||||
"confidence": 0.5,
|
||||
"feedback": "No se pudo evaluar correctamente. Intenta de nuevo.",
|
||||
"strengths": [],
|
||||
"weaknesses": ["Evaluación fallida"],
|
||||
"meets_objective": False
|
||||
}
|
||||
|
||||
def _evaluate_basic(
|
||||
self,
|
||||
artifact: Any,
|
||||
objective: str,
|
||||
function: FunctionType
|
||||
) -> Dict[str, Any]:
|
||||
"""Evaluación básica sin LLM."""
|
||||
|
||||
artifact_str = str(artifact) if artifact else ""
|
||||
|
||||
# Heurísticas simples
|
||||
confidence = 0.5
|
||||
|
||||
# Verificar longitud mínima
|
||||
if len(artifact_str) < 50:
|
||||
confidence -= 0.2
|
||||
elif len(artifact_str) > 200:
|
||||
confidence += 0.1
|
||||
|
||||
# Verificar que contiene palabras del objetivo
|
||||
objective_words = set(objective.lower().split())
|
||||
artifact_words = set(artifact_str.lower().split())
|
||||
overlap = len(objective_words & artifact_words)
|
||||
|
||||
if overlap >= len(objective_words) * 0.3:
|
||||
confidence += 0.2
|
||||
|
||||
confidence = max(0.0, min(1.0, confidence))
|
||||
|
||||
return {
|
||||
"confidence": confidence,
|
||||
"feedback": "Evaluación básica aplicada. Considera revisar manualmente.",
|
||||
"strengths": ["Artefacto generado"],
|
||||
"weaknesses": ["Evaluación automática limitada"],
|
||||
"meets_objective": confidence >= 0.7,
|
||||
"cost_usd": 0.0
|
||||
}
|
||||
|
||||
def _prepare_artifact_for_eval(self, artifact: Any, function: FunctionType) -> str:
|
||||
"""Prepara artefacto para evaluación."""
|
||||
|
||||
if artifact is None:
|
||||
return "[No artifact generated]"
|
||||
|
||||
if function == FunctionType.IMAGE_GENERATION:
|
||||
if isinstance(artifact, dict):
|
||||
return f"[Image generated]\nURL: {artifact.get('url', 'N/A')}\nPrompt used: {artifact.get('prompt', 'N/A')}"
|
||||
return "[Image generated]"
|
||||
|
||||
artifact_str = str(artifact)
|
||||
|
||||
# Truncar si es muy largo
|
||||
if len(artifact_str) > 4000:
|
||||
return artifact_str[:4000] + "\n[...truncado para evaluación...]"
|
||||
|
||||
return artifact_str
|
||||
Reference in New Issue
Block a user