# PRD: Entity Alias Dedup — resolver duplicatas de entities na extraction

> Generated via /vibeflow:discover on 2026-03-19

## Problema

Quando um usuário manda uma mensagem com pessoas referenciadas por múltiplos nomes (apelido + nome completo, nome coloquial + nome formal, role + nome), o SDK cria entities duplicadas pra mesma pessoa/lugar/coisa.

Exemplo real: "meu amigo Guili (Guilherme Maturana)" gera `person:guili` e `person:guilherme_maturana` — duas entities, facts duplicados, relations duplicadas. Inflação de ~2x em todas as métricas.

O LLM na extraction **entende** que são a mesma entidade (cria facts tipo "Guili's full name is Guilherme Maturana" e relations `same_as`). Mas o entity resolution não consegue casar os nomes porque "Guili" e "Guilherme Maturana" são strings completamente diferentes — nenhum algoritmo de similaridade de texto pega isso.

O problema é genérico — não se limita a apelido/nome completo:
- Escrita diferente: "Bokita" / "Boquita"
- Nome coloquial: "Sampa" / "São Paulo"
- Nome parcial → completo: "Pedro" / "Pedro Menezes"
- Variação de empresa: "Stone" / "StoneCo"
- Role → nome: "minha terapeuta" / "Dra. Carla"

Em teste com ~15 pessoas reais: 27 entity keys (esperado ~15), 80 facts (esperado ~40), 54 relations (esperado ~25). Zero merges em 10 pares de duplicatas.

## Público-alvo

Qualquer integrador do SDK que manda mensagens com múltiplas entities. O problema piora proporcionalmente à densidade de entities por mensagem.

## Solução proposta

Resolver o dedup **na extraction** (entity scan), que é o único momento do pipeline onde o LLM tem a mensagem original e entende o contexto. Não dá pra resolver no entity resolution porque ele recebe nomes isolados, sem contexto.

Duas mudanças:

### 1. Campo `aliases` no schema `ExtractedEntity`

Hoje:
```python
class ExtractedEntity(BaseModel):
    name: str
    type: Literal["person", "organization", "place", "pet", "other"]
```

Proposta:
```python
class ExtractedEntity(BaseModel):
    name: str
    type: Literal["person", "organization", "place", "pet", "other"]
    aliases: list[str] = Field(default_factory=list)
```

O LLM passa a poder expressar: `{"name": "Guilherme Maturana", "type": "person", "aliases": ["Guili"]}` — uma entity só, com os nomes alternativos.

### 2. Ajuste nos prompts de entity scan (multi-pass e single-pass)

Atualizar `ENTITY_SCAN_PROMPT`, `EXTRACTION_SYSTEM_PROMPT` e `REFLEXION_SYSTEM_PROMPT` pra incluir:
- Instrução clara sobre o campo `aliases`
- Regra: quando a mesma entidade aparece com nomes diferentes na mensagem, criar UMA entity com o `name` mais completo/formal e os outros nomes em `aliases`
- Exemplos concretos no prompt (apelido, escrita diferente, nome parcial)

### 3. Entity resolution registra aliases da extraction

No `resolve_entities()`, ao criar uma entity nova, registrar todos os `aliases` do `ExtractedEntity` na tabela `memory_entity_aliases`. Isso garante que em mensagens futuras, o Phase 1 (exact match) já encontra a entity pelo alias.

O entity resolution continua funcionando como segunda linha de dedup cross-mensagem (fuzzy match, LLM match contra o banco). Sem mudança nas phases existentes.

### 4. Rewrite de subjects em facts e relations

Depois que o entity scan devolve entities com aliases, os passos seguintes (fact extraction, relation extraction) precisam usar o `name` canônico, não os aliases. Garantir que o `subject` dos facts e `source`/`target` das relations referenciem o nome canônico da entity.

No multi-pass isso já é natural (os entity names são passados pro fact extraction). No single-pass, a extraction devolve tudo junto — se o LLM usou o alias como subject de um fact, precisamos normalizar antes de passar pro entity resolution.

## Critérios de sucesso

- Mensagem com ~15 pessoas (cada uma com apelido + nome completo) produz ~15 entity keys, não ~27
- Facts e relations não são duplicados por conta de entities duplicadas
- Em mensagens futuras, mencionar só o apelido resolve pra entity correta via alias
- Não adiciona chamada de LLM extra ao pipeline
- Backward-compatible: mensagens sem aliases continuam funcionando normalmente

## Escopo v0

- [ ] Campo `aliases` em `ExtractedEntity` (constants.py)
- [ ] Atualizar prompts: `ENTITY_SCAN_PROMPT`, `EXTRACTION_SYSTEM_PROMPT`, `REFLEXION_SYSTEM_PROMPT`
- [ ] Atualizar `_parse_extraction_output` e `_entity_scan` pra propagar aliases
- [ ] No entity resolution: registrar aliases ao criar entity nova
- [ ] Normalização de subjects: se um fact/relation usa um alias como subject, mapear pro nome canônico antes do entity resolution
- [ ] Testes: FakeLLMProvider retornando entities com aliases, validando dedup e alias registration

## Anti-escopo

- **Dedup cross-mensagem**: se mensagem 1 cria "Guili" e mensagem 2 cria "Guilherme Maturana" sem contexto de que são a mesma pessoa, isso não é resolvido aqui. O entity resolution existente (fuzzy + LLM) já tenta cobrir esse caso.
- **Merge de entities já existentes no banco**: se já existem duplicatas no banco, não fazemos retroactive merge. Isso seria uma feature separada de entity consolidation.
- **Chamada de LLM extra pra dedup**: a solução roda dentro da extraction existente, sem chamada adicional.
- **Bug da tabela `memory_entities` vazia**: reportado junto mas é problema separado (entidades criadas no entity resolution mas não aparecendo pra quem consulta).
- **`facts_deleted`/`facts_unchanged` retornando `int` vs `list`**: melhoria de API separada.

## Contexto técnico

### Arquivos afetados
- `src/arandu/constants.py` — `ExtractedEntity` schema (add `aliases` field)
- `src/arandu/write/extract.py` — prompts e parsing
- `src/arandu/write/entity_resolution.py` — `resolve_entities()` e `_create_entity()` (registrar aliases)
- `src/arandu/write/pipeline.py` — possível normalização de subjects entre extraction e resolution
- `tests/test_extract.py` — testes de extraction com aliases
- `tests/test_entity_resolution.py` — testes de alias registration

### Padrões a seguir (.vibeflow/)
- **Pipeline orchestration**: extraction não faz DB writes, entity resolution sim (via savepoints)
- **Dependency injection**: LLM calls via injected provider
- **Error handling**: fail-safe — se aliases field não vier, funciona como hoje (empty list default)
- **Testing**: FakeLLMProvider com respostas JSON fixas, sem DB

### Fluxo do pipeline após mudança
```
Extract (entity scan com aliases) → Entity Resolution (registra aliases) → Reconcile → Upsert → Relations
```
Nenhum passo novo. A mudança é interna à extraction (schema + prompt) e ao entity resolution (registrar aliases).

## Questões em aberto

Nenhuma.
