# Read Pipeline

Quando você chama `memory.retrieve()`, o SDK busca tudo que sabe sobre um usuário e retorna os fatos mais relevantes pra sua query — ranqueados, pontuados e formatados numa string que você pode colar direto num prompt de LLM.

**Você não precisa entender os internals pra usar.** Basta chamar `retrieve()` e usar `result.context`. 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["Query"] --> B["Plan"]
    B --> C["Retrieve\n(3 signals)"]
    C --> D["Enhance"]
    D --> E["Rerank"]
    E --> F["RetrieveResult"]
```

## Visão Geral

Toda chamada a `memory.retrieve(user_id, query)` executa cinco estágios:

1. **Plan** — Descobre *o que* buscar. Reformula sua query, identifica quais pessoas/lugares/coisas são relevantes.
2. **Retrieve** — Busca fatos usando três métodos em paralelo: similaridade de significado, match de palavras-chave e travessia do grafo de relacionamentos.
3. **Enhance** — Expande o contexto seguindo relacionamentos entre entidades pra encontrar fatos relacionados que não foram diretamente matcheados.
4. **Rerank** — Um LLM reavalia os top resultados e reordena pela relevância real pra sua query.
5. **Format** — Comprime os fatos ranqueados numa string com orçamento de tokens, organizada por tiers de relevância.

---

## Estágio 1: Retrieval Agent (Planner)

**Em português claro:** Antes de buscar, o pipeline pergunta a um LLM: "O que essa pessoa tá procurando?" O LLM reformula a query pra melhores resultados de busca, identifica quais entidades (pessoas, lugares, empresas) são relevantes, e decide a estratégia.

O retrieval agent é um planner alimentado por LLM que decide **como** recuperar, não apenas **o que** recuperar. Ele analisa a query e produz um `RetrievalPlan`.

### O Que o Planner Decide

| Campo | Descrição | Exemplo |
|-------|-----------|---------|
| `strategy` | Estratégia de retrieval | `"multi_signal"` (padrão) ou `"skip"` (para saudações) |
| `similarity_query` | Query reformulada para busca semântica | "user location city" (de "onde eu moro?") |
| `entities` | Entity keys para sinal graph | `["person:ana", "organization:acme"]` |
| `as_of_range` | Janela de time-travel (opcional) | `{"start": "2024-01-01", "end": "2024-06-30"}` |
| `broad_query` | Se deve expandir o escopo do graph | `true` para "me conte tudo sobre..." |
| `reason` | Explicação da estratégia | Para debugging e observabilidade |

### Reformulação da Query

O planner não simplesmente passa a query do usuário para busca semântica. Ele **reformula** para melhorar o matching por similaridade vetorial:

- `"onde eu moro?"` → `"user location city residence"`
- `"o que a Ana faz de trabalho?"` → `"Ana profession occupation job role"`

Isso fecha a lacuna de vocabulário entre como os usuários fazem perguntas e como os fatos são armazenados.

### Planejamento Schema-Aware

O planner inspeciona o schema real da memória para este usuário:

- Quais tipos de entidade existem (pessoas, organizações, lugares...)
- Quais entidades têm mais fatos
- Quais atributos estão armazenados

Isso ancora o plano na realidade — o planner não vai buscar entidades que não existem.

### Entity Resolution Híbrida

Quando você pergunta "Onde o Carlos mora?", o pipeline precisa descobrir que "Carlos" significa a entidade `person:carlos` no banco. Ele usa três métodos pra isso — se um falhar, os outros cobrem:

1. **Resolução determinística (primária)** — Faz match de palavras da query contra aliases de entidades conhecidas (`MemoryEntityAlias`), display names (`MemoryEntity.display_name`) e slugs de entity_keys. Rápido (< 10ms), confiável, custo zero de LLM. Por exemplo, "Onde o Carlos mora?" resolve deterministicamente para `person:carlos` via slug match.

2. **LLM planner (suplementar)** — O retrieval agent extrai entidades com base no entendimento da query. Captura referências indiretas que o determinístico não resolve (ex: "meu chefe" → `person:kevin`).

3. **Query expansion (priming de aliases)** — `expand_query()` resolve aliases e busca vizinhos 1-hop no KG, adicionando entidades relacionadas.

As três fontes são **unificadas** antes do graph gate. Se qualquer fonte identificar uma entidade, o graph traversal roda. Isso torna o sinal de grafo tolerante a falhas: se o LLM falha em extrair entidades (variância do LLM), a camada determinística cobre.

O trace step `"retrieval"` inclui um breakdown `entities_sources` mostrando quais entidades vieram de cada fonte (`llm`, `deterministic`, `expansion`).

### Estratégia Skip

Para saudações e mensagens casuais ("oi", "como vai?"), o planner retorna `strategy: "skip"`, fazendo short-circuit no pipeline. Sem queries no banco, sem chamadas LLM, resposta instantânea.

!!! info "Paralelo com neurociência"
    O retrieval agent espelha **pistas de recuperação** na psicologia cognitiva. Quando você tenta lembrar algo, seu cérebro não faz uma busca exaustiva — ele usa pistas contextuais para estreitar o espaço de busca. O planner identifica entidades e reformula queries como pistas que guiam os sinais de retrieval.

---

## Estágio 2: Retrieval Multi-Signal

**Em português claro:** O pipeline busca fatos relevantes usando três métodos diferentes ao mesmo tempo — por significado, por palavras exatas e por conexões entre entidades. Isso captura fatos que qualquer método sozinho perderia.

Três sinais independentes rodam **em paralelo** via `asyncio.gather()`, cada um encontrando candidatos de um ângulo diferente:

```mermaid
flowchart TD
    P["RetrievalPlan"] --> S["Semantic Search\n(pgvector cosine)"]
    P --> K["Keyword Search\n(SQL ILIKE)"]
    P --> G["Graph Traversal\n(BFS 2-hop)"]
    S --> M["Merge & Rank\n(RRF + weighted scoring)"]
    K --> M
    G --> M
