
Proyecto PINSAPIA — Línea von Neumann: “Dados Cargados: Temperatura y Muestreo”
¿Sabe un Modelo de Lenguaje (LLM) cuál es la “única respuesta correcta” cuando le hacemos una pregunta? La respuesta corta es no. Un LLM no es un motor de búsqueda que recupera un texto estático; es una máquina estadística que calcula probabilidades para predecir cuál debe ser el siguiente token. En cada paso de la generación, el modelo “tira unos dados”.
En esta nueva entrega del Proyecto PINSAPIA, inauguramos la Línea von Neumann, bautizada en honor a Klara von Neumann, pionera de la programación en el ENIAC y cofundadora de los métodos Monte Carlo: algoritmos de simulación que utilizan la aleatoriedad controlada para resolver problemas complejos.
Mediante nuestra aplicación interactiva (ejecutada localmente en el puerto 8089), el alumnado puede visualizar cómo el parámetro Temperatura ($T$) altera el comportamiento de la IA, convirtiéndola en una calculadora rígida o en un generador de ideas impredecible.
🎯 Guía de Exploración Práctica para el Alumnado: ¡A Jugar con los Dados de la IA!
Esta guía está preparada para que el alumnado experimente en el aula, analice las variaciones en las respuestas y extraiga sus propias conclusiones sobre el funcionamiento interno del muestreo probabilístico.
Objetivo del Laboratorio
Comprender que las respuestas de una IA no están grabadas en piedra, sino que dependen de una distribución de probabilidad. A través de la interfaz web, analizaremos cómo la Temperatura ($T$) incrementa o reduce la diversidad de los resultados generados en $N$ tiradas independientes.
🧪 Misiones y Retos Prácticos
Reto 1: La Máquina Determinista ($T = 0.0$)
- Selecciona la Opción 1: Tarea determinista (matemáticas) (
¿Cuánto es 15 multiplicado por 7?). - Asegúrate de que el slider de Temperatura esté en 0.0.
- Selecciona 5 Tiradas y pulsa 🎲 LANZAR LOS DADOS N VECES.
- Observa las tarjetas generadas y el Índice de Diversidad.
Pregunta de análisis: Si $T = 0.0$, ¿han salido respuestas distintas o son exactamente iguales? ¿Por qué para una tarea con una sola respuesta lógica necesitamos que la temperatura sea cero?
Reto 2: Lluvia de Ideas y Creatividad ($T = 1.1$)
- Pulsa sobre el preset Lluvia de ideas creativa (
Dame un nombre original para una mascota robot de instituto). - El slider de Temperatura se ajustará automáticamente a 1.1 (Rango rojo: Alta aleatoriedad).
- Selecciona 5 o 7 Tiradas y lanza el experimento.
- Revisa las tarjetas con bordes de colores e inspecciona la métrica 1ª Palabra Diversidad.
Pregunta de análisis: ¿Cuántas respuestas distintas han salido de las 7 tiradas? ¿Varía únicamente la primera palabra o cambia la estructura completa de la frase?
Reto 3: El Botón de Contraste “⚡ Probar a Temperatura 0.0”
- Con la misma frase de la mascota robot aún en pantalla, haz clic en el botón verde ⚡ PROBAR A TEMPERATURA 0.0.
- Compara el nuevo Índice de Diversidad ($I_D = \frac{\text{Respuestas Únicas}}{\text{Respuestas Totales}}$) con el resultado del Reto 2.
- Observa el gráfico de barras inferior sobre la dispersión de longitud.
Pregunta de análisis: ¿Qué ha ocurrido al forzar $T = 0.0$ en una tarea creativa? ¿Es la respuesta devuelta mala o simplemente repetitiva?
Reto 4: El Límite del Caos ($T = 1.5$)
- Selecciona el preset Frase ambigua a propósito (
Continúa la frase: El mejor día de mi vida fue...). - Eleva manualmente la Temperatura a 1.5 (el máximo en el slider).
- Ejecuta 7 Tiradas.
Pregunta de análisis: Al subir la temperatura al máximo, ¿las respuestas siguen teniendo sentido gramatical o empiezan a volverse incoherentes? ¿Dónde está el límite entre “creatividad” y “caos”?


