Merge pull request #4156 from pipecat-ai/mb/mem0-improvements

fix(mem0): improve Mem0 service reliability and add get_memories() method
This commit is contained in:
Mark Backman
2026-03-26 14:09:34 -04:00
committed by GitHub
8 changed files with 114 additions and 81 deletions

1
changelog/4156.added.md Normal file
View File

@@ -0,0 +1 @@
- Added `Mem0MemoryService.get_memories()` convenience method for retrieving all stored memories outside the pipeline (e.g. to build a personalized greeting at connection time). This avoids the need to manually handle client type branching, filter construction, and async wrapping.

View File

@@ -0,0 +1 @@
- ⚠️ Bumped `mem0ai` dependency from `~=0.1.94` to `>=1.0.8,<2`. Users of the `mem0` extra will need to update their mem0ai package.

View File

@@ -0,0 +1 @@
- Fixed `Mem0MemoryService` failing to store messages when the context contained system or developer role messages. The Mem0 API only accepts user and assistant roles, so other roles are now filtered out before storing.

1
changelog/4156.fixed.md Normal file
View File

@@ -0,0 +1 @@
- `Mem0MemoryService` no longer blocks the event loop during memory storage and retrieval. All Mem0 API calls now run in a background thread, and message storage is fire-and-forget so it doesn't delay downstream processing.

View File

