# arandu

**Memória de longo prazo para agentes de IA.** Extraia fatos de conversas, resolva entidades, reconcilie conhecimento ao longo do tempo e recupere contexto relevante - tudo com PostgreSQL e pgvector.

> *O nome "Arandu" vem da palavra Guarani que significa "sabedoria adquirida pela experiencia" - literalmente "ouvir o tempo." Assim como o conceito Guarani descreve o conhecimento construido atraves da vivencia, o Arandu da ao seu agente de IA a capacidade de acumular, consolidar e recuperar conhecimento ao longo do tempo.*

---

## Por que arandu?

A maioria dos agentes de IA é stateless. Eles esquecem tudo entre sessões. O `arandu` dá ao seu agente uma memória persistente e estruturada que fica mais inteligente com o tempo:

- **Extração automática de fatos** - O write pipeline usa LLMs para extrair entidades, fatos e relacionamentos de linguagem natural.
- **Entity resolution** - Reconhece que "minha esposa Ana", "Ana" e "ela" se referem à mesma pessoa, usando um resolver de 3 fases (exact → fuzzy → LLM).
- **Reconciliação de conhecimento** - Decide se uma informação nova deve ADD, UPDATE ou DELETE fatos existentes. Sem duplicatas, sem dados obsoletos.
- **Retrieval multi-signal** - Combina busca semântica (pgvector), keyword matching, graph traversal e scoring de recência para encontrar os fatos mais relevantes.
- **Manutenção em background** - Clustering, consolidation e importance scoring mantêm a memória organizada e atualizada - como o cérebro consolida durante o sono.
- **Provider-agnostic** - Traga seu próprio LLM e embedding provider via protocolos Python simples. Providers OpenAI e Anthropic (Claude) incluídos.

## Instalação

```bash
pip install arandu
```

Com suporte a OpenAI (recomendado):

```bash
pip install arandu[openai]
```

### Requisitos

