Skip to content
Open
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
29 changes: 26 additions & 3 deletions src/memos/mem_os/utils/format_utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import math
import random
import re

from typing import Any


# Strip the outer markdown fences from an LLM JSON response. The opening
# fence may include any info string (``json`` or otherwise); the closing
# fence is bare. Both patterns are anchored so interior triple-backtick
# sequences inside a JSON string value are preserved verbatim (issue
# #2420 — suggestions/responses that contained code snippets were being
# silently corrupted before ``json.loads``).
_JSON_FENCE_OPEN_RE = re.compile(r"^```[^\n`]*\n?")
_JSON_FENCE_CLOSE_RE = re.compile(r"\n?```\s*$")

from memos.log import get_logger
from memos.memories.activation.item import KVCacheItem

Expand Down Expand Up @@ -533,7 +544,7 @@ def convert_graph_to_tree_forworkmem(
node_name = extract_node_name(memory)
memory_key = node.get("metadata", {}).get("key", node_name)
usage = node.get("metadata", {}).get("usage", [])
frequency = len(usage) if len(usage) < 100 else 100
frequency = min(100, len(usage))
node_map[node["id"]] = {
"id": node["id"],
"value": memory,
Expand Down Expand Up @@ -1394,11 +1405,16 @@ def clean_json_response(response: str) -> str:
"""
Remove markdown JSON code block formatting from LLM response.

Only an opening fence (with its optional ``json`` info string) at the
very start and a closing fence at the very end are markup; interior
triple-backtick sequences are part of the JSON payload (issue #2420)
and must be preserved verbatim.

Args:
response: Raw response string that may contain ```json and ```

Returns:
str: Clean JSON string without markdown formatting
str: Clean JSON string without the outer markdown fences.

Raises:
ValueError: If ``response`` is None. This is almost always an upstream
Expand All @@ -1413,4 +1429,11 @@ def clean_json_response(response: str) -> str:
"failed silently (check timed_with_status / generate() error "
"handling)."
)
return response.replace("```json", "").replace("```", "").strip()
s = response.strip()
# Anchor at the start so interior ```json / ``` markers inside JSON
# string values are left alone. The opening-fence info string ("json" or
# any other word) and the trailing newline are stripped.
s = _JSON_FENCE_OPEN_RE.sub("", s, count=1)
# Anchor at the end so a stray closing fence inside a value is kept.
s = _JSON_FENCE_CLOSE_RE.sub("", s, count=1)
return s.strip()
34 changes: 34 additions & 0 deletions tests/mem_os/test_format_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,37 @@ def test_clean_json_response_preserves_internal_backticks(self):
result = clean_json_response(input_str)
assert "`example`" in result
assert result.count("`") == 2 # Only internal backticks remain

def test_clean_json_response_preserves_internal_fenced_code(self):
"""Issue #2420 reproducer: a JSON string value that itself contains a
fenced code block must not have its fences stripped.

Before the fix the global ``response.replace("```", "")`` call
removed *every* triple-backtick sequence, including the one inside
the JSON value, silently corrupting suggestions and chat metadata
that contained code snippets.
"""
input_str = '```json\n{"text": "```py\\ncode```"}\n```'
result = clean_json_response(input_str)
assert result == '{"text": "```py\\ncode```"}'

def test_clean_json_response_preserves_unclosed_fence_in_value(self):
"""A JSON value containing a single unclosed fence (e.g. a partial
snippet the LLM didn't close) must also pass through unchanged.
"""
input_str = '```json\n{"text": "here is some ```partial"}\n```'
result = clean_json_response(input_str)
assert result == '{"text": "here is some ```partial"}'

def test_clean_json_response_handles_only_opening_fence(self):
"""An opening fence with no matching closing fence should still be
removed (best effort; it can never make the JSON valid)."""
input_str = '```json\n{"key": "value"}'
result = clean_json_response(input_str)
assert result == '{"key": "value"}'

def test_clean_json_response_handles_only_closing_fence(self):
"""A closing fence with no opening fence should still be removed."""
input_str = '{"key": "value"}\n```'
result = clean_json_response(input_str)
assert result == '{"key": "value"}'
Loading