# Getting Started

This guide walks you through setting up `arandu` from scratch: installing dependencies, configuring PostgreSQL with pgvector, writing your first facts, and retrieving them.

## Prerequisites

- **Python 3.11+**
- **PostgreSQL 15+** with the [pgvector](https://github.com/pgvector/pgvector) extension installed
- An **OpenAI API key** (or any LLM/embedding provider — see [Custom Providers](#custom-providers))

## Step 1: Install

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

This installs the core SDK plus the bundled OpenAI provider. If you're using a different LLM provider, install just the core:

```bash
pip install arandu
```

## Step 2: Set Up PostgreSQL + pgvector

`arandu` stores facts, entities, and embeddings in PostgreSQL using the pgvector extension for vector similarity search.

### Option A: Docker (recommended for development)

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

The `pgvector/pgvector` image comes with the extension pre-installed. Your connection string will be:
`postgresql+psycopg://memory:memory@localhost:5432/memory`

!!! warning "psycopg vs psycopg2"
    Arandu uses `psycopg` (async driver), **not** `psycopg2` (sync). Your connection string must start with `postgresql+psycopg://`, not `postgresql+psycopg2://`. Many Django/Flask tutorials use psycopg2 — make sure you're using the right one.

### Option B: Existing PostgreSQL

If you already have PostgreSQL running, enable the pgvector extension:

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

!!! note "pgvector installation"
    If you don't have pgvector installed on your server, follow the
    [pgvector installation guide](https://github.com/pgvector/pgvector#installation).

## Step 3: Initialize the Client

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

async def main():
    # Create the LLM + embedding provider
    provider = OpenAIProvider(api_key="sk-...")

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

    # Create tables (safe to call multiple times)
    await memory.initialize()

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

asyncio.run(main())
```

`initialize()` creates all required tables and indexes (including pgvector HNSW indexes). It's idempotent — safe to call on every startup.

!!! info "About `user_id`"
    The `user_id` is your **partitioning key**. Each user_id gets its own isolated memory space — facts written for one user are never returned for another. Use your application's user identifier (database ID, email, UUID — any string). The same user_id must be used in both `write()` and `retrieve()` calls for the same user.

## Step 4: Write Your First Facts

The `write()` method takes a natural language message and automatically:

1. Extracts entities, facts, and relationships using an LLM
2. Resolves entities to canonical records (deduplication)
3. Reconciles new facts against existing knowledge
4. Upserts the results into the database

```python
async def write_example(memory: MemoryClient):
    # First message
    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")

    # Second message — the system recognizes "Rafael" and updates knowledge
    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"
```

### Understanding WriteResult

The `WriteResult` object tells you exactly what happened:

| Field | Type | Description |
|-------|------|-------------|
| `event_id` | `str` | Unique ID for this write event |
| `facts_added` | `list` | New facts created (ADD decisions) |
| `facts_updated` | `list` | Existing facts superseded (UPDATE decisions) |
| `facts_unchanged` | `int` | Facts confirmed but not changed (NOOP decisions) |
| `facts_deleted` | `int` | Facts retracted (DELETE decisions) |
| `entities_resolved` | `list` | Entities identified and resolved |
| `duration_ms` | `float` | Total pipeline duration |
| `success` | `bool` | Whether the pipeline completed without errors |
| `error` | `str \| None` | Error message if the pipeline failed internally |

## Step 5: Retrieve Context

The `retrieve()` method finds facts relevant to a query using multiple signals:

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

    # Option 1: Pre-formatted string — paste directly into your LLM prompt
    print(result.context)

    # Option 2: Individual scored facts — for programmatic access
    for fact in result.facts:
        print(f"  [{fact.score:.2f}] {fact.entity_name}: {fact.value}")

    # With config adjustments (e.g., disable reranker for faster results)
    fast_result = await memory.retrieve(
        user_id="user_123",
        query="where does Rafael live?",
        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`** when you just need a string to inject into an LLM prompt — it's pre-formatted with tier labels (CORE MEMORY, EXTENDED CONTEXT, etc.). Use **`result.facts`** when you need programmatic access to individual facts, scores, and metadata.

### Per-request Config Overrides

You can override any `MemoryConfig` field for a single request without changing the client's default config:

```python
result = await memory.retrieve(
    user_id="user_123",
    query="where does Rafael live?",
    config_overrides={
        "enable_reranker": False,
        "topk_facts": 5,
        "spreading_activation_hops": 0,
    },
)

# config_effective shows the actual config used for this request
print(result.config_effective)
```

Only the provided keys are overridden; all other fields inherit from the client's `MemoryConfig`.

### Understanding RetrieveResult

| Field | Type | Description |
|-------|------|-------------|
| `facts` | `list[ScoredFact]` | Ranked facts with scores |
| `context` | `str` | Pre-formatted context string for LLM prompts |
| `total_candidates` | `int` | Total facts evaluated before ranking |
| `duration_ms` | `float` | Total pipeline duration |
| `config_effective` | `dict` | Effective config values used for this request |

Each `ScoredFact` contains:

| Field | Type | Description |
|-------|------|-------------|
| `fact_id` | `str` | Unique fact identifier |
| `entity_name` | `str` | Human-readable entity name |
| `attribute_key` | `str` | Fact category/attribute |
| `value` | `str` | The fact content |
| `score` | `float` | Combined relevance score (0-1) |
| `scores` | `dict` | Breakdown by signal (semantic, recency, etc.) |

## Step 6: Configure (Optional)

Every aspect of the pipeline is configurable via `MemoryConfig`:

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

# Main provider (extraction, entity resolution, reconciliation, planner)
llm = OpenAIProvider(api_key="sk-...", model="gpt-4o")

# Cheaper provider for reranker
reranker = OpenAIProvider(api_key="sk-...", model="gpt-4o-mini")

config = MemoryConfig(
    # Tight timeout for real-time chat
    extraction_timeout_sec=15.0,

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

    # Adjust score weights
    score_weights={
        "semantic": 0.60,
        "recency": 0.25,
        "importance": 0.15,
    },

    # Set timezone for recency calculations
    timezone="America/Sao_Paulo",
)

memory = MemoryClient(
    database_url="postgresql+psycopg://memory:memory@localhost/memory",
    llm=llm,
    embeddings=llm,
    reranker_llm=reranker,  # optional, falls back to llm if not provided
    config=config,
)
```

All parameters have sensible defaults — you only need to override what matters for your use case.

## Debugging with Verbose Mode

Pass `verbose=True` to `write()` or `retrieve()` to get a detailed trace of every pipeline step:

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

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

The trace includes steps like `extraction`, `entity_resolution`, `reconciliation`, and `upsert`, each with timing and intermediate data. If the pipeline fails internally, an `error` step is added with the exception details — useful for diagnosing silent failures.

You can serialize the full trace with `result.pipeline.to_dict()`.

## Step 7: Cleanup

Always close the client when done to release database connections:

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

Or use it as an async context pattern:

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

## Complete Example

Here's a full working example putting it all together:

```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:
        # Write some facts
        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.",
        )

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

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

`arandu` uses Python protocols for dependency injection. You can bring any LLM or embedding provider by implementing two simple interfaces:

```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:
        # Call your LLM here
        ...

class MyEmbeddingProvider:
    async def embed(self, texts: list[str]) -> list[list[float]]:
        # Return embeddings for a batch
        ...

    async def embed_one(self, text: str) -> list[float] | None:
        # Return embedding for a single text
        ...
```

No inheritance required — just implement the methods with the right signatures.

## Next Steps

- [**Write Pipeline**](concepts/write-pipeline.md) — Understand how facts are extracted, entities resolved, and knowledge reconciled
- [**Read Pipeline**](concepts/read-pipeline.md) — Learn how multi-signal retrieval finds the most relevant facts
- [**Data Types & Schema**](advanced/data-types.md) — Database schema reference (tables, columns, types) for direct SQL queries
- [**Background Jobs**](concepts/background-jobs.md) — Set up clustering, consolidation, and importance scoring
- [**Design Philosophy**](concepts/design-philosophy.md) — Explore the neuroscience-inspired architecture
