From c26ed8064e872ba23d8fbf79057d4caeec064b3d Mon Sep 17 00:00:00 2001 From: Jehu Date: Fri, 21 Aug 2026 08:34:59 +0200 Subject: [PATCH 1/2] Keep numpy scalars out of memory metadata and API JSON Memory consolidation stores FAISS relevance scores (numpy scalars) in document metadata. The values get pickled into the docstore and crash json.dumps with 'Object of type float32 is not JSON serializable' in helpers/api.py for handlers that return full metadata. Coerce scores to native floats at the source and serialize responses with default=float. --- helpers/api.py | 2 +- plugins/_memory/helpers/memory.py | 2 +- plugins/_memory/helpers/memory_consolidation.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/helpers/api.py b/helpers/api.py index 6047f3eb9..653f0804e 100644 --- a/helpers/api.py +++ b/helpers/api.py @@ -83,7 +83,7 @@ class ApiHandler: if isinstance(output, Response): return output else: - response_json = json.dumps(output) + response_json = json.dumps(output, default=float) return Response( response=response_json, status=200, mimetype="application/json" ) diff --git a/plugins/_memory/helpers/memory.py b/plugins/_memory/helpers/memory.py index 364bfe76c..e1d8339cc 100644 --- a/plugins/_memory/helpers/memory.py +++ b/plugins/_memory/helpers/memory.py @@ -594,7 +594,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 From 454d45c434e6f02f786c0054e907a015ea476f0a Mon Sep 17 00:00:00 2001 From: Alessandro <155005371+3clyp50@users.noreply.github.com> Date: Sun, 23 Aug 2026 16:47:41 +0200 Subject: [PATCH 2/2] Scope memory JSON compatibility to the dashboard Keep shared API serialization strict while converting legacy NumPy consolidation scores in the memory dashboard response. Add regressions for native relevance scores and legacy dashboard metadata without mutating stored documents. --- helpers/api.py | 2 +- plugins/_memory/AGENTS.md | 1 + plugins/_memory/api/memory_dashboard.py | 7 +++++- tests/test_memory_json_serialization.py | 33 +++++++++++++++++++++++++ 4 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 tests/test_memory_json_serialization.py diff --git a/helpers/api.py b/helpers/api.py index 653f0804e..6047f3eb9 100644 --- a/helpers/api.py +++ b/helpers/api.py @@ -83,7 +83,7 @@ class ApiHandler: if isinstance(output, Response): return output else: - response_json = json.dumps(output, default=float) + response_json = json.dumps(output) return Response( response=response_json, status=200, mimetype="application/json" ) diff --git a/plugins/_memory/AGENTS.md b/plugins/_memory/AGENTS.md index ed8b56d11..51969949b 100644 --- a/plugins/_memory/AGENTS.md +++ b/plugins/_memory/AGENTS.md @@ -21,6 +21,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/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)