Example 3: Memory Patterns¶
This example demonstrates memory patterns with agents, showcasing the WorkingMemoryAgent and memory management strategies for maintaining conversation context.
File: examples/agents/03_memory_patterns.py (283 lines)
Focus: Memory management and conversation context
Best For: Understanding stateful conversations and working memory patterns
Overview¶
This example shows how to:
Use the
WorkingMemoryAgentfor conversation contextImplement custom memory managers for development and testing
Handle multiple concurrent conversations
Manage conversation state and context across interactions
Test error conditions and edge cases with memory systems
Simulate real-world conversation patterns
Key Concepts Demonstrated¶
- Working Memory Agent
Reference implementation that manages conversation context automatically.
- Memory Manager Interface
How agents work with external storage systems for persistence.
- Conversation Context
Building and maintaining context across multiple interactions.
- Multiple Conversations
Handling concurrent conversation threads with separate context.
- Error Handling
Graceful degradation when memory systems are unavailable.
Code Walkthrough¶
1. Simple Memory Manager Implementation
The example starts with an in-memory storage system for demonstration:
class InMemoryManager:
"""Simple in-memory storage for demonstration."""
def __init__(self):
self.memories = {}
self.access_count = {}
async def store(self, data: Dict[str, Any]):
"""Store memory for a conversation."""
conv_id = data.get("conversation_id")
if conv_id:
self.memories[conv_id] = data.get("working_memory", "")
self.access_count[conv_id] = self.access_count.get(conv_id, 0) + 1
async def retrieve(self, query: Dict[str, Any]):
"""Retrieve memory for a conversation."""
conv_id = query.get("conversation_id")
if conv_id and conv_id in self.memories:
return [type('obj', (), {'working_memory': self.memories[conv_id]})()]
return None
Key Points: - Simple dictionary-based storage for development/testing - Tracks access patterns for debugging - Returns structured objects matching the expected interface - Handles missing conversations gracefully
2. Conversation Simulator
Demonstrates how to build conversation context over time:
class ConversationSimulator:
"""Simulates a conversation to demonstrate memory patterns."""
async def add_interaction(self, conversation_id: str, interaction: str) -> str:
"""Add an interaction and update memory."""
input_data = IAgentInput(
message=interaction,
metadata={"conversation_id": conversation_id}
)
# Process memory update
result = await self.memory_agent.process(input_data)
return result
Key Points: - Shows how conversation_id ties interactions together - Demonstrates the metadata pattern for passing context - Illustrates memory accumulation over multiple turns
3. Basic Memory Operations Test
Shows fundamental memory patterns:
conversation_id = "user_alice_session_001"
interactions = [
"My name is Alice and I work as a software engineer at TechCorp",
"I'm interested in learning about machine learning for my current project",
"Specifically, I need help with natural language processing techniques",
"I prefer practical examples over theoretical explanations"
]
for i, interaction in enumerate(interactions, 1):
await simulator.add_interaction(conversation_id, interaction)
memory_manager.show_memory(conversation_id)
Key Points: - Progressive context building across multiple interactions - Shows how memory accumulates user preferences and context - Demonstrates memory inspection for debugging
4. Multiple Conversations
Tests conversation isolation:
# Second conversation with different user
conversation_2 = "user_bob_session_002"
bob_interactions = [
"I'm Bob, a product manager looking to understand AI capabilities",
"My team is considering implementing chatbots for customer support"
]
for interaction in bob_interactions:
await simulator.add_interaction(conversation_2, interaction)
Key Points: - Shows how different conversation IDs maintain separate context - Demonstrates memory isolation between users - Tests concurrent conversation handling
5. Error Handling Tests
Covers edge cases and error conditions:
# Test without conversation_id
result = await memory_agent.process(IAgentInput(
message="This message has no conversation ID",
metadata={}
))
# Test with None metadata
result = await memory_agent.process(IAgentInput(
message="This message has None metadata",
metadata=None
))
# Test agent without memory manager
standalone_agent = WorkingMemoryAgent(
llm_client=llm_client,
memory_manager=None
)
Key Points: - Tests graceful handling of missing conversation IDs - Shows behavior when metadata is None or missing - Demonstrates agent operation without external storage
6. Advanced Memory Patterns
Shows real-world usage patterns:
# Customer support conversation simulation
support_conversation = "support_ticket_12345"
support_interactions = [
"Customer Jane Smith called about billing issue with invoice #4567",
"Issue: Charged twice for the same service in March 2024",
"Customer provided transaction IDs: TXN001, TXN002",
"Resolution: Refunded duplicate charge of $99.99",
"Customer satisfied, case closed"
]
Key Points: - Demonstrates structured conversation tracking - Shows how memory captures important details over time - Illustrates business context accumulation
Running the Example¶
Prerequisites:
export OPENROUTER_API_KEY=your_key_here
Run the example:
cd examples/agents
python 03_memory_patterns.py
Expected Output:
The example runs through six test scenarios:
Basic Memory Operations - Shows memory building across 4 interactions
Multiple Conversations - Demonstrates isolated conversation contexts
Error Handling - Tests edge cases and missing data
Agent without Memory Manager - Shows graceful degradation
Advanced Memory Patterns - Customer support scenario
Memory Retrieval Simulation - Returning customer context
Key Takeaways¶
1. Memory Manager Interface
The WorkingMemoryAgent expects memory managers to implement:
store(data: Dict[str, Any])- Save conversation memoryretrieve(query: Dict[str, Any])- Get conversation memory
2. Conversation ID Pattern
Always pass conversation_id in metadata for memory continuity:
input_data = IAgentInput(
message="User message",
metadata={"conversation_id": "unique_conversation_id"}
)
3. Memory Status Responses
The WorkingMemoryAgent returns status indicators:
"success"- Memory updated successfully"error: description"- Error occurred with details
4. External Storage Flexibility
The agent works with any storage backend:
In-memory dictionaries (development)
Redis (production caching)
Databases (persistent storage)
Cloud storage (distributed systems)
5. Graceful Degradation
The agent handles missing components gracefully:
No memory manager: generates memory but doesn’t store
No conversation_id: returns error status
Storage failures: continues operation with reduced functionality
Real-World Implementation Patterns¶
Production Memory Manager Example:
import redis.asyncio as redis
import json
class RedisMemoryManager:
def __init__(self, redis_url: str):
self.redis = redis.from_url(redis_url)
async def store(self, data: Dict[str, Any]):
conv_id = data.get("conversation_id")
if conv_id:
memory = data.get("working_memory", "")
await self.redis.set(f"memory:{conv_id}", memory, ex=86400) # 24h TTL
async def retrieve(self, query: Dict[str, Any]):
conv_id = query.get("conversation_id")
if conv_id:
memory = await self.redis.get(f"memory:{conv_id}")
if memory:
return [type('obj', (), {'working_memory': memory.decode()})()]
return None
Database Memory Manager Example:
class DatabaseMemoryManager:
def __init__(self, db_connection):
self.db = db_connection
async def store(self, data: Dict[str, Any]):
conv_id = data.get("conversation_id")
memory = data.get("working_memory", "")
async with self.db.cursor() as cursor:
await cursor.execute("""
INSERT INTO conversation_memory (conversation_id, memory, updated_at)
VALUES (%s, %s, NOW())
ON DUPLICATE KEY UPDATE
memory = VALUES(memory), updated_at = NOW()
""", (conv_id, memory))
async def retrieve(self, query: Dict[str, Any]):
conv_id = query.get("conversation_id")
async with self.db.cursor() as cursor:
await cursor.execute(
"SELECT memory FROM conversation_memory WHERE conversation_id = %s",
(conv_id,)
)
result = await cursor.fetchone()
if result:
return [type('obj', (), {'working_memory': result['memory']})()]
return None
Configuration-Driven Setup:
def create_memory_agent(config: dict):
"""Factory function for memory-enabled agents."""
# Create LLM client
llm_config = ILLMConfig(
model=config["model"],
temperature=config.get("temperature", 0.7)
)
llm_client = OpenRouterClient(llm_config)
# Create memory manager based on configuration
memory_type = config.get("memory_type", "memory")
if memory_type == "redis":
memory_manager = RedisMemoryManager(config["redis_url"])
elif memory_type == "database":
memory_manager = DatabaseMemoryManager(config["db_connection"])
else:
memory_manager = InMemoryManager()
# Create memory agent
return WorkingMemoryAgent(
llm_client=llm_client,
memory_manager=memory_manager
)
Testing Memory Patterns¶
Unit Test Example:
import pytest
from unittest.mock import AsyncMock
@pytest.mark.asyncio
async def test_memory_agent_with_context():
"""Test memory agent retrieves and uses context."""
# Mock LLM client
mock_llm = AsyncMock()
mock_llm.chat.return_value = {
"llm_response": "Memory updated with user context",
"usage": {"total_tokens": 50}
}
# Mock memory manager
mock_memory = AsyncMock()
mock_memory.retrieve.return_value = [
type('obj', (), {'working_memory': 'User likes technical details'})()
]
# Create agent
agent = WorkingMemoryAgent(mock_llm, mock_memory)
# Test with existing memory
result = await agent.process(IAgentInput(
message="Tell me about APIs",
metadata={"conversation_id": "test_123"}
))
# Verify memory was retrieved and used
mock_memory.retrieve.assert_called_once()
mock_memory.store.assert_called_once()
assert "success" in result
Common Use Cases¶
1. Customer Support
Track customer issues and context across multiple interactions:
# Each support ticket gets a unique conversation ID
conversation_id = f"support_ticket_{ticket_id}"
# Agent builds context of customer issues, solutions tried, etc.
await memory_agent.process(IAgentInput(
message="Customer reporting login issues after password reset",
metadata={"conversation_id": conversation_id}
))
2. Educational Tutoring
Maintain student learning context and progress:
# Track student's learning journey
conversation_id = f"student_{student_id}_session_{session_id}"
# Agent remembers topics covered, learning style, progress
await memory_agent.process(IAgentInput(
message="Student struggled with recursion concepts",
metadata={"conversation_id": conversation_id}
))
3. Personal Assistants
Remember user preferences and context across sessions:
# User's ongoing conversation with personal assistant
conversation_id = f"user_{user_id}_assistant"
# Agent remembers preferences, past requests, context
await memory_agent.process(IAgentInput(
message="User prefers evening meetings and detailed summaries",
metadata={"conversation_id": conversation_id}
))
4. Project Management
Track project context and decisions:
# Project-specific conversation context
conversation_id = f"project_{project_id}_discussions"
# Agent builds context of decisions, requirements, issues
await memory_agent.process(IAgentInput(
message="Team decided to use microservices architecture",
metadata={"conversation_id": conversation_id}
))
Next Steps¶
After understanding memory patterns:
Implement Your Memory Manager: Create storage backend for your use case
Integrate with Your Application: Use memory agents in your conversation flows
Handle Memory Lifecycle: Implement cleanup and retention policies
Monitor Memory Usage: Track storage and performance metrics
Test Memory Patterns: Ensure reliable memory behavior in your tests
Related Examples: - Example 4: Tool Integration - Combine memory with external tools - 05-agent-composition - Use memory agents in larger systems - 06-testing-agents - Test memory-enabled agents thoroughly