Proyecto IA Monterroso (XIII): preparando el RAG para más de una asignatura
Con el acceso en red ya funcionando, tocaba mirar hacia la siguiente idea: que MonteIA pueda “aprender” una asignatura como piloto, empezando por Computación y Robótica. Antes de meter ni un solo documento de clase, había que resolver un problema de fondo: rag.py solo sabía buscar en un sitio, el de los documentos del centro. Esta entrada es justo eso — la parte de arquitectura, sin ningún documento de asignatura todavía.
Qué vamos a hacer
Generalizar rag.py para que pueda haber varias colecciones de documentos independientes (varios corpus) sin duplicar el código de troceado e indexado por cada una. Cada corpus sigue siendo, como hasta ahora, una carpeta dentro de documentosmonterroso/; la diferencia es que ahora puede haber más de una carpeta activa a la vez, cada una con su propio índice BM25.
Importante: esta entrada no añade ningún corpus nuevo con contenido real. Solo prepara el terreno. El corpus del centro (aptorag/, los 42 documentos ya curados) sigue funcionando exactamente igual que antes — mismos documentos, mismos resultados. Cuando haya materiales curados de Computación y Robótica, añadir ese segundo corpus será: crear una carpeta, meter los PDF ya revisados por privacidad, y añadir una línea a un diccionario. Nada más.
Por qué ahora y no cuando lleguen los documentos
Podría parecer que esto se podía dejar para cuando hubiera materiales de la asignatura. Pero mezclar “diseñar la arquitectura” con “meter documentos reales de clase, con las preguntas de privacidad que eso trae” en la misma tarea es buscarse líos. Separando las dos cosas, hoy solo hay un cambio de código que se puede probar con los datos que ya conocemos (los 42 documentos del centro), y el día que lleguen los de la asignatura, esa tarea será solo “añadir una carpeta”, sin tocar nada de lo que ya funciona.
Código
Cambios en ia-monterroso-api/rag.py. La carpeta de documentos deja de estar fija a aptorag/; ahora hay un diccionario de corpus:
RAIZ_DOCUMENTOS = os.path.join(os.path.dirname(__file__), "..", "documentosmonterroso")
# Cada entrada es "nombre_de_corpus": "nombre_de_carpeta_dentro_de_documentosmonterroso".
# "centro" es el corpus original. Los corpus de asignatura se añaden aquí
# cuando haya materiales curados y listos.
CORPUS_DISPONIBLES = {
"centro": "aptorag",
}
CORPUS_POR_DEFECTO = "centro"
Cada corpus se indexa por separado al arrancar la API:
def _construir_indice(nombre_corpus, nombre_carpeta):
carpeta = os.path.join(RAIZ_DOCUMENTOS, nombre_carpeta)
documentos = _listar_documentos(carpeta)
fragmentos = _cargar_fragmentos(carpeta, documentos)
corpus_tokenizado = [_tokenizar(f["texto"]) for f in fragmentos]
bm25 = BM25Okapi(corpus_tokenizado) if corpus_tokenizado else None
print(f"[rag] Corpus '{nombre_corpus}' ({nombre_carpeta}/): "
f"{len(fragmentos)} fragmentos de {len(documentos)} documentos.")
return {"documentos": documentos, "fragmentos": fragmentos, "bm25": bm25}
_INDICES = {
nombre: _construir_indice(nombre, carpeta)
for nombre, carpeta in CORPUS_DISPONIBLES.items()
}
Y buscar_contexto() recibe un parámetro nuevo, corpus, con "centro" como valor por defecto para no romper nada de lo que ya llamaba a esta función:
def buscar_contexto(pregunta, corpus=CORPUS_POR_DEFECTO, k=FRAGMENTOS_POR_RESPUESTA):
indice = _INDICES.get(corpus)
if indice is None:
print(f"[rag] Aviso: corpus '{corpus}' no reconocido, usando '{CORPUS_POR_DEFECTO}'.")
indice = _INDICES.get(CORPUS_POR_DEFECTO)
if indice is None or indice["bm25"] is None or not pregunta.strip():
return ""
# ... resto de la búsqueda igual que antes, pero sobre indice["fragmentos"]
Dos detalles pensados para que esto no se rompa el día que se añada un corpus real: si se pide un corpus que no existe en el diccionario, no falla la petición — cae al corpus por defecto y avisa por consola. Y si la carpeta de un corpus todavía no existe (por ejemplo, uno registrado pero sin documentos copiados aún), tampoco falla — simplemente ese corpus se queda vacío hasta que se copien archivos.
En ia-monterroso-api/main.py, el modelo de la petición tiene un campo nuevo:
class PeticionChat(BaseModel):
mensajes: list[Mensaje]
personalidad: str = PERSONALIDAD_POR_DEFECTO
corpus: str = "centro"
y la llamada al RAG lo usa:
contexto_documentos = buscar_contexto(ultima_pregunta, corpus=datos.corpus)
No hay ningún cambio en la interfaz web. No tiene sentido añadir un selector de asignatura cuando solo existe una opción real (“centro”); eso llegará junto con el primer corpus de asignatura, no antes.
Cómo se comprobó
Con los 42 documentos reales de aptorag/ en un entorno de pruebas: el corpus “centro” sigue indexando exactamente los mismos 2183 fragmentos que antes del cambio, y una pregunta de prueba sobre el centro devuelve el mismo tipo de resultado que siempre. Además, se probó a pedir un corpus inexistente (cae al de centro sin fallar, con aviso por consola) y a registrar un corpus con una carpeta vacía (se queda sin resultados, sin romper la petición). El resto de la API — filtro de entrada, rate limiting, código de acceso — se probó junto con esto y sigue funcionando igual.
Instalación
Sin dependencias nuevas. En ROCKY, antes de sobrescribir:
copy main.py mainv12.py
copy rag.py ragv2.py
y luego sustituir main.py y rag.py por las versiones nuevas, y reiniciar la API. El arranque debe mostrar el mismo mensaje de siempre (ahora con el nombre del corpus):
[rag] Corpus 'centro' (aptorag/): 2183 fragmentos de 42 documentos.
Cómo comprobarlo
- Preguntar algo relacionado con los documentos del centro (por ejemplo, sobre el calendario o la convivencia) → debe responder igual que antes de este cambio, citando los mismos documentos si aplica.
- Comprobar en la consola de la API que el mensaje de arranque menciona el corpus “centro” con 2183 fragmentos, igual que antes.
- No debería notarse ningún cambio de comportamiento desde la interfaz — este cambio es puramente interno.
Qué queda pendiente
Lo de siempre: falta el contenido real. El siguiente paso es reunir un primer bloque de materiales propios de Computación y Robótica (páginas explicativas de clase, no entregas ni datos de alumnado), revisarlos con los mismos criterios de privacidad que ya aplicamos a los documentos del centro, y entonces sí: crear la carpeta, añadir la línea al diccionario, y por fin añadir un selector en la interfaz para elegir entre “MonteIA centro” y el modo asignatura — con un tono más animado, como se ha decidido para este piloto.

Etiqueta:AIDARAC, ARQUITECTURA, BM25, corpus, ies monterroso, montesteam, RAG


