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

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

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

## 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?",
    )

    # String de contexto pré-formatada (pronta para injetar em um prompt LLM)
    print(result.context)

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

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

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

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

config = MemoryConfig(
    # Usar extração single-pass para mensagens mais simples
    extraction_mode="single_pass",

    # 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=provider,
    embeddings=provider,
    config=config,
)
```

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

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

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

        # Retrieval direcionado
        result = await memory.retrieve(user_id="demo", 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
- [**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
