mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-25 14:06:18 +00:00
* feat(memory): add cognitiveStyle for collaboration preferences Extend cross-session memory with user.cognitiveStyle and cognitive facts, backward-compatible normalization, prompt injection as Thinking Style, and settings UI. Add design notes and unit tests. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(memory): normalize legacy memory imports on frontend Add normalizeMemoryPayload aligned with backend normalize_memory_data, use it for Settings import and API reads, and add unit/regression tests. Document schema-evolution steps and sync FACT_EXTRACTION_PROMPT cognitive category. * fix(memory): harden legacy memory normalization * fix(memory): keep import envelope validation strict * fix(memory): keep unknown fields through frontend import normalization normalizeMemoryPayload rebuilt the memory document from a section whitelist, so every key it did not name was dropped. Because readMemoryResponse routes all memory API responses through it, the loss showed up on read, export, and import alike: revision vanished, and any future backend field would silently break lossless export/import. The Gateway models are extra="allow" and deermem normalizes additively, which left the frontend as the only lossy hop in the chain. Spread before overriding at each nesting level so normalization only validates and defaults contract-owned fields, leaving the envelope check as strict as before. Covered by a unit test on the strict import path, two backend tests for extension preservation and caller immutability, and an E2E round-trip that asserts on the captured import request body. * docs(memory): state the normalization invariant that actually holds The docstring cited `display` as a backend-driven field alongside `revision`, but #4546 was closed unmerged and no such field exists in the contract. It also promised lossless export/import, which the Gateway's `extra="ignore"` models preclude for any undeclared field. Replace both with the invariant the code is really defending: the frontend must not be narrower than the Gateway contract, using the declared top-level `revision` as the concrete example. * fix(memory): preserve legacy migration and reject lossy imports * fix(memory): align legacy fact normalization defaults * test(memory): use authenticated app for import regression --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
107 lines
3.7 KiB
Python
107 lines
3.7 KiB
Python
"""Tests for memory schema normalization."""
|
|
|
|
import copy
|
|
|
|
import pytest
|
|
|
|
from deerflow.agents.memory.backends.deermem.deermem.core.storage import create_empty_memory, normalize_memory_data
|
|
|
|
|
|
def test_normalize_memory_data_adds_cognitive_style() -> None:
|
|
legacy = {
|
|
"version": "1.0",
|
|
"lastUpdated": "",
|
|
"user": {
|
|
"workContext": {"summary": "work", "updatedAt": "2026-01-01T00:00:00Z"},
|
|
"personalContext": {"summary": "", "updatedAt": ""},
|
|
"topOfMind": {"summary": "", "updatedAt": ""},
|
|
},
|
|
"history": {
|
|
"recentMonths": {"summary": "", "updatedAt": ""},
|
|
"earlierContext": {"summary": "", "updatedAt": ""},
|
|
"longTermBackground": {"summary": "", "updatedAt": ""},
|
|
},
|
|
"facts": [],
|
|
}
|
|
|
|
result = normalize_memory_data(legacy)
|
|
|
|
assert "cognitiveStyle" in result["user"]
|
|
assert result["user"]["cognitiveStyle"]["summary"] == ""
|
|
assert result["user"]["cognitiveStyle"]["updatedAt"] == ""
|
|
|
|
|
|
def test_create_empty_memory_includes_cognitive_style() -> None:
|
|
empty = create_empty_memory()
|
|
assert empty["user"]["cognitiveStyle"] == {"summary": "", "updatedAt": ""}
|
|
|
|
|
|
def test_normalize_memory_data_preserves_unknown_fields() -> None:
|
|
payload = {
|
|
"version": "1.0",
|
|
"revision": 7,
|
|
"lastUpdated": "2026-01-01T00:00:00Z",
|
|
"display": {"title": "Memory export"},
|
|
"data": {"future": True},
|
|
"user": {
|
|
"workContext": {
|
|
"summary": "work",
|
|
"updatedAt": "2026-01-01T00:00:00Z",
|
|
"confidence": 0.8,
|
|
},
|
|
"providerState": {"loaded": True},
|
|
},
|
|
"history": {
|
|
"timeline": {"entries": ["2026-01"]},
|
|
},
|
|
"facts": [
|
|
{
|
|
"content": "User prefers conclusions first.",
|
|
"category": "cognitive",
|
|
"topics": ["communication"],
|
|
}
|
|
],
|
|
}
|
|
|
|
result = normalize_memory_data(payload)
|
|
|
|
assert result["revision"] == 7
|
|
assert result["display"] == {"title": "Memory export"}
|
|
assert result["data"] == {"future": True}
|
|
assert result["user"]["providerState"] == {"loaded": True}
|
|
assert result["user"]["workContext"]["confidence"] == 0.8
|
|
assert result["user"]["workContext"]["summary"] == "work"
|
|
assert result["history"]["timeline"] == {"entries": ["2026-01"]}
|
|
assert result["facts"][0]["topics"] == ["communication"]
|
|
assert result["user"]["cognitiveStyle"] == {"summary": "", "updatedAt": ""}
|
|
|
|
|
|
def test_normalize_memory_data_does_not_mutate_caller() -> None:
|
|
payload = {
|
|
"version": "1.0",
|
|
"lastUpdated": "",
|
|
"user": {"workContext": {"summary": "work"}},
|
|
"history": {},
|
|
"facts": [{"content": "kept"}],
|
|
}
|
|
snapshot = copy.deepcopy(payload)
|
|
|
|
result = normalize_memory_data(payload)
|
|
|
|
assert result is not payload
|
|
assert payload == snapshot
|
|
|
|
|
|
@pytest.mark.parametrize("confidence,expected", [(None, 0.5), (True, 0.5), ("invalid", 0.5), (float("nan"), 0.5), (float("inf"), 0.5), (0, 0), (-1, 0), (2, 1), ("0.8", 0.8)])
|
|
def test_normalize_legacy_fact_metadata(confidence, expected):
|
|
fact = {"id": "legacy", "content": " Keep conclusions first. ", "confidence": confidence, "source": " "}
|
|
result = normalize_memory_data({"facts": [fact]})["facts"][0]
|
|
assert result["confidence"] == expected
|
|
assert result["content"] == "Keep conclusions first."
|
|
assert result["source"] == "unknown"
|
|
|
|
|
|
def test_normalize_missing_fact_confidence_uses_neutral_default():
|
|
result = normalize_memory_data({"facts": [{"content": "Legacy preference"}]})
|
|
assert result["facts"][0]["confidence"] == 0.5
|