Refactor agent persona runtime layering

This commit is contained in:
jxxghp
2026-04-29 14:12:47 +08:00
parent 2c7fb5786c
commit 344280cd61
26 changed files with 1529 additions and 752 deletions

View 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,
)

View 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,
)

View 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,
)