--- id: "0038" title: "GLiNER entity extractor (zero-shot NER)" status: completado type: feature domain: [] scope: multi-app priority: alta depends: [] blocks: [] related: [] created: 2026-05-17 updated: 2026-05-17 tags: [] --- # 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]`), 50–200x 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.