# Building a Personal Assistant

This guide shows how to give an AI agent **human-like memory** using Arandu — so it remembers what it heard, what it said, who told it what, and what it did.

## The Problem

Think about how your own memory works. When you talk to someone:

- You remember **what they told you** — "Pedro told me he lives in Porto Alegre"
- You remember **what you said** — "I recommended a restaurant to him"
- You remember **what you learned about someone else** from the conversation — "Pedro mentioned his girlfriend Ana is a designer"
- You remember **what you did** — "I created a document for Pedro"
- When asked about any topic, you recall **everything you know**, regardless of who told you or when

An AI agent has none of this by default. Every session starts blank. The agent doesn't know what happened before, who said what, or what it did. It has no memory.

Arandu gives your agent the same kind of memory a human has. Every fact carries who it's about, who said it, and when — and retrieval works like human recall: ask about a topic, get everything relevant.

## Architecture

A personal assistant needs three layers of memory:

| Layer | Tool | What it stores |
|---|---|---|
| **Semantic memory** | **Arandu** | Facts about people, preferences, actions, knowledge |
| **Procedural memory** | System prompt / config | How the assistant should behave |
| **Episodic memory** | File system / database | Daily logs, task boards, structured records |

Arandu handles semantic memory — the "what I know" layer. It extracts facts from natural language, reconciles them with existing knowledge, and retrieves by relevance.

## Core Pattern: Record Both Sides

The key insight: **record both the user's messages and the assistant's responses**.

```python
# User said something → record with user's speaker_name
await memory.write(
    agent_id="my-assistant",
    message="My girlfriend Ana is a designer, she works at Stone",
    speaker_name="Pedro",
)
# Extracted facts:
#   "Ana is a designer" (entity: person:ana)
#   "Ana works at Stone" (entity: person:ana, organization:stone)
#   "Ana is Pedro's girlfriend" (entity: person:ana)

# Assistant responded → record with assistant's speaker_name
await memory.write(
    agent_id="my-assistant",
    message="Created the note 'Ana.md' in the vault and scheduled a birthday reminder for May 15",
    speaker_name="Assistant",
)
# Extracted facts:
#   "Assistant created note Ana.md in the vault" (entity: person:assistant)
#   "Assistant scheduled birthday reminder for May 15" (entity: person:assistant)
```

The `speaker_name` resolves pronouns: "I created" with `speaker_name="Assistant"` becomes "Assistant created". Each fact carries `speaker` metadata showing who said it.

## Session Flow

```
┌─────────────────────────────────────────────────────┐
│ Session start                                        │
│                                                      │
│  1. retrieve(query="recent context")                 │
│     → Recovers what the assistant knows and did      │
│     → Inject into system prompt as context           │
│                                                      │
├─────────────────────────────────────────────────────┤
│ During conversation                                  │
│                                                      │
│  2. User sends message                               │
│     → write(message=msg, speaker_name="Pedro")       │
│                                                      │
│  3. Assistant processes and responds                 │
│     → retrieve(query=msg) to get relevant context    │
│     → Generate response                              │
│     → write(message=response, speaker_name="Assist") │
│                                                      │
├─────────────────────────────────────────────────────┤
│ Session end                                          │
│                                                      │
│  Nothing special. Everything was recorded during     │
│  the conversation. Next session starts with retrieve.│
└─────────────────────────────────────────────────────┘
```

## Retrieval Examples

Once both sides are recorded, the assistant can answer questions about anyone and anything:

```python
# "What do I know about Ana?"
result = await memory.retrieve(agent_id="my-assistant", query="Ana")
# Returns ALL facts about Ana, regardless of who said them:
#   - Ana is a designer (Pedro said)
#   - Ana works at Stone (Pedro said)
#   - Assistant created note Ana.md (Assistant did)

# "What did I do recently?"
result = await memory.retrieve(agent_id="my-assistant", query="what was done recently")
# Returns assistant's actions:
#   - Created note Ana.md
#   - Scheduled birthday reminder

# Scoped to a specific entity
result = await memory.retrieve(
    agent_id="my-assistant",
    query="work",
    entity_keys=["person:ana"],
)
# Returns only work-related facts about Ana
```

