
Proyecto IA Monterroso (III): una página para preguntar, sin depender de la terminal
En la entrada anterior montamos nuestra primera API propia con FastAPI, que hace de puente entre quien pregunta y el modelo cargado en LM Studio. Funcionaba, pero para probarla había que usar la página de documentación automática de FastAPI (/docs) o la terminal con curl. Perfectamente válido para nosotros como desarrolladores, pero no es algo que le pongas delante a un alumno.
Tocaba el siguiente escalón lógico de la arquitectura: una interfaz sencilla, de verdad pensada para escribir una pregunta y ver la respuesta, sin tecnicismos de por medio.
Qué vamos a hacer (y qué no, todavía)
Aquí conviene separar dos cosas que a primera vista parecen la misma pero no lo son:
- Una página bonita para preguntar y recibir respuesta → esto es lo que hacemos hoy. Sencillo, sin riesgo, no cambia nada de lo que ya teníamos montado.
- Que el alumnado acceda desde sus propios dispositivos, en red → eso es un paso distinto y más delicado, que dejamos para más adelante, a propósito. Con el hardware actual (sin GPU, ~7.4 tokens por segundo) varias preguntas a la vez formarían cola: la tercera persona podría esperar más de un minuto. Y abrir el acceso, aunque sea solo dentro de la red del instituto, es una decisión de seguridad que merece su propio análisis, no algo que se cuele de rebote al hacer una interfaz.
Así que la página de hoy la seguimos probando solo en el propio ordenador (ROCKY), en localhost. El acceso multiusuario y en red queda pendiente, como paso deliberado, para cuando el proyecto lo requiera.
Solución recomendada
Nada de frameworks de frontend ni build steps: una única página HTML con un poco de JavaScript, servida directamente por la misma API de FastAPI que ya teníamos. Cero infraestructura nueva, cero dependencias adicionales.
Código
Sustituye el contenido de tu archivo ia-monterroso-api/main.py por esta versión completa (incluye la API de antes más la página nueva):
python
from fastapi import FastAPI, HTTPException
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
import requests
# --- Configuración ---
LM_STUDIO_URL = "http://localhost:1234/v1/chat/completions"
MODEL_NAME = "llama-3.2-3b-instruct"
TIMEOUT_SEGUNDOS = 120
app = FastAPI(title="IA Monterroso - API")
class Pregunta(BaseModel):
pregunta: str
class Respuesta(BaseModel):
respuesta: str
@app.post("/preguntar", response_model=Respuesta)
def preguntar(datos: Pregunta):
cuerpo_peticion = {
"model": MODEL_NAME,
"messages": [{"role": "user", "content": datos.pregunta}],
"temperature": 0.7,
}
try:
respuesta_lm_studio = requests.post(
LM_STUDIO_URL, json=cuerpo_peticion, timeout=TIMEOUT_SEGUNDOS
)
respuesta_lm_studio.raise_for_status()
except requests.exceptions.ConnectionError:
raise HTTPException(
status_code=503,
detail="No se puede conectar con LM Studio. ¿Está abierto y con el servidor activado?",
)
except requests.exceptions.Timeout:
raise HTTPException(status_code=504, detail="El modelo ha tardado demasiado en responder.")
datos_respuesta = respuesta_lm_studio.json()
texto_generado = datos_respuesta["choices"][0]["message"]["content"]
return Respuesta(respuesta=texto_generado)
@app.get("/salud")
def salud():
return {"estado": "ok"}
PAGINA_HTML = """
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>IA Monterroso</title>
<style>
body { font-family: system-ui, sans-serif; background: #f4f4f5; margin: 0;
display: flex; justify-content: center; padding: 40px 16px; }
.tarjeta { background: white; border-radius: 12px; padding: 24px 28px;
max-width: 560px; width: 100%; box-shadow: 0 1px 4px rgba(0,0,0,0.1); }
h1 { font-size: 1.3rem; margin-top: 0; }
textarea { width: 100%; box-sizing: border-box; min-height: 80px; font-size: 1rem;
padding: 10px; border-radius: 8px; border: 1px solid #ccc; resize: vertical; }
button { margin-top: 10px; padding: 10px 18px; font-size: 1rem; border: none;
border-radius: 8px; background: #4f46e5; color: white; cursor: pointer; }
button:disabled { background: #a5a5a5; cursor: not-allowed; }
#respuesta { margin-top: 18px; white-space: pre-wrap; line-height: 1.5; }
.aviso { font-size: 0.85rem; color: #666; margin-top: 6px; }
</style>
</head>
<body>
<div class="tarjeta">
<h1>IA Monterroso (prototipo)</h1>
<p class="aviso">Versión de pruebas. Puede tardar varios segundos en responder
y puede equivocarse en datos concretos.</p>
<textarea id="pregunta" placeholder="Escribe tu pregunta..."></textarea>
<br>
<button id="boton" onclick="preguntar()">Preguntar</button>
<div id="respuesta"></div>
</div>
<script>
async function preguntar() {
const pregunta = document.getElementById('pregunta').value.trim();
const boton = document.getElementById('boton');
const respuestaDiv = document.getElementById('respuesta');
if (!pregunta) return;
boton.disabled = true;
boton.textContent = "Pensando...";
respuestaDiv.textContent = "";
try {
const res = await fetch('/preguntar', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({pregunta: pregunta})
});
if (!res.ok) throw new Error("Error del servidor");
const datos = await res.json();
respuestaDiv.textContent = datos.respuesta;
} catch (e) {
respuestaDiv.textContent = "Ha ocurrido un error. ¿Está LM Studio abierto con el servidor activado?";
} finally {
boton.disabled = false;
boton.textContent = "Preguntar";
}
}
</script>
</body>
</html>
"""
@app.get("/", response_class=HTMLResponse)
def interfaz():
return PAGINA_HTML
No hace falta tocar requirements.txt: no se añade ninguna dependencia nueva, solo usamos piezas que FastAPI ya trae incluidas.
Cómo probarlo
- Sustituye el
main.pyanterior por este. - Si
uvicornya estaba corriendo, páralo (Ctrl+C) y vuelve a lanzarlo:
bash
uvicorn main:app --reload --port 8000
- Abre en el navegador, en el propio ordenador:
http://localhost:8000/ - Escribe una pregunta y pulsa “Preguntar”.
Verás un aviso fijo en la página recordando que es una versión de pruebas y que puede tardar o equivocarse — nos parece importante que esa advertencia esté ahí desde el primer prototipo, no como algo que se añade después.
Qué hemos conseguido con esto
Ya no hace falta saber nada de APIs, JSON ni terminales para hablar con nuestro modelo: basta con abrir una página y escribir. Es un cambio pequeño en código, pero grande en usabilidad — es la primera versión de IA Monterroso que se le podría enseñar a alguien sin tener que explicarle cómo funciona por dentro.
Sigue siendo, eso sí, una prueba de un solo usuario en un solo ordenador. El acceso para varias personas a la vez, y desde otros dispositivos, queda como un paso propio y deliberado más adelante — cuando lo abordemos, será porque hemos decidido resolver antes las preguntas de rendimiento y seguridad que conlleva, no como efecto secundario de tener ya una página bonita.
José Luis Usero — Profesor de Informática, IES Monterroso (Estepona)




