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