mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-11 16:21:09 +08:00
233 lines
8.6 KiB
Python
233 lines
8.6 KiB
Python
"""Agent 人格统一管理工具。"""
|
|
|
|
import json
|
|
from typing import Literal, Optional, Type
|
|
|
|
from pydantic import BaseModel, Field, model_validator
|
|
|
|
from app.agent.runtime import agent_runtime_manager
|
|
from app.agent.tools.base import MoviePilotTool
|
|
from app.agent.tools.tags import ToolTag
|
|
from app.runtime.log import logger
|
|
|
|
PersonaAction = Literal["list", "switch", "update"]
|
|
|
|
|
|
class PersonaInput(BaseModel): # type: ignore[misc]
|
|
"""查询、切换或更新 Agent 人格的统一输入参数。"""
|
|
|
|
action: PersonaAction = Field(
|
|
...,
|
|
description="Action to perform: list, switch, or update.",
|
|
)
|
|
query: Optional[str] = Field(
|
|
None,
|
|
description=("Optional list filter for persona_id, label, description, or aliases."),
|
|
)
|
|
persona_id: Optional[str] = Field(
|
|
None,
|
|
description=(
|
|
"Target persona for switch or update. Existing labels and aliases are "
|
|
"accepted; new personas require a lowercase persona_id."
|
|
),
|
|
)
|
|
label: Optional[str] = Field(None, description="Replacement display label.")
|
|
description: Optional[str] = Field(
|
|
None,
|
|
description="Replacement description of the intended persona style.",
|
|
)
|
|
aliases: Optional[list[str]] = Field(
|
|
None,
|
|
description="Full replacement list of persona aliases.",
|
|
)
|
|
instructions: Optional[str] = Field(
|
|
None,
|
|
description="Full replacement PERSONA.md body without YAML frontmatter.",
|
|
)
|
|
append_instructions: Optional[list[str]] = Field(
|
|
None,
|
|
description="Additional persona rules appended to the existing body.",
|
|
)
|
|
create_if_missing: bool = Field(
|
|
False,
|
|
description="Create a new runtime persona when update cannot resolve one.",
|
|
)
|
|
|
|
@model_validator(mode="after") # type: ignore[misc]
|
|
def validate_action(self) -> "PersonaInput":
|
|
"""按动作校验目标人格和更新字段。"""
|
|
if self.query is not None:
|
|
self.query = self.query.strip() or None
|
|
if self.persona_id is not None:
|
|
self.persona_id = self.persona_id.strip()
|
|
if self.action in {"switch", "update"} and not self.persona_id:
|
|
raise ValueError(f"{self.action} 必须提供 persona_id")
|
|
if self.action == "update" and not any(
|
|
value is not None
|
|
for value in (
|
|
self.label,
|
|
self.description,
|
|
self.aliases,
|
|
self.instructions,
|
|
self.append_instructions,
|
|
)
|
|
):
|
|
raise ValueError("update 必须提供至少一个人格定义字段")
|
|
return self
|
|
|
|
|
|
class PersonaTool(MoviePilotTool):
|
|
"""通过一个动作式接口查询、切换或管理 Agent 人格。"""
|
|
|
|
name: str = "persona"
|
|
tags: list[str] = [ToolTag.Read, ToolTag.Write, ToolTag.Persona]
|
|
description: str = (
|
|
"Manage agent personas with one structured action. Use list to discover personas "
|
|
"and the active state, switch when the user explicitly requests a speaking style, "
|
|
"and update only for an explicit administrator request to create or rewrite a "
|
|
"persona definition."
|
|
)
|
|
args_schema: Type[BaseModel] = PersonaInput
|
|
|
|
def get_tool_message(self, **kwargs: object) -> str:
|
|
"""生成统一人格操作的用户可见提示。"""
|
|
action = str(kwargs.get("action") or "list")
|
|
action_name = {
|
|
"list": "查询人格",
|
|
"switch": "切换人格",
|
|
"update": "更新人格定义",
|
|
}.get(action, action)
|
|
target = kwargs.get("persona_id") or kwargs.get("query")
|
|
return f"{action_name}: {target}" if target else action_name
|
|
|
|
async def run( # type: ignore[override]
|
|
self,
|
|
action: PersonaAction,
|
|
query: Optional[str] = None,
|
|
persona_id: Optional[str] = None,
|
|
label: Optional[str] = None,
|
|
description: Optional[str] = None,
|
|
aliases: Optional[list[str]] = None,
|
|
instructions: Optional[str] = None,
|
|
append_instructions: Optional[list[str]] = None,
|
|
create_if_missing: bool = False,
|
|
**kwargs: object,
|
|
) -> str:
|
|
"""执行查询、切换或更新人格定义。"""
|
|
payload = PersonaInput(
|
|
action=action,
|
|
query=query,
|
|
persona_id=persona_id,
|
|
label=label,
|
|
description=description,
|
|
aliases=aliases,
|
|
instructions=instructions,
|
|
append_instructions=append_instructions,
|
|
create_if_missing=create_if_missing,
|
|
)
|
|
logger.info(
|
|
"执行工具: %s, 参数: action=%s, persona_id=%s",
|
|
self.name,
|
|
payload.action,
|
|
payload.persona_id,
|
|
)
|
|
try:
|
|
if payload.action == "list":
|
|
return self._list_personas(payload.query)
|
|
if payload.action == "switch":
|
|
return self._switch_persona(payload.persona_id)
|
|
if not await self.is_admin_user():
|
|
return json.dumps(
|
|
{
|
|
"success": False,
|
|
"message": "只有系统管理员才能更新人格定义。",
|
|
},
|
|
ensure_ascii=False,
|
|
)
|
|
return self._update_persona(payload)
|
|
except Exception as error: # noqa: BLE001
|
|
logger.error(
|
|
"人格操作失败: action=%s, error=%s",
|
|
payload.action,
|
|
error,
|
|
exc_info=True,
|
|
)
|
|
return json.dumps(
|
|
{
|
|
"success": False,
|
|
"message": f"人格操作失败: {str(error)}",
|
|
},
|
|
ensure_ascii=False,
|
|
)
|
|
|
|
@staticmethod
|
|
def _list_personas(query: Optional[str]) -> str:
|
|
"""列出可用人格并按可选关键词过滤。"""
|
|
runtime_config = agent_runtime_manager.load_runtime_config()
|
|
personas = runtime_config.list_personas()
|
|
if query:
|
|
normalized = query.casefold()
|
|
personas = [
|
|
persona
|
|
for persona in personas
|
|
if normalized in persona["persona_id"].casefold()
|
|
or normalized in persona["label"].casefold()
|
|
or normalized in persona["description"].casefold()
|
|
or any(normalized in alias.casefold() for alias in persona["aliases"])
|
|
]
|
|
return json.dumps(
|
|
{
|
|
"active_persona": runtime_config.active_persona,
|
|
"count": len(personas),
|
|
"personas": personas,
|
|
},
|
|
ensure_ascii=False,
|
|
indent=2,
|
|
)
|
|
|
|
@staticmethod
|
|
def _switch_persona(persona_id: Optional[str]) -> str:
|
|
"""切换当前持久化激活人格。"""
|
|
if persona_id is None:
|
|
raise ValueError("switch 缺少 persona_id")
|
|
runtime_config = agent_runtime_manager.set_active_persona(persona_id)
|
|
return json.dumps(
|
|
{
|
|
"success": True,
|
|
"active_persona": runtime_config.active_persona,
|
|
"persona": runtime_config.persona.to_dict(is_active=True),
|
|
"message": f"已切换为人格 `{runtime_config.active_persona}`",
|
|
},
|
|
ensure_ascii=False,
|
|
indent=2,
|
|
)
|
|
|
|
@staticmethod
|
|
def _update_persona(payload: PersonaInput) -> str:
|
|
"""创建或更新一项运行时人格定义。"""
|
|
if payload.persona_id is None:
|
|
raise ValueError("update 缺少 persona_id")
|
|
persona, created = agent_runtime_manager.update_persona_definition(
|
|
payload.persona_id,
|
|
label=payload.label,
|
|
description=payload.description,
|
|
aliases=payload.aliases,
|
|
instructions=payload.instructions,
|
|
append_instructions=payload.append_instructions,
|
|
create_if_missing=payload.create_if_missing,
|
|
)
|
|
runtime_config = agent_runtime_manager.load_runtime_config()
|
|
return json.dumps(
|
|
{
|
|
"success": True,
|
|
"created": created,
|
|
"active_persona": runtime_config.active_persona,
|
|
"persona": persona.to_dict(is_active=persona.persona_id == runtime_config.active_persona),
|
|
"message": (
|
|
f"已创建人格 `{persona.persona_id}`" if created else f"已更新人格 `{persona.persona_id}` 的定义"
|
|
),
|
|
},
|
|
ensure_ascii=False,
|
|
indent=2,
|
|
)
|