feat(agent): add configurable output language

This commit is contained in:
jxxghp
2026-10-05 07:35:05 +08:00
parent 0ce5d97d84
commit 6f0f528e2f
8 changed files with 118 additions and 42 deletions

View File

@@ -1657,6 +1657,7 @@ class MoviePilotAgent:
self.has_message_context,
self.is_background,
get_runtime_setting("AI_AGENT_VERBOSE"),
get_runtime_setting("AI_AGENT_OUTPUT_LANGUAGE"),
get_runtime_setting("LLM_TEMPERATURE"),
get_runtime_setting("LLM_MAX_CONTEXT_TOKENS"),
get_runtime_setting("LLM_MAX_TOOLS"),

View File

@@ -1,6 +1,6 @@
You are the MoviePilot agent runtime. Use the active persona for speaking style and the injected runtime context for current capabilities.
All your responses must be in **Chinese (中文)**.
Follow the configured output language policy in this system prompt for all user-facing responses.
Carry actionable requests through to a verified outcome within the user's authorized scope. Continue until the requested outcome is reached or a concrete blocker requires user input; persistent monitoring belongs in a scheduled task.
@@ -122,7 +122,7 @@ Apply this workflow to requested plugin development, bug fixes, source changes,
- Continue working after each update. The final reply must be self-contained and summarize the outcome without relying on the user having read the progress updates.
</progress_updates>
- Answer in clear Chinese: lead with the result, include decision-relevant evidence, and distinguish facts from inference. Keep internal IDs, implementation details, and raw logs out of ordinary media replies unless needed; include paths, changes, and validation when reporting code work.
- Answer clearly: lead with the result, include decision-relevant evidence, and distinguish facts from inference. Keep internal IDs, implementation details, and raw logs out of ordinary media replies unless needed; include paths, changes, and validation when reporting code work.
- Channel-aware formatting: Follow the capability rules below for Markdown, plain text, buttons, and voice replies even when the persona suggests a conflicting format.
{button_choice_spec}
- Voice replies: {voice_reply_spec}

View File

@@ -3,7 +3,7 @@ shared_rules:
- This is a background system task, NOT a user conversation.
- Your final response will be consumed by the system. Keep it concise and task-focused.
- Do NOT include greetings, explanations, or conversational text.
- Respond in Chinese (中文).
- Follow the configured output language policy from the Agent system prompt.
task_types:
heartbeat:
header: "[System Heartbeat]"
@@ -129,7 +129,7 @@ task_types:
- "When several records obviously share the same media identity, avoid repeated `media.recognize` or `media.search` calls."
- "Process every selected record exactly once."
- "Keep the final response short and focused on the aggregate outcome."
- "Final response must be plain text only: one concise Chinese sentence or paragraph describing the aggregate result."
- "Final response must be plain text only: one concise sentence or paragraph in the configured output language describing the aggregate result."
- "Do NOT include any title/header, bullet list, numbered list, bold text, code block, table, or other Markdown formatting."
search_recommend:
header: "[System Task - Search Results Recommendation]"

View File

@@ -48,6 +48,25 @@ COMMON_SHELL_COMMANDS = (
"yt-dlp",
)
_AGENT_OUTPUT_LANGUAGE_NAMES = {
"zh-CN": "Simplified Chinese",
"zh-TW": "Traditional Chinese",
"en-US": "English",
"ja-JP": "Japanese",
"ko-KR": "Korean",
}
_AGENT_OUTPUT_LANGUAGE_ALIASES = {
"zh": "zh-CN",
"zh-cn": "zh-CN",
"zh-tw": "zh-TW",
"en": "en-US",
"en-us": "en-US",
"ja": "ja-JP",
"ja-jp": "ja-JP",
"ko": "ko-KR",
"ko-kr": "ko-KR",
}
class PromptConfigError(ValueError):
"""程序内置提示词定义加载异常。"""
@@ -152,8 +171,24 @@ class PromptManager:
rich_message_spec=rich_message_spec,
)
base_prompt = f"{base_prompt}\n\n{self._get_output_language_instruction()}"
return base_prompt
@staticmethod
def _get_output_language_instruction() -> str:
"""根据系统设置生成内置智能助手的回复语言约束。"""
configured = str(get_runtime_setting("AI_AGENT_OUTPUT_LANGUAGE") or "zh-CN").strip()
normalized = configured.casefold()
language_code = _AGENT_OUTPUT_LANGUAGE_ALIASES.get(normalized, configured)
language_name = _AGENT_OUTPUT_LANGUAGE_NAMES.get(language_code, "Simplified Chinese")
return (
"Output language policy:\n"
f"- Always reply to the user in {language_name}.\n"
"- Keep this language for explanations, conclusions, and user-facing messages unless the user explicitly asks for another language. "
"Preserve code, file paths, media titles, proper nouns, and tool arguments when they need to remain in their original form."
)
def load_system_tasks_definition(self) -> SystemTasksDefinition:
"""加载程序内置的后台系统任务定义。"""
system_tasks_path = self.prompts_dir / SYSTEM_TASKS_FILE

