Módulo 1: Fundamentos de Prompt Engineering

8. Proyecto: Prompt Analyzer

Descripción del proyecto

En este proyecto construirás un Prompt Analyzer: un sistema que recibe cualquier prompt, lo clasifica por técnica (zero-shot, few-shot, CoT, mixto), identifica sus componentes (instrucción, contexto, input, output format), puntúa las dimensiones CRISPE presentes, y genera sugerencias de mejora específicas y accionables. El output es JSON estructurado con un schema definido y validado con Pydantic.

Este proyecto integra todo lo aprendido en el Módulo 1: anatomía de prompts, roles, parámetros, el framework CRISPE, la diferencia entre prompts casuales y engineered, y diferencias entre proveedores. No es un ejercicio teórico — es una herramienta que usarás a lo largo de toda la guía: cuando diseñes un prompt de CoT en el Módulo 4, puedes pasarlo por el Analyzer para verificar que tiene todos los componentes.

Objetivo: Construir un sistema funcional de análisis de prompts con output estructurado, que demuestre que los prompts tienen estructura analizable de forma sistemática.


Especificaciones Técnicas

Estructura del proyecto

prompt-analyzer/
├── analyzer.py        # Lógica principal de análisis
├── schemas.py         # Pydantic models para input/output
├── main.py            # CLI e interface de prueba
├── test_cases.py      # Casos de prueba con prompts de ejemplo
├── .env               # API keys (no commitear)
└── requirements.txt   # Dependencias

Dependencias

# requirements.txt
openai>=1.0.0
anthropic>=0.25.0
python-dotenv>=1.0.0
pydantic>=2.0.0
tiktoken>=0.5.0

Schema de salida

El Prompt Analyzer devuelve un JSON con cuatro secciones:

{
  "clasificacion": {
    "tecnica": "zero-shot|few-shot|chain-of-thought|mixto",
    "confianza": 0.0-1.0,
    "razon": "Explicación de por qué se clasificó así"
  },
  "componentes": {
    "instruccion": "presente|ausente|implicito",
    "contexto": "presente|ausente|implicito",
    "input": "presente|ausente|implicito",
    "output_format": "presente|ausente|implicito"
  },
  "crispe": {
    "capacity": "presente|ausente|implicito",
    "role": "presente|ausente|implicito",
    "insight": "presente|ausente|implicito",
    "statement": "presente|ausente|implicito",
    "personality": "presente|ausente|implicito",
    "experiment": "presente|ausente|implicito",
    "score": 0-6
  },
  "sugerencias": [
    "Sugerencia 1 específica y accionable",
    "Sugerencia 2 específica y accionable"
  ],
  "calidad": {
    "nivel": "casual|basico|engineered|avanzado",
    "puntuacion": 0-100,
    "resumen": "Evaluación en 1 frase"
  },
  "metadata": {
    "longitud_caracteres": 0,
    "estimacion_tokens": 0,
    "tiene_system_prompt": true|false
  }
}

Implementación Paso a Paso

Paso 1: schemas.py — Definir los modelos Pydantic

# schemas.py
from pydantic import BaseModel, Field
from typing import Literal

EstadoComponente = Literal["presente", "ausente", "implicito"]
TecnicaPrompt = Literal["zero-shot", "few-shot", "chain-of-thought", "mixto"]
NivelCalidad = Literal["casual", "basico", "engineered", "avanzado"]

class Clasificacion(BaseModel):
    tecnica: TecnicaPrompt = Field(description="Técnica de prompting detectada")
    confianza: float = Field(ge=0.0, le=1.0, description="Confianza en la clasificación")
    razon: str = Field(description="Por qué se clasificó con esta técnica")

class Componentes(BaseModel):
    instruccion: EstadoComponente = Field(description="Instrucción o tarea principal")
    contexto: EstadoComponente = Field(description="Contexto o información de fondo")
    input: EstadoComponente = Field(description="El input que procesará el modelo")
    output_format: EstadoComponente = Field(description="Formato de salida esperado")

