Repository navigation
fix(mem_reader): raise LLMResponseParseError instead of returning {} on malformed JSON (#2456) - #2487
Conversation
🤖 Open Code ReviewTarget: PR #2487 🔍 OpenCodeReview found 2 issue(s) in this PR. 1.
|
❌ Automated Test Results: FAILED
Failed tests:
Error detailsBranch: |
|
Fixed in 05981a4 — the AutoTest
The module now imports the real modules directly ( This also covers the three OpenCodeReview findings on this file: the CWD-relative path loading, the permanent Production behaviour of |
…on malformed JSON (MemTensor#2456) parse_json_result silently returned {} on every parse failure, so a malformed LLM extraction was reported as an empty (but successful) /product/add response. The HTTP layer returned {"code": 200, "data": []} for an extraction that actually failed, hiding the wasted tokens and never alerting operators that a request had silently dropped. This makes parse_json_result raise LLMResponseParseError (a new ParserError subclass in memos.exceptions) on any non-recoverable parse failure, so callers can distinguish "the model produced no memories" from "the model produced output we couldn't parse". Every in-tree caller already wraps parse_json_result in a try/except (multi_modal_struct, simple_struct, strategy_struct, file_content_parser, searcher, task_goal_parser), so the exception is caught and surfaces as the synthetic-memory-list fallback instead of the silent {} return path. The literal {} JSON value still returns {} so successful empty extractions are preserved. The fix keeps test_simple_struct_fallback.py::test_process_chat_data_fine_yields_node_when_llm_unparseable passing: _safe_parse catches the new exception and returns None, which still triggers the chat fallback that emits a salvageable UserMemory item. - New exception LLMResponseParseError(ParserError) in memos.exceptions - parse_json_result now raises on empty input, missing JSON object, every unrecoverable JSONDecodeError, while still returning the dict when one is found (including a literal {}) - New tests/mem_reader/test_parse_json_result_strict.py covers the trailing-comma reproducer from the issue, the prose-only 'I cannot output JSON' symptom, empty/None input, no JSON object, and the exception-chain contract (the chained cause preserves the underlying JSONDecodeError for debugging) - Updated test_simple_structure.py::test_parse_json_result_failure and the docstring on test_simple_struct_fallback.py to match the new contract
The test module injected stub packages for memos/memos.log/memos.exceptions/
memos.mem_reader at import time. Those stubs stayed in sys.modules for the
whole pytest session and broke every later test that imported the real
package: memos.log lost _LOGGING_CONFIGURED_PID/summarize_* helpers, the
stub root package shadowed real submodules (memos.chunkers, memos.mem_user),
and server_router tests saw 'module memos has no attribute log'. The stub
loader also resolved src/... paths against the current working directory,
so collecting the file from any other directory raised FileNotFoundError
(autotest 'collection' failures).
Replace the stub machinery with normal top-level imports — pyproject.toml
already sets pythonpath=src and sibling tests import the same modules the
same way. Also assert the exception chain directly via
exc_info.value.__cause__ being a json.JSONDecodeError, which the current
or-clause ('Failed' in message) could never fail.
05981a4 to
d476cd3
Compare
✅ Automated Test Results: PASSEDAll tests passed (19/19 executed). memos_python_core/changed-repo-python: 19/19. Duration: 11s Branch: |
Summary
parse_json_resultsilently returned{}on every parse failure, so a malformed LLM extraction was reported as an empty (but successful)/product/addresponse. The HTTP layer returned{"code": 200, "data": []}for an extraction that actually failed, hiding the wasted tokens and never alerting operators that a request had silently dropped.Root cause
src/memos/mem_reader/utils.py::parse_json_resulthad three silent return paths:Both branches are reached when the LLM emits invalid JSON or non-JSON chatter. Downstream, every
/product/addcaller (multi_modal_struct,simple_struct,strategy_struct,file_content_parser) checks the empty{}against truthiness and either short-circuits to a "no items" log line or falls into an unintended code path — never raising, never alerting.Fix
LLMResponseParseError(ParserError)tomemos.exceptionsso callers can distinguish "no memories produced" from "could not parse the response".parse_json_resultnow raises on empty input, on missing JSON object, and on every unrecoverableJSONDecodeError. The chained__cause__preserves the underlying decode error for debugging.{}) still return the parsed dict.Caller audit
Every in-tree caller of
parse_json_resultalready wraps the call intry/except Exception, so the new exception is caught and routed to the existing fallback instead of the silent-{}path:mem_reader/multi_modal_struct.py:504{}→ "no items prepared" →200 OKwith emptydata)mem_reader/multi_modal_struct.py:600Nonefrom mergemem_reader/simple_struct.py:118(and_safe_parseat:269)None, downstream fallback firesmem_reader/strategy_struct.py:55mem_reader/read_multi_modal/file_content_parser.py:94{}memories/textual/tree_text_memory/retrieve/task_goal_parser.py:117ValueErrorafter N attemptsmemories/textual/tree_text_memory/retrieve/searcher.py:1384[query]textual/general.pyandtextual/tree_text_memory/.../retrieve_utils.pydefine their ownparse_json_resultwith a different body — those are not affected by this change.Tests
tests/mem_reader/test_parse_json_result_strict.py— 10 red/green cases:{}, markdown-fence strippingNoneinput, prose-only "I cannot output JSON" symptom, exception-chain preservationtests/mem_reader/test_simple_structure.py::test_parse_json_result_failureto expect the new raisetests/mem_reader/test_simple_struct_fallback.py::test_process_chat_data_fine_yields_node_when_llm_unparseableto describe the new exception path; the test still passes because_safe_parsecatches the new exception and returnsNone, so the symptom-level chat fallback (which is the actual user-facing contract for fix: /product/add returns 200 success but stores no memory (No add/update items prepared) #1493) continues to emit a salvageableUserMemoryitem.Verification
The full existing
test_simple_structure.pysuite pulls in the heavy LLM/embedder stack and was not exercised end-to-end here, but every individual test case the PR touches was verified against the new behavior in isolation.Out of scope
memosor the OpenAPI contract.Makefile/pyproject.toml/.pre-commit-config.yaml/.github/workflows/.Fixes #2456.