# arandu

[![CI](https://github.com/pe-menezes/arandu/actions/workflows/ci.yml/badge.svg)](https://github.com/pe-menezes/arandu/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/arandu)](https://pypi.org/project/arandu/)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/pe-menezes/arandu/blob/main/LICENSE)
[![Documentation](https://img.shields.io/badge/docs-GitHub%20Pages-blue)](https://pe-menezes.github.io/arandu/)

Long-term memory system for AI agents. Extract facts from conversations, resolve entities, reconcile updates, and retrieve relevant context — works with any LLM provider.

> **[Read the full documentation →](https://pe-menezes.github.io/arandu/)**

## Quickstart

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

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

provider = OpenAIProvider(api_key="sk-...")
memory = MemoryClient(
    database_url="postgresql+psycopg://user:pass@localhost/mydb",
    llm=provider,
    embeddings=provider,
)

async def main():
    await memory.initialize()  # creates tables (idempotent)

    # Write — extracts facts automatically
    result = await memory.write(user_id="user_123", message="I live in São Paulo with my wife Ana")
    print(result.facts_added)  # [{"fact_text": "Lives in São Paulo", ...}]

    # Retrieve — semantic search + keyword + LLM reranking
    context = await memory.retrieve(user_id="user_123", query="where does the user live?")
    print(context.context)  # "## Known facts about the user:\n- Lives in São Paulo ..."

asyncio.run(main())
```

## Requirements

- Python 3.11+
- PostgreSQL with [pgvector](https://github.com/pgvector/pgvector) extension
- An LLM provider (OpenAI included, or implement your own)

## Custom Providers

Implement the `LLMProvider` and `EmbeddingProvider` protocols to use any backend:

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

class MyProvider:
    async def complete(self, messages, temperature=0, response_format=None, max_tokens=None) -> str:
        ...  # your LLM call

    async def embed(self, texts: list[str]) -> list[list[float]]:
        ...  # your embedding call

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

## Configuration

```python
from arandu import MemoryConfig

config = MemoryConfig(
    topk_facts=20,              # max facts to retrieve
    min_similarity=0.20,        # cosine similarity threshold
    min_confidence=0.55,        # minimum fact confidence
    enable_reranker=True,       # LLM reranking of results
    recency_half_life_days=14,  # exponential decay half-life
)

memory = MemoryClient(database_url="...", llm=provider, embeddings=provider, config=config)
```

## Architecture

The SDK implements a 4-stage write pipeline and a 3-signal read pipeline:

**Write:** Message → Extract (LLM) → Entity Resolution (exact/fuzzy/LLM) → Reconcile (ADD/UPDATE/NOOP/DELETE) → Upsert

**Read:** Query → Semantic Search (pgvector) + Keyword Search (ILIKE) + Recency Scoring → LLM Rerank → Context Formatting

## Documentation

For comprehensive documentation including conceptual guides, configuration reference, and cookbook examples:

**[https://pe-menezes.github.io/arandu/](https://pe-menezes.github.io/arandu/)**

## Contributing

Contributions are welcome! Please read the [Contributing Guide](CONTRIBUTING.md) before submitting a pull request.

## License

[MIT](LICENSE)
