# 0051 — Funciones pendientes del pipeline de extraccion (NER+RE+OpenIE) ## APP Metadata | Campo | Valor | |-------|-------| | **ID** | 0051 | | **Estado** | pendiente | | **Prioridad** | media | | **Tipo** | feature — `python/functions/{datascience,pipelines,core}/` | ## Dependencias - Las 18 funciones NER/RE creadas el 2026-05-04 (gliner2_load_model, extract_graph_gliner2, extract_triples_spacy_es, etc.) — base ya construida. - `extract_pdf_text_py_core` — ya existente, se reusa. **Desbloquea:** integracion completa del pipeline GLiNER2 + spaCy ES + chunking + coref + post-filter en `graph_explorer` panel `paste_extract` (issues 0041 y 0042 del sub-repo). --- ## Contexto En la sesion del 2026-05-04 se construyeron 18 funciones NER+RE (ver CHANGELOG.md y `vaults/osint_nlp_models/`). Quedan **5 huecos** que no se construyeron en esa ronda y que deberian existir para cerrar el ciclo: 1. NuExtract loader + extractor — descartado por velocidad pero util como engine "Rich extraction" opcional cuando hay GPU. 2. `extract_graph_from_pdf` pipeline — composicion `extract_pdf_text + clean_pdf_text + chunk_with_overlap + extract_graph_gliner2 + ...`. 3. spaCy ES V2 reglas — soportar pasiva refleja, copulares, coref simple de pronombres. 4. Fix del kernel startup que sombrea paquetes pip (`bigquery/datasets.py` rompe `import datasets` de HF). 5. `extract_relations_rebel` (paralela a `extract_relations_mrebel`) para texto en ingles con licencia Apache. Cada hueco se desglosa abajo con plantilla suficiente para que un proximo `fn-constructor` lo pueda construir sin abrir la conversacion original. --- ## A. NuExtract 2.0 loader + extractor ### Justificacion NuExtract 2.0-2B (`numind/NuExtract-2.0-2B`, **MIT license**) emite **JSON estructurado** rellenando un template. Util cuando el usuario quiere ficha rica por entidad (ej. para cada empresa: `{name, ceo, headquarters, subsidiaries, founded_in}`). Mas lento que GLiNER2 (310s vs 139s sobre el PDF de BBVA) pero mejor recall de atributos por entidad. Ver `notebooks/07_nuextract_vs_gliner2.ipynb` y `vaults/osint_nlp_models/models/` (no hay md de nuextract todavia, anadirlo). ### Funciones a crear **A1. `nuextract_load_model_py_datascience` (impure)** ```python """LICENSE: MIT (NuExtract-2.0-2B). Comercial OK. Version 4B es CC BY-NC-Qwen-Research (no comercial). Version 8B es MIT. """ from typing import Any _MODEL_CACHE: dict = {} def nuextract_load_model( model_name: str = "numind/NuExtract-2.0-2B", device: str = "auto", ) -> tuple[Any, Any]: """Loads (and caches) NuExtract tokenizer + model. Returns (tokenizer, model). Note: AutoProcessor is broken in transformers 5.x for Qwen2-VL — use AutoTokenizer + AutoModelForImageTextToText directly (no AutoProcessor). For GPU: bfloat16, attn_implementation='sdpa'. For CPU: float32, attn_implementation='eager' (much slower, 10-30s/extraction). """ import torch from transformers import AutoTokenizer, AutoModelForImageTextToText use_gpu = device == "cuda" or (device == "auto" and torch.cuda.is_available()) resolved = "cuda" if use_gpu else "cpu" dtype = torch.bfloat16 if use_gpu else torch.float32 attn = "sdpa" if use_gpu else "eager" key = (model_name, resolved) if key in _MODEL_CACHE: return _MODEL_CACHE[key] tok = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True, padding_side="left") mdl = AutoModelForImageTextToText.from_pretrained( model_name, trust_remote_code=True, torch_dtype=dtype, attn_implementation=attn, ) if use_gpu: mdl = mdl.to(resolved) mdl.eval() _MODEL_CACHE[key] = (tok, mdl) return tok, mdl ``` **A2. `extract_structured_nuextract_py_datascience` (impure)** ```python def extract_structured_nuextract( text: str, template: str, # JSON schema as string tokenizer, model, max_new_tokens: int = 1024, repetition_penalty: float = 1.15, # CRITICO — sin esto degenera en bucles num_beams: int = 1, ) -> dict: """Extract structured info from text using NuExtract 2.0 with a JSON template. Returns: {"raw_text": str (el JSON crudo del modelo), "parsed": dict | None (parseado con find('{') + truncate progresivo), "elapsed_s": float, "n_input_tokens": int, "n_output_tokens": int} IMPORTANTE: NuExtract degenera en texto largo si repetition_penalty < ~1.1. Usar repetition_penalty=1.15 (default) y trocear texto largo con chunk_with_overlap. """ ``` Parser de output esta en `run_nuextract_full.py` linea ~120 (find('{') + truncate progresivo). ### Tests - A1: cache hit/miss. - A2: con stub de modelo, validar parser de JSON. Con corpus real solo si GPU disponible (skip otherwise). --- ## B. `extract_graph_from_pdf_py_pipelines` ### Justificacion Composicion natural: `extract_pdf_text` ya existe en `python/functions/core/`. Combinarlo con todo lo nuevo: ``` extract_pdf_text (existente) → clean_pdf_text (NUEVO 2026-05-04) → chunk_with_overlap (NUEVO 2026-05-04) → extract_graph_gliner2 (×N, NUEVO 2026-05-04) → aggregate_extraction_results → filter_relations_by_entity_types → merge_entity_aliases → grafo final ``` ### Firma ```python def extract_graph_from_pdf( pdf_path: str, entity_labels: list[str], relation_labels, allowed: dict, model, # GLiNER2 model threshold: float = 0.3, max_chars_per_chunk: int = 1500, overlap_sentences: int = 2, ) -> dict: """End-to-end pipeline: PDF -> graph. Internally: 1. extract_pdf_text (existing) 2. clean_pdf_text 3. chunk_with_overlap if len(text) > max_chars_per_chunk 4. extract_graph_gliner2 per chunk 5. aggregate_extraction_results 6. filter_relations_by_entity_types 7. merge_entity_aliases Returns: same shape as extract_graph_from_text. """ ``` Esto es essencialmente `extract_graph_from_text(extract_pdf_text(path), ...)` con la limpieza intermedia. ### Tests - Test smoke con PDF de fixture pequeño (1-2 paginas). - Test que fallback a chunking solo dispara cuando `len(text) > max_chars`. ### Donde poner el PDF de fixture `python/functions/pipelines/tests/fixtures/sample.pdf` — un PDF corto de uso libre. O reusar `vaults/osint_nlp_models/test_documents/politica_proteccion_datos.pdf` con un path absoluto en el test (skip si no existe). --- ## C. spaCy ES V2 — reglas mejoradas ### Justificacion Notebook 09 mostro que las reglas V1 (`extract_triples_spacy_es_py_datascience`) fallan en: 1. **Pasiva refleja**: `Se firmaron acuerdos entre Iberdrola y Endesa.` → vacio. Debe emitir `(Iberdrola, firmar[pass], Endesa)` o similar. 2. **Copulares**: `Pablo Isla es expresidente de Inditex.` → vacio. Debe emitir `(Pablo Isla, ser, expresidente de Inditex)`. 3. **Coreferencia pronombres**: `Sara llamo a su madre Lucia.` → tripleta con span `'su madre Lucia'`. Debe resolver `su` al sujeto previo (Sara). 4. **Lematizacion**: `movilizara` → `movilizarar` (lemma incorrecta del modelo `es_core_news_md`). Considerar `es_core_news_lg` o post-process. ### Funciones a crear **C1. `extract_triples_spacy_es_v2_py_datascience` (impure)** Mismo patron que V1 pero con reglas adicionales: ```python def extract_triples_spacy_es_v2(text: str, nlp: Any, resolve_pronouns: bool = True) -> dict: """Improved Spanish OpenIE via spaCy dependency parsing. V2 changes vs V1: - Pasiva refleja: detect 'se' + verb conjugated -> treat agent as subject if available - Copulares: 'X es Y', 'X esta Y' -> emit (X, ser/estar, Y) - Coref simple: track previous subject, resolve 'su X' to that subject - Lemma override: hardcoded fixes for common errors (movilizarar -> movilizar) Returns: same shape as extract_triples_spacy_es V1. """ ``` ### Tests - Test pasiva refleja: `'Se firmaron acuerdos entre Iberdrola y Endesa'` -> tripleta con `firmar[pass]`. - Test copular: `'Pablo es presidente'` -> `(Pablo, ser, presidente)`. - Test coref: `'Sara llamo a su madre Lucia'` -> sujeto canonico Sara (no `'su madre Lucia'`). - Test lemma override: `movilizara` -> lemma `movilizar`. --- ## D. Fix kernel startup shadow de paquetes pip ### Sintoma `.ipython/profile_default/startup/00_fn_registry.py` añade cada subdir de `python/functions/` al sys.path top-level. Como hay un `bigquery/datasets.py` en el registry, **shadows** el paquete `datasets` de HuggingFace que `transformers` necesita. Resultado: en cada notebook hay que aplicar un workaround: ```python _pf = '/home/lucas/fn_registry/python/functions' sys.path = [p for p in sys.path if not p.startswith(_pf + '/')] if _pf not in sys.path: sys.path.insert(0, _pf) ``` ### Fix propuesto Modificar el template `write_jupyter_registry_kernel` (la funcion del registry que genera ese startup file en cada analysis nuevo) para: ```python # Solo el directorio padre 'python/functions/' (no los subdirs) sys.path.insert(0, str(_python_functions)) # El usuario importa con paquete: # from datascience.gliner_load_model import gliner_load_model # from core.extract_pdf_text import extract_pdf_text # (no `from gliner_load_model import ...` directo) ``` Esto requiere actualizar: 1. La funcion del registry que genera el startup file. 2. Re-generar el startup file en analyses existentes (script de migracion). 3. Documentar en `.claude/CLAUDE.md` que los imports en notebooks de analysis siguen el patron `from import `. ### Tests - Test que el startup nuevo permite `import datasets` (huggingface) sin shadow. - Test que sigue funcionando `from datascience.gliner_load_model import gliner_load_model`. --- ## E. `extract_relations_rebel_py_datascience` ### Justificacion `extract_relations_mrebel` ya existe (creado en ronda 1 del 2026-05-04). Para texto en **ingles** y casos donde se necesita licencia comercial sin caveat, REBEL (`Babelscape/rebel-large`, **Apache 2.0**) es la alternativa. ### Firma ```python def extract_relations_rebel( text: str, entities: list, # list[EntityCandidate] tokenizer, model, sentence_split_re: str = r"(?<=[\.])\s+", min_sentence_chars: int = 20, num_beams: int = 4, max_length: int = 256, ) -> list: """Extract relations from English text using REBEL, sentence by sentence. Same wire format as mREBEL — reuses `parse_rebel_output` and `align_relations_to_entities` from the registry. LICENSE: Apache 2.0 (commercial OK). """ ``` Practicamente identica a `extract_relations_mrebel` pero sin el `tgt_lang='tp_XX'` (REBEL es monolingue). --- ## Priorizacion sugerida | # | Item | Impacto | Coste | Cuando | |---|---|---|---|---| | B | `extract_graph_from_pdf` pipeline | ⭐⭐⭐ — la composicion mas usada | Bajo (compone existentes) | Inmediato | | C | spaCy ES V2 reglas | ⭐⭐ — desbloquea mas casos ES | Medio (reglas + tests) | Cuando V1 limita | | D | Fix kernel startup | ⭐⭐ — limpia el flow notebooks | Medio (refactor + migracion) | Cuando se cree un analysis nuevo | | A | NuExtract loader/extractor | ⭐ — engine alternativo opcional | Medio (GPU testing) | Cuando se quiera "Rich mode" | | E | REBEL EN extractor | ⭐ — solo si llega caso EN comercial | Bajo (copy de mREBEL) | Cuando aparezca el caso | --- ## Definicion de hecho (todos los items) - Funciones implementadas + frontmatter + tests pytest verdes. - `./fn index` suma exactamente las funciones declaradas. - `./fn check params` no marca ninguna nueva sin params_schema. - Documentadas en `vaults/osint_nlp_models/models/` o seccion correspondiente del vault. - Notas operativas en `app.md` del consumidor (graph_explorer) si toca uses_functions. ## Out of scope explicito - LLM-as-validator para mejorar relaciones (Claude Haiku post-NuExtract). El usuario indico explicitamente que no quiere LLMs pesados en el flow. - GLiDRE / ReLiK / AlignRE — solo si surge necesidad concreta. Listados en `vaults/osint_nlp_models/models/candidates.md`.