feat(agent): add ToolTag-based tags to all agent tools; implement tags.py for unified tool capability tagging

This commit is contained in:
jxxghp
2026-05-31 18:30:39 +08:00
parent 855681ff35
commit c89df496a5
87 changed files with 1212 additions and 6 deletions
+40 -1
View File
@@ -36,6 +36,11 @@ from app.agent.middleware.memory import MemoryMiddleware
from app.agent.middleware.patch_tool_calls import PatchToolCallsMiddleware from app.agent.middleware.patch_tool_calls import PatchToolCallsMiddleware
from app.agent.middleware.runtime_config import RuntimeConfigMiddleware from app.agent.middleware.runtime_config import RuntimeConfigMiddleware
from app.agent.middleware.skills import SkillsMiddleware from app.agent.middleware.skills import SkillsMiddleware
from app.agent.middleware.subagents import (
SUBAGENT_TASK_TOOL_NAME,
create_subagent_middlewares,
is_subagent_stream_metadata,
)
from app.agent.middleware.tool_selection import ToolSelectorMiddleware from app.agent.middleware.tool_selection import ToolSelectorMiddleware
from app.agent.middleware.usage import UsageMiddleware from app.agent.middleware.usage import UsageMiddleware
from app.agent.prompt import prompt_manager from app.agent.prompt import prompt_manager
@@ -774,6 +779,25 @@ class MoviePilotAgent:
allow_message_tools=self.allow_message_tools, allow_message_tools=self.allow_message_tools,
) )
def _initialize_subagent_tools(self) -> List:
"""
初始化子代理专用静默工具列表。
"""
return MoviePilotToolFactory.create_tools(
session_id=self.session_id,
user_id=self.user_id,
channel=self.channel,
source=self.source,
username=self.username,
stream_handler=None,
agent_context={
"user_reply_sent": False,
"reply_mode": None,
"should_dispatch_reply": False,
},
allow_message_tools=False,
)
async def _create_agent(self, streaming: bool = False): async def _create_agent(self, streaming: bool = False):
""" """
创建 LangGraph Agent(使用 create_agent + SummarizationMiddleware 创建 LangGraph Agent(使用 create_agent + SummarizationMiddleware
@@ -796,10 +820,21 @@ class MoviePilotAgent:
# 工具列表 # 工具列表
tools = self._initialize_tools() tools = self._initialize_tools()
subagent_middlewares, subagent_task_tools = create_subagent_middlewares(
model=non_streaming_model,
tools=self._initialize_subagent_tools(),
stream_handler=self.stream_handler,
)
max_tools = settings.LLM_MAX_TOOLS max_tools = settings.LLM_MAX_TOOLS
always_include_tools = ( always_include_tools = (
MoviePilotToolFactory.get_tool_selector_always_include_names(tools) MoviePilotToolFactory.get_tool_selector_always_include_names(tools)
) )
if subagent_task_tools:
always_include_tools.extend(
tool.name
for tool in subagent_task_tools
if getattr(tool, "name", None) == SUBAGENT_TASK_TOOL_NAME
)
# 中间件 # 中间件
middlewares = [ middlewares = [
@@ -822,6 +857,8 @@ class MoviePilotAgent:
), ),
# 错误工具调用修复 # 错误工具调用修复
PatchToolCallsMiddleware(), PatchToolCallsMiddleware(),
# 子代理委派
*subagent_middlewares,
# 用量统计 # 用量统计
UsageMiddleware(on_usage=self._record_usage), UsageMiddleware(on_usage=self._record_usage),
] ]
@@ -839,7 +876,7 @@ class MoviePilotAgent:
middlewares.append( middlewares.append(
ToolSelectorMiddleware( ToolSelectorMiddleware(
model=non_streaming_model, model=non_streaming_model,
selection_tools=tools, selection_tools=[*tools, *subagent_task_tools],
max_tools=max_tools, max_tools=max_tools,
always_include=always_include_tools, always_include=always_include_tools,
) )
@@ -942,6 +979,8 @@ class MoviePilotAgent:
): ):
if chunk["type"] == "messages": if chunk["type"] == "messages":
token, metadata = chunk["data"] token, metadata = chunk["data"]
if is_subagent_stream_metadata(metadata):
continue
if not token or not hasattr(token, "tool_call_chunks"): if not token or not hasattr(token, "tool_call_chunks"):
continue continue
+4
View File
@@ -293,6 +293,8 @@ class StreamingHandler:
tool_message = (tool_message or "").strip() tool_message = (tool_message or "").strip()
tool_message_lower = tool_message.lower() tool_message_lower = tool_message.lower()
if tool_name == "task":
return "subagent", tool_kwargs.get("subagent_type")
if tool_name == "read_file": if tool_name == "read_file":
return "file_read", tool_kwargs.get("file_path") return "file_read", tool_kwargs.get("file_path")
if tool_name in {"write_file", "edit_file"}: if tool_name in {"write_file", "edit_file"}:
@@ -408,6 +410,8 @@ class StreamingHandler:
return f"执行了 {count} 次操作" return f"执行了 {count} 次操作"
if category == "interaction": if category == "interaction":
return f"发起了 {count} 次交互" return f"发起了 {count} 次交互"
if category == "subagent":
return f"已调用 {count} 个子代理"
return f"调用了 {count} 次工具" return f"调用了 {count} 次工具"
def _can_stream(self) -> bool: def _can_stream(self) -> bool:
+516
View File
@@ -0,0 +1,516 @@
"""MoviePilot 子代理中间件适配。"""
import uuid
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from functools import lru_cache
from typing import Any, Optional
from langchain.agents import create_agent
from langchain.agents.middleware.types import (
AgentMiddleware,
ContextT,
ModelRequest,
ModelResponse,
ResponseT,
ToolCallRequest,
)
from langchain_core.language_models.chat_models import BaseChatModel
from langchain_core.messages import AIMessage, HumanMessage
from langchain_core.tools import BaseTool, StructuredTool
from pydantic import BaseModel, Field
from app.agent.middleware.utils import append_to_system_message
from app.agent.tools.tags import ToolTag
from app.log import logger
SUBAGENT_TASK_TOOL_NAME = "task"
SUBAGENT_STREAM_MARKER_KEY = "ls_agent_type"
SUBAGENT_STREAM_MARKER_VALUE = "subagent"
SUBAGENT_PARENT_PROMPT = """<subagents>
You may use the `task` tool to delegate independent research, retrieval,
diagnosis, or planning work to built-in subagents.
Rules:
- Delegate when a task benefits from focused investigation, such as media identity checks, site/resource search, subscription analysis, download/transfer diagnosis, or read-only system inspection.
- Subagent output is private context for your decision-making. Do not expose a subagent's process or final report verbatim to the user.
- Subagents must not send messages to the user, ask for interaction, or reveal their internal tool activity.
- Give the user only your synthesized final answer and the minimum necessary next step.
- If a task requires configuration changes, deletion, adding downloads, adding subscriptions, or any high-impact action, the main agent must handle it directly under the confirmation policy.
</subagents>"""
SUBAGENT_TASK_DESCRIPTION = (
"Delegate an isolated MoviePilot investigation or planning task to a built-in "
"subagent. The subagent result is private context for the main agent and must "
"not be forwarded verbatim to the user."
)
SUBAGENT_BASE_PROMPT = """You are a silent subagent working for the MoviePilot main agent.
Requirements:
- Handle only the delegated subtask from the main agent. Do not converse with the user.
- Do not send messages, request user interaction, or output progress updates.
- Use tool results only for analysis, and return the final result only to the main agent.
- Unless the task explicitly requires it and your tool set permits it, limit yourself to read-only inspection and diagnosis.
- If user confirmation or a high-impact change is needed, explain why the main agent must confirm it instead of executing it yourself.
- Return a concise structured Chinese result with key evidence, judgment, and recommended next step.
"""
@dataclass(frozen=True)
class _SubAgentProfile:
"""内置子代理定义。"""
name: str
description: str
prompt: str
include_tags: frozenset[str]
exclude_tags: frozenset[str]
class _TaskToolInput(BaseModel):
"""子代理任务工具输入。"""
description: str = Field(..., description="Complete task description for the subagent")
subagent_type: str = Field(
default="general-purpose",
description="Subagent type to invoke, such as general-purpose or media-researcher",
)
def is_subagent_stream_metadata(metadata: Any) -> bool:
"""判断流式 token 元数据是否来自子代理。"""
if not isinstance(metadata, dict):
return False
if metadata.get(SUBAGENT_STREAM_MARKER_KEY) == SUBAGENT_STREAM_MARKER_VALUE:
return True
nested_metadata = metadata.get("metadata")
if isinstance(nested_metadata, dict) and nested_metadata.get(
SUBAGENT_STREAM_MARKER_KEY
) == SUBAGENT_STREAM_MARKER_VALUE:
return True
configurable = metadata.get("configurable")
if isinstance(configurable, dict) and configurable.get(
SUBAGENT_STREAM_MARKER_KEY
) == SUBAGENT_STREAM_MARKER_VALUE:
return True
return bool(metadata.get("lc_agent_name") in builtin_subagent_names())
@lru_cache(maxsize=1)
def builtin_subagent_names() -> frozenset[str]:
"""返回内置子代理名称集合。"""
return frozenset(profile.name for profile in _builtin_subagent_profiles())
@lru_cache(maxsize=1)
def _builtin_subagent_profiles() -> tuple[_SubAgentProfile, ...]:
"""构建 MoviePilot 默认内置子代理定义。"""
default_exclude_tags = frozenset(
{
ToolTag.Write.value,
ToolTag.Message.value,
ToolTag.UserInteraction.value,
}
)
general_tags = frozenset(
{
ToolTag.Media.value,
ToolTag.Resource.value,
ToolTag.Site.value,
ToolTag.Subscription.value,
ToolTag.Download.value,
ToolTag.Library.value,
ToolTag.Transfer.value,
ToolTag.System.value,
ToolTag.Settings.value,
ToolTag.Plugin.value,
ToolTag.Workflow.value,
ToolTag.Scheduler.value,
ToolTag.File.value,
ToolTag.Directory.value,
ToolTag.Web.value,
ToolTag.Command.value,
ToolTag.FilterRule.value,
ToolTag.Persona.value,
ToolTag.SlashCommand.value,
ToolTag.Recommendation.value,
ToolTag.Metadata.value,
}
)
return (
_SubAgentProfile(
name="general-purpose",
description="General read-only investigation subagent for cross-domain MoviePilot analysis and execution recommendations.",
prompt=(
f"{SUBAGENT_BASE_PROMPT}\n"
"You specialize in synthesizing media, site, subscription, download, and system status signals."
),
include_tags=general_tags,
exclude_tags=default_exclude_tags,
),
_SubAgentProfile(
name="media-researcher",
description="Media research subagent for title recognition, people, episodes, metadata, and library existence checks.",
prompt=(
f"{SUBAGENT_BASE_PROMPT}\n"
"You specialize in media identity resolution, metadata validation, person credits, and library status analysis."
),
include_tags=frozenset(
{
ToolTag.Media.value,
ToolTag.Library.value,
ToolTag.Recommendation.value,
ToolTag.Metadata.value,
ToolTag.Web.value,
}
),
exclude_tags=default_exclude_tags,
),
_SubAgentProfile(
name="resource-searcher",
description="Site and resource search subagent for site checks, torrent search, and resource quality analysis.",
prompt=(
f"{SUBAGENT_BASE_PROMPT}\n"
"You specialize in site status, site user data, torrent search results, and resource quality judgment."
),
include_tags=frozenset(
{
ToolTag.Resource.value,
ToolTag.Site.value,
ToolTag.Web.value,
ToolTag.Media.value,
}
),
exclude_tags=default_exclude_tags,
),
_SubAgentProfile(
name="subscription-analyst",
description="Subscription analysis subagent for subscriptions, history, filter rules, and custom identifiers.",
prompt=(
f"{SUBAGENT_BASE_PROMPT}\n"
"You specialize in current subscription state, subscription history, filter rules, and subscription optimization suggestions."
),
include_tags=frozenset(
{
ToolTag.Subscription.value,
ToolTag.FilterRule.value,
ToolTag.Settings.value,
ToolTag.Media.value,
}
),
exclude_tags=default_exclude_tags,
),
_SubAgentProfile(
name="system-diagnostician",
description="System diagnosis subagent for read-only inspection of settings, schedulers, workflows, plugins, directories, and command output.",
prompt=(
f"{SUBAGENT_BASE_PROMPT}\n"
"You specialize in settings, plugins, scheduled tasks, workflows, directories, and read-only command diagnostics."
),
include_tags=frozenset(
{
ToolTag.System.value,
ToolTag.Settings.value,
ToolTag.Plugin.value,
ToolTag.Workflow.value,
ToolTag.Scheduler.value,
ToolTag.File.value,
ToolTag.Directory.value,
ToolTag.Web.value,
ToolTag.Command.value,
ToolTag.Persona.value,
ToolTag.SlashCommand.value,
}
),
exclude_tags=default_exclude_tags,
),
_SubAgentProfile(
name="download-diagnostician",
description="Download and transfer diagnosis subagent for downloaders, download tasks, transfer history, and library status.",
prompt=(
f"{SUBAGENT_BASE_PROMPT}\n"
"You specialize in downloaders, download tasks, transfer history, directory settings, and library ingestion state."
),
include_tags=frozenset(
{
ToolTag.Download.value,
ToolTag.Transfer.value,
ToolTag.Library.value,
ToolTag.Directory.value,
ToolTag.File.value,
ToolTag.Media.value,
}
),
exclude_tags=default_exclude_tags,
),
)
def _tool_tag_values(tool: BaseTool) -> set[str]:
"""读取工具实例上的标签集合。"""
tags = getattr(tool, "tags", None) or []
if isinstance(tags, str):
return {tags}
return {str(tag) for tag in tags if tag}
def _select_tools(tools: list[BaseTool], profile: _SubAgentProfile) -> list[BaseTool]:
"""根据工具标签筛选子代理可用工具。"""
selected_tools = []
for tool in tools:
tags = _tool_tag_values(tool)
if ToolTag.Read.value not in tags:
continue
if profile.exclude_tags & tags:
continue
if profile.include_tags & tags:
selected_tools.append(tool)
return selected_tools
def _format_subagent_catalog(profiles: tuple[_SubAgentProfile, ...]) -> str:
"""渲染子代理目录供任务工具描述使用。"""
return "\n".join(
f"- {profile.name}: {profile.description}" for profile in profiles
)
def _extract_text_content(content: Any) -> str:
"""从模型消息内容中提取可读文本。"""
if content is None:
return ""
if isinstance(content, str):
return content
if isinstance(content, list):
text_parts: list[str] = []
for block in content:
if isinstance(block, str):
text_parts.append(block)
continue
if isinstance(block, dict):
if block.get("thought"):
continue
if block.get("type") in {
"thinking",
"reasoning_content",
"reasoning",
"thought",
}:
continue
if isinstance(block.get("text"), str):
text_parts.append(block["text"])
return "".join(text_parts)
return str(content)
def _extract_final_text(result: Any) -> str:
"""从子代理执行结果中提取最后一条 AI 文本。"""
if isinstance(result, dict):
messages = result.get("messages") or []
else:
messages = getattr(result, "messages", []) or []
for message in reversed(messages):
if isinstance(message, AIMessage) and message.content:
text = _extract_text_content(message.content).strip()
if text:
return text
return _extract_text_content(result).strip()
class MoviePilotSubAgentMiddleware(AgentMiddleware):
"""MoviePilot 本地子代理中间件兜底实现。"""
def __init__(
self,
*,
model: BaseChatModel,
profiles: tuple[_SubAgentProfile, ...],
tools: list[BaseTool],
system_prompt: str = SUBAGENT_PARENT_PROMPT,
task_description: str = SUBAGENT_TASK_DESCRIPTION,
) -> None:
self.system_prompt = system_prompt
self._model = model
self._profiles = {profile.name: profile for profile in profiles}
self._tools = tools
self._agents = {}
self._default_agent_name = "general-purpose"
self.tools = [
StructuredTool.from_function(
coroutine=self._run_task,
name=SUBAGENT_TASK_TOOL_NAME,
description=(
f"{task_description}\n\nAvailable subagents:\n"
f"{_format_subagent_catalog(profiles)}"
),
args_schema=_TaskToolInput,
)
]
def _get_agent(self, agent_name: str) -> Any:
"""懒加载指定名称的子代理图。"""
profile = self._profiles.get(agent_name) or self._profiles[
self._default_agent_name
]
cached_agent = self._agents.get(profile.name)
if cached_agent:
return cached_agent
subagent_tools = _select_tools(self._tools, profile)
agent = create_agent(
model=self._model,
tools=subagent_tools,
system_prompt=profile.prompt,
name=profile.name,
)
self._agents[profile.name] = agent
return agent
async def _run_task(self, description: str, subagent_type: str) -> str:
"""调用指定子代理并只返回供主代理读取的结果。"""
agent_name = subagent_type or self._default_agent_name
agent = self._get_agent(agent_name)
result = await agent.ainvoke(
{"messages": [HumanMessage(content=description)]},
config={
"configurable": {
"thread_id": f"subagent-{agent_name}-{uuid.uuid4().hex}",
SUBAGENT_STREAM_MARKER_KEY: SUBAGENT_STREAM_MARKER_VALUE,
},
"metadata": {
"lc_agent_name": agent_name,
SUBAGENT_STREAM_MARKER_KEY: SUBAGENT_STREAM_MARKER_VALUE,
},
},
)
final_text = _extract_final_text(result)
return final_text or "The subagent did not return a usable result."
async def awrap_model_call(
self,
request: ModelRequest[ContextT],
handler: Callable[
[ModelRequest[ContextT]], Awaitable[ModelResponse[ResponseT]]
],
) -> ModelResponse[ResponseT]:
"""在主代理模型调用前注入子代理使用说明。"""
new_system_message = append_to_system_message(
request.system_message,
self.system_prompt,
)
return await handler(request.override(system_message=new_system_message))
class SubAgentCallSummaryMiddleware(AgentMiddleware):
"""记录子代理调用次数的中间件。"""
def __init__(self, *, stream_handler: Any = None) -> None:
self.stream_handler = stream_handler
self.tools = []
async def awrap_tool_call(
self,
request: ToolCallRequest,
handler: Callable[[ToolCallRequest], Awaitable[Any]],
) -> Any:
"""在子代理任务工具执行时记录聚合摘要。"""
tool = request.tool
if (
tool
and getattr(tool, "name", None) == SUBAGENT_TASK_TOOL_NAME
and self.stream_handler
and getattr(self.stream_handler, "is_streaming", False)
):
tool_call = request.tool_call or {}
self.stream_handler.record_tool_call(
tool_name=SUBAGENT_TASK_TOOL_NAME,
tool_message="Subagent invoked",
tool_kwargs=tool_call.get("args") or {},
)
return await handler(request)
def _deepagents_spec(
profiles: tuple[_SubAgentProfile, ...], tools: list[BaseTool]
) -> list[dict[str, Any]]:
"""将内置定义转换为 Deep Agents 子代理配置。"""
specs = []
for profile in profiles:
specs.append(
{
"name": profile.name,
"description": profile.description,
"prompt": profile.prompt,
"tools": _select_tools(tools, profile),
}
)
return specs
def _try_create_deepagents_middleware(
*,
profiles: tuple[_SubAgentProfile, ...],
tools: list[BaseTool],
model: BaseChatModel,
) -> Optional[AgentMiddleware]:
"""优先创建 Deep Agents 官方子代理中间件。"""
try:
from deepagents.backends import StateBackend
from deepagents.middleware.subagents import SubAgentMiddleware
return SubAgentMiddleware(
backend=StateBackend(),
subagents=_deepagents_spec(profiles, tools),
default_model=model,
system_prompt=SUBAGENT_PARENT_PROMPT,
task_description=SUBAGENT_TASK_DESCRIPTION,
)
except ImportError:
return None
except Exception as err:
logger.debug(f"Deep Agents 子代理中间件不可用,使用本地实现: {err}")
return None
def create_subagent_middlewares(
*,
model: BaseChatModel,
tools: list[BaseTool],
stream_handler: Any = None,
) -> tuple[list[AgentMiddleware], list[BaseTool]]:
"""创建子代理中间件列表和任务工具列表。"""
profiles = _builtin_subagent_profiles()
subagent_middleware = _try_create_deepagents_middleware(
profiles=profiles,
tools=tools,
model=model,
)
if subagent_middleware is None:
subagent_middleware = MoviePilotSubAgentMiddleware(
model=model,
profiles=profiles,
tools=tools,
)
task_tools = list(getattr(subagent_middleware, "tools", []) or [])
return [
subagent_middleware,
SubAgentCallSummaryMiddleware(stream_handler=stream_handler),
], task_tools
__all__ = [
"SUBAGENT_TASK_TOOL_NAME",
"create_subagent_middlewares",
"is_subagent_stream_metadata",
]
+22
View File
@@ -10,6 +10,7 @@ from langchain_core.tools import BaseTool
from pydantic import PrivateAttr from pydantic import PrivateAttr
from app.agent import StreamingHandler from app.agent import StreamingHandler
from app.agent.tools.tags import ToolTag
from app.chain import ChainBase from app.chain import ChainBase
from app.core.config import settings from app.core.config import settings
from app.db.user_oper import UserOper from app.db.user_oper import UserOper
@@ -132,6 +133,27 @@ class MoviePilotTool(BaseTool, metaclass=ABCMeta):
self._session_id = session_id self._session_id = session_id
self._user_id = user_id self._user_id = user_id
self._require_admin = getattr(self.__class__, "require_admin", False) self._require_admin = getattr(self.__class__, "require_admin", False)
self.tags = self._build_tool_tags()
@staticmethod
def _normalize_tag_values(tags: Optional[Any]) -> set[str]:
"""规范化 LangChain 工具标签。"""
if not tags:
return set()
if isinstance(tags, (str, ToolTag)):
tags = [tags]
normalized_tags = set()
for tag in tags:
if isinstance(tag, ToolTag):
normalized_tags.add(tag.value)
elif tag:
normalized_tags.add(str(tag))
return normalized_tags
def _build_tool_tags(self) -> list[str]:
"""规范化工具实现中显式声明的标签。"""
explicit_tags = self._normalize_tag_values(getattr(self, "tags", None))
return sorted(explicit_tags | {ToolTag.AgentTool.value})
def _run(self, *args: Any, **kwargs: Any) -> Any: def _run(self, *args: Any, **kwargs: Any) -> Any:
raise NotImplementedError("MoviePilotTool 只支持异步调用,请使用 _arun") raise NotImplementedError("MoviePilotTool 只支持异步调用,请使用 _arun")
@@ -302,7 +302,8 @@ class _TerminalSessionManager:
session.wait_task = asyncio.create_task(self._wait_pipe_process(session)) session.wait_task = asyncio.create_task(self._wait_pipe_process(session))
return session return session
async def _read_pty(self, session: _TerminalSession) -> None: @staticmethod
async def _read_pty(session: _TerminalSession) -> None:
"""持续从 PTY 读取增量输出。""" """持续从 PTY 读取增量输出。"""
while session.master_fd is not None: while session.master_fd is not None:
try: try:
@@ -319,9 +320,9 @@ class _TerminalSessionManager:
break break
session.append_output("pty", data) session.append_output("pty", data)
@staticmethod
async def _read_pipe( async def _read_pipe(
self, session: _TerminalSession,
session: _TerminalSession,
stream: asyncio.StreamReader, stream: asyncio.StreamReader,
stream_name: str, stream_name: str,
) -> None: ) -> None:
@@ -361,7 +362,8 @@ class _TerminalSessionManager:
finally: finally:
await self._finish_reader_tasks(session) await self._finish_reader_tasks(session)
async def _finish_reader_tasks(self, session: _TerminalSession) -> None: @staticmethod
async def _finish_reader_tasks(session: _TerminalSession) -> None:
"""等待输出读取任务退出,超时后取消残留任务。""" """等待输出读取任务退出,超时后取消残留任务。"""
if not session.reader_tasks: if not session.reader_tasks:
return return
@@ -6,6 +6,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.agent.tools.impl._filter_rule_utils import ( from app.agent.tools.impl._filter_rule_utils import (
get_custom_rules, get_custom_rules,
normalize_custom_rule, normalize_custom_rule,
@@ -46,6 +47,11 @@ class AddCustomFilterRuleInput(BaseModel):
class AddCustomFilterRuleTool(MoviePilotTool): class AddCustomFilterRuleTool(MoviePilotTool):
name: str = "add_custom_filter_rule" name: str = "add_custom_filter_rule"
tags: list[str] = [
ToolTag.Write,
ToolTag.FilterRule,
ToolTag.Admin,
]
description: str = ( description: str = (
"Add a custom filter rule to CustomFilterRules. " "Add a custom filter rule to CustomFilterRules. "
"The new rule can then be referenced by rule ID inside filter rule groups." "The new rule can then be referenced by rule ID inside filter rule groups."
+6
View File
@@ -7,6 +7,7 @@ from typing import List, Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.chain.media import MediaChain from app.chain.media import MediaChain
from app.chain.search import SearchChain from app.chain.search import SearchChain
from app.chain.download import DownloadChain from app.chain.download import DownloadChain
@@ -37,6 +38,11 @@ class AddDownloadInput(BaseModel):
class AddDownloadTool(MoviePilotTool): class AddDownloadTool(MoviePilotTool):
name: str = "add_download" name: str = "add_download"
tags: list[str] = [
ToolTag.Write,
ToolTag.Download,
ToolTag.Resource,
]
description: str = "Add torrent download tasks using refs from get_search_results or magnet links." description: str = "Add torrent download tasks using refs from get_search_results or magnet links."
args_schema: Type[BaseModel] = AddDownloadInput args_schema: Type[BaseModel] = AddDownloadInput
+6
View File
@@ -6,6 +6,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.agent.tools.impl._filter_rule_utils import ( from app.agent.tools.impl._filter_rule_utils import (
build_custom_rule_map, build_custom_rule_map,
collect_rule_group_usages, collect_rule_group_usages,
@@ -46,6 +47,11 @@ class AddRuleGroupInput(BaseModel):
class AddRuleGroupTool(MoviePilotTool): class AddRuleGroupTool(MoviePilotTool):
name: str = "add_rule_group" name: str = "add_rule_group"
tags: list[str] = [
ToolTag.Write,
ToolTag.FilterRule,
ToolTag.Admin,
]
description: str = ( description: str = (
"Add a new filter rule group to UserFilterRuleGroups. " "Add a new filter rule group to UserFilterRuleGroups. "
"Rule groups are matched level by level from left to right and can be linked to search/subscription flows. " "Rule groups are matched level by level from left to right and can be linked to search/subscription flows. "
+6
View File
@@ -5,6 +5,7 @@ from typing import List, Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.chain.subscribe import SubscribeChain from app.chain.subscribe import SubscribeChain
from app.db.user_oper import UserOper from app.db.user_oper import UserOper
from app.log import logger from app.log import logger
@@ -72,6 +73,11 @@ class AddSubscribeInput(BaseModel):
class AddSubscribeTool(MoviePilotTool): class AddSubscribeTool(MoviePilotTool):
name: str = "add_subscribe" name: str = "add_subscribe"
tags: list[str] = [
ToolTag.Write,
ToolTag.Subscription,
ToolTag.Media,
]
description: str = ( description: str = (
"Add media subscription to create automated download rules for movies and TV shows. " "Add media subscription to create automated download rules for movies and TV shows. "
"The system will automatically search and download new episodes or releases based on the subscription criteria. " "The system will automatically search and download new episodes or releases based on the subscription criteria. "
+7
View File
@@ -5,6 +5,7 @@ from typing import List, Optional, Type
from pydantic import BaseModel, Field, model_validator from pydantic import BaseModel, Field, model_validator
from app.agent.tools.base import MoviePilotTool, ToolChain from app.agent.tools.base import MoviePilotTool, ToolChain
from app.agent.tools.tags import ToolTag
from app.helper.interaction import ( from app.helper.interaction import (
AgentInteractionOption, AgentInteractionOption,
agent_interaction_manager, agent_interaction_manager,
@@ -67,6 +68,12 @@ class AskUserChoiceTool(MoviePilotTool):
"""发送按钮选择并让当前 Agent 轮次等待用户回调消息。""" """发送按钮选择并让当前 Agent 轮次等待用户回调消息。"""
name: str = "ask_user_choice" name: str = "ask_user_choice"
tags: list[str] = [
ToolTag.Write,
ToolTag.Message,
ToolTag.UserInteraction,
ToolTag.TerminalResponse,
]
sends_message: bool = True sends_message: bool = True
return_direct: bool = True return_direct: bool = True
description: str = ( description: str = (
+5
View File
@@ -9,6 +9,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.core.config import settings from app.core.config import settings
from app.log import logger from app.log import logger
@@ -89,6 +90,10 @@ class BrowseWebpageInput(BaseModel):
class BrowseWebpageTool(MoviePilotTool): class BrowseWebpageTool(MoviePilotTool):
name: str = "browse_webpage" name: str = "browse_webpage"
tags: list[str] = [
ToolTag.Read,
ToolTag.Web,
]
description: str = ( description: str = (
"Control a real browser (Playwright) to interact with web pages. " "Control a real browser (Playwright) to interact with web pages. "
"Supports navigating to URLs, reading page content, taking screenshots, " "Supports navigating to URLs, reading page content, taking screenshots, "
@@ -6,6 +6,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.agent.tools.impl._filter_rule_utils import ( from app.agent.tools.impl._filter_rule_utils import (
collect_custom_rule_group_refs, collect_custom_rule_group_refs,
get_custom_rules, get_custom_rules,
@@ -26,6 +27,11 @@ class DeleteCustomFilterRuleInput(BaseModel):
class DeleteCustomFilterRuleTool(MoviePilotTool): class DeleteCustomFilterRuleTool(MoviePilotTool):
name: str = "delete_custom_filter_rule" name: str = "delete_custom_filter_rule"
tags: list[str] = [
ToolTag.Write,
ToolTag.FilterRule,
ToolTag.Admin,
]
description: str = ( description: str = (
"Delete a custom filter rule from CustomFilterRules. " "Delete a custom filter rule from CustomFilterRules. "
"If the rule is still referenced by rule groups, the deletion is blocked to avoid breaking rule_string expressions." "If the rule is still referenced by rule groups, the deletion is blocked to avoid breaking rule_string expressions."
+6
View File
@@ -5,6 +5,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.chain.download import DownloadChain from app.chain.download import DownloadChain
from app.log import logger from app.log import logger
@@ -29,6 +30,11 @@ class DeleteDownloadInput(BaseModel):
class DeleteDownloadTool(MoviePilotTool): class DeleteDownloadTool(MoviePilotTool):
name: str = "delete_download" name: str = "delete_download"
tags: list[str] = [
ToolTag.Write,
ToolTag.Download,
ToolTag.Admin,
]
description: str = "Delete a download task from the downloader by task hash only. Optionally specify the downloader name and whether to delete downloaded files." description: str = "Delete a download task from the downloader by task hash only. Optionally specify the downloader name and whether to delete downloaded files."
args_schema: Type[BaseModel] = DeleteDownloadInput args_schema: Type[BaseModel] = DeleteDownloadInput
require_admin: bool = True require_admin: bool = True
@@ -5,6 +5,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.db import AsyncSessionFactory from app.db import AsyncSessionFactory
from app.db.models.downloadhistory import DownloadHistory from app.db.models.downloadhistory import DownloadHistory
from app.log import logger from app.log import logger
@@ -22,6 +23,11 @@ class DeleteDownloadHistoryInput(BaseModel):
class DeleteDownloadHistoryTool(MoviePilotTool): class DeleteDownloadHistoryTool(MoviePilotTool):
name: str = "delete_download_history" name: str = "delete_download_history"
tags: list[str] = [
ToolTag.Write,
ToolTag.Download,
ToolTag.Admin,
]
description: str = "Delete a download history record by ID. This only removes the record from the database, does not delete any actual files." description: str = "Delete a download history record by ID. This only removes the record from the database, does not delete any actual files."
args_schema: Type[BaseModel] = DeleteDownloadHistoryInput args_schema: Type[BaseModel] = DeleteDownloadHistoryInput
require_admin: bool = True require_admin: bool = True
@@ -6,6 +6,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.agent.tools.impl._filter_rule_utils import ( from app.agent.tools.impl._filter_rule_utils import (
get_rule_groups, get_rule_groups,
remove_rule_group_references, remove_rule_group_references,
@@ -25,6 +26,11 @@ class DeleteRuleGroupInput(BaseModel):
class DeleteRuleGroupTool(MoviePilotTool): class DeleteRuleGroupTool(MoviePilotTool):
name: str = "delete_rule_group" name: str = "delete_rule_group"
tags: list[str] = [
ToolTag.Write,
ToolTag.FilterRule,
ToolTag.Admin,
]
description: str = ( description: str = (
"Delete a filter rule group from UserFilterRuleGroups. " "Delete a filter rule group from UserFilterRuleGroups. "
"The tool also removes dangling references from global settings and subscriptions." "The tool also removes dangling references from global settings and subscriptions."
+6
View File
@@ -5,6 +5,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.core.event import eventmanager from app.core.event import eventmanager
from app.db.subscribe_oper import SubscribeOper from app.db.subscribe_oper import SubscribeOper
from app.helper.server import MoviePilotServerHelper from app.helper.server import MoviePilotServerHelper
@@ -25,6 +26,11 @@ class DeleteSubscribeInput(BaseModel):
class DeleteSubscribeTool(MoviePilotTool): class DeleteSubscribeTool(MoviePilotTool):
name: str = "delete_subscribe" name: str = "delete_subscribe"
tags: list[str] = [
ToolTag.Write,
ToolTag.Subscription,
ToolTag.Admin,
]
description: str = "Delete a media subscription by its ID. This will remove the subscription and stop automatic downloads for that media." description: str = "Delete a media subscription by its ID. This will remove the subscription and stop automatic downloads for that media."
args_schema: Type[BaseModel] = DeleteSubscribeInput args_schema: Type[BaseModel] = DeleteSubscribeInput
require_admin: bool = True require_admin: bool = True
@@ -5,6 +5,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.db.transferhistory_oper import TransferHistoryOper from app.db.transferhistory_oper import TransferHistoryOper
from app.log import logger from app.log import logger
@@ -21,6 +22,11 @@ class DeleteTransferHistoryInput(BaseModel):
class DeleteTransferHistoryTool(MoviePilotTool): class DeleteTransferHistoryTool(MoviePilotTool):
name: str = "delete_transfer_history" name: str = "delete_transfer_history"
tags: list[str] = [
ToolTag.Write,
ToolTag.Transfer,
ToolTag.Admin,
]
description: str = "Delete a specific transfer history record by its ID. This is useful when you need to remove a failed transfer record before retrying the transfer, as the system skips files that already have transfer history." description: str = "Delete a specific transfer history record by its ID. This is useful when you need to remove a failed transfer record before retrying the transfer, as the system skips files that already have transfer history."
args_schema: Type[BaseModel] = DeleteTransferHistoryInput args_schema: Type[BaseModel] = DeleteTransferHistoryInput
require_admin: bool = True require_admin: bool = True
+6
View File
@@ -7,6 +7,7 @@ from anyio import Path as AsyncPath
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.log import logger from app.log import logger
@@ -20,6 +21,11 @@ class EditFileInput(BaseModel):
class EditFileTool(MoviePilotTool): class EditFileTool(MoviePilotTool):
name: str = "edit_file" name: str = "edit_file"
tags: list[str] = [
ToolTag.Write,
ToolTag.File,
ToolTag.Admin,
]
description: str = "Edit a file by replacing specific old text with new text. Useful for modifying configuration files, code, or scripts." description: str = "Edit a file by replacing specific old text with new text. Useful for modifying configuration files, code, or scripts."
args_schema: Type[BaseModel] = EditFileInput args_schema: Type[BaseModel] = EditFileInput
require_admin: bool = True require_admin: bool = True
+7 -1
View File
@@ -14,7 +14,8 @@ from typing import Any, Literal, Optional, TextIO, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.impl.terminal_session import ( from app.agent.tools.tags import ToolTag
from app.agent.tools.impl._terminal_session import (
TERMINAL_DEFAULT_READ_BYTES, TERMINAL_DEFAULT_READ_BYTES,
TERMINAL_MAX_READ_BYTES, TERMINAL_MAX_READ_BYTES,
TERMINAL_WAIT_DEFAULT_MS, TERMINAL_WAIT_DEFAULT_MS,
@@ -200,6 +201,11 @@ class ExecuteCommandTool(MoviePilotTool):
"""统一执行和管理 Shell 命令的 Agent 工具。""" """统一执行和管理 Shell 命令的 Agent 工具。"""
name: str = "execute_command" name: str = "execute_command"
tags: list[str] = [
ToolTag.Read,
ToolTag.Command,
ToolTag.Admin,
]
description: str = ( description: str = (
"Start and manage shell commands on the server. By default action=start " "Start and manage shell commands on the server. By default action=start "
"launches a background session and immediately returns session_id/status/" "launches a background session and immediately returns session_id/status/"
@@ -6,6 +6,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.chain.recommend import RecommendChain from app.chain.recommend import RecommendChain
from app.log import logger from app.log import logger
from app.schemas.types import MediaType, media_type_to_agent from app.schemas.types import MediaType, media_type_to_agent
@@ -44,6 +45,11 @@ class GetRecommendationsInput(BaseModel):
class GetRecommendationsTool(MoviePilotTool): class GetRecommendationsTool(MoviePilotTool):
name: str = "get_recommendations" name: str = "get_recommendations"
tags: list[str] = [
ToolTag.Read,
ToolTag.Media,
ToolTag.Recommendation,
]
description: str = "Get trending and popular media recommendations from various sources. Returns curated lists of popular movies, TV shows, and anime based on different criteria like trending, ratings, or calendar schedules. Supports pagination with 20 items per page." description: str = "Get trending and popular media recommendations from various sources. Returns curated lists of popular movies, TV shows, and anime based on different criteria like trending, ratings, or calendar schedules. Supports pagination with 20 items per page."
args_schema: Type[BaseModel] = GetRecommendationsInput args_schema: Type[BaseModel] = GetRecommendationsInput
@@ -7,6 +7,7 @@ from typing import List, Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.chain.search import SearchChain from app.chain.search import SearchChain
from app.log import logger from app.log import logger
from ._torrent_search_utils import ( from ._torrent_search_utils import (
@@ -47,6 +48,10 @@ class GetSearchResultsInput(BaseModel):
class GetSearchResultsTool(MoviePilotTool): class GetSearchResultsTool(MoviePilotTool):
name: str = "get_search_results" name: str = "get_search_results"
tags: list[str] = [
ToolTag.Read,
ToolTag.Resource,
]
description: str = "Get cached torrent search results from search_torrents with optional filters. Supports pagination with up to 50 results per page." description: str = "Get cached torrent search results from search_torrents with optional filters. Supports pagination with up to 50 results per page."
args_schema: Type[BaseModel] = GetSearchResultsInput args_schema: Type[BaseModel] = GetSearchResultsInput
+6
View File
@@ -6,6 +6,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.agent.tools.impl._plugin_tool_utils import ( from app.agent.tools.impl._plugin_tool_utils import (
get_plugin_snapshot, get_plugin_snapshot,
install_plugin_runtime, install_plugin_runtime,
@@ -36,6 +37,11 @@ class InstallPluginInput(BaseModel):
class InstallPluginTool(MoviePilotTool): class InstallPluginTool(MoviePilotTool):
name: str = "install_plugin" name: str = "install_plugin"
tags: list[str] = [
ToolTag.Write,
ToolTag.Plugin,
ToolTag.Admin,
]
description: str = ( description: str = (
"Install a plugin by exact plugin_id from the plugin market or local plugin repositories. " "Install a plugin by exact plugin_id from the plugin market or local plugin repositories. "
"Use query_market_plugins first when you need filtering or discovery." "Use query_market_plugins first when you need filtering or discovery."
+6
View File
@@ -8,6 +8,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.chain.storage import StorageChain from app.chain.storage import StorageChain
from app.log import logger from app.log import logger
from app.schemas.file import FileItem from app.schemas.file import FileItem
@@ -24,6 +25,11 @@ class ListDirectoryInput(BaseModel):
class ListDirectoryTool(MoviePilotTool): class ListDirectoryTool(MoviePilotTool):
name: str = "list_directory" name: str = "list_directory"
tags: list[str] = [
ToolTag.Read,
ToolTag.Directory,
ToolTag.File,
]
description: str = "List actual files and folders in a file system directory (NOT configuration). Shows files and subdirectories with their names, types, sizes, and modification times. Returns up to 20 items and the total count if there are more items. Use 'query_directory_settings' to query directory configuration settings." description: str = "List actual files and folders in a file system directory (NOT configuration). Shows files and subdirectories with their names, types, sizes, and modification times. Returns up to 20 items and the total count if there are more items. Use 'query_directory_settings' to query directory configuration settings."
args_schema: Type[BaseModel] = ListDirectoryInput args_schema: Type[BaseModel] = ListDirectoryInput
@@ -6,6 +6,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.log import logger from app.log import logger
@@ -18,6 +19,11 @@ class ListSlashCommandsInput(BaseModel):
class ListSlashCommandsTool(MoviePilotTool): class ListSlashCommandsTool(MoviePilotTool):
name: str = "list_slash_commands" name: str = "list_slash_commands"
tags: list[str] = [
ToolTag.Read,
ToolTag.SlashCommand,
ToolTag.Admin,
]
description: str = ( description: str = (
"List all available slash commands in the system, including system preset commands " "List all available slash commands in the system, including system preset commands "
"(e.g. /cookiecloud, /sites, /subscribes, /downloading, /transfer, /restart, etc.) " "(e.g. /cookiecloud, /sites, /subscribes, /downloading, /transfer, /restart, etc.) "
+6
View File
@@ -5,6 +5,7 @@ from typing import Optional, Type, List
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.chain.download import DownloadChain from app.chain.download import DownloadChain
from app.log import logger from app.log import logger
@@ -37,6 +38,11 @@ class ModifyDownloadTool(MoviePilotTool):
"""修改下载任务工具""" """修改下载任务工具"""
name: str = "modify_download" name: str = "modify_download"
tags: list[str] = [
ToolTag.Write,
ToolTag.Download,
ToolTag.Admin,
]
description: str = ( description: str = (
"Modify a download task in the downloader by task hash. " "Modify a download task in the downloader by task hash. "
"Supports: 1) Setting tags on a download task, " "Supports: 1) Setting tags on a download task, "
@@ -6,6 +6,7 @@ from typing import Optional, Type, List
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.agent.tools.impl._filter_rule_utils import ( from app.agent.tools.impl._filter_rule_utils import (
get_builtin_rules, get_builtin_rules,
serialize_builtin_rule, serialize_builtin_rule,
@@ -27,6 +28,10 @@ class QueryBuiltinFilterRulesInput(BaseModel):
class QueryBuiltinFilterRulesTool(MoviePilotTool): class QueryBuiltinFilterRulesTool(MoviePilotTool):
name: str = "query_builtin_filter_rules" name: str = "query_builtin_filter_rules"
tags: list[str] = [
ToolTag.Read,
ToolTag.FilterRule,
]
description: str = ( description: str = (
"Query built-in filter rules defined by the backend filter module. " "Query built-in filter rules defined by the backend filter module. "
"These rule IDs can be used directly inside rule_string expressions for filter rule groups. " "These rule IDs can be used directly inside rule_string expressions for filter rule groups. "
@@ -6,6 +6,7 @@ from typing import Optional, Type, List
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.agent.tools.impl._filter_rule_utils import ( from app.agent.tools.impl._filter_rule_utils import (
collect_custom_rule_group_refs, collect_custom_rule_group_refs,
get_custom_rules, get_custom_rules,
@@ -32,6 +33,10 @@ class QueryCustomFilterRulesInput(BaseModel):
class QueryCustomFilterRulesTool(MoviePilotTool): class QueryCustomFilterRulesTool(MoviePilotTool):
name: str = "query_custom_filter_rules" name: str = "query_custom_filter_rules"
tags: list[str] = [
ToolTag.Read,
ToolTag.FilterRule,
]
description: str = ( description: str = (
"Query custom filter rules stored in CustomFilterRules. " "Query custom filter rules stored in CustomFilterRules. "
"Custom rules can be referenced from rule_string expressions in filter rule groups. " "Custom rules can be referenced from rule_string expressions in filter rule groups. "
@@ -6,6 +6,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.db.systemconfig_oper import SystemConfigOper from app.db.systemconfig_oper import SystemConfigOper
from app.log import logger from app.log import logger
from app.schemas.types import SystemConfigKey from app.schemas.types import SystemConfigKey
@@ -20,6 +21,11 @@ class QueryCustomIdentifiersInput(BaseModel):
class QueryCustomIdentifiersTool(MoviePilotTool): class QueryCustomIdentifiersTool(MoviePilotTool):
name: str = "query_custom_identifiers" name: str = "query_custom_identifiers"
tags: list[str] = [
ToolTag.Read,
ToolTag.FilterRule,
ToolTag.Admin,
]
description: str = ( description: str = (
"Query all currently configured custom identifiers (自定义识别词). " "Query all currently configured custom identifiers (自定义识别词). "
"Returns the list of identifier rules used for preprocessing torrent/file names before media recognition. " "Returns the list of identifier rules used for preprocessing torrent/file names before media recognition. "
@@ -6,6 +6,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.helper.directory import DirectoryHelper from app.helper.directory import DirectoryHelper
from app.log import logger from app.log import logger
@@ -23,6 +24,12 @@ class QueryDirectorySettingsInput(BaseModel):
class QueryDirectorySettingsTool(MoviePilotTool): class QueryDirectorySettingsTool(MoviePilotTool):
name: str = "query_directory_settings" name: str = "query_directory_settings"
tags: list[str] = [
ToolTag.Read,
ToolTag.Directory,
ToolTag.Settings,
ToolTag.Admin,
]
description: str = "Query system directory configuration settings (NOT file listings). Returns configured directory paths, storage types, transfer modes, and other directory-related settings. Use 'list_directory' to list actual files and folders in a directory." description: str = "Query system directory configuration settings (NOT file listings). Returns configured directory paths, storage types, transfer modes, and other directory-related settings. Use 'list_directory' to list actual files and folders in a directory."
require_admin: bool = True require_admin: bool = True
args_schema: Type[BaseModel] = QueryDirectorySettingsInput args_schema: Type[BaseModel] = QueryDirectorySettingsInput
@@ -6,6 +6,7 @@ from typing import Any, Dict, List, Optional, Type, Union
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.chain.download import DownloadChain from app.chain.download import DownloadChain
from app.db.downloadhistory_oper import DownloadHistoryOper from app.db.downloadhistory_oper import DownloadHistoryOper
from app.log import logger from app.log import logger
@@ -27,6 +28,10 @@ class QueryDownloadTasksInput(BaseModel):
class QueryDownloadTasksTool(MoviePilotTool): class QueryDownloadTasksTool(MoviePilotTool):
name: str = "query_download_tasks" name: str = "query_download_tasks"
tags: list[str] = [
ToolTag.Read,
ToolTag.Download,
]
description: str = "Query download status and list download tasks. Can query all active downloads, or search for specific tasks by hash, title, or tag. Shows download progress, completion status, tags, and task details from configured downloaders." description: str = "Query download status and list download tasks. Can query all active downloads, or search for specific tasks by hash, title, or tag. Shows download progress, completion status, tags, and task details from configured downloaders."
args_schema: Type[BaseModel] = QueryDownloadTasksInput args_schema: Type[BaseModel] = QueryDownloadTasksInput
@@ -6,6 +6,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.db.systemconfig_oper import SystemConfigOper from app.db.systemconfig_oper import SystemConfigOper
from app.log import logger from app.log import logger
from app.schemas.types import SystemConfigKey from app.schemas.types import SystemConfigKey
@@ -18,6 +19,11 @@ class QueryDownloadersInput(BaseModel):
class QueryDownloadersTool(MoviePilotTool): class QueryDownloadersTool(MoviePilotTool):
name: str = "query_downloaders" name: str = "query_downloaders"
tags: list[str] = [
ToolTag.Read,
ToolTag.Download,
ToolTag.Admin,
]
description: str = "Query downloader configuration and list all available downloaders. Shows downloader status, connection details, and configuration settings." description: str = "Query downloader configuration and list all available downloaders. Shows downloader status, connection details, and configuration settings."
require_admin: bool = True require_admin: bool = True
args_schema: Type[BaseModel] = QueryDownloadersInput args_schema: Type[BaseModel] = QueryDownloadersInput
@@ -6,6 +6,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.chain.tmdb import TmdbChain from app.chain.tmdb import TmdbChain
from app.log import logger from app.log import logger
@@ -20,6 +21,10 @@ class QueryEpisodeScheduleInput(BaseModel):
class QueryEpisodeScheduleTool(MoviePilotTool): class QueryEpisodeScheduleTool(MoviePilotTool):
name: str = "query_episode_schedule" name: str = "query_episode_schedule"
tags: list[str] = [
ToolTag.Read,
ToolTag.Media,
]
description: str = "Query TV series episode air dates and schedule. Returns non-duplicated schedule fields, including episode list, air-date statistics, and per-episode metadata. Filters out episodes without air dates." description: str = "Query TV series episode air dates and schedule. Returns non-duplicated schedule fields, including episode list, air-date statistics, and per-episode metadata. Filters out episodes without air dates."
args_schema: Type[BaseModel] = QueryEpisodeScheduleInput args_schema: Type[BaseModel] = QueryEpisodeScheduleInput
@@ -6,6 +6,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.agent.tools.impl._plugin_tool_utils import ( from app.agent.tools.impl._plugin_tool_utils import (
DEFAULT_PLUGIN_CANDIDATE_LIMIT, DEFAULT_PLUGIN_CANDIDATE_LIMIT,
MAX_PLUGIN_CANDIDATE_LIMIT, MAX_PLUGIN_CANDIDATE_LIMIT,
@@ -34,6 +35,11 @@ class QueryInstalledPluginsInput(BaseModel):
class QueryInstalledPluginsTool(MoviePilotTool): class QueryInstalledPluginsTool(MoviePilotTool):
name: str = "query_installed_plugins" name: str = "query_installed_plugins"
tags: list[str] = [
ToolTag.Read,
ToolTag.Plugin,
ToolTag.Admin,
]
description: str = ( description: str = (
"Query installed plugins in MoviePilot. Returns all installed plugins or filters them by keywords. " "Query installed plugins in MoviePilot. Returns all installed plugins or filters them by keywords. "
"Use this tool to find the exact plugin_id before uninstall_plugin or other plugin management tools are used." "Use this tool to find the exact plugin_id before uninstall_plugin or other plugin management tools are used."
@@ -8,6 +8,7 @@ from typing import Optional, Type, Any
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.chain.mediaserver import MediaServerChain from app.chain.mediaserver import MediaServerChain
from app.helper.mediaserver import MediaServerHelper from app.helper.mediaserver import MediaServerHelper
from app.log import logger from app.log import logger
@@ -84,6 +85,11 @@ class QueryLibraryExistsInput(BaseModel):
class QueryLibraryExistsTool(MoviePilotTool): class QueryLibraryExistsTool(MoviePilotTool):
name: str = "query_library_exists" name: str = "query_library_exists"
tags: list[str] = [
ToolTag.Read,
ToolTag.Library,
ToolTag.Media,
]
description: str = "Check whether media already exists in Plex, Emby, or Jellyfin by media ID. Results are grouped by media server; TV results include existing episodes, total episodes, and missing episodes/seasons. Requires tmdb_id or douban_id from search_media." description: str = "Check whether media already exists in Plex, Emby, or Jellyfin by media ID. Results are grouped by media server; TV results include existing episodes, total episodes, and missing episodes/seasons. Requires tmdb_id or douban_id from search_media."
args_schema: Type[BaseModel] = QueryLibraryExistsInput args_schema: Type[BaseModel] = QueryLibraryExistsInput
@@ -7,6 +7,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.chain.mediaserver import MediaServerChain from app.chain.mediaserver import MediaServerChain
from app.helper.service import ServiceConfigHelper from app.helper.service import ServiceConfigHelper
from app.log import logger from app.log import logger
@@ -30,6 +31,11 @@ class QueryLibraryLatestInput(BaseModel):
class QueryLibraryLatestTool(MoviePilotTool): class QueryLibraryLatestTool(MoviePilotTool):
name: str = "query_library_latest" name: str = "query_library_latest"
tags: list[str] = [
ToolTag.Read,
ToolTag.Library,
ToolTag.Media,
]
description: str = "Query the latest media items added to the media server (Plex, Emby, Jellyfin). Returns recently added movies and TV series with their titles, images, links, and other metadata. Supports pagination with 20 items per page." description: str = "Query the latest media items added to the media server (Plex, Emby, Jellyfin). Returns recently added movies and TV series with their titles, images, links, and other metadata. Supports pagination with 20 items per page."
args_schema: Type[BaseModel] = QueryLibraryLatestInput args_schema: Type[BaseModel] = QueryLibraryLatestInput
@@ -6,6 +6,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.agent.tools.impl._plugin_tool_utils import ( from app.agent.tools.impl._plugin_tool_utils import (
DEFAULT_PLUGIN_CANDIDATE_LIMIT, DEFAULT_PLUGIN_CANDIDATE_LIMIT,
MAX_PLUGIN_CANDIDATE_LIMIT, MAX_PLUGIN_CANDIDATE_LIMIT,
@@ -38,6 +39,11 @@ class QueryMarketPluginsInput(BaseModel):
class QueryMarketPluginsTool(MoviePilotTool): class QueryMarketPluginsTool(MoviePilotTool):
name: str = "query_market_plugins" name: str = "query_market_plugins"
tags: list[str] = [
ToolTag.Read,
ToolTag.Plugin,
ToolTag.Admin,
]
description: str = ( description: str = (
"Query available plugins from the plugin market and local plugin repositories. " "Query available plugins from the plugin market and local plugin repositories. "
"Can return the full plugin list or filter by keywords before install_plugin is used." "Can return the full plugin list or filter by keywords before install_plugin is used."
@@ -6,6 +6,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.chain.media import MediaChain from app.chain.media import MediaChain
from app.log import logger from app.log import logger
from app.schemas.types import MediaType from app.schemas.types import MediaType
@@ -25,6 +26,10 @@ class QueryMediaDetailInput(BaseModel):
class QueryMediaDetailTool(MoviePilotTool): class QueryMediaDetailTool(MoviePilotTool):
name: str = "query_media_detail" name: str = "query_media_detail"
tags: list[str] = [
ToolTag.Read,
ToolTag.Media,
]
description: str = "Query supplementary media details from TMDB by ID and media_type. Accepts tmdb_id or douban_id (at least one required). media_type accepts 'movie' or 'tv'. Returns non-duplicated detail fields such as status, genres, directors, actors, and season info for TV series." description: str = "Query supplementary media details from TMDB by ID and media_type. Accepts tmdb_id or douban_id (at least one required). media_type accepts 'movie' or 'tv'. Returns non-duplicated detail fields such as status, genres, directors, actors, and season info for TV series."
args_schema: Type[BaseModel] = QueryMediaDetailInput args_schema: Type[BaseModel] = QueryMediaDetailInput
+5
View File
@@ -7,6 +7,7 @@ from pydantic import BaseModel, Field
from app.agent.runtime import agent_runtime_manager from app.agent.runtime import agent_runtime_manager
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.log import logger from app.log import logger
@@ -26,6 +27,10 @@ class QueryPersonasInput(BaseModel):
class QueryPersonasTool(MoviePilotTool): class QueryPersonasTool(MoviePilotTool):
name: str = "query_personas" name: str = "query_personas"
tags: list[str] = [
ToolTag.Read,
ToolTag.Persona,
]
description: str = ( description: str = (
"List all available personas (人格) and show which one is currently active. " "List all available personas (人格) and show which one is currently active. "
"Use this before switching persona when the user asks for a different speaking style but does not name " "Use this before switching persona when the user asks for a different speaking style but does not name "
@@ -6,6 +6,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.core.plugin import PluginManager from app.core.plugin import PluginManager
from app.log import logger from app.log import logger
@@ -25,6 +26,11 @@ class QueryPluginCapabilitiesInput(BaseModel):
class QueryPluginCapabilitiesTool(MoviePilotTool): class QueryPluginCapabilitiesTool(MoviePilotTool):
name: str = "query_plugin_capabilities" name: str = "query_plugin_capabilities"
tags: list[str] = [
ToolTag.Read,
ToolTag.Plugin,
ToolTag.Admin,
]
description: str = ( description: str = (
"Query the capabilities of installed plugins, including supported commands and scheduled services. " "Query the capabilities of installed plugins, including supported commands and scheduled services. "
"Commands are slash-commands (e.g. /xxx) that can be executed via the run_slash_command tool. " "Commands are slash-commands (e.g. /xxx) that can be executed via the run_slash_command tool. "
@@ -6,6 +6,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.agent.tools.impl._plugin_tool_utils import get_plugin_snapshot from app.agent.tools.impl._plugin_tool_utils import get_plugin_snapshot
from app.core.plugin import PluginManager from app.core.plugin import PluginManager
from app.log import logger from app.log import logger
@@ -24,6 +25,11 @@ class QueryPluginConfigInput(BaseModel):
class QueryPluginConfigTool(MoviePilotTool): class QueryPluginConfigTool(MoviePilotTool):
name: str = "query_plugin_config" name: str = "query_plugin_config"
tags: list[str] = [
ToolTag.Read,
ToolTag.Plugin,
ToolTag.Admin,
]
description: str = ( description: str = (
"Query the saved configuration of an installed plugin. " "Query the saved configuration of an installed plugin. "
"Returns the current saved config and, when available, the plugin's default config model. " "Returns the current saved config and, when available, the plugin's default config model. "
@@ -6,6 +6,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.agent.tools.impl._plugin_tool_utils import ( from app.agent.tools.impl._plugin_tool_utils import (
PLUGIN_DATA_KEY_PREVIEW_LIMIT, PLUGIN_DATA_KEY_PREVIEW_LIMIT,
build_preview_payload, build_preview_payload,
@@ -36,6 +37,11 @@ class QueryPluginDataInput(BaseModel):
class QueryPluginDataTool(MoviePilotTool): class QueryPluginDataTool(MoviePilotTool):
name: str = "query_plugin_data" name: str = "query_plugin_data"
tags: list[str] = [
ToolTag.Read,
ToolTag.Plugin,
ToolTag.Admin,
]
description: str = ( description: str = (
"Query persisted data of an installed plugin. " "Query persisted data of an installed plugin. "
"Optionally specify a key to read a single data item; otherwise all plugin data entries are returned. " "Optionally specify a key to read a single data item; otherwise all plugin data entries are returned. "
@@ -7,6 +7,7 @@ import cn2an
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.core.context import MediaInfo from app.core.context import MediaInfo
from app.helper.server import MoviePilotServerHelper from app.helper.server import MoviePilotServerHelper
from app.log import logger from app.log import logger
@@ -30,6 +31,11 @@ class QueryPopularSubscribesInput(BaseModel):
class QueryPopularSubscribesTool(MoviePilotTool): class QueryPopularSubscribesTool(MoviePilotTool):
name: str = "query_popular_subscribes" name: str = "query_popular_subscribes"
tags: list[str] = [
ToolTag.Read,
ToolTag.Subscription,
ToolTag.Recommendation,
]
description: str = "Query popular subscriptions based on user shared data. Shows media with the most subscribers, supports filtering by genre, rating, minimum subscribers, and pagination." description: str = "Query popular subscriptions based on user shared data. Shows media with the most subscribers, supports filtering by genre, rating, minimum subscribers, and pagination."
args_schema: Type[BaseModel] = QueryPopularSubscribesInput args_schema: Type[BaseModel] = QueryPopularSubscribesInput
@@ -6,6 +6,7 @@ from typing import Optional, Type, List
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.agent.tools.impl._filter_rule_utils import ( from app.agent.tools.impl._filter_rule_utils import (
collect_rule_group_usages, collect_rule_group_usages,
get_rule_groups, get_rule_groups,
@@ -32,6 +33,10 @@ class QueryRuleGroupsInput(BaseModel):
class QueryRuleGroupsTool(MoviePilotTool): class QueryRuleGroupsTool(MoviePilotTool):
name: str = "query_rule_groups" name: str = "query_rule_groups"
tags: list[str] = [
ToolTag.Read,
ToolTag.FilterRule,
]
description: str = ( description: str = (
"Query filter rule groups (过滤规则组 / 优先级规则组). " "Query filter rule groups (过滤规则组 / 优先级规则组). "
"Each rule group contains a rule_string made of built-in rules and/or custom rules. " "Each rule group contains a rule_string made of built-in rules and/or custom rules. "
+5
View File
@@ -6,6 +6,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.log import logger from app.log import logger
@@ -16,6 +17,10 @@ class QuerySchedulersInput(BaseModel):
class QuerySchedulersTool(MoviePilotTool): class QuerySchedulersTool(MoviePilotTool):
name: str = "query_schedulers" name: str = "query_schedulers"
tags: list[str] = [
ToolTag.Read,
ToolTag.Scheduler,
]
description: str = "Query scheduled tasks and list all available scheduler jobs. Shows job status, next run time, and provider information." description: str = "Query scheduled tasks and list all available scheduler jobs. Shows job status, next run time, and provider information."
args_schema: Type[BaseModel] = QuerySchedulersInput args_schema: Type[BaseModel] = QuerySchedulersInput
@@ -6,6 +6,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.db import AsyncSessionFactory from app.db import AsyncSessionFactory
from app.db.models.site import Site from app.db.models.site import Site
from app.db.models.siteuserdata import SiteUserData from app.db.models.siteuserdata import SiteUserData
@@ -37,6 +38,11 @@ class QuerySiteUserdataInput(BaseModel):
class QuerySiteUserdataTool(MoviePilotTool): class QuerySiteUserdataTool(MoviePilotTool):
name: str = "query_site_userdata" name: str = "query_site_userdata"
tags: list[str] = [
ToolTag.Read,
ToolTag.Site,
ToolTag.Admin,
]
description: str = "Query user data for a specific site including username, user level, upload/download statistics, seeding information, bonus points, and other account details. Supports querying data for a specific date or latest data." description: str = "Query user data for a specific site including username, user level, upload/download statistics, seeding information, bonus points, and other account details. Supports querying data for a specific date or latest data."
require_admin: bool = True require_admin: bool = True
args_schema: Type[BaseModel] = QuerySiteUserdataInput args_schema: Type[BaseModel] = QuerySiteUserdataInput
+6
View File
@@ -6,6 +6,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.db.site_oper import SiteOper from app.db.site_oper import SiteOper
from app.log import logger from app.log import logger
@@ -26,6 +27,11 @@ class QuerySitesInput(BaseModel):
class QuerySitesTool(MoviePilotTool): class QuerySitesTool(MoviePilotTool):
name: str = "query_sites" name: str = "query_sites"
tags: list[str] = [
ToolTag.Read,
ToolTag.Site,
ToolTag.Admin,
]
description: str = "Query site status and list all configured sites. Shows site name, domain, status, priority, and basic configuration. Site priority (pri): smaller values have higher priority (e.g., pri=1 has higher priority than pri=10)." description: str = "Query site status and list all configured sites. Shows site name, domain, status, priority, and basic configuration. Site priority (pri): smaller values have higher priority (e.g., pri=1 has higher priority than pri=10)."
require_admin: bool = True require_admin: bool = True
args_schema: Type[BaseModel] = QuerySitesInput args_schema: Type[BaseModel] = QuerySitesInput
@@ -6,6 +6,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.db import AsyncSessionFactory from app.db import AsyncSessionFactory
from app.db.models.subscribehistory import SubscribeHistory from app.db.models.subscribehistory import SubscribeHistory
from app.log import logger from app.log import logger
@@ -33,6 +34,10 @@ class QuerySubscribeHistoryInput(BaseModel):
class QuerySubscribeHistoryTool(MoviePilotTool): class QuerySubscribeHistoryTool(MoviePilotTool):
name: str = "query_subscribe_history" name: str = "query_subscribe_history"
tags: list[str] = [
ToolTag.Read,
ToolTag.Subscription,
]
description: str = "Query subscription history records. Shows completed subscriptions with their details including name, type, rating, completion date, and other subscription information. Supports filtering by media type and name. Supports pagination with 20 records per page." description: str = "Query subscription history records. Shows completed subscriptions with their details including name, type, rating, completion date, and other subscription information. Supports filtering by media type and name. Supports pagination with 20 records per page."
args_schema: Type[BaseModel] = QuerySubscribeHistoryInput args_schema: Type[BaseModel] = QuerySubscribeHistoryInput
@@ -6,6 +6,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.helper.server import MoviePilotServerHelper from app.helper.server import MoviePilotServerHelper
from app.log import logger from app.log import logger
@@ -26,6 +27,10 @@ class QuerySubscribeSharesInput(BaseModel):
class QuerySubscribeSharesTool(MoviePilotTool): class QuerySubscribeSharesTool(MoviePilotTool):
name: str = "query_subscribe_shares" name: str = "query_subscribe_shares"
tags: list[str] = [
ToolTag.Read,
ToolTag.Subscription,
]
description: str = "Query shared subscriptions from other users. Shows popular subscriptions shared by the community with filtering and pagination support." description: str = "Query shared subscriptions from other users. Shows popular subscriptions shared by the community with filtering and pagination support."
args_schema: Type[BaseModel] = QuerySubscribeSharesInput args_schema: Type[BaseModel] = QuerySubscribeSharesInput
+5
View File
@@ -6,6 +6,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.db.subscribe_oper import SubscribeOper from app.db.subscribe_oper import SubscribeOper
from app.log import logger from app.log import logger
from app.schemas.subscribe import Subscribe as SubscribeSchema from app.schemas.subscribe import Subscribe as SubscribeSchema
@@ -71,6 +72,10 @@ class QuerySubscribesInput(BaseModel):
class QuerySubscribesTool(MoviePilotTool): class QuerySubscribesTool(MoviePilotTool):
name: str = "query_subscribes" name: str = "query_subscribes"
tags: list[str] = [
ToolTag.Read,
ToolTag.Subscription,
]
description: str = "Query subscription status and list user subscriptions. Returns full subscription parameters for each matched subscription. Supports pagination with 100 items per page." description: str = "Query subscription status and list user subscriptions. Returns full subscription parameters for each matched subscription. Supports pagination with 100 items per page."
args_schema: Type[BaseModel] = QuerySubscribesInput args_schema: Type[BaseModel] = QuerySubscribesInput
@@ -6,6 +6,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.agent.tools.impl._system_setting_utils import ( from app.agent.tools.impl._system_setting_utils import (
SettingSpec, SettingSpec,
list_setting_specs, list_setting_specs,
@@ -56,6 +57,12 @@ class QuerySystemSettingsInput(BaseModel):
class QuerySystemSettingsTool(MoviePilotTool): class QuerySystemSettingsTool(MoviePilotTool):
name: str = "query_system_settings" name: str = "query_system_settings"
tags: list[str] = [
ToolTag.Read,
ToolTag.System,
ToolTag.Settings,
ToolTag.Admin,
]
description: str = ( description: str = (
"Query system settings across both the basic Settings module and all SystemConfig-backed categories. " "Query system settings across both the basic Settings module and all SystemConfig-backed categories. "
"Use this tool to inspect downloaders, media servers, notification channels, storages, directories, search-site ranges, " "Use this tool to inspect downloaders, media servers, notification channels, storages, directories, search-site ranges, "
@@ -6,6 +6,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.db import AsyncSessionFactory from app.db import AsyncSessionFactory
from app.db.models.transferhistory import TransferHistory from app.db.models.transferhistory import TransferHistory
from app.log import logger from app.log import logger
@@ -24,6 +25,10 @@ class QueryTransferHistoryInput(BaseModel):
class QueryTransferHistoryTool(MoviePilotTool): class QueryTransferHistoryTool(MoviePilotTool):
name: str = "query_transfer_history" name: str = "query_transfer_history"
tags: list[str] = [
ToolTag.Read,
ToolTag.Transfer,
]
description: str = "Query file transfer history records. Shows transfer status, source and destination paths, media information, and transfer details. Supports filtering by title and status." description: str = "Query file transfer history records. Shows transfer status, source and destination paths, media information, and transfer details. Supports filtering by title and status."
args_schema: Type[BaseModel] = QueryTransferHistoryInput args_schema: Type[BaseModel] = QueryTransferHistoryInput
+5
View File
@@ -6,6 +6,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.db import AsyncSessionFactory from app.db import AsyncSessionFactory
from app.db.workflow_oper import WorkflowOper from app.db.workflow_oper import WorkflowOper
from app.log import logger from app.log import logger
@@ -21,6 +22,10 @@ class QueryWorkflowsInput(BaseModel):
class QueryWorkflowsTool(MoviePilotTool): class QueryWorkflowsTool(MoviePilotTool):
name: str = "query_workflows" name: str = "query_workflows"
tags: list[str] = [
ToolTag.Read,
ToolTag.Workflow,
]
description: str = "Query workflow list and status. Shows workflow name, description, trigger type, state, execution count, and other workflow details. Supports filtering by state, name, and trigger type." description: str = "Query workflow list and status. Shows workflow name, description, trigger type, state, execution count, and other workflow details. Supports filtering by state, name, and trigger type."
args_schema: Type[BaseModel] = QueryWorkflowsInput args_schema: Type[BaseModel] = QueryWorkflowsInput
+5
View File
@@ -7,6 +7,7 @@ from anyio import Path as AsyncPath
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.log import logger from app.log import logger
# 最大读取大小 50KB # 最大读取大小 50KB
@@ -22,6 +23,10 @@ class ReadFileInput(BaseModel):
class ReadFileTool(MoviePilotTool): class ReadFileTool(MoviePilotTool):
name: str = "read_file" name: str = "read_file"
tags: list[str] = [
ToolTag.Read,
ToolTag.File,
]
description: str = "Read the content of a text file. Supports reading by line range. Each read is limited to 50KB; content exceeding this limit will be truncated." description: str = "Read the content of a text file. Supports reading by line range. Each read is limited to 50KB; content exceeding this limit will be truncated."
args_schema: Type[BaseModel] = ReadFileInput args_schema: Type[BaseModel] = ReadFileInput
+6
View File
@@ -6,6 +6,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.chain.media import MediaChain from app.chain.media import MediaChain
from app.core.context import Context from app.core.context import Context
from app.core.metainfo import MetaInfo from app.core.metainfo import MetaInfo
@@ -23,6 +24,11 @@ class RecognizeMediaInput(BaseModel):
class RecognizeMediaTool(MoviePilotTool): class RecognizeMediaTool(MoviePilotTool):
name: str = "recognize_media" name: str = "recognize_media"
tags: list[str] = [
ToolTag.Read,
ToolTag.Media,
ToolTag.Metadata,
]
description: str = "Extract/identify media information from torrent titles or file paths (NOT database search). Supports two modes: 1) Extract from torrent title and optional subtitle, 2) Extract from file path. Returns detailed media information. Use 'search_media' to search TMDB database, or 'scrape_metadata' to generate metadata files for existing files." description: str = "Extract/identify media information from torrent titles or file paths (NOT database search). Supports two modes: 1) Extract from torrent title and optional subtitle, 2) Extract from file path. Returns detailed media information. Use 'search_media' to search TMDB database, or 'scrape_metadata' to generate metadata files for existing files."
args_schema: Type[BaseModel] = RecognizeMediaInput args_schema: Type[BaseModel] = RecognizeMediaInput
+6
View File
@@ -6,6 +6,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.agent.tools.impl._plugin_tool_utils import ( from app.agent.tools.impl._plugin_tool_utils import (
get_plugin_snapshot, get_plugin_snapshot,
reload_plugin_runtime, reload_plugin_runtime,
@@ -26,6 +27,11 @@ class ReloadPluginInput(BaseModel):
class ReloadPluginTool(MoviePilotTool): class ReloadPluginTool(MoviePilotTool):
name: str = "reload_plugin" name: str = "reload_plugin"
tags: list[str] = [
ToolTag.Write,
ToolTag.Plugin,
ToolTag.Admin,
]
description: str = ( description: str = (
"Reload an installed plugin so its latest saved configuration takes effect. " "Reload an installed plugin so its latest saved configuration takes effect. "
"This also refreshes the plugin's registered commands, scheduled services, and API routes." "This also refreshes the plugin's registered commands, scheduled services, and API routes."
+6
View File
@@ -5,6 +5,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.log import logger from app.log import logger
@@ -21,6 +22,11 @@ class RunSchedulerInput(BaseModel):
class RunSchedulerTool(MoviePilotTool): class RunSchedulerTool(MoviePilotTool):
name: str = "run_scheduler" name: str = "run_scheduler"
tags: list[str] = [
ToolTag.Write,
ToolTag.Scheduler,
ToolTag.Admin,
]
description: str = "Manually trigger a scheduled task to run immediately. This will execute the specified scheduler job by its ID." description: str = "Manually trigger a scheduled task to run immediately. This will execute the specified scheduler job by its ID."
args_schema: Type[BaseModel] = RunSchedulerInput args_schema: Type[BaseModel] = RunSchedulerInput
require_admin: bool = True require_admin: bool = True
@@ -6,6 +6,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.core.event import eventmanager from app.core.event import eventmanager
from app.log import logger from app.log import logger
from app.schemas.types import EventType, MessageChannel from app.schemas.types import EventType, MessageChannel
@@ -27,6 +28,11 @@ class RunSlashCommandInput(BaseModel):
class RunSlashCommandTool(MoviePilotTool): class RunSlashCommandTool(MoviePilotTool):
name: str = "run_slash_command" name: str = "run_slash_command"
tags: list[str] = [
ToolTag.Write,
ToolTag.SlashCommand,
ToolTag.Admin,
]
description: str = ( description: str = (
"Execute a slash command (system or plugin) by sending a CommandExcute event. " "Execute a slash command (system or plugin) by sending a CommandExcute event. "
"This tool supports ALL registered slash commands, including: " "This tool supports ALL registered slash commands, including: "
+6
View File
@@ -5,6 +5,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.chain.workflow import WorkflowChain from app.chain.workflow import WorkflowChain
from app.db import AsyncSessionFactory from app.db import AsyncSessionFactory
from app.db.workflow_oper import WorkflowOper from app.db.workflow_oper import WorkflowOper
@@ -27,6 +28,11 @@ class RunWorkflowInput(BaseModel):
class RunWorkflowTool(MoviePilotTool): class RunWorkflowTool(MoviePilotTool):
name: str = "run_workflow" name: str = "run_workflow"
tags: list[str] = [
ToolTag.Write,
ToolTag.Workflow,
ToolTag.Admin,
]
description: str = "Execute a specific workflow manually by workflow ID. Supports running from the beginning or continuing from the last executed action." description: str = "Execute a specific workflow manually by workflow ID. Supports running from the beginning or continuing from the last executed action."
args_schema: Type[BaseModel] = RunWorkflowInput args_schema: Type[BaseModel] = RunWorkflowInput
require_admin: bool = True require_admin: bool = True
+8
View File
@@ -7,6 +7,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.chain.media import MediaChain from app.chain.media import MediaChain
from app.log import logger from app.log import logger
from app.schemas import FileItem from app.schemas import FileItem
@@ -33,6 +34,13 @@ class ScrapeMetadataInput(BaseModel):
class ScrapeMetadataTool(MoviePilotTool): class ScrapeMetadataTool(MoviePilotTool):
name: str = "scrape_metadata" name: str = "scrape_metadata"
tags: list[str] = [
ToolTag.Write,
ToolTag.Media,
ToolTag.Metadata,
ToolTag.File,
ToolTag.Admin,
]
description: str = "Generate metadata files (NFO files, posters, backgrounds, etc.) for existing media files or directories. Automatically recognizes media information from the file path and creates metadata files. Supports both local and remote storage. Use 'search_media' to search TMDB database, or 'recognize_media' to extract info from torrent titles/file paths without generating files." description: str = "Generate metadata files (NFO files, posters, backgrounds, etc.) for existing media files or directories. Automatically recognizes media information from the file path and creates metadata files. Supports both local and remote storage. Use 'search_media' to search TMDB database, or 'recognize_media' to extract info from torrent titles/file paths without generating files."
require_admin: bool = True require_admin: bool = True
args_schema: Type[BaseModel] = ScrapeMetadataInput args_schema: Type[BaseModel] = ScrapeMetadataInput
+5
View File
@@ -6,6 +6,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.chain.media import MediaChain from app.chain.media import MediaChain
from app.log import logger from app.log import logger
from app.schemas.types import MediaType, media_type_to_agent from app.schemas.types import MediaType, media_type_to_agent
@@ -24,6 +25,10 @@ class SearchMediaInput(BaseModel):
class SearchMediaTool(MoviePilotTool): class SearchMediaTool(MoviePilotTool):
name: str = "search_media" name: str = "search_media"
tags: list[str] = [
ToolTag.Read,
ToolTag.Media,
]
description: str = "Search TMDB database for media resources (movies, TV shows, anime, etc.) by title, year, type, and other criteria. Returns detailed media information from TMDB. Use 'recognize_media' to extract info from torrent titles/file paths, or 'scrape_metadata' to generate metadata files." description: str = "Search TMDB database for media resources (movies, TV shows, anime, etc.) by title, year, type, and other criteria. Returns detailed media information from TMDB. Use 'recognize_media' to extract info from torrent titles/file paths, or 'scrape_metadata' to generate metadata files."
args_schema: Type[BaseModel] = SearchMediaInput args_schema: Type[BaseModel] = SearchMediaInput
+5
View File
@@ -6,6 +6,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.chain.media import MediaChain from app.chain.media import MediaChain
from app.log import logger from app.log import logger
@@ -18,6 +19,10 @@ class SearchPersonInput(BaseModel):
class SearchPersonTool(MoviePilotTool): class SearchPersonTool(MoviePilotTool):
name: str = "search_person" name: str = "search_person"
tags: list[str] = [
ToolTag.Read,
ToolTag.Media,
]
description: str = "Search for person information including actors, directors, etc. Supports searching by name. Returns detailed person information from TMDB, Douban, or Bangumi database." description: str = "Search for person information including actors, directors, etc. Supports searching by name. Returns detailed person information from TMDB, Douban, or Bangumi database."
args_schema: Type[BaseModel] = SearchPersonInput args_schema: Type[BaseModel] = SearchPersonInput
@@ -6,6 +6,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.chain.douban import DoubanChain from app.chain.douban import DoubanChain
from app.chain.tmdb import TmdbChain from app.chain.tmdb import TmdbChain
from app.chain.bangumi import BangumiChain from app.chain.bangumi import BangumiChain
@@ -22,6 +23,10 @@ class SearchPersonCreditsInput(BaseModel):
class SearchPersonCreditsTool(MoviePilotTool): class SearchPersonCreditsTool(MoviePilotTool):
name: str = "search_person_credits" name: str = "search_person_credits"
tags: list[str] = [
ToolTag.Read,
ToolTag.Media,
]
description: str = "Search for films and TV shows that a person/actor has appeared in (filmography). Supports searching by person ID from TMDB, Douban, or Bangumi database. Returns a list of media works the person has participated in." description: str = "Search for films and TV shows that a person/actor has appeared in (filmography). Supports searching by person ID from TMDB, Douban, or Bangumi database. Returns a list of media works the person has participated in."
args_schema: Type[BaseModel] = SearchPersonCreditsInput args_schema: Type[BaseModel] = SearchPersonCreditsInput
+7
View File
@@ -6,6 +6,7 @@ from typing import Optional, Type, List
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.chain.subscribe import SubscribeChain from app.chain.subscribe import SubscribeChain
from app.db.subscribe_oper import SubscribeOper from app.db.subscribe_oper import SubscribeOper
from app.log import logger from app.log import logger
@@ -23,6 +24,12 @@ class SearchSubscribeInput(BaseModel):
class SearchSubscribeTool(MoviePilotTool): class SearchSubscribeTool(MoviePilotTool):
name: str = "search_subscribe" name: str = "search_subscribe"
tags: list[str] = [
ToolTag.Read,
ToolTag.Write,
ToolTag.Subscription,
ToolTag.Resource,
]
description: str = "Search for missing episodes/resources for a specific subscription. This tool will search torrent sites for the missing episodes of the subscription and automatically download matching resources. Use this when a user wants to search for missing episodes of a specific subscription." description: str = "Search for missing episodes/resources for a specific subscription. This tool will search torrent sites for the missing episodes of the subscription and automatically download matching resources. Use this when a user wants to search for missing episodes of a specific subscription."
args_schema: Type[BaseModel] = SearchSubscribeInput args_schema: Type[BaseModel] = SearchSubscribeInput
+7
View File
@@ -6,6 +6,7 @@ from typing import List, Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.chain.search import SearchChain from app.chain.search import SearchChain
from app.db.systemconfig_oper import SystemConfigOper from app.db.systemconfig_oper import SystemConfigOper
from app.helper.sites import SitesHelper from app.helper.sites import SitesHelper
@@ -29,6 +30,12 @@ class SearchTorrentsInput(BaseModel):
class SearchTorrentsTool(MoviePilotTool): class SearchTorrentsTool(MoviePilotTool):
name: str = "search_torrents" name: str = "search_torrents"
tags: list[str] = [
ToolTag.Read,
ToolTag.Resource,
ToolTag.Site,
ToolTag.Media,
]
description: str = ("Search for torrent files by media ID across configured indexer sites, cache the matched results, " description: str = ("Search for torrent files by media ID across configured indexer sites, cache the matched results, "
"and return available filter options for follow-up selection. " "and return available filter options for follow-up selection. "
"Requires tmdb_id or douban_id (can be obtained from search_media tool) for accurate matching.") "Requires tmdb_id or douban_id (can be obtained from search_media tool) for accurate matching.")
+5
View File
@@ -9,6 +9,7 @@ from ddgs import DDGS
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.core.config import settings from app.core.config import settings
from app.log import logger from app.log import logger
@@ -82,6 +83,10 @@ class SearchWebTool(MoviePilotTool):
""" """
name: str = "search_web" name: str = "search_web"
tags: list[str] = [
ToolTag.Read,
ToolTag.Web,
]
description: str = ( description: str = (
"Search the web for information when you need current information, facts, " "Search the web for information when you need current information, facts, "
"or references. Supports DDGS-backed search engine selection, automatic " "or references. Supports DDGS-backed search engine selection, automatic "
+6
View File
@@ -6,6 +6,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field, model_validator from pydantic import BaseModel, Field, model_validator
from app.agent.tools.base import MoviePilotTool, ToolChain from app.agent.tools.base import MoviePilotTool, ToolChain
from app.agent.tools.tags import ToolTag
from app.log import logger from app.log import logger
from app.schemas import Notification, NotificationType from app.schemas import Notification, NotificationType
from app.schemas.message import ChannelCapabilityManager, ChannelCapability from app.schemas.message import ChannelCapabilityManager, ChannelCapability
@@ -43,6 +44,11 @@ class SendLocalFileInput(BaseModel):
class SendLocalFileTool(MoviePilotTool): class SendLocalFileTool(MoviePilotTool):
name: str = "send_local_file" name: str = "send_local_file"
tags: list[str] = [
ToolTag.Write,
ToolTag.Message,
ToolTag.File,
]
sends_message: bool = True sends_message: bool = True
description: str = ( description: str = (
"Send a local image or file from the server filesystem to the current user. " "Send a local image or file from the server filesystem to the current user. "
+6
View File
@@ -5,6 +5,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field, model_validator from pydantic import BaseModel, Field, model_validator
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.log import logger from app.log import logger
@@ -35,6 +36,11 @@ class SendMessageInput(BaseModel):
class SendMessageTool(MoviePilotTool): class SendMessageTool(MoviePilotTool):
name: str = "send_message" name: str = "send_message"
tags: list[str] = [
ToolTag.Write,
ToolTag.Message,
ToolTag.Admin,
]
sends_message: bool = True sends_message: bool = True
description: str = "Send notification message to the user through configured notification channels (Telegram, Slack, WeChat, etc.). Supports optional image_url on channels that can send images. Used to inform users about operation results, errors, important updates, or proactively send a relevant image." description: str = "Send notification message to the user through configured notification channels (Telegram, Slack, WeChat, etc.). Supports optional image_url on channels that can send images. Used to inform users about operation results, errors, important updates, or proactively send a relevant image."
args_schema: Type[BaseModel] = SendMessageInput args_schema: Type[BaseModel] = SendMessageInput
@@ -7,6 +7,7 @@ from pydantic import BaseModel, Field
from app.agent.llm.capability import AgentCapabilityManager from app.agent.llm.capability import AgentCapabilityManager
from app.agent.tools.base import MoviePilotTool, ToolChain from app.agent.tools.base import MoviePilotTool, ToolChain
from app.agent.tools.tags import ToolTag
from app.core.config import settings from app.core.config import settings
from app.log import logger from app.log import logger
from app.schemas import Notification, NotificationType from app.schemas import Notification, NotificationType
@@ -29,6 +30,11 @@ class SendVoiceMessageTool(MoviePilotTool):
"""发送 Agent 语音回复的工具。""" """发送 Agent 语音回复的工具。"""
name: str = "send_voice_message" name: str = "send_voice_message"
tags: list[str] = [
ToolTag.Write,
ToolTag.Message,
ToolTag.TerminalResponse,
]
sends_message: bool = True sends_message: bool = True
return_direct: bool = True return_direct: bool = True
description: str = ( description: str = (
+5
View File
@@ -7,6 +7,7 @@ from pydantic import BaseModel, Field
from app.agent.runtime import agent_runtime_manager from app.agent.runtime import agent_runtime_manager
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.log import logger from app.log import logger
@@ -26,6 +27,10 @@ class SwitchPersonaInput(BaseModel):
class SwitchPersonaTool(MoviePilotTool): class SwitchPersonaTool(MoviePilotTool):
name: str = "switch_persona" name: str = "switch_persona"
tags: list[str] = [
ToolTag.Write,
ToolTag.Persona,
]
description: str = ( description: str = (
"Switch the active persona (人格) used by the agent runtime. " "Switch the active persona (人格) used by the agent runtime. "
"This change is persistent for future turns. " "This change is persistent for future turns. "
+5
View File
@@ -5,6 +5,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.chain.site import SiteChain from app.chain.site import SiteChain
from app.db.site_oper import SiteOper from app.db.site_oper import SiteOper
from app.log import logger from app.log import logger
@@ -18,6 +19,10 @@ class TestSiteInput(BaseModel):
class TestSiteTool(MoviePilotTool): class TestSiteTool(MoviePilotTool):
name: str = "test_site" name: str = "test_site"
tags: list[str] = [
ToolTag.Read,
ToolTag.Site,
]
description: str = "Test site connectivity and availability. This will check if a site is accessible and can be logged in. Accepts site ID only." description: str = "Test site connectivity and availability. This will check if a site is accessible and can be logged in. Accepts site ID only."
args_schema: Type[BaseModel] = TestSiteInput args_schema: Type[BaseModel] = TestSiteInput
+8
View File
@@ -6,6 +6,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.log import logger from app.log import logger
from app.schemas import FileItem, MediaType from app.schemas import FileItem, MediaType
@@ -54,6 +55,13 @@ class TransferFileInput(BaseModel):
class TransferFileTool(MoviePilotTool): class TransferFileTool(MoviePilotTool):
name: str = "transfer_file" name: str = "transfer_file"
tags: list[str] = [
ToolTag.Write,
ToolTag.Transfer,
ToolTag.Library,
ToolTag.File,
ToolTag.Admin,
]
description: str = "Transfer/organize a file or directory to the media library. Automatically recognizes media information and organizes files according to configured rules. Supports custom target paths, media identification, and transfer modes." description: str = "Transfer/organize a file or directory to the media library. Automatically recognizes media information and organizes files according to configured rules. Supports custom target paths, media identification, and transfer modes."
args_schema: Type[BaseModel] = TransferFileInput args_schema: Type[BaseModel] = TransferFileInput
require_admin: bool = True require_admin: bool = True
+6
View File
@@ -6,6 +6,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.agent.tools.impl._plugin_tool_utils import ( from app.agent.tools.impl._plugin_tool_utils import (
list_installed_plugins, list_installed_plugins,
summarize_plugin, summarize_plugin,
@@ -27,6 +28,11 @@ class UninstallPluginInput(BaseModel):
class UninstallPluginTool(MoviePilotTool): class UninstallPluginTool(MoviePilotTool):
name: str = "uninstall_plugin" name: str = "uninstall_plugin"
tags: list[str] = [
ToolTag.Write,
ToolTag.Plugin,
ToolTag.Admin,
]
description: str = ( description: str = (
"Uninstall an installed plugin by exact plugin_id. " "Uninstall an installed plugin by exact plugin_id. "
"Use query_installed_plugins first when you need filtering or discovery." "Use query_installed_plugins first when you need filtering or discovery."
@@ -6,6 +6,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.agent.tools.impl._filter_rule_utils import ( from app.agent.tools.impl._filter_rule_utils import (
collect_custom_rule_group_refs, collect_custom_rule_group_refs,
get_custom_rules, get_custom_rules,
@@ -58,6 +59,11 @@ class UpdateCustomFilterRuleInput(BaseModel):
class UpdateCustomFilterRuleTool(MoviePilotTool): class UpdateCustomFilterRuleTool(MoviePilotTool):
name: str = "update_custom_filter_rule" name: str = "update_custom_filter_rule"
tags: list[str] = [
ToolTag.Write,
ToolTag.FilterRule,
ToolTag.Admin,
]
description: str = ( description: str = (
"Update an existing custom filter rule. " "Update an existing custom filter rule. "
"If the rule ID is renamed, all rule groups that reference the old ID are updated automatically." "If the rule ID is renamed, all rule groups that reference the old ID are updated automatically."
@@ -6,6 +6,7 @@ from typing import List, Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.db.systemconfig_oper import SystemConfigOper from app.db.systemconfig_oper import SystemConfigOper
from app.log import logger from app.log import logger
from app.schemas.types import SystemConfigKey from app.schemas.types import SystemConfigKey
@@ -33,6 +34,11 @@ class UpdateCustomIdentifiersInput(BaseModel):
class UpdateCustomIdentifiersTool(MoviePilotTool): class UpdateCustomIdentifiersTool(MoviePilotTool):
name: str = "update_custom_identifiers" name: str = "update_custom_identifiers"
tags: list[str] = [
ToolTag.Write,
ToolTag.FilterRule,
ToolTag.Admin,
]
description: str = ( description: str = (
"Update the full list of custom identifiers (自定义识别词) used for preprocessing torrent/file names. " "Update the full list of custom identifiers (自定义识别词) used for preprocessing torrent/file names. "
"This tool REPLACES all existing identifier rules with the provided list. " "This tool REPLACES all existing identifier rules with the provided list. "
@@ -7,6 +7,7 @@ from pydantic import BaseModel, Field
from app.agent.runtime import agent_runtime_manager from app.agent.runtime import agent_runtime_manager
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.log import logger from app.log import logger
@@ -56,6 +57,11 @@ class UpdatePersonaDefinitionInput(BaseModel):
class UpdatePersonaDefinitionTool(MoviePilotTool): class UpdatePersonaDefinitionTool(MoviePilotTool):
name: str = "update_persona_definition" name: str = "update_persona_definition"
tags: list[str] = [
ToolTag.Write,
ToolTag.Persona,
ToolTag.Admin,
]
description: str = ( description: str = (
"Create or update a runtime persona definition (人格定义) without manually editing PERSONA.md files. " "Create or update a runtime persona definition (人格定义) without manually editing PERSONA.md files. "
"Use this when the user explicitly asks to modify how a persona is defined, such as changing tone rules, " "Use this when the user explicitly asks to modify how a persona is defined, such as changing tone rules, "
@@ -6,6 +6,7 @@ from typing import Any, Dict, List, Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.agent.tools.impl._plugin_tool_utils import get_plugin_snapshot from app.agent.tools.impl._plugin_tool_utils import get_plugin_snapshot
from app.core.plugin import PluginManager from app.core.plugin import PluginManager
from app.log import logger from app.log import logger
@@ -42,6 +43,11 @@ class UpdatePluginConfigInput(BaseModel):
class UpdatePluginConfigTool(MoviePilotTool): class UpdatePluginConfigTool(MoviePilotTool):
name: str = "update_plugin_config" name: str = "update_plugin_config"
tags: list[str] = [
ToolTag.Write,
ToolTag.Plugin,
ToolTag.Admin,
]
description: str = ( description: str = (
"Update the saved configuration of an installed plugin. " "Update the saved configuration of an installed plugin. "
"By default this performs a partial merge update and does NOT reload the plugin automatically. " "By default this performs a partial merge update and does NOT reload the plugin automatically. "
@@ -6,6 +6,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.agent.tools.impl._filter_rule_utils import ( from app.agent.tools.impl._filter_rule_utils import (
build_custom_rule_map, build_custom_rule_map,
collect_rule_group_usages, collect_rule_group_usages,
@@ -50,6 +51,11 @@ class UpdateRuleGroupInput(BaseModel):
class UpdateRuleGroupTool(MoviePilotTool): class UpdateRuleGroupTool(MoviePilotTool):
name: str = "update_rule_group" name: str = "update_rule_group"
tags: list[str] = [
ToolTag.Write,
ToolTag.FilterRule,
ToolTag.Admin,
]
description: str = ( description: str = (
"Update a filter rule group. " "Update a filter rule group. "
"If the rule group name changes, its references in global search/subscription settings and per-subscription bindings are updated automatically. " "If the rule group name changes, its references in global search/subscription settings and per-subscription bindings are updated automatically. "
+6
View File
@@ -6,6 +6,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.core.event import eventmanager from app.core.event import eventmanager
from app.db import AsyncSessionFactory from app.db import AsyncSessionFactory
from app.db.models.site import Site from app.db.models.site import Site
@@ -66,6 +67,11 @@ class UpdateSiteInput(BaseModel):
class UpdateSiteTool(MoviePilotTool): class UpdateSiteTool(MoviePilotTool):
name: str = "update_site" name: str = "update_site"
tags: list[str] = [
ToolTag.Write,
ToolTag.Site,
ToolTag.Admin,
]
description: str = "Update site configuration including URL, priority, authentication credentials (cookie, UA, API key), proxy settings, rate limits, and other site properties. Supports updating multiple site attributes at once. Site priority (pri): smaller values have higher priority (e.g., pri=1 has higher priority than pri=10)." description: str = "Update site configuration including URL, priority, authentication credentials (cookie, UA, API key), proxy settings, rate limits, and other site properties. Supports updating multiple site attributes at once. Site priority (pri): smaller values have higher priority (e.g., pri=1 has higher priority than pri=10)."
args_schema: Type[BaseModel] = UpdateSiteInput args_schema: Type[BaseModel] = UpdateSiteInput
require_admin: bool = True require_admin: bool = True
@@ -5,6 +5,7 @@ from typing import Optional, Type
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.chain.site import SiteChain from app.chain.site import SiteChain
from app.db.site_oper import SiteOper from app.db.site_oper import SiteOper
from app.log import logger from app.log import logger
@@ -29,6 +30,11 @@ class UpdateSiteCookieInput(BaseModel):
class UpdateSiteCookieTool(MoviePilotTool): class UpdateSiteCookieTool(MoviePilotTool):
name: str = "update_site_cookie" name: str = "update_site_cookie"
tags: list[str] = [
ToolTag.Write,
ToolTag.Site,
ToolTag.Admin,
]
description: str = "Update site Cookie and User-Agent by logging in with username and password. This tool can automatically obtain and update the site's authentication credentials. Supports two-step verification for sites that require it. Accepts site ID only." description: str = "Update site Cookie and User-Agent by logging in with username and password. This tool can automatically obtain and update the site's authentication credentials. Supports two-step verification for sites that require it. Accepts site ID only."
args_schema: Type[BaseModel] = UpdateSiteCookieInput args_schema: Type[BaseModel] = UpdateSiteCookieInput
require_admin: bool = True require_admin: bool = True
+6
View File
@@ -6,6 +6,7 @@ from typing import Optional, Type, List
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.core.event import eventmanager from app.core.event import eventmanager
from app.db import AsyncSessionFactory from app.db import AsyncSessionFactory
from app.db.models.subscribe import Subscribe from app.db.models.subscribe import Subscribe
@@ -89,6 +90,11 @@ class UpdateSubscribeInput(BaseModel):
class UpdateSubscribeTool(MoviePilotTool): class UpdateSubscribeTool(MoviePilotTool):
name: str = "update_subscribe" name: str = "update_subscribe"
tags: list[str] = [
ToolTag.Write,
ToolTag.Subscription,
ToolTag.Admin,
]
description: str = "Update subscription properties including filters, episode counts, state, and other settings. Supports updating quality/resolution filters, episode tracking, subscription state, and download configuration." description: str = "Update subscription properties including filters, episode counts, state, and other settings. Supports updating quality/resolution filters, episode tracking, subscription state, and download configuration."
args_schema: Type[BaseModel] = UpdateSubscribeInput args_schema: Type[BaseModel] = UpdateSubscribeInput
require_admin: bool = True require_admin: bool = True
@@ -7,6 +7,7 @@ from typing import Any, Literal, Optional, Type, Union
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.agent.tools.impl._system_setting_utils import ( from app.agent.tools.impl._system_setting_utils import (
SettingSpec, SettingSpec,
get_default_list_match_field, get_default_list_match_field,
@@ -73,6 +74,12 @@ class UpdateSystemSettingsInput(BaseModel):
class UpdateSystemSettingsTool(MoviePilotTool): class UpdateSystemSettingsTool(MoviePilotTool):
name: str = "update_system_settings" name: str = "update_system_settings"
tags: list[str] = [
ToolTag.Write,
ToolTag.System,
ToolTag.Settings,
ToolTag.Admin,
]
description: str = ( description: str = (
"Update system settings across both the basic Settings module and all SystemConfig-backed categories. " "Update system settings across both the basic Settings module and all SystemConfig-backed categories. "
"Supports full replacement, shallow dict merge, and generic list item upsert/remove so the agent can manage downloaders, media servers, notification channels, storages, directories, search-site ranges, subscribe-site ranges, site auth params, AI agent config, and other system settings through one tool." "Supports full replacement, shallow dict merge, and generic list item upsert/remove so the agent can manage downloaders, media servers, notification channels, storages, directories, search-site ranges, subscribe-site ranges, site auth params, AI agent config, and other system settings through one tool."
+6
View File
@@ -7,6 +7,7 @@ from anyio import Path as AsyncPath
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.log import logger from app.log import logger
@@ -19,6 +20,11 @@ class WriteFileInput(BaseModel):
class WriteFileTool(MoviePilotTool): class WriteFileTool(MoviePilotTool):
name: str = "write_file" name: str = "write_file"
tags: list[str] = [
ToolTag.Write,
ToolTag.File,
ToolTag.Admin,
]
description: str = "Write full content to a file. If the file already exists, it will be overwritten. Automatically creates parent directories if they don't exist." description: str = "Write full content to a file. If the file already exists, it will be overwritten. Automatically creates parent directories if they don't exist."
args_schema: Type[BaseModel] = WriteFileInput args_schema: Type[BaseModel] = WriteFileInput
require_admin: bool = True require_admin: bool = True
+39
View File
@@ -0,0 +1,39 @@
"""Agent 工具标签定义。"""
from enum import Enum
class ToolTag(str, Enum):
"""Agent 工具能力标签。"""
AgentTool = "agent_tool"
Read = "read"
Write = "write"
Admin = "admin"
Message = "message"
UserInteraction = "user_interaction"
TerminalResponse = "terminal_response"
Media = "media"
Resource = "resource"
Site = "site"
Subscription = "subscription"
Download = "download"
Library = "library"
Transfer = "transfer"
System = "system"
Settings = "settings"
Plugin = "plugin"
Workflow = "workflow"
Scheduler = "scheduler"
File = "file"
Directory = "directory"
Web = "web"
Command = "command"
FilterRule = "filter_rule"
Persona = "persona"
SlashCommand = "slash_command"
Recommendation = "recommendation"
Metadata = "metadata"
__all__ = ["ToolTag"]
+4
View File
@@ -317,11 +317,13 @@ class AgentBackgroundOutputTest(unittest.IsolatedAsyncioTestCase):
user_id="system", user_id="system",
) )
agent._initialize_tools = lambda: [] agent._initialize_tools = lambda: []
agent._initialize_subagent_tools = lambda: []
with ( with (
patch.object(settings, "LLM_MAX_TOOLS", 0), patch.object(settings, "LLM_MAX_TOOLS", 0),
patch.object(agent, "_initialize_llm", new=AsyncMock(return_value=object())), patch.object(agent, "_initialize_llm", new=AsyncMock(return_value=object())),
patch("app.agent.prompt_manager.get_agent_prompt", return_value="PROMPT"), patch("app.agent.prompt_manager.get_agent_prompt", return_value="PROMPT"),
patch("app.agent.create_subagent_middlewares", return_value=([], [])),
patch( patch(
"app.agent.MoviePilotToolFactory.get_tool_selector_always_include_names", "app.agent.MoviePilotToolFactory.get_tool_selector_always_include_names",
return_value=[], return_value=[],
@@ -356,11 +358,13 @@ class AgentBackgroundOutputTest(unittest.IsolatedAsyncioTestCase):
async def test_create_agent_keeps_activity_log_for_normal_session(self): async def test_create_agent_keeps_activity_log_for_normal_session(self):
agent = MoviePilotAgent(session_id="normal-session", user_id="system") agent = MoviePilotAgent(session_id="normal-session", user_id="system")
agent._initialize_tools = lambda: [] agent._initialize_tools = lambda: []
agent._initialize_subagent_tools = lambda: []
with ( with (
patch.object(settings, "LLM_MAX_TOOLS", 0), patch.object(settings, "LLM_MAX_TOOLS", 0),
patch.object(agent, "_initialize_llm", new=AsyncMock(return_value=object())), patch.object(agent, "_initialize_llm", new=AsyncMock(return_value=object())),
patch("app.agent.prompt_manager.get_agent_prompt", return_value="PROMPT"), patch("app.agent.prompt_manager.get_agent_prompt", return_value="PROMPT"),
patch("app.agent.create_subagent_middlewares", return_value=([], [])),
patch( patch(
"app.agent.MoviePilotToolFactory.get_tool_selector_always_include_names", "app.agent.MoviePilotToolFactory.get_tool_selector_always_include_names",
return_value=[], return_value=[],
+87
View 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( patch.object(
agent_module.prompt_manager, "get_agent_prompt", return_value="prompt" 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, "create_agent", side_effect=_fake_create_agent),
patch.object(agent_module.settings, "LLM_MAX_TOOLS", 0), patch.object(agent_module.settings, "LLM_MAX_TOOLS", 0),
): ):
@@ -93,6 +96,9 @@ class TestAgentSummarizationStreaming(unittest.TestCase):
patch.object( patch.object(
agent_module.prompt_manager, "get_agent_prompt", return_value="prompt" agent_module.prompt_manager, "get_agent_prompt", return_value="prompt"
), ),
patch.object(
agent_module, "create_subagent_middlewares", return_value=([], [])
),
patch.object( patch.object(
agent_module, agent_module,
"ToolSelectorMiddleware", "ToolSelectorMiddleware",
@@ -138,6 +144,9 @@ class TestAgentSummarizationStreaming(unittest.TestCase):
patch.object( patch.object(
agent_module.prompt_manager, "get_agent_prompt", return_value="prompt" 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, "create_agent", side_effect=_fake_create_agent),
patch.object(agent_module.settings, "LLM_MAX_TOOLS", 0), patch.object(agent_module.settings, "LLM_MAX_TOOLS", 0),
): ):
@@ -167,6 +176,9 @@ class TestAgentSummarizationStreaming(unittest.TestCase):
patch.object( patch.object(
agent_module.prompt_manager, "get_agent_prompt", return_value="prompt" 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, "create_agent", side_effect=_fake_create_agent),
patch.object(agent_module.settings, "LLM_MAX_TOOLS", 0), patch.object(agent_module.settings, "LLM_MAX_TOOLS", 0),
): ):
+31
View File
@@ -9,6 +9,7 @@ if not hasattr(langchain_agents, "create_agent"):
langchain_agents.create_agent = lambda *args, **kwargs: None langchain_agents.create_agent = lambda *args, **kwargs: None
from app.agent.callback import StreamingHandler 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.base import MoviePilotTool
from app.agent.tools.impl.send_voice_message import SendVoiceMessageTool from app.agent.tools.impl.send_voice_message import SendVoiceMessageTool
from app.api.endpoints.openai import _OpenAIStreamingHandler from app.api.endpoints.openai import _OpenAIStreamingHandler
@@ -114,6 +115,36 @@ class TestAgentToolStreaming(unittest.TestCase):
"处理中:\n\n(执行了 2 次搜索,读取了 2 个文件)\n\n继续分析", "处理中:\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): def test_openai_streaming_handler_flushes_pending_summary_to_queue(self):
async def _run(): async def _run():
handler = _OpenAIStreamingHandler() handler = _OpenAIStreamingHandler()