Files
fn_registry/dev/issues/0051-extraction-pipeline-followups.md
T
egutierrez faac610745 feat: extraccion masiva footprint_aurgi (41 funcs + 4 types + stack Docker geo)
Extrae al registry funciones del proyecto interno footprint_aurgi:
- core (6): slugify_ascii, normalize_for_join, cp_provincia_es, infer_provincia_from_cp, safe_read_csv_fallback, csv_to_parquet_duckdb
- geo puras (7): haversine_km, point_in_ring, point_in_polygon, point_in_polygons_bbox, polygon_bbox, extent_with_padding, distance_bucket
- geo I/O (4): load_geojson_polygons, load_boundary_gdf, add_basemap_osm, add_basemap_with_timeout
- valhalla client (4): valhalla_route, valhalla_isochrone, valhalla_isochrones_async, valhalla_matrix_1_to_n
- datascience stats (7): trimmed_mean, geometric_mean, detect_distribution_type, best_central_tendency, summary_stats, kde_density_levels, alpha_shape_concave_hull
- datascience fuzzy (3): fuzzy_merge_adaptive (rapidfuzz), words_to_dataset, remove_words_from_column
- datascience viz (2): plot_kde_2d, plot_heatmap_log
- infra (4): compress_pdf_ghostscript, render_table_page_pdfpages, add_header_logo, osm2pgsql_ingest
- pipelines (4): setup_geo_stack_docker, compute_centers_reachability, generate_isochrones_by_zone, count_points_per_zone
- types geo (4): LonLat, BBox, IsochroneRequest, Centro

Incluye:
- apps/footprint_geo_stack/ (PostGIS + Martin + Valhalla via docker-compose)
- 131/132 tests pasan (1 skip esperado: osm2pgsql en PATH)
- Issue tracker dev/issues/0052-footprint-aurgi-extraction.md
- Atribucion uniforme: source_repo internal:footprint_aurgi, source_license internal-aurgi
- Build con 9 agentes en paralelo (8 wave 1 + 1 wave 2 pipelines)

Tambien commitea trabajo previo no commiteado: aggregate_extraction_results, chunk_with_overlap, clean_pdf_text, merge_entity_aliases, extract_graph_gliner2, extract_relations_mrebel, extract_triples_spacy_es, gliner2/mrebel/marianmt/rebel/spacy_es load_model, parse_rebel_output, translate_es_to_en, issue 0050/0051.

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

12 KiB
Raw Blame History

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

# 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.