mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-06 16:07:01 +08:00
Configure subagent profiles from runtime files
This commit is contained in:
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
version: 1
|
||||||
|
subagent_id: download-diagnostician
|
||||||
|
label: 下载诊断
|
||||||
|
description: Download and transfer diagnosis subagent for downloaders, download tasks, transfer history, and library status.
|
||||||
|
include_tags:
|
||||||
|
- download
|
||||||
|
- transfer
|
||||||
|
- library
|
||||||
|
- directory
|
||||||
|
- file
|
||||||
|
- media
|
||||||
|
exclude_tags:
|
||||||
|
- write
|
||||||
|
- message
|
||||||
|
- user_interaction
|
||||||
|
---
|
||||||
|
# SUBAGENT
|
||||||
|
|
||||||
|
You specialize in downloaders, download tasks, transfer history, directory settings, and library ingestion state.
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
version: 1
|
||||||
|
subagent_id: general-purpose
|
||||||
|
label: 通用调查
|
||||||
|
description: General read-only investigation subagent for cross-domain MoviePilot analysis and execution recommendations.
|
||||||
|
include_tags:
|
||||||
|
- media
|
||||||
|
- resource
|
||||||
|
- site
|
||||||
|
- subscription
|
||||||
|
- download
|
||||||
|
- library
|
||||||
|
- transfer
|
||||||
|
- system
|
||||||
|
- settings
|
||||||
|
- plugin
|
||||||
|
- workflow
|
||||||
|
- scheduler
|
||||||
|
- file
|
||||||
|
- directory
|
||||||
|
- web
|
||||||
|
- command
|
||||||
|
- filter_rule
|
||||||
|
- persona
|
||||||
|
- slash_command
|
||||||
|
- recommendation
|
||||||
|
- metadata
|
||||||
|
exclude_tags:
|
||||||
|
- write
|
||||||
|
- message
|
||||||
|
- user_interaction
|
||||||
|
---
|
||||||
|
# SUBAGENT
|
||||||
|
|
||||||
|
You specialize in synthesizing media, site, subscription, download, and system status signals.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
version: 1
|
||||||
|
subagent_id: media-researcher
|
||||||
|
label: 媒体研究
|
||||||
|
description: Media research subagent for title recognition, people, episodes, metadata, and library existence checks.
|
||||||
|
include_tags:
|
||||||
|
- media
|
||||||
|
- library
|
||||||
|
- recommendation
|
||||||
|
- metadata
|
||||||
|
- web
|
||||||
|
exclude_tags:
|
||||||
|
- write
|
||||||
|
- message
|
||||||
|
- user_interaction
|
||||||
|
---
|
||||||
|
# SUBAGENT
|
||||||
|
|
||||||
|
You specialize in media identity resolution, metadata validation, person credits, and library status analysis.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
version: 1
|
||||||
|
subagent_id: moviepilot-explorer
|
||||||
|
label: 代码探索
|
||||||
|
description: MoviePilot exploration subagent for source-code inspection, configuration structure analysis, logs, and code-level troubleshooting clues.
|
||||||
|
include_tags:
|
||||||
|
- system
|
||||||
|
- settings
|
||||||
|
- file
|
||||||
|
- directory
|
||||||
|
- command
|
||||||
|
exclude_tags:
|
||||||
|
- write
|
||||||
|
- message
|
||||||
|
- user_interaction
|
||||||
|
---
|
||||||
|
# SUBAGENT
|
||||||
|
|
||||||
|
You specialize in MoviePilot source-code structure, local configuration files, directory layout, logs or read-only command output, and code-level root-cause troubleshooting. Prefer reading relevant code paths before judging behavior, and distinguish code/config evidence from runtime system state.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
version: 1
|
||||||
|
subagent_id: resource-searcher
|
||||||
|
label: 资源搜索
|
||||||
|
description: Site and resource search subagent for site checks, torrent search, and resource quality analysis.
|
||||||
|
include_tags:
|
||||||
|
- resource
|
||||||
|
- site
|
||||||
|
- web
|
||||||
|
- media
|
||||||
|
exclude_tags:
|
||||||
|
- write
|
||||||
|
- message
|
||||||
|
- user_interaction
|
||||||
|
---
|
||||||
|
# SUBAGENT
|
||||||
|
|
||||||
|
You specialize in site status, site user data, torrent search results, and resource quality judgment.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
version: 1
|
||||||
|
subagent_id: subscription-analyst
|
||||||
|
label: 订阅分析
|
||||||
|
description: Subscription analysis subagent for subscriptions, history, filter rules, and custom identifiers.
|
||||||
|
include_tags:
|
||||||
|
- subscription
|
||||||
|
- filter_rule
|
||||||
|
- settings
|
||||||
|
- media
|
||||||
|
exclude_tags:
|
||||||
|
- write
|
||||||
|
- message
|
||||||
|
- user_interaction
|
||||||
|
---
|
||||||
|
# SUBAGENT
|
||||||
|
|
||||||
|
You specialize in current subscription state, subscription history, filter rules, and subscription optimization suggestions.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
version: 1
|
||||||
|
subagent_id: system-diagnostician
|
||||||
|
label: 系统诊断
|
||||||
|
description: System diagnosis subagent for read-only inspection of settings, schedulers, workflows, plugins, directories, and command output.
|
||||||
|
include_tags:
|
||||||
|
- system
|
||||||
|
- settings
|
||||||
|
- plugin
|
||||||
|
- workflow
|
||||||
|
- scheduler
|
||||||
|
- file
|
||||||
|
- directory
|
||||||
|
- web
|
||||||
|
- command
|
||||||
|
- persona
|
||||||
|
- slash_command
|
||||||
|
exclude_tags:
|
||||||
|
- write
|
||||||
|
- message
|
||||||
|
- user_interaction
|
||||||
|
---
|
||||||
|
# SUBAGENT
|
||||||
|
|
||||||
|
You specialize in settings, plugins, scheduled tasks, workflows, directories, and read-only command diagnostics.
|
||||||
@@ -24,6 +24,7 @@ from langchain_core.tools import BaseTool, StructuredTool
|
|||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from app.agent.middleware.utils import append_to_system_message
|
from app.agent.middleware.utils import append_to_system_message
|
||||||
|
from app.agent.runtime import SubAgentDefinition, agent_runtime_manager
|
||||||
from app.agent.tools.tags import ToolTag
|
from app.agent.tools.tags import ToolTag
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
|
|
||||||
@@ -87,7 +88,7 @@ Requirements:
|
|||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class _SubAgentProfile:
|
class _SubAgentProfile:
|
||||||
"""内置子代理定义。"""
|
"""子代理运行时定义。"""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
description: str
|
description: str
|
||||||
@@ -197,40 +198,15 @@ def builtin_subagent_names() -> frozenset[str]:
|
|||||||
|
|
||||||
@lru_cache(maxsize=1)
|
@lru_cache(maxsize=1)
|
||||||
def _builtin_subagent_profiles() -> tuple[_SubAgentProfile, ...]:
|
def _builtin_subagent_profiles() -> tuple[_SubAgentProfile, ...]:
|
||||||
"""构建 MoviePilot 默认内置子代理定义。"""
|
"""从运行时配置目录加载 MoviePilot 子代理定义。"""
|
||||||
default_exclude_tags = frozenset(
|
definitions = agent_runtime_manager.list_subagents()
|
||||||
{
|
profiles = tuple(
|
||||||
ToolTag.Write.value,
|
_profile_from_runtime_definition(definition)
|
||||||
ToolTag.Message.value,
|
for definition in definitions
|
||||||
ToolTag.UserInteraction.value,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
general_tags = frozenset(
|
if profiles:
|
||||||
{
|
return profiles
|
||||||
ToolTag.Media.value,
|
logger.warning("未加载到任何子代理定义,使用通用兜底子代理。")
|
||||||
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 (
|
return (
|
||||||
_SubAgentProfile(
|
_SubAgentProfile(
|
||||||
name="general-purpose",
|
name="general-purpose",
|
||||||
@@ -239,126 +215,34 @@ def _builtin_subagent_profiles() -> tuple[_SubAgentProfile, ...]:
|
|||||||
f"{SUBAGENT_BASE_PROMPT}\n"
|
f"{SUBAGENT_BASE_PROMPT}\n"
|
||||||
"You specialize in synthesizing media, site, subscription, download, and system status signals."
|
"You specialize in synthesizing media, site, subscription, download, and system status signals."
|
||||||
),
|
),
|
||||||
include_tags=general_tags,
|
include_tags=frozenset(tag.value for tag in ToolTag),
|
||||||
exclude_tags=default_exclude_tags,
|
exclude_tags=frozenset(
|
||||||
),
|
|
||||||
_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.Write.value,
|
||||||
ToolTag.Library.value,
|
ToolTag.Message.value,
|
||||||
ToolTag.Recommendation.value,
|
ToolTag.UserInteraction.value,
|
||||||
ToolTag.Metadata.value,
|
|
||||||
ToolTag.Web.value,
|
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
exclude_tags=default_exclude_tags,
|
|
||||||
),
|
|
||||||
_SubAgentProfile(
|
|
||||||
name="moviepilot-explorer",
|
|
||||||
description="MoviePilot exploration subagent for source-code inspection, configuration structure analysis, logs, and code-level troubleshooting clues.",
|
|
||||||
prompt=(
|
|
||||||
f"{SUBAGENT_BASE_PROMPT}\n"
|
|
||||||
"You specialize in MoviePilot source-code structure, local configuration files, directory layout, logs or read-only command output, and code-level root-cause troubleshooting. "
|
|
||||||
"Prefer reading relevant code paths before judging behavior, and distinguish code/config evidence from runtime system state."
|
|
||||||
),
|
|
||||||
include_tags=frozenset(
|
|
||||||
{
|
|
||||||
ToolTag.System.value,
|
|
||||||
ToolTag.Settings.value,
|
|
||||||
ToolTag.File.value,
|
|
||||||
ToolTag.Directory.value,
|
|
||||||
ToolTag.Command.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 _profile_from_runtime_definition(
|
||||||
|
definition: SubAgentDefinition,
|
||||||
|
) -> _SubAgentProfile:
|
||||||
|
"""把运行时子代理定义转换为中间件可用的 profile。"""
|
||||||
|
prompt_parts = [SUBAGENT_BASE_PROMPT]
|
||||||
|
if definition.text.strip():
|
||||||
|
prompt_parts.append(definition.text.strip())
|
||||||
|
return _SubAgentProfile(
|
||||||
|
name=definition.subagent_id,
|
||||||
|
description=definition.description,
|
||||||
|
prompt="\n".join(prompt_parts),
|
||||||
|
include_tags=frozenset(definition.include_tags),
|
||||||
|
exclude_tags=frozenset(definition.exclude_tags),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _tool_tag_values(tool: BaseTool) -> set[str]:
|
def _tool_tag_values(tool: BaseTool) -> set[str]:
|
||||||
"""读取工具实例上的标签集合。"""
|
"""读取工具实例上的标签集合。"""
|
||||||
tags = getattr(tool, "tags", None) or []
|
tags = getattr(tool, "tags", None) or []
|
||||||
@@ -1055,6 +939,8 @@ def create_subagent_middlewares(
|
|||||||
stream_handler: Any = None,
|
stream_handler: Any = None,
|
||||||
) -> tuple[list[AgentMiddleware], list[BaseTool]]:
|
) -> tuple[list[AgentMiddleware], list[BaseTool]]:
|
||||||
"""创建子代理中间件列表和任务工具列表。"""
|
"""创建子代理中间件列表和任务工具列表。"""
|
||||||
|
_builtin_subagent_profiles.cache_clear()
|
||||||
|
builtin_subagent_names.cache_clear()
|
||||||
profiles = _builtin_subagent_profiles()
|
profiles = _builtin_subagent_profiles()
|
||||||
subagent_middleware = _try_create_deepagents_middleware(
|
subagent_middleware = _try_create_deepagents_middleware(
|
||||||
profiles=profiles,
|
profiles=profiles,
|
||||||
|
|||||||
+172
-1
@@ -22,8 +22,11 @@ JOBS_DIR = "jobs"
|
|||||||
ACTIVITY_DIR = "activity"
|
ACTIVITY_DIR = "activity"
|
||||||
PERSONAS_DIR = "personas"
|
PERSONAS_DIR = "personas"
|
||||||
PERSONA_FILE = "PERSONA.md"
|
PERSONA_FILE = "PERSONA.md"
|
||||||
|
SUBAGENTS_DIR = "subagents"
|
||||||
|
SUBAGENT_FILE = "SUBAGENT.md"
|
||||||
CURRENT_PERSONA_SCHEMA_VERSION = 3
|
CURRENT_PERSONA_SCHEMA_VERSION = 3
|
||||||
PERSONA_SCHEMA_VERSION = 1
|
PERSONA_SCHEMA_VERSION = 1
|
||||||
|
SUBAGENT_SCHEMA_VERSION = 1
|
||||||
DEFAULT_PERSONA_ID = "default"
|
DEFAULT_PERSONA_ID = "default"
|
||||||
PERSONA_ID_PATTERN = re.compile(r"^[a-z0-9][a-z0-9_-]{0,63}$")
|
PERSONA_ID_PATTERN = re.compile(r"^[a-z0-9][a-z0-9_-]{0,63}$")
|
||||||
|
|
||||||
@@ -111,6 +114,41 @@ class PersonaDefinition:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SubAgentDefinition:
|
||||||
|
"""单个子代理定义。"""
|
||||||
|
|
||||||
|
subagent_id: str
|
||||||
|
path: Path
|
||||||
|
description: str
|
||||||
|
text: str
|
||||||
|
include_tags: list[str]
|
||||||
|
exclude_tags: list[str]
|
||||||
|
version: int = SUBAGENT_SCHEMA_VERSION
|
||||||
|
label: str = ""
|
||||||
|
|
||||||
|
def summary_line(self) -> str:
|
||||||
|
"""渲染可读的一行子代理摘要。"""
|
||||||
|
parts = [f"`{self.subagent_id}`"]
|
||||||
|
if self.label and self.label != self.subagent_id:
|
||||||
|
parts.append(self.label)
|
||||||
|
if self.description:
|
||||||
|
parts.append(self.description)
|
||||||
|
return " - ".join(parts)
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
"""输出给查询或调试入口的结构化信息。"""
|
||||||
|
return {
|
||||||
|
"subagent_id": self.subagent_id,
|
||||||
|
"label": self.label,
|
||||||
|
"description": self.description,
|
||||||
|
"include_tags": self.include_tags,
|
||||||
|
"exclude_tags": self.exclude_tags,
|
||||||
|
"version": self.version,
|
||||||
|
"path": str(self.path),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class AgentRuntimeConfig:
|
class AgentRuntimeConfig:
|
||||||
"""一次加载后的根层配置快照。"""
|
"""一次加载后的根层配置快照。"""
|
||||||
@@ -120,6 +158,7 @@ class AgentRuntimeConfig:
|
|||||||
current_persona_path: Path
|
current_persona_path: Path
|
||||||
persona: PersonaDefinition
|
persona: PersonaDefinition
|
||||||
available_personas: list[PersonaDefinition]
|
available_personas: list[PersonaDefinition]
|
||||||
|
available_subagents: list[SubAgentDefinition]
|
||||||
extra_context_paths: list[Path]
|
extra_context_paths: list[Path]
|
||||||
extra_contexts: list[tuple[Path, str]]
|
extra_contexts: list[tuple[Path, str]]
|
||||||
warnings: list[str] = field(default_factory=list)
|
warnings: list[str] = field(default_factory=list)
|
||||||
@@ -135,6 +174,12 @@ class AgentRuntimeConfig:
|
|||||||
if self.available_personas:
|
if self.available_personas:
|
||||||
sections.append("- Available personas:")
|
sections.append("- Available personas:")
|
||||||
sections.extend(f" - {persona.summary_line()}" for persona in self.available_personas)
|
sections.extend(f" - {persona.summary_line()}" for persona in self.available_personas)
|
||||||
|
if self.available_subagents:
|
||||||
|
sections.append("- Available subagents:")
|
||||||
|
sections.extend(
|
||||||
|
f" - {subagent.summary_line()}"
|
||||||
|
for subagent in self.available_subagents
|
||||||
|
)
|
||||||
sections.append("</agent_runtime_config>")
|
sections.append("</agent_runtime_config>")
|
||||||
|
|
||||||
if self.warnings:
|
if self.warnings:
|
||||||
@@ -201,6 +246,7 @@ class AgentRuntimeManager:
|
|||||||
self.skills_dir = self.agent_root_dir / SKILLS_DIR
|
self.skills_dir = self.agent_root_dir / SKILLS_DIR
|
||||||
self.jobs_dir = self.agent_root_dir / JOBS_DIR
|
self.jobs_dir = self.agent_root_dir / JOBS_DIR
|
||||||
self.activity_dir = self.agent_root_dir / ACTIVITY_DIR
|
self.activity_dir = self.agent_root_dir / ACTIVITY_DIR
|
||||||
|
self.subagents_dir = self.runtime_dir / SUBAGENTS_DIR
|
||||||
self.bundled_defaults_dir = bundled_defaults_dir or (
|
self.bundled_defaults_dir = bundled_defaults_dir or (
|
||||||
Path(__file__).parent / "defaults"
|
Path(__file__).parent / "defaults"
|
||||||
)
|
)
|
||||||
@@ -216,6 +262,7 @@ class AgentRuntimeManager:
|
|||||||
self.skills_dir.mkdir(parents=True, exist_ok=True)
|
self.skills_dir.mkdir(parents=True, exist_ok=True)
|
||||||
self.jobs_dir.mkdir(parents=True, exist_ok=True)
|
self.jobs_dir.mkdir(parents=True, exist_ok=True)
|
||||||
self.activity_dir.mkdir(parents=True, exist_ok=True)
|
self.activity_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
self.subagents_dir.mkdir(parents=True, exist_ok=True)
|
||||||
self._migrate_root_runtime_files()
|
self._migrate_root_runtime_files()
|
||||||
self._remove_obsolete_runtime_files()
|
self._remove_obsolete_runtime_files()
|
||||||
self._sync_bundled_defaults()
|
self._sync_bundled_defaults()
|
||||||
@@ -278,6 +325,10 @@ class AgentRuntimeManager:
|
|||||||
"""列出当前可用人格。"""
|
"""列出当前可用人格。"""
|
||||||
return self.load_runtime_config().available_personas
|
return self.load_runtime_config().available_personas
|
||||||
|
|
||||||
|
def list_subagents(self) -> list[SubAgentDefinition]:
|
||||||
|
"""列出当前可用子代理。"""
|
||||||
|
return self.load_runtime_config().available_subagents
|
||||||
|
|
||||||
def update_persona_definition(
|
def update_persona_definition(
|
||||||
self,
|
self,
|
||||||
persona_query: str,
|
persona_query: str,
|
||||||
@@ -382,7 +433,7 @@ class AgentRuntimeManager:
|
|||||||
return tuple(entries)
|
return tuple(entries)
|
||||||
|
|
||||||
def _sync_bundled_defaults(self) -> None:
|
def _sync_bundled_defaults(self) -> None:
|
||||||
"""仅复制缺失的默认运行时文件,避免覆盖用户自定义。"""
|
"""同步默认运行时文件,并按版本更新内置子代理定义。"""
|
||||||
if not self.bundled_defaults_dir.exists():
|
if not self.bundled_defaults_dir.exists():
|
||||||
return
|
return
|
||||||
for path in sorted(self.bundled_defaults_dir.rglob("*")):
|
for path in sorted(self.bundled_defaults_dir.rglob("*")):
|
||||||
@@ -392,11 +443,43 @@ class AgentRuntimeManager:
|
|||||||
target.mkdir(parents=True, exist_ok=True)
|
target.mkdir(parents=True, exist_ok=True)
|
||||||
continue
|
continue
|
||||||
if target.exists():
|
if target.exists():
|
||||||
|
if self._should_update_bundled_subagent(relative, path, target):
|
||||||
|
shutil.copy2(path, target)
|
||||||
|
logger.info(f"已更新默认 Agent 子代理定义: {target}")
|
||||||
continue
|
continue
|
||||||
target.parent.mkdir(parents=True, exist_ok=True)
|
target.parent.mkdir(parents=True, exist_ok=True)
|
||||||
shutil.copy2(path, target)
|
shutil.copy2(path, target)
|
||||||
logger.info("已同步默认 Agent 运行时文件: %s", target)
|
logger.info("已同步默认 Agent 运行时文件: %s", target)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _should_update_bundled_subagent(
|
||||||
|
cls,
|
||||||
|
relative_path: Path,
|
||||||
|
source_path: Path,
|
||||||
|
target_path: Path,
|
||||||
|
) -> bool:
|
||||||
|
"""判断是否需要用更高版本的内置子代理定义覆盖用户目录副本。"""
|
||||||
|
parts = relative_path.parts
|
||||||
|
if len(parts) < 3 or parts[0] != SUBAGENTS_DIR or relative_path.name != SUBAGENT_FILE:
|
||||||
|
return False
|
||||||
|
|
||||||
|
source_version = cls._read_markdown_version(source_path)
|
||||||
|
target_version = cls._read_markdown_version(target_path)
|
||||||
|
return source_version > target_version
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _read_markdown_version(path: Path) -> int:
|
||||||
|
"""读取 Markdown frontmatter 中的整数版本,失败时按 0 处理。"""
|
||||||
|
try:
|
||||||
|
document = AgentRuntimeManager._read_markdown(path)
|
||||||
|
except AgentRuntimeConfigError as err:
|
||||||
|
logger.warning(f"读取 Agent 运行时文件版本失败 {path}: {err}")
|
||||||
|
return 0
|
||||||
|
return AgentRuntimeManager._coerce_int_metadata(
|
||||||
|
document.metadata.get("version"),
|
||||||
|
default=0,
|
||||||
|
)
|
||||||
|
|
||||||
def _migrate_root_runtime_files(self) -> None:
|
def _migrate_root_runtime_files(self) -> None:
|
||||||
"""兼容早期直接放在 `config/agent` 根目录的 CURRENT_PERSONA。"""
|
"""兼容早期直接放在 `config/agent` 根目录的 CURRENT_PERSONA。"""
|
||||||
source = self.agent_root_dir / CURRENT_PERSONA_FILE
|
source = self.agent_root_dir / CURRENT_PERSONA_FILE
|
||||||
@@ -451,6 +534,7 @@ class AgentRuntimeManager:
|
|||||||
|
|
||||||
available_personas = self._load_personas(root)
|
available_personas = self._load_personas(root)
|
||||||
persona = self._resolve_persona_definition(active_persona, available_personas)
|
persona = self._resolve_persona_definition(active_persona, available_personas)
|
||||||
|
available_subagents = self._load_subagents(root)
|
||||||
extra_contexts = [
|
extra_contexts = [
|
||||||
(path, self._read_markdown(path).body)
|
(path, self._read_markdown(path).body)
|
||||||
for path in extra_context_paths
|
for path in extra_context_paths
|
||||||
@@ -468,6 +552,7 @@ class AgentRuntimeManager:
|
|||||||
current_persona_path=current_persona_path,
|
current_persona_path=current_persona_path,
|
||||||
persona=persona,
|
persona=persona,
|
||||||
available_personas=available_personas,
|
available_personas=available_personas,
|
||||||
|
available_subagents=available_subagents,
|
||||||
extra_context_paths=extra_context_paths,
|
extra_context_paths=extra_context_paths,
|
||||||
extra_contexts=extra_contexts,
|
extra_contexts=extra_contexts,
|
||||||
warnings=warnings,
|
warnings=warnings,
|
||||||
@@ -513,6 +598,71 @@ class AgentRuntimeManager:
|
|||||||
raise AgentRuntimeConfigError(f"{personas_root} 中未找到任何人格定义")
|
raise AgentRuntimeConfigError(f"{personas_root} 中未找到任何人格定义")
|
||||||
return personas
|
return personas
|
||||||
|
|
||||||
|
def _load_subagents(self, root: Path) -> list[SubAgentDefinition]:
|
||||||
|
"""扫描并解析所有可用子代理。"""
|
||||||
|
subagents_root = root / SUBAGENTS_DIR
|
||||||
|
if not subagents_root.exists():
|
||||||
|
raise AgentRuntimeConfigError(f"缺少 subagents 目录: {subagents_root}")
|
||||||
|
|
||||||
|
subagents: list[SubAgentDefinition] = []
|
||||||
|
seen_ids: set[str] = set()
|
||||||
|
for subagent_dir in sorted(subagents_root.iterdir()):
|
||||||
|
if not subagent_dir.is_dir():
|
||||||
|
continue
|
||||||
|
subagent_path = subagent_dir / SUBAGENT_FILE
|
||||||
|
if not subagent_path.exists():
|
||||||
|
continue
|
||||||
|
document = self._read_markdown(subagent_path)
|
||||||
|
subagent_id = str(
|
||||||
|
document.metadata.get("subagent_id") or subagent_dir.name
|
||||||
|
).strip()
|
||||||
|
if not subagent_id:
|
||||||
|
raise AgentRuntimeConfigError(f"{subagent_path} 缺少 subagent_id")
|
||||||
|
if not PERSONA_ID_PATTERN.fullmatch(subagent_id):
|
||||||
|
raise AgentRuntimeConfigError(
|
||||||
|
f"{subagent_path} 的 subagent_id 只能使用小写字母、数字、下划线和中划线,且必须以字母或数字开头"
|
||||||
|
)
|
||||||
|
if subagent_id in seen_ids:
|
||||||
|
raise AgentRuntimeConfigError(f"检测到重复的子代理 ID: {subagent_id}")
|
||||||
|
seen_ids.add(subagent_id)
|
||||||
|
|
||||||
|
description = str(document.metadata.get("description") or "").strip()
|
||||||
|
if not description:
|
||||||
|
raise AgentRuntimeConfigError(f"{subagent_path} 缺少 description")
|
||||||
|
include_tags = self._normalize_string_list(
|
||||||
|
document.metadata.get("include_tags"),
|
||||||
|
f"{subagent_path}.include_tags",
|
||||||
|
)
|
||||||
|
if not include_tags:
|
||||||
|
raise AgentRuntimeConfigError(f"{subagent_path} 缺少 include_tags")
|
||||||
|
exclude_tags = self._normalize_string_list(
|
||||||
|
document.metadata.get("exclude_tags"),
|
||||||
|
f"{subagent_path}.exclude_tags",
|
||||||
|
)
|
||||||
|
text = self._normalize_subagent_body(document.body)
|
||||||
|
if not text:
|
||||||
|
raise AgentRuntimeConfigError(f"{subagent_path} 子代理正文不能为空")
|
||||||
|
|
||||||
|
subagents.append(
|
||||||
|
SubAgentDefinition(
|
||||||
|
subagent_id=subagent_id,
|
||||||
|
path=subagent_path,
|
||||||
|
label=str(document.metadata.get("label") or subagent_id).strip(),
|
||||||
|
description=description,
|
||||||
|
text=text,
|
||||||
|
include_tags=include_tags,
|
||||||
|
exclude_tags=exclude_tags,
|
||||||
|
version=self._coerce_int_metadata(
|
||||||
|
document.metadata.get("version"),
|
||||||
|
default=SUBAGENT_SCHEMA_VERSION,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not subagents:
|
||||||
|
raise AgentRuntimeConfigError(f"{subagents_root} 中未找到任何子代理定义")
|
||||||
|
return subagents
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _resolve_persona_definition(
|
def _resolve_persona_definition(
|
||||||
persona_query: str,
|
persona_query: str,
|
||||||
@@ -653,6 +803,27 @@ class AgentRuntimeManager:
|
|||||||
return remainder.strip()
|
return remainder.strip()
|
||||||
return normalized
|
return normalized
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _normalize_subagent_body(body: Optional[str]) -> str:
|
||||||
|
"""去掉重复的 SUBAGENT 标题,保持正文可安全加载。"""
|
||||||
|
normalized = (body or "").strip()
|
||||||
|
if not normalized:
|
||||||
|
return ""
|
||||||
|
if normalized.startswith("# SUBAGENT"):
|
||||||
|
_, _, remainder = normalized.partition("\n")
|
||||||
|
return remainder.strip()
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _coerce_int_metadata(value: Any, *, default: int = 0) -> int:
|
||||||
|
"""将 frontmatter 中的整数型元数据规范化。"""
|
||||||
|
if value is None:
|
||||||
|
return default
|
||||||
|
try:
|
||||||
|
return int(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return default
|
||||||
|
|
||||||
def _validate_runtime_config(
|
def _validate_runtime_config(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -45,6 +45,22 @@ class TestAgentRuntimeConfig(unittest.TestCase):
|
|||||||
/ "PERSONA.md"
|
/ "PERSONA.md"
|
||||||
).exists()
|
).exists()
|
||||||
)
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
(
|
||||||
|
self.agent_root
|
||||||
|
/ "runtime"
|
||||||
|
/ "subagents"
|
||||||
|
/ "general-purpose"
|
||||||
|
/ "SUBAGENT.md"
|
||||||
|
).exists()
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
"media-researcher",
|
||||||
|
[
|
||||||
|
subagent.subagent_id
|
||||||
|
for subagent in runtime_config.available_subagents
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
def test_legacy_root_markdown_is_migrated_to_memory_directory(self):
|
def test_legacy_root_markdown_is_migrated_to_memory_directory(self):
|
||||||
self.agent_root.mkdir(parents=True, exist_ok=True)
|
self.agent_root.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -109,6 +125,8 @@ class TestAgentRuntimeConfig(unittest.TestCase):
|
|||||||
self.assertIn("<agent_persona>", sections)
|
self.assertIn("<agent_persona>", sections)
|
||||||
self.assertIn("Active persona: `default`", sections)
|
self.assertIn("Active persona: `default`", sections)
|
||||||
self.assertIn("`guide`", sections)
|
self.assertIn("`guide`", sections)
|
||||||
|
self.assertIn("Available subagents:", sections)
|
||||||
|
self.assertIn("`media-researcher`", sections)
|
||||||
|
|
||||||
def test_set_active_persona_supports_id_and_alias(self):
|
def test_set_active_persona_supports_id_and_alias(self):
|
||||||
manager = self._manager()
|
manager = self._manager()
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
import textwrap
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from app.agent.runtime import AgentRuntimeManager
|
||||||
|
import app.agent.middleware.subagents as subagent_module
|
||||||
|
|
||||||
|
|
||||||
|
def _write_current_persona(defaults_root: Path) -> None:
|
||||||
|
"""写入最小可用的人格激活配置。"""
|
||||||
|
defaults_root.mkdir(parents=True, exist_ok=True)
|
||||||
|
(defaults_root / "CURRENT_PERSONA.md").write_text(
|
||||||
|
textwrap.dedent(
|
||||||
|
"""\
|
||||||
|
---
|
||||||
|
version: 3
|
||||||
|
active_persona: default
|
||||||
|
extra_context_files: []
|
||||||
|
deprecated_phrases: []
|
||||||
|
---
|
||||||
|
# CURRENT_PERSONA
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
persona_dir = defaults_root / "personas" / "default"
|
||||||
|
persona_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
(persona_dir / "PERSONA.md").write_text(
|
||||||
|
textwrap.dedent(
|
||||||
|
"""\
|
||||||
|
---
|
||||||
|
version: 1
|
||||||
|
persona_id: default
|
||||||
|
label: 默认
|
||||||
|
description: 默认人格
|
||||||
|
aliases: []
|
||||||
|
---
|
||||||
|
# PERSONA
|
||||||
|
|
||||||
|
测试人格。
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _write_subagent(
|
||||||
|
root: Path,
|
||||||
|
subagent_id: str,
|
||||||
|
*,
|
||||||
|
version: int = 1,
|
||||||
|
description: str = "测试子代理",
|
||||||
|
body: str = "测试子代理提示。",
|
||||||
|
) -> Path:
|
||||||
|
"""写入一个子代理定义文件。"""
|
||||||
|
subagent_dir = root / "subagents" / subagent_id
|
||||||
|
subagent_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
path = subagent_dir / "SUBAGENT.md"
|
||||||
|
path.write_text(
|
||||||
|
textwrap.dedent(
|
||||||
|
f"""\
|
||||||
|
---
|
||||||
|
version: {version}
|
||||||
|
subagent_id: {subagent_id}
|
||||||
|
label: 测试
|
||||||
|
description: {description}
|
||||||
|
include_tags:
|
||||||
|
- media
|
||||||
|
- web
|
||||||
|
exclude_tags:
|
||||||
|
- write
|
||||||
|
- message
|
||||||
|
---
|
||||||
|
# SUBAGENT
|
||||||
|
|
||||||
|
{body}
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def test_runtime_syncs_and_parses_subagent_definitions(tmp_path):
|
||||||
|
"""运行时应同步并解析 defaults/subagents 下的子代理定义。"""
|
||||||
|
defaults_root = tmp_path / "defaults"
|
||||||
|
_write_current_persona(defaults_root)
|
||||||
|
_write_subagent(
|
||||||
|
defaults_root,
|
||||||
|
"custom-reader",
|
||||||
|
description="Custom reader subagent.",
|
||||||
|
body="Only inspect custom media signals.",
|
||||||
|
)
|
||||||
|
|
||||||
|
manager = AgentRuntimeManager(
|
||||||
|
agent_root_dir=tmp_path / "agent",
|
||||||
|
bundled_defaults_dir=defaults_root,
|
||||||
|
)
|
||||||
|
runtime_config = manager.load_runtime_config()
|
||||||
|
|
||||||
|
copied_path = (
|
||||||
|
tmp_path
|
||||||
|
/ "agent"
|
||||||
|
/ "runtime"
|
||||||
|
/ "subagents"
|
||||||
|
/ "custom-reader"
|
||||||
|
/ "SUBAGENT.md"
|
||||||
|
)
|
||||||
|
subagent = runtime_config.available_subagents[0]
|
||||||
|
assert copied_path.exists()
|
||||||
|
assert subagent.subagent_id == "custom-reader"
|
||||||
|
assert subagent.description == "Custom reader subagent."
|
||||||
|
assert subagent.include_tags == ["media", "web"]
|
||||||
|
assert subagent.exclude_tags == ["write", "message"]
|
||||||
|
assert "Only inspect custom media signals." in subagent.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_runtime_updates_bundled_subagent_when_version_increases(tmp_path):
|
||||||
|
"""内置子代理版本升高时应覆盖用户目录里的旧版副本。"""
|
||||||
|
defaults_root = tmp_path / "defaults"
|
||||||
|
_write_current_persona(defaults_root)
|
||||||
|
_write_subagent(
|
||||||
|
defaults_root,
|
||||||
|
"custom-reader",
|
||||||
|
version=1,
|
||||||
|
body="version one",
|
||||||
|
)
|
||||||
|
|
||||||
|
manager = AgentRuntimeManager(
|
||||||
|
agent_root_dir=tmp_path / "agent",
|
||||||
|
bundled_defaults_dir=defaults_root,
|
||||||
|
)
|
||||||
|
manager.ensure_layout()
|
||||||
|
|
||||||
|
_write_subagent(
|
||||||
|
defaults_root,
|
||||||
|
"custom-reader",
|
||||||
|
version=2,
|
||||||
|
body="version two",
|
||||||
|
)
|
||||||
|
manager.invalidate_cache()
|
||||||
|
runtime_config = manager.load_runtime_config()
|
||||||
|
|
||||||
|
subagent = runtime_config.available_subagents[0]
|
||||||
|
assert subagent.version == 2
|
||||||
|
assert "version two" in subagent.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_middleware_profiles_are_loaded_from_runtime_config(monkeypatch, tmp_path):
|
||||||
|
"""子代理中间件应从运行时 YAML 定义生成 profile。"""
|
||||||
|
defaults_root = tmp_path / "defaults"
|
||||||
|
_write_current_persona(defaults_root)
|
||||||
|
_write_subagent(
|
||||||
|
defaults_root,
|
||||||
|
"custom-reader",
|
||||||
|
description="Runtime custom reader.",
|
||||||
|
body="Runtime-only prompt.",
|
||||||
|
)
|
||||||
|
manager = AgentRuntimeManager(
|
||||||
|
agent_root_dir=tmp_path / "agent",
|
||||||
|
bundled_defaults_dir=defaults_root,
|
||||||
|
)
|
||||||
|
|
||||||
|
monkeypatch.setattr(subagent_module, "agent_runtime_manager", manager)
|
||||||
|
subagent_module._builtin_subagent_profiles.cache_clear()
|
||||||
|
subagent_module.builtin_subagent_names.cache_clear()
|
||||||
|
|
||||||
|
profiles = subagent_module._builtin_subagent_profiles()
|
||||||
|
|
||||||
|
assert [profile.name for profile in profiles] == ["custom-reader"]
|
||||||
|
assert profiles[0].description == "Runtime custom reader."
|
||||||
|
assert profiles[0].include_tags == frozenset({"media", "web"})
|
||||||
|
assert "Runtime-only prompt." in profiles[0].prompt
|
||||||
Reference in New Issue
Block a user