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

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

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 `user_id`"
    O `user_id` é sua **chave de particionamento**. Cada user_id tem seu próprio espaço de memória isolado — fatos escritos para um user nunca são retornados para outro. Use o identificador de usuário da sua aplicação (ID do banco, email, UUID — qualquer string). O mesmo user_id deve ser usado em `write()` e `retrieve()` para o mesmo usuário.

## 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(
        user_id="user_123",
        message="My name is Rafael and I live in São Paulo. I work at Acme Corp as a backend engineer.",
    )
    print(f"Facts added: {len(result.facts_added)}")
    for fact in result.facts_added:
        print(f"  [{fact.entity_name}] {fact.value_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(
        user_id="user_123",
        message="I just moved to Rio de Janeiro. Still working at Acme though.",
    )
    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` | `int` | Fatos confirmados mas não alterados (decisões NOOP) |
| `facts_deleted` | `int` | 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(
        user_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.value}")

    # Com ajustes de config (ex: desabilitar reranker para resultados mais rápidos)
    fast_result = await memory.retrieve(
        user_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(
    user_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 |
| `value` | `str` | O conteúdo do fato |
| `score` | `float` | Score combinado de relevância (0-1) |
| `scores` | `dict` | Detalhamento por sinal (semantic, recency, etc.) |

## Passo 6: 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 principal (extração, entity resolution, reconciliação, planner)
llm = OpenAIProvider(api_key="sk-...", model="gpt-4o")

# Provider mais barato para o reranker
reranker = OpenAIProvider(api_key="sk-...", model="gpt-4o-mini")

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,

    # Ajustar pesos dos scores
    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,
    reranker_llm=reranker,  # opcional, faz fallback para llm se não fornecido
    config=config,
)
```

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

## 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(user_id="user_123", message="...", 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 7: 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(
            user_id="user_123",
            message="I'm a software engineer living in Berlin. I love cycling and craft coffee.",
        )
        await memory.write(
            user_id="user_123",
            message="My girlfriend Ana is a designer. We adopted a cat named Pixel last month.",
        )

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

        # Retrieval direcionado
        result = await memory.retrieve(user_id="user_123", query="who is Ana?")
        for fact in result.facts:
            print(f"  [{fact.score:.2f}] {fact.entity_name}: {fact.value}")
    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, EmbeddingProvider

class MyLLMProvider:
    async def complete(
        self,
        messages: list[dict],
        temperature: float = 0,
        response_format: dict | None = None,
        max_tokens: int | None = None,
    ) -> str:
        # Chame seu LLM aqui
        ...

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