class CRISPE(BaseModel):
    capacity: EstadoComponente
    role: EstadoComponente
    insight: EstadoComponente
    statement: EstadoComponente
    personality: EstadoComponente
    experiment: EstadoComponente
    score: int = Field(ge=0, le=6, description="Cuántos componentes CRISPE están presentes")

class Calidad(BaseModel):
    nivel: NivelCalidad
    puntuacion: int = Field(ge=0, le=100)
    resumen: str = Field(description="Evaluación en 1 frase")

class Metadata(BaseModel):
    longitud_caracteres: int
    estimacion_tokens: int
    tiene_system_prompt: bool

class PromptAnalysis(BaseModel):
    clasificacion: Clasificacion
    componentes: Componentes
    crispe: CRISPE
    sugerencias: list[str] = Field(min_length=1, max_length=6)
    calidad: Calidad
    metadata: Metadata

Paso 2: analyzer.py — Lógica principal

# analyzer.py
import json
import re
import tiktoken
from openai import OpenAI
from dotenv import load_dotenv
from schemas import PromptAnalysis, Metadata

load_dotenv()
client = OpenAI()

ANALYZER_SYSTEM = """
Eres un analizador experto de prompts para sistemas de IA. Analiza el prompt proporcionado
y devuelve un JSON estructurado con el análisis completo.

DEFINICIONES:

TÉCNICAS DE PROMPTING:
- zero-shot: Solo instrucción, sin ejemplos, sin razonamiento explícito
- few-shot: Incluye ejemplos input→output antes de la tarea real
- chain-of-thought: Incluye o solicita razonamiento paso a paso explícito
- mixto: Combina varias técnicas

COMPONENTES DEL PROMPT:
- instruccion: La tarea o acción principal que debe ejecutar el modelo
- contexto: Información de fondo, definiciones de dominio, reglas del negocio
- input: El dato o texto que el modelo debe procesar (puede ser un placeholder)
- output_format: Especificación del formato de salida (JSON, lista, formato específico)

DIMENSIONES CRISPE:
- capacity: Capacidad/acción principal (verbo de la tarea)
- role: Rol, expertise o persona que adopta el modelo
- insight: Contexto o información de fondo necesaria
- statement: Instrucción concreta y específica
- personality: Tono, estilo, restricciones de comportamiento
- experiment: Formato de salida, ejemplos

CALIDAD:
- casual: Sin estructura, sin restricciones, output no controlado
- basico: Tiene instrucción clara pero sin formato ni restricciones
- engineered: Tiene instrucción + formato + restricciones + system prompt o roles
- avanzado: engineered + CRISPE ≥4 + ejemplos o CoT + manejo de edge cases

PUNTUACIÓN DE CALIDAD (0-100):
- 0-25: Casual (prompt sin estructura)
- 26-50: Básico (instrucción clara, poco más)
- 51-75: Engineered (estructura clara, formato, restricciones)
- 76-100: Avanzado (CRISPE completo, ejemplos, edge cases, producción-ready)

SUGERENCIAS ESPECÍFICAS (no genéricas):
- ❌ Malo: "Mejora el prompt"
- ✅ Bueno: "Falta output_format. Añade: 'Responde en JSON: {\"campo\": \"valor\"}'"
- ✅ Bueno: "Falta Role (R en CRISPE). Añade: 'Eres un [expert] especializado en [dominio]'"
- ✅ Bueno: "Sin manejo de edge cases: añade 'Si el input está vacío, responde {\"error\": \"input_vacio\"}'"

Responde ÚNICAMENTE con JSON válido que siga exactamente el schema proporcionado.
"""

SCHEMA_PROMPT = """
{
  "clasificacion": {
    "tecnica": "zero-shot|few-shot|chain-of-thought|mixto",
    "confianza": 0.0-1.0,
    "razon": "string"
  },
  "componentes": {
    "instruccion": "presente|ausente|implicito",
    "contexto": "presente|ausente|implicito",
    "input": "presente|ausente|implicito",
    "output_format": "presente|ausente|implicito"
  },
  "crispe": {
    "capacity": "presente|ausente|implicito",
    "role": "presente|ausente|implicito",
    "insight": "presente|ausente|implicito",
    "statement": "presente|ausente|implicito",
    "personality": "presente|ausente|implicito",
    "experiment": "presente|ausente|implicito",
    "score": 0-6
  },
  "sugerencias": ["string", "string"],
  "calidad": {
    "nivel": "casual|basico|engineered|avanzado",
    "puntuacion": 0-100,
    "resumen": "string"
  },
  "metadata": {
    "longitud_caracteres": 0,
    "estimacion_tokens": 0,
    "tiene_system_prompt": true|false
  }
}
"""

