docs(issues): cerrar 0038 — GLiNER entity extractor

- Move dev/issues/0038-gliner-entity-extractor.md a completed/
- Update README link y estado a completado

Closes #0038

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-30 16:33:53 +02:00
parent 0a76353a13
commit fa9b1d449d
2 changed files with 1 additions and 1 deletions
@@ -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.