mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-07-21 18:25:48 +00:00
* feat(memory): template externalization — externalized prompts + signal patterns Externalize two categories of hardcoded content in the DeerMem memory backend so operators can customise them without touching Python code. Prompt externalization (plugin 06): - 4 memory extraction prompts moved from Python string constants to YAML files under core/prompts/ (consolidation / fact_extraction / memory_update.chat / staleness_review). - load_prompt(name) and load_prompt_messages(name, variables) loaders. - memory_update uses the chat format (system / user message split) for prompt-caching-friendly system prefixes; other prompts stay text. - The extraction callback + per-agent prompt directories + Jinja2 dependency are intentionally not included (minimal surface). - Compatible with the upstream prompt additions from #4143 (expected_valid_days, staleFactsToExtend, KEEP/REMOVE/EXTEND in staleness review). Signal-pattern externalization (plugin 07, regex only): - Correction and reinforcement detection patterns externalised to core/message_patterns/{correction,reinforcement}.yaml. - load_patterns(name, patterns_dir) loader with bundled defaults. - detect_correction / detect_reinforcement accept a keyword-only patterns= parameter; signatures are backward-compatible. - patterns_dir config field added to DeerMemConfig. - Importance scorer (importance.py, build_importance_scorer, _prepare_update changes) is NOT included — deferred to a later PR. Co-Authored-By: Claude <noreply@anthropic.com> * fix(memory): fail loudly on invalid yaml in prompt/pattern loaders Replace silent error handling in load_prompt / load_prompt_messages / load_patterns so malformed yaml or missing required keys raise ValueError with the file path rather than a raw YAMLError traceback or silent empty-string / empty-list return. Changes: - load_prompt: YAMLError -> ValueError(path); missing/empty 'template' key -> ValueError - load_prompt_messages: YAMLError -> ValueError(path); missing/empty 'messages' key -> ValueError; KeyError from .format (placeholder mismatch) -> ValueError - load_patterns: YAMLError -> ValueError (was silent warn+return []). OSError still degrades (permission, not format). Not-a-list yaml -> ValueError (was silent warn+return []). Co-Authored-By: Claude <noreply@anthropic.com> * fix(memory): wire prompts_dir through updater, fail-loud patterns_dir, bump config_version Addresses PR feedback on the template-externalization PR: P1 — Wire prompts_dir into the DeerMem update path: - Add prompts_dir field to DeerMemConfig (default None = bundled defaults). - Thread it through DeerMem -> MemoryUpdater.__init__(prompts_dir=). - _build_staleness_section / _build_consolidation_section accept prompts_dir= keyword and call load_prompt() instead of relying on module-level shim constants. - _prepare_update_prompt passes prompts_dir to load_prompt_messages and to both section builders. - Add _PROMPT_CACHE to load_prompt so repeated lookups (once per memory-update cycle) do not re-read yaml from disk. P2 — Explicit patterns_dir must find its files: - When patterns_dir is explicitly set and a YAML file is missing, load_patterns() raises FileNotFoundError instead of silently caching an empty list. Bundled defaults (patterns_dir=None) still log a WARNING and return [] for a missing bundled file (packaging bug). P3 — Bump config_version and sync Helm: - config.example.yaml: 26 -> 27 - deploy/helm/deer-flow/values.yaml: 26 -> 27 - deploy/helm/deer-flow/README.md: 26 -> 27 - scripts/check_config_version.sh confirms parity. Tests: 224 passed (218 memory + 6 config_version). Lint: ruff check + ruff format --check pass. Co-Authored-By: Claude <noreply@anthropic.com> * fix(memory): cache load_prompt_messages, validate format fields, fail-loud load_patterns Review feedback from willem-bd: Cache for load_prompt_messages: Add _CHAT_TEMPLATE_CACHE + _render_messages() helper so the parsed chat templates are cached per (name, agent, prompts_dir). On cache hit only .format() rendering runs; the yaml file is read once per key. Validate format field in both loaders: load_prompt rejects format='chat' (redirects to load_prompt_messages); load_prompt_messages rejects format='text' (redirects to load_prompt). Prevents operators from loading a chat yaml via the text loader (or vice versa) without a clear error. Load_patterns fail-loud for explicit directories: - OSError (permission, etc.) now raises for explicit patterns_dir instead of silently disabling detection. - Malformed entries (missing/empty pattern key, wrong type) are skipped with a WARNING instead of silent skip. - Unknown flag names are warned instead of silently dropped. - re.error from compile() raises ValueError with file path and entry index instead of a bare re.error traceback. Co-Authored-By: Claude <noreply@anthropic.com> * fix(memory): thread agent_name through section builders, validate explicit prompts at construction, bump config_version to 28 P1 — Thread agent_name through staleness/consolidation section builders: _build_staleness_section and _build_consolidation_section now accept agent_name= and pass it to load_prompt(), so per-agent prompt overrides work for section templates too (not just memory_update). Previously only prompts_dir was threaded; agent_name was ignored for section builders. P1 — Validate explicit prompts at DeerMem construction: When DeerMemConfig.prompts_dir is explicitly set, DeerMem.__init__ now pre-loads all four prompt templates (staleness_review, consolidation, fact_extraction, memory_update with dummy variables) at construction time. A missing file, malformed YAML, or invalid placeholder raises immediately at startup instead of being caught by the updater's generic error handler and silently dropped as a failed update. P2 — Advance config_version to 28: Upstream main already consumed the previous 26→27 bump with a different schema change. Bump config.example.yaml, Helm values.yaml, and Helm README.md to 28 to version this PR's patterns_dir and prompts_dir additions. Co-Authored-By: Claude <noreply@anthropic.com> * fix(memory): render text templates at construction, propagate PromptConfigurationError Keep prompt-configuration failures out of the recoverable update catch: - Define PromptConfigurationError(ValueError) in prompt.py. Raised by load_prompt, load_prompt_messages, and _render_messages for bad yaml, missing keys, and invalid placeholders. - _do_update_memory_sync_impl, update_memory's executor path, and _process_queue all re-raise (PromptConfigurationError, FileNotFoundError, OSError) before the generic except Exception, so a bad explicit prompt is never silently returned as False. - DeerMem.__init__ prompt validation now renders text templates with dummy variables (.format(stale_facts="") etc.) so an unknown placeholder in staleness_review.yaml, consolidation.yaml, or fact_extraction.yaml is caught at construction. Co-Authored-By: Claude <noreply@anthropic.com> * fix(memory): drop re-raise guards, soften validation comment, exclude dormant fact_extraction - Remove (PromptConfigurationError, FileNotFoundError, OSError) re-raise guards from _do_update_memory_sync_impl, update_memory executor path, and _process_queue. The existing except Exception: logger.exception(...) already logs prompt-config errors at ERROR with full traceback; the re-raise was killing the entire batch and only surfacing via stderr. Per-agent override errors now log at ERROR per-item without aborting co-tenant updates. - Soften the construction validation comment to clarify it only covers global templates. Per-agent overrides are validated lazily at first use and logged at ERROR by the updater's exception handler. - Drop fact_extraction from construction validation (dormant prompt with no runtime caller; the shim + yaml remain for backward compat). Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
132 lines
4.3 KiB
Python
132 lines
4.3 KiB
Python
"""Tests for message_processing: externalized signal patterns.
|
|
|
|
Covers:
|
|
- load_patterns (bundled defaults, caching, custom dir override, missing file)
|
|
- detect_correction / detect_reinforcement (backward-compatible signature,
|
|
patterns override, last-6 window)
|
|
- DeerMem._prepare_update (3-tuple, returns None when missing a role)
|
|
- DeerMemConfig.patterns_dir default
|
|
"""
|
|
|
|
import re
|
|
|
|
from langchain_core.messages import AIMessage, HumanMessage
|
|
|
|
from deerflow.agents.memory.backends.deermem.deer_mem import DeerMem
|
|
from deerflow.agents.memory.backends.deermem.deermem.config import DeerMemConfig
|
|
from deerflow.agents.memory.backends.deermem.deermem.core.message_processing import (
|
|
detect_correction,
|
|
detect_reinforcement,
|
|
filter_messages_for_memory,
|
|
load_patterns,
|
|
)
|
|
|
|
|
|
def _human(text: str) -> HumanMessage:
|
|
return HumanMessage(content=text)
|
|
|
|
|
|
def _ai(text: str) -> AIMessage:
|
|
return AIMessage(content=text)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# load_patterns
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_load_patterns_bundled_nonempty():
|
|
assert len(load_patterns("correction")) > 0
|
|
assert len(load_patterns("reinforcement")) > 0
|
|
|
|
|
|
def test_load_patterns_cached():
|
|
assert load_patterns("correction") is load_patterns("correction")
|
|
|
|
|
|
def test_load_patterns_custom_dir_overrides(tmp_path):
|
|
(tmp_path / "correction.yaml").write_text("- 'foobarbaz'\n", encoding="utf-8")
|
|
pats = load_patterns("correction", patterns_dir=str(tmp_path))
|
|
assert len(pats) == 1
|
|
assert pats[0].search("hello foobarbaz world")
|
|
# bundled defaults remain intact (different cache key)
|
|
assert len(load_patterns("correction")) > 1
|
|
|
|
|
|
def test_load_patterns_missing_file_explicit_dir_raises(tmp_path):
|
|
import pytest
|
|
|
|
with pytest.raises(FileNotFoundError, match="nope"):
|
|
load_patterns("nope", patterns_dir=str(tmp_path))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# detect_correction / detect_reinforcement
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_detect_correction_default_bundled():
|
|
msgs = [_human("That's wrong, use uv"), _ai("ok")]
|
|
assert detect_correction(msgs) is True
|
|
assert detect_reinforcement(msgs) is False
|
|
|
|
|
|
def test_detect_reinforcement_default_bundled():
|
|
msgs = [_human("perfect, exactly right"), _ai("great")]
|
|
assert detect_reinforcement(msgs) is True
|
|
|
|
|
|
def test_detect_correction_patterns_override():
|
|
custom = [re.compile(r"zzz")]
|
|
assert detect_correction([_human("zzz here")], patterns=custom) is True
|
|
assert detect_correction([_human("That's wrong")], patterns=custom) is False
|
|
|
|
|
|
def test_detect_window_is_last_six():
|
|
# 7 human turns; a correction in the oldest (outside [-6:]) is not detected.
|
|
msgs = [_human(f"msg {i}") for i in range(7)]
|
|
msgs[0] = _human("That's wrong, old")
|
|
assert detect_correction(msgs) is False
|
|
msgs[-1] = _human("That's wrong, recent")
|
|
assert detect_correction(msgs) is True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# DeerMem._prepare_update (3-tuple, signal detection with externalized patterns)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _make_deermem(tmp_path) -> DeerMem:
|
|
return DeerMem(backend_config={"storage_path": str(tmp_path)})
|
|
|
|
|
|
def test_prepare_update_missing_role_returns_none(tmp_path):
|
|
m = _make_deermem(tmp_path)
|
|
assert m._prepare_update([_human("only human")]) is None
|
|
assert m._prepare_update([_ai("only ai")]) is None
|
|
assert m._prepare_update([]) is None
|
|
|
|
|
|
def test_prepare_update_returns_3tuple_with_correction_true(tmp_path):
|
|
m = _make_deermem(tmp_path)
|
|
r = m._prepare_update([_human("That's wrong, use uv"), _ai("ok")])
|
|
assert r is not None and len(r) == 3
|
|
filtered, corr, rein = r
|
|
assert corr is True
|
|
assert rein is False
|
|
assert len(filtered) == 2
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Config defaults
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_config_patterns_dir_default():
|
|
assert DeerMemConfig().patterns_dir is None
|
|
|
|
|
|
def test_filter_messages_backward_compat():
|
|
filtered = filter_messages_for_memory([_human("hello"), _ai("hi there")])
|
|
assert len(filtered) == 2
|