mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-08 13:09:36 +00:00
P0: DynamicContextMiddleware role isolation — framework data as SystemMessage, memory as HumanMessage (OWASP LLM01), user input unchanged. Includes E2E hash stabilization: frontend hide_from_ui filter fix in suggest_agent, backend _canonical_messages() hide_from_ui skip, fixture hash updates. Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
parent
5b61214f7b
commit
c0ce759763
@ -3,11 +3,11 @@
|
|||||||
The system prompt is kept fully static for maximum prefix-cache reuse across users
|
The system prompt is kept fully static for maximum prefix-cache reuse across users
|
||||||
and sessions. The current date is always injected. Per-user memory is also injected
|
and sessions. The current date is always injected. Per-user memory is also injected
|
||||||
when ``memory.injection_enabled`` is True in the app config. Both are delivered once
|
when ``memory.injection_enabled`` is True in the app config. Both are delivered once
|
||||||
per conversation as a dedicated <system-reminder> HumanMessage inserted before the
|
per conversation as a dedicated <system-reminder> SystemMessage inserted before the
|
||||||
first user message (frozen-snapshot pattern).
|
first user message (frozen-snapshot pattern).
|
||||||
|
|
||||||
When a conversation spans midnight the middleware detects the date change and injects
|
When a conversation spans midnight the middleware detects the date change and injects
|
||||||
a lightweight date-update reminder as a separate HumanMessage before the current turn.
|
a lightweight date-update reminder as a separate SystemMessage before the current turn.
|
||||||
This correction is persisted so subsequent turns on the new day see a consistent history
|
This correction is persisted so subsequent turns on the new day see a consistent history
|
||||||
and do not re-inject.
|
and do not re-inject.
|
||||||
|
|
||||||
@ -36,7 +36,7 @@ from datetime import datetime
|
|||||||
from typing import TYPE_CHECKING, override
|
from typing import TYPE_CHECKING, override
|
||||||
|
|
||||||
from langchain.agents.middleware import AgentMiddleware
|
from langchain.agents.middleware import AgentMiddleware
|
||||||
from langchain_core.messages import HumanMessage
|
from langchain_core.messages import HumanMessage, SystemMessage
|
||||||
from langgraph.runtime import Runtime
|
from langgraph.runtime import Runtime
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@ -63,7 +63,10 @@ def _extract_date(content: str) -> str | None:
|
|||||||
|
|
||||||
def is_dynamic_context_reminder(message: object) -> bool:
|
def is_dynamic_context_reminder(message: object) -> bool:
|
||||||
"""Return whether *message* is a hidden dynamic-context reminder."""
|
"""Return whether *message* is a hidden dynamic-context reminder."""
|
||||||
return isinstance(message, HumanMessage) and bool(message.additional_kwargs.get(_DYNAMIC_CONTEXT_REMINDER_KEY))
|
# DEPRECATED: HumanMessage reminders only exist in pre-PR checkpoints.
|
||||||
|
# Once all active checkpoints are migrated, the HumanMessage branch can be
|
||||||
|
# removed and this function can check SystemMessage exclusively.
|
||||||
|
return isinstance(message, (HumanMessage, SystemMessage)) and bool(message.additional_kwargs.get(_DYNAMIC_CONTEXT_REMINDER_KEY))
|
||||||
|
|
||||||
|
|
||||||
def _last_injected_date(messages: list) -> str | None:
|
def _last_injected_date(messages: list) -> str | None:
|
||||||
@ -86,7 +89,7 @@ def _is_user_injection_target(message: object) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
class DynamicContextMiddleware(AgentMiddleware):
|
class DynamicContextMiddleware(AgentMiddleware):
|
||||||
"""Inject memory and current date into HumanMessages as a <system-reminder>.
|
"""Inject memory and current date as a SystemMessage <system-reminder>.
|
||||||
|
|
||||||
First turn
|
First turn
|
||||||
----------
|
----------
|
||||||
@ -108,22 +111,31 @@ class DynamicContextMiddleware(AgentMiddleware):
|
|||||||
self._agent_name = agent_name
|
self._agent_name = agent_name
|
||||||
self._app_config = app_config
|
self._app_config = app_config
|
||||||
|
|
||||||
def _build_full_reminder(self) -> str:
|
def _build_full_reminder(self) -> tuple[str, str | None]:
|
||||||
|
"""Return (date_reminder, memory_block | None).
|
||||||
|
|
||||||
|
Framework-owned data (date) is separated from user-owned data (memory)
|
||||||
|
so the downstream SystemMessage carries only framework authority and
|
||||||
|
memory stays at role:user — preventing untrusted content from gaining
|
||||||
|
system privilege (OWASP LLM01).
|
||||||
|
"""
|
||||||
from deerflow.agents.lead_agent.prompt import _get_memory_context
|
from deerflow.agents.lead_agent.prompt import _get_memory_context
|
||||||
|
|
||||||
# Memory injection is gated by injection_enabled; date is always included.
|
|
||||||
injection_enabled = self._app_config.memory.injection_enabled if self._app_config else True
|
injection_enabled = self._app_config.memory.injection_enabled if self._app_config else True
|
||||||
memory_context = _get_memory_context(self._agent_name, app_config=self._app_config) if injection_enabled else ""
|
memory_context = _get_memory_context(self._agent_name, app_config=self._app_config) if injection_enabled else ""
|
||||||
current_date = datetime.now().strftime("%Y-%m-%d, %A")
|
current_date = datetime.now().strftime("%Y-%m-%d, %A")
|
||||||
|
|
||||||
lines: list[str] = ["<system-reminder>"]
|
date_reminder = "\n".join(
|
||||||
if memory_context:
|
[
|
||||||
lines.append(memory_context.strip())
|
"<system-reminder>",
|
||||||
lines.append("") # blank line separating memory from date
|
f"<current_date>{current_date}</current_date>",
|
||||||
lines.append(f"<current_date>{current_date}</current_date>")
|
"</system-reminder>",
|
||||||
lines.append("</system-reminder>")
|
]
|
||||||
|
)
|
||||||
|
|
||||||
return "\n".join(lines)
|
memory_block = memory_context.strip() if memory_context else None
|
||||||
|
|
||||||
|
return date_reminder, memory_block
|
||||||
|
|
||||||
def _build_date_update_reminder(self) -> str:
|
def _build_date_update_reminder(self) -> str:
|
||||||
current_date = datetime.now().strftime("%Y-%m-%d, %A")
|
current_date = datetime.now().strftime("%Y-%m-%d, %A")
|
||||||
@ -136,29 +148,51 @@ class DynamicContextMiddleware(AgentMiddleware):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _make_reminder_and_user_messages(original: HumanMessage, reminder_content: str) -> tuple[HumanMessage, HumanMessage]:
|
def _make_reminder_and_user_messages(
|
||||||
"""Return (reminder_msg, user_msg) using the ID-swap technique.
|
original: HumanMessage,
|
||||||
|
reminder_content: str,
|
||||||
|
memory_content: str | None = None,
|
||||||
|
) -> list[SystemMessage | HumanMessage]:
|
||||||
|
"""Return messages using the ID-swap technique.
|
||||||
|
|
||||||
reminder_msg takes the original message's ID so that add_messages replaces it
|
SystemMessage carries framework-owned data (date, metadata) — takes
|
||||||
in-place (preserving position). user_msg carries the original content with a
|
the original ID so add_messages replaces it in-place. Optional
|
||||||
derived ``{id}__user`` ID and is appended immediately after by add_messages.
|
HumanMessage carries user-owned memory content with ``{id}__memory``.
|
||||||
|
The actual user message gets ``{id}__user``.
|
||||||
|
|
||||||
If the original message has no ID a stable UUID is generated so the derived
|
SystemMessage is used — system context must not masquerade as user
|
||||||
``{id}__user`` ID never collapses to the ambiguous ``None__user`` string.
|
input (#3630). Memory is deliberately kept as HumanMessage so
|
||||||
|
user-influenceable content does not gain system authority (OWASP LLM01).
|
||||||
"""
|
"""
|
||||||
stable_id = original.id or str(uuid.uuid4())
|
stable_id = original.id or str(uuid.uuid4())
|
||||||
reminder_msg = HumanMessage(
|
messages: list[SystemMessage | HumanMessage] = []
|
||||||
content=reminder_content,
|
|
||||||
id=stable_id,
|
messages.append(
|
||||||
additional_kwargs={"hide_from_ui": True, _DYNAMIC_CONTEXT_REMINDER_KEY: True},
|
SystemMessage(
|
||||||
|
content=reminder_content,
|
||||||
|
id=stable_id,
|
||||||
|
additional_kwargs={"hide_from_ui": True, _DYNAMIC_CONTEXT_REMINDER_KEY: True},
|
||||||
|
)
|
||||||
)
|
)
|
||||||
user_msg = HumanMessage(
|
|
||||||
content=original.content,
|
if memory_content:
|
||||||
id=f"{stable_id}__user",
|
messages.append(
|
||||||
name=original.name,
|
HumanMessage(
|
||||||
additional_kwargs=original.additional_kwargs,
|
content=memory_content,
|
||||||
|
id=f"{stable_id}__memory",
|
||||||
|
additional_kwargs={"hide_from_ui": True, _DYNAMIC_CONTEXT_REMINDER_KEY: True},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
messages.append(
|
||||||
|
HumanMessage(
|
||||||
|
content=original.content,
|
||||||
|
id=f"{stable_id}__user",
|
||||||
|
name=original.name,
|
||||||
|
additional_kwargs=original.additional_kwargs,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return reminder_msg, user_msg
|
return messages
|
||||||
|
|
||||||
def _inject(self, state) -> dict | None:
|
def _inject(self, state) -> dict | None:
|
||||||
messages = list(state.get("messages", []))
|
messages = list(state.get("messages", []))
|
||||||
@ -175,32 +209,31 @@ class DynamicContextMiddleware(AgentMiddleware):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if last_date is None:
|
if last_date is None:
|
||||||
# ── First turn: inject full reminder as a separate HumanMessage ─────
|
# ── First turn: inject full reminder as a SystemMessage ─────
|
||||||
first_idx = next((i for i, m in enumerate(messages) if _is_user_injection_target(m)), None)
|
first_idx = next((i for i, m in enumerate(messages) if _is_user_injection_target(m)), None)
|
||||||
if first_idx is None:
|
if first_idx is None:
|
||||||
return None
|
return None
|
||||||
full_reminder = self._build_full_reminder()
|
date_reminder, memory_block = self._build_full_reminder()
|
||||||
logger.info(
|
logger.info(
|
||||||
"DynamicContextMiddleware: injecting full reminder (len=%d, has_memory=%s) into first HumanMessage id=%r",
|
"DynamicContextMiddleware: injecting full reminder (has_memory=%s) into first HumanMessage id=%r",
|
||||||
len(full_reminder),
|
memory_block is not None,
|
||||||
"<memory>" in full_reminder,
|
|
||||||
messages[first_idx].id,
|
messages[first_idx].id,
|
||||||
)
|
)
|
||||||
reminder_msg, user_msg = self._make_reminder_and_user_messages(messages[first_idx], full_reminder)
|
result_msgs = self._make_reminder_and_user_messages(messages[first_idx], date_reminder, memory_block)
|
||||||
return {"messages": [reminder_msg, user_msg]}
|
return {"messages": result_msgs}
|
||||||
|
|
||||||
if last_date == current_date:
|
if last_date == current_date:
|
||||||
# ── Same day: nothing to do ──────────────────────────────────────────
|
# ── Same day: nothing to do ──────────────────────────────────────────
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# ── Midnight crossed: inject date-update reminder as a separate HumanMessage ──
|
# ── Midnight crossed: inject date-update reminder as a SystemMessage ──
|
||||||
last_human_idx = next((i for i in reversed(range(len(messages))) if _is_user_injection_target(messages[i])), None)
|
last_human_idx = next((i for i in reversed(range(len(messages))) if _is_user_injection_target(messages[i])), None)
|
||||||
if last_human_idx is None:
|
if last_human_idx is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
reminder_msg, user_msg = self._make_reminder_and_user_messages(messages[last_human_idx], self._build_date_update_reminder())
|
result_msgs = self._make_reminder_and_user_messages(messages[last_human_idx], self._build_date_update_reminder())
|
||||||
logger.info("DynamicContextMiddleware: midnight crossing detected — injected date update before current turn")
|
logger.info("DynamicContextMiddleware: midnight crossing detected — injected date update before current turn")
|
||||||
return {"messages": [reminder_msg, user_msg]}
|
return {"messages": result_msgs}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def before_agent(self, state, runtime: Runtime) -> dict | None:
|
def before_agent(self, state, runtime: Runtime) -> dict | None:
|
||||||
|
|||||||
@ -59,8 +59,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"caller": "middleware:title",
|
"caller": "middleware:title",
|
||||||
"conversation_hash": "3598aeb87e221ca8f554e4d61ce6d5e8801754606fa5c95a89c38bd6cb623045",
|
"conversation_hash": "958df7bac4e74830d46d29bc19cdea85cae2fc3ebc65ca00a9a5d308bba8ad30",
|
||||||
"input_hash": "75101f9faa453b1a35deff920b1e3c1a9f0b013a7627fbbaa03436752776b953",
|
"input_hash": "f1d96cb1f7cfbe6baf6b2c7b82d32f5561656b1c1492e2714c51631808b50b79",
|
||||||
"output": {
|
"output": {
|
||||||
"type": "ai",
|
"type": "ai",
|
||||||
"data": {
|
"data": {
|
||||||
@ -174,8 +174,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"caller": "suggest_agent",
|
"caller": "suggest_agent",
|
||||||
"conversation_hash": "8b98ebdbb53e88f000556c4753adede8eaa076ff6fd7b8a1285bfd18aee8144d",
|
"conversation_hash": "036fcf61f733a11153586db4fc8c39c412c90ac99d5043c22e812c90572176bf",
|
||||||
"input_hash": "dcd855d389d7179a1e4bc7074fa9ba7ce697570af8947225d6bacb538f14a0cb",
|
"input_hash": "d8b5968661b908283a8c3aadae3f4529ba3a5b0098849b0dde330d5b6ce7941a",
|
||||||
"output": {
|
"output": {
|
||||||
"type": "ai",
|
"type": "ai",
|
||||||
"data": {
|
"data": {
|
||||||
|
|||||||
@ -156,8 +156,17 @@ _BOUNDARY_BEGIN_RE = re.compile(r"--- BEGIN USER INPUT ---\n?")
|
|||||||
_BOUNDARY_END_RE = re.compile(r"\n?--- END USER INPUT ---")
|
_BOUNDARY_END_RE = re.compile(r"\n?--- END USER INPUT ---")
|
||||||
|
|
||||||
|
|
||||||
|
# After _SYSTEM_REMINDER_RE strips a <system-reminder> block, a role label like
|
||||||
|
# "User: " or "Assistant: " may remain with nothing after it (just whitespace/newline).
|
||||||
|
# Collapse those empty role lines so the hash is resilient to reminder leaks from the
|
||||||
|
# frontend (e.g. a HumanMessage(hide_from_ui=True) whose <system-reminder> content
|
||||||
|
# was stripped, leaving "User: \n" residue that would otherwise cause a hash mismatch).
|
||||||
|
_EMPTY_ROLE_LINE_RE = re.compile(r"^(User|Assistant):\s*$\n?", re.MULTILINE)
|
||||||
|
|
||||||
|
|
||||||
def _normalize_text(text: str) -> str:
|
def _normalize_text(text: str) -> str:
|
||||||
text = _SYSTEM_REMINDER_RE.sub("", text)
|
text = _SYSTEM_REMINDER_RE.sub("", text)
|
||||||
|
text = _EMPTY_ROLE_LINE_RE.sub("", text)
|
||||||
text = _BOUNDARY_BEGIN_RE.sub("", text)
|
text = _BOUNDARY_BEGIN_RE.sub("", text)
|
||||||
text = _BOUNDARY_END_RE.sub("", text)
|
text = _BOUNDARY_END_RE.sub("", text)
|
||||||
text = _UUID_RE.sub("<UUID>", text)
|
text = _UUID_RE.sub("<UUID>", text)
|
||||||
@ -202,6 +211,19 @@ def _canonical_messages(messages: list[BaseMessage]) -> str:
|
|||||||
# most-edited part of the prompt and not part of the contract under test.
|
# most-edited part of the prompt and not part of the contract under test.
|
||||||
if message.type == "system":
|
if message.type == "system":
|
||||||
continue
|
continue
|
||||||
|
# Exclude framework-injected hidden messages (dynamic-context reminders,
|
||||||
|
# memory injections, etc.) regardless of type. These carry volatile
|
||||||
|
# per-session data (current date, user memory) and are not user-authored
|
||||||
|
# content, so they must not participate in the match key. On the p1
|
||||||
|
# branch they were HumanMessages whose <system-reminder> content was
|
||||||
|
# stripped → empty → excluded by the empty-content check below. On p0
|
||||||
|
# they may be SystemMessages (excluded by type) or HumanMessages with
|
||||||
|
# standalone <memory> content (not stripped by _SYSTEM_REMINDER_RE,
|
||||||
|
# not empty, so previously leaked into the hash). Checking hide_from_ui
|
||||||
|
# directly makes the hash stable across all middleware implementations.
|
||||||
|
additional_kwargs = getattr(message, "additional_kwargs", None) or {}
|
||||||
|
if additional_kwargs.get("hide_from_ui"):
|
||||||
|
continue
|
||||||
content = _normalize_text(_content_to_text(message.content))
|
content = _normalize_text(_content_to_text(message.content))
|
||||||
tool_calls = getattr(message, "tool_calls", None)
|
tool_calls = getattr(message, "tool_calls", None)
|
||||||
# Drop messages that are empty after normalization — e.g. a turn that was
|
# Drop messages that are empty after normalization — e.g. a turn that was
|
||||||
|
|||||||
@ -7,7 +7,7 @@ the first HumanMessage exactly once per session (frozen-snapshot pattern).
|
|||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from langchain_core.messages import AIMessage, HumanMessage
|
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
|
||||||
|
|
||||||
from deerflow.agents.middlewares.dynamic_context_middleware import (
|
from deerflow.agents.middlewares.dynamic_context_middleware import (
|
||||||
_DYNAMIC_CONTEXT_REMINDER_KEY,
|
_DYNAMIC_CONTEXT_REMINDER_KEY,
|
||||||
@ -26,7 +26,11 @@ def _fake_runtime():
|
|||||||
|
|
||||||
|
|
||||||
def _reminder_msg(content: str, msg_id: str) -> HumanMessage:
|
def _reminder_msg(content: str, msg_id: str) -> HumanMessage:
|
||||||
"""Build a reminder HumanMessage the way the middleware would produce it."""
|
"""Build a pre-PR HumanMessage reminder — simulates historical checkpoints.
|
||||||
|
|
||||||
|
Uses HumanMessage (DEPRECATED format) to exercise the backward-compat
|
||||||
|
path in ``is_dynamic_context_reminder``. New reminders are SystemMessage.
|
||||||
|
"""
|
||||||
return HumanMessage(
|
return HumanMessage(
|
||||||
content=content,
|
content=content,
|
||||||
id=msg_id,
|
id=msg_id,
|
||||||
@ -52,7 +56,7 @@ def test_injects_system_reminder_into_first_human_message():
|
|||||||
assert len(updated_msgs) == 2
|
assert len(updated_msgs) == 2
|
||||||
|
|
||||||
reminder_msg = updated_msgs[0]
|
reminder_msg = updated_msgs[0]
|
||||||
assert isinstance(reminder_msg, HumanMessage)
|
assert isinstance(reminder_msg, SystemMessage)
|
||||||
assert reminder_msg.id == "msg-1" # takes the original ID (position swap)
|
assert reminder_msg.id == "msg-1" # takes the original ID (position swap)
|
||||||
assert reminder_msg.additional_kwargs.get(_DYNAMIC_CONTEXT_REMINDER_KEY) is True
|
assert reminder_msg.additional_kwargs.get(_DYNAMIC_CONTEXT_REMINDER_KEY) is True
|
||||||
assert _SYSTEM_REMINDER_TAG in reminder_msg.content
|
assert _SYSTEM_REMINDER_TAG in reminder_msg.content
|
||||||
@ -79,11 +83,18 @@ def test_memory_included_when_present():
|
|||||||
mock_dt.now.return_value.strftime.return_value = "2026-05-08, Friday"
|
mock_dt.now.return_value.strftime.return_value = "2026-05-08, Friday"
|
||||||
result = mw.before_agent(state, _fake_runtime())
|
result = mw.before_agent(state, _fake_runtime())
|
||||||
|
|
||||||
# Reminder is the first returned message; user query is the second
|
# Memory is a separate HumanMessage — not merged into SystemMessage (OWASP LLM01)
|
||||||
reminder_content = result["messages"][0].content
|
msgs = result["messages"]
|
||||||
assert "User prefers Python." in reminder_content
|
assert len(msgs) == 3 # date SystemMessage + memory HumanMessage + user HumanMessage
|
||||||
assert "<current_date>2026-05-08, Friday</current_date>" in reminder_content
|
|
||||||
assert result["messages"][1].content == "Hi"
|
assert isinstance(msgs[0], SystemMessage)
|
||||||
|
assert "<current_date>2026-05-08, Friday</current_date>" in msgs[0].content
|
||||||
|
assert "User prefers Python." not in msgs[0].content # memory NOT in system role
|
||||||
|
|
||||||
|
assert isinstance(msgs[1], HumanMessage)
|
||||||
|
assert "User prefers Python." in msgs[1].content
|
||||||
|
|
||||||
|
assert msgs[2].content == "Hi"
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@ -279,6 +290,9 @@ def test_midnight_crossing_injects_date_update_as_separate_message():
|
|||||||
msgs = result["messages"]
|
msgs = result["messages"]
|
||||||
assert len(msgs) == 2
|
assert len(msgs) == 2
|
||||||
|
|
||||||
|
# Midnight-cross reminder is also a SystemMessage — both paths are covered
|
||||||
|
assert isinstance(msgs[0], SystemMessage)
|
||||||
|
|
||||||
# Date-update reminder takes the current message's ID
|
# Date-update reminder takes the current message's ID
|
||||||
assert msgs[0].id == "msg-2"
|
assert msgs[0].id == "msg-2"
|
||||||
assert msgs[0].additional_kwargs.get(_DYNAMIC_CONTEXT_REMINDER_KEY) is True
|
assert msgs[0].additional_kwargs.get(_DYNAMIC_CONTEXT_REMINDER_KEY) is True
|
||||||
@ -310,6 +324,44 @@ def test_midnight_crossing_id_swap():
|
|||||||
assert result["messages"][1].id == "msg-2__user"
|
assert result["messages"][1].id == "msg-2__user"
|
||||||
|
|
||||||
|
|
||||||
|
def test_memory_message_carries_reminder_key_for_title_eligibility():
|
||||||
|
"""Regression: memory HumanMessage must carry _DYNAMIC_CONTEXT_REMINDER_KEY.
|
||||||
|
|
||||||
|
Without it, title_middleware._is_user_message_for_title counts the memory
|
||||||
|
block as a second user message and skips title generation entirely.
|
||||||
|
Similarly, summarization_middleware._preserve_dynamic_context_reminders
|
||||||
|
would not rescue the memory block from summary compression.
|
||||||
|
"""
|
||||||
|
from deerflow.agents.middlewares.dynamic_context_middleware import is_dynamic_context_reminder
|
||||||
|
|
||||||
|
mw = _make_middleware()
|
||||||
|
state = {"messages": [HumanMessage(content="Hi", id="msg-1")]}
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch(
|
||||||
|
"deerflow.agents.lead_agent.prompt._get_memory_context",
|
||||||
|
return_value="<memory>\nUser prefers Python.\n</memory>",
|
||||||
|
),
|
||||||
|
mock.patch("deerflow.agents.middlewares.dynamic_context_middleware.datetime") as mock_dt,
|
||||||
|
):
|
||||||
|
mock_dt.now.return_value.strftime.return_value = "2026-05-08, Friday"
|
||||||
|
result = mw.before_agent(state, _fake_runtime())
|
||||||
|
|
||||||
|
msgs = result["messages"]
|
||||||
|
# Memory message must be recognized as a dynamic-context reminder
|
||||||
|
memory_msg = msgs[1]
|
||||||
|
assert isinstance(memory_msg, HumanMessage)
|
||||||
|
assert memory_msg.id == "msg-1__memory"
|
||||||
|
assert is_dynamic_context_reminder(memory_msg) is True
|
||||||
|
|
||||||
|
# Only the actual user message is title-eligible
|
||||||
|
from deerflow.agents.middlewares.title_middleware import TitleMiddleware
|
||||||
|
|
||||||
|
title_eligible = [m for m in msgs if TitleMiddleware._is_user_message_for_title(m)]
|
||||||
|
assert len(title_eligible) == 1
|
||||||
|
assert title_eligible[0].content == "Hi"
|
||||||
|
|
||||||
|
|
||||||
def test_no_second_midnight_injection_once_date_updated():
|
def test_no_second_midnight_injection_once_date_updated():
|
||||||
"""After a midnight update is persisted, the same-day path skips re-injection."""
|
"""After a midnight update is persisted, the same-day path skips re-injection."""
|
||||||
mw = _make_middleware()
|
mw = _make_middleware()
|
||||||
|
|||||||
@ -59,6 +59,7 @@ import {
|
|||||||
import { fetch } from "@/core/api/fetcher";
|
import { fetch } from "@/core/api/fetcher";
|
||||||
import { getBackendBaseURL } from "@/core/config";
|
import { getBackendBaseURL } from "@/core/config";
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
|
import { isHiddenFromUIMessage } from "@/core/messages/utils";
|
||||||
import { useModels } from "@/core/models/hooks";
|
import { useModels } from "@/core/models/hooks";
|
||||||
import type { Skill } from "@/core/skills";
|
import type { Skill } from "@/core/skills";
|
||||||
import { useSkills } from "@/core/skills/hooks";
|
import { useSkills } from "@/core/skills/hooks";
|
||||||
@ -533,6 +534,7 @@ export function InputBox({
|
|||||||
|
|
||||||
const recent = messagesRef.current
|
const recent = messagesRef.current
|
||||||
.filter((m) => m.type === "human" || m.type === "ai")
|
.filter((m) => m.type === "human" || m.type === "ai")
|
||||||
|
.filter((m) => !isHiddenFromUIMessage(m))
|
||||||
.map((m) => {
|
.map((m) => {
|
||||||
const role = m.type === "human" ? "user" : "assistant";
|
const role = m.type === "human" ? "user" : "assistant";
|
||||||
const content = textOfMessage(m) ?? "";
|
const content = textOfMessage(m) ?? "";
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user