# 0040 — Pipeline hibrido extraccion entidades+relaciones (regex + GLiNER/GLiREL + LLM fallback)

## Metadata

| Campo | Valor |
|-------|-------|
| **ID** | 0040 |
| **Estado** | pendiente |
| **Prioridad** | media |
| **Tipo** | feature — Python pipeline (`python/functions/pipelines/`) |

## Dependencias

Bloquea-por: **0037** (IoC regex), **0038** (GLiNER), **0039** (GLiREL).

**Desbloquea:** flujo OSINT/grafo de produccion con coste predecible. Reusable desde apps en `apps/*/` y desde notebooks en `analysis/*/`.

---

## Objetivo

Pipeline que combina los tres extractores en cascada para obtener triplets `(entidad, relacion, entidad)` masivamente con buen coste/calidad:

1. **Capa regex** (0037) — extrae IoCs tecnicos con precision 100%. Coste 0.
2. **Capa GLiNER** (0038) — extrae entidades semanticas (person, organization, location, event...) zero-shot. Coste bajo.
3. **Capa GLiREL** (0039) — relaciones zero-shot entre las entidades de capa 1+2.
4. **Capa LLM fallback** (existente: `extract_entities_llm` + `extract_relations_llm`) — solo se invoca cuando `confidence < threshold` o sobre los chunks marcados como "complejos".

Output: `(list[EntityCandidate], list[RelationCandidate])` listos para `deduplicate_entities` → `deduplicate_relations` → `ops_to_sigma_json` / `ops_to_rdf_triples`.

## Funcion a crear

| Function ID | Rol |
|---|---|
| `extract_graph_hybrid_py_pipelines` | Pipeline orquestador. `kind: pipeline`, `purity: impure`, `uses_functions: [...]` con todos los anteriores |

## Contrato

```python
def extract_graph_hybrid(
    chunks: list[str],
    entity_schema: list[dict],
    relation_types: list[str],
    gliner_model,                           # inyectado
    glirel_model,                           # inyectado
    llm_chat_json: Callable | None = None,  # opcional; si None, sin fallback LLM
    ioc_types: list[str] | None = None,     # None = todos
    confidence_threshold: float = 0.6,      # bajo este umbral se llama LLM
    languages: str = "Respond in Spanish.",
) -> tuple[list[EntityCandidate], list[RelationCandidate]]
```

Logica interna por chunk:
1. `extract_iocs(chunk, ioc_types)` → `EntityCandidate` con `type_ref` tecnico (ip/email/hash/...).
2. `extract_entities_gliner(chunk, entity_schema, gliner_model)` → entidades semanticas.
3. Si hay chunks con < N entidades o confidence baja **y** hay `llm_chat_json` → `extract_entities_llm` sobre esos chunks; mergear.
4. `extract_relations_glirel(chunk, entities_del_chunk, relation_types, glirel_model)`.
5. Si baja cobertura de relaciones y hay LLM → `extract_relations_llm` sobre el chunk.
6. Devolver listas concatenadas (sin deduplicar — eso lo hace el caller con `deduplicate_*`).

## Pureza

`kind: pipeline` → `purity: impure` (regla del registry).
`uses_functions`: lista los 5+ extractores invocados. `error_type: error_go_core`.

## Deliverables

- `python/functions/pipelines/extract_graph_hybrid.py` + `.md`
- Test de integracion en `python/functions/pipelines/tests/test_extract_graph_hybrid.py` con un corpus pequeño (2-3 textos OSINT realistas, mock del LLM).
- `.md` documenta: cuando usar fallback LLM, latencia esperada por chunk, recomendacion de batch size.

## Validacion

```bash
./fn run extract_graph_hybrid_py_pipelines
```

Bench end-to-end sobre 100 KB de texto:
- Solo LLM (linea base actual): registrar tiempo y coste estimado.
- Pipeline hibrido: registrar tiempo, coste (solo chunks con fallback) y delta de calidad vs solo-LLM.

Registrar resultados en el `.md` para tener referencia historica.

## Notas

- La deduplicacion fuzzy (Levenshtein + Union-Find) ya esta hecha en `deduplicate_entities` — NO replicar aqui.
- IoCs y entidades semanticas pueden solapar (ej: GLiNER detecta `apple.com` como organization, regex como domain). Resolver dejando ambas con `type_ref` distinto y que `deduplicate_entities` con `same_type_only=True` no las mezcle. Documentar esta decision.
- Pensar en un app `apps/osint_extractor/` que use este pipeline + sigma viz como demo. Fuera de scope de este issue — proponer en proposals despues.