@@ -42,7 +42,6 @@ The bot runs as part of a pipeline that processes audio frames and manages the c
"""
import os
from typing import Union
from dotenv import load_dotenv
from loguru import logger
@@ -69,58 +68,35 @@ from pipecat.transports.websocket.fastapi import FastAPIWebsocketParams
load_dotenv(override=True)
try:
from mem0 import Memory, MemoryClient # noqa: F401
except ModuleNotFoundError as e:
logger.error(f"Exception: {e}")
logger.error(
"In order to use Mem0, you need to `pip install mem0ai`. Also, set the environment variable MEM0_API_KEY."
)
raise Exception(f"Missing module: {e}")
async def get_initial_greeting(
memory_client: Union[MemoryClient, Memory], user_id: str, agent_id: str, run_id: str
) -> str:
async def get_initial_greeting(memory_service: Mem0MemoryService) -> str:
"""Fetch all memories for the user and create a personalized greeting.
Args:
memory_service: The Mem0 memory service instance.
Returns:
A personalized greeting based on user memories
A personalized greeting based on user memories.
"""
try:
if isinstance(memory_client, Memory):
filters = {"user_id": user_id, "agent_id": agent_id, "run_id": run_id}
filters = {k: v for k, v in filters.items() if v is not None}
memories = memory_client.get_all(**filters)
else:
# Create filters based on available IDs
id_pairs = [("user_id", user_id), ("agent_id", agent_id), ("run_id", run_id)]
clauses = [{name: value} for name, value in id_pairs if value is not None]
filters = {"AND": clauses} if clauses else {}
# Get all memories for this user
memories = memory_client.get_all(filters=filters, version="v2", output_format="v1.1")
if not memories or len(memories) == 0:
logger.debug(f"!!! No memories found for this user. {memories}")
results = await memory_service.get_memories()
if not results:
logger.debug("No memories found for this user.")
return "Hello! It's nice to meet you. How can I help you today?"
# Create a personalized greeting based on memories
greeting = "Hello! It's great to see you again. "
greeting += "Based on our previous conversations, I remember: "
for i, memory in enumerate(results[:3], 1):
memory_content = memory.get("memory", "")
# Keep memory references brief
if len(memory_content) > 100:
memory_content = memory_content[:97] + "..."
greeting += f"{memory_content} "
# Add some personalization based on memories (limit to 3 memories for brevity)
if len(memories) > 0:
greeting += "Based on our previous conversations, I remember: "
for i, memory in enumerate(memories["results"][:3], 1):
memory_content = memory.get("memory", "")
# Keep memory references brief
if len(memory_content) > 100:
memory_content = memory_content[:97] + "..."
greeting += f"{memory_content} "
greeting += "How can I help you today?"
greeting += "How can I help you today?"
logger.debug(f"Created personalized greeting from {len(memories)} memories")
logger.debug(f"Created personalized greeting from {len(results)} memories")
return greeting
except Exception as e:
@@ -265,22 +241,17 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
idle_timeout_secs=runner_args.pipeline_idle_timeout_secs,
)
@task.rtvi.event_handler("on_client_ready")
async def on_client_ready(rtvi):
# Get personalized greeting based on user memories. Can pass agent_id and run_id as per requirement of the application to manage short term memory or agent specific memory.
greeting = await get_initial_greeting(
memory_client=memory.memory_client, user_id=USER_ID, agent_id=None, run_id=None
)
# Add the greeting as an assistant message to start the conversation
context.add_message({"role": "assistant", "content": greeting})
# Queue the context frame to start the conversation
await task.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_connected")
async def on_client_connected(transport, client):
logger.info(f"Client connected")
# Get personalized greeting based on user memories
greeting = await get_initial_greeting(memory)
# Add the greeting as an assistant message to start the conversation
context.add_message({"role": "developer", "content": greeting})
# Queue the context frame to start the conversation
await task.queue_frames([LLMRunFrame()])
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(transport, client):

View File

@@ -88,7 +88,7 @@ lmnt = [ "pipecat-ai[websockets-base]" ]
local = [ "pyaudio~=0.2.14" ]
local-smart-turn = [ "coremltools>=8.0", "transformers>=4.48.0,<6", "torch>=2.5.0,<3", "torchaudio>=2.5.0,<3" ]
mcp = [ "mcp[cli]>=1.11.0,<2" ]
mem0 = [ "mem0ai~=0.1.94" ]
mem0 = [ "mem0ai>=1.0.8,<2" ]
mistral = []
mlx-whisper = [ "mlx-whisper~=0.4.2" ]
moondream = [ "accelerate~=1.10.0", "einops~=0.8.0", "pyvips[binary]~=3.0.0", "timm~=1.0.13", "transformers>=4.48.0,<6" ]

View File

@@ -11,6 +11,7 @@ and retrieve conversational memories, enhancing LLM context with relevant
historical information.
"""
import asyncio
from typing import Any, Dict, List, Optional
from loguru import logger
@@ -112,9 +113,51 @@ class Mem0MemoryService(FrameProcessor):
self.last_query = None
logger.info(f"Initialized Mem0MemoryService with {user_id=}, {agent_id=}, {run_id=}")
def _store_messages(self, messages: List[Dict[str, Any]]):
async def get_memories(self) -> List[Dict[str, Any]]:
"""Retrieve all stored memories for the configured user/agent/run IDs.
This is a convenience method for accessing memories outside the pipeline,
e.g. to build a personalized greeting at connection time. It wraps the
blocking Mem0 ``get_all()`` call in a background thread.
Returns:
List of memory dictionaries. Each dict contains at least a
``"memory"`` key with the memory text. Returns an empty list on
error.
"""
try:
if isinstance(self.memory_client, Memory):
params = {
"user_id": self.user_id,
"agent_id": self.agent_id,
"run_id": self.run_id,
}
params = {k: v for k, v in params.items() if v is not None}
memories = await asyncio.to_thread(lambda: self.memory_client.get_all(**params))
else:
id_pairs = [
("user_id", self.user_id),
("agent_id", self.agent_id),
("run_id", self.run_id),
]
clauses = [{name: value} for name, value in id_pairs if value is not None]
filters = {"OR": clauses} if clauses else {}
memories = await asyncio.to_thread(
lambda: self.memory_client.get_all(filters=filters)
)
results = memories.get("results", []) if isinstance(memories, dict) else memories
return results
except Exception as e:
logger.error(f"Error retrieving memories from Mem0: {e}")
return []
async def _store_messages(self, messages: List[Dict[str, Any]]):
"""Store messages in Mem0.
Runs the blocking Mem0 API call in a background thread to avoid
blocking the event loop.
Args:
messages: List of message dictionaries to store in memory.
"""
@@ -131,14 +174,16 @@ class Mem0MemoryService(FrameProcessor):
if isinstance(self.memory_client, Memory):
del params["output_format"]
# Note: You can run this in background to avoid blocking the conversation
self.memory_client.add(**params)
await asyncio.to_thread(lambda: self.memory_client.add(**params))
except Exception as e:
logger.error(f"Error storing messages in Mem0: {e}")
def _retrieve_memories(self, query: str) -> List[Dict[str, Any]]:
async def _retrieve_memories(self, query: str) -> List[Dict[str, Any]]:
"""Retrieve relevant memories from Mem0.
Runs the blocking Mem0 API call in a background thread to avoid
blocking the event loop.
Args:
query: The query to search for relevant memories.
@@ -156,7 +201,7 @@ class Mem0MemoryService(FrameProcessor):
"limit": self.search_limit,
}
params = {k: v for k, v in params.items() if v is not None}
results = self.memory_client.search(**params)
results = await asyncio.to_thread(lambda: self.memory_client.search(**params))
else:
id_pairs = [
("user_id", self.user_id),
@@ -165,13 +210,15 @@ class Mem0MemoryService(FrameProcessor):
]
clauses = [{name: value} for name, value in id_pairs if value is not None]
filters = {"OR": clauses} if clauses else {}
results = self.memory_client.search(
query=query,
filters=filters,
version=self.api_version,
top_k=self.search_limit,
threshold=self.search_threshold,
output_format="v1.1",
results = await asyncio.to_thread(
lambda: self.memory_client.search(
query=query,
filters=filters,
version=self.api_version,
top_k=self.search_limit,
threshold=self.search_threshold,
output_format="v1.1",
)
)
logger.debug(f"Retrieved {len(results)} memories from Mem0")
@@ -180,7 +227,9 @@ class Mem0MemoryService(FrameProcessor):
logger.error(f"Error retrieving memories from Mem0: {e}")
return []
def _enhance_context_with_memories(self, context: LLMContext | OpenAILLMContext, query: str):
async def _enhance_context_with_memories(
self, context: LLMContext | OpenAILLMContext, query: str
):
"""Enhance the LLM context with relevant memories.
Args:
@@ -193,7 +242,7 @@ class Mem0MemoryService(FrameProcessor):
self.last_query = query
memories = self._retrieve_memories(query)
memories = await self._retrieve_memories(query)
if not memories:
return
@@ -203,11 +252,14 @@ class Mem0MemoryService(FrameProcessor):
memory_text += f"{i}. {memory.get('memory', '')}\n\n"
# Add memories as a system message or user message based on configuration
if self.add_as_system_message:
context.add_message({"role": "system", "content": memory_text})
else:
# Add as a user message that provides context
context.add_message({"role": "user", "content": memory_text})
role = "system" if self.add_as_system_message else "user"
memory_message = {"role": role, "content": memory_text}
messages = context.get_messages()
position = max(0, min(self.position, len(messages)))
messages.insert(position, memory_message)
context.set_messages(messages)
logger.debug(f"Enhanced context with {len(memories)} memories")
async def process_frame(self, frame: Frame, direction: FrameDirection):
@@ -240,10 +292,15 @@ class Mem0MemoryService(FrameProcessor):
break
if latest_user_message:
# Filter to only user/assistant messages — Mem0 API
# doesn't accept other roles (system, developer, etc.)
messages_to_store = [
m for m in context_messages if m.get("role") in ("user", "assistant")
]
# Enhance context with memories before passing it downstream
self._enhance_context_with_memories(context, latest_user_message)
# Store the conversation in Mem0. Only call this when user message is detected
self._store_messages(context_messages)
await self._enhance_context_with_memories(context, latest_user_message)
# Store the conversation in Mem0 as a background task
self.create_task(self._store_messages(messages_to_store), name="mem0_store")
# If we received an LLMMessagesFrame, create a new one with the enhanced messages
if messages is not None:

