mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-05-21 15:36:48 +00:00
feat: support manual add and edit for memory facts (#1538)
* feat: support manual add and edit for memory facts * fix: restore memory updater save helper * fix: address memory fact review feedback * fix: remove duplicate memory fact edit action * docs: simplify memory fact review setup * docs: relax memory review startup instructions * fix: clear rebase marker in memory settings page * fix: address memory fact review and format issues * fix: address memory fact review feedback * refactor: make memory fact updates explicit patch semantics --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
@@ -673,6 +673,21 @@ class TestMemoryManagement:
|
||||
result = client.clear_memory()
|
||||
assert result == data
|
||||
|
||||
def test_create_memory_fact(self, client):
|
||||
data = {"version": "1.0", "facts": []}
|
||||
with patch("deerflow.agents.memory.updater.create_memory_fact", return_value=data) as create_fact:
|
||||
result = client.create_memory_fact(
|
||||
"User prefers concise code reviews.",
|
||||
category="preference",
|
||||
confidence=0.88,
|
||||
)
|
||||
create_fact.assert_called_once_with(
|
||||
content="User prefers concise code reviews.",
|
||||
category="preference",
|
||||
confidence=0.88,
|
||||
)
|
||||
assert result == data
|
||||
|
||||
def test_delete_memory_fact(self, client):
|
||||
data = {"version": "1.0", "facts": []}
|
||||
with patch("deerflow.agents.memory.updater.delete_memory_fact", return_value=data) as delete_fact:
|
||||
@@ -680,6 +695,38 @@ class TestMemoryManagement:
|
||||
delete_fact.assert_called_once_with("fact_123")
|
||||
assert result == data
|
||||
|
||||
def test_update_memory_fact(self, client):
|
||||
data = {"version": "1.0", "facts": []}
|
||||
with patch("deerflow.agents.memory.updater.update_memory_fact", return_value=data) as update_fact:
|
||||
result = client.update_memory_fact(
|
||||
"fact_123",
|
||||
"User prefers spaces",
|
||||
category="workflow",
|
||||
confidence=0.91,
|
||||
)
|
||||
update_fact.assert_called_once_with(
|
||||
fact_id="fact_123",
|
||||
content="User prefers spaces",
|
||||
category="workflow",
|
||||
confidence=0.91,
|
||||
)
|
||||
assert result == data
|
||||
|
||||
def test_update_memory_fact_preserves_omitted_fields(self, client):
|
||||
data = {"version": "1.0", "facts": []}
|
||||
with patch("deerflow.agents.memory.updater.update_memory_fact", return_value=data) as update_fact:
|
||||
result = client.update_memory_fact(
|
||||
"fact_123",
|
||||
"User prefers spaces",
|
||||
)
|
||||
update_fact.assert_called_once_with(
|
||||
fact_id="fact_123",
|
||||
content="User prefers spaces",
|
||||
category=None,
|
||||
confidence=None,
|
||||
)
|
||||
assert result == data
|
||||
|
||||
def test_get_memory_config(self, client):
|
||||
config = MagicMock()
|
||||
config.enabled = True
|
||||
|
||||
@@ -36,6 +36,37 @@ def test_clear_memory_route_returns_cleared_memory() -> None:
|
||||
assert response.json()["facts"] == []
|
||||
|
||||
|
||||
def test_create_memory_fact_route_returns_updated_memory() -> None:
|
||||
app = FastAPI()
|
||||
app.include_router(memory.router)
|
||||
updated_memory = _sample_memory(
|
||||
facts=[
|
||||
{
|
||||
"id": "fact_new",
|
||||
"content": "User prefers concise code reviews.",
|
||||
"category": "preference",
|
||||
"confidence": 0.88,
|
||||
"createdAt": "2026-03-20T00:00:00Z",
|
||||
"source": "manual",
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
with patch("app.gateway.routers.memory.create_memory_fact", return_value=updated_memory):
|
||||
with TestClient(app) as client:
|
||||
response = client.post(
|
||||
"/api/memory/facts",
|
||||
json={
|
||||
"content": "User prefers concise code reviews.",
|
||||
"category": "preference",
|
||||
"confidence": 0.88,
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["facts"] == updated_memory["facts"]
|
||||
|
||||
|
||||
def test_delete_memory_fact_route_returns_updated_memory() -> None:
|
||||
app = FastAPI()
|
||||
app.include_router(memory.router)
|
||||
@@ -70,3 +101,106 @@ def test_delete_memory_fact_route_returns_404_for_missing_fact() -> None:
|
||||
|
||||
assert response.status_code == 404
|
||||
assert response.json()["detail"] == "Memory fact 'fact_missing' not found."
|
||||
|
||||
|
||||
def test_update_memory_fact_route_returns_updated_memory() -> None:
|
||||
app = FastAPI()
|
||||
app.include_router(memory.router)
|
||||
updated_memory = _sample_memory(
|
||||
facts=[
|
||||
{
|
||||
"id": "fact_edit",
|
||||
"content": "User prefers spaces",
|
||||
"category": "workflow",
|
||||
"confidence": 0.91,
|
||||
"createdAt": "2026-03-20T00:00:00Z",
|
||||
"source": "manual",
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
with patch("app.gateway.routers.memory.update_memory_fact", return_value=updated_memory):
|
||||
with TestClient(app) as client:
|
||||
response = client.patch(
|
||||
"/api/memory/facts/fact_edit",
|
||||
json={
|
||||
"content": "User prefers spaces",
|
||||
"category": "workflow",
|
||||
"confidence": 0.91,
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["facts"] == updated_memory["facts"]
|
||||
|
||||
|
||||
def test_update_memory_fact_route_preserves_omitted_fields() -> None:
|
||||
app = FastAPI()
|
||||
app.include_router(memory.router)
|
||||
updated_memory = _sample_memory(
|
||||
facts=[
|
||||
{
|
||||
"id": "fact_edit",
|
||||
"content": "User prefers spaces",
|
||||
"category": "preference",
|
||||
"confidence": 0.8,
|
||||
"createdAt": "2026-03-20T00:00:00Z",
|
||||
"source": "manual",
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
with patch("app.gateway.routers.memory.update_memory_fact", return_value=updated_memory) as update_fact:
|
||||
with TestClient(app) as client:
|
||||
response = client.patch(
|
||||
"/api/memory/facts/fact_edit",
|
||||
json={
|
||||
"content": "User prefers spaces",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
update_fact.assert_called_once_with(
|
||||
fact_id="fact_edit",
|
||||
content="User prefers spaces",
|
||||
category=None,
|
||||
confidence=None,
|
||||
)
|
||||
assert response.json()["facts"] == updated_memory["facts"]
|
||||
|
||||
|
||||
def test_update_memory_fact_route_returns_404_for_missing_fact() -> None:
|
||||
app = FastAPI()
|
||||
app.include_router(memory.router)
|
||||
|
||||
with patch("app.gateway.routers.memory.update_memory_fact", side_effect=KeyError("fact_missing")):
|
||||
with TestClient(app) as client:
|
||||
response = client.patch(
|
||||
"/api/memory/facts/fact_missing",
|
||||
json={
|
||||
"content": "User prefers spaces",
|
||||
"category": "workflow",
|
||||
"confidence": 0.91,
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
assert response.json()["detail"] == "Memory fact 'fact_missing' not found."
|
||||
|
||||
|
||||
def test_update_memory_fact_route_returns_specific_error_for_invalid_confidence() -> None:
|
||||
app = FastAPI()
|
||||
app.include_router(memory.router)
|
||||
|
||||
with patch("app.gateway.routers.memory.update_memory_fact", side_effect=ValueError("confidence")):
|
||||
with TestClient(app) as client:
|
||||
response = client.patch(
|
||||
"/api/memory/facts/fact_edit",
|
||||
json={
|
||||
"content": "User prefers spaces",
|
||||
"confidence": 0.91,
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json()["detail"] == "Invalid confidence value; must be between 0 and 1."
|
||||
|
||||
@@ -5,7 +5,9 @@ from deerflow.agents.memory.updater import (
|
||||
MemoryUpdater,
|
||||
_extract_text,
|
||||
clear_memory_data,
|
||||
create_memory_fact,
|
||||
delete_memory_fact,
|
||||
update_memory_fact,
|
||||
)
|
||||
from deerflow.config.memory_config import MemoryConfig
|
||||
|
||||
@@ -184,6 +186,43 @@ def test_delete_memory_fact_removes_only_matching_fact() -> None:
|
||||
assert [fact["id"] for fact in result["facts"]] == ["fact_keep"]
|
||||
|
||||
|
||||
def test_create_memory_fact_appends_manual_fact() -> None:
|
||||
with (
|
||||
patch("deerflow.agents.memory.updater.get_memory_data", return_value=_make_memory()),
|
||||
patch("deerflow.agents.memory.updater._save_memory_to_file", return_value=True),
|
||||
):
|
||||
result = create_memory_fact(
|
||||
content=" User prefers concise code reviews. ",
|
||||
category="preference",
|
||||
confidence=0.88,
|
||||
)
|
||||
|
||||
assert len(result["facts"]) == 1
|
||||
assert result["facts"][0]["content"] == "User prefers concise code reviews."
|
||||
assert result["facts"][0]["category"] == "preference"
|
||||
assert result["facts"][0]["confidence"] == 0.88
|
||||
assert result["facts"][0]["source"] == "manual"
|
||||
|
||||
|
||||
def test_create_memory_fact_rejects_empty_content() -> None:
|
||||
try:
|
||||
create_memory_fact(content=" ")
|
||||
except ValueError as exc:
|
||||
assert exc.args == ("content",)
|
||||
else:
|
||||
raise AssertionError("Expected ValueError for empty fact content")
|
||||
|
||||
|
||||
def test_create_memory_fact_rejects_invalid_confidence() -> None:
|
||||
for confidence in (-0.1, 1.1, float("nan"), float("inf"), float("-inf")):
|
||||
try:
|
||||
create_memory_fact(content="User likes tests", confidence=confidence)
|
||||
except ValueError as exc:
|
||||
assert exc.args == ("confidence",)
|
||||
else:
|
||||
raise AssertionError("Expected ValueError for invalid fact confidence")
|
||||
|
||||
|
||||
def test_delete_memory_fact_raises_for_unknown_id() -> None:
|
||||
with patch("deerflow.agents.memory.updater.get_memory_data", return_value=_make_memory()):
|
||||
try:
|
||||
@@ -194,6 +233,121 @@ def test_delete_memory_fact_raises_for_unknown_id() -> None:
|
||||
raise AssertionError("Expected KeyError for missing fact id")
|
||||
|
||||
|
||||
def test_update_memory_fact_updates_only_matching_fact() -> None:
|
||||
current_memory = _make_memory(
|
||||
facts=[
|
||||
{
|
||||
"id": "fact_keep",
|
||||
"content": "User likes Python",
|
||||
"category": "preference",
|
||||
"confidence": 0.9,
|
||||
"createdAt": "2026-03-18T00:00:00Z",
|
||||
"source": "thread-a",
|
||||
},
|
||||
{
|
||||
"id": "fact_edit",
|
||||
"content": "User prefers tabs",
|
||||
"category": "preference",
|
||||
"confidence": 0.8,
|
||||
"createdAt": "2026-03-18T00:00:00Z",
|
||||
"source": "manual",
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
with (
|
||||
patch("deerflow.agents.memory.updater.get_memory_data", return_value=current_memory),
|
||||
patch("deerflow.agents.memory.updater._save_memory_to_file", return_value=True),
|
||||
):
|
||||
result = update_memory_fact(
|
||||
fact_id="fact_edit",
|
||||
content="User prefers spaces",
|
||||
category="workflow",
|
||||
confidence=0.91,
|
||||
)
|
||||
|
||||
assert result["facts"][0]["content"] == "User likes Python"
|
||||
assert result["facts"][1]["content"] == "User prefers spaces"
|
||||
assert result["facts"][1]["category"] == "workflow"
|
||||
assert result["facts"][1]["confidence"] == 0.91
|
||||
assert result["facts"][1]["createdAt"] == "2026-03-18T00:00:00Z"
|
||||
assert result["facts"][1]["source"] == "manual"
|
||||
|
||||
|
||||
def test_update_memory_fact_preserves_omitted_fields() -> None:
|
||||
current_memory = _make_memory(
|
||||
facts=[
|
||||
{
|
||||
"id": "fact_edit",
|
||||
"content": "User prefers tabs",
|
||||
"category": "preference",
|
||||
"confidence": 0.8,
|
||||
"createdAt": "2026-03-18T00:00:00Z",
|
||||
"source": "manual",
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
with (
|
||||
patch("deerflow.agents.memory.updater.get_memory_data", return_value=current_memory),
|
||||
patch("deerflow.agents.memory.updater._save_memory_to_file", return_value=True),
|
||||
):
|
||||
result = update_memory_fact(
|
||||
fact_id="fact_edit",
|
||||
content="User prefers spaces",
|
||||
)
|
||||
|
||||
assert result["facts"][0]["content"] == "User prefers spaces"
|
||||
assert result["facts"][0]["category"] == "preference"
|
||||
assert result["facts"][0]["confidence"] == 0.8
|
||||
|
||||
|
||||
def test_update_memory_fact_raises_for_unknown_id() -> None:
|
||||
with patch("deerflow.agents.memory.updater.get_memory_data", return_value=_make_memory()):
|
||||
try:
|
||||
update_memory_fact(
|
||||
fact_id="fact_missing",
|
||||
content="User prefers concise code reviews.",
|
||||
category="preference",
|
||||
confidence=0.88,
|
||||
)
|
||||
except KeyError as exc:
|
||||
assert exc.args == ("fact_missing",)
|
||||
else:
|
||||
raise AssertionError("Expected KeyError for missing fact id")
|
||||
|
||||
|
||||
def test_update_memory_fact_rejects_invalid_confidence() -> None:
|
||||
current_memory = _make_memory(
|
||||
facts=[
|
||||
{
|
||||
"id": "fact_edit",
|
||||
"content": "User prefers tabs",
|
||||
"category": "preference",
|
||||
"confidence": 0.8,
|
||||
"createdAt": "2026-03-18T00:00:00Z",
|
||||
"source": "manual",
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
for confidence in (-0.1, 1.1, float("nan"), float("inf"), float("-inf")):
|
||||
with patch(
|
||||
"deerflow.agents.memory.updater.get_memory_data",
|
||||
return_value=current_memory,
|
||||
):
|
||||
try:
|
||||
update_memory_fact(
|
||||
fact_id="fact_edit",
|
||||
content="User prefers spaces",
|
||||
confidence=confidence,
|
||||
)
|
||||
except ValueError as exc:
|
||||
assert exc.args == ("confidence",)
|
||||
else:
|
||||
raise AssertionError("Expected ValueError for invalid fact confidence")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _extract_text — LLM response content normalization
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user