Files
fn_registry/dev/issues/0051-extraction-pipeline-followups.md
T
egutierrez fc4180cbb3 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

12 KiB
Raw Blame History

id, title, status, type, domain, scope, priority, depends, blocks, related, created, updated, tags
id title status type domain scope priority depends blocks related created updated tags
0051 Funciones pendientes del pipeline de extraccion (NER+RE+OpenIE) pendiente feature
multi-app media
2026-05-17 2026-05-17

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)

"""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)

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

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: movilizaramovilizarar (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:

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:

_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:

# 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

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.