# 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 três chamadas LLM sequenciais para maior acurácia:

    1. **Entity scan** — Identifica todas as entidades mencionadas na mensagem
    2. **Fact extraction** — Para cada lote de entidades, extrai fatos (agrupados por `entity_batch_size`)
    3. **Relation extraction** — Identifica relacionamentos entre entidades

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

=== "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. Faz fallback automático quando a contagem de entidades é menor ou igual a `multi_pass_entity_threshold`.

    Inclui um **reflexion pass** opcional — uma segunda chamada LLM que revisa a extração para controle de qualidade.

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

### 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 (via pgvector) 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 um LLM (`gpt-4o-mini`) 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 com inferência de tipo pessoa
- **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 |
| `entity_resolution_model` | `"gpt-4o-mini"` | Modelo para resolução LLM |

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

### Configuração

| Parâmetro | Default | Descrição |
|-----------|---------|-----------|
| `reconciliation_model` | `"gpt-4o-mini"` | Modelo para decisões de reconciliaçã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 `valid_to` definido para o timestamp atual, 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

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

---

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