Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions plugins/_memory/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion plugins/_memory/api/memory_dashboard.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import numpy as np

from helpers.api import ApiHandler, Request, Response
from helpers import files
from helpers.localization import Localization
Expand Down Expand Up @@ -216,7 +218,10 @@ def get_sort_key(m):

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"),
Expand Down
2 changes: 1 addition & 1 deletion plugins/_memory/helpers/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -594,7 +594,7 @@ def _cosine_normalizer(val: float) -> float:
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]:
Expand Down
4 changes: 2 additions & 2 deletions plugins/_memory/helpers/memory_consolidation.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ async def _find_similar_memories(
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
Expand All @@ -358,7 +358,7 @@ async def _find_similar_memories(
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
Expand Down
33 changes: 33 additions & 0 deletions tests/test_memory_json_serialization.py
Original file line number Diff line number Diff line change
@@ -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)