mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-02 13:28:32 +08:00
feat(agent): add configurable output language
This commit is contained in:
@@ -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"),
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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]"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 决断是否覆盖",
|
||||
|
||||
@@ -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模型名称
|
||||
|
||||
31
tests/test_agent_prompt_language.py
Normal file
31
tests/test_agent_prompt_language.py
Normal 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
|
||||
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user