# Write Pipeline

Quando você chama `memory.write()`, o SDK lê uma mensagem em linguagem natural e automaticamente extrai quem e o que foi mencionado, descobre se é informação nova ou atualizada, e armazena tudo como fatos estruturados e versionados - numa única chamada.

**Você não precisa entender os internals pra usar.** Basta chamar `write()` e verificar `result.facts_added`. Esta página explica o que acontece por baixo dos panos, pra quando você quiser ajustar o comportamento ou debugar resultados.

```mermaid
flowchart LR
    A["Message"] --> B["Alias Lookup +\nPré-retrieval"]
    B --> C["Extração Informada\n(1 chamada LLM)"]
    C --> D["Resolve Entities"]
    D --> E["Upsert"]
    E --> F["WriteResult"]
```

## Visão Geral

Toda chamada a `memory.write(agent_id, message, speaker_name)` (opcionalmente com `occurred_at`) executa estes passos:

0. **Guard** - Mensagens vazias retornam imediatamente. Sem evento, sem chamada LLM, sem tokens consumidos.
1. **Registrar o evento** - A mensagem bruta e salva como trilha de auditoria imutavel (nunca modificada ou deletada). O timestamp do evento usa `occurred_at` se fornecido, caso contrario usa o momento atual.
2. **Detectar emoção** - Classifica a emoção, intensidade e nível de energia da mensagem.
3. **Alias lookup + Pré-retrieval** - Escaneia a mensagem por entidades conhecidas e carrega contexto existente (perfis de entidade ou fatos via pgvector).
4. **Extração informada** - Uma única chamada LLM recebe a mensagem + contexto existente e retorna entidades, fatos (com action NEW/UPDATE e importance_category) e relações.
5. **Resolver entidades** - Deduplica menções ("Ana", "minha esposa Ana", "Aninha") em uma única entidade canônica.
6. **Upsert** - Salva os resultados no banco de dados.

!!! note "Fallback para extração cega"
    Se a extração informada falhar (timeout, erro do LLM, etc.), o pipeline automaticamente cai para o fluxo antigo de extração cega (entity scan + fact extraction + relation extraction) seguido de reconciliação. O resultado final é equivalente - apenas o caminho é diferente.

Cada estágio é independentemente fail-safe: se a extração falhar, o evento ainda é registrado. Se a reconciliação falhar para um fato, os outros prosseguem normalmente.

---

## Estágio 1: Extração Informada por Memória

**Em português claro:** Em vez de mandar a mensagem pro LLM "às cegas", o SDK primeiro descobre o que já sabe sobre as entidades mencionadas. Depois manda a mensagem junto com esse contexto existente pro LLM, que consegue decidir de forma mais inteligente o que e novo e o que ja e conhecido. Resultado: menos duplicatas, menos ruido, e fatos ja conhecidos nao sao re-extraidos.

A partir da v0.9.0, o write pipeline usa **extração informada** como caminho padrão. Em vez de 3-4 chamadas LLM no fluxo antigo (entity scan + fact extraction + relation extraction + reconciliation), o novo fluxo faz tudo em 3 fases, sendo apenas 1 delas uma chamada LLM.

### Como Funciona

A extração informada opera em 3 fases:

1. **Alias lookup** (sem LLM) - Escaneia a mensagem procurando nomes de entidades conhecidas via word-boundary matching no cache de aliases. Identifica quais entidades mencionadas na mensagem ja existem no sistema.

2. **Pre-retrieval** (sem LLM) - Para cada entidade conhecida encontrada no passo anterior, carrega contexto existente:
    - Quando a entidade tem um **perfil** (`profile_text`), usa o perfil diretamente (mais compacto e informativo)
    - Quando nao tem perfil, busca os top-K fatos existentes por entidade via pgvector
    - O orcamento de contexto e controlado por `informed_extraction_context_budget_tokens` (padrao 800 tokens)

3. **Extracao informada** (1 chamada LLM) - Recebe a mensagem + speaker context + contexto existente (perfis ou fatos) e retorna um unico JSON com:
    - **Entidades** com aliases
    - **Fatos** com `action` (NEW ou UPDATE) e `importance_category`
    - **Relacoes** entre entidades
    - **Perfis atualizados** (`updated_profiles`) para as entidades mencionadas

O prompt de extracao instrui o LLM a **extrair TODA informacao factual** da mensagem, incluindo detalhes especificos. A deduplicacao fica por conta do pipeline (reconciliacao ADD/UPDATE/NOOP/DELETE e dedup semantico pos-extracao). Essa abordagem maximiza o recall -- detalhes especificos que antes eram filtrados pelo LLM agora sao capturados e reconciliados pelo sistema.

**Fallback:** Se a extracao informada falhar (timeout, JSON invalido, etc.), o pipeline cai automaticamente pro fluxo antigo de extracao cega + reconciliacao. O resultado final e equivalente.

### Importancia Semantica (importance_category)

Cada fato extraido recebe uma **categoria de importancia semantica** atribuida pelo LLM durante a extracao. A categoria e mapeada para um valor numerico que alimenta o score de importancia do fato:

| Categoria | Valor | Descricao |
|-----------|-------|-----------|
| `biographical_milestone` | 0.95 | Marcos de vida (nascimento, casamento, formatura) |
| `relationship_change` | 0.90 | Mudancas em relacionamentos (novo emprego, mudanca de cidade) |
| `stable_preference` | 0.75 | Preferencias estaveis (comida favorita, hobby) |
| `specific_event` | 0.65 | Eventos especificos (viagem, reuniao) |
| `routine_activity` | 0.50 | Atividades rotineiras (almocou no restaurante X) |
| `conversational` | 0.30 | Informacao conversacional sem valor duradouro |

Isso substitui a importancia base fixa (0.5) do fluxo antigo por uma avaliacao semantica que reflete melhor o valor da informacao.

### Extracao Cega (Fallback)