```

### Sinal 1: Semantic Search

Usa similaridade de cosseno do pgvector para encontrar fatos cujos embeddings são próximos ao embedding da query.

- Embeds a query reformulada (do planner)
- Busca na tabela `MemoryFact` com índice HNSW
- Retorna top-N candidatos acima do threshold `min_similarity`
- Filtros: `user_id`, fatos ativos (`valid_to IS NULL`), confiança >= `min_confidence`

Este é o sinal primário — encontra fatos que são **semanticamente similares** à query, mesmo que não compartilhem keywords exatas.

### Sinal 2: Keyword Search

Matching SQL ILIKE em `fact_text` para hits exatos ou parciais de keywords.

- Extrai palavras significativas (> 2 caracteres) da query
- Faz match contra o texto do fato (até 5 keywords)
- Score = fração de palavras da query encontradas no fato

Complementa a busca semântica capturando matches exatos que a similaridade de embedding pode perder (ex: nomes próprios, termos técnicos, abreviações).

### Sinal 3: Graph Retrieval

Percorre relacionamentos de entidades para encontrar fatos conectados às entidades da query.

- Começa das entidades identificadas pelo planner
- Traversal BFS até 2 hops em `MemoryEntityRelationship`
- Fatos são buscados via **entity links** (`MemoryFactEntityLink`), não só pelo `entity_key` primário. Isso significa que um fato "Clara saiu da Vertix" (subject primário: Clara) também é encontrado ao buscar sobre Vertix — porque o fato tem um entity link secundário pra Vertix.
- Fórmula de scoring: `edge_strength × recency_factor × edge_recency_factor × query_bonus`
- `query_bonus`: 1.5x quando o nome da entidade aparece no texto da query
- **Fallback**: se a tabela de entity links está vazia (pré-migration), retrieval faz fallback pra match direto por `entity_key`

Graph retrieval se destaca em encontrar fatos **contextuais**. Quando você pergunta sobre uma pessoa, ele também encontra fatos sobre seu local de trabalho, seus relacionamentos e seus projetos.

### Merge & Rank

Depois que os três sinais retornam, os resultados são mesclados:

1. **Deduplicar** por fact ID (o mesmo fato pode aparecer em múltiplos sinais)
2. **Aplicar decay de recência** — Decay exponencial com half-life configurável (`recency_half_life_days`, padrão 14)
3. **Aplicar decay de confiança** — Fatos mais antigos com menor confiança são penalizados
4. **Calcular score combinado** — Soma ponderada:

!!! warning "O reranker sobrescreve estes pesos"
    Por padrão, `enable_reranker=True` — o reranker LLM determina o ranking **final**, não estes pesos. Os pesos abaixo afetam apenas a pré-seleção de candidatos e a ordem pré-reranking. Para usar estes pesos no ranking final, configure `enable_reranker=False`.

```python
score = (
    score_weights["semantic"]   * semantic_score +    # default 0.70
    score_weights["recency"]    * recency_score +     # default 0.20
    score_weights["importance"] * importance_score     # default 0.10
)
```

### Breakdown Completo do Score

Cada fato é pontuado em múltiplas dimensões. Você pode inspecionar em `fact.scores` pra entender **por que** um fato ficou onde ficou no ranking:

Cada dict `ScoredFact.scores` contém **todos os 6 sinais** computados ao longo do pipeline. A fórmula ponderada acima produz o score combinado base; estágios posteriores adicionam sinais que podem modificar o ranking final:

| Chave | Origem | Range | Descrição |
|-------|--------|-------|-----------|
| `semantic` | Busca semântica | 0.0–1.0 | Similaridade de cosseno entre embeddings da query e do fato. Sinal primário de retrieval. |
| `keyword` | Busca por keyword | 0.0–1.0 | Fração de palavras da query encontradas no texto do fato. Complementa semântico para matches exatos. |
| `recency` | Merge & Rank | 0.0–1.0 | Decay exponencial a partir do `created_at`, half-life = `recency_half_life_days` (padrão 14). |
| `importance` | Importância dinâmica | 0.05–3.0 | Computado a partir de frequência de retrieval, recência de uso, correções do usuário e participação em padrões. Começa em 0.5 para fatos novos e evolui conforme o fato é recuperado e confirmado. Requer o job de importância em background para produzir valores não-default. |
| `confidence` | Merge & Rank | 0.0–1.0 | Confiança efetiva após decay temporal. A confiança base é atribuída pelo LLM durante extração (tipicamente 0.95 para afirmações assertivas). Decai ao longo do tempo e é usada como filtro (`min_confidence`) e sinal de scoring. |
| `reranker` | Reranking | 0.0, 0.3, 0.5, 0.8, 1.0 | Score de relevância baseado em LLM. Presente apenas quando `enable_reranker=True`. Valores discretos atribuídos pelo reranker LLM. |

Sinais adicionais computados durante enhancement (não estão em `score_weights` mas afetam o score final):

| Chave | Origem | Descrição |
|-------|--------|-----------|
| `pattern` | Enhancement | Boost aditivo para fatos confirmados recentemente (até +0.10). |
| `graph` | Graph traversal | Score do traversal BFS de 2 saltos em relacionamentos de entidades. |

### Configuração

| Parâmetro | Default | Descrição |
|-----------|---------|-----------|
| `topk_facts` | `20` | Máximo de fatos a retornar |
| `topk_events` | `8` | Máximo de eventos a considerar |
| `min_similarity` | `0.20` | Similaridade de cosseno mínima para resultados semânticos |
| `min_confidence` | `0.55` | Confiança mínima do fato |
| `recency_half_life_days` | `14` | Half-life para decay de recência |
| `score_weights` | Veja acima | Pesos para cada sinal de scoring |
| `enable_reranker` | `True` | Se deve usar reranking LLM |

!!! info "Paralelo com neurociência"
    O retrieval multi-signal espelha **spreading activation** em redes semânticas (Collins & Loftus, 1975). Quando você pensa em "médico", a ativação se espalha para conceitos relacionados ("hospital", "remédio", "consulta") através de links associativos. Da mesma forma, graph retrieval se espalha a partir de entidades da query ao longo de arestas de relacionamento, enquanto busca semântica ativa fatos através de proximidade de embedding.

---

## Estágio 3: Enhancement

**Em português claro:** Depois de encontrar os resultados iniciais, o pipeline segue conexões pra descobrir fatos relacionados. Se você pergunta sobre uma pessoa, ele pode puxar fatos sobre o trabalho dela, projetos e equipe — coisas que você não perguntou diretamente mas que adicionam contexto útil.

### Spreading Activation

A partir dos top-K fatos semente, o pipeline expande o contexto seguindo relacionamentos de entidades:

- Para cada fato semente, encontra os relacionamentos da entidade
- Percorre relacionamentos por N hops (`spreading_activation_hops`, padrão 2). Defina como `0` para desabilitar spreading completamente.
- Aplica fator de decay por hop (`spreading_decay_factor`, padrão 0.50). Hop 1 usa o fator diretamente; Hop 2 usa o fator ao quadrado (decay composto).
- Retorna até `spreading_facts_per_entity` fatos adicionais por entidade (padrão 3), aplicado tanto no Hop 1 quanto no Hop 2.

Isso captura contexto importante que não foi diretamente matcheado. Se você perguntar "o que o Rafael faz?", spreading activation pode trazer fatos sobre seu local de trabalho, time e projetos.

!!! tip "Quando o spreading activation faz diferença?"
    O spreading tem mais impacto com **20+ entidades** e relações cruzadas entre domínios (ex: pessoas → projetos → clientes → tecnologias). Com datasets pequenos (< 15 entidades), os sinais semântico, keyword e graph já cobrem todo o espaço de fatos — o spreading pode retornar candidatos mas eles serão dedupados contra os resultados existentes. Os campos do trace `spreading_candidates_returned` e `spreading_candidates_unique` permitem confirmar se o spreading está contribuindo fatos novos pro seu dataset.

### Sinal de Padrão

Fatos que foram confirmados recentemente (decisões NOOP no write atualizam `last_confirmed_at`) recebem um boost aditivo no score:

- Fatos confirmados recentemente → até 0.10 de score extra
- Captura fatos frequentemente mencionados e bem estabelecidos

### Configuração

| Parâmetro | Default | Descrição |
|-----------|---------|-----------|
| `spreading_activation_hops` | `2` | Máximo de hops a partir de fatos semente. Defina como `0` para desabilitar. |
| `spreading_decay_factor` | `0.50` | Decay de score por hop. Hop 1 = fator, Hop 2 = fator² |
| `spreading_facts_per_entity` | `3` | Máximo de fatos buscados por entidade no Hop 1 e Hop 2 |
| `spreading_max_related_entities` | `5` | Máximo de entidades KG-relacionadas exploradas no Hop 1 |

---

## Estágio 4: Reranking (Opcional)

**Em português claro:** Os estágios anteriores encontram fatos relevantes, mas o ranking é baseado em matemática (scores de similaridade, keyword overlap). O reranker pergunta a um LLM: "Dado o que essa pessoa tá perguntando, quais desses fatos são realmente mais úteis?" Isso produz um ranking final mais inteligente.

Quando `enable_reranker=True`, os top candidatos são rerankeados por um LLM que considera a intenção da query:

- Respeita o significado semântico da query (não apenas overlap de keywords)
- Pode promover fatos que são indiretamente relevantes mas importantes
- Degradação graceful: se o reranker falhar ou exceder `reranker_timeout_sec` (padrão 5.0s), o ranking original é preservado
- O timeout é enforced via `asyncio.wait_for` — a chamada LLM é cancelada se exceder o timeout configurado
- Usa um `reranker_llm` separado se configurado (tipicamente um modelo mais barato como gpt-4o-mini)

O reranker é o estágio mais custoso, mas fornece a maior melhoria de qualidade para queries complexas.

!!! info "Scoring por weighted blend"
    O reranker NÃO substitui o score da fórmula. Ele faz **blend**:

    `final_score = (1 - reranker_weight) × formula_score + reranker_weight × reranker_score`

    Com o default `reranker_weight=0.60`, o reranker tem influência forte mas não consegue zerar um fato com bons sinais de retrieval. Um fato com formula=0.9 e reranker=0.0 fica com final=0.36 (não 0.0). O dict `scores` preserva tanto `formula` (pré-reranker) quanto `reranker` (score do LLM) pra debugging.

---

## Estágio 5: Formatação

**Em português claro:** O pipeline pega os fatos ranqueados e organiza numa string pronta pra usar no prompt do seu LLM. Os fatos mais importantes vão primeiro (CORE MEMORY), fatos de suporte depois (EXTENDED CONTEXT), e histórico por último (RELEVANT_EVENTS) — tudo dentro de um orçamento de tokens pra não estourar seu prompt.

### Compressão de Contexto

Fatos são divididos em três tiers dentro de um orçamento de tokens (`context_max_tokens`):

!!! note "`context_max_tokens` é um orçamento proporcional, não um limite rígido"
    O parâmetro `context_max_tokens` controla o tamanho **relativo** do contexto de saída, mas a contagem real de tokens pode exceder o valor configurado. O pipeline garante um contexto mínimo para fatos core e usa o parâmetro como orçamento proporcional entre tiers. Trate como um target, não um limite estrito. Por exemplo, configurar `context_max_tokens=100` pode produzir ~240 tokens devido às garantias mínimas do hot tier.

| Tier | Config Key | Label no Output | Parcela | Conteúdo |
|------|-----------|----------------|---------|----------|
| **Hot** | `hot_tier_ratio` | `CORE MEMORY` | 50% | Fatos mais relevantes (scores mais altos) |
| **Warm** | `warm_tier_ratio` | `EXTENDED CONTEXT` | 30% | Contexto de suporte |
| **Cold** | (restante) | `RELEVANT_EVENTS` | 20% | Fatos de background e histórico de eventos |

### Configuração

| Parâmetro | Default | Descrição |
|-----------|---------|-----------|
| `context_max_tokens` | `2000` | Máximo de tokens no contexto formatado |
| `hot_tier_ratio` | `0.50` | Parcela do orçamento para fatos top |
| `warm_tier_ratio` | `0.30` | Parcela do orçamento para fatos de suporte |

### Formato de Output

A string `context` é formatada para injeção direta em prompts LLM:

```
## Known facts about the user:
- Lives in São Paulo (confidence: 0.95)
- Works at Acme Corp as a backend engineer (confidence: 0.90)
- Wife's name is Ana (confidence: 0.92)
```

---

## RetrieveResult

```python
result = await memory.retrieve(user_id="user_123", query="...")

