Spin up a fitness coach that remembers goals, adapts tone, and keeps sessions personal.
Essentially, creating a companion out of LLMs is as simple as a loop. But these loops work great for one type of character without personalization and fall short as soon as you restart the chat.Problem: LLMs are stateless. GPT doesn’t remember conversations. You could stuff everything inside the context window, but that becomes slow, expensive, and breaks at scale.The solution: Mem0. It extracts and stores what matters from conversations, then retrieves it when needed. Your companion remembers user preferences, past events, and history.In this cookbook we’ll build a fitness companion that:
Remembers user goals across sessions
Recalls past workouts and progress
Adapts its personality based on user preferences
Handles both short-term context (today’s chat) and long-term memory (months of history)
By the end, you’ll have a working fitness companion and know how to handle common production challenges.
Max wants to train for a marathon. He starts chatting with Ray, an AI running coach.
Copy
Ask AI
from openai import OpenAIfrom mem0 import MemoryClientopenai_client = OpenAI(api_key="your-openai-key")mem0_client = MemoryClient(api_key="your-mem0-key")def chat(user_input, user_id): # Retrieve relevant memories memories = mem0_client.search(user_input, user_id=user_id, limit=5) context = "\\n".join(m["memory"] for m in memories["results"]) # Call LLM with memory context response = openai_client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": f"You're Ray, a running coach. Memories:\\n{context}"}, {"role": "user", "content": user_input} ] ).choices[0].message.content # Store the exchange mem0_client.add([ {"role": "user", "content": user_input}, {"role": "assistant", "content": response} ], user_id=user_id) return response
Session 1:
Copy
Ask AI
chat("I want to run a marathon in under 4 hours", user_id="max")# Output: "That's a solid goal. What's your current weekly mileage?"# Stored in Mem0: "Max wants to run sub-4 marathon"
Session 2 (next day, app restarted):
Copy
Ask AI
chat("What should I focus on today?", user_id="max")# Output: "Based on your sub-4 marathon goal, let's work on building your aerobic base..."
Ray remembers Max’s goal across sessions. The app restarted, but the memory persisted. This is the core pattern: retrieve memories, pass them as context, store new exchanges.
Ray remembers. Restart the app, and the goal persists. From here on, we’ll focus on just the Mem0 API calls.
Max mentions his knee hurts. That’s different from his marathon goal - one is temporary, the other is long-term.Categories vs Metadata:
Categories: AI-assigned by Mem0 based on content (you can’t force them)
Metadata: Manually set by you for forced tagging
Define custom categories at the project level. Mem0 will automatically tag memories with relevant categories based on content:
Copy
Ask AI
mem0_client.project.update(custom_categories=[ {"goals": "Race targets and training objectives"}, {"constraints": "Injuries, limitations, recovery needs"}, {"preferences": "Training style, surfaces, schedules"}])
Categories vs Metadata: Categories are AI-assigned by Mem0 based on content semantics. You define the palette, Mem0 picks which ones apply. If you need guaranteed tagging, use metadata instead.
Now when you add memories, Mem0 automatically assigns the appropriate categories:
Copy
Ask AI
# Add goal - Mem0 automatically tags it as "goals"mem0_client.add( [{"role": "user", "content": "Sub-4 marathon is my A-race"}], user_id="max")# Add constraint - Mem0 automatically tags it as "constraints"mem0_client.add( [{"role": "user", "content": "My right knee flares up on downhills"}], user_id="max")
Mem0 reads the content and intelligently picks which categories apply. You define the palette, it handles the tagging.Important: You cannot force specific categories. Mem0’s platform decides which categories are relevant based on content. If you need to force-tag something, use metadata instead:
Copy
Ask AI
# Force tag using metadata (not categories)mem0_client.add( [{"role": "user", "content": "Some workout note"}], user_id="max", metadata={"workout_type": "speed", "forced_tag": "custom_label"})
constraints = mem0_client.search( query="injury concerns", filters={ "AND": [ {"user_id": "max"}, {"categories": {"in": ["constraints"]}} ] }, threshold=0.0 # optional: widen recall for short phrases)print([m["memory"] for m in constraints["results"]])# Output: ["Max's right knee flares up on downhills"]
Ray can plan workouts that avoid aggravating Max’s knee, without pulling in race goals or other unrelated memories.
Run the basic loop for a week and check what’s stored:
Copy
Ask AI
memories = mem0_client.get_all(filters={"AND": [{"user_id": "max"}]})print([m["memory"] for m in memories["results"]])# Output: ["Max wants to run marathon under 4 hours", "hey", "lol ok", "cool thanks", "gtg bye"]
Without filters, Mem0 stores everything—greetings, filler, and casual chat. This pollutes retrieval: instead of pulling “marathon goal,” you get “lol ok.” Set custom instructions to keep memory clean.
mem0_client.project.update(custom_instructions="""Extract from running coach conversations:- Training goals and race targets- Physical constraints or injuries- Training preferences (time of day, surfaces, weather)- Progress milestonesExclude:- Greetings and filler- Casual chatter- Hypotheticals unless planning related""")
Now chat again:
Copy
Ask AI
chat("hey how's it going", user_id="max")chat("I prefer trail running over roads", user_id="max")memories = mem0_client.get_all(filters={"AND": [{"user_id": "max"}]})print([m["memory"] for m in memories["results"]])# Output: ["Max wants to run marathon under 4 hours", "Max prefers trail running over roads"]
Expected output: Only 2 memories stored—the marathon goal and trail preference. The greeting “hey how’s it going” was filtered out automatically. Custom instructions are working.
Only meaningful facts. Filler gets dropped automatically.
Max prefers direct feedback, not motivational fluff. Ray needs to remember how to communicate - that’s agent memory, separate from user memory.Store agent personality:
# Get coach personalityagent_memories = mem0_client.search("coaching style", agent_id="ray_coach")# Output: ["Max wants direct, data-driven feedback. Skip motivational language."]# Store conversations with agent_idmem0_client.add([ {"role": "user", "content": "How'd my run look today?"}, {"role": "assistant", "content": "Pace was 8:15/mile. Heart rate 152, zone 2."}], user_id="max", agent_id="ray_coach")
Expected behavior: Ray’s responses are now data-driven and direct. The agent memory stored the coaching style preference, so future responses adapt automatically without Max having to repeat his preference.
No “Great job!” or “Keep it up!” - just data. Ray adapts to Max’s preference.
Don’t send every single message to Mem0. Keep recent context in memory, let Mem0 handle the important long-term facts.
Copy
Ask AI
# Store only meaningful exchanges in Mem0mem0_client.add([ {"role": "user", "content": "I want to run a marathon"}, {"role": "assistant", "content": "Let's build a training plan"}], user_id="max")# Skip storing filler# "hey" → don't store# "cool thanks" → don't store# Or rely on custom_instructions to filter automatically
Last 10 messages in your app’s buffer. Important facts in Mem0. Faster, cheaper, still works.
mem0_client.add([ {"role": "user", "content": "I want to run a sub-4 marathon"}, {"role": "assistant", "content": "Got it. Let's build a training plan."}], user_id="max", agent_id="ray", categories=["goals"])mem0_client.add([ {"role": "user", "content": "I prefer trail running over roads"}], user_id="max", categories=["preferences"])
old_logs = [ [{"role": "user", "content": "Completed 20-mile long run"}], [{"role": "user", "content": "Hit 8:00 pace on tempo run"}],]for log in old_logs: mem0_client.add(log, user_id="max")
# Find the old memorymemories = mem0_client.get_all(filters={"AND": [{"user_id": "max"}]})goal_memory = [m for m in memories["results"] if "sub-4" in m["memory"]][0]# Update itmem0_client.update(goal_memory["id"], "Max wants to run sub-3:45 marathon")
Scales to production - batching, metadata, pruning
This pattern works for any companion: fitness coaches, tutors, roleplay characters, therapy bots, creative writing partners.
Start with 2-3 categories max (e.g., goals, constraints, preferences). More categories dilute tagging accuracy. You can always add more later after seeing what Mem0 extracts.