From 0c855693c4c5b7540f340c72b2950db9a41c3976 Mon Sep 17 00:00:00 2001 From: damaozi <1811866786@qq.com> Date: Sun, 1 Mar 2026 04:34:40 +0800 Subject: [PATCH 1/3] fix(graph_dbs): add missing _flatten_info_fields() in Neo4jCommunityGraphDB.add_node (#1122) Neo4jCommunityGraphDB.add_node() was missing the _flatten_info_fields() call that exists in the parent Neo4jGraphDB.add_node(). This caused nested 'info' dict in metadata to be passed directly to Neo4j, which rejects Map-type property values with CypherTypeError. --- src/memos/graph_dbs/neo4j_community.py | 3 + .../test_neo4j_community_flatten_info.py | 96 +++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 tests/graph_dbs/test_neo4j_community_flatten_info.py diff --git a/src/memos/graph_dbs/neo4j_community.py b/src/memos/graph_dbs/neo4j_community.py index 09ad46c42..d2228e281 100644 --- a/src/memos/graph_dbs/neo4j_community.py +++ b/src/memos/graph_dbs/neo4j_community.py @@ -56,6 +56,9 @@ def add_node( # Safely process metadata metadata = _prepare_node_metadata(metadata) + # Flatten info fields to top level (for Neo4j flat structure) + metadata = _flatten_info_fields(metadata) + # Initialize delete_time and delete_record_id fields metadata.setdefault("delete_time", "") metadata.setdefault("delete_record_id", "") diff --git a/tests/graph_dbs/test_neo4j_community_flatten_info.py b/tests/graph_dbs/test_neo4j_community_flatten_info.py new file mode 100644 index 000000000..943db3fdd --- /dev/null +++ b/tests/graph_dbs/test_neo4j_community_flatten_info.py @@ -0,0 +1,96 @@ +""" +Regression tests for issue #1122: +Neo4jCommunityGraphDB.add_node() must flatten nested 'info' dict in metadata +to avoid Neo4j CypherTypeError on Map-type property values. +""" + +import uuid +from datetime import datetime +from unittest.mock import MagicMock, patch + +import pytest + +from memos.configs.graph_db import Neo4jGraphDBConfig + + +@pytest.fixture +def neo4j_community_config(): + return Neo4jGraphDBConfig( + uri="bolt://localhost:7687", + user="neo4j", + password="test", + db_name="neo4j", + auto_create=False, + use_multi_db=False, + user_name="test-user", + embedding_dimension=3, + ) + + +@pytest.fixture +def neo4j_community_db(neo4j_community_config): + with patch("neo4j.GraphDatabase") as mock_gd: + mock_driver = MagicMock() + mock_gd.driver.return_value = mock_driver + + from memos.graph_dbs.neo4j_community import Neo4jCommunityGraphDB + + db = object.__new__(Neo4jCommunityGraphDB) + db.config = neo4j_community_config + db.driver = mock_driver + db.db_name = neo4j_community_config.db_name + db.vec_db = MagicMock() + yield db + + +class TestNeo4jCommunityFlattenInfo: + """Regression: add_node must flatten nested info dict before passing to Neo4j.""" + + def test_add_node_flattens_info_field(self, neo4j_community_db): + """Nested 'info' dict should be flattened to top-level keys in metadata.""" + node_id = str(uuid.uuid4()) + memory = "User prefers Python for AI development" + now = datetime.utcnow().isoformat() + metadata = { + "embedding": [0.1, 0.2, 0.3], + "created_at": now, + "updated_at": now, + "sources": [], + "info": { + "preference": "python", + }, + } + + session_mock = neo4j_community_db.driver.session.return_value.__enter__.return_value + + neo4j_community_db.add_node( + id=node_id, memory=memory, metadata=metadata, user_name="test-user" + ) + + call_args = session_mock.run.call_args + passed_metadata = call_args.kwargs.get("metadata", call_args[1].get("metadata", {})) + + assert "info" not in passed_metadata, ( + f"'info' field was not flattened: metadata={passed_metadata}" + ) + assert passed_metadata.get("preference") == "python" + + def test_add_node_without_info_field_still_works(self, neo4j_community_db): + """add_node should work normally when metadata has no 'info' field.""" + node_id = str(uuid.uuid4()) + memory = "Simple memory" + now = datetime.utcnow().isoformat() + metadata = { + "embedding": [0.1, 0.2, 0.3], + "created_at": now, + "updated_at": now, + "sources": [], + } + + session_mock = neo4j_community_db.driver.session.return_value.__enter__.return_value + + neo4j_community_db.add_node( + id=node_id, memory=memory, metadata=metadata, user_name="test-user" + ) + + assert session_mock.run.called From 7012edc4f8e4441b95a7c6974b0d40b76e554fec Mon Sep 17 00:00:00 2001 From: damaozi <1811866786@qq.com> Date: Sun, 1 Mar 2026 05:30:32 +0800 Subject: [PATCH 2/3] fix(feedback): guard against empty added_ids in _single_add_operation (#1122) When memory_manager.add() returns an empty list (e.g. due to upstream Neo4j CypherTypeError), added_ids[0] raises IndexError. Add empty list guard to return {"id": None} gracefully instead of crashing. --- src/memos/mem_feedback/feedback.py | 7 ++ tests/mem_feedback/__init__.py | 0 .../test_feedback_empty_added_ids.py | 77 +++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 tests/mem_feedback/__init__.py create mode 100644 tests/mem_feedback/test_feedback_empty_added_ids.py diff --git a/src/memos/mem_feedback/feedback.py b/src/memos/mem_feedback/feedback.py index 6c6d1821f..b285cd185 100644 --- a/src/memos/mem_feedback/feedback.py +++ b/src/memos/mem_feedback/feedback.py @@ -250,6 +250,13 @@ def _single_add_operation( ) logger.info(f"[Memory Feedback ADD] memory id: {added_ids!s}") + if not added_ids: + logger.warning("[Memory Feedback ADD] add returned empty list, memory may not have been persisted") + return { + "id": None, + "text": to_add_memory.memory, + "source_doc_id": None, + } return { "id": added_ids[0], "text": to_add_memory.memory, diff --git a/tests/mem_feedback/__init__.py b/tests/mem_feedback/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/mem_feedback/test_feedback_empty_added_ids.py b/tests/mem_feedback/test_feedback_empty_added_ids.py new file mode 100644 index 000000000..4e462a694 --- /dev/null +++ b/tests/mem_feedback/test_feedback_empty_added_ids.py @@ -0,0 +1,77 @@ +""" +Regression tests for issue #1122 (Bug #2): +MemFeedback._single_add_operation() must handle empty added_ids +without raising IndexError. +""" + +import uuid +from datetime import datetime +from unittest.mock import MagicMock, patch + +import pytest + +from memos.mem_feedback.feedback import MemFeedback +from memos.memories.textual.item import TextualMemoryItem, TreeNodeTextualMemoryMetadata + + +def _make_memory_item(memory_text="I prefer Python for AI development"): + """Create a minimal TextualMemoryItem for testing.""" + return TextualMemoryItem( + id=str(uuid.uuid4()), + memory=memory_text, + metadata=TreeNodeTextualMemoryMetadata( + user_id="user1", + memory_type="WorkingMemory", + sources=[], + embedding=[0.1, 0.2, 0.3], + created_at=datetime.now().isoformat(), + background="", + key="test-key", + tags=[], + ), + ) + + +@pytest.fixture +def feedback_instance(): + """Create a MemFeedback instance bypassing __init__.""" + fb = object.__new__(MemFeedback) + fb.memory_manager = MagicMock() + fb.embedder = MagicMock() + return fb + + +class TestSingleAddOperationEmptyIds: + """Test _single_add_operation when memory_manager.add() returns empty list.""" + + def test_empty_added_ids_returns_none_id(self, feedback_instance): + """When added_ids is empty, should return {"id": None} instead of IndexError.""" + feedback_instance.memory_manager.add.return_value = [] + + new_item = _make_memory_item() + result = feedback_instance._single_add_operation( + old_memory_item=None, + new_memory_item=new_item, + user_id="user1", + user_name="test-user", + ) + + assert result["id"] is None + assert result["text"] == new_item.memory + assert result["source_doc_id"] is None + + def test_normal_added_ids_returns_first_id(self, feedback_instance): + """When added_ids has values, should return the first id normally.""" + expected_id = str(uuid.uuid4()) + feedback_instance.memory_manager.add.return_value = [expected_id] + + new_item = _make_memory_item() + result = feedback_instance._single_add_operation( + old_memory_item=None, + new_memory_item=new_item, + user_id="user1", + user_name="test-user", + ) + + assert result["id"] == expected_id + assert result["text"] == new_item.memory From 0a431955ec71bae3d06d3917530832ffcd8e1db2 Mon Sep 17 00:00:00 2001 From: damaozi <1811866786@qq.com> Date: Sun, 1 Mar 2026 05:34:55 +0800 Subject: [PATCH 3/3] style: fix ruff lint and format issues --- src/memos/mem_feedback/feedback.py | 4 +++- tests/graph_dbs/test_neo4j_community_flatten_info.py | 1 + tests/mem_feedback/test_feedback_empty_added_ids.py | 3 ++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/memos/mem_feedback/feedback.py b/src/memos/mem_feedback/feedback.py index b285cd185..cc4fc035a 100644 --- a/src/memos/mem_feedback/feedback.py +++ b/src/memos/mem_feedback/feedback.py @@ -251,7 +251,9 @@ def _single_add_operation( logger.info(f"[Memory Feedback ADD] memory id: {added_ids!s}") if not added_ids: - logger.warning("[Memory Feedback ADD] add returned empty list, memory may not have been persisted") + logger.warning( + "[Memory Feedback ADD] add returned empty list, memory may not have been persisted" + ) return { "id": None, "text": to_add_memory.memory, diff --git a/tests/graph_dbs/test_neo4j_community_flatten_info.py b/tests/graph_dbs/test_neo4j_community_flatten_info.py index 943db3fdd..e0cd02812 100644 --- a/tests/graph_dbs/test_neo4j_community_flatten_info.py +++ b/tests/graph_dbs/test_neo4j_community_flatten_info.py @@ -5,6 +5,7 @@ """ import uuid + from datetime import datetime from unittest.mock import MagicMock, patch diff --git a/tests/mem_feedback/test_feedback_empty_added_ids.py b/tests/mem_feedback/test_feedback_empty_added_ids.py index 4e462a694..173524926 100644 --- a/tests/mem_feedback/test_feedback_empty_added_ids.py +++ b/tests/mem_feedback/test_feedback_empty_added_ids.py @@ -5,8 +5,9 @@ """ import uuid + from datetime import datetime -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock import pytest