Skip to content

Commit 3c8857b

Browse files
committed
feat: agregar scripts de pregeneración y empaquetado de bibliografía
Tanda 3/3 (última) de la indexación RAG de bibliografía: agrega scripts/pregenerar_indice_bibliografia.py (indexa bibliografia/*.pdf en lecciones/.chroma en paralelo, con retry para el TypeError espurio del tokenizer de HuggingFace) y scripts/empaquetar_distribucion_bibliografia.py (empaqueta PDFs + índice pre-generado en dist/bibliografia_paquete.zip para que los alumnos no reindexen desde cero, proceso que toma varios minutos). Agrega bibliografia/*.pdf, *.epub y dist/ a .gitignore (los PDFs tienen copyright, mismo criterio que docs/legado/) y bibliografia/README.md documentando el uso y los 3 libros actualmente presentes. Mejora sobre el diseño original (portado de Probabilidad y Estadística): el script de empaquetado ahora limpia dist/bibliografia_paquete/ por completo al inicio de cada corrida, evitando que un PDF eliminado o renombrado en bibliografia/ deje una entrada "fantasma" en el índice distribuido de una corrida anterior -- hallazgo de security-reviewer, aplicado aquí y no presente en el repo hermano de origen. Verificado con ejecución real: ambos scripts corridos contra los 3 PDFs reales de bibliografia/ (357+1776+1074=3207 chunks, 0 fallidos), el paquete .zip generado dos veces seguidas (con y sin el rmtree) sin errores. Revisado por python-reviewer y security-reviewer sin hallazgos CRITICAL/HIGH. Cierra la pieza 5 del plan de uniformización con Probabilidad.
1 parent 7703c17 commit 3c8857b

4 files changed

Lines changed: 396 additions & 0 deletions

File tree

‎.gitignore‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,18 @@ __pycache__/
3131
# Se conserva localmente en docs/legado/ pero no se publica en GitHub.
3232
docs/legado/
3333

34+
# PDFs de bibliografía académica (libros de texto con copyright) para
35+
# referencia del curso y el indexado RAG de TutorAgent. Se conservan
36+
# localmente, nunca se publican — mismo criterio que docs/legado/. El
37+
# README.md de la carpeta sí se versiona.
38+
bibliografia/*.pdf
39+
bibliografia/*.epub
40+
41+
# Paquete de distribución de bibliografía (PDFs + índice ChromaDB
42+
# pre-generado) -- se genera localmente con
43+
# scripts/empaquetar_distribucion_bibliografia.py, nunca se publica.
44+
dist/
45+
3446
# Worktrees locales para trabajo aislado (sandboxes de desarrollo)
3547
.worktrees/
3648

‎bibliografia/README.md‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Bibliografía
2+
3+
## Qué va aquí
4+
5+
Libros de texto de referencia para el curso. PDFs actualmente presentes en esta carpeta:
6+
7+
- *Lógica de Programación* (Omar Iván Trejos Buriticá)
8+
- *Algoritmos Resueltos con Diagramas de Flujo y Pseudocódigo* (Francisco Javier Pinales Delgado, César Adolfo Serrano Escamilla)
9+
- *Numerical Python: Scientific Computing and Data Science Applications with NumPy, SciPy and Matplotlib*
10+
11+
## Uso con `TutorAgent`
12+
13+
`TutorAgent` indexa estos PDFs automáticamente en una colección ChromaDB
14+
separada (`bibliografia_curso`), distinta de la colección de lecciones
15+
(`lecciones_curso`). La indexación ocurre la primera vez que se instancia
16+
el agente (`bibliografia_collection.count() > 0` evita reprocesar en
17+
instancias siguientes) y usa `pdf_indexer.py` (extracción con `pypdf`,
18+
chunking por página).
19+
20+
`_search_local_docs` combina 3 resultados de lecciones + 1 de bibliografía
21+
en cada consulta, para que el contexto pedagógico de las lecciones del
22+
curso nunca quede ahogado por el volumen de los libros completos.
23+
24+
Un PDF escaneado (sin texto seleccionable) o corrupto se omite sin
25+
detener la indexación de los demás — se loguea como warning/error.
26+
27+
## Pre-generar el índice (para distribuir a los alumnos)
28+
29+
Indexar los PDFs desde cero puede tardar varios minutos. Para no obligar
30+
a cada alumno a esperar esa indexación la primera vez que usa `TutorAgent`,
31+
se puede pre-generar el índice y empaquetarlo:
32+
33+
```bash
34+
# Indexa bibliografia/ en lecciones/.chroma (el índice de trabajo de este repo)
35+
python scripts/pregenerar_indice_bibliografia.py
36+
37+
# Empaqueta bibliografia/*.pdf + un índice ChromaDB pre-generado en
38+
# dist/bibliografia_paquete.zip, listo para distribuir (Classroom, Drive, USB)
39+
python scripts/empaquetar_distribucion_bibliografia.py
40+
```
41+
42+
El alumno solo necesita descomprimir `bibliografia_paquete.zip` dentro de
43+
la carpeta del curso (reemplazando/creando `bibliografia/` y `lecciones/.chroma`)
44+
para tener el índice ya construido, sin esperar la indexación.
45+
46+
## Por qué está gitignored
47+
48+
Los PDFs de libros de texto tienen copyright — mismo criterio que
49+
`docs/legado/` (material institucional): se conservan localmente para uso
50+
del curso, nunca se publican en el repo público. El `README.md` de esta
51+
carpeta sí se versiona.
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
"""
2+
Empaqueta bibliografia/*.pdf + un índice ChromaDB pre-generado en un .zip
3+
listo para distribuir a los alumnos (evita que cada alumno tenga que
4+
reindexar los libros completos desde cero, proceso que puede tardar
5+
varios minutos).
6+
7+
A diferencia de scripts/pregenerar_indice_bibliografia.py (que indexa
8+
sobre lecciones/.chroma, el índice de trabajo de este repo), este script
9+
construye el índice en dist/bibliografia_paquete/.chroma -- una ruta
10+
ASCII-safe dentro del repo, para que quede embebido en el paquete sin
11+
depender de rutas de esta máquina.
12+
13+
Los PDFs tienen copyright y dist/ está en .gitignore: este script nunca
14+
publica nada en git, solo genera un artefacto local para distribuir por
15+
fuera (Drive, Classroom, USB).
16+
17+
Uso: python scripts/empaquetar_distribucion_bibliografia.py
18+
"""
19+
20+
import logging
21+
import shutil
22+
import sys
23+
from pathlib import Path
24+
25+
REPO_ROOT = Path(__file__).resolve().parent.parent
26+
SCRIPTS_DIR = Path(__file__).resolve().parent
27+
if str(REPO_ROOT) not in sys.path:
28+
sys.path.insert(0, str(REPO_ROOT))
29+
if str(SCRIPTS_DIR) not in sys.path:
30+
sys.path.insert(0, str(SCRIPTS_DIR))
31+
32+
from pregenerar_indice_bibliografia import BIBLIOGRAFIA_DIR
33+
from pregenerar_indice_bibliografia import main as indexar_bibliografia
34+
35+
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
36+
logger = logging.getLogger(__name__)
37+
38+
DIST_DIR = REPO_ROOT / "dist"
39+
PAQUETE_DIR = DIST_DIR / "bibliografia_paquete"
40+
PAQUETE_CHROMA_PATH = PAQUETE_DIR / ".chroma"
41+
PAQUETE_BIBLIOGRAFIA_DIR = PAQUETE_DIR / "bibliografia"
42+
ZIP_BASENAME = DIST_DIR / "bibliografia_paquete"
43+
44+
45+
def _copiar_pdfs(origen: Path, destino: Path) -> int:
46+
"""Copia los PDFs de `origen` a `destino`, creando el directorio
47+
destino si no existe. Devuelve cuántos PDFs se copiaron."""
48+
destino.mkdir(parents=True, exist_ok=True)
49+
pdfs = sorted(origen.glob("*.pdf"))
50+
for pdf in pdfs:
51+
shutil.copy2(pdf, destino / pdf.name)
52+
return len(pdfs)
53+
54+
55+
def main() -> None:
56+
if not BIBLIOGRAFIA_DIR.is_dir() or not list(BIBLIOGRAFIA_DIR.glob("*.pdf")):
57+
logger.error("No hay PDFs en %s -- nada que empaquetar.", BIBLIOGRAFIA_DIR)
58+
return
59+
60+
# Se limpia PAQUETE_DIR completo antes de reconstruirlo: si un PDF fue
61+
# eliminado o renombrado en bibliografia/ desde el último empaquetado,
62+
# su entrada vieja en el índice de una corrida anterior nunca se
63+
# borraría por sí sola (el upsert solo actualiza/agrega, nunca
64+
# elimina) y quedaría como contenido "fantasma" en el .zip distribuido
65+
# sin que nada lo detecte. Es seguro borrar PAQUETE_DIR por completo:
66+
# vive bajo dist/ (gitignored, artefacto desechable) y se reconstruye
67+
# aquí mismo en cada corrida.
68+
if PAQUETE_DIR.exists():
69+
shutil.rmtree(PAQUETE_DIR)
70+
71+
logger.info("Copiando PDFs a %s ...", PAQUETE_BIBLIOGRAFIA_DIR)
72+
n_copiados = _copiar_pdfs(BIBLIOGRAFIA_DIR, PAQUETE_BIBLIOGRAFIA_DIR)
73+
logger.info("Copiados %d PDFs.", n_copiados)
74+
75+
logger.info(
76+
"Construyendo índice ChromaDB en %s (esto puede tardar varios " "minutos)...",
77+
PAQUETE_CHROMA_PATH,
78+
)
79+
pdfs_fallidos = indexar_bibliografia(
80+
bibliografia_dir=PAQUETE_BIBLIOGRAFIA_DIR, chroma_path=PAQUETE_CHROMA_PATH
81+
)
82+
if pdfs_fallidos:
83+
logger.error(
84+
"Abortando empaquetado: %d PDF(s) no se pudieron indexar (%s). "
85+
"El paquete distribuiría un índice incompleto sin que sea "
86+
"evidente para el alumno. Vuelve a correr este script para "
87+
"reintentar (upsert no duplica los PDFs ya indexados).",
88+
len(pdfs_fallidos),
89+
", ".join(pdfs_fallidos),
90+
)
91+
return
92+
93+
logger.info("Comprimiendo paquete en %s.zip ...", ZIP_BASENAME)
94+
ruta_zip = shutil.make_archive(
95+
base_name=str(ZIP_BASENAME), format="zip", root_dir=str(PAQUETE_DIR)
96+
)
97+
98+
tamano_mb = Path(ruta_zip).stat().st_size / (1024 * 1024)
99+
logger.info(
100+
"Paquete listo: %s (%.1f MB). Contiene bibliografia/*.pdf + .chroma "
101+
"ya indexado -- los alumnos solo necesitan descomprimirlo dentro de "
102+
"la carpeta del curso.",
103+
ruta_zip,
104+
tamano_mb,
105+
)
106+
107+
108+
if __name__ == "__main__":
109+
main()

0 commit comments

Comments
 (0)