Skip to content

Conversation

@Amnah199
Copy link
Contributor

@Amnah199 Amnah199 commented Oct 28, 2025

Related Issues

  • partially fixes - #219

Proposed Changes:

  • This PR integrates mem0 as a memory store in Haystack, supporting basic add, retrieve, and delete operations.
  • It updates the memory_agents.Agent class to enable retrieving and persisting long-term conversational context.

Key Changes

  • Added memory support to memory_agents.Agent.
  • Before each execution, relevant memories are fetched from Mem0MemoryStore using the latest user message as the query.
  • Retrieved memories are injected into the conversation as [MEMORY #n] ChatMessage objects.
  • When memories exist, the system prompt is updated to tell the LLM that [MEMORY] messages represent long-term context.
  • After each run, new user and assistant messages are saved to the memory store.
  • Memory is optional; agents behave normally when memory_store=None.

How did you test it?

  • Unit and Integration Tests
  • Tested locally with examples

Notes for the reviewer

Support for Memory.from_config will be revised in a separate PR as it has a slightly different implementation than mem0 MemoryClient.
https://docs.mem0.ai/platform/platform-vs-oss

Checklist

@coveralls
Copy link

coveralls commented Oct 28, 2025

Pull Request Test Coverage Report for Build 20066169239

Details

  • 0 of 0 changed or added relevant lines in 0 files are covered.
  • 94 unchanged lines in 1 file lost coverage.
  • Overall coverage decreased (-2.2%) to 71.082%

Files with Coverage Reduction New Missed Lines %
components/agents/agent.py 94 53.88%
Totals Coverage Status
Change from base Build 20012634360: -2.2%
Covered Lines: 1288
Relevant Lines: 1812

💛 - Coveralls

@Amnah199 Amnah199 marked this pull request as ready for review December 3, 2025 11:54
@Amnah199 Amnah199 requested a review from a team as a code owner December 3, 2025 11:54
@Amnah199 Amnah199 requested review from mpangrazzi and sjrl and removed request for a team and mpangrazzi December 3, 2025 11:54
@Amnah199 Amnah199 requested a review from sjrl December 9, 2025 14:08
@Amnah199 Amnah199 changed the title Add Mem0 integration / Support memory in Agent Add Mem0 integration - support for Mem0 platform Jan 15, 2026
confirmation_strategies: Optional[dict[str, ConfirmationStrategy]] = None,
tool_invoker_kwargs: Optional[dict[str, Any]] = None,
chat_message_store: Optional[ChatMessageStore] = None,
memory_store: Optional["Mem0MemoryStore"] = None,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should use the type defined in memory_stores.types.protocol

Comment on lines +1 to +3
# SPDX-FileCopyrightText: 2022-present deepset GmbH <[email protected]>
#
# SPDX-License-Identifier: Apache-2.0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should move this example out of this PR and it could be the start of the cookbook or tutorial

Comment on lines +1 to +2
from __future__ import annotations

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed?

Comment on lines +58 to +59
if TYPE_CHECKING:
from haystack_experimental.memory_stores.mem0.memory_store import Mem0MemoryStore
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be dropped once this comment is addressed

result = {**exe_context.state.data}
if msgs := result.get("messages"):
result["last_message"] = msgs[-1]
result["messages"] = msgs
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be removed, it looks redundant. msgs is already equal to result["messages"]

Comment on lines +559 to +563
new_memories = [
message for message in msgs if message.role.value == "user" or message.role.value == "assistant"
]
if self._memory_store:
self._memory_store.add_memories(messages=new_memories, **memory_store_kwargs)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's create the new list only if it will be used. Also I think it's simpler to just throw out the system message, since that's what contains the retrieved memories currently right?

Suggested change
new_memories = [
message for message in msgs if message.role.value == "user" or message.role.value == "assistant"
]
if self._memory_store:
self._memory_store.add_memories(messages=new_memories, **memory_store_kwargs)
if self._memory_store:
new_memories = [
message for message in msgs if message.role.value != "system"
]
self._memory_store.add_memories(messages=new_memories, **memory_store_kwargs)

if msgs := result.get("messages"):
result["last_message"] = msgs[-1]
result["messages"] = msgs
result["last_message"] = msgs[-1] if msgs else None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revert back. The if msgs := result.get("messages"): already covers this. Using the walrus operator here means that if msgs is None then everything under the if statement is skipped

Suggested change
result["last_message"] = msgs[-1] if msgs else None
result["last_message"] = msgs[-1]

@sjrl
Copy link
Contributor

sjrl commented Jan 16, 2026

@Amnah199 some remaining tasks:

Search for memories in the store.
:param query: Text query to search for. If not provided, all memories may be returned.
:param filters: Backend-specific filter structure.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think ideally we don't directly use the backend-specific filter structure but instead use the Haystack filter syntax. I think we should reuse the filter structure we already have here https://docs.haystack.deepset.ai/docs/metadata-filtering

So action items would-be:

  • update this docstring to indicate that Haystack filters are used. Look at the protocol for DocumentStore for inspiration
  • update the Mem0MemoryStore to convert Haystack filters into ones used by Mem0. You can find an example of this how we do this for our OpenSearch integration here. Basically the normalize_filters function converts Haystack filters to OpenSearch filters.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the clear instructions!

Comment on lines +54 to +56
self,
*,
query: Optional[str] = None,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get the impression that query should always be required right? Or is there a way to call Mem0 without a query to search for memories?

user_id: Optional[str] = None,
run_id: Optional[str] = None,
agent_id: Optional[str] = None,
include_memory_metadata: bool = False,
Copy link
Contributor

@sjrl sjrl Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's replace this with just kwargs

Suggested change
include_memory_metadata: bool = False,
**kwargs: Any,

this way any specific implementation of MemoryStore can add additional keyword arguments and still satisfy the protocol.

And the include_memory_metadata doesn't feel appropriate to enforce for a generic protocol.

Comment on lines +34 to +41
def add_memories(
self,
*,
messages: list[ChatMessage],
user_id: Optional[str] = None,
run_id: Optional[str] = None,
agent_id: Optional[str] = None,
) -> list[str]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a good middle of the road approach here would be to change this to

Suggested change
def add_memories(
self,
*,
messages: list[ChatMessage],
user_id: Optional[str] = None,
run_id: Optional[str] = None,
agent_id: Optional[str] = None,
) -> list[str]:
def add_memories(
self,
*,
messages: list[ChatMessage],
user_id: Optional[str] = None,
**kwargs: Any,
) -> list[str]:

so we always require an implementation of MemoryStore at least has a user_id but then all other ids that may be specific to the specific backend aren't required for all implementations.

If you agree let's update all functions in this protocol to work this way


# Retrieve memories from the memory store
if self._memory_store:
retrieved_memory = self._memory_store.search_memories_as_single_message(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use the search_memories method here otherwise if we implement another backend MemoryStore it won't work with Agent since Agent relies on a method not defined in the protocol

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, but I think one of the concerns was that we wanted to pass memories as a single message instead of a bunch of system messages for the model to process memories more efficiently.
I am thinking we can add this method to the protocol perhaps.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right okay. Perhaps we can go back on what I said earlier and do the formatting of the single ChatMessage inside of agent here and drop the search_memories_as_single_message from the memory store

Comment on lines +154 to +170
if include_memory_metadata:
# we also include the mem0 related metadata i.e. memory_id, score, etc.
# metadata
for memory in memories["results"]:
meta = memory["metadata"].copy() if memory["metadata"] else {}
meta["retrieved_memory_metadata"] = memory.copy()
meta["retrieved_memory_metadata"].pop("memory")
messages = [
ChatMessage.from_system(text=memory["memory"], meta=meta) for memory in memories["results"]
]
else:
# we only include the metadata stored in the memory in ChatMessage
messages = [
ChatMessage.from_system(text=memory["memory"], meta=memory["metadata"])
for memory in memories["results"]
]
return messages
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can simplify this to

Suggested change
if include_memory_metadata:
# we also include the mem0 related metadata i.e. memory_id, score, etc.
# metadata
for memory in memories["results"]:
meta = memory["metadata"].copy() if memory["metadata"] else {}
meta["retrieved_memory_metadata"] = memory.copy()
meta["retrieved_memory_metadata"].pop("memory")
messages = [
ChatMessage.from_system(text=memory["memory"], meta=meta) for memory in memories["results"]
]
else:
# we only include the metadata stored in the memory in ChatMessage
messages = [
ChatMessage.from_system(text=memory["memory"], meta=memory["metadata"])
for memory in memories["results"]
]
return messages
messages = []
for memory in memories["results"]:
meta = memory["metadata"].copy() if memory["metadata"] else {}
# we also include the mem0 related metadata i.e. memory_id, score, etc.
if include_memory_metadata:
meta["retrieved_memory_metadata"] = memory.copy()
meta["retrieved_memory_metadata"].pop("memory")
messages.append(
ChatMessage.from_system(text=memory["memory"], meta=meta)
)
return messages


try:
self.client.delete_all(**ids, **kwargs)
logger.info(f"All memories deleted successfully for scope {ids}")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make sure to not use f-strings in log statements and use the expected Haystack format

Suggested change
logger.info(f"All memories deleted successfully for scope {ids}")
logger.info("All memories deleted successfully for scope {ids}", ids=ids)

"""
try:
self.client.delete(memory_id=memory_id, **kwargs)
logger.info(f"Memory {memory_id} deleted successfully")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
logger.info(f"Memory {memory_id} deleted successfully")
logger.info("Memory {memory_id} deleted successfully", memory_id=memory_id)

Comment on lines +234 to +235
# mem0 doesn't allow passing filter to delete endpoint,
# we can delete all memories for a user by passing the user_id
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes sense it doesn't all passing the filters otherwise it wouldn't be all right?

I think it's fine to remove this comment.

Comment on lines +79 to +85
def delete_all_memories(
self,
*,
user_id: Optional[str] = None,
run_id: Optional[str] = None,
agent_id: Optional[str] = None,
) -> None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that in your Mem0 implementation that you are exposing additional kwargs in this function so we should also add it to the protocol. Dropping run_id and agent_id based on this comment

Suggested change
def delete_all_memories(
self,
*,
user_id: Optional[str] = None,
run_id: Optional[str] = None,
agent_id: Optional[str] = None,
) -> None:
def delete_all_memories(
self,
*,
user_id: Optional[str] = None,
**kwargs: Any
) -> None:

"""
...

def delete_memory(self, memory_id: str) -> None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here I noticed that you expose kwargs in the Mem0 implementation so let's add it to the protocol

Suggested change
def delete_memory(self, memory_id: str) -> None:
def delete_memory(self, memory_id: str, **kwargs: Any) -> None:

Comment on lines +236 to +243
memory_text = f"Here are the relevant memories for the user's query: {retrieved_memory.text}"
updated_memory = ChatMessage.from_system(text=memory_text, meta=retrieved_memory.meta)
else:
updated_memory = None

combined_messages = messages + [updated_memory] if updated_memory else messages
if updated_system_prompt is not None:
combined_messages = [ChatMessage.from_system(updated_system_prompt)] + combined_messages
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this is a good idea. Typically only one system prompt is ever used in the Chat History.

Sorry if we discussed this before, but which model providers have you tested that this works with?

If we really want to put the memories in a system prompt (I'm still not 100% this is best idea) we should probably add them directly to the updated_system_prompt so then at least we have a single system message.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants