diff --git a/plugins/_memory/AGENTS.md b/plugins/_memory/AGENTS.md index 1d7f19b44..dec5c1636 100644 --- a/plugins/_memory/AGENTS.md +++ b/plugins/_memory/AGENTS.md @@ -24,6 +24,7 @@ ## Work Guidance +- Keep dashboard metadata JSON-safe without changing shared API serialization. - Coordinate tool, prompt, and consolidation changes so saved memories remain useful and bounded. ## Verification diff --git a/plugins/_memory/api/memory_dashboard.py b/plugins/_memory/api/memory_dashboard.py index f6d2c35b7..af3e3498f 100644 --- a/plugins/_memory/api/memory_dashboard.py +++ b/plugins/_memory/api/memory_dashboard.py @@ -1,3 +1,5 @@ +import numpy as np + from helpers.api import ApiHandler, Request, Response from helpers import files from helpers.localization import Localization @@ -216,7 +218,10 @@ class MemoryDashboard(ApiHandler): def _format_memory_for_dashboard(self, m: Document) -> dict: """Format a memory document for the dashboard.""" - metadata = m.metadata + metadata = dict(m.metadata) + similarity = metadata.get("_consolidation_similarity") + if isinstance(similarity, np.generic): + metadata["_consolidation_similarity"] = float(similarity) timestamp = self._serialize_memory_timestamp(metadata.get("timestamp", "unknown")) return { "id": metadata.get("id", "unknown"), diff --git a/plugins/_memory/helpers/memory.py b/plugins/_memory/helpers/memory.py index d598f7ba6..3529775b9 100644 --- a/plugins/_memory/helpers/memory.py +++ b/plugins/_memory/helpers/memory.py @@ -614,7 +614,7 @@ class Memory: res = max( 0, min(1, res) ) # float precision can cause values like 1.0000000596046448 - return res + return float(res) # native float, not numpy scalar (JSON serializable) @staticmethod def format_docs_plain(docs: list[Document]) -> list[str]: diff --git a/plugins/_memory/helpers/memory_consolidation.py b/plugins/_memory/helpers/memory_consolidation.py index 227fb11e8..c2eb243b9 100644 --- a/plugins/_memory/helpers/memory_consolidation.py +++ b/plugins/_memory/helpers/memory_consolidation.py @@ -344,7 +344,7 @@ class MemoryConsolidator: filter=f"area == '{area}'" ) for doc, score in semantic_results: - doc.metadata['_consolidation_similarity'] = score + doc.metadata['_consolidation_similarity'] = float(score) if score is not None else 0.0 all_similar.append(doc) # Step 3: Keyword-based searches with real scores @@ -358,7 +358,7 @@ class MemoryConsolidator: filter=f"area == '{area}'" ) for doc, score in keyword_results: - doc.metadata['_consolidation_similarity'] = score + doc.metadata['_consolidation_similarity'] = float(score) if score is not None else 0.0 all_similar.append(doc) # Step 4: Deduplicate by document ID, keep highest score per memory ID diff --git a/tests/test_memory_json_serialization.py b/tests/test_memory_json_serialization.py new file mode 100644 index 000000000..47a1221fa --- /dev/null +++ b/tests/test_memory_json_serialization.py @@ -0,0 +1,33 @@ +import json +import threading + +import numpy as np +from langchain_core.documents import Document + +from plugins._memory.api.memory_dashboard import MemoryDashboard +from plugins._memory.helpers.memory import Memory + + +def test_cosine_normalizer_returns_native_float(): + score = Memory._cosine_normalizer(np.float32(0.8)) + + assert type(score) is float + + +def test_memory_dashboard_serializes_legacy_numpy_similarity(): + dashboard = MemoryDashboard(app=None, thread_lock=threading.RLock()) + document = Document( + page_content="legacy memory", + metadata={ + "id": "memory-1", + "area": "main", + "timestamp": "unknown", + "_consolidation_similarity": np.float32(0.75), + }, + ) + + formatted = dashboard._format_memory_for_dashboard(document) + + assert type(formatted["metadata"]["_consolidation_similarity"]) is float + assert isinstance(document.metadata["_consolidation_similarity"], np.float32) + json.dumps(formatted)