# Contexto pré-formatado (pronto para prompts LLM)
print(result.context)

# Fatos individuais com scores
for fact in result.facts:
    print(f"[{fact.score:.2f}] {fact.entity_name}: {fact.value}")
    print(f"  Scores: {fact.scores}")  # {"semantic": 0.85, "recency": 0.72, ...}

# Stats do pipeline
print(f"Candidates evaluated: {result.total_candidates}")
print(f"Duration: {result.duration_ms:.0f}ms")
```

---

## Diagrama do Pipeline (Completo)

```mermaid
flowchart TD
    Q["User query"] --> AG["Retrieval Agent\n(LLM planner)"]
    AG -->|skip| SKIP["Return empty\n(greeting/casual)"]
    AG -->|multi_signal| PAR["Parallel retrieval"]
    PAR --> SEM["Semantic Search\n(pgvector cosine)"]
    PAR --> KW["Keyword Search\n(SQL ILIKE)"]
    PAR --> GR["Graph Traversal\n(BFS 2-hop)"]
    SEM --> MERGE["Merge & Rank\n(dedup + weighted scoring)"]
    KW --> MERGE
    GR --> MERGE
    MERGE --> SA["Spreading Activation\n(expand context along edges)"]
    SA --> RR{"Reranker\nenabled?"}
    RR -->|yes| RERANK["LLM Rerank"]
    RR -->|no| FMT["Format & Compress"]
    RERANK --> FMT
    FMT --> RES["RetrieveResult"]
```

!!! info "Paralelo com neurociência"
    A compressão em tiers (hot/warm/cold) espelha **níveis de ativação** na working memory. No modelo de processos embutidos de Cowan, um número pequeno de itens está no foco de atenção (hot tier), cercado por memória de longo prazo ativada (warm tier), com o restante da memória de longo prazo disponível mas não ativa (cold tier). O orçamento de tokens age como o limite de capacidade da working memory.
