mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-07-27 08:28:00 +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:
|
||||
fid = fact.get("id", "?")
|
||||
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_short = created_raw[:10] if isinstance(created_raw, str) and len(created_raw) >= 10 else created_raw
|
||||
content = html.escape(str(fact.get("content", "")))
|
||||
@ -1038,7 +1038,7 @@ class MemoryUpdater:
|
||||
max_stale = config.staleness_max_removals_per_cycle
|
||||
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.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]}
|
||||
|
||||
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.updater import (
|
||||
MemoryUpdater,
|
||||
_build_staleness_section,
|
||||
_coerce_source_confidence,
|
||||
_extract_text,
|
||||
_parse_memory_update_response,
|
||||
clear_memory_data,
|
||||
@ -1446,6 +1448,70 @@ class TestSyncUpdateBindsTraceContextVar:
|
||||
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:
|
||||
"""``factsToRemove`` is optional in the memory-update JSON acceptance gate.
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user