From c8f9f20dffc7bb5b8e374bb783e8e097362c7027 Mon Sep 17 00:00:00 2001 From: Parshva Daftari <89991302+parshvadaftari@users.noreply.github.com> Date: Tue, 2 Sep 2025 04:26:43 +0530 Subject: [PATCH 01/18] Updated integration docs (#3392) --- .../mem0-google-adk-healthcare-assistant.mdx | 17 +++-- docs/integrations/agentops.mdx | 4 +- docs/integrations/agno.mdx | 17 ++--- docs/integrations/autogen.mdx | 25 ++++--- docs/integrations/elevenlabs.mdx | 2 +- docs/integrations/google-ai-adk.mdx | 27 ++++--- docs/integrations/langchain.mdx | 70 +++++++++++-------- docs/integrations/langgraph.mdx | 62 +++++++++++----- docs/integrations/llama-index.mdx | 32 +++++---- docs/integrations/pipecat.mdx | 2 +- 10 files changed, 163 insertions(+), 95 deletions(-) diff --git a/docs/examples/mem0-google-adk-healthcare-assistant.mdx b/docs/examples/mem0-google-adk-healthcare-assistant.mdx index 169f2b41b..c6b40ac1b 100644 --- a/docs/examples/mem0-google-adk-healthcare-assistant.mdx +++ b/docs/examples/mem0-google-adk-healthcare-assistant.mdx @@ -24,8 +24,7 @@ Before you begin, make sure you have: Installed Google ADK and Mem0 SDK: ```bash -pip install google-adk -pip install mem0ai +pip install google-adk mem0ai python-dotenv ``` ## Code Breakdown @@ -35,21 +34,25 @@ Let's get started and understand the different components required in building a ```python # Import dependencies import os +import asyncio from google.adk.agents import Agent -from google.adk.sessions import InMemorySessionService from google.adk.runners import Runner +from google.adk.sessions import InMemorySessionService from google.genai import types from mem0 import MemoryClient +from dotenv import load_dotenv -# Set up API keys (replace with your actual keys) -os.environ["GOOGLE_API_KEY"] = "your-google-api-key" -os.environ["MEM0_API_KEY"] = "your-mem0-api-key" +load_dotenv() + +# Set up environment variables +# os.environ["GOOGLE_API_KEY"] = "your-google-api-key" +# os.environ["MEM0_API_KEY"] = "your-mem0-api-key" # Define a global user ID for simplicity USER_ID = "Alex" # Initialize Mem0 client -mem0_client = MemoryClient() +mem0 = MemoryClient() ``` ## Define Memory Tools diff --git a/docs/integrations/agentops.mdx b/docs/integrations/agentops.mdx index 315cca554..ba25c4057 100644 --- a/docs/integrations/agentops.mdx +++ b/docs/integrations/agentops.mdx @@ -17,7 +17,7 @@ Before setting up Mem0 with AgentOps, ensure you have: 1. Installed the required packages: ```bash -pip install mem0ai agentops +pip install mem0ai agentops python-dotenv ``` 2. Valid API keys: @@ -37,7 +37,9 @@ import asyncio import logging from dotenv import load_dotenv import agentops +import openai +load_dotenv() #Set up environment variables for API keys os.environ["AGENTOPS_API_KEY"] = os.getenv("AGENTOPS_API_KEY") os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY") diff --git a/docs/integrations/agno.mdx b/docs/integrations/agno.mdx index e35e94bf9..f04c69aa4 100644 --- a/docs/integrations/agno.mdx +++ b/docs/integrations/agno.mdx @@ -18,7 +18,7 @@ Before setting up Mem0 with Agno, ensure you have: 1. Installed the required packages: ```bash -pip install agno mem0ai +pip install agno mem0ai python-dotenv ``` 2. Valid API keys: @@ -81,7 +81,7 @@ agent = Agent( def chat_user( user_input: Optional[str] = None, - user_id: str = "user_123", + user_id: str = "alex", image_path: Optional[str] = None ) -> str: """ @@ -120,13 +120,13 @@ def chat_user( }) # Store messages in memory - client.add(messages, user_id=user_id) + client.add(messages, user_id=user_id, output_format='v1.1') print("✅ Image and text stored in memory.") if user_input: # Search for relevant memories - memories = client.search(user_input, user_id=user_id) - memory_context = "\n".join(f"- {m['memory']}" for m in memories.get('results', [])) + memories = client.search(user_input, user_id=user_id, output_format='v1.1') + memory_context = "\n".join(f"- {m['memory']}" for m in memories['results']) # Construct the prompt prompt = f""" @@ -150,7 +150,8 @@ User question: response = agent.run(prompt) # Store the interaction in memory - client.add(f"User: {user_input}\nAssistant: {response.content}", user_id=user_id) + interaction_message = [{"role": "user", "content": f"User: {user_input}\nAssistant: {response.content}"}] + client.add(interaction_message, user_id=user_id, output_format='v1.1') return response.content return "No user input or image provided." @@ -159,9 +160,9 @@ User question: # Example Usage if __name__ == "__main__": response = chat_user( - "This is the picture of what I brought with me in the trip to Bahamas", + "I like to travel and my favorite destination is London", image_path="travel_items.jpeg", - user_id="user_123" + user_id="alex" ) print(response) ``` diff --git a/docs/integrations/autogen.mdx b/docs/integrations/autogen.mdx index 82e407f45..5fc38fc7b 100644 --- a/docs/integrations/autogen.mdx +++ b/docs/integrations/autogen.mdx @@ -1,3 +1,7 @@ +--- +title: AutoGen +--- + Build conversational AI agents with memory capabilities. This integration combines AutoGen for creating AI agents with Mem0 for memory management, enabling context-aware and personalized interactions. ## Overview @@ -10,7 +14,7 @@ In this guide, we'll explore an example of creating a conversational AI system w Install necessary libraries: ```bash -pip install pyautogen mem0ai openai +pip install autogen mem0ai openai python-dotenv ``` First, we'll import the necessary libraries and set up our configurations. @@ -22,15 +26,18 @@ import os from autogen import ConversableAgent from mem0 import MemoryClient from openai import OpenAI +from dotenv import load_dotenv + +load_dotenv() # Configuration -OPENAI_API_KEY = 'sk-xxx' # Replace with your actual OpenAI API key -MEM0_API_KEY = 'your-mem0-key' # Replace with your actual Mem0 API key from https://app.mem0.ai -USER_ID = "customer_service_bot" +# OPENAI_API_KEY = 'sk-xxx' # Replace with your actual OpenAI API key +# MEM0_API_KEY = 'your-mem0-key' # Replace with your actual Mem0 API key from https://app.mem0.ai +USER_ID = "alice" # Set up OpenAI API key -os.environ['OPENAI_API_KEY'] = OPENAI_API_KEY -os.environ['MEM0_API_KEY'] = MEM0_API_KEY +OPENAI_API_KEY = os.environ.get('OPENAI_API_KEY') +# os.environ['MEM0_API_KEY'] = MEM0_API_KEY # Initialize Mem0 and AutoGen agents memory_client = MemoryClient() @@ -55,7 +62,7 @@ conversation = [ {"role": "assistant", "content": "Thank you for the information. Let's troubleshoot this issue..."} ] -memory_client.add(messages=conversation, user_id=USER_ID) +memory_client.add(messages=conversation, user_id=USER_ID, output_format="v1.1") print("Conversation added to memory.") ``` @@ -65,7 +72,7 @@ Create a function to get context-aware responses based on user's question and pr ```python def get_context_aware_response(question): - relevant_memories = memory_client.search(question, user_id=USER_ID) + relevant_memories = memory_client.search(question, user_id=USER_ID, output_format='v1.1') context = "\n".join([m["memory"] for m in relevant_memories.get('results', [])]) prompt = f"""Answer the user question considering the previous interactions: @@ -97,7 +104,7 @@ manager = ConversableAgent( ) def escalate_to_manager(question): - relevant_memories = memory_client.search(question, user_id=USER_ID) + relevant_memories = memory_client.search(question, user_id=USER_ID, output_format='v1.1') context = "\n".join([m["memory"] for m in relevant_memories.get('results', [])]) prompt = f""" diff --git a/docs/integrations/elevenlabs.mdx b/docs/integrations/elevenlabs.mdx index 6e6710fa3..ede81687b 100644 --- a/docs/integrations/elevenlabs.mdx +++ b/docs/integrations/elevenlabs.mdx @@ -16,7 +16,7 @@ In this guide, we'll build a voice agent that: Install necessary libraries: ```bash -pip install elevenlabs mem0 python-dotenv +pip install elevenlabs mem0ai python-dotenv ``` Configure your environment variables: diff --git a/docs/integrations/google-ai-adk.mdx b/docs/integrations/google-ai-adk.mdx index 7220a34d4..59e317770 100644 --- a/docs/integrations/google-ai-adk.mdx +++ b/docs/integrations/google-ai-adk.mdx @@ -17,7 +17,7 @@ Before setting up Mem0 with Google ADK, ensure you have: 1. Installed the required packages: ```bash -pip install google-adk mem0ai +pip install google-adk mem0ai python-dotenv ``` 2. Valid API keys: @@ -30,15 +30,19 @@ The following example demonstrates how to create a Google ADK agent with Mem0 me ```python import os +import asyncio from google.adk.agents import Agent from google.adk.runners import Runner from google.adk.sessions import InMemorySessionService from google.genai import types from mem0 import MemoryClient +from dotenv import load_dotenv + +load_dotenv() # Set up environment variables -os.environ["GOOGLE_API_KEY"] = "your-google-api-key" -os.environ["MEM0_API_KEY"] = "your-mem0-api-key" +# os.environ["GOOGLE_API_KEY"] = "your-google-api-key" +# os.environ["MEM0_API_KEY"] = "your-mem0-api-key" # Initialize Mem0 client mem0 = MemoryClient() @@ -46,17 +50,18 @@ mem0 = MemoryClient() # Define memory function tools def search_memory(query: str, user_id: str) -> dict: """Search through past conversations and memories""" - memories = mem0.search(query, user_id=user_id) + memories = mem0.search(query, user_id=user_id, output_format='v1.1') if memories.get('results', []): - memory_context = "\n".join([f"- {mem['memory']}" for mem in memories.get('results', [])]) + memory_list = memories['results'] + memory_context = "\n".join([f"- {mem['memory']}" for mem in memory_list]) return {"status": "success", "memories": memory_context} return {"status": "no_memories", "message": "No relevant memories found"} def save_memory(content: str, user_id: str) -> dict: """Save important information to memory""" try: - mem0.add([{"role": "user", "content": content}], user_id=user_id) - return {"status": "success", "message": "Information saved to memory"} + result = mem0.add([{"role": "user", "content": content}], user_id=user_id, output_format='v1.1') + return {"status": "success", "message": "Information saved to memory", "result": result} except Exception as e: return {"status": "error", "message": f"Failed to save memory: {str(e)}"} @@ -72,7 +77,7 @@ personal_assistant = Agent( tools=[search_memory, save_memory] ) -def chat_with_agent(user_input: str, user_id: str) -> str: +async def chat_with_agent(user_input: str, user_id: str) -> str: """ Handle user input with automatic memory integration. @@ -85,7 +90,7 @@ def chat_with_agent(user_input: str, user_id: str) -> str: """ # Set up session and runner session_service = InMemorySessionService() - session = session_service.create_session( + session = await session_service.create_session( app_name="memory_assistant", user_id=user_id, session_id=f"session_{user_id}" @@ -107,10 +112,10 @@ def chat_with_agent(user_input: str, user_id: str) -> str: # Example usage if __name__ == "__main__": - response = chat_with_agent( + response = asyncio.run(chat_with_agent( "I love Italian food and I'm planning a trip to Rome next month", user_id="alice" - ) + )) print(response) ``` diff --git a/docs/integrations/langchain.mdx b/docs/integrations/langchain.mdx index b485e4167..f79499e7a 100644 --- a/docs/integrations/langchain.mdx +++ b/docs/integrations/langchain.mdx @@ -16,7 +16,7 @@ In this guide, we'll create a Travel Agent AI that: Install necessary libraries: ```bash -pip install langchain langchain_openai mem0ai +pip install langchain langchain_openai mem0ai python-dotenv ``` Import required modules and set up configurations: @@ -30,10 +30,13 @@ from langchain_openai import ChatOpenAI from langchain_core.messages import SystemMessage, HumanMessage, AIMessage from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder from mem0 import MemoryClient +from dotenv import load_dotenv + +load_dotenv() # Configuration -os.environ["OPENAI_API_KEY"] = "your-openai-api-key" -os.environ["MEM0_API_KEY"] = "your-mem0-api-key" +# os.environ["OPENAI_API_KEY"] = "your-openai-api-key" +# os.environ["MEM0_API_KEY"] = "your-mem0-api-key" # Initialize LangChain and Mem0 llm = ChatOpenAI(model="gpt-4o-mini") @@ -61,19 +64,26 @@ Create functions to handle context retrieval, response generation, and addition ```python def retrieve_context(query: str, user_id: str) -> List[Dict]: """Retrieve relevant context from Mem0""" - memories = mem0.search(query, user_id=user_id) - serialized_memories = ' '.join([mem["memory"] for mem in memories.get('results', [])]) - context = [ - { - "role": "system", - "content": f"Relevant information: {serialized_memories}" - }, - { - "role": "user", - "content": query - } - ] - return context + try: + memories = mem0.search(query, user_id=user_id, output_format='v1.1') + memory_list = memories['results'] + + serialized_memories = ' '.join([mem["memory"] for mem in memory_list]) + context = [ + { + "role": "system", + "content": f"Relevant information: {serialized_memories}" + }, + { + "role": "user", + "content": query + } + ] + return context + except Exception as e: + print(f"Error retrieving memories: {e}") + # Return empty context if there's an error + return [{"role": "user", "content": query}] def generate_response(input: str, context: List[Dict]) -> str: """Generate a response using the language model""" @@ -86,17 +96,21 @@ def generate_response(input: str, context: List[Dict]) -> str: def save_interaction(user_id: str, user_input: str, assistant_response: str): """Save the interaction to Mem0""" - interaction = [ - { - "role": "user", - "content": user_input - }, - { - "role": "assistant", - "content": assistant_response - } - ] - mem0.add(interaction, user_id=user_id) + try: + interaction = [ + { + "role": "user", + "content": user_input + }, + { + "role": "assistant", + "content": assistant_response + } + ] + result = mem0.add(interaction, user_id=user_id, output_format='v1.1') + print(f"Memory saved successfully: {len(result.get('results', []))} memories added") + except Exception as e: + print(f"Error saving interaction: {e}") ``` ## Create Chat Turn Function @@ -124,7 +138,7 @@ Set up the main program loop for user interaction: ```python if __name__ == "__main__": print("Welcome to your personal Travel Agent Planner! How can I assist you with your travel plans today?") - user_id = "john" + user_id = "alice" while True: user_input = input("You: ") diff --git a/docs/integrations/langgraph.mdx b/docs/integrations/langgraph.mdx index c4d5f5a4e..0755dacee 100644 --- a/docs/integrations/langgraph.mdx +++ b/docs/integrations/langgraph.mdx @@ -16,7 +16,7 @@ In this guide, we'll create a Customer Support AI Agent that: Install necessary libraries: ```bash -pip install langgraph langchain-openai mem0ai +pip install langgraph langchain-openai mem0ai python-dotenv ``` @@ -31,14 +31,17 @@ from langgraph.graph.message import add_messages from langchain_openai import ChatOpenAI from mem0 import MemoryClient from langchain_core.messages import SystemMessage, HumanMessage, AIMessage +from dotenv import load_dotenv + +load_dotenv() # Configuration -OPENAI_API_KEY = 'sk-xxx' # Replace with your actual OpenAI API key -MEM0_API_KEY = 'your-mem0-key' # Replace with your actual Mem0 API key +# OPENAI_API_KEY = 'sk-xxx' # Replace with your actual OpenAI API key +# MEM0_API_KEY = 'your-mem0-key' # Replace with your actual Mem0 API key # Initialize LangChain and Mem0 -llm = ChatOpenAI(model="gpt-4", api_key=OPENAI_API_KEY) -mem0 = MemoryClient(api_key=MEM0_API_KEY) +llm = ChatOpenAI(model="gpt-4") +mem0 = MemoryClient() ``` ## Define State and Graph @@ -62,22 +65,47 @@ def chatbot(state: State): messages = state["messages"] user_id = state["mem0_user_id"] - # Retrieve relevant memories - memories = mem0.search(messages[-1].content, user_id=user_id) + try: + # Retrieve relevant memories + memories = mem0.search(messages[-1].content, user_id=user_id, output_format='v1.1') + + # Handle dict response format + memory_list = memories['results'] - context = "Relevant information from previous conversations:\n" - for memory in memories.get('results', []): - context += f"- {memory['memory']}\n" + context = "Relevant information from previous conversations:\n" + for memory in memory_list: + context += f"- {memory['memory']}\n" - system_message = SystemMessage(content=f"""You are a helpful customer support assistant. Use the provided context to personalize your responses and remember user preferences and past interactions. + system_message = SystemMessage(content=f"""You are a helpful customer support assistant. Use the provided context to personalize your responses and remember user preferences and past interactions. {context}""") - full_messages = [system_message] + messages - response = llm.invoke(full_messages) + full_messages = [system_message] + messages + response = llm.invoke(full_messages) - # Store the interaction in Mem0 - mem0.add(f"User: {messages[-1].content}\nAssistant: {response.content}", user_id=user_id) - return {"messages": [response]} + # Store the interaction in Mem0 + try: + interaction = [ + { + "role": "user", + "content": messages[-1].content + }, + { + "role": "assistant", + "content": response.content + } + ] + result = mem0.add(interaction, user_id=user_id, output_format='v1.1') + print(f"Memory saved: {len(result.get('results', []))} memories added") + except Exception as e: + print(f"Error saving memory: {e}") + + return {"messages": [response]} + + except Exception as e: + print(f"Error in chatbot: {e}") + # Fallback response without memory context + response = llm.invoke(messages) + return {"messages": [response]} ``` ## Set Up Graph Structure @@ -115,7 +143,7 @@ Set up the main program loop for user interaction: ```python if __name__ == "__main__": print("Welcome to Customer Support! How can I assist you today?") - mem0_user_id = "customer_123" # You can generate or retrieve this based on your user management system + mem0_user_id = "alice" # You can generate or retrieve this based on your user management system while True: user_input = input("You: ") if user_input.lower() in ['quit', 'exit', 'bye']: diff --git a/docs/integrations/llama-index.mdx b/docs/integrations/llama-index.mdx index 472f3f7d2..8316a449d 100644 --- a/docs/integrations/llama-index.mdx +++ b/docs/integrations/llama-index.mdx @@ -13,7 +13,7 @@ LlamaIndex supports Mem0 as a [memory store](https://llamahub.ai/l/memory/llama- To install the required package, run: ```bash -pip install llama-index-core llama-index-memory-mem0 +pip install llama-index-core llama-index-memory-mem0 python-dotenv ``` ### Setup with Mem0 Platform @@ -25,18 +25,23 @@ Set your Mem0 Platform API key as an environment variable. You can replace ` ```python -os.environ["MEM0_API_KEY"] = "" +from dotenv import load_dotenv +import os + +load_dotenv() + +# os.environ["MEM0_API_KEY"] = "" ``` Import the necessary modules and create a Mem0Memory instance: ```python from llama_index.memory.mem0 import Mem0Memory -context = {"user_id": "user_1"} +context = {"user_id": "alice"} memory_from_client = Mem0Memory.from_client( context=context, - api_key="", search_msg_limit=4, # optional, default is 5 + output_format='v1.1', # Remove deprecation warnings ) ``` @@ -44,8 +49,8 @@ Context is used to identify the user, agent or the conversation in the Mem0. It ```python context = { - "user_id": "user_1", - "agent_id": "agent_1", + "user_id": "alice", + "agent_id": "llama_agent_1", "run_id": "run_1", } ``` @@ -98,17 +103,20 @@ memory_from_config = Mem0Memory.from_config( context=context, config=config, search_msg_limit=4, # optional, default is 5 + output_format='v1.1', # Remove deprecation warnings ) ``` Initialize the LLM ```python -import os from llama_index.llms.openai import OpenAI +from dotenv import load_dotenv -os.environ["OPENAI_API_KEY"] = "" -llm = OpenAI(model="gpt-4o") +load_dotenv() + +# os.environ["OPENAI_API_KEY"] = "" +llm = OpenAI(model="gpt-4o-mini") ``` ### SimpleChatEngine @@ -122,7 +130,7 @@ agent = SimpleChatEngine.from_defaults( ) # Start the chat -response = agent.chat("Hi, My name is Mayank") +response = agent.chat("Hi, My name is Alice") print(response) ``` Now we will learn how to use Mem0 with FunctionCalling and ReAct agents. @@ -165,7 +173,7 @@ agent = FunctionCallingAgent.from_tools( ) # Start the chat -response = agent.chat("Hi, My name is Mayank") +response = agent.chat("Hi, My name is Alice") print(response) ``` @@ -182,7 +190,7 @@ agent = ReActAgent.from_tools( ) # Start the chat -response = agent.chat("Hi, My name is Mayank") +response = agent.chat("Hi, My name is Alice") print(response) ``` diff --git a/docs/integrations/pipecat.mdx b/docs/integrations/pipecat.mdx index 231451b08..626edb29b 100644 --- a/docs/integrations/pipecat.mdx +++ b/docs/integrations/pipecat.mdx @@ -92,7 +92,7 @@ async def websocket_endpoint(websocket: WebSocket): await websocket.accept() # Basic setup with minimal configuration - user_id = "user123" + user_id = "alice" # WebSocket transport transport = FastAPIWebsocketTransport( From 64cbe84089887026d422f4144a767dfd3e193dd8 Mon Sep 17 00:00:00 2001 From: Parshva Daftari <89991302+parshvadaftari@users.noreply.github.com> Date: Tue, 2 Sep 2025 21:10:04 +0530 Subject: [PATCH 02/18] Updated favicon logo (#3398) --- docs/logo/favicon.png | Bin 67249 -> 76952 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/logo/favicon.png b/docs/logo/favicon.png index 683d39cb2672d712dc21d84867d20f92abb713a3..e05a01f72b2eb4e823495e1dd17d7637c4ed011b 100644 GIT binary patch literal 76952 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 rsp?3hAYFQ_ry Date: Tue, 2 Sep 2025 21:23:59 +0530 Subject: [PATCH 03/18] support `store` for openai (#3399) --- mem0/configs/llms/openai.py | 3 +++ mem0/llms/openai.py | 8 +++++++- tests/llms/test_openai.py | 4 ++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/mem0/configs/llms/openai.py b/mem0/configs/llms/openai.py index 960dc0f23..e0a0a6f2d 100644 --- a/mem0/configs/llms/openai.py +++ b/mem0/configs/llms/openai.py @@ -28,6 +28,7 @@ class OpenAIConfig(BaseLlmConfig): openrouter_base_url: Optional[str] = None, site_url: Optional[str] = None, app_name: Optional[str] = None, + store: bool = False, # Response monitoring callback response_callback: Optional[Callable[[Any, dict, dict], None]] = None, ): @@ -72,5 +73,7 @@ class OpenAIConfig(BaseLlmConfig): self.openrouter_base_url = openrouter_base_url self.site_url = site_url self.app_name = app_name + self.store = store + # Response monitoring self.response_callback = response_callback diff --git a/mem0/llms/openai.py b/mem0/llms/openai.py index dae1dd48a..b6ad538d0 100644 --- a/mem0/llms/openai.py +++ b/mem0/llms/openai.py @@ -123,7 +123,13 @@ class OpenAILLM(LLMBase): openrouter_params["extra_headers"] = extra_headers params.update(**openrouter_params) - + + else: + openai_specific_generation_params = ["store"] + for param in openai_specific_generation_params: + if hasattr(self.config, param): + params[param] = getattr(self.config, param) + if response_format: params["response_format"] = response_format if tools: # TODO: Remove tools if no issues found with new memory addition logic diff --git a/tests/llms/test_openai.py b/tests/llms/test_openai.py index f5b9abc3a..a98feb07d 100644 --- a/tests/llms/test_openai.py +++ b/tests/llms/test_openai.py @@ -55,7 +55,7 @@ def test_generate_response_without_tools(mock_openai_client): response = llm.generate_response(messages) mock_openai_client.chat.completions.create.assert_called_once_with( - model="gpt-4o", messages=messages, temperature=0.7, max_tokens=100, top_p=1.0 + model="gpt-4o", messages=messages, temperature=0.7, max_tokens=100, top_p=1.0, store=False ) assert response == "I'm doing well, thank you for asking!" @@ -97,7 +97,7 @@ def test_generate_response_with_tools(mock_openai_client): response = llm.generate_response(messages, tools=tools) mock_openai_client.chat.completions.create.assert_called_once_with( - model="gpt-4o", messages=messages, temperature=0.7, max_tokens=100, top_p=1.0, tools=tools, tool_choice="auto" + model="gpt-4o", messages=messages, temperature=0.7, max_tokens=100, top_p=1.0, tools=tools, tool_choice="auto", store=False ) assert response["content"] == "I've added the memory for you." From 163dafb21625f23db3561728797fc0297ebe97fe Mon Sep 17 00:00:00 2001 From: Prateek Chhikara <46902268+prateekchhikara@users.noreply.github.com> Date: Tue, 2 Sep 2025 13:24:22 -0700 Subject: [PATCH 04/18] Add version param in search v2 API documentation (#3401) --- docs/openapi.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/openapi.json b/docs/openapi.json index 38d3db9d4..b80016053 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -5074,6 +5074,12 @@ "type": "string", "description": "The query to search for in the memory." }, + "version": { + "title": "Version", + "type": "string", + "default": "v2", + "description": "The version of the memory to use. This should always be v2." + }, "filters": { "title": "Filters", "type": "object", From 617aabe5b39c4047bf174b6da0785ce44a830c11 Mon Sep 17 00:00:00 2001 From: Prateek Chhikara <46902268+prateekchhikara@users.noreply.github.com> Date: Tue, 2 Sep 2025 13:32:23 -0700 Subject: [PATCH 05/18] [FIX] Graph Docs page was missing on the side bar (#3402) --- docs/docs.json | 1 + docs/platform/features/platform-overview.mdx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/docs.json b/docs/docs.json index 40a87ae93..4af51ca4b 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -58,6 +58,7 @@ "platform/features/platform-overview", "platform/features/contextual-add", "platform/features/async-client", + "platform/features/graph-memory", "platform/features/advanced-retrieval", "platform/features/criteria-retrieval", "platform/features/multimodal-support", diff --git a/docs/platform/features/platform-overview.mdx b/docs/platform/features/platform-overview.mdx index 1f96eb595..2163f7078 100644 --- a/docs/platform/features/platform-overview.mdx +++ b/docs/platform/features/platform-overview.mdx @@ -36,7 +36,7 @@ Learn about the key features and capabilities that make Mem0 a powerful platform Export memories in structured formats using customizable Pydantic schemas. - + Add memories in the form of nodes and edges in a graph database and search for related memories. From c8d864c1b628285e6afa096135dd24cc1b43139f Mon Sep 17 00:00:00 2001 From: Shili Cao Date: Wed, 3 Sep 2025 17:54:33 +0800 Subject: [PATCH 06/18] fix: add missing provider for baidu vector db (#3405) --- mem0/utils/factory.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mem0/utils/factory.py b/mem0/utils/factory.py index e7eced5d2..1f11da8e1 100644 --- a/mem0/utils/factory.py +++ b/mem0/utils/factory.py @@ -174,6 +174,7 @@ class VectorStoreFactory: "faiss": "mem0.vector_stores.faiss.FAISS", "langchain": "mem0.vector_stores.langchain.Langchain", "s3_vectors": "mem0.vector_stores.s3_vectors.S3Vectors", + "baidu": "mem0.vector_stores.baidu.BaiduDB", } @classmethod From 5e8d5e46645aea9de1b34c5404e6a3a6bc61d9da Mon Sep 17 00:00:00 2001 From: Parshva Daftari <89991302+parshvadaftari@users.noreply.github.com> Date: Wed, 3 Sep 2025 23:09:00 +0530 Subject: [PATCH 07/18] Release 0.1.117 (#3411) --- docs/changelog.mdx | 48 +++++++++++++++++++ docs/docs.json | 5 +- docs/platform/features/advanced-retrieval.mdx | 2 - pyproject.toml | 2 +- 4 files changed, 51 insertions(+), 6 deletions(-) diff --git a/docs/changelog.mdx b/docs/changelog.mdx index 947d4416b..996a68786 100644 --- a/docs/changelog.mdx +++ b/docs/changelog.mdx @@ -7,6 +7,49 @@ mode: "wide" + + +**New Features & Updates:** +- **OpenMemory:** + - Added memory export / import feature + - Added vector store integrations: Weaviate, FAISS, PGVector, Chroma, Redis, Elasticsearch, Milvus + - Added `export_openmemory.sh` migration script +- **Vector Stores:** + - Added Amazon S3 Vectors support + - Added Databricks Mosaic AI vector store support + - Added support for OpenAI Store +- **Graph Memory:** Added support for graph memory using Kuzu +- **Azure:** Added Azure Identity for Azure OpenAI and Azure AI Search authentication +- **Elasticsearch:** Added headers configuration support + +**Improvements:** + - Added custom connection client to enable connecting to local containers for Weaviate + - Updated configuration AWS Bedrock + - Fixed dependency issues and tests; updated docstrings +- **Documentation:** + - Fixed Graph Docs page missing in sidebar + - Updated integration documentation + - Added version param in Search V2 API documentation + - Updated Databricks documentation and refactored docs + - Updated favicon logo + - Fixed typos and Typescript docs + +**Bug Fixes:** +- Baidu: Added missing provider for Baidu vector DB +- MongoDB: Replaced `query_vector` args in search method +- Fixed new memory mistaken for current +- AsyncMemory._add_to_vector_store: handled edge case when no facts found +- Fixed missing commas in Kuzu graph INSERT queries +- Fixed inconsistent created and updated properties for Graph +- Fixed missing `app_id` on client for Neptune Analytics +- Correctly pick AWS region from environment variable +- Fixed Ollama model existence check + +**Refactoring:** +- **PGVector:** Use internal connection pools and context managers + + + **New Features & Updates:** @@ -1039,6 +1082,11 @@ mode: "wide" + +**Bug Fix:** +- **Vercel AI SDK:** Fixed streaming response in the AI SDK. + + **New Features:** - **Vercel AI SDK:** Added a new param `host` to the config. diff --git a/docs/docs.json b/docs/docs.json index 4af51ca4b..9dd514619 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -1,6 +1,5 @@ { "$schema": "https://mintlify.com/docs.json", - "theme": "maple", "name": "Mem0", "description": "Mem0 is a self-improving memory layer for LLM applications, enabling personalized AI experiences that save costs and delight users.", "colors": { @@ -46,7 +45,7 @@ }, { "group": "Platform", - "icon": "cogs", + "icon": "globe", "pages": [ "platform/overview", "platform/quickstart", @@ -381,7 +380,7 @@ "background": { "color": { "light": "#fff", - "dark": "#0f1117" + "dark": "#09090b" } }, "navbar": { diff --git a/docs/platform/features/advanced-retrieval.mdx b/docs/platform/features/advanced-retrieval.mdx index 7cc2aa83a..3a49226eb 100644 --- a/docs/platform/features/advanced-retrieval.mdx +++ b/docs/platform/features/advanced-retrieval.mdx @@ -9,8 +9,6 @@ description: "Advanced memory search with keyword expansion, intelligent reranki Advanced Retrieval gives you precise control over how memories are found and ranked. While basic search uses semantic similarity, these advanced options help you find exactly what you need, when you need it. - - ## Search Enhancement Options ### Keyword Search diff --git a/pyproject.toml b/pyproject.toml index 7227de60f..386e828fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "mem0ai" -version = "0.1.116" +version = "0.1.117" description = "Long-term memory for AI Agents" authors = [ { name = "Mem0", email = "founders@mem0.ai" } From f0e8c3f76097635a1673e5f31c3acee6738e61e7 Mon Sep 17 00:00:00 2001 From: Saket Aryan Date: Thu, 4 Sep 2025 03:22:12 +0530 Subject: [PATCH 08/18] feat: Add metadata param to TS-SDK in client.update (#3415) --- docs/changelog.mdx | 5 +++++ mem0-ts/package.json | 2 +- mem0-ts/src/client/mem0.ts | 12 ++++++++++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/docs/changelog.mdx b/docs/changelog.mdx index 996a68786..0a03a8a52 100644 --- a/docs/changelog.mdx +++ b/docs/changelog.mdx @@ -611,6 +611,11 @@ mode: "wide" + +**New Features:** +- **Client:** Added `metadata` param to `update` method. + + **New Features:** - **OSS:** Added `RedisCloud` search module check diff --git a/mem0-ts/package.json b/mem0-ts/package.json index 161577c69..94c1ecdf1 100644 --- a/mem0-ts/package.json +++ b/mem0-ts/package.json @@ -1,6 +1,6 @@ { "name": "mem0ai", - "version": "2.1.37", + "version": "2.1.38", "description": "The Memory Layer For Your AI Apps", "main": "./dist/index.js", "module": "./dist/index.mjs", diff --git a/mem0-ts/src/client/mem0.ts b/mem0-ts/src/client/mem0.ts index 60d3238b2..a17355aa5 100644 --- a/mem0-ts/src/client/mem0.ts +++ b/mem0-ts/src/client/mem0.ts @@ -251,11 +251,19 @@ export default class MemoryClient { return response; } - async update(memoryId: string, message: string): Promise> { + async update( + memoryId: string, + { text, metadata }: { text?: string; metadata?: Record }, + ): Promise> { + if (text === undefined && metadata === undefined) { + throw new Error("Either text or metadata must be provided for update."); + } + if (this.telemetryId === "") await this.ping(); this._validateOrgProject(); const payload = { - text: message, + text: text, + metadata: metadata, }; const payloadKeys = Object.keys(payload); From b27879bfd47c18384932ce08e9f3f8032322461d Mon Sep 17 00:00:00 2001 From: Gabe Goodhart Date: Thu, 4 Sep 2025 08:48:25 -0600 Subject: [PATCH 09/18] fix: Use ConfigDict instead of class-based Config (#3409) Signed-off-by: Gabe Goodhart --- mem0/client/project.py | 6 ++---- mem0/configs/vector_stores/azure_ai_search.py | 6 ++---- mem0/configs/vector_stores/baidu.py | 6 ++---- mem0/configs/vector_stores/chroma.py | 6 ++---- mem0/configs/vector_stores/databricks.py | 6 ++---- mem0/configs/vector_stores/faiss.py | 6 ++---- mem0/configs/vector_stores/langchain.py | 6 ++---- mem0/configs/vector_stores/milvus.py | 6 ++---- mem0/configs/vector_stores/pinecone.py | 6 ++---- mem0/configs/vector_stores/qdrant.py | 6 ++---- mem0/configs/vector_stores/redis.py | 6 ++---- mem0/configs/vector_stores/s3_vectors.py | 6 ++---- mem0/configs/vector_stores/upstash_vector.py | 6 ++---- mem0/configs/vector_stores/vertex_ai_vector_search.py | 4 ++-- mem0/configs/vector_stores/weaviate.py | 6 ++---- openmemory/api/app/schemas.py | 5 ++--- 16 files changed, 32 insertions(+), 61 deletions(-) diff --git a/mem0/client/project.py b/mem0/client/project.py index 8b1aef72f..371c9932d 100644 --- a/mem0/client/project.py +++ b/mem0/client/project.py @@ -3,7 +3,7 @@ from abc import ABC, abstractmethod from typing import Any, Dict, List, Optional import httpx -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field from mem0.client.utils import api_error_handler from mem0.memory.telemetry import capture_client_event @@ -20,9 +20,7 @@ class ProjectConfig(BaseModel): project_id: Optional[str] = Field(default=None, description="Project ID") user_email: Optional[str] = Field(default=None, description="User email") - class Config: - validate_assignment = True - extra = "forbid" + model_config = ConfigDict(validate_assignment=True, extra="forbid") class BaseProject(ABC): diff --git a/mem0/configs/vector_stores/azure_ai_search.py b/mem0/configs/vector_stores/azure_ai_search.py index 79cfe1796..9b1a33ae5 100644 --- a/mem0/configs/vector_stores/azure_ai_search.py +++ b/mem0/configs/vector_stores/azure_ai_search.py @@ -1,6 +1,6 @@ from typing import Any, Dict, Optional -from pydantic import BaseModel, Field, model_validator +from pydantic import BaseModel, ConfigDict, Field, model_validator class AzureAISearchConfig(BaseModel): @@ -54,6 +54,4 @@ class AzureAISearchConfig(BaseModel): return values - model_config = { - "arbitrary_types_allowed": True, - } + model_config = ConfigDict(arbitrary_types_allowed=True) diff --git a/mem0/configs/vector_stores/baidu.py b/mem0/configs/vector_stores/baidu.py index ad27f4cb3..6018fe3cf 100644 --- a/mem0/configs/vector_stores/baidu.py +++ b/mem0/configs/vector_stores/baidu.py @@ -1,6 +1,6 @@ from typing import Any, Dict -from pydantic import BaseModel, Field, model_validator +from pydantic import BaseModel, ConfigDict, Field, model_validator class BaiduDBConfig(BaseModel): @@ -24,6 +24,4 @@ class BaiduDBConfig(BaseModel): ) return values - model_config = { - "arbitrary_types_allowed": True, - } + model_config = ConfigDict(arbitrary_types_allowed=True) diff --git a/mem0/configs/vector_stores/chroma.py b/mem0/configs/vector_stores/chroma.py index 664807b85..cf9104237 100644 --- a/mem0/configs/vector_stores/chroma.py +++ b/mem0/configs/vector_stores/chroma.py @@ -1,6 +1,6 @@ from typing import Any, ClassVar, Dict, Optional -from pydantic import BaseModel, Field, model_validator +from pydantic import BaseModel, ConfigDict, Field, model_validator class ChromaDbConfig(BaseModel): @@ -35,6 +35,4 @@ class ChromaDbConfig(BaseModel): ) return values - model_config = { - "arbitrary_types_allowed": True, - } + model_config = ConfigDict(arbitrary_types_allowed=True) diff --git a/mem0/configs/vector_stores/databricks.py b/mem0/configs/vector_stores/databricks.py index 8729590a6..6af0664bc 100644 --- a/mem0/configs/vector_stores/databricks.py +++ b/mem0/configs/vector_stores/databricks.py @@ -1,6 +1,6 @@ from typing import Any, Dict, Optional -from pydantic import BaseModel, Field, model_validator +from pydantic import BaseModel, ConfigDict, Field, model_validator from databricks.sdk.service.vectorsearch import EndpointType, VectorIndexType, PipelineType @@ -58,6 +58,4 @@ class DatabricksConfig(BaseModel): return self - model_config = { - "arbitrary_types_allowed": True, - } + model_config = ConfigDict(arbitrary_types_allowed=True) diff --git a/mem0/configs/vector_stores/faiss.py b/mem0/configs/vector_stores/faiss.py index b77a8add3..bbefc6dc5 100644 --- a/mem0/configs/vector_stores/faiss.py +++ b/mem0/configs/vector_stores/faiss.py @@ -1,6 +1,6 @@ from typing import Any, Dict, Optional -from pydantic import BaseModel, Field, model_validator +from pydantic import BaseModel, ConfigDict, Field, model_validator class FAISSConfig(BaseModel): @@ -34,6 +34,4 @@ class FAISSConfig(BaseModel): ) return values - model_config = { - "arbitrary_types_allowed": True, - } + model_config = ConfigDict(arbitrary_types_allowed=True) diff --git a/mem0/configs/vector_stores/langchain.py b/mem0/configs/vector_stores/langchain.py index c41784067..d312b4642 100644 --- a/mem0/configs/vector_stores/langchain.py +++ b/mem0/configs/vector_stores/langchain.py @@ -1,6 +1,6 @@ from typing import Any, ClassVar, Dict -from pydantic import BaseModel, Field, model_validator +from pydantic import BaseModel, ConfigDict, Field, model_validator class LangchainConfig(BaseModel): @@ -27,6 +27,4 @@ class LangchainConfig(BaseModel): ) return values - model_config = { - "arbitrary_types_allowed": True, - } + model_config = ConfigDict(arbitrary_types_allowed=True) diff --git a/mem0/configs/vector_stores/milvus.py b/mem0/configs/vector_stores/milvus.py index 4ad964457..2227ffe5d 100644 --- a/mem0/configs/vector_stores/milvus.py +++ b/mem0/configs/vector_stores/milvus.py @@ -1,7 +1,7 @@ from enum import Enum from typing import Any, Dict -from pydantic import BaseModel, Field, model_validator +from pydantic import BaseModel, ConfigDict, Field, model_validator class MetricType(str, Enum): @@ -39,6 +39,4 @@ class MilvusDBConfig(BaseModel): ) return values - model_config = { - "arbitrary_types_allowed": True, - } + model_config = ConfigDict(arbitrary_types_allowed=True) diff --git a/mem0/configs/vector_stores/pinecone.py b/mem0/configs/vector_stores/pinecone.py index 0474b9ac6..caacf3c64 100644 --- a/mem0/configs/vector_stores/pinecone.py +++ b/mem0/configs/vector_stores/pinecone.py @@ -1,7 +1,7 @@ import os from typing import Any, Dict, Optional -from pydantic import BaseModel, Field, model_validator +from pydantic import BaseModel, ConfigDict, Field, model_validator class PineconeConfig(BaseModel): @@ -52,6 +52,4 @@ class PineconeConfig(BaseModel): ) return values - model_config = { - "arbitrary_types_allowed": True, - } + model_config = ConfigDict(arbitrary_types_allowed=True) diff --git a/mem0/configs/vector_stores/qdrant.py b/mem0/configs/vector_stores/qdrant.py index f8628d332..556b45ed0 100644 --- a/mem0/configs/vector_stores/qdrant.py +++ b/mem0/configs/vector_stores/qdrant.py @@ -1,6 +1,6 @@ from typing import Any, ClassVar, Dict, Optional -from pydantic import BaseModel, Field, model_validator +from pydantic import BaseModel, ConfigDict, Field, model_validator class QdrantConfig(BaseModel): @@ -44,6 +44,4 @@ class QdrantConfig(BaseModel): ) return values - model_config = { - "arbitrary_types_allowed": True, - } + model_config = ConfigDict(arbitrary_types_allowed=True) diff --git a/mem0/configs/vector_stores/redis.py b/mem0/configs/vector_stores/redis.py index efa442dc1..6ae3a56f7 100644 --- a/mem0/configs/vector_stores/redis.py +++ b/mem0/configs/vector_stores/redis.py @@ -1,6 +1,6 @@ from typing import Any, Dict -from pydantic import BaseModel, Field, model_validator +from pydantic import BaseModel, ConfigDict, Field, model_validator # TODO: Upgrade to latest pydantic version @@ -21,6 +21,4 @@ class RedisDBConfig(BaseModel): ) return values - model_config = { - "arbitrary_types_allowed": True, - } + model_config = ConfigDict(arbitrary_types_allowed=True) diff --git a/mem0/configs/vector_stores/s3_vectors.py b/mem0/configs/vector_stores/s3_vectors.py index 1cd4f2a3c..95c50f675 100644 --- a/mem0/configs/vector_stores/s3_vectors.py +++ b/mem0/configs/vector_stores/s3_vectors.py @@ -1,6 +1,6 @@ from typing import Any, Dict, Optional -from pydantic import BaseModel, Field, model_validator +from pydantic import BaseModel, ConfigDict, Field, model_validator class S3VectorsConfig(BaseModel): @@ -29,6 +29,4 @@ class S3VectorsConfig(BaseModel): ) return values - model_config = { - "arbitrary_types_allowed": True, - } + model_config = ConfigDict(arbitrary_types_allowed=True) diff --git a/mem0/configs/vector_stores/upstash_vector.py b/mem0/configs/vector_stores/upstash_vector.py index b7c4d14f5..d4c3c7c3b 100644 --- a/mem0/configs/vector_stores/upstash_vector.py +++ b/mem0/configs/vector_stores/upstash_vector.py @@ -1,7 +1,7 @@ import os from typing import Any, ClassVar, Dict, Optional -from pydantic import BaseModel, Field, model_validator +from pydantic import BaseModel, ConfigDict, Field, model_validator try: from upstash_vector import Index @@ -31,6 +31,4 @@ class UpstashVectorConfig(BaseModel): raise ValueError("Either a client or URL and token must be provided.") return values - model_config = { - "arbitrary_types_allowed": True, - } + model_config = ConfigDict(arbitrary_types_allowed=True) diff --git a/mem0/configs/vector_stores/vertex_ai_vector_search.py b/mem0/configs/vector_stores/vertex_ai_vector_search.py index e21532901..8de876062 100644 --- a/mem0/configs/vector_stores/vertex_ai_vector_search.py +++ b/mem0/configs/vector_stores/vertex_ai_vector_search.py @@ -1,6 +1,6 @@ from typing import Optional -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field class GoogleMatchingEngineConfig(BaseModel): @@ -14,7 +14,7 @@ class GoogleMatchingEngineConfig(BaseModel): credentials_path: Optional[str] = Field(None, description="Path to service account credentials file") vector_search_api_endpoint: Optional[str] = Field(None, description="Vector search API endpoint") - model_config = {"extra": "forbid"} + model_config = ConfigDict(extra="forbid") def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/mem0/configs/vector_stores/weaviate.py b/mem0/configs/vector_stores/weaviate.py index 39b465112..f248344ad 100644 --- a/mem0/configs/vector_stores/weaviate.py +++ b/mem0/configs/vector_stores/weaviate.py @@ -1,6 +1,6 @@ from typing import Any, ClassVar, Dict, Optional -from pydantic import BaseModel, Field, model_validator +from pydantic import BaseModel, ConfigDict, Field, model_validator class WeaviateConfig(BaseModel): @@ -38,6 +38,4 @@ class WeaviateConfig(BaseModel): return values - model_config = { - "arbitrary_types_allowed": True, - } + model_config = ConfigDict(arbitrary_types_allowed=True) diff --git a/openmemory/api/app/schemas.py b/openmemory/api/app/schemas.py index f5462e7f6..fd47b643d 100644 --- a/openmemory/api/app/schemas.py +++ b/openmemory/api/app/schemas.py @@ -2,7 +2,7 @@ from datetime import datetime from typing import List, Optional from uuid import UUID -from pydantic import BaseModel, Field, validator +from pydantic import BaseModel, ConfigDict, Field, validator class MemoryBase(BaseModel): @@ -33,8 +33,7 @@ class Memory(MemoryBase): categories: Optional[List[Category]] = None app: App - class Config: - from_attributes = True + model_config = ConfigDict(from_attributes=True) class MemoryUpdate(BaseModel): content: Optional[str] = None From 947718458294157534d76affe6159e8efeed16cb Mon Sep 17 00:00:00 2001 From: Josh Hayes <35790761+hayescode@users.noreply.github.com> Date: Fri, 5 Sep 2025 05:52:09 -0500 Subject: [PATCH 10/18] databricks bug fixes (#3416) --- mem0/vector_stores/databricks.py | 4 ++-- tests/vector_stores/test_databricks.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/mem0/vector_stores/databricks.py b/mem0/vector_stores/databricks.py index 0a5b06f49..6b5660e74 100644 --- a/mem0/vector_stores/databricks.py +++ b/mem0/vector_stores/databricks.py @@ -465,7 +465,7 @@ class Databricks(VectorStoreBase): # Parse results result_data = sdk_results.result if hasattr(sdk_results, "result") else sdk_results - data_array = result_data.data_array if hasattr(result_data, "data_array") else [] + data_array = result_data.data_array if getattr(result_data, "data_array", None) else [] memory_results = [] for row in data_array: @@ -708,7 +708,7 @@ class Databricks(VectorStoreBase): pass memory_id = row_dict.get("memory_id") or row_dict.get("id") memory_results.append(MemoryResult(id=memory_id, payload=payload)) - return memory_results + return [memory_results] except Exception as e: logger.error(f"Failed to list memories: {e}") return [] diff --git a/tests/vector_stores/test_databricks.py b/tests/vector_stores/test_databricks.py index 5b87def07..9b2d1c560 100644 --- a/tests/vector_stores/test_databricks.py +++ b/tests/vector_stores/test_databricks.py @@ -300,8 +300,9 @@ def test_list_memories(db_instance_delta, mock_workspace_client): result=SimpleNamespace(data_array=[row]) ) res = db_instance_delta.list(limit=1) - assert len(res) == 1 - assert res[0].id == "id3" + assert isinstance(res, list) + assert len(res[0]) == 1 + assert res[0][0].id == "id3" # ---------------------- Reset Tests ---------------------- # From 77b4b6a2b94b950aeb203a0e3ddea662dec6b3c8 Mon Sep 17 00:00:00 2001 From: Ranjith kumar Date: Fri, 5 Sep 2025 22:38:04 +0530 Subject: [PATCH 11/18] fix: :bug: replace hardcoded llm provider with provider from config (#3423) --- mem0-ts/src/oss/src/memory/graph_memory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mem0-ts/src/oss/src/memory/graph_memory.ts b/mem0-ts/src/oss/src/memory/graph_memory.ts index d0f92887a..afd2e8d19 100644 --- a/mem0-ts/src/oss/src/memory/graph_memory.ts +++ b/mem0-ts/src/oss/src/memory/graph_memory.ts @@ -89,7 +89,7 @@ export class MemoryGraph { this.llm = LLMFactory.create(this.llmProvider, this.config.llm.config); this.structuredLlm = LLMFactory.create( - "openai_structured", + this.llmProvider, this.config.llm.config, ); this.threshold = 0.7; From f5e0fb9e4b3f64f0b1c3d85ae0b986d9db1e1a60 Mon Sep 17 00:00:00 2001 From: Parshva Daftari <89991302+parshvadaftari@users.noreply.github.com> Date: Tue, 9 Sep 2025 22:22:09 +0530 Subject: [PATCH 12/18] Added support for chromadb cloud (#3436) --- docs/components/vectordbs/dbs/chroma.mdx | 11 ++++++++-- mem0/configs/vector_stores/chroma.py | 28 ++++++++++++++++++++---- mem0/vector_stores/chroma.py | 13 +++++++++++ 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/docs/components/vectordbs/dbs/chroma.mdx b/docs/components/vectordbs/dbs/chroma.mdx index ed798ebbe..2e546b883 100644 --- a/docs/components/vectordbs/dbs/chroma.mdx +++ b/docs/components/vectordbs/dbs/chroma.mdx @@ -1,7 +1,9 @@ -[Chroma](https://www.trychroma.com/) is an AI-native open-source vector database that simplifies building LLM apps by providing tools for storing, embedding, and searching embeddings with a focus on simplicity and speed. +[Chroma](https://www.trychroma.com/) is an AI-native open-source vector database that simplifies building LLM apps by providing tools for storing, embedding, and searching embeddings with a focus on simplicity and speed. It supports both local deployment and cloud hosting through ChromaDB Cloud. ### Usage +#### Local Installation + ```python import os from mem0 import Memory @@ -14,6 +16,9 @@ config = { "config": { "collection_name": "test", "path": "db", + # Optional: ChromaDB Cloud configuration + # "api_key": "your-chroma-cloud-api-key", + # "tenant": "your-chroma-cloud-tenant-id", } } } @@ -38,4 +43,6 @@ Here are the parameters available for configuring Chroma: | `client` | Custom client for Chroma | `None` | | `path` | Path for the Chroma database | `db` | | `host` | The host where the Chroma server is running | `None` | -| `port` | The port where the Chroma server is running | `None` | \ No newline at end of file +| `port` | The port where the Chroma server is running | `None` | +| `api_key` | ChromaDB Cloud API key (for cloud usage) | `None` | +| `tenant` | ChromaDB Cloud tenant ID (for cloud usage) | `None` | \ No newline at end of file diff --git a/mem0/configs/vector_stores/chroma.py b/mem0/configs/vector_stores/chroma.py index cf9104237..764e6a381 100644 --- a/mem0/configs/vector_stores/chroma.py +++ b/mem0/configs/vector_stores/chroma.py @@ -10,17 +10,37 @@ class ChromaDbConfig(BaseModel): raise ImportError("The 'chromadb' library is required. Please install it using 'pip install chromadb'.") Client: ClassVar[type] = Client - collection_name: str = Field("mem0", description="Default name for the collection") + collection_name: str = Field("mem0", description="Default name for the collection/database") client: Optional[Client] = Field(None, description="Existing ChromaDB client instance") path: Optional[str] = Field(None, description="Path to the database directory") host: Optional[str] = Field(None, description="Database connection remote host") port: Optional[int] = Field(None, description="Database connection remote port") + # ChromaDB Cloud configuration + api_key: Optional[str] = Field(None, description="ChromaDB Cloud API key") + tenant: Optional[str] = Field(None, description="ChromaDB Cloud tenant ID") @model_validator(mode="before") - def check_host_port_or_path(cls, values): + def check_connection_config(cls, values): host, port, path = values.get("host"), values.get("port"), values.get("path") - if not path and not (host and port): - raise ValueError("Either 'host' and 'port' or 'path' must be provided.") + api_key, tenant = values.get("api_key"), values.get("tenant") + + # Check if cloud configuration is provided + cloud_config = bool(api_key and tenant) + + # If cloud configuration is provided, remove any default path that might have been added + if cloud_config and path == "/tmp/chroma": + values.pop("path", None) + return values + + # Check if local/server configuration is provided (excluding default tmp path for cloud config) + local_config = bool(path and path != "/tmp/chroma") or bool(host and port) + + if not cloud_config and not local_config: + raise ValueError("Either ChromaDB Cloud configuration (api_key, tenant) or local configuration (path or host/port) must be provided.") + + if cloud_config and local_config: + raise ValueError("Cannot specify both cloud configuration and local configuration. Choose one.") + return values @model_validator(mode="before") diff --git a/mem0/vector_stores/chroma.py b/mem0/vector_stores/chroma.py index 681d4626c..8d231719c 100644 --- a/mem0/vector_stores/chroma.py +++ b/mem0/vector_stores/chroma.py @@ -28,6 +28,8 @@ class ChromaDB(VectorStoreBase): host: Optional[str] = None, port: Optional[int] = None, path: Optional[str] = None, + api_key: Optional[str] = None, + tenant: Optional[str] = None, ): """ Initialize the Chromadb vector store. @@ -38,10 +40,21 @@ class ChromaDB(VectorStoreBase): host (str, optional): Host address for chromadb server. Defaults to None. port (int, optional): Port for chromadb server. Defaults to None. path (str, optional): Path for local chromadb database. Defaults to None. + api_key (str, optional): ChromaDB Cloud API key. Defaults to None. + tenant (str, optional): ChromaDB Cloud tenant ID. Defaults to None. """ if client: self.client = client + elif api_key and tenant: + # Initialize ChromaDB Cloud client + logger.info("Initializing ChromaDB Cloud client") + self.client = chromadb.CloudClient( + api_key=api_key, + tenant=tenant, + database="mem0" # Use fixed database name for cloud + ) else: + # Initialize local or server client self.settings = Settings(anonymized_telemetry=False) if host and port: From e64488b5989baf89c096319102ff6ce8630b6377 Mon Sep 17 00:00:00 2001 From: Prateek Chhikara <46902268+prateekchhikara@users.noreply.github.com> Date: Tue, 9 Sep 2025 11:47:09 -0700 Subject: [PATCH 13/18] updates to the category docs (#3437) --- .../memory/v2-search-memories.mdx | 36 +++++++ docs/docs.json | 1 + docs/openapi.json | 4 +- docs/platform/advanced-memory-operations.mdx | 94 +++++++++++++++---- 4 files changed, 114 insertions(+), 21 deletions(-) diff --git a/docs/api-reference/memory/v2-search-memories.mdx b/docs/api-reference/memory/v2-search-memories.mdx index 044d9f722..a81b898a2 100644 --- a/docs/api-reference/memory/v2-search-memories.mdx +++ b/docs/api-reference/memory/v2-search-memories.mdx @@ -70,3 +70,39 @@ The v2 search API is powerful and flexible, allowing for more precise memory ret ) ``` + + + ```python Categories Filter Examples + # Example 1: Using 'contains' for partial matching + finance_memories = m.search( + query="What are my financial goals?", + version="v2", + filters={ + "AND": [ + { "user_id": "alice" }, + { + "categories": { + "contains": "finance" + } + } + ] + }, + ) + + # Example 2: Using 'in' for exact matching + personal_memories = m.search( + query="What personal information do you have?", + version="v2", + filters={ + "AND": [ + { "user_id": "alice" }, + { + "categories": { + "in": ["personal_information"] + } + } + ] + }, + ) + ``` + diff --git a/docs/docs.json b/docs/docs.json index 9dd514619..ebf1e2a1c 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -2,6 +2,7 @@ "$schema": "https://mintlify.com/docs.json", "name": "Mem0", "description": "Mem0 is a self-improving memory layer for LLM applications, enabling personalized AI experiences that save costs and delight users.", + "theme": "maple", "colors": { "primary": "#6c60f0", "light": "#E6FFA2", diff --git a/docs/openapi.json b/docs/openapi.json index b80016053..9a1cf6f41 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -1094,7 +1094,7 @@ } } }, - "description": "Filters to apply to the memories. Available fields are: user_id, agent_id, app_id, run_id, created_at, updated_at, categories, keywords. Supports logical operators (AND, OR) and comparison operators (in, gte, lte, gt, lt, ne, contains, icontains)", + "description": "Filters to apply to the memories. Available fields are: user_id, agent_id, app_id, run_id, created_at, updated_at, categories, keywords. Supports logical operators (AND, OR) and comparison operators (in, gte, lte, gt, lt, ne, contains, icontains). For categories field, use 'contains' for partial matching (e.g., {\"categories\": {\"contains\": \"finance\"}}) or 'in' for exact matching (e.g., {\"categories\": {\"in\": [\"personal_information\"]}}).", "style": "deepObject", "explode": true }, @@ -5083,7 +5083,7 @@ "filters": { "title": "Filters", "type": "object", - "description": "A dictionary of filters to apply to the search. Available fields are: user_id, agent_id, app_id, run_id, created_at, updated_at, categories, keywords. Supports logical operators (AND, OR) and comparison operators (in, gte, lte, gt, lt, ne, contains, icontains).", + "description": "A dictionary of filters to apply to the search. Available fields are: user_id, agent_id, app_id, run_id, created_at, updated_at, categories, keywords. Supports logical operators (AND, OR) and comparison operators (in, gte, lte, gt, lt, ne, contains, icontains). For categories field, use 'contains' for partial matching (e.g., {\"categories\": {\"contains\": \"finance\"}}) or 'in' for exact matching (e.g., {\"categories\": {\"in\": [\"personal_information\"]}}).", "properties": { "user_id": {"type": "string"}, "agent_id": {"type": "string"}, diff --git a/docs/platform/advanced-memory-operations.mdx b/docs/platform/advanced-memory-operations.mdx index 15061cbc6..4d4af306a 100644 --- a/docs/platform/advanced-memory-operations.mdx +++ b/docs/platform/advanced-memory-operations.mdx @@ -285,6 +285,10 @@ curl -X POST "https://api.mem0.ai/v1/memories/" \ Our advanced search allows you to set custom search filters. You can filter by user_id, agent_id, app_id, run_id, created_at, updated_at, categories, and text. The filters support logical operators (AND, OR) and comparison operators (in, gte, lte, gt, lt, ne, contains, icontains, `*`). The wildcard character (`*`) matches everything for a specific field. +For the **categories** field specifically: +- Use `contains` for partial matching (e.g., `{"categories": {"contains": "finance"}}`) +- Use `in` for exact matching (e.g., `{"categories": {"in": ["personal_information"]}}`). + Here you need to define `version` as `v2` in the search method. #### Example 1: Search using user_id and agent_id filters @@ -407,58 +411,106 @@ curl -X POST "https://api.mem0.ai/v1/memories/search/?version=v2" \ ``` -#### Example 3: Search using metadata and categories +#### Example 3: Search using categories filters ```python Python -query = "What do you know about me?" +# Example 3a: Using 'contains' for partial matching +query = "What are my financial goals?" filters = { "AND": [ - {"metadata": {"food": "vegan"}}, + { "user_id": "alice" }, { - "categories":{ - "contains": "food_preferences" - } - } + "categories": { + "contains": "finance" + } + } + ] +} +client.search(query, version="v2", filters=filters) + +# Example 3b: Using 'in' for exact matching +query = "What personal information do you have?" +filters = { + "AND": [ + { "user_id": "alice" }, + { + "categories": { + "in": ["personal_information"] + } + } ] } client.search(query, version="v2", filters=filters) ``` ```javascript JavaScript -const query = "What do you know about me?"; -const filters = { +// Example 3a: Using 'contains' for partial matching +const query1 = "What are my financial goals?"; +const filters1 = { "AND": [ - {"metadata": {"food": "vegan"}}, + { "user_id": "alice" }, { "categories": { - "contains": "food_preferences" + "contains": "finance" } } ] }; -client.search(query, { version: "v2", filters }) +client.search(query1, { version: "v2", filters: filters1 }) + .then(results => console.log(results)) + .catch(error => console.error(error)); + +// Example 3b: Using 'in' for exact matching +const query2 = "What personal information do you have?"; +const filters2 = { + "AND": [ + { "user_id": "alice" }, + { + "categories": { + "in": ["personal_information"] + } + } + ] +}; + +client.search(query2, { version: "v2", filters: filters2 }) .then(results => console.log(results)) .catch(error => console.error(error)); ``` ```bash cURL +# Example 3a: Using 'contains' for partial matching curl -X POST "https://api.mem0.ai/v1/memories/search/?version=v2" \ -H "Authorization: Token your-api-key" \ -H "Content-Type: application/json" \ -d '{ - "query": "What do you know about me?", + "query": "What are my financial goals?", "filters": { "AND": [ - { - "metadata": { - "food": "vegan" - } - }, + { "user_id": "alice" }, { "categories": { - "contains": "food_preferences" + "contains": "finance" + } + } + ] + } + }' + +# Example 3b: Using 'in' for exact matching +curl -X POST "https://api.mem0.ai/v1/memories/search/?version=v2" \ + -H "Authorization: Token your-api-key" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "What personal information do you have?", + "filters": { + "AND": [ + { "user_id": "alice" }, + { + "categories": { + "in": ["personal_information"] } } ] @@ -693,6 +745,10 @@ curl -X GET "https://api.mem0.ai/v1/memories/?user_id=alex&keywords=to play&page Our advanced retrieval allows you to set custom filters when fetching memories. You can filter by user_id, agent_id, app_id, run_id, created_at, updated_at, categories, and keywords. The filters support logical operators (AND, OR) and comparison operators (in, gte, lte, gt, lt, ne, contains, icontains, `*`). The wildcard character (`*`) matches everything for a specific field. +For the **categories** field specifically: +- Use `contains` for partial matching (e.g., `{"categories": {"contains": "finance"}}`) +- Use `in` for exact matching (e.g., `{"categories": {"in": ["personal_information"]}}`). + Here you need to define `version` as `v2` in the get_all method. From e3f0277cb940d575ec817cdf9f8fcc667fa7c47d Mon Sep 17 00:00:00 2001 From: Swarnaprakash Udayakumar Date: Tue, 9 Sep 2025 15:31:53 -0700 Subject: [PATCH 14/18] feat(vector-store): Add Valkey vector store support (#3272) --- Makefile | 2 +- docs/components/vectordbs/config.mdx | 2 +- docs/components/vectordbs/dbs/valkey.mdx | 49 ++ docs/components/vectordbs/overview.mdx | 3 +- docs/docs.json | 1 + mem0/configs/vector_stores/valkey.py | 15 + mem0/utils/factory.py | 1 + mem0/vector_stores/configs.py | 1 + mem0/vector_stores/valkey.py | 824 ++++++++++++++++++++++ pyproject.toml | 1 + tests/vector_stores/test_valkey.py | 862 +++++++++++++++++++++++ 11 files changed, 1758 insertions(+), 3 deletions(-) create mode 100644 docs/components/vectordbs/dbs/valkey.mdx create mode 100644 mem0/configs/vector_stores/valkey.py create mode 100644 mem0/vector_stores/valkey.py create mode 100644 tests/vector_stores/test_valkey.py diff --git a/Makefile b/Makefile index 3c8287edd..14098f00c 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ install: install_all: pip install ruff==0.6.9 groq together boto3 litellm ollama chromadb weaviate weaviate-client sentence_transformers vertexai \ google-generativeai elasticsearch opensearch-py vecs "pinecone<7.0.0" pinecone-text faiss-cpu langchain-community \ - upstash-vector azure-search-documents langchain-memgraph langchain-neo4j langchain-aws rank-bm25 pymochow pymongo psycopg kuzu databricks-sdk + upstash-vector azure-search-documents langchain-memgraph langchain-neo4j langchain-aws rank-bm25 pymochow pymongo psycopg kuzu databricks-sdk valkey # Format code with ruff format: diff --git a/docs/components/vectordbs/config.mdx b/docs/components/vectordbs/config.mdx index a36e55795..89d995d21 100644 --- a/docs/components/vectordbs/config.mdx +++ b/docs/components/vectordbs/config.mdx @@ -8,7 +8,7 @@ iconType: "solid" The `config` is defined as an object with two main keys: - `vector_store`: Specifies the vector database provider and its configuration - - `provider`: The name of the vector database (e.g., "chroma", "pgvector", "qdrant", "milvus", "upstash_vector", "azure_ai_search", "vertex_ai_vector_search") + - `provider`: The name of the vector database (e.g., "chroma", "pgvector", "qdrant", "milvus", "upstash_vector", "azure_ai_search", "vertex_ai_vector_search", "valkey") - `config`: A nested dictionary containing provider-specific settings diff --git a/docs/components/vectordbs/dbs/valkey.mdx b/docs/components/vectordbs/dbs/valkey.mdx new file mode 100644 index 000000000..3c6d72e84 --- /dev/null +++ b/docs/components/vectordbs/dbs/valkey.mdx @@ -0,0 +1,49 @@ +# Valkey Vector Store + +[Valkey](https://valkey.io/) is an open source (BSD) high-performance key/value datastore that supports a variety of workloads and rich datastructures including vector search. + +## Installation + +```bash +pip install mem0ai[vector_stores] +``` + +## Usage + +```python +config = { + "vector_store": { + "provider": "valkey", + "config": { + "collection_name": "test", + "valkey_url": "valkey://localhost:6379", + "embedding_model_dims": 1536, + "index_type": "flat" + } + } +} + +m = Memory.from_config(config) +messages = [ + {"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"}, + {"role": "assistant", "content": "How about a thriller movies? They can be quite engaging."}, + {"role": "user", "content": "I'm not a big fan of thriller movies but I love sci-fi movies."}, + {"role": "assistant", "content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future."} +] +m.add(messages, user_id="alice", metadata={"category": "movies"}) +``` + +## Parameters + +Let's see the available parameters for the `valkey` config: + +| Parameter | Description | Default Value | +| --- | --- | --- | +| `collection_name` | The name of the collection to store the vectors | `mem0` | +| `valkey_url` | Connection URL for the Valkey server | `valkey://localhost:6379` | +| `embedding_model_dims` | Dimensions of the embedding model | `1536` | +| `index_type` | Vector index algorithm (`hnsw` or `flat`) | `hnsw` | +| `hnsw_m` | Number of bi-directional links for HNSW | `16` | +| `hnsw_ef_construction` | Size of dynamic candidate list for HNSW | `200` | +| `hnsw_ef_runtime` | Size of dynamic candidate list for search | `10` | +| `distance_metric` | Distance metric for vector similarity | `cosine` | diff --git a/docs/components/vectordbs/overview.mdx b/docs/components/vectordbs/overview.mdx index 83b55d20c..ba504541c 100644 --- a/docs/components/vectordbs/overview.mdx +++ b/docs/components/vectordbs/overview.mdx @@ -11,7 +11,7 @@ Mem0 includes built-in support for various popular databases. Memory can utilize See the list of supported vector databases below. - The following vector databases are supported in the Python implementation. The TypeScript implementation currently only supports Qdrant, Redis,Vectorize and in-memory vector database. + The following vector databases are supported in the Python implementation. The TypeScript implementation currently only supports Qdrant, Redis, Valkey, Vectorize and in-memory vector database. @@ -24,6 +24,7 @@ See the list of supported vector databases below. + diff --git a/docs/docs.json b/docs/docs.json index ebf1e2a1c..2001418e9 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -152,6 +152,7 @@ "components/vectordbs/dbs/mongodb", "components/vectordbs/dbs/azure", "components/vectordbs/dbs/redis", + "components/vectordbs/dbs/valkey", "components/vectordbs/dbs/elasticsearch", "components/vectordbs/dbs/opensearch", "components/vectordbs/dbs/supabase", diff --git a/mem0/configs/vector_stores/valkey.py b/mem0/configs/vector_stores/valkey.py new file mode 100644 index 000000000..1c04049e6 --- /dev/null +++ b/mem0/configs/vector_stores/valkey.py @@ -0,0 +1,15 @@ +from pydantic import BaseModel + + +class ValkeyConfig(BaseModel): + """Configuration for Valkey vector store.""" + + valkey_url: str + collection_name: str + embedding_model_dims: int + timezone: str = "UTC" + index_type: str = "hnsw" # Default to HNSW, can be 'hnsw' or 'flat' + # HNSW specific parameters with recommended defaults + hnsw_m: int = 16 # Number of connections per layer (default from Valkey docs) + hnsw_ef_construction: int = 200 # Search width during construction + hnsw_ef_runtime: int = 10 # Search width during queries diff --git a/mem0/utils/factory.py b/mem0/utils/factory.py index 1f11da8e1..a27d16c69 100644 --- a/mem0/utils/factory.py +++ b/mem0/utils/factory.py @@ -165,6 +165,7 @@ class VectorStoreFactory: "pinecone": "mem0.vector_stores.pinecone.PineconeDB", "mongodb": "mem0.vector_stores.mongodb.MongoDB", "redis": "mem0.vector_stores.redis.RedisDB", + "valkey": "mem0.vector_stores.valkey.ValkeyDB", "databricks": "mem0.vector_stores.databricks.Databricks", "elasticsearch": "mem0.vector_stores.elasticsearch.ElasticsearchDB", "vertex_ai_vector_search": "mem0.vector_stores.vertex_ai_vector_search.GoogleMatchingEngine", diff --git a/mem0/vector_stores/configs.py b/mem0/vector_stores/configs.py index b2e14bbe0..f9570f749 100644 --- a/mem0/vector_stores/configs.py +++ b/mem0/vector_stores/configs.py @@ -21,6 +21,7 @@ class VectorStoreConfig(BaseModel): "upstash_vector": "UpstashVectorConfig", "azure_ai_search": "AzureAISearchConfig", "redis": "RedisDBConfig", + "valkey": "ValkeyConfig", "databricks": "DatabricksConfig", "elasticsearch": "ElasticsearchConfig", "vertex_ai_vector_search": "GoogleMatchingEngineConfig", diff --git a/mem0/vector_stores/valkey.py b/mem0/vector_stores/valkey.py new file mode 100644 index 000000000..c4539dcd2 --- /dev/null +++ b/mem0/vector_stores/valkey.py @@ -0,0 +1,824 @@ +import json +import logging +from datetime import datetime +from typing import Dict + +import numpy as np +import pytz +import valkey +from pydantic import BaseModel +from valkey.exceptions import ResponseError + +from mem0.memory.utils import extract_json +from mem0.vector_stores.base import VectorStoreBase + +logger = logging.getLogger(__name__) + +# Default fields for the Valkey index +DEFAULT_FIELDS = [ + {"name": "memory_id", "type": "tag"}, + {"name": "hash", "type": "tag"}, + {"name": "agent_id", "type": "tag"}, + {"name": "run_id", "type": "tag"}, + {"name": "user_id", "type": "tag"}, + {"name": "memory", "type": "tag"}, # Using TAG instead of TEXT for Valkey compatibility + {"name": "metadata", "type": "tag"}, # Using TAG instead of TEXT for Valkey compatibility + {"name": "created_at", "type": "numeric"}, + {"name": "updated_at", "type": "numeric"}, + { + "name": "embedding", + "type": "vector", + "attrs": {"distance_metric": "cosine", "algorithm": "flat", "datatype": "float32"}, + }, +] + +excluded_keys = {"user_id", "agent_id", "run_id", "hash", "data", "created_at", "updated_at"} + + +class OutputData(BaseModel): + id: str + score: float + payload: Dict + + +class ValkeyDB(VectorStoreBase): + def __init__( + self, + valkey_url: str, + collection_name: str, + embedding_model_dims: int, + timezone: str = "UTC", + index_type: str = "hnsw", + hnsw_m: int = 16, + hnsw_ef_construction: int = 200, + hnsw_ef_runtime: int = 10, + ): + """ + Initialize the Valkey vector store. + + Args: + valkey_url (str): Valkey URL. + collection_name (str): Collection name. + embedding_model_dims (int): Embedding model dimensions. + timezone (str, optional): Timezone for timestamps. Defaults to "UTC". + index_type (str, optional): Index type ('hnsw' or 'flat'). Defaults to "hnsw". + hnsw_m (int, optional): HNSW M parameter (connections per node). Defaults to 16. + hnsw_ef_construction (int, optional): HNSW ef_construction parameter. Defaults to 200. + hnsw_ef_runtime (int, optional): HNSW ef_runtime parameter. Defaults to 10. + """ + self.embedding_model_dims = embedding_model_dims + self.collection_name = collection_name + self.prefix = f"mem0:{collection_name}" + self.timezone = timezone + self.index_type = index_type.lower() + self.hnsw_m = hnsw_m + self.hnsw_ef_construction = hnsw_ef_construction + self.hnsw_ef_runtime = hnsw_ef_runtime + + # Validate index type + if self.index_type not in ["hnsw", "flat"]: + raise ValueError(f"Invalid index_type: {index_type}. Must be 'hnsw' or 'flat'") + + # Connect to Valkey + try: + self.client = valkey.from_url(valkey_url) + logger.debug(f"Successfully connected to Valkey at {valkey_url}") + except Exception as e: + logger.exception(f"Failed to connect to Valkey at {valkey_url}: {e}") + raise + + # Create the index schema + self._create_index(embedding_model_dims) + + def _build_index_schema(self, collection_name, embedding_dims, distance_metric, prefix): + """ + Build the FT.CREATE command for index creation. + + Args: + collection_name (str): Name of the collection/index + embedding_dims (int): Vector embedding dimensions + distance_metric (str): Distance metric (e.g., "COSINE", "L2", "IP") + prefix (str): Key prefix for the index + + Returns: + list: Complete FT.CREATE command as list of arguments + """ + # Build the vector field configuration based on index type + if self.index_type == "hnsw": + vector_config = [ + "embedding", + "VECTOR", + "HNSW", + "12", # Attribute count: TYPE, FLOAT32, DIM, dims, DISTANCE_METRIC, metric, M, m, EF_CONSTRUCTION, ef_construction, EF_RUNTIME, ef_runtime + "TYPE", + "FLOAT32", + "DIM", + str(embedding_dims), + "DISTANCE_METRIC", + distance_metric, + "M", + str(self.hnsw_m), + "EF_CONSTRUCTION", + str(self.hnsw_ef_construction), + "EF_RUNTIME", + str(self.hnsw_ef_runtime), + ] + elif self.index_type == "flat": + vector_config = [ + "embedding", + "VECTOR", + "FLAT", + "6", # Attribute count: TYPE, FLOAT32, DIM, dims, DISTANCE_METRIC, metric + "TYPE", + "FLOAT32", + "DIM", + str(embedding_dims), + "DISTANCE_METRIC", + distance_metric, + ] + else: + # This should never happen due to constructor validation, but be defensive + raise ValueError(f"Unsupported index_type: {self.index_type}. Must be 'hnsw' or 'flat'") + + # Build the complete command (comma is default separator for TAG fields) + cmd = [ + "FT.CREATE", + collection_name, + "ON", + "HASH", + "PREFIX", + "1", + prefix, + "SCHEMA", + "memory_id", + "TAG", + "hash", + "TAG", + "agent_id", + "TAG", + "run_id", + "TAG", + "user_id", + "TAG", + "memory", + "TAG", + "metadata", + "TAG", + "created_at", + "NUMERIC", + "updated_at", + "NUMERIC", + ] + vector_config + + return cmd + + def _create_index(self, embedding_model_dims): + """ + Create the search index with the specified schema. + + Args: + embedding_model_dims (int): Dimensions for the vector embeddings. + + Raises: + ValueError: If the search module is not available. + Exception: For other errors during index creation. + """ + # Check if the search module is available + try: + # Try to execute a search command + self.client.execute_command("FT._LIST") + except ResponseError as e: + if "unknown command" in str(e).lower(): + raise ValueError( + "Valkey search module is not available. Please ensure Valkey is running with the search module enabled. " + "The search module can be loaded using the --loadmodule option with the valkey-search library. " + "For installation and setup instructions, refer to the Valkey Search documentation." + ) + else: + logger.exception(f"Error checking search module: {e}") + raise + + # Check if the index already exists + try: + self.client.ft(self.collection_name).info() + return + except ResponseError as e: + if "not found" not in str(e).lower(): + logger.exception(f"Error checking index existence: {e}") + raise + + # Build and execute the index creation command + cmd = self._build_index_schema( + self.collection_name, + embedding_model_dims, + "COSINE", # Fixed distance metric for initialization + self.prefix, + ) + + try: + self.client.execute_command(*cmd) + logger.info(f"Successfully created {self.index_type.upper()} index {self.collection_name}") + except Exception as e: + logger.exception(f"Error creating index {self.collection_name}: {e}") + raise + + def create_col(self, name=None, vector_size=None, distance=None): + """ + Create a new collection (index) in Valkey. + + Args: + name (str, optional): Name for the collection. Defaults to None, which uses the current collection_name. + vector_size (int, optional): Size of the vector embeddings. Defaults to None, which uses the current embedding_model_dims. + distance (str, optional): Distance metric to use. Defaults to None, which uses 'cosine'. + + Returns: + The created index object. + """ + # Use provided parameters or fall back to instance attributes + collection_name = name or self.collection_name + embedding_dims = vector_size or self.embedding_model_dims + distance_metric = distance or "COSINE" + prefix = f"mem0:{collection_name}" + + # Try to drop the index if it exists (cleanup before creation) + self._drop_index(collection_name, log_level="silent") + + # Build and execute the index creation command + cmd = self._build_index_schema( + collection_name, + embedding_dims, + distance_metric, # Configurable distance metric + prefix, + ) + + try: + self.client.execute_command(*cmd) + logger.info(f"Successfully created {self.index_type.upper()} index {collection_name}") + + # Update instance attributes if creating a new collection + if name: + self.collection_name = collection_name + self.prefix = prefix + + return self.client.ft(collection_name) + except Exception as e: + logger.exception(f"Error creating collection {collection_name}: {e}") + raise + + def insert(self, vectors: list, payloads: list = None, ids: list = None): + """ + Insert vectors and their payloads into the index. + + Args: + vectors (list): List of vectors to insert. + payloads (list, optional): List of payloads corresponding to the vectors. + ids (list, optional): List of IDs for the vectors. + """ + for vector, payload, id in zip(vectors, payloads, ids): + try: + # Create the key for the hash + key = f"{self.prefix}:{id}" + + # Check for required fields and provide defaults if missing + if "data" not in payload: + # Silently use default value for missing 'data' field + pass + + # Ensure created_at is present + if "created_at" not in payload: + payload["created_at"] = datetime.now(pytz.timezone(self.timezone)).isoformat() + + # Prepare the hash data + hash_data = { + "memory_id": id, + "hash": payload.get("hash", f"hash_{id}"), # Use a default hash if not provided + "memory": payload.get("data", f"data_{id}"), # Use a default data if not provided + "created_at": int(datetime.fromisoformat(payload["created_at"]).timestamp()), + "embedding": np.array(vector, dtype=np.float32).tobytes(), + } + + # Add optional fields + for field in ["agent_id", "run_id", "user_id"]: + if field in payload: + hash_data[field] = payload[field] + + # Add metadata + hash_data["metadata"] = json.dumps({k: v for k, v in payload.items() if k not in excluded_keys}) + + # Store in Valkey + self.client.hset(key, mapping=hash_data) + logger.debug(f"Successfully inserted vector with ID {id}") + except KeyError as e: + logger.error(f"Error inserting vector with ID {id}: Missing required field {e}") + except Exception as e: + logger.exception(f"Error inserting vector with ID {id}: {e}") + raise + + def _build_search_query(self, knn_part, filters=None): + """ + Build a search query string with filters. + + Args: + knn_part (str): The KNN part of the query. + filters (dict, optional): Filters to apply to the search. Each key-value pair + becomes a tag filter (@key:{value}). None values are ignored. + Values are used as-is (no validation) - wildcards, lists, etc. are + passed through literally to Valkey search. Multiple filters are + combined with AND logic (space-separated). + + Returns: + str: The complete search query string in format "filter_expr =>[KNN...]" + or "*=>[KNN...]" if no valid filters. + """ + # No filters, just use the KNN search + if not filters or not any(value is not None for key, value in filters.items()): + return f"*=>{knn_part}" + + # Build filter expression + filter_parts = [] + for key, value in filters.items(): + if value is not None: + # Use the correct filter syntax for Valkey + filter_parts.append(f"@{key}:{{{value}}}") + + # No valid filter parts + if not filter_parts: + return f"*=>{knn_part}" + + # Combine filter parts with proper syntax + filter_expr = " ".join(filter_parts) + return f"{filter_expr} =>{knn_part}" + + def _execute_search(self, query, params): + """ + Execute a search query. + + Args: + query (str): The search query to execute. + params (dict): The query parameters. + + Returns: + The search results. + """ + try: + return self.client.ft(self.collection_name).search(query, query_params=params) + except ResponseError as e: + logger.error(f"Search failed with query '{query}': {e}") + raise + + def _process_search_results(self, results): + """ + Process search results into OutputData objects. + + Args: + results: The search results from Valkey. + + Returns: + list: List of OutputData objects. + """ + memory_results = [] + for doc in results.docs: + # Extract the score + score = float(doc.vector_score) if hasattr(doc, "vector_score") else None + + # Create the payload + payload = { + "hash": doc.hash, + "data": doc.memory, + "created_at": self._format_timestamp(int(doc.created_at), self.timezone), + } + + # Add updated_at if available + if hasattr(doc, "updated_at"): + payload["updated_at"] = self._format_timestamp(int(doc.updated_at), self.timezone) + + # Add optional fields + for field in ["agent_id", "run_id", "user_id"]: + if hasattr(doc, field): + payload[field] = getattr(doc, field) + + # Add metadata + if hasattr(doc, "metadata"): + try: + metadata = json.loads(extract_json(doc.metadata)) + payload.update(metadata) + except (json.JSONDecodeError, TypeError) as e: + logger.warning(f"Failed to parse metadata: {e}") + + # Create the result + memory_results.append(OutputData(id=doc.memory_id, score=score, payload=payload)) + + return memory_results + + def search(self, query: str, vectors: list, limit: int = 5, filters: dict = None, ef_runtime: int = None): + """ + Search for similar vectors in the index. + + Args: + query (str): The search query. + vectors (list): The vector to search for. + limit (int, optional): Maximum number of results to return. Defaults to 5. + filters (dict, optional): Filters to apply to the search. Defaults to None. + ef_runtime (int, optional): HNSW ef_runtime parameter for this query. Only used with HNSW index. Defaults to None. + + Returns: + list: List of OutputData objects. + """ + # Convert the vector to bytes + vector_bytes = np.array(vectors, dtype=np.float32).tobytes() + + # Build the KNN part with optional EF_RUNTIME for HNSW + if self.index_type == "hnsw" and ef_runtime is not None: + knn_part = f"[KNN {limit} @embedding $vec_param EF_RUNTIME {ef_runtime} AS vector_score]" + else: + # For FLAT indexes or when ef_runtime is None, use basic KNN + knn_part = f"[KNN {limit} @embedding $vec_param AS vector_score]" + + # Build the complete query + q = self._build_search_query(knn_part, filters) + + # Log the query for debugging (only in debug mode) + logger.debug(f"Valkey search query: {q}") + + # Set up the query parameters + params = {"vec_param": vector_bytes} + + # Execute the search + results = self._execute_search(q, params) + + # Process the results + return self._process_search_results(results) + + def delete(self, vector_id): + """ + Delete a vector from the index. + + Args: + vector_id (str): ID of the vector to delete. + """ + try: + key = f"{self.prefix}:{vector_id}" + self.client.delete(key) + logger.debug(f"Successfully deleted vector with ID {vector_id}") + except Exception as e: + logger.exception(f"Error deleting vector with ID {vector_id}: {e}") + raise + + def update(self, vector_id=None, vector=None, payload=None): + """ + Update a vector in the index. + + Args: + vector_id (str): ID of the vector to update. + vector (list, optional): New vector data. + payload (dict, optional): New payload data. + """ + try: + key = f"{self.prefix}:{vector_id}" + + # Check for required fields and provide defaults if missing + if "data" not in payload: + # Silently use default value for missing 'data' field + pass + + # Ensure created_at is present + if "created_at" not in payload: + payload["created_at"] = datetime.now(pytz.timezone(self.timezone)).isoformat() + + # Prepare the hash data + hash_data = { + "memory_id": vector_id, + "hash": payload.get("hash", f"hash_{vector_id}"), # Use a default hash if not provided + "memory": payload.get("data", f"data_{vector_id}"), # Use a default data if not provided + "created_at": int(datetime.fromisoformat(payload["created_at"]).timestamp()), + "embedding": np.array(vector, dtype=np.float32).tobytes(), + } + + # Add updated_at if available + if "updated_at" in payload: + hash_data["updated_at"] = int(datetime.fromisoformat(payload["updated_at"]).timestamp()) + + # Add optional fields + for field in ["agent_id", "run_id", "user_id"]: + if field in payload: + hash_data[field] = payload[field] + + # Add metadata + hash_data["metadata"] = json.dumps({k: v for k, v in payload.items() if k not in excluded_keys}) + + # Update in Valkey + self.client.hset(key, mapping=hash_data) + logger.debug(f"Successfully updated vector with ID {vector_id}") + except KeyError as e: + logger.error(f"Error updating vector with ID {vector_id}: Missing required field {e}") + except Exception as e: + logger.exception(f"Error updating vector with ID {vector_id}: {e}") + raise + + def _format_timestamp(self, timestamp, timezone=None): + """ + Format a timestamp with the specified timezone. + + Args: + timestamp (int): The timestamp to format. + timezone (str, optional): The timezone to use. Defaults to UTC. + + Returns: + str: The formatted timestamp. + """ + # Use UTC as default timezone if not specified + tz = pytz.timezone(timezone or "UTC") + return datetime.fromtimestamp(timestamp, tz=tz).isoformat(timespec="microseconds") + + def _process_document_fields(self, result, vector_id): + """ + Process document fields from a Valkey hash result. + + Args: + result (dict): The hash result from Valkey. + vector_id (str): The vector ID. + + Returns: + dict: The processed payload. + str: The memory ID. + """ + # Create the payload with error handling + payload = {} + + # Convert bytes to string for text fields + for k in result: + if k not in ["embedding"]: + if isinstance(result[k], bytes): + try: + result[k] = result[k].decode("utf-8") + except UnicodeDecodeError: + # If decoding fails, keep the bytes + pass + + # Add required fields with error handling + for field in ["hash", "memory", "created_at"]: + if field in result: + if field == "created_at": + try: + payload[field] = self._format_timestamp(int(result[field]), self.timezone) + except (ValueError, TypeError): + payload[field] = result[field] + else: + payload[field] = result[field] + else: + # Use default values for missing fields + if field == "hash": + payload[field] = "unknown" + elif field == "memory": + payload[field] = "unknown" + elif field == "created_at": + payload[field] = self._format_timestamp( + int(datetime.now(tz=pytz.timezone(self.timezone)).timestamp()), self.timezone + ) + + # Rename memory to data for consistency + if "memory" in payload: + payload["data"] = payload.pop("memory") + + # Add updated_at if available + if "updated_at" in result: + try: + payload["updated_at"] = self._format_timestamp(int(result["updated_at"]), self.timezone) + except (ValueError, TypeError): + payload["updated_at"] = result["updated_at"] + + # Add optional fields + for field in ["agent_id", "run_id", "user_id"]: + if field in result: + payload[field] = result[field] + + # Add metadata + if "metadata" in result: + try: + metadata = json.loads(extract_json(result["metadata"])) + payload.update(metadata) + except (json.JSONDecodeError, TypeError): + logger.warning(f"Failed to parse metadata: {result.get('metadata')}") + + # Use memory_id from result if available, otherwise use vector_id + memory_id = result.get("memory_id", vector_id) + + return payload, memory_id + + def _convert_bytes(self, data): + """Convert bytes data back to string""" + if isinstance(data, bytes): + try: + return data.decode("utf-8") + except UnicodeDecodeError: + return data + if isinstance(data, dict): + return {self._convert_bytes(key): self._convert_bytes(value) for key, value in data.items()} + if isinstance(data, list): + return [self._convert_bytes(item) for item in data] + if isinstance(data, tuple): + return tuple(self._convert_bytes(item) for item in data) + return data + + def get(self, vector_id): + """ + Get a vector by ID. + + Args: + vector_id (str): ID of the vector to get. + + Returns: + OutputData: The retrieved vector. + """ + try: + key = f"{self.prefix}:{vector_id}" + result = self.client.hgetall(key) + + if not result: + raise KeyError(f"Vector with ID {vector_id} not found") + + # Convert bytes keys/values to strings + result = self._convert_bytes(result) + + logger.debug(f"Retrieved result keys: {result.keys()}") + + # Process the document fields + payload, memory_id = self._process_document_fields(result, vector_id) + + return OutputData(id=memory_id, payload=payload, score=0.0) + except KeyError: + raise + except Exception as e: + logger.exception(f"Error getting vector with ID {vector_id}: {e}") + raise + + def list_cols(self): + """ + List all collections (indices) in Valkey. + + Returns: + list: List of collection names. + """ + try: + # Use the FT._LIST command to list all indices + return self.client.execute_command("FT._LIST") + except Exception as e: + logger.exception(f"Error listing collections: {e}") + raise + + def _drop_index(self, collection_name, log_level="error"): + """ + Drop an index by name using the documented FT.DROPINDEX command. + + Args: + collection_name (str): Name of the index to drop. + log_level (str): Logging level for missing index ("silent", "info", "error"). + """ + try: + self.client.execute_command("FT.DROPINDEX", collection_name) + logger.info(f"Successfully deleted index {collection_name}") + return True + except ResponseError as e: + if "Unknown index name" in str(e): + # Index doesn't exist - handle based on context + if log_level == "silent": + pass # No logging in situations where this is expected such as initial index creation + elif log_level == "info": + logger.info(f"Index {collection_name} doesn't exist, skipping deletion") + return False + else: + # Real error - always log and raise + logger.error(f"Error deleting index {collection_name}: {e}") + raise + except Exception as e: + # Non-ResponseError exceptions - always log and raise + logger.error(f"Error deleting index {collection_name}: {e}") + raise + + def delete_col(self): + """ + Delete the current collection (index). + """ + return self._drop_index(self.collection_name, log_level="info") + + def col_info(self, name=None): + """ + Get information about a collection (index). + + Args: + name (str, optional): Name of the collection. Defaults to None, which uses the current collection_name. + + Returns: + dict: Information about the collection. + """ + try: + collection_name = name or self.collection_name + return self.client.ft(collection_name).info() + except Exception as e: + logger.exception(f"Error getting collection info for {collection_name}: {e}") + raise + + def reset(self): + """ + Reset the index by deleting and recreating it. + """ + try: + collection_name = self.collection_name + logger.warning(f"Resetting index {collection_name}...") + + # Delete the index + self.delete_col() + + # Recreate the index + self._create_index(self.embedding_model_dims) + + return True + except Exception as e: + logger.exception(f"Error resetting index {self.collection_name}: {e}") + raise + + def _build_list_query(self, filters=None): + """ + Build a query for listing vectors. + + Args: + filters (dict, optional): Filters to apply to the list. Each key-value pair + becomes a tag filter (@key:{value}). None values are ignored. + Values are used as-is (no validation) - wildcards, lists, etc. are + passed through literally to Valkey search. + + Returns: + str: The query string. Returns "*" if no valid filters provided. + """ + # Default query + q = "*" + + # Add filters if provided + if filters and any(value is not None for key, value in filters.items()): + filter_conditions = [] + for key, value in filters.items(): + if value is not None: + filter_conditions.append(f"@{key}:{{{value}}}") + + if filter_conditions: + q = " ".join(filter_conditions) + + return q + + def list(self, filters: dict = None, limit: int = None) -> list: + """ + List all recent created memories from the vector store. + + Args: + filters (dict, optional): Filters to apply to the list. Each key-value pair + becomes a tag filter (@key:{value}). None values are ignored. + Values are used as-is without validation - wildcards, special characters, + lists, etc. are passed through literally to Valkey search. + Multiple filters are combined with AND logic. + limit (int, optional): Maximum number of results to return. Defaults to 1000 + if not specified. + + Returns: + list: Nested list format [[MemoryResult(), ...]] matching Redis implementation. + Each MemoryResult contains id and payload with hash, data, timestamps, etc. + """ + try: + # Since Valkey search requires vector format, use a dummy vector search + # that returns all documents by using a zero vector and large K + dummy_vector = [0.0] * self.embedding_model_dims + search_limit = limit if limit is not None else 1000 # Large default + + # Use the existing search method which handles filters properly + search_results = self.search("", dummy_vector, limit=search_limit, filters=filters) + + # Convert search results to list format (match Redis format) + class MemoryResult: + def __init__(self, id: str, payload: dict, score: float = None): + self.id = id + self.payload = payload + self.score = score + + memory_results = [] + for result in search_results: + # Create payload in the expected format + payload = { + "hash": result.payload.get("hash", ""), + "data": result.payload.get("data", ""), + "created_at": result.payload.get("created_at"), + "updated_at": result.payload.get("updated_at"), + } + + # Add metadata (exclude system fields) + for key, value in result.payload.items(): + if key not in ["data", "hash", "created_at", "updated_at"]: + payload[key] = value + + # Create MemoryResult object (matching Redis format) + memory_results.append(MemoryResult(id=result.id, payload=payload)) + + # Return nested list format like Redis + return [memory_results] + + except Exception as e: + logger.exception(f"Error in list method: {e}") + return [[]] # Return empty result on error diff --git a/pyproject.toml b/pyproject.toml index 386e828fa..537c24105 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ vector_stores = [ "psycopg-pool>=3.2.6,<4.0.0", "pymongo>=4.13.2", "pymochow>=2.2.9", + "valkey>=6.0.0", "databricks-sdk>=0.63.0", "azure-identity>=1.24.0", "redis>=5.0.0,<6.0.0", diff --git a/tests/vector_stores/test_valkey.py b/tests/vector_stores/test_valkey.py new file mode 100644 index 000000000..482f9b83d --- /dev/null +++ b/tests/vector_stores/test_valkey.py @@ -0,0 +1,862 @@ +import json +from datetime import datetime +from unittest.mock import MagicMock, patch + +import numpy as np +import pytest +import pytz +from valkey.exceptions import ResponseError + +from mem0.vector_stores.valkey import ValkeyDB + + +@pytest.fixture +def mock_valkey_client(): + """Create a mock Valkey client.""" + with patch("valkey.from_url") as mock_client: + # Mock the ft method + mock_ft = MagicMock() + mock_client.return_value.ft = MagicMock(return_value=mock_ft) + mock_client.return_value.execute_command = MagicMock() + mock_client.return_value.hset = MagicMock() + mock_client.return_value.hgetall = MagicMock() + mock_client.return_value.delete = MagicMock() + yield mock_client.return_value + + +@pytest.fixture +def valkey_db(mock_valkey_client): + """Create a ValkeyDB instance with a mock client.""" + # Initialize the ValkeyDB with test parameters + valkey_db = ValkeyDB( + valkey_url="valkey://localhost:6379", + collection_name="test_collection", + embedding_model_dims=1536, + ) + # Replace the client with our mock + valkey_db.client = mock_valkey_client + return valkey_db + + +def test_search_filter_syntax(valkey_db, mock_valkey_client): + """Test that the search filter syntax is correctly formatted for Valkey.""" + # Mock search results + mock_doc = MagicMock() + mock_doc.memory_id = "test_id" + mock_doc.hash = "test_hash" + mock_doc.memory = "test_data" + mock_doc.created_at = str(int(datetime.now().timestamp())) + mock_doc.metadata = json.dumps({"key": "value"}) + mock_doc.vector_score = "0.5" + + mock_results = MagicMock() + mock_results.docs = [mock_doc] + + mock_ft = mock_valkey_client.ft.return_value + mock_ft.search.return_value = mock_results + + # Test with user_id filter + valkey_db.search( + query="test query", + vectors=np.random.rand(1536).tolist(), + limit=5, + filters={"user_id": "test_user"}, + ) + + # Check that the search was called with the correct filter syntax + args, kwargs = mock_ft.search.call_args + assert "@user_id:{test_user}" in args[0] + assert "=>[KNN" in args[0] + + # Test with multiple filters + valkey_db.search( + query="test query", + vectors=np.random.rand(1536).tolist(), + limit=5, + filters={"user_id": "test_user", "agent_id": "test_agent"}, + ) + + # Check that the search was called with the correct filter syntax + args, kwargs = mock_ft.search.call_args + assert "@user_id:{test_user}" in args[0] + assert "@agent_id:{test_agent}" in args[0] + assert "=>[KNN" in args[0] + + +def test_search_without_filters(valkey_db, mock_valkey_client): + """Test search without filters.""" + # Mock search results + mock_doc = MagicMock() + mock_doc.memory_id = "test_id" + mock_doc.hash = "test_hash" + mock_doc.memory = "test_data" + mock_doc.created_at = str(int(datetime.now().timestamp())) + mock_doc.metadata = json.dumps({"key": "value"}) + mock_doc.vector_score = "0.5" + + mock_results = MagicMock() + mock_results.docs = [mock_doc] + + mock_ft = mock_valkey_client.ft.return_value + mock_ft.search.return_value = mock_results + + # Test without filters + results = valkey_db.search( + query="test query", + vectors=np.random.rand(1536).tolist(), + limit=5, + ) + + # Check that the search was called with the correct syntax + args, kwargs = mock_ft.search.call_args + assert "*=>[KNN" in args[0] + + # Check that results are processed correctly + assert len(results) == 1 + assert results[0].id == "test_id" + assert results[0].payload["hash"] == "test_hash" + assert results[0].payload["data"] == "test_data" + assert "created_at" in results[0].payload + + +def test_insert(valkey_db, mock_valkey_client): + """Test inserting vectors.""" + # Prepare test data + vectors = [np.random.rand(1536).tolist()] + payloads = [{"hash": "test_hash", "data": "test_data", "user_id": "test_user"}] + ids = ["test_id"] + + # Call insert + valkey_db.insert(vectors=vectors, payloads=payloads, ids=ids) + + # Check that hset was called with the correct arguments + mock_valkey_client.hset.assert_called_once() + args, kwargs = mock_valkey_client.hset.call_args + assert args[0] == "mem0:test_collection:test_id" + assert "memory_id" in kwargs["mapping"] + assert kwargs["mapping"]["memory_id"] == "test_id" + assert kwargs["mapping"]["hash"] == "test_hash" + assert kwargs["mapping"]["memory"] == "test_data" + assert kwargs["mapping"]["user_id"] == "test_user" + assert "created_at" in kwargs["mapping"] + assert "embedding" in kwargs["mapping"] + + +def test_insert_handles_missing_created_at(valkey_db, mock_valkey_client): + """Test inserting vectors with missing created_at field.""" + # Prepare test data + vectors = [np.random.rand(1536).tolist()] + payloads = [{"hash": "test_hash", "data": "test_data"}] # No created_at + ids = ["test_id"] + + # Call insert + valkey_db.insert(vectors=vectors, payloads=payloads, ids=ids) + + # Check that hset was called with the correct arguments + mock_valkey_client.hset.assert_called_once() + args, kwargs = mock_valkey_client.hset.call_args + assert "created_at" in kwargs["mapping"] # Should be added automatically + + +def test_delete(valkey_db, mock_valkey_client): + """Test deleting a vector.""" + # Call delete + valkey_db.delete("test_id") + + # Check that delete was called with the correct key + mock_valkey_client.delete.assert_called_once_with("mem0:test_collection:test_id") + + +def test_update(valkey_db, mock_valkey_client): + """Test updating a vector.""" + # Prepare test data + vector = np.random.rand(1536).tolist() + payload = { + "hash": "test_hash", + "data": "updated_data", + "created_at": datetime.now(pytz.timezone("UTC")).isoformat(), + "user_id": "test_user", + } + + # Call update + valkey_db.update(vector_id="test_id", vector=vector, payload=payload) + + # Check that hset was called with the correct arguments + mock_valkey_client.hset.assert_called_once() + args, kwargs = mock_valkey_client.hset.call_args + assert args[0] == "mem0:test_collection:test_id" + assert kwargs["mapping"]["memory_id"] == "test_id" + assert kwargs["mapping"]["memory"] == "updated_data" + + +def test_update_handles_missing_created_at(valkey_db, mock_valkey_client): + """Test updating vectors with missing created_at field.""" + # Prepare test data + vector = np.random.rand(1536).tolist() + payload = {"hash": "test_hash", "data": "updated_data"} # No created_at + + # Call update + valkey_db.update(vector_id="test_id", vector=vector, payload=payload) + + # Check that hset was called with the correct arguments + mock_valkey_client.hset.assert_called_once() + args, kwargs = mock_valkey_client.hset.call_args + assert "created_at" in kwargs["mapping"] # Should be added automatically + + +def test_get(valkey_db, mock_valkey_client): + """Test getting a vector.""" + # Mock hgetall to return a vector + mock_valkey_client.hgetall.return_value = { + "memory_id": "test_id", + "hash": "test_hash", + "memory": "test_data", + "created_at": str(int(datetime.now().timestamp())), + "metadata": json.dumps({"key": "value"}), + "user_id": "test_user", + } + + # Call get + result = valkey_db.get("test_id") + + # Check that hgetall was called with the correct key + mock_valkey_client.hgetall.assert_called_once_with("mem0:test_collection:test_id") + + # Check the result + assert result.id == "test_id" + assert result.payload["hash"] == "test_hash" + assert result.payload["data"] == "test_data" + assert "created_at" in result.payload + assert result.payload["key"] == "value" # From metadata + assert result.payload["user_id"] == "test_user" + + +def test_get_not_found(valkey_db, mock_valkey_client): + """Test getting a vector that doesn't exist.""" + # Mock hgetall to return empty dict (not found) + mock_valkey_client.hgetall.return_value = {} + + # Call get should raise KeyError + with pytest.raises(KeyError, match="Vector with ID test_id not found"): + valkey_db.get("test_id") + + +def test_list_cols(valkey_db, mock_valkey_client): + """Test listing collections.""" + # Reset the mock to clear previous calls + mock_valkey_client.execute_command.reset_mock() + + # Mock execute_command to return list of indices + mock_valkey_client.execute_command.return_value = ["test_collection", "another_collection"] + + # Call list_cols + result = valkey_db.list_cols() + + # Check that execute_command was called with the correct command + mock_valkey_client.execute_command.assert_called_with("FT._LIST") + + # Check the result + assert result == ["test_collection", "another_collection"] + + +def test_delete_col(valkey_db, mock_valkey_client): + """Test deleting a collection.""" + # Reset the mock to clear previous calls + mock_valkey_client.execute_command.reset_mock() + + # Test successful deletion + result = valkey_db.delete_col() + assert result is True + + # Check that execute_command was called with the correct command + mock_valkey_client.execute_command.assert_called_once_with("FT.DROPINDEX", "test_collection") + + # Test error handling - real errors should still raise + mock_valkey_client.execute_command.side_effect = ResponseError("Error dropping index") + with pytest.raises(ResponseError, match="Error dropping index"): + valkey_db.delete_col() + + # Test idempotent behavior - "Unknown index name" should return False, not raise + mock_valkey_client.execute_command.side_effect = ResponseError("Unknown index name") + result = valkey_db.delete_col() + assert result is False + + +def test_context_aware_logging(valkey_db, mock_valkey_client): + """Test that _drop_index handles different log levels correctly.""" + # Mock "Unknown index name" error + mock_valkey_client.execute_command.side_effect = ResponseError("Unknown index name") + + # Test silent mode - should not log anything (we can't easily test log output, but ensure no exception) + result = valkey_db._drop_index("test_collection", log_level="silent") + assert result is False + + # Test info mode - should not raise exception + result = valkey_db._drop_index("test_collection", log_level="info") + assert result is False + + # Test default mode - should not raise exception + result = valkey_db._drop_index("test_collection") + assert result is False + + +def test_col_info(valkey_db, mock_valkey_client): + """Test getting collection info.""" + # Mock ft().info() to return index info + mock_ft = mock_valkey_client.ft.return_value + + # Reset the mock to clear previous calls + mock_ft.info.reset_mock() + + mock_ft.info.return_value = {"index_name": "test_collection", "num_docs": 100} + + # Call col_info + result = valkey_db.col_info() + + # Check that ft().info() was called + assert mock_ft.info.called + + # Check the result + assert result["index_name"] == "test_collection" + assert result["num_docs"] == 100 + + +def test_create_col(valkey_db, mock_valkey_client): + """Test creating a new collection.""" + # Call create_col + valkey_db.create_col(name="new_collection", vector_size=768, distance="IP") + + # Check that execute_command was called to create the index + assert mock_valkey_client.execute_command.called + args = mock_valkey_client.execute_command.call_args[0] + assert args[0] == "FT.CREATE" + assert args[1] == "new_collection" + + # Check that the distance metric was set correctly + distance_metric_index = args.index("DISTANCE_METRIC") + assert args[distance_metric_index + 1] == "IP" + + # Check that the vector size was set correctly + dim_index = args.index("DIM") + assert args[dim_index + 1] == "768" + + +def test_list(valkey_db, mock_valkey_client): + """Test listing vectors.""" + # Mock search results + mock_doc = MagicMock() + mock_doc.memory_id = "test_id" + mock_doc.hash = "test_hash" + mock_doc.memory = "test_data" + mock_doc.created_at = str(int(datetime.now().timestamp())) + mock_doc.metadata = json.dumps({"key": "value"}) + mock_doc.vector_score = "0.5" # Add missing vector_score + + mock_results = MagicMock() + mock_results.docs = [mock_doc] + + mock_ft = mock_valkey_client.ft.return_value + mock_ft.search.return_value = mock_results + + # Call list + results = valkey_db.list(filters={"user_id": "test_user"}, limit=10) + + # Check that search was called with the correct arguments + mock_ft.search.assert_called_once() + args, kwargs = mock_ft.search.call_args + # Now expects full search query with KNN part due to dummy vector approach + assert "@user_id:{test_user}" in args[0] + assert "=>[KNN" in args[0] + # Verify the results format + assert len(results) == 1 + assert len(results[0]) == 1 + assert results[0][0].id == "test_id" + + # Check the results + assert len(results) == 1 # One list of results + assert len(results[0]) == 1 # One result in the list + assert results[0][0].id == "test_id" + assert results[0][0].payload["hash"] == "test_hash" + assert results[0][0].payload["data"] == "test_data" + + +def test_search_error_handling(valkey_db, mock_valkey_client): + """Test search error handling when query fails.""" + # Mock search to fail with an error + mock_ft = mock_valkey_client.ft.return_value + mock_ft.search.side_effect = ResponseError("Invalid filter expression") + + # Call search should raise the error + with pytest.raises(ResponseError, match="Invalid filter expression"): + valkey_db.search( + query="test query", + vectors=np.random.rand(1536).tolist(), + limit=5, + filters={"user_id": "test_user"}, + ) + + # Check that search was called once + assert mock_ft.search.call_count == 1 + + +def test_drop_index_error_handling(valkey_db, mock_valkey_client): + """Test error handling when dropping an index.""" + # Reset the mock to clear previous calls + mock_valkey_client.execute_command.reset_mock() + + # Test 1: Real error (not "Unknown index name") should raise + mock_valkey_client.execute_command.side_effect = ResponseError("Error dropping index") + with pytest.raises(ResponseError, match="Error dropping index"): + valkey_db._drop_index("test_collection") + + # Test 2: "Unknown index name" with default log_level should return False + mock_valkey_client.execute_command.side_effect = ResponseError("Unknown index name") + result = valkey_db._drop_index("test_collection") + assert result is False + + # Test 3: "Unknown index name" with silent log_level should return False + mock_valkey_client.execute_command.side_effect = ResponseError("Unknown index name") + result = valkey_db._drop_index("test_collection", log_level="silent") + assert result is False + + # Test 4: "Unknown index name" with info log_level should return False + mock_valkey_client.execute_command.side_effect = ResponseError("Unknown index name") + result = valkey_db._drop_index("test_collection", log_level="info") + assert result is False + + # Test 5: Successful deletion should return True + mock_valkey_client.execute_command.side_effect = None # Reset to success + result = valkey_db._drop_index("test_collection") + assert result is True + + +def test_reset(valkey_db, mock_valkey_client): + """Test resetting an index.""" + # Mock delete_col and _create_index + with ( + patch.object(valkey_db, "delete_col", return_value=True) as mock_delete_col, + patch.object(valkey_db, "_create_index") as mock_create_index, + ): + # Call reset + result = valkey_db.reset() + + # Check that delete_col and _create_index were called + mock_delete_col.assert_called_once() + mock_create_index.assert_called_once_with(1536) + + # Check the result + assert result is True + + +def test_build_list_query(valkey_db): + """Test building a list query with and without filters.""" + # Test without filters + query = valkey_db._build_list_query(None) + assert query == "*" + + # Test with empty filters + query = valkey_db._build_list_query({}) + assert query == "*" + + # Test with filters + query = valkey_db._build_list_query({"user_id": "test_user"}) + assert query == "@user_id:{test_user}" + + # Test with multiple filters + query = valkey_db._build_list_query({"user_id": "test_user", "agent_id": "test_agent"}) + assert "@user_id:{test_user}" in query + assert "@agent_id:{test_agent}" in query + + +def test_process_document_fields(valkey_db): + """Test processing document fields from hash results.""" + # Create a mock result with all fields + result = { + "memory_id": "test_id", + "hash": "test_hash", + "memory": "test_data", + "created_at": "1625097600", # 2021-07-01 00:00:00 UTC + "updated_at": "1625184000", # 2021-07-02 00:00:00 UTC + "user_id": "test_user", + "agent_id": "test_agent", + "metadata": json.dumps({"key": "value"}), + } + + # Process the document fields + payload, memory_id = valkey_db._process_document_fields(result, "default_id") + + # Check the results + assert memory_id == "test_id" + assert payload["hash"] == "test_hash" + assert payload["data"] == "test_data" # memory renamed to data + assert "created_at" in payload + assert "updated_at" in payload + assert payload["user_id"] == "test_user" + assert payload["agent_id"] == "test_agent" + assert payload["key"] == "value" # From metadata + + # Test with missing fields + result = { + # No memory_id + "hash": "test_hash", + # No memory + # No created_at + } + + # Process the document fields + payload, memory_id = valkey_db._process_document_fields(result, "default_id") + + # Check the results + assert memory_id == "default_id" # Should use default_id + assert payload["hash"] == "test_hash" + assert "data" in payload # Should have default value + assert "created_at" in payload # Should have default value + + +def test_init_connection_error(): + """Test that initialization handles connection errors.""" + # Mock the from_url to raise an exception + with patch("valkey.from_url") as mock_from_url: + mock_from_url.side_effect = Exception("Connection failed") + + # Initialize ValkeyDB should raise the exception + with pytest.raises(Exception, match="Connection failed"): + ValkeyDB( + valkey_url="valkey://localhost:6379", + collection_name="test_collection", + embedding_model_dims=1536, + ) + + +def test_build_search_query(valkey_db): + """Test building search queries with different filter scenarios.""" + # Test with no filters + knn_part = "[KNN 5 @embedding $vec_param AS vector_score]" + query = valkey_db._build_search_query(knn_part) + assert query == f"*=>{knn_part}" + + # Test with empty filters + query = valkey_db._build_search_query(knn_part, {}) + assert query == f"*=>{knn_part}" + + # Test with None values in filters + query = valkey_db._build_search_query(knn_part, {"user_id": None}) + assert query == f"*=>{knn_part}" + + # Test with single filter + query = valkey_db._build_search_query(knn_part, {"user_id": "test_user"}) + assert query == f"@user_id:{{test_user}} =>{knn_part}" + + # Test with multiple filters + query = valkey_db._build_search_query(knn_part, {"user_id": "test_user", "agent_id": "test_agent"}) + assert "@user_id:{test_user}" in query + assert "@agent_id:{test_agent}" in query + assert f"=>{knn_part}" in query + + +def test_get_error_handling(valkey_db, mock_valkey_client): + """Test error handling in the get method.""" + # Mock hgetall to raise an exception + mock_valkey_client.hgetall.side_effect = Exception("Unexpected error") + + # Call get should raise the exception + with pytest.raises(Exception, match="Unexpected error"): + valkey_db.get("test_id") + + +def test_list_error_handling(valkey_db, mock_valkey_client): + """Test error handling in the list method.""" + # Mock search to raise an exception + mock_ft = mock_valkey_client.ft.return_value + mock_ft.search.side_effect = Exception("Unexpected error") + + # Call list should return empty result on error + results = valkey_db.list(filters={"user_id": "test_user"}) + + # Check that the result is an empty list + assert results == [[]] + + +def test_create_index_other_error(): + """Test that initialization handles other errors during index creation.""" + # Mock the execute_command to raise a different error + with patch("valkey.from_url") as mock_client: + mock_client.return_value.execute_command.side_effect = ResponseError("Some other error") + mock_client.return_value.ft = MagicMock() + mock_client.return_value.ft.return_value.info.side_effect = ResponseError("not found") + + # Initialize ValkeyDB should raise the exception + with pytest.raises(ResponseError, match="Some other error"): + ValkeyDB( + valkey_url="valkey://localhost:6379", + collection_name="test_collection", + embedding_model_dims=1536, + ) + + +def test_create_col_error(valkey_db, mock_valkey_client): + """Test error handling in create_col method.""" + # Mock execute_command to raise an exception + mock_valkey_client.execute_command.side_effect = Exception("Failed to create index") + + # Call create_col should raise the exception + with pytest.raises(Exception, match="Failed to create index"): + valkey_db.create_col(name="new_collection", vector_size=768) + + +def test_list_cols_error(valkey_db, mock_valkey_client): + """Test error handling in list_cols method.""" + # Reset the mock to clear previous calls + mock_valkey_client.execute_command.reset_mock() + + # Mock execute_command to raise an exception + mock_valkey_client.execute_command.side_effect = Exception("Failed to list indices") + + # Call list_cols should raise the exception + with pytest.raises(Exception, match="Failed to list indices"): + valkey_db.list_cols() + + +def test_col_info_error(valkey_db, mock_valkey_client): + """Test error handling in col_info method.""" + # Mock ft().info() to raise an exception + mock_ft = mock_valkey_client.ft.return_value + mock_ft.info.side_effect = Exception("Failed to get index info") + + # Call col_info should raise the exception + with pytest.raises(Exception, match="Failed to get index info"): + valkey_db.col_info() + + +# Additional tests to improve coverage + + +def test_invalid_index_type(): + """Test validation of invalid index type.""" + with pytest.raises(ValueError, match="Invalid index_type: invalid. Must be 'hnsw' or 'flat'"): + ValkeyDB( + valkey_url="valkey://localhost:6379", + collection_name="test_collection", + embedding_model_dims=1536, + index_type="invalid", + ) + + +def test_index_existence_check_error(mock_valkey_client): + """Test error handling when checking index existence.""" + # Mock ft().info() to raise a ResponseError that's not "not found" + mock_ft = MagicMock() + mock_ft.info.side_effect = ResponseError("Some other error") + mock_valkey_client.ft.return_value = mock_ft + + with patch("valkey.from_url", return_value=mock_valkey_client): + with pytest.raises(ResponseError): + ValkeyDB( + valkey_url="valkey://localhost:6379", + collection_name="test_collection", + embedding_model_dims=1536, + ) + + +def test_flat_index_creation(mock_valkey_client): + """Test creation of FLAT index type.""" + mock_ft = MagicMock() + # Mock the info method to raise ResponseError with "not found" to trigger index creation + mock_ft.info.side_effect = ResponseError("Index not found") + mock_valkey_client.ft.return_value = mock_ft + + with patch("valkey.from_url", return_value=mock_valkey_client): + # Mock the execute_command to avoid the actual exception + mock_valkey_client.execute_command.return_value = None + + ValkeyDB( + valkey_url="valkey://localhost:6379", + collection_name="test_collection", + embedding_model_dims=1536, + index_type="flat", + ) + + # Verify that execute_command was called (index creation) + assert mock_valkey_client.execute_command.called + + +def test_index_creation_error(mock_valkey_client): + """Test error handling during index creation.""" + mock_ft = MagicMock() + mock_ft.info.side_effect = ResponseError("Unknown index name") # Index doesn't exist + mock_valkey_client.ft.return_value = mock_ft + mock_valkey_client.execute_command.side_effect = Exception("Failed to create index") + + with patch("valkey.from_url", return_value=mock_valkey_client): + with pytest.raises(Exception, match="Failed to create index"): + ValkeyDB( + valkey_url="valkey://localhost:6379", + collection_name="test_collection", + embedding_model_dims=1536, + ) + + +def test_insert_missing_required_field(valkey_db, mock_valkey_client): + """Test error handling when inserting vector with missing required field.""" + # Mock hset to raise KeyError (missing required field) + mock_valkey_client.hset.side_effect = KeyError("missing_field") + + # This should not raise an exception but should log the error + valkey_db.insert(vectors=[np.random.rand(1536).tolist()], payloads=[{"memory": "test"}], ids=["test_id"]) + + +def test_insert_general_error(valkey_db, mock_valkey_client): + """Test error handling for general exceptions during insert.""" + # Mock hset to raise a general exception + mock_valkey_client.hset.side_effect = Exception("Database error") + + with pytest.raises(Exception, match="Database error"): + valkey_db.insert(vectors=[np.random.rand(1536).tolist()], payloads=[{"memory": "test"}], ids=["test_id"]) + + +def test_search_with_invalid_metadata(valkey_db, mock_valkey_client): + """Test search with invalid JSON metadata.""" + # Mock search results with invalid JSON metadata + mock_doc = MagicMock() + mock_doc.memory_id = "test_id" + mock_doc.hash = "test_hash" + mock_doc.memory = "test_data" + mock_doc.created_at = str(int(datetime.now().timestamp())) + mock_doc.metadata = "invalid_json" # Invalid JSON + mock_doc.vector_score = "0.5" + + mock_result = MagicMock() + mock_result.docs = [mock_doc] + mock_valkey_client.ft.return_value.search.return_value = mock_result + + # Should handle invalid JSON gracefully + results = valkey_db.search(query="test query", vectors=np.random.rand(1536).tolist(), limit=5) + + assert len(results) == 1 + + +def test_search_with_hnsw_ef_runtime(valkey_db, mock_valkey_client): + """Test search with HNSW ef_runtime parameter.""" + valkey_db.index_type = "hnsw" + valkey_db.hnsw_ef_runtime = 20 + + mock_result = MagicMock() + mock_result.docs = [] + mock_valkey_client.ft.return_value.search.return_value = mock_result + + valkey_db.search(query="test query", vectors=np.random.rand(1536).tolist(), limit=5) + + # Verify the search was called + assert mock_valkey_client.ft.return_value.search.called + + +def test_delete_error(valkey_db, mock_valkey_client): + """Test error handling during vector deletion.""" + mock_valkey_client.delete.side_effect = Exception("Delete failed") + + with pytest.raises(Exception, match="Delete failed"): + valkey_db.delete("test_id") + + +def test_update_missing_required_field(valkey_db, mock_valkey_client): + """Test error handling when updating vector with missing required field.""" + mock_valkey_client.hset.side_effect = KeyError("missing_field") + + # This should not raise an exception but should log the error + valkey_db.update(vector_id="test_id", vector=np.random.rand(1536).tolist(), payload={"memory": "updated"}) + + +def test_update_general_error(valkey_db, mock_valkey_client): + """Test error handling for general exceptions during update.""" + mock_valkey_client.hset.side_effect = Exception("Update failed") + + with pytest.raises(Exception, match="Update failed"): + valkey_db.update(vector_id="test_id", vector=np.random.rand(1536).tolist(), payload={"memory": "updated"}) + + +def test_get_with_binary_data_and_unicode_error(valkey_db, mock_valkey_client): + """Test get method with binary data that fails UTF-8 decoding.""" + # Mock result with binary data that can't be decoded + mock_result = { + "memory_id": "test_id", + "hash": b"\xff\xfe", # Invalid UTF-8 bytes + "memory": "test_memory", + "created_at": "1234567890", + "updated_at": "invalid_timestamp", + "metadata": "{}", + "embedding": b"binary_embedding_data", + } + mock_valkey_client.hgetall.return_value = mock_result + + result = valkey_db.get("test_id") + + # Should handle binary data gracefully + assert result.id == "test_id" + assert result.payload["data"] == "test_memory" + + +def test_get_with_invalid_timestamps(valkey_db, mock_valkey_client): + """Test get method with invalid timestamp values.""" + mock_result = { + "memory_id": "test_id", + "hash": "test_hash", + "memory": "test_memory", + "created_at": "invalid_timestamp", + "updated_at": "also_invalid", + "metadata": "{}", + "embedding": b"binary_data", + } + mock_valkey_client.hgetall.return_value = mock_result + + result = valkey_db.get("test_id") + + # Should handle invalid timestamps gracefully + assert result.id == "test_id" + assert "created_at" in result.payload + + +def test_get_with_invalid_metadata_json(valkey_db, mock_valkey_client): + """Test get method with invalid JSON metadata.""" + mock_result = { + "memory_id": "test_id", + "hash": "test_hash", + "memory": "test_memory", + "created_at": "1234567890", + "updated_at": "1234567890", + "metadata": "invalid_json{", # Invalid JSON + "embedding": b"binary_data", + } + mock_valkey_client.hgetall.return_value = mock_result + + result = valkey_db.get("test_id") + + # Should handle invalid JSON gracefully + assert result.id == "test_id" + + +def test_list_with_missing_fields_and_defaults(valkey_db, mock_valkey_client): + """Test list method with documents missing various fields.""" + # Mock search results with missing fields but valid timestamps + mock_doc1 = MagicMock() + mock_doc1.memory_id = "fallback_id" + mock_doc1.hash = "test_hash" # Provide valid hash + mock_doc1.memory = "test_memory" # Provide valid memory + mock_doc1.created_at = str(int(datetime.now().timestamp())) # Valid timestamp + mock_doc1.updated_at = str(int(datetime.now().timestamp())) # Valid timestamp + mock_doc1.metadata = json.dumps({"key": "value"}) # Valid JSON + mock_doc1.vector_score = "0.5" + + mock_result = MagicMock() + mock_result.docs = [mock_doc1] + mock_valkey_client.ft.return_value.search.return_value = mock_result + + results = valkey_db.list() + + # Should handle the search-based list approach + assert len(results) == 1 + inner_results = results[0] + assert len(inner_results) == 1 + result = inner_results[0] + assert result.id == "fallback_id" + assert "hash" in result.payload + assert "data" in result.payload # memory is renamed to data From 9e5810dfb7b71287f3325c4dd9bfbdaa00886104 Mon Sep 17 00:00:00 2001 From: Andrew Carbonetto Date: Wed, 10 Sep 2025 14:25:45 -0700 Subject: [PATCH 15/18] Fix bedrock anthropic models to use system field (#3438) Signed-off-by: Andrew Carbonetto --- mem0/llms/aws_bedrock.py | 119 +++++++++++++++++++++++++++++---------- 1 file changed, 89 insertions(+), 30 deletions(-) diff --git a/mem0/llms/aws_bedrock.py b/mem0/llms/aws_bedrock.py index 56fa6ef42..b29cf59b8 100644 --- a/mem0/llms/aws_bedrock.py +++ b/mem0/llms/aws_bedrock.py @@ -136,23 +136,27 @@ class AWSBedrockLLM(LLMBase): else: self._format_messages = self._format_messages_generic - def _format_messages_anthropic(self, messages: List[Dict[str, str]]) -> List[Dict[str, Any]]: + def _format_messages_anthropic(self, messages: List[Dict[str, str]]) -> tuple[List[Dict[str, Any]], Optional[str]]: """Format messages for Anthropic models.""" formatted_messages = [] + system_message = None for message in messages: role = message["role"] content = message["content"] if role == "system": - # Anthropic doesn't support system messages, prepend to first user message - continue + # Anthropic supports system messages as a separate parameter + # see: https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/system-prompts + system_message = content elif role == "user": - formatted_messages.append({"role": "user", "content": [{"type": "text", "text": content}]}) + # Use Converse API format + formatted_messages.append({"role": "user", "content": [{"text": content}]}) elif role == "assistant": - formatted_messages.append({"role": "assistant", "content": [{"type": "text", "text": content}]}) + # Use Converse API format + formatted_messages.append({"role": "assistant", "content": [{"text": content}]}) - return formatted_messages + return formatted_messages, system_message def _format_messages_cohere(self, messages: List[Dict[str, str]]) -> str: """Format messages for Cohere models.""" @@ -451,48 +455,103 @@ class AWSBedrockLLM(LLMBase): logger.error(f"Failed to generate response: {e}") raise RuntimeError(f"Failed to generate response: {e}") + @staticmethod + def _convert_tools_to_converse_format(tools: List[Dict]) -> List[Dict]: + """Convert OpenAI-style tools to Converse API format.""" + if not tools: + return [] + + converse_tools = [] + for tool in tools: + if tool.get("type") == "function" and "function" in tool: + func = tool["function"] + converse_tool = { + "toolSpec": { + "name": func["name"], + "description": func.get("description", ""), + "inputSchema": { + "json": func.get("parameters", {}) + } + } + } + converse_tools.append(converse_tool) + + return converse_tools + def _generate_with_tools(self, messages: List[Dict[str, str]], tools: List[Dict], stream: bool = False) -> Dict[str, Any]: - """Generate response with tool calling support.""" + """Generate response with tool calling support using correct message format.""" # Format messages for tool-enabled models + system_message = None if self.provider == "anthropic": - formatted_messages = self._format_messages_anthropic(messages) + formatted_messages, system_message = self._format_messages_anthropic(messages) elif self.provider == "amazon": formatted_messages = self._format_messages_amazon(messages) else: - formatted_messages = [{"role": "user", "content": messages[-1]["content"]}] + formatted_messages = [{"role": "user", "content": [{"text": messages[-1]["content"]}]}] - # Prepare inference configuration - inference_config = { - "temperature": self.model_config.get("temperature", 0.1), - "maxTokens": self.model_config.get("max_tokens", 2000), - "topP": self.model_config.get("top_p", 0.9), + # Prepare tool configuration in Converse API format + tool_config = None + if tools: + converse_tools = self._convert_tools_to_converse_format(tools) + if converse_tools: + tool_config = {"tools": converse_tools} + + # Prepare converse parameters + converse_params = { + "modelId": self.config.model, + "messages": formatted_messages, + "inferenceConfig": { + "maxTokens": self.model_config.get("max_tokens", 2000), + "temperature": self.model_config.get("temperature", 0.1), + "topP": self.model_config.get("top_p", 0.9), + } } - # Prepare tools configuration - tools_config = {"tools": self._convert_tool_format(tools)} + # Add system message if present (for Anthropic) + if system_message: + converse_params["system"] = [{"text": system_message}] + + # Add tool config if present + if tool_config: + converse_params["toolConfig"] = tool_config # Make API call - response = self.client.converse( - modelId=self.config.model, - messages=formatted_messages, - inferenceConfig=inference_config, - toolConfig=tools_config, - ) + response = self.client.converse(**converse_params) return self._parse_response(response, tools) def _generate_standard(self, messages: List[Dict[str, str]], stream: bool = False) -> str: - """Generate standard text response.""" - # Format messages according to provider + """Generate standard text response using Converse API for Anthropic models.""" + # For Anthropic models, always use Converse API if self.provider == "anthropic": - formatted_messages = self._format_messages_anthropic(messages) - input_body = { + formatted_messages, system_message = self._format_messages_anthropic(messages) + + # Prepare converse parameters + converse_params = { + "modelId": self.config.model, "messages": formatted_messages, - "max_tokens": self.model_config.get("max_tokens", 2000), - "temperature": self.model_config.get("temperature", 0.1), - "top_p": self.model_config.get("top_p", 0.9), - "anthropic_version": "bedrock-2023-05-31", + "inferenceConfig": { + "maxTokens": self.model_config.get("max_tokens", 2000), + "temperature": self.model_config.get("temperature", 0.1), + "topP": self.model_config.get("top_p", 0.9), + } } + + # Add system message if present + if system_message: + converse_params["system"] = [{"text": system_message}] + + # Use converse API for Anthropic models + response = self.client.converse(**converse_params) + + # Parse Converse API response + if hasattr(response, 'output') and hasattr(response.output, 'message'): + return response.output.message.content[0].text + elif 'output' in response and 'message' in response['output']: + return response['output']['message']['content'][0]['text'] + else: + return str(response) + elif self.provider == "amazon" and "nova" in self.config.model.lower(): # Nova models use converse API even without tools formatted_messages = self._format_messages_amazon(messages) From 21d69307dc18129b85e7e5e944addaeb11484eb3 Mon Sep 17 00:00:00 2001 From: Saket Aryan Date: Thu, 11 Sep 2025 19:10:19 +0530 Subject: [PATCH 16/18] docs: Update Search V2/Get All V2 Filters (#3450) --- docs/openapi.json | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/openapi.json b/docs/openapi.json index 9a1cf6f41..16c99114b 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -1076,9 +1076,14 @@ "run_id": {"type": "string"}, "created_at": {"type": "string", "format": "date-time"}, "updated_at": {"type": "string", "format": "date-time"}, - "categories": {"type": "array", "items": {"type": "string"}}, + "categories": {"type": "object", "properties": { + "in": {"type": "array", "items": {"type": "string"}} + }}, "metadata": {"type": "object"}, - "keywords": {"type": "string"} + "keywords": {"type": "object", "properties": { + "contains": {"type": "string"}, + "icontains": {"type": "string"} + }} }, "additionalProperties": { "type": "object", @@ -5091,8 +5096,13 @@ "run_id": {"type": "string"}, "created_at": {"type": "string", "format": "date-time"}, "updated_at": {"type": "string", "format": "date-time"}, - "text": {"type": "string"}, - "categories": {"type": "array", "items": {"type": "string"}}, + "keywords": {"type": "object", "properties": { + "contains": {"type": "string"}, + "icontains": {"type": "string"} + }}, + "categories": {"type": "object", "properties": { + "in": {"type": "array", "items": {"type": "string"}} + }}, "metadata": {"type": "object"} }, "additionalProperties": { From d48b1832c727f59d39c4f0de71ad42e081d5ebd7 Mon Sep 17 00:00:00 2001 From: Parshva Daftari <89991302+parshvadaftari@users.noreply.github.com> Date: Fri, 12 Sep 2025 01:39:39 +0530 Subject: [PATCH 17/18] Fixes ollama and updates openai dependency (#3452) --- mem0/llms/ollama.py | 8 ++++++++ mem0/memory/main.py | 20 ++++++++++++++++---- mem0/vector_stores/faiss.py | 6 ++++++ pyproject.toml | 4 ++-- tests/memory/test_main.py | 8 ++++---- 5 files changed, 36 insertions(+), 10 deletions(-) diff --git a/mem0/llms/ollama.py b/mem0/llms/ollama.py index b19342143..9c5b0f36f 100644 --- a/mem0/llms/ollama.py +++ b/mem0/llms/ollama.py @@ -91,6 +91,14 @@ class OllamaLLM(LLMBase): "messages": messages, } + # Handle JSON response format by modifying the system prompt + if response_format and response_format.get("type") == "json_object": + # Add JSON format instruction to the last message or create a system message + if messages and messages[-1]["role"] == "user": + messages[-1]["content"] += "\n\nPlease respond with valid JSON only." + else: + messages.append({"role": "user", "content": "Please respond with valid JSON only."}) + # Add options for Ollama (temperature, num_predict, top_p) options = { "temperature": self.config.temperature, diff --git a/mem0/memory/main.py b/mem0/memory/main.py index 35c59ab4c..c997983ff 100644 --- a/mem0/memory/main.py +++ b/mem0/memory/main.py @@ -7,6 +7,7 @@ import logging import os import uuid import warnings + from copy import deepcopy from datetime import datetime from typing import Any, Dict, Optional @@ -38,6 +39,9 @@ from mem0.utils.factory import ( VectorStoreFactory, ) +# Suppress SWIG deprecation warnings globally +warnings.filterwarnings("ignore", category=DeprecationWarning, message=".*SwigPy.*") +warnings.filterwarnings("ignore", category=DeprecationWarning, message=".*swigvarlink.*") def _build_filters_and_metadata( *, # Enforce keyword-only arguments @@ -386,8 +390,12 @@ class Memory(MemoryBase): response = "" try: - response = remove_code_blocks(response) - new_memories_with_actions = json.loads(response) + if not response or not response.strip(): + logger.warning("Empty response from LLM, no memories to extract") + new_memories_with_actions = {} + else: + response = remove_code_blocks(response) + new_memories_with_actions = json.loads(response) except Exception as e: logger.error(f"Invalid JSON response: {e}") new_memories_with_actions = {} @@ -1229,8 +1237,12 @@ class AsyncMemory(MemoryBase): logger.error(f"Error in new memory actions response: {e}") response = "" try: - response = remove_code_blocks(response) - new_memories_with_actions = json.loads(response) + if not response or not response.strip(): + logger.warning("Empty response from LLM, no memories to extract") + new_memories_with_actions = {} + else: + response = remove_code_blocks(response) + new_memories_with_actions = json.loads(response) except Exception as e: logger.error(f"Invalid JSON response: {e}") new_memories_with_actions = {} diff --git a/mem0/vector_stores/faiss.py b/mem0/vector_stores/faiss.py index cb2cd2254..141df5eae 100644 --- a/mem0/vector_stores/faiss.py +++ b/mem0/vector_stores/faiss.py @@ -8,7 +8,13 @@ from typing import Dict, List, Optional import numpy as np from pydantic import BaseModel +import warnings + try: + # Suppress SWIG deprecation warnings from FAISS + warnings.filterwarnings("ignore", category=DeprecationWarning, message=".*SwigPy.*") + warnings.filterwarnings("ignore", category=DeprecationWarning, message=".*swigvarlink.*") + logging.getLogger("faiss").setLevel(logging.WARNING) logging.getLogger("faiss.loader").setLevel(logging.WARNING) diff --git a/pyproject.toml b/pyproject.toml index 537c24105..4127345e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ requires-python = ">=3.9,<4.0" dependencies = [ "qdrant-client>=1.9.1", "pydantic>=2.7.3", - "openai>=1.90.0,<1.100.0", + "openai>=1.90.0,<1.110.0", "posthog>=3.5.0", "pytz>=2024.1", "sqlalchemy>=2.0.31", @@ -54,7 +54,7 @@ llms = [ "groq>=0.3.0", "together>=0.2.10", "litellm>=1.74.0", - "openai>=1.90.0,<1.100.0", + "openai>=1.90.0,<1.110.0", "ollama>=0.1.0", "vertexai>=0.1.0", "google-generativeai>=0.3.0", diff --git a/tests/memory/test_main.py b/tests/memory/test_main.py index 90ceff17d..0290fc677 100644 --- a/tests/memory/test_main.py +++ b/tests/memory/test_main.py @@ -66,7 +66,7 @@ class TestAddToVectorStoreErrors: mock_memory.llm.generate_response.side_effect = ['{"facts": ["test fact"]}', ""] # Execute - with caplog.at_level(logging.ERROR): + with caplog.at_level(logging.WARNING): result = mock_memory._add_to_vector_store( messages=[{"role": "user", "content": "test"}], metadata={}, filters={}, infer=True ) @@ -74,7 +74,7 @@ class TestAddToVectorStoreErrors: # Verify assert mock_memory.llm.generate_response.call_count == 2 assert result == [] # Should return empty list when no memories processed - assert "Invalid JSON response" in caplog.text + assert "Empty response from LLM, no memories to extract" in caplog.text @pytest.mark.asyncio @@ -117,11 +117,11 @@ class TestAsyncAddToVectorStoreErrors: mock_capture_event = mocker.MagicMock() mocker.patch("mem0.memory.main.capture_event", mock_capture_event) - with caplog.at_level(logging.ERROR): + with caplog.at_level(logging.WARNING): result = await mock_async_memory._add_to_vector_store( messages=[{"role": "user", "content": "test"}], metadata={}, effective_filters={}, infer=True ) assert result == [] - assert "Invalid JSON response" in caplog.text + assert "Empty response from LLM, no memories to extract" in caplog.text assert mock_capture_event.call_count == 1 From a0685f3e8cd8951437a201192e5542d7c7db1f72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=97=A2=20=E5=BE=87=20=E2=97=A4?= <24852067+codexvn@users.noreply.github.com> Date: Fri, 12 Sep 2025 17:37:37 +0800 Subject: [PATCH 18/18] fix: correct typo in knowledge graph extraction guidelines (#3449) --- mem0/graphs/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mem0/graphs/utils.py b/mem0/graphs/utils.py index de1384774..ffa14f55e 100644 --- a/mem0/graphs/utils.py +++ b/mem0/graphs/utils.py @@ -50,7 +50,7 @@ Entity Consistency: - Ensure that relationships are coherent and logically align with the context of the message. - Maintain consistent naming for entities across the extracted data. -Strive to construct a coherent and easily understandable knowledge graph by eshtablishing all the relationships among the entities and adherence to the user’s context. +Strive to construct a coherent and easily understandable knowledge graph by establishing all the relationships among the entities and adherence to the user’s context. Adhere strictly to these guidelines to ensure high-quality knowledge graph extraction."""