feat(datascience): GLiREL relation extractor (zero-shot triplets) drop-in con LLM
- glirel_load_model: cache por (model_name, device); device='auto' resuelve via torch - extract_relations_glirel: tokeniza por whitespace, mapea spans char->token, llama predict_relations y devuelve RelationCandidate; fallback text.find si la entidad llega sin offsets; max_pairs=N -> top-N por score - pyproject.toml: glirel en extra nlp Closes #0039 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
---
|
||||
name: extract_relations_glirel
|
||||
kind: function
|
||||
lang: py
|
||||
domain: datascience
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def extract_relations_glirel(text: str, entities: list[EntityCandidate], relation_types: list[str], model: Any, threshold: float = 0.5, max_pairs: int = 0) -> list[RelationCandidate]"
|
||||
description: "Extrae relaciones zero-shot con GLiREL. Drop-in del contrato de extract_relations_llm pero sin coste por token y mas rapido para corpus grandes. Tokeniza por whitespace, mapea spans de entidades (de attributes['start'/'end'] o fallback text.find) a indices de tokens, y devuelve RelationCandidate cuyos from_name/to_name siempre coinciden con entidades del input."
|
||||
tags: [glirel, relation, nlp, extract, zero-shot, knowledge-graph, fuzzygraph, graph, datascience, python]
|
||||
uses_functions: [glirel_load_model_py_datascience]
|
||||
uses_types:
|
||||
- entity_candidate_py_datascience
|
||||
- relation_candidate_py_datascience
|
||||
returns:
|
||||
- relation_candidate_py_datascience
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [warnings, re]
|
||||
params:
|
||||
- name: text
|
||||
desc: "mismo chunk de texto que se uso para extraer las entidades (parrafo, doc corto)"
|
||||
- name: entities
|
||||
desc: "lista de EntityCandidate ya extraidas (de extract_entities_gliner, extract_entities_llm o regex). Si tienen attributes['start'/'end'] se usan; si no, fallback a text.find(name) con warning."
|
||||
- name: relation_types
|
||||
desc: "tipos de relacion permitidos, ej: ['works_for','owns','communicated_with']. Vacio lanza ValueError."
|
||||
- name: model
|
||||
desc: "instancia GLiREL cargada con glirel_load_model. Inyectar para evitar penalty de carga en batch."
|
||||
- name: threshold
|
||||
desc: "score minimo para aceptar una relacion (0.0-1.0). Defecto 0.5."
|
||||
- name: max_pairs
|
||||
desc: "0 = todas las relaciones encontradas. >0 = top N por score (descarta el resto)."
|
||||
output: "lista de RelationCandidate(from_name, to_name, relation_type, description='', confidence). from_name/to_name siempre coinciden con entidades del input."
|
||||
tested: true
|
||||
tests:
|
||||
- "Schema basico y modelo stub retorna RelationCandidate triplets validos"
|
||||
- "Threshold se propaga al modelo"
|
||||
- "relation_types vacio lanza ValueError"
|
||||
- "Menos de 2 entidades retorna vacio"
|
||||
- "Entidad sin offsets usa fallback text.find con warning"
|
||||
- "Entidad cuyo nombre no aparece en el texto se descarta"
|
||||
- "Excepcion del modelo se captura y retorna vacio"
|
||||
- "Relation_type fuera del set permitido se descarta"
|
||||
- "max_pairs=N limita el output a top N por score"
|
||||
- "head_pos/tail_pos resuelven entidades por posicion de token"
|
||||
- "Fallback por head_text/tail_text si head_pos no esta presente"
|
||||
test_file_path: "python/functions/datascience/tests/test_extract_relations_glirel.py"
|
||||
file_path: "python/functions/datascience/extract_relations_glirel.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
from python.functions.datascience import (
|
||||
glirel_load_model,
|
||||
extract_relations_glirel,
|
||||
)
|
||||
from python.types.datascience.entity_candidate import EntityCandidate
|
||||
|
||||
model = glirel_load_model(device="auto")
|
||||
|
||||
text = "Alice Johnson works at OpenAI in San Francisco."
|
||||
entities = [
|
||||
EntityCandidate(name="Alice Johnson", type_label="Person",
|
||||
attributes={"start": 0, "end": 13}, confidence=0.92),
|
||||
EntityCandidate(name="OpenAI", type_label="Organization",
|
||||
attributes={"start": 23, "end": 29}, confidence=0.87),
|
||||
EntityCandidate(name="San Francisco", type_label="Location",
|
||||
attributes={"start": 33, "end": 46}, confidence=0.81),
|
||||
]
|
||||
|
||||
relations = extract_relations_glirel(
|
||||
text=text,
|
||||
entities=entities,
|
||||
relation_types=["works_for", "located_in", "owns"],
|
||||
model=model,
|
||||
threshold=0.5,
|
||||
)
|
||||
# [RelationCandidate(from_name='Alice Johnson', to_name='OpenAI',
|
||||
# relation_type='works_for', confidence=0.91), ...]
|
||||
```
|
||||
|
||||
## Drop-in con extract_relations_llm
|
||||
|
||||
El retorno es identico (`list[RelationCandidate]`) y `from_name`/`to_name` siempre
|
||||
coinciden con entidades del input — `deduplicate_relations_py_datascience` lo
|
||||
acepta sin cambios. Diferencias:
|
||||
|
||||
- **Coste**: GLiREL = 0 USD/token. LLM = depende del modelo.
|
||||
- **Latencia**: GLiREL es mucho mas rapido en GPU; en CPU depende del numero de
|
||||
pares (entidades x relation_types).
|
||||
- **Razonamiento implicito**: el LLM lo deduce ("CEO de la empresa" -> persona
|
||||
works_for empresa); GLiREL solo extrae lo explicito en el texto.
|
||||
- **Esquemas grandes**: GLiREL escala bien con muchos relation_types; el LLM
|
||||
pierde foco con esquemas muy largos.
|
||||
- **Idiomas**: GLiREL-large-v0 esta entrenado principalmente en ingles. Para ES
|
||||
evaluar precision/recall caso a caso o caer al LLM.
|
||||
|
||||
## Spans de entidades
|
||||
|
||||
GLiREL necesita los spans (token indices) de cada entidad en el texto. Esta funcion:
|
||||
|
||||
1. Lee `attributes["start"]` y `attributes["end"]` (offsets de caracteres) si
|
||||
existen — el output natural de `extract_entities_gliner` y `extract_iocs`.
|
||||
2. Si faltan, usa `text.find(entity.name)` como fallback (con warning).
|
||||
3. Tokeniza por whitespace y mapea cada char span a un span de tokens
|
||||
(`[start_token, end_token]`).
|
||||
4. Pasa todo a `model.predict_relations(tokens, labels=..., ner=...)`.
|
||||
|
||||
Si la entidad no se puede localizar en el texto, se descarta (no se le pueden
|
||||
buscar relaciones sin saber donde esta).
|
||||
|
||||
## Notas
|
||||
|
||||
- impure: el modelo es estado externo. `error_type: error_go_core` segun la regla
|
||||
de pureza del registry.
|
||||
- Si dos entidades tienen el mismo nombre, GLiREL podria mezclarlas; el matcheo
|
||||
por `head_pos`/`tail_pos` (token start) las distingue mejor que `head_text`.
|
||||
- Una `relation_type` que no aparece en el output NO es un error — solo significa
|
||||
que GLiREL no encontro evidencia.
|
||||
- Combinar con LLM para razonamiento implicito: ver issue 0040 (pipeline hibrido).
|
||||
- Para precision maxima, ajustar `threshold` por dominio: 0.3-0.4 = recall alto;
|
||||
0.6-0.8 = precision alta.
|
||||
|
||||
## Limitacion
|
||||
|
||||
GLiREL es bueno para relaciones explicitas en el texto (`X trabaja en Y`,
|
||||
`A llamo a B`), malo para razonamiento implicito (`la nueva CEO`, `su empresa`).
|
||||
Para razonamiento implicito seguir usando `extract_relations_llm`. El pipeline
|
||||
hibrido (issue 0040) compone GLiREL para extraccion masiva + LLM para los casos
|
||||
implicitos que GLiREL no cubre.
|
||||
Reference in New Issue
Block a user