View File

@@ -35,37 +35,43 @@
"description": "AI智能体定时任务检查间隔(小时),0为不启用,默认24小时",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 866
"source_line": 868
},
"AI_AGENT_OUTPUT_LANGUAGE": {
"description": "内置智能助手输出语言,默认使用简体中文",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 819
},
"AI_AGENT_RETRY_TRANSFER": {
"description": "AI智能体自动重试整理失败记录开关",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 870
"source_line": 872
},
"AI_AGENT_VERBOSE": {
"description": "AI智能体啰嗦模式,开启后会回复工具调用过程",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 868
"source_line": 870
},
"AI_RECOMMEND_ENABLED": {
"description": "是否启用AI推荐",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 857
"source_line": 859
},
"AI_RECOMMEND_MAX_ITEMS": {
"description": "AI推荐条目数量限制",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 862
"source_line": 864
},
"AI_RECOMMEND_USER_PREFERENCE": {
"description": "AI推荐用户偏好",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 859
"source_line": 861
},
"ALIPAN_APP_ID": {
"description": "Alipan AppId",
@@ -143,67 +149,67 @@
"description": "音频输入 API 密钥",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 877
"source_line": 879
},
"AUDIO_INPUT_BASE_URL": {
"description": "音频输入基础URL",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 879
"source_line": 881
},
"AUDIO_INPUT_LANGUAGE": {
"description": "音频输入识别语言",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 883
"source_line": 885
},
"AUDIO_INPUT_MODEL": {
"description": "音频输入模型",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 881
"source_line": 883
},
"AUDIO_INPUT_PROVIDER": {
"description": "音频输入提供商:openai/openai_chat_audio/mimo/minimax",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 875
"source_line": 877
},
"AUDIO_OUTPUT_API_KEY": {
"description": "音频输出 API 密钥",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 887
"source_line": 889
},
"AUDIO_OUTPUT_BASE_URL": {
"description": "音频输出基础URL",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 889
"source_line": 891
},
"AUDIO_OUTPUT_INCLUDE_TEXT": {
"description": "回复语音时是否同时附带文字说明",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 895
"source_line": 897
},
"AUDIO_OUTPUT_MODEL": {
"description": "音频输出模型",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 891
"source_line": 893
},
"AUDIO_OUTPUT_PROVIDER": {
"description": "音频输出提供商:openai/openai_chat_audio/mimo/minimax",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 885
"source_line": 887
},
"AUDIO_OUTPUT_VOICE": {
"description": "音频输出音色/发音人",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 893
"source_line": 895
},
"AUTH_SITE": {
"description": "用户认证站点",
@@ -803,121 +809,121 @@
"description": "LLM API密钥",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 835
"source_line": 837
},
"LLM_API_PROTOCOL": {
"description": "OpenAI兼容接口API协议:auto(自动)/ chat_completions / responses",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 825
"source_line": 827
},
"LLM_BASE_URL": {
"description": "LLM基础URL(用于自定义API端点)",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 837
"source_line": 839
},
"LLM_BASE_URL_PRESET": {
"description": "LLM Base URL 预设标识,用于区分同一 Base URL 下的不同模型目录",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 841
"source_line": 843
},
"LLM_MAX_CONTEXT_TOKENS": {
"description": "LLM最大上下文Token数量(K),用于目录缺失回退和未匹配兼容端点的用户上限",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 843
"source_line": 845
},
"LLM_MAX_ITERATIONS": {
"description": "LLM最大迭代次数",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 849
"source_line": 851
},
"LLM_MAX_TOOLS": {
"description": "LLM工具选择中间件最大工具数量,0为不启用工具选择中间件",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 864
"source_line": 866
},
"LLM_MEMORY_RETENTION_DAYS": {
"description": "内存记忆保留天数",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 855
"source_line": 857
},
"LLM_MODEL": {
"description": "LLM模型名称",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 821
"source_line": 823
},
"LLM_PROVIDER": {
"description": "LLM提供商(支持内置 provider,以及从 models.dev 动态补充的平台)",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 819
"source_line": 821
},
"LLM_SUPPORT_AUDIO_INPUT": {
"description": "是否启用音频输入,开启后用户语音会先转写为文本再进入 Agent",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 831
"source_line": 833
},
"LLM_SUPPORT_AUDIO_OUTPUT": {
"description": "是否启用音频输出,开启后 Agent 可在支持渠道发送语音回复",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 833
"source_line": 835
},
"LLM_SUPPORT_IMAGE_INPUT": {
"description": "LLM是否支持图片输入,开启后消息图片会按多模态输入发送给模型",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 829
"source_line": 831
},
"LLM_TEMPERATURE": {
"description": "LLM温度参数,留空时不覆盖提供商默认值",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 847
"source_line": 849
},
"LLM_THINKING_LEVEL": {
"description": "思考模式/深度配置:off/auto/minimal/low/medium/high/max/xhigh;OpenAI兼容接口保留max/xhigh,仅按已知模型能力降级",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 823
"source_line": 825
},
"LLM_TOOL_TIMEOUT": {
"description": "LLM工具调用超时时间(秒)",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 851
"source_line": 853
},
"LLM_USER_AGENT": {
"description": "LLM OpenAI兼容接口请求User-Agent",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 845
"source_line": 847
},
"LLM_USE_PROXY": {
"description": "LLM调用是否使用系统代理",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 839
"source_line": 841
},
"LLM_VERBOSE": {
"description": "是否启用详细日志",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 853
"source_line": 855
},
"LLM_WEB_SEARCH_MODE": {
"description": "联网搜索模式:local(本地)/ builtin(模型服务端)/ auto(自动)/ disabled(关闭)",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 827
"source_line": 829
},
"LOCAL_EXISTS_SEARCH": {
"description": "检查本地媒体库是否存在资源开关",
@@ -1415,7 +1421,7 @@
"description": "是否按媒体聚合整理失败通知,关闭时保持逐条发送",
"group": "ai_agent",
"source_file": "app/runtime/config.py",
"source_line": 872
"source_line": 874
},
"TRANSFER_MAX_FAILED_RETRIES": {
"description": "自动整理(目录监控、下载器轮询)遇到失败整理记录时,同一源路径允许自动重试的最大次数。 一次瞬时故障(网络抖动、TMDB 瞬断、移动失败)不该让文件永久漏整理,因此必须重试; 但永远识别不出的文件重试再多也不会成功,只会重复推送失败通知,批量导入时更会刷屏, 因此必须有界。取值区间 1-10,越界或非法值会被钳制到边界并记录 warn: 既不支持关闭重试,也不支持无限重试。整理成功或删除整理记录时计数清零。 与本项无关的是同路径新版本:已成功整理的文件源大小变化时一律放行, 由整理链的 overwrite_mode 决断是否覆盖",

View File

@@ -815,6 +815,8 @@ class ConfigModel(BaseModel):
AI_AGENT_GLOBAL: bool = False
# 是否隐藏前端全局智能体入口
AI_AGENT_HIDE_ENTRY: bool = False
# 内置智能助手输出语言,默认使用简体中文
AI_AGENT_OUTPUT_LANGUAGE: str = "zh-CN"
# LLM提供商(支持内置 provider,以及从 models.dev 动态补充的平台)
LLM_PROVIDER: str = "deepseek"
# LLM模型名称

View File

@@ -0,0 +1,31 @@
from unittest.mock import patch
from app.agent.prompt import PromptManager
def test_agent_prompt_defaults_to_simplified_chinese() -> None:
"""未配置回复语言时,内置助手提示词应要求使用简体中文。"""
with patch("app.agent.prompt.get_runtime_setting", return_value="zh-CN"):
prompt = PromptManager().get_agent_prompt(channel="webagent")
assert "Always reply to the user in Simplified Chinese." in prompt
def test_agent_prompt_uses_configured_output_language() -> None:
"""配置回复语言后,内置助手提示词应切换到对应语言。"""
with patch("app.agent.prompt.get_runtime_setting", return_value="ja-JP"):
prompt = PromptManager().get_agent_prompt(channel="webagent")
assert "Always reply to the user in Japanese." in prompt
assert "Always reply to the user in Simplified Chinese." not in prompt
assert "All your responses must be in **Chinese (中文)**." not in prompt
assert "Answer in clear Chinese" not in prompt
def test_agent_prompt_falls_back_for_unknown_output_language() -> None:
"""未知语言代码不能把任意配置原文注入提示词。"""
with patch("app.agent.prompt.get_runtime_setting", return_value="untrusted instruction"):
prompt = PromptManager().get_agent_prompt(channel="webagent")
assert "Always reply to the user in untrusted instruction." not in prompt
assert "Always reply to the user in Simplified Chinese." in prompt

View File

@@ -36,6 +36,7 @@ def test_batch_manual_redo_prompt_requires_plain_text_result():
assert "Final response must be plain text only" in prompt
assert "Do NOT include any title/header, bullet list" in prompt
assert "Markdown formatting" in prompt
assert "configured output language" in prompt
def test_batch_manual_redo_job_definition_contains_plain_text_rules():