mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-30 20:05:42 +08:00
fix(agent): honor verbose mode in WebAgent tool display
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user