mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-06-12 03:01:45 +08:00
feat(agent): add ToolTag-based tags to all agent tools; implement tags.py for unified tool capability tagging
This commit is contained in:
@@ -317,11 +317,13 @@ class AgentBackgroundOutputTest(unittest.IsolatedAsyncioTestCase):
|
||||
user_id="system",
|
||||
)
|
||||
agent._initialize_tools = lambda: []
|
||||
agent._initialize_subagent_tools = lambda: []
|
||||
|
||||
with (
|
||||
patch.object(settings, "LLM_MAX_TOOLS", 0),
|
||||
patch.object(agent, "_initialize_llm", new=AsyncMock(return_value=object())),
|
||||
patch("app.agent.prompt_manager.get_agent_prompt", return_value="PROMPT"),
|
||||
patch("app.agent.create_subagent_middlewares", return_value=([], [])),
|
||||
patch(
|
||||
"app.agent.MoviePilotToolFactory.get_tool_selector_always_include_names",
|
||||
return_value=[],
|
||||
@@ -356,11 +358,13 @@ class AgentBackgroundOutputTest(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_create_agent_keeps_activity_log_for_normal_session(self):
|
||||
agent = MoviePilotAgent(session_id="normal-session", user_id="system")
|
||||
agent._initialize_tools = lambda: []
|
||||
agent._initialize_subagent_tools = lambda: []
|
||||
|
||||
with (
|
||||
patch.object(settings, "LLM_MAX_TOOLS", 0),
|
||||
patch.object(agent, "_initialize_llm", new=AsyncMock(return_value=object())),
|
||||
patch("app.agent.prompt_manager.get_agent_prompt", return_value="PROMPT"),
|
||||
patch("app.agent.create_subagent_middlewares", return_value=([], [])),
|
||||
patch(
|
||||
"app.agent.MoviePilotToolFactory.get_tool_selector_always_include_names",
|
||||
return_value=[],
|
||||
|
||||
87
tests/test_agent_subagents.py
Normal file
87
tests/test_agent_subagents.py
Normal file
@@ -0,0 +1,87 @@
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from langchain_core.language_models.fake_chat_models import FakeListChatModel
|
||||
|
||||
import app.agent.middleware.subagents as subagent_module
|
||||
from app.agent.middleware.subagents import (
|
||||
MoviePilotSubAgentMiddleware,
|
||||
SUBAGENT_TASK_TOOL_NAME,
|
||||
create_subagent_middlewares,
|
||||
)
|
||||
from app.agent.tools.tags import ToolTag
|
||||
|
||||
|
||||
class TestAgentSubagents(unittest.TestCase):
|
||||
def test_create_subagent_middlewares_registers_task_tool(self):
|
||||
"""子代理中间件应向主 Agent 注册 task 委派工具。"""
|
||||
model = FakeListChatModel(responses=["ok"])
|
||||
|
||||
middlewares, task_tools = create_subagent_middlewares(
|
||||
model=model,
|
||||
tools=[],
|
||||
stream_handler=None,
|
||||
)
|
||||
|
||||
self.assertEqual(len(middlewares), 2)
|
||||
self.assertEqual([tool.name for tool in task_tools], [SUBAGENT_TASK_TOOL_NAME])
|
||||
self.assertIn("media-researcher", task_tools[0].description)
|
||||
self.assertIn("system-diagnostician", task_tools[0].description)
|
||||
|
||||
def test_subagent_tools_are_selected_by_tags(self):
|
||||
"""子代理应根据工具标签筛选工具,而不是依赖工具名名单。"""
|
||||
model = FakeListChatModel(responses=["ok"])
|
||||
tools = [
|
||||
SimpleNamespace(
|
||||
name="custom_media_lookup",
|
||||
tags=[ToolTag.Read.value, ToolTag.Media.value],
|
||||
),
|
||||
SimpleNamespace(
|
||||
name="custom_media_writer",
|
||||
tags=[ToolTag.Read.value, ToolTag.Write.value, ToolTag.Media.value],
|
||||
),
|
||||
SimpleNamespace(
|
||||
name="custom_site_lookup",
|
||||
tags=[ToolTag.Read.value, ToolTag.Site.value],
|
||||
),
|
||||
]
|
||||
captured = {}
|
||||
|
||||
def _fake_create_agent(**kwargs):
|
||||
captured.update(kwargs)
|
||||
return kwargs
|
||||
|
||||
middleware = MoviePilotSubAgentMiddleware(
|
||||
model=model,
|
||||
profiles=subagent_module._builtin_subagent_profiles(),
|
||||
tools=tools,
|
||||
)
|
||||
|
||||
with patch.object(subagent_module, "create_agent", side_effect=_fake_create_agent):
|
||||
middleware._get_agent("media-researcher")
|
||||
|
||||
self.assertEqual(
|
||||
[tool.name for tool in captured["tools"]],
|
||||
["custom_media_lookup"],
|
||||
)
|
||||
|
||||
def test_builtin_tools_declare_tags_in_implementation(self):
|
||||
"""所有内置工具实现都应显式声明 tags。"""
|
||||
impl_dir = Path(__file__).resolve().parents[1] / "app" / "agent" / "tools" / "impl"
|
||||
missing_tools = []
|
||||
for path in sorted(impl_dir.glob("*.py")):
|
||||
text = path.read_text()
|
||||
for block in text.split("\nclass "):
|
||||
if "(MoviePilotTool)" not in block:
|
||||
continue
|
||||
class_name = block.split("(", 1)[0].strip()
|
||||
if "tags: list[str]" not in block:
|
||||
missing_tools.append(f"{path.name}:{class_name}")
|
||||
|
||||
self.assertEqual([], missing_tools)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -35,6 +35,9 @@ class TestAgentSummarizationStreaming(unittest.TestCase):
|
||||
patch.object(
|
||||
agent_module.prompt_manager, "get_agent_prompt", return_value="prompt"
|
||||
),
|
||||
patch.object(
|
||||
agent_module, "create_subagent_middlewares", return_value=([], [])
|
||||
),
|
||||
patch.object(agent_module, "create_agent", side_effect=_fake_create_agent),
|
||||
patch.object(agent_module.settings, "LLM_MAX_TOOLS", 0),
|
||||
):
|
||||
@@ -93,6 +96,9 @@ class TestAgentSummarizationStreaming(unittest.TestCase):
|
||||
patch.object(
|
||||
agent_module.prompt_manager, "get_agent_prompt", return_value="prompt"
|
||||
),
|
||||
patch.object(
|
||||
agent_module, "create_subagent_middlewares", return_value=([], [])
|
||||
),
|
||||
patch.object(
|
||||
agent_module,
|
||||
"ToolSelectorMiddleware",
|
||||
@@ -138,6 +144,9 @@ class TestAgentSummarizationStreaming(unittest.TestCase):
|
||||
patch.object(
|
||||
agent_module.prompt_manager, "get_agent_prompt", return_value="prompt"
|
||||
),
|
||||
patch.object(
|
||||
agent_module, "create_subagent_middlewares", return_value=([], [])
|
||||
),
|
||||
patch.object(agent_module, "create_agent", side_effect=_fake_create_agent),
|
||||
patch.object(agent_module.settings, "LLM_MAX_TOOLS", 0),
|
||||
):
|
||||
@@ -167,6 +176,9 @@ class TestAgentSummarizationStreaming(unittest.TestCase):
|
||||
patch.object(
|
||||
agent_module.prompt_manager, "get_agent_prompt", return_value="prompt"
|
||||
),
|
||||
patch.object(
|
||||
agent_module, "create_subagent_middlewares", return_value=([], [])
|
||||
),
|
||||
patch.object(agent_module, "create_agent", side_effect=_fake_create_agent),
|
||||
patch.object(agent_module.settings, "LLM_MAX_TOOLS", 0),
|
||||
):
|
||||
|
||||
@@ -9,6 +9,7 @@ if not hasattr(langchain_agents, "create_agent"):
|
||||
langchain_agents.create_agent = lambda *args, **kwargs: None
|
||||
|
||||
from app.agent.callback import StreamingHandler
|
||||
from app.agent.middleware.subagents import is_subagent_stream_metadata
|
||||
from app.agent.tools.base import MoviePilotTool
|
||||
from app.agent.tools.impl.send_voice_message import SendVoiceMessageTool
|
||||
from app.api.endpoints.openai import _OpenAIStreamingHandler
|
||||
@@ -114,6 +115,36 @@ class TestAgentToolStreaming(unittest.TestCase):
|
||||
"处理中:\n\n(执行了 2 次搜索,读取了 2 个文件)\n\n继续分析",
|
||||
)
|
||||
|
||||
def test_non_verbose_tool_summary_counts_subagents(self):
|
||||
async def _run():
|
||||
handler = StreamingHandler()
|
||||
await handler.start_streaming()
|
||||
handler.emit("处理中:")
|
||||
handler.record_tool_call(
|
||||
tool_name="task",
|
||||
tool_message="Subagent invoked",
|
||||
tool_kwargs={"subagent_type": "media-researcher"},
|
||||
)
|
||||
handler.record_tool_call(
|
||||
tool_name="task",
|
||||
tool_message="Subagent invoked",
|
||||
tool_kwargs={"subagent_type": "resource-searcher"},
|
||||
)
|
||||
return await handler.take()
|
||||
|
||||
buffered_message = asyncio.run(_run())
|
||||
|
||||
self.assertEqual(buffered_message, "处理中:\n\n(已调用 2 个子代理)\n\n")
|
||||
|
||||
def test_subagent_stream_metadata_is_suppressed(self):
|
||||
self.assertTrue(
|
||||
is_subagent_stream_metadata(
|
||||
{"metadata": {"ls_agent_type": "subagent"}}
|
||||
)
|
||||
)
|
||||
self.assertTrue(is_subagent_stream_metadata({"lc_agent_name": "media-researcher"}))
|
||||
self.assertFalse(is_subagent_stream_metadata({"lc_agent_name": "main"}))
|
||||
|
||||
def test_openai_streaming_handler_flushes_pending_summary_to_queue(self):
|
||||
async def _run():
|
||||
handler = _OpenAIStreamingHandler()
|
||||
|
||||
Reference in New Issue
Block a user