def estimar_tokens(texto: str, model: str = "gpt-4o-mini") -> int:
    """Cuenta tokens usando tiktoken. Fallback a aproximación si falla."""
    try:
        encoding = tiktoken.encoding_for_model(model)
        return len(encoding.encode(texto))
    except Exception:
        return len(texto) // 4  # Aprox: 4 chars ≈ 1 token

def detectar_system_prompt(prompt: str) -> bool:
    """Heurística: detecta si el prompt parece un system prompt."""
    indicadores = [
        "eres un", "you are a", "actúa como", "act as",
        "tu rol es", "your role is", "## capacity", "## role"
    ]
    lower = prompt.lower()
    return any(ind in lower for ind in indicadores)

def limpiar_json(raw: str) -> str:
    """Limpia respuesta del modelo para extraer JSON válido."""
    # Remover markdown code blocks
    if "```" in raw:
        match = re.search(r'```(?:json)?\s*\n?(.*?)\n?```', raw, re.DOTALL)
        if match:
            return match.group(1).strip()
    
    # Buscar primer objeto JSON { ... }
    start = raw.find('{')
    if start != -1:
        depth = 0
        for i, char in enumerate(raw[start:], start):
            if char == '{':
                depth += 1
            elif char == '}':
                depth -= 1
                if depth == 0:
                    return raw[start:i+1]
    
    return raw.strip()

def analyze_prompt(prompt: str) -> PromptAnalysis:
    """
    Analiza un prompt y devuelve análisis estructurado.
    
    Args:
        prompt: El prompt a analizar (puede ser solo user o system+user)
    
    Returns:
        PromptAnalysis con clasificación, componentes, CRISPE, sugerencias, calidad
    """
    tokens = estimar_tokens(prompt)
    tiene_system = detectar_system_prompt(prompt)
    
    user_content = f"""Analiza este prompt:

---INICIO DEL PROMPT---
{prompt}
---FIN DEL PROMPT---

Datos conocidos para metadata:
- longitud_caracteres: {len(prompt)}
- estimacion_tokens: {tokens}
- tiene_system_prompt: {tiene_system}

Responde con JSON que siga exactamente este schema:
{SCHEMA_PROMPT}
"""
    
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": ANALYZER_SYSTEM},
            {"role": "user", "content": user_content}
        ],
        temperature=0,
        max_tokens=800
    )
    
    raw = response.choices[0].message.content.strip()
    json_str = limpiar_json(raw)
    
    try:
        data = json.loads(json_str)
    except json.JSONDecodeError as e:
        raise ValueError(f"El modelo no devolvió JSON válido: {e}\nRaw: {raw[:200]}")
    
    # Asegurar que metadata tiene los valores correctos
    if "metadata" in data:
        data["metadata"]["longitud_caracteres"] = len(prompt)
        data["metadata"]["estimacion_tokens"] = tokens
        data["metadata"]["tiene_system_prompt"] = tiene_system
    
    return PromptAnalysis(**data)

Paso 3: main.py — Interface de prueba y CLI

# main.py
import json
from analyzer import analyze_prompt

