mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-07-21 10:15:47 +00:00
Fix UnboundLocalError in memory injection when facts are empty (#3992)
`format_memory_for_injection` bound `facts_header` / `all_fact_lines` only inside the `if isinstance(facts_data, list) and facts_data:` block, but the structure-aware overflow-truncation path at the end of the function references both unconditionally. When a user's memory has sizeable user-context / history (so `sections` is non-empty and the assembled output exceeds `max_tokens`) but an empty or missing `facts` list, that block is skipped, so the truncation branch hits `UnboundLocalError: cannot access local variable 'all_fact_lines'` and aborts memory injection entirely. Hoist the two initializers to function scope, alongside the existing `guaranteed_line_tokens = 0`, so they are always bound. Behaviour is unchanged when facts are present. Adds a regression test (empty facts + oversized user context) that fails with UnboundLocalError before the fix and truncates gracefully after.
This commit is contained in:
@@ -589,6 +589,13 @@ def format_memory_for_injection(
|
||||
# performs a single-pass confidence-only ranking.
|
||||
facts_data = memory_data.get("facts", [])
|
||||
guaranteed_line_tokens = 0 # used later for the effective truncation limit
|
||||
# Initialise the facts-block markers at function scope (alongside
|
||||
# ``guaranteed_line_tokens`` above) so the structure-aware truncation at the
|
||||
# bottom can reference them even when there are no facts and the block below
|
||||
# never runs. Otherwise the overflow path raises ``UnboundLocalError`` when a
|
||||
# user has sizeable context/history but an empty ``facts`` list.
|
||||
facts_header = "Facts:\n"
|
||||
all_fact_lines: list[str] = []
|
||||
if isinstance(facts_data, list) and facts_data:
|
||||
# Token cost of sections built above (user context, history).
|
||||
base_text = "\n\n".join(sections)
|
||||
@@ -599,13 +606,6 @@ def format_memory_for_injection(
|
||||
# redoing validation work on the hot prompt-injection path.
|
||||
valid_facts = [f for f in facts_data if isinstance(f, dict) and isinstance(f.get("content"), str) and f.get("content", "").strip()]
|
||||
|
||||
# Initialise the facts-block markers *before* the try so the
|
||||
# structure-aware truncation at the bottom of the function can
|
||||
# reason about them regardless of whether the primary path or
|
||||
# the except/fallback path produced the final Facts section.
|
||||
facts_header = "Facts:\n"
|
||||
all_fact_lines: list[str] = []
|
||||
|
||||
try:
|
||||
# Partition valid facts into guaranteed vs regular groups.
|
||||
# Use the *raw* category field (no ``or "context"`` default) so
|
||||
|
||||
@@ -508,6 +508,36 @@ def test_structure_aware_truncation_preserves_guaranteed_on_overflow(monkeypatch
|
||||
assert result.rstrip().endswith("(avoid: pip is deprecated)")
|
||||
|
||||
|
||||
def test_structure_aware_truncation_no_facts_does_not_raise(monkeypatch) -> None:
|
||||
"""When preceding sections overflow but there are no facts at all, the
|
||||
truncation path must still clip gracefully instead of raising
|
||||
``UnboundLocalError``.
|
||||
|
||||
Regression: ``facts_header`` / ``all_fact_lines`` were only bound inside the
|
||||
``if isinstance(facts_data, list) and facts_data:`` block, yet the
|
||||
overflow-truncation path below references them unconditionally. With an empty
|
||||
``facts`` list and an oversized user-context section, the truncation branch
|
||||
raised ``UnboundLocalError`` and aborted memory injection entirely.
|
||||
"""
|
||||
monkeypatch.setattr(
|
||||
"deerflow.agents.memory.prompt._count_tokens",
|
||||
lambda text, encoding_name="cl100k_base", *, use_tiktoken=True: len(text),
|
||||
)
|
||||
|
||||
memory_data = {
|
||||
"user": {"workContext": {"summary": "X" * 4000}},
|
||||
"facts": [], # no facts -> the facts-block initializers are skipped
|
||||
}
|
||||
|
||||
result = format_memory_for_injection(memory_data, max_tokens=200, use_tiktoken=False)
|
||||
|
||||
assert isinstance(result, str)
|
||||
assert "User Context:" in result
|
||||
# The oversized preceding section was clipped from the tail.
|
||||
assert result.rstrip().endswith("...")
|
||||
assert len(result) < 4000
|
||||
|
||||
|
||||
def test_single_inter_section_separator_between_user_and_facts() -> None:
|
||||
"""[P2] Exactly one ``\\n\\n`` separator between ``User Context:`` and
|
||||
``Facts:`` — never four newlines.
|
||||
|
||||
Reference in New Issue
Block a user