O fluxo antigo de extracao cega ainda existe como fallback e roda 3 chamadas LLM, com as duas ultimas concorrentes:

1. **Entity scan** - Identifica todas as entidades mencionadas na mensagem
2. **Fact extraction + Relation extraction** - Rodam concorrentemente via `asyncio.gather()`: fact extraction recebe todas as entidades numa unica chamada, enquanto relation extraction identifica relacionamentos entre entidades

A extracao de relations inclui um **retry automatico**: se o LLM retornar 0 relations mas 2+ entities foram encontradas, o SDK repete a chamada uma vez. Quando `verbose=True`, o trace inclui `relation_retry_triggered`.

Quando o fluxo cego e usado, o pipeline tambem executa o **estagio de reconciliacao** (comparando fatos novos contra existentes) que nao e necessario na extracao informada, ja que o LLM ja decide NEW/UPDATE na fonte.

### Resolucao Temporal (`occurred_at`)

O parametro `occurred_at` permite especificar a data/hora em que a mensagem foi originalmente enviada. Por padrao e `datetime.now(UTC)`.

```python
from datetime import datetime, UTC

# Importar conversa historica de junho de 2025
result = await memory.write(
    agent_id="user_123",
    message="Yesterday I moved to a new apartment in Brooklyn",
    speaker_name="Rafael",
    occurred_at=datetime(2025, 6, 15, tzinfo=UTC),
)
# O LLM resolve "yesterday" para 2025-06-14 (baseado no occurred_at),
# nao para a data de hoje
```

O `occurred_at` e usado em dois lugares:

1. **Timestamp do evento** - O `MemoryEvent.occurred_at` recebe esse valor (em vez de `now`)
2. **Resolucao temporal na extracao** - O prompt de extracao recebe a data da mensagem como contexto (`"This message was sent on June 15, 2025"`). Quando a mensagem contem referencias temporais relativas ("yesterday", "last week", "three years ago"), o LLM as resolve para datas absolutas baseadas nessa data, nao na data atual

Isso e essencial para **imports historicos**: sem `occurred_at`, importar uma mensagem de 2024 que diz "yesterday" resolveria "yesterday" para ontem (data atual), produzindo fatos incorretos.

**Extracao subject-centric:** Facts sao extraidos do ponto de vista do subject principal apenas. "Carlos mora em Curitiba" e um fact sobre Carlos - o sistema NAO cria "Curitiba e onde Carlos mora". O relacionamento `Carlos -> lives_in -> Curitiba` + entity links cuidam do retrieval cross-entity.

**Dedup semantico:** Apos a extracao (informada ou cega), facts sao comparados par a par por cosine similarity de embeddings. Near-duplicates (> 0.85 de similaridade) sao removidos, mantendo a primeira ocorrencia. Isso elimina reformulacoes cross-entity que o LLM as vezes produz apesar das instrucoes do prompt.

### Configuração

| Parâmetro | Default | Descrição |
|-----------|---------|-----------|
| `extraction_timeout_sec` | `30.0` | Timeout por chamada LLM |
| `enable_informed_extraction` | `True` | Habilita extração informada por memória. Quando `False`, usa o fluxo antigo de extração cega + reconciliação |
| `informed_extraction_topk` | `10` | Número máximo de fatos existentes a carregar por entidade no pré-retrieval (quando perfil não está disponível) |
| `informed_extraction_context_budget_tokens` | `800` | Orçamento máximo de tokens para o contexto existente enviado ao LLM na extração informada |

### O Que é Extraído

Para cada mensagem, o estágio de extraction produz:

- **Entidades** - Coisas nomeadas: pessoas, organizações, lugares, conceitos, etc.
- **Fatos** - Afirmações auto-contidas sobre entidades em linguagem natural (ex: "Fernanda Lima é engenheira de software", "Marcos Tavares mora em Porto Alegre"). Cada texto de fato sempre inclui o nome da entidade - nunca apenas "é engenheira de software" sem um sujeito. Toda relação também gera um fato correspondente - então "Sarah é minha esposa" produz tanto uma relação (`user → spouse_of → sarah`) quanto um fato ("Sarah é esposa do user"). Fatos duplicados (mesmo subject + mesmo texto, ignorando pontuação) são removidos automaticamente pós-extração.
- **Relações** - Conexões entre entidades (ex: "Rafael" → `works_at` → "Acme Corp"). Relações servem como arestas de grafo pra traversal; o fato pareado torna a informação pesquisável via texto/embedding.

Cada fato inclui um **nível de confiança**:

| Nível | Score | Exemplo |
|-------|-------|---------|
| Declaração explícita | 0.95 | "Eu moro em São Paulo" |
| Inferência forte | 0.80 | "Fomos ao escritório de São Paulo" (implica localização) |
| Inferência fraca | 0.60 | Implicação contextual |
| Especulação | 0.40 | Informação incerta |

!!! note "Como a confiança funciona na prática"
    A confiança é atribuída pelo LLM durante a extração com base em como a informação foi declarada. Afirmações diretas ("Eu moro em SP") recebem alta confiança; afirmações incertas ("Acho que talvez...") recebem confiança mais baixa. Você não pode definir a confiança diretamente - ela é inferida. Você pode filtrar fatos de baixa confiança no retrieval usando `min_confidence` no MemoryConfig (padrão 0.55).