def imprimir_reporte(analysis, prompt_preview: str):
    """Imprime un reporte legible del análisis."""
    a = analysis
    
    print("=" * 60)
    print(f"PROMPT ANALIZADO: {prompt_preview[:50]}...")
    print("=" * 60)
    
    print(f"\n📊 CALIDAD: {a.calidad.nivel.upper()} ({a.calidad.puntuacion}/100)")
    print(f"   {a.calidad.resumen}")
    
    print(f"\n🏷️  TÉCNICA: {a.clasificacion.tecnica.upper()} (confianza: {a.clasificacion.confianza:.0%})")
    print(f"   {a.clasificacion.razon}")
    
    print("\n🧩 COMPONENTES:")
    for comp, estado in a.componentes.model_dump().items():
        icon = "✅" if estado == "presente" else ("⚠️" if estado == "implicito" else "❌")
        print(f"   {icon} {comp}: {estado}")
    
    print(f"\n🎯 CRISPE ({a.crispe.score}/6):")
    for dim, estado in a.crispe.model_dump().items():
        if dim == "score":
            continue
        icon = "✅" if estado == "presente" else ("⚠️" if estado == "implicito" else "❌")
        print(f"   {icon} {dim}: {estado}")
    
    print("\n💡 SUGERENCIAS:")
    for i, sug in enumerate(a.sugerencias, 1):
        print(f"   {i}. {sug}")
    
    print(f"\n📏 METADATA:")
    print(f"   Caracteres: {a.metadata.longitud_caracteres}")
    print(f"   Tokens est.: {a.metadata.estimacion_tokens}")
    print(f"   System prompt: {'Sí' if a.metadata.tiene_system_prompt else 'No'}")
    print()

# Casos de prueba con prompts de distintos niveles de calidad
PROMPTS_TEST = {
    "casual": "Resume esto.",
    
    "basico": "Resume el siguiente artículo en 3 puntos principales.",
    
    "engineered": """Eres un editor técnico especializado en artículos de IA.
    
Resume el siguiente artículo en exactamente 3 puntos clave.

FORMATO:
1. [Punto en máximo 20 palabras]
2. [Punto en máximo 20 palabras]
3. [Punto en máximo 20 palabras]

Sin introducción ni conclusión.""",
    
    "few_shot": """Clasifica el sentimiento del texto. 
    
Ejemplos:
- "Me encantó el producto" → POSITIVO
- "Horrible experiencia" → NEGATIVO
- "Normal, sin destacar" → NEUTRO

Texto a clasificar: [INPUT]""",
    
    "cot": """Resuelve este problema matemático paso a paso.

Piensa en voz alta mostrando cada paso del razonamiento.
Verifica tu respuesta al final.

Formato:
Paso 1: [razonamiento]
Paso 2: [razonamiento]
...
Respuesta final: [resultado]
Verificación: [comprobación]

Problema: [INPUT]""",
    
    "crispe_completo": """## Capacity
Clasificar tickets de soporte en categorías predefinidas.

## Role
Eres un clasificador automático para el sistema de soporte técnico de empresa SaaS.

## Insight
Categorías:
- TECNICO: Errores de software, fallos de funcionalidad, rendimiento
- FACTURACION: Cobros, facturas, planes, reembolsos
- GENERAL: Preguntas de uso, features, onboarding

Prioridad: TECNICO > FACTURACION > GENERAL si hay ambigüedad.

## Statement
Clasifica el siguiente ticket en exactamente UNA categoría.

## Personality
- Solo el nombre de la categoría en mayúsculas
- Sin explicación ni puntuación

## Experiment
TECNICO → Para: "Error 404 al acceder al dashboard"
FACTURACION → Para: "Mi factura tiene un cargo incorrecto"
GENERAL → Para: "¿Cómo exporto mis datos?"
"""
}

if __name__ == "__main__":
    print("🔍 PROMPT ANALYZER — Demo de casos\n")
    
    for nombre, prompt in PROMPTS_TEST.items():
        print(f"\n{'='*60}")
        print(f"CASO: {nombre.upper()}")
        try:
            analysis = analyze_prompt(prompt)
            imprimir_reporte(analysis, prompt)
        except Exception as e:
            print(f"Error analizando '{nombre}': {e}")

Paso 4: test_cases.py — Pruebas de clasificación

# test_cases.py
"""
Ejecuta el Prompt Analyzer con casos conocidos y verifica que clasifica correctamente.
No usa pytest — es validación manual.
"""
from analyzer import analyze_prompt