`entity_keys` accepts aliases too — pass `"person:ana"` even if the canonical key is `person:ana_silva`, and the SDK resolves it via `memory_entity_aliases`. Keys that do not resolve are surfaced in `result.warnings` so the caller can distinguish "no matches" from "bad key":

```python
result = await memory.retrieve(
    agent_id="my-assistant",
    query="work",
    entity_keys=["person:ana", "person:unknown"],
)
# result.warnings → ["entity_key 'person:unknown' not found (not canonical, no matching alias)"]
```

## Speaker Provenance

Every fact carries a `speaker` field showing who said the message it was extracted from. This is available in both `FactDetail` (from `get`/`get_all`) and `ScoredFact` (from `retrieve`):

```python
result = await memory.retrieve(agent_id="my-assistant", query="Stone")
for fact in result.facts:
    print(f"[{fact.speaker}] {fact.fact_text}")
# Output:
#   [Pedro] Ana works at Stone
#   [Assistant] Stone is a Brazilian fintech, founded in 2012
```

No competitor persists speaker provenance natively — this is an Arandu differentiator.

## Managing Memory

The assistant can inspect and manage its own memory:

```python
# List all known entities
entities = await memory.entities(agent_id="my-assistant")
for e in entities:
    print(f"[{e.entity_type}] {e.display_name} ({e.fact_count} facts)")
# [person] Pedro (12 facts)
# [person] Ana (5 facts)
# [organization] Stone (3 facts)
# [person] Assistant (8 facts)

# List facts about a specific entity
ana_facts = await memory.get_all(
    agent_id="my-assistant",
    entity_keys=["person:ana"],
)

# Delete a specific fact
await memory.delete(agent_id="my-assistant", fact_id="some-uuid")

# Reset all memory (use with caution)
await memory.delete_all(agent_id="my-assistant")
```

## Complete Example

A minimal personal assistant with memory:

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


async def handle_message(
    memory: MemoryClient,
    agent_id: str,
    user_msg: str,
    speaker: str,
) -> str:
    # 1. Record user message
    await memory.write(
        agent_id=agent_id,
        message=user_msg,
        speaker_name=speaker,
    )

    # 2. Retrieve relevant context
    context = await memory.retrieve(agent_id=agent_id, query=user_msg)

    # 3. Generate response (replace with your LLM call)
    llm = memory._llm  # reuse the SDK's provider
    response = await llm.complete(
        messages=[
            {
                "role": "system",
                "content": f"You are a personal assistant. Memory context:\n{context.context}",
            },
            {"role": "user", "content": user_msg},
        ]
    )

    # 4. Record assistant response
    await memory.write(
        agent_id=agent_id,
        message=response.text,
        speaker_name="Assistant",
    )

    return response.text


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

    agent_id = "pedro-assistant"

    try:
        # Session start: check what happened before
        recent = await memory.retrieve(agent_id=agent_id, query="recent context")
        if recent.facts:
            print("Resuming with context from previous sessions...")

        # Simulate conversation
        reply = await handle_message(
            memory, agent_id,
            "My girlfriend Ana is a designer at Stone",
            speaker="Pedro",
        )
        print(f"Assistant: {reply}")

        reply = await handle_message(
            memory, agent_id,
            "What do you know about Ana?",
            speaker="Pedro",
        )
        print(f"Assistant: {reply}")
    finally:
        await memory.close()


asyncio.run(main())
```

## How Arandu Compares

| Capability | Mem0 | Zep | Letta | Arandu |
|---|---|---|---|---|
| Fact extraction from text | Yes | Yes | No (manual) | Yes |
| Filter on retrieve | `user_id`/`agent_id` | `group_ids` | Block labels | `entity_keys` |
| Speaker provenance | No | Indirect (episode lookup) | No | **Yes (native)** |
| Entity graph | Optional | Yes | No | Yes |
| CRUD operations | Yes | Yes | Yes | Yes |
| Reconciliation (dedup) | Yes | Automatic | Manual | Yes (LLM-based) |
