mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-06-11 02:31:25 +08:00
Refactor agent persona runtime layering
This commit is contained in:
75
app/agent/tools/impl/query_personas.py
Normal file
75
app/agent/tools/impl/query_personas.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""查询可用人格工具。"""
|
||||
|
||||
import json
|
||||
from typing import Optional, Type
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.agent.runtime import agent_runtime_manager
|
||||
from app.agent.tools.base import MoviePilotTool
|
||||
from app.log import logger
|
||||
|
||||
|
||||
class QueryPersonasInput(BaseModel):
|
||||
"""查询人格工具的输入参数模型。"""
|
||||
|
||||
explanation: str = Field(
|
||||
...,
|
||||
description="Clear explanation of why this tool is being used in the current context",
|
||||
)
|
||||
query: Optional[str] = Field(
|
||||
None,
|
||||
description=(
|
||||
"Optional search keyword for persona_id, label, description, or aliases. "
|
||||
"Use this when the user asks for a certain speaking style but the exact persona name is unknown."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class QueryPersonasTool(MoviePilotTool):
|
||||
name: str = "query_personas"
|
||||
description: str = (
|
||||
"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 "
|
||||
"an exact persona_id. The result includes persona_id, label, description, aliases, and whether it is active."
|
||||
)
|
||||
args_schema: Type[BaseModel] = QueryPersonasInput
|
||||
|
||||
def get_tool_message(self, **kwargs) -> Optional[str]:
|
||||
query = kwargs.get("query")
|
||||
if query:
|
||||
return f"查询人格列表: {query}"
|
||||
return "查询人格列表"
|
||||
|
||||
async def run(self, query: Optional[str] = None, **kwargs) -> str:
|
||||
logger.info("执行工具: %s, 参数: query=%s", self.name, query)
|
||||
try:
|
||||
runtime_config = agent_runtime_manager.load_runtime_config()
|
||||
personas = runtime_config.list_personas()
|
||||
|
||||
if query:
|
||||
normalized = query.strip().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"])
|
||||
]
|
||||
|
||||
payload = {
|
||||
"active_persona": runtime_config.active_persona,
|
||||
"count": len(personas),
|
||||
"personas": personas,
|
||||
}
|
||||
return json.dumps(payload, ensure_ascii=False, indent=2)
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.error("查询人格列表失败: %s", e, exc_info=True)
|
||||
return json.dumps(
|
||||
{
|
||||
"success": False,
|
||||
"message": f"查询人格列表时发生错误: {str(e)}",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
62
app/agent/tools/impl/switch_persona.py
Normal file
62
app/agent/tools/impl/switch_persona.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""切换当前激活人格工具。"""
|
||||
|
||||
import json
|
||||
from typing import Type
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.agent.runtime import agent_runtime_manager
|
||||
from app.agent.tools.base import MoviePilotTool
|
||||
from app.log import logger
|
||||
|
||||
|
||||
class SwitchPersonaInput(BaseModel):
|
||||
"""切换人格工具的输入参数模型。"""
|
||||
|
||||
explanation: str = Field(
|
||||
...,
|
||||
description="Clear explanation of why this tool is being used in the current context",
|
||||
)
|
||||
persona_id: str = Field(
|
||||
...,
|
||||
description=(
|
||||
"The target persona to activate. This can be the exact persona_id, label, or one of the persona aliases. "
|
||||
"If the exact persona is unclear, call query_personas first."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class SwitchPersonaTool(MoviePilotTool):
|
||||
name: str = "switch_persona"
|
||||
description: str = (
|
||||
"Switch the active persona (人格) used by the agent runtime. "
|
||||
"This change is persistent for future turns. "
|
||||
"Use this when the user explicitly asks to change the speaking style, tone, or response persona. "
|
||||
"If the user asks for a vague style and you are not sure which persona matches best, call query_personas first."
|
||||
)
|
||||
args_schema: Type[BaseModel] = SwitchPersonaInput
|
||||
|
||||
def get_tool_message(self, **kwargs) -> str:
|
||||
persona_id = kwargs.get("persona_id") or "未知人格"
|
||||
return f"切换人格: {persona_id}"
|
||||
|
||||
async def run(self, persona_id: str, **kwargs) -> str:
|
||||
logger.info("执行工具: %s, 参数: persona_id=%s", self.name, persona_id)
|
||||
try:
|
||||
runtime_config = agent_runtime_manager.set_active_persona(persona_id)
|
||||
payload = {
|
||||
"success": True,
|
||||
"active_persona": runtime_config.active_persona,
|
||||
"persona": runtime_config.persona.to_dict(is_active=True),
|
||||
"message": f"已切换为人格 `{runtime_config.active_persona}`",
|
||||
}
|
||||
return json.dumps(payload, ensure_ascii=False, indent=2)
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.error("切换人格失败: %s", e, exc_info=True)
|
||||
return json.dumps(
|
||||
{
|
||||
"success": False,
|
||||
"message": f"切换人格时发生错误: {str(e)}",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
131
app/agent/tools/impl/update_persona_definition.py
Normal file
131
app/agent/tools/impl/update_persona_definition.py
Normal file
@@ -0,0 +1,131 @@
|
||||
"""更新人格定义工具。"""
|
||||
|
||||
import json
|
||||
from typing import Optional, Type
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.agent.runtime import agent_runtime_manager
|
||||
from app.agent.tools.base import MoviePilotTool
|
||||
from app.log import logger
|
||||
|
||||
|
||||
class UpdatePersonaDefinitionInput(BaseModel):
|
||||
"""更新人格定义工具的输入参数模型。"""
|
||||
|
||||
explanation: str = Field(
|
||||
...,
|
||||
description="Clear explanation of why this tool is being used in the current context",
|
||||
)
|
||||
persona_id: str = Field(
|
||||
...,
|
||||
description=(
|
||||
"Target persona to update. For existing personas this can be persona_id, label, or alias. "
|
||||
"For new personas, provide the new lowercase persona_id."
|
||||
),
|
||||
)
|
||||
label: Optional[str] = Field(
|
||||
None,
|
||||
description="Optional new label shown to users, such as 默认 or 说明型.",
|
||||
)
|
||||
description: Optional[str] = Field(
|
||||
None,
|
||||
description="Optional short description of the persona's intended style.",
|
||||
)
|
||||
aliases: Optional[list[str]] = Field(
|
||||
None,
|
||||
description="Optional full replacement list of aliases for this persona.",
|
||||
)
|
||||
instructions: Optional[str] = Field(
|
||||
None,
|
||||
description=(
|
||||
"Optional full replacement body for PERSONA.md, excluding YAML frontmatter. "
|
||||
"Use this when the persona definition should be rewritten completely."
|
||||
),
|
||||
)
|
||||
append_instructions: Optional[list[str]] = Field(
|
||||
None,
|
||||
description=(
|
||||
"Optional extra persona rules to append to the existing PERSONA body. "
|
||||
"Use this for small adjustments such as '回答更短' or '复杂问题给两步解释'."
|
||||
),
|
||||
)
|
||||
create_if_missing: bool = Field(
|
||||
False,
|
||||
description="Whether to create a new runtime persona if the target persona does not already exist.",
|
||||
)
|
||||
|
||||
|
||||
class UpdatePersonaDefinitionTool(MoviePilotTool):
|
||||
name: str = "update_persona_definition"
|
||||
description: str = (
|
||||
"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, "
|
||||
"rewriting the persona body, adjusting aliases, or creating a new persona."
|
||||
)
|
||||
args_schema: Type[BaseModel] = UpdatePersonaDefinitionInput
|
||||
require_admin: bool = True
|
||||
|
||||
def get_tool_message(self, **kwargs) -> str:
|
||||
persona_id = kwargs.get("persona_id") or "未知人格"
|
||||
action = "创建/更新人格定义"
|
||||
return f"{action}: {persona_id}"
|
||||
|
||||
async def run(
|
||||
self,
|
||||
persona_id: str,
|
||||
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,
|
||||
) -> str:
|
||||
logger.info("执行工具: %s, 参数: persona_id=%s", self.name, persona_id)
|
||||
if not any(
|
||||
value is not None
|
||||
for value in (label, description, aliases, instructions, append_instructions)
|
||||
):
|
||||
return json.dumps(
|
||||
{
|
||||
"success": False,
|
||||
"message": "未提供任何要更新的人格定义字段。",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
try:
|
||||
persona, created = agent_runtime_manager.update_persona_definition(
|
||||
persona_id,
|
||||
label=label,
|
||||
description=description,
|
||||
aliases=aliases,
|
||||
instructions=instructions,
|
||||
append_instructions=append_instructions,
|
||||
create_if_missing=create_if_missing,
|
||||
)
|
||||
runtime_config = agent_runtime_manager.load_runtime_config()
|
||||
payload = {
|
||||
"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}` 的定义"
|
||||
),
|
||||
}
|
||||
return json.dumps(payload, ensure_ascii=False, indent=2)
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.error("更新人格定义失败: %s", e, exc_info=True)
|
||||
return json.dumps(
|
||||
{
|
||||
"success": False,
|
||||
"message": f"更新人格定义时发生错误: {str(e)}",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
Reference in New Issue
Block a user