Building Emma: A Memory-Powered Chatbot
Follow along as we create Emma, an AI assistant that remembers everything about her users and gets smarter with every conversation.
The Challenge
Imagine you're building a customer support chatbot for your SaaS company. Users are frustrated because every time they start a new conversation, they have to re-explain their preferences, their account details, and their previous issues.
What if your chatbot could remember everything? What if it knew that Sarah prefers technical explanations, that John always asks about billing on Fridays, and that Maria's account has had three support tickets this month?
Building Emma: Step by Step
Step 1: Environment Setup
First, let's set up our environment with the necessary dependencies:
# Install the required packages
pip install memorystack openai python-dotenv
# Create your .env file
echo "MEMORY_OS_API_KEY=your_memory_os_key_here" > .env
echo "OPENAI_API_KEY=your_openai_key_here" >> .envStep 2: Emma's Core Brain
Here's Emma's main class - her "brain" that handles memory and conversations:
from memorystack import MemoryStackClient
from openai import OpenAI
import os
from dotenv import load_dotenv
from datetime import datetime
load_dotenv()
class Emma:
"""Emma - A memory-powered chatbot that remembers everything"""
def __init__(self, user_id: str):
self.user_id = user_id
self.name = "Emma"
# Initialize Memorystack client
self.memory = MemoryStackClient(
api_key=os.getenv("MEMORYSTACK_API_KEY"),
user_id=user_id
)
# Initialize OpenAI client
self.ai = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
print(f"š Hi! I'm {self.name}, your memory-powered assistant!")
def chat(self, user_message: str) -> str:
"""The main conversation method"""
print(f"\nš§ Processing: '{user_message}'")
# Step 1: Store the user's message
self._store_user_message(user_message)
# Step 2: Retrieve relevant memories
relevant_memories = self._get_relevant_memories(user_message)
# Step 3: Generate response with context
response = self._generate_response(user_message, relevant_memories)
# Step 4: Store the response and learn
self._store_response(response)
self._learn_from_interaction(user_message)
return responseStep 3: Complete Emma Implementation
Here are all the helper methods that make Emma work:
def _store_user_message(self, message: str):
"""Store user message in memory"""
self.memory.create_memory(
content=f"User said: {message}",
memory_type="conversation",
metadata={"role": "user", "timestamp": datetime.now().isoformat()}
)
def _get_relevant_memories(self, query: str) -> list:
"""Retrieve memories relevant to the current query"""
memories = self.memory.search_memories(query=query, limit=8)
results = memories.get('results', [])
print(f"š Found {len(results)} relevant memories")
return results
def _generate_response(self, user_message: str, memories: list) -> str:
"""Generate contextual response using memories"""
context_parts = []
for memory in memories:
content = memory.get('content', '')
memory_type = memory.get('memory_type', 'unknown')
context_parts.append(f"[{memory_type}] {content}")
context_text = "\n".join(context_parts)
system_prompt = f"""You are Emma, a friendly and intelligent AI assistant with perfect memory.
PERSONALITY:
- Warm, helpful, and personable
- Remember details about users and reference them naturally
- Adapt your communication style to user preferences
CONTEXT FROM MEMORY:
{context_text}
Use this context to provide personalized responses."""
response = self.ai.chat.completions.create(
model="gpt-4",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_message}
],
temperature=0.7
)
return response.choices[0].message.content
def _store_response(self, response: str):
"""Store Emma's response in memory"""
self.memory.create_memory(
content=f"Emma said: {response}",
memory_type="conversation",
metadata={"role": "assistant", "timestamp": datetime.now().isoformat()}
)
def _learn_from_interaction(self, user_message: str):
"""Extract and store learnings from user message"""
analysis = self.ai.chat.completions.create(
model="gpt-4",
messages=[{
"role": "system",
"content": """Extract preferences, facts, and context from the message.
Return as JSON: {"preferences": [], "facts": [], "context": []}"""
}, {"role": "user", "content": user_message}]
)
try:
import json
learnings = json.loads(analysis.choices[0].message.content)
for pref in learnings.get('preferences', []):
self.memory.create_memory(
content=f"User preference: {pref}",
memory_type="preference"
)
for fact in learnings.get('facts', []):
self.memory.create_memory(
content=f"User fact: {fact}",
memory_type="fact"
)
except Exception as e:
print(f"ā ļø Learning extraction failed: {e}")Using Emma: See Her in Action
Complete Usage Example
Here's how to use Emma in your application:
# main.py - Let's chat with Emma!
def main():
# Create Emma for a specific user
emma = Emma(user_id="sarah_engineer_123")
print("š¤ Emma is ready to chat!")
print("=" * 50)
# First conversation - Emma learns about Sarah
response1 = emma.chat(
"Hi Emma! I'm Sarah, a software engineer. I prefer detailed technical explanations."
)
print(f"Emma: {response1}")
# Second conversation - Emma remembers
response2 = emma.chat(
"I'm getting a 429 error when making requests. What should I do?"
)
print(f"Emma: {response2}")
# Interactive chat loop
while True:
user_input = input("\nYou: ")
if user_input.lower() in ['quit', 'exit', 'bye']:
print("Emma: Goodbye! I'll remember our conversation! š")
break
response = emma.chat(user_input)
print(f"Emma: {response}")
if __name__ == "__main__":
main()Expected Output
Emma: Hello Sarah! Nice to meet you. I've noted that you're a software engineer who prefers detailed technical explanations.
---
Emma: Hi Sarah! That 429 error indicates you've hit rate limits. Here's the technical solution: implement exponential backoff with jitter...
Making Emma Even Smarter
šÆ Smart Context Selection
Prioritize memories based on recency, importance, and relevance:
def _get_smart_context(self, query: str):
# Get recent conversations
recent = self.memory.search_memories(
query=query,
limit=3,
metadata_filter={"days_ago": "< 7"}
)
# Get important facts
facts = self.memory.search_memories(
query=query,
memory_type="fact",
limit=2
)
return recent + factsš Conversation Summaries
Create summaries of long conversations to maintain context efficiently:
def create_conversation_summary(self):
# Get last 20 messages
recent_messages = self.memory.search_memories(
memory_type="conversation",
limit=20
)
# Create summary
summary = self.ai.chat.completions.create(
model="gpt-4",
messages=[{
"role": "system",
"content": "Summarize key points"
}]
)
# Store summary
self.memory.create_memory(
content=summary,
memory_type="summary"
)Best Practices
1. Store Both Sides of Conversation
Always store both user messages and bot responses to maintain complete context.
2. Use Semantic Search
Leverage Memorystack's semantic search to find relevant context, not just keyword matches.
3. Limit Context Window
Retrieve only the most relevant memories (5-10) to avoid overwhelming the AI model.
4. Add Metadata
Include timestamps, roles, and other metadata for better organization and filtering.
