fix: prevent memory drift for referential queries in fast mode#2154
fix: prevent memory drift for referential queries in fast mode#2154Zhe-SH-CN wants to merge 1 commit into
Conversation
🤖 Open Code ReviewTarget: PR #2154 🔍 OpenCodeReview found 16 issue(s) in this PR. 1.
|
| Concept | GetMemoryData |
GetKnowledgebaseFileData |
|---|---|---|
| page size | size |
page_size |
| current page | current |
page |
| total items/pages | total (items) / pages (pages) |
total (items only) |
Consider aligning on a single convention (e.g. page / page_size / total) across both models to reduce integration confusion for API consumers.
11. src/memos/memories/textual/tree_text_memory/retrieve/task_goal_parser.py (L20-L34)
The hint "it" in _MULTI_CHAR_REFERENTIAL_HINTS is matched with a plain substring check (any(h in task_description for h in _MULTI_CHAR_REFERENTIAL_HINTS)), which will produce false positives for any English query containing "it" as a substring — e.g., "submit a report", "capital city", "unit test plan", "commit changes". These queries will all be misclassified as referential and have their effective search query silently altered.
The single-char hints correctly guard against this with startswith, but "it" needs word-boundary matching. Consider splitting the English multi-char hints into a separate set and using re.search(r'\bit\b', task_description), or move "it" to a list that is matched as a whole word:
import re
_ENGLISH_WORD_REFERENTIAL_HINTS = re.compile(
r'\b(?:it|this|that|more|other|another)\b', re.IGNORECASE
)
# then in is_referential:
or bool(_ENGLISH_WORD_REFERENTIAL_HINTS.search(task_description))This avoids the false-positive augmentation that could silently degrade retrieval quality for self-contained English queries.
12. tests/api/test_product_models.py (L25-L27)
Direct key access schema["example"] will raise an unguarded KeyError if the "example" key is absent, rather than producing a descriptive assertion failure. This test runs independently of test_add_request_exposes_model_level_example, so a failing schema will surface as KeyError with no helpful message instead of the intended assertion message.
Use .get() with an explicit fallback and assert on it, or use pytest.fail:
example = APIADDRequest.model_json_schema().get("example")
assert example is not None, "APIADDRequest schema is missing the 'example' key"💡 Suggested Change
Before:
example = APIADDRequest.model_json_schema()["example"]
messages = example.get("messages")
After:
schema = APIADDRequest.model_json_schema()
example = schema.get("example")
assert example is not None, "APIADDRequest schema is missing the 'example' key"
messages = example.get("messages")
13. tests/api/test_product_models.py (L38-L40)
Same unguarded schema["example"] key access as in test_add_request_example_messages_is_structured_list. If the model-level example is absent, this test will raise a KeyError with no descriptive message rather than an assertion failure.
Apply the same guard:
schema = APIADDRequest.model_json_schema()
example = schema.get("example")
assert example is not None, "APIADDRequest schema is missing the 'example' key"💡 Suggested Change
Before:
example = APIADDRequest.model_json_schema()["example"]
assert "user_id" in example
After:
schema = APIADDRequest.model_json_schema()
example = schema.get("example")
assert example is not None, "APIADDRequest schema is missing the 'example' key"
assert "user_id" in example
14. tests/api/test_client.py (L14-L24)
The _install_memos_package_stub guard silently becomes a no-op in the standard test environment. pyproject.toml sets pythonpath = "src", so pytest puts src/ on sys.path and the real memos package is importable (and typically already imported by the time any test file runs). Because of the if "memos" not in sys.modules check, the stub is skipped and _load_client_module just imports the real module — meaning the stub infrastructure provides no value. Either remove the stub entirely (since pythonpath = "src" already makes the real package available), or replace the guard with an unconditional forced-reload using importlib.reload if isolation is truly needed.
💡 Suggested Change
Before:
def _install_memos_package_stub() -> None:
if "memos" not in sys.modules:
memos_pkg = types.ModuleType("memos")
memos_pkg.__path__ = [str(SRC_DIR)]
sys.modules["memos"] = memos_pkg
if "memos.api" not in sys.modules:
api_pkg = types.ModuleType("memos.api")
api_pkg.__path__ = [str(SRC_DIR / "api")]
sys.modules["memos.api"] = api_pkg
sys.modules["memos"].api = api_pkg
After:
# The stub is unnecessary because pyproject.toml already sets pythonpath = "src".
# Simply importing the module directly works without any sys.modules manipulation.
def _install_memos_package_stub() -> None:
pass # no-op: 'src/' is on sys.path via pytest's pythonpath config
15. tests/api/test_client.py (L55-L57)
Setting self.json_called = True before raising AssertionError makes the flag unreliable as a guard. If json() is ever called, the flag is set to True and then the AssertionError propagates — the test will abort at that point and never reach the assertion assert stream_response.json_called is False (line 685). Conversely, if json() is never called, json_called stays False and line 685 trivially passes. The flag can never be True at the assertion site, making it a dead check. Move the flag assignment after the raise, or redesign: don't raise inside json(), instead just set the flag and return a sentinel, then assert on the flag afterwards.
💡 Suggested Change
Before:
def json(self) -> dict:
self.json_called = True
raise AssertionError("streaming responses must not be parsed as JSON")
After:
def json(self) -> dict:
self.json_called = True
# Returning a sentinel allows the flag to be inspected after the call
# without aborting the test via AssertionError.
return {} # or raise RuntimeError(...) if you want a hard stop
16. tests/memories/textual/test_task_goal_parser_context.py (L84-L85)
The second assertion assert "A" * 121 not in result.rephrased_query is logically vacuous and never validates truncation.
result.rephrased_query is constructed as f"{last_user_msg} {task_description}" where last_user_msg is at most 120 As. A 120-character string of As cannot contain a 121-character substring of As, so "A" * 121 not in result.rephrased_query is always True — even if the truncation logic is removed entirely.
To actually verify that truncation occurred (i.e., the full 300-char message was NOT used), assert that the augmented query does not contain the un-truncated portion:
assert "A" * 121 not in result.rephrased_query # vacuous — always passes
# Replace with:
assert "A" * 300 not in result.rephrased_query # confirms the full message was truncated
# Or more directly:
assert "A" * 121 not in result.rephrased_query # keep as-is only for < 121 truncation limitSuggested fix:
💡 Suggested Change
Before:
assert "A" * 120 in result.rephrased_query
assert "A" * 121 not in result.rephrased_query # Verify truncation actually occurred
After:
assert "A" * 120 in result.rephrased_query
assert "A" * 300 not in result.rephrased_query # Verify full message was not used (real truncation check)
Generated by cloud-assistant via Open Code Review.
❌ Automated Test Results: FAILED
Failed tests:
Error detailsBranch: |
0bc447e to
b572073
Compare
b572073 to
a5b1378
Compare
|
Fixes MemTensor#1365 In fast search mode, TaskGoalParser._parse_fast ignored conversation history entirely. Referential queries like "再找找其他价格优惠的" were embedded as-is, causing them to match unrelated older memories instead of the current conversation topic. Fix: when conversation history is available and the query looks referential (short or contains referential hints like 其他/这个/再/more), prepend the most recent user message to the query before embedding. This is a zero-LLM-call heuristic that disambiguates context without adding latency to fast mode. Addresses OCR findings: - Move _REFERENTIAL_HINTS to module level constants - Split single-char hints (match at query start only) from multi-char - Add isinstance(content, str) guard for multi-modal content blocks - Use head truncation to preserve topic anchor - Strengthen test assertions
a5b1378 to
ceebe82
Compare
✅ Automated Test Results: PASSEDAll tests passed (8/8 executed). memos_python_core/changed-repo-python: 8/8. Duration: 5s [advisory, non-gating] AI-generated tests on branch test/auto-gen-152e9c3f92a867df-20260724093617: 101/101 passed — these do NOT affect the PR verdict; review the branch manually. Branch: |
Description
In fast search mode,
TaskGoalParser._parse_fastignored conversation history entirely. Referential queries like "再找找其他价格优惠的" were embedded as-is, causing them to match unrelated older memories instead of the current conversation topic (issue #1365).This PR adds lightweight context-aware query expansion in fast mode:
Related Issue: Fixes #1365
Type of change
How Has This Been Tested
parse()method integrationpython3 -c "import ast; ast.parse(...)"syntax verificationChecklist