Files
fn_registry/python/functions/pipelines/extract_graph_hybrid.md
T
egutierrez 4f743e0840 feat(pipelines): extract_graph_hybrid (regex + GLiNER + GLiREL + LLM fallback)
Pipeline en cascada que combina extract_iocs (regex, coste 0), GLiNER
(zero-shot NER), GLiREL (zero-shot RE) y un fallback LLM opcional para
chunks con baja confianza o pocas entidades. Devuelve listas concatenadas
listas para deduplicate_entities/deduplicate_relations.

Cierra 0040.
2026-04-30 16:52:46 +02:00

8.9 KiB

name, kind, lang, domain, version, purity, signature, description, tags, uses_functions, uses_types, returns, returns_optional, error_type, imports, params, output, tested, tests, test_file_path, file_path
name kind lang domain version purity signature description tags uses_functions uses_types returns returns_optional error_type imports params output tested tests test_file_path file_path
extract_graph_hybrid pipeline py pipelines 1.0.0 impure def extract_graph_hybrid(chunks: list[str], entity_schema: list[dict], relation_types: list[str], gliner_model: Any, glirel_model: Any, llm_chat_json: Callablelist[dict, dict] | None = None, ioc_types: list[str] | None = None, confidence_threshold: float = 0.6, languages: str = 'Respond in Spanish.', min_entities_per_chunk: int = 2) -> tuple[list[EntityCandidate], list[RelationCandidate]] Pipeline hibrido en cascada que combina extract_iocs (regex, coste 0), GLiNER (zero-shot NER, coste bajo), GLiREL (zero-shot RE) y un LLM fallback opcional para chunks complejos o de baja confianza. Devuelve listas concatenadas listas para deduplicate_entities/deduplicate_relations.
pipeline
extraction
entities
relations
gliner
glirel
ioc
regex
llm
nlp
datascience
cybersecurity
hybrid
extract_iocs_py_cybersecurity
extract_entities_gliner_py_datascience
extract_relations_glirel_py_datascience
extract_entities_llm_py_datascience
extract_relations_llm_py_datascience
entity_candidate_py_datascience
relation_candidate_py_datascience
entity_candidate_py_datascience
relation_candidate_py_datascience
false error_go_core
typing.Any
typing.Callable
warnings
name desc
chunks Lista de fragmentos de texto ya cortados (p.ej. via split_text_into_chunks).
name desc
entity_schema Schema para GLiNER y LLM. Lista de dicts con type_ref, label y opcional metadata_fields.
name desc
relation_types Tipos de relacion permitidos para GLiREL/LLM (ej: ['operates','owns','communicates_with']).
name desc
gliner_model Instancia GLiNER cargada con gliner_load_model. Inyectada por el caller.
name desc
glirel_model Instancia GLiREL cargada con glirel_load_model. Inyectada por el caller.
name desc
llm_chat_json Cliente LLM inyectado (sin acoplamiento al proveedor). Si None, no hay fallback LLM.
name desc
ioc_types Subset de tipos para extract_iocs (email, ip_address, domain, file_hash, ...). None = todos.
name desc
confidence_threshold Por debajo de este umbral, GLiNER se considera de baja confianza y se invoca el LLM.
name desc
languages Instruccion de idioma passthrough al LLM (ej: 'Respond in Spanish.').
name desc
min_entities_per_chunk Si un chunk arroja menos entidades que esto, se invoca el LLM como fallback (default 2).
Tupla (entities, relations) con candidatas concatenadas (sin deduplicar). El caller debe pasar por deduplicate_entities y deduplicate_relations. true
corpus OSINT con IoCs y entidades semanticas devuelve mezcla regex+GLiNER
chunks vacios o con solo whitespace se saltan
entity_schema vacio lanza ValueError
chunks no-lista lanza ValueError
GLiNER produciendo pocas entidades dispara fallback LLM si llm_chat_json esta presente
sin llm_chat_json no se invoca ningun fallback LLM
GLiREL sin relaciones dispara fallback LLM relations
ioc_types acota el set de extractores regex
errores de extractores se capturan con warnings y no abortan el pipeline
python/functions/pipelines/tests/test_extract_graph_hybrid.py python/functions/pipelines/extract_graph_hybrid.py

Ejemplo

from python.functions.pipelines.extract_graph_hybrid import extract_graph_hybrid
from python.functions.datascience.gliner_load_model import gliner_load_model
from python.functions.datascience.glirel_load_model import glirel_load_model
from python.functions.datascience.deduplicate_entities import deduplicate_entities
from python.functions.datascience.deduplicate_relations import deduplicate_relations

gliner = gliner_load_model("urchade/gliner_multi-v2.1", device="auto")
glirel = glirel_load_model("jackboyla/glirel-large-v0", device="auto")

entity_schema = [
    {"type_ref": "osint_person_go_cybersecurity",       "label": "Person"},
    {"type_ref": "osint_organization_go_cybersecurity", "label": "Organization"},
    {"type_ref": "osint_location_go_cybersecurity",     "label": "Location"},
]
relation_types = ["operates", "owns", "communicates_with", "employed_by"]

