Files
fn_registry/dev/issues/0051-extraction-pipeline-followups.md
T
egutierrez 7913116a8e chore: auto-commit (129 archivos)
- .claude/agents/fn-analizador/SKILL.md
- .claude/agents/fn-constructor/SKILL.md
- .claude/agents/fn-executor/SKILL.md
- .claude/agents/fn-mejorador/SKILL.md
- .claude/agents/fn-orquestador/SKILL.md
- .claude/agents/fn-recopilador/SKILL.md
- .claude/commands/app.md
- .claude/commands/compile.md
- .claude/commands/cpp-app.md
- .claude/commands/create_functions.md
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 22:23:12 +02:00

329 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: "0051"
title: "Funciones pendientes del pipeline de extraccion (NER+RE+OpenIE)"
status: pendiente
type: feature
domain: []
scope: multi-app
priority: media
depends: []
blocks: []
related: []
created: 2026-05-17
updated: 2026-05-17
tags: []
---
# 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/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 <domain> import <function_name>`.
### 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`.