# Write Pipeline

O write pipeline transforma mensagens em linguagem natural em conhecimento estruturado e versionado. Toda chamada a `memory.write()` executa quatro estágios: **Extract → Resolve → Reconcile → Upsert**.

```mermaid
flowchart LR
    A["Message"] --> B["Extract"]
    B --> C["Resolve Entities"]
    C --> D["Reconcile"]
    D --> E["Upsert"]
    E --> F["WriteResult"]
```

## Visão Geral

Quando você chama `memory.write(user_id, message)`, o pipeline:

1. **Cria um evento imutável** — A mensagem bruta é registrada como um `MemoryEvent` com seu embedding. Esse registro nunca é modificado ou deletado — é a trilha de auditoria.
2. **Extrai entidades, fatos e relacionamentos** — Um LLM analisa a mensagem para identificar informações estruturadas.
3. **Resolve entidades para registros canônicos** — Deduplica menções ("Ana", "minha esposa Ana", "ela") em uma única entidade.
4. **Reconcilia contra o conhecimento existente** — Decide se cada fato é novo (ADD), substitui um fato antigo (UPDATE), já é conhecido (NOOP) ou retrata algo (DELETE).
5. **Faz upsert dos resultados** — Executa as decisões de reconciliação no banco de dados.

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: Extraction

O estágio de extraction usa um LLM para transformar linguagem natural em dados estruturados: entidades, fatos e relacionamentos.

### Dois Modos

=== "Multi-pass (padrão)"

    A extração multi-pass executa um entity scan primeiro, depois fact batches e relation extraction rodam concorrentemente via `asyncio.gather`. O número total de chamadas LLM depende da contagem de entidades e do `entity_batch_size`.

    1. **Entity scan** — Identifica todas as entidades mencionadas na mensagem
    2. **Fact extraction + Relation extraction** — Rodam concorrentemente: fact extraction agrupa entidades por `entity_batch_size`, enquanto relation extraction identifica relacionamentos entre entidades

    Melhor para mensagens que mencionam múltiplas entidades ou contêm informações complexas.

    Faz fallback automático para single-pass quando a contagem de entidades é menor ou igual a `multi_pass_entity_threshold`.

=== "Single-pass"

    A extração single-pass executa uma única chamada LLM que extrai entidades, fatos e relacionamentos juntos.

    Mais rápida e barata, mas pode perder relacionamentos nuançados em mensagens complexas. Também usado como fallback automático quando multi-pass encontra poucas entidades (≤ `multi_pass_entity_threshold`).

    Inclui um **reflexion pass** automático — uma segunda chamada LLM que revisa a extração para controle de qualidade. Inclui retry embutido (1 tentativa) em caso de falha na extração antes de retornar vazio.

### Configuração

| Parâmetro | Default | Descrição |
|-----------|---------|-----------|
| `extraction_model` | `"gpt-4o"` | Modelo LLM para extração |
| `extraction_mode` | `"multi_pass"` | `"multi_pass"` ou `"single_pass"` |
| `extraction_timeout_sec` | `30.0` | Timeout por chamada LLM |
| `multi_pass_entity_threshold` | `3` | Contagem de entidades abaixo da qual multi-pass faz fallback para single-pass |
| `entity_batch_size` | `5` | Entidades por lote de extração de fatos no modo multi-pass |
| `reflexion_timeout_sec` | `10.0` | Timeout para o pass de revisão de qualidade reflexion (apenas single-pass) |

### 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 sobre entidades em linguagem natural (ex: "mora em São Paulo", "trabalha como engenheiro backend")
- **Relações** — Conexões entre entidades (ex: "Rafael" → `works_at` → "Acme Corp")

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 |

### 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.

!!! 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.

---

## Estágio 2: Entity Resolution

A entity resolution mapeia nomes de entidades extraídos para registros canônicos. Isso previne duplicatas: "Ana", "minha esposa Ana" e "Aninha" resolvem para a mesma entidade `person:ana`.

### 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).

**Fase 2: Fuzzy match**

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

- **>= 0.85** — Match de alta confiança, resolve diretamente
- **0.50-0.85** — Ambíguo, encaminha top-3 candidatos para a Fase 3
- **< 0.50** — Sem match, cria uma nova entidade

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.

### Casos Especiais

- **Auto-referências** — "I", "me", "eu", "myself" resolvem automaticamente para `user:self`
- **Termos de relacionamento** — "my girlfriend", "my brother", "meu amigo" resolvem para `user:self` (o relacionamento é sobre o usuário, não uma entidade separada)
- **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.

### 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

A reconciliação compara novos fatos contra o conhecimento existente e decide o que fazer com cada um. É assim que a memória se mantém precisa ao longo do tempo — contradições são resolvidas, redundâncias são ignoradas e informações desatualizadas são substituídas.

### 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" |

- **Baixa similaridade (< 0.50)**: Auto-ADD sem chamada LLM (fast path)
- **Similaridade ambígua (0.50+)**: LLM decide a ação com contexto completo

### 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

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`) |

### 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
- **Reforço de strength**: relacionamentos repetidos aumentam `strength` (até 1.0)
- Usa `ON CONFLICT DO UPDATE` para upserts idempotentes

#### 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 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)

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.

!!! 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(user_id="user_123", message="...")

# O que aconteceu
print(result.facts_added)       # Lista de fatos criados
print(result.facts_updated)     # Lista de fatos substituídos
print(result.facts_unchanged)   # Contagem de decisões NOOP
print(result.facts_deleted)     # Contagem de 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.pipeline)          # PipelineTrace (quando verbose=True)
print(result.success)           # True se o pipeline completou sem erros
print(result.error)             # Mensagem de erro se o pipeline falhou (None caso contrário)
```

---

## Diagrama do Pipeline (Completo)

```mermaid
flowchart TD
    MSG["User message"] --> EVT["Create MemoryEvent\n(immutable log + embedding)"]
    EVT --> EXT{"Extraction mode?"}
    EXT -->|multi-pass| MP["Entity Scan → Fact Batches → Relations"]
    EXT -->|single-pass| SP["Single LLM call\n(entities + facts + relations)"]
    MP --> RES["Entity Resolution\n(exact → fuzzy → LLM)"]
    SP --> RES
    RES --> REC["Reconciliation\n(ADD / UPDATE / NOOP / DELETE)"]
    REC --> UPS["Upsert\n(with savepoints)"]
    UPS --> REL["Relationship Tracking\n(strength reinforcement)"]
    REL --> WR["WriteResult"]
```
