---
name: filter_relations_by_entity_types
kind: function
lang: py
domain: core
version: "1.0.0"
purity: pure
signature: "def filter_relations_by_entity_types(relations: dict, name_to_type: dict, allowed: dict) -> tuple[list, list]"
description: "Post-filtrado tipado de relaciones NER+RE: descarta pares donde los tipos de entidad (head_type, tail_type) no coinciden con los permitidos por relation kind. Ej: descarta 'Madrid president_of Persona' porque Madrid es location no person."
tags: [nlp, relations, filter, entity-types, graph, ner, re, post-process, gliner2]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
params:
  - name: relations
    desc: "Dict {rel_type: [(head_name, tail_name), ...]}. Los nombres deben ser strings no vacios. Ej: {'president_of': [('Carlos Torres', 'BBVA')]}"
  - name: name_to_type
    desc: "Dict {nombre_lowercased: entity_type}. Se construye del resultado de extract_graph_gliner2 o aggregate_extraction_results. Ej: {'carlos torres': 'person', 'bbva': 'organization'}"
  - name: allowed
    desc: "Dict {rel_type: (allowed_head_types, allowed_tail_types)}. Cada valor es una tupla de dos listas de strings. Si un rel_type no esta en allowed, todos sus pares se aceptan. Ej: {'president_of': (['person'], ['organization'])}"
output: "Tupla (kept, dropped). Cada elemento es lista de dicts {from, kind, to, head_type, tail_type}. kept tiene los validos, dropped los rechazados (util para debugging)."
tested: true
tests:
  - "pares validos se incluyen en kept"
  - "pares con tipos incompatibles van a dropped"
  - "rel_type no en allowed se acepta siempre"
  - "entidad no encontrada en name_to_type va a dropped"
test_file_path: "python/functions/core/tests/test_filter_relations_by_entity_types.py"
file_path: "python/functions/core/filter_relations_by_entity_types.py"
notes: |
  Validado en playground/server.py del analisis gliner_glirel_tuning.
  La regla (head_type, tail_type) evita falsos positivos comunes en grafos
  de conocimiento como "Madrid preside Santander" (Location -> Organization).
  El parametro dropped permite inspeccionar facilmente que relaciones se
  eliminaron y por que (head_type/tail_type None indica entidad desconocida).
---

## Ejemplo

```python
from core.filter_relations_by_entity_types import filter_relations_by_entity_types

relations = {
    "president_of": [
        ("Carlos Torres", "BBVA"),        # person -> organization: OK
        ("Madrid", "Santander"),           # location -> organization: INVALIDO
    ],
    "unknown_rel": [("A", "B")],          # no en allowed: se acepta
}
name_to_type = {
    "carlos torres": "person",
    "bbva": "organization",
    "madrid": "location",
    "santander": "organization",
    "a": "person", "b": "person",
}
allowed = {
    "president_of": (["person"], ["organization"]),
}
kept, dropped = filter_relations_by_entity_types(relations, name_to_type, allowed)
# kept:    [{"from": "Carlos Torres", "kind": "president_of", "to": "BBVA", ...},
#           {"from": "A", "kind": "unknown_rel", "to": "B", ...}]
# dropped: [{"from": "Madrid", "kind": "president_of", "to": "Santander", ...}]
```