CASOS_CONOCIDOS = [
    {
        "nombre": "zero_shot_simple",
        "prompt": "Traduce al inglés: 'El perro come'",
        "tecnica_esperada": "zero-shot",
        "calidad_min": 10,
        "calidad_max": 50
    },
    {
        "nombre": "few_shot_con_ejemplos",
        "prompt": """Clasifica el email. 
        
SPAM: "Has ganado un premio, haz click aquí"
LEGITIMO: "Tu factura de marzo está disponible"
SPAM: "Oferta exclusiva solo por hoy!!!"

Email a clasificar: "Reunión de equipo el lunes a las 10am"
""",
        "tecnica_esperada": "few-shot",
        "calidad_min": 40,
        "calidad_max": 80
    },
    {
        "nombre": "cot_explicito",
        "prompt": """Resuelve paso a paso:

Piensa en cada paso antes de dar la respuesta final.
Muestra tu razonamiento completo.

Pregunta: Si una tienda tiene 120 productos y vende el 30%, ¿cuántos quedan?
""",
        "tecnica_esperada": "chain-of-thought",
        "calidad_min": 30,
        "calidad_max": 70
    }
]

def run_tests():
    print("🧪 Ejecutando casos de prueba...\n")
    resultados = []
    
    for caso in CASOS_CONOCIDOS:
        print(f"Caso: {caso['nombre']}")
        analysis = analyze_prompt(caso['prompt'])
        
        # Verificar técnica
        tecnica_correcta = analysis.clasificacion.tecnica == caso['tecnica_esperada']
        
        # Verificar calidad en rango
        calidad_correcta = (
            caso['calidad_min'] <= analysis.calidad.puntuacion <= caso['calidad_max']
        )
        
        estado = "✅ PASS" if (tecnica_correcta and calidad_correcta) else "❌ FAIL"
        
        print(f"  {estado}")
        print(f"  Técnica: {analysis.clasificacion.tecnica} (esperado: {caso['tecnica_esperada']}) {'✅' if tecnica_correcta else '❌'}")
        print(f"  Calidad: {analysis.calidad.puntuacion}/100 (rango: {caso['calidad_min']}-{caso['calidad_max']}) {'✅' if calidad_correcta else '❌'}")
        print(f"  CRISPE score: {analysis.crispe.score}/6\n")
        
        resultados.append(tecnica_correcta and calidad_correcta)
    
    total = len(resultados)
    pasados = sum(resultados)
    print(f"\n📊 RESULTADO FINAL: {pasados}/{total} casos pasaron")
    return pasados == total

if __name__ == "__main__":
    run_tests()

Criterios de Éxito

Al completar el proyecto, verifica que:

  • analyze_prompt("Resume esto.") devuelve técnica zero-shot, calidad casual (<50 puntos), y al menos 2 sugerencias específicas con qué añadir
  • analyze_prompt con un prompt que incluye ejemplos clasifica como few-shot
  • analyze_prompt con un prompt que pide razonamiento paso a paso clasifica como chain-of-thought
  • El CRISPE score del prompt CRISPE_completo del test es ≥5/6
  • Todos los outputs validan con Pydantic sin errores (PromptAnalysis(**data))
  • test_cases.py pasa los 3 casos conocidos
  • Las sugerencias son específicas: mencionan qué añadir exactamente, no "mejora el prompt"
  • El código ejecuta sin errores con python main.py

Ejemplo de Ejecución Completa

Input (prompt casual):

Resume este artículo en 3 puntos.

Output del Analyzer:

============================================================
PROMPT ANALIZADO: Resume este artículo en 3 puntos....
============================================================

📊 CALIDAD: BASICO (35/100)
   Instrucción clara pero sin formato ni restricciones de output

🏷️  TÉCNICA: ZERO-SHOT (confianza: 95%)
   Solo instrucción directa, sin ejemplos, sin razonamiento explícito

🧩 COMPONENTES:
   ✅ instruccion: presente
   ❌ contexto: ausente
   ⚠️ input: implicito
   ❌ output_format: ausente

🎯 CRISPE (1/6):
   ❌ capacity: implicito
   ❌ role: ausente
   ❌ insight: ausente
   ✅ statement: presente
   ❌ personality: ausente
   ❌ experiment: ausente

