Proyecto IA Monterroso (XV): una puerta de entrada sencilla para apps del alumnado
El chat de MonteIA (/preguntar) está pensado para la interfaz web: recibe todo el historial de la conversación y devuelve la respuesta en streaming, palabra por palabra. Perfecto para el chat, pero es más de lo que necesita una app sencilla hecha por un alumno en Scratch, App Inventor o un proyecto de Technovation. Hoy toca abrir una vía más simple, pensada para eso.
Por qué esto, y por qué ahora
La idea viene de algo muy concreto: una app que ya usa la API de Gemini, llamándola con una URL y unos parámetros, tal cual. Y una pregunta razonable detrás: ¿y si un día empiezan a cobrar por eso, o cambian las condiciones? Ahí está justo el motivo de tener una IA propia, gratuita y sustituible — y para que sirva de verdad como alternativa, tiene que poder llamarse de una forma igual de simple.
Qué vamos a hacer
Un segundo endpoint, GET /preguntar_simple, que se usa exactamente así: una URL con parámetros, sin cuerpo JSON que construir, sin gestionar una respuesta en streaming. Se manda la pregunta, se espera un poco, y se recibe la respuesta completa en JSON. /preguntar sigue igual, para el chat de la web; esta es una puerta de entrada alternativa para todo lo demás.
Cómo se usa
GET /preguntar_simple?pregunta=¿qué es un agente inteligente?&token=el-codigo-de-acceso&corpus=cyr1eso&personalidad=informal
Respuesta:
{"respuesta": "Un agente inteligente es..."}
Solo pregunta es obligatorio; token es el código de acceso (ver más abajo), y personalidad y corpus son opcionales (por defecto, formal y el corpus del centro).
Tres decisiones de diseño, explicadas
Sin historial. Cada llamada es una pregunta suelta, sin memoria de nada anterior. Es la diferencia principal con el chat. Si una app necesita que MonteIA tenga en cuenta algo dicho antes, tiene que incluirlo ella misma dentro de la propia pregunta.
Sin streaming. Se espera a la respuesta completa de LM Studio y se devuelve de una vez. Se pierde el efecto de verla escribirse en directo, pero a cambio cualquier app sencilla puede consumirlo sin saber leer una respuesta a trozos — que es justo lo que hace falta para Scratch, App Inventor o cualquier bloque que solo sabe “llamar a una URL y leer el resultado”.
El código de acceso, en la cabecera o en la URL. La interfaz web sigue mandándolo en la cabecera X-MonteIA-Token, como hasta ahora. Pero muchas apps sencillas no pueden añadir cabeceras propias a una petición, solo construir una URL — así que este endpoint acepta también ?token=... como parámetro. Aviso honesto: un token en la URL es algo menos seguro que en una cabecera, porque las URLs quedan guardadas en sitios (historial del navegador, registros de red) donde una cabecera no queda. Para este piloto no cambia gran cosa —el código ya viaja sin cifrar dentro de la red del centro— pero si esto crece en serio, es una razón más para pasar a HTTPS más adelante.
Importante: las mismas protecciones de siempre siguen aplicando aquí. Longitud máxima del mensaje, filtro contra los patrones de prompt injection más habituales, límite de 20 peticiones por minuto por IP, y el código de acceso. No es una puerta trasera con menos control, es la misma protección con una forma de llamada más sencilla.
Código
Cambios en ia-monterroso-api/main.py. Primero, la construcción del system prompt (personalidad + conocimiento del centro + fragmentos de RAG) se extrae a una función propia, para que /preguntar y el endpoint nuevo compartan exactamente la misma lógica en vez de duplicarla:
def _construir_system_prompt(nombre_personalidad, nombre_corpus, pregunta):
personalidad = PERSONALIDADES.get(nombre_personalidad, PERSONALIDADES[PERSONALIDAD_POR_DEFECTO])
contexto_documentos = buscar_contexto(pregunta, corpus=nombre_corpus)
descripcion = DESCRIPCION_FRAGMENTOS.get(nombre_corpus, DESCRIPCION_FRAGMENTOS["centro"])
partes_system_prompt = [personalidad, CONOCIMIENTO_CENTRO]
if contexto_documentos:
partes_system_prompt.append(
f"{descripcion} relacionados con la última pregunta (puede que no "
"todos sean útiles: usa solo lo que responda de verdad a la "
"pregunta, y si te basas en uno, puedes citar el documento entre "
"paréntesis):\n\n" + contexto_documentos
)
return "\n\n".join(partes_system_prompt)
La comprobación del código de acceso ahora acepta cabecera o parámetro:
def verificar_token(
x_monteia_token: str = Header(None, alias="X-MonteIA-Token"),
token: str = Query(None),
):
codigo = x_monteia_token or token
if codigo != MONTEIA_TOKEN:
raise HTTPException(status_code=401, detail="Código de acceso incorrecto o no proporcionado.")
Y el endpoint nuevo:
@app.get("/preguntar_simple", dependencies=[Depends(verificar_token)])
@limiter.limit("20/minute")
def preguntar_simple(
request: Request,
pregunta: str = Query(..., min_length=1),
personalidad: str = Query(PERSONALIDAD_POR_DEFECTO),
corpus: str = Query("centro"),
):
if len(pregunta) > LONGITUD_MAXIMA_MENSAJE:
raise HTTPException(status_code=400, detail="El mensaje es demasiado largo...")
if _mensaje_sospechoso(pregunta):
raise HTTPException(status_code=400, detail="MonteIA no puede atender esa petición...")
system_prompt = _construir_system_prompt(personalidad, corpus, pregunta)
mensajes_para_el_modelo = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": pregunta},
]
respuesta_lm_studio = requests.post(
LM_STUDIO_URL,
json={"model": MODEL_NAME, "messages": mensajes_para_el_modelo, "temperature": 0.7, "stream": False},
timeout=TIMEOUT_SEGUNDOS,
)
respuesta_lm_studio.raise_for_status()
texto_respuesta = respuesta_lm_studio.json()["choices"][0]["message"]["content"]
return {"respuesta": texto_respuesta}
(El código completo, con el manejo de errores de conexión y timeout, es algo más largo — esto es el núcleo.)
Cómo se comprobó
Con un cliente de pruebas, antes de instalar nada: el token funciona igual por cabecera que por parámetro de la URL; sin token o con uno incorrecto, 401; una pregunta con un patrón de prompt injection, bloqueada igual que en el chat; y al disparar 25 peticiones seguidas, las últimas se cortan con el mismo límite de siempre. Todo esto sin tocar el comportamiento de /preguntar, que sigue funcionando exactamente igual que antes.
Instalación
Solo cambia main.py. Sin dependencias nuevas. Backup del archivo actual, sustituir, reiniciar.
Cómo comprobarlo
Desde el navegador o cualquier cliente HTTP, con la API en marcha:
http://ip-de-rocky:8000/preguntar_simple?pregunta=hola&token=tu-codigo
Debe devolver un JSON con la respuesta. Probar también con el corpus de la asignatura (&corpus=cyr1eso) y sin token (debe dar 401).
Qué queda pendiente
Esto abre la puerta a que una app real de Technovation, o cualquier proyecto del alumnado, hable con MonteIA en vez de con un servicio externo de pago. Falta documentarlo de forma más formal (algo como una página sencilla con los parámetros y ejemplos, pensada para que un alumno la lea sin tener que preguntar) y, cuando llegue el momento de probarlo con una app real, ver si el límite de 20 peticiones por minuto y la falta de historial son suficientes para ese caso de uso o hay que ajustar algo — mejor descubrirlo con un uso real que adivinarlo de antemano.