📋 Ficha de Reflexión y Debate
| Pregunta de Reflexión | Tu Análisis |
| 1. Índice de Diversidad ($I_D$): Si en 5 tiradas obtienes 5 respuestas distintas, ¿cuál es tu $I_D$? ¿Qué significa un $I_D = 0.20$? | |
| 2. Primeras Palabras: ¿Por qué la primera palabra generada suele condicionar el resto de la frase producida por la IA? | |
| 3. Caso de Uso: Si estuvieras diseñando un bot asistente para un banco que informa sobre saldos de cuentas, ¿qué temperatura le asignarías? |
🛠️ Arquitectura Técnica del Proyecto (Puerto 8089)
Para construir un experimento científicamente honesto y fácil de entender sin necesidad de librerías avanzadas de NLP (como embeddings o similitud coseno), hemos tomado las siguientes decisiones de diseño:
- Muestreo Secuencial Integrado: Las $N$ peticiones se ejecutan de forma secuencial. Esto permite ofrecer una barra de progreso real en el navegador (
Generando tirada 3 de 7...) y simular el coste temporal exacto de la inferencia local en hardware estándar. - Medición Intuitiva de Diversidad:
- Índice de Diversidad ($I_D$): Calculado como $I_D = \frac{\text{Respuestas Únicas}}{\text{Respuestas Totales}}$, fluctuando entre $0.0$ (todas idénticas) y $1.0$ (todas diferentes).
- Agrupación Visual de Duplicados: Las respuestas exactamente iguales comparten el mismo color de borde en las tarjetas.
- Diversidad de la Primera Palabra: Muestra cómo divergen las ramas del árbol de búsqueda probabilístico desde el primer token.
- Métricas Estadísticas: Desviación típica y longitud media en caracteres graficadas dinámicamente con Chart.js.
💻 Código Fuente Completo (app_vonneumann.py)
Crea un archivo llamado app_vonneumann.py e introduce el siguiente código:
Python
import json
import re
import statistics
import time
import urllib.request
from typing import Any, Dict, List, Optional
from fastapi import FastAPI, HTTPException
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
import uvicorn
app = FastAPI(
title="Proyecto PINSAPIA - Línea von Neumann",
description="Simulación Monte Carlo de Muestreo y Temperatura en LLMs",
version="1.0.0",
)
# -----------------------------------------------------------------------------
# 1. PRESETS PEDAGÓGICOS
# -----------------------------------------------------------------------------
PRESETS = [
{
"id": "preset_1",
"categoria": "Tarea determinista (matemáticas)",
"prompt": "¿Cuánto es 15 multiplicado por 7? Responde solo con el número.",
"temperatura_sugerida": 0.0,
},
{
"id": "preset_2",
"categoria": "Lluvia de ideas creativa",
"prompt": "Dame un nombre original para una mascota robot de instituto",
"temperatura_sugerida": 1.1,
},
{
"id": "preset_3",
"categoria": "Resumen (¿debe variar?)",
"prompt": "Resume en una frase: la fotosíntesis es el proceso por el cual las plantas convierten la luz solar en energía química",
"temperatura_sugerida": 0.3,
},
{
"id": "preset_4",
"categoria": "Frase ambigua a propósito",
"prompt": "Continúa la frase: El mejor día de mi vida fue...",
"temperatura_sugerida": 0.9,
},
]
# -----------------------------------------------------------------------------
# 2. MODELOS DE DATOS (PYDANTIC)
# -----------------------------------------------------------------------------
class GenerarUnaRequest(BaseModel):
prompt: str
temperatura: float = 0.7
class CalcularEstadisticasRequest(BaseModel):
prompt: str
temperatura: float
respuestas: List[Dict[str, Any]]
# -----------------------------------------------------------------------------
# 3. FUNCIONES DE INFERENCIA Y ESTADÍSTICA
# -----------------------------------------------------------------------------
def generar_respuesta_lmstudio(prompt: str, temperatura: float, timeout_sec: int = 30) -> Dict[str, Any]:
inicio = time.perf_counter()
url = "http://localhost:1234/v1/chat/completions"
payload = {
"messages": [{"role": "user", "content": prompt}],
"temperature": float(temperatura),
"top_p": 1.0,
"max_tokens": 120
}
try:
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=timeout_sec) as response:
res_body = json.loads(response.read().decode("utf-8"))
texto = res_body["choices"][0]["message"]["content"].strip()
tiempo_ms = round((time.perf_counter() - inicio) * 1000, 2)
palabras = re.findall(r'\w+', texto)
primera = palabras[0] if palabras else (texto[:10] if texto else "N/A")
return {
"ok": True,
"texto": texto,
"num_caracteres": len(texto),
"tiempo_ms": tiempo_ms,
"primera_palabra": primera,
"error": None
}
except Exception as e:
tiempo_ms = round((time.perf_counter() - inicio) * 1000, 2)
return {
"ok": False,
"texto": f"⚠️ Error de conexión con LM Studio: {str(e)}",
"num_caracteres": 0,
"tiempo_ms": tiempo_ms,
"primera_palabra": "ERROR",
"error": str(e)
}
def procesar_estadisticas(prompt: str, temperatura: float, respuestas: List[Dict[str, Any]]) -> Dict[str, Any]:
n_totales = len(respuestas)
if n_totales == 0:
return {}
mapa_grupos = {}
grupo_id_counter = 0
colores_grupos = [
"#6366F1", "#10B981", "#F59E0B", "#EC4899",
"#8B5CF6", "#06B6D4", "#F97316", "#14B8A6"
]
respuestas_procesadas = []
longitudes = []
primeras_palabras = []
for idx, item in enumerate(respuestas, 1):
txt = item.get("texto", "").strip()
norm_txt = re.sub(r'\s+', ' ', txt).lower()
if norm_txt not in mapa_grupos:
mapa_grupos[norm_txt] = {
"grupo_id": grupo_id_counter,
"color": colores_grupos[grupo_id_counter % len(colores_grupos)]
}
grupo_id_counter += 1
grp_info = mapa_grupos[norm_txt]
n_chars = item.get("num_caracteres", len(txt))
p_palabra = item.get("primera_palabra", "N/A")
if item.get("ok", True):
longitudes.append(n_chars)
primeras_palabras.append(p_palabra)
respuestas_procesadas.append({
"indice": idx,
"texto": txt,
"num_caracteres": n_chars,
"tiempo_ms": item.get("tiempo_ms", 0),
"primera_palabra": p_palabra,
"grupo_id": grp_info["grupo_id"],
"color_borde": grp_info["color"],
"ok": item.get("ok", True)
})
respuestas_unicas = len(mapa_grupos)
indice_div = round(respuestas_unicas / n_totales, 2)
long_media = round(statistics.mean(longitudes), 1) if longitudes else 0.0
long_desv = round(statistics.stdev(longitudes), 2) if len(longitudes) > 1 else 0.0
p_palabras_unicas = len(set(primeras_palabras))
tiempo_total = sum(r["tiempo_ms"] for r in respuestas_procesadas)
return {
"prompt": prompt,
"temperatura": temperatura,
"n_repeticiones": n_totales,
"respuestas": respuestas_procesadas,
"estadisticas": {
"respuestas_unicas": respuestas_unicas,
"respuestas_totales": n_totales,
"indice_diversidad": indice_div,
"longitud_media": long_media,
"longitud_desviacion": long_desv,
"primeras_palabras_unicas": p_palabras_unicas,
"tiempo_total_ms": round(tiempo_total, 2)
}
}
# -----------------------------------------------------------------------------
# 4. ENDPOINTS DE LA API
# -----------------------------------------------------------------------------
@app.get("/api/presets")
def get_presets():
return PRESETS
@app.post("/api/generar_una")
def api_generar_una(req: GenerarUnaRequest):
if not req.prompt.strip():
raise HTTPException(status_code=400, detail="El prompt no puede estar vacío.")
return generar_respuesta_lmstudio(req.prompt, req.temperatura)
@app.post("/api/calcular_estadisticas")
def api_calcular_estadisticas(req: CalcularEstadisticasRequest):
return procesar_estadisticas(req.prompt, req.temperatura, req.respuestas)
# -----------------------------------------------------------------------------
# 5. FRONTEND HTML EMBEDDED
# -----------------------------------------------------------------------------
@app.get("/", response_class=HTMLResponse)
def index():
return HTML_CONTENT
HTML_CONTENT = """<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PINSAPIA - Línea von Neumann: Temperatura y Muestreo</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body class="bg-slate-100 font-sans min-h-screen pb-12">
<div class="max-w-7xl mx-auto px-4 py-6">
<header class="bg-slate-900 text-white rounded-2xl p-6 shadow-xl border border-slate-800 mb-6">
<div class="flex items-center justify-between">
<div>
<span class="text-xs font-bold uppercase tracking-widest text-indigo-400 bg-indigo-950/60 px-3 py-1 rounded-full border border-indigo-800">
PROYECTO PINSAPIA - LÍNEA VON NEUMANN
</span>
<h1 class="text-2xl md:text-3xl font-extrabold mt-2 flex items-center gap-3">
<span>🎲 Dados Cargados: Temperatura y Muestreo Monte Carlo</span>
</h1>
<p class="text-slate-400 text-xs mt-1">Homenaje a Klara von Neumann: simulaciones de aleatoriedad controlada en la inferencia de LLMs</p>
</div>
<i class="fa-solid fa-dice-five text-indigo-400 text-5xl opacity-80 hidden sm:block"></i>
</div>
</header>
<section class="bg-white border border-slate-200 rounded-xl p-4 mb-6 shadow-sm">
<div class="font-bold text-slate-800 text-sm mb-3 flex items-center gap-2">
<i class="fa-solid fa-wand-magic-sparkles text-indigo-600"></i>
PRESETS DE EXPERIMENTACIÓN
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3" id="presets-container"></div>
</section>
<section class="bg-white border border-slate-200 rounded-2xl p-6 shadow-sm mb-6">
<div class="mb-4">
<label class="block font-bold text-slate-800 text-sm mb-2">PROMPT DE ENTRADA:</label>
<textarea id="prompt-input" class="w-full border border-slate-300 rounded-xl p-3 text-slate-800 font-mono text-sm focus:ring-2 focus:ring-indigo-500 focus:outline-none min-h-[80px]" placeholder="Escribe tu instrucción o pregunta..."></textarea>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<div class="bg-slate-50 p-4 rounded-xl border border-slate-200">
<div class="flex justify-between items-center mb-2">
<label class="font-bold text-slate-700 text-xs uppercase">Temperatura (T):</label>
<span id="temp-val" class="font-mono font-extrabold text-lg text-indigo-600">0.7</span>
</div>
<input type="range" id="temp-range" min="0.0" max="1.5" step="0.1" value="0.7" oninput="actualizarTemperaturaUI(this.value)" class="w-full h-2 bg-slate-300 rounded-lg appearance-none cursor-pointer accent-indigo-600">
<div class="mt-2 flex justify-between items-center">
<span id="temp-badge" class="px-2.5 py-0.5 rounded text-xs font-bold text-white bg-amber-500">Equilibrado</span>
<span class="text-[11px] text-slate-500">Rango: 0.0 (fijo) a 1.5 (creativo/caótico)</span>
</div>
</div>
<div class="bg-slate-50 p-4 rounded-xl border border-slate-200 flex flex-col justify-between">
<div>
<label class="block font-bold text-slate-700 text-xs uppercase mb-2">Nº de Tiradas (Muestras N):</label>
<select id="n-repeticiones" class="w-full border border-slate-300 rounded-lg p-2 font-mono text-sm bg-white focus:ring-2 focus:ring-indigo-500 focus:outline-none">
<option value="3">3 Tiradas</option>
<option value="5" selected>5 Tiradas</option>
<option value="7">7 Tiradas</option>
<option value="10">10 Tiradas (Máximo)</option>
</select>
</div>
<p class="text-[11px] text-slate-500 mt-2">Ejecución secuencial para medir dispersión real.</p>
</div>
</div>
<div class="flex flex-col sm:flex-row gap-3">
<button id="btn-lanzar" onclick="ejecutarExperimento()" class="flex-1 bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-3.5 px-6 rounded-xl shadow-md transition flex items-center justify-center gap-2">
<i class="fa-solid fa-dice"></i> 🎲 LANZAR LOS DADOS N VECES
</button>
<button id="btn-comparar" onclick="ejecutarComparativaTZero()" class="bg-teal-600 hover:bg-teal-700 text-white font-bold py-3.5 px-6 rounded-xl shadow-md transition flex items-center justify-center gap-2">
<i class="fa-solid fa-bolt"></i> ⚡ PROBAR A TEMPERATURA 0.0
</button>
</div>
<div id="progress-container" class="hidden mt-4">
<div class="flex justify-between text-xs font-bold text-slate-600 mb-1">
<span id="progress-text">Iniciando inferencias...</span>
<span id="progress-percent">0%</span>
</div>
<div class="w-full bg-slate-200 rounded-full h-3 overflow-hidden">
<div id="progress-bar" class="bg-indigo-600 h-3 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
</div>
</section>
<div id="results-section" class="hidden space-y-6">
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div class="bg-white p-4 rounded-xl border-2 border-indigo-200 shadow-sm">
<div class="text-xs text-slate-500 font-bold uppercase">ÍNDICE DIVERSIDAD</div>
<div id="metric-div-index" class="text-2xl font-black text-indigo-600 font-mono mt-1">0.00</div>
<div id="metric-div-sub" class="text-[11px] text-slate-500 mt-0.5">0 respuestas únicas</div>
</div>
<div class="bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
<div class="text-xs text-slate-500 font-bold uppercase">LONGITUD MEDIA</div>
<div id="metric-len-mean" class="text-2xl font-black text-slate-800 font-mono mt-1">0.0</div>
<div id="metric-len-sd" class="text-[11px] text-slate-500 mt-0.5">Desviación: ±0.0 chars</div>
</div>
<div class="bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
<div class="text-xs text-slate-500 font-bold uppercase">1ª PALABRA DIVERSIDAD</div>
<div id="metric-first-word" class="text-2xl font-black text-emerald-600 font-mono mt-1">0</div>
<div class="text-[11px] text-slate-500 mt-0.5">primeras palabras distintas</div>
</div>
<div class="bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
<div class="text-xs text-slate-500 font-bold uppercase">TIEMPO TOTAL ACUMULADO</div>
<div id="metric-total-time" class="text-2xl font-black text-amber-600 font-mono mt-1">0 ms</div>
<div id="metric-avg-time" class="text-[11px] text-slate-500 mt-0.5">0 ms / respuesta</div>
</div>
</div>
<div>
<h3 class="font-bold text-slate-800 text-sm uppercase mb-3 flex items-center gap-2">
<i class="fa-solid fa-list-check text-indigo-600"></i>
TIRADAS GENERADAS (Bordes idénticos indican respuestas repetidas)
</h3>
<div id="cards-container" class="grid grid-cols-1 md:grid-cols-2 gap-4"></div>
</div>
<div class="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm">
<h3 class="font-bold text-slate-800 text-xs uppercase mb-4 flex items-center gap-2">
<i class="fa-solid fa-chart-column text-indigo-600"></i>
DISPERSIÓN DE LONGITUD DE CADA RESPUESTA (CARACTERES)
</h3>
<div class="h-56 relative">
<canvas id="chart-longitudes"></canvas>
</div>
</div>
</div>
<section class="bg-slate-900 text-slate-200 rounded-2xl p-6 shadow-xl border border-slate-800 mt-6">
<h3 class="text-xs font-bold uppercase tracking-widest text-indigo-400 mb-3 flex items-center gap-2">
<i class="fa-solid fa-code"></i>
PANEL DE TRAZABILIDAD Y REGISTRO RAW
</h3>
<div class="p-3 bg-slate-800 rounded-lg border border-slate-700 text-xs text-slate-300 mb-4 flex items-center gap-2">
<i class="fa-solid fa-triangle-exclamation text-amber-400"></i>
<span>La velocidad depende del hardware local. En CPU sin GPU, cada tirada secuencial requiere inferencia completa.</span>
</div>
<details class="cursor-pointer">
<summary class="text-xs text-slate-300 font-bold hover:text-indigo-400 transition mb-2">
▼ Ver objeto JSON crudo devuelto por el servidor
</summary>
<pre id="raw-json" class="p-4 bg-slate-950 text-indigo-300 rounded-xl text-xs font-mono overflow-x-auto max-h-80 border border-slate-800 whitespace-pre-wrap break-all">{}</pre>
</details>
</section>
</div>
<script>
let chartInstance = null;
document.addEventListener("DOMContentLoaded", cargarPresets);
async function cargarPresets() {
try {
const res = await fetch("/api/presets");
const presets = await res.json();
const container = document.getElementById("presets-container");
container.innerHTML = "";
presets.forEach(p => {
const card = document.createElement("div");
card.className = "bg-slate-50 p-3 rounded-lg border border-slate-200 hover:border-indigo-500 cursor-pointer transition flex flex-col justify-between";
card.onclick = () => {
document.getElementById("prompt-input").value = p.prompt;
actualizarTemperaturaUI(p.temperatura_sugerida);
};
card.innerHTML = `
<div>
<span class="text-[10px] font-bold uppercase tracking-wider text-indigo-600 bg-indigo-50 px-2 py-0.5 rounded">${p.categoria}</span>
<div class="font-medium text-xs text-slate-800 mt-2 line-clamp-2">"${p.prompt}"</div>
</div>
<div class="mt-2 text-[10px] text-slate-500 font-mono text-right">T sugerida: <strong>${p.temperatura_sugerida}</strong></div>
`;
container.appendChild(card);
});
} catch (e) { console.error(e); }
}
function actualizarTemperaturaUI(val) {
val = parseFloat(val).toFixed(1);
document.getElementById("temp-range").value = val;
document.getElementById("temp-val").innerText = val;
const badge = document.getElementById("temp-badge");
if (val <= 0.3) {
badge.className = "px-2.5 py-0.5 rounded text-xs font-bold text-white bg-emerald-600";
badge.innerText = "Determinista / Predecible";
} else if (val <= 0.8) {
badge.className = "px-2.5 py-0.5 rounded text-xs font-bold text-white bg-amber-500";
badge.innerText = "Equilibrado";
} else {
badge.className = "px-2.5 py-0.5 rounded text-xs font-bold text-white bg-rose-600";
badge.innerText = "Alta aleatoriedad / Creativo";
}
}
async function ejecutarExperimento() {
const prompt = document.getElementById("prompt-input").value.trim();
const temp = parseFloat(document.getElementById("temp-range").value);
const n = parseInt(document.getElementById("n-repeticiones").value);
if (!prompt) {
alert("Por favor, introduce un prompt.");
return;
}
prepararUIEjecucion(n);
let respuestasAcumuladas = [];
for (let i = 1; i <= n; i++) {
actualizarProgreso(i, n);
try {
const res = await fetch("/api/generar_una", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt: prompt, temperatura: temp })
});
const data = await res.json();
respuestasAcumuladas.push(data);
} catch (err) {
respuestasAcumuladas.push({
ok: false,
texto: "⚠️ Error al conectar con LM Studio",
num_caracteres: 0,
tiempo_ms: 0,
primera_palabra: "ERROR"
});
}
}
finalizarExperimento(prompt, temp, respuestasAcumuladas);
}
function ejecutarComparativaTZero() {
actualizarTemperaturaUI(0.0);
ejecutarExperimento();
}
function prepararUIEjecucion(n) {
document.getElementById("btn-lanzar").disabled = true;
document.getElementById("btn-comparar").disabled = true;
document.getElementById("progress-container").classList.remove("hidden");
document.getElementById("results-section").classList.add("hidden");
document.getElementById("cards-container").innerHTML = "";
}
function actualizarProgreso(actual, total) {
const pct = Math.round((actual / total) * 100);
document.getElementById("progress-bar").style.width = pct + "%";
document.getElementById("progress-percent").innerText = pct + "%";
document.getElementById("progress-text").innerText = `Generando tirada ${actual} de ${total}...`;
}
async function finalizarExperimento(prompt, temp, respuestasBrutas) {
try {
const res = await fetch("/api/calcular_estadisticas", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
prompt: prompt,
temperatura: temp,
respuestas: respuestasBrutas
})
});
const data = await res.json();
renderizarResultados(data);
} catch (err) {
console.error("Error procesando estadísticas:", err);
} finally {
document.getElementById("btn-lanzar").disabled = false;
document.getElementById("btn-comparar").disabled = false;
document.getElementById("progress-container").classList.add("hidden");
}
}
function renderizarResultados(data) {
document.getElementById("results-section").classList.remove("hidden");
document.getElementById("raw-json").textContent = JSON.stringify(data, null, 2);
const st = data.estadisticas;
document.getElementById("metric-div-index").innerText = st.indice_diversidad.toFixed(2);
document.getElementById("metric-div-sub").innerText = `${st.respuestas_unicas} de ${st.respuestas_totales} respuestas distintas`;
document.getElementById("metric-len-mean").innerText = `${st.longitud_media} chars`;
document.getElementById("metric-len-sd").innerText = `Desviación: ±${st.longitud_desviacion} chars`;
document.getElementById("metric-first-word").innerText = st.primeras_palabras_unicas;
document.getElementById("metric-total-time").innerText = `${st.tiempo_total_ms.toFixed(0)} ms`;
const avgTime = (st.tiempo_total_ms / st.respuestas_totales).toFixed(0);
document.getElementById("metric-avg-time").innerText = `${avgTime} ms / respuesta`;
const cardsBox = document.getElementById("cards-container");
cardsBox.innerHTML = "";
data.respuestas.forEach(r => {
const card = document.createElement("div");
card.className = "bg-white p-4 rounded-xl border-l-8 shadow-sm flex flex-col justify-between";
card.style.borderLeftColor = r.color_borde;
card.innerHTML = `
<div>
<div class="flex justify-between items-center mb-2">
<span class="text-xs font-bold text-slate-500 uppercase">TIRADA #${r.indice}</span>
<span class="text-[10px] font-mono bg-slate-100 text-slate-600 px-2 py-0.5 rounded border border-slate-200">1ª Palabra: "${r.primera_palabra}"</span>
</div>
<p class="text-slate-800 text-sm font-mono whitespace-pre-wrap leading-relaxed">${escapeHtml(r.texto)}</p>
</div>
<div class="mt-4 pt-2 border-t border-slate-100 flex justify-between text-[11px] font-mono text-slate-500">
<span>Longitud: <strong>${r.num_caracteres}</strong> chars</span>
<span>Tiempo: <strong>${r.tiempo_ms}</strong> ms</span>
</div>
`;
cardsBox.appendChild(card);
});
renderizarGrafico(data.respuestas);
}
function renderizarGrafico(respuestas) {
const ctx = document.getElementById("chart-longitudes").getContext("2d");
if (chartInstance) chartInstance.destroy();
const labels = respuestas.map(r => `Tirada ${r.indice}`);
const dataLengths = respuestas.map(r => r.num_caracteres);
const backgroundColors = respuestas.map(r => r.color_borde);
chartInstance = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Número de Caracteres',
data: dataLengths,
backgroundColor: backgroundColors,
borderRadius: 6
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
y: { beginAtZero: true, title: { display: true, text: 'Caracteres' } }
}
}
});
}
function escapeHtml(str) {
return String(str || '').replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
}
</script>
</body>
</html>
"""
if __name__ == "__main__":
uvicorn.run("app_vonneumann:app", host="0.0.0.0", port=8089, reload=True)
🚀 Instrucciones de Ejecución
- Asegúrate de tener instalado LM Studio con un modelo cargado y el servidor local activado en el puerto
1234. - Instala las dependencias necesarias en tu entorno de Python:
Bashpip install fastapi uvicorn pydantic - Ejecuta la aplicación desde la terminal utilizando Uvicorn:
Bashuvicorn app_vonneumann:app --host 0.0.0.0 --port 8089 --reload - Abre tu navegador e ingresa a:
http://localhost:8089
Si necesitas que la IA te dé siempre la misma respuesta exacta a la misma pregunta (por ejemplo, para corregir exámenes automáticamente), ¿qué temperatura usarías? ¿Y si quieres que te ayude a hacer una lluvia de ideas creativas? ¿Hay algún riesgo en usar temperatura muy alta para tareas donde la precisión importa, como resumir un texto legal?