9
uv.lock generated
View File

@@ -3514,19 +3514,20 @@ wheels = [
[[package]]
name = "mem0ai"
version = "0.1.115"
version = "1.0.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "openai" },
{ name = "posthog" },
{ name = "protobuf" },
{ name = "pydantic" },
{ name = "pytz" },
{ name = "qdrant-client" },
{ name = "sqlalchemy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/72/2a/ccf4a8b79a4e67d3a2475599ff8276d3bdccce5c5da2e14deb449e7dfb1a/mem0ai-0.1.115.tar.gz", hash = "sha256:147a6593604188acd30281c40171112aed9f16e196fa528627430c15e00f1e32", size = 115605, upload-time = "2025-07-24T09:49:10.467Z" }
sdist = { url = "https://files.pythonhosted.org/packages/b7/a6/292b42445cf2f5fb2207d523e31a823c10d5da2e939ece78dac0800edfb1/mem0ai-1.0.8.tar.gz", hash = "sha256:9af38c30b0250b3401f58a6004debf1f84f976b43fc4c6d830700c42b75af54c", size = 198898, upload-time = "2026-03-26T16:54:59.058Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/85/d5/55a5504077c175f4ff18259672df6fd0eae863236d730c87677d6de43f9a/mem0ai-0.1.115-py3-none-any.whl", hash = "sha256:29310bd5bcab644f7a4dbf87bd1afd878eb68458a2fb36cfcbf20bdff46fbdaf", size = 178065, upload-time = "2025-07-24T09:49:08.54Z" },
{ url = "https://files.pythonhosted.org/packages/85/b6/46f94bfa5863e86a1f3f77815728529d2e5b3851e259c611182e2c8c81ea/mem0ai-1.0.8-py3-none-any.whl", hash = "sha256:4852f403d213e5cb463454940f92211eadb6e38c75f2f1ec17f279033d3ba194", size = 296802, upload-time = "2026-03-26T16:54:57.356Z" },
]
[[package]]
@@ -4922,7 +4923,7 @@ requires-dist = [
{ name = "loguru", specifier = "~=0.7.3" },
{ name = "markdown", specifier = ">=3.7,<4" },
{ name = "mcp", extras = ["cli"], marker = "extra == 'mcp'", specifier = ">=1.11.0,<2" },
{ name = "mem0ai", marker = "extra == 'mem0'", specifier = "~=0.1.94" },
{ name = "mem0ai", marker = "extra == 'mem0'", specifier = ">=1.0.8,<2" },
{ name = "mlx-whisper", marker = "extra == 'mlx-whisper'", specifier = "~=0.4.2" },
{ name = "nltk", specifier = ">=3.9.4,<4" },
{ name = "noisereduce", marker = "extra == 'noisereduce'", specifier = "~=3.0.3" },