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

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

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

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

    # Pre-formatted context string (ready to inject into an LLM prompt)
    print(result.context)

    # Individual scored facts
    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")
```

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

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

config = MemoryConfig(
    # Use single-pass extraction for simpler messages
    extraction_mode="single_pass",

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

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

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

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

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

`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
- [**Background Jobs**](concepts/background-jobs.md) — Set up clustering, consolidation, and importance scoring
- [**Design Philosophy**](concepts/design-philosophy.md) — Explore the neuroscience-inspired architecture