??? example "Passo a passo: como a extração informada funciona"
    **Mensagem:** "Clara Rezende saiu da Vertix e foi pra Orion Tech como head de engenharia. O Thiago Nogueira a contratou pessoalmente."

    **Contexto existente:** Clara Rezende e Vertix já existem no sistema. Clara tem perfil: "Software engineer at Vertix since 2023."

    **Fase 1 - Alias lookup** (sem LLM):
    ```
    Entidades conhecidas encontradas: [Clara Rezende, Vertix]
    Entidades novas: [Orion Tech, Thiago Nogueira]
    ```

    **Fase 2 - Pré-retrieval** (sem LLM):
    ```
    Clara Rezende → perfil carregado: "Software engineer at Vertix since 2023."
    Vertix → sem perfil, carrega top-10 fatos via pgvector
    ```

    **Fase 3 - Extração informada** (1 chamada LLM):
    ```json
    {
      "entities": [
        {"name": "Clara Rezende", "type": "person"},
        {"name": "Vertix", "type": "organization"},
        {"name": "Orion Tech", "type": "organization"},
        {"name": "Thiago Nogueira", "type": "person"}
      ],
      "facts": [
        {"subject": "Clara Rezende", "text": "Clara Rezende left Vertix", "action": "NEW", "importance_category": "relationship_change"},
        {"subject": "Clara Rezende", "text": "Clara Rezende joined Orion Tech as head of engineering", "action": "NEW", "importance_category": "relationship_change"},
        {"subject": "Thiago Nogueira", "text": "Thiago Nogueira personally hired Clara Rezende", "action": "NEW", "importance_category": "specific_event"}
      ],
      "relations": [
        {"source": "Clara Rezende", "target": "Vertix", "type": "former_employee_of"},
        {"source": "Clara Rezende", "target": "Orion Tech", "type": "works_at"},
        {"source": "Thiago Nogueira", "target": "Clara Rezende", "type": "hired"}
      ],
      "updated_profiles": {
        "Clara Rezende": "Former software engineer at Vertix. Now head of engineering at Orion Tech, hired personally by Thiago Nogueira."
      }
    }
    ```
    Note que o LLM marcou todos como `action: "NEW"` porque, apesar de já conhecer Clara, a informação sobre a saída e o novo cargo são novas. O fato existente "works at Vertix" não foi re-extraído.

    **Passo 4 - Dedup semântico:**
    Nenhum near-duplicate encontrado (todos os fatos são distintos). 3 fatos passam.

    **Resultado:** 4 entidades, 3 fatos, 3 relações, 1 perfil atualizado. Total: 1 chamada LLM.

??? example "Passo a passo: como a extração cega funciona (fallback)"
    **Mensagem:** "Clara Rezende saiu da Vertix e foi pra Orion Tech como head de engenharia. O Thiago Nogueira a contratou pessoalmente."

    **Passo 1 - Entity scan** (1 chamada LLM):
    ```
    Entities: [Clara Rezende (person), Vertix (organization), Orion Tech (organization), Thiago Nogueira (person)]
    ```

    **Passo 2a - Fact extraction** (1 chamada LLM, todas as entidades):
    ```
    Facts:
      [Clara Rezende] "Clara Rezende left Vertix" (0.95)
      [Clara Rezende] "Clara Rezende joined Orion Tech as head of engineering" (0.95)
      [Thiago Nogueira] "Thiago Nogueira personally hired Clara Rezende" (0.95)
    ```

    **Passo 2b - Relation extraction** (1 chamada LLM, concorrente com 2a):
    ```
    Relations:
      Clara Rezende → former_employee_of → Vertix
      Clara Rezende → works_at → Orion Tech
      Thiago Nogueira → hired → Clara Rezende
    ```

    **Passo 3 - Dedup semântico:**
    Nenhum near-duplicate encontrado (todos os fatos são distintos). 3 fatos passam.

    **Resultado:** 4 entidades, 3 fatos, 3 relações. Total: 3 chamadas LLM (+ reconciliação depois).

### Agrupamento de Aliases e Normalização de Subjects

Quando a mesma entidade é mencionada por múltiplos nomes em uma única mensagem (ex: "meu amigo Guili (Guilherme Maturana)"), a extração agrupa tudo em uma **única entidade com aliases** ao invés de criar duplicatas.

O LLM é instruído a escolher um nome canônico (geralmente o mais completo) e listar os outros como aliases:

```json
{
  "entities": [
    {"name": "Guilherme Maturana", "type": "person", "aliases": ["Guili"]}
  ]
}
```

Após a extração, um passo de **normalização de subjects** reescreve qualquer fato ou relação que referencia um alias para usar o nome canônico. Relações de identidade (ex: `same_as` entre um alias e seu nome canônico) são removidas automaticamente, já que se tornam auto-referenciantes após a normalização.

Isso elimina a duplicação de entidades intra-mensagem na origem - antes mesmo do entity resolution rodar.

### Entity Types

Entity types são **strings livres** - o LLM escolhe o tipo mais apropriado pra cada entidade. Tipos comuns incluem `person`, `organization`, `place`, `product`, `event`, `concept`, `pet`, mas qualquer tipo descritivo é aceito. Types são normalizados pra lowercase durante a entity resolution (ex: `"Person"` → `"person"`, `"PRODUCT"` → `"product"`).

O prompt de extração instrui o LLM a classificar types com cuidado - por exemplo, cidades são `place`, empresas são `organization`, produtos de software são `product`.

### Comportamento Fail-safe

Se uma chamada LLM falhar (timeout, JSON inválido, rate limit), a extraction retorna um resultado vazio em vez de lançar uma exceção. O evento ainda é registrado - nenhum dado é perdido. A próxima mensagem pode capturar a mesma informação.

!!! tip "Detectando timeouts"
    Quando a extração sofre timeout, o resultado é indistinguível de "mensagem sem conteúdo extraível" - 0 entidades, 0 fatos, sem exceção. Para detectar timeouts, compare o `duration_ms` da extração no trace com o `extraction_timeout_sec` configurado, ou verifique 0 entidades apesar de uma mensagem com conteúdo.

!!! info "Paralelo com neurociência"
    A extraction espelha a **codificação** na memória humana - o processo de converter input sensorial (uma conversa) em um traço de memória. Assim como a codificação humana é seletiva (não lembramos cada palavra), o LLM extrai apenas fatos e entidades salientes. A extração informada vai além: assim como o cérebro humano codifica informação nova em relação ao que já sabe (resposta de orientação), o LLM recebe contexto existente e decide na fonte o que é genuinamente novo. Isso espelha a codificação orientada por esquema - novos estímulos são processados no contexto de esquemas mentais existentes, não no vácuo.

