# Build an AI Agent with Persistent Memory in Python (Full Guide) > This guide builds a stateful AI agent in Python that remembers users across sessions using Feather DB for memory storage and retrieval, and OpenAI for LLM responses — complete working code from install to production. - **Category**: Architecture - **Read time**: 12 min read - **Date**: July 8, 2026 - **Author**: Feather DB (Engineering) - **URL**: https://getfeather.store/theory/build-ai-agent-persistent-memory-python --- An AI agent with persistent memory remembers users between sessions: their preferences, past issues, ongoing tasks, and conversational history. Without persistent memory, every conversation starts from zero. This guide builds a complete stateful AI agent using Feather DB for memory storage and retrieval and OpenAI for responses — covering installation, memory ingestion, context retrieval, session management, and production patterns. All code is working and production-ready. ## Prerequisites ``` pip install feather-db openai # Set your OpenAI API key export OPENAI_API_KEY="your-key-here" ``` ## Architecture Overview The agent has three components: - **Memory store:** Feather DB stores all memories as vector-embedded facts. It handles semantic search, temporal decay, and graph traversal. - **Context builder:** Before each LLM call, retrieves the most relevant memories and formats them into a context block. - **LLM interface:** Sends the context block + current message to GPT-4o and receives the response. Between sessions, memories persist in the `.feather` file. When the session starts, the agent loads the user's profile from memory and is immediately personalized. ## Step 1: Memory Manager ``` import feather_db as fdb from datetime import datetime from typing import Optional class MemoryManager: """Handles all memory operations for the AI agent.""" def __init__(self, storage_path: str = "agent_memories.feather"): self.db = fdb.FeatherDB(storage_path) def store(self, text: str, user_id: str, memory_type: str = "episodic", importance: float = 0.6, session_id: Optional[str] = None) -> str: """Store a memory and return its ID.""" half_life_map = { "episodic": 30, # Session events fade in ~1 month "semantic": 180, # User facts persist ~6 months "procedural": 365, # How-to knowledge persists ~1 year } node_id = self.db.add( text=text, metadata={ "user_id": user_id, "memory_type": memory_type, "session_id": session_id or "unknown", "stored_at": datetime.now().isoformat() }, importance=importance, half_life_days=half_life_map.get(memory_type, 60) ) return node_id def link(self, source_id: str, target_id: str, edge_type: str = "related", weight: float = 0.8): """Create a typed edge between two memories.""" self.db.link_nodes(source_id, target_id, edge_type, weight) def recall(self, query: str, user_id: str, top_k: int = 8, use_graph: bool = True) -> list: """Retrieve the most relevant memories for a query.""" if use_graph: return self.db.context_chain( query=query, hops=2, top_k=top_k, filters={"user_id": user_id} ) return self.db.search( query=query, top_k=top_k, filters={"user_id": user_id} ) def get_profile(self, user_id: str) -> list: """Get user's semantic profile — called once at session start.""" return self.db.search( query="user background preferences goals tech stack", top_k=10, filters={"user_id": user_id, "memory_type": "semantic"} ) def format_context(self, memories: list) -> str: """Format retrieved memories as a context block.""" if not memories: return "No relevant context found." return "\n".join([f"- {m['text']}" for m in memories]) def count(self, user_id: str) -> int: """Count total memories for a user.""" return self.db.count(filters={"user_id": user_id}) ``` ## Step 2: Memory Extractor Before storing a memory, extract the key fact from the conversation turn. This keeps memories atomic and searchable: ``` from openai import OpenAI import json client = OpenAI() def extract_memories(conversation_turn: str) -> list[dict]: """ Extract memorable facts from a conversation turn. Returns list of dicts with 'text', 'memory_type', 'importance'. """ extraction_prompt = """Extract memorable facts from this conversation excerpt. Return a JSON array. Each item has: - text: the fact as a declarative statement - memory_type: "semantic" (general user fact/preference) or "episodic" (specific event) - importance: 0.0-1.0 (how important this fact is to remember) Only extract genuinely memorable facts. Skip pleasantries and filler. Conversation: """ + conversation_turn response = client.chat.completions.create( model="gpt-4o-mini", # Cheap model for extraction messages=[{"role": "user", "content": extraction_prompt}], response_format={"type": "json_object"} ) result = json.loads(response.choices[0].message.content) return result.get("memories", []) # Example output: # [ # {"text": "User builds AI agents for enterprise clients", # "memory_type": "semantic", "importance": 0.8}, # {"text": "User reported latency issue with their agent in session 42", # "memory_type": "episodic", "importance": 0.6} # ] ``` ## Step 3: The Stateful Agent ``` from openai import OpenAI import feather_db as fdb from datetime import datetime from typing import Optional client = OpenAI() class StatefulAgent: """ An AI agent that remembers users across sessions. Uses Feather DB for persistent memory storage and retrieval. """ def __init__(self, user_id: str, memory_path: str = "agent_memories.feather"): self.user_id = user_id self.session_id = f"session_{datetime.now().strftime('%Y%m%d_%H%M%S')}" self.memory = MemoryManager(memory_path) self.conversation_history = [] # In-session working memory # Load user profile at session start self.user_profile = self.memory.get_profile(user_id) self.session_context = self.memory.format_context(self.user_profile) print(f"Session {self.session_id} started for user {user_id}") print(f"Loaded {len(self.user_profile)} profile memories") print(f"Total memories: {self.memory.count(user_id)}") def chat(self, user_message: str) -> str: """Process a user message and return the agent's response.""" # 1. Retrieve relevant memories for this specific query turn_memories = self.memory.recall( query=user_message, user_id=self.user_id, top_k=8, use_graph=True ) turn_context = self.memory.format_context(turn_memories) # 2. Build the system prompt with both profile and turn context system_prompt = f"""You are a helpful AI assistant with persistent memory of past interactions. User profile: {self.session_context} Relevant memories for this query: {turn_context} Use this context to give personalized responses. If you're referencing a past event, mention it naturally.""" # 3. Maintain a short in-session conversation buffer (last 6 turns) self.conversation_history.append({"role": "user", "content": user_message}) messages = [ {"role": "system", "content": system_prompt} ] + self.conversation_history[-6:] # Last 6 turns only # 4. LLM call response = client.chat.completions.create( model="gpt-4o", messages=messages ) assistant_reply = response.choices[0].message.content self.conversation_history.append( {"role": "assistant", "content": assistant_reply} ) return assistant_reply def learn_from_turn(self, user_message: str, assistant_reply: str): """Extract and store memories from a conversation turn.""" turn_text = f"User: {user_message}\nAssistant: {assistant_reply}" extracted = extract_memories(turn_text) stored_ids = [] for memory in extracted: memory_id = self.memory.store( text=memory["text"], user_id=self.user_id, memory_type=memory["memory_type"], importance=memory["importance"], session_id=self.session_id ) stored_ids.append(memory_id) # Link related memories within this turn for i in range(len(stored_ids) - 1): self.memory.link( stored_ids[i], stored_ids[i + 1], edge_type="same_session", weight=0.6 ) return len(stored_ids) def chat_and_learn(self, user_message: str) -> str: """Chat with the user and automatically store memories from the turn.""" reply = self.chat(user_message) memories_stored = self.learn_from_turn(user_message, reply) print(f" [Memory: {memories_stored} facts stored]") return reply ``` ## Step 4: Running the Agent ``` def main(): print("AI Agent with Persistent Memory") print("=" * 40) user_id = input("Enter your user ID: ").strip() or "user_001" # Initialize agent — loads memory from previous sessions automatically agent = StatefulAgent(user_id=user_id) print("\nChat with the agent. Type 'quit' to exit.") print("Your memories persist between sessions.\n") while True: user_input = input(f"You: ").strip() if user_input.lower() in ["quit", "exit", "q"]: print(f"Session ended. Total memories: {agent.memory.count(user_id)}") break if not user_input: continue reply = agent.chat_and_learn(user_input) print(f"Agent: {reply}\n") if __name__ == "__main__": main() ``` ## Step 5: Testing Persistence Across Sessions Run the agent twice to verify memory persists: ``` # Session 1: # You: My name is Priya and I build fintech apps in Python # Agent: Nice to meet you, Priya! What kind of fintech applications are you working on? # [Memory: 2 facts stored] # You: I'm building a payment reconciliation system using FastAPI # Agent: That sounds interesting — payment reconciliation has some tricky edge cases... # [Memory: 1 facts stored] # You: quit # Session ended. Total memories: 3 # Session 2 (new Python process, same user_id): # Loaded 3 profile memories # You: Can you help me with my project? # Agent: Of course, Priya! Based on what you told me, you're building a payment # reconciliation system in FastAPI. What specifically do you need help with? # (Agent remembers from session 1 without being told again) ``` ## Production Patterns ### Memory deduplication ``` def store_if_novel(memory: MemoryManager, text: str, user_id: str, similarity_threshold: float = 0.92, **kwargs) -> Optional[str]: """Only store a memory if it's not already represented.""" similar = memory.recall(text, user_id, top_k=1) if similar and similar[0]['score'] > similarity_threshold: # Update importance of existing memory instead of duplicating memory.db.update_importance( similar[0]['id'], similar[0]['importance'] + 0.05 # Bump importance on re-mention ) return None # Not stored (duplicate) return memory.store(text, user_id, **kwargs) ``` ### Multi-user deployment ``` # Option A: One file per user (recommended for clear isolation) agent_alice = StatefulAgent(user_id="alice", memory_path="memories_alice.feather") agent_bob = StatefulAgent(user_id="bob", memory_path="memories_bob.feather") # Option B: Shared file with namespace isolation # (All queries filter by user_id — never cross boundaries) agent_alice = StatefulAgent(user_id="alice", memory_path="shared_memories.feather") agent_bob = StatefulAgent(user_id="bob", memory_path="shared_memories.feather") ``` ### Async support for high-concurrency APIs ``` from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class ChatRequest(BaseModel): user_id: str message: str agents: dict[str, StatefulAgent] = {} # In production, use a proper cache @app.post("/chat") async def chat_endpoint(request: ChatRequest): if request.user_id not in agents: agents[request.user_id] = StatefulAgent(request.user_id) agent = agents[request.user_id] reply = agent.chat_and_learn(request.message) return { "reply": reply, "total_memories": agent.memory.count(request.user_id) } ``` ## Cost at Scale For a production deployment handling 1,000 sessions/day: - Feather DB retrieval: $0 (in-process, no API cost) - Extraction LLM (GPT-4o-mini, ~200 tokens/turn): ~$0.0003/turn - Response LLM (GPT-4o, ~4,300 tokens/session): ~$0.022/session - Total: ~$0.022/session vs $0.285/session with full-context stuffing - Monthly savings at 1,000 sessions/day: ~$7,900/month ## FAQ ### How do I handle memory for a user who interacts with multiple agents? Use a single shared `.feather` file for the user with agent-scoped metadata. All agents query the same memory store but can filter by `agent_id` for agent-specific memories, or omit the filter to access the full user context. ### What happens if the agent stores a false memory? False memories (hallucinated facts stored as memories) are the main failure mode. Mitigate by: (1) only extracting facts directly stated by the user, not inferred by the assistant; (2) setting lower importance scores for extracted facts (0.5 vs 0.9 for user-explicit statements); (3) implementing a contradiction check that marks conflicting memories with a `contradicts` edge. ### How do I inspect what memories the agent has for a user? Use `db.search("*", filters={"user_id": user_id}, top_k=50)` to retrieve all memories sorted by relevance, or `db.get_all(filters={"user_id": user_id})` to retrieve every memory unsorted. ### Does this work with Claude / Anthropic models? Yes. Replace the OpenAI client with the Anthropic SDK and update the API calls. Feather DB is model-agnostic — it only affects the context block injected into the prompt, not the model itself. ### How do I prevent sensitive memories from being stored? Add a pre-storage filter using regex or a classifier to detect PII, financial data, or health information before calling `memory.store()`. Feather DB does not automatically redact sensitive content — this is an application-layer responsibility. --- *This is the machine-readable mirror of the theory post at [getfeather.store/theory/build-ai-agent-persistent-memory-python](https://getfeather.store/theory/build-ai-agent-persistent-memory-python). For the full Feather DB documentation, see [getfeather.store/llms-full.txt](https://getfeather.store/llms-full.txt).*