docs(issues): añadir 0037-0040 — extraccion de entidades y relaciones

- 0037: IoC regex extractor (IP, email, dominio, hash, wallet, CVE, MAC)
- 0038: GLiNER entity extractor (zero-shot NER multilingue)
- 0039: GLiREL relation extractor (zero-shot triplets)
- 0040: pipeline hibrido extraccion grafos (regex + GLiNER + GLiREL + LLM fallback)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-28 18:41:44 +02:00
parent 73e2f688b6
commit b837b8281a
5 changed files with 332 additions and 0 deletions
+82
View File
@@ -0,0 +1,82 @@
# 0037 — IoC regex extractor (cybersecurity)
## Metadata
| Campo | Valor |
|-------|-------|
| **ID** | 0037 |
| **Estado** | pendiente |
| **Prioridad** | alta |
| **Tipo** | feature — Python (`python/functions/cybersecurity/`) |
## Dependencias
Ninguna. Es la pieza de mayor ROI del plan: regex puros, sin modelos, 100% precision para IoCs tecnicos. Complementa (no sustituye) a GLiNER, que es malo para identificadores tecnicos.
**Desbloquea:** mejor precision del pipeline hibrido (0040). Permite filtrar IoCs antes del paso LLM/NER en cualquier flujo OSINT.
---
## Objetivo
Familia de funciones puras que extraen IoCs (Indicators of Compromise) de texto via regex. Cada una retorna `list[dict]` con `value`, `start`, `end` y `type` para que el caller pueda construir `EntityCandidate` o `triple` sin reparsing.
## Funciones a crear
| Function ID | Que extrae |
|---|---|
| `extract_iocs_py_cybersecurity` | Pipeline pure: corre todos los extractors abajo y devuelve lista unificada con `type` |
| `extract_ip_addresses_py_cybersecurity` | IPv4 + IPv6, valida rangos (no `999.999.999.999`) |
| `extract_emails_py_cybersecurity` | RFC 5322 simplificado |
| `extract_domains_py_cybersecurity` | FQDNs con TLD valido (lista compilada) |
| `extract_file_hashes_py_cybersecurity` | MD5 (32 hex), SHA1 (40), SHA256 (64), SHA512 (128) — devuelve `algorithm` |
| `extract_crypto_wallets_py_cybersecurity` | BTC (legacy + bech32), ETH (0x + 40 hex con checksum opcional) |
| `extract_cve_ids_py_cybersecurity` | `CVE-YYYY-NNNN+` |
| `extract_mac_addresses_py_cybersecurity` | `xx:xx:xx:xx:xx:xx` y `xx-xx-xx-xx-xx-xx` |
| `extract_phone_numbers_py_cybersecurity` | E.164 + formatos comunes ES/EU |
`extract_urls_py_cybersecurity` ya existe — no duplicar.
## Contrato
```python
def extract_<ioc_type>(text: str) -> list[dict]:
"""
Returns:
[{"value": str, "start": int, "end": int, "type": "<ioc_type>"}, ...]
"""
```
`extract_iocs_py_cybersecurity` (pipeline) compone los anteriores y unifica resultados:
```python
def extract_iocs(
text: str,
types: list[str] | None = None, # None = todos
) -> list[dict]
```
## Pureza
Todas `purity: pure`. Solo regex compilado y validacion estructural. Sin red, sin disco, deterministas.
## Deliverables
- 9 archivos `.py` + 9 `.md` en `python/functions/cybersecurity/`
- 1 archivo `.py` adicional para el pipeline `extract_iocs.py`
- Tests unitarios en `python/functions/cybersecurity/tests/test_extract_iocs.py` con corpus pequeño de positivos y negativos por tipo
- Frontmatter completo: `params`, `output`, `domain: cybersecurity`, `purity: pure`
## Validacion
```bash
./fn run extract_iocs_py_cybersecurity # corre tests
./fn show extract_iocs_py_cybersecurity # verifica frontmatter
```
Bench informal: 1 MB de texto < 100 ms por extractor en CPU.
## Notas
- No validar TLDs con DNS — solo lista estatica de TLDs validos (publicsuffix opcional pero no obligatorio).
- Devolver offsets `start`/`end` siempre — los necesitan los issues 0038-0040 para reconciliar con spans de GLiNER.
- IPs privadas (10.x, 192.168.x) se extraen igual; el filtrado de relevancia es del caller, no del extractor.
@@ -0,0 +1,82 @@
# 0038 — GLiNER entity extractor (zero-shot NER)
## Metadata
| Campo | Valor |
|-------|-------|
| **ID** | 0038 |
| **Estado** | pendiente |
| **Prioridad** | alta |
| **Tipo** | feature — Python (`python/functions/datascience/`) |
## Dependencias
Recomendado leer `extract_entities_llm_py_datascience`, `entity_candidate_py_datascience`, `build_entity_schema_prompt_py_datascience`. El contrato debe ser **drop-in** con la version LLM.
**Desbloquea:** 0039 (GLiREL), 0040 (pipeline hibrido). Permite extraccion masiva sin coste por token.
---
## Objetivo
Wrapper sobre GLiNER (`urchade/gliner_multi-v2.1` por defecto, multilingue ES/EN) que extrae entidades zero-shot a partir del mismo `entity_schema: list[dict]` que ya consume `extract_entities_llm`. Misma firma, mismo retorno (`list[EntityCandidate]`), 50200x mas rapido y sin coste por token.
## Funciones a crear
| Function ID | Rol |
|---|---|
| `gliner_load_model_py_datascience` | Carga + cachea el modelo. `purity: impure`, `kind: function` |
| `extract_entities_gliner_py_datascience` | Extractor por chunk. `purity: impure`, mismo contrato que la version LLM |
## Contrato
```python
# gliner_load_model.py
def gliner_load_model(
model_name: str = "urchade/gliner_multi-v2.1",
device: str = "auto", # "auto" | "cpu" | "cuda" | "cuda:0"
) -> "GLiNER": ...
# extract_entities_gliner.py
def extract_entities_gliner(
text: str,
entity_schema: list[dict], # mismo formato que extract_entities_llm
model: "GLiNER", # inyectado, cargado una sola vez
threshold: float = 0.5,
flat_ner: bool = True, # False = nested entities
) -> list[EntityCandidate]
```
`entity_schema` se traduce internamente: cada `{"name": "person", "label": "Person", ...}` se convierte al label que GLiNER espera. `EntityCandidate` se rellena con:
- `name` = span text
- `type_ref` = entity name del schema
- `type_label` = label legible del schema
- `confidence` = score de GLiNER
- `attributes["start"]`, `attributes["end"]` = offsets en el chunk
- `source_chunk_indices` lo setea el caller (igual que en la version LLM)
## Pureza
`gliner_load_model`: impure (lee disco/red la primera vez, descarga ~200 MB de HF).
`extract_entities_gliner`: impure (modelo es estado externo) — `error_type: error_go_core` siguiendo la regla del registry.
## Deliverables
- `python/functions/datascience/gliner_load_model.py` + `.md`
- `python/functions/datascience/extract_entities_gliner.py` + `.md`
- Tests en `python/functions/datascience/tests/test_extract_entities_gliner.py` con un corpus minimo (3-5 textos cortos ES/EN, schema con 4-5 tipos).
- `pyproject.toml`: añadir `gliner>=0.2.13` como dep opcional bajo `[project.optional-dependencies]` `nlp = ["gliner", ...]`. NO añadir a deps principales para no inflar `.venv` de quien no use NER.
- Documentar en el `.md`: `pip install gliner` (o `uv pip install gliner`), tamaño del modelo, latencia esperada CPU vs GPU.
## Validacion
1. `./fn run extract_entities_gliner_py_datascience` corre los tests.
2. Bench manual: medir tokens/seg en un texto de 10 KB con 8 labels, CPU y GPU si hay.
3. Compararlo con `extract_entities_llm` sobre el mismo corpus pequeño y registrar precision/recall en el `.md`.
## Notas
- Mantener el contrato **identico** al de la version LLM permite usar `deduplicate_entities`, `merge_entity_attributes` y todo el resto del pipeline sin cambios.
- GLiNER es malo con IoCs tecnicos (IPs, hashes, wallets) — eso lo cubre 0037. Documentar la limitacion.
- El modelo se carga UNA vez por proceso: el caller lo inyecta. No cargarlo dentro de `extract_entities_gliner` — penalty fatal en batch.
- Si la GPU no esta disponible, `device="auto"` debe caer a CPU sin error.
@@ -0,0 +1,76 @@
# 0039 — GLiREL relation extractor (zero-shot relations → triplets)
## Metadata
| Campo | Valor |
|-------|-------|
| **ID** | 0039 |
| **Estado** | pendiente |
| **Prioridad** | media |
| **Tipo** | feature — Python (`python/functions/datascience/`) |
## Dependencias
Bloquea-por: **0038** (las entidades vienen del extractor GLiNER o del LLM). Recomendado leer `extract_relations_llm_py_datascience` y `relation_candidate_py_datascience` para mantener contrato.
**Desbloquea:** 0040 (pipeline hibrido), extraccion de triplets `(sujeto, predicado, objeto)` masiva sin LLM.
---
## Objetivo
Wrapper sobre GLiREL (`jackboyla/glirel-large-v0`) que, dadas entidades ya extraidas y un set de tipos de relacion, devuelve `list[RelationCandidate]` con el mismo contrato que `extract_relations_llm`. Output natural en triplets `(from, relation_type, to)` reusables por `deduplicate_relations`, `merge_graphs` y `ops_to_sigma_json`.
## Funciones a crear
| Function ID | Rol |
|---|---|
| `glirel_load_model_py_datascience` | Carga + cachea el modelo. `purity: impure` |
| `extract_relations_glirel_py_datascience` | Extractor de relaciones. `purity: impure` |
## Contrato
```python
def glirel_load_model(
model_name: str = "jackboyla/glirel-large-v0",
device: str = "auto",
) -> "GLiREL": ...
def extract_relations_glirel(
text: str,
entities: list[EntityCandidate], # del paso anterior (GLiNER o LLM)
relation_types: list[str], # ej: ["works_for","owns","communicated_with"]
model: "GLiREL",
threshold: float = 0.5,
max_pairs: int = 0, # 0 = todas las parejas
) -> list[RelationCandidate]
```
GLiREL necesita los spans de las entidades (`attributes["start"]`/`end`), por eso 0038 los expone. Si la entidad viene del LLM y no tiene offsets, se buscan con `text.find(name)` como fallback (con warning).
## Pureza
Ambas `purity: impure`. `error_type: error_go_core`.
## Deliverables
- `python/functions/datascience/glirel_load_model.py` + `.md`
- `python/functions/datascience/extract_relations_glirel.py` + `.md`
- Tests con 2-3 textos donde las relaciones son explicitas (`X trabaja en Y`, `A llamo a B`), schema de 3-5 relation types.
- `pyproject.toml`: añadir `glirel` al extra `nlp`.
- Documentar limitacion: GLiREL es bueno para relaciones explicitas en el texto, malo para razonamiento implicito. Para esos casos seguir usando LLM (eso es lo que orquesta 0040).
## Validacion
```bash
./fn run extract_relations_glirel_py_datascience
```
Comparar precision/recall vs `extract_relations_llm` sobre el mismo corpus + entidades. Registrar en el `.md`.
## Notas
- Triplets: el output es `RelationCandidate(from_name, to_name, relation_type, ...)`. `from_name`/`to_name` deben coincidir con entidades del input — si no, descartar (igual que la version LLM).
- Si una `relation_type` no aparece en el output, no es un error — solo significa que GLiREL no encontro evidencia.
- `deduplicate_relations_py_datascience` ya soporta este formato sin cambios.
- Ver issue 0040 para combinar GLiREL con LLM-fallback en el mismo pipeline.
@@ -0,0 +1,88 @@
# 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.
+4
View File
@@ -42,3 +42,7 @@
| [0034](completed/0034-cpp-scientific-viz.md) | C++ scientific viz (treemap, sankey, chord, contour, voronoi) | completado | media | feature | — |
| [0035](0035-cpp-map-tiles.md) | C++ map_tiles (slippy map OSM) | pendiente | baja | feature | — |
| [0036](0036-cpp-image-canvas-webcam.md) | C++ image_canvas + webcam_texture | pendiente | baja | feature | — |
| [0037](0037-ioc-regex-extractor.md) | IoC regex extractor (IP, email, dominio, hash, wallet, CVE, MAC) | pendiente | alta | feature | — |
| [0038](0038-gliner-entity-extractor.md) | GLiNER entity extractor (zero-shot NER multilingue) | pendiente | alta | feature | 0039, 0040 |
| [0039](0039-glirel-relation-extractor.md) | GLiREL relation extractor (zero-shot triplets) | pendiente | media | feature | 0040 |
| [0040](0040-hybrid-extraction-pipeline.md) | Pipeline hibrido extraccion grafos (regex + GLiNER + GLiREL + LLM fallback) | pendiente | media | feature | — |