- Python 3.11+
- PostgreSQL com a extensão [pgvector](https://github.com/pgvector/pgvector)

## Quick Start

```python
import asyncio
from arandu import MemoryClient
from arandu.providers.openai import OpenAIProvider

async def main():
    # 1. Configurar providers
    provider = OpenAIProvider(api_key="sk-...")

    # 2. Criar client
    memory = MemoryClient(
        database_url="postgresql+psycopg://user:pass@localhost/mydb",
        llm=provider,
        embeddings=provider,
    )

    # 3. Inicializar tabelas (idempotente)
    await memory.initialize()

    # 4. Write — extrai fatos automaticamente
    result = await memory.write(
        agent_id="user_123",
        message="I live in São Paulo and work at Acme Corp as a backend engineer.",
    )
    print(f"Added {len(result.facts_added)} facts, resolved {len(result.entities_resolved)} entities")

    # 5. Retrieve — encontra contexto relevante
    context = await memory.retrieve(
        agent_id="user_123",
        query="where does the user live and work?",
    )
    print(context.context)

    # 6. Cleanup
    await memory.close()

asyncio.run(main())
```

## Como Funciona

### Write Pipeline

```
Message → Extract (LLM) → Resolve Entities → Reconcile → Upsert
```

Toda mensagem passa por quatro estágios: o LLM extrai fatos estruturados, entidades são resolvidas para registros canônicos, novos fatos são reconciliados contra o conhecimento existente, e as decisões (ADD/UPDATE/NOOP/DELETE) são executadas.

> [Saiba mais sobre o Write Pipeline](concepts/write-pipeline.md)

### Read Pipeline

```
Query → Plan (deterministic) → Retrieve (semantic + keyword + graph) → Rerank → Format
```

Queries passam por um planner deterministico (sem LLM) que decide a estratégia de retrieval, depois três sinais paralelos são combinados, opcionalmente rerankeados, e comprimidos em uma string de contexto.

> [Saiba mais sobre o Read Pipeline](concepts/read-pipeline.md)

### Background Jobs

```
Clustering → Consolidation → Importance Scoring → Summary Refresh
```

Jobs periódicos em background mantêm a memória organizada e atualizada - como o processamento durante o sono no cérebro.

> [Saiba mais sobre Background Jobs](concepts/background-jobs.md)

## Arquitetura

O `arandu` é projetado em torno de três princípios:

1. **DI baseada em Protocol** - LLM e embedding providers são injetados via `typing.Protocol`. Sem vendor lock-in.
2. **Fail-safe por padrão** - Toda chamada LLM tem timeouts e fallbacks. Uma extração falha ainda registra o evento. Uma reconciliação falha tem default ADD.
3. **Composição sobre herança** - Módulos pequenos e focados compostos em pipelines. Sem hierarquias profundas de classes.

> [Saiba mais sobre a Filosofia de Design](concepts/design-philosophy.md)

## Próximos Passos

<div class="grid cards" markdown>

- :material-rocket-launch:{ .lg .middle } **Primeiros Passos**

    ---

    Guia completo de setup: PostgreSQL, pgvector, primeiro write e retrieve.

    [:octicons-arrow-right-24: Primeiros Passos](getting-started.md)

- :material-brain:{ .lg .middle } **Conceitos**

    ---

    Deep dive em como cada pipeline funciona e por quê.

    [:octicons-arrow-right-24: Write Pipeline](concepts/write-pipeline.md)

</div>
# Primeiros Passos

Este guia te leva do zero ao funcionamento com `arandu`: instalando dependências, configurando PostgreSQL com pgvector, escrevendo seus primeiros fatos e recuperando-os.

## Pré-requisitos

- **Python 3.11+**
- **PostgreSQL 15+** com a extensão [pgvector](https://github.com/pgvector/pgvector) instalada
- Uma **chave de API da OpenAI** (ou qualquer LLM/embedding provider - veja [Custom Providers](#custom-providers))

## Passo 1: Instalação

```bash
pip install arandu[openai]
```

Isso instala o SDK core mais o provider OpenAI incluído. Se você usa um LLM provider diferente, instale apenas o core:

```bash
pip install arandu
```

## Passo 2: Configurar PostgreSQL + pgvector

O `arandu` armazena fatos, entidades e embeddings no PostgreSQL usando a extensão pgvector para busca por similaridade vetorial.

### Opção A: Docker (recomendado para desenvolvimento)

```bash
docker run -d \
  --name memory-db \
  -e POSTGRES_USER=memory \
  -e POSTGRES_PASSWORD=memory \
  -e POSTGRES_DB=memory \
  -p 5432:5432 \
  pgvector/pgvector:pg16
```

A imagem `pgvector/pgvector` já vem com a extensão pré-instalada. Sua connection string será:
`postgresql+psycopg://memory:memory@localhost:5432/memory`

!!! warning "psycopg vs psycopg2"
    O Arandu usa `psycopg` (driver async), **não** `psycopg2` (sync). Sua connection string deve começar com `postgresql+psycopg://`, não `postgresql+psycopg2://`. Muitos tutoriais de Django/Flask usam psycopg2 - certifique-se de usar o correto.

### Opção B: PostgreSQL existente

Se você já tem PostgreSQL rodando, habilite a extensão pgvector:

```sql
CREATE EXTENSION IF NOT EXISTS vector;
```

!!! note "Instalação do pgvector"
    Se você não tem o pgvector instalado no seu servidor, siga o
    [guia de instalação do pgvector](https://github.com/pgvector/pgvector#installation).

## Entendendo Agent, Session e Speaker

Todo `write()` usa três identificadores. Aqui vai o que cada um faz:

**`agent_id`** identifica de quem é a memória. Pense como um cérebro. Um agente = um espaço de memória. Todos os fatos, entidades e relacionamentos vivem dentro da memória daquele agente. Se você tem dois chatbots, cada um tem seu próprio `agent_id` e eles não compartilham memórias.

**`speaker_name`** identifica quem está falando. Quando alguém diz "Eu moro em São Paulo", o SDK precisa saber quem é "Eu". Se o speaker é Rafael, "Eu" vira "Rafael mora em São Paulo". Sem `speaker_name`, o SDK não sabe a quem atribuir o "Eu" e vai levantar um `ValueError`.

**`session_id`** marca o contexto da conversa. É opcional (default `"default"`). Use quando quiser rastrear de qual conversa uma mensagem veio. Por exemplo: um ticket de suporte, uma sessão de terapia, ou uma reunião.

!!! info "A memória NÃO é separada por sessão"
    Mudar o `session_id` **não** cria uma memória separada. Todos os fatos vão pra mesma memória do agente, independente da sessão. Quando você chama `retrieve()`, ele busca tudo que o agente sabe, de todas as sessões. O `session_id` é metadata no evento, não uma chave de partição.

```python
await memory.write(
    agent_id="meu-assistente",      # de quem é a memória
    message="Eu moro em São Paulo",
    speaker_name="Rafael",          # quem está falando
    session_id="ticket-suporte-42", # tag de contexto (opcional)
)
```

Pra um chatbot simples com um usuário, você só precisa de `agent_id` e `speaker_name`. Adicione `session_id` quando quiser rastrear de onde uma conversa veio.

!!! question "Por que três campos separados?"
    Um sistema de memória precisa responder três perguntas: de quem é o cérebro que armazena? (agent), quem falou? (speaker), e em que contexto? (session). Misturar tudo num único identificador quebra quando duas pessoas falam com o mesmo agente, ou a mesma pessoa tem várias conversas. Separar mantém o modelo limpo e flexível.

## Passo 3: Inicializar o Client

```python
import asyncio
from arandu import MemoryClient, MemoryConfig
from arandu.providers.openai import OpenAIProvider

async def main():
    # Criar o provider de LLM + embedding
    provider = OpenAIProvider(api_key="sk-...")

    # Criar o memory client
    memory = MemoryClient(
        database_url="postgresql+psycopg://memory:memory@localhost:5432/memory",
        llm=provider,
        embeddings=provider,
    )

    # Criar tabelas (seguro chamar múltiplas vezes)
    await memory.initialize()

    print("Memory initialized!")
    await memory.close()

asyncio.run(main())
```

!!! tip "Usando Anthropic (Claude) em vez de OpenAI"
    ```python
    from arandu.providers.anthropic import AnthropicProvider
    from arandu.providers.openai import OpenAIProvider

    llm = AnthropicProvider(api_key="sk-ant-...")  # Claude para LLM
    embeddings = OpenAIProvider(api_key="sk-...")   # OpenAI apenas para embeddings

    memory = MemoryClient(
        database_url="postgresql+psycopg://...",
        llm=llm,
        embeddings=embeddings,  # Anthropic não oferece embeddings
    )
    ```
    Instale com: `pip install arandu[anthropic]`

!!! tip "Usando DeepSeek, Groq ou modelos locais"
    Qualquer provider compatível com OpenAI funciona com `OpenAIProvider`. Basta definir `base_url`:
    ```python
    llm = OpenAIProvider(api_key="sk-...", model="deepseek-chat", base_url="https://api.deepseek.com/v1")
    ```
    Veja o [Cookbook](cookbook.md) para mais exemplos.

O `initialize()` cria todas as tabelas e índices necessários (incluindo índices HNSW do pgvector). É idempotente - seguro para chamar a cada startup.

!!! info "Sobre o `agent_id`"
    O `agent_id` é sua **chave de particionamento**. Cada agent_id tem seu próprio espaço de memória isolado - fatos escritos para um agente nunca são retornados para outro. Pense nele como um cérebro: um agente, uma memória. Use qualquer string (ID do banco, UUID, slug). O mesmo agent_id deve ser usado em `write()` e `retrieve()` para o mesmo agente.

!!! info "Sobre o `session_id`"
    O `session_id` identifica o **contexto da conversa** (default: `"default"`). Pense nele como uma thread de conversa no WhatsApp - mesmo agente, conversas diferentes. Quando não fornecido, todas as escritas vão para a sessão `"default"`.

!!! info "Sobre o `speaker_name`"
    O `speaker_name` identifica **quem está falando** a mensagem. É um parâmetro **obrigatório** de `write()`. Pronomes como "eu", "I", "me", "myself" são automaticamente resolvidos para a entidade do speaker (`person:{speaker_slug}`). Por exemplo, se `speaker_name="Rafael"` e a mensagem diz "I live in São Paulo", o sistema entende que **Rafael** mora em São Paulo. O `agent_id` é o agente/cérebro que armazena a memória; o `speaker_name` é quem está falando.

## Passo 4: Escrever Seus Primeiros Fatos

O método `write()` recebe uma mensagem em linguagem natural e automaticamente:

1. Extrai entidades, fatos e relacionamentos usando um LLM
2. Resolve entidades para registros canônicos (deduplicação)
3. Reconcilia novos fatos contra o conhecimento existente
4. Faz upsert dos resultados no banco de dados

```python
async def write_example(memory: MemoryClient):
    # Primeira mensagem
    result = await memory.write(
        agent_id="user_123",
        message="My name is Rafael and I live in São Paulo. I work at Acme Corp as a backend engineer.",
        speaker_name="Rafael",
    )
    print(f"Facts added: {len(result.facts_added)}")
    for fact in result.facts_added:
        print(f"  [{fact.entity_name}] {fact.fact_text} (confidence: {fact.confidence})")
    # Output:
    #   [Rafael] Lives in São Paulo (confidence: 0.95)
    #   [Rafael] Works at Acme Corp as a backend engineer (confidence: 0.95)
    #   [Acme Corp] Rafael works at Acme Corp (confidence: 0.95)
    print(f"Entities resolved: {len(result.entities_resolved)}")
    print(f"Duration: {result.duration_ms:.0f}ms")

    # Segunda mensagem — o sistema reconhece "Rafael" e atualiza o conhecimento
    result = await memory.write(
        agent_id="user_123",
        message="I just moved to Rio de Janeiro. Still working at Acme though.",
        speaker_name="Rafael",
        session_id="onboarding",  # opcional — default é "default"
    )
    print(f"Facts added: {len(result.facts_added)}")
    print(f"Facts updated: {len(result.facts_updated)}")  # "lives in São Paulo" → "lives in Rio"
```

### Entendendo o WriteResult

O objeto `WriteResult` te diz exatamente o que aconteceu:

| Campo | Tipo | Descrição |
|-------|------|-----------|
| `event_id` | `str` | ID único deste evento de escrita |
| `facts_added` | `list` | Novos fatos criados (decisões ADD) |
| `facts_updated` | `list` | Fatos existentes substituídos (decisões UPDATE) |
| `facts_unchanged` | `list` | Fatos confirmados mas não alterados (decisões NOOP) |
| `facts_deleted` | `list` | Fatos retratados (decisões DELETE) |
| `entities_resolved` | `list` | Entidades identificadas e resolvidas |
| `duration_ms` | `float` | Duração total do pipeline |
| `success` | `bool` | Se o pipeline completou sem erros |
| `error` | `str \| None` | Mensagem de erro se o pipeline falhou internamente |

## Passo 5: Recuperar Contexto

O método `retrieve()` encontra fatos relevantes para uma query usando múltiplos sinais:

```python
async def retrieve_example(memory: MemoryClient):
    result = await memory.retrieve(
        agent_id="user_123",
        query="where does Rafael live and what does he do?",
    )

    # Opção 1: String pré-formatada — cole direto no prompt do LLM
    print(result.context)

    # Opção 2: Fatos individuais — para acesso programático
    for fact in result.facts:
        print(f"  [{fact.score:.2f}] {fact.entity_name}: {fact.fact_text}")

    # Retrieve dentro de uma sessão específica (opcional — omita para buscar em todas)
    session_result = await memory.retrieve(
        agent_id="user_123",
        query="onde o Rafael mora?",
        session_id="onboarding",  # opcional — default busca em todas as sessões
    )

    # Com ajustes de config (ex: desabilitar reranker para resultados mais rápidos)
    fast_result = await memory.retrieve(
        agent_id="user_123",
        query="onde o Rafael mora?",
        config_overrides={"enable_reranker": False, "topk_facts": 5},
    )

    print(f"Total candidates evaluated: {result.total_candidates}")
    print(f"Duration: {result.duration_ms:.0f}ms")
```

!!! tip "`.context` vs `.facts`"
    Use **`result.context`** quando precisa apenas de uma string para injetar no prompt do LLM - já vem formatada com labels de tier (CORE MEMORY, EXTENDED CONTEXT, etc.). Use **`result.facts`** quando precisa de acesso programático aos fatos individuais, scores e metadados.

### Overrides de Config por Request

Você pode sobrescrever qualquer campo do `MemoryConfig` para uma única request, sem alterar o config padrão do client:

```python
result = await memory.retrieve(
    agent_id="user_123",
    query="onde o Rafael mora?",
    config_overrides={
        "enable_reranker": False,
        "topk_facts": 5,
        "spreading_activation_hops": 0,
    },
)

# config_effective mostra o config efetivo usado nesta request
print(result.config_effective)
```

Apenas as chaves fornecidas são sobrescritas; todos os outros campos herdam do `MemoryConfig` do client.

### Entendendo o RetrieveResult

| Campo | Tipo | Descrição |
|-------|------|-----------|
| `facts` | `list[ScoredFact]` | Fatos ranqueados com scores |
| `context` | `str` | String de contexto pré-formatada para prompts LLM |
| `total_candidates` | `int` | Total de fatos avaliados antes do ranking |
| `duration_ms` | `float` | Duração total do pipeline |
| `config_effective` | `dict` | Valores de config efetivos usados nesta request |

Cada `ScoredFact` contém:

| Campo | Tipo | Descrição |
|-------|------|-----------|
| `fact_id` | `str` | Identificador único do fato |
| `entity_name` | `str` | Nome legível da entidade |
| `attribute_key` | `str` | Categoria/atributo do fato |
| `fact_text` | `str` | O conteúdo do fato |
| `score` | `float` | Score combinado de relevância (0-1) |
| `scores` | `dict` | Detalhamento por sinal (semantic, recency, etc.) |
| `speaker` | `str \| None` | Quem falou a mensagem de onde este fato foi extraído |

## Passo 6: Gerenciando Fatos

Além de `write()` e `retrieve()`, o SDK oferece operações CRUD para gerenciar fatos individualmente: buscar por ID, listar todos e deletar.

### Buscar um fato específico

```python
detail = await memory.get(agent_id="user_123", fact_id="algum-uuid-aqui")
if detail:
    print(f"[{detail.entity_name}] {detail.fact_text}")
    print(f"  confidence: {detail.confidence}, importance: {detail.importance}")
    print(f"  criado em: {detail.created_at}")
else:
    print("Fato não encontrado")
```

`get()` retorna um `FactDetail` ou `None`. Busca qualquer fato pelo ID — incluindo fatos que foram soft-deleted pelo pipeline de reconciliação (ou seja, substituídos por uma versão mais nova). Use para lookups diretos quando você tem o ID.

### Listar todos os fatos

```python
# Primeira página (mais recentes primeiro)
facts = await memory.get_all(agent_id="user_123", limit=50, offset=0)
for fact in facts:
    print(f"[{fact.fact_id}] {fact.entity_name}: {fact.fact_text}")

# Próxima página
page2 = await memory.get_all(agent_id="user_123", limit=50, offset=50)
```

`get_all()` retorna apenas fatos **ativos** (`valid_to IS NULL`), ordenados por `created_at` decrescente. Use `limit` e `offset` para paginação.

#### Filtrando por entidade

```python
# Só fatos sobre a Ana
ana_facts = await memory.get_all(agent_id="user_123", entity_keys=["person:ana"])

# Fatos sobre Pedro OU Ana
facts = await memory.get_all(agent_id="user_123", entity_keys=["person:pedro", "person:ana"])
```

O filtro `entity_keys` também funciona com `retrieve()`:

```python
# Busca semântica restrita a fatos sobre a Ana
result = await memory.retrieve(
    agent_id="user_123",
    query="o que ela faz de trabalho?",
    entity_keys=["person:ana"],
)
```

Quando `entity_keys` é fornecido, apenas fatos linkados a pelo menos uma das entidades especificadas são retornados (lógica OR). Sem `entity_keys`, todos os fatos são buscados como antes.

!!! tip "Aliases são resolvidos automaticamente"
    `entity_keys` aceita tanto chaves canônicas (`person:pedro_menezes`) quanto aliases (`person:pedro` ou só `pedro`). O SDK resolve aliases via `memory_entity_aliases` antes de filtrar, então você não precisa saber a forma canônica. Qualquer chave que não resolva aparece em `result.warnings` — `retrieve()` nunca retorna "zero silencioso" por causa de uma chave errada.

```python
result = await memory.retrieve(
    agent_id="user_123",
    query="o que ela faz?",
    entity_keys=["person:pedro", "person:unknown"],
)
# result.facts → filtrado pela canônica do Pedro (alias resolvido)
# result.warnings → ["entity_key 'person:unknown' not found (not canonical, no matching alias)"]
```

### Deletar um fato

```python
deleted = await memory.delete(agent_id="user_123", fact_id="algum-uuid-aqui")
print(f"Deletado: {deleted}")  # True se encontrado e removido, False caso contrário
```

`delete()` faz um **hard delete** — a row é fisicamente removida do banco de dados. Entity links associados são removidos automaticamente via cascade. Esta é a ação explícita do usuário ("quero que isso suma"); o soft-delete do pipeline via `valid_to` é um mecanismo separado para reconciliação.

### Deletar todos os fatos

```python
count = await memory.delete_all(agent_id="user_123")
print(f"{count} fatos deletados")
```

`delete_all()` remove **todos** os fatos do agente. Use com cuidado — é irreversível. Destinado a cenários de reset/debug.

### Listar entidades

```python
entities = await memory.entities(agent_id="user_123", limit=50)
for entity in entities:
    print(f"[{entity.entity_type}] {entity.display_name} ({entity.fact_count} fatos)")
    if entity.summary_text:
        print(f"  Resumo: {entity.summary_text}")
```

`entities()` retorna entidades ativas ordenadas por `last_seen_at` decrescente. Use pra ver o que o agente conhece — pessoas, lugares, organizações, etc.

### Entendendo o FactDetail

| Campo | Tipo | Descrição |
|-------|------|-----------|
| `fact_id` | `str` | Identificador único do fato |
| `entity_name` | `str` | Nome legível da entidade |
| `entity_key` | `str` | Chave canônica da entidade |
| `entity_type` | `str` | Tipo da entidade (ex: "person", "organization") |
| `attribute_key` | `str \| None` | Categoria/atributo do fato |
| `fact_text` | `str` | O conteúdo do fato |
| `category` | `str \| None` | Categoria do fato |
| `confidence` | `float` | Score de confiança (0-1) |
| `importance` | `float` | Score de importância (0-1) |
| `valid_from` | `datetime \| None` | Quando o fato se tornou válido |
| `created_at` | `datetime \| None` | Quando o fato foi criado |
| `source_context` | `str \| None` | Trecho do contexto original |
| `speaker` | `str \| None` | Quem falou a mensagem de onde este fato foi extraído |

### Entendendo o EntityDetail

| Campo | Tipo | Descrição |
|-------|------|-----------|
| `entity_id` | `str` | Identificador único da entidade |
| `canonical_key` | `str` | Chave canônica da entidade (ex: "person::rafael") |
| `display_name` | `str` | Nome legível da entidade |
| `entity_type` | `str` | Tipo da entidade (ex: "person", "organization") |
| `summary_text` | `str \| None` | Resumo auto-gerado da entidade |
| `fact_count` | `int` | Número de fatos ligados a esta entidade |
| `importance_score` | `float \| None` | Score de importância computado |
| `first_seen_at` | `datetime \| None` | Quando a entidade foi mencionada pela primeira vez |
| `last_seen_at` | `datetime \| None` | Quando a entidade foi mencionada pela última vez |
| `profile_text` | `str \| None` | Perfil consolidado da entidade |

## Passo 7: Configurar (Opcional)

Todos os aspectos do pipeline são configuráveis via `MemoryConfig`:

```python
from arandu import MemoryConfig
from arandu.providers.openai import OpenAIProvider

# Provider único para todas as operações LLM (extração, reranker, etc.)
llm = OpenAIProvider(api_key="sk-...", model="gpt-4o")

config = MemoryConfig(
    # Timeout curto para chat em tempo real
    extraction_timeout_sec=15.0,

    # Ajustar retrieval
    topk_facts=30,
    min_similarity=0.25,
    enable_reranker=True,

    # Pesos customizados (default: semantic=0.70, recency=0.20, importance=0.10)
    score_weights={
        "semantic": 0.60,
        "recency": 0.25,
        "importance": 0.15,
    },

    # Definir timezone para cálculos de recência
    timezone="America/Sao_Paulo",
)

memory = MemoryClient(
    database_url="postgresql+psycopg://memory:memory@localhost/memory",
    llm=llm,
    embeddings=llm,
    config=config,
)
```

Todos os parâmetros têm defaults sensatos - você só precisa sobrescrever o que importa para o seu caso de uso.

## Passo 8: Debug com Verbose Mode

Passe `verbose=True` para `write()` ou `retrieve()` para obter um trace detalhado de cada step do pipeline:

```python
result = await memory.write(agent_id="user_123", message="...", speaker_name="Rafael", verbose=True)

# Acessar o trace do pipeline
if result.pipeline:
    for step in result.pipeline.steps:
        print(f"  {step.name}: {step.duration_ms:.1f}ms")
        print(f"    data: {step.data}")
```

O trace inclui steps como `extraction`, `entity_resolution`, `reconciliation` e `upsert`, cada um com timing e dados intermediários. Se o pipeline falhar internamente, um step `error` é adicionado com os detalhes da exceção - útil para diagnosticar falhas silenciosas.

Você pode serializar o trace completo com `result.pipeline.to_dict()`.

## Passo 9: Cleanup

Sempre feche o client ao terminar para liberar conexões do banco:

```python
await memory.close()
```

Ou use como um padrão de contexto async:

```python
memory = MemoryClient(...)
await memory.initialize()
try:
    # ... usar memory
finally:
    await memory.close()
```

## Exemplo Completo

Aqui está um exemplo completo funcional juntando tudo:

```python
import asyncio
from arandu import MemoryClient
from arandu.providers.openai import OpenAIProvider


async def main():
    provider = OpenAIProvider(api_key="sk-...")
    memory = MemoryClient(
        database_url="postgresql+psycopg://memory:memory@localhost:5432/memory",
        llm=provider,
        embeddings=provider,
    )
    await memory.initialize()

    try:
        # Escrever alguns fatos
        await memory.write(
            agent_id="user_123",
            message="I'm a software engineer living in Berlin. I love cycling and craft coffee.",
            speaker_name="Rafael",
        )
        await memory.write(
            agent_id="user_123",
            message="My girlfriend Ana is a designer. We adopted a cat named Pixel last month.",
            speaker_name="Rafael",
        )

        # Recuperar contexto
        result = await memory.retrieve(agent_id="user_123", query="tell me about this person")
        print(result.context)

        # Retrieval direcionado
        result = await memory.retrieve(agent_id="user_123", query="who is Ana?")
        for fact in result.facts:
            print(f"  [{fact.score:.2f}] {fact.entity_name}: {fact.fact_text}")
    finally:
        await memory.close()


asyncio.run(main())
```

## Custom Providers

O `arandu` usa protocolos Python para injeção de dependência. Você pode trazer qualquer LLM ou embedding provider implementando duas interfaces simples:

```python
from arandu.protocols import LLMProvider, LLMResult, EmbeddingProvider

class MyLLMProvider:
    async def complete(
        self,
        messages: list[dict],
        temperature: float = 0,
        response_format: dict | None = None,
        max_tokens: int | None = None,
    ) -> LLMResult:
        # Chame seu LLM aqui
        text = ...  # obtenha o texto de resposta do seu LLM
        return LLMResult(text=text, usage=None)

class MyEmbeddingProvider:
    async def embed(self, texts: list[str]) -> list[list[float]]:
        # Retorne embeddings para um batch
        ...

    async def embed_one(self, text: str) -> list[float] | None:
        # Retorne embedding para um único texto
        ...
```

Sem herança necessária - apenas implemente os métodos com as assinaturas corretas.

## Próximos Passos

- [**Write Pipeline**](concepts/write-pipeline.md) - Entenda como fatos são extraídos, entidades resolvidas e conhecimento reconciliado
- [**Read Pipeline**](concepts/read-pipeline.md) - Aprenda como o retrieval multi-signal encontra os fatos mais relevantes
- [**Tipos de Dados & Schema**](advanced/data-types.md) - Referência do schema do banco (tabelas, colunas, tipos) para queries SQL diretas
- [**Background Jobs**](concepts/background-jobs.md) - Configure clustering, consolidation e importance scoring
- [**Filosofia de Design**](concepts/design-philosophy.md) - Explore a arquitetura inspirada em neurociência
# 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"]
```
# Read Pipeline

Quando você chama `memory.retrieve()`, o SDK busca tudo que sabe sobre um agente 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(agent_id, query)` executa cinco estágios:

1. **Plan** - Descobre *o que* buscar. Detecta saudações, padrões de agregação e queries amplas. Entity resolution roda deterministicamente em paralelo.
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: Planner Determinístico

**Em português claro:** Antes de buscar, o pipeline analisa sua query pra decidir que tipo de busca rodar. Detecta saudações (skip), queries de agregação ("quem são meus amigos?"), requests amplas ("me conta tudo") e identifica quais entidades são mencionadas. Tudo **determinístico** — mesma query sempre produz o mesmo plano, zero chamadas LLM.

O planner produz um `RetrievalPlan` usando regex pattern matching e lookups no schema. Nenhum LLM é envolvido.

!!! note "Por que sem LLM no planner?"
    Antes da v0.13.0, o planner chamava um LLM pra reformulação de query e extração de entidades. Isso introduzia **não-determinismo**: a mesma query contra a mesma memória podia retornar fatos diferentes entre runs, porque APIs cloud de LLM não são determinísticas mesmo com `temperature=0` (comportamento documentado — batching, fp16 rounding, roteamento de GPU introduzem variação). A v0.13.0 substitui o planner LLM por uma implementação totalmente determinística. A query vai direto pra busca semântica sem alteração, e extração de entidades é feita por um resolver determinístico dedicado.

### 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 para busca semântica (sempre a original) | `"onde eu moro?"` (passada sem alteração) |
| `pattern_queries` | Padrões SQL LIKE para agregação | `["person:%"]` (de "quem são meus amigos?") |
| `broad_query` | Se deve expandir o escopo do graph | `true` para "me conte tudo sobre..." |
| `reason` | Explicação da estratégia | `"deterministic"`, `"aggregation"`, `"broad"`, `"greeting"` |

### Entity Resolution

Quando você pergunta "Onde o Carlos mora?", o pipeline precisa descobrir que "Carlos" significa a entidade `person:carlos` no banco. Ele usa dois métodos determinísticos:

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. **Query expansion (priming de aliases)** — `expand_query()` resolve aliases e busca vizinhos 1-hop no KG, adicionando entidades relacionadas.

Ambas as fontes são **unificadas** antes do graph gate. Se qualquer fonte identificar uma entidade, o graph traversal roda.

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

### Detecção de Agregação

Para queries como "quem são meus amigos?" ou "meus projetos", o planner faz match de keywords contra prefixos do schema e gera padrões SQL LIKE (ex: `person:%`). Isso só dispara quando o prefixo realmente existe no schema do usuário.

### Estratégia Skip

Para saudações e mensagens casuais ("oi", "bom dia", "e aí"), o planner retorna `strategy: "skip"` via regex matching, fazendo short-circuit no pipeline. Sem queries no banco, sem chamadas LLM, resposta instantânea.

### Resolução de Anáfora (Responsabilidade do Caller)

Se sua query contém pronomes ("Onde **ela** mora?"), o Arandu SDK **não** resolve. Resolução de pronomes depende do contexto de conversa (memória de curto prazo), que é domínio do caller. Resolva pronomes antes de chamar `retrieve()`:

```python
# O caller (seu agente) resolve "ela" → "Ana" usando contexto de conversa
resolved_query = "Onde a Ana mora?"  # não "Onde ela mora?"
result = await memory.retrieve(agent_id="user_123", query=resolved_query)
```

!!! info "Paralelo com neurociência"
    O planner 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 detecta padrões de query como pistas que guiam os sinais de retrieval.

??? example "Passo a passo: ciclo de vida completo de uma query"
    **Query:** "Onde o Marcos Tavares mora?"

    **Estágio 1 — Planejamento (determinístico):**
    ```
    Check saudação: sem match → prossegue
    Check agregação: sem match → sem pattern queries
    Check broad: sem match → broad_query = false
    Resolver determinístico: "marcos tavares" → person:marcos_tavares (slug match)
    similarity_query = "Onde o Marcos Tavares mora?" (original, sem alteração)
    ```

    **Estágio 2 — Retrieval multi-signal (paralelo):**
    ```
    Semântico: embedding("Onde o Marcos Tavares mora?") → top match: "Marcos Tavares lives in Porto Alegre" (0.91)
    Keyword: "marcos" + "tavares" → 4 fatos sobre Marcos
    Graph: BFS a partir de person:marcos_tavares → encontra fatos via entity links + relationships
    ```

    **Estágio 3 — Enhancement:**
    ```
    Spreading activation: a partir do seed "lives in Porto Alegre" → encontra fatos relacionados:
      "Marcos Tavares is married to Carolina" (via entity hop)
      "Carolina is an architect" (via 2-hop)
    ```

    **Estágio 4 — Reranking (blend multiplicativo):**
    ```
    "Marcos Tavares lives in Porto Alegre" → formula=0.91, reranker=1.0 → final=0.91
    "Marcos Tavares is a product manager at Vertix" → formula=0.65, reranker=0.2 → final=0.28
    "Carolina is an architect" → formula=0.30, reranker=0.0 → final=0.09 → filtrado (< 0.15)
    ```

    **Estágio 5 — Formatação:**
    ```
    Known facts:
    - Marcos Tavares lives in Porto Alegre
    ```

    **Resultado:** 1 fato altamente relevante, 210ms, 800 tokens.

---

## 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(dedup + 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 (passada sem alteração pelo planner)
- Busca na tabela `MemoryFact` com índice HNSW
- Retorna top-N candidatos acima do threshold `min_similarity`
- Filtros: `agent_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`
- **Hop decay**: Fatos no Hop 1 recebem score completo (1.0×). Fatos no Hop 2 recebem penalidade de 0.5×. Isso evita que fatos distantes dominem o pool de candidatos.
- 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 × hop_decay`
- `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.

??? example "Passo a passo: retrieval cross-entity via entity links"
    **Query:** "O que aconteceu com a Vertix?"

    **Sem entity links (comportamento antigo):**
    ```
    Graph começa de organization:vertix
    Busca MemoryFact WHERE entity_key = 'organization:vertix'
    Encontra: apenas fatos onde Vertix é o subject PRIMÁRIO
    Perde: "Clara Rezende left Vertix" (entity_key = person:clara_rezende)
    ```

    **Com entity links (comportamento atual):**
    ```
    Graph começa de organization:vertix
    Busca MemoryFactEntityLink WHERE entity_key = 'organization:vertix'
    Encontra fact_ids linkados à Vertix, independente do subject primário:
      → "Clara Rezende left Vertix" (primário: Clara, link: Vertix) ✅
      → "Vertix received Series A of R$ 20M" (primário: Vertix) ✅
      → "Ricardo Gomes is co-founder of Vertix" (primário: Ricardo, link: Vertix) ✅
      → "Vertix signed contract with Ambev" (primário: Vertix) ✅
    ```

    **Resultado:** 4 fatos encontrados vs 2 sem links. A query sobre Vertix traz fatos de Clara, Ricardo e o deal da Ambev - todos linkados à Vertix mas não primariamente sobre a Vertix.

### 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 faz blend com estes pesos"
    Por padrão, `enable_reranker=True` - o reranker LLM usa um blend multiplicativo com o score da fórmula computado a partir destes pesos. O score da fórmula continua sendo importante porque o reranker só pode atenuar ou amplificar, nunca zerar. Configure `enable_reranker=False` para depender apenas destes pesos no ranking final.

```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.0 - 1.0 | Valor raw de importância do banco de dados. Computado pelo job de importância em background 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 ao longo do tempo. |
| `confidence` | Merge & Rank | 0.0 - 1.0 | Confiança efetiva após decay temporal. Presente no dict `scores` para debugging, mas NÃO faz parte da fórmula ponderada (`score_weights` usa apenas `semantic`, `recency`, `importance`). 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`). |
| `reranker` | Reranking | 0.0 - 1.0 | Score de relevância baseado em LLM. Presente apenas quando `enable_reranker=True`. Float contínuo retornado 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 com alto `reinforcement_count` (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 |
| `min_score` | `0.15` | Score final mínimo para fatos retornados |
| `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 com alto `reinforcement_count` (incrementado por decisões NOOP no write) recebem um boost aditivo no score:

- Alto reinforcement count → 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:

- O reranker avalia **2× `topk_facts`** candidatos (padrão: 40). Esse pool expandido garante que fatos semanticamente relevantes além do top-20 inicial cheguem no reranker. Aumente `topk_facts` pra expandir a cobertura do reranker.
- 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 o mesmo provider LLM configurado no client (não precisa de provider separado)

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

### Configuração

| Parâmetro | Tipo | Default | Descrição |
|-----------|------|---------|-----------|
| `reranker_weight` | float | `0.70` | Peso do score do reranker no blend multiplicativo |
| `min_reranker_score` | float | `0.10` | Score mínimo do reranker; fatos abaixo são eliminados |
| `reranker_timeout_sec` | float | `5.0` | Timeout para a chamada LLM do reranker (segundos) |

!!! warning "Veto do reranker: `min_reranker_score`"
    Quando `enable_reranker=True`, qualquer fato que recebe um score do reranker **abaixo** de `min_reranker_score` (padrão `0.10`) é eliminado dos resultados (final_score definido como 0.0). Isso dá ao reranker poder de veto sobre fatos completamente irrelevantes - mesmo que o score da fórmula seja alto (ex: graph BFS dá 0.80 pra um fato distante e não relacionado). Quando `enable_reranker=False`, esta configuração não tem efeito. Ajuste: `config_overrides={"min_reranker_score": 0.05}` para resultados mais permissivos, `0.20` para filtragem mais rigorosa.

!!! info "Scoring por multiplicative blend"
    O reranker NÃO substitui o score da fórmula. Ele usa um **blend multiplicativo**:

    `final_score = formula_score × (floor + reranker_weight × reranker_score)`

    onde `floor = 1 - reranker_weight`. Com o default `reranker_weight=0.70`, um fato com formula=0.9 e reranker=0.0 fica com final = 0.9 × (0.30 + 0) = 0.27 (não 0.0). Um fato com formula=0.9 e reranker=1.0 fica com final = 0.9 × (0.30 + 0.70) = 0.90. O reranker pode amplificar ou atenuar fatos mas **não consegue zerar** um fato com bons sinais de retrieval apenas pelo blend. Porém, `min_reranker_score` É uma exceção - fatos com score abaixo dele são zerados independentemente do score da fórmula. O dict `scores` preserva tanto `formula` (pré-reranker) quanto `reranker` (score do LLM) pra debugging.

??? example "Passo a passo: como o blend do reranker funciona"
    **Query:** "Qual o time de futebol do Bruno Almeida?"

    **Candidatos pré-reranker (scores da fórmula):**
    ```
    1. "Bruno Almeida runs marathons" → formula = 0.45 (semântico: similaridade com "sports")
    2. "Bruno Almeida developed an ML model" → formula = 0.38
    3. "Bruno Almeida works at Orion Tech" → formula = 0.35
    ```

    **Scores do reranker (avaliação do LLM):**
    ```
    1. "Bruno Almeida runs marathons" → reranker = 0.0 (maratona ≠ futebol)
    2. "Bruno Almeida developed an ML model" → reranker = 0.0 (irrelevante)
    3. "Bruno Almeida works at Orion Tech" → reranker = 0.0 (irrelevante)
    ```

    **Blend multiplicativo** (weight=0.70, floor=0.30):
    ```
    1. final = 0.45 × (0.30 + 0.70 × 0.0) = 0.45 × 0.30 = 0.135 → filtrado (< 0.15)
    2. final = 0.38 × 0.30 = 0.114 → filtrado
    3. final = 0.35 × 0.30 = 0.105 → filtrado
    ```

    **Resultado:** 0 fatos retornados. Correto - não existe informação sobre o time de futebol do Bruno na memória.

    **Compare com uma query relevante** - "O que o Bruno Almeida desenvolveu?":
    ```
    "Bruno developed an ML model for fraud detection" → formula=0.92, reranker=1.0
    final = 0.92 × (0.30 + 0.70 × 1.0) = 0.92 × 1.0 = 0.92 ✅
    ```

---

## Estágio 5: Formatação

**Em portugues claro:** O pipeline pega os fatos ranqueados e organiza numa string limpa, pronta pra consumo por LLM. O formato foi projetado para ser direto e sem ruido -- fatos aparecem como uma lista simples em "Known facts:", conversas relevantes em "Relevant conversations:", e padroes observados em "Observed patterns:". Tudo dentro de um orcamento de tokens pra nao estourar seu prompt.

### Formato de Contexto

O contexto e organizado em tres secoes dentro de um orcamento de tokens (`context_max_tokens`):

!!! note "`context_max_tokens` e um orcamento proporcional, nao um limite rigido"
    O parametro `context_max_tokens` controla o tamanho **relativo** do contexto de saida, mas a contagem real de tokens pode exceder o valor configurado. O pipeline garante um contexto minimo para fatos e usa o parametro como orcamento proporcional entre secoes. Trate como um target, nao um limite estrito.

| Secao | Conteudo |
|-------|----------|
| **Known facts** | Lista limpa de fatos, ordenados por score. Sem timestamps, sem prefixo de entidade, sem qualificadores como "assert confidently" |
| **Observed patterns** | Meta-observations (padroes, tendencias, insights detectados pelos background jobs) |
| **Relevant conversations** | Texto original de eventos recentes (ate 300 caracteres cada), com data |

O formato foi projetado para **consumo direto por LLMs** -- limpo, sem ruido, sem metadados desnecessarios.

!!! note "Perfis de entidade nao sao injetados no contexto"
    Perfis de entidade (`profile_text`) sao usados internamente pelo write pipeline como contexto para a extracao informada. Eles **nao** sao incluidos no contexto de retrieval retornado por `retrieve()`. O retrieval retorna apenas fatos individuais, padroes e eventos.

### Configuracao

| Parametro | Default | Descricao |
|-----------|---------|-----------|
| `context_max_tokens` | `2000` | Maximo de tokens no contexto formatado |
| `hot_tier_ratio` | `0.50` | Parcela do orcamento para fatos com scores mais altos |
| `warm_tier_ratio` | `0.30` | Parcela do orcamento para fatos de suporte |

### Formato de Output

A string `context` e formatada para injecao direta em prompts LLM:

```
Known facts:
- Lives in Sao Paulo
- Works at Acme Corp as a backend engineer
- Wife's name is Ana

Observed patterns:
- Prefers working late at night

Relevant conversations:
- (2026-03-28) Had a great meeting with the product team about the roadmap...
```

---

## RetrieveResult

```python
result = await memory.retrieve(agent_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.fact_text}")
    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["Planner Determinístico\n(regex + schema)"]
    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 neurociencia"
    O orcamento de tokens age como o limite de capacidade da working memory. Fatos com scores mais altos recebem prioridade (foco de atencao), enquanto padroes e eventos fornecem contexto de suporte. Isso espelha o modelo de processos embutidos de Cowan, onde um numero pequeno de itens esta no foco de atencao, cercado por memoria de longo prazo ativada.
# Filosofia de Design

O `arandu` é projetado em torno de duas bases: **princípios de engenharia de software** que o tornam confiável e extensível, e **modelos de ciência cognitiva** que informam sua arquitetura. Esta página cobre ambos - as decisões de engenharia e os paralelos com neurociência que as inspiraram.

---

## Princípios de Engenharia

### Injeção de Dependência Baseada em Protocol

O SDK usa `typing.Protocol` do Python para todas as dependências externas (LLM, embeddings). Sem herança necessária - apenas implemente as assinaturas dos métodos:

```python
@runtime_checkable
class LLMProvider(Protocol):
    async def complete(
        self,
        messages: list[dict],
        temperature: float = 0,
        response_format: dict | None = None,
        max_tokens: int | None = None,
    ) -> LLMResult: ...
```

**Por quê:** Vendor lock-in mata adoção. Ao usar tipagem estrutural (duck typing), qualquer LLM provider funciona sem herdar de uma classe base. O provider OpenAI está incluído por conveniência, mas você pode trocar por Anthropic, modelos locais ou endpoints customizados sem nenhuma mudança no SDK.

### Conteudo Gerado em Ingles

Todo conteudo gerado por LLM -- perfis de entidade, summaries, resumos de cluster, meta-observations e diretivas procedurais -- e produzido em **ingles**, independente da lingua da conversa do usuario.

**Por que:** Consistencia no retrieval. Quando perfis e summaries estao numa lingua unica, a comparacao semantica via embeddings e mais precisa (sem mistura de idiomas no mesmo embedding space). Alem disso, LLMs tendem a produzir output mais consistente e preciso em ingles.

**Fatos permanecem na lingua original:** Os fatos (`fact_text`) sao extraidos na lingua em que o usuario escreveu. Se a conversa e em portugues, os fatos ficam em portugues. Apenas o conteudo sintetizado (perfis, summaries, padroes) e normalizado para ingles.

### Fail-Safe por Padrao

Todo estagio do pipeline tem comportamento de fallback:

| Estágio | Falha | Fallback |
|---------|-------|----------|
| Extraction | Timeout/erro do LLM | Retorna extração vazia; evento ainda é registrado |
| Entity Resolution | Fallback LLM falha | Cria nova entidade (prefere duplicatas a dados perdidos) |
| Reconciliation | Erro do LLM | Default para ADD |
| Reranking | Reranker falha | Mantém ranking original |
| Background jobs | Qualquer job falha | Outros jobs continuam independentemente |

**Por quê:** Em um agente de IA em produção, a memória é um sistema de suporte - nunca deve crashar o fluxo principal. Uma resposta degradada (faltando algum contexto) é sempre melhor que um erro.

### Composição Sobre Herança

O SDK não tem classes base abstratas, nem hierarquias profundas de classes. É construído com módulos pequenos e focados compostos em pipelines:

- `write/extract.py` → `write/entity_resolution.py` → `write/reconcile.py` → `write/upsert.py`
- `read/retrieval_agent.py` → `read/retrieval.py` → `read/reranker.py`

**Por quê:** Cada módulo tem uma única responsabilidade com inputs e outputs claros. Você pode entender, testar e substituir qualquer módulo independentemente. Segue a filosofia Unix: faça uma coisa bem feita.

### Segurança Transacional com Savepoints

Operações de escrita usam savepoints do banco de dados (`session.begin_nested()`) para que uma falha em um fato não aborte o batch inteiro:

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

**Por quê:** Em um pipeline que processa múltiplos fatos por mensagem, transações atômicas tudo-ou-nada são frágeis demais. Savepoints dão atomicidade por fato mantendo a transação externa viva.

---

## Paralelos com Neurociência

A arquitetura do `arandu` se inspira em modelos estabelecidos de neurociência cognitiva. Cada paralelo abaixo mapeia um componente do sistema para seu correspondente biológico.

### Codificação: O Write Pipeline

**Sistema:** Message → Alias Lookup → Pré-retrieval → Extração Informada → Resolve → Upsert

**Cérebro:** Input sensorial → Resposta de orientação → Ativação de esquemas → Codificação orientada por esquema → Associação → Armazenamento

Quando você vivencia algo, seu cérebro não grava um vídeo bruto. Ele codifica uma **representação seletiva** - extraindo características salientes, vinculando-as ao conhecimento existente, e armazenando o resultado de forma que possa ser recuperado depois. O write pipeline faz o mesmo:

- **Alias lookup + Pré-retrieval** é a resposta de orientação: antes de codificar informação nova, o cérebro primeiro verifica "eu já sei algo sobre isso?" -- ativando esquemas existentes que vão guiar a codificação. Da mesma forma, o pipeline carrega perfis e fatos existentes antes de chamar o LLM.
- **Extração informada** é a codificação orientada por esquema: o cérebro humano não codifica informação no vácuo -- novos estímulos são processados no contexto de esquemas mentais existentes (Bartlett, 1932). O LLM recebe contexto existente e decide na fonte o que é genuinamente novo (NEW) vs. o que atualiza conhecimento existente (UPDATE). Isso reduz drasticamente a redundância.
- **Entity resolution** é a associação: vincular novas menções a traços de memória existentes
- **Upsert** é o armazenamento: comprometer o traço processado na memória de longo prazo

!!! note "Da extração cega para a extração informada"
    Na versão anterior, o pipeline usava extração cega (sem contexto) seguida de reconciliação separada. Isso espelhava um modelo onde codificação e reconsolidação são processos separados. A extração informada unifica esses dois processos, assim como a neurociência moderna reconhece que codificação e reconsolidação compartilham circuitos hipocampais -- a codificação de nova informação é inerentemente influenciada por memórias existentes.

### Memória Associativa: Entity Resolution

**Sistema:** Resolução em 3 fases (exact → fuzzy → LLM)

**Cérebro:** Pattern completion em circuitos hipocampais-neocorticais

O cérebro não armazena memórias como registros isolados - armazena como padrões de ativação em redes neurais. Quando você encontra uma pista parcial ("Carol"), seu cérebro completa o padrão para recuperar a representação completa ("Carolina, minha colega do trabalho").

A entity resolution espelha esse processo:

- **Exact match** = recuperação direta (associações fortes e bem estabelecidas)
- **Fuzzy match** = pattern completion (pista parcial ativa o padrão existente mais similar)
- **LLM fallback** = recall deliberado (esforço consciente para desambiguar quando a recuperação automática falha)

O **fuzzy threshold** (0.85) e a **faixa de fallback LLM** (0.50-0.85) modelam o gradiente de confiança do cérebro: matches fortes são automáticos, matches ambíguos requerem deliberação.

### Reconsolidação: Reconciliação de Fatos

**Sistema:** Decisões ADD / UPDATE / NOOP / DELETE

**Cérebro:** Reconsolidação de memória (Nader, Schiller, & LeDoux, 2000)

Quando uma memória é recuperada, ela entra em um **estado lábil** onde pode ser modificada. Isso é a reconsolidação - o mecanismo do cérebro para atualizar memórias com nova informação preservando o traço original.

O estágio de reconciliação modela esse processo:

- **NOOP** = recuperação sem modificação (memória confirmada, `last_confirmed_at` atualizado)
- **UPDATE** = reconsolidação (memória antiga substituída, nova versão criada com link de proveniência via `supersedes_fact_id`)
- **ADD** = nova codificação (sem memória existente para reconsolidar)
- **DELETE** = esquecimento ativo (retração explícita, modelado definindo `invalidated_at`)

O sistema de versionamento de fatos (`valid_from`, `valid_to`, `supersedes_fact_id`) preserva o histórico completo - assim como o cérebro retém traços de memórias originais mesmo após a reconsolidação.

### Spreading Activation: Graph Retrieval

**Sistema:** Traversal BFS 2-hop com fator de decay

**Cérebro:** Spreading activation em redes semânticas (Collins & Loftus, 1975)

No modelo de Collins e Loftus, quando um conceito é ativado (ex: "caminhão de bombeiros"), a ativação se espalha ao longo de links associativos para conceitos relacionados ("vermelho", "caminhão", "emergência"), com a força diminuindo conforme a distância aumenta.

O graph retrieval implementa isso diretamente:

- **Entidades semente** da query ativam os nós iniciais
- **Hop 1** ativa vizinhos diretos (sem pruning - todas as conexões disparam)
- **Hop 2** ativa conexões de segundo grau (com pruning por `min_edge_strength`)
- **Fator de decay** (0.50 por hop) modela a atenuação da ativação ao longo da distância
- **Edge strength** modela a força associativa entre conceitos (reforçada por co-menção repetida)

O `query_bonus` (1.5x) para entidades cujos nomes aparecem na query modela **priming top-down** - quando você menciona explicitamente uma entidade, suas conexões são mais fortemente ativadas.

### Sleep-Time Compute: Processamento em Background

**Sistema:** Clustering, consolidation, importance scoring, summary refresh

**Cérebro:** Consolidação de memória durante o sono (Diekelmann & Born, 2010)

Durante o sono, o cérebro realiza manutenção crítica:

1. **Replay hipocampal** - Experiências recentes são reexecutadas em forma comprimida, transferindo-as do armazenamento de curto prazo (hipocampal) para longo prazo (neocortical)
2. **Homeostase sináptica** - Sinapses fortemente ativadas são mantidas enquanto fracamente ativadas são podadas (Tononi & Cirelli)
3. **Detecção de padrões** - O neocórtex detecta regularidades estatísticas entre episódios
4. **Extração de essência** - Memórias episódicas detalhadas são comprimidas em conhecimento semântico

Os background jobs mapeiam para esses processos:

| Processo cerebral | Job do sistema | Mecanismo |
|-------------------|---------------|-----------|
| Replay hipocampal | Consolidation | Revisa eventos recentes, detecta padrões e contradições |
| Homeostase sináptica | Importance scoring | Pontua entidades por densidade + recência + frequência de retrieval + conectividade |
| Detecção de padrões | Community detection | Encontra grupos de entidades relacionadas via análise de grafos |
| Extração de essência | Summary refresh + Memify | Gera resumos comprimidos a partir de fatos detalhados |

### Curva de Esquecimento: Vitalidade e Recência

**Sistema:** Decay de recência, scoring de vitalidade, pruning baseado em importância

**Cérebro:** Curva de esquecimento de Ebbinghaus (1885)

Hermann Ebbinghaus demonstrou que a retenção de memória decai exponencialmente ao longo do tempo, mas cada recuperação (prática) reseta a curva e desacelera o decay futuro. Este é o **efeito de espaçamento** - o achado mais robusto na pesquisa sobre memória.

O `arandu` modela isso com:

- **Decay de recência** - Decay exponencial com half-life configurável (`recency_half_life_days`). Fatos recentes pontuam mais alto. Isso modela a curva básica de esquecimento.
- **Reforço por retrieval** - Cada decisão NOOP (fato confirmado durante write) atualiza `last_confirmed_at`, efetivamente "praticando" o fato e resetando sua curva de decay.
- **Scoring de vitalidade** - Combina recência, recência de confirmação (`last_confirmed_at`) e importância para determinar quão "vivo" um fato está. Fatos de baixa vitalidade são candidatos a consolidation ou pruning.

### Atenção Seletiva: Reranking

**Sistema:** LLM reranker em candidatos de retrieval

**Cérebro:** Atenção seletiva (Broadbent, 1958; Treisman, 1964)

O cérebro não processa todo input sensorial igualmente - a atenção seletiva filtra e prioriza informações com base nos objetivos atuais. O efeito cocktail party demonstra isso: você consegue focar em uma conversa em um ambiente barulhento filtrando sinais irrelevantes.

O reranker age como o filtro de atenção:

- Sinais brutos de retrieval (semantic, keyword, graph) produzem um conjunto amplo de candidatos - como o input sensorial completo
- O reranker avalia cada candidato contra a intenção da query - como a seleção atencional
- Apenas os fatos mais relevantes passam para o contexto - como o sinal atendido

É por isso que o reranker usa um LLM (não apenas heurísticas de scoring): a atenção é direcionada a objetivos e requer compreender o **significado** tanto da query quanto dos candidatos.

### Working Memory: Orçamento de Contexto

**Sistema:** Orçamento de tokens com tiers hot/warm/cold

**Cérebro:** Working memory (Baddeley & Hitch, 1974; Cowan, 2001)

A working memory tem um limite de capacidade estrito -- Cowan estima que 4 mais ou menos 1 itens podem ser mantidos no foco de atencao simultaneamente. O orcamento de contexto modela essa restricao:

- **Orcamento de tokens** = limite de capacidade (voce nao pode enviar contexto infinito para um LLM)
- **Known facts** = foco de atencao (fatos mais relevantes, ordenados por score)
- **Observed patterns** = conhecimento de alto nivel (padroes, tendencias, insights)
- **Relevant conversations** = contexto episodico de suporte (eventos recentes)

Essa abordagem garante que o LLM receba um contexto focado e priorizado, em vez de um despejo ruidoso de tudo que o sistema sabe.

---

## Tabela Resumo

| Componente do Sistema | Modelo de Neurociencia | Referencia |
|----------------------|----------------------|------------|
| Write Pipeline | Codificacao orientada por esquema | Bartlett (1932) |
| Extracao Informada | Resposta de orientacao + codificacao contextual | -- |
| Perfis de Entidade | Esquemas mentais / representacoes consolidadas | -- |
| Entity Resolution | Memória associativa / Pattern completion | -- |
| Reconciliation | Reconsolidação | Nader, Schiller, & LeDoux (2000) |
| Graph Retrieval | Spreading activation | Collins & Loftus (1975) |
| Decay de Recência | Curva de esquecimento | Ebbinghaus (1885) |
| Background Jobs | Consolidação durante o sono | Diekelmann & Born (2010) |
| Importance Scoring | Homeostase sináptica | Tononi & Cirelli (SHY) |
| Summary Refresh | Formação de memória de essência | -- |
| Reranking | Atenção seletiva | Broadbent (1958) |
| Orçamento de Contexto | Capacidade da working memory | Baddeley & Hitch (1974); Cowan (2001) |
| Vitalidade/Reforço | Efeito de espaçamento | Ebbinghaus (1885) |

!!! note "Estes são analogias, não afirmações"
    Os paralelos acima são inspirações arquiteturais, não afirmações científicas. O `arandu` é um sistema de engenharia, não um modelo cognitivo. O cérebro é vastamente mais complexo - esses paralelos destacam as intuições de design, não os mecanismos biológicos.
# Background Jobs

Os background jobs melhoram a qualidade da memória ao longo do tempo. Eles rodam **separadamente** de `write()` e `retrieve()` - você agenda eles (a cada algumas horas, via cron, APScheduler ou um loop simples).

### Preciso deles?

**Pra começar: não.** Os pipelines de `write()` e `retrieve()` funcionam sem background jobs. Seu agente ainda vai extrair fatos, resolver entidades e retornar contexto relevante.

**Pra produção: sim.** Sem eles, scores de importância ficam flat (0.5 pra tudo), resumos de entidades nunca são gerados, padrões e contradições passam despercebidos, e a qualidade do retrieval degrada com o tempo conforme a memória cresce.

```mermaid
flowchart LR
    A["Scheduler\n(periodic)"] --> B["Clustering"]
    A --> C["Consolidation"]
    A --> D["Importance\nScoring"]
    A --> E["Summary\nRefresh"]
```

## Visão Geral

O `arandu` fornece quatro categorias de background jobs:

| Job | Proposito | Usa LLM? | Frequencia |
|-----|-----------|----------|------------|
| **Clustering** | Agrupar fatos relacionados semanticamente | Sim (resumos) | A cada 4-8 horas |
| **Consolidation** | Detectar padroes, contradicoes, tendencias | Sim | A cada 4-8 horas |
| **Memify** | Converter fatos episodicos em conhecimento procedural/semantico | Sim | Diario |
| **Sleep-time compute** | Pontuar importancia, atualizar resumos, consolidar perfis, detectar comunidades | Parcialmente | A cada 4-8 horas |

Todos os jobs são expostos como funções async que você pode chamar diretamente ou agendar com seu task runner preferido (APScheduler, Celery, cron, etc.).

!!! info "Paralelo com neurociência"
    Os background jobs espelham o **processamento durante o sono** no cérebro. Durante o sono, o cérebro consolida memórias, transfere informações do hipocampo (curto prazo) para o neocórtex (longo prazo), poda conexões irrelevantes e fortalece as importantes. Esses jobs realizam as mesmas operações na memória do seu agente.

---

## Clustering

**Em português claro:** Agrupa fatos relacionados. Fatos sobre o trabalho, colegas e projetos de alguém ficam num cluster. Isso torna o retrieval mais contextual - quando você pergunta sobre o trabalho de alguém, o sistema sabe quais fatos são relacionados.

### Fact Clustering

```python
from arandu import cluster_user_facts, ClusteringResult

result: ClusteringResult = await cluster_user_facts(
    session=db_session,
    agent_id="user_123",
    embedding_provider=embedding_provider,
    llm_provider=llm_provider,
    config=memory_config,
)
```

!!! note "Nomes legados de funções"
    Algumas funções de background jobs ainda mantêm "user" nos nomes (ex: `cluster_user_facts`, `get_entities_for_user`). Elas aceitam `agent_id` como parâmetro - os nomes são históricos e serão aliasados em uma versão futura.

**Como funciona:**

1. Agrupa fatos por `(entity_type, entity_key)` - fatos sobre a mesma entidade ficam juntos
2. Gera um resumo de 2-3 frases por cluster usando um LLM
3. Calcula e armazena embeddings do cluster para detecção de comunidades posterior
4. Idempotente - atualiza clusters existentes em vez de criar duplicatas

### Detecção de Comunidades

```python
from arandu import detect_communities, CommunityDetectionResult

result: CommunityDetectionResult = await detect_communities(
    session=db_session,
    agent_id="user_123",
    embedding_provider=embedding_provider,
    llm_provider=llm_provider,
    config=memory_config,
)
```

**Como funciona:**

1. Compara embeddings de clusters usando similaridade de cosseno
2. Agrupa clusters acima do `community_similarity_threshold` (padrão 0.75)
3. Cria registros `MemoryMetaObservation` com tipo `"community_theme"`
4. Exemplo: uma comunidade "trabalho" pode incluir clusters sobre colegas, projetos e fatos da empresa

### Configuração

| Parâmetro | Default | Descrição |
|-----------|---------|-----------|
| `cluster_max_age_days` | `90` | Idade máxima dos fatos a incluir no clustering |
| `community_similarity_threshold` | `0.75` | Threshold de similaridade de cosseno para agrupar clusters |

??? example "Passo a passo: antes e depois do clustering"
    **Antes do clustering** - 12 fatos sobre um usuário, sem agrupamento:
    ```
    person:fernanda → "Fernanda works at Orion Tech"
    person:fernanda → "Fernanda graduated from Unicamp"
    person:fernanda → "Fernanda built the data pipeline"
    person:bruno → "Bruno works at Orion Tech"
    person:bruno → "Bruno developed the ML model"
    person:bruno → "Bruno runs marathons"
    person:marcos → "Marcos lives in Porto Alegre"
    person:marcos → "Marcos is a product manager"
    ...
    ```

    **Depois do clustering:**
    ```
    Cluster 1: "Orion Tech engineering team"
      → Fernanda works at Orion Tech
      → Bruno works at Orion Tech
      → Fernanda built the data pipeline
      → Bruno developed the ML model
      Summary: "The Orion Tech data/ML team includes Fernanda (pipeline) and Bruno (ML model)"

    Cluster 2: "Marcos's personal life"
      → Marcos lives in Porto Alegre
      → Marcos is married to Carolina
      Summary: "Marcos lives in Porto Alegre with his wife Carolina"
    ```

    **Impacto no retrieval:** Quando alguém pergunta "Me fale sobre o time da Orion Tech", o resumo do cluster e seus fatos pontuam mais alto porque estão agrupados - o sistema entende que são relacionados.

---

## Consolidation

**Em português claro:** Olha pra todos os fatos e eventos recentes e encontra padrões maiores: "Essa pessoa menciona corrida toda segunda" (padrão), "Ela disse que mora em SP mas também em RJ" (contradição), "O humor dela tá melhorando" (tendência). Armazena como meta-observações que enriquecem o retrieval.

### Consolidação Periódica (L2)

```python
from arandu import run_consolidation, ConsolidationResult

result: ConsolidationResult = await run_consolidation(
    session=db_session,
    agent_id="user_123",
    llm_provider=llm_provider,
    config=memory_config,
)
```

**Como funciona:**

1. Analisa eventos e fatos em uma janela de lookback (`consolidation_lookback_days`)
2. Detecta padrões entre fatos:
   - **Insights** - Compreensão emergente de múltiplos fatos
   - **Padrões** - Comportamentos ou preferências repetidos
   - **Contradições** - Fatos conflitantes que precisam de resolução
   - **Tendências** - Mudanças ao longo do tempo
3. Gera registros `MemoryMetaObservation`
4. Marca eventos com emoções (emoção, intensidade, nível de energia)

### Consolidação de Perfil (L3)

```python
from arandu import run_profile_consolidation

await run_profile_consolidation(
    session=db_session,
    agent_id="user_123",
    llm_provider=llm_provider,
)
```

**Como funciona:**

1. Atualiza resumos de entidades via LLM - uma visão de nível mais alto de cada entidade
2. Atualiza a visão geral do perfil
3. Disparado periodicamente (menos frequente que L2)

### Configuração

| Parâmetro | Default | Descrição |
|-----------|---------|-----------|
| `consolidation_min_events` | `3` | Mínimo de eventos antes de rodar consolidation |
| `consolidation_lookback_days` | `7` | Quantos dias olhar para trás em busca de padrões |

!!! info "Paralelo com neurociência"
    A consolidation espelha a **consolidação de memória durante o sono** do cérebro. O hipocampo repete experiências recentes, o neocórtex detecta padrões e os integra em estruturas de conhecimento existentes, e contradições são sinalizadas para resolução. A consolidação L2 é análoga ao replay durante o sono de ondas lentas (SWS), enquanto a consolidação de perfil L3 é análoga ao papel do sono REM na integração de memórias em conhecimento semântico.

---

## Memify

**Em português claro:** Com o tempo, detalhes específicos ("foi num meetup de Python dia 5 de março") viram conhecimento geral ("frequenta meetups de tech regularmente"). O Memify destila fatos episódicos em conhecimento de nível mais alto e poda fatos que não são mencionados há tempo.

### Executar Memify

```python
from arandu import run_memify, MemifyResult

result: MemifyResult = await run_memify(
    session=db_session,
    agent_id="user_123",
    llm_provider=llm_provider,
    embedding_provider=embedding_provider,
    config=memory_config,
)
```

**Como funciona:**

1. Agrupa fatos relacionados por entidade e tópico
2. Gera resumos destilados (conhecimento procedural/semântico)
3. Verifica vitalidade - fatos mencionados recentemente são mantidos; fatos obsoletos podem ser deprecados
4. Mescla procedimentos similares para prevenir fragmentação de conhecimento

### Scoring de Vitalidade

```python
from arandu import compute_vitality

# Síncrono, por fato — NÃO é async
score = compute_vitality(fact)          # usa datetime.now(UTC)
score = compute_vitality(fact, now=ts)  # timestamp customizado
```

A vitalidade mede quão "vivo" um fato está com base em:

- **Retrieval** (0.30) - Escala logarítmica de times_retrieved
- **Recência** (0.25) - Decay exponencial a partir de last_retrieved_at (half-life de 30 dias)
- **Confiança** (0.20) - Valor bruto de confidence
- **Reforço** (0.15) - Contagem limitada de reinforcement (até 5)

Penalidade por correção: `0.8 ^ user_correction_count`. Fatos supersedidos (valid_to is not None) retornam 0.0.

!!! info "Paralelo com neurociência"
    O memify espelha a **curva de esquecimento** descrita por Hermann Ebbinghaus (1885). Memórias decaem exponencialmente ao longo do tempo a menos que sejam reforçadas através de prática de recuperação. Fatos com alta vitalidade (acessados frequentemente) resistem ao decay, enquanto fatos de baixa vitalidade vão se apagando gradualmente - assim como o cérebro poda conexões sinápticas para informações não utilizadas.

---

## Sleep-Time Compute

**Em portugues claro:** Quatro jobs de manutencao que mantem o retrieval afiado: (1) pontuar quais entidades sao mais importantes, (2) atualizar resumos das entidades importantes, (3) consolidar perfis de entidade a partir de todos os fatos, (4) detectar comunidades de entidades relacionadas. O primeiro e SQL puro (barato), os outros tres usam LLM.

### Job 1: Entity Importance Scoring

```python
from arandu import compute_entity_importance, EntityImportanceResult

result: EntityImportanceResult = await compute_entity_importance(
    session=db_session,
    agent_id="user_123",
    config=memory_config,
)
```

Computação pura em SQL (sem chamadas LLM). Pontua cada entidade de 0.0 a 1.0 usando quatro sinais normalizados:

| Sinal | Peso | Descrição |
|-------|------|-----------|
| Densidade de fatos | 0.30 | Número de fatos linkados à entidade (via `MemoryFactEntityLink`). Inclui fatos onde a entidade é subject primário E fatos que apenas a mencionam. |
| Recência | 0.25 | Decay exponencial (half-life de 30 dias) |
| Frequência de retrieval | 0.25 | Quão frequentemente fatos sobre essa entidade são recuperados |
| Grau de relacionamento | 0.20 | Número de relacionamentos entrantes + saintes |

O importance score é usado como sinal no scoring de retrieval e como fator de prioridade para atualização de resumos.

??? example "Passo a passo: como o importance scoring muda o retrieval"
    **Antes do importance scoring** - todas as entidades com importância padrão = 0.5:
    ```
    person:fernanda  → importance: 0.50 (padrão)
    person:marcos    → importance: 0.50 (padrão)
    organization:vertix → importance: 0.50 (padrão)
    product:xgboost  → importance: 0.50 (padrão)
    ```

    **Depois do importance scoring:**
    ```
    person:fernanda  → importance: 0.85 (12 fatos, recente, alta frequência de retrieval)
    organization:vertix → importance: 0.78 (8 fatos, muitos relacionamentos)
    person:marcos    → importance: 0.55 (4 fatos, atividade moderada)
    product:xgboost  → importance: 0.25 (1 fato, mencionado uma vez, sem relacionamentos)
    ```

    **Impacto no retrieval:** Quando dois fatos têm scores semânticos similares, o sobre Fernanda (importance=0.85) ranqueia acima do sobre XGBoost (importance=0.25). Isso reflete a realidade de que Fernanda é uma entidade central na memória deste usuário enquanto XGBoost é um detalhe periférico.

    **Impacto no summary refresh:** Fernanda e Vertix recebem atualização de resumo primeiro (maior prioridade). XGBoost pode nunca receber um resumo - a prioridade é muito baixa.

### Job 2: Entity Summary Refresh

```python
from arandu import refresh_entity_summaries, SummaryRefreshResult

result: SummaryRefreshResult = await refresh_entity_summaries(
    session=db_session,
    agent_id="user_123",
    llm_provider=llm_provider,
    config=memory_config,
)
```

Atualiza resumos obsoletos de entidades:

- **Condição de obsolescência**: `summary_text IS NULL` ou última atualização > 7 dias atrás
- **Prioridade**: entidades com `importance_score` mais alto são atualizadas primeiro
- **Limite**: 10 entidades por execução (previne timeout)
- Gera resumos de 2-3 frases a partir dos fatos da entidade usando um LLM

### Job 3: Consolidacao de Perfis de Entidade

```python
from arandu import consolidate_entity_profiles, ProfileConsolidationResult

result: ProfileConsolidationResult = await consolidate_entity_profiles(
    session=db_session,
    agent_id="user_123",
    llm_provider=llm_provider,
    config=memory_config,
)
```

Reconstroi perfis de entidade (`profile_text`) a partir de **todos** os fatos ativos de cada entidade. Diferente do write pipeline (que apenas semeia perfis curtos para entidades novas sem perfil), este job gera perfis abrangentes que cobrem todos os aspectos relevantes da entidade.

**Como funciona:**

1. Seleciona entidades com mais fatos primeiro (priorizacao por densidade)
2. Filtra entidades cujo perfil e `NULL` ou esta obsoleto (mais antigo que `summary_refresh_interval_days`)
3. Carrega ate 50 fatos ativos por entidade
4. Gera perfil abrangente via LLM (timeout de 15s por entidade)
5. Processa ate 20 entidades por execucao

**Relacao com o write pipeline:** O write pipeline so semeia perfis para entidades que ainda nao possuem um -- nunca sobrescreve perfis existentes. Este background job e a **fonte autoritativa** para perfis. Na pratica, o fluxo e:

1. Primeira mensagem sobre "Ana" → write pipeline cria perfil inicial simples
2. Mensagens subsequentes sobre "Ana" → write pipeline nao toca o perfil
3. Background job roda → le todos os fatos sobre "Ana" e gera perfil abrangente, sobrescrevendo o perfil inicial

### Job 4: Entity Community Detection

```python
from arandu import detect_entity_communities

result = await detect_entity_communities(
    session=db_session,
    agent_id="user_123",
    config=memory_config,
)
```

Clustering por componentes conectados no grafo de relacionamentos de entidades (sem chamadas LLM):

1. Carrega entidades ativas e arestas (strength >= 0.3)
2. Executa Union-Find (com path compression + union by rank) para encontrar componentes conectados
3. Agrupa entidades em comunidades (componentes com >= 2 membros)
4. Retorna `{"communities_found": int, "total_entities_assigned": int}`

!!! info "Paralelo com neurociência"
    O sleep-time compute espelha o **processamento offline durante o sono**. O cérebro não apenas armazena memórias passivamente durante o sono - ele as reorganiza ativamente. O importance scoring é análogo ao processo de **homeostase sináptica** (Tononi & Cirelli), onde sinapses fortemente ativadas são mantidas enquanto fracamente ativadas são podadas. O summary refresh espelha a formação de **memórias de essência** - representações comprimidas que capturam a essência de episódios detalhados.

---

## Agendamento

O `arandu` não inclui um scheduler - você traz o seu. Todas as funções de background são simples callables async que podem ser integradas com qualquer sistema de agendamento.

### Exemplo: APScheduler

```python
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from arandu import (
    cluster_user_facts,
    run_consolidation,
    compute_entity_importance,
    consolidate_entity_profiles,
    refresh_entity_summaries,
)

scheduler = AsyncIOScheduler()

async def maintenance_cycle():
    async with get_session() as session:
        for agent_id in await get_active_users(session):
            await compute_entity_importance(session, agent_id, config=config)
            await refresh_entity_summaries(session, agent_id, llm_provider=llm, config=config)
            await consolidate_entity_profiles(session, agent_id, llm_provider=llm, config=config)
            await cluster_user_facts(session, agent_id, embeddings, llm, config)
            await run_consolidation(session, agent_id, llm_provider=llm, config=config)

scheduler.add_job(maintenance_cycle, "interval", hours=4)
scheduler.start()
```

### Exemplo: Loop Simples

```python
import asyncio

async def background_loop():
    while True:
        await maintenance_cycle()
        await asyncio.sleep(4 * 3600)  # a cada 4 horas
```

### Cadência Recomendada

| Job | Frequencia | Custo |
|-----|-----------|-------|
| Entity importance | A cada 4h | Barato (apenas SQL) |
| Summary refresh | A cada 4h | Moderado (LLM, limitado a 10/execucao) |
| Profile consolidation | A cada 4h | Moderado (LLM, limitado a 20/execucao) |
| Clustering | A cada 4-8h | Moderado (LLM para resumos) |
| Consolidation | A cada 4-8h | Moderado (LLM para deteccao de padroes) |
| Memify | Diario | Moderado (LLM para destilacao) |
| Community detection | Diario | Moderado (LLM + embeddings) |

Execute importance scoring primeiro - sua saída é usada pelo summary refresh para priorizar entidades.
# Configuração

Todos os parâmetros do sistema de memória são configurados através de uma única dataclass `MemoryConfig`. Todo parâmetro tem um default sensato - sobrescreva apenas o que importa para o seu caso de uso.

```python
from arandu import MemoryClient, MemoryConfig

config = MemoryConfig(
    extraction_timeout_sec=15.0,
    topk_facts=30,
    enable_reranker=True,
)

memory = MemoryClient(
    database_url="postgresql+psycopg://...",
    llm=provider,
    embeddings=provider,
    config=config,
)
```

---

## Extraction

Parâmetros que controlam como fatos, entidades e relacionamentos são extraídos de mensagens.

| Parâmetro | Tipo | Default | Descrição |
|-----------|------|---------|-----------|
| `extraction_timeout_sec` | `float` | `30.0` | Timeout por chamada LLM durante extração. No timeout, a extração retorna resultado vazio (fail-safe) - sem exceção |
| `enable_informed_extraction` | `bool` | `True` | Habilita extração informada por memória (v0.9.0+). Quando habilitado, o pipeline usa uma única chamada LLM informada pelo conhecimento existente em vez de 4 chamadas separadas. Cai pro fallback (extração cega) em caso de falha. `False` para usar a extração multi-pass legada |
| `informed_extraction_topk` | `int` | `10` | Máximo de fatos existentes a recuperar por entidade como contexto da extração informada. Só usado quando a entidade não tem profile_text |
| `informed_extraction_context_budget_tokens` | `int` | `800` | Budget de tokens para o contexto de conhecimento existente injetado no prompt de extração informada. Distribuído entre as entidades encontradas |

**Dicas:**

- O modelo de extração é determinado pelo `LLMProvider` injetado no `MemoryClient`. Para usar um modelo mais barato na extração, injete um provider configurado com esse modelo
- Reduza `extraction_timeout_sec` se precisa de respostas mais rápidas ao custo de extrações potencialmente perdidas
- Use `enable_informed_extraction=False` se seu provider LLM tiver dificuldade com o prompt unificado (ex: modelos menores). O sistema cai automaticamente pra extração legada de 4 chamadas + reconciliação

---

## Entity Resolution

Parâmetros que controlam como menções de entidades extraídas são resolvidas para registros canônicos.

| Parâmetro | Tipo | Default | Descrição |
|-----------|------|---------|-----------|
| `fuzzy_threshold` | `float` | `0.85` | Threshold de similaridade de cosseno para fuzzy match direto. Score ≥ esse valor resolve diretamente; score entre 0.50 e esse valor encaminha para LLM; score < 0.50 cria nova entidade. Reduzir esse valor expande a faixa de fuzzy-resolve e reduz chamadas LLM |
| `enable_llm_resolution` | `bool` | `True` | Se deve usar um LLM para fuzzy matches ambíguos (faixa 0.50 - `fuzzy_threshold`). Quando `False`, candidatos ambíguos criam nova entidade |

**Dicas:**

- Reduza `fuzzy_threshold` (ex: 0.75) para ser mais agressivo no matching - isso encolhe a faixa "ambígua" que requer chamadas LLM
- Defina `enable_llm_resolution=False` para pular o fallback LLM em matches ambíguos (mais rápido, mas pode criar mais entidades duplicadas)
- O modelo LLM para entity resolution e reconciliação é determinado pelo `LLMProvider` injetado no `MemoryClient`

---

## Retrieval

Parâmetros que controlam como fatos são recuperados em resposta a queries.

| Parâmetro | Tipo | Default | Descrição |
|-----------|------|---------|-----------|
| `topk_facts` | `int` | `20` | Número máximo de fatos a retornar |
| `topk_events` | `int` | `8` | Número máximo de eventos a considerar para contexto |
| `event_max_scan` | `int` | `200` | Máximo de eventos a escanear durante retrieval |
| `min_similarity` | `float` | `0.20` | Similaridade de cosseno mínima para resultados de busca semântica |
| `min_confidence` | `float` | `0.55` | Confiança mínima do fato para incluir nos resultados de retrieval |

!!! warning "`min_confidence` é um **filtro apenas de read-time**"
    Todos os fatos são persistidos durante o write independente do score de confidence. A filtragem acontece durante `memory.retrieve()`. Isso é por design: a confidence pode ser ajustada ao longo do tempo via reforço (confirmações NOOP), e descartar fatos no write-time seria irreversível.
| `recency_half_life_days` | `int` | `14` | Half-life (em dias) para decay exponencial de recência |
| `enable_reranker` | `bool` | `True` | Se deve usar reranking LLM nos resultados de retrieval |
| `reranker_timeout_sec` | `float` | `5.0` | Timeout para chamadas LLM do reranker |

**Dicas:**

- Aumente `topk_facts` (ex: 50) para contexto mais amplo ao custo de mais ruído
- Reduza `min_similarity` (ex: 0.10) para capturar matches semânticos mais distantes
- Aumente `recency_half_life_days` (ex: 30) se fatos mais antigos devem permanecer relevantes por mais tempo
- Defina `enable_reranker=False` para retrieval mais rápido quando precisão é menos crítica

---

## Score Weights

Pesos para a fórmula de ranking híbrido que combina múltiplos sinais de retrieval.

| Parâmetro | Tipo | Default | Descrição |
|-----------|------|---------|-----------|
| `score_weights` | `dict` | `{"semantic": 0.70, "recency": 0.20, "importance": 0.10}` | Pesos para cada sinal de scoring (devem somar ~1.0) |

```python
config = MemoryConfig(
    score_weights={
        "semantic": 0.60,   # reduzir semântico, aumentar outros sinais
        "recency": 0.25,
        "importance": 0.15,
    },
)
```

**Dicas:**

- Aumente o peso de `"recency"` para aplicações onde frescor importa mais que relevância semântica
- Aumente o peso de `"importance"` para favorecer entidades bem estabelecidas e fatos frequentemente mencionados

---

## Confidence

Parâmetros que controlam os níveis de confiança atribuídos a fatos extraídos.

| Parâmetro | Tipo | Default | Descrição |
|-----------|------|---------|-----------|
| `confidence_level_map` | `dict` | `{"explicit_statement": 0.95, "strong_inference": 0.80, "weak_inference": 0.60, "speculation": 0.40}` | Mapeamento de nomes de nível de confiança para scores numéricos |
| `confidence_default` | `float` | `0.60` | Confiança padrão quando o LLM não especifica um nível |

---

## Spreading Activation

Parâmetros que controlam como o contexto se expande a partir de fatos semente ao longo de relacionamentos de entidades.

| Parâmetro | Tipo | Default | Descrição |
|-----------|------|---------|-----------|
| `spreading_activation_hops` | `int` | `2` | Número máximo de hops de relacionamento a partir de fatos semente |
| `spreading_decay_factor` | `float` | `0.50` | Multiplicador de decay de score por hop (0.5 = dividido pela metade a cada hop) |
| `spreading_max_related_entities` | `int` | `5` | Máximo de entidades relacionadas a seguir por semente |
| `spreading_facts_per_entity` | `int` | `3` | Máximo de fatos a puxar de cada entidade relacionada |

---

## Compressão de Contexto

Parâmetros que controlam como fatos recuperados são comprimidos na string de contexto final.

| Parâmetro | Tipo | Default | Descrição |
|-----------|------|---------|-----------|
| `context_max_tokens` | `int` | `2000` | Máximo de tokens no output de contexto formatado |
| `hot_tier_ratio` | `float` | `0.50` | Parcela do orçamento de tokens para fatos com scores mais altos |
| `warm_tier_ratio` | `float` | `0.30` | Parcela do orçamento de tokens para fatos de suporte |

O orçamento restante (1 - hot - warm = 0.20) vai para o cold tier (contexto de background).

---

## Tendências Emocionais

Parâmetros para detectar padrões emocionais em mensagens do usuário.

| Parâmetro | Tipo | Default | Descrição |
|-----------|------|---------|-----------|
| `emotional_trend_window_days` | `int` | `30` | Janela para análise de tendências emocionais |
| `emotional_trend_min_events` | `int` | `5` | Mínimo de eventos necessários para detectar uma tendência |

---

## Clustering

Parâmetros para o background job de fact clustering.

| Parâmetro | Tipo | Default | Descrição |
|-----------|------|---------|-----------|
| `cluster_max_age_days` | `int` | `90` | Idade máxima dos fatos a incluir no clustering |
| `cluster_min_facts` | `int` | `2` | Mínimo de fatos por cluster |
| `community_similarity_threshold` | `float` | `0.75` | Threshold de similaridade de cosseno para agrupar clusters em comunidades |
| `community_min_clusters` | `int` | `2` | Mínimo de clusters para formar uma comunidade |

---

## Consolidation

Parâmetros para o background job de consolidation.

| Parâmetro | Tipo | Default | Descrição |
|-----------|------|---------|-----------|
| `consolidation_min_events` | `int` | `3` | Mínimo de eventos antes de rodar consolidation |
| `consolidation_lookback_days` | `int` | `7` | Quantos dias olhar para trás (em dias) em busca de padrões |

---

## Sleep-Time Compute

Parâmetros para importance scoring e summary refresh em background.

| Parâmetro | Tipo | Default | Descrição |
|-----------|------|---------|-----------|
| `importance_recency_halflife_days` | `int` | `30` | Half-life para sinal de recência no importance scoring |
| `summary_refresh_interval_days` | `int` | `7` | Dias antes de um resumo de entidade ser considerado obsoleto |

---

## Memify

Parâmetros para o background job de memify (conhecimento episódico → procedural).

| Parâmetro | Tipo | Default | Descrição |
|-----------|------|---------|-----------|
| `vitality_stale_threshold` | `float` | `0.2` | Score de vitalidade abaixo do qual um fato é considerado obsoleto |
| `memify_merge_similarity_threshold` | `float` | `0.90` | Threshold de similaridade para mesclar procedimentos similares |

---

## Memória Procedural

Parâmetros para retrieval de memória diretiva/procedural.

| Parâmetro | Tipo | Default | Descrição |
|-----------|------|---------|-----------|
| `directive_max_tokens` | `int` | `300` | Máximo de tokens para diretivas procedurais |
| `directive_cache_ttl_minutes` | `int` | `30` | Cache TTL para lookups de diretivas |
| `contradiction_similarity_threshold` | `float` | `0.80` | Threshold para detectar diretivas contraditórias |

---

## Locale / Deploy

| Parametro | Tipo | Default | Descricao |
|-----------|------|---------|-----------|
| `timezone` | `str` | `"UTC"` | Timezone IANA para interpretar referencias temporais relativas |

O parametro `timezone` afeta como referencias temporais relativas ("ontem", "semana passada", "hoje de manha") sao interpretadas durante a extracao de fatos e retrieval. Todos os timestamps no banco sao armazenados em **UTC** independente desta configuracao.

Por exemplo: se `timezone="Asia/Tokyo"` e o usuario diz "ontem", o SDK interpreta "ontem" relativo ao horario de Tokyo (JST), nao UTC.

---

## Parametros de `write()`

Alem do `MemoryConfig`, o metodo `write()` aceita parametros por chamada:

| Parametro | Tipo | Default | Descricao |
|-----------|------|---------|-----------|
| `occurred_at` | `datetime \| None` | `None` (usa `now`) | Timestamp de quando a mensagem foi enviada. Usado para imports historicos -- o LLM resolve referencias temporais relativas ("yesterday", "last week") baseado nessa data, nao na data atual |
| `dry_run` | `bool` | `False` | Roda extracao sem persistir nada no banco |
| `recent_messages` | `list[str] \| None` | `None` | Contexto de conversa recente para resolver pronomes |
| `verbose` | `bool` | `False` | Inclui pipeline trace com dados intermediarios |
| `config_overrides` | `dict \| None` | `None` | Sobrescreve campos do `MemoryConfig` para esta chamada |

---

## Catálogo Aberto (Extensões do Deployer)

Parâmetros para estender o catálogo de atributos built-in com entradas customizadas.

| Parâmetro | Tipo | Default | Descrição |
|-----------|------|---------|-----------|
| `extra_attribute_keys` | `set[str]` | `set()` | Attribute keys adicionais reconhecidas durante extração |
| `attribute_aliases` | `dict[str, str]` | `{}` | Aliases para attribute keys (ex: `{"hometown": "city"}`) |
| `extra_namespaces` | `set[str]` | `set()` | Namespaces de entidade adicionais além dos tipos built-in |
| `extra_self_references` | `frozenset[str]` | `frozenset()` | Palavras adicionais tratadas como auto-referências (ex: `{"yo"}` para espanhol) |
| `extra_relationship_hints` | `frozenset[str]` | `frozenset()` | Palavras adicionais de dica de relacionamento para entity resolution |

---

## Limites

| Parâmetro | Tipo | Default | Descrição |
|-----------|------|---------|-----------|
| `max_facts_per_event` | `int` | `100` | Máximo de fatos extraídos de uma única mensagem |
| `embedding_dimensions` | `int` | `1536` | Dimensionalidade dos vetores de embedding (deve corresponder ao seu provider) |
# Custom Providers

O `arandu` usa protocolos Python para injeção de dependência. Você pode usar qualquer backend de LLM ou embedding implementando duas interfaces simples - sem herança necessária.

## Providers Incluídos

O SDK inclui dois providers built-in:

| Provider | Instalação | LLM | Embeddings |
|----------|-----------|-----|-----------|
| **OpenAI** | `pip install arandu[openai]` | ✅ GPT-4o, GPT-4o-mini, etc. | ✅ text-embedding-3-small, etc. |
| **Anthropic** | `pip install arandu[anthropic]` | ✅ Claude Sonnet, Opus, Haiku | ❌ Use OpenAI pra embeddings |

```python
# OpenAI (LLM + embeddings num provider só)
from arandu.providers.openai import OpenAIProvider
provider = OpenAIProvider(api_key="sk-...")
memory = MemoryClient(database_url="...", llm=provider, embeddings=provider)

# Anthropic (Claude pra LLM, OpenAI pra embeddings)
from arandu.providers.anthropic import AnthropicProvider
from arandu.providers.openai import OpenAIProvider
llm = AnthropicProvider(api_key="sk-ant-...")
embeddings = OpenAIProvider(api_key="sk-...")
memory = MemoryClient(database_url="...", llm=llm, embeddings=embeddings)
```

### Providers Compatíveis com OpenAI

O `OpenAIProvider` funciona com qualquer API que siga o formato de chat completions da OpenAI. Basta definir `base_url` para apontar ao endpoint do provider:

```python
from arandu.providers.openai import OpenAIProvider

# DeepSeek
llm = OpenAIProvider(api_key="sk-deepseek-...", model="deepseek-chat", base_url="https://api.deepseek.com/v1")

# Groq
llm = OpenAIProvider(api_key="gsk_...", model="llama-3.3-70b-versatile", base_url="https://api.groq.com/openai/v1")

# Together AI
llm = OpenAIProvider(api_key="tog_...", model="meta-llama/Llama-3.3-70B-Instruct-Turbo", base_url="https://api.together.xyz/v1")

# Fireworks AI
llm = OpenAIProvider(api_key="fw_...", model="accounts/fireworks/models/llama-v3p3-70b-instruct", base_url="https://api.fireworks.ai/inference/v1")

# Ollama (local)
llm = OpenAIProvider(api_key="ollama", model="llama3.1", base_url="http://localhost:11434/v1")
```

Isso cobre apenas chamadas de LLM. Embeddings ainda precisam de OpenAI ou de um `EmbeddingProvider` customizado, já que a maioria desses providers não oferece API de embeddings.

Se os providers incluídos atendem seu caso de uso, não precisa ler o resto desta página.

---

## Os Protocolos

Se você precisa de um provider diferente (Ollama, LiteLLM, Groq, etc.), implemente os protocolos:

### LLMProvider

```python
from arandu.protocols import LLMResult, TokenUsage

class LLMProvider(Protocol):
    async def complete(
        self,
        messages: list[dict],
        temperature: float = 0,
        response_format: dict | None = None,
        max_tokens: int | None = None,
    ) -> LLMResult: ...
```

| Parâmetro | Descrição |
|-----------|-----------|
| `messages` | Lista de dicts com chaves `"role"` e `"content"` (formato OpenAI) |
| `temperature` | Temperatura de sampling (0 = determinístico) |
| `response_format` | Especificação de formato opcional (ex: `{"type": "json_object"}`) |
| `max_tokens` | Máximo opcional de tokens para a resposta |
| **Retorna** | `LLMResult(text="...", usage=TokenUsage(...))` |

!!! important "Suporte a modo JSON"
    O pipeline depende de respostas JSON (`response_format={"type": "json_object"}`).
    Se seu backend não suporta nativamente, appende uma instrução no system prompt.

### EmbeddingProvider

```python
class EmbeddingProvider(Protocol):
    async def embed(self, texts: list[str]) -> list[list[float]]: ...
    async def embed_one(self, text: str) -> list[float] | None: ...
```

| Método | Descrição |
|--------|-----------|
| `embed(texts)` | Gera embeddings para um batch de textos. Retorna um vetor por input. |
| `embed_one(text)` | Gera embedding para um único texto. Retorna `None` se vazio/inválido. |

!!! note "Dimensões de embedding"
    O `embedding_dimensions` padrão é 1536 (OpenAI `text-embedding-3-small`).
    Se seu provider usa dimensões diferentes, defina `MemoryConfig(embedding_dimensions=...)`.

---

## Exemplo: Provider de Modelo Local

Para rodar com modelos locais (ex: via Ollama):

```python
import httpx
from arandu.protocols import LLMResult, TokenUsage


class OllamaProvider:
    """LLM + Embedding provider usando um servidor Ollama local."""

    def __init__(
        self,
        base_url: str = "http://localhost:11434",
        model: str = "llama3.1",
        embedding_model: str = "nomic-embed-text",
    ) -> None:
        self._base_url = base_url
        self._model = model
        self._embedding_model = embedding_model
        self._client = httpx.AsyncClient(timeout=60.0)

    # -- LLMProvider --

    async def complete(
        self,
        messages: list[dict],
        temperature: float = 0,
        response_format: dict | None = None,
        max_tokens: int | None = None,
    ) -> LLMResult:
        payload: dict = {
            "model": self._model,
            "messages": messages,
            "stream": False,
            "options": {"temperature": temperature},
        }
        if response_format and response_format.get("type") == "json_object":
            payload["format"] = "json"

        response = await self._client.post(
            f"{self._base_url}/api/chat",
            json=payload,
        )
        response.raise_for_status()
        text = response.json()["message"]["content"]
        return LLMResult(text=text, usage=None)  # Ollama doesn't report usage

    # -- EmbeddingProvider --

    async def embed(self, texts: list[str]) -> list[list[float]]:
        results = []
        for text in texts:
            if not text.strip():
                continue
            response = await self._client.post(
                f"{self._base_url}/api/embed",
                json={"model": self._embedding_model, "input": text},
            )
            response.raise_for_status()
            results.append(response.json()["embeddings"][0])
        return results

    async def embed_one(self, text: str) -> list[float] | None:
        if not text or not text.strip():
            return None
        results = await self.embed([text])
        return results[0] if results else None
```

!!! warning "Dimensões de embedding"
    Quando usar modelos locais, configure as dimensões:

    ```python
    config = MemoryConfig(
        embedding_dimensions=768,  # nomic-embed-text usa 768 dims
    )
    ```

---

## Testando Seu Provider

Verifique se seu provider funciona antes de ir pra produção:

```python
import asyncio
from arandu import MemoryClient, MemoryConfig


async def test_provider():
    provider = YourProvider(...)
    memory = MemoryClient(
        database_url="postgresql+psycopg://memory:memory@localhost/memory",
        llm=provider,
        embeddings=provider,
    )
    await memory.initialize()

    try:
        # Testar write
        result = await memory.write(
            agent_id="test",
            message="Testing the provider. My name is Alice and I work at Acme.",
        )
        assert len(result.facts_added) > 0, "No facts extracted — check LLM responses"
        assert len(result.entities_resolved) > 0, "No entities resolved"
        print(f"Write OK: {len(result.facts_added)} facts, {len(result.entities_resolved)} entities")

        # Testar retrieve
        context = await memory.retrieve(agent_id="test", query="who is Alice?")
        assert len(context.facts) > 0, "No facts retrieved — check embeddings"
        print(f"Retrieve OK: {len(context.facts)} facts found")
        print(f"Context: {context.context}")
    finally:
        await memory.close()


asyncio.run(test_provider())
```

## Requisitos Importantes

1. **`LLMResult`** - `complete()` retorna `LLMResult(text=..., usage=...)`, não `str`. Se seu backend não reporta usage, passe `usage=None`.

2. **Modo JSON** - O pipeline envia `response_format={"type": "json_object"}` frequentemente. Seu provider deve retornar JSON válido quando isso é definido.

3. **Async** - Ambos os protocolos são async. Se o SDK do seu backend é síncrono, encapsule com `asyncio.to_thread()`.

4. **Tratamento de vazio/erro** - `embed_one` retorna `None` para input vazio. `embed` retorna `[]` para input vazio.

5. **Timeout** - Adicione timeouts ao seu provider. O SDK define timeouts do lado dele, mas timeouts no provider adicionam segurança.

6. **Dimensões de embedding** - Defina `MemoryConfig(embedding_dimensions=N)` pra corresponder ao seu provider.
# Cookbook

Exemplos completos, prontos para copiar e colar, para casos de uso comuns.

---

## Uso Básico

A integração mais simples: escrever fatos de mensagens do usuário e recuperar contexto para respostas.

```python
import asyncio
from arandu import MemoryClient
from arandu.providers.openai import OpenAIProvider


async def main():
    provider = OpenAIProvider(api_key="sk-...")
    memory = MemoryClient(
        database_url="postgresql+psycopg://memory:memory@localhost:5432/memory",
        llm=provider,
        embeddings=provider,
    )
    await memory.initialize()

    try:
        # Simular uma conversa
        messages = [
            "Hi, I'm Rafael. I'm a backend engineer at Acme Corp in São Paulo.",
            "My girlfriend Ana is a UX designer. We have a cat named Pixel.",
            "I've been learning Rust lately, mostly on weekends.",
            "Actually, I just moved to Rio de Janeiro. Still remote at Acme.",
        ]

        for msg in messages:
            result = await memory.write(agent_id="rafael", message=msg, speaker_name="Rafael")
            added = len(result.facts_added)
            updated = len(result.facts_updated)
            print(f"Write: +{added} facts, ~{updated} updates ({result.duration_ms:.0f}ms)")

        # Recuperar contexto para diferentes queries
        queries = [
            "where does Rafael live?",
            "tell me about Rafael's relationships",
            "what are Rafael's hobbies?",
        ]

        for query in queries:
            result = await memory.retrieve(agent_id="rafael", query=query)
            print(f"\nQuery: {query}")
            print(f"Found {len(result.facts)} facts ({result.duration_ms:.0f}ms)")
            for fact in result.facts[:5]:
                print(f"  [{fact.score:.2f}] {fact.entity_name}: {fact.fact_text}")
    finally:
        await memory.close()


asyncio.run(main())
```

---

## Usando Anthropic (Claude)

Use Claude como seu LLM mantendo OpenAI para embeddings (Anthropic não oferece API de embeddings):

```python
import asyncio
from arandu import MemoryClient, MemoryConfig
from arandu.providers.anthropic import AnthropicProvider
from arandu.providers.openai import OpenAIProvider


async def main():
    # Claude para raciocínio, OpenAI para embeddings
    llm = AnthropicProvider(api_key="sk-ant-...", model="claude-sonnet-4-20250514")
    embeddings = OpenAIProvider(api_key="sk-...")

    memory = MemoryClient(
        database_url="postgresql+psycopg://memory:memory@localhost/memory",
        llm=llm,
        embeddings=embeddings,
    )
    await memory.initialize()

    try:
        result = await memory.write(
            agent_id="demo",
            message="I love hiking in the mountains. Last weekend I went to Serra da Mantiqueira.",
            speaker_name="Rafael",
        )
        print(f"Extracted {len(result.facts_added)} facts using Claude")

        context = await memory.retrieve(agent_id="demo", query="outdoor activities")
        print(context.context)
    finally:
        await memory.close()


asyncio.run(main())
```

---

## Usando Outros Providers de LLM

O `OpenAIProvider` funciona com qualquer provider que exponha uma API compatível com OpenAI. Basta mudar `api_key`, `model` e `base_url`:

```python
from arandu import MemoryClient
from arandu.providers.openai import OpenAIProvider

# DeepSeek V3 (barato, alta qualidade)
llm = OpenAIProvider(
    api_key="sk-deepseek-...",
    model="deepseek-chat",
    base_url="https://api.deepseek.com/v1",
)

# Groq (inferência rápida)
llm = OpenAIProvider(
    api_key="gsk_...",
    model="llama-3.3-70b-versatile",
    base_url="https://api.groq.com/openai/v1",
)

# Together AI
llm = OpenAIProvider(
    api_key="tog_...",
    model="meta-llama/Llama-3.3-70B-Instruct-Turbo",
    base_url="https://api.together.xyz/v1",
)

# Fireworks AI
llm = OpenAIProvider(
    api_key="fw_...",
    model="accounts/fireworks/models/llama-v3p3-70b-instruct",
    base_url="https://api.fireworks.ai/inference/v1",
)

# Modelo local via Ollama
llm = OpenAIProvider(
    api_key="ollama",  # qualquer string não-vazia
    model="llama3.1",
    base_url="http://localhost:11434/v1",
)

# Usar com Arandu (igual ao OpenAI)
memory = MemoryClient(
    database_url="postgresql+psycopg://...",
    llm=llm,
    embeddings=OpenAIProvider(api_key="sk-..."),  # embeddings ainda precisam de OpenAI
)
```

!!! note "Embeddings ainda precisam de OpenAI (ou um provider customizado)"
    A maioria dos providers compatíveis com OpenAI não oferece API de embeddings. Use `OpenAIProvider` com sua chave real da OpenAI para embeddings, ou implemente um `EmbeddingProvider` customizado (veja [Custom Providers](custom-providers.md)).

---

## Configuração Avançada (Tuning de Retrieval)

Ajuste fino do retrieval para diferentes casos de uso:

```python
import asyncio
from arandu import MemoryClient, MemoryConfig
from arandu.providers.openai import OpenAIProvider


async def main():
    # Provider único para todas as operações LLM (extração, reranker, etc.)
    llm = OpenAIProvider(api_key="sk-...", model="gpt-4o")

    # Configuração para um chatbot que precisa de contexto amplo e recente
    config = MemoryConfig(
        # Extraction: timeout curto para chat em tempo real
        extraction_timeout_sec=15.0,

        # Retrieval: mais resultados, favorecer recência
        topk_facts=40,
        min_similarity=0.15,          # rede mais ampla
        recency_half_life_days=7,     # favorecer fatos recentes mais agressivamente

        # Pesos de score: boost de recência para conversa dinâmica
        score_weights={
            "semantic": 0.50,
            "recency": 0.35,
            "importance": 0.15,
        },

        # Reranker
        enable_reranker=True,

        # Contexto: orçamento maior para respostas ricas
        context_max_tokens=3000,

        # Spreading activation: expansão de contexto mais ampla
        spreading_activation_hops=3,
        spreading_max_related_entities=8,

        # Timezone para cálculos de recência
        timezone="America/Sao_Paulo",
    )

    memory = MemoryClient(
        database_url="postgresql+psycopg://memory:memory@localhost/memory",
        llm=llm,
        embeddings=llm,
        config=config,
    )
    await memory.initialize()

    try:
        # Escrever uma série de mensagens
        await memory.write(agent_id="demo", message="I started a new job at TechCorp today!", speaker_name="Rafael")
        await memory.write(agent_id="demo", message="My manager's name is Sarah. She seems great.", speaker_name="Rafael")
        await memory.write(agent_id="demo", message="The office is in downtown with a nice view.", speaker_name="Rafael")

        # Recuperar com configurações ajustadas
        result = await memory.retrieve(agent_id="demo", query="what's new with the user?")
        print(f"Retrieved {len(result.facts)} facts")
        print(f"Context ({len(result.context)} chars):")
        print(result.context)

        # Verificar scores individuais para validar o tuning
        for fact in result.facts:
            print(f"\n  [{fact.score:.3f}] {fact.fact_text}")
            print(f"    Scores: {fact.scores}")
    finally:
        await memory.close()


asyncio.run(main())
```

---

## Integração de Background Jobs

Configure manutenção periódica para manter a memória organizada:

```python
import asyncio
from arandu import (
    MemoryClient,
    MemoryConfig,
    cluster_user_facts,
    compute_entity_importance,
    detect_communities,
    refresh_entity_summaries,
    run_consolidation,
    run_memify,
)
from arandu.providers.openai import OpenAIProvider
from arandu.db import create_engine, create_session_factory


async def run_maintenance(
    database_url: str,
    agent_ids: list[str],
    provider: OpenAIProvider,
    config: MemoryConfig,
) -> None:
    """Executa todos os jobs de manutenção em background para uma lista de usuários."""
    engine = create_engine(database_url)
    session_factory = create_session_factory(engine)

    try:
        async with session_factory() as session:
            for agent_id in agent_ids:
                print(f"\n--- Manutenção para {agent_id} ---")

                # 1. Importance scoring (barato, apenas SQL)
                importance = await compute_entity_importance(session, agent_id, config)
                print(f"  Importance: scored {importance.entities_scored} entities")

                # 2. Summary refresh (moderado, LLM)
                summaries = await refresh_entity_summaries(
                    session, agent_id, provider, config
                )
                print(f"  Summaries: refreshed {summaries.summaries_refreshed}")

                # 3. Clustering (moderado, LLM)
                clusters = await cluster_user_facts(
                    session, agent_id, provider, provider, config
                )
                print(f"  Clustering: {clusters.clusters_created} clusters")

                # 4. Community detection
                communities = await detect_communities(
                    session, agent_id, provider, provider, config
                )
                print(f"  Communities: {communities.communities_created} created")

                # 5. Consolidation (moderado, LLM)
                consolidation = await run_consolidation(session, agent_id, provider, config)
                print(f"  Consolidation: {consolidation.observations_created} observations")

                # 6. Memify (moderado, LLM)
                memify = await run_memify(session, agent_id, provider, provider, config)
                print(f"  Memify: {memify.facts_scored} facts scored")

            await session.commit()
    finally:
        await engine.dispose()


async def main():
    provider = OpenAIProvider(api_key="sk-...")
    config = MemoryConfig()
    database_url = "postgresql+psycopg://memory:memory@localhost/memory"

    # Executar uma vez
    await run_maintenance(database_url, ["user_123", "user_456"], provider, config)

    # Ou agendar com asyncio
    # while True:
    #     await run_maintenance(database_url, agent_ids, provider, config)
    #     await asyncio.sleep(4 * 3600)  # a cada 4 horas


asyncio.run(main())
```

---

## Setup Multi-Agente

Gerencie múltiplos agentes com espaços de memória isolados:

```python
import asyncio
from arandu import MemoryClient
from arandu.providers.openai import OpenAIProvider


async def main():
    provider = OpenAIProvider(api_key="sk-...")
    memory = MemoryClient(
        database_url="postgresql+psycopg://memory:memory@localhost/memory",
        llm=provider,
        embeddings=provider,
    )
    await memory.initialize()

    try:
        # Cada agente tem memória completamente isolada
        await memory.write(
            agent_id="alice",
            message="I work at Google as a PM. I live in Mountain View.",
            speaker_name="Alice",
        )
        await memory.write(
            agent_id="bob",
            message="I'm a freelance designer based in Berlin.",
            speaker_name="Bob",
        )

        # Contexto do agente alice mostra apenas fatos da alice
        alice_ctx = await memory.retrieve(agent_id="alice", query="where do they work?")
        print("Alice:", alice_ctx.context)

        # Contexto do agente bob mostra apenas fatos do bob
        bob_ctx = await memory.retrieve(agent_id="bob", query="where do they work?")
        print("Bob:", bob_ctx.context)
    finally:
        await memory.close()


asyncio.run(main())
```

---

## Multi-Speaker (Sessão de Terapia)

Dois speakers escrevendo no mesmo agente com a mesma sessão - pronomes como "Eu" resolvem para o speaker correto a cada vez.

```python
import asyncio
from arandu import MemoryClient
from arandu.providers.openai import OpenAIProvider


async def main():
    provider = OpenAIProvider(api_key="sk-...")
    memory = MemoryClient(
        database_url="postgresql+psycopg://memory:memory@localhost/memory",
        llm=provider,
        embeddings=provider,
    )
    await memory.initialize()

    try:
        # Dois speakers, mesmo agente, mesma sessão
        await memory.write(
            agent_id="therapy_bot",
            message="Eu me sinto ignorada pelo Carlos. Ele nunca me ouve.",
            speaker_name="Ana Silva",
            session_id="sessao_001",
        )
        await memory.write(
            agent_id="therapy_bot",
            message="Eu trabalho 12 horas por dia pra sustentar a família.",
            speaker_name="Carlos Silva",
            session_id="sessao_001",
        )

        # Retrieve — fatos atribuídos aos speakers corretos
        result = await memory.retrieve(agent_id="therapy_bot", query="Como a Ana se sente?")
        # Retorna: "Ana Silva se sente ignorada pelo Carlos" (não os fatos do Carlos)
        for fact in result.facts:
            print(f"  [{fact.score:.2f}] {fact.entity_name}: {fact.fact_text}")
    finally:
        await memory.close()


asyncio.run(main())
```

---

## Multi-Session (Trabalho + Pessoal)

Mesmo speaker, mesmo agente, session_ids diferentes. Retrieve sem session_id busca em todas as sessões.

```python
import asyncio
from arandu import MemoryClient
from arandu.providers.openai import OpenAIProvider


async def main():
    provider = OpenAIProvider(api_key="sk-...")
    memory = MemoryClient(
        database_url="postgresql+psycopg://memory:memory@localhost/memory",
        llm=provider,
        embeddings=provider,
    )
    await memory.initialize()

    try:
        # Contexto de trabalho
        await memory.write(
            agent_id="assistant",
            message="Preciso entregar o relatório até sexta.",
            speaker_name="Marcos",
            session_id="work",
        )

        # Contexto pessoal
        await memory.write(
            agent_id="assistant",
            message="Minha mãe tá doente, vou visitar ela no fim de semana.",
            speaker_name="Marcos",
            session_id="personal",
        )

        # Retrieve busca em TODAS as sessões por padrão
        result = await memory.retrieve(agent_id="assistant", query="O que o Marcos precisa fazer?")
        # Retorna fatos de AMBAS as sessões (work e personal)
        for fact in result.facts:
            print(f"  [{fact.score:.2f}] {fact.entity_name}: {fact.fact_text}")
    finally:
        await memory.close()


asyncio.run(main())
```

---

## Bot de Atendimento ao Cliente (Múltiplos Clientes)

Mesmo agente atendendo diferentes clientes - os fatos de cada cliente são isolados pelo speaker_name.

```python
import asyncio
from arandu import MemoryClient
from arandu.providers.openai import OpenAIProvider


async def main():
    provider = OpenAIProvider(api_key="sk-...")
    memory = MemoryClient(
        database_url="postgresql+psycopg://memory:memory@localhost/memory",
        llm=provider,
        embeddings=provider,
    )
    await memory.initialize()

    try:
        # Cliente 1
        await memory.write(
            agent_id="support_bot",
            message="Meu pedido #1234 não chegou.",
            speaker_name="Maria Oliveira",
        )

        # Cliente 2
        await memory.write(
            agent_id="support_bot",
            message="Quero trocar o produto que comprei ontem.",
            speaker_name="João Santos",
        )

        # Retrieve para cliente específico — agente lembra cada um separadamente
        result = await memory.retrieve(agent_id="support_bot", query="Qual o problema da Maria?")
        for fact in result.facts:
            print(f"  [{fact.score:.2f}] {fact.entity_name}: {fact.fact_text}")
    finally:
        await memory.close()


asyncio.run(main())
```
# Construindo um Assistente Pessoal

Este guia mostra como dar a um agente de IA **memória igual a de um humano** usando o Arandu — pra ele lembrar o que ouviu, o que falou, quem falou o quê, e o que fez.

## O Problema

Pensa em como a tua memória funciona. Quando tu conversa com alguém:

- Tu lembra **o que a pessoa te contou** — "Pedro me disse que mora em Porto Alegre"
- Tu lembra **o que tu falou** — "Eu recomendei um restaurante pra ele"
- Tu lembra **o que aprendeu sobre outra pessoa** na conversa — "Pedro mencionou que a namorada Ana é designer"
- Tu lembra **o que tu fez** — "Eu criei um documento pro Pedro"
- Quando alguém pergunta sobre qualquer assunto, tu lembra **tudo que sabe**, independente de quem te contou ou quando

Um agente de IA não tem nada disso por padrão. Cada sessão começa em branco. O agente não sabe o que aconteceu antes, quem disse o quê, ou o que ele fez. Ele não tem memória.

O Arandu dá ao seu agente o mesmo tipo de memória que um humano tem. Cada fato carrega sobre quem é, quem disse, e quando — e a busca funciona como a lembrança humana: pergunta sobre um assunto, recebe tudo que é relevante.

## Arquitetura

Um assistente pessoal precisa de três camadas de memória:

| Camada | Ferramenta | O que guarda |
|---|---|---|
| **Memória semântica** | **Arandu** | Fatos sobre pessoas, preferências, ações, conhecimento |
| **Memória procedimental** | System prompt / config | Como o assistente deve se comportar |
| **Memória episódica** | Sistema de arquivos / banco | Logs diários, boards de tarefas, registros estruturados |

O Arandu cuida da memória semântica — a camada de "o que eu sei". Ele extrai fatos de linguagem natural, reconcilia com o conhecimento existente, e recupera por relevância.

## Padrão Principal: Gravar as Duas Pontas

O segredo: **gravar tanto as mensagens do usuário quanto as respostas do assistente**.

```python
# Usuário falou → gravar com speaker_name do usuário
await memory.write(
    agent_id="meu-assistente",
    message="Minha namorada Ana é designer, ela trabalha na Stone",
    speaker_name="Pedro",
)
# Fatos extraídos:
#   "Ana é designer" (entidade: person:ana)
#   "Ana trabalha na Stone" (entidade: person:ana, organization:stone)
#   "Ana é namorada do Pedro" (entidade: person:ana)

# Assistente respondeu → gravar com speaker_name do assistente
await memory.write(
    agent_id="meu-assistente",
    message="Criei a nota 'Ana.md' no vault e agendei lembrete de aniversário pro dia 15/05",
    speaker_name="Assistente",
)
# Fatos extraídos:
#   "Assistente criou nota Ana.md no vault" (entidade: person:assistente)
#   "Assistente agendou lembrete de aniversário pro dia 15/05" (entidade: person:assistente)
```

O `speaker_name` resolve pronomes: "eu criei" com `speaker_name="Assistente"` vira "Assistente criou". Cada fato carrega o metadado `speaker` dizendo quem falou.

## Fluxo de Sessão

```
┌─────────────────────────────────────────────────────┐
│ Início da sessão                                     │
│                                                      │
│  1. retrieve(query="contexto recente")               │
│     → Recupera o que o assistente sabe e fez         │
│     → Injeta no system prompt como contexto          │
│                                                      │
├─────────────────────────────────────────────────────┤
│ Durante a conversa                                   │
│                                                      │
│  2. Usuário manda mensagem                           │
│     → write(message=msg, speaker_name="Pedro")       │
│                                                      │
│  3. Assistente processa e responde                   │
│     → retrieve(query=msg) pra buscar contexto        │
│     → Gera resposta                                  │
│     → write(message=resposta, speaker_name="Assist") │
│                                                      │
├─────────────────────────────────────────────────────┤
│ Fim da sessão                                        │
│                                                      │
│  Nada especial. Tudo já foi gravado durante          │
│  a conversa. A próxima sessão começa com retrieve.   │
└─────────────────────────────────────────────────────┘
```

## Exemplos de Retrieval

Uma vez que as duas pontas estão gravadas, o assistente consegue responder perguntas sobre qualquer pessoa e assunto:

```python
# "O que eu sei sobre a Ana?"
result = await memory.retrieve(agent_id="meu-assistente", query="Ana")
# Retorna TODOS os fatos sobre Ana, independente de quem falou:
#   - Ana é designer (Pedro contou)
#   - Ana trabalha na Stone (Pedro contou)
#   - Assistente criou nota Ana.md (Assistente fez)

# "O que eu fiz recentemente?"
result = await memory.retrieve(agent_id="meu-assistente", query="o que foi feito recentemente")
# Retorna ações do assistente:
#   - Criou nota Ana.md
#   - Agendou lembrete de aniversário

# Restrito a uma entidade específica
result = await memory.retrieve(
    agent_id="meu-assistente",
    query="trabalho",
    entity_keys=["person:ana"],
)
# Retorna só fatos sobre trabalho relacionados à Ana
```

`entity_keys` aceita aliases também — passa `"person:ana"` mesmo que a canônica seja `person:ana_silva`, que o SDK resolve via `memory_entity_aliases`. Chaves que não resolvem aparecem em `result.warnings` pro caller distinguir "sem matches" de "chave errada":

```python
result = await memory.retrieve(
    agent_id="meu-assistente",
    query="trabalho",
    entity_keys=["person:ana", "person:unknown"],
)
# result.warnings → ["entity_key 'person:unknown' not found (not canonical, no matching alias)"]
```

## Provenance do Speaker

Todo fato carrega o campo `speaker` dizendo quem falou a mensagem de onde foi extraído. Disponível tanto no `FactDetail` (via `get`/`get_all`) quanto no `ScoredFact` (via `retrieve`):

```python
result = await memory.retrieve(agent_id="meu-assistente", query="Stone")
for fact in result.facts:
    print(f"[{fact.speaker}] {fact.fact_text}")
# Output:
#   [Pedro] Ana trabalha na Stone
#   [Assistente] Stone é uma fintech brasileira, fundada em 2012
```

Nenhum concorrente persiste provenance do speaker nativamente — isso é um diferencial do Arandu.

## Gerenciando a Memória

O assistente pode inspecionar e gerenciar sua própria memória:

```python
# Listar entidades conhecidas
entities = await memory.entities(agent_id="meu-assistente")
for e in entities:
    print(f"[{e.entity_type}] {e.display_name} ({e.fact_count} fatos)")
# [person] Pedro (12 fatos)
# [person] Ana (5 fatos)
# [organization] Stone (3 fatos)
# [person] Assistente (8 fatos)

# Listar fatos de uma entidade específica
ana_facts = await memory.get_all(
    agent_id="meu-assistente",
    entity_keys=["person:ana"],
)

# Deletar um fato específico
await memory.delete(agent_id="meu-assistente", fact_id="algum-uuid")

# Resetar toda a memória (use com cuidado)
await memory.delete_all(agent_id="meu-assistente")
```

## Exemplo Completo

Um assistente pessoal mínimo com memória:

```python
import asyncio
from arandu import MemoryClient
from arandu.providers.openai import OpenAIProvider


async def handle_message(
    memory: MemoryClient,
    agent_id: str,
    user_msg: str,
    speaker: str,
) -> str:
    # 1. Gravar mensagem do usuário
    await memory.write(
        agent_id=agent_id,
        message=user_msg,
        speaker_name=speaker,
    )

    # 2. Recuperar contexto relevante
    context = await memory.retrieve(agent_id=agent_id, query=user_msg)

    # 3. Gerar resposta (substitua pela sua chamada LLM)
    llm = memory._llm  # reutiliza o provider do SDK
    response = await llm.complete(
        messages=[
            {
                "role": "system",
                "content": f"Você é um assistente pessoal. Contexto da memória:\n{context.context}",
            },
            {"role": "user", "content": user_msg},
        ]
    )

    # 4. Gravar resposta do assistente
    await memory.write(
        agent_id=agent_id,
        message=response.text,
        speaker_name="Assistente",
    )

    return response.text


async def main():
    provider = OpenAIProvider(api_key="sk-...")
    memory = MemoryClient(
        database_url="postgresql+psycopg://memory:memory@localhost:5432/memory",
        llm=provider,
        embeddings=provider,
    )
    await memory.initialize()

    agent_id = "assistente-pedro"

    try:
        # Início da sessão: verificar o que aconteceu antes
        recent = await memory.retrieve(agent_id=agent_id, query="contexto recente")
        if recent.facts:
            print("Retomando com contexto de sessões anteriores...")

        # Simular conversa
        reply = await handle_message(
            memory, agent_id,
            "Minha namorada Ana é designer na Stone",
            speaker="Pedro",
        )
        print(f"Assistente: {reply}")

        reply = await handle_message(
            memory, agent_id,
            "O que você sabe sobre a Ana?",
            speaker="Pedro",
        )
        print(f"Assistente: {reply}")
    finally:
        await memory.close()


asyncio.run(main())
```

## Como o Arandu se Compara

| Capacidade | Mem0 | Zep | Letta | Arandu |
|---|---|---|---|---|
| Extração de fatos do texto | Sim | Sim | Não (manual) | Sim |
| Filtro no retrieve | `user_id`/`agent_id` | `group_ids` | Labels de bloco | `entity_keys` |
| Provenance do speaker | Não | Indireta (lookup no episódio) | Não | **Sim (nativo)** |
| Grafo de entidades | Opcional | Sim | Não | Sim |
| Operações CRUD | Sim | Sim | Sim | Sim |
| Reconciliação (dedup) | Sim | Automática | Manual | Sim (via LLM) |
# API do Read Pipeline

!!! warning "API Avancada"
    Estas sao APIs avancadas para usuarios que desejam interagir diretamente com estagios individuais de retrieval. A maioria dos usuarios deve usar [`MemoryClient.retrieve()`](../reference/index.md), que orquestra o pipeline multi-signal completo automaticamente.

Todas as funcoes do read pipeline sao exportadas de `arandu.read`.

```python
from arandu.read import (
    run_read_pipeline,
    plan_retrieval, expand_query,
    retrieve_relevant_events, compute_pattern_signal,
    retrieve_graph_facts, spread_activation,
    compress_context, compress_broad_context,
    materialize_emotional_trends, get_emotional_summary_for_context,
    compute_dynamic_importance,
    generate_optimized_directives, check_directive_contradiction,
    effective_confidence, invalidate_directive_cache,
)
```

---

## Orquestrador do Pipeline

### run_read_pipeline

Executa o pipeline completo de leitura: **agent** -> **retrieve (multi-signal)** -> **rerank** -> **format**.

O retrieval multi-signal executa semantic + keyword + graph em paralelo via `asyncio.gather()`. O retrieval agent planeja a estrategia de retrieval de forma deterministica (sem LLM).

```python
async def run_read_pipeline(
    session: AsyncSession,
    agent_id: str,
    query: str,
    llm: LLMProvider,
    embeddings: EmbeddingProvider,
    config: MemoryConfig,
    trace: PipelineTrace | None = None,
) -> ReadResult
```

| Parametro | Tipo | Descricao |
|-----------|------|-----------|
| `session` | `AsyncSession` | Sessao do banco (o caller gerencia transacao). |
| `agent_id` | `str` | Identificador do agente. |
| `query` | `str` | A query para buscar na memoria. |
| `llm` | `LLMProvider` | Provider de LLM injetado. |
| `embeddings` | `EmbeddingProvider` | Provider de embeddings injetado. |
| `config` | `MemoryConfig` | Configuracao da memoria. |

**Retorna:** `ReadResult` com `facts` (lista de `ScoredFact`), `context` (string pronta para prompt), `total_candidates` e `duration_ms`.

---

## Retrieval Agent

O retrieval agent e um planejador deterministico (sem LLM) que analisa a query do usuario e decide a estrategia de retrieval antes de qualquer busca acontecer.

### PatternQuery

Uma query baseada em padrao para matching de sinal de keyword.

| Campo | Tipo | Descricao |
|-------|------|-----------|
| `entity_pattern` | `str` | Padrao SQL LIKE para matching de entity_key. |
| `attribute_filter` | `str | None` | Filtro opcional de chave de atributo (sempre `None` no V5). |

### RetrievalPlan

Saida do retrieval agent. V5 executa todos os sinais (semantic, graph, keyword) em paralelo.

| Campo | Tipo | Padrao | Descricao |
|-------|------|--------|-----------|
| `strategy` | `str` | `"multi_signal"` | `"multi_signal"` (padrao) ou `"skip"`. |
| `entities` | `list[str]` | `[]` | entity_keys detectadas para sinal de grafo. |
| `pattern_queries` | `list[PatternQuery]` | `[]` | Pattern queries para sinal de keyword. |
| `similarity_query` | `str | None` | `None` | Sempre a query original (sem reformulacao). |
| `max_facts` | `int` | `50` | Budget por sinal. |
| `reason` | `str` | `""` | Motivo da escolha do plano. |
| `latency_ms` | `float` | `0.0` | Tempo gasto no planejamento. |
| `as_of_range` | `tuple[datetime, datetime] | None` | `None` | Janela temporal opcional (time-travel). |
| `broad_query` | `bool` | `False` | True para queries abrangentes. |

### plan_retrieval

Planejador deterministico que decide a estrategia de retrieval com base em heuristicas (sem chamada LLM). A resolucao de entidades na query e feita por `resolve_query_entities` em etapa separada. Resolucao de anafora e responsabilidade do caller.

```python
def plan_retrieval(
    query_text: str,
) -> RetrievalPlan
```

| Parametro | Tipo | Descricao |
|-----------|------|-----------|
| `query_text` | `str` | A query do usuario. |

**Retorna:** `RetrievalPlan` com estrategia e parametros de query.

---

## Expansao de Query

Pos-processa um `RetrievalPlan` com entity priming -- resolve entidades mencionadas na query via knowledge graph (aliases + relacionamentos) e injeta termos de contexto.

### ExpandedQuery

| Campo | Tipo | Descricao |
|-------|------|-----------|
| `primed_entities` | `list[str]` | Entity keys descobertas via alias + KG priming. |
| `temporal_range` | `tuple[datetime, datetime] | None` | Faixa de datas resolvida. |
| `expanded_terms` | `list[str]` | Termos de contexto adicionais dos fatos das entidades. |

### expand_query

Expande um plano de retrieval com entity priming. Fail-safe: qualquer excecao retorna um `ExpandedQuery` vazio.

```python
async def expand_query(
    session: AsyncSession,
    agent_id: str,
    query: str,
    plan: RetrievalPlan,
    llm: object,
) -> ExpandedQuery
```

| Parametro | Tipo | Descricao |
|-----------|------|-----------|
| `session` | `AsyncSession` | Sessao do banco de dados. |
| `agent_id` | `str` | Identificador do agente. |
| `query` | `str` | Texto original da query do usuario. |
| `plan` | `RetrievalPlan` | RetrievalPlan do retrieval agent. |
| `llm` | `object` | Provider de LLM (reservado para uso futuro). |

**Retorna:** `ExpandedQuery` com entidades primadas, faixa temporal e termos expandidos.

---

## Retrieval de Fatos

### retrieve_relevant_events

Recupera eventos relevantes por similaridade de embedding + scoring de recencia.

```python
async def retrieve_relevant_events(
    session: AsyncSession,
    agent_id: str,
    query_embedding: list[float],
    config: MemoryConfig,
    limit: int | None = None,
) -> list[dict[str, Any]]
```

| Parametro | Tipo | Descricao |
|-----------|------|-----------|
| `session` | `AsyncSession` | Sessao do banco de dados. |
| `agent_id` | `str` | Identificador do agente. |
| `query_embedding` | `list[float]` | Vetor de embedding da query. |
| `config` | `MemoryConfig` | Configuracao da memoria. |
| `limit` | `int | None` | Maximo de eventos a retornar. |

**Retorna:** Lista de dicts de evento com `date`, `text`, `score`, `event_id`.

### compute_pattern_signal

Impulsiona fatos confirmados recentemente (sinal de padrão). Fatos com `last_confirmed_at` recente (confirmados via decisões NOOP no write) recebem um boost aditivo pequeno (até 0.1).

```python
def compute_pattern_signal(
    candidates: list[RetrievalCandidate],
) -> list[RetrievalCandidate]
```

| Parametro | Tipo | Descricao |
|-----------|------|-----------|
| `candidates` | `list[RetrievalCandidate]` | Candidatos ranqueados atuais. |

**Retorna:** Candidatos com scores atualizados, ordenados por `final_score`.

---

## Retrieval de Grafo

Travessia BFS de 2 saltos no knowledge graph `MemoryEntityRelationship` com poda de relevancia.

### GraphRetrievalResult

| Campo | Tipo | Descricao |
|-------|------|-----------|
| `facts` | `list[dict[str, Any]]` | Dicts de fatos pontuados com `source="graph"`. |
| `neighbor_keys` | `list[str]` | Entity keys descobertas via BFS. |
| `edges_traversed` | `int` | Total de arestas examinadas durante BFS. |
| `edges` | `list[dict[str, Any]]` | Dicts de arestas deduplicadas com nomes de exibicao. |

### retrieve_graph_facts

Retrieval BFS de 2 saltos com scoring composto: `edge_strength * recency * edge_recency * query_bonus`.

```python
async def retrieve_graph_facts(
    session: AsyncSession,
    agent_id: str,
    entity_keys: list[str],
    *,
    min_confidence: float = 0.3,
    as_of_start: datetime | None = None,
    as_of_end: datetime | None = None,
    broad_query: bool = False,
    max_facts: int | None = None,
    query_text: str = "",
    min_edge_strength: float = 0.5,
) -> GraphRetrievalResult
```

| Parametro | Tipo | Padrao | Descricao |
|-----------|------|--------|-----------|
| `session` | `AsyncSession` | -- | Sessao do banco de dados. |
| `agent_id` | `str` | -- | Identificador do agente. |
| `entity_keys` | `list[str]` | -- | entity_keys semente para iniciar BFS. |
| `min_confidence` | `float` | `0.3` | Threshold minimo de confianca do fato. |
| `as_of_start` | `datetime | None` | `None` | Inicio da janela temporal. |
| `as_of_end` | `datetime | None` | `None` | Fim da janela temporal. |
| `broad_query` | `bool` | `False` | Quando True, permite budget expandido. |
| `max_facts` | `int | None` | `None` | Override do limite padrao (30). |
| `query_text` | `str` | `""` | Texto original da query para scoring de query_bonus. |
| `min_edge_strength` | `float` | `0.5` | Forca minima de aresta para poda no salto 2+. |

**Retorna:** `GraphRetrievalResult` com fatos pontuados e metadados do grafo.

---

## Spreading Activation

Expande contexto a partir de fatos semente seguindo links de `entity_key`, `cluster_id` e relacionamentos do knowledge graph. Usa scoring de importancia dinamica com decaimento por salto.

### SpreadingActivationResult

| Campo | Tipo | Descricao |
|-------|------|-----------|
| `candidates` | `list[RetrievalCandidate]` | Candidatos expandidos dos saltos 1-2. |
| `meta_observations` | `list[Any]` | Meta-observacoes relevantes referenciando fatos semente. |
| `entities_explored` | `list[str]` | Entity keys exploradas durante spreading. |
| `clusters_explored` | `list[str]` | Cluster IDs explorados durante spreading. |
| `hop1_count` | `int` | Numero de fatos encontrados no salto 1. |
| `hop2_count` | `int` | Numero de fatos encontrados no salto 2. |
| `kg_relationships_explored` | `int` | Numero de relacionamentos KG percorridos. |

### spread_activation

Expande contexto a partir de fatos semente via entity_key, cluster_id e relacionamentos KG (saltos 1-2).

```python
async def spread_activation(
    session: AsyncSession,
    agent_id: str,
    seed_fact_ids: list[str],
    config: MemoryConfig,
    *,
    seed_scores: dict[str, float] | None = None,
    allowed_keys: set[str] | None = None,
) -> list[RetrievalCandidate]
```

| Parametro | Tipo | Descricao |
|-----------|------|-----------|
| `session` | `AsyncSession` | Sessao do banco de dados. |
| `agent_id` | `str` | Identificador do agente. |
| `seed_fact_ids` | `list[str]` | IDs dos fatos semente para expandir. |
| `config` | `MemoryConfig` | Configuracao da memoria com parametros de spreading activation. |
| `seed_scores` | `dict[str, float] | None` | Dict opcional mapeando ID do fato semente para score. |
| `allowed_keys` | `set[str] | None` | Conjunto opcional de chaves de atributo permitidas. |

**Retorna:** Lista de objetos `RetrievalCandidate` do spreading activation. Fail-safe: retorna lista vazia em caso de erro.

---

## Compressao de Contexto

Constroi uma string de contexto pronta para prompt a partir de fatos pontuados, eventos, clusters e meta-observacoes usando um sistema em camadas: **Hot** (Tier 1), **Warm** (Tier 2), **Cold** (Tier 3).

### CompressedContext

| Campo | Tipo | Descricao |
|-------|------|-----------|
| `context_text` | `str` | String de contexto final pronta para prompt. |
| `hot_count` | `int` | Numero de fatos na camada hot (Tier 1). |
| `warm_count` | `int` | Numero de fatos na camada warm (Tier 2). |
| `cold_count` | `int` | Numero de itens na camada cold (Tier 3). |
| `total_tokens` | `int` | Contagem estimada de tokens do context_text. |

### compress_context

Constroi texto de contexto em camadas dentro do budget de tokens.

```python
async def compress_context(
    facts: list[dict[str, Any]],
    events: list[dict[str, Any]],
    config: MemoryConfig,
    *,
    clusters: list[Any] | None = None,
    meta_observations: list[Any] | None = None,
    stale_keys: set[str] | None = None,
    stale_threshold_days: int = 90,
    now: datetime | None = None,
) -> CompressedContext
```

| Parametro | Tipo | Descricao |
|-----------|------|-----------|
| `facts` | `list[dict]` | Dicts de fatos pontuados (devem ter chaves `score`, `fact`, `entity`, `attribute`, `value`, `date`). |
| `events` | `list[dict]` | Dicts de evento com chaves `date` e `text`. |
| `config` | `MemoryConfig` | Configuracao da memoria com budget de tokens e ratios de camada. |
| `clusters` | `list | None` | Objetos de cluster opcionais. |
| `meta_observations` | `list | None` | Objetos de meta-observacao opcionais. |
| `stale_keys` | `set[str] | None` | Chaves de atributo consideradas sempre obsoletas. |
| `stale_threshold_days` | `int` | Dias apos os quais um fato e considerado obsoleto (padrao 90). |
| `now` | `datetime | None` | Timestamp atual (padrao UTC now). |

**Retorna:** `CompressedContext` com texto de contexto em camadas.

### compress_broad_context

Constroi contexto para queries abrangentes usando clusters como unidade primaria.

```python
async def compress_broad_context(
    cluster_facts: dict[str, list[dict[str, Any]]],
    clusters: list[Any],
    config: MemoryConfig,
    *,
    meta_observations: list[Any] | None = None,
    events: list[dict[str, Any]] | None = None,
) -> CompressedContext
```

| Parametro | Tipo | Descricao |
|-----------|------|-----------|
| `cluster_facts` | `dict[str, list[dict]]` | Mapeamento de cluster_label para dicts de fatos. |
| `clusters` | `list[Any]` | Objetos de cluster com `label`, `summary_text`, `fact_count`. |
| `config` | `MemoryConfig` | Configuracao da memoria. |
| `meta_observations` | `list | None` | Objetos de meta-observacao opcionais. |
| `events` | `list[dict] | None` | Dicts de evento opcionais. |

**Retorna:** `CompressedContext` com texto de contexto cluster-first.

---

## Tendencias Emocionais

Materializa tendencias emocionais a partir de eventos de memoria e fornece sumarios formatados para injecao no contexto de retrieval.

### EmotionalTrendsResult

| Campo | Tipo | Descricao |
|-------|------|-----------|
| `emotion_counts` | `dict[str, int]` | Mapeamento de emocao para contagem de ocorrencias. |
| `trend_direction` | `str` | `"increasing"`, `"decreasing"` ou `"stable"`. |
| `dominant_emotion` | `str | None` | Emocao mais frequente, ou None. |
| `trigger_keywords` | `list[str]` | Top keywords de eventos de alta intensidade. |
| `avg_intensity` | `float` | Intensidade emocional media entre eventos. |
| `dominant_intensity` | `float` | Intensidade media da emocao dominante. |
| `dominant_energy` | `str` | Nivel de energia predominante (high/medium/low). |
| `events_analyzed` | `int` | Numero de eventos analisados. |
| `observation_created` | `bool` | Se uma meta-observacao foi criada/atualizada. |
| `observation_id` | `str | None` | ID da observacao criada/atualizada. |

### materialize_emotional_trends

Agrega dados de emocao de eventos, detecta tendencias e materializa como meta-observacao.

```python
async def materialize_emotional_trends(
    session: AsyncSession,
    agent_id: str,
    config: MemoryConfig,
) -> EmotionalTrendsResult
```

| Parametro | Tipo | Descricao |
|-----------|------|-----------|
| `session` | `AsyncSession` | Sessao do banco de dados. |
| `agent_id` | `str` | Identificador do agente. |
| `config` | `MemoryConfig` | Configuracao da memoria com janela de tendencia e minimo de eventos. |

**Retorna:** `EmotionalTrendsResult` com dados de tendencia agregados.

### get_emotional_summary_for_context

Retorna sumario emocional formatado para injecao no contexto de retrieval. Retorna `None` se nao existir tendencia emocional ativa recente (7 dias).

```python
async def get_emotional_summary_for_context(
    session: AsyncSession,
    agent_id: str,
) -> str | None
```

**Retorna:** String de sumario formatada, ou `None`.

---

## Importancia Dinamica

### compute_dynamic_importance

Calcula score de importancia dinamica para um fato de memoria. Inspirado em modelos cognitivos de forca de memoria.

Componentes:

- **retrieval_boost**: `log(1 + times_retrieved)` -- satura gradualmente
- **recency_of_use_boost**: decai a partir de `last_retrieved_at` (meia-vida de 7 dias)
- **correction_penalty**: `0.8^n` para cada correcao do usuario
- **pattern_boost**: 1.3x se o fato faz parte de uma meta-observacao ativa

```python
def compute_dynamic_importance(
    base_importance: float,
    times_retrieved: int,
    last_retrieved_at: datetime | None,
    user_correction_count: int,
    is_in_active_pattern: bool,
    now: datetime | None = None,
) -> float
```

| Parametro | Tipo | Descricao |
|-----------|------|-----------|
| `base_importance` | `float` | Score de importancia base (tipicamente 0.5). |
| `times_retrieved` | `int` | Numero de vezes que este fato foi recuperado. |
| `last_retrieved_at` | `datetime | None` | Quando o fato foi recuperado pela ultima vez. |
| `user_correction_count` | `int` | Numero de correcoes do usuario neste fato. |
| `is_in_active_pattern` | `bool` | Se o fato faz parte de uma meta-observacao ativa. |
| `now` | `datetime | None` | Timestamp atual (padrao UTC now). |

**Retorna:** Score de importancia dinamica, limitado a `[0.05, 3.0]`.

---

## Memoria Procedimental

Sistema de diretivas comportamentais otimizado para LLM que comprime persona + preferencias comportamentais aprendidas em blocos de instrucao coesos.

### DirectiveBlock

| Campo | Tipo | Descricao |
|-------|------|-----------|
| `text` | `str` | Bloco de instrucoes comportamentais coeso. |
| `directive_count` | `int` | Numero de diretivas ativas usadas. |
| `cache_hit` | `bool` | Se foi servido do cache. |

### ContradictionResult

| Campo | Tipo | Descricao |
|-------|------|-----------|
| `has_contradiction` | `bool` | Se uma contradicao foi encontrada. |
| `conflicting_directive` | `str | None` | Titulo da diretiva conflitante. |
| `resolution` | `str | None` | Explicacao de como a contradicao foi resolvida. |

### generate_optimized_directives

Gera um bloco de instrucoes comportamentais otimizado por LLM integrando persona + diretivas aprendidas.

```python
async def generate_optimized_directives(
    session: AsyncSession,
    agent_id: str,
    llm_provider: LLMProvider,
    config: MemoryConfig,
    *,
    persona_text: str = "",
) -> DirectiveBlock
```

| Parametro | Tipo | Descricao |
|-----------|------|-----------|
| `session` | `AsyncSession` | Sessao do banco de dados. |
| `agent_id` | `str` | Identificador do agente. |
| `llm_provider` | `LLMProvider` | Provider de LLM injetado. |
| `config` | `MemoryConfig` | Configuracao da memoria. |
| `persona_text` | `str` | Descricao de persona opcional. |

**Retorna:** `DirectiveBlock` com texto gerado. Resultado e cacheado por hash dos IDs de diretivas + contagens de reforco. Fail-safe: retorna `DirectiveBlock` vazio em caso de erro.

### check_directive_contradiction

Verifica uma nova diretiva contra existentes para contradicoes. Usa similaridade de embedding como pre-filtro, depois LLM como juiz.

```python
async def check_directive_contradiction(
    session: AsyncSession,
    agent_id: str,
    new_directive: str,
    embedding_provider: EmbeddingProvider,
    llm_provider: LLMProvider,
    *,
    similarity_threshold: float = 0.80,
) -> ContradictionResult
```

| Parametro | Tipo | Descricao |
|-----------|------|-----------|
| `session` | `AsyncSession` | Sessao do banco de dados. |
| `agent_id` | `str` | Identificador do agente. |
| `new_directive` | `str` | Texto da nova diretiva para verificar. |
| `embedding_provider` | `EmbeddingProvider` | Provider de embedding injetado. |
| `llm_provider` | `LLMProvider` | Provider de LLM injetado. |
| `similarity_threshold` | `float` | Similaridade minima para acionar verificacao LLM (padrao 0.80). |

**Retorna:** `ContradictionResult` com resultado da verificacao. Fail-safe: retorna sem contradicao em caso de erro.

### effective_confidence

Aplica decaimento temporal na confianca de diretivas. Formula: `base_confidence * 0.95^semanas`.

```python
def effective_confidence(
    base_confidence: float,
    created_at: datetime,
    now: datetime | None = None,
) -> float
```

| Parametro | Tipo | Descricao |
|-----------|------|-----------|
| `base_confidence` | `float` | Valor de confianca original (0.0-1.0). |
| `created_at` | `datetime` | Quando a diretiva foi criada. |
| `now` | `datetime | None` | Timestamp atual (padrao UTC now). |

**Retorna:** Confianca com decaimento, piso em 0.10.

### invalidate_directive_cache

Invalida manualmente o cache de diretivas de um usuario.

```python
def invalidate_directive_cache(agent_id: str) -> None
```
# API do Write Pipeline

!!! warning "API Avançada"
    Estas são APIs avançadas para usuários que desejam interagir diretamente com estágios individuais do pipeline. A maioria dos usuários deve usar [`MemoryClient.write()`](../reference/index.md), que orquestra o pipeline completo automaticamente.

Todas as funções do write pipeline são exportadas de `arandu.write`.

```python
from arandu.write import (
    classify_input, select_strategy, run_write_pipeline,
    canonicalize_attribute_key, normalize_key, validate_proposed_key,
    create_or_update_entity, get_entities_for_user, get_entity_by_key,
    detect_and_record_corrections, is_user_correction,
    get_pending, clear_pending, save_pending_execution, save_pending_selection,
)
```

---

## Orquestrador do Pipeline

### run_write_pipeline

Executa o pipeline completo de escrita. No fluxo padrão (extração informada): **alias lookup** -> **pré-retrieval** -> **extração informada** -> **resolve** -> **upsert**. No fallback (extração cega): **extract** -> **resolve** -> **reconcile** -> **upsert**.

```python
async def run_write_pipeline(
    session: AsyncSession,
    agent_id: str,
    message: str,
    llm: LLMProvider,
    embeddings: EmbeddingProvider,
    config: MemoryConfig,
    speaker_name: str,
    source: str = "api",
    recent_messages: list[str] | None = None,
    trace: PipelineTrace | None = None,
) -> dict
```

| Parametro | Tipo | Descricao |
|-----------|------|-----------|
| `session` | `AsyncSession` | Sessao do banco (o caller gerencia transacao/commit). |
| `agent_id` | `str` | Identificador unico do agente. |
| `message` | `str` | Texto da mensagem do usuario. |
| `llm` | `LLMProvider` | Provider de LLM injetado. |
| `embeddings` | `EmbeddingProvider` | Provider de embeddings injetado. |
| `config` | `MemoryConfig` | Configuracao da memoria. |
| `speaker_name` | `str` | Nome de quem esta falando a mensagem. Pronomes como "I", "me", "eu" sao resolvidos para a entidade do speaker (`person:{speaker_slug}`). |
| `source` | `str` | Identificador do canal de origem (padrao `"api"`). |
| `recent_messages` | `list[str] | None` | Contexto de conversacao opcional (ultimas N mensagens) para resolver pronomes e anaforas. |

**Retorna:** `dict` com chaves `event_id`, `facts_added`, `facts_updated`, `facts_unchanged`, `facts_deleted`, `entities_resolved`, `duration_ms`.

O pipeline cria um `MemoryEvent` imutavel primeiro (sobrevive mesmo se estagios posteriores falharem), depois executa o fluxo de extracao (informada por padrao, com fallback para extracao cega + reconciliacao), resolucao de entidades e upsert dentro de um savepoint para atomicidade. Quando a extracao informada esta habilitada (`enable_informed_extraction=True`, padrao), o pipeline faz alias lookup e pre-retrieval antes da chamada LLM, e o estagio de reconciliacao e eliminado pois o LLM decide NEW/UPDATE na fonte.

---

## Estrategia de Extracao

Funcoes puras (sem LLM, sem DB) que classificam o texto de entrada e escolhem um modo de extracao baseado em heuristicas.

### InputType

```python
class InputType(str, Enum):
    SHORT = "short"        # < 500 caracteres
    MEDIUM = "medium"      # 500-2000 caracteres, nao estruturado
    LONG = "long"          # > 2000 caracteres, nao estruturado
    STRUCTURED = "structured"  # > 500 caracteres com headers/bullets/tabelas
```

### ExtractionMode

```python
class ExtractionMode(str, Enum):
    SINGLE_SHOT = "single_shot"
    CHUNKED = "chunked"
```

### InputClassification

Resultado da analise do texto de entrada.

| Campo | Tipo | Descricao |
|-------|------|-----------|
| `input_type` | `InputType` | Tipo de entrada classificado. |
| `char_count` | `int` | Numero de caracteres. |
| `estimated_tokens` | `int` | Contagem estimada de tokens (chars // 4). |
| `has_headers` | `bool` | Se headers foram detectados. |
| `has_bullets` | `bool` | Se bullet points foram detectados. |
| `has_tables` | `bool` | Se tabelas foram detectadas. |
| `section_count` | `int` | Numero de secoes de texto. |
| `line_count` | `int` | Numero de linhas. |

### ExtractionStrategy

Estrategia de extracao selecionada.

| Campo | Tipo | Descricao |
|-------|------|-----------|
| `mode` | `ExtractionMode` | Modo de extracao (single_shot ou chunked). |
| `reason` | `str` | Motivo legivel da selecao. |
| `max_tokens_per_call` | `int` | Maximo de tokens por chamada LLM. |
| `estimated_chunks` | `int` | Numero esperado de chunks (1 para single-shot). |
| `chunk_context_hint` | `str | None` | Dica sobre tipo de documento para modo chunked. |

### classify_input

Classifica o texto de entrada usando heuristicas (sem chamada LLM).

```python
def classify_input(text: str) -> InputClassification
```

| Parametro | Tipo | Descricao |
|-----------|------|-----------|
| `text` | `str` | Texto de entrada para classificar. |

**Retorna:** `InputClassification` com features detectadas.

```python
from arandu.write import classify_input, select_strategy

classification = classify_input("Minha esposa se chama Ana e moramos em Sao Paulo.")
print(classification.input_type)  # InputType.SHORT
print(classification.char_count)  # 50
```

### select_strategy

Seleciona a estrategia de extracao a partir de um resultado de classificacao.

```python
def select_strategy(classification: InputClassification) -> ExtractionStrategy
```

| Parametro | Tipo | Descricao |
|-----------|------|-----------|
| `classification` | `InputClassification` | Resultado de `classify_input()`. |

**Retorna:** `ExtractionStrategy` com modo e parametros.

```python
strategy = select_strategy(classification)
print(strategy.mode)             # ExtractionMode.SINGLE_SHOT
print(strategy.estimated_chunks) # 1
```

---

## Canonicalizacao de Chaves de Atributo

Pipeline: **match exato** -> **alias** -> **variante pontuada** -> **sufixo** -> **catalogo aberto** -> **drop**.

### normalize_key

Normaliza uma chave de atributo bruta: lowercase, strip, espacos/hifens para pontos. Underscores sao preservados.

```python
def normalize_key(raw: str) -> str
```

| Parametro | Tipo | Descricao |
|-----------|------|-----------|
| `raw` | `str` | String bruta da chave de atributo. |

**Retorna:** String da chave normalizada.

```python
from arandu.write import normalize_key

normalize_key("Personal Info")    # "personal.info"
normalize_key("food_preference")  # "food_preference"
```

### validate_proposed_key

Valida se uma chave proposta atende as regras de nomenclatura.

```python
def validate_proposed_key(
    key: str,
    extra_namespaces: set[str] | None = None,
) -> bool
```

| Parametro | Tipo | Descricao |
|-----------|------|-----------|
| `key` | `str` | Chave normalizada para validar. |
| `extra_namespaces` | `set[str] | None` | Namespaces adicionais fornecidos pelo deployer. |

**Retorna:** `True` se a chave e bem formada e esta em um namespace permitido.

### canonicalize_attribute_key

Canonicaliza uma chave de atributo via catalogo, alias e estrategias de recuperacao. Funcao async que consulta o banco de dados.

```python
async def canonicalize_attribute_key(
    session: AsyncSession,
    agent_id: str,
    raw_key: str,
    config: MemoryConfig,
) -> tuple[str | None, Literal["allow", "map", "propose", "drop"], dict[str, Any]]
```

| Parametro | Tipo | Descricao |
|-----------|------|-----------|
| `session` | `AsyncSession` | Sessao do banco de dados. |
| `agent_id` | `str` | Identificador do agente. |
| `raw_key` | `str` | Chave bruta do atributo vinda da extracao. |
| `config` | `MemoryConfig` | Configuracao da memoria. |

**Retorna:** Tupla de `(canonical_key, action, metadata)` onde action e um de `"allow"`, `"map"`, `"propose"` ou `"drop"`.

---

## Helpers de Entidades

Operacoes CRUD async para registros `MemoryEntity` usando upsert `ON CONFLICT` do PostgreSQL.

### create_or_update_entity

Cria um `MemoryEntity` ou atualiza se ja existir.

```python
async def create_or_update_entity(
    session: AsyncSession,
    agent_id: str,
    canonical_key: str,
    display_name: str | None = None,
    entity_type: str = "other",
) -> MemoryEntity
```

| Parametro | Tipo | Descricao |
|-----------|------|-----------|
| `session` | `AsyncSession` | Sessao do banco de dados. |
| `agent_id` | `str` | Identificador do agente. |
| `canonical_key` | `str` | Chave canonica da entidade. |
| `display_name` | `str | None` | Nome de exibicao opcional. |
| `entity_type` | `str` | Tipo da entidade (person, pet, place, etc.). Padrao `"other"`. |

**Retorna:** O `MemoryEntity` criado ou atualizado.

### get_entity_by_key

Obtem um unico `MemoryEntity` por agent_id e canonical_key.

```python
async def get_entity_by_key(
    session: AsyncSession,
    agent_id: str,
    canonical_key: str,
) -> MemoryEntity | None
```

**Retorna:** `MemoryEntity` ou `None` se nao encontrado.

### get_entities_for_user

Lista todos os registros `MemoryEntity` de um usuario.

```python
async def get_entities_for_user(
    session: AsyncSession,
    agent_id: str,
    active_only: bool = True,
) -> list[MemoryEntity]
```

| Parametro | Tipo | Descricao |
|-----------|------|-----------|
| `session` | `AsyncSession` | Sessao do banco de dados. |
| `agent_id` | `str` | Identificador do agente. |
| `active_only` | `bool` | Se True, retorna apenas entidades ativas. Padrao `True`. |

**Retorna:** Lista de registros `MemoryEntity`, ordenados por `last_seen_at` decrescente.

---

## Deteccao de Correcoes

Detecta quando usuarios corrigem fatos da memoria comparando valores antigos vs novos para o mesmo attribute_key.

### CorrectionResult

| Campo | Tipo | Descricao |
|-------|------|-----------|
| `corrections_detected` | `int` | Numero de correcoes encontradas. Padrao `0`. |
| `corrected_keys` | `list[str]` | Chaves de atributo que foram corrigidas. |
| `facts_corrected_ids` | `list[str]` | IDs dos fatos antigos que foram corrigidos. |

### is_user_correction

Verifica se um novo fato corrige um fato antigo (mesma chave, valor diferente).

```python
def is_user_correction(old_fact: object, new_fact: object) -> bool
```

| Parametro | Tipo | Descricao |
|-----------|------|-----------|
| `old_fact` | `object` | O fato existente sendo substituido. |
| `new_fact` | `object` | O novo fato que o substitui. |

**Retorna:** `True` se for uma correcao do usuario.

### detect_and_record_corrections

Detecta supersedes com mudancas de valor e incrementa o contador de correcoes nos fatos antigos.

```python
async def detect_and_record_corrections(
    session: AsyncSession,
    agent_id: str,
    saved_facts: list[Any],
) -> CorrectionResult
```

| Parametro | Tipo | Descricao |
|-----------|------|-----------|
| `session` | `AsyncSession` | Sessao do banco de dados. |
| `agent_id` | `str` | Identificador do agente. |
| `saved_facts` | `list[Any]` | Lista de objetos MemoryFact recem-salvos. |

**Retorna:** `CorrectionResult` com estatisticas de deteccao.

---

## Operacoes Pendentes

Armazenamento em memoria para operacoes destrutivas pendentes com TTL de 5 minutos. O estado e por processo e perdido ao reiniciar.

### save_pending_selection

Salva uma selecao pendente quando uma busca retornou resultados aguardando escolha do usuario.

```python
def save_pending_selection(
    agent_id: str,
    intent: str,
    transactions: list[Any],
    confirmation_text: str,
    edit_params: dict[str, Any] | None = None,
) -> None
```

| Parametro | Tipo | Descricao |
|-----------|------|-----------|
| `agent_id` | `str` | Identificador do agente. |
| `intent` | `str` | A intencao do usuario (delete, edit, etc.). |
| `transactions` | `list[Any]` | Lista de transacoes candidatas. |
| `confirmation_text` | `str` | Texto para mostrar ao usuario para confirmacao. |
| `edit_params` | `dict | None` | Parametros opcionais para operacoes de edicao. |

### save_pending_execution

Salva uma execucao pendente quando uma operacao destrutiva foi bloqueada.

```python
def save_pending_execution(
    agent_id: str,
    tool_calls: list[Any],
    search_result: str,
    confirmation_text: str,
) -> None
```

| Parametro | Tipo | Descricao |
|-----------|------|-----------|
| `agent_id` | `str` | Identificador do agente. |
| `tool_calls` | `list[Any]` | Tool calls bloqueadas. |
| `search_result` | `str` | Contexto da busca. |
| `confirmation_text` | `str` | Texto para mostrar ao usuario para confirmacao. |

### get_pending

Obtem operacao pendente se existir e nao tiver expirado (TTL de 5 minutos).

```python
def get_pending(agent_id: str) -> dict[str, Any] | None
```

**Retorna:** Dict da operacao pendente, ou `None` se expirado/ausente.

### clear_pending

Remove operacao pendente apos execucao ou cancelamento.

```python
def clear_pending(agent_id: str) -> None
```
# Database Schema

Arandu uses PostgreSQL with pgvector. All tables are created automatically by `memory.initialize()`. This page documents each table for debugging, querying, and understanding the data model.

---

## Core Tables

### memory_events

Immutable audit log. Every `write()` call creates one event.

| Column | Type | Description |
|--------|------|-------------|
| `id` | UUID | Primary key |
| `agent_id` | TEXT | Agent identifier |
| `content` | TEXT | Raw message text |
| `embedding` | VECTOR | Message embedding |
| `emotion` | VARCHAR | Detected emotion (joy, sadness, anger, etc.) |
| `emotion_intensity` | FLOAT | Emotion intensity 0.0 - 1.0 |
| `energy_level` | VARCHAR | high, medium, or low |
| `created_at` | TIMESTAMP | When the event was created |

### memory_facts

Versioned factual knowledge. Each fact is a self-contained natural language statement about an entity.

| Column | Type | Description |
|--------|------|-------------|
| `id` | UUID | Primary key |
| `agent_id` | TEXT | Agent identifier |
| `entity_type` | VARCHAR | Free-form entity type (person, organization, place, etc.) |
| `entity_key` | VARCHAR | Canonical entity key (e.g., `person:carlos`) |
| `entity_name` | VARCHAR | Display name of the entity |
| `attribute_key` | VARCHAR | Optional attribute category |
| `fact_text` | TEXT | The fact in natural language |
| `embedding` | VECTOR | Fact text embedding |
| `confidence` | FLOAT | Extraction confidence 0.0 - 1.0 |
| `importance` | FLOAT | Base importance score |
| `source_event_id` | UUID | FK to the event that created this fact |
| `supersedes_fact_id` | UUID | FK to the fact this one replaces (UPDATE chain) |
| `valid_from` | TIMESTAMP | When this fact became active |
| `valid_to` | TIMESTAMP | When this fact was superseded (NULL = active) |
| `invalidated_at` | TIMESTAMP | When explicitly invalidated |
| `is_stale` | BOOLEAN | Marked stale by memify |
| `last_confirmed_at` | TIMESTAMP | Last NOOP confirmation |
| `times_retrieved` | INT | Retrieval counter |
| `last_retrieved_at` | TIMESTAMP | Last retrieval time |
| `source_context` | VARCHAR | Origin marker (e.g., `inferred_from_relation` for mirror facts) |
| `cluster_id` | UUID | FK to cluster |
| `created_at` | TIMESTAMP | Row creation time |

### memory_fact_entity_links

Cross-entity links. Each fact is linked to ALL entities it mentions, not just its primary subject.

| Column | Type | Description |
|--------|------|-------------|
| `id` | UUID | Primary key |
| `fact_id` | UUID | FK to memory_facts (CASCADE delete) |
| `entity_key` | VARCHAR | Entity this fact is linked to |
| `is_primary` | BOOLEAN | True if this is the fact's primary subject |
| `agent_id` | TEXT | Agent identifier |

**Unique constraint:** `(fact_id, entity_key)` - one link per fact-entity pair.

**Indexes:** `(agent_id, entity_key)` for retrieval queries, `(fact_id)` for cascade operations.

### memory_entities

Canonical entity records. Created during entity resolution.

| Column | Type | Description |
|--------|------|-------------|
| `id` | UUID | Primary key |
| `agent_id` | TEXT | Agent identifier |
| `canonical_key` | VARCHAR | Unique key (e.g., `person:carlos`) |
| `display_name` | VARCHAR | Human-readable name |
| `entity_type` | VARCHAR | Free-form type string |
| `embedding_vec` | VECTOR | Entity name embedding |
| `summary_text` | TEXT | Resumo gerado por LLM (via background jobs) |
| `profile_text` | TEXT | Perfil vivo da entidade (v0.9.0+). Atualizado pelo write pipeline durante a extração informada. Resumo conciso (~100-300 tokens) usado como contexto para extrações subsequentes e injetado no contexto de retrieval |
| `profile_refreshed_at` | TIMESTAMPTZ | Quando `profile_text` foi atualizado pela última vez |
| `importance_score` | FLOAT | Importância computada 0.0 - 1.0 |
| `fact_count` | INT | Número de fatos linkados |
| `is_active` | BOOLEAN | Se a entidade está ativa |

### memory_entity_aliases

Cache de aliases para entity resolution por exact-match.

| Column | Type | Description |
|--------|------|-------------|
| `id` | UUID | Primary key |
| `agent_id` | TEXT | Agent identifier |
| `alias` | VARCHAR | Normalized alias text |
| `canonical_entity_key` | VARCHAR | Resolved entity key |
| `canonical_entity_type` | VARCHAR | Entity type |

**Unique constraint:** `(agent_id, alias)` - first-write-wins semantics.

### memory_entity_relationships

Knowledge graph edges between entities.

| Column | Type | Description |
|--------|------|-------------|
| `id` | UUID | Primary key |
| `agent_id` | TEXT | Agent identifier |
| `source_entity_key` | VARCHAR | Source entity |
| `target_entity_key` | VARCHAR | Target entity |
| `rel_type` | VARCHAR | Relationship type (snake_case, free-form) |
| `strength` | FLOAT | 0.0 - 1.0, reinforced on repetition |
| `evidence_fact_id` | UUID | FK to the fact supporting this relationship |
| `valid_from` | TIMESTAMP | When created |
| `valid_to` | TIMESTAMP | When invalidated (NULL = active) |
| `invalidated_at` | TIMESTAMP | Cascade invalidation timestamp |

**Unique constraint:** `(agent_id, source_entity_key, target_entity_key, rel_type)`.

!!! warning "Relationships are unidirectional"
    `ana → works_at → acme` does NOT create `acme → employs → ana`. Graph retrieval traverses both directions, but the edge itself is one-way.

---

## Supporting Tables

### memory_clusters

Semantic fact clusters (created by background jobs).

| Column | Type | Description |
|--------|------|-------------|
| `id` | UUID | Primary key |
| `agent_id` | TEXT | Agent identifier |
| `entity_type` | VARCHAR | Cluster entity type |
| `entity_key` | VARCHAR | Cluster entity key |
| `summary` | TEXT | LLM-generated cluster summary |
| `embedding` | VECTOR | Cluster embedding |
| `created_at` | TIMESTAMP | Creation time |

### memory_meta_observations

Higher-order patterns detected by consolidation.

| Column | Type | Description |
|--------|------|-------------|
| `id` | UUID | Primary key |
| `agent_id` | TEXT | Agent identifier |
| `observation_type` | VARCHAR | Type: insight, pattern, contradiction, trend, entity_community |
| `content` | TEXT | Observation text |
| `supporting_fact_ids` | JSONB | Array of fact IDs supporting this observation |
| `is_active` | BOOLEAN | Whether still relevant |
| `created_at` | TIMESTAMP | Creation time |

### memory_attribute_registry

Tracks known attribute keys per user.

| Column | Type | Description |
|--------|------|-------------|
| `id` | UUID | Primary key |
| `agent_id` | TEXT | Agent identifier |
| `attribute_key` | VARCHAR | Attribute key |
| `first_seen_at` | TIMESTAMP | When first used |

### memory_intentions

User intentions detected from events (experimental).

| Column | Type | Description |
|--------|------|-------------|
| `id` | UUID | Primary key |
| `agent_id` | TEXT | Agent identifier |
| `intention` | TEXT | Detected intention |
| `source_event_id` | UUID | Source event |
| `confidence` | FLOAT | Detection confidence |
| `created_at` | TIMESTAMP | Creation time |
# Utilitarios de Banco de Dados

O modulo `arandu.db` fornece funcoes de baixo nivel para configuracao do banco de dados. Sao usadas internamente pelo `MemoryClient`, mas estao disponiveis para casos de uso avancados onde voce precisa de controle direto sobre o engine e o ciclo de vida das sessoes.

```python
from arandu.db import create_engine, create_session_factory, init_db
```

---

## create_engine

Cria um engine async do SQLAlchemy a partir de uma string de conexao.

Converte automaticamente `postgresql://` para `postgresql+psycopg://` se o prefixo do driver async estiver ausente.

```python
def create_engine(database_url: str) -> AsyncEngine
```

| Parametro | Tipo | Descricao |
|-----------|------|-----------|
| `database_url` | `str` | String de conexao PostgreSQL. |

**Retorna:** Instancia de `AsyncEngine`.

```python
from arandu.db import create_engine

engine = create_engine("postgresql://user:pass@localhost:5432/mydb")
# Internamente se torna: postgresql+psycopg://user:pass@localhost:5432/mydb
```

---

## create_session_factory

Cria uma fabrica de sessoes async vinculada ao engine fornecido.

```python
def create_session_factory(engine: AsyncEngine) -> async_sessionmaker[AsyncSession]
```

| Parametro | Tipo | Descricao |
|-----------|------|-----------|
| `engine` | `AsyncEngine` | O engine async para vincular as sessoes. |

**Retorna:** `async_sessionmaker[AsyncSession]` com `expire_on_commit=False`.

```python
from arandu.db import create_engine, create_session_factory

engine = create_engine("postgresql://user:pass@localhost:5432/mydb")
SessionFactory = create_session_factory(engine)

async with SessionFactory() as session:
    # Use a sessao para queries
    ...
```

---

## init_db

Cria todas as tabelas de memoria no banco de dados do consumidor.

Usa `Base.metadata.create_all` -- seguro para chamar multiplas vezes (cria apenas tabelas que ainda nao existem). Garante que todas as classes de modelo SQLAlchemy estejam registradas antes de criar as tabelas.

```python
async def init_db(engine: AsyncEngine) -> None
```

| Parametro | Tipo | Descricao |
|-----------|------|-----------|
| `engine` | `AsyncEngine` | O engine async para criar as tabelas. |

```python
from arandu.db import create_engine, init_db

engine = create_engine("postgresql://user:pass@localhost:5432/mydb")
await init_db(engine)
```

---

## Schema do Banco de Dados

O SDK define seus modelos SQLAlchemy em `arandu.models`. As tabelas principais incluem:

| Tabela | Descricao |
|--------|-----------|
| `memory_events` | Registros de eventos imutaveis (mensagens do usuario com embeddings). |
| `memory_facts` | Fatos extraidos com triplas entidade/atributo/valor e embeddings. |
| `memory_entities` | Registro de entidades (pessoas, lugares, pets, etc.). |
| `memory_entity_aliases` | Aliases para resolucao de entidades. |
| `memory_entity_relationships` | Arestas do knowledge graph entre entidades. |
| `memory_clusters` | Clusters semanticos de fatos relacionados. |
| `memory_meta_observations` | Padroes detectados, insights e preferencias comportamentais. |
| `memory_attribute_registry` | Registro de chaves de atributo customizadas por usuario. |
| `session_observations` | Observacoes de nivel L1 de sessao do observer. |

Todas as tabelas usam chaves primarias UUID e incluem `agent_id` para isolamento multi-tenant. As tabelas `memory_facts` e `memory_events` possuem colunas de embedding `pgvector` para busca semantica.

!!! info "Gerenciamento de Schema"
    Para deploys em producao, considere usar migracoes Alembic em vez de `init_db()`. A funcao `init_db()` e conveniente para desenvolvimento e testes, mas nao lida com migracoes de schema para tabelas existentes.
# Referencia de Tipos de Dados

Esta pagina documenta todas as dataclasses, enums e tipos de resultado usados nos pipelines de escrita, leitura e jobs de background que nao sao cobertos na [Referencia da API](../reference/index.md) principal.

---

## Tipos do Write Pipeline

### InputType

```python
class InputType(str, Enum)
```

Tipos de classificacao de texto de entrada, determinados por heuristicas em `classify_input()`.

| Valor | Descricao |
|-------|-----------|
| `SHORT` | Menos de 500 caracteres. |
| `MEDIUM` | 500-2000 caracteres, nao estruturado. |
| `LONG` | Mais de 2000 caracteres, nao estruturado. |
| `STRUCTURED` | Mais de 500 caracteres com headers, bullets ou tabelas. |

### ExtractionMode

```python
class ExtractionMode(str, Enum)
```

| Valor | Descricao |
|-------|-----------|
| `SINGLE_SHOT` | Chamada unica de LLM para extracao. |
| `CHUNKED` | Entrada e dividida em chunks, cada um processado separadamente. |

### InputClassification

```python
@dataclass
class InputClassification
```

Resultado de `classify_input()`. Veja [API do Write Pipeline](write-api.md#inputclassification) para referencia completa dos campos.

### ExtractionStrategy

```python
@dataclass
class ExtractionStrategy
```

Resultado de `select_strategy()`. Veja [API do Write Pipeline](write-api.md#extractionstrategy) para referencia completa dos campos.

### CorrectionResult

```python
@dataclass
class CorrectionResult
```

Resultado da deteccao de correcoes.

| Campo | Tipo | Padrao | Descricao |
|-------|------|--------|-----------|
| `corrections_detected` | `int` | `0` | Numero de correcoes encontradas. |
| `corrected_keys` | `list[str]` | `[]` | Chaves de atributo que foram corrigidas. |
| `facts_corrected_ids` | `list[str]` | `[]` | IDs dos fatos antigos que foram corrigidos. |

---

## Tipos do Read Pipeline

### ExpandedQuery

```python
@dataclass
class ExpandedQuery
```

Resultado da expansao de query (entity priming).

| Campo | Tipo | Descricao |
|-------|------|-----------|
| `primed_entities` | `list[str]` | Entity keys descobertas via alias + KG priming. |
| `temporal_range` | `tuple[datetime, datetime] | None` | Faixa de datas resolvida. |
| `expanded_terms` | `list[str]` | Termos de contexto adicionais dos fatos das entidades. |

### PatternQuery

```python
@dataclass
class PatternQuery
```

Uma query baseada em padrao para matching de sinal de keyword.

| Campo | Tipo | Padrao | Descricao |
|-------|------|--------|-----------|
| `entity_pattern` | `str` | -- | Padrao SQL LIKE para matching de entity_key. |
| `attribute_filter` | `str | None` | `None` | Filtro opcional de chave de atributo. |

### RetrievalPlan

```python
@dataclass
class RetrievalPlan
```

Saida do retrieval agent (planner deterministico). Veja [API do Read Pipeline](read-api.md#retrievalplan) para referencia completa dos campos.

### GraphRetrievalResult

```python
@dataclass
class GraphRetrievalResult
```

Resultado do retrieval baseado em grafo BFS de 2 saltos.

| Campo | Tipo | Padrao | Descricao |
|-------|------|--------|-----------|
| `facts` | `list[dict[str, Any]]` | `[]` | Dicts de fatos pontuados com `source="graph"`. |
| `neighbor_keys` | `list[str]` | `[]` | Entity keys descobertas via BFS. |
| `edges_traversed` | `int` | `0` | Total de arestas examinadas durante BFS. |
| `edges` | `list[dict[str, Any]]` | `[]` | Dicts de arestas deduplicadas com nomes de exibicao. |

### SpreadingActivationResult

```python
@dataclass
class SpreadingActivationResult
```

Resultado da expansao por spreading activation.

| Campo | Tipo | Padrao | Descricao |
|-------|------|--------|-----------|
| `candidates` | `list[RetrievalCandidate]` | `[]` | Candidatos expandidos dos saltos 1-2. |
| `meta_observations` | `list[Any]` | `[]` | Meta-observacoes relevantes referenciando fatos semente. |
| `entities_explored` | `list[str]` | `[]` | Entity keys exploradas durante spreading. |
| `clusters_explored` | `list[str]` | `[]` | Cluster IDs explorados durante spreading. |
| `hop1_count` | `int` | `0` | Numero de fatos encontrados no salto 1. |
| `hop2_count` | `int` | `0` | Numero de fatos encontrados no salto 2. |
| `kg_relationships_explored` | `int` | `0` | Numero de relacionamentos KG percorridos. |

### CompressedContext

```python
@dataclass
class CompressedContext
```

Resultado da compressao de contexto (camadas hot/warm/cold).

| Campo | Tipo | Padrao | Descricao |
|-------|------|--------|-----------|
| `context_text` | `str` | `""` | String de contexto final pronta para prompt. |
| `hot_count` | `int` | `0` | Numero de fatos na camada hot (Tier 1). |
| `warm_count` | `int` | `0` | Numero de fatos na camada warm (Tier 2). |
| `cold_count` | `int` | `0` | Numero de itens na camada cold (Tier 3). |
| `total_tokens` | `int` | `0` | Contagem estimada de tokens do context_text. |

### EmotionalTrendsResult

```python
@dataclass
class EmotionalTrendsResult
```

Resultado da materializacao de tendencias emocionais.

| Campo | Tipo | Padrao | Descricao |
|-------|------|--------|-----------|
| `emotion_counts` | `dict[str, int]` | `{}` | Mapeamento de emocao para contagem de ocorrencias. |
| `trend_direction` | `str` | `"stable"` | `"increasing"`, `"decreasing"` ou `"stable"`. |
| `dominant_emotion` | `str | None` | `None` | Emocao mais frequente. |
| `trigger_keywords` | `list[str]` | `[]` | Top keywords de eventos de alta intensidade. |
| `avg_intensity` | `float` | `0.0` | Intensidade emocional media. |
| `dominant_intensity` | `float` | `0.0` | Intensidade media da emocao dominante. |
| `dominant_energy` | `str` | `"medium"` | Nivel de energia predominante. |
| `events_analyzed` | `int` | `0` | Numero de eventos analisados. |
| `observation_created` | `bool` | `False` | Se uma meta-observacao foi criada/atualizada. |
| `observation_id` | `str | None` | `None` | ID da observacao criada/atualizada. |

### DirectiveBlock

```python
@dataclass
class DirectiveBlock
```

Resultado da geracao de diretivas (memoria procedimental).

| Campo | Tipo | Padrao | Descricao |
|-------|------|--------|-----------|
| `text` | `str` | `""` | Bloco de instrucoes comportamentais coeso. |
| `directive_count` | `int` | `0` | Numero de diretivas ativas usadas. |
| `cache_hit` | `bool` | `False` | Se foi servido do cache. |

### ContradictionResult

```python
@dataclass
class ContradictionResult
```

Resultado da verificacao de contradicao entre diretivas.

| Campo | Tipo | Padrao | Descricao |
|-------|------|--------|-----------|
| `has_contradiction` | `bool` | `False` | Se uma contradicao foi encontrada. |
| `conflicting_directive` | `str | None` | `None` | Titulo da diretiva conflitante. |
| `resolution` | `str | None` | `None` | Explicacao da resolucao. |

---

## Tipos de Resultado de Background Jobs

### ClusteringResult

```python
@dataclass
class ClusteringResult
```

Resultado do clustering de fatos.

| Campo | Tipo | Padrao | Descricao |
|-------|------|--------|-----------|
| `clusters_created` | `int` | `0` | Numero de novos clusters criados. |
| `clusters_reinforced` | `int` | `0` | Numero de clusters existentes atualizados. |
| `summaries_generated` | `int` | `0` | Numero de sumarios de cluster gerados via LLM. |
| `facts_assigned` | `int` | `0` | Numero de fatos atribuidos a clusters. |

### CommunityDetectionResult

```python
@dataclass
class CommunityDetectionResult
```

Resultado da deteccao de comunidades cross-entity.

| Campo | Tipo | Padrao | Descricao |
|-------|------|--------|-----------|
| `communities_created` | `int` | `0` | Novas observacoes de comunidade criadas. |
| `communities_reinforced` | `int` | `0` | Observacoes de comunidade existentes reforcadas. |
| `clusters_in_communities` | `int` | `0` | Total de clusters atribuidos a comunidades. |
| `skipped` | `bool` | `False` | Se a deteccao foi pulada. |
| `skip_reason` | `str | None` | `None` | Motivo de ter sido pulada. |

### ConsolidationResult

```python
@dataclass
class ConsolidationResult
```

Resultado da consolidacao L2/L3.

| Campo | Tipo | Padrao | Descricao |
|-------|------|--------|-----------|
| `events_processed` | `int` | `0` | Numero de eventos analisados. |
| `observations_created` | `int` | `0` | Novas meta-observacoes criadas. |
| `observations_reinforced` | `int` | `0` | Observacoes existentes reforcadas. |
| `skipped` | `bool` | `False` | Se a consolidacao foi pulada. |
| `skip_reason` | `str | None` | `None` | Motivo de ter sido pulada. |

### MemifyResult

```python
@dataclass
class MemifyResult
```

Resultado do pipeline memify (scoring de vitalidade, marcacao de obsolescencia, gerenciamento de arestas).

| Campo | Tipo | Padrao | Descricao |
|-------|------|--------|-----------|
| `facts_scored` | `int` | `0` | Numero de fatos pontuados por vitalidade. |
| `facts_marked_stale` | `int` | `0` | Numero de fatos marcados como obsoletos. |
| `edges_reinforced` | `int` | `0` | Numero de arestas KG reforcadas. |
| `merges_executed` | `int` | `0` | Numero de merges de entidade executados. |

### EntityImportanceResult

```python
@dataclass
class EntityImportanceResult
```

Resultado do scoring de importancia de entidades (sleep-time compute).

| Campo | Tipo | Padrao | Descricao |
|-------|------|--------|-----------|
| `entities_scored` | `int` | `0` | Numero de entidades pontuadas. |
| `top_entities` | `list[tuple[str, float]]` | `[]` | Top entidades por score (pares chave, score). |

### SummaryRefreshResult

```python
@dataclass
class SummaryRefreshResult
```

Resultado da atualizacao de sumarios de entidades (sleep-time compute).

| Campo | Tipo | Padrao | Descricao |
|-------|------|--------|-----------|
| `summaries_refreshed` | `int` | `0` | Numero de sumarios gerados. |
| `summaries_skipped` | `int` | `0` | Numero de entidades puladas. |

---

## Funcoes de Background

### tag_event_emotion

Infere emocao, intensidade e energia a partir de texto de evento via LLM.

```python
async def tag_event_emotion(
    event_text: str,
    llm: LLMProvider,
) -> dict[str, Any] | None
```

| Parametro | Tipo | Descricao |
|-----------|------|-----------|
| `event_text` | `str` | Texto para analisar. |
| `llm` | `LLMProvider` | Provider de LLM injetado. |

**Retorna:** Dict com chaves `emotion`, `intensity`, `energy`, ou `None` em caso de falha.

```python
from arandu.background import tag_event_emotion

result = await tag_event_emotion("Estou muito feliz hoje!", llm)
# {"emotion": "joy", "intensity": 0.85, "energy": "high"}
```

---

## Modelos de Banco de Dados

Os modelos SQLAlchemy abaixo definem a camada de persistencia. Eles vivem em `arandu.models` e sao uteis para queries avancadas executadas diretamente contra o banco de dados.

```mermaid
erDiagram
    MemoryEvent ||--o{ MemoryFact : "source_event_id"
    MemoryFact ||--o{ MemoryFactEntityLink : "fact_id"
    MemoryFact ||--o| MemoryCluster : "cluster_id"
    MemoryFact ||--o| MemoryFact : "supersedes_fact_id"
    MemoryEntity ||--o{ MemoryEntityAlias : "canonical_entity_key"
    MemoryEntity ||--o{ MemoryFactEntityLink : "entity_key"
    MemoryEntity ||--o{ MemoryEntityRelationship : "source/target"
    MemoryEntityRelationship ||--o| MemoryFact : "evidence_fact_id"
    MemoryFact }o--o| MemoryAttributeRegistry : "attribute_key"
    MemoryIntention }o--|| MemoryEvent : "agent_id"
    SessionObservation }o--|| MemoryEvent : "agent_id"

    MemoryEvent {
        UUID id PK
        Text agent_id
        DateTime occurred_at
        Text text
        Vector embedding_vec
    }
    MemoryFact {
        UUID id PK
        Text agent_id
        String entity_key
        Text fact_text
        Float confidence
        DateTime valid_from
        DateTime valid_to
        Vector embedding_vec
    }
    MemoryEntity {
        UUID id PK
        Text agent_id
        String canonical_key UK
        String display_name
        String entity_type
        Text summary_text
        Float importance_score
    }
    MemoryFactEntityLink {
        UUID id PK
        UUID fact_id FK
        String entity_key
        Boolean is_primary
        Text agent_id
    }
    MemoryEntityRelationship {
        UUID id PK
        Text agent_id
        String source_entity_key
        String target_entity_key
        String rel_type
        Float strength
        UUID evidence_fact_id FK
    }
    MemoryEntityAlias {
        UUID id PK
        Text agent_id
        String alias UK
        String canonical_entity_key
    }
    MemoryCluster {
        UUID id PK
        Text agent_id
        String label
        Text summary_text
        Vector embedding_vec
    }
    MemoryMetaObservation {
        UUID id PK
        Text agent_id
        String observation_type
        Text text
        Float confidence
    }
    MemoryAttributeRegistry {
        UUID id PK
        String key UK
        String status
        String value_type
        Integer seen_count
    }
    MemoryIntention {
        UUID id PK
        Text agent_id
        String trigger_type
        Text trigger_condition
        Text intended_action
        String status
    }
    SessionObservation {
        UUID id PK
        Text agent_id
        Text content
        String topic
        Boolean is_active
    }
```

### MemoryFact

Ledger de fatos versionados - armazena fatos estruturados com janelas de validade.

| Coluna | Tipo | Descricao |
|--------|------|-----------|
| `id` | `UUID` | Chave primaria. |
| `agent_id` | `Text` | ID do agente proprietario (qualquer string: UUID, slug, numerico, etc.). |
| `entity_type` | `String` | Tipo da entidade (ex: `"person"`). |
| `entity_key` | `String` | Chave canonica da entidade (ex: `"person:ana"`). |
| `entity_name` | `String?` | Nome legivel da entidade. |
| `attribute_key` | `String?` | Chave do atributo (ex: `"occupation"`). |
| `fact_text` | `Text` | Fato em linguagem natural. |
| `category` | `String(50)?` | Categoria do fato. |
| `confidence` | `Float` | Score de confianca (padrao 0.8). |
| `importance` | `Float` | Score de importancia (padrao 0.5). |
| `is_sensitive` | `Boolean` | Se o fato contem dados sensiveis. |
| `valid_from` | `DateTime` | Inicio da janela de validade. |
| `valid_to` | `DateTime?` | Fim da validade (`NULL` = ativo atualmente). |
| `ttl_days` | `Integer?` | Time-to-live opcional em dias. |
| `source_event_id` | `UUID?` | FK para `MemoryEvent`. |
| `supersedes_fact_id` | `UUID?` | ID do fato que este substitui. |
| `embedding_vec` | `Vector(1536)` | Embedding pgvector para busca semantica. |
| `vitality_score` | `Float?` | Score de vitalidade do sleep-time. |
| `is_stale` | `Boolean` | Se foi marcado como obsoleto pelo memify. |
| `cluster_id` | `UUID?` | FK para `MemoryCluster`. |
| `value_json` | `JSONB?` | Valor estruturado (JSON) para o atributo. |
| `needs_confirmation` | `Boolean` | Se o fato requer confirmacao do usuario. |
| `last_confirmed_at` | `DateTime?` | Quando o fato foi confirmado pela ultima vez pelo usuario. |
| `times_retrieved` | `Integer` | Quantas vezes este fato foi recuperado. |
| `last_retrieved_at` | `DateTime?` | Quando o fato foi recuperado pela ultima vez. |
| `user_correction_count` | `Integer` | Numero de vezes que o usuario corrigiu este fato. |
| `source_context` | `String(512)?` | Contexto da fonte da entrada original. |
| `agent_annotation` | `Text?` | Anotacao de texto livre adicionada pelo agente. |
| `embedding` | `JSONB?` | Embedding bruto como JSON (fallback nao-pgvector). |
| `search_vector` | `TSVECTOR` | Coluna de indice de busca full-text. |
| `created_at` | `DateTime` | Timestamp de criacao do registro. |
| `ingested_at` | `DateTime` | Timestamp de ingestao bi-temporal. |
| `invalidated_at` | `DateTime?` | Quando o fato foi invalidado (bi-temporal). |

### MemoryEntity

No de entidade de primeira classe no knowledge graph.

| Coluna | Tipo | Descricao |
|--------|------|-----------|
| `id` | `UUID` | Chave primaria. |
| `agent_id` | `Text` | ID do agente proprietario (qualquer string: UUID, slug, numerico, etc.). |
| `canonical_key` | `String(128)` | Chave canonica unica (ex: `"person:ana"`). |
| `display_name` | `String(256)?` | Nome de exibicao legivel. |
| `entity_type` | `String(32)` | Tipo (`"person"`, `"organization"`, etc.). |
| `summary_text` | `Text?` | Sumario da entidade gerado por LLM. |
| `embedding_vec` | `Vector(1536)` | Embedding da entidade. |
| `fact_count` | `Integer` | Numero de fatos vinculados. |
| `importance_score` | `Float?` | Score de importancia do sleep-time. |
| `is_active` | `Boolean` | Se a entidade esta ativa. |
| `first_seen_at` | `DateTime` | Quando a entidade foi observada pela primeira vez. |
| `last_seen_at` | `DateTime` | Quando a entidade foi observada pela ultima vez. |
| `summary_refreshed_at` | `DateTime?` | Quando o sumario da entidade foi atualizado pela ultima vez. |
| `created_at` | `DateTime` | Timestamp de criacao do registro. |

Constraint unica: `(agent_id, canonical_key)`.

### MemoryEntityAlias

Mapeia nomes de alias para chaves canonicas de entidade para resolucao de entidades.

| Coluna | Tipo | Descricao |
|--------|------|-----------|
| `id` | `UUID` | Chave primaria. |
| `agent_id` | `Text` | ID do agente proprietario (qualquer string: UUID, slug, numerico, etc.). |
| `alias` | `String` | Texto do alias (ex: `"Ana"`). |
| `canonical_entity_key` | `String` | Chave canonica de destino. |
| `canonical_entity_type` | `String` | Tipo da entidade de destino. |
| `created_at` | `DateTime` | Timestamp de criacao do registro. |

Constraint unica: `(agent_id, alias)`.

### MemoryEntityRelationship

Aresta direcionada entre duas entidades no knowledge graph.

| Coluna | Tipo | Descricao |
|--------|------|-----------|
| `id` | `UUID` | Chave primaria. |
| `agent_id` | `Text` | ID do agente proprietario (qualquer string: UUID, slug, numerico, etc.). |
| `source_entity_key` | `String(128)` | Chave canonica da entidade de origem. |
| `target_entity_key` | `String(128)` | Chave canonica da entidade de destino. |
| `rel_type` | `String(64)` | Tipo de relacionamento (ex: `"works_at"`, `"mentored_by"`). |
| `strength` | `Float` | Forca da aresta (padrao 0.8). |
| `evidence_fact_id` | `UUID?` | FK para o fato que evidencia esta aresta. |
| `provenance` | `String(16)` | Como a aresta foi criada (`"rule"`, `"llm"`). |
| `valid_from` | `DateTime` | Inicio da validade. |
| `valid_to` | `DateTime?` | Fim da validade (`NULL` = ativa). |
| `created_at` | `DateTime` | Timestamp de criacao do registro. |
| `updated_at` | `DateTime` | Timestamp da ultima atualizacao. |
| `last_used_at` | `DateTime?` | Quando o relacionamento foi usado pela ultima vez em retrieval. |
| `invalidated_at` | `DateTime?` | Quando o relacionamento foi invalidado. |

Constraint unica: `(agent_id, source_entity_key, target_entity_key, rel_type)`.

#### Tipos de Relacionamento Dinamicos

O campo `rel_type` aceita **qualquer** string curta e descritiva em `snake_case` - nao e restrito a um conjunto fixo. O pipeline de extracao instrui o LLM a escolher o tipo mais descritivo para cada relacionamento.

Tipos comuns (usados como exemplos no prompt de extracao, nao como restricoes):

`works_at`, `manages`, `reports_to`, `family_of`, `friend_of`, `partner_of`, `owns`, `lives_in`, `member_of`, `studies_at`, `works_with`

O LLM tambem pode produzir tipos como `mentored_by`, `inspired_by`, `competed_with`, ou qualquer outro tipo descritivo.

**Normalizacao**: Todos os tipos de relacionamento sao normalizados via `normalize_rel_type()` antes da persistencia:

- Lowercase + underscores (ex: `"Mentored By"` → `"mentored_by"`)
- Aliases conhecidos sao mapeados para tipos comuns (ex: `"boss"` → `"reports_to"`, `"spouse"` → `"partner_of"`)
- Tipos desconhecidos passam apos sanitizacao

O set `CANONICAL_REL_TYPES` em `arandu.constants` esta disponivel como **referencia** para consumers que queiram filtrar por tipos conhecidos, mas nao e usado como filtro de validacao.

Veja [Vinculação de Evidência e Cascata de Invalidação](../concepts/write-pipeline.md#vinculacao-de-evidencia-e-cascata-de-invalidacao) para como relacionamentos são vinculados a fatos e automaticamente limpos quando fatos mudam.

### MemoryEvent

Log imutavel de eventos - armazena todas as mensagens do usuario com embeddings.

| Coluna | Tipo | Descricao |
|--------|------|-----------|
| `id` | `UUID` | Chave primaria. |
| `agent_id` | `Text` | ID do agente proprietario (qualquer string: UUID, slug, numerico, etc.). |
| `occurred_at` | `DateTime` | Quando o evento aconteceu. |
| `text` | `Text` | Conteudo textual do evento. |
| `source` | `String` | Origem (padrao `"api"`). |
| `importance` | `Float` | Score de importancia (padrao 0.5). |
| `embedding_vec` | `Vector(1536)` | Embedding do evento para retrieval. |
| `embedding` | `JSONB?` | Embedding bruto como JSON (fallback nao-pgvector). |
| `trace_json` | `JSONB?` | Metadados de trace/debug do evento. |
| `created_at` | `DateTime` | Timestamp de criacao do registro. |
| `emotion_primary` | `String(32)?` | Label de emocao primaria. |
| `emotion_intensity` | `Float?` | Intensidade da emocao (0-1). |
| `energy_level` | `String(16)?` | Nivel de energia (`"low"`, `"medium"`, `"high"`). |

### MemoryCluster

Cluster semantico agrupando fatos relacionados para contexto mais rico.

| Coluna | Tipo | Descricao |
|--------|------|-----------|
| `id` | `UUID` | Chave primaria. |
| `agent_id` | `Text` | ID do agente proprietario (qualquer string: UUID, slug, numerico, etc.). |
| `label` | `String(128)` | Label do cluster. |
| `summary_text` | `Text?` | Sumario do cluster gerado por LLM. |
| `cluster_type` | `String(32)` | Tipo do cluster (padrao `"auto"`). |
| `fact_count` | `Integer` | Numero de fatos no cluster. |
| `importance` | `Float` | Importancia do cluster (padrao 0.5). |
| `embedding_vec` | `Vector(1536)` | Embedding do cluster. |
| `is_active` | `Boolean` | Se o cluster esta ativo. |
| `last_updated_at` | `DateTime` | Quando o cluster foi atualizado pela ultima vez. |
| `created_at` | `DateTime` | Timestamp de criacao do registro. |

### MemoryMetaObservation

Meta-observacoes derivadas da consolidacao - padroes, insights, tendencias.

| Coluna | Tipo | Descricao |
|--------|------|-----------|
| `id` | `UUID` | Chave primaria. |
| `agent_id` | `Text` | ID do agente proprietario (qualquer string: UUID, slug, numerico, etc.). |
| `observation_type` | `String(32)` | Tipo (`"pattern"`, `"trend"`, `"community"`, etc.). |
| `title` | `String(256)` | Titulo curto. |
| `text` | `Text` | Texto completo da observacao. |
| `supporting_event_ids` | `JSONB` | Lista de UUIDs de eventos de suporte. |
| `supporting_fact_ids` | `JSONB` | Lista de UUIDs de fatos de suporte. |
| `confidence` | `Float` | Confianca (padrao 0.7). |
| `importance` | `Float` | Importancia (padrao 0.5). |
| `times_reinforced` | `Integer` | Quantas vezes esta observacao foi reforcada. |
| `is_active` | `Boolean` | Se a observacao esta ativa. |
| `embedding_vec` | `Vector(1536)` | Embedding da observacao. |
| `first_detected_at` | `DateTime` | Quando a observacao foi detectada pela primeira vez. |
| `last_reinforced_at` | `DateTime` | Quando a observacao foi reforcada pela ultima vez. |
| `created_at` | `DateTime` | Timestamp de criacao do registro. |

### MemoryFactEntityLink

Tabela de referencia cruzada linkando cada fato a TODAS as entidades que ele menciona - habilita retrieval cross-entity sem duplicacao de fatos.

| Coluna | Tipo | Descricao |
|--------|------|-----------|
| `id` | `UUID` | Chave primaria. |
| `fact_id` | `UUID` | FK para `MemoryFact`. |
| `entity_key` | `String` | Chave canonica da entidade (ex: `"person:clara_rezende"`). |
| `is_primary` | `Boolean` | Se esta entidade e o subject primario do fato. |
| `agent_id` | `Text` | ID do usuario. |

Constraint unico: `(fact_id, entity_key)`.

Indices: `(agent_id, entity_key)` para retrieval, `(fact_id)` para cascade deletes.

### MemoryAttributeRegistry

Registro para gerenciamento de chaves de atributo - rastreia chaves propostas vs ativas.

| Coluna | Tipo | Descricao |
|--------|------|-----------|
| `id` | `UUID` | Chave primaria. |
| `key` | `String(64)` | Chave de atributo unica. |
| `status` | `String(20)` | `"proposed"` ou `"active"`. |
| `value_type` | `String(20)` | Tipo de valor esperado (padrao `"string"`). |
| `conflict_policy` | `String(20)` | Como lidar com conflitos (padrao `"supersede"`). |
| `ttl_days` | `Integer?` | TTL padrao opcional para fatos com esta chave. |
| `seen_count` | `Integer` | Quantas vezes esta chave foi vista. |
| `proposed_by` | `String(20)` | Quem propos a chave (`"llm"`, `"user"`). |
| `reason` | `Text?` | Por que a chave foi proposta. |
| `first_seen_at` | `DateTime` | Quando a chave de atributo foi vista pela primeira vez. |
| `last_seen_at` | `DateTime` | Quando a chave de atributo foi vista pela ultima vez. |
| `example_raw_key` | `String(128)?` | Exemplo da chave bruta antes da normalizacao. |
| `created_at` | `DateTime` | Timestamp de criacao do registro. |
| `updated_at` | `DateTime?` | Timestamp da ultima atualizacao. |

### MemoryIntention

Memoria prospectiva -- intencoes futuras com triggers baseados em tempo ou evento.

| Coluna | Tipo | Descricao |
|--------|------|-----------|
| `id` | `UUID` | Chave primaria. |
| `agent_id` | `Text` | ID do agente proprietario (qualquer string: UUID, slug, numerico, etc.). |
| `trigger_type` | `String(16)` | Tipo de trigger (`"time"`, `"event"`, etc.). |
| `trigger_condition` | `Text` | Descricao em linguagem natural da condicao de trigger. |
| `intended_action` | `Text` | O que o agente deve fazer quando ativado. |
| `due_date` | `DateTime?` | Data limite opcional para triggers baseados em tempo. |
| `status` | `String(16)` | Status (`"pending"`, `"triggered"`, `"fulfilled"`, `"expired"`). Padrao `"pending"`. |
| `trigger_embedding_vec` | `Vector(1536)` | Embedding da condicao de trigger para matching semantico. |
| `source_context` | `String(32)?` | Identificador de contexto da fonte. |
| `outcome_note` | `Text?` | Nota sobre o resultado apos cumprimento. |
| `created_at` | `DateTime` | Timestamp de criacao do registro. |
| `triggered_at` | `DateTime?` | Quando a intencao foi ativada. |
| `fulfilled_at` | `DateTime?` | Quando a intencao foi cumprida. |

### SessionObservation

Observacoes de sessao persistentes criadas pelo Observer LLM -- captura contexto intra-sessao que persiste entre turnos.

| Coluna | Tipo | Descricao |
|--------|------|-----------|
| `id` | `UUID` | Chave primaria. |
| `agent_id` | `Text` | ID do agente proprietario (qualquer string: UUID, slug, numerico, etc.). |
| `content` | `Text` | Conteudo textual da observacao. |
| `topic` | `String(64)?` | Tag de topico para a observacao. |
| `entities_mentioned` | `JSONB` | Lista de entity keys mencionadas na observacao. |
| `created_at` | `DateTime` | Timestamp de criacao do registro. |
| `referenced_at` | `DateTime?` | Quando a observacao foi referenciada pela ultima vez. |
| `relative_offset` | `String(64)?` | Descritor de offset temporal relativo (ex: `"2 messages ago"`). |
| `source_message_ids` | `JSONB` | Lista de IDs de mensagens de origem desta observacao. |
| `is_active` | `Boolean` | Se a observacao esta ativa. |
| `merged_into_id` | `UUID?` | ID da observacao na qual esta foi mergeada. |
| `emotion_label` | `String(32)?` | Label de emocao detectada para o contexto de sessao. |
| `embedding_vec` | `Vector(1536)` | Embedding da observacao para retrieval semantico. |
# Configuration Reference

All tuning parameters for the Arandu SDK live in `MemoryConfig`. Every field has a sensible default - override only what you need.

```python
from arandu.config import MemoryConfig

config = MemoryConfig(
    topk_facts=10,
    enable_reranker=False,
    min_score=0.15,
)
```

You can also override per-request via `config_overrides`:

```python
result = await memory.retrieve(
    agent_id="user_123",
    query="...",
    config_overrides={"topk_facts": 5, "enable_reranker": False},
)
```

---

## Extraction

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `extraction_timeout_sec` | `float` | `30.0` | Timeout per LLM call during extraction. |

## Entity Resolution

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `fuzzy_threshold` | `float` | `0.85` | Cosine similarity threshold for direct fuzzy match. Above this → auto-resolve. Below 0.50 → new entity. Between → LLM decides. |
| `enable_llm_resolution` | `bool` | `True` | Whether to use LLM for ambiguous entity matches. When `False`, ambiguous cases create new entities instead. |

## Retrieval

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `topk_facts` | `int` | `20` | Maximum facts returned by `retrieve()`. |
| `topk_events` | `int` | `8` | Maximum events included in context. |
| `event_max_scan` | `int` | `200` | Maximum events scanned for relevance. |
| `min_similarity` | `float` | `0.20` | Minimum cosine similarity for semantic search candidates. |
| `min_confidence` | `float` | `0.55` | Minimum confidence for facts to be considered. |
| `min_score` | `float` | `0.15` | Minimum final score for facts to be included in results. Set higher (e.g., `0.20`) to filter low-relevance facts. |
| `recency_half_life_days` | `int` | `14` | Half-life for recency decay scoring. |

## Reranker

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `enable_reranker` | `bool` | `True` | Enable LLM-based reranking. When enabled, the reranker uses a multiplicative blend with the formula score - `score_weights` still matter for the base score. |
| `reranker_timeout_sec` | `float` | `5.0` | Timeout for the reranker LLM call. Falls back to original ranking on timeout. |
| `reranker_weight` | `float` | `0.70` | Weight of reranker score in the multiplicative blend: `final = formula × (floor + w × reranker)` where `floor = 1 - w`. Higher = more reranker influence. Lower = more formula score influence. |
| `min_reranker_score` | `float` | `0.10` | Minimum reranker score for a fact to survive. Facts below this threshold are eliminated (final_score = 0.0), giving the reranker veto power over irrelevant facts. Only applies when `enable_reranker=True`. Set `0.05` for more permissive, `0.20` for stricter. |

## Score Weights

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `score_weights` | `dict` | `{"semantic": 0.70, "recency": 0.20, "importance": 0.10}` | Weights for hybrid ranking formula. Always affects the base formula score, which the reranker blends with multiplicatively. |

## Confidence

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `confidence_level_map` | `dict` | `{explicit: 0.95, strong: 0.80, weak: 0.60, speculation: 0.40}` | Confidence scores assigned during extraction. |
| `confidence_default` | `float` | `0.60` | Default confidence when LLM doesn't specify. |

## Spreading Activation

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `spreading_activation_hops` | `int` | `2` | Maximum hops from seed facts. Set to `0` to disable. |
| `spreading_decay_factor` | `float` | `0.50` | Score decay per hop. Hop 1 = factor, Hop 2 = factor². |
| `spreading_max_related_entities` | `int` | `5` | Max KG-related entities explored per hop. |
| `spreading_facts_per_entity` | `int` | `3` | Max facts fetched per entity in spreading. |

## Context Compression

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `context_max_tokens` | `int` | `2000` | Proportional token budget for context compression. Not a hard cap. |
| `hot_tier_ratio` | `float` | `0.5` | Fraction of budget for top-scoring facts (full detail). |
| `warm_tier_ratio` | `float` | `0.3` | Fraction of budget for mid-scoring facts (summarized). |

## Emotional Trends

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `emotional_trend_window_days` | `int` | `30` | Lookback window for emotional trend detection. |
| `emotional_trend_min_events` | `int` | `5` | Minimum events to compute a trend. |

## Clustering

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `cluster_max_age_days` | `int` | `90` | Maximum age of facts included in clustering. |
| `cluster_min_facts` | `int` | `2` | Minimum facts per cluster. |
| `community_similarity_threshold` | `float` | `0.75` | Cosine similarity threshold for grouping clusters into communities. |
| `community_min_clusters` | `int` | `2` | Minimum clusters to form a community. |

## Consolidation

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `consolidation_min_events` | `int` | `3` | Minimum events before running consolidation. |
| `consolidation_lookback_days` | `int` | `7` | How far back to look for patterns. |

## Sleep-Time Compute

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `importance_recency_halflife_days` | `int` | `30` | Half-life for importance score recency signal. |
| `summary_refresh_interval_days` | `int` | `7` | Entity summaries older than this are marked stale. |

## Memify

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `vitality_stale_threshold` | `float` | `0.2` | Vitality score below which facts are considered stale. |
| `memify_merge_similarity_threshold` | `float` | `0.90` | Threshold for merging similar procedural memories. |

## Procedural Memory

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `directive_max_tokens` | `int` | `300` | Max tokens for procedural directive generation. |
| `directive_cache_ttl_minutes` | `int` | `30` | TTL for directive cache. |
| `contradiction_similarity_threshold` | `float` | `0.80` | Threshold for detecting contradictions. |

## Locale

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `timezone` | `str` | `"UTC"` | IANA timezone for temporal resolution in retrieval. |

## Open Catalog (Extensions)

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `extra_attribute_keys` | `set[str]` | `set()` | Additional attribute keys accepted by the system. |
| `attribute_aliases` | `dict[str, str]` | `{}` | Aliases for attribute keys. |
| `extra_namespaces` | `set[str]` | `set()` | Additional entity namespaces. |
| `extra_self_references` | `frozenset[str]` | `frozenset()` | Additional terms that resolve to `user:self`. |
| `extra_relationship_hints` | `frozenset[str]` | `frozenset()` | Additional relationship hint patterns. |

## Limits

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `max_facts_per_event` | `int` | `100` | Maximum facts extracted per message (safety limit). |
| `embedding_dimensions` | `int` | `1536` | Embedding vector dimensions (must match your provider). |
# Referência da API

Gerada automaticamente a partir dos docstrings do código-fonte. Para guias conceituais sobre como esses componentes funcionam juntos, veja a seção [Conceitos](../concepts/write-pipeline.md).

---

## Client

### MemoryClient

::: arandu.MemoryClient
    options:
      show_source: false
      heading_level: 4
      members_order: source

### WriteResult

::: arandu.WriteResult
    options:
      show_source: false
      heading_level: 4

### RetrieveResult

::: arandu.RetrieveResult
    options:
      show_source: false
      heading_level: 4

### FactDetail

::: arandu.FactDetail
    options:
      show_source: false
      heading_level: 4

### EntityDetail

::: arandu.EntityDetail
    options:
      show_source: false
      heading_level: 4

### ScoredFact

::: arandu.ScoredFact
    options:
      show_source: false
      heading_level: 4

### PipelineTrace

::: arandu.PipelineTrace
    options:
      show_source: false
      heading_level: 4

---

## Configuração

### MemoryConfig

::: arandu.MemoryConfig
    options:
      show_source: false
      heading_level: 4

---

## Protocolos

### LLMProvider

::: arandu.LLMProvider
    options:
      show_source: false
      heading_level: 4

### EmbeddingProvider

::: arandu.EmbeddingProvider
    options:
      show_source: false
      heading_level: 4

---

## Providers

### OpenAIProvider

::: arandu.providers.openai.OpenAIProvider
    options:
      show_source: false
      heading_level: 4

### AnthropicProvider

Provider LLM integrado para modelos Anthropic Claude. Implementa apenas `LLMProvider` - não fornece embeddings (use `OpenAIProvider` ou outro provider de embeddings junto).

```python
from arandu.providers.anthropic import AnthropicProvider

llm = AnthropicProvider(
    api_key="sk-ant-...",       # Chave de API Anthropic
    model="claude-sonnet-4-20250514",  # Modelo padrão
    timeout=30.0,               # Timeout da requisição em segundos
    max_tokens=4096,            # Máximo de tokens por resposta
)
```

Instalação: `pip install arandu[anthropic]`

**Comportamentos principais:**

- Mensagens de sistema são extraídas da lista de mensagens e passadas via parâmetro `system` da Anthropic
- `response_format={"type": "json_object"}` adiciona uma instrução JSON ao system prompt (Anthropic não suporta isso nativamente)
- Uso de tokens é rastreado via `LLMResult.usage`
- Code fences markdown são removidos automaticamente

---

## Exceções

Todas as exceções do SDK herdam de `MemoryError`. O SDK é **fail-safe por padrão** - a maioria dos erros é capturada internamente e resulta em degradação gradual (resultados vazios, warnings logados). Essas exceções são lançadas apenas quando explicitamente documentado.

```
MemoryError (base)
├── ExtractionError    — extração LLM falhou
├── ResolutionError    — resolução de entidades falhou
├── ReconciliationError — reconciliação de fatos falhou
├── RetrievalError     — recuperação de memória falhou
└── UpsertError        — persistência no banco falhou
```

### MemoryError

Exceção base para todos os erros do SDK arandu. Capture esta para tratamento genérico.

```python
from arandu import MemoryError

try:
    result = await memory.write(agent_id, message, speaker_name="Rafael")
except MemoryError as e:
    print(f"Erro do SDK: {e}")
    print(f"Causado por: {e.__cause__}")  # exceção original, se houver
```

### ExtractionError

Lançada quando a extração de fatos/entidades de uma mensagem falha (timeout do LLM, resposta JSON inválida, rate limit). Na prática, o write pipeline captura isso internamente e retorna um `WriteResult` vazio com `success=True` - o evento ainda é registrado.

### ResolutionError

Lançada quando a resolução de entidades falha (provider de embeddings fora do ar, timeout de desambiguação do LLM). O pipeline captura isso internamente - entidades não resolvidas são criadas como novas entidades.

### ReconciliationError

Lançada quando a reconciliação de fatos contra o conhecimento existente falha (chamada LLM falha na avaliação de similaridade). O pipeline captura isso internamente - o padrão é ADD (melhor ter um quase-duplicado do que perder informação).

### RetrievalError

Lançada quando a recuperação de memória falha (erro na busca semântica, timeout do reranker). O read pipeline captura isso internamente - retorna resultados vazios em vez de crashar.

### UpsertError

Lançada quando o upsert de fatos ou entidades no banco de dados falha (violação de constraint, erro de conexão). Upserts individuais de fatos usam savepoints - se um fato falha, os outros prosseguem normalmente.

### Exemplo de tratamento de erros

```python
from arandu import MemoryError, ExtractionError, RetrievalError

# Write — fail-safe por padrão
result = await memory.write(agent_id, message, speaker_name="Rafael")
if not result.success:
    print(f"Write falhou: {result.error}")
# Não precisa de try/except — o pipeline trata erros internamente

# Retrieve — também fail-safe
result = await memory.retrieve(agent_id, query)
# Facts vazios = nada encontrado OU erro ocorreu
# Verifique result.duration_ms para detectar timeouts
```

---

## Funções de Background

### Clustering

#### cluster_user_facts

::: arandu.cluster_user_facts
    options:
      show_source: false
      heading_level: 5

#### detect_communities

::: arandu.detect_communities
    options:
      show_source: false
      heading_level: 5

### Consolidation

#### run_consolidation

::: arandu.run_consolidation
    options:
      show_source: false
      heading_level: 5

#### run_profile_consolidation

::: arandu.run_profile_consolidation
    options:
      show_source: false
      heading_level: 5

### Memify

#### run_memify

::: arandu.run_memify
    options:
      show_source: false
      heading_level: 5

#### compute_vitality

::: arandu.compute_vitality
    options:
      show_source: false
      heading_level: 5

### Sleep-Time Compute

#### compute_entity_importance

::: arandu.compute_entity_importance
    options:
      show_source: false
      heading_level: 5

#### refresh_entity_summaries

::: arandu.refresh_entity_summaries
    options:
      show_source: false
      heading_level: 5

#### detect_entity_communities

::: arandu.detect_entity_communities
    options:
      show_source: false
      heading_level: 5

---

## Dataclasses de Resultado

### ClusteringResult

::: arandu.ClusteringResult
    options:
      show_source: false
      heading_level: 4

### CommunityDetectionResult

::: arandu.CommunityDetectionResult
    options:
      show_source: false
      heading_level: 4

### ConsolidationResult

::: arandu.ConsolidationResult
    options:
      show_source: false
      heading_level: 4

### MemifyResult

::: arandu.MemifyResult
    options:
      show_source: false
      heading_level: 4

### EntityImportanceResult

::: arandu.EntityImportanceResult
    options:
      show_source: false
      heading_level: 4

### SummaryRefreshResult

::: arandu.SummaryRefreshResult
    options:
      show_source: false
      heading_level: 4

---

## Veja Tambem: API Avancada

Para documentacao de funcoes internas do pipeline, exports de sub-modulos e tipos de dados adicionais nao cobertos aqui, veja a secao **Avancado**:

- [API do Write Pipeline](../advanced/write-api.md) -- estrategia de extracao, canonicalizacao, helpers de entidades, deteccao de correcoes, operacoes pendentes e `run_write_pipeline()`.
- [API do Read Pipeline](../advanced/read-api.md) -- retrieval agent, expansao de query, retrieval de grafo, spreading activation, compressao de contexto, tendencias emocionais, importancia dinamica, memoria procedimental e `run_read_pipeline()`.
- [Utilitarios de Banco de Dados](../advanced/database.md) -- `create_engine()`, `create_session_factory()`, `init_db()` e visao geral do schema.
- [Referencia de Tipos de Dados](../advanced/data-types.md) -- todos os enums, dataclasses e tipos de resultado dos modulos de escrita, leitura e background.
