Proyecto independiente No afiliado, patrocinado ni avalado por la Watch Tower Bible and Tract Society o Jehovah's Witnesses.
jw-agent-toolkit
EN

Guía

Ingest de PDFs históricos y docs Office (Fase 62)

Cómo añadir al RAG personal Atalayas/Awake escaneadas, libros JW pre-EPUB y documentos compartidos por hermanos (guiones de discursos, programas de circuito, hojas de asistencia).

¿Por qué dos loaders distintos?

LoaderCubreBackend
jw_rag.loaders.pdf_marker.pdf (incluye escaneos OCR)marker-pdf
jw_rag.loaders.docs_markitdown.docx, .pptx, .xlsxmarkitdown

markitdown también lee .pdf, pero su layout-parser pierde estructura en escaneos históricos JW. Para PDFs siempre usar pdf_marker.

Instalación

Ambos extras son opt-in para mantener la instalación base ligera (marker-pdf trae torch + transformers, ~2 GB):

# Solo PDF:
uv add 'jw-rag[pdf-marker]'

# Solo Office docs:
uv add 'jw-rag[doc-markitdown]'

# Los dos juntos:
uv add 'jw-rag[loaders-all]'

Si invocas el loader sin el extra instalado:

ModuleNotFoundError: marker-pdf is not installed.
Run: uv add 'jw-rag[pdf-marker]'

Uso desde la CLI

# PDF de Atalaya 1950 (escaneo personal del usuario)
jw rag ingest-pdf ~/Documents/atalaya_1950_marzo.pdf --language es

# Programa de circuito compartido por el superintendente
jw rag ingest-office ~/Documents/programa_circuito_2026.docx --language es

# Store custom (por default ./jw-rag-store)
jw rag ingest-pdf ./mi.pdf --store ~/.jw-agent-toolkit/rag --language en

Si el extra falta, el comando sale con código 3 y un hint:

$ jw rag ingest-office hoja.docx
markitdown is not installed. Run: uv add 'jw-rag[doc-markitdown]'
$ echo $?
3

Uso desde Python

from pathlib import Path
from jw_rag.embed import FakeEmbedder      # o el embedder real
from jw_rag.store import VectorStore
from jw_rag.loaders import ingest_pdf, ingest_office_doc

store = VectorStore(Path("./jw-rag-store"), FakeEmbedder())
store.load()

n_pdf  = ingest_pdf(store, Path("atalaya_1950.pdf"), language="es")
n_docx = ingest_office_doc(store, Path("circuito.docx"), language="es")

print(f"Indexed {n_pdf + n_docx} new chunks")
store.save()

Uso desde el servidor MCP (Claude Desktop / Claude Code)

Las dos tools quedan disponibles automáticamente cuando el host MCP se conecta a jw-mcp:

// Tool call desde el agente
{"name": "ingest_pdf",        "arguments": {"pdf_path": "/Users/x/a.pdf", "language": "es"}}
{"name": "ingest_office_doc", "arguments": {"doc_path": "/Users/x/b.docx", "language": "es"}}

Respuesta JSON:

{
  "pdf_path": "/Users/x/a.pdf",
  "language": "es",
  "chunks_added": 47,
  "store_total": 12834
}

Si el extra opcional no está instalado en el venv del servidor, la respuesta llega con {"error": "..."} — el agente la ve sin romper la sesión.

Detección automática “¿es contenido JW?”

pdf_marker busca firmas conocidas en el markdown extraído:

  • Watch Tower, The Watchtower, JW.ORG
  • Atalaya, Awake!, Despertad!
  • Kingdom Hall, Jehovah's Witnesses, Testigos de Jehová

Si encuentra al menos una → metadata.is_jw = True. Permite queries filtradas a posteriori:

hits = store.hybrid_search("trinidad", top_k=20)
jw_only = [h for h in hits if h.chunk.metadata.get("is_jw")]

Importante: el loader nunca bloquea ingest si is_jw es False — el RAG personal del usuario puede tener material no-JW (apuntes, estudios externos, etc.) que también es legítimo indexar.

docs_markitdown no aplica la firma JW por simplicidad (los Office docs son típicamente material producido por el propio hermano), pero el caller puede pasar custom_meta={"is_jw": True} si quiere etiquetar manualmente.

Idempotencia

Re-ingest del mismo archivo (mismo sha256) es no-op: el loader calcula el hash, deriva source_id = "pdf:<hash8>" o "doc:<ext>:<hash8>", consulta store.has_source(source_id) y devuelve 0 si ya estaba indexado. Útil para:

  • Re-procesar un corpus completo en CI sin duplicar chunks.
  • Reescaneo del usuario (si el PDF cambia → hash cambia → re-indexa).
  • Pipelines de ingesta cron que apuntan a un mismo directorio.

GPU y LLM opt-in (marker)

Por default marker corre CPU only y sin LLM remoto — coherente con la filosofía local-first del toolkit. Para acelerar y mejorar layout en PDFs complejos:

JW_MARKER_USE_GPU=1 \
JW_MARKER_USE_LLM=1 \
OPENAI_API_KEY=sk-... \
    jw rag ingest-pdf ~/Documents/atalaya_dificil.pdf

use_llm=True envía fragmentos del documento al modelo configurado (OpenAI/Anthropic según marker’s config). Sólo activarlo cuando el usuario sabe que el contenido es público y la mejora vale el costo.

Metadata por chunk

Cada chunk producido por estos loaders trae como mínimo:

{
  "source_kind": "pdf_marker" | "office_markitdown",
  "source_path": "/abs/path/file.pdf",
  "source_format": "pdf" | "docx" | "pptx" | "xlsx",  // solo office
  "file_hash":   "<sha256 completo>",
  "language":    "es",
  "is_jw":       true,    // solo pdf_marker
  "para_count":  3,        // del ParagraphChunker
  "chunker":     "paragraph"
}

custom_meta adicional se mergea encima (ej. {"sender": "hermano_pablo"}).

Limitaciones

  • Tablas complejas: marker hace su mejor esfuerzo, ocasionalmente pierde celdas mergeadas. Verificar manualmente si el corpus depende de ellas.
  • OCR de escaneos baja resolución: < 150 DPI puede dar texto basura. Re-escanear a 300 DPI antes de ingerir.
  • Cifrado: PDFs cifrados con contraseña fallan — descifrar primero.
  • Office macros: markitdown ignora macros; el contenido visible se extrae correctamente.
  • PDFs sólo-imagen sin OCR: pendiente fallback a Tesseract en una fase posterior; por ahora el loader devuelve un markdown vacío y cero chunks.

Editar esta página en docs/guias/historical-pdf-ingest.md