diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b4420bc9..28dad2a7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,18 +16,19 @@ To make a contribution, follow these steps: For more details about pull requests, please read [GitHub's guides](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request). -### ๐Ÿ“ฆ Package manager +### ๐Ÿ“ฆ Development Environment -We use `poetry` as our package manager. You can install poetry by following the instructions [here](https://python-poetry.org/docs/#installation). - -Please DO NOT use pip or conda to install the dependencies. Instead, use poetry: +We use `hatch` for managing development environments. To set up: ```bash -make install_all +# Activate environment for specific Python version: +hatch shell dev_py_3_9 # Python 3.9 +hatch shell dev_py_3_10 # Python 3.10 +hatch shell dev_py_3_11 # Python 3.11 -#activate - -poetry shell +# The environment will automatically install all dev dependencies +# Run tests within the activated shell: +make test ``` ### ๐Ÿ“Œ Pre-commit @@ -40,16 +41,21 @@ pre-commit install ### ๐Ÿงช Testing -We use `pytest` to test our code. You can run the tests by running the following command: +We use `pytest` to test our code across multiple Python versions. You can run tests using: ```bash -poetry run pytest tests - -# or - +# Run tests with default Python version make test + +# Test specific Python versions: +make test-py-3.9 # Python 3.9 environment +make test-py-3.10 # Python 3.10 environment +make test-py-3.11 # Python 3.11 environment + +# When using hatch shells, run tests with: +make test # After activating a shell with hatch shell test_XX ``` -Several packages have been removed from Poetry to make the package lighter. Therefore, it is recommended to run `make install_all` to install the remaining packages and ensure all tests pass. Make sure that all tests pass before submitting a pull request. +Make sure that all tests pass across all supported Python versions before submitting a pull request. -We look forward to your pull requests and can't wait to see your contributions! \ No newline at end of file +We look forward to your pull requests and can't wait to see your contributions! diff --git a/Makefile b/Makefile index 3c93d58c..59c745ca 100644 --- a/Makefile +++ b/Makefile @@ -41,3 +41,12 @@ clean: test: hatch run test + +test-py-3.9: + hatch run dev_py_3_9:test + +test-py-3.10: + hatch run dev_py_3_10:test + +test-py-3.11: + hatch run dev_py_3_11:test diff --git a/mem0/memory/main.py b/mem0/memory/main.py index f86fdfbf..56814895 100644 --- a/mem0/memory/main.py +++ b/mem0/memory/main.py @@ -28,8 +28,8 @@ from mem0.memory.utils import ( get_fact_retrieval_messages, parse_messages, parse_vision_messages, - remove_code_blocks, process_telemetry_filters, + remove_code_blocks, ) from mem0.utils.factory import EmbedderFactory, LlmFactory, VectorStoreFactory @@ -338,10 +338,9 @@ class Memory(MemoryBase): except Exception as e: logging.error(f"Error in new_retrieved_facts: {e}") new_retrieved_facts = [] - + if not new_retrieved_facts: logger.debug("No new facts retrieved from input. Skipping memory update LLM call.") - return [] retrieved_old_memory = [] new_message_embeddings = {} @@ -369,24 +368,27 @@ class Memory(MemoryBase): temp_uuid_mapping[str(idx)] = item["id"] retrieved_old_memory[idx]["id"] = str(idx) - function_calling_prompt = get_update_memory_messages( - retrieved_old_memory, new_retrieved_facts, self.config.custom_update_memory_prompt - ) - - try: - response: str = self.llm.generate_response( - messages=[{"role": "user", "content": function_calling_prompt}], - response_format={"type": "json_object"}, + if new_retrieved_facts: + function_calling_prompt = get_update_memory_messages( + retrieved_old_memory, new_retrieved_facts, self.config.custom_update_memory_prompt ) - except Exception as e: - logging.error(f"Error in new memory actions response: {e}") - response = "" - try: - response = remove_code_blocks(response) - new_memories_with_actions = json.loads(response) - except Exception as e: - logging.error(f"Invalid JSON response: {e}") + try: + response: str = self.llm.generate_response( + messages=[{"role": "user", "content": function_calling_prompt}], + response_format={"type": "json_object"}, + ) + except Exception as e: + logging.error(f"Error in new memory actions response: {e}") + response = "" + + try: + response = remove_code_blocks(response) + new_memories_with_actions = json.loads(response) + except Exception as e: + logging.error(f"Invalid JSON response: {e}") + new_memories_with_actions = {} + else: new_memories_with_actions = {} returned_memories = [] @@ -1162,13 +1164,11 @@ class AsyncMemory(MemoryBase): response = remove_code_blocks(response) new_retrieved_facts = json.loads(response)["facts"] except Exception as e: - new_retrieved_facts = [] - - if not new_retrieved_facts: - logger.info("No new facts retrieved from input. Skipping memory update LLM call.") - return [] logging.error(f"Error in new_retrieved_facts: {e}") new_retrieved_facts = [] + + if not new_retrieved_facts: + logger.debug("No new facts retrieved from input. Skipping memory update LLM call.") retrieved_old_memory = [] new_message_embeddings = {} @@ -1200,31 +1200,25 @@ class AsyncMemory(MemoryBase): temp_uuid_mapping[str(idx)] = item["id"] retrieved_old_memory[idx]["id"] = str(idx) - function_calling_prompt = get_update_memory_messages( - retrieved_old_memory, new_retrieved_facts, self.config.custom_update_memory_prompt - ) - try: - response = await asyncio.to_thread( - self.llm.generate_response, - messages=[{"role": "user", "content": function_calling_prompt}], - response_format={"type": "json_object"}, + if new_retrieved_facts: + function_calling_prompt = get_update_memory_messages( + retrieved_old_memory, new_retrieved_facts, self.config.custom_update_memory_prompt ) - except Exception as e: - response = "" - logging.error(f"Error in new memory actions response: {e}") - response = "" - try: - response = remove_code_blocks(response) - new_memories_with_actions = json.loads(response) - except Exception as e: - new_memories_with_actions = {} - - if not new_memories_with_actions: - logger.info("No new facts retrieved from input (async). Skipping memory update LLM call.") - return [] - - logging.error(f"Invalid JSON response: {e}") - new_memories_with_actions = {} + try: + response = await asyncio.to_thread( + self.llm.generate_response, + messages=[{"role": "user", "content": function_calling_prompt}], + response_format={"type": "json_object"}, + ) + except Exception as e: + logging.error(f"Error in new memory actions response: {e}") + response = "" + try: + response = remove_code_blocks(response) + new_memories_with_actions = json.loads(response) + except Exception as e: + logging.error(f"Invalid JSON response: {e}") + new_memories_with_actions = {} returned_memories = [] try: diff --git a/pyproject.toml b/pyproject.toml index 1ade265f..dd882d05 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,32 @@ graph = [ "neo4j>=5.23.1", "rank-bm25>=0.2.2", ] +vector_stores = [ + "vecs>=0.4.0", + "chromadb>=0.4.24", + "weaviate-client>=4.4.0", + "pinecone<7.0.0", + "pinecone-text>=0.1.1", + "faiss-cpu>=1.7.4", + "upstash-vector>=0.1.0", + "azure-search-documents>=11.4.0b8", +] +llms = [ + "groq>=0.3.0", + "together>=0.2.10", + "litellm>=0.1.0", + "ollama>=0.1.0", + "vertexai>=0.1.0", + "google-generativeai>=0.3.0", +] +extras = [ + "boto3>=1.34.0", + "langchain-community>=0.0.0", + "sentence-transformers>=2.2.2", + "elasticsearch>=8.0.0", + "opensearch-py>=2.0.0", + "langchain-memgraph>=0.1.0", +] test = [ "pytest>=8.2.2", "pytest-mock>=3.14.0", @@ -53,6 +79,36 @@ only-include = ["mem0"] [tool.hatch.build.targets.wheel.shared-data] "README.md" = "README.md" +[tool.hatch.envs.dev_py_3_9] +python = "3.9" +features = [ + "test", + "graph", + "vector_stores", + "llms", + "extras", +] + +[tool.hatch.envs.dev_py_3_10] +python = "3.10" +features = [ + "test", + "graph", + "vector_stores", + "llms", + "extras", +] + +[tool.hatch.envs.dev_py_3_11] +python = "3.11" +features = [ + "test", + "graph", + "vector_stores", + "llms", + "extras", +] + [tool.hatch.envs.default.scripts] format = [ "ruff format", diff --git a/tests/memory/test_main.py b/tests/memory/test_main.py index 64a8f837..90ceff17 100644 --- a/tests/memory/test_main.py +++ b/tests/memory/test_main.py @@ -40,10 +40,12 @@ class TestAddToVectorStoreErrors: return memory - def test_empty_llm_response_fact_extraction(self, mock_memory, caplog): + def test_empty_llm_response_fact_extraction(self, mocker, mock_memory, caplog): """Test empty response from LLM during fact extraction""" # Setup mock_memory.llm.generate_response.return_value = "" + mock_capture_event = mocker.MagicMock() + mocker.patch("mem0.memory.main.capture_event", mock_capture_event) # Execute with caplog.at_level(logging.ERROR): @@ -52,9 +54,10 @@ class TestAddToVectorStoreErrors: ) # Verify - assert mock_memory.llm.generate_response.call_count == 2 + assert mock_memory.llm.generate_response.call_count == 1 assert result == [] # Should return empty list when no memories processed assert "Error in new_retrieved_facts" in caplog.text + assert mock_capture_event.call_count == 1 def test_empty_llm_response_memory_actions(self, mock_memory, caplog): """Test empty response from LLM during memory actions""" @@ -94,25 +97,31 @@ class TestAsyncAddToVectorStoreErrors: """Test empty response in AsyncMemory._add_to_vector_store""" mocker.patch("mem0.utils.factory.EmbedderFactory.create", return_value=MagicMock()) mock_async_memory.llm.generate_response.return_value = "" + mock_capture_event = mocker.MagicMock() + mocker.patch("mem0.memory.main.capture_event", mock_capture_event) with caplog.at_level(logging.ERROR): result = await mock_async_memory._add_to_vector_store( - messages=[{"role": "user", "content": "test"}], metadata={}, filters={}, infer=True + messages=[{"role": "user", "content": "test"}], metadata={}, effective_filters={}, infer=True ) - + assert mock_async_memory.llm.generate_response.call_count == 1 assert result == [] assert "Error in new_retrieved_facts" in caplog.text + assert mock_capture_event.call_count == 1 @pytest.mark.asyncio async def test_async_empty_llm_response_memory_actions(self, mock_async_memory, caplog, mocker): """Test empty response in AsyncMemory._add_to_vector_store""" mocker.patch("mem0.utils.factory.EmbedderFactory.create", return_value=MagicMock()) mock_async_memory.llm.generate_response.side_effect = ['{"facts": ["test fact"]}', ""] + mock_capture_event = mocker.MagicMock() + mocker.patch("mem0.memory.main.capture_event", mock_capture_event) with caplog.at_level(logging.ERROR): result = await mock_async_memory._add_to_vector_store( - messages=[{"role": "user", "content": "test"}], metadata={}, filters={}, infer=True + messages=[{"role": "user", "content": "test"}], metadata={}, effective_filters={}, infer=True ) assert result == [] assert "Invalid JSON response" in caplog.text + assert mock_capture_event.call_count == 1 diff --git a/tests/test_main.py b/tests/test_main.py index 41afa05b..c5b45b34 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -19,13 +19,14 @@ def mock_openai(): def memory_instance(): with ( patch("mem0.utils.factory.EmbedderFactory") as mock_embedder, - patch("mem0.utils.factory.VectorStoreFactory") as mock_vector_store, + patch("mem0.memory.main.VectorStoreFactory") as mock_vector_store, patch("mem0.utils.factory.LlmFactory") as mock_llm, patch("mem0.memory.telemetry.capture_event"), patch("mem0.memory.graph_memory.MemoryGraph"), ): mock_embedder.create.return_value = Mock() mock_vector_store.create.return_value = Mock() + mock_vector_store.create.return_value.search.return_value = [] mock_llm.create.return_value = Mock() config = MemoryConfig(version="v1.1") @@ -37,13 +38,14 @@ def memory_instance(): def memory_custom_instance(): with ( patch("mem0.utils.factory.EmbedderFactory") as mock_embedder, - patch("mem0.utils.factory.VectorStoreFactory") as mock_vector_store, + patch("mem0.memory.main.VectorStoreFactory") as mock_vector_store, patch("mem0.utils.factory.LlmFactory") as mock_llm, patch("mem0.memory.telemetry.capture_event"), patch("mem0.memory.graph_memory.MemoryGraph"), ): mock_embedder.create.return_value = Mock() mock_vector_store.create.return_value = Mock() + mock_vector_store.create.return_value.search.return_value = [] mock_llm.create.return_value = Mock() config = MemoryConfig( @@ -250,7 +252,11 @@ def test_get_all(memory_instance, version, enable_graph, expected_result): def test_custom_prompts(memory_custom_instance): messages = [{"role": "user", "content": "Test message"}] + from mem0.embeddings.mock import MockEmbeddings memory_custom_instance.llm.generate_response = Mock() + memory_custom_instance.llm.generate_response.return_value = '{"facts": ["fact1", "fact2"]}' + memory_custom_instance.embedding_model = MockEmbeddings() + with patch("mem0.memory.main.parse_messages", return_value="Test message") as mock_parse_messages: with patch( @@ -273,7 +279,7 @@ def test_custom_prompts(memory_custom_instance): ## custom update memory prompt ## mock_get_update_memory_messages.assert_called_once_with( - [], [], memory_custom_instance.config.custom_update_memory_prompt + [], ["fact1", "fact2"], memory_custom_instance.config.custom_update_memory_prompt ) memory_custom_instance.llm.generate_response.assert_any_call(