💡 SUGERENCIAS:
   1. Falta output_format: añade 'Formato: 1. [punto] 2. [punto] 3. [punto]'
   2. Falta Role (R): añade 'Eres un editor técnico' para calibrar vocabulario
   3. Input implícito: añade 'Artículo: [texto]' para dejar claro qué procesar
   4. Sin restricciones de longitud: añade 'máximo 20 palabras por punto'

📏 METADATA:
   Caracteres: 35
   Tokens est.: 10
   System prompt: No

Extensiones Opcionales

Extensión 1: Análisis de batch

def analyze_batch(prompts: list[str]) -> list[dict]:
    """Analiza múltiples prompts y genera reporte comparativo."""
    resultados = []
    for prompt in prompts:
        analysis = analyze_prompt(prompt)
        resultados.append({
            "preview": prompt[:50],
            "tecnica": analysis.clasificacion.tecnica,
            "calidad": analysis.calidad.puntuacion,
            "crispe_score": analysis.crispe.score,
            "sugerencias_count": len(analysis.sugerencias)
        })
    return resultados

# Uso
prompts = [p for p in PROMPTS_TEST.values()]
reporte = analyze_batch(prompts)
for r in reporte:
    print(f"{r['calidad']:3d}/100 | CRISPE {r['crispe_score']}/6 | {r['tecnica']:18} | {r['preview']}")

Extensión 2: Detección de portabilidad multi-proveedor

def analizar_portabilidad(prompt: str) -> dict:
    """
    Analiza si el prompt usa features no portables entre proveedores.
    """
    issues = []
    
    # Features específicas de OpenAI
    if "response_format" in prompt:
        issues.append("Usa 'response_format' (solo OpenAI). Para Anthropic: instrucción de JSON en texto")
    
    # Features específicas de Anthropic  
    if "<output>" in prompt or "</output>" in prompt:
        issues.append("XML tags personalizados — funcionan en todos pero son idiomáticos de Anthropic")
    
    # Bien portable
    if '"format":' in prompt.lower() or "json" in prompt.lower():
        pass  # JSON en texto es portable
    
    return {
        "es_portable": len(issues) == 0,
        "issues": issues,
        "recomendacion": "Portable" if not issues else f"Ajustar: {'; '.join(issues)}"
    }

Extensión 3: Comparar versiones de un prompt

def comparar_versiones(prompt_v1: str, prompt_v2: str) -> dict:
    """Compara dos versiones del mismo prompt y muestra qué mejoró."""
    a1 = analyze_prompt(prompt_v1)
    a2 = analyze_prompt(prompt_v2)
    
    return {
        "calidad_delta": a2.calidad.puntuacion - a1.calidad.puntuacion,
        "crispe_delta": a2.crispe.score - a1.crispe.score,
        "tecnica_cambio": f"{a1.clasificacion.tecnica}{a2.clasificacion.tecnica}",
        "mejoras": [
            s for s in a1.sugerencias
            if not any(comp in str(a2.componentes) for comp in ["presente"])
        ]
    }

Troubleshooting

Problema 1: El modelo no devuelve JSON válido

Causa: A veces incluye texto antes del JSON ("Aquí está el análisis:") o usa json ... .

Solución: La función limpiar_json() ya maneja esto. Si aún falla:

# Añadir al system prompt
ANALYZER_SYSTEM += """
CRÍTICO: Tu respuesta debe empezar DIRECTAMENTE con { y terminar con }.
Sin "```json", sin texto introductorio, sin explicaciones.
"""

Problema 2: Sugerencias genéricas ("mejora el prompt")

Causa: El system prompt no especifica con suficiente claridad qué es una sugerencia específica.

Solución: Añadir más ejemplos negativos al system prompt:

# Añadir al ANALYZER_SYSTEM
"""
SUGERENCIAS: Deben mencionar QUÉ AÑADIR EXACTAMENTE con un ejemplo de texto a agregar.
- ❌ "Añade más contexto"
- ✅ "Falta Insight: añade 'Prioridades: urgente > normal > bajo' para guiar la clasificación"
- ❌ "Especifica el formato"
- ✅ "Falta Experiment: añade 'Responde en JSON: {\"categoria\": \"TECNICO|FACTURACION|GENERAL\"}'"
"""

