fix(agent): honor verbose mode in WebAgent tool display

This commit is contained in:
jxxghp
2026-09-12 23:09:48 +08:00
parent aae90b1cdb
commit ef51cf6da4
3 changed files with 92 additions and 15 deletions

View File

@@ -127,6 +127,10 @@ class StreamingHandler:
)
return self.emit(f"\n\n{tool_message}\n\n")
def _is_verbose_mode(self) -> bool:
"""判断当前是否启用逐条工具调用展示。"""
return bool(get_runtime_setting("AI_AGENT_VERBOSE"))
def report_tool_call(
self,
tool_name: str,
@@ -140,7 +144,7 @@ class StreamingHandler:
相同的详细模式语义,避免无条件调用 ``record_tool_call`` 后只显示汇总。
"""
safe_message = sanitize_for_host(tool_message) if tool_message else tool_message
if get_runtime_setting("AI_AGENT_VERBOSE") and safe_message:
if self._is_verbose_mode() and safe_message:
return self.emit_tool_message(str(safe_message))
self.record_tool_call(
tool_name=tool_name,

View File

@@ -43,6 +43,11 @@ class _WebAgentStreamingHandlerMixin:
"""更新当前 Web 请求的结构化工具生命周期回调。"""
self._on_tool_event = on_tool_event
def _uses_structured_tool_events(self) -> bool:
"""仅在啰嗦模式下使用 WebAgent 的逐条工具生命周期事件。"""
# ``StreamingHandler`` 是运行时组合进当前 MRO 的基础实现。
return bool(self._on_tool_event and super()._is_verbose_mode()) # type: ignore[misc]
def _publish_tool_event(self, event: dict[str, Any]) -> None:
"""向 Web SSE 发布结构化工具生命周期事件,并隔离回调异常。"""
if not self._on_tool_event:
@@ -58,7 +63,7 @@ class _WebAgentStreamingHandlerMixin:
tool_message: Optional[str] = None,
) -> str:
"""发布真实工具开始事件,调用方随后用同一 ID 收口结果。"""
if not self._on_tool_event:
if not self._uses_structured_tool_events():
return ""
tool_id = f"tool-{uuid.uuid4().hex}"
self._publish_tool_event(
@@ -92,7 +97,7 @@ class _WebAgentStreamingHandlerMixin:
tool_kwargs: Optional[dict[str, Any]] = None,
) -> str:
"""为没有 MoviePilotTool 包装层的内部工具建立完整生命周期。"""
if self._on_tool_event:
if self._uses_structured_tool_events():
del tool_kwargs
return self.tool_call_started(tool_name, tool_message)
return str(super().report_tool_call( # type: ignore[misc]
@@ -107,8 +112,8 @@ class _WebAgentStreamingHandlerMixin:
tool_message: Optional[str] = None,
tool_kwargs: Optional[dict[str, Any]] = None,
) -> None:
"""记录 Web 工具调用;结构化生命周期模式下不再生成重复文本摘要。"""
if self._on_tool_event:
"""记录 Web 工具调用;详细模式使用生命周期事件,其他模式保留计数汇总。"""
if self._uses_structured_tool_events():
return
# 该方法由运行时组合进 MRO 的 StreamingHandler 提供。
super().record_tool_call( # type: ignore[misc]
@@ -116,7 +121,10 @@ class _WebAgentStreamingHandlerMixin:
tool_message=tool_message,
tool_kwargs=tool_kwargs,
)
self.flush_pending_tool_summary()
# 结构化回调存在但未启用详细模式时,等下一段正文或流结束后一次性输出
# 多种工具的计数,避免每个调用各自生成一条摘要。
if not self._on_tool_event:
self.flush_pending_tool_summary()
def emit_tool_message(self, message: str) -> str:
"""将工具摘要压成一行,防止多行参数被 SSE 文本解析器拆入正文。
@@ -138,7 +146,7 @@ class _WebAgentStreamingHandlerMixin:
def flush_pending_tool_summary(self) -> str:
"""输出延迟聚合的工具摘要。"""
if self._on_tool_event:
if self._uses_structured_tool_events():
return ""
# 该方法由运行时组合进 MRO 的 StreamingHandler 提供。
emitted = super().flush_pending_tool_summary() # type: ignore[misc]

View File

@@ -554,14 +554,15 @@ class TestAgentToolStreaming:
user_id="1",
username="admin",
)
tool_id = handler.tool_call_started("search", "查询媒体")
handler.emit_tool_message("查询媒体")
handler.record_tool_call(
tool_name="search",
tool_message="查询媒体",
tool_kwargs={"query": "MoviePilot"},
)
handler.tool_call_finished(tool_id, "done")
with patch("app.agent.callback.get_runtime_setting", return_value=True):
tool_id = handler.tool_call_started("search", "查询媒体")
handler.emit_tool_message("查询媒体")
handler.record_tool_call(
tool_name="search",
tool_message="查询媒体",
tool_kwargs={"query": "MoviePilot"},
)
handler.tool_call_finished(tool_id, "done")
return tool_id, emitted, tool_events
tool_id, emitted, tool_events = asyncio.run(_run())
@@ -579,6 +580,70 @@ class TestAgentToolStreaming:
{"type": "tool", "status": "done", "tool_id": tool_id},
]
def test_web_streaming_handler_aggregates_tools_when_not_verbose(self):
"""WebAgent 关闭啰嗦模式时应隐藏逐条事件并汇总各类工具次数。"""
async def _run():
emitted = []
tool_events = []
handler = _get_web_agent_streaming_handler_type()(
emitted.append,
tool_events.append,
)
await handler.start_streaming(
channel=NotificationChannel.WebAgent.value,
source="web-agent",
user_id="1",
username="admin",
)
with patch("app.agent.callback.get_runtime_setting", return_value=False):
assert handler.tool_call_started("search", "查询媒体") == ""
handler.report_tool_call(
tool_name="search_web",
tool_message="搜索网络内容",
tool_kwargs={"query": "MoviePilot"},
)
handler.report_tool_call(
tool_name="read_file",
tool_message="读取文件",
tool_kwargs={"file_path": "/tmp/app.py"},
)
handler.emit("查询完成")
return emitted, tool_events
emitted, tool_events = asyncio.run(_run())
assert tool_events == []
assert emitted == ["(执行了 1 次搜索,读取了 1 个文件)\n\n查询完成"]
def test_web_streaming_handler_aggregates_normal_tool_when_not_verbose(self):
"""普通工具在 WebAgent 非啰嗦模式下也应走计数汇总路径。"""
async def _run():
emitted = []
tool_events = []
handler = _get_web_agent_streaming_handler_type()(
emitted.append,
tool_events.append,
)
await handler.start_streaming(
channel=NotificationChannel.WebAgent.value,
source="web-agent",
user_id="1",
username="admin",
)
tool = DummyTool(session_id="session-1", user_id="1")
tool.set_stream_handler(handler)
with patch.object(settings, "AI_AGENT_VERBOSE", False):
await tool._arun()
handler.emit("查询完成")
return emitted, tool_events
emitted, tool_events = asyncio.run(_run())
assert tool_events == []
assert emitted == ["(调用了 1 次工具)\n\n查询完成"]
def test_rich_message_keeps_body_text_unquoted_for_telegram(self):
"""校验 Telegram 富文本只转换工具摘要行,正文保持原样。"""
handler = StreamingHandler()