chunks = [
    "Alice Johnson works at OpenAI in San Francisco. Contact: alice@openai.com.",
    "The C2 server lives at 192.168.0.1 and resolves to evil-corp.com.",
]

# Sin LLM (coste cero, solo regex + GLiNER + GLiREL)
entities, relations = extract_graph_hybrid(
    chunks=chunks,
    entity_schema=entity_schema,
    relation_types=relation_types,
    gliner_model=gliner,
    glirel_model=glirel,
    llm_chat_json=None,
)

# Con LLM fallback solo en chunks complejos
def llm_chat_json(messages):
    # llamar a OpenAI/Anthropic/Ollama y devolver el JSON ya parseado
    ...

entities, relations = extract_graph_hybrid(
    chunks=chunks,
    entity_schema=entity_schema,
    relation_types=relation_types,
    gliner_model=gliner,
    glirel_model=glirel,
    llm_chat_json=llm_chat_json,
    confidence_threshold=0.6,
    min_entities_per_chunk=2,
)

# Deduplicar antes de persistir
dedup = deduplicate_entities(entities, name_threshold=0.85)
final_relations = deduplicate_relations(relations, dedup.name_to_id)

Algoritmo

Por cada chunk:

  1. Regex (capa tecnica)extract_iocs(chunk, ioc_types) devuelve dicts {value, start, end, type} que se mapean a EntityCandidate con type_ref propio (ioc_email, ioc_ip_address, ioc_domain, ...) y confidence=1.0. Los offsets se anotan en attributes['start'/'end'] para que GLiREL pueda mapearlos a tokens sin fallback text.find.
  2. GLiNER (capa semantica)extract_entities_gliner con el schema y el confidence_threshold como filtro de score.
  3. Merge — IoCs + GLiNER deduplicados por (name, type_ref). NO se colapsa fuzzy aqui; eso lo hace el caller.
  4. LLM fallback (opcional) — si el chunk tiene menos de min_entities_per_chunk entidades o mean(gliner_confidence) < confidence_threshold y llm_chat_json is not None, se invoca extract_entities_llm y se mezcla.
  5. GLiREL (relaciones zero-shot) — solo si hay >=2 entidades.
  6. LLM fallback de relaciones (opcional) — si GLiREL no devolvio nada con >=2 entidades y hay llm_chat_json, se invoca extract_relations_llm para ese chunk.

source_chunk_indices y source_chunk_index se rellenan para que deduplicate_relations pueda reconstruir el grafo origen→destino.

Por que cascada y no all-LLM

Capa Coste por 100 KB Latencia Calidad
extract_iocs (regex) 0 <50 ms Precision 100% en IoCs tecnicos
GLiNER (gliner_multi-v2.1) 0 (modelo local, GPU/CPU) ~1-3 s/chunk en CPU, <0.5 s en GPU F1 0.7-0.85 en NER zero-shot
GLiREL (glirel-large-v0) 0 (modelo local) ~2-4 s/chunk en CPU F1 0.5-0.75 en RE zero-shot
LLM (GPT-4 / Claude Sonnet) $0.5-3 por 100 KB 5-15 s/chunk F1 0.85-0.95

El pipeline hibrido reserva el LLM (caro y lento) para los chunks que GLiNER/GLiREL no resuelven con suficiente confianza. En corpus OSINT tipicos el LLM se invoca en <20% de los chunks → coste total 5-10x menor que un pipeline 100% LLM con perdida de calidad <5 puntos F1.

Solapamiento IoC ↔ GLiNER

GLiNER puede detectar apple.com como Organization mientras que regex lo detecta como domain. Decision intencional: ambos se conservan con type_ref distinto (osint_organization_go_cybersecurity vs ioc_domain). deduplicate_entities(..., same_type_only=True) no las mezcla. El caller decide si quiere unificar (por ejemplo, anotando una relacion domain_of entre las dos).

Recomendaciones operativas

  • Batch size: ~100-200 chunks de 500-1000 caracteres por llamada al pipeline. Mas chunks → mas paralelismo aprovechable; menos chunks → menos overhead de carga del modelo.
  • Latencia esperada (CPU): ~3-5 s/chunk sin LLM, +5-15 s/chunk si cae al LLM fallback.
  • Latencia esperada (GPU): ~0.5-1 s/chunk sin LLM.
  • Cuando bajar confidence_threshold: en corpus con jerga muy especifica donde GLiNER no aprendio bien — pero esto incrementa el coste si hay LLM (mas chunks caen al fallback).
  • Cuando subir min_entities_per_chunk: si quieres forzar fallback LLM en chunks "ricos" para asegurar cobertura completa.

Notas

  • La deduplicacion fuzzy (Levenshtein + Union-Find) la hace deduplicate_entities — NO replicar aqui.
  • Los errores de cualquier extractor en cualquier chunk se capturan con warnings.warn y NO abortan el pipeline (robustez sobre completitud).
  • Las funciones LLM aceptan language_instruction; aqui se pasa como languages (default "Respond in Spanish.").
  • Pensar en una app apps/osint_extractor/ que use este pipeline + sigma viz como demo. Fuera de scope de este issue.