### Perfis de Entidade no Write Pipeline

A extração informada produz e consome **perfis de entidade** (`profile_text`) - resumos concisos (~100-300 tokens) que capturam o essencial de cada entidade.

**Como input:** Quando a extração informada roda, perfis existentes são carregados durante o pré-retrieval e enviados ao LLM como contexto. Perfis são mais compactos que fatos individuais e fornecem uma visão consolidada da entidade, permitindo ao LLM tomar decisões melhores sobre o que é novo vs. conhecido.

**Como output:** O LLM retorna `updated_profiles` como parte do JSON de saída. Quando a mensagem contém informação que muda o perfil de uma entidade (novo emprego, mudança de cidade, etc.), o LLM retorna o perfil atualizado. Perfis são persistidos na mesma transação do upsert.

**Cold start:** Na primeira mensagem sobre uma entidade (quando ainda nao existe perfil), o perfil inicial e criado a partir dos fatos extraidos.

**Semeadura vs consolidacao:** O write pipeline so semeia perfis para entidades que ainda nao possuem um (`profile_text IS NULL`). Perfis existentes nao sao sobrescritos pelo write pipeline. A fonte autoritativa para perfis abrangentes e o background job [`consolidate_entity_profiles()`](background-jobs.md#job-3-consolidacao-de-perfis-de-entidade), que le todos os fatos por entidade e gera perfis completos.

!!! note "`profile_text` vs `summary_text`"
    `profile_text` é gerado inline no write pipeline durante a extração informada. `summary_text` é gerado por background jobs (sleep-time compute). São mecanismos separados que coexistem: o perfil é atualizado em tempo real a cada write, enquanto o summary é recalculado periodicamente em background.

!!! note "Lingua do conteudo gerado"
    Todo conteudo gerado por LLM -- perfis de entidade, summaries, resumos de cluster, meta-observations e diretivas -- e sempre produzido em **ingles**, independente da lingua da conversa. Isso garante consistencia no retrieval e na comparacao semantica. Os **fatos** (`fact_text`), por outro lado, permanecem na lingua original da conversa (ex: se o usuario fala em portugues, os fatos sao extraidos em portugues).

---

## Estágio 2: Entity Resolution

**Em português claro:** Quando alguém fala "Ana", "minha esposa Ana" e "Aninha" em mensagens diferentes, estão falando da mesma pessoa. Este estágio descobre isso e linka tudo a uma única entidade canônica - pra você não acabar com três registros separados de "Ana" no banco.

### Resolução em Três Fases

```mermaid
flowchart LR
    A["Entity name"] --> B{"Exact match?"}
    B -->|Yes| F["Resolved"]
    B -->|No| C{"Fuzzy match?"}
    C -->|"≥ 0.85"| F
    C -->|"0.50–0.85"| D{"LLM decides"}
    C -->|"< 0.50"| E["Create new entity"]
    D -->|Match| F
    D -->|No match| E
    E --> F
```

**Fase 1: Exact match**

Verifica o cache de aliases, slugs de entidades e display names. Instantâneo, sem chamada LLM.

Inclui **prefix/diminutive matching** para entidades do tipo pessoa: "Carol" corresponde a "Carolina" (mínimo 3 caracteres). Nota: "Jo" NÃO corresponde a "João" (< 3 chars). "Bob" corresponde a "Roberto" apenas se registrado como alias, não via prefix matching.

**Fase 2: Fuzzy match**

Usa similaridade de cosseno via embedding (in-memory) para encontrar candidatos:

- **≥ `fuzzy_threshold`** (default 0.85) - Match de alta confiança, resolve diretamente
- **0.50 - `fuzzy_threshold`** - Ambíguo, encaminha top-3 candidatos para a Fase 3 (LLM)
- **< 0.50** - Sem match, cria uma nova entidade

Reduzir `fuzzy_threshold` expande a faixa de fuzzy-resolve e reduz chamadas LLM. Por exemplo, definir `fuzzy_threshold=0.50` elimina a faixa ambígua inteiramente - tudo acima de 0.50 resolve diretamente.

Faz fallback para `difflib.SequenceMatcher` quando embeddings não estão disponíveis.

**Fase 3: LLM fallback**

Envia candidatos ambíguos para o `LLMProvider` injetado para desambiguação. O LLM vê o nome da entidade, os candidatos, e decide qual (se algum) é um match.

??? example "Passo a passo: como a entity resolution funciona"
    **Mensagem:** "Falei com o Guili sobre o projeto. O Guilherme disse que tá no prazo."

    1. **Extração:** Dois nomes encontrados: "Guili" e "Guilherme"
    2. **Fase 1 (exata):** "Guilherme" bate com a entidade existente `person:guilherme_maturana`
    3. **Fase 2 (fuzzy):** "Guili" tem 0.87 de similaridade com "Guilherme" → resolve automaticamente
    4. **Resultado:** Ambos os nomes resolvem para a mesma entidade. Alias "Guili" registrado.

    Da próxima vez que "Guili" aparecer, a Fase 1 resolve instantaneamente via cache de aliases - sem precisar de fuzzy ou LLM.

### Casos Especiais

- **Pronomes do speaker** - "I", "me", "eu", "myself" resolvem automaticamente para a entidade do speaker (`person:{speaker_slug}`). Por exemplo, com `speaker_name="Rafael"`, "I live in São Paulo" resolve "I" para `person:rafael`.
- **Termos de relacionamento** - "girlfriend", "brother", "amigo" resolvem para a entidade do speaker quando a palavra bare é o nome da entidade (não "my girlfriend Ana" - nesse caso "Ana" é a entidade). O match é ativado quando o nome da entidade em si é um termo de relacionamento, não a frase completa.
- **Dicas relacionais** - `"Carol (namorada do Rafael)"` remove a dica e força `type="person"`

### Registro de Aliases

Quando um novo alias é descoberto (ex: "Aninha" resolve para `person:ana`), ele é registrado em `MemoryEntityAlias` com semântica **first-write-wins** - escritas concorrentes não criam aliases conflitantes. Aliases são scoped por `agent_id`: o mesmo alias pode mapear para entidades diferentes para agentes diferentes.

**Aliases da extração** também são registrados automaticamente: quando o entity resolution cria uma nova entidade que tem aliases da extração (ex: "Guili" para "Guilherme Maturana"), todos os aliases são registrados em `MemoryEntityAlias` e adicionados ao cache de aliases in-memory. Isso significa que entidades subsequentes no mesmo batch podem resolver imediatamente via Fase 1 exact match - sem chamadas fuzzy ou LLM.

Isso cria uma defesa em duas linhas contra duplicatas:

1. **Intra-mensagem** - Agrupamento de aliases na extração previne duplicatas dentro de uma mesma mensagem
2. **Cross-mensagem** - Aliases registrados habilitam exact match em mensagens futuras (ex: se mensagem 1 cria "Guilherme Maturana" com alias "Guili", mensagem 2 mencionando "Guili" resolve instantaneamente via Fase 1)

### Persistência de Entidades

Após o entity resolution completar, o pipeline garante que **toda entidade resolvida** tenha um row na tabela `memory_entities` - não apenas as recém-criadas. Entidades resolvidas via exact match, fuzzy match ou LLM também recebem upsert via `ON CONFLICT DO UPDATE` (idempotente).

Isso é crítico porque background jobs (importance scoring, summary refresh, spreading activation) leem de `memory_entities`. Sem um row, esses jobs ficam cegos à entidade e não operam sobre ela.

O upsert de entidades é fail-safe: se uma entidade falhar ao persistir (ex: constraint violation), as outras prosseguem normalmente e o pipeline continua.

### Configuração

| Parâmetro | Default | Descrição |
|-----------|---------|-----------|
| `fuzzy_threshold` | `0.85` | Threshold de similaridade de cosseno para fuzzy match direto |
| `enable_llm_resolution` | `True` | Se deve usar LLM para casos ambíguos. Quando `False`, candidatos ambíguos criam uma nova entidade ao invés de chamar LLM. |

!!! note "Seleção de modelo"
    O modelo LLM usado para entity resolution é determinado pelo `LLMProvider` injetado no `MemoryClient`. Para usar um modelo diferente para resolução vs. extração, injete providers diferentes.

!!! info "Paralelo com neurociência"
    A entity resolution espelha a **memória associativa** - a capacidade do cérebro de vincular novos estímulos a representações existentes. Ouvir "Carol" ativa o padrão neural de "Carolina" através de pattern completion, assim como o fuzzy matching ativa entidades candidatas através de similaridade de embedding.

---

## Estágio 3: Reconciliação

!!! note "Reconciliação sempre roda"
    A reconciliação roda **incondicionalmente**, independente do modo de extração (informada ou cega). A extração informada fornece contexto que melhora a qualidade dos fatos extraídos, mas a decisão do que fazer com cada fato (ADD, UPDATE, NOOP, DELETE) é sempre feita por `reconcile_facts()`. Isso garante que fatos contraditórios sejam detectados e invalidados.

**Em português claro:** Se o usuário disse "Eu moro em São Paulo" na semana passada e agora diz "Me mudei pro Rio", o sistema precisa entender que isso é uma atualização, não uma segunda casa. Este estágio compara cada fato novo contra o que já tá armazenado e decide: é informação nova? Atualização de algo existente? Já conhecido? Ou uma retratação?

### Lógica de Decisão

Para cada fato extraído, o reconciler:

1. **Busca fatos existentes** para a mesma entidade
2. **Calcula similaridade** entre o novo fato e cada fato existente (via embeddings)
3. **Decide a ação**:

| Ação | Quando | Exemplo |
|------|--------|---------|
| **ADD** | Informação nova, sem fato existente similar (similaridade < 0.50) | "fala francês" quando não existe fato de idioma |
| **UPDATE** | Substitui um fato existente (similaridade 0.50-0.85+) | "mora no Rio" substitui "mora em São Paulo" |
| **NOOP** | Já é conhecido (alta similaridade) | "trabalha na Acme" quando esse fato já existe |
| **DELETE** | Retrata explicitamente um fato | "Não trabalho mais na Acme" |

??? example "Passo a passo: como a reconciliação decide"
    **Cenário:** O usuário disse anteriormente "Ricardo mora em São Paulo". Agora diz "Ricardo se mudou pra Austin, Texas."

    **Passo 1 - Buscar fatos existentes para Ricardo:**
    ```
    Existente: "Ricardo Gomes lives in São Paulo" (confidence: 0.95, ativo)
    ```

    **Passo 2 - Calcular similaridade:**
    ```
    Novo fato: "Ricardo Gomes moved to Austin, Texas"
    vs existente: "Ricardo Gomes lives in São Paulo"
    Similaridade de cosseno: 0.72 (ambos sobre localização do Ricardo)
    ```

    **Passo 3 - Similaridade >= 0.50 → slow path (chamada LLM):**
    O LLM vê ambos os fatos e decide: é um UPDATE. O usuário se mudou.

    **Resultado:**
    ```
    Fato antigo: "Ricardo Gomes lives in São Paulo" → valid_to = now, invalidated_at = now
    Fato novo: "Ricardo Gomes moved to Austin, Texas" → supersedes_fact_id = old_fact.id
    Relacionamento: ricardo → lives_in → sao_paulo → INVALIDADO (cascata)
    Novo relacionamento: ricardo → lives_in → austin
    ```

    Se a similaridade fosse < 0.50 (ex: "Ricardo gosta de jazz"), seria auto-ADD sem chamada LLM - fast path.

### Performance da Reconciliação

- **Fast path (similaridade < 0.50):** Auto-ADD sem chamada LLM (~300ms). Caminho comum para informação nova.
- **Slow path (similaridade ≥ 0.50):** LLM avalia se deve ADD, UPDATE, DELETE ou NOOP (~2-3s). Requer chamada LLM com contexto completo.

Planeje adequadamente: importações em massa de dados novos são rápidas; atualizações de conhecimento existente requerem decisão do LLM.

!!! note "Chains de UPDATE podem ramificar"
    O LLM de reconciliação pode escolher ADD ao invés de UPDATE quando interpreta a nova informação como distinta, não como substituição. Por exemplo, "me mudei pra BH" pode criar fatos separados para "mora em BH" e "morou no RJ" ao invés de um simples update chain. Isso preserva mais informação mas pode quebrar a chain de `supersedes_fact_id`. Este é o comportamento esperado - o LLM prioriza preservação de informação.

### Comportamento Fail-safe

Se a chamada LLM de reconciliação falhar, o sistema faz default para **ADD** - é melhor ter uma quase-duplicata do que perder informação. Os jobs de consolidation em background (clustering, deduplicação) limpam duplicatas depois.

### Versionamento de Fatos

Fatos são versionados usando janelas de validade temporal (`valid_from`, `valid_to`):

- **Fatos ativos** têm `valid_to = NULL`
- **Fatos atualizados** recebem tanto `valid_to` quanto `invalidated_at` definidos, e um novo fato é criado com `supersedes_fact_id` apontando para o antigo
- **Fatos deletados** recebem tanto `valid_to` quanto `invalidated_at` definidos

Isso permite queries de time-travel: você pode perguntar o que o sistema sabia em qualquer momento no tempo.

!!! info "Paralelo com neurociência"
    A reconciliação espelha a **reconsolidação** - o processo pelo qual memórias recuperadas se tornam lábeis e podem ser modificadas. Quando você recupera uma memória ("mora em São Paulo") e encontra nova informação ("acabou de se mudar pro Rio"), a memória original é atualizada. O cérebro não simplesmente sobrescreve - ele cria um novo traço ligado ao original, assim como UPDATE cria um novo fato com `supersedes_fact_id`.

---

## Estágio 4: Upsert

**Em português claro:** Aqui as decisões do estágio anterior são de fato salvas no banco. Fatos novos são inseridos, fatos desatualizados são marcados como substituídos, e relacionamentos entre entidades são criados ou reforçados. Tudo roda dentro de uma transação - se um fato falhar ao salvar, os outros ainda passam.

O estágio de upsert executa as decisões de reconciliação no banco de dados:

| Decisão | Ação no banco |
|---------|---------------|
| ADD | Cria novo `MemoryFact` com embedding |
| UPDATE | Fecha fato antigo (`valid_to = now`), cria novo com `supersedes_fact_id` |
| NOOP | Atualiza `last_confirmed_at` no fato existente |
| DELETE | Fecha fato (`valid_to = now`, `invalidated_at = now`) |

### Fact-Entity Links

Após cada fato ser persistido (ADD ou UPDATE), o pipeline cria **entity links** conectando o fato a todas as entidades que ele menciona - não só ao subject primário. Isso permite retrieval cross-entity sem duplicar fatos.

Por exemplo, "Clara Rezende saiu da Vertix" é armazenado **uma vez** com `entity_key = person:clara_rezende` (subject primário). Mas entity links são criados tanto pra `person:clara_rezende` (primário) quanto pra `organization:vertix` (secundário). Quando se busca sobre Vertix, o sistema encontra esse fato via link - sem necessidade de fato duplicado.

Links são criados por match de display names contra o texto do fato (case-insensitive substring match). Nomes muito curtos (< 3 caracteres) são pulados pra evitar false positives. A criação de links é fail-safe: se falhar, o fato persiste normalmente.

??? example "Passo a passo: como upsert + entity links funcionam"
    **Fato a persistir:** "Clara Rezende joined Orion Tech as head of engineering" (decisão: ADD)

    **Passo 1 - Criar MemoryFact:**
    ```
    id: fact_abc123
    entity_key: person:clara_rezende (subject primário)
    fact_text: "Clara Rezende joined Orion Tech as head of engineering"
    confidence: 0.95
    valid_from: now
    valid_to: NULL
    ```

    **Passo 2 - Criar entity links:**
    ```
    Entity map contém: {Clara Rezende → person:clara_rezende, Orion Tech → organization:orion_tech, ...}

    Escanear fact_text por display names de entidades:
      "Clara Rezende" encontrado → link (fact_abc123, person:clara_rezende, is_primary=true)
      "Orion Tech" encontrado → link (fact_abc123, organization:orion_tech, is_primary=false)
    ```

    **Passo 3 - Persistir relacionamento:**
    ```
    clara_rezende → works_at → orion_tech (strength: 0.8)
    Evidência: fact_abc123 (match porque fact_text menciona tanto "Clara Rezende" quanto "Orion Tech")
    ```

    **Resultado:** 1 fato, 2 entity links, 1 relacionamento. Quando alguém perguntar "Quem trabalha na Orion Tech?", o sistema encontra esse fato via o link `organization:orion_tech` - sem precisar de um fato separado sobre a Orion Tech.

### Rastreamento de Relacionamentos

Durante o upsert, relacionamentos extraídos também são persistidos:

- Cria/atualiza registros `MemoryEntityRelationship`
- Resolve entidades de origem e destino via entity map
- **Filtro de self-referencing**: relacionamentos onde source e target resolvem para a mesma entidade sao descartados silenciosamente (ex: "caroline child_of caroline"). Isso pode ocorrer quando a normalizacao de aliases faz dois nomes diferentes resolverem para a mesma entity key
- **Reforço de strength**: relacionamentos repetidos aumentam `strength` (inicial: 0.8, reforçado até 1.0 ao longo de múltiplas mensagens)
- Usa `ON CONFLICT DO UPDATE` para upserts idempotentes

!!! warning "Relacionamentos são **unidirecionais**"
    Escrever "Ana trabalha na Acme" cria `ana → works_at → acme_corp`, mas **não** `acme_corp → employs → ana`. Isso significa que o graph retrieval começando por "Acme Corp" não vai encontrar Ana via relacionamentos (mas pode encontrá-la via similaridade semântica). Para criar ambas as direções, mencione-as explicitamente: "Ana trabalha na Acme. A Acme tem a Ana como data scientist."

#### Vinculação de Evidência e Cascata de Invalidação

**O problema:** Sem vinculação entre fatos e relacionamentos, arestas contraditórias se acumulam. Se um usuário diz "moro em Curitiba" e depois "me mudei pra São Paulo", o relacionamento antigo `user --[lives_in]--> curitiba` continuaria ativo junto com o novo - poluindo o retrieval com contexto obsoleto.

**A solução:** Cada relacionamento é vinculado ao fato que o sustenta via `evidence_fact_id`. Quando esse fato é substituído (UPDATE) ou retratado (DELETE), o relacionamento é **automaticamente invalidado** - sem necessidade de limpeza manual.

Como a vinculação de evidência funciona:

1. Após os fatos serem persistidos no estágio de upsert, um match heurístico associa cada relacionamento a um fato correspondente. Para um relacionamento `(source, target)`, o matcher busca fatos cujo `fact_text` menciona ambos os nomes de entidade.
2. Se múltiplos fatos fizerem match, o de maior confidence é selecionado.
3. O ID do fato é armazenado como `evidence_fact_id` no relacionamento.

Quando um fato é invalidado (via UPDATE ou DELETE), a **cascata de invalidação** automaticamente seta `invalidated_at` e `valid_to` em todos os relacionamentos que o referenciam. O [BFS do graph retrieval](../advanced/read-api.md) já filtra relacionamentos invalidados, então arestas obsoletas são imediatamente excluídas do contexto.

```
Usuário: "Moro em Curitiba"
  → fato: "User mora em Curitiba" (fact_1)
  → rel:  user --[lives_in]--> curitiba (evidence_fact_id = fact_1)

Usuário: "Me mudei pra São Paulo"
  → reconciliação: UPDATE fact_1 → fact_2 "User mora em São Paulo"
  → cascata: rel lives_in→curitiba INVALIDADA (evidence_fact_id = fact_1)
  → nova rel: user --[lives_in]--> sao_paulo (evidence_fact_id = fact_2)
```

!!! note "Tipos de relacionamento são dinâmicos"
    O campo `rel_type` aceita qualquer string descritiva em `snake_case` - não apenas um conjunto fixo. Tipos comuns incluem `works_at`, `lives_in`, `family_of`, mas o LLM também pode produzir tipos como `mentored_by` ou `inspired_by`. Veja [Tipos de Relacionamento Dinâmicos](../advanced/data-types.md#tipos-de-relacionamento-dinamicos) para detalhes sobre normalização e aliases.

#### Fatos-espelho

Às vezes o LLM infere um relacionamento a partir do contexto sem extrair um fato correspondente. Por exemplo, "vou pra Curitiba ver minha mãe" implica `mãe --[lives_in]--> curitiba`, mas o LLM pode extrair apenas um fato sobre a viagem do usuário - não sobre onde a mãe mora. Sem um fato, o relacionamento não participa da cascata de invalidação e não é encontrável via busca semântica.

Para resolver isso, um **fato-espelho** é criado automaticamente como fallback quando nenhum match heurístico é encontrado. O fato-espelho é uma frase simples em linguagem natural gerada a partir do relacionamento: `"{source_name} {rel_type} {target_name}"` (ex: `"Mom lives in Curitiba"`).

**Fatos-espelho passam pelo pipeline canônico.** A partir da v0.11.7, fatos-espelho não são persistidos diretamente — eles são construídos como `ExtractedFact` sintéticos e roteados pelo mesmo caminho `reconcile_facts → execute_upsert` que os fatos extraídos pelo LLM. Isso garante:

- **Dedup semântica:** o reconciliador detecta quase-duplicatas como `"Pedro lives in Brazil"` vs `"Pedro lives in Brasil"` via similaridade de embedding + raciocínio do LLM — não apenas match de string.
- **Propagação de speaker:** o campo `speaker` é preenchido nos fatos-espelho pelo mesmo mecanismo dos fatos normais, então a proveniência é preservada.
- **Ponto de entrada único:** a única forma de criar um `MemoryFact` no write pipeline é via `_add_fact` (para ADD) ou `_update_fact` (para UPDATE versionado). Sem caminhos paralelos.

Fatos-espelho são marcados com:

- `confidence = 0.60` (inferência fraca - prioridade menor no ranking de retrieval)
- `source_context = "inferred_from_relation"` (permite filtrar ou reduzir prioridade se necessário)

!!! note "Fatos-espelho podem ficar stale"
    Fatos-espelho não são automaticamente invalidados quando o relacionamento de origem é removido. Eles podem persistir como dados obsoletos. Aplicações devem considerar filtrar por `source_context` quando a acurácia é crítica.

O ID do fato-espelho é usado como `evidence_fact_id` do relacionamento, então a cascata de invalidação funciona também para relacionamentos inferidos. Quando o reconciliador decide `NOOP` (um fato equivalente já existe), o ID do fato existente é usado como link de evidência — fortalecendo a cadeia de provenance.

!!! tip "Reduzindo fatos-espelho"
    Os prompts de extração instruem o LLM a extrair fatos implícitos junto com relacionamentos (ex: "minha mãe mora em Curitiba" como fato, não apenas como relação). Conforme a extração LLM melhora, menos fatos-espelho são necessários - eles são a rede de segurança, não o mecanismo principal.

### Segurança Transacional

O write pipeline inteiro roda dentro de uma transação do banco de dados. Upserts individuais de fatos usam **savepoints** (`session.begin_nested()`) para que uma falha em um fato não aborte o batch inteiro:

```python
# Se este fato falhar, apenas este savepoint faz rollback
async with session.begin_nested():
    session.add(new_fact)
    await session.flush()
```

O registro do evento é criado e flushed primeiro, então ele sobrevive mesmo se todos os estágios subsequentes falharem.

---

## WriteResult

Após o pipeline completar, você recebe um `WriteResult` com observabilidade total:

```python
result = await memory.write(
    agent_id="user_123",
    message="...",
    speaker_name="Rafael",
    recent_messages=["mensagem anterior para resolução de pronomes"],  # opcional
    occurred_at=datetime(2025, 6, 15, tzinfo=UTC),  # opcional, default: now
)

# O que aconteceu
print(result.facts_added)       # Lista de fatos criados
print(result.facts_updated)     # Lista de fatos substituídos
print(result.facts_unchanged)   # Lista de fatos confirmados (decisões NOOP)
print(result.facts_deleted)     # Lista de fatos retratados (decisões DELETE)
print(result.entities_resolved) # Lista de entidades resolvidas
print(result.duration_ms)       # Tempo total do pipeline
print(result.event_id)          # ID único do evento para esta escrita
print(result.tokens_used)       # TokenUsage(input_tokens=..., output_tokens=..., total_tokens=...)
print(result.pipeline)          # PipelineTrace (quando verbose=True)
print(result.success)           # True se o pipeline completou sem erros (sempre verifique)
print(result.error)             # Mensagem de erro se o pipeline falhou (None em sucesso)
```

### Enriquecimento do Trace (verbose=True)

Quando `verbose=True`, o step de extraction no `PipelineTrace` inclui metadados adicionais:

| Campo | Tipo | Descrição |
|-------|------|-----------|
| `relation_retry_triggered` | `bool` | Se o retry automático de relations foi utilizado |

```python
result = await memory.write(agent_id, message, speaker_name="Rafael", verbose=True)
extraction_step = result.pipeline.steps[0]  # "extraction"
print(extraction_step.data["relation_retry_triggered"])  # True/False
```

### Token Usage

`tokens_used` reporta o total de tokens LLM consumidos em todas as chamadas do pipeline (extraction, entity resolution, reconciliation). Útil pra benchmarking e estimativa de custo.

```python
result = await memory.write(agent_id, message, speaker_name="Rafael")
print(result.tokens_used.input_tokens)   # ex: 1200
print(result.tokens_used.output_tokens)  # ex: 350
print(result.tokens_used.total_tokens)   # ex: 1550
```

!!! note "Token tracking requer suporte do provider"
    `tokens_used` é populado a partir de `LLMResult.usage` retornado pelo seu `LLMProvider`. O `OpenAIProvider` built-in reporta usage automaticamente. Providers customizados que retornam `LLMResult(text=..., usage=None)` mostrarão zero tokens.

### Config Overrides

Sobrescreva qualquer campo do `MemoryConfig` pra uma única chamada `write()` sem criar novo client:

```python
result = await memory.write(
    agent_id="user_123",
    message="...",
    speaker_name="Rafael",
    config_overrides={"extraction_timeout_sec": 60.0},
)
```

Só as chaves fornecidas são alteradas; todas as outras herdam do config do client. Chaves inválidas emitem warning e são ignoradas. Incompatibilidade de tipo levanta `ValueError`.

### Dry Run

Rode extraction sem persistir nada no banco:

```python
result = await memory.write(
    agent_id="user_123",
    message="Eu moro em São Paulo com minha esposa Ana",
    speaker_name="Rafael",
    dry_run=True,
)
# result.facts_added contém o que SERIA extraído
# result.tokens_used mostra o custo dessa extraction
# Nenhum evento, fato ou entidade é persistido
```

Útil pra benchmarking: rode a mesma mensagem com `dry_run=True` e compare `tokens_used` entre configurações diferentes.

---

## Diagrama do Pipeline (Completo)

### Fluxo Padrão (Extração Informada)

```mermaid
flowchart TD
    MSG["User message"] --> EVT["Create MemoryEvent\n(immutable log + embedding)"]
    EVT --> ALIAS["Alias Lookup\n(word-boundary match no cache)"]
    ALIAS --> PRE["Pré-retrieval\n(carregar perfis ou fatos via pgvector)"]
    PRE --> INF["Extração Informada\n(1 chamada LLM: entities + facts + relations + profiles)"]
    INF --> DEDUP["Semantic Dedup\n(remove near-duplicates)"]
    DEDUP --> RES["Entity Resolution\n(exact → fuzzy → LLM)"]
    RES --> UPS["Upsert + Entity Links + Perfis\n(with savepoints)"]
    UPS --> REL["Relationship Tracking\n(strength reinforcement)"]
    REL --> WR["WriteResult"]
```

### Fluxo Fallback (Extração Cega)

```mermaid
flowchart TD
    MSG["User message"] --> EVT["Create MemoryEvent\n(immutable log + embedding)"]
    EVT --> EXT["Extraction\n(Entity Scan → Facts + Relations concurrent)"]
    EXT --> DEDUP["Semantic Dedup\n(remove near-duplicates)"]
    DEDUP --> RES["Entity Resolution\n(exact → fuzzy → LLM)"]
    RES --> REC["Reconciliation\n(ADD / UPDATE / NOOP / DELETE)"]
    REC --> UPS["Upsert + Entity Links\n(with savepoints)"]
    UPS --> REL["Relationship Tracking\n(strength reinforcement)"]
    REL --> WR["WriteResult"]
```
