mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-08-01 10:56:02 +00:00
fix(memory): coerce null source.confidence so it no longer blocks memory updates (#4074)
* fix(memory): coerce null confidence when ranking stale facts
_build_staleness_section and the per-cycle removal cap in _apply_updates
used fact.get("confidence", 0.0/0), which only defaults when the key is
absent. A fact whose confidence is explicitly null (or otherwise malformed)
returned None, breaking the numeric sort/format. Use
_coerce_source_confidence, which normalizes null/malformed values and clamps
to [0, 1], so null-confidence facts no longer block staleness handling.
* ci: retrigger cancelled CI workflow
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
parent
6af6cfc70b
commit
490deeb931
@ -592,7 +592,7 @@ def _build_staleness_section(
|
|||||||
for fact in stale_candidates:
|
for fact in stale_candidates:
|
||||||
fid = fact.get("id", "?")
|
fid = fact.get("id", "?")
|
||||||
cat = html.escape(str(fact.get("category", "context")).strip() or "context")
|
cat = html.escape(str(fact.get("category", "context")).strip() or "context")
|
||||||
conf = fact.get("confidence", 0.0)
|
conf = _coerce_source_confidence(fact)
|
||||||
created_raw = fact.get("createdAt", "")
|
created_raw = fact.get("createdAt", "")
|
||||||
created_short = created_raw[:10] if isinstance(created_raw, str) and len(created_raw) >= 10 else created_raw
|
created_short = created_raw[:10] if isinstance(created_raw, str) and len(created_raw) >= 10 else created_raw
|
||||||
content = html.escape(str(fact.get("content", "")))
|
content = html.escape(str(fact.get("content", "")))
|
||||||
@ -1038,7 +1038,7 @@ class MemoryUpdater:
|
|||||||
max_stale = config.staleness_max_removals_per_cycle
|
max_stale = config.staleness_max_removals_per_cycle
|
||||||
if len(stale_ids_to_remove) > max_stale:
|
if len(stale_ids_to_remove) > max_stale:
|
||||||
stale_facts = [f for f in current_memory.get("facts", []) if f.get("id") in stale_ids_to_remove]
|
stale_facts = [f for f in current_memory.get("facts", []) if f.get("id") in stale_ids_to_remove]
|
||||||
stale_facts.sort(key=lambda f: f.get("confidence", 0))
|
stale_facts.sort(key=_coerce_source_confidence)
|
||||||
stale_ids_to_remove = {f["id"] for f in stale_facts[:max_stale]}
|
stale_ids_to_remove = {f["id"] for f in stale_facts[:max_stale]}
|
||||||
|
|
||||||
current_memory["facts"] = [f for f in current_memory.get("facts", []) if f.get("id") not in stale_ids_to_remove]
|
current_memory["facts"] = [f for f in current_memory.get("facts", []) if f.get("id") not in stale_ids_to_remove]
|
||||||
|
|||||||
@ -5,6 +5,8 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
|||||||
from deerflow.agents.memory.prompt import format_conversation_for_update
|
from deerflow.agents.memory.prompt import format_conversation_for_update
|
||||||
from deerflow.agents.memory.updater import (
|
from deerflow.agents.memory.updater import (
|
||||||
MemoryUpdater,
|
MemoryUpdater,
|
||||||
|
_build_staleness_section,
|
||||||
|
_coerce_source_confidence,
|
||||||
_extract_text,
|
_extract_text,
|
||||||
_parse_memory_update_response,
|
_parse_memory_update_response,
|
||||||
clear_memory_data,
|
clear_memory_data,
|
||||||
@ -1446,6 +1448,70 @@ class TestSyncUpdateBindsTraceContextVar:
|
|||||||
assert get_current_trace_id() == "outer-trace"
|
assert get_current_trace_id() == "outer-trace"
|
||||||
|
|
||||||
|
|
||||||
|
class TestNullConfidenceDoesNotBlockUpdates:
|
||||||
|
"""A fact persisted with ``"confidence": null`` (corrupted or hand-edited
|
||||||
|
memory file) must not crash confidence-sensitive code paths.
|
||||||
|
|
||||||
|
``dict.get("confidence", 0.0)`` returns the stored ``None`` when the key is
|
||||||
|
present, which then propagates into ``f"{conf:.2f}"`` formatting and into
|
||||||
|
``list.sort`` comparisons and raises ``TypeError``. ``_coerce_source_confidence``
|
||||||
|
guards both call sites.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_build_staleness_section_handles_null_confidence(self) -> None:
|
||||||
|
stale = [
|
||||||
|
{
|
||||||
|
"id": "fact_null",
|
||||||
|
"content": "User prefers concise answers",
|
||||||
|
"category": "preference",
|
||||||
|
"confidence": None,
|
||||||
|
"createdAt": "2000-01-01T00:00:00Z",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
# Must not raise TypeError on ``f"{None:.2f}"``.
|
||||||
|
section = _build_staleness_section(stale, age_days=90)
|
||||||
|
|
||||||
|
assert isinstance(section, str)
|
||||||
|
assert "fact_null" in section
|
||||||
|
|
||||||
|
def test_apply_updates_staleness_sort_handles_null_confidence(self) -> None:
|
||||||
|
updater = MemoryUpdater()
|
||||||
|
aged = "2000-01-01T00:00:00Z" # far older than staleness_age_days
|
||||||
|
facts = [
|
||||||
|
{"id": "f_null", "content": "a", "category": "context", "confidence": None, "createdAt": aged},
|
||||||
|
{"id": "f_high", "content": "b", "category": "context", "confidence": 0.9, "createdAt": aged},
|
||||||
|
{"id": "f_low", "content": "c", "category": "context", "confidence": 0.2, "createdAt": aged},
|
||||||
|
]
|
||||||
|
memory = _make_memory(facts)
|
||||||
|
update_data = {
|
||||||
|
"user": {},
|
||||||
|
"history": {},
|
||||||
|
"newFacts": [],
|
||||||
|
"factsToRemove": [],
|
||||||
|
# LLM asks to remove all three; the per-cycle cap keeps only the
|
||||||
|
# lowest-confidence one, which forces the sort over null confidence.
|
||||||
|
"staleFactsToRemove": [{"id": "f_null"}, {"id": "f_high"}, {"id": "f_low"}],
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"deerflow.agents.memory.updater.get_memory_config",
|
||||||
|
return_value=_memory_config(staleness_max_removals_per_cycle=1, staleness_age_days=90),
|
||||||
|
):
|
||||||
|
# Must not raise TypeError comparing None with floats during sort.
|
||||||
|
result = updater._apply_updates(memory, update_data)
|
||||||
|
|
||||||
|
remaining_ids = {fact["id"] for fact in result["facts"]}
|
||||||
|
# Lowest confidence (0.2) is removed first; null coerces to 0.5, so it stays.
|
||||||
|
assert "f_low" not in remaining_ids
|
||||||
|
assert remaining_ids == {"f_null", "f_high"}
|
||||||
|
|
||||||
|
def test_coerce_source_confidence_defaults_null_to_midpoint(self) -> None:
|
||||||
|
assert _coerce_source_confidence({"confidence": None}) == 0.5
|
||||||
|
assert _coerce_source_confidence({}) == 0.5
|
||||||
|
assert _coerce_source_confidence({"confidence": 0.83}) == 0.83
|
||||||
|
|
||||||
|
|
||||||
class TestParseMemoryUpdateFactsToRemoveGate:
|
class TestParseMemoryUpdateFactsToRemoveGate:
|
||||||
"""``factsToRemove`` is optional in the memory-update JSON acceptance gate.
|
"""``factsToRemove`` is optional in the memory-update JSON acceptance gate.
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user