Repository: mem0ai/mem0 (python)

Issue:
error logging lines are unreachable and `e` is unbound
### 🐛 Describe the bug

In `mem0/memory/main.py` error logging lines are unreachable and `e` is unbound for
`AsyncMemory._add_to_vector_store`
```
logging.error(f"Error in new_retrieved_facts: {e}")
```
and
```
logging.error(f"Invalid JSON response: {e}")
```

Pull request description:
Feat/add python version test envs
## Description

## Dependency Management Improvements

### Changes Made:
1. **Dependency Version Pinning**:
   - Pinned `pinecone` to version `<7.0.0` in Makefile to ensure compatibility

2. **Test Environment Expansion**:
   - Added dedicated test environments for Python 3.9, 3.10, and 3.11 in pyproject.toml
   - Each environment includes all required dependency groups (test, graph, vector_stores, llms, extras)

3. **Dependency Organization**:
   - Restructured dependencies in pyproject.toml into logical groups:
     - `vector_stores`: All vector database clients
     - `llms`: Language model providers
     - `extras`: Additional utility packages

### Testing:
- All existing tests pass across Python 3.9-3.11 environments
- Verified dependency resolution works correctly with pinned versions

### Impact:
- Improves maintainability by better organizing dependencies
- Ensures compatibility with specific Pinecone versions
- Expands testing coverage across Python versions

Fixes #2867 and #2868

## Type of change

Please delete options that are not relevant.

- [x] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration

Please delete options that are not relevant.

- [x] Unit Test

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream modules
- [x] I have checked my code and corrected any misspellings

## Maintainer Checklist

- [ ] closes #xxxx (Replace xxxx with the GitHub issue number)
- [ ] Made sure Checks passed


Patch:
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
+# 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
 
-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
+# Run tests with default Python version
+make test
 
-# or
+# 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
 
-make test
+# 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 91f123b8..59c745ca 100644
--- a/Makefile
+++ b/Makefile
@@ -12,7 +12,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 pinecone-text faiss-cpu langchain-community \
+	                        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 rank-bm25
 
 # Format code with ruff
@@ -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/client/main.py b/mem0/client/main.py
index 5bdf9210..e7bc245c 100644
--- a/mem0/client/main.py
+++ b/mem0/client/main.py
@@ -472,6 +472,22 @@ class MemoryClient:
         capture_client_event("client.get_memory_export", self, {"keys": list(kwargs.keys()), "sync_type": "sync"})
         return response.json()
 
+    @api_error_handler
+    def get_summary(self, filters: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
+        """Get the summary of a memory export.
+
+        Args:
+            filters: Optional filters to apply to the summary request
+
+        Returns:
+            Dict containing the export status and summary data
+        """
+
+        response = self.client.post("/v1/summary/", json=self._prepare_params({"filters": filters}))
+        response.raise_for_status()
+        capture_client_event("client.get_summary", self, {"sync_type": "sync"})
+        return response.json()
+
     @api_error_handler
     def get_project(self, fields: Optional[List[str]] = None) -> Dict[str, Any]:
         """Get instructions or categories for the current project.
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 1bed7579..dd882d05 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
 
 [project]
 name = "mem0ai"
-version = "0.1.107"
+version = "0.1.107rc2"
 description = "Long-term memory for AI Agents"
 authors = [
     { name = "Mem0", email = "founders@mem0.ai" }
@@ -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",


Test patch:
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(