fix(pipeline): ground local-agent system prompt with current date (#2399)

The local-agent runner's system prompt is a static string with no
template-variable support, so the model had no anchor for "today" and
resolved relative time references (e.g. "this quarter", "latest")
against whichever period was best represented in training data instead
of the real date, sometimes confidently answering with stale
information for time-sensitive questions.

PreProcessor now appends a short, deterministically-computed
"Current date: ..." note to the system prompt on every request for
local-agent pipelines, alongside guidance to verify time-sensitive
facts with a search tool rather than answering from memory. The
existing skill-awareness prompt injection is refactored to share the
same append-to-system-prompt helper.
This commit is contained in:
leonoxo
2026-08-08 22:29:54 +08:00
committed by GitHub
parent 78068db9c8
commit 22c389edc1
2 changed files with 153 additions and 21 deletions
+41 -21
View File
@@ -41,6 +41,29 @@ class PreProcessor(stage.PipelineStage):
selected_tool_names = {tool for tool in selected_tools if isinstance(tool, str)}
return [tool for tool in tools if tool.name in selected_tool_names]
@staticmethod
def _append_to_system_prompt(
messages: list[provider_message.Message],
addition: str,
) -> None:
"""Append text to the first system message, creating one if none exists.
Handles both plain-string and content-element (list) message bodies.
"""
if messages and messages[0].role == 'system':
head = messages[0]
if isinstance(head.content, str):
head.content = head.content + addition
elif isinstance(head.content, list):
for ce in head.content:
if getattr(ce, 'type', None) == 'text':
ce.text = (ce.text or '') + addition
break
else:
head.content.append(provider_message.ContentElement(type='text', text=addition))
else:
messages.insert(0, provider_message.Message(role='system', content=addition.strip()))
async def process(
self,
query: pipeline_query.Query,
@@ -275,6 +298,23 @@ class PreProcessor(stage.PipelineStage):
query.prompt.messages = event_ctx.event.default_prompt
query.messages = event_ctx.event.prompt
# =========== Current date grounding for the local-agent runner ===========
# local-agent system prompts are static strings with no template-variable
# support, so without an explicit anchor the LLM resolves relative time
# references (e.g. "this quarter", "latest", "currently") against whichever
# period is best represented in its training data instead of the real date,
# and won't reliably know to double check time-sensitive facts with a tool.
if selected_runner == 'local-agent':
date_addition = (
f'\n\nCurrent date: {datetime.datetime.now().strftime("%Y-%m-%d (%A)")}. '
'Resolve relative time references (e.g. "today", "this quarter", "latest", '
'"currently") based on this date, not your training cutoff. For anything '
'time-sensitive that may have changed since training — stock prices, '
'financial results, news, current events, exchange rates, or similar — '
'verify with a search tool if one is available rather than answering from memory.'
)
self._append_to_system_prompt(query.prompt.messages, date_addition)
# =========== Skill awareness for the local-agent runner ===========
# The actual activation goes through the ``activate`` Tool Call so the
# LLM doesn't see full SKILL.md instructions until it commits to a
@@ -310,27 +350,7 @@ class PreProcessor(stage.PipelineStage):
bound_skills=bound_skills,
)
if skill_addition:
# Append to the first system message; create one if the
# prompt has none. Handles both plain-string and
# content-element (list) message bodies.
if query.prompt.messages and query.prompt.messages[0].role == 'system':
head = query.prompt.messages[0]
if isinstance(head.content, str):
head.content = head.content + skill_addition
elif isinstance(head.content, list):
appended = False
for ce in head.content:
if getattr(ce, 'type', None) == 'text':
ce.text = (ce.text or '') + skill_addition
appended = True
break
if not appended:
head.content.append(provider_message.ContentElement(type='text', text=skill_addition))
else:
query.prompt.messages.insert(
0,
provider_message.Message(role='system', content=skill_addition.strip()),
)
self._append_to_system_prompt(query.prompt.messages, skill_addition)
self.ap.logger.debug(
f'Skill index injected into system prompt: '
f'pipeline={query.pipeline_uuid} '
+112
View File
@@ -488,3 +488,115 @@ class TestPreProcessorToolSelection:
result = await stage.process(query, 'PreProcessor')
assert [tool.name for tool in result.new_query.use_funcs] == ['plugin_tool']
class TestPreProcessorDateGrounding:
"""Tests for current-date injection into the local-agent system prompt."""
@pytest.mark.asyncio
async def test_local_agent_appends_date_to_existing_system_message(self):
"""Date grounding text should be appended to an existing system prompt."""
preproc = get_preproc_module()
app = FakeApp()
mock_session = make_session()
app.sess_mgr.get_session = AsyncMock(return_value=mock_session)
mock_conversation = Mock()
mock_conversation.prompt = Mock(messages=[])
mock_conversation.prompt.copy = Mock(return_value=Mock(messages=[]))
mock_conversation.messages = []
mock_conversation.uuid = None
app.sess_mgr.get_conversation = AsyncMock(return_value=mock_conversation)
app.model_mgr.get_model_by_uuid = AsyncMock(return_value=None)
app.tool_mgr.get_all_tools = AsyncMock(return_value=[])
from langbot_plugin.api.entities.builtin.provider import message as provider_message
system_message = provider_message.Message(role='system', content='You are a helpful assistant.')
mock_event_ctx = Mock()
mock_event_ctx.event = Mock(default_prompt=[system_message], prompt=[])
app.plugin_connector.emit_event = AsyncMock(return_value=mock_event_ctx)
stage = preproc.PreProcessor(app)
query = text_query('hello')
result = await stage.process(query, 'PreProcessor')
messages = result.new_query.prompt.messages
assert len(messages) == 1
assert messages[0].role == 'system'
assert messages[0].content.startswith('You are a helpful assistant.')
assert 'Current date:' in messages[0].content
@pytest.mark.asyncio
async def test_local_agent_creates_system_message_when_none_exists(self):
"""A system message should be created when the prompt has none."""
preproc = get_preproc_module()
app = FakeApp()
mock_session = make_session()
app.sess_mgr.get_session = AsyncMock(return_value=mock_session)
mock_conversation = Mock()
mock_conversation.prompt = Mock(messages=[])
mock_conversation.prompt.copy = Mock(return_value=Mock(messages=[]))
mock_conversation.messages = []
mock_conversation.uuid = None
app.sess_mgr.get_conversation = AsyncMock(return_value=mock_conversation)
app.model_mgr.get_model_by_uuid = AsyncMock(return_value=None)
app.tool_mgr.get_all_tools = AsyncMock(return_value=[])
mock_event_ctx = Mock()
mock_event_ctx.event = Mock(default_prompt=[], prompt=[])
app.plugin_connector.emit_event = AsyncMock(return_value=mock_event_ctx)
stage = preproc.PreProcessor(app)
query = text_query('hello')
result = await stage.process(query, 'PreProcessor')
messages = result.new_query.prompt.messages
assert len(messages) == 1
assert messages[0].role == 'system'
assert 'Current date:' in messages[0].content
@pytest.mark.asyncio
async def test_non_local_agent_runner_skips_date_injection(self):
"""Runners other than local-agent should not get the date addition."""
preproc = get_preproc_module()
app = FakeApp()
mock_session = make_session()
app.sess_mgr.get_session = AsyncMock(return_value=mock_session)
mock_conversation = Mock()
mock_conversation.prompt = Mock(messages=[])
mock_conversation.prompt.copy = Mock(return_value=Mock(messages=[]))
mock_conversation.messages = []
mock_conversation.uuid = None
app.sess_mgr.get_conversation = AsyncMock(return_value=mock_conversation)
app.model_mgr.get_model_by_uuid = AsyncMock(return_value=None)
app.tool_mgr.get_all_tools = AsyncMock(return_value=[])
mock_event_ctx = Mock()
mock_event_ctx.event = Mock(default_prompt=[], prompt=[])
app.plugin_connector.emit_event = AsyncMock(return_value=mock_event_ctx)
stage = preproc.PreProcessor(app)
query = text_query('hello')
query.pipeline_config = {
'ai': {
'runner': {'runner': 'dify-service-api'},
'local-agent': {'model': {'primary': '', 'fallbacks': []}, 'prompt': 'default'},
},
'output': {'misc': {'at-sender': False}},
'trigger': {'misc': {}},
}
result = await stage.process(query, 'PreProcessor')
assert result.new_query.prompt.messages == []