From 6f0f528e2f79a518c4aff8ef7b95041da3efe28f Mon Sep 17 00:00:00 2001 From: jxxghp Date: Mon, 5 Oct 2026 07:35:05 +0800 Subject: [PATCH] feat(agent): add configurable output language --- app/agent/orchestrator.py | 1 + app/agent/prompt/System Core Prompt.txt | 4 +- app/agent/prompt/System Tasks.yaml | 4 +- app/agent/prompt/__init__.py | 35 ++++++++ .../resources/system_settings_catalog.json | 82 ++++++++++--------- app/runtime/config.py | 2 + tests/test_agent_prompt_language.py | 31 +++++++ tests/test_history_batch_ai_redo_prompt.py | 1 + 8 files changed, 118 insertions(+), 42 deletions(-) create mode 100644 tests/test_agent_prompt_language.py diff --git a/app/agent/orchestrator.py b/app/agent/orchestrator.py index 023eeec2e..9f6012df6 100644 --- a/app/agent/orchestrator.py +++ b/app/agent/orchestrator.py @@ -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"), diff --git a/app/agent/prompt/System Core Prompt.txt b/app/agent/prompt/System Core Prompt.txt index f43591feb..3733137df 100644 --- a/app/agent/prompt/System Core Prompt.txt +++ b/app/agent/prompt/System Core Prompt.txt @@ -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. -- 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} diff --git a/app/agent/prompt/System Tasks.yaml b/app/agent/prompt/System Tasks.yaml index 1bf07fe8d..cc9b0c415 100644 --- a/app/agent/prompt/System Tasks.yaml +++ b/app/agent/prompt/System Tasks.yaml @@ -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]" diff --git a/app/agent/prompt/__init__.py b/app/agent/prompt/__init__.py index 900364bef..ee18ad101 100644 --- a/app/agent/prompt/__init__.py +++ b/app/agent/prompt/__init__.py @@ -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 diff --git a/app/application/settings/resources/system_settings_catalog.json b/app/application/settings/resources/system_settings_catalog.json index 75bcc98fb..98bba0922 100644 --- a/app/application/settings/resources/system_settings_catalog.json +++ b/app/application/settings/resources/system_settings_catalog.json @@ -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 决断是否覆盖", diff --git a/app/runtime/config.py b/app/runtime/config.py index 998dc8196..d0b5942c9 100644 --- a/app/runtime/config.py +++ b/app/runtime/config.py @@ -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模型名称 diff --git a/tests/test_agent_prompt_language.py b/tests/test_agent_prompt_language.py new file mode 100644 index 000000000..2bff01883 --- /dev/null +++ b/tests/test_agent_prompt_language.py @@ -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 diff --git a/tests/test_history_batch_ai_redo_prompt.py b/tests/test_history_batch_ai_redo_prompt.py index 3f63e5d3e..63eb40c0c 100644 --- a/tests/test_history_batch_ai_redo_prompt.py +++ b/tests/test_history_batch_ai_redo_prompt.py @@ -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():