Feat/add python version test envs (#2774)
This commit is contained in:
@@ -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).
|
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).
|
We use `hatch` for managing development environments. To set up:
|
||||||
|
|
||||||
Please DO NOT use pip or conda to install the dependencies. Instead, use poetry:
|
|
||||||
|
|
||||||
```bash
|
```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
|
# The environment will automatically install all dev dependencies
|
||||||
|
# Run tests within the activated shell:
|
||||||
poetry shell
|
make test
|
||||||
```
|
```
|
||||||
|
|
||||||
### 📌 Pre-commit
|
### 📌 Pre-commit
|
||||||
@@ -40,16 +41,21 @@ pre-commit install
|
|||||||
|
|
||||||
### 🧪 Testing
|
### 🧪 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
|
```bash
|
||||||
poetry run pytest tests
|
# Run tests with default Python version
|
||||||
|
|
||||||
# or
|
|
||||||
|
|
||||||
make test
|
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!
|
We look forward to your pull requests and can't wait to see your contributions!
|
||||||
|
|||||||
9
Makefile
9
Makefile
@@ -41,3 +41,12 @@ clean:
|
|||||||
|
|
||||||
test:
|
test:
|
||||||
hatch run 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
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ from mem0.memory.utils import (
|
|||||||
get_fact_retrieval_messages,
|
get_fact_retrieval_messages,
|
||||||
parse_messages,
|
parse_messages,
|
||||||
parse_vision_messages,
|
parse_vision_messages,
|
||||||
remove_code_blocks,
|
|
||||||
process_telemetry_filters,
|
process_telemetry_filters,
|
||||||
|
remove_code_blocks,
|
||||||
)
|
)
|
||||||
from mem0.utils.factory import EmbedderFactory, LlmFactory, VectorStoreFactory
|
from mem0.utils.factory import EmbedderFactory, LlmFactory, VectorStoreFactory
|
||||||
|
|
||||||
@@ -338,10 +338,9 @@ class Memory(MemoryBase):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error in new_retrieved_facts: {e}")
|
logging.error(f"Error in new_retrieved_facts: {e}")
|
||||||
new_retrieved_facts = []
|
new_retrieved_facts = []
|
||||||
|
|
||||||
if not new_retrieved_facts:
|
if not new_retrieved_facts:
|
||||||
logger.debug("No new facts retrieved from input. Skipping memory update LLM call.")
|
logger.debug("No new facts retrieved from input. Skipping memory update LLM call.")
|
||||||
return []
|
|
||||||
|
|
||||||
retrieved_old_memory = []
|
retrieved_old_memory = []
|
||||||
new_message_embeddings = {}
|
new_message_embeddings = {}
|
||||||
@@ -369,24 +368,27 @@ class Memory(MemoryBase):
|
|||||||
temp_uuid_mapping[str(idx)] = item["id"]
|
temp_uuid_mapping[str(idx)] = item["id"]
|
||||||
retrieved_old_memory[idx]["id"] = str(idx)
|
retrieved_old_memory[idx]["id"] = str(idx)
|
||||||
|
|
||||||
function_calling_prompt = get_update_memory_messages(
|
if new_retrieved_facts:
|
||||||
retrieved_old_memory, new_retrieved_facts, self.config.custom_update_memory_prompt
|
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"},
|
|
||||||
)
|
)
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error in new memory actions response: {e}")
|
|
||||||
response = ""
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = remove_code_blocks(response)
|
response: str = self.llm.generate_response(
|
||||||
new_memories_with_actions = json.loads(response)
|
messages=[{"role": "user", "content": function_calling_prompt}],
|
||||||
except Exception as e:
|
response_format={"type": "json_object"},
|
||||||
logging.error(f"Invalid JSON response: {e}")
|
)
|
||||||
|
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 = {}
|
new_memories_with_actions = {}
|
||||||
|
|
||||||
returned_memories = []
|
returned_memories = []
|
||||||
@@ -1162,13 +1164,11 @@ class AsyncMemory(MemoryBase):
|
|||||||
response = remove_code_blocks(response)
|
response = remove_code_blocks(response)
|
||||||
new_retrieved_facts = json.loads(response)["facts"]
|
new_retrieved_facts = json.loads(response)["facts"]
|
||||||
except Exception as e:
|
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}")
|
logging.error(f"Error in new_retrieved_facts: {e}")
|
||||||
new_retrieved_facts = []
|
new_retrieved_facts = []
|
||||||
|
|
||||||
|
if not new_retrieved_facts:
|
||||||
|
logger.debug("No new facts retrieved from input. Skipping memory update LLM call.")
|
||||||
|
|
||||||
retrieved_old_memory = []
|
retrieved_old_memory = []
|
||||||
new_message_embeddings = {}
|
new_message_embeddings = {}
|
||||||
@@ -1200,31 +1200,25 @@ class AsyncMemory(MemoryBase):
|
|||||||
temp_uuid_mapping[str(idx)] = item["id"]
|
temp_uuid_mapping[str(idx)] = item["id"]
|
||||||
retrieved_old_memory[idx]["id"] = str(idx)
|
retrieved_old_memory[idx]["id"] = str(idx)
|
||||||
|
|
||||||
function_calling_prompt = get_update_memory_messages(
|
if new_retrieved_facts:
|
||||||
retrieved_old_memory, new_retrieved_facts, self.config.custom_update_memory_prompt
|
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"},
|
|
||||||
)
|
)
|
||||||
except Exception as e:
|
try:
|
||||||
response = ""
|
response = await asyncio.to_thread(
|
||||||
logging.error(f"Error in new memory actions response: {e}")
|
self.llm.generate_response,
|
||||||
response = ""
|
messages=[{"role": "user", "content": function_calling_prompt}],
|
||||||
try:
|
response_format={"type": "json_object"},
|
||||||
response = remove_code_blocks(response)
|
)
|
||||||
new_memories_with_actions = json.loads(response)
|
except Exception as e:
|
||||||
except Exception as e:
|
logging.error(f"Error in new memory actions response: {e}")
|
||||||
new_memories_with_actions = {}
|
response = ""
|
||||||
|
try:
|
||||||
if not new_memories_with_actions:
|
response = remove_code_blocks(response)
|
||||||
logger.info("No new facts retrieved from input (async). Skipping memory update LLM call.")
|
new_memories_with_actions = json.loads(response)
|
||||||
return []
|
except Exception as e:
|
||||||
|
logging.error(f"Invalid JSON response: {e}")
|
||||||
logging.error(f"Invalid JSON response: {e}")
|
new_memories_with_actions = {}
|
||||||
new_memories_with_actions = {}
|
|
||||||
|
|
||||||
returned_memories = []
|
returned_memories = []
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -26,6 +26,32 @@ graph = [
|
|||||||
"neo4j>=5.23.1",
|
"neo4j>=5.23.1",
|
||||||
"rank-bm25>=0.2.2",
|
"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 = [
|
test = [
|
||||||
"pytest>=8.2.2",
|
"pytest>=8.2.2",
|
||||||
"pytest-mock>=3.14.0",
|
"pytest-mock>=3.14.0",
|
||||||
@@ -53,6 +79,36 @@ only-include = ["mem0"]
|
|||||||
[tool.hatch.build.targets.wheel.shared-data]
|
[tool.hatch.build.targets.wheel.shared-data]
|
||||||
"README.md" = "README.md"
|
"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]
|
[tool.hatch.envs.default.scripts]
|
||||||
format = [
|
format = [
|
||||||
"ruff format",
|
"ruff format",
|
||||||
|
|||||||
@@ -40,10 +40,12 @@ class TestAddToVectorStoreErrors:
|
|||||||
|
|
||||||
return memory
|
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"""
|
"""Test empty response from LLM during fact extraction"""
|
||||||
# Setup
|
# Setup
|
||||||
mock_memory.llm.generate_response.return_value = ""
|
mock_memory.llm.generate_response.return_value = ""
|
||||||
|
mock_capture_event = mocker.MagicMock()
|
||||||
|
mocker.patch("mem0.memory.main.capture_event", mock_capture_event)
|
||||||
|
|
||||||
# Execute
|
# Execute
|
||||||
with caplog.at_level(logging.ERROR):
|
with caplog.at_level(logging.ERROR):
|
||||||
@@ -52,9 +54,10 @@ class TestAddToVectorStoreErrors:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Verify
|
# 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 result == [] # Should return empty list when no memories processed
|
||||||
assert "Error in new_retrieved_facts" in caplog.text
|
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):
|
def test_empty_llm_response_memory_actions(self, mock_memory, caplog):
|
||||||
"""Test empty response from LLM during memory actions"""
|
"""Test empty response from LLM during memory actions"""
|
||||||
@@ -94,25 +97,31 @@ class TestAsyncAddToVectorStoreErrors:
|
|||||||
"""Test empty response in AsyncMemory._add_to_vector_store"""
|
"""Test empty response in AsyncMemory._add_to_vector_store"""
|
||||||
mocker.patch("mem0.utils.factory.EmbedderFactory.create", return_value=MagicMock())
|
mocker.patch("mem0.utils.factory.EmbedderFactory.create", return_value=MagicMock())
|
||||||
mock_async_memory.llm.generate_response.return_value = ""
|
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):
|
with caplog.at_level(logging.ERROR):
|
||||||
result = await mock_async_memory._add_to_vector_store(
|
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 result == []
|
||||||
assert "Error in new_retrieved_facts" in caplog.text
|
assert "Error in new_retrieved_facts" in caplog.text
|
||||||
|
assert mock_capture_event.call_count == 1
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_async_empty_llm_response_memory_actions(self, mock_async_memory, caplog, mocker):
|
async def test_async_empty_llm_response_memory_actions(self, mock_async_memory, caplog, mocker):
|
||||||
"""Test empty response in AsyncMemory._add_to_vector_store"""
|
"""Test empty response in AsyncMemory._add_to_vector_store"""
|
||||||
mocker.patch("mem0.utils.factory.EmbedderFactory.create", return_value=MagicMock())
|
mocker.patch("mem0.utils.factory.EmbedderFactory.create", return_value=MagicMock())
|
||||||
mock_async_memory.llm.generate_response.side_effect = ['{"facts": ["test fact"]}', ""]
|
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):
|
with caplog.at_level(logging.ERROR):
|
||||||
result = await mock_async_memory._add_to_vector_store(
|
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 result == []
|
||||||
assert "Invalid JSON response" in caplog.text
|
assert "Invalid JSON response" in caplog.text
|
||||||
|
assert mock_capture_event.call_count == 1
|
||||||
|
|||||||
@@ -19,13 +19,14 @@ def mock_openai():
|
|||||||
def memory_instance():
|
def memory_instance():
|
||||||
with (
|
with (
|
||||||
patch("mem0.utils.factory.EmbedderFactory") as mock_embedder,
|
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.utils.factory.LlmFactory") as mock_llm,
|
||||||
patch("mem0.memory.telemetry.capture_event"),
|
patch("mem0.memory.telemetry.capture_event"),
|
||||||
patch("mem0.memory.graph_memory.MemoryGraph"),
|
patch("mem0.memory.graph_memory.MemoryGraph"),
|
||||||
):
|
):
|
||||||
mock_embedder.create.return_value = Mock()
|
mock_embedder.create.return_value = Mock()
|
||||||
mock_vector_store.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()
|
mock_llm.create.return_value = Mock()
|
||||||
|
|
||||||
config = MemoryConfig(version="v1.1")
|
config = MemoryConfig(version="v1.1")
|
||||||
@@ -37,13 +38,14 @@ def memory_instance():
|
|||||||
def memory_custom_instance():
|
def memory_custom_instance():
|
||||||
with (
|
with (
|
||||||
patch("mem0.utils.factory.EmbedderFactory") as mock_embedder,
|
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.utils.factory.LlmFactory") as mock_llm,
|
||||||
patch("mem0.memory.telemetry.capture_event"),
|
patch("mem0.memory.telemetry.capture_event"),
|
||||||
patch("mem0.memory.graph_memory.MemoryGraph"),
|
patch("mem0.memory.graph_memory.MemoryGraph"),
|
||||||
):
|
):
|
||||||
mock_embedder.create.return_value = Mock()
|
mock_embedder.create.return_value = Mock()
|
||||||
mock_vector_store.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()
|
mock_llm.create.return_value = Mock()
|
||||||
|
|
||||||
config = MemoryConfig(
|
config = MemoryConfig(
|
||||||
@@ -250,7 +252,11 @@ def test_get_all(memory_instance, version, enable_graph, expected_result):
|
|||||||
|
|
||||||
def test_custom_prompts(memory_custom_instance):
|
def test_custom_prompts(memory_custom_instance):
|
||||||
messages = [{"role": "user", "content": "Test message"}]
|
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 = 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("mem0.memory.main.parse_messages", return_value="Test message") as mock_parse_messages:
|
||||||
with patch(
|
with patch(
|
||||||
@@ -273,7 +279,7 @@ def test_custom_prompts(memory_custom_instance):
|
|||||||
## custom update memory prompt
|
## custom update memory prompt
|
||||||
##
|
##
|
||||||
mock_get_update_memory_messages.assert_called_once_with(
|
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(
|
memory_custom_instance.llm.generate_response.assert_any_call(
|
||||||
|
|||||||
Reference in New Issue
Block a user