Problema 3: Classificación incorrecta few-shot vs zero-shot

Causa: El modelo confunde ejemplos en el contexto con few-shot real.

Solución: Añadir definición más precisa al system prompt:

"""
CRÍTICO para clasificación:
- zero-shot: SOLO instrucción. Sin ejemplos input→output antes de la tarea.
- few-shot: Prompt INCLUYE ejemplos en formato "Input: X → Output: Y" o similar, ANTES de pedir la clasificación del caso actual.
- chain-of-thought: Prompt INCLUYE o SOLICITA explícitamente razonamiento paso a paso ("piensa paso a paso", "muestra tu razonamiento", etc.)
"""

Problema 4: Error de validación Pydantic

Causa: El JSON del modelo no cumple con los tipos del schema (ej: confianza fuera de 0-1, score fuera de 0-6).

Solución: Añadir sanitización antes de validar:

def sanitizar_data(data: dict) -> dict:
    """Corrige valores fuera de rango antes de validar con Pydantic."""
    if "clasificacion" in data:
        conf = data["clasificacion"].get("confianza", 0.5)
        data["clasificacion"]["confianza"] = max(0.0, min(1.0, float(conf)))
    
    if "crispe" in data:
        score = data["crispe"].get("score", 0)
        data["crispe"]["score"] = max(0, min(6, int(score)))
    
    if "calidad" in data:
        pts = data["calidad"].get("puntuacion", 0)
        data["calidad"]["puntuacion"] = max(0, min(100, int(pts)))
    
    return data

# En analyze_prompt():
data = json.loads(json_str)
data = sanitizar_data(data)
return PromptAnalysis(**data)

Problema 5: tiktoken no reconoce el modelo

Causa: Si usas un modelo nuevo que tiktoken aún no tiene en su registro.

Solución: La función estimar_tokens ya tiene fallback. Si quieres más precisión:

def estimar_tokens(texto: str, model: str = "gpt-4o-mini") -> int:
    try:
        encoding = tiktoken.encoding_for_model(model)
        return len(encoding.encode(texto))
    except KeyError:
        # Fallback: usar codificación cl100k_base (GPT-4 family)
        encoding = tiktoken.get_encoding("cl100k_base")
        return len(encoding.encode(texto))

Resumen del Proyecto

En este proyecto construiste:

  • Schema Pydantic completo con validación de tipos, rangos, y literales — base para todo output estructurado de la guía
  • Sistema de análisis con LLM que usa un model bien instruido para analizar otro prompt — técnica de LLM-as-judge que verás en detalle en el Módulo 7
  • Parser robusto de JSON que maneja los formatos variables que los modelos pueden devolver
  • Framework de casos de prueba sin pytest, pero con verificación sistemática de resultados esperados
  • Detectores heurísticos (system prompt, portabilidad) que complementan el análisis del LLM con lógica determinística

Cómo usarás este Analyzer en los módulos siguientes:

  • Módulo 2: Verificar que tus prompts few-shot tienen el formato correcto en "experiment"
  • Módulo 4: Verificar que tu CoT tiene "statement" con instrucción de razonamiento paso a paso
  • Módulo 6: Verificar que cada prompt en una cadena tiene sus componentes bien definidos
  • Módulo 7: El Prompt Analyzer es un precursor del evaluation framework con LLM-as-judge

Recursos adicionales

  1. OpenAI Structured Outputs — JSON mode y JSON Schema en OpenAI, base para el response_format que usas en el extractor
  2. Pydantic v2 Validators — Validators avanzados para el schema del Analyzer (útil si añades más validaciones)
  3. tiktoken GitHub — Librería para contar tokens exactamente; crucial para optimizar costos en producción
  4. OpenAI Evals — Framework para evaluar prompts a escala; lo que construiste manualmente aquí, pero con automatización
  5. Pydantic BaseModel — Documentación de BaseModel con todos los field types y validadores disponibles