
Proyecto IA Monterroso (XIV): el primer piloto de asignatura, Computación y Robótica
En la entrada anterior dejamos preparado el mecanismo para que rag.py pudiera manejar varios corpus de documentos a la vez, pero sin ningún corpus de asignatura real todavía. Hoy toca eso: el primer piloto de verdad, con material propio de Computación y Robótica de 1º de ESO.
Qué se ha añadido
Un corpus nuevo, cyr1eso, con 15 documentos: apuntes propios de José Luis sobre inteligencia artificial, agentes inteligentes, ciberseguridad, propiedad intelectual, páginas web e Internet, un decálogo de ciberseguridad, la tabla del temario de programación de 1º a 3º de ESO y la programación didáctica oficial de la asignatura. Todo material pensado y elaborado para el alumnado, no sacado de un libro de texto ni de terceros.
Cambio técnico: rag.py ya lee .odt y .docx, no solo PDF
Casi todo el material real de clase está en formato OpenDocument (LibreOffice) o Word, no en PDF. Hacía falta enseñarle a rag.py a leer esos formatos también.
Se añaden dos librerías (python-docx para .docx, odfpy para .odt) y una función que decide cómo extraer el texto según la extensión:
EXTENSIONES_ADMITIDAS = (".pdf", ".docx", ".odt")
def _extraer_paginas_pdf(ruta):
lector = pypdf.PdfReader(ruta)
return [
(pagina.extract_text() or "", num_pagina)
for num_pagina, pagina in enumerate(lector.pages, start=1)
]
def _extraer_paginas_docx(ruta):
# .docx no tiene un concepto de "página" fiable (depende de cómo se
# reflowee al abrir el archivo): se trata como un solo bloque de texto,
# incluyendo también las tablas, que es donde a veces va contenido real.
documento = DocumentoWord(ruta)
partes = [p.text for p in documento.paragraphs]
for tabla in documento.tables:
for fila in tabla.rows:
for celda in fila.cells:
partes.append(celda.text)
return [("\n".join(partes), None)]
def _extraer_paginas_odt(ruta):
# Igual que .docx: sin página real. odf.teletype.extractText recorre
# todo el árbol del documento (párrafos, títulos, listas, tablas), a
# diferencia de mirar solo los párrafos de primer nivel.
documento = cargar_odt(ruta)
return [(extraer_texto_odt(documento.text), None)]
def _extraer_paginas(ruta):
if ruta.lower().endswith(".pdf"):
return _extraer_paginas_pdf(ruta)
if ruta.lower().endswith(".docx"):
return _extraer_paginas_docx(ruta)
if ruta.lower().endswith(".odt"):
return _extraer_paginas_odt(ruta)
return []
Como .docx y .odt no tienen número de página real, la cita en el system prompt se ajusta sola según haga falta:
def _cita(f):
if f["pagina"] is not None:
return f"[Fuente: {f['fuente']}, página {f['pagina']}]\n{f['texto']}"
return f"[Fuente: {f['fuente']}]\n{f['texto']}"
Y el segundo corpus queda registrado junto al del centro:
CORPUS_DISPONIBLES = {
"centro": "aptorag",
"cyr1eso": "cyr1eso",
}
El selector de “Tema” en la interfaz
Con un segundo corpus real, ya tenía sentido el selector que en la entrada anterior se dejó fuera a propósito. Se añade junto al de personalidad:
<label class="selector_personalidad">
Tema:
<select id="corpus" onchange="nuevaConversacion()">
<option value="centro">Centro (IES Monterroso)</option>
<option value="cyr1eso">Computación y Robótica 1º ESO (piloto)</option>
</select>
</label>
y se manda en cada petición junto con la personalidad:
body: JSON.stringify({ mensajes: historial, personalidad: selectorPersonalidad.value, corpus: selectorCorpus.value })
Nota sobre el tono “más animado” que se había planteado para este piloto: no se ha creado una personalidad nueva. La personalidad “Cercana (informal)”, que ya existía, cubre exactamente eso — basta con elegirla junto con el tema “Computación y Robótica” para tener el efecto buscado, sin añadir una tercera pieza al sistema.
Cómo se comprobó
Con los 15 documentos reales, antes de instalar nada: 371 fragmentos indexados sin errores, y preguntas de prueba sobre agentes inteligentes, propiedad intelectual, ciberseguridad e Internet de las cosas devuelven fragmentos correctos de los documentos correspondientes. Se comprobó también que el corpus del centro (aptorag/, 42 documentos) sigue indexando exactamente igual que antes — los dos corpus conviven sin interferirse.
Instalación
pip install -r requirements.txt
(añade python-docx y odfpy a lo que ya había). Luego, backup de main.py y rag.py actuales, sustituir por los nuevos, y reiniciar. En el arranque deben aparecer dos líneas, una por corpus:
[rag] Corpus 'centro' (aptorag/): 2183 fragmentos de 42 documentos.
[rag] Corpus 'cyr1eso' (cyr1eso/): 371 fragmentos de 15 documentos.
Cómo comprobarlo
- En la interfaz, elegir el tema “Computación y Robótica 1º ESO (piloto)” y preguntar algo del temario (por ejemplo, “¿qué es un agente inteligente?” o “¿qué es la propiedad intelectual?”) → debe responder citando alguno de los 15 documentos.
- Cambiar el tema a “Centro” y preguntar algo de siempre (calendario, convivencia) → debe seguir funcionando exactamente igual que antes, sin mezclarse con el contenido de la asignatura.
- Combinar el tema “Computación y Robótica” con la personalidad “Cercana (informal)” para el tono más animado.
Qué queda pendiente
Quince documentos son un piloto, no una cobertura completa del temario. Si el piloto va bien, lo siguiente sería ampliar el corpus con más apuntes propios de José Luis, y más adelante añadir OCR para poder aprovechar también el material que hoy solo existe como imagen — no urgente mientras el piloto funcione con lo que ya hay.


