Skip to content

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.

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

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

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

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:

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

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)