From 423352d6e6c899ae446b06430ff5d111da543a40 Mon Sep 17 00:00:00 2001 From: ylzhangah <1194926515@qq.com> Date: Fri, 25 Jul 2025 16:06:20 +0800 Subject: [PATCH 01/15] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=84=9A=E6=9C=AC?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scripts/2-install-tools/install_tools.sh | 25 ++++-- .../install_eulercopilot.sh | 80 ++++++++++--------- 2 files changed, 59 insertions(+), 46 deletions(-) diff --git a/deploy/scripts/2-install-tools/install_tools.sh b/deploy/scripts/2-install-tools/install_tools.sh index 804c735d..8ca6d9aa 100755 --- a/deploy/scripts/2-install-tools/install_tools.sh +++ b/deploy/scripts/2-install-tools/install_tools.sh @@ -338,6 +338,16 @@ function check_k3s_status() { fi } +check_hub_connection() { + if curl -sSf http://hub.oepkgs.net >/dev/null 2>&1; then + echo -e "[Info] 镜像站连接正常" + return 1 + else + echo -e "[Error] 镜像站连接失败" + return 1 + fi +} + function main { # 创建工具目录 mkdir -p "$TOOLS_DIR" @@ -356,13 +366,6 @@ function main { else echo -e "[Info] K3s 已经安装,跳过安装步骤" fi - # 优先检查网络 - if check_network; then - echo -e "\033[32m[Info] 在线环境,跳过镜像导入\033[0m" - else - echo -e "\033[33m[Info] 离线环境,开始导入本地镜像,请确保本地目录已存在所有镜像文件\033[0m" - bash "$IMPORT_SCRIPT/9-other-script/import_images.sh" -v "$eulercopilot_version" - fi # 安装Helm(如果尚未安装) if ! command -v helm &> /dev/null; then @@ -374,6 +377,14 @@ function main { ln -sf /etc/rancher/k3s/k3s.yaml ~/.kube/config check_k3s_status + # 优先检查网络 + if check_hub_connection; then + echo -e "\033[32m[Info] 在线环境,跳过镜像导入\033[0m" + else + echo -e "\033[33m[Info] 离线环境,开始导入本地镜像,请确保本地目录已存在所有镜像文件\033[0m" + bash "$IMPORT_SCRIPT/9-other-script/import_images.sh" -v "$eulercopilot_version" + fi + echo -e "\n\033[32m=== 全部工具安装完成 ===\033[0m" echo -e "K3s 版本:$(k3s --version | head -n1)" echo -e "Helm 版本:$(helm version --short)" diff --git a/deploy/scripts/8-install-EulerCopilot/install_eulercopilot.sh b/deploy/scripts/8-install-EulerCopilot/install_eulercopilot.sh index af351df5..cf878866 100755 --- a/deploy/scripts/8-install-EulerCopilot/install_eulercopilot.sh +++ b/deploy/scripts/8-install-EulerCopilot/install_eulercopilot.sh @@ -76,6 +76,32 @@ parse_arguments() { done } + +# 安装成功信息显示函数 +show_success_message() { + local host=$1 + local arch=$2 + + echo -e "\n${GREEN}==================================================${NC}" + echo -e "${GREEN} EulerCopilot 部署完成! ${NC}" + echo -e "${GREEN}==================================================${NC}" + + echo -e "${YELLOW}访问信息:${NC}" + echo -e "EulerCopilot UI: ${eulercopilot_address}" + echo -e "AuthHub 管理界面: ${authhub_address}" + + echo -e "\n${YELLOW}系统信息:${NC}" + echo -e "内网IP: ${host}" + echo -e "系统架构: $(uname -m) (识别为: ${arch})" + echo -e "插件目录: ${PLUGINS_DIR}" + echo -e "Chart目录: ${DEPLOY_DIR}/chart/" + + echo -e "${BLUE}操作指南:${NC}" + echo -e "1. 查看集群状态: kubectl get all -n $NAMESPACE" + echo -e "2. 查看实时日志: kubectl logs -n $NAMESPACE -f deployment/$NAMESPACE" + echo -e "3. 查看POD状态:kubectl get pods -n $NAMESPACE" +} + # 获取系统架构 get_architecture() { local arch=$(uname -m) @@ -258,17 +284,19 @@ uninstall_eulercopilot() { echo -e "${YELLOW}未找到需要清理的Helm Release: euler-copilot${NC}" fi - # 删除 PVC: framework-semantics-claim - local pvc_name="framework-semantics-claim" - if kubectl get pvc "$pvc_name" -n euler-copilot &>/dev/null; then - echo -e "${GREEN}找到PVC: ${pvc_name},开始清理...${NC}" - if ! kubectl delete pvc "$pvc_name" -n euler-copilot --force --grace-period=0; then - echo -e "${RED}错误:删除PVC ${pvc_name} 失败!${NC}" >&2 - return 1 + # 删除 PVC: framework-semantics-claim 和 web-static + local pvc_names=("framework-semantics-claim" "web-static") + for pvc_name in "${pvc_names[@]}"; do + if kubectl get pvc "$pvc_name" -n euler-copilot &>/dev/null; then + echo -e "${GREEN}找到PVC: ${pvc_name},开始清理...${NC}" + if ! kubectl delete pvc "$pvc_name" -n euler-copilot --force --grace-period=0; then + echo -e "${RED}错误:删除PVC ${pvc_name} 失败!${NC}" >&2 + return 1 + fi + else + echo -e "${YELLOW}未找到需要清理的PVC: ${pvc_name}${NC}" fi - else - echo -e "${YELLOW}未找到需要清理的PVC: ${pvc_name}${NC}" - fi + done # 删除 Secret: euler-copilot-system local secret_name="euler-copilot-system" @@ -295,6 +323,7 @@ modify_yaml() { # 添加其他必填参数 set_args+=( + "--set" "globals.arch=$arch" "--set" "login.client.id=${client_id}" "--set" "login.client.secret=${client_secret}" "--set" "domain.euler_copilot=${eulercopilot_address}" @@ -350,11 +379,10 @@ pre_install_checks() { # 执行安装 execute_helm_install() { - local arch=$1 echo -e "${BLUE}开始部署EulerCopilot(架构: $arch)...${NC}" >&2 enter_chart_directory - helm upgrade --install $NAMESPACE -n $NAMESPACE ./euler_copilot --set globals.arch=$arch --create-namespace || { + helm upgrade --install $NAMESPACE -n $NAMESPACE ./euler_copilot --create-namespace || { echo -e "${RED}Helm 安装 EulerCopilot 失败!${NC}" >&2 exit 1 } @@ -439,7 +467,7 @@ main() { modify_yaml "$host" "$preserve_models" echo -e "${BLUE}开始Helm安装...${NC}" - execute_helm_install "$arch" + execute_helm_install if check_pods_status; then echo -e "${GREEN}所有组件已就绪!${NC}" @@ -449,30 +477,4 @@ main() { fi } -# 添加安装成功信息显示函数 -show_success_message() { - local host=$1 - local arch=$2 - - - echo -e "\n${GREEN}==================================================${NC}" - echo -e "${GREEN} EulerCopilot 部署完成! ${NC}" - echo -e "${GREEN}==================================================${NC}" - - echo -e "${YELLOW}访问信息:${NC}" - echo -e "EulerCopilot UI: ${eulercopilot_address}" - echo -e "AuthHub 管理界面: ${authhub_address}" - - echo -e "\n${YELLOW}系统信息:${NC}" - echo -e "内网IP: ${host}" - echo -e "系统架构: $(uname -m) (识别为: ${arch})" - echo -e "插件目录: ${PLUGINS_DIR}" - echo -e "Chart目录: ${DEPLOY_DIR}/chart/" - - echo -e "${BLUE}操作指南:${NC}" - echo -e "1. 查看集群状态: kubectl get all -n $NAMESPACE" - echo -e "2. 查看实时日志: kubectl logs -n $NAMESPACE -f deployment/$NAMESPACE" - echo -e "3. 查看POD状态:kubectl get pods -n $NAMESPACE" -} - main "$@" -- Gitee From 4d7770f257b1ff74bcbdf742c8c57f5cc87b790d Mon Sep 17 00:00:00 2001 From: zhangyale <1194926515@qq.com> Date: Tue, 29 Jul 2025 07:38:16 +0000 Subject: [PATCH 02/15] update deploy/scripts/2-install-tools/install_tools.sh. Signed-off-by: zhangyale <1194926515@qq.com> --- deploy/scripts/2-install-tools/install_tools.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/scripts/2-install-tools/install_tools.sh b/deploy/scripts/2-install-tools/install_tools.sh index 8ca6d9aa..8536d036 100755 --- a/deploy/scripts/2-install-tools/install_tools.sh +++ b/deploy/scripts/2-install-tools/install_tools.sh @@ -341,7 +341,7 @@ function check_k3s_status() { check_hub_connection() { if curl -sSf http://hub.oepkgs.net >/dev/null 2>&1; then echo -e "[Info] 镜像站连接正常" - return 1 + return 0 else echo -e "[Error] 镜像站连接失败" return 1 -- Gitee From 77881149e435ef9ebe25b96e096cb583bc36abe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=B6=B5=E5=8D=9A?= <2428333123@qq.com> Date: Mon, 4 Aug 2025 17:27:04 +0800 Subject: [PATCH 03/15] =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=9B=BD=E9=99=85?= =?UTF-8?q?=E5=8C=96=E9=83=A8=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- apps/llm/function.py | 47 ++-- apps/llm/patterns/core.py | 23 +- apps/llm/patterns/executor.py | 108 +++++++-- apps/llm/patterns/facts.py | 58 ++++- apps/llm/patterns/rewoo.py | 207 +++++++++++++++--- apps/llm/patterns/rewrite.py | 77 ++++++- apps/routers/chat.py | 3 + apps/routers/flow.py | 14 +- apps/scheduler/call/api/api.py | 16 +- apps/scheduler/call/choice/choice.py | 19 +- apps/scheduler/call/core.py | 7 +- apps/scheduler/call/facts/facts.py | 24 +- apps/scheduler/call/facts/prompt.py | 98 ++++++++- apps/scheduler/call/graph/graph.py | 14 +- apps/scheduler/call/llm/llm.py | 15 +- apps/scheduler/call/llm/prompt.py | 94 ++++++-- apps/scheduler/call/mcp/mcp.py | 78 +++++-- apps/scheduler/call/rag/rag.py | 14 +- apps/scheduler/call/search/search.py | 15 +- apps/scheduler/call/slot/prompt.py | 93 +++++++- apps/scheduler/call/slot/slot.py | 24 +- apps/scheduler/call/sql/sql.py | 42 ++-- apps/scheduler/call/suggest/prompt.py | 100 ++++++++- apps/scheduler/call/suggest/suggest.py | 33 ++- apps/scheduler/call/summary/summary.py | 20 +- apps/scheduler/executor/flow.py | 99 +++++++-- apps/scheduler/executor/step.py | 2 +- apps/scheduler/mcp/host.py | 15 +- apps/scheduler/mcp/plan.py | 7 +- apps/scheduler/mcp/prompt.py | 290 +++++++++++++++++++++++-- apps/scheduler/mcp/select.py | 8 +- apps/scheduler/pool/loader/app.py | 4 +- apps/scheduler/pool/loader/flow.py | 14 +- apps/scheduler/pool/pool.py | 4 +- apps/scheduler/scheduler/message.py | 8 +- apps/scheduler/scheduler/scheduler.py | 4 +- apps/schemas/request_data.py | 1 + apps/schemas/task.py | 1 + apps/services/flow.py | 53 +++-- apps/services/rag.py | 210 ++++++++++++------ 41 files changed, 1594 insertions(+), 371 deletions(-) diff --git a/.gitignore b/.gitignore index c62e334e..7e0cc625 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,4 @@ logs .git-credentials .ruff_cache/ config -uv.lock +uv.lock \ No newline at end of file diff --git a/apps/llm/function.py b/apps/llm/function.py index 1f995fe7..4e56d147 100644 --- a/apps/llm/function.py +++ b/apps/llm/function.py @@ -68,7 +68,6 @@ class FunctionLLM: api_key=self._config.api_key, ) - async def _call_openai( self, messages: list[dict[str, str]], @@ -86,11 +85,13 @@ class FunctionLLM: :return: 生成的JSON :rtype: str """ - self._params.update({ - "messages": messages, - "max_tokens": max_tokens, - "temperature": temperature, - }) + self._params.update( + { + "messages": messages, + "max_tokens": max_tokens, + "temperature": temperature, + } + ) if self._config.backend == "vllm": self._params["extra_body"] = {"guided_json": schema} @@ -123,9 +124,11 @@ class FunctionLLM: }, ] - response = await self._client.chat.completions.create(**self._params) # type: ignore[arg-type] + response = await self._client.chat.completions.create(**self._params) # type: ignore[arg-type] try: - logger.info("[FunctionCall] 大模型输出:%s", response.choices[0].message.tool_calls[0].function.arguments) + logger.info( + "[FunctionCall] 大模型输出:%s", response.choices[0].message.tool_calls[0].function.arguments + ) return response.choices[0].message.tool_calls[0].function.arguments except Exception: # noqa: BLE001 ans = response.choices[0].message.content @@ -169,7 +172,6 @@ class FunctionLLM: return json_str - async def _call_ollama( self, messages: list[dict[str, str]], @@ -187,19 +189,20 @@ class FunctionLLM: :return: 生成的对话回复 :rtype: str """ - self._params.update({ - "messages": messages, - "options": { - "temperature": temperature, - "num_predict": max_tokens, - }, - "format": schema, - }) + self._params.update( + { + "messages": messages, + "options": { + "temperature": temperature, + "num_predict": max_tokens, + }, + "format": schema, + } + ) - response = await self._client.chat(**self._params) # type: ignore[arg-type] + response = await self._client.chat(**self._params) # type: ignore[arg-type] return await self.process_response(response.message.content or "") - async def call( self, messages: list[dict[str, Any]], @@ -254,7 +257,6 @@ class JsonGenerator: ) self._err_info = "" - async def _assemble_message(self) -> str: """组装消息""" # 检查类型 @@ -271,7 +273,9 @@ class JsonGenerator: err_info=self._err_info, ) - async def _single_trial(self, max_tokens: int | None = None, temperature: float | None = None) -> dict[str, Any]: + async def _single_trial( + self, max_tokens: int | None = None, temperature: float | None = None + ) -> dict[str, Any]: """单次尝试""" prompt = await self._assemble_message() messages = [ @@ -281,7 +285,6 @@ class JsonGenerator: function = FunctionLLM() return await function.call(messages, self._schema, max_tokens, temperature) - async def generate(self) -> dict[str, Any]: """生成JSON""" Draft7Validator.check_schema(self._schema) diff --git a/apps/llm/patterns/core.py b/apps/llm/patterns/core.py index 4ef8133a..c2c98f86 100644 --- a/apps/llm/patterns/core.py +++ b/apps/llm/patterns/core.py @@ -3,14 +3,14 @@ from abc import ABC, abstractmethod from textwrap import dedent - +from typing import Union class CorePattern(ABC): """基础大模型范式抽象类""" - system_prompt: str = "" + system_prompt: Union[str, dict[str, str]] = "" """系统提示词""" - user_prompt: str = "" + user_prompt: Union[str, dict[str, str]] = "" """用户提示词""" input_tokens: int = 0 """输入Token数量""" @@ -18,7 +18,9 @@ class CorePattern(ABC): """输出Token数量""" - def __init__(self, system_prompt: str | None = None, user_prompt: str | None = None) -> None: + def __init__(self, + system_prompt: Union[str, dict[str, str], None] = None, + user_prompt: Union[str, dict[str, str], None] = None) -> None: """ 检查是否已经自定义了Prompt;有的话就用自定义的;同时对Prompt进行空格清除 @@ -35,8 +37,17 @@ class CorePattern(ABC): err = "必须设置用户提示词!" raise ValueError(err) - self.system_prompt = dedent(self.system_prompt).strip("\n") - self.user_prompt = dedent(self.user_prompt).strip("\n") + + self.system_prompt = { + lang: dedent(prompt).strip("\n") + for lang, prompt in self.system_prompt.items() + } if isinstance(self.system_prompt, dict) else dedent(self.system_prompt).strip("\n") + + + self.user_prompt = { + lang: dedent(prompt).strip("\n") + for lang, prompt in self.user_prompt.items() + } if isinstance(self.user_prompt, dict) else dedent(self.user_prompt).strip("\n") @abstractmethod async def generate(self, **kwargs): # noqa: ANN003, ANN201 diff --git a/apps/llm/patterns/executor.py b/apps/llm/patterns/executor.py index f872fd2a..390379e0 100644 --- a/apps/llm/patterns/executor.py +++ b/apps/llm/patterns/executor.py @@ -1,7 +1,7 @@ # Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. """使用大模型生成Executor的思考内容""" -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Union from apps.llm.patterns.core import CorePattern from apps.llm.reasoning import ReasoningLLM @@ -14,7 +14,8 @@ if TYPE_CHECKING: class ExecutorThought(CorePattern): """通过大模型生成Executor的思考内容""" - user_prompt: str = r""" + user_prompt: dict[str, str] = { + "zh_cn": r""" 你是一个可以使用工具的智能助手。 @@ -44,10 +45,46 @@ class ExecutorThought(CorePattern): 请综合以上信息,再次一步一步地进行思考,并给出见解和行动: - """ + """, + "en": r""" + + + You are an intelligent assistant who can use tools. + When answering user questions, you use a tool to get more information. + Please summarize the process of using the tool briefly, provide your insights, and give the next action. + + Note: + The information about the tool is given in the tag. + To help you better understand what happened, your previous thought process is given in the tag. + Do not include XML tags in the output, and keep the output brief and clear. + + + + + {tool_name} + {tool_description} + {tool_output} + + + + {last_thought} + + + + The question you need to solve is: + {user_question} + + + Please integrate the above information, think step by step again, provide insights, and give actions: + """, + } """用户提示词""" - def __init__(self, system_prompt: str | None = None, user_prompt: str | None = None) -> None: + def __init__( + self, + system_prompt: Union[str, dict[str, str]] | None = None, + user_prompt: Union[str, dict[str, str]] | None = None, + ) -> None: """处理Prompt""" super().__init__(system_prompt, user_prompt) @@ -57,19 +94,23 @@ class ExecutorThought(CorePattern): last_thought: str = kwargs["last_thought"] user_question: str = kwargs["user_question"] tool_info: dict[str, Any] = kwargs["tool_info"] + language: str = kwargs.get("language", "zh_cn") except Exception as e: err = "参数不正确!" raise ValueError(err) from e messages = [ {"role": "system", "content": "You are a helpful assistant."}, - {"role": "user", "content": self.user_prompt.format( - last_thought=last_thought, - user_question=user_question, - tool_name=tool_info["name"], - tool_description=tool_info["description"], - tool_output=tool_info["output"], - )}, + { + "role": "user", + "content": self.user_prompt[language].format( + last_thought=last_thought, + user_question=user_question, + tool_name=tool_info["name"], + tool_description=tool_info["description"], + tool_output=tool_info["output"], + ), + }, ] llm = ReasoningLLM() @@ -85,7 +126,8 @@ class ExecutorThought(CorePattern): class ExecutorSummary(CorePattern): """使用大模型进行生成Executor初始背景""" - user_prompt: str = r""" + user_prompt: dict[str, str] = { + "zh_cn": r""" 根据给定的对话记录和关键事实,生成一个三句话背景总结。这个总结将用于后续对话的上下文理解。 @@ -105,10 +147,36 @@ class ExecutorSummary(CorePattern): 现在,请开始生成背景总结: - """ + """, + "en": r""" + + Based on the given conversation records and key facts, generate a three-sentence background summary. This summary will be used for context understanding in subsequent conversations. + + The requirements for generating the summary are as follows: + 1. Highlight important information points, such as time, location, people, events, etc. + 2. The content in the "key facts" can be used as known information when generating the summary. + 3. Do not include XML tags in the output, ensure the accuracy of the information, and do not make up information. + 4. The summary should be less than 3 sentences and less than 300 words. + + The conversation records will be given in the tag, and the key facts will be given in the tag. + + + {conversation} + + + {facts} + + + Now, please start generating the background summary: + """, + } """用户提示词""" - def __init__(self, system_prompt: str | None = None, user_prompt: str | None = None) -> None: + def __init__( + self, + system_prompt: Union[str, dict[str, str], None] = None, + user_prompt: Union[str, dict[str, str], None] = None, + ) -> None: """初始化Background模式""" super().__init__(system_prompt, user_prompt) @@ -117,13 +185,17 @@ class ExecutorSummary(CorePattern): background: ExecutorBackground = kwargs["background"] conversation_str = convert_context_to_prompt(background.conversation) facts_str = facts_to_prompt(background.facts) + language = kwargs.get("language", "zh_cn") messages = [ {"role": "system", "content": "You are a helpful assistant."}, - {"role": "user", "content": self.user_prompt.format( - facts=facts_str, - conversation=conversation_str, - )}, + { + "role": "user", + "content": self.user_prompt[language].format( + facts=facts_str, + conversation=conversation_str, + ), + }, ] result = "" diff --git a/apps/llm/patterns/facts.py b/apps/llm/patterns/facts.py index 0b0381ff..c5abb905 100644 --- a/apps/llm/patterns/facts.py +++ b/apps/llm/patterns/facts.py @@ -4,6 +4,7 @@ import logging from pydantic import BaseModel, Field +from typing import Union from apps.llm.function import JsonGenerator from apps.llm.patterns.core import CorePattern @@ -25,7 +26,8 @@ class Facts(CorePattern): system_prompt: str = "You are a helpful assistant." """系统提示词(暂不使用)""" - user_prompt: str = r""" + user_prompt: dict[str, str] = { + "zh_cn": r""" 从对话中提取关键信息,并将它们组织成独一无二的、易于理解的事实,包含用户偏好、关系、实体等有用信息。 @@ -63,21 +65,65 @@ class Facts(CorePattern): {conversation} - """ - """用户提示词""" + """, + "en": r""" + + + Extract key information from the conversation and organize it into unique, easily understandable facts that include user preferences, relationships, entities, etc. + The following are the types of information to be paid attention to and detailed instructions on how to handle the input data. + + **Types of information to be paid attention to** + 1. Entities: Entities involved in the conversation. For example: names, locations, organizations, events, etc. + 2. Preferences: Attitudes towards entities. For example: like, dislike, etc. + 3. Relationships: Relationships between the user and entities, or between two entities. For example: include, parallel, exclusive, etc. + 4. Actions: Specific actions that affect entities. For example: query, search, browse, click, etc. + + **Requirements** + 1. Facts must be accurate and can only be extracted from the conversation. Do not include information from the sample in the output. + 2. Facts must be clear, concise, and easy to understand. Must be less than 30 words. + 3. Output in the following JSON format: + + {{ + "facts": ["fact1", "fact2", "fact3"] + }} + + + + + What are the attractions in West Lake, Hangzhou? + West Lake in Hangzhou is a famous scenic spot in Hangzhou, Zhejiang Province, China, famous for its beautiful natural scenery and rich cultural heritage. There are many famous attractions around West Lake, including the famous Su Causeway, Bai Causeway, Broken Bridge, Three Pools Mirroring the Moon, etc. West Lake is famous for its clear water and surrounding mountains, and is one of the most famous lakes in China. + + + + {{ + "facts": ["West Lake has the famous attractions of Suzhou Embankment, Bai Embankment, Qiantang Bridge, San Tang Yin Yue, etc."] + }} + + + + {conversation} + + """, + } + """用户提示词""" - def __init__(self, system_prompt: str | None = None, user_prompt: str | None = None) -> None: + def __init__( + self, + system_prompt: Union[str, dict[str, str], None] = None, + user_prompt: Union[str, dict[str, str], None] = None, + ) -> None: """初始化Prompt""" super().__init__(system_prompt, user_prompt) - async def generate(self, **kwargs) -> list[str]: # noqa: ANN003 """事实提取""" conversation = convert_context_to_prompt(kwargs["conversation"]) + language = kwargs.get("language", "zh_cn") + messages = [ {"role": "system", "content": self.system_prompt}, - {"role": "user", "content": self.user_prompt.format(conversation=conversation)}, + {"role": "user", "content": self.user_prompt[language].format(conversation=conversation)}, ] result = "" llm = ReasoningLLM() diff --git a/apps/llm/patterns/rewoo.py b/apps/llm/patterns/rewoo.py index ef78d926..43e2b1bc 100644 --- a/apps/llm/patterns/rewoo.py +++ b/apps/llm/patterns/rewoo.py @@ -3,12 +3,14 @@ from apps.llm.patterns.core import CorePattern from apps.llm.reasoning import ReasoningLLM +from typing import Union class InitPlan(CorePattern): """规划生成命令行""" - system_prompt: str = r""" + system_prompt: dict[str, str] = { + "zh_cn": r""" 你是一个计划生成器。对于给定的目标,**制定一个简单的计划**,该计划可以逐步生成合适的命令行参数和标志。 你会收到一个"命令前缀",这是已经确定和生成的命令部分。你需要基于这个前缀使用标志和参数来完成命令。 @@ -41,10 +43,54 @@ class InitPlan(CorePattern): 示例结束 让我们开始! - """ + """, + "en": r""" + You are a plan generator. For a given goal, **draft a simple plan** that can step-by-step generate the \ + appropriate command line arguments and flags. + + You will receive a "command prefix", which is the already determined and generated command part. You need to \ + use the flags and arguments based on this prefix to complete the command. + + In each step, specify which external tool to use and the tool input to get the evidence. + + The tool can be one of the following: + (1) Option["instruction"]: Query the most similar command line flag. Only accepts one input parameter, \ + "instruction" must be a search string. The search string should be detailed and contain necessary data. + (2) Argument["name"]: Place the data from the task into a specific position in the command line. \ + Accepts two input parameters. + + All steps must start with "Plan: " and be less than 150 words. + Do not add any extra steps. + Ensure each step contains all the required information - do not skip steps. + Do not add any extra data after the evidence. + + Start example + + Task: Run a new alpine:latest container in the background, mount the host /root folder to /data, and execute \ + the top command. + Prefix: `docker run` + Usage: `docker run ${OPTS} ${image} ${command}`. This is a Python template string. OPTS is a placeholder for all \ + flags. The arguments must be one of ["image", "command"]. + Prefix description: The description of binary program `docker` is "Docker container platform", and the \ + description of `run` subcommand is "Create and run a new container from an image". + + Plan: I need a flag to make the container run in the background. #E1 = Option[Run a single container in the \ + background] + Plan: I need a flag to mount the host /root directory to /data directory in the container. #E2 = Option[Mount \ + host /root directory to /data directory] + Plan: I need to parse the image name from the task. #E3 = Argument[image] + Plan: I need to specify the command to be run in the container. #E4 = Argument[command] + Final: Assemble the above clues to generate the final command. #F + + End example + + Let's get started! + """, + } """系统提示词""" - user_prompt: str = r""" + user_prompt: dict[str, str] = { + "zh_cn": r""" 任务:{instruction} 前缀:`{binary_name} {subcmd_name}` 用法:`{subcmd_usage}`。这是一个Python模板字符串。OPTS是所有标志的占位符。参数必须是 {argument_list} 其中之一。 @@ -52,10 +98,25 @@ class InitPlan(CorePattern): "{subcmd_description}"。 请生成相应的计划。 - """ + """, + "en": r""" + Task: {instruction} + Prefix: `{binary_name} {subcmd_name}` + Usage: `{subcmd_usage}`. This is a Python template string. OPTS is a placeholder for all flags. The arguments \ + must be one of {argument_list}. + Prefix description: The description of binary program `{binary_name}` is "{binary_description}", and the \ + description of `{subcmd_name}` subcommand is "{subcmd_description}". + + Please generate the corresponding plan. + """, + } """用户提示词""" - def __init__(self, system_prompt: str | None = None, user_prompt: str | None = None) -> None: + def __init__( + self, + system_prompt: Union[str, dict[str, str], None] = None, + user_prompt: Union[str, dict[str, str], None] = None, + ) -> None: """处理Prompt""" super().__init__(system_prompt, user_prompt) @@ -64,6 +125,7 @@ class InitPlan(CorePattern): spec = kwargs["spec"] binary_name = kwargs["binary_name"] subcmd_name = kwargs["subcmd_name"] + language = kwargs.get("language", "zh_cn") binary_description = spec[binary_name][0] subcmd_usage = spec[binary_name][2][subcmd_name][1] subcmd_description = spec[binary_name][2][subcmd_name][0] @@ -73,16 +135,19 @@ class InitPlan(CorePattern): argument_list += [key] messages = [ - {"role": "system", "content": self.system_prompt}, - {"role": "user", "content": self.user_prompt.format( - instruction=kwargs["instruction"], - binary_name=binary_name, - subcmd_name=subcmd_name, - binary_description=binary_description, - subcmd_description=subcmd_description, - subcmd_usage=subcmd_usage, - argument_list=argument_list, - )}, + {"role": "system", "content": self.system_prompt[language]}, + { + "role": "user", + "content": self.user_prompt[language].format( + instruction=kwargs["instruction"], + binary_name=binary_name, + subcmd_name=subcmd_name, + binary_description=binary_description, + subcmd_description=subcmd_description, + subcmd_usage=subcmd_usage, + argument_list=argument_list, + ), + }, ] result = "" @@ -98,7 +163,8 @@ class InitPlan(CorePattern): class PlanEvaluator(CorePattern): """计划评估器""" - system_prompt: str = r""" + system_prompt: dict[str, str] = { + "zh_cn": r""" 你是一个计划评估器。你的任务是评估给定的计划是否合理和完整。 一个好的计划应该: @@ -115,29 +181,64 @@ class PlanEvaluator(CorePattern): 请回复: "VALID" - 如果计划良好且完整 "INVALID: <原因>" - 如果计划有问题,请解释原因 - """ + """, + "en": r""" + You are a plan evaluator. Your task is to evaluate whether the given plan is reasonable and complete. + + A good plan should: + 1. Cover all requirements of the original task + 2. Use appropriate tools to collect necessary information + 3. Have clear and logical steps + 4. Have no redundant or unnecessary steps + + For each step in the plan, evaluate: + 1. Whether the tool selection is appropriate + 2. Whether the input parameters are clear and sufficient + 3. Whether this step helps achieve the final goal + + Please reply: + "VALID" - If the plan is good and complete + "INVALID: <原因>" - If the plan has problems, please explain the reason + """, + } """系统提示词""" - user_prompt: str = r""" + user_prompt: dict[str, str] = { + "zh_cn": r""" 任务:{instruction} 计划:{plan} 评估计划并回复"VALID"或"INVALID: <原因>"。 - """ + """, + "en": r""" + Task: {instruction} + Plan: {plan} + + Evaluate the plan and reply with "VALID" or "INVALID: <原因>". + """, + } """用户提示词""" - def __init__(self, system_prompt: str | None = None, user_prompt: str | None = None) -> None: + def __init__( + self, + system_prompt: Union[str, dict[str, str], None] = None, + user_prompt: Union[str, dict[str, str], None] = None, + ) -> None: """初始化Prompt""" super().__init__(system_prompt, user_prompt) async def generate(self, **kwargs) -> str: """生成计划评估结果""" + language = kwargs.get("language", "zh_cn") messages = [ - {"role": "system", "content": self.system_prompt}, - {"role": "user", "content": self.user_prompt.format( - instruction=kwargs["instruction"], - plan=kwargs["plan"], - )}, + {"role": "system", "content": self.system_prompt[language]}, + { + "role": "user", + "content": self.user_prompt[language].format( + instruction=kwargs["instruction"], + plan=kwargs["plan"], + ), + }, ] result = "" @@ -153,7 +254,8 @@ class PlanEvaluator(CorePattern): class RePlanner(CorePattern): """重新规划器""" - system_prompt: str = r""" + system_prompt: dict[str, str] = { + "zh_cn": r""" 你是一个计划重新规划器。当计划被评估为无效时,你需要生成一个新的、改进的计划。 新计划应该: @@ -167,31 +269,64 @@ class RePlanner(CorePattern): - 包含带有适当参数的工具使用 - 保持步骤简洁和重点突出 - 以"Final"步骤结束 - """ + """, + "en": r""" + You are a plan replanner. When the plan is evaluated as invalid, you need to generate a new, improved plan. + + The new plan should: + 1. Solve all problems mentioned in the evaluation + 2. Keep the same format as the original plan + 3. Be more precise and complete + 4. Use appropriate tools for each step + + Follow the same format as the original plan: + - Each step should start with "Plan: " + - Include tool usage with appropriate parameters + - Keep steps concise and focused + - End with the "Final" step + """, + } """系统提示词""" - user_prompt: str = r""" + user_prompt: dict[str, str] = { + "zh_cn": r""" 任务:{instruction} 原始计划:{plan} 评估:{evaluation} 生成一个新的、改进的计划,解决评估中提到的所有问题。 - """ + """, + "en": r""" + Task: {instruction} + Original Plan: {plan} + Evaluation: {evaluation} + + Generate a new, improved plan that solves all problems mentioned in the evaluation. + """, + } """用户提示词""" - def __init__(self, system_prompt: str | None = None, user_prompt: str | None = None) -> None: + def __init__( + self, + system_prompt: Union[str, dict[str, str], None] = None, + user_prompt: Union[str, dict[str, str], None] = None, + ) -> None: """初始化Prompt""" super().__init__(system_prompt, user_prompt) async def generate(self, **kwargs) -> str: """生成重新规划结果""" + language = kwargs.get("language", "zh_cn") messages = [ - {"role": "system", "content": self.system_prompt}, - {"role": "user", "content": self.user_prompt.format( - instruction=kwargs["instruction"], - plan=kwargs["plan"], - evaluation=kwargs["evaluation"], - )}, + {"role": "system", "content": self.system_prompt[language]}, + { + "role": "user", + "content": self.user_prompt[language].format( + instruction=kwargs["instruction"], + plan=kwargs["plan"], + evaluation=kwargs["evaluation"], + ), + }, ] result = "" diff --git a/apps/llm/patterns/rewrite.py b/apps/llm/patterns/rewrite.py index 15d52ab2..20de656a 100644 --- a/apps/llm/patterns/rewrite.py +++ b/apps/llm/patterns/rewrite.py @@ -4,6 +4,7 @@ import logging from pydantic import BaseModel, Field +from textwrap import dedent from apps.llm.function import JsonGenerator from apps.llm.patterns.core import CorePattern @@ -21,7 +22,8 @@ class QuestionRewriteResult(BaseModel): class QuestionRewrite(CorePattern): """问题补全与重写""" - system_prompt: str = r""" + system_prompt: dict[str, str] = { + "zh_cn": r""" 根据历史对话,推断用户的实际意图并补全用户的提问内容,历史对话被包含在标签中,用户意图被包含在标签中。 @@ -72,26 +74,87 @@ class QuestionRewrite(CorePattern): {question} + """, + "en": r""" + + + Based on the historical dialogue, infer the user's actual intent and complete the user's question. The historical dialogue is contained within the tags, and the user's intent is contained within the tags. + Requirements: + 1. Please output in JSON format, referring to the example provided below; do not include any XML tags or any explanatory notes; + 2. If the user's current question is unrelated to the previous dialogue or you believe the user's question is already complete enough, directly output the user's question. + 3. The completed content must be precise and appropriate; do not fabricate any content. + 4. Output only the completed question; do not include any other content. + Example output format: + {{ + "question": "The completed question" + }} + + + + + + + What are the features of openEuler? + + + Compared to other operating systems, openEuler's features include support for multiple hardware architectures and providing a stable, secure, and efficient operating system platform. + + + + + What are the advantages of openEuler? + + + The advantages of openEuler include being open-source, having community support, and optimizations for cloud and edge computing. + + + + + + More details? + + + {{ + "question": "What are the features of openEuler? Please elaborate on its advantages and application scenarios." + }} + + + + + {history} + + + {question} + """ + } + """用户提示词""" - user_prompt: str = """ + user_prompt: dict[str, str] = { + "zh_cn": r""" 请输出补全后的问题 - """ + """, + "en": r""" + + Please output the completed question + + """} async def generate(self, **kwargs) -> str: # noqa: ANN003 """问题补全与重写""" history = kwargs.get("history", []) question = kwargs["question"] llm = kwargs.get("llm", None) + language = kwargs.get("language", "zh_cn") if not llm: llm = ReasoningLLM() leave_tokens = llm._config.max_tokens leave_tokens -= TokenCalculator().calculate_token_length( messages=[ - {"role": "system", "content": self.system_prompt.format(history="", question=question)}, - {"role": "user", "content": self.user_prompt} + {"role": "system", "content": self.system_prompt[language].format(history="", question=question)}, + {"role": "user", "content": self.user_prompt[language]} ] ) if leave_tokens <= 0: @@ -113,8 +176,8 @@ class QuestionRewrite(CorePattern): qa = sub_qa + qa index += 2 messages = [ - {"role": "system", "content": self.system_prompt.format(history=qa, question=question)}, - {"role": "user", "content": self.user_prompt} + {"role": "system", "content": self.system_prompt[language].format(history=qa, question=question)}, + {"role": "user", "content": self.user_prompt[language]} ] result = "" async for chunk in llm.call(messages, streaming=False): diff --git a/apps/routers/chat.py b/apps/routers/chat.py index 26a87481..65d121a9 100644 --- a/apps/routers/chat.py +++ b/apps/routers/chat.py @@ -47,6 +47,9 @@ async def init_task(post_body: RequestData, user_sub: str, session_id: str) -> T if post_body.new_task: task.runtime.question = post_body.question task.ids.group_id = post_body.group_id + task.runtime.question = post_body.question + task.ids.group_id = post_body.group_id + task.language = "zh_cn" if post_body.language in {"zh_cn", "zh"} else "en" return task diff --git a/apps/routers/flow.py b/apps/routers/flow.py index 68c0c9f3..c054f459 100644 --- a/apps/routers/flow.py +++ b/apps/routers/flow.py @@ -36,6 +36,11 @@ router = APIRouter( ], ) +""" +get_services, get_flow, put_flow 三个函数需要前端传入 language 参数,已验证可行 +""" + + @router.get( "/service", @@ -46,9 +51,10 @@ router = APIRouter( ) async def get_services( user_sub: Annotated[str, Depends(get_user)], + language: str = Query("zh_cn", description="语言参数,默认为中文") ) -> NodeServiceListRsp: """获取用户可访问的节点元数据所在服务的信息""" - services = await FlowManager.get_service_by_user_id(user_sub) + services = await FlowManager.get_service_by_user_id(user_sub, language) if services is None: return NodeServiceListRsp( code=status.HTTP_404_NOT_FOUND, @@ -75,6 +81,7 @@ async def get_flow( user_sub: Annotated[str, Depends(get_user)], app_id: Annotated[str, Query(alias="appId")], flow_id: Annotated[str, Query(alias="flowId")], + language: str = Query("zh_cn", description="语言参数,默认为中文") ) -> JSONResponse: """获取流拓扑结构""" if not await AppManager.validate_user_app_access(user_sub, app_id): @@ -86,7 +93,7 @@ async def get_flow( result=FlowStructureGetMsg(), ).model_dump(exclude_none=True, by_alias=True), ) - result = await FlowManager.get_flow_by_app_and_flow_id(app_id, flow_id) + result = await FlowManager.get_flow_by_app_and_flow_id(app_id, flow_id, language) if result is None: return JSONResponse( status_code=status.HTTP_404_NOT_FOUND, @@ -121,6 +128,7 @@ async def put_flow( app_id: Annotated[str, Query(alias="appId")], flow_id: Annotated[str, Query(alias="flowId")], put_body: Annotated[PutFlowReq, Body(...)], + language: str = Query("zh_cn", description="语言参数,默认为中文") ) -> JSONResponse: """修改流拓扑结构""" if not await AppManager.validate_app_belong_to_user(user_sub, app_id): @@ -148,7 +156,7 @@ async def put_flow( result=FlowStructurePutMsg(), ).model_dump(exclude_none=True, by_alias=True), ) - flow = await FlowManager.get_flow_by_app_and_flow_id(app_id, flow_id) + flow = await FlowManager.get_flow_by_app_and_flow_id(app_id, flow_id, language) await AppCenterManager.update_app_publish_status(app_id, user_sub) if flow is None: return JSONResponse( diff --git a/apps/scheduler/call/api/api.py b/apps/scheduler/call/api/api.py index e1891f72..f156b176 100644 --- a/apps/scheduler/call/api/api.py +++ b/apps/scheduler/call/api/api.py @@ -60,9 +60,17 @@ class API(CoreCall, input_model=APIInput, output_model=APIOutput): query: dict[str, Any] = Field(description="已知的部分请求参数", default={}) @classmethod - def info(cls) -> CallInfo: + def info(cls, language: str = "zh_cn") -> CallInfo: """返回Call的名称和描述""" - return CallInfo(name="API调用", description="向某一个API接口发送HTTP请求,获取数据。") + name_map = { + "zh_cn": "API调用", + "en": "API Call", + } + description_map = { + "zh_cn": "向某一个API接口发送HTTP请求,获取数据", + "en": "Send an HTTP request to an API to obtain data", + } + return CallInfo(name=name_map[language], description=description_map[language]) async def _init(self, call_vars: CallVars) -> APIInput: """初始化API调用工具""" @@ -99,8 +107,8 @@ class API(CoreCall, input_model=APIInput, output_model=APIOutput): body=self.body, ) - async def _exec(self, input_data: dict[str, Any]) -> AsyncGenerator[CallOutputChunk, None]: - """调用API,然后返回LLM解析后的数据""" + async def _exec(self, input_data: dict[str, Any], language="zh_cn") -> AsyncGenerator[CallOutputChunk, None]: + """调用API,然后返回LLM解析后的数据""" # !!!!! self._client = httpx.AsyncClient(timeout=self.timeout) input_obj = APIInput.model_validate(input_data) try: diff --git a/apps/scheduler/call/choice/choice.py b/apps/scheduler/call/choice/choice.py index 01ac7106..1a2a22c4 100644 --- a/apps/scheduler/call/choice/choice.py +++ b/apps/scheduler/call/choice/choice.py @@ -38,9 +38,22 @@ class Choice(CoreCall, input_model=ChoiceInput, output_model=ChoiceOutput): ChoiceBranch(conditions=[Condition()], is_default=False)]) @classmethod - def info(cls) -> CallInfo: + def info(cls, language: str = "zh_cn") -> CallInfo: """返回Call的名称和描述""" - return CallInfo(name="选择器", description="使用大模型或使用程序做出判断") + name_map = { + "zh_cn": "Choice", + "en": "Choice", + } + description_map = { + "zh_cn": "使用大模型或使用程序做出判断", + "en": "Use a large model or a program to make a decision", + } + return CallInfo(name=name_map[language], description=description_map[language]) + + def _raise_value_error(self, msg: str) -> None: + """统一处理 ValueError 异常抛出""" + logger.warning(msg) + raise ValueError(msg) async def _prepare_message(self, call_vars: CallVars) -> list[dict[str, Any]]: """替换choices中的系统变量""" @@ -135,7 +148,7 @@ class Choice(CoreCall, input_model=ChoiceInput, output_model=ChoiceOutput): ) async def _exec( - self, input_data: dict[str, Any] + self, input_data: dict[str, Any], language: str = "zh_cn" ) -> AsyncGenerator[CallOutputChunk, None]: """执行Choice工具""" # 解析输入数据 diff --git a/apps/scheduler/call/core.py b/apps/scheduler/call/core.py index 5bed8030..551ae5ef 100644 --- a/apps/scheduler/call/core.py +++ b/apps/scheduler/call/core.py @@ -181,9 +181,12 @@ class CoreCall(BaseModel): async def _after_exec(self, input_data: dict[str, Any]) -> None: """Call类实例的执行后方法""" - async def exec(self, executor: "StepExecutor", input_data: dict[str, Any]) -> AsyncGenerator[CallOutputChunk, None]: + + async def exec(self, executor: "StepExecutor", input_data: dict[str, Any], language: str = "zh_cn") -> AsyncGenerator[CallOutputChunk, None]: """Call类实例的执行方法""" - async for chunk in self._exec(input_data): + # 部分节点如 start/end 不能传入 language + exec_args = (input_data, language) if input_data else (self.input,) + async for chunk in self._exec(*exec_args): yield chunk await self._after_exec(input_data) diff --git a/apps/scheduler/call/facts/facts.py b/apps/scheduler/call/facts/facts.py index 2b9df0c6..eed98495 100644 --- a/apps/scheduler/call/facts/facts.py +++ b/apps/scheduler/call/facts/facts.py @@ -31,9 +31,17 @@ class FactsCall(CoreCall, input_model=FactsInput, output_model=FactsOutput): answer: str = Field(description="用户输入") @classmethod - def info(cls) -> CallInfo: + def info(cls, language: str = "zh_cn") -> CallInfo: """返回Call的名称和描述""" - return CallInfo(name="提取事实", description="从对话上下文和文档片段中提取事实。") + name_map = { + "zh_cn": "提取事实", + "en": "Fact Extraction", + } + description_map = { + "zh_cn": "从对话上下文和文档片段中提取事实。", + "en": "Extract facts from the conversation context and document snippnets.", + } + return CallInfo(name=name_map[language], description=description_map[language]) @classmethod async def instance(cls, executor: "StepExecutor", node: NodePool | None, **kwargs: Any) -> Self: @@ -62,7 +70,8 @@ class FactsCall(CoreCall, input_model=FactsInput, output_model=FactsOutput): message=message, ) - async def _exec(self, input_data: dict[str, Any]) -> AsyncGenerator[CallOutputChunk, None]: + + async def _exec(self, input_data: dict[str, Any], language: str = "zh_cn") -> AsyncGenerator[CallOutputChunk, None]: """执行工具""" data = FactsInput(**input_data) # jinja2 环境 @@ -74,7 +83,7 @@ class FactsCall(CoreCall, input_model=FactsInput, output_model=FactsOutput): ) # 提取事实信息 - facts_tpl = env.from_string(FACTS_PROMPT) + facts_tpl = env.from_string(FACTS_PROMPT[language]) facts_prompt = facts_tpl.render(conversation=data.message) facts_obj: FactsGen = await self._json([ {"role": "system", "content": "You are a helpful assistant."}, @@ -82,7 +91,7 @@ class FactsCall(CoreCall, input_model=FactsInput, output_model=FactsOutput): ], FactsGen) # type: ignore[arg-type] # 更新用户画像 - domain_tpl = env.from_string(DOMAIN_PROMPT) + domain_tpl = env.from_string(DOMAIN_PROMPT[language]) domain_prompt = domain_tpl.render(conversation=data.message) domain_list: DomainGen = await self._json([ {"role": "system", "content": "You are a helpful assistant."}, @@ -100,9 +109,10 @@ class FactsCall(CoreCall, input_model=FactsInput, output_model=FactsOutput): ).model_dump(by_alias=True, exclude_none=True), ) - async def exec(self, executor: "StepExecutor", input_data: dict[str, Any]) -> AsyncGenerator[CallOutputChunk, None]: + + async def exec(self, executor: "StepExecutor", input_data: dict[str, Any], language: str = "zh_cn") -> AsyncGenerator[CallOutputChunk, None]: """执行工具""" - async for chunk in self._exec(input_data): + async for chunk in self._exec(input_data, language=language): content = chunk.content if not isinstance(content, dict): err = "[FactsCall] 工具输出格式错误" diff --git a/apps/scheduler/call/facts/prompt.py b/apps/scheduler/call/facts/prompt.py index b2b2513f..a8b7b0be 100644 --- a/apps/scheduler/call/facts/prompt.py +++ b/apps/scheduler/call/facts/prompt.py @@ -3,7 +3,9 @@ from textwrap import dedent -DOMAIN_PROMPT: str = dedent(r""" +DOMAIN_PROMPT: dict[str, str] = { + "zh_cn": dedent( + r""" 根据对话上文,提取推荐系统所需的关键词标签,要求: @@ -35,8 +37,47 @@ DOMAIN_PROMPT: str = dedent(r""" {% endfor %} -""") -FACTS_PROMPT: str = dedent(r""" +""" + ), + "en": dedent( + r""" + + + Extract keywords for recommendation system based on the previous conversation, requirements: + 1. Entity nouns, technical terms, time range, location, product, etc. can be keyword tags + 2. At least one keyword is related to the topic of the conversation + 3. Tags should be concise and not repeated, not exceeding 10 characters + 4. Output in JSON format, do not include XML tags, do not include any explanatory notes + + + + + What's the weather like in Beijing? + Beijing is sunny today. + + + + { + "keywords": ["Beijing", "weather"] + } + + + + + + {% for item in conversation %} + <{{item['role']}}> + {{item['content']}} + + {% endfor %} + + +""" + ), +} + +FACTS_PROMPT: dict[str, str] = {"zh_cn":dedent( + r""" 从对话中提取关键信息,并将它们组织成独一无二的、易于理解的事实,包含用户偏好、关系、实体等有用信息。 @@ -80,4 +121,53 @@ FACTS_PROMPT: str = dedent(r""" {% endfor %} -""") +""" +), + "en": dedent( + r""" + + + Extract key information from the conversation and organize it into unique, easily understandable facts, including user preferences, relationships, entities, etc. + The following are the types of information you need to pay attention to and detailed instructions on how to handle input data. + + **Types of information you need to pay attention to** + 1. Entities: Entities involved in the conversation. For example: names, locations, organizations, events, etc. + 2. Preferences: Attitudes towards entities. For example: like, dislike, etc. + 3. Relationships: Relationships between users and entities, or between two entities. For example: include, parallel, mutually exclusive, etc. + 4. Actions: Specific actions that affect entities. For example: query, search, browse, click, etc. + + **Requirements** + 1. Facts must be accurate and can only be extracted from the conversation. Do not include the information in the example in the output. + 2. Facts must be clear, concise, and easy to understand. Must be less than 30 words. + 3. Output in the following JSON format: + + { + "facts": ["Fact 1", "Fact 2", "Fact 3"] + } + + + + + What are the attractions in Hangzhou West Lake? + West Lake in Hangzhou, Zhejiang Province, China, is a famous scenic spot known for its beautiful natural scenery and rich cultural heritage. Many notable attractions surround West Lake, including the renowned Su Causeway, Bai Causeway, Broken Bridge, and the Three Pools Mirroring the Moon. Famous for its crystal-clear waters and the surrounding mountains, West Lake is one of China's most famous lakes. + + + + { + "facts": ["Hangzhou West Lake has famous attractions such as Suzhou Embankment, Bai Budi, Qiantang Bridge, San Tang Yue, etc."] + } + + + + + + {% for item in conversation %} + <{{item['role']}}> + {{item['content']}} + + {% endfor %} + + +""" + ) +} diff --git a/apps/scheduler/call/graph/graph.py b/apps/scheduler/call/graph/graph.py index c2728f17..2ca2c359 100644 --- a/apps/scheduler/call/graph/graph.py +++ b/apps/scheduler/call/graph/graph.py @@ -27,9 +27,17 @@ class Graph(CoreCall, input_model=RenderInput, output_model=RenderOutput): @classmethod - def info(cls) -> CallInfo: + def info(cls, language: str = "zh_cn") -> CallInfo: """返回Call的名称和描述""" - return CallInfo(name="图表", description="将SQL查询出的数据转换为图表") + name_map = { + "zh_cn": "图表", + "en": "Chart", + } + description_map = { + "zh_cn": "将SQL查询出的数据转换为图表。", + "en": "Convert the data queried by SQL into a chart.", + } + return CallInfo(name=name_map[language], description=description_map[language]) async def _init(self, call_vars: CallVars) -> RenderInput: @@ -55,7 +63,7 @@ class Graph(CoreCall, input_model=RenderInput, output_model=RenderOutput): ) - async def _exec(self, input_data: dict[str, Any]) -> AsyncGenerator[CallOutputChunk, None]: + async def _exec(self, input_data: dict[str, Any], language: str = "zh_cn") -> AsyncGenerator[CallOutputChunk, None]: """运行Render Call""" data = RenderInput(**input_data) diff --git a/apps/scheduler/call/llm/llm.py b/apps/scheduler/call/llm/llm.py index 6a679dce..2922994b 100644 --- a/apps/scheduler/call/llm/llm.py +++ b/apps/scheduler/call/llm/llm.py @@ -40,10 +40,17 @@ class LLM(CoreCall, input_model=LLMInput, output_model=LLMOutput): @classmethod - def info(cls) -> CallInfo: + def info(cls, language: str = "zh_cn") -> CallInfo: """返回Call的名称和描述""" - return CallInfo(name="大模型", description="以指定的提示词和上下文信息调用大模型,并获得输出。") - + name_map = { + "zh_cn": "大模型", + "en": "Foundation Model", + } + description_map = { + "zh_cn": "以指定的提示词和上下文信息调用大模型,并获得输出。", + "en": "Call the foundation model with specified prompt and context, and obtain the output.", + } + return CallInfo(name=name_map[language], description=description_map[language]) async def _prepare_message(self, call_vars: CallVars) -> list[dict[str, Any]]: """准备消息""" @@ -101,7 +108,7 @@ class LLM(CoreCall, input_model=LLMInput, output_model=LLMOutput): ) - async def _exec(self, input_data: dict[str, Any]) -> AsyncGenerator[CallOutputChunk, None]: + async def _exec(self, input_data: dict[str, Any], language: str = "zh_cn") -> AsyncGenerator[CallOutputChunk, None]: """运行LLM Call""" data = LLMInput(**input_data) try: diff --git a/apps/scheduler/call/llm/prompt.py b/apps/scheduler/call/llm/prompt.py index 0f227dca..277fe189 100644 --- a/apps/scheduler/call/llm/prompt.py +++ b/apps/scheduler/call/llm/prompt.py @@ -4,14 +4,31 @@ from textwrap import dedent LLM_CONTEXT_PROMPT = dedent( + # r""" + # 以下是对用户和AI间对话的简短总结,在中给出: + # + # {{ summary }} + # + # 你作为AI,在回答用户的问题前,需要获取必要的信息。为此,你调用了一些工具,并获得了它们的输出: + # 工具的输出数据将在中给出, 其中为工具的名称,为工具的输出数据。 + # + # {% for tool in history_data %} + # + # {{ tool.step_name }} + # {{ tool.step_description }} + # {{ tool.output_data }} + # + # {% endfor %} + # + # """, r""" - 以下是对用户和AI间对话的简短总结,在中给出: + The following is a brief summary of the user and AI conversation, given in : {{ summary }} - 你作为AI,在回答用户的问题前,需要获取必要的信息。为此,你调用了一些工具,并获得了它们的输出: - 工具的输出数据将在中给出, 其中为工具的名称,为工具的输出数据。 + As an AI, before answering the user's question, you need to obtain necessary information. For this purpose, you have called some tools and obtained their outputs: + The output data of the tools will be given in , where is the name of the tool and is the output data of the tool. {% for tool in history_data %} @@ -21,15 +38,33 @@ LLM_CONTEXT_PROMPT = dedent( {% endfor %} - """, + """ ).strip("\n") + LLM_DEFAULT_PROMPT = dedent( + # r""" + # + # 你是一个乐于助人的智能助手。请结合给出的背景信息, 回答用户的提问。 + # 当前时间:{{ time }},可以作为时间参照。 + # 用户的问题将在中给出,上下文背景信息将在中给出。 + # 注意:输出不要包含任何XML标签,不要编造任何信息。若你认为用户提问与背景信息无关,请忽略背景信息直接作答。 + # + # + # {{ question }} + # + # + # {{ context }} + # + # 现在,输出你的回答: + # """, r""" - 你是一个乐于助人的智能助手。请结合给出的背景信息, 回答用户的提问。 - 当前时间:{{ time }},可以作为时间参照。 - 用户的问题将在中给出,上下文背景信息将在中给出。 - 注意:输出不要包含任何XML标签,不要编造任何信息。若你认为用户提问与背景信息无关,请忽略背景信息直接作答。 + You are a helpful AI assistant. Please answer the user's question based on the given background information. + Current time: {{ time }}, which can be used as a reference. + The user's question will be given in , and the context background information will be given in . + + Respond using the same language as the user's question, unless the user explicitly requests a specific language—then follow that request. + Note: Do not include any XML tags in the output. Do not make up any information. If you think the user's question is unrelated to the background information, please ignore the background information and answer directly. @@ -39,12 +74,13 @@ LLM_DEFAULT_PROMPT = dedent( {{ context }} - - 现在,输出你的回答: - """, + Now, please output your answer: + """ ).strip("\n") -LLM_ERROR_PROMPT = dedent( - r""" + +LLM_ERROR_PROMPT = { + "zh_cn": dedent( + r""" 你是一位智能助手,能够根据用户的问题,使用Python工具获取信息,并作出回答。你在使用工具解决回答用户的问题时,发生了错误。 你的任务是:分析工具(Python程序)的异常信息,分析造成该异常可能的原因,并以通俗易懂的方式,将原因告知用户。 @@ -67,8 +103,36 @@ LLM_ERROR_PROMPT = dedent( 现在,输出你的回答: - """, -).strip("\n") + """ + ).strip("\n"), + "en": dedent( + r""" + + You are an intelligent assistant. When using Python tools to answer user questions, an error occurred. + Your task is: Analyze the exception information of the tool (Python program), analyze the possible causes of the error, and inform the user in an easy-to-understand way. + + Current time: {{ time }}, which can be used as a reference. + The program exception information that occurred will be given in , the user's question will be given in , and the context background information will be given in . + Note: Do not include any XML tags in the output. Do not make up any information. If you think the user's question is unrelated to the background information, please ignore the background information. + + + + {{ error_info }} + + + + {{ question }} + + + + {{ context }} + + + Now, please output your answer: + """ + ).strip("\n"), +} + RAG_ANSWER_PROMPT = dedent( r""" diff --git a/apps/scheduler/call/mcp/mcp.py b/apps/scheduler/call/mcp/mcp.py index bd0257b4..0588d186 100644 --- a/apps/scheduler/call/mcp/mcp.py +++ b/apps/scheduler/call/mcp/mcp.py @@ -36,19 +36,29 @@ class MCP(CoreCall, input_model=MCPInput, output_model=MCPOutput): to_user: bool = Field(description="是否将结果返回给用户", default=True) @classmethod - def info(cls) -> CallInfo: + def info(cls, language: str = "zh_cn") -> CallInfo: """ 返回Call的名称和描述 :return: Call的名称和描述 :rtype: CallInfo """ - return CallInfo(name="MCP", description="调用MCP Server,执行工具") + name_map = { + "zh_cn": "MCP", + "en": "MCP", + } + description_map = { + "zh_cn": "调用MCP Server,执行工具", + "en": "Call the MCP Server to execute tools", + } + return CallInfo(name=name_map[language], description=description_map[language]) async def _init(self, call_vars: CallVars) -> MCPInput: """初始化MCP""" # 获取MCP交互类 - self._host = MCPHost(call_vars.ids.user_sub, call_vars.ids.task_id, call_vars.ids.flow_id, self.description) + self._host = MCPHost( + call_vars.ids.user_sub, call_vars.ids.task_id, call_vars.ids.flow_id, self.description + ) self._tool_list = await self._host.get_tool_list(self.mcp_list) self._call_vars = call_vars @@ -61,31 +71,37 @@ class MCP(CoreCall, input_model=MCPInput, output_model=MCPOutput): return MCPInput(avaliable_tools=avaliable_tools, max_steps=self.max_steps) - async def _exec(self, input_data: dict[str, Any]) -> AsyncGenerator[CallOutputChunk, None]: + + async def _exec(self, input_data: dict[str, Any], language: str = "zh_cn") -> AsyncGenerator[CallOutputChunk, None]: """执行MCP""" # 生成计划 - async for chunk in self._generate_plan(): + async for chunk in self._generate_plan(language): yield chunk # 执行计划 plan_list = deepcopy(self._plan.plans) while len(plan_list) > 0: - async for chunk in self._execute_plan_item(plan_list.pop(0)): + async for chunk in self._execute_plan_item(plan_list.pop(0), language): yield chunk # 生成总结 - async for chunk in self._generate_answer(): + async for chunk in self._generate_answer(language): yield chunk - async def _generate_plan(self) -> AsyncGenerator[CallOutputChunk, None]: + async def _generate_plan(self, language) -> AsyncGenerator[CallOutputChunk, None]: """生成执行计划""" + start_output = { + "zh_cn": "[MCP] 开始生成计划...\n\n\n\n", + "en": "[MCP] Start generating plan...\n\n\n\n", + } + # 开始提示 - yield self._create_output("[MCP] 开始生成计划...\n\n\n\n", MCPMessageType.PLAN_BEGIN) + yield self._create_output(start_output[language], MCPMessageType.PLAN_BEGIN) # 选择工具并生成计划 selector = MCPSelector() - top_tool = await selector.select_top_tool(self._call_vars.question, self.mcp_list) - planner = MCPPlanner(self._call_vars.question) + top_tool = await selector.select_top_tool(self._call_vars.question, self.mcp_list, language=language) + planner = MCPPlanner(self._call_vars.question, language) self._plan = await planner.create_plan(top_tool, self.max_steps) # 输出计划 @@ -93,13 +109,18 @@ class MCP(CoreCall, input_model=MCPInput, output_model=MCPOutput): for plan_item in self._plan.plans: plan_str += f"[+] {plan_item.content}; {plan_item.tool}[{plan_item.instruction}]\n\n" + end_output = { + "zh_cn": f"[MCP] 计划生成完成:\n\n{plan_str}\n\n\n\n", + "en": f"[MCP] Plan generation completed: \n\n{plan_str}\n\n\n\n", + } + yield self._create_output( - f"[MCP] 计划生成完成:\n\n{plan_str}\n\n\n\n", + end_output[language], MCPMessageType.PLAN_END, data=self._plan.model_dump(), ) - async def _execute_plan_item(self, plan_item: MCPPlanItem) -> AsyncGenerator[CallOutputChunk, None]: + async def _execute_plan_item(self, plan_item: MCPPlanItem, language:str = "zh_cn") -> AsyncGenerator[CallOutputChunk, None]: """执行单个计划项""" # 判断是否为Final if plan_item.tool == "Final": @@ -120,7 +141,7 @@ class MCP(CoreCall, input_model=MCPInput, output_model=MCPOutput): # 调用工具 try: - result = await self._host.call_tool(tool, plan_item) + result = await self._host.call_tool(tool, plan_item, language) except Exception as e: err = f"[MCP] 工具 {tool.name} 调用失败: {e!s}" logger.exception(err) @@ -136,21 +157,29 @@ class MCP(CoreCall, input_model=MCPInput, output_model=MCPOutput): }, ) - async def _generate_answer(self) -> AsyncGenerator[CallOutputChunk, None]: + async def _generate_answer(self, language) -> AsyncGenerator[CallOutputChunk, None]: """生成总结""" # 提示开始总结 + start_output = { + "zh_cn": "[MCP] 正在总结任务结果...\n\n", + "en": "[MCP] Start summarizing task results...\n\n", + } yield self._create_output( - "[MCP] 正在总结任务结果...\n\n", + start_output[language], MCPMessageType.FINISH_BEGIN, ) # 生成答案 - planner = MCPPlanner(self._call_vars.question) + planner = MCPPlanner(self._call_vars.question, language) answer = await planner.generate_answer(self._plan, await self._host.assemble_memory()) + end_output = { + "zh_cn": f"[MCP] 任务完成\n\n---\n\n{answer}\n\n", + "en": f"[MCP] Task summary completed\n\n{answer}\n\n" + } # 输出结果 yield self._create_output( - f"[MCP] 任务完成\n\n---\n\n{answer}\n\n", + end_output[language], MCPMessageType.FINISH_END, data=MCPOutput( message=answer, @@ -166,8 +195,11 @@ class MCP(CoreCall, input_model=MCPInput, output_model=MCPOutput): """创建输出""" if self.text_output: return CallOutputChunk(type=CallOutputType.TEXT, content=text) - return CallOutputChunk(type=CallOutputType.DATA, content=MCPMessage( - msg_type=msg_type, - message=text.strip(), - data=data or {}, - ).model_dump_json()) + return CallOutputChunk( + type=CallOutputType.DATA, + content=MCPMessage( + msg_type=msg_type, + message=text.strip(), + data=data or {}, + ).model_dump_json(), + ) diff --git a/apps/scheduler/call/rag/rag.py b/apps/scheduler/call/rag/rag.py index e27327d8..3be38576 100644 --- a/apps/scheduler/call/rag/rag.py +++ b/apps/scheduler/call/rag/rag.py @@ -38,9 +38,17 @@ class RAG(CoreCall, input_model=RAGInput, output_model=RAGOutput): tokens_limit: int = Field(description="token限制", default=8192) @classmethod - def info(cls) -> CallInfo: + def info(cls, language:str = "zh_cn") -> CallInfo: """返回Call的名称和描述""" - return CallInfo(name="知识库", description="查询知识库,从文档中获取必要信息") + name_map = { + "zh_cn": "知识库", + "en": "Knowledge Base", + } + description_map = { + "zh_cn": "查询知识库,从文档中获取必要信息", + "en": "Query the knowledge base and obtain necessary information from documents", + } + return CallInfo(name=name_map[language], description=description_map[language]) async def _init(self, call_vars: CallVars) -> RAGInput: """初始化RAG工具""" @@ -58,7 +66,7 @@ class RAG(CoreCall, input_model=RAGInput, output_model=RAGOutput): tokensLimit=self.tokens_limit, ) - async def _exec(self, input_data: dict[str, Any]) -> AsyncGenerator[CallOutputChunk, None]: + async def _exec(self, input_data: dict[str, Any], language:str = "zh_cn") -> AsyncGenerator[CallOutputChunk, None]: """调用RAG工具""" data = RAGInput(**input_data) question_obj = QuestionRewrite() diff --git a/apps/scheduler/call/search/search.py b/apps/scheduler/call/search/search.py index 73d21d7b..b2af66a0 100644 --- a/apps/scheduler/call/search/search.py +++ b/apps/scheduler/call/search/search.py @@ -17,17 +17,22 @@ class Search(CoreCall, input_model=SearchInput, output_model=SearchOutput): """搜索工具""" @classmethod - def info(cls) -> CallInfo: + def info(cls, language: str = "zh_cn") -> CallInfo: """返回Call的名称和描述""" - return CallInfo(name="搜索", description="获取搜索引擎的结果") - + name_map = { + "zh_cn": "搜索", + "en": "Search", + } + description_map = { + "zh_cn": "获取搜索引擎的结果。", + "en": "Get the results of the search engine.", + } + return CallInfo(name=name_map[language], description=description_map[language]) async def _init(self, call_vars: CallVars) -> SearchInput: """初始化工具""" pass - async def _exec(self, input_data: dict[str, Any]) -> AsyncGenerator[CallOutputChunk, None]: """执行工具""" pass - diff --git a/apps/scheduler/call/slot/prompt.py b/apps/scheduler/call/slot/prompt.py index e5650a4c..e5e7b97b 100644 --- a/apps/scheduler/call/slot/prompt.py +++ b/apps/scheduler/call/slot/prompt.py @@ -1,7 +1,8 @@ # Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. """自动参数填充工具的提示词""" -SLOT_GEN_PROMPT = r""" +SLOT_GEN_PROMPT:dict[str, str] = { + "zh_cn": r""" 你是一个可以使用工具的AI助手,正尝试使用工具来完成任务。 目前,你正在生成一个JSON参数对象,以作为调用工具的输入。 @@ -17,6 +18,7 @@ SLOT_GEN_PROMPT = r""" 3. 只输出JSON对象,不要输出任何解释说明,不要输出任何其他内容。 4. 如果JSON Schema中描述的JSON字段是可选的,则可以不输出该字段。 5. example中仅为示例,不要照搬example中的内容,不要将example中的内容作为输出。 + 6. 优先使用与用户问题相同的语言回答,但如果用户明确要求回答语言,则遵循用户的要求。 @@ -85,4 +87,91 @@ SLOT_GEN_PROMPT = r""" {{schema}} - """ + """, + "en": r""" + + You are an AI assistant capable of using tools to complete tasks. + Currently, you are generating a JSON parameter object as input for calling a tool. + Please generate a compliant JSON object based on user input, background information, tool information, and JSON Schema content. + + Background information will be provided in , tool information in , JSON Schema in , \ + and the user's question in . + Output the generated JSON object in . + + Requirements: + 1. Strictly follow the JSON format described in the JSON Schema. Do not fabricate non-existent fields. + 2. Prioritize using values from user input for JSON fields. If not available, use content from background information. + 3. Only output the JSON object. Do not include any explanations or additional content. + 4. Optional fields in the JSON Schema may be omitted. + 5. Examples are for illustration only. Do not copy content from examples or use them as output. + 6. Respond in the same language as the user's question by default, unless explicitly requested otherwise. + + + + + User asked about today's weather in Hangzhou. AI replied it's sunny, 20℃. User then asks about tomorrow's weather in Hangzhou. + + + What's the weather like in Hangzhou tomorrow? + + + Tool name: check_weather + Tool description: Query weather information for specified cities + + + { + "type": "object", + "properties": { + "city": { + "type": "string", + "description": "City name" + }, + "date": { + "type": "string", + "description": "Query date" + }, + "required": ["city", "date"] + } + } + + + { + "city": "Hangzhou", + "date": "tomorrow" + } + + + + + Historical summary of tasks given by user, provided in : + + {{summary}} + + Additional itemized information: + {{ facts }} + + + During this task, you have called some tools and obtained their outputs, provided in : + + {% for tool in history_data %} + + {{ tool.step_name }} + {{ tool.step_description }} + {{ tool.output_data }} + + {% endfor %} + + + + {{question}} + + + Tool name: {{current_tool["name"]}} + Tool description: {{current_tool["description"]}} + + + {{schema}} + + + """, +} diff --git a/apps/scheduler/call/slot/slot.py b/apps/scheduler/call/slot/slot.py index d24e1661..69fdf7fd 100644 --- a/apps/scheduler/call/slot/slot.py +++ b/apps/scheduler/call/slot/slot.py @@ -33,11 +33,20 @@ class Slot(CoreCall, input_model=SlotInput, output_model=SlotOutput): step_num: int = Field(description="历史步骤数", default=1) @classmethod - def info(cls) -> CallInfo: + def info(cls, language: str = "zh_cn") -> CallInfo: """返回Call的名称和描述""" - return CallInfo(name="参数自动填充", description="根据步骤历史,自动填充参数") - - async def _llm_slot_fill(self, remaining_schema: dict[str, Any]) -> tuple[str, dict[str, Any]]: + name_map = { + "zh_cn": "参数自动填充", + "en": "Parameter Auto-Fill", + } + description_map = { + "zh_cn": "根据步骤历史,自动填充参数", + "en": "Auto-fill parameters based on step history.", + } + return CallInfo(name=name_map[language], description=description_map[language]) + + + async def _llm_slot_fill(self, remaining_schema: dict[str, Any], language:str = "zh_cn") -> tuple[str, dict[str, Any]]: """使用大模型填充参数;若大模型解析度足够,则直接返回结果""" env = SandboxedEnvironment( loader=BaseLoader(), @@ -45,7 +54,7 @@ class Slot(CoreCall, input_model=SlotInput, output_model=SlotOutput): trim_blocks=True, lstrip_blocks=True, ) - template = env.from_string(SLOT_GEN_PROMPT) + template = env.from_string(SLOT_GEN_PROMPT[language]) conversation = [ {"role": "system", "content": "You are a helpful assistant."}, @@ -123,7 +132,8 @@ class Slot(CoreCall, input_model=SlotInput, output_model=SlotOutput): remaining_schema=remaining_schema, ) - async def _exec(self, input_data: dict[str, Any]) -> AsyncGenerator[CallOutputChunk, None]: + + async def _exec(self, input_data: dict[str, Any], language: str = "zh_cn") -> AsyncGenerator[CallOutputChunk, None]: """执行参数填充""" data = SlotInput(**input_data) @@ -137,7 +147,7 @@ class Slot(CoreCall, input_model=SlotInput, output_model=SlotOutput): ).model_dump(by_alias=True, exclude_none=True), ) return - answer, slot_data = await self._llm_slot_fill(data.remaining_schema) + answer, slot_data = await self._llm_slot_fill(data.remaining_schema, language) slot_data = self._processor.convert_json(slot_data) remaining_schema = self._processor.check_json(slot_data) diff --git a/apps/scheduler/call/sql/sql.py b/apps/scheduler/call/sql/sql.py index 3e24301d..032e6321 100644 --- a/apps/scheduler/call/sql/sql.py +++ b/apps/scheduler/call/sql/sql.py @@ -27,24 +27,29 @@ class SQL(CoreCall, input_model=SQLInput, output_model=SQLOutput): """SQL工具。用于调用外置的Chat2DB工具的API,获得SQL语句;再在PostgreSQL中执行SQL语句,获得数据。""" database_url: str = Field(description="数据库连接地址") - table_name_list: list[str] = Field(description="表名列表",default=[]) - top_k: int = Field(description="生成SQL语句数量",default=5) + table_name_list: list[str] = Field(description="表名列表", default=[]) + top_k: int = Field(description="生成SQL语句数量", default=5) use_llm_enhancements: bool = Field(description="是否使用大模型增强", default=False) - @classmethod - def info(cls) -> CallInfo: + def info(cls, language: str = "zh_cn") -> CallInfo: """返回Call的名称和描述""" - return CallInfo(name="SQL查询", description="使用大模型生成SQL语句,用于查询数据库中的结构化数据") - + name_map = { + "zh_cn": "SQL查询", + "en": "SQL Query", + } + description_map = { + "zh_cn": "使用大模型生成SQL语句,用于查询数据库中的结构化数据", + "en": "Use the foundation model to generate SQL statements to query structured data in the databases", + } + return CallInfo(name=name_map[language], description=description_map[language]) - async def _init(self, call_vars: CallVars) -> SQLInput: + async def _init(self, call_vars: CallVars, language: str = "zh_cn") -> SQLInput: """初始化SQL工具。""" return SQLInput( question=call_vars.question, ) - async def _generate_sql(self, data: SQLInput) -> list[dict[str, Any]]: """生成SQL语句列表""" post_data = { @@ -82,7 +87,6 @@ class SQL(CoreCall, input_model=SQLInput, output_model=SQLOutput): return sql_list - async def _execute_sql( self, sql_list: list[dict[str, Any]], @@ -113,16 +117,26 @@ class SQL(CoreCall, input_model=SQLInput, output_model=SQLOutput): return None, None - - async def _exec(self, input_data: dict[str, Any]) -> AsyncGenerator[CallOutputChunk, None]: + async def _exec( + self, input_data: dict[str, Any], language: str = "zh_cn" + ) -> AsyncGenerator[CallOutputChunk, None]: """运行SQL工具""" data = SQLInput(**input_data) - + message = { + "invaild": { + "zh_cn": "SQL查询错误:无法生成有效的SQL语句!", + "en": "SQL query error: Unable to generate valid SQL statements!", + }, + "fail": { + "zh_cn": "SQL查询错误:SQL语句执行失败!", + "en": "SQL query error: SQL statement execution failed!", + } + } # 生成SQL语句 sql_list = await self._generate_sql(data) if not sql_list: raise CallError( - message="SQL查询错误:无法生成有效的SQL语句!", + message=message["invaild"][language], data={}, ) @@ -130,7 +144,7 @@ class SQL(CoreCall, input_model=SQLInput, output_model=SQLOutput): sql_exec_results, sql_exec = await self._execute_sql(sql_list) if sql_exec_results is None or sql_exec is None: raise CallError( - message="SQL查询错误:SQL语句执行失败!", + message=message["fail"][language], data={}, ) diff --git a/apps/scheduler/call/suggest/prompt.py b/apps/scheduler/call/suggest/prompt.py index abc5e7d1..1e2ecf7d 100644 --- a/apps/scheduler/call/suggest/prompt.py +++ b/apps/scheduler/call/suggest/prompt.py @@ -3,7 +3,9 @@ from textwrap import dedent -SUGGEST_PROMPT = dedent(r""" +SUGGEST_PROMPT: dict[str, str] = { + "zh_cn": dedent( + r""" 根据提供的对话和附加信息(用户倾向、历史问题列表、工具信息等),生成三个预测问题。 @@ -89,4 +91,98 @@ SUGGEST_PROMPT = dedent(r""" 现在,进行问题生成: -""") +""" + ), + "en": dedent( + r""" + + + Generate three predicted questions based on the provided conversation and additional information (user preferences, historical question list, tool information, etc.). + The historical question list displays questions asked by the user before the historical conversation and is for background reference only. + The conversation will be given in the tag, the user preferences will be given in the tag, + the historical question list will be given in the tag, and the tool information will be given in the tag. + + Requirements for generating predicted questions: + + 1. Generate three predicted questions in the user's voice. They must be interrogative or imperative sentences and must be less than 30 words. + + 2. Predicted questions must be concise, without repetition, unnecessary information, or text other than the question. + + 3. Output must be in the following format: + + ```json + { + "predicted_questions": [ + "Predicted question 1", + "Predicted question 2", + "Predicted question 3" + ] + } + ``` + + + + What are the famous attractions in Hangzhou? + Hangzhou West Lake is a famous scenic spot in Hangzhou, Zhejiang Province, China, known for its beautiful natural scenery and rich cultural heritage. There are many famous attractions around West Lake, including the renowned Su Causeway, Bai Causeway, Broken Bridge, and the Three Pools Mirroring the Moon. West Lake is renowned for its clear waters and surrounding mountains, making it one of China's most famous lakes. + + + Briefly introduce Hangzhou + What are the famous attractions in Hangzhou? + + + Scenic Spot Search + Scenic Spot Information Search + + ["Hangzhou", "Tourism"] + + Now, generate questions: + + { + "predicted_questions": [ + "What is the ticket price for the West Lake Scenic Area in Hangzhou?", + "What are the famous attractions in Hangzhou?", + "What's the weather like in Hangzhou?" + ] + } + + + + Here's the actual data: + + + {% for message in conversation %} + <{{ message.role }}>{{ message.content }} + {% endfor %} + + + + {% if history %} + {% for question in history %} + {{ question }} + {% endfor %} + {% else %} + (No history question) + {% endif %} + + + + {% if tool %} + {{ tool.name }} + {{ tool.description }} + {% else %} + (No tool information) + {% endif %} + + + + {% if preference %} + {{ preference }} + {% else %} + (no user preference) + {% endif %} + + + Now, generate the question: + """ + ), +} diff --git a/apps/scheduler/call/suggest/suggest.py b/apps/scheduler/call/suggest/suggest.py index 1788fa0f..df9e8c5d 100644 --- a/apps/scheduler/call/suggest/suggest.py +++ b/apps/scheduler/call/suggest/suggest.py @@ -42,16 +42,25 @@ class Suggestion(CoreCall, input_model=SuggestionInput, output_model=SuggestionO to_user: bool = Field(default=True, description="是否将推荐的问题推送给用户") configs: list[SingleFlowSuggestionConfig] = Field(description="问题推荐配置", default=[]) - num: int = Field(default=3, ge=1, le=6, description="推荐问题的总数量(必须大于等于configs中涉及的Flow的数量)") + num: int = Field( + default=3, ge=1, le=6, description="推荐问题的总数量(必须大于等于configs中涉及的Flow的数量)" + ) context: SkipJsonSchema[list[dict[str, str]]] = Field(description="Executor的上下文", exclude=True) conversation_id: SkipJsonSchema[str] = Field(description="对话ID", exclude=True) @classmethod - def info(cls) -> CallInfo: + def info(cls, language: str = "zh_cn") -> CallInfo: """返回Call的名称和描述""" - return CallInfo(name="问题推荐", description="在答案下方显示推荐的下一个问题") - + name_map = { + "zh_cn": "问题推荐", + "en": "Question Suggestion", + } + description_map = { + "zh_cn": "在答案下方显示推荐的下一个问题", + "en": "Display the suggested next question under the answer", + } + return CallInfo(name=name_map[language], description=description_map[language]) @classmethod async def instance(cls, executor: "StepExecutor", node: NodePool | None, **kwargs: Any) -> Self: @@ -77,7 +86,6 @@ class Suggestion(CoreCall, input_model=SuggestionInput, output_model=SuggestionO await obj._set_input(executor) return obj - async def _init(self, call_vars: CallVars) -> SuggestionInput: """初始化""" from apps.services.appcenter import AppCenterManager @@ -109,7 +117,6 @@ class Suggestion(CoreCall, input_model=SuggestionInput, output_model=SuggestionO history_questions=self._history_questions, ) - async def _get_history_questions(self, user_sub: str, conversation_id: str) -> list[str]: """获取当前对话的历史问题""" records = await RecordManager.query_record_by_conversation_id( @@ -124,8 +131,7 @@ class Suggestion(CoreCall, input_model=SuggestionInput, output_model=SuggestionO history_questions.append(record_data.question) return history_questions - - async def _exec(self, input_data: dict[str, Any]) -> AsyncGenerator[CallOutputChunk, None]: + async def _exec(self, input_data: dict[str, Any], language: str = "zh_cn") -> AsyncGenerator[CallOutputChunk, None]: """运行问题推荐""" data = SuggestionInput(**input_data) @@ -141,7 +147,7 @@ class Suggestion(CoreCall, input_model=SuggestionInput, output_model=SuggestionO # 已推送问题数量 pushed_questions = 0 # 初始化Prompt - prompt_tpl = self._env.from_string(SUGGEST_PROMPT) + prompt_tpl = self._env.from_string(SUGGEST_PROMPT[language]) # 先处理configs for config in self.configs: @@ -171,7 +177,9 @@ class Suggestion(CoreCall, input_model=SuggestionInput, output_model=SuggestionO schema=SuggestGenResult.model_json_schema(), ) questions = SuggestGenResult.model_validate(result) - question = questions.predicted_questions[random.randint(0, len(questions.predicted_questions) - 1)] # noqa: S311 + question = questions.predicted_questions[ + random.randint(0, len(questions.predicted_questions) - 1) + ] # noqa: S311 yield CallOutputChunk( type=CallOutputType.DATA, @@ -184,7 +192,6 @@ class Suggestion(CoreCall, input_model=SuggestionInput, output_model=SuggestionO ) pushed_questions += 1 - while pushed_questions < self.num: prompt = prompt_tpl.render( conversation=self.context, @@ -203,7 +210,9 @@ class Suggestion(CoreCall, input_model=SuggestionInput, output_model=SuggestionO schema=SuggestGenResult.model_json_schema(), ) questions = SuggestGenResult.model_validate(result) - question = questions.predicted_questions[random.randint(0, len(questions.predicted_questions) - 1)] # noqa: S311 + question = questions.predicted_questions[ + random.randint(0, len(questions.predicted_questions) - 1) + ] # noqa: S311 # 只会关联当前flow for question in questions.predicted_questions: diff --git a/apps/scheduler/call/summary/summary.py b/apps/scheduler/call/summary/summary.py index b605204e..b7af8153 100644 --- a/apps/scheduler/call/summary/summary.py +++ b/apps/scheduler/call/summary/summary.py @@ -29,9 +29,17 @@ class Summary(CoreCall, input_model=DataBase, output_model=SummaryOutput): context: ExecutorBackground = Field(description="对话上下文") @classmethod - def info(cls) -> CallInfo: + def info(cls, language: str = "zh_cn") -> CallInfo: """返回Call的名称和描述""" - return CallInfo(name="理解上下文", description="使用大模型,理解对话上下文") + name_map = { + "zh_cn": "理解上下文", + "en": "Context Understanding", + } + description_map = { + "zh_cn": "使用大模型,理解对话上下文", + "en": "Use the foundation model to understand the conversation context", + } + return CallInfo(name=name_map[language], description=description_map[language]) @classmethod async def instance(cls, executor: "StepExecutor", node: NodePool | None, **kwargs: Any) -> Self: @@ -52,19 +60,19 @@ class Summary(CoreCall, input_model=DataBase, output_model=SummaryOutput): return DataBase() - async def _exec(self, _input_data: dict[str, Any]) -> AsyncGenerator[CallOutputChunk, None]: + async def _exec(self, _input_data: dict[str, Any], language: str = "zh_cn") -> AsyncGenerator[CallOutputChunk, None]: """执行工具""" summary_obj = ExecutorSummary() - summary = await summary_obj.generate(background=self.context) + summary = await summary_obj.generate(background=self.context, language=language) self.tokens.input_tokens += summary_obj.input_tokens self.tokens.output_tokens += summary_obj.output_tokens yield CallOutputChunk(type=CallOutputType.TEXT, content=summary) - async def exec(self, executor: "StepExecutor", input_data: dict[str, Any]) -> AsyncGenerator[CallOutputChunk, None]: + async def exec(self, executor: "StepExecutor", input_data: dict[str, Any], language= "zh_cn") -> AsyncGenerator[CallOutputChunk, None]: """执行工具""" - async for chunk in self._exec(input_data): + async for chunk in self._exec(input_data, language): content = chunk.content if not isinstance(content, str): err = "[SummaryCall] 工具输出格式错误" diff --git a/apps/scheduler/executor/flow.py b/apps/scheduler/executor/flow.py index d8400fdf..63e089bb 100644 --- a/apps/scheduler/executor/flow.py +++ b/apps/scheduler/executor/flow.py @@ -20,21 +20,37 @@ from apps.services.task import TaskManager logger = logging.getLogger(__name__) # 开始前的固定步骤 FIXED_STEPS_BEFORE_START = [ - Step( - name="理解上下文", - description="使用大模型,理解对话上下文", - node=SpecialCallType.SUMMARY.value, - type=SpecialCallType.SUMMARY.value, - ), + { + "zh_cn": Step( + name="理解上下文", + description="使用大模型,理解对话上下文", + node=SpecialCallType.SUMMARY.value, + type=SpecialCallType.SUMMARY.value, + ), + "en": Step( + name="Understand context", + description="Use large model to understand the context of the dialogue", + node=SpecialCallType.SUMMARY.value, + type=SpecialCallType.SUMMARY.value, + ), + } ] # 结束后的固定步骤 FIXED_STEPS_AFTER_END = [ - Step( - name="记忆存储", - description="理解对话答案,并存储到记忆中", - node=SpecialCallType.FACTS.value, - type=SpecialCallType.FACTS.value, - ), + { + "zh_cn": Step( + name="记忆存储", + description="理解对话答案,并存储到记忆中", + node=SpecialCallType.FACTS.value, + type=SpecialCallType.FACTS.value, + ), + "en": Step( + name="Memory storage", + description="Understand the answer of the dialogue and store it in the memory", + node=SpecialCallType.FACTS.value, + type=SpecialCallType.FACTS.value, + ), + } ] @@ -67,14 +83,18 @@ class FlowExecutor(BaseExecutor): step_status=StepStatus.RUNNING, app_id=str(self.post_body_app.app_id), step_id="start", - step_name="开始", + step_name="开始" if self.task.language == "zh_cn" else "Start", ) self.validate_flow_state(self.task) # 是否到达Flow结束终点(变量) self._reached_end: bool = False self.step_queue: deque[StepQueueItem] = deque() +<<<<<<< HEAD async def _invoke_runner(self) -> None: +======= + async def _invoke_runner(self, queue_item: StepQueueItem) -> None: +>>>>>>> 7642418 (完善国际化部分) """单一Step执行""" # 创建步骤Runner step_runner = StepExecutor( @@ -102,7 +122,11 @@ class FlowExecutor(BaseExecutor): break # 执行Step +<<<<<<< HEAD await self._invoke_runner() +======= + await self._invoke_runner(queue_item) +>>>>>>> 7642418 (完善国际化部分) async def _find_next_id(self, step_id: str) -> list[str]: """查找下一个节点""" @@ -164,12 +188,14 @@ class FlowExecutor(BaseExecutor): # 头插开始前的系统步骤,并执行 for step in FIXED_STEPS_BEFORE_START: - self.step_queue.append(StepQueueItem( - step_id=str(uuid.uuid4()), - step=step, - enable_filling=False, - to_user=False, - )) + self.step_queue.append( + StepQueueItem( + step_id=str(uuid.uuid4()), + step=step.get(self.task.language, step["zh_cn"]), + enable_filling=False, + to_user=False, + ) + ) await self._step_process() # 插入首个步骤 @@ -179,6 +205,7 @@ class FlowExecutor(BaseExecutor): is_error = False while not self._reached_end: # 如果当前步骤出错,执行错误处理步骤 +<<<<<<< HEAD if self.task.state.step_status == StepStatus.ERROR: # type: ignore[arg-type] logger.warning("[FlowExecutor] Executor出错,执行错误处理步骤") self.step_queue.clear() @@ -200,6 +227,30 @@ class FlowExecutor(BaseExecutor): to_user=False, )) is_error = True +======= + if self.task.state.status == StepStatus.ERROR: # type: ignore[arg-type] + logger.warning("[FlowExecutor] Executor出错,执行错误处理步骤") + self.step_queue.clear() + self.step_queue.appendleft( + StepQueueItem( + step_id=str(uuid.uuid4()), + step=Step( + name="错误处理" if self.task.language == "zh_cn" else "Error Handling", + description="错误处理" if self.task.language == "zh_cn" else "Error Handling", + node=SpecialCallType.LLM.value, + type=SpecialCallType.LLM.value, + params={ + "user_prompt": LLM_ERROR_PROMPT[self.task.language].replace( + "{{ error_info }}", + self.task.state.error_info["err_msg"], # type: ignore[arg-type] + ), + }, + ), + enable_filling=False, + to_user=False, + ) + ) +>>>>>>> 7642418 (完善国际化部分) # 错误处理后结束 self._reached_end = True @@ -222,10 +273,12 @@ class FlowExecutor(BaseExecutor): # 尾插运行结束后的系统步骤 for step in FIXED_STEPS_AFTER_END: - self.step_queue.append(StepQueueItem( - step_id=str(uuid.uuid4()), - step=step, - )) + self.step_queue.append( + StepQueueItem( + step_id=str(uuid.uuid4()), + step=step.get(self.task.language, step["zh_cn"]), + ) + ) await self._step_process() # FlowStop需要返回总时间,需要倒推最初的开始时间(当前时间减去当前已用总时间) diff --git a/apps/scheduler/executor/step.py b/apps/scheduler/executor/step.py index 377a4c6e..d1ac0435 100644 --- a/apps/scheduler/executor/step.py +++ b/apps/scheduler/executor/step.py @@ -215,7 +215,7 @@ class StepExecutor(BaseExecutor): await self.push_message(EventType.STEP_INPUT.value, self.obj.input) # 执行步骤 - iterator = self.obj.exec(self, self.obj.input) + iterator = self.obj.exec(self, self.obj.input, language=self.task.language) try: content = await self._process_chunk(iterator, to_user=self.obj.to_user) diff --git a/apps/scheduler/mcp/host.py b/apps/scheduler/mcp/host.py index aa196112..bb4df75e 100644 --- a/apps/scheduler/mcp/host.py +++ b/apps/scheduler/mcp/host.py @@ -58,7 +58,8 @@ class MCPHost: logger.warning("用户 %s 的MCP %s 没有运行中的实例,请检查环境", self._user_sub, mcp_id) return None - async def assemble_memory(self) -> str: + + async def assemble_memory(self, language: str = "zh_cn") -> str: """组装记忆""" task = await TaskManager.get_task_by_task_id(self._task_id) if not task: @@ -72,7 +73,7 @@ class MCPHost: continue context_list.append(context) - return self._env.from_string(MEMORY_TEMPLATE).render( + return self._env.from_string(MEMORY_TEMPLATE[language]).render( context_list=context_list, ) @@ -123,7 +124,8 @@ class MCPHost: return output_data - async def _fill_params(self, tool: MCPTool, query: str) -> dict[str, Any]: + + async def _fill_params(self, tool: MCPTool, query: str, language:str = "zh_cn") -> dict[str, Any]: """填充工具参数""" # 更清晰的输入·指令,这样可以调用generate llm_query = rf""" @@ -137,13 +139,14 @@ class MCPHost: llm_query, [ {"role": "system", "content": "You are a helpful assistant."}, - {"role": "user", "content": await self.assemble_memory()}, + {"role": "user", "content": await self.assemble_memory(language)}, ], tool.input_schema, ) return await json_generator.generate() - async def call_tool(self, tool: MCPTool, plan_item: MCPPlanItem) -> list[dict[str, Any]]: + + async def call_tool(self, tool: MCPTool, plan_item: MCPPlanItem, language: str = "zh_cn") -> list[dict[str, Any]]: """调用工具""" # 拿到Client client = await MCPPool().get(tool.mcp_id, self._user_sub) @@ -153,7 +156,7 @@ class MCPHost: raise ValueError(err) # 填充参数 - params = await self._fill_params(tool, plan_item.instruction) + params = await self._fill_params(tool, plan_item.instruction, language) # 调用工具 result = await client.call_tool(tool.name, params) # 保存记忆 diff --git a/apps/scheduler/mcp/plan.py b/apps/scheduler/mcp/plan.py index cd4f5975..6a5e932a 100644 --- a/apps/scheduler/mcp/plan.py +++ b/apps/scheduler/mcp/plan.py @@ -13,9 +13,10 @@ from apps.schemas.mcp import MCPPlan, MCPTool class MCPPlanner: """MCP 用户目标拆解与规划""" - def __init__(self, user_goal: str) -> None: + def __init__(self, user_goal: str, language: str = "zh_cn") -> None: """初始化MCP规划器""" self.user_goal = user_goal + self.language = language self._env = SandboxedEnvironment( loader=BaseLoader, autoescape=True, @@ -38,7 +39,7 @@ class MCPPlanner: async def _get_reasoning_plan(self, tool_list: list[MCPTool], max_steps: int) -> str: """获取推理大模型的结果""" # 格式化Prompt - template = self._env.from_string(CREATE_PLAN) + template = self._env.from_string(CREATE_PLAN[self.language]) prompt = template.render( goal=self.user_goal, tools=tool_list, @@ -88,7 +89,7 @@ class MCPPlanner: async def generate_answer(self, plan: MCPPlan, memory: str) -> str: """生成最终回答""" - template = self._env.from_string(FINAL_ANSWER) + template = self._env.from_string(FINAL_ANSWER[self.language]) prompt = template.render( plan=plan, memory=memory, diff --git a/apps/scheduler/mcp/prompt.py b/apps/scheduler/mcp/prompt.py index b322fb08..5942a895 100644 --- a/apps/scheduler/mcp/prompt.py +++ b/apps/scheduler/mcp/prompt.py @@ -3,7 +3,9 @@ from textwrap import dedent -MCP_SELECT = dedent(r""" +MCP_SELECT: dict[str, str] = { + "zh_cn": dedent( + r""" 你是一个乐于助人的智能助手。 你的任务是:根据当前目标,选择最合适的MCP Server。 @@ -61,8 +63,73 @@ MCP_SELECT = dedent(r""" ### 请一步一步思考: -""") -CREATE_PLAN = dedent(r""" +""" + ), + "en": dedent( + r""" + You are a helpful intelligent assistant. + Your task is to select the most appropriate MCP server based on your current goals. + + ## Things to note when selecting an MCP server: + + 1. Ensure you fully understand your current goals and select the most appropriate MCP server. + 2. Please select from the provided list of MCP servers; do not generate your own. + 3. Please provide the rationale for your choice before making your selection. + 4. Your current goals will be listed below, along with the list of MCP servers. + Please include your thought process in the "Thought Process" section and your selection in the "Selection Results" section. + 5. Your selection must be in JSON format, strictly following the template below. Do not output any additional content: + + ```json + { + "mcp": "The name of your selected MCP server" + } + ``` + + 6. The following example is for reference only. Do not use it as a basis for selecting an MCP server. + + ## Example + + ### Goal + + I need an MCP server to complete a task. + + ### MCP Server List + + - **mcp_1**: "MCP Server 1"; Description of MCP Server 1 + - **mcp_2**: "MCP Server 2"; Description of MCP Server 2 + + ### Think step by step: + + Because the current goal requires an MCP server to complete a task, select mcp_1. + + ### Select Result + + ```json + { + "mcp": "mcp_1" + } + ``` + + ## Let's get started! + + ### Goal + + {{goal}} + + ### MCP Server List + + {% for mcp in mcp_list %} + - **{{mcp.id}}**: "{{mcp.name}}"; {{mcp.description}} + {% endfor %} + + ### Think step by step: + +""" + ), +} +CREATE_PLAN: dict[str, str] = { + "zh_cn": dedent( + r""" 你是一个计划生成器。 请分析用户的目标,并生成一个计划。你后续将根据这个计划,一步一步地完成用户的目标。 @@ -93,8 +160,7 @@ CREATE_PLAN = dedent(r""" } ``` - - 在生成计划之前,请一步一步思考,解析用户的目标,并指导你接下来的生成。\ -思考过程应放置在 XML标签中。 + - 在生成计划之前,请一步一步思考,解析用户的目标,并指导你接下来的生成。思考过程应按步骤顺序放置在 XML标签中。 - 计划内容中,可以使用"Result[]"来引用之前计划步骤的结果。例如:"Result[3]"表示引用第三条计划执行后的结果。 - 计划不得多于{{ max_num }}条,且每条计划内容应少于150字。 @@ -106,8 +172,7 @@ CREATE_PLAN = dedent(r""" {% for tool in tools %} - {{ tool.id }}{{tool.name}};{{ tool.description }} {% endfor %} - - Final结束步骤,当执行到这一步时,\ -表示计划执行结束,所得到的结果将作为最终结果。 + - Final结束步骤,当执行到这一步时,表示计划执行结束,所得到的结果将作为最终结果。 # 样例 @@ -162,8 +227,114 @@ CREATE_PLAN = dedent(r""" {{goal}} # 计划 -""") -EVALUATE_PLAN = dedent(r""" +""" + ), + "en": dedent( + r""" + You are a plan generator. + Please analyze the user's goal and generate a plan. You will then follow this plan to achieve the user's goal step by step. + + # A good plan should: + + 1. Be able to successfully achieve the user's goal. + 2. Each step in the plan must use only one tool. + 3. The steps in the plan must have clear and logical steps, without redundant or unnecessary steps. + 4. The last step in the plan must be a Final tool to ensure that the plan is executed. + + # Things to note when generating plans: + + - Each plan contains three parts: + - Plan content: Describes the general content of a single plan step + - Tool ID: Must be selected from the tool list below + - Tool instructions: Rewrite the user's goal to make it more consistent with the tool's input requirements + - Plans must be generated in the following format. Do not output any additional data: + + ```json + { + "plans": [ + { + "content":"Plan content", + "tool":"Tool ID", + "instruction":"Tool instructions" + } + ] + } + ``` + + - Before generating a plan, please think step by step, analyze the user's goal, and guide your next steps. The thought process should be placed in sequential steps within XML tags. + - In the plan content, you can use "Result[]" to reference the results of the previous plan steps. For example: "Result[3]" refers to the result after the third plan is executed. + - The plan should not have more than {{ max_num }} items, and each plan content should be less than 150 words. + + # Tools + + You can access and use some tools, which will be given in the XML tags. + + + {% for tool in tools %} + - {{ tool.id }}{{tool.name}}; {{ tool.description }} + {% endfor %} + - FinalEnd step. When this step is executed, \ + Indicates that the plan execution is completed and the result obtained will be used as the final result. + + + # Example + + ## Target + + Run a new alpine:latest container in the background, mount the host/root folder to /data, and execute the top command. + + ## Plan + + + 1. This goal needs to be completed using Docker. First, you need to select a suitable MCP Server. + 2. The goal can be broken down into the following parts: + - Run the alpine:latest container + - Mount the host directory + - Run in the background + - Execute the top command + 3. You need to select an MCP Server first, then generate the Docker command, and finally execute the command. + + + ```json + { + "plans": [ + { + "content": "Select an MCP Server that supports Docker", + "tool": "mcp_selector", + "instruction": "You need an MCP Server that supports running Docker containers" + }, + { + "content": "Use the MCP Server selected in Result[0] to generate Docker commands", + "tool": "command_generator", + "instruction": "Generate Docker command: Run the alpine:latest container in the background, mount /root to /data, and execute the top command" + }, + { + "content": "Execute the command generated by Result[1] on the MCP Server of Result[0]", + "tool": "command_executor", + "instruction": "Execute Docker command" + }, + { + "content": "Task execution completed, the container is running in the background, the result is Result[2]", + "tool": "Final", + "instruction": "" + } + ] + } + ``` + + # Now start generating the plan: + + ## Goal + + {{goal}} + + # Plan +""" + ), +} +EVALUATE_PLAN: dict[str, str] = { + "zh_cn": dedent( + r""" 你是一个计划评估器。 请根据给定的计划,和当前计划执行的实际情况,分析当前计划是否合理和完整,并生成改进后的计划。 @@ -209,8 +380,61 @@ EVALUATE_PLAN = dedent(r""" # 现在开始评估计划: -""") -FINAL_ANSWER = dedent(r""" +""" + ), + "en": dedent( + r""" + You are a plan evaluator. + Based on the given plan and the actual execution of the current plan, analyze whether the current plan is reasonable and complete, and generate an improved plan. + + # A good plan should: + + 1. Be able to successfully achieve the user's goal. + 2. Each step in the plan must use only one tool. + 3. The steps in the plan must have clear and logical steps, without redundant or unnecessary steps. + 4. The last step in the plan must be a Final tool to ensure the completion of the plan execution. + + # Your previous plan was: + + {{ plan }} + + # The execution status of this plan is: + + The execution status of the plan will be placed in the XML tags. + + + {{ memory }} + + + # Notes when conducting the evaluation: + + - Please think step by step, analyze the user's goal, and guide your subsequent generation. The thinking process should be placed in the XML tags. + - The evaluation results are divided into two parts: + - Conclusion of the plan evaluation + - Improved plan + - Please output the evaluation results in the following JSON format: + + ```json + { + "evaluation": "Evaluation results", + "plans": [ + { + "content": "Improved plan content", + "tool": "Tool ID", + "instruction": "Tool instructions" + } + ] + } + ``` + + # Start evaluating the plan now: + +""" + ), +} +FINAL_ANSWER: dict[str, str] = { + "zh_cn": dedent( + r""" 综合理解计划执行结果和背景信息,向用户报告目标的完成情况。 # 用户目标 @@ -229,12 +453,50 @@ FINAL_ANSWER = dedent(r""" # 现在,请根据以上信息,向用户报告目标的完成情况: -""") -MEMORY_TEMPLATE = dedent(r""" +""" + ), + "en": dedent( + r""" + Based on the understanding of the plan execution results and background information, report to the user the completion status of the goal. + + # User goal + + {{ goal }} + + # Plan execution status + + To achieve the above goal, you implemented the following plan: + + {{ memory }} + + # Other background information: + + {{ status }} + + # Now, based on the above information, please report to the user the completion status of the goal: + +""" + ), +} +MEMORY_TEMPLATE: dict[str, str] = { + "zh_cn": dedent( + r""" {% for ctx in context_list %} - 第{{ loop.index }}步:{{ ctx.step_description }} - 调用工具 `{{ ctx.step_id }}`,并提供参数 `{{ ctx.input_data }}` - 执行状态:{{ ctx.status }} - 得到数据:`{{ ctx.output_data }}` {% endfor %} -""") +""" + ), + "en": dedent( + r""" + {% for ctx in context_list %} + - Step {{ loop.index }}: {{ ctx.step_description }} + - Called tool `{{ ctx.step_id }}` and provided parameters `{{ ctx.input_data }}` + - Execution status: {{ ctx.status }} + - Got data: `{{ ctx.output_data }}` + {% endfor %} +""" + ), +} diff --git a/apps/scheduler/mcp/select.py b/apps/scheduler/mcp/select.py index 2ff50344..62ef2b37 100644 --- a/apps/scheduler/mcp/select.py +++ b/apps/scheduler/mcp/select.py @@ -78,6 +78,7 @@ class MCPSelector: query: str, mcp_list: list[dict[str, str]], mcp_ids: list[str], + language ) -> MCPSelectResult: """通过LLM选择最合适的MCP Server""" # 初始化jinja2环境 @@ -87,7 +88,7 @@ class MCPSelector: trim_blocks=True, lstrip_blocks=True, ) - template = env.from_string(MCP_SELECT) + template = env.from_string(MCP_SELECT[language]) # 渲染模板 mcp_prompt = template.render( mcp_list=mcp_list, @@ -141,6 +142,7 @@ class MCPSelector: self, query: str, mcp_list: list[str], + language = "zh_cn" ) -> MCPSelectResult: """ 选择最合适的MCP Server @@ -151,11 +153,11 @@ class MCPSelector: llm_mcp_list = await self._get_top_mcp_by_embedding(query, mcp_list) # 通过LLM选择最合适的 - return await self._get_mcp_by_llm(query, llm_mcp_list, mcp_list) + return await self._get_mcp_by_llm(query, llm_mcp_list, mcp_list, language) @staticmethod - async def select_top_tool(query: str, mcp_list: list[str], top_n: int = 10) -> list[MCPTool]: + async def select_top_tool(query: str, mcp_list: list[str], top_n: int = 10, language: str = "zh_cn") -> list[MCPTool]: """选择最合适的工具""" tool_vector = await LanceDB().get_table("mcp_tool") query_embedding = await Embedding.get_embedding([query]) diff --git a/apps/scheduler/pool/loader/app.py b/apps/scheduler/pool/loader/app.py index a85395bf..2514357d 100644 --- a/apps/scheduler/pool/loader/app.py +++ b/apps/scheduler/pool/loader/app.py @@ -24,7 +24,7 @@ BASE_PATH = Path(Config().get_config().deploy.data_dir) / "semantics" / "app" class AppLoader: """应用加载器""" - async def load(self, app_id: str, hashes: dict[str, str]) -> None: # noqa: C901 + async def load(self, app_id: str, hashes: dict[str, str], language: str = "zh_cn") -> None: # noqa: C901 """ 从文件系统中加载应用 @@ -52,7 +52,7 @@ class AppLoader: async for flow_file in flow_path.rglob("*.yaml"): if flow_file.stem not in flow_ids: logger.warning("[AppLoader] 工作流 %s 不在元数据中", flow_file) - flow = await flow_loader.load(app_id, flow_file.stem) + flow = await flow_loader.load(app_id, flow_file.stem, language) if not flow: err = f"[AppLoader] 工作流 {flow_file} 加载失败" raise ValueError(err) diff --git a/apps/scheduler/pool/loader/flow.py b/apps/scheduler/pool/loader/flow.py index 57344d40..5b5939c5 100644 --- a/apps/scheduler/pool/loader/flow.py +++ b/apps/scheduler/pool/loader/flow.py @@ -69,7 +69,7 @@ class FlowLoader: else: return flow_yaml - async def _process_steps(self, flow_yaml: dict[str, Any], flow_id: str, app_id: str) -> dict[str, Any]: + async def _process_steps(self, flow_yaml: dict[str, Any], flow_id: str, app_id: str, language) -> dict[str, Any]: """处理工作流步骤的转换""" logger.info("[FlowLoader] 应用 %s:解析工作流 %s 的步骤", flow_id, app_id) for key, step in flow_yaml["steps"].items(): @@ -78,12 +78,12 @@ class FlowLoader: logger.error(err) raise ValueError(err) if key == "start": - step["name"] = "开始" - step["description"] = "开始节点" + step["name"] = "开始" if language in {"zh", "zh_cn"} else "Start" + step["description"] = "开始节点" if language in {"zh", "zh_cn"} else "Start Node" step["type"] = "start" elif key == "end": - step["name"] = "结束" - step["description"] = "结束节点" + step["name"] = "结束" if language in {"zh", "zh_cn"} else "End" + step["description"] = "结束节点" if language in {"zh", "zh_cn"} else "End Node" step["type"] = "end" else: try: @@ -98,7 +98,7 @@ class FlowLoader: ) return flow_yaml - async def load(self, app_id: str, flow_id: str) -> Flow | None: + async def load(self, app_id: str, flow_id: str, language:str = "zh_cn") -> Flow | None: """从文件系统中加载【单个】工作流""" logger.info("[FlowLoader] 应用 %s:加载工作流 %s...", flow_id, app_id) @@ -118,7 +118,7 @@ class FlowLoader: for processor in [ lambda y: self._validate_basic_fields(y, flow_path), lambda y: self._process_edges(y, flow_id, app_id), - lambda y: self._process_steps(y, flow_id, app_id), + lambda y: self._process_steps(y, flow_id, app_id, language), ]: flow_yaml = await processor(flow_yaml) if not flow_yaml: diff --git a/apps/scheduler/pool/pool.py b/apps/scheduler/pool/pool.py index 7710d24d..b1c7f57f 100644 --- a/apps/scheduler/pool/pool.py +++ b/apps/scheduler/pool/pool.py @@ -146,11 +146,11 @@ class Pool: return flow_metadata_list - async def get_flow(self, app_id: str, flow_id: str) -> Flow | None: + async def get_flow(self, app_id: str, flow_id: str, language:str = "zh_cn") -> Flow | None: """从文件系统中获取单个Flow的全部数据""" logger.info("[Pool] 获取工作流 %s", flow_id) flow_loader = FlowLoader() - return await flow_loader.load(app_id, flow_id) + return await flow_loader.load(app_id, flow_id, language) async def get_call(self, call_id: str) -> Any: diff --git a/apps/scheduler/scheduler/message.py b/apps/scheduler/scheduler/message.py index d43ba7fb..82330262 100644 --- a/apps/scheduler/scheduler/message.py +++ b/apps/scheduler/scheduler/message.py @@ -26,7 +26,11 @@ logger = logging.getLogger(__name__) async def push_init_message( - task: Task, queue: MessageQueue, context_num: int, *, is_flow: bool = False, + task: Task, + queue: MessageQueue, + context_num: int, + *, + is_flow: bool = False, ) -> Task: """推送初始化消息""" # 组装feature @@ -66,7 +70,7 @@ async def push_rag_message( """推送RAG消息""" full_answer = "" try: - async for chunk in RAG.chat_with_llm_base_on_rag(user_sub, llm, history, doc_ids, rag_data): + async for chunk in RAG.chat_with_llm_base_on_rag(user_sub, llm, history, doc_ids, rag_data, task.language): task, content_obj = await _push_rag_chunk(task, queue, chunk) if content_obj.event_type == EventType.TEXT_ADD.value: # 如果是文本消息,直接拼接到答案中 diff --git a/apps/scheduler/scheduler/scheduler.py b/apps/scheduler/scheduler/scheduler.py index 91930f8c..0e508758 100644 --- a/apps/scheduler/scheduler/scheduler.py +++ b/apps/scheduler/scheduler/scheduler.py @@ -212,7 +212,7 @@ class Scheduler: if app_info.flow_id: logger.info("[Scheduler] 获取工作流定义") flow_id = app_info.flow_id - flow_data = await Pool().get_flow(app_info.app_id, flow_id) + flow_data = await Pool().get_flow(app_info.app_id, flow_id, post_body.language) else: # 如果用户没有选特定的Flow,则根据语义选择一个Flow logger.info("[Scheduler] 选择最合适的流") @@ -220,7 +220,7 @@ class Scheduler: flow_id = await flow_chooser.get_top_flow() self.task = flow_chooser.task logger.info("[Scheduler] 获取工作流定义") - flow_data = await Pool().get_flow(app_info.app_id, flow_id) + flow_data = await Pool().get_flow(app_info.app_id, flow_id, post_body.language) # 如果flow_data为空,则直接返回 if not flow_data: diff --git a/apps/schemas/request_data.py b/apps/schemas/request_data.py index 793ff456..38e3a3e8 100644 --- a/apps/schemas/request_data.py +++ b/apps/schemas/request_data.py @@ -12,6 +12,7 @@ from apps.schemas.flow_topology import FlowItem from apps.schemas.mcp import MCPType + class RequestDataApp(BaseModel): """模型对话中包含的app信息""" diff --git a/apps/schemas/task.py b/apps/schemas/task.py index 9e9f1531..5db7275b 100644 --- a/apps/schemas/task.py +++ b/apps/schemas/task.py @@ -98,6 +98,7 @@ class Task(BaseModel): tokens: TaskTokens = Field(description="Token信息") runtime: TaskRuntime = Field(description="任务运行时数据") created_at: float = Field(default_factory=lambda: round(datetime.now(tz=UTC).timestamp(), 3)) + language: str = Field(description="语言", default="zh") class StepQueueItem(BaseModel): diff --git a/apps/services/flow.py b/apps/services/flow.py index 4d682e5a..35fb3b53 100644 --- a/apps/services/flow.py +++ b/apps/services/flow.py @@ -19,6 +19,7 @@ from apps.schemas.flow_topology import ( NodeServiceItem, PositionItem, ) +from apps.scheduler.pool.pool import Pool from apps.services.node import NodeManager logger = logging.getLogger(__name__) @@ -65,10 +66,12 @@ class FlowManager: logger.exception("[FlowManager] 验证用户对服务的访问权限失败") return False else: - return (result > 0) + return result > 0 @staticmethod - async def get_node_id_by_service_id(service_id: str) -> list[NodeMetaDataItem] | None: + async def get_node_id_by_service_id( + service_id: str, language: str = "zh_cn" + ) -> list[NodeMetaDataItem] | None: """ serviceId获取service的接口数据,并将接口转换为节点元数据 @@ -91,11 +94,17 @@ class FlowManager: except Exception: logger.exception("[FlowManager] generate_from_schema 失败") continue + + if service_id == "": + call_class = await Pool().get_call(node_pool_record["_id"]) + node_name = call_class.info(language).name + node_description = call_class.info(language).description + node_meta_data_item = NodeMetaDataItem( nodeId=node_pool_record["_id"], callId=node_pool_record["call_id"], - name=node_pool_record["name"], - description=node_pool_record["description"], + name=node_pool_record["name"] if service_id else node_name, + description=node_pool_record["description"] if service_id else node_description, editable=True, createdAt=node_pool_record["created_at"], parameters=parameters, # 添加 parametersTemplate 参数 @@ -108,7 +117,7 @@ class FlowManager: return nodes_meta_data_items @staticmethod - async def get_service_by_user_id(user_sub: str) -> list[NodeServiceItem] | None: + async def get_service_by_user_id(user_sub: str, language: str = "zh_cn") -> list[NodeServiceItem] | None: """ 通过user_id获取用户自己上传的、其他人公开的且收藏的、受保护且有权限访问并收藏的service @@ -148,7 +157,14 @@ class FlowManager: sort=[("created_at", ASCENDING)], ) service_records = await service_records_cursor.to_list(length=None) - service_items = [NodeServiceItem(serviceId="", name="系统", type="system", nodeMetaDatas=[])] + service_items = [ + NodeServiceItem( + serviceId="", + name="系统" if language == "zh_cn" else "System", + type="system", + nodeMetaDatas=[], + ) + ] service_items += [ NodeServiceItem( serviceId=record["_id"], @@ -160,7 +176,9 @@ class FlowManager: for record in service_records ] for service_item in service_items: - node_meta_datas = await FlowManager.get_node_id_by_service_id(service_item.service_id) + node_meta_datas = await FlowManager.get_node_id_by_service_id( + service_item.service_id, language + ) if node_meta_datas is None: node_meta_datas = [] service_item.node_meta_datas = node_meta_datas @@ -202,7 +220,9 @@ class FlowManager: return None @staticmethod - async def get_flow_by_app_and_flow_id(app_id: str, flow_id: str) -> FlowItem | None: # noqa: C901, PLR0911, PLR0912 + async def get_flow_by_app_and_flow_id( + app_id: str, flow_id: str, language: str = "zh_cn" + ) -> FlowItem | None: # noqa: C901, PLR0911, PLR0912 """ 通过appId flowId获取flow config的路径和focus,并通过flow config的路径获取flow config,并将其转换为flow item。 @@ -238,7 +258,7 @@ class FlowManager: return None try: - flow_config = await FlowLoader().load(app_id, flow_id) + flow_config = await FlowLoader().load(app_id, flow_id, language) if not flow_config: logger.error("[FlowManager] 获取流配置失败") return None @@ -271,8 +291,7 @@ class FlowManager: editable=True, callId=node_config.type, parameters=parameters, - position=PositionItem( - x=node_config.pos.x, y=node_config.pos.y), + position=PositionItem(x=node_config.pos.x, y=node_config.pos.y), ) flow_item.nodes.append(node_item) @@ -347,11 +366,7 @@ class FlowManager: return sorted(edge_list_1) == sorted(edge_list_2) @staticmethod - async def put_flow_by_app_and_flow_id( - app_id: str, - flow_id: str, - flow_item: FlowItem, - ) -> FlowItem | None: + async def put_flow_by_app_and_flow_id(app_id: str, flow_id: str, flow_item: FlowItem, language: str = "zh_cn") -> FlowItem | None: """ 存储/更新flow的数据库数据和配置文件 @@ -401,7 +416,7 @@ class FlowManager: flow_config.edges.append(edge_config) flow_loader = FlowLoader() - old_flow_config = await flow_loader.load(app_id, flow_id) + old_flow_config = await flow_loader.load(app_id, flow_id, language) if old_flow_config is None: error_msg = f"[FlowManager] 流 {flow_id} 不存在;可能为新创建" @@ -446,7 +461,7 @@ class FlowManager: return flow_id @staticmethod - async def update_flow_debug_by_app_and_flow_id(app_id: str, flow_id: str, *, debug: bool) -> bool: + async def update_flow_debug_by_app_and_flow_id(app_id: str, flow_id: str, *, debug: bool, language: str = "zh_cn") -> bool: """ 更新flow的debug状态 @@ -457,7 +472,7 @@ class FlowManager: """ try: flow_loader = FlowLoader() - flow = await flow_loader.load(app_id, flow_id) + flow = await flow_loader.load(app_id, flow_id, language) if flow is None: return False flow.debug = debug diff --git a/apps/services/rag.py b/apps/services/rag.py index 8b95d2b1..83fb753b 100644 --- a/apps/services/rag.py +++ b/apps/services/rag.py @@ -28,59 +28,109 @@ class RAG: system_prompt: str = "You are a helpful assistant." """系统提示词""" - user_prompt = """' - - 你是openEuler社区的智能助手。请结合给出的背景信息, 回答用户的提问,并且基于给出的背景信息在相关句子后进行脚注。 - 一个例子将在中给出。 - 上下文背景信息将在中给出。 - 用户的提问将在中给出。 - 注意: - 1.输出不要包含任何XML标签,不要编造任何信息。若你认为用户提问与背景信息无关,请忽略背景信息直接作答。 - 2.脚注的格式为[[1]],[[2]],[[3]]等,脚注的内容为提供的文档的id。 - 3.脚注只出现在回答的句子的末尾,例如句号、问号等标点符号后面。 - 4.不要对脚注本身进行解释或说明。 - 5.请不要使用中的文档的id作为脚注。 - - + user_prompt: dict[str, str] = { + "zh_cn" : r""" + + 你是openEuler社区的智能助手。请结合给出的背景信息, 回答用户的提问,并且基于给出的背景信息在相关句子后进行脚注。 + 一个例子将在中给出。 + 上下文背景信息将在中给出。 + 用户的提问将在中给出。 + 注意: + 1.输出不要包含任何XML标签,不要编造任何信息。若你认为用户提问与背景信息无关,请忽略背景信息直接作答。 + 2.脚注的格式为[[1]],[[2]],[[3]]等,脚注的内容为提供的文档的id。 + 3.脚注只出现在回答的句子的末尾,例如句号、问号等标点符号后面。 + 4.不要对脚注本身进行解释或说明。 + 5.请不要使用中的文档的id作为脚注。 + + + + + + openEuler社区是一个开源操作系统社区,致力于推动Linux操作系统的发展。 + + + openEuler社区的目标是为用户提供一个稳定、安全、高效的操作系统平台,并且支持多种硬件架构。 + + + + + openEuler社区的成员来自世界各地,包括开发者、用户和企业。 + + + openEuler社区的成员共同努力,推动开源操作系统的发展,并且为用户提供支持和帮助。 + + + + + openEuler社区的目标是什么? + + + openEuler社区是一个开源操作系统社区,致力于推动Linux操作系统的发展。[[1]] + openEuler社区的目标是为用户提供一个稳定、安全、高效的操作系统平台,并且支持多种硬件架构。[[1]] + + + - - - openEuler社区是一个开源操作系统社区,致力于推动Linux操作系统的发展。 - - - openEuler社区的目标是为用户提供一个稳定、安全、高效的操作系统平台,并且支持多种硬件架构。 - - - - - openEuler社区的成员来自世界各地,包括开发者、用户和企业。 - - - openEuler社区的成员共同努力,推动开源操作系统的发展,并且为用户提供支持和帮助。 - - + {bac_info} - openEuler社区的目标是什么? + {user_question} - - openEuler社区是一个开源操作系统社区,致力于推动Linux操作系统的发展。[[1]] - openEuler社区的目标是为用户提供一个稳定、安全、高效的操作系统平台,并且支持多种硬件架构。[[1]] - - - - - {bac_info} - - - {user_question} - - """ + """, + "en" : r""" + + You are a helpful assistant of openEuler community. Please answer the user's question based on the given background information and add footnotes after the related sentences. + An example will be given in . + The background information will be given in . + The user's question will be given in . + Note: + 1. Do not include any XML tags in the output, and do not make up any information. If you think the user's question is unrelated to the background information, please ignore the background information and directly answer. + 2. The format of the footnotes is [[1]], [[2]], [[3]], etc., and the content of the footnotes is the id of the provided document. + 3. The footnotes only appear at the end of the answer sentences, such as after the punctuation marks such as periods, question marks, etc. + 4. Do not explain or explain the footnotes themselves. + 5. Do not use the id of the document in as the footnote. + + + + + + openEuler community is an open source operating system community, committed to promoting the development of the Linux operating system. + + + openEuler community aims to provide users with a stable, secure, and efficient operating system platform, and support multiple hardware architectures. + + + + + Members of the openEuler community come from all over the world, including developers, users, and enterprises. + + + Members of the openEuler community work together to promote the development of open source operating systems, and provide support and assistance to users. + + + + + What is the goal of openEuler community? + + + openEuler community is an open source operating system community, committed to promoting the development of the Linux operating system. [[1]] + openEuler community aims to provide users with a stable, secure, and efficient operating system platform, and support multiple hardware architectures. [[1]] + + + + + {bac_info} + + + {user_question} + + """ + } @staticmethod - async def get_doc_info_from_rag(user_sub: str, max_tokens: int, - doc_ids: list[str], - data: RAGQueryReq) -> list[dict[str, Any]]: + async def get_doc_info_from_rag( + user_sub: str, max_tokens: int, doc_ids: list[str], data: RAGQueryReq + ) -> list[dict[str, Any]]: """获取RAG服务的文档信息""" session_id = await SessionManager.get_session_by_user_sub(user_sub) url = Config().get_config().rag.rag_service.rstrip("/") + "/chunk/search" @@ -138,15 +188,23 @@ class RAG: doc_cnt += 1 doc_id_map[doc_chunk["docId"]] = doc_cnt doc_index = doc_id_map[doc_chunk["docId"]] - leave_tokens -= token_calculator.calculate_token_length(messages=[ - {"role": "user", "content": f''''''}, - {"role": "user", "content": ""} + leave_tokens -= token_calculator.calculate_token_length( + messages=[ + { + "role": "user", + "content": f"""""", + }, + {"role": "user", "content": ""}, + ], + pure_text=True, + ) + tokens_of_chunk_element = token_calculator.calculate_token_length( + messages=[ + {"role": "user", "content": ""}, + {"role": "user", "content": ""}, ], - pure_text=True) - tokens_of_chunk_element = token_calculator.calculate_token_length(messages=[ - {"role": "user", "content": ""}, - {"role": "user", "content": ""}, - ], pure_text=True) + pure_text=True, + ) doc_cnt = 0 doc_id_map = {} for doc_chunk in doc_chunk_list: @@ -166,31 +224,35 @@ class RAG: doc_index = doc_id_map[doc_chunk["docId"]] if bac_info: bac_info += "\n\n" - bac_info += f''' + bac_info += f""" - ''' + """ for chunk in doc_chunk["chunks"]: if leave_tokens <= tokens_of_chunk_element: break chunk_text = chunk["text"] chunk_text = TokenCalculator.get_k_tokens_words_from_content( - content=chunk_text, k=leave_tokens) - leave_tokens -= token_calculator.calculate_token_length(messages=[ - {"role": "user", "content": ""}, - {"role": "user", "content": chunk_text}, - {"role": "user", "content": ""}, - ], pure_text=True) - bac_info += f''' + content=chunk_text, k=leave_tokens + ) + leave_tokens -= token_calculator.calculate_token_length( + messages=[ + {"role": "user", "content": ""}, + {"role": "user", "content": chunk_text}, + {"role": "user", "content": ""}, + ], + pure_text=True, + ) + bac_info += f""" {chunk_text} - ''' + """ bac_info += "" return bac_info, doc_info_list @staticmethod async def chat_with_llm_base_on_rag( - user_sub: str, llm: LLM, history: list[dict[str, str]], doc_ids: list[str], data: RAGQueryReq + user_sub: str, llm: LLM, history: list[dict[str, str]], doc_ids: list[str], data: RAGQueryReq, language: str = "zh_cn" ) -> AsyncGenerator[str, None]: """获取RAG服务的结果""" reasion_llm = ReasoningLLM( @@ -204,13 +266,17 @@ class RAG: if history: try: question_obj = QuestionRewrite() - data.query = await question_obj.generate(history=history, question=data.query, llm=reasion_llm) + data.query = await question_obj.generate( + history=history, question=data.query, llm=reasion_llm, language=language + ) except Exception: logger.exception("[RAG] 问题重写失败") doc_chunk_list = await RAG.get_doc_info_from_rag( - user_sub=user_sub, max_tokens=llm.max_tokens, doc_ids=doc_ids, data=data) + user_sub=user_sub, max_tokens=llm.max_tokens, doc_ids=doc_ids, data=data + ) bac_info, doc_info_list = await RAG.assemble_doc_info( - doc_chunk_list=doc_chunk_list, max_tokens=llm.max_tokens) + doc_chunk_list=doc_chunk_list, max_tokens=llm.max_tokens + ) messages = [ *history, { @@ -219,7 +285,7 @@ class RAG: }, { "role": "user", - "content": RAG.user_prompt.format( + "content": RAG.user_prompt[language].format( bac_info=bac_info, user_question=data.query, ), @@ -263,8 +329,8 @@ class RAG: while index >= max(0, len(chunk) - max_footnote_length) and chunk[index] != "]": index -= 1 if index >= 0: - buffer = chunk[index + 1:] - chunk = chunk[:index + 1] + buffer = chunk[index + 1 :] + chunk = chunk[: index + 1] else: buffer = "" output_tokens += TokenCalculator().calculate_token_length( -- Gitee From 5c52737b326dceac874acfd7e3a82ca8cad0e7e5 Mon Sep 17 00:00:00 2001 From: ylzhangah <1194926515@qq.com> Date: Tue, 5 Aug 2025 15:53:03 +0800 Subject: [PATCH 04/15] add deepinsight --- .../euler_copilot/configs/deepinsight/.env | 38 +++++++ .../configs/deepinsight/.env.backup | 27 +++++ .../deepinsight-web-config.yaml | 10 ++ .../deepinsight-web/deepinsight-web.yaml | 80 ++++++++++++++ .../deepinsight/deepinsight-config.yaml | 21 ++++ .../templates/deepinsight/deepinsight.yaml | 102 ++++++++++++++++++ .../templates/web/web-config.yaml | 3 +- .../euler_copilot/templates/web/web.yaml | 2 +- 8 files changed, 281 insertions(+), 2 deletions(-) create mode 100644 deploy/chart/euler_copilot/configs/deepinsight/.env create mode 100644 deploy/chart/euler_copilot/configs/deepinsight/.env.backup create mode 100644 deploy/chart/euler_copilot/templates/deepinsight-web/deepinsight-web-config.yaml create mode 100644 deploy/chart/euler_copilot/templates/deepinsight-web/deepinsight-web.yaml create mode 100644 deploy/chart/euler_copilot/templates/deepinsight/deepinsight-config.yaml create mode 100644 deploy/chart/euler_copilot/templates/deepinsight/deepinsight.yaml diff --git a/deploy/chart/euler_copilot/configs/deepinsight/.env b/deploy/chart/euler_copilot/configs/deepinsight/.env new file mode 100644 index 00000000..d3fa5fb6 --- /dev/null +++ b/deploy/chart/euler_copilot/configs/deepinsight/.env @@ -0,0 +1,38 @@ +# .env.example +# 大模型配置 +# 支持列表见camel官方文档:https://docs.camel-ai.org/key_modules/models#direct-integrations +MODEL_PLATFORM=deepseek +# 支持列表见camel官方文档:https://docs.camel-ai.org/key_modules/models#direct-integrations +MODEL_TYPE=deepseek-chat +MODEL_API_KEY=sk-882ee9907bdf4bf4afee6994dea5697b +DEEPSEEK_API_KEY=sk-882ee9907bdf4bf4afee6994dea5697b +# 数据库配置,默认使用sqlite,支持postgresql和sqlite +DB_TYPE=sqlite +# postgresql配置 +POSTGRES_USER= +POSTGRES_PASSWORD= +POSTGRES_HOST= +POSTGRES_PORT= +POSTGRES_DB= + +# MongoDB 配置 +MONGODB_USER= +MONGODB_PASSWORD= +MONGODB_HOST= +MONGODB_PORT= + +# 是否需要认证鉴权: deepInsight集成到openeuler intelligence依赖认证健全,如果单用户部署则不需要,默认用户为admin +REQUIRE_AUTHENTICATION=false + +# 默认监听地址和端口 +HOST=0.0.0.0 +PORT=9380 +API_PREFIX=/deepinsight + +# 日志配置 +LOG_DIR= +LOG_FILENAME= +LOG_LEVEL= +LOG_MAX_BYTES= +LOG_BACKUP_COUNT= + diff --git a/deploy/chart/euler_copilot/configs/deepinsight/.env.backup b/deploy/chart/euler_copilot/configs/deepinsight/.env.backup new file mode 100644 index 00000000..48520425 --- /dev/null +++ b/deploy/chart/euler_copilot/configs/deepinsight/.env.backup @@ -0,0 +1,27 @@ +# .env.example + +# 默认监听地址和端口 +HOST=0.0.0.0 +PORT=9380 +API_PREFIX=/deepinsight + +# 数据库配置,默认使用postgresql +# DB_TYPE=opengauss +# postgresql配置 +# POSTGRES_USER=opengauss +# POSTGRES_PASSWORD=${gauss-password} +# POSTGRES_HOST=opengauss-db.{{ .Release.Namespace }}.svc.cluster.local +# POSTGRES_PORT=5432 +# POSTGRES_DB=postgres + + +# MongoDB +MONGODB_USER=euler_copilot +MONGODB_PASSWORD=${mongo-password} +MONGODB_HOST=mongo-db.{{ .Release.Namespace }}.svc.cluster.local +MONGODB_PORT=27017 +MONGODB_DATABASE=euler_copilot + + +# 是否需要认证鉴权: deepInsight集成到openeuler intelligence依赖认证健全,如果单用户部署则不需要,默认用户为admin +REQUIRE_AUTHENTICATION=false diff --git a/deploy/chart/euler_copilot/templates/deepinsight-web/deepinsight-web-config.yaml b/deploy/chart/euler_copilot/templates/deepinsight-web/deepinsight-web-config.yaml new file mode 100644 index 00000000..2981fce5 --- /dev/null +++ b/deploy/chart/euler_copilot/templates/deepinsight-web/deepinsight-web-config.yaml @@ -0,0 +1,10 @@ +{{- if .Values.euler_copilot.deepinsight_web.enabled -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: deepinsight-web-config + namespace: {{ .Release.Namespace }} +data: + .env: |- + DEEPINSIGHT_BACEND_URL=http://deepinsight-service.{{ .Release.Namespace }}.svc.cluster.local:9380 +{{- end -}} diff --git a/deploy/chart/euler_copilot/templates/deepinsight-web/deepinsight-web.yaml b/deploy/chart/euler_copilot/templates/deepinsight-web/deepinsight-web.yaml new file mode 100644 index 00000000..1e645538 --- /dev/null +++ b/deploy/chart/euler_copilot/templates/deepinsight-web/deepinsight-web.yaml @@ -0,0 +1,80 @@ +{{- if .Values.euler_copilot.deepinsight_web.enabled -}} +--- +apiVersion: v1 +kind: Service +metadata: + name: deepinsight-web-service + namespace: {{ .Release.Namespace }} +spec: + type: {{ default "ClusterIP" .Values.euler_copilot.deepinsight_web.service.type }} + selector: + app: deepinsight-web + ports: + - port: 9222 + targetPort: 9222 + nodePort: {{ default nil .Values.euler_copilot.deepinsight_web.service.nodePort }} + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deepinsight-web-deploy + namespace: {{ .Release.Namespace }} + labels: + app: deepinsight-web +spec: + replicas: {{ default 1 .Values.globals.replicaCount }} + selector: + matchLabels: + app: deepinsight-web + template: + metadata: + annotations: + checksum/config: {{ include (print $.Template.BasePath "/deepinsight-web/deepinsight-web-config.yaml") . | sha256sum }} + labels: + app: deepinsight-web + spec: + automountServiceAccountToken: false + containers: + - name: deepinsight-web + image: {{ .Values.euler_copilot.deepinsight_web.image | default (printf "%s/neocopilot/deepinsight-web:0.9.6-%s" (.Values.globals.imageRegistry | default "hub.oepkgs.net") (.Values.globals.arch | default "x86")) }} + imagePullPolicy: {{ default "IfNotPresent" .Values.globals.imagePullPolicy }} + ports: + - containerPort: 9222 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: 9222 + scheme: HTTP + failureThreshold: 5 + initialDelaySeconds: 60 + periodSeconds: 90 + env: + - name: TZ + value: "Asia/Shanghai" + volumeMounts: + - mountPath: /config + name: deepinsight-web-config-volume + - mountPath: /var/lib/nginx/tmp + name: deepinsight-web-tmp + - mountPath: /opt/.env + name: deepinsight-web-env-volume + subPath: .env + resources: + requests: + cpu: 0.05 + memory: 64Mi + limits: + {{ toYaml .Values.euler_copilot.deepinsight_web.resourceLimits | nindent 14 }} + volumes: + - name: deepinsight-web-config-volume + emptyDir: + medium: Memory + - name: deepinsight-web-env-volume + configMap: + name: deepinsight-web-config + - name: deepinsight-web-tmp + emptyDir: + medium: Memory +{{- end -}} diff --git a/deploy/chart/euler_copilot/templates/deepinsight/deepinsight-config.yaml b/deploy/chart/euler_copilot/templates/deepinsight/deepinsight-config.yaml new file mode 100644 index 00000000..0a0506ae --- /dev/null +++ b/deploy/chart/euler_copilot/templates/deepinsight/deepinsight-config.yaml @@ -0,0 +1,21 @@ +{{- if .Values.euler_copilot.deepinsight.enabled -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: deepinsight-config + namespace: {{ .Release.Namespace }} +data: + .env: |- +{{ tpl (.Files.Get "configs/deepinsight/.env") . | indent 4 }} + copy-config.yaml: |- + copy: + - from: /config/.env + to: /config-rw/.env + mode: + uid: 0 + gid: 0 + mode: "0o650" + secrets: + - /db-secrets + - /system-secrets +{{- end -}} diff --git a/deploy/chart/euler_copilot/templates/deepinsight/deepinsight.yaml b/deploy/chart/euler_copilot/templates/deepinsight/deepinsight.yaml new file mode 100644 index 00000000..0553728d --- /dev/null +++ b/deploy/chart/euler_copilot/templates/deepinsight/deepinsight.yaml @@ -0,0 +1,102 @@ +{{- if .Values.euler_copilot.deepinsight.enabled -}} +--- +apiVersion: v1 +kind: Service +metadata: + name: deepinsight-service + namespace: {{ .Release.Namespace }} +spec: + type: {{ default "ClusterIP" .Values.euler_copilot.deepinsight.service.type }} + selector: + app: deepinsight + ports: + - name: deepinsight + port: 9380 + targetPort: 9380 + nodePort: {{ default nil .Values.euler_copilot.deepinsight.service.nodePort }} + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deepinsight-deploy + namespace: {{ .Release.Namespace }} + labels: + app: deepinsight +spec: + replicas: {{ default 1 .Values.globals.replicaCount }} + selector: + matchLabels: + app: deepinsight + template: + metadata: + annotations: + checksum/config: {{ include (print $.Template.BasePath "/deepinsight/deepinsight-config.yaml") . | sha256sum }} + labels: + app: deepinsight + spec: + automountServiceAccountToken: false + containers: + - name: deepinsight + image: {{ .Values.euler_copilot.deepinsight.image | default (printf "%s/neocopilot/deepinsight_backend:0.9.6-%s" (.Values.globals.imageRegistry | default "hub.oepkgs.net") (.Values.globals.arch | default "x86")) }} + imagePullPolicy: {{ default "IfNotPresent" .Values.globals.imagePullPolicy }} + ports: + - containerPort: 9380 + protocol: TCP + # livenessProbe: + # httpGet: + # path: /health_check + # port: 9380 + # scheme: HTTP + # failureThreshold: 5 + # initialDelaySeconds: 60 + # periodSeconds: 90 + env: + - name: TZ + value: "Asia/Shanghai" + volumeMounts: + - mountPath: /app/backend/config + name: deepinsight-shared + resources: + requests: + cpu: 0.25 + memory: 512Mi + limits: + {{ toYaml .Values.euler_copilot.deepinsight.resourceLimits | nindent 14 }} + initContainers: + - name: deepinsight-copy-secret + image: {{ .Values.euler_copilot.secretInject.image | default (printf "%s/neocopilot/secret_inject:dev-%s" (.Values.globals.imageRegistry | default "hub.oepkgs.net") (.Values.globals.arch | default "x86")) }} + imagePullPolicy: {{ default "IfNotPresent" .Values.globals.imagePullPolicy }} + command: + - python3 + - ./main.py + - --config + - config.yaml + - --copy + volumeMounts: + - mountPath: /config/.env + name: deepinsight-config-vl + subPath: .env + - mountPath: /app/config.yaml + name: deepinsight-config-vl + subPath: copy-config.yaml + - mountPath: /config-rw + name: deepinsight-shared + - mountPath: /db-secrets + name: database-secret + - mountPath: /system-secrets + name: system-secret + volumes: + - name: deepinsight-config-vl + configMap: + name: deepinsight-config + - name: database-secret + secret: + secretName: euler-copilot-database + - name: system-secret + secret: + secretName: euler-copilot-system + - name: deepinsight-shared + emptyDir: + medium: Memory +{{- end -}} diff --git a/deploy/chart/euler_copilot/templates/web/web-config.yaml b/deploy/chart/euler_copilot/templates/web/web-config.yaml index 1793023e..da421eda 100644 --- a/deploy/chart/euler_copilot/templates/web/web-config.yaml +++ b/deploy/chart/euler_copilot/templates/web/web-config.yaml @@ -8,4 +8,5 @@ data: .env: |- RAG_WEB_URL=http://rag-web-service.{{ .Release.Namespace }}.svc.cluster.local:9888 FRAMEWORK_URL=http://framework-service.{{ .Release.Namespace }}.svc.cluster.local:8002 -{{- end -}} \ No newline at end of file + DEEPINSIGHT_WEB_URL=http://deepinsight-web-service.{{ .Release.Namespace }}.svc.cluster.local:9222 +{{- end -}} diff --git a/deploy/chart/euler_copilot/templates/web/web.yaml b/deploy/chart/euler_copilot/templates/web/web.yaml index c20c045d..3bb939b6 100644 --- a/deploy/chart/euler_copilot/templates/web/web.yaml +++ b/deploy/chart/euler_copilot/templates/web/web.yaml @@ -36,7 +36,7 @@ spec: automountServiceAccountToken: false containers: - name: web - image: {{ .Values.euler_copilot.web.image | default (printf "%s/neocopilot/euler-copilot-web:0.9.6-%s" (.Values.globals.imageRegistry | default "hub.oepkgs.net") (.Values.globals.arch | default "x86")) }} + image: {{ .Values.euler_copilot.web.image | default (printf "%s/neocopilot/euler-copilot-web:deepinsight-%s" (.Values.globals.imageRegistry | default "hub.oepkgs.net") (.Values.globals.arch | default "x86")) }} imagePullPolicy: {{ default "IfNotPresent" .Values.globals.imagePullPolicy }} ports: - containerPort: 8080 -- Gitee From 140c1a1bad5349c00e82347a5242640632d3fa37 Mon Sep 17 00:00:00 2001 From: zhanghb <2428333123@qq.com> Date: Wed, 6 Aug 2025 15:03:00 +0800 Subject: [PATCH 05/15] =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=9B=BD=E9=99=85?= =?UTF-8?q?=E5=8C=96=E9=83=A8=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/llm/function.py | 1 - apps/llm/patterns/core.py | 28 ++++----- apps/llm/patterns/executor.py | 4 +- apps/llm/patterns/facts.py | 5 +- apps/llm/patterns/rewoo.py | 12 ++-- apps/llm/patterns/select.py | 82 ++++++++++++++++++++++---- apps/routers/chat.py | 3 +- apps/scheduler/call/api/api.py | 34 +++++++---- apps/scheduler/call/choice/choice.py | 42 ++++++++----- apps/scheduler/call/core.py | 23 +++++--- apps/scheduler/call/empty.py | 2 +- apps/scheduler/call/facts/facts.py | 33 +++++++---- apps/scheduler/call/graph/graph.py | 32 ++++++---- apps/scheduler/call/llm/llm.py | 40 ++++++++----- apps/scheduler/call/mcp/mcp.py | 82 +++++++++++++++----------- apps/scheduler/call/rag/rag.py | 33 +++++++---- apps/scheduler/call/search/search.py | 33 +++++++---- apps/scheduler/call/slot/slot.py | 72 +++++++++++++--------- apps/scheduler/call/sql/sql.py | 33 +++++++---- apps/scheduler/call/suggest/suggest.py | 33 +++++++---- apps/scheduler/call/summary/summary.py | 33 +++++++---- apps/scheduler/executor/flow.py | 42 ++----------- apps/scheduler/mcp/host.py | 19 +++--- apps/scheduler/pool/loader/app.py | 4 +- apps/scheduler/pool/loader/flow.py | 17 +++--- apps/scheduler/pool/pool.py | 4 +- apps/scheduler/scheduler/context.py | 2 +- apps/scheduler/scheduler/message.py | 15 +++-- apps/services/flow.py | 26 ++++---- apps/services/rag.py | 5 +- 30 files changed, 477 insertions(+), 317 deletions(-) diff --git a/apps/llm/function.py b/apps/llm/function.py index 4e56d147..25f887ba 100644 --- a/apps/llm/function.py +++ b/apps/llm/function.py @@ -135,7 +135,6 @@ class FunctionLLM: logger.info("[FunctionCall] 大模型输出:%s", ans) return await FunctionLLM.process_response(ans) - @staticmethod async def process_response(response: str) -> str: """处理大模型的输出""" diff --git a/apps/llm/patterns/core.py b/apps/llm/patterns/core.py index c2c98f86..60d814ee 100644 --- a/apps/llm/patterns/core.py +++ b/apps/llm/patterns/core.py @@ -5,22 +5,24 @@ from abc import ABC, abstractmethod from textwrap import dedent from typing import Union + class CorePattern(ABC): """基础大模型范式抽象类""" - system_prompt: Union[str, dict[str, str]] = "" + system_prompt: dict[str, str] = {} """系统提示词""" - user_prompt: Union[str, dict[str, str]] = "" + user_prompt: dict[str, str] = {} """用户提示词""" input_tokens: int = 0 """输入Token数量""" output_tokens: int = 0 """输出Token数量""" - - def __init__(self, - system_prompt: Union[str, dict[str, str], None] = None, - user_prompt: Union[str, dict[str, str], None] = None) -> None: + def __init__( + self, + system_prompt: dict[str, str] | None = None, + user_prompt: dict[str, str] | None = None, + ) -> None: """ 检查是否已经自定义了Prompt;有的话就用自定义的;同时对Prompt进行空格清除 @@ -37,17 +39,9 @@ class CorePattern(ABC): err = "必须设置用户提示词!" raise ValueError(err) - - self.system_prompt = { - lang: dedent(prompt).strip("\n") - for lang, prompt in self.system_prompt.items() - } if isinstance(self.system_prompt, dict) else dedent(self.system_prompt).strip("\n") - - - self.user_prompt = { - lang: dedent(prompt).strip("\n") - for lang, prompt in self.user_prompt.items() - } if isinstance(self.user_prompt, dict) else dedent(self.user_prompt).strip("\n") + self.system_prompt = {lang: dedent(prompt).strip("\n") for lang, prompt in self.system_prompt.items()} + + self.user_prompt = {lang: dedent(prompt).strip("\n") for lang, prompt in self.user_prompt.items()} @abstractmethod async def generate(self, **kwargs): # noqa: ANN003, ANN201 diff --git a/apps/llm/patterns/executor.py b/apps/llm/patterns/executor.py index 390379e0..881839ce 100644 --- a/apps/llm/patterns/executor.py +++ b/apps/llm/patterns/executor.py @@ -174,8 +174,8 @@ class ExecutorSummary(CorePattern): def __init__( self, - system_prompt: Union[str, dict[str, str], None] = None, - user_prompt: Union[str, dict[str, str], None] = None, + system_prompt: dict[str, str] | None = None, + user_prompt: dict[str, str] | None = None, ) -> None: """初始化Background模式""" super().__init__(system_prompt, user_prompt) diff --git a/apps/llm/patterns/facts.py b/apps/llm/patterns/facts.py index c5abb905..9fc4bced 100644 --- a/apps/llm/patterns/facts.py +++ b/apps/llm/patterns/facts.py @@ -4,7 +4,6 @@ import logging from pydantic import BaseModel, Field -from typing import Union from apps.llm.function import JsonGenerator from apps.llm.patterns.core import CorePattern @@ -110,8 +109,8 @@ class Facts(CorePattern): def __init__( self, - system_prompt: Union[str, dict[str, str], None] = None, - user_prompt: Union[str, dict[str, str], None] = None, + system_prompt: dict[str, str] | None = None, + user_prompt: dict[str, str] | None = None, ) -> None: """初始化Prompt""" super().__init__(system_prompt, user_prompt) diff --git a/apps/llm/patterns/rewoo.py b/apps/llm/patterns/rewoo.py index 43e2b1bc..da1c48b7 100644 --- a/apps/llm/patterns/rewoo.py +++ b/apps/llm/patterns/rewoo.py @@ -114,8 +114,8 @@ class InitPlan(CorePattern): def __init__( self, - system_prompt: Union[str, dict[str, str], None] = None, - user_prompt: Union[str, dict[str, str], None] = None, + system_prompt: dict[str, str] | None = None, + user_prompt: dict[str, str] | None = None, ) -> None: """处理Prompt""" super().__init__(system_prompt, user_prompt) @@ -221,8 +221,8 @@ class PlanEvaluator(CorePattern): def __init__( self, - system_prompt: Union[str, dict[str, str], None] = None, - user_prompt: Union[str, dict[str, str], None] = None, + system_prompt: dict[str, str] | None = None, + user_prompt: dict[str, str] | None = None, ) -> None: """初始化Prompt""" super().__init__(system_prompt, user_prompt) @@ -308,8 +308,8 @@ class RePlanner(CorePattern): def __init__( self, - system_prompt: Union[str, dict[str, str], None] = None, - user_prompt: Union[str, dict[str, str], None] = None, + system_prompt: dict[str, str] | None = None, + user_prompt: dict[str, str] | None = None, ) -> None: """初始化Prompt""" super().__init__(system_prompt, user_prompt) diff --git a/apps/llm/patterns/select.py b/apps/llm/patterns/select.py index a6c496bd..197f4895 100644 --- a/apps/llm/patterns/select.py +++ b/apps/llm/patterns/select.py @@ -18,10 +18,14 @@ logger = logging.getLogger(__name__) class Select(CorePattern): """通过投票选择最佳答案""" - system_prompt: str = "You are a helpful assistant." + system_prompt: dict[str, str] = { + "zh_cn": "你是一个有用的助手。", + "en": "You are a helpful assistant.", + } """系统提示词""" - user_prompt: str = r""" + user_prompt: dict[str, str] = { + "zh_cn": r""" 根据历史对话(包括工具调用结果)和用户问题,从给出的选项列表中,选出最符合要求的那一项。 @@ -71,7 +75,63 @@ class Select(CorePattern): 让我们一步一步思考。 - """ + """, + "en": r""" + + + Based on the historical dialogue (including tool call results) and user question, select the most \ + suitable option from the given option list. + Before outputting, please think carefully and use the "" tag to give the thinking process. + The output needs to be in JSON format, the output format is: {{ "choice": "option name" }} + + + + + Use the weather API to query the weather information of Hangzhou tomorrow + + + + API + HTTP request, get the returned JSON data + + + SQL + Query the database, get the data in the database table + + + + + + The API tool can get external data through API, and the weather information may be stored in \ + external data. Since the user clearly mentioned the use of weather API, it should be given \ + priority to the API tool.\ + The SQL tool is used to get information from the database, considering the variability and \ + dynamism of weather data, it is unlikely to be stored in the database, so the priority of \ + the SQL tool is relatively low, \ + The best choice seems to be "API: request a specific API, get the returned JSON data". + + + + {{ "choice": "API" }} + + + + + + {question} + + + + {choice_list} + + + + + Let's think step by step. + + + """, + } """用户提示词""" slot_schema: ClassVar[dict[str, Any]] = { @@ -86,17 +146,19 @@ class Select(CorePattern): } """最终输出的JSON Schema""" - - def __init__(self, system_prompt: str | None = None, user_prompt: str | None = None) -> None: - """初始化Prompt""" + def __init__( + self, + system_prompt: dict[str, str] | None = None, + user_prompt: dict[str, str] | None = None, + ) -> None: + """处理Prompt""" super().__init__(system_prompt, user_prompt) - async def _generate_single_attempt(self, user_input: str, choice_list: list[str]) -> str: """使用ReasoningLLM进行单次尝试""" logger.info("[Select] 单次选择尝试: %s", user_input) messages = [ - {"role": "system", "content": self.system_prompt}, + {"role": "system", "content": self.system_prompt[self.language]}, {"role": "user", "content": user_input}, ] result = "" @@ -120,7 +182,6 @@ class Select(CorePattern): function_result = await json_gen.generate() return function_result["choice"] - async def generate(self, **kwargs) -> str: # noqa: ANN003 """使用大模型做出选择""" logger.info("[Select] 使用LLM选择") @@ -128,6 +189,7 @@ class Select(CorePattern): result_list = [] background = kwargs.get("background", "无背景信息。") + self.language = kwargs.get("language", "zh_cn") data_str = json.dumps(kwargs.get("data", {}), ensure_ascii=False) choice_prompt, choices_list = choices_to_prompt(kwargs["choices"]) @@ -141,7 +203,7 @@ class Select(CorePattern): return choices_list[0] logger.info("[Select] 选项列表: %s", choice_prompt) - user_input = self.user_prompt.format( + user_input = self.user_prompt[self.language].format( question=kwargs["question"], background=background, data=data_str, diff --git a/apps/routers/chat.py b/apps/routers/chat.py index 65d121a9..4050e61f 100644 --- a/apps/routers/chat.py +++ b/apps/routers/chat.py @@ -49,7 +49,7 @@ async def init_task(post_body: RequestData, user_sub: str, session_id: str) -> T task.ids.group_id = post_body.group_id task.runtime.question = post_body.question task.ids.group_id = post_body.group_id - task.language = "zh_cn" if post_body.language in {"zh_cn", "zh"} else "en" + task.language = post_body.language return task @@ -126,6 +126,7 @@ async def chat( session_id: Annotated[str, Depends(get_session)], ) -> StreamingResponse: """LLM流式对话接口""" + post_body.language = "zh_cn" if post_body.language in {"zh_cn", "zh"} else "en" # 问题黑名单检测 if not await QuestionBlacklistManager.check_blacklisted_questions(input_question=post_body.question): # 用户扣分 diff --git a/apps/scheduler/call/api/api.py b/apps/scheduler/call/api/api.py index f156b176..cde2c268 100644 --- a/apps/scheduler/call/api/api.py +++ b/apps/scheduler/call/api/api.py @@ -5,7 +5,7 @@ import json import logging from collections.abc import AsyncGenerator from functools import partial -from typing import Any +from typing import Any, ClassVar import httpx from fastapi import status @@ -59,19 +59,27 @@ class API(CoreCall, input_model=APIInput, output_model=APIOutput): body: dict[str, Any] = Field(description="已知的部分请求体", default={}) query: dict[str, Any] = Field(description="已知的部分请求参数", default={}) - @classmethod - def info(cls, language: str = "zh_cn") -> CallInfo: - """返回Call的名称和描述""" - name_map = { - "zh_cn": "API调用", - "en": "API Call", - } - description_map = { - "zh_cn": "向某一个API接口发送HTTP请求,获取数据", - "en": "Send an HTTP request to an API to obtain data", - } - return CallInfo(name=name_map[language], description=description_map[language]) + i18n_info: ClassVar[dict[str, dict]] = { + "zh_cn": { + "name": "API调用", + "description": "向某一个API接口发送HTTP请求,获取数据", + }, + "en": { + "name": "API Call", + "description": "Send an HTTP request to an API to obtain data", + }, + } + @classmethod + def info(cls) -> CallInfo: + """ + 返回Call的名称和描述 + + :return: Call的名称和描述 + :rtype: CallInfo + """ + lang_info = cls.i18n_info.get(cls.language, cls.i18n_info["zh_cn"]) + return CallInfo(name=lang_info["name"], description=lang_info["description"]) async def _init(self, call_vars: CallVars) -> APIInput: """初始化API调用工具""" self._service_id = "" diff --git a/apps/scheduler/call/choice/choice.py b/apps/scheduler/call/choice/choice.py index 1a2a22c4..7847df52 100644 --- a/apps/scheduler/call/choice/choice.py +++ b/apps/scheduler/call/choice/choice.py @@ -5,7 +5,7 @@ import ast import copy import logging from collections.abc import AsyncGenerator -from typing import Any +from typing import Any, ClassVar from pydantic import Field @@ -34,21 +34,35 @@ class Choice(CoreCall, input_model=ChoiceInput, output_model=ChoiceOutput): """Choice工具""" to_user: bool = Field(default=False) - choices: list[ChoiceBranch] = Field(description="分支", default=[ChoiceBranch(), - ChoiceBranch(conditions=[Condition()], is_default=False)]) + choices: list[ChoiceBranch] = Field( + description="分支", + default=[ + ChoiceBranch(), + ChoiceBranch(conditions=[Condition()], is_default=False) + ] + ) + + i18n_info: ClassVar[dict[str, dict]] = { + "zh_cn": { + "name": "判断", + "description": "使用大模型或使用程序做出判断", + }, + "en": { + "name": "Choice", + "description": "Use a large model or a program to make a decision", + }, + } @classmethod - def info(cls, language: str = "zh_cn") -> CallInfo: - """返回Call的名称和描述""" - name_map = { - "zh_cn": "Choice", - "en": "Choice", - } - description_map = { - "zh_cn": "使用大模型或使用程序做出判断", - "en": "Use a large model or a program to make a decision", - } - return CallInfo(name=name_map[language], description=description_map[language]) + def info(cls) -> CallInfo: + """ + 返回Call的名称和描述 + + :return: Call的名称和描述 + :rtype: CallInfo + """ + lang_info = cls.i18n_info.get(cls.language, cls.i18n_info["zh_cn"]) + return CallInfo(name=lang_info["name"], description=lang_info["description"]) def _raise_value_error(self, msg: str) -> None: """统一处理 ValueError 异常抛出""" diff --git a/apps/scheduler/call/core.py b/apps/scheduler/call/core.py index 551ae5ef..42d113a0 100644 --- a/apps/scheduler/call/core.py +++ b/apps/scheduler/call/core.py @@ -52,7 +52,9 @@ class CoreCall(BaseModel): name: SkipJsonSchema[str] = Field(description="Step的名称", exclude=True) description: SkipJsonSchema[str] = Field(description="Step的描述", exclude=True) node: SkipJsonSchema[NodePool | None] = Field(description="节点信息", exclude=True) - enable_filling: SkipJsonSchema[bool] = Field(description="是否需要进行自动参数填充", default=False, exclude=True) + enable_filling: SkipJsonSchema[bool] = Field( + description="是否需要进行自动参数填充", default=False, exclude=True + ) tokens: SkipJsonSchema[CallTokens] = Field( description="Call的输入输出Tokens信息", default=CallTokens(), @@ -68,6 +70,12 @@ class CoreCall(BaseModel): exclude=True, frozen=True, ) + language: ClassVar[SkipJsonSchema[str]] = Field( + description="语言", + default="zh_cn", + exclude=True, + ) + i18n_info: ClassVar[SkipJsonSchema[dict[str, dict]]] = {} to_user: bool = Field(description="是否需要将输出返回给用户", default=False) @@ -76,7 +84,9 @@ class CoreCall(BaseModel): extra="allow", ) - def __init_subclass__(cls, input_model: type[DataBase], output_model: type[DataBase], **kwargs: Any) -> None: + def __init_subclass__( + cls, input_model: type[DataBase], output_model: type[DataBase], **kwargs: Any + ) -> None: """初始化子类""" super().__init_subclass__(**kwargs) cls.input_model = input_model @@ -181,12 +191,11 @@ class CoreCall(BaseModel): async def _after_exec(self, input_data: dict[str, Any]) -> None: """Call类实例的执行后方法""" - - async def exec(self, executor: "StepExecutor", input_data: dict[str, Any], language: str = "zh_cn") -> AsyncGenerator[CallOutputChunk, None]: + async def exec( + self, executor: "StepExecutor", input_data: dict[str, Any], language: str = "zh_cn" + ) -> AsyncGenerator[CallOutputChunk, None]: """Call类实例的执行方法""" - # 部分节点如 start/end 不能传入 language - exec_args = (input_data, language) if input_data else (self.input,) - async for chunk in self._exec(*exec_args): + async for chunk in self._exec(input_data, language): yield chunk await self._after_exec(input_data) diff --git a/apps/scheduler/call/empty.py b/apps/scheduler/call/empty.py index 5865bc7e..f0dc5fbd 100644 --- a/apps/scheduler/call/empty.py +++ b/apps/scheduler/call/empty.py @@ -34,7 +34,7 @@ class Empty(CoreCall, input_model=DataBase, output_model=DataBase): return DataBase() - async def _exec(self, input_data: dict[str, Any]) -> AsyncGenerator[CallOutputChunk, None]: + async def _exec(self, input_data: dict[str, Any], language: str) -> AsyncGenerator[CallOutputChunk, None]: """ 执行Call diff --git a/apps/scheduler/call/facts/facts.py b/apps/scheduler/call/facts/facts.py index eed98495..eea66a1f 100644 --- a/apps/scheduler/call/facts/facts.py +++ b/apps/scheduler/call/facts/facts.py @@ -2,7 +2,7 @@ """提取事实工具""" from collections.abc import AsyncGenerator -from typing import TYPE_CHECKING, Any, Self +from typing import TYPE_CHECKING, Any, Self, ClassVar from jinja2 import BaseLoader from jinja2.sandbox import SandboxedEnvironment @@ -30,18 +30,27 @@ class FactsCall(CoreCall, input_model=FactsInput, output_model=FactsOutput): answer: str = Field(description="用户输入") + i18n_info: ClassVar[dict[str, dict]] = { + "zh_cn": { + "name": "提取事实", + "description": "从对话上下文和文档片段中提取事实。", + }, + "en": { + "name": "Fact Extraction", + "description": "Extract facts from the conversation context and document snippets.", + }, + } + @classmethod - def info(cls, language: str = "zh_cn") -> CallInfo: - """返回Call的名称和描述""" - name_map = { - "zh_cn": "提取事实", - "en": "Fact Extraction", - } - description_map = { - "zh_cn": "从对话上下文和文档片段中提取事实。", - "en": "Extract facts from the conversation context and document snippnets.", - } - return CallInfo(name=name_map[language], description=description_map[language]) + def info(cls) -> CallInfo: + """ + 返回Call的名称和描述 + + :return: Call的名称和描述 + :rtype: CallInfo + """ + lang_info = cls.i18n_info.get(cls.language, cls.i18n_info["zh_cn"]) + return CallInfo(name=lang_info["name"], description=lang_info["description"]) @classmethod async def instance(cls, executor: "StepExecutor", node: NodePool | None, **kwargs: Any) -> Self: diff --git a/apps/scheduler/call/graph/graph.py b/apps/scheduler/call/graph/graph.py index 2ca2c359..557d67f5 100644 --- a/apps/scheduler/call/graph/graph.py +++ b/apps/scheduler/call/graph/graph.py @@ -3,7 +3,7 @@ import json from collections.abc import AsyncGenerator -from typing import Any +from typing import Any, ClassVar from anyio import Path from pydantic import Field @@ -25,19 +25,27 @@ class Graph(CoreCall, input_model=RenderInput, output_model=RenderOutput): dataset_key: str = Field(description="图表的数据来源(字段名)", default="") + i18n_info: ClassVar[dict[str, dict]] = { + "zh_cn": { + "name": "图表", + "description": "将SQL查询出的数据转换为图表。", + }, + "en": { + "name": "Chart", + "description": "Convert the data queried by SQL into a chart.", + }, + } @classmethod - def info(cls, language: str = "zh_cn") -> CallInfo: - """返回Call的名称和描述""" - name_map = { - "zh_cn": "图表", - "en": "Chart", - } - description_map = { - "zh_cn": "将SQL查询出的数据转换为图表。", - "en": "Convert the data queried by SQL into a chart.", - } - return CallInfo(name=name_map[language], description=description_map[language]) + def info(cls) -> CallInfo: + """ + 返回Call的名称和描述 + + :return: Call的名称和描述 + :rtype: CallInfo + """ + lang_info = cls.i18n_info.get(cls.language, cls.i18n_info["zh_cn"]) + return CallInfo(name=lang_info["name"], description=lang_info["description"]) async def _init(self, call_vars: CallVars) -> RenderInput: diff --git a/apps/scheduler/call/llm/llm.py b/apps/scheduler/call/llm/llm.py index 2922994b..70f0f0e4 100644 --- a/apps/scheduler/call/llm/llm.py +++ b/apps/scheduler/call/llm/llm.py @@ -4,7 +4,7 @@ import logging from collections.abc import AsyncGenerator from datetime import datetime -from typing import Any +from typing import Any, ClassVar import pytz from jinja2 import BaseLoader @@ -38,19 +38,27 @@ class LLM(CoreCall, input_model=LLMInput, output_model=LLMOutput): system_prompt: str = Field(description="大模型系统提示词", default="You are a helpful assistant.") user_prompt: str = Field(description="大模型用户提示词", default=LLM_DEFAULT_PROMPT) + i18n_info: ClassVar[dict[str, dict]] = { + "zh_cn": { + "name": "大模型", + "description": "以指定的提示词和上下文信息调用大模型,并获得输出。", + }, + "en": { + "name": "Foundation Model", + "description": "Call the foundation model with specified prompt and context, and obtain the output.", + }, + } @classmethod - def info(cls, language: str = "zh_cn") -> CallInfo: - """返回Call的名称和描述""" - name_map = { - "zh_cn": "大模型", - "en": "Foundation Model", - } - description_map = { - "zh_cn": "以指定的提示词和上下文信息调用大模型,并获得输出。", - "en": "Call the foundation model with specified prompt and context, and obtain the output.", - } - return CallInfo(name=name_map[language], description=description_map[language]) + def info(cls) -> CallInfo: + """ + 返回Call的名称和描述 + + :return: Call的名称和描述 + :rtype: CallInfo + """ + lang_info = cls.i18n_info.get(cls.language, cls.i18n_info["zh_cn"]) + return CallInfo(name=lang_info["name"], description=lang_info["description"]) async def _prepare_message(self, call_vars: CallVars) -> list[dict[str, Any]]: """准备消息""" @@ -64,7 +72,7 @@ class LLM(CoreCall, input_model=LLMInput, output_model=LLMOutput): # 上下文信息 step_history = [] - for ids in call_vars.history_order[-self.step_history_size:]: + for ids in call_vars.history_order[-self.step_history_size :]: step_history += [call_vars.history[ids]] if self.enable_context: @@ -100,15 +108,15 @@ class LLM(CoreCall, input_model=LLMInput, output_model=LLMOutput): {"role": "user", "content": user_input}, ] - async def _init(self, call_vars: CallVars) -> LLMInput: """初始化LLM工具""" return LLMInput( message=await self._prepare_message(call_vars), ) - - async def _exec(self, input_data: dict[str, Any], language: str = "zh_cn") -> AsyncGenerator[CallOutputChunk, None]: + async def _exec( + self, input_data: dict[str, Any], language: str = "zh_cn" + ) -> AsyncGenerator[CallOutputChunk, None]: """运行LLM Call""" data = LLMInput(**input_data) try: diff --git a/apps/scheduler/call/mcp/mcp.py b/apps/scheduler/call/mcp/mcp.py index 0588d186..9e8ffd22 100644 --- a/apps/scheduler/call/mcp/mcp.py +++ b/apps/scheduler/call/mcp/mcp.py @@ -4,7 +4,7 @@ import logging from collections.abc import AsyncGenerator from copy import deepcopy -from typing import Any +from typing import Any, ClassVar from pydantic import Field @@ -26,6 +26,27 @@ from apps.schemas.scheduler import ( logger = logging.getLogger(__name__) +MCP_GENERATE: dict[str, dict[str, str]] = { + "START": { + "zh_cn": "[MCP] 开始生成计划...\n\n\n\n", + "en": "[MCP] Start generating plan...\n\n\n\n", + }, + "END": { + "zh_cn": "[MCP] 计划生成完成:\n\n{plan_str}\n\n\n\n", + "en": "[MCP] Plan generation completed: \n\n{plan_str}\n\n\n\n", + }, +} + +MCP_SUMMARY: dict[str, dict[str, str]] = { + "START": { + "zh_cn": "[MCP] 正在总结任务结果...\n\n", + "en": "[MCP] Start summarizing task results...\n\n", + }, + "END": { + "zh_cn": "[MCP] 任务完成\n\n---\n\n{answer}\n\n", + "en": "[MCP] Task summary completed\n\n{answer}\n\n", + }, +} class MCP(CoreCall, input_model=MCPInput, output_model=MCPOutput): """MCP工具""" @@ -35,23 +56,27 @@ class MCP(CoreCall, input_model=MCPInput, output_model=MCPOutput): text_output: bool = Field(description="是否将结果以文本形式返回", default=True) to_user: bool = Field(description="是否将结果返回给用户", default=True) + i18n_info: ClassVar[dict[str, dict]] = { + "zh_cn": { + "name": "MCP", + "description": "调用MCP Server,执行工具", + }, + "en": { + "name": "MCP", + "description": "Call the MCP Server to execute tools", + }, + } + @classmethod - def info(cls, language: str = "zh_cn") -> CallInfo: + def info(cls) -> CallInfo: """ 返回Call的名称和描述 :return: Call的名称和描述 :rtype: CallInfo """ - name_map = { - "zh_cn": "MCP", - "en": "MCP", - } - description_map = { - "zh_cn": "调用MCP Server,执行工具", - "en": "Call the MCP Server to execute tools", - } - return CallInfo(name=name_map[language], description=description_map[language]) + lang_info = cls.i18n_info.get(cls.language, cls.i18n_info["zh_cn"]) + return CallInfo(name=lang_info["name"], description=lang_info["description"]) async def _init(self, call_vars: CallVars) -> MCPInput: """初始化MCP""" @@ -71,8 +96,9 @@ class MCP(CoreCall, input_model=MCPInput, output_model=MCPOutput): return MCPInput(avaliable_tools=avaliable_tools, max_steps=self.max_steps) - - async def _exec(self, input_data: dict[str, Any], language: str = "zh_cn") -> AsyncGenerator[CallOutputChunk, None]: + async def _exec( + self, input_data: dict[str, Any], language: str = "zh_cn" + ) -> AsyncGenerator[CallOutputChunk, None]: """执行MCP""" # 生成计划 async for chunk in self._generate_plan(language): @@ -90,13 +116,8 @@ class MCP(CoreCall, input_model=MCPInput, output_model=MCPOutput): async def _generate_plan(self, language) -> AsyncGenerator[CallOutputChunk, None]: """生成执行计划""" - start_output = { - "zh_cn": "[MCP] 开始生成计划...\n\n\n\n", - "en": "[MCP] Start generating plan...\n\n\n\n", - } - # 开始提示 - yield self._create_output(start_output[language], MCPMessageType.PLAN_BEGIN) + yield self._create_output(MCP_GENERATE["START"][language], MCPMessageType.PLAN_BEGIN) # 选择工具并生成计划 selector = MCPSelector() @@ -109,18 +130,15 @@ class MCP(CoreCall, input_model=MCPInput, output_model=MCPOutput): for plan_item in self._plan.plans: plan_str += f"[+] {plan_item.content}; {plan_item.tool}[{plan_item.instruction}]\n\n" - end_output = { - "zh_cn": f"[MCP] 计划生成完成:\n\n{plan_str}\n\n\n\n", - "en": f"[MCP] Plan generation completed: \n\n{plan_str}\n\n\n\n", - } - yield self._create_output( - end_output[language], + MCP_GENERATE["END"][language].format(plan_str=plan_str), MCPMessageType.PLAN_END, data=self._plan.model_dump(), ) - async def _execute_plan_item(self, plan_item: MCPPlanItem, language:str = "zh_cn") -> AsyncGenerator[CallOutputChunk, None]: + async def _execute_plan_item( + self, plan_item: MCPPlanItem, language: str = "zh_cn" + ) -> AsyncGenerator[CallOutputChunk, None]: """执行单个计划项""" # 判断是否为Final if plan_item.tool == "Final": @@ -160,12 +178,8 @@ class MCP(CoreCall, input_model=MCPInput, output_model=MCPOutput): async def _generate_answer(self, language) -> AsyncGenerator[CallOutputChunk, None]: """生成总结""" # 提示开始总结 - start_output = { - "zh_cn": "[MCP] 正在总结任务结果...\n\n", - "en": "[MCP] Start summarizing task results...\n\n", - } yield self._create_output( - start_output[language], + MCP_SUMMARY["START"][language], MCPMessageType.FINISH_BEGIN, ) @@ -173,13 +187,9 @@ class MCP(CoreCall, input_model=MCPInput, output_model=MCPOutput): planner = MCPPlanner(self._call_vars.question, language) answer = await planner.generate_answer(self._plan, await self._host.assemble_memory()) - end_output = { - "zh_cn": f"[MCP] 任务完成\n\n---\n\n{answer}\n\n", - "en": f"[MCP] Task summary completed\n\n{answer}\n\n" - } # 输出结果 yield self._create_output( - end_output[language], + MCP_SUMMARY["END"][language].format(answer=answer), MCPMessageType.FINISH_END, data=MCPOutput( message=answer, diff --git a/apps/scheduler/call/rag/rag.py b/apps/scheduler/call/rag/rag.py index 3be38576..e321ac96 100644 --- a/apps/scheduler/call/rag/rag.py +++ b/apps/scheduler/call/rag/rag.py @@ -3,7 +3,7 @@ import logging from collections.abc import AsyncGenerator -from typing import Any +from typing import Any, ClassVar import httpx from fastapi import status @@ -37,18 +37,27 @@ class RAG(CoreCall, input_model=RAGInput, output_model=RAGOutput): is_compress: bool = Field(description="是否压缩", default=False) tokens_limit: int = Field(description="token限制", default=8192) + i18n_info: ClassVar[dict[str, dict]] = { + "zh_cn": { + "name": "知识库", + "description": "查询知识库,从文档中获取必要信息", + }, + "en": { + "name": "Knowledge Base", + "description": "Query the knowledge base and obtain necessary information from documents", + }, + } + @classmethod - def info(cls, language:str = "zh_cn") -> CallInfo: - """返回Call的名称和描述""" - name_map = { - "zh_cn": "知识库", - "en": "Knowledge Base", - } - description_map = { - "zh_cn": "查询知识库,从文档中获取必要信息", - "en": "Query the knowledge base and obtain necessary information from documents", - } - return CallInfo(name=name_map[language], description=description_map[language]) + def info(cls) -> CallInfo: + """ + 返回Call的名称和描述 + + :return: Call的名称和描述 + :rtype: CallInfo + """ + lang_info = cls.i18n_info.get(cls.language, cls.i18n_info["zh_cn"]) + return CallInfo(name=lang_info["name"], description=lang_info["description"]) async def _init(self, call_vars: CallVars) -> RAGInput: """初始化RAG工具""" diff --git a/apps/scheduler/call/search/search.py b/apps/scheduler/call/search/search.py index b2af66a0..ead0ed9d 100644 --- a/apps/scheduler/call/search/search.py +++ b/apps/scheduler/call/search/search.py @@ -1,7 +1,7 @@ """搜索工具""" from collections.abc import AsyncGenerator -from typing import Any +from typing import Any, ClassVar from apps.scheduler.call.core import CoreCall from apps.scheduler.call.search.schema import SearchInput, SearchOutput @@ -16,18 +16,27 @@ from apps.schemas.scheduler import ( class Search(CoreCall, input_model=SearchInput, output_model=SearchOutput): """搜索工具""" + i18n_info: ClassVar[dict[str, dict]] = { + "zh_cn": { + "name": "搜索", + "description": "获取搜索引擎的结果。", + }, + "en": { + "name": "Search", + "description": "Get the results of the search engine.", + }, + } + @classmethod - def info(cls, language: str = "zh_cn") -> CallInfo: - """返回Call的名称和描述""" - name_map = { - "zh_cn": "搜索", - "en": "Search", - } - description_map = { - "zh_cn": "获取搜索引擎的结果。", - "en": "Get the results of the search engine.", - } - return CallInfo(name=name_map[language], description=description_map[language]) + def info(cls) -> CallInfo: + """ + 返回Call的名称和描述 + + :return: Call的名称和描述 + :rtype: CallInfo + """ + lang_info = cls.i18n_info.get(cls.language, cls.i18n_info["zh_cn"]) + return CallInfo(name=lang_info["name"], description=lang_info["description"]) async def _init(self, call_vars: CallVars) -> SearchInput: """初始化工具""" diff --git a/apps/scheduler/call/slot/slot.py b/apps/scheduler/call/slot/slot.py index 69fdf7fd..b32eda10 100644 --- a/apps/scheduler/call/slot/slot.py +++ b/apps/scheduler/call/slot/slot.py @@ -3,7 +3,7 @@ import json from collections.abc import AsyncGenerator -from typing import TYPE_CHECKING, Any, Self +from typing import TYPE_CHECKING, Any, Self, ClassVar from jinja2 import BaseLoader from jinja2.sandbox import SandboxedEnvironment @@ -32,21 +32,31 @@ class Slot(CoreCall, input_model=SlotInput, output_model=SlotOutput): facts: list[str] = Field(description="事实信息", default=[]) step_num: int = Field(description="历史步骤数", default=1) + i18n_info: ClassVar[dict[str, dict]] = { + "zh_cn": { + "name": "参数自动填充", + "description": "根据步骤历史,自动填充参数", + }, + "en": { + "name": "Parameter Auto-Fill", + "description": "Auto-fill parameters based on step history.", + }, + } + @classmethod - def info(cls, language: str = "zh_cn") -> CallInfo: - """返回Call的名称和描述""" - name_map = { - "zh_cn": "参数自动填充", - "en": "Parameter Auto-Fill", - } - description_map = { - "zh_cn": "根据步骤历史,自动填充参数", - "en": "Auto-fill parameters based on step history.", - } - return CallInfo(name=name_map[language], description=description_map[language]) - - - async def _llm_slot_fill(self, remaining_schema: dict[str, Any], language:str = "zh_cn") -> tuple[str, dict[str, Any]]: + def info(cls) -> CallInfo: + """ + 返回Call的名称和描述 + + :return: Call的名称和描述 + :rtype: CallInfo + """ + lang_info = cls.i18n_info.get(cls.language, cls.i18n_info["zh_cn"]) + return CallInfo(name=lang_info["name"], description=lang_info["description"]) + + async def _llm_slot_fill( + self, remaining_schema: dict[str, Any], language: str = "zh_cn" + ) -> tuple[str, dict[str, Any]]: """使用大模型填充参数;若大模型解析度足够,则直接返回结果""" env = SandboxedEnvironment( loader=BaseLoader(), @@ -58,17 +68,20 @@ class Slot(CoreCall, input_model=SlotInput, output_model=SlotOutput): conversation = [ {"role": "system", "content": "You are a helpful assistant."}, - {"role": "user", "content": template.render( - current_tool={ - "name": self.name, - "description": self.description, - }, - schema=remaining_schema, - history_data=self._flow_history, - summary=self.summary, - question=self._question, - facts=self.facts, - )}, + { + "role": "user", + "content": template.render( + current_tool={ + "name": self.name, + "description": self.description, + }, + schema=remaining_schema, + history_data=self._flow_history, + summary=self.summary, + question=self._question, + facts=self.facts, + ), + }, ] # 使用大模型进行尝试 @@ -117,7 +130,7 @@ class Slot(CoreCall, input_model=SlotInput, output_model=SlotOutput): """初始化""" self._flow_history = [] self._question = call_vars.question - for key in call_vars.history_order[:-self.step_num]: + for key in call_vars.history_order[: -self.step_num]: self._flow_history += [call_vars.history[key]] if not self.current_schema: @@ -132,8 +145,9 @@ class Slot(CoreCall, input_model=SlotInput, output_model=SlotOutput): remaining_schema=remaining_schema, ) - - async def _exec(self, input_data: dict[str, Any], language: str = "zh_cn") -> AsyncGenerator[CallOutputChunk, None]: + async def _exec( + self, input_data: dict[str, Any], language: str = "zh_cn" + ) -> AsyncGenerator[CallOutputChunk, None]: """执行参数填充""" data = SlotInput(**input_data) diff --git a/apps/scheduler/call/sql/sql.py b/apps/scheduler/call/sql/sql.py index 032e6321..4180bc96 100644 --- a/apps/scheduler/call/sql/sql.py +++ b/apps/scheduler/call/sql/sql.py @@ -3,7 +3,7 @@ import logging from collections.abc import AsyncGenerator -from typing import Any +from typing import Any, ClassVar import httpx from fastapi import status @@ -31,18 +31,27 @@ class SQL(CoreCall, input_model=SQLInput, output_model=SQLOutput): top_k: int = Field(description="生成SQL语句数量", default=5) use_llm_enhancements: bool = Field(description="是否使用大模型增强", default=False) + i18n_info: ClassVar[dict[str, dict]] = { + "zh_cn": { + "name": "SQL查询", + "description": "使用大模型生成SQL语句,用于查询数据库中的结构化数据", + }, + "en": { + "name": "SQL Query", + "description": "Use the foundation model to generate SQL statements to query structured data in the databases", + }, + } + @classmethod - def info(cls, language: str = "zh_cn") -> CallInfo: - """返回Call的名称和描述""" - name_map = { - "zh_cn": "SQL查询", - "en": "SQL Query", - } - description_map = { - "zh_cn": "使用大模型生成SQL语句,用于查询数据库中的结构化数据", - "en": "Use the foundation model to generate SQL statements to query structured data in the databases", - } - return CallInfo(name=name_map[language], description=description_map[language]) + def info(cls) -> CallInfo: + """ + 返回Call的名称和描述 + + :return: Call的名称和描述 + :rtype: CallInfo + """ + lang_info = cls.i18n_info.get(cls.language, cls.i18n_info["zh_cn"]) + return CallInfo(name=lang_info["name"], description=lang_info["description"]) async def _init(self, call_vars: CallVars, language: str = "zh_cn") -> SQLInput: """初始化SQL工具。""" diff --git a/apps/scheduler/call/suggest/suggest.py b/apps/scheduler/call/suggest/suggest.py index df9e8c5d..a722de8c 100644 --- a/apps/scheduler/call/suggest/suggest.py +++ b/apps/scheduler/call/suggest/suggest.py @@ -3,7 +3,7 @@ import random from collections.abc import AsyncGenerator -from typing import TYPE_CHECKING, Any, Self +from typing import TYPE_CHECKING, Any, Self, ClassVar from jinja2 import BaseLoader from jinja2.sandbox import SandboxedEnvironment @@ -49,18 +49,27 @@ class Suggestion(CoreCall, input_model=SuggestionInput, output_model=SuggestionO context: SkipJsonSchema[list[dict[str, str]]] = Field(description="Executor的上下文", exclude=True) conversation_id: SkipJsonSchema[str] = Field(description="对话ID", exclude=True) + i18n_info: ClassVar[dict[str, dict]] = { + "zh_cn": { + "name": "问题推荐", + "description": "在答案下方显示推荐的下一个问题", + }, + "en": { + "name": "Question Suggestion", + "description": "Display the suggested next question under the answer", + }, + } + @classmethod - def info(cls, language: str = "zh_cn") -> CallInfo: - """返回Call的名称和描述""" - name_map = { - "zh_cn": "问题推荐", - "en": "Question Suggestion", - } - description_map = { - "zh_cn": "在答案下方显示推荐的下一个问题", - "en": "Display the suggested next question under the answer", - } - return CallInfo(name=name_map[language], description=description_map[language]) + def info(cls) -> CallInfo: + """ + 返回Call的名称和描述 + + :return: Call的名称和描述 + :rtype: CallInfo + """ + lang_info = cls.i18n_info.get(cls.language, cls.i18n_info["zh_cn"]) + return CallInfo(name=lang_info["name"], description=lang_info["description"]) @classmethod async def instance(cls, executor: "StepExecutor", node: NodePool | None, **kwargs: Any) -> Self: diff --git a/apps/scheduler/call/summary/summary.py b/apps/scheduler/call/summary/summary.py index b7af8153..87c9e7b3 100644 --- a/apps/scheduler/call/summary/summary.py +++ b/apps/scheduler/call/summary/summary.py @@ -2,7 +2,7 @@ """总结上下文工具""" from collections.abc import AsyncGenerator -from typing import TYPE_CHECKING, Any, Self +from typing import TYPE_CHECKING, Any, Self, ClassVar from pydantic import Field @@ -28,18 +28,27 @@ class Summary(CoreCall, input_model=DataBase, output_model=SummaryOutput): context: ExecutorBackground = Field(description="对话上下文") + i18n_info: ClassVar[dict[str, dict]] = { + "zh_cn": { + "name": "理解上下文", + "description": "使用大模型,理解对话上下文", + }, + "en": { + "name": "Context Understanding", + "description": "Use the foundation model to understand the conversation context", + }, + } + @classmethod - def info(cls, language: str = "zh_cn") -> CallInfo: - """返回Call的名称和描述""" - name_map = { - "zh_cn": "理解上下文", - "en": "Context Understanding", - } - description_map = { - "zh_cn": "使用大模型,理解对话上下文", - "en": "Use the foundation model to understand the conversation context", - } - return CallInfo(name=name_map[language], description=description_map[language]) + def info(cls) -> CallInfo: + """ + 返回Call的名称和描述 + + :return: Call的名称和描述 + :rtype: CallInfo + """ + lang_info = cls.i18n_info.get(cls.language, cls.i18n_info["zh_cn"]) + return CallInfo(name=lang_info["name"], description=lang_info["description"]) @classmethod async def instance(cls, executor: "StepExecutor", node: NodePool | None, **kwargs: Any) -> Self: diff --git a/apps/scheduler/executor/flow.py b/apps/scheduler/executor/flow.py index 63e089bb..035b7132 100644 --- a/apps/scheduler/executor/flow.py +++ b/apps/scheduler/executor/flow.py @@ -71,7 +71,7 @@ class FlowExecutor(BaseExecutor): """从数据库中加载FlowExecutor的状态""" logger.info("[FlowExecutor] 加载Executor状态") # 尝试恢复State - if self.task.state: + if self.task.state and self.task.state.flow_status != FlowStatus.UNKNOWN: self.task.context = await TaskManager.get_context_by_task_id(self.task.id) else: # 创建ExecutorState @@ -90,11 +90,7 @@ class FlowExecutor(BaseExecutor): self._reached_end: bool = False self.step_queue: deque[StepQueueItem] = deque() -<<<<<<< HEAD async def _invoke_runner(self) -> None: -======= - async def _invoke_runner(self, queue_item: StepQueueItem) -> None: ->>>>>>> 7642418 (完善国际化部分) """单一Step执行""" # 创建步骤Runner step_runner = StepExecutor( @@ -122,11 +118,7 @@ class FlowExecutor(BaseExecutor): break # 执行Step -<<<<<<< HEAD await self._invoke_runner() -======= - await self._invoke_runner(queue_item) ->>>>>>> 7642418 (完善国际化部分) async def _find_next_id(self, step_id: str) -> list[str]: """查找下一个节点""" @@ -205,19 +197,18 @@ class FlowExecutor(BaseExecutor): is_error = False while not self._reached_end: # 如果当前步骤出错,执行错误处理步骤 -<<<<<<< HEAD if self.task.state.step_status == StepStatus.ERROR: # type: ignore[arg-type] logger.warning("[FlowExecutor] Executor出错,执行错误处理步骤") self.step_queue.clear() self.step_queue.appendleft(StepQueueItem( step_id=str(uuid.uuid4()), step=Step( - name="错误处理", - description="错误处理", + name="错误处理" if self.task.language == "zh_cn" else "Error Handling", + description="错误处理" if self.task.language == "zh_cn" else "Error Handling", node=SpecialCallType.LLM.value, type=SpecialCallType.LLM.value, params={ - "user_prompt": LLM_ERROR_PROMPT.replace( + "user_prompt": LLM_ERROR_PROMPT[self.task.language].replace( "{{ error_info }}", self.task.state.error_info["err_msg"], # type: ignore[arg-type] ), @@ -227,30 +218,7 @@ class FlowExecutor(BaseExecutor): to_user=False, )) is_error = True -======= - if self.task.state.status == StepStatus.ERROR: # type: ignore[arg-type] - logger.warning("[FlowExecutor] Executor出错,执行错误处理步骤") - self.step_queue.clear() - self.step_queue.appendleft( - StepQueueItem( - step_id=str(uuid.uuid4()), - step=Step( - name="错误处理" if self.task.language == "zh_cn" else "Error Handling", - description="错误处理" if self.task.language == "zh_cn" else "Error Handling", - node=SpecialCallType.LLM.value, - type=SpecialCallType.LLM.value, - params={ - "user_prompt": LLM_ERROR_PROMPT[self.task.language].replace( - "{{ error_info }}", - self.task.state.error_info["err_msg"], # type: ignore[arg-type] - ), - }, - ), - enable_filling=False, - to_user=False, - ) - ) ->>>>>>> 7642418 (完善国际化部分) + # 错误处理后结束 self._reached_end = True diff --git a/apps/scheduler/mcp/host.py b/apps/scheduler/mcp/host.py index bb4df75e..efac1eb4 100644 --- a/apps/scheduler/mcp/host.py +++ b/apps/scheduler/mcp/host.py @@ -25,10 +25,11 @@ logger = logging.getLogger(__name__) class MCPHost: """MCP宿主服务""" - def __init__(self, user_sub: str, task_id: str, runtime_id: str, runtime_name: str) -> None: + def __init__(self, user_sub: str, task_id: str, runtime_id: str, runtime_name: str, language="zh_cn") -> None: """初始化MCP宿主""" self._user_sub = user_sub self._task_id = task_id + self.language = language # 注意:runtime在工作流中是flow_id和step_description,在Agent中可为标识Agent的id和description self._runtime_id = runtime_id self._runtime_name = runtime_name @@ -59,7 +60,7 @@ class MCPHost: return None - async def assemble_memory(self, language: str = "zh_cn") -> str: + async def assemble_memory(self) -> str: """组装记忆""" task = await TaskManager.get_task_by_task_id(self._task_id) if not task: @@ -68,12 +69,12 @@ class MCPHost: context_list = [] for ctx_id in self._context_list: - context = next((ctx for ctx in task.context if ctx["_id"] == ctx_id), None) + context = next((ctx for ctx in task.context if ctx.id == ctx_id), None) if not context: continue context_list.append(context) - return self._env.from_string(MEMORY_TEMPLATE[language]).render( + return self._env.from_string(MEMORY_TEMPLATE[self.language]).render( context_list=context_list, ) @@ -119,13 +120,13 @@ class MCPHost: logger.error("任务 %s 不存在", self._task_id) return {} self._context_list.append(context.id) - task.context.append(context.model_dump(by_alias=True, exclude_none=True)) + task.context.append(context.model_dump(exclude_none=True, by_alias=True)) await TaskManager.save_task(self._task_id, task) return output_data - async def _fill_params(self, tool: MCPTool, query: str, language:str = "zh_cn") -> dict[str, Any]: + async def _fill_params(self, tool: MCPTool, query: str) -> dict[str, Any]: """填充工具参数""" # 更清晰的输入·指令,这样可以调用generate llm_query = rf""" @@ -139,14 +140,14 @@ class MCPHost: llm_query, [ {"role": "system", "content": "You are a helpful assistant."}, - {"role": "user", "content": await self.assemble_memory(language)}, + {"role": "user", "content": await self.assemble_memory()}, ], tool.input_schema, ) return await json_generator.generate() - async def call_tool(self, tool: MCPTool, plan_item: MCPPlanItem, language: str = "zh_cn") -> list[dict[str, Any]]: + async def call_tool(self, tool: MCPTool, plan_item: MCPPlanItem) -> list[dict[str, Any]]: """调用工具""" # 拿到Client client = await MCPPool().get(tool.mcp_id, self._user_sub) @@ -156,7 +157,7 @@ class MCPHost: raise ValueError(err) # 填充参数 - params = await self._fill_params(tool, plan_item.instruction, language) + params = await self._fill_params(tool, plan_item.instruction) # 调用工具 result = await client.call_tool(tool.name, params) # 保存记忆 diff --git a/apps/scheduler/pool/loader/app.py b/apps/scheduler/pool/loader/app.py index 2514357d..a85395bf 100644 --- a/apps/scheduler/pool/loader/app.py +++ b/apps/scheduler/pool/loader/app.py @@ -24,7 +24,7 @@ BASE_PATH = Path(Config().get_config().deploy.data_dir) / "semantics" / "app" class AppLoader: """应用加载器""" - async def load(self, app_id: str, hashes: dict[str, str], language: str = "zh_cn") -> None: # noqa: C901 + async def load(self, app_id: str, hashes: dict[str, str]) -> None: # noqa: C901 """ 从文件系统中加载应用 @@ -52,7 +52,7 @@ class AppLoader: async for flow_file in flow_path.rglob("*.yaml"): if flow_file.stem not in flow_ids: logger.warning("[AppLoader] 工作流 %s 不在元数据中", flow_file) - flow = await flow_loader.load(app_id, flow_file.stem, language) + flow = await flow_loader.load(app_id, flow_file.stem) if not flow: err = f"[AppLoader] 工作流 {flow_file} 加载失败" raise ValueError(err) diff --git a/apps/scheduler/pool/loader/flow.py b/apps/scheduler/pool/loader/flow.py index 5b5939c5..6ea8b687 100644 --- a/apps/scheduler/pool/loader/flow.py +++ b/apps/scheduler/pool/loader/flow.py @@ -26,6 +26,8 @@ BASE_PATH = Path(Config().get_config().deploy.data_dir) / "semantics" / "app" class FlowLoader: """工作流加载器""" + def __init__(self, language: str="zh_cn"): + self.language = language async def _load_yaml_file(self, flow_path: Path) -> dict[str, Any]: """从YAML文件加载工作流配置""" @@ -68,8 +70,7 @@ class FlowLoader: return {} else: return flow_yaml - - async def _process_steps(self, flow_yaml: dict[str, Any], flow_id: str, app_id: str, language) -> dict[str, Any]: + async def _process_steps(self, flow_yaml: dict[str, Any], flow_id: str, app_id: str) -> dict[str, Any]: """处理工作流步骤的转换""" logger.info("[FlowLoader] 应用 %s:解析工作流 %s 的步骤", flow_id, app_id) for key, step in flow_yaml["steps"].items(): @@ -78,12 +79,12 @@ class FlowLoader: logger.error(err) raise ValueError(err) if key == "start": - step["name"] = "开始" if language in {"zh", "zh_cn"} else "Start" - step["description"] = "开始节点" if language in {"zh", "zh_cn"} else "Start Node" + step["name"] = "开始" if self.language in {"zh", "zh_cn"} else "Start" + step["description"] = "开始节点" if self.language in {"zh", "zh_cn"} else "Start Node" step["type"] = "start" elif key == "end": - step["name"] = "结束" if language in {"zh", "zh_cn"} else "End" - step["description"] = "结束节点" if language in {"zh", "zh_cn"} else "End Node" + step["name"] = "结束" if self.language in {"zh", "zh_cn"} else "End" + step["description"] = "结束节点" if self.language in {"zh", "zh_cn"} else "End Node" step["type"] = "end" else: try: @@ -98,7 +99,7 @@ class FlowLoader: ) return flow_yaml - async def load(self, app_id: str, flow_id: str, language:str = "zh_cn") -> Flow | None: + async def load(self, app_id: str, flow_id: str) -> Flow | None: """从文件系统中加载【单个】工作流""" logger.info("[FlowLoader] 应用 %s:加载工作流 %s...", flow_id, app_id) @@ -118,7 +119,7 @@ class FlowLoader: for processor in [ lambda y: self._validate_basic_fields(y, flow_path), lambda y: self._process_edges(y, flow_id, app_id), - lambda y: self._process_steps(y, flow_id, app_id, language), + lambda y: self._process_steps(y, flow_id, app_id), ]: flow_yaml = await processor(flow_yaml) if not flow_yaml: diff --git a/apps/scheduler/pool/pool.py b/apps/scheduler/pool/pool.py index b1c7f57f..625964bf 100644 --- a/apps/scheduler/pool/pool.py +++ b/apps/scheduler/pool/pool.py @@ -149,8 +149,8 @@ class Pool: async def get_flow(self, app_id: str, flow_id: str, language:str = "zh_cn") -> Flow | None: """从文件系统中获取单个Flow的全部数据""" logger.info("[Pool] 获取工作流 %s", flow_id) - flow_loader = FlowLoader() - return await flow_loader.load(app_id, flow_id, language) + flow_loader = FlowLoader(language) + return await flow_loader.load(app_id, flow_id) async def get_call(self, call_id: str) -> Any: diff --git a/apps/scheduler/scheduler/context.py b/apps/scheduler/scheduler/context.py index 72f64a71..fcdb71cb 100644 --- a/apps/scheduler/scheduler/context.py +++ b/apps/scheduler/scheduler/context.py @@ -193,7 +193,7 @@ async def save_data(task: Task, user_sub: str, post_body: RequestData) -> None: flow_id=task.state.flow_id, flow_name=task.state.flow_name, flow_status=task.state.flow_status, - history_ids=[context.id for context in task.context], + history_ids=[context["_id"] for context in task.context], # context 不存在 "id" 属性, 未适配task.context结构 ) ) diff --git a/apps/scheduler/scheduler/message.py b/apps/scheduler/scheduler/message.py index 82330262..8ccead7f 100644 --- a/apps/scheduler/scheduler/message.py +++ b/apps/scheduler/scheduler/message.py @@ -64,13 +64,20 @@ async def push_init_message( async def push_rag_message( - task: Task, queue: MessageQueue, user_sub: str, llm: LLM, history: list[dict[str, str]], - doc_ids: list[str], - rag_data: RAGQueryReq,) -> None: + task: Task, + queue: MessageQueue, + user_sub: str, + llm: LLM, + history: list[dict[str, str]], + doc_ids: list[str], + rag_data: RAGQueryReq, +) -> None: """推送RAG消息""" full_answer = "" try: - async for chunk in RAG.chat_with_llm_base_on_rag(user_sub, llm, history, doc_ids, rag_data, task.language): + async for chunk in RAG.chat_with_llm_base_on_rag( + user_sub, llm, history, doc_ids, rag_data, task.language + ): task, content_obj = await _push_rag_chunk(task, queue, chunk) if content_obj.event_type == EventType.TEXT_ADD.value: # 如果是文本消息,直接拼接到答案中 diff --git a/apps/services/flow.py b/apps/services/flow.py index 35fb3b53..dbbb5126 100644 --- a/apps/services/flow.py +++ b/apps/services/flow.py @@ -4,6 +4,7 @@ import logging from pymongo import ASCENDING +from pydantic import BaseModel from apps.common.mongo import MongoDB from apps.scheduler.pool.loader.flow import FlowLoader @@ -96,19 +97,24 @@ class FlowManager: continue if service_id == "": - call_class = await Pool().get_call(node_pool_record["_id"]) - node_name = call_class.info(language).name - node_description = call_class.info(language).description + call_class: type[BaseModel] = await Pool().get_call(node_pool_record["_id"]) + call_class.language = language + node_name = call_class.info().name + node_description = call_class.info().description + else: + node_name = node_pool_record["name"] + node_description = node_pool_record["description"] node_meta_data_item = NodeMetaDataItem( nodeId=node_pool_record["_id"], callId=node_pool_record["call_id"], - name=node_pool_record["name"] if service_id else node_name, - description=node_pool_record["description"] if service_id else node_description, + name=node_name, + description=node_description, editable=True, createdAt=node_pool_record["created_at"], parameters=parameters, # 添加 parametersTemplate 参数 ) + nodes_meta_data_items.append(node_meta_data_item) except Exception: logger.exception("[FlowManager] 获取节点元数据失败") @@ -258,7 +264,7 @@ class FlowManager: return None try: - flow_config = await FlowLoader().load(app_id, flow_id, language) + flow_config = await FlowLoader(language).load(app_id, flow_id) if not flow_config: logger.error("[FlowManager] 获取流配置失败") return None @@ -415,8 +421,8 @@ class FlowManager: ) flow_config.edges.append(edge_config) - flow_loader = FlowLoader() - old_flow_config = await flow_loader.load(app_id, flow_id, language) + flow_loader = FlowLoader(language) + old_flow_config = await flow_loader.load(app_id, flow_id) if old_flow_config is None: error_msg = f"[FlowManager] 流 {flow_id} 不存在;可能为新创建" @@ -471,8 +477,8 @@ class FlowManager: :return: 是否更新成功 """ try: - flow_loader = FlowLoader() - flow = await flow_loader.load(app_id, flow_id, language) + flow_loader = FlowLoader(language) + flow = await flow_loader.load(app_id, flow_id) if flow is None: return False flow.debug = debug diff --git a/apps/services/rag.py b/apps/services/rag.py index 83fb753b..9fc91ad2 100644 --- a/apps/services/rag.py +++ b/apps/services/rag.py @@ -85,10 +85,7 @@ class RAG: The user's question will be given in . Note: 1. Do not include any XML tags in the output, and do not make up any information. If you think the user's question is unrelated to the background information, please ignore the background information and directly answer. - 2. The format of the footnotes is [[1]], [[2]], [[3]], etc., and the content of the footnotes is the id of the provided document. - 3. The footnotes only appear at the end of the answer sentences, such as after the punctuation marks such as periods, question marks, etc. - 4. Do not explain or explain the footnotes themselves. - 5. Do not use the id of the document in as the footnote. + 2. Your response should not exceed 250 words. -- Gitee From e337450e74f53271437c0467065b1a437fb582c6 Mon Sep 17 00:00:00 2001 From: ylzhangah <1194926515@qq.com> Date: Wed, 6 Aug 2025 15:10:31 +0800 Subject: [PATCH 06/15] update file --- .../euler_copilot/configs/deepinsight/.env | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/deploy/chart/euler_copilot/configs/deepinsight/.env b/deploy/chart/euler_copilot/configs/deepinsight/.env index d3fa5fb6..b753d5c3 100644 --- a/deploy/chart/euler_copilot/configs/deepinsight/.env +++ b/deploy/chart/euler_copilot/configs/deepinsight/.env @@ -8,18 +8,21 @@ MODEL_API_KEY=sk-882ee9907bdf4bf4afee6994dea5697b DEEPSEEK_API_KEY=sk-882ee9907bdf4bf4afee6994dea5697b # 数据库配置,默认使用sqlite,支持postgresql和sqlite DB_TYPE=sqlite -# postgresql配置 -POSTGRES_USER= -POSTGRES_PASSWORD= -POSTGRES_HOST= -POSTGRES_PORT= -POSTGRES_DB= -# MongoDB 配置 -MONGODB_USER= -MONGODB_PASSWORD= -MONGODB_HOST= -MONGODB_PORT= +# opengauss +DATABASE_TYPE=opengauss +DATABASE_HOST=opengauss-db.{{ .Release.Namespace }}.svc.cluster.local +DATABASE_PORT=5432 +DATABASE_USER=postgres +DATABASE_PASSWORD=${gauss-password} +DATABASE_DB=postgres + +# MongoDB +MONGODB_USER=euler_copilot +MONGODB_PASSWORD=${mongo-password} +MONGODB_HOST=mongo-db.{{ .Release.Namespace }}.svc.cluster.local +MONGODB_PORT=27017 +MONGODB_DATABASE=euler_copilot # 是否需要认证鉴权: deepInsight集成到openeuler intelligence依赖认证健全,如果单用户部署则不需要,默认用户为admin REQUIRE_AUTHENTICATION=false -- Gitee From adede1effa9241a61458eaa73443a9d33e79bff4 Mon Sep 17 00:00:00 2001 From: ylzhangah <1194926515@qq.com> Date: Wed, 6 Aug 2025 15:12:20 +0800 Subject: [PATCH 07/15] updata file --- .../configs/deepinsight/.env.backup | 27 ------------------- 1 file changed, 27 deletions(-) delete mode 100644 deploy/chart/euler_copilot/configs/deepinsight/.env.backup diff --git a/deploy/chart/euler_copilot/configs/deepinsight/.env.backup b/deploy/chart/euler_copilot/configs/deepinsight/.env.backup deleted file mode 100644 index 48520425..00000000 --- a/deploy/chart/euler_copilot/configs/deepinsight/.env.backup +++ /dev/null @@ -1,27 +0,0 @@ -# .env.example - -# 默认监听地址和端口 -HOST=0.0.0.0 -PORT=9380 -API_PREFIX=/deepinsight - -# 数据库配置,默认使用postgresql -# DB_TYPE=opengauss -# postgresql配置 -# POSTGRES_USER=opengauss -# POSTGRES_PASSWORD=${gauss-password} -# POSTGRES_HOST=opengauss-db.{{ .Release.Namespace }}.svc.cluster.local -# POSTGRES_PORT=5432 -# POSTGRES_DB=postgres - - -# MongoDB -MONGODB_USER=euler_copilot -MONGODB_PASSWORD=${mongo-password} -MONGODB_HOST=mongo-db.{{ .Release.Namespace }}.svc.cluster.local -MONGODB_PORT=27017 -MONGODB_DATABASE=euler_copilot - - -# 是否需要认证鉴权: deepInsight集成到openeuler intelligence依赖认证健全,如果单用户部署则不需要,默认用户为admin -REQUIRE_AUTHENTICATION=false -- Gitee From 7f677f120b9a3f7eca1dbaf75a7f5c6b6805ab62 Mon Sep 17 00:00:00 2001 From: z30057876 Date: Thu, 7 Aug 2025 15:26:53 +0800 Subject: [PATCH 08/15] =?UTF-8?q?=E4=BF=AE=E6=AD=A3Choice=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E7=B1=BB=E5=9E=8B=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/scheduler/call/choice/choice.py | 77 ++++---- .../call/choice/condition_handler.py | 176 ++++++++++-------- apps/scheduler/call/choice/schema.py | 13 +- 3 files changed, 148 insertions(+), 118 deletions(-) diff --git a/apps/scheduler/call/choice/choice.py b/apps/scheduler/call/choice/choice.py index 01ac7106..f294c9cb 100644 --- a/apps/scheduler/call/choice/choice.py +++ b/apps/scheduler/call/choice/choice.py @@ -11,15 +11,15 @@ from pydantic import Field from apps.scheduler.call.choice.condition_handler import ConditionHandler from apps.scheduler.call.choice.schema import ( - Condition, ChoiceBranch, ChoiceInput, ChoiceOutput, + Condition, Logic, ) -from apps.schemas.parameters import Type from apps.scheduler.call.core import CoreCall from apps.schemas.enum_var import CallOutputType +from apps.schemas.parameters import Type from apps.schemas.scheduler import ( CallError, CallInfo, @@ -42,7 +42,7 @@ class Choice(CoreCall, input_model=ChoiceInput, output_model=ChoiceOutput): """返回Call的名称和描述""" return CallInfo(name="选择器", description="使用大模型或使用程序做出判断") - async def _prepare_message(self, call_vars: CallVars) -> list[dict[str, Any]]: + async def _prepare_message(self, call_vars: CallVars) -> list[ChoiceBranch]: # noqa: C901, PLR0912, PLR0915 """替换choices中的系统变量""" valid_choices = [] @@ -50,8 +50,8 @@ class Choice(CoreCall, input_model=ChoiceInput, output_model=ChoiceOutput): try: # 验证逻辑运算符 if choice.logic not in [Logic.AND, Logic.OR]: - msg = f"无效的逻辑运算符: {choice.logic}" - logger.warning(f"[Choice] 分支 {choice.branch_id} 条件处理失败: {msg}") + msg = f"[Choice] 分支 {choice.branch_id} 条件处理失败:无效的逻辑运算符:{choice.logic}" + logger.warning(msg) continue valid_conditions = [] @@ -60,62 +60,74 @@ class Choice(CoreCall, input_model=ChoiceInput, output_model=ChoiceOutput): # 处理左值 if condition.left.step_id is not None: condition.left.value = self._extract_history_variables( - condition.left.step_id+'/'+condition.left.value, call_vars.history) + f"{condition.left.step_id}/{condition.left.value}", call_vars.history) # 检查历史变量是否成功提取 if condition.left.value is None: - msg = f"步骤 {condition.left.step_id} 的历史变量不存在" - logger.warning(f"[Choice] 分支 {choice.branch_id} 条件处理失败: {msg}") + msg = (f"[Choice] 分支 {choice.branch_id} 条件处理失败:" + f"步骤 {condition.left.step_id} 的历史变量不存在") + logger.warning(msg) continue - if not ConditionHandler.check_value_type( - condition.left.value, condition.left.type): - msg = f"左值类型不匹配: {condition.left.value} 应为 {condition.left.type.value}" - logger.warning(f"[Choice] 分支 {choice.branch_id} 条件处理失败: {msg}") + if not ConditionHandler.check_value_type(condition.left, condition.left.type): + msg = (f"[Choice] 分支 {choice.branch_id} 条件处理失败:" + f"左值类型不匹配:{condition.left.value}" + f"应为 {condition.left.type.value if condition.left.type else 'None'}") + logger.warning(msg) continue else: - msg = "左侧变量缺少step_id" - logger.warning(f"[Choice] 分支 {choice.branch_id} 条件处理失败: {msg}") + msg = f"[Choice] 分支 {choice.branch_id} 条件处理失败:左侧变量缺少step_id" + logger.warning(msg) continue # 处理右值 if condition.right.step_id is not None: condition.right.value = self._extract_history_variables( - condition.right.step_id+'/'+condition.right.value, call_vars.history) + f"{condition.right.step_id}/{condition.right.value}", call_vars.history, + ) # 检查历史变量是否成功提取 if condition.right.value is None: - msg = f"步骤 {condition.right.step_id} 的历史变量不存在" - logger.warning(f"[Choice] 分支 {choice.branch_id} 条件处理失败: {msg}") + msg = (f"[Choice] 分支 {choice.branch_id} 条件处理失败:" + f"步骤 {condition.right.step_id} 的历史变量不存在") + logger.warning(msg) continue if not ConditionHandler.check_value_type( - condition.right.value, condition.right.type): - msg = f"右值类型不匹配: {condition.right.value} 应为 {condition.right.type.value}" - logger.warning(f"[Choice] 分支 {choice.branch_id} 条件处理失败: {msg}") + condition.right, condition.right.type, + ): + msg = (f"[Choice] 分支 {choice.branch_id} 条件处理失败:" + f"右值类型不匹配:{condition.right.value}" + f"应为 {condition.right.type.value if condition.right.type else 'None'}") + logger.warning(msg) continue else: # 如果右值没有step_id,尝试从call_vars中获取 right_value_type = await ConditionHandler.get_value_type_from_operate( - condition.operate) + condition.operate, + ) if right_value_type is None: - msg = f"不支持的运算符: {condition.operate}" - logger.warning(f"[Choice] 分支 {choice.branch_id} 条件处理失败: {msg}") + msg = f"[Choice] 分支 {choice.branch_id} 条件处理失败:不支持的运算符:{condition.operate}" + logger.warning(msg) continue if condition.right.type != right_value_type: - msg = f"右值类型不匹配: {condition.right.value} 应为 {right_value_type.value}" - logger.warning(f"[Choice] 分支 {choice.branch_id} 条件处理失败: {msg}") + msg = (f"[Choice] 分支 {choice.branch_id} 条件处理失败:" + f"右值类型不匹配:{condition.right.value} 应为 {right_value_type.value}") + logger.warning(msg) continue if right_value_type == Type.STRING: condition.right.value = str(condition.right.value) else: condition.right.value = ast.literal_eval(condition.right.value) if not ConditionHandler.check_value_type( - condition.right.value, condition.right.type): - msg = f"右值类型不匹配: {condition.right.value} 应为 {condition.right.type.value}" - logger.warning(f"[Choice] 分支 {choice.branch_id} 条件处理失败: {msg}") + condition.right, condition.right.type, + ): + msg = (f"[Choice] 分支 {choice.branch_id} 条件处理失败:" + f"右值类型不匹配:{condition.right.value}" + f"应为 {condition.right.type.value if condition.right.type else 'None'}") + logger.warning(msg) continue valid_conditions.append(condition) # 如果所有条件都无效,抛出异常 if not valid_conditions and not choice.is_default: - msg = "分支没有有效条件" - logger.warning(f"[Choice] 分支 {choice.branch_id} 条件处理失败: {msg}") + msg = f"[Choice] 分支 {choice.branch_id} 条件处理失败:没有有效条件" + logger.warning(msg) continue # 更新有效条件 @@ -123,7 +135,8 @@ class Choice(CoreCall, input_model=ChoiceInput, output_model=ChoiceOutput): valid_choices.append(choice) except ValueError as e: - logger.warning("分支 %s 处理失败: %s,已跳过", choice.branch_id, str(e)) + msg = f"[Choice] 分支 {choice.branch_id} 处理失败:{e!s},已跳过" + logger.warning(msg) continue return valid_choices @@ -135,7 +148,7 @@ class Choice(CoreCall, input_model=ChoiceInput, output_model=ChoiceOutput): ) async def _exec( - self, input_data: dict[str, Any] + self, input_data: dict[str, Any], ) -> AsyncGenerator[CallOutputChunk, None]: """执行Choice工具""" # 解析输入数据 diff --git a/apps/scheduler/call/choice/condition_handler.py b/apps/scheduler/call/choice/condition_handler.py index 6f10f2c8..3c1354c9 100644 --- a/apps/scheduler/call/choice/condition_handler.py +++ b/apps/scheduler/call/choice/condition_handler.py @@ -1,25 +1,24 @@ # Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. """处理条件分支的工具""" - import logging +import re from pydantic import BaseModel -from apps.schemas.parameters import ( - Type, - NumberOperate, - StringOperate, - ListOperate, - BoolOperate, - DictOperate, -) - from apps.scheduler.call.choice.schema import ( ChoiceBranch, Condition, Logic, - Value + Value, +) +from apps.schemas.parameters import ( + BoolOperate, + DictOperate, + ListOperate, + NumberOperate, + StringOperate, + Type, ) logger = logging.getLogger(__name__) @@ -27,9 +26,11 @@ logger = logging.getLogger(__name__) class ConditionHandler(BaseModel): """条件分支处理器""" + @staticmethod - async def get_value_type_from_operate(operate: NumberOperate | StringOperate | ListOperate | - BoolOperate | DictOperate) -> Type: + async def get_value_type_from_operate( # noqa: PLR0911 + operate: NumberOperate | StringOperate | ListOperate | BoolOperate | DictOperate | None, + ) -> Type | None: """获取右值的类型""" if isinstance(operate, NumberOperate): return Type.NUMBER @@ -58,7 +59,7 @@ class ConditionHandler(BaseModel): return None @staticmethod - def check_value_type(value: Value, expected_type: Type) -> bool: + def check_value_type(value: Value, expected_type: Type | None) -> bool: """检查值的类型是否符合预期""" if expected_type == Type.STRING and isinstance(value.value, str): return True @@ -68,14 +69,11 @@ class ConditionHandler(BaseModel): return True if expected_type == Type.DICT and isinstance(value.value, dict): return True - if expected_type == Type.BOOL and isinstance(value.value, bool): - return True - return False + return bool(expected_type == Type.BOOL and isinstance(value.value, bool)) @staticmethod def handler(choices: list[ChoiceBranch]) -> str: """处理条件""" - for block_judgement in choices[::-1]: results = [] if block_judgement.is_default: @@ -85,7 +83,8 @@ class ConditionHandler(BaseModel): if result is not None: results.append(result) if not results: - logger.warning(f"[Choice] 分支 {block_judgement.branch_id} 条件处理失败: 没有有效的条件") + err = f"[Choice] 分支 {block_judgement.branch_id} 条件处理失败: 没有有效的条件" + logger.warning(err) continue if block_judgement.logic == Logic.AND: final_result = all(results) @@ -94,7 +93,6 @@ class ConditionHandler(BaseModel): if final_result: return block_judgement.branch_id - return "" @staticmethod @@ -112,27 +110,27 @@ class ConditionHandler(BaseModel): left = condition.left operate = condition.operate right = condition.right - value_type = condition.type + value_type = condition.left.type - result = None - if value_type == Type.STRING: + result = False + if value_type == Type.STRING and isinstance(operate, StringOperate): result = ConditionHandler._judge_string_condition(left, operate, right) - elif value_type == Type.NUMBER: + elif value_type == Type.NUMBER and isinstance(operate, NumberOperate): result = ConditionHandler._judge_number_condition(left, operate, right) - elif value_type == Type.BOOL: + elif value_type == Type.BOOL and isinstance(operate, BoolOperate): result = ConditionHandler._judge_bool_condition(left, operate, right) - elif value_type == Type.LIST: + elif value_type == Type.LIST and isinstance(operate, ListOperate): result = ConditionHandler._judge_list_condition(left, operate, right) - elif value_type == Type.DICT: + elif value_type == Type.DICT and isinstance(operate, DictOperate): result = ConditionHandler._judge_dict_condition(left, operate, right) else: - msg = f"不支持的数据类型: {value_type}" - logger.error(f"[Choice] 条件处理失败: {msg}") - return None + msg = f"[Choice] 条件处理失败: 不支持的数据类型: {value_type}" + logger.error(msg) + return False return result @staticmethod - def _judge_string_condition(left: Value, operate: StringOperate, right: Value) -> bool: + def _judge_string_condition(left: Value, operate: StringOperate, right: Value) -> bool: # noqa: C901, PLR0911, PLR0912 """ 判断字符串类型的条件。 @@ -149,33 +147,37 @@ class ConditionHandler(BaseModel): if not isinstance(left_value, str): msg = f"左值必须是字符串类型 ({left_value})" logger.warning(msg) - return None + return False right_value = right.value + if not isinstance(right_value, str): + msg = f"右值必须是字符串类型 ({right_value})" + logger.warning(msg) + return False + if operate == StringOperate.EQUAL: return left_value == right_value - elif operate == StringOperate.NOT_EQUAL: + if operate == StringOperate.NOT_EQUAL: return left_value != right_value - elif operate == StringOperate.CONTAINS: + if operate == StringOperate.CONTAINS: return right_value in left_value - elif operate == StringOperate.NOT_CONTAINS: + if operate == StringOperate.NOT_CONTAINS: return right_value not in left_value - elif operate == StringOperate.STARTS_WITH: + if operate == StringOperate.STARTS_WITH: return left_value.startswith(right_value) - elif operate == StringOperate.ENDS_WITH: + if operate == StringOperate.ENDS_WITH: return left_value.endswith(right_value) - elif operate == StringOperate.REGEX_MATCH: - import re + if operate == StringOperate.REGEX_MATCH: return bool(re.match(right_value, left_value)) - elif operate == StringOperate.LENGTH_EQUAL: + if operate == StringOperate.LENGTH_EQUAL: return len(left_value) == right_value - elif operate == StringOperate.LENGTH_GREATER_THAN: - return len(left_value) > right_value - elif operate == StringOperate.LENGTH_GREATER_THAN_OR_EQUAL: - return len(left_value) >= right_value - elif operate == StringOperate.LENGTH_LESS_THAN: - return len(left_value) < right_value - elif operate == StringOperate.LENGTH_LESS_THAN_OR_EQUAL: - return len(left_value) <= right_value + if operate == StringOperate.LENGTH_GREATER_THAN: + return len(left_value) > len(right_value) + if operate == StringOperate.LENGTH_GREATER_THAN_OR_EQUAL: + return len(left_value) >= len(right_value) + if operate == StringOperate.LENGTH_LESS_THAN: + return len(left_value) < len(right_value) + if operate == StringOperate.LENGTH_LESS_THAN_OR_EQUAL: + return len(left_value) <= len(right_value) return False @staticmethod @@ -196,19 +198,24 @@ class ConditionHandler(BaseModel): if not isinstance(left_value, (int, float)): msg = f"左值必须是数字类型 ({left_value})" logger.warning(msg) - return None + return False right_value = right.value + if not isinstance(right_value, (int, float)): + msg = f"右值必须是数字类型 ({right_value})" + logger.warning(msg) + return False + if operate == NumberOperate.EQUAL: return left_value == right_value - elif operate == NumberOperate.NOT_EQUAL: + if operate == NumberOperate.NOT_EQUAL: return left_value != right_value - elif operate == NumberOperate.GREATER_THAN: + if operate == NumberOperate.GREATER_THAN: return left_value > right_value - elif operate == NumberOperate.LESS_THAN: # noqa: PLR2004 + if operate == NumberOperate.LESS_THAN: return left_value < right_value - elif operate == NumberOperate.GREATER_THAN_OR_EQUAL: + if operate == NumberOperate.GREATER_THAN_OR_EQUAL: return left_value >= right_value - elif operate == NumberOperate.LESS_THAN_OR_EQUAL: + if operate == NumberOperate.LESS_THAN_OR_EQUAL: return left_value <= right_value return False @@ -230,20 +237,21 @@ class ConditionHandler(BaseModel): if not isinstance(left_value, bool): msg = "左值必须是布尔类型" logger.warning(msg) - return None + return False right_value = right.value + if not isinstance(right_value, bool): + msg = "右值必须是布尔类型" + logger.warning(msg) + return False + if operate == BoolOperate.EQUAL: return left_value == right_value - elif operate == BoolOperate.NOT_EQUAL: + if operate == BoolOperate.NOT_EQUAL: return left_value != right_value - elif operate == BoolOperate.IS_EMPTY: - return not left_value - elif operate == BoolOperate.NOT_EMPTY: - return left_value return False @staticmethod - def _judge_list_condition(left: Value, operate: ListOperate, right: Value): + def _judge_list_condition(left: Value, operate: ListOperate, right: Value) -> bool: # noqa: C901, PLR0911 """ 判断列表类型的条件。 @@ -260,30 +268,35 @@ class ConditionHandler(BaseModel): if not isinstance(left_value, list): msg = f"左值必须是列表类型 ({left_value})" logger.warning(msg) - return None + return False right_value = right.value + if not isinstance(right_value, list): + msg = f"右值必须是列表类型 ({right_value})" + logger.warning(msg) + return False + if operate == ListOperate.EQUAL: return left_value == right_value - elif operate == ListOperate.NOT_EQUAL: + if operate == ListOperate.NOT_EQUAL: return left_value != right_value - elif operate == ListOperate.CONTAINS: + if operate == ListOperate.CONTAINS: return right_value in left_value - elif operate == ListOperate.NOT_CONTAINS: + if operate == ListOperate.NOT_CONTAINS: return right_value not in left_value - elif operate == ListOperate.LENGTH_EQUAL: + if operate == ListOperate.LENGTH_EQUAL: return len(left_value) == right_value - elif operate == ListOperate.LENGTH_GREATER_THAN: - return len(left_value) > right_value - elif operate == ListOperate.LENGTH_GREATER_THAN_OR_EQUAL: - return len(left_value) >= right_value - elif operate == ListOperate.LENGTH_LESS_THAN: - return len(left_value) < right_value - elif operate == ListOperate.LENGTH_LESS_THAN_OR_EQUAL: - return len(left_value) <= right_value + if operate == ListOperate.LENGTH_GREATER_THAN: + return len(left_value) > len(right_value) + if operate == ListOperate.LENGTH_GREATER_THAN_OR_EQUAL: + return len(left_value) >= len(right_value) + if operate == ListOperate.LENGTH_LESS_THAN: + return len(left_value) < len(right_value) + if operate == ListOperate.LENGTH_LESS_THAN_OR_EQUAL: + return len(left_value) <= len(right_value) return False @staticmethod - def _judge_dict_condition(left: Value, operate: DictOperate, right: Value): + def _judge_dict_condition(left: Value, operate: DictOperate, right: Value) -> bool: # noqa: PLR0911 """ 判断字典类型的条件。 @@ -300,14 +313,19 @@ class ConditionHandler(BaseModel): if not isinstance(left_value, dict): msg = f"左值必须是字典类型 ({left_value})" logger.warning(msg) - return None + return False right_value = right.value + if not isinstance(right_value, dict): + msg = f"右值必须是字典类型 ({right_value})" + logger.warning(msg) + return False + if operate == DictOperate.EQUAL: return left_value == right_value - elif operate == DictOperate.NOT_EQUAL: + if operate == DictOperate.NOT_EQUAL: return left_value != right_value - elif operate == DictOperate.CONTAINS_KEY: + if operate == DictOperate.CONTAINS_KEY: return right_value in left_value - elif operate == DictOperate.NOT_CONTAINS_KEY: + if operate == DictOperate.NOT_CONTAINS_KEY: return right_value not in left_value return False diff --git a/apps/scheduler/call/choice/schema.py b/apps/scheduler/call/choice/schema.py index d97a0c8d..95532270 100644 --- a/apps/scheduler/call/choice/schema.py +++ b/apps/scheduler/call/choice/schema.py @@ -1,20 +1,19 @@ # Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. """Choice Call的输入和输出""" import uuid - from enum import Enum -from pydantic import BaseModel, Field +from pydantic import Field +from apps.scheduler.call.core import DataBase from apps.schemas.parameters import ( - Type, - NumberOperate, - StringOperate, - ListOperate, BoolOperate, DictOperate, + ListOperate, + NumberOperate, + StringOperate, + Type, ) -from apps.scheduler.call.core import DataBase class Logic(str, Enum): -- Gitee From 75f5bf8ffdd2084bbbfa4de4bd83aa0d88959a4d Mon Sep 17 00:00:00 2001 From: zhanghb <2428333123@qq.com> Date: Fri, 8 Aug 2025 17:58:31 +0800 Subject: [PATCH 09/15] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E8=AF=AD=E8=A8=80?= =?UTF-8?q?=E6=9E=9A=E4=B8=BE=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/llm/patterns/core.py | 10 ++-- apps/llm/patterns/executor.py | 26 ++++----- apps/llm/patterns/facts.py | 13 +++-- apps/llm/patterns/rewoo.py | 55 ++++++++++--------- apps/llm/patterns/rewrite.py | 15 ++--- apps/llm/patterns/select.py | 17 +++--- apps/routers/chat.py | 3 +- apps/routers/flow.py | 7 ++- apps/scheduler/call/api/api.py | 19 ++++--- apps/scheduler/call/choice/choice.py | 34 +++++------- apps/scheduler/call/core.py | 12 ++-- apps/scheduler/call/facts/facts.py | 43 +++++++++------ apps/scheduler/call/facts/prompt.py | 17 +++--- apps/scheduler/call/graph/graph.py | 20 +++---- apps/scheduler/call/llm/llm.py | 10 ++-- apps/scheduler/call/llm/prompt.py | 5 +- apps/scheduler/call/mcp/mcp.py | 33 +++++------ apps/scheduler/call/rag/rag.py | 12 ++-- apps/scheduler/call/search/search.py | 7 ++- apps/scheduler/call/slot/prompt.py | 7 ++- apps/scheduler/call/slot/slot.py | 12 ++-- apps/scheduler/call/sql/sql.py | 40 +++++++------- apps/scheduler/call/suggest/prompt.py | 7 ++- apps/scheduler/call/suggest/suggest.py | 12 ++-- apps/scheduler/call/summary/summary.py | 23 ++++---- apps/scheduler/executor/flow.py | 67 +++++++++++++---------- apps/scheduler/mcp/host.py | 14 +++-- apps/scheduler/mcp/plan.py | 7 +-- apps/scheduler/mcp/prompt.py | 21 +++---- apps/scheduler/mcp/select.py | 76 ++++++++++++++++---------- apps/scheduler/pool/loader/flow.py | 12 ++-- apps/scheduler/pool/pool.py | 10 ++-- apps/scheduler/scheduler/scheduler.py | 16 +++--- apps/schemas/enum_var.py | 7 +++ apps/schemas/request_data.py | 4 +- apps/schemas/task.py | 4 +- apps/services/flow.py | 23 +++++--- apps/services/rag.py | 39 +++++++------ 38 files changed, 420 insertions(+), 339 deletions(-) diff --git a/apps/llm/patterns/core.py b/apps/llm/patterns/core.py index 60d814ee..f80c275d 100644 --- a/apps/llm/patterns/core.py +++ b/apps/llm/patterns/core.py @@ -3,15 +3,15 @@ from abc import ABC, abstractmethod from textwrap import dedent -from typing import Union +from apps.schemas.enum_var import LanguageType class CorePattern(ABC): """基础大模型范式抽象类""" - system_prompt: dict[str, str] = {} + system_prompt: dict[LanguageType, str] = {} """系统提示词""" - user_prompt: dict[str, str] = {} + user_prompt: dict[LanguageType, str] = {} """用户提示词""" input_tokens: int = 0 """输入Token数量""" @@ -20,8 +20,8 @@ class CorePattern(ABC): def __init__( self, - system_prompt: dict[str, str] | None = None, - user_prompt: dict[str, str] | None = None, + system_prompt: dict[LanguageType, str] | None = None, + user_prompt: dict[LanguageType, str] | None = None, ) -> None: """ 检查是否已经自定义了Prompt;有的话就用自定义的;同时对Prompt进行空格清除 diff --git a/apps/llm/patterns/executor.py b/apps/llm/patterns/executor.py index 881839ce..30531be6 100644 --- a/apps/llm/patterns/executor.py +++ b/apps/llm/patterns/executor.py @@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Any, Union from apps.llm.patterns.core import CorePattern from apps.llm.reasoning import ReasoningLLM from apps.llm.snippet import convert_context_to_prompt, facts_to_prompt - +from apps.schemas.enum_var import LanguageType if TYPE_CHECKING: from apps.schemas.scheduler import ExecutorBackground @@ -14,8 +14,8 @@ if TYPE_CHECKING: class ExecutorThought(CorePattern): """通过大模型生成Executor的思考内容""" - user_prompt: dict[str, str] = { - "zh_cn": r""" + user_prompt: dict[LanguageType, str] = { + LanguageType.CHINESE: r""" 你是一个可以使用工具的智能助手。 @@ -46,7 +46,7 @@ class ExecutorThought(CorePattern): 请综合以上信息,再次一步一步地进行思考,并给出见解和行动: """, - "en": r""" + LanguageType.ENGLISH: r""" You are an intelligent assistant who can use tools. @@ -82,8 +82,8 @@ class ExecutorThought(CorePattern): def __init__( self, - system_prompt: Union[str, dict[str, str]] | None = None, - user_prompt: Union[str, dict[str, str]] | None = None, + system_prompt: dict[LanguageType, str] | None = None, + user_prompt: dict[LanguageType, str] | None = None, ) -> None: """处理Prompt""" super().__init__(system_prompt, user_prompt) @@ -94,7 +94,7 @@ class ExecutorThought(CorePattern): last_thought: str = kwargs["last_thought"] user_question: str = kwargs["user_question"] tool_info: dict[str, Any] = kwargs["tool_info"] - language: str = kwargs.get("language", "zh_cn") + language: str = kwargs.get("language", LanguageType.CHINESE) except Exception as e: err = "参数不正确!" raise ValueError(err) from e @@ -126,8 +126,8 @@ class ExecutorThought(CorePattern): class ExecutorSummary(CorePattern): """使用大模型进行生成Executor初始背景""" - user_prompt: dict[str, str] = { - "zh_cn": r""" + user_prompt: dict[LanguageType, str] = { + LanguageType.CHINESE: r""" 根据给定的对话记录和关键事实,生成一个三句话背景总结。这个总结将用于后续对话的上下文理解。 @@ -148,7 +148,7 @@ class ExecutorSummary(CorePattern): 现在,请开始生成背景总结: """, - "en": r""" + LanguageType.ENGLISH: r""" Based on the given conversation records and key facts, generate a three-sentence background summary. This summary will be used for context understanding in subsequent conversations. @@ -174,8 +174,8 @@ class ExecutorSummary(CorePattern): def __init__( self, - system_prompt: dict[str, str] | None = None, - user_prompt: dict[str, str] | None = None, + system_prompt: dict[LanguageType, str] | None = None, + user_prompt: dict[LanguageType, str] | None = None, ) -> None: """初始化Background模式""" super().__init__(system_prompt, user_prompt) @@ -185,7 +185,7 @@ class ExecutorSummary(CorePattern): background: ExecutorBackground = kwargs["background"] conversation_str = convert_context_to_prompt(background.conversation) facts_str = facts_to_prompt(background.facts) - language = kwargs.get("language", "zh_cn") + language = kwargs.get("language", LanguageType.CHINESE) messages = [ {"role": "system", "content": "You are a helpful assistant."}, diff --git a/apps/llm/patterns/facts.py b/apps/llm/patterns/facts.py index 9fc4bced..ac833f51 100644 --- a/apps/llm/patterns/facts.py +++ b/apps/llm/patterns/facts.py @@ -9,6 +9,7 @@ from apps.llm.function import JsonGenerator from apps.llm.patterns.core import CorePattern from apps.llm.reasoning import ReasoningLLM from apps.llm.snippet import convert_context_to_prompt +from apps.schemas.enum_var import LanguageType logger = logging.getLogger(__name__) @@ -25,8 +26,8 @@ class Facts(CorePattern): system_prompt: str = "You are a helpful assistant." """系统提示词(暂不使用)""" - user_prompt: dict[str, str] = { - "zh_cn": r""" + user_prompt: dict[LanguageType, str] = { + LanguageType.CHINESE: r""" 从对话中提取关键信息,并将它们组织成独一无二的、易于理解的事实,包含用户偏好、关系、实体等有用信息。 @@ -65,7 +66,7 @@ class Facts(CorePattern): {conversation} """, - "en": r""" + LanguageType.ENGLISH: r""" Extract key information from the conversation and organize it into unique, easily understandable facts that include user preferences, relationships, entities, etc. @@ -109,8 +110,8 @@ class Facts(CorePattern): def __init__( self, - system_prompt: dict[str, str] | None = None, - user_prompt: dict[str, str] | None = None, + system_prompt: dict[LanguageType, str] | None = None, + user_prompt: dict[LanguageType, str] | None = None, ) -> None: """初始化Prompt""" super().__init__(system_prompt, user_prompt) @@ -118,7 +119,7 @@ class Facts(CorePattern): async def generate(self, **kwargs) -> list[str]: # noqa: ANN003 """事实提取""" conversation = convert_context_to_prompt(kwargs["conversation"]) - language = kwargs.get("language", "zh_cn") + language = kwargs.get("language", LanguageType.CHINESE) messages = [ {"role": "system", "content": self.system_prompt}, diff --git a/apps/llm/patterns/rewoo.py b/apps/llm/patterns/rewoo.py index da1c48b7..ec4c3d39 100644 --- a/apps/llm/patterns/rewoo.py +++ b/apps/llm/patterns/rewoo.py @@ -3,14 +3,15 @@ from apps.llm.patterns.core import CorePattern from apps.llm.reasoning import ReasoningLLM +from apps.schemas.enum_var import LanguageType from typing import Union class InitPlan(CorePattern): """规划生成命令行""" - system_prompt: dict[str, str] = { - "zh_cn": r""" + system_prompt: dict[LanguageType, str] = { + LanguageType.CHINESE: r""" 你是一个计划生成器。对于给定的目标,**制定一个简单的计划**,该计划可以逐步生成合适的命令行参数和标志。 你会收到一个"命令前缀",这是已经确定和生成的命令部分。你需要基于这个前缀使用标志和参数来完成命令。 @@ -44,7 +45,7 @@ class InitPlan(CorePattern): 让我们开始! """, - "en": r""" + LanguageType.ENGLISH: r""" You are a plan generator. For a given goal, **draft a simple plan** that can step-by-step generate the \ appropriate command line arguments and flags. @@ -89,8 +90,8 @@ class InitPlan(CorePattern): } """系统提示词""" - user_prompt: dict[str, str] = { - "zh_cn": r""" + user_prompt: dict[LanguageType, str] = { + LanguageType.CHINESE: r""" 任务:{instruction} 前缀:`{binary_name} {subcmd_name}` 用法:`{subcmd_usage}`。这是一个Python模板字符串。OPTS是所有标志的占位符。参数必须是 {argument_list} 其中之一。 @@ -99,7 +100,7 @@ class InitPlan(CorePattern): 请生成相应的计划。 """, - "en": r""" + LanguageType.ENGLISH: r""" Task: {instruction} Prefix: `{binary_name} {subcmd_name}` Usage: `{subcmd_usage}`. This is a Python template string. OPTS is a placeholder for all flags. The arguments \ @@ -114,8 +115,8 @@ class InitPlan(CorePattern): def __init__( self, - system_prompt: dict[str, str] | None = None, - user_prompt: dict[str, str] | None = None, + system_prompt: dict[LanguageType, str] | None = None, + user_prompt: dict[LanguageType, str] | None = None, ) -> None: """处理Prompt""" super().__init__(system_prompt, user_prompt) @@ -125,7 +126,7 @@ class InitPlan(CorePattern): spec = kwargs["spec"] binary_name = kwargs["binary_name"] subcmd_name = kwargs["subcmd_name"] - language = kwargs.get("language", "zh_cn") + language = kwargs.get("language", LanguageType.CHINESE) binary_description = spec[binary_name][0] subcmd_usage = spec[binary_name][2][subcmd_name][1] subcmd_description = spec[binary_name][2][subcmd_name][0] @@ -163,8 +164,8 @@ class InitPlan(CorePattern): class PlanEvaluator(CorePattern): """计划评估器""" - system_prompt: dict[str, str] = { - "zh_cn": r""" + system_prompt: dict[LanguageType, str] = { + LanguageType.CHINESE: r""" 你是一个计划评估器。你的任务是评估给定的计划是否合理和完整。 一个好的计划应该: @@ -182,7 +183,7 @@ class PlanEvaluator(CorePattern): "VALID" - 如果计划良好且完整 "INVALID: <原因>" - 如果计划有问题,请解释原因 """, - "en": r""" + LanguageType.ENGLISH: r""" You are a plan evaluator. Your task is to evaluate whether the given plan is reasonable and complete. A good plan should: @@ -203,14 +204,14 @@ class PlanEvaluator(CorePattern): } """系统提示词""" - user_prompt: dict[str, str] = { - "zh_cn": r""" + user_prompt: dict[LanguageType, str] = { + LanguageType.CHINESE: r""" 任务:{instruction} 计划:{plan} 评估计划并回复"VALID"或"INVALID: <原因>"。 """, - "en": r""" + LanguageType.ENGLISH: r""" Task: {instruction} Plan: {plan} @@ -221,15 +222,15 @@ class PlanEvaluator(CorePattern): def __init__( self, - system_prompt: dict[str, str] | None = None, - user_prompt: dict[str, str] | None = None, + system_prompt: dict[LanguageType, str] | None = None, + user_prompt: dict[LanguageType, str] | None = None, ) -> None: """初始化Prompt""" super().__init__(system_prompt, user_prompt) async def generate(self, **kwargs) -> str: """生成计划评估结果""" - language = kwargs.get("language", "zh_cn") + language = kwargs.get("language", LanguageType.CHINESE) messages = [ {"role": "system", "content": self.system_prompt[language]}, { @@ -254,8 +255,8 @@ class PlanEvaluator(CorePattern): class RePlanner(CorePattern): """重新规划器""" - system_prompt: dict[str, str] = { - "zh_cn": r""" + system_prompt: dict[LanguageType, str] = { + LanguageType.CHINESE: r""" 你是一个计划重新规划器。当计划被评估为无效时,你需要生成一个新的、改进的计划。 新计划应该: @@ -270,7 +271,7 @@ class RePlanner(CorePattern): - 保持步骤简洁和重点突出 - 以"Final"步骤结束 """, - "en": r""" + LanguageType.ENGLISH: r""" You are a plan replanner. When the plan is evaluated as invalid, you need to generate a new, improved plan. The new plan should: @@ -288,15 +289,15 @@ class RePlanner(CorePattern): } """系统提示词""" - user_prompt: dict[str, str] = { - "zh_cn": r""" + user_prompt: dict[LanguageType, str] = { + LanguageType.CHINESE: r""" 任务:{instruction} 原始计划:{plan} 评估:{evaluation} 生成一个新的、改进的计划,解决评估中提到的所有问题。 """, - "en": r""" + LanguageType.ENGLISH: r""" Task: {instruction} Original Plan: {plan} Evaluation: {evaluation} @@ -308,15 +309,15 @@ class RePlanner(CorePattern): def __init__( self, - system_prompt: dict[str, str] | None = None, - user_prompt: dict[str, str] | None = None, + system_prompt: dict[LanguageType, str] | None = None, + user_prompt: dict[LanguageType, str] | None = None, ) -> None: """初始化Prompt""" super().__init__(system_prompt, user_prompt) async def generate(self, **kwargs) -> str: """生成重新规划结果""" - language = kwargs.get("language", "zh_cn") + language = kwargs.get("language", LanguageType.CHINESE) messages = [ {"role": "system", "content": self.system_prompt[language]}, { diff --git a/apps/llm/patterns/rewrite.py b/apps/llm/patterns/rewrite.py index 20de656a..6cf36e47 100644 --- a/apps/llm/patterns/rewrite.py +++ b/apps/llm/patterns/rewrite.py @@ -10,6 +10,7 @@ from apps.llm.function import JsonGenerator from apps.llm.patterns.core import CorePattern from apps.llm.reasoning import ReasoningLLM from apps.llm.token import TokenCalculator +from apps.schemas.enum_var import LanguageType logger = logging.getLogger(__name__) @@ -22,8 +23,8 @@ class QuestionRewriteResult(BaseModel): class QuestionRewrite(CorePattern): """问题补全与重写""" - system_prompt: dict[str, str] = { - "zh_cn": r""" + system_prompt: dict[LanguageType, str] = { + LanguageType.CHINESE: r""" 根据历史对话,推断用户的实际意图并补全用户的提问内容,历史对话被包含在标签中,用户意图被包含在标签中。 @@ -75,7 +76,7 @@ class QuestionRewrite(CorePattern): {question} """, - "en": r""" + LanguageType.ENGLISH: r""" Based on the historical dialogue, infer the user's actual intent and complete the user's question. The historical dialogue is contained within the tags, and the user's intent is contained within the tags. @@ -130,13 +131,13 @@ class QuestionRewrite(CorePattern): } """用户提示词""" - user_prompt: dict[str, str] = { - "zh_cn": r""" + user_prompt: dict[LanguageType, str] = { + LanguageType.CHINESE: r""" 请输出补全后的问题 """, - "en": r""" + LanguageType.ENGLISH: r""" Please output the completed question @@ -147,7 +148,7 @@ class QuestionRewrite(CorePattern): history = kwargs.get("history", []) question = kwargs["question"] llm = kwargs.get("llm", None) - language = kwargs.get("language", "zh_cn") + language = kwargs.get("language", LanguageType.CHINESE) if not llm: llm = ReasoningLLM() leave_tokens = llm._config.max_tokens diff --git a/apps/llm/patterns/select.py b/apps/llm/patterns/select.py index 197f4895..1aaa10df 100644 --- a/apps/llm/patterns/select.py +++ b/apps/llm/patterns/select.py @@ -11,6 +11,7 @@ from apps.llm.function import JsonGenerator from apps.llm.patterns.core import CorePattern from apps.llm.reasoning import ReasoningLLM from apps.llm.snippet import choices_to_prompt +from apps.schemas.enum_var import LanguageType logger = logging.getLogger(__name__) @@ -18,14 +19,14 @@ logger = logging.getLogger(__name__) class Select(CorePattern): """通过投票选择最佳答案""" - system_prompt: dict[str, str] = { - "zh_cn": "你是一个有用的助手。", - "en": "You are a helpful assistant.", + system_prompt: dict[LanguageType, str] = { + LanguageType.CHINESE: "你是一个有用的助手。", + LanguageType.ENGLISH: "You are a helpful assistant.", } """系统提示词""" - user_prompt: dict[str, str] = { - "zh_cn": r""" + user_prompt: dict[LanguageType, str] = { + LanguageType.CHINESE: r""" 根据历史对话(包括工具调用结果)和用户问题,从给出的选项列表中,选出最符合要求的那一项。 @@ -76,7 +77,7 @@ class Select(CorePattern): 让我们一步一步思考。 """, - "en": r""" + LanguageType.ENGLISH: r""" Based on the historical dialogue (including tool call results) and user question, select the most \ @@ -148,7 +149,7 @@ class Select(CorePattern): def __init__( self, - system_prompt: dict[str, str] | None = None, + system_prompt: dict[LanguageType, str] | None = None, user_prompt: dict[str, str] | None = None, ) -> None: """处理Prompt""" @@ -189,7 +190,7 @@ class Select(CorePattern): result_list = [] background = kwargs.get("background", "无背景信息。") - self.language = kwargs.get("language", "zh_cn") + self.language = kwargs.get("language", LanguageType.CHINESE) data_str = json.dumps(kwargs.get("data", {}), ensure_ascii=False) choice_prompt, choices_list = choices_to_prompt(kwargs["choices"]) diff --git a/apps/routers/chat.py b/apps/routers/chat.py index e6ba854c..a2470928 100644 --- a/apps/routers/chat.py +++ b/apps/routers/chat.py @@ -18,6 +18,7 @@ from apps.scheduler.scheduler import Scheduler from apps.scheduler.scheduler.context import save_data from apps.schemas.request_data import RequestData from apps.schemas.response_data import ResponseData +from apps.schemas.enum_var import LanguageType from apps.schemas.task import Task from apps.services.activity import Activity from apps.services.blacklist import QuestionBlacklistManager, UserBlacklistManager @@ -138,7 +139,7 @@ async def chat( session_id: Annotated[str, Depends(get_session)], ) -> StreamingResponse: """LLM流式对话接口""" - post_body.language = "zh_cn" if post_body.language in {"zh_cn", "zh"} else "en" + post_body.language = LanguageType.CHINESE if post_body.language in {"zh", LanguageType.CHINESE} else LanguageType.ENGLISH # 前端 Flow-Debug 传输为“zh" # 问题黑名单检测 if not await QuestionBlacklistManager.check_blacklisted_questions(input_question=post_body.question): # 用户扣分 diff --git a/apps/routers/flow.py b/apps/routers/flow.py index c054f459..ff863a21 100644 --- a/apps/routers/flow.py +++ b/apps/routers/flow.py @@ -21,6 +21,7 @@ from apps.schemas.response_data import ( NodeServiceListRsp, ResponseData, ) +from apps.schemas.enum_var import LanguageType from apps.services.appcenter import AppCenterManager from apps.services.application import AppManager from apps.services.flow import FlowManager @@ -51,7 +52,7 @@ get_services, get_flow, put_flow 三个函数需要前端传入 language 参数 ) async def get_services( user_sub: Annotated[str, Depends(get_user)], - language: str = Query("zh_cn", description="语言参数,默认为中文") + language: str = Query(LanguageType.CHINESE, description="语言参数,默认为中文") ) -> NodeServiceListRsp: """获取用户可访问的节点元数据所在服务的信息""" services = await FlowManager.get_service_by_user_id(user_sub, language) @@ -81,7 +82,7 @@ async def get_flow( user_sub: Annotated[str, Depends(get_user)], app_id: Annotated[str, Query(alias="appId")], flow_id: Annotated[str, Query(alias="flowId")], - language: str = Query("zh_cn", description="语言参数,默认为中文") + language: str = Query(LanguageType.CHINESE, description="语言参数,默认为中文") ) -> JSONResponse: """获取流拓扑结构""" if not await AppManager.validate_user_app_access(user_sub, app_id): @@ -128,7 +129,7 @@ async def put_flow( app_id: Annotated[str, Query(alias="appId")], flow_id: Annotated[str, Query(alias="flowId")], put_body: Annotated[PutFlowReq, Body(...)], - language: str = Query("zh_cn", description="语言参数,默认为中文") + language: str = Query(LanguageType.CHINESE, description="语言参数,默认为中文") ) -> JSONResponse: """修改流拓扑结构""" if not await AppManager.validate_app_belong_to_user(user_sub, app_id): diff --git a/apps/scheduler/call/api/api.py b/apps/scheduler/call/api/api.py index cde2c268..66aa8386 100644 --- a/apps/scheduler/call/api/api.py +++ b/apps/scheduler/call/api/api.py @@ -15,7 +15,7 @@ from pydantic.json_schema import SkipJsonSchema from apps.common.oidc import oidc_provider from apps.scheduler.call.api.schema import APIInput, APIOutput from apps.scheduler.call.core import CoreCall -from apps.schemas.enum_var import CallOutputType, ContentType, HTTPMethod +from apps.schemas.enum_var import CallOutputType, ContentType, HTTPMethod, LanguageType from apps.schemas.scheduler import ( CallError, CallInfo, @@ -60,11 +60,11 @@ class API(CoreCall, input_model=APIInput, output_model=APIOutput): query: dict[str, Any] = Field(description="已知的部分请求参数", default={}) i18n_info: ClassVar[dict[str, dict]] = { - "zh_cn": { + LanguageType.CHINESE: { "name": "API调用", "description": "向某一个API接口发送HTTP请求,获取数据", }, - "en": { + LanguageType.ENGLISH: { "name": "API Call", "description": "Send an HTTP request to an API to obtain data", }, @@ -78,8 +78,9 @@ class API(CoreCall, input_model=APIInput, output_model=APIOutput): :return: Call的名称和描述 :rtype: CallInfo """ - lang_info = cls.i18n_info.get(cls.language, cls.i18n_info["zh_cn"]) + lang_info = cls.i18n_info.get(cls.language, cls.i18n_info[LanguageType.CHINESE]) return CallInfo(name=lang_info["name"], description=lang_info["description"]) + async def _init(self, call_vars: CallVars) -> APIInput: """初始化API调用工具""" self._service_id = "" @@ -115,8 +116,10 @@ class API(CoreCall, input_model=APIInput, output_model=APIOutput): body=self.body, ) - async def _exec(self, input_data: dict[str, Any], language="zh_cn") -> AsyncGenerator[CallOutputChunk, None]: - """调用API,然后返回LLM解析后的数据""" # !!!!! + async def _exec( + self, input_data: dict[str, Any], language: LanguageType = LanguageType.CHINESE + ) -> AsyncGenerator[CallOutputChunk, None]: + """调用API,然后返回LLM解析后的数据""" self._client = httpx.AsyncClient(timeout=self.timeout) input_obj = APIInput.model_validate(input_data) try: @@ -128,7 +131,9 @@ class API(CoreCall, input_model=APIInput, output_model=APIOutput): finally: await self._client.aclose() - async def _make_api_call(self, data: APIInput, files: dict[str, tuple[str, bytes, str]]) -> httpx.Response: + async def _make_api_call( + self, data: APIInput, files: dict[str, tuple[str, bytes, str]] + ) -> httpx.Response: """组装API请求""" # 获取必要参数 if self._auth: diff --git a/apps/scheduler/call/choice/choice.py b/apps/scheduler/call/choice/choice.py index 7847df52..2c3b363d 100644 --- a/apps/scheduler/call/choice/choice.py +++ b/apps/scheduler/call/choice/choice.py @@ -19,7 +19,7 @@ from apps.scheduler.call.choice.schema import ( ) from apps.schemas.parameters import Type from apps.scheduler.call.core import CoreCall -from apps.schemas.enum_var import CallOutputType +from apps.schemas.enum_var import CallOutputType, LanguageType from apps.schemas.scheduler import ( CallError, CallInfo, @@ -35,19 +35,15 @@ class Choice(CoreCall, input_model=ChoiceInput, output_model=ChoiceOutput): to_user: bool = Field(default=False) choices: list[ChoiceBranch] = Field( - description="分支", - default=[ - ChoiceBranch(), - ChoiceBranch(conditions=[Condition()], is_default=False) - ] + description="分支", default=[ChoiceBranch(), ChoiceBranch(conditions=[Condition()], is_default=False)] ) i18n_info: ClassVar[dict[str, dict]] = { - "zh_cn": { + LanguageType.CHINESE: { "name": "判断", "description": "使用大模型或使用程序做出判断", }, - "en": { + LanguageType.ENGLISH: { "name": "Choice", "description": "Use a large model or a program to make a decision", }, @@ -61,7 +57,7 @@ class Choice(CoreCall, input_model=ChoiceInput, output_model=ChoiceOutput): :return: Call的名称和描述 :rtype: CallInfo """ - lang_info = cls.i18n_info.get(cls.language, cls.i18n_info["zh_cn"]) + lang_info = cls.i18n_info.get(cls.language, cls.i18n_info[LanguageType.CHINESE]) return CallInfo(name=lang_info["name"], description=lang_info["description"]) def _raise_value_error(self, msg: str) -> None: @@ -87,14 +83,14 @@ class Choice(CoreCall, input_model=ChoiceInput, output_model=ChoiceOutput): # 处理左值 if condition.left.step_id is not None: condition.left.value = self._extract_history_variables( - condition.left.step_id+'/'+condition.left.value, call_vars.history) + condition.left.step_id + "/" + condition.left.value, call_vars.history + ) # 检查历史变量是否成功提取 if condition.left.value is None: msg = f"步骤 {condition.left.step_id} 的历史变量不存在" logger.warning(f"[Choice] 分支 {choice.branch_id} 条件处理失败: {msg}") continue - if not ConditionHandler.check_value_type( - condition.left.value, condition.left.type): + if not ConditionHandler.check_value_type(condition.left.value, condition.left.type): msg = f"左值类型不匹配: {condition.left.value} 应为 {condition.left.type.value}" logger.warning(f"[Choice] 分支 {choice.branch_id} 条件处理失败: {msg}") continue @@ -105,21 +101,22 @@ class Choice(CoreCall, input_model=ChoiceInput, output_model=ChoiceOutput): # 处理右值 if condition.right.step_id is not None: condition.right.value = self._extract_history_variables( - condition.right.step_id+'/'+condition.right.value, call_vars.history) + condition.right.step_id + "/" + condition.right.value, call_vars.history + ) # 检查历史变量是否成功提取 if condition.right.value is None: msg = f"步骤 {condition.right.step_id} 的历史变量不存在" logger.warning(f"[Choice] 分支 {choice.branch_id} 条件处理失败: {msg}") continue - if not ConditionHandler.check_value_type( - condition.right.value, condition.right.type): + if not ConditionHandler.check_value_type(condition.right.value, condition.right.type): msg = f"右值类型不匹配: {condition.right.value} 应为 {condition.right.type.value}" logger.warning(f"[Choice] 分支 {choice.branch_id} 条件处理失败: {msg}") continue else: # 如果右值没有step_id,尝试从call_vars中获取 right_value_type = await ConditionHandler.get_value_type_from_operate( - condition.operate) + condition.operate + ) if right_value_type is None: msg = f"不支持的运算符: {condition.operate}" logger.warning(f"[Choice] 分支 {choice.branch_id} 条件处理失败: {msg}") @@ -132,8 +129,7 @@ class Choice(CoreCall, input_model=ChoiceInput, output_model=ChoiceOutput): condition.right.value = str(condition.right.value) else: condition.right.value = ast.literal_eval(condition.right.value) - if not ConditionHandler.check_value_type( - condition.right.value, condition.right.type): + if not ConditionHandler.check_value_type(condition.right.value, condition.right.type): msg = f"右值类型不匹配: {condition.right.value} 应为 {condition.right.type.value}" logger.warning(f"[Choice] 分支 {choice.branch_id} 条件处理失败: {msg}") continue @@ -162,7 +158,7 @@ class Choice(CoreCall, input_model=ChoiceInput, output_model=ChoiceOutput): ) async def _exec( - self, input_data: dict[str, Any], language: str = "zh_cn" + self, input_data: dict[str, Any], language: LanguageType ) -> AsyncGenerator[CallOutputChunk, None]: """执行Choice工具""" # 解析输入数据 diff --git a/apps/scheduler/call/core.py b/apps/scheduler/call/core.py index 42d113a0..16db7215 100644 --- a/apps/scheduler/call/core.py +++ b/apps/scheduler/call/core.py @@ -14,7 +14,7 @@ from pydantic.json_schema import SkipJsonSchema from apps.llm.function import FunctionLLM from apps.llm.reasoning import ReasoningLLM -from apps.schemas.enum_var import CallOutputType +from apps.schemas.enum_var import CallOutputType, LanguageType from apps.schemas.pool import NodePool from apps.schemas.scheduler import ( CallError, @@ -25,6 +25,7 @@ from apps.schemas.scheduler import ( CallVars, ) from apps.schemas.task import FlowStepHistory +from apps.schemas.enum_var import LanguageType if TYPE_CHECKING: from apps.scheduler.executor.step import StepExecutor @@ -70,9 +71,9 @@ class CoreCall(BaseModel): exclude=True, frozen=True, ) - language: ClassVar[SkipJsonSchema[str]] = Field( + language: ClassVar[SkipJsonSchema[LanguageType]] = Field( description="语言", - default="zh_cn", + default=LanguageType.CHINESE, exclude=True, ) i18n_info: ClassVar[SkipJsonSchema[dict[str, dict]]] = {} @@ -192,7 +193,10 @@ class CoreCall(BaseModel): """Call类实例的执行后方法""" async def exec( - self, executor: "StepExecutor", input_data: dict[str, Any], language: str = "zh_cn" + self, + executor: "StepExecutor", + input_data: dict[str, Any], + language: LanguageType = LanguageType.CHINESE, ) -> AsyncGenerator[CallOutputChunk, None]: """Call类实例的执行方法""" async for chunk in self._exec(input_data, language): diff --git a/apps/scheduler/call/facts/facts.py b/apps/scheduler/call/facts/facts.py index eea66a1f..2bf35aba 100644 --- a/apps/scheduler/call/facts/facts.py +++ b/apps/scheduler/call/facts/facts.py @@ -16,7 +16,7 @@ from apps.scheduler.call.facts.schema import ( FactsInput, FactsOutput, ) -from apps.schemas.enum_var import CallOutputType +from apps.schemas.enum_var import CallOutputType, LanguageType from apps.schemas.pool import NodePool from apps.schemas.scheduler import CallInfo, CallOutputChunk, CallVars from apps.services.user_domain import UserDomainManager @@ -31,11 +31,11 @@ class FactsCall(CoreCall, input_model=FactsInput, output_model=FactsOutput): answer: str = Field(description="用户输入") i18n_info: ClassVar[dict[str, dict]] = { - "zh_cn": { + LanguageType.CHINESE: { "name": "提取事实", "description": "从对话上下文和文档片段中提取事实。", }, - "en": { + LanguageType.ENGLISH: { "name": "Fact Extraction", "description": "Extract facts from the conversation context and document snippets.", }, @@ -49,7 +49,7 @@ class FactsCall(CoreCall, input_model=FactsInput, output_model=FactsOutput): :return: Call的名称和描述 :rtype: CallInfo """ - lang_info = cls.i18n_info.get(cls.language, cls.i18n_info["zh_cn"]) + lang_info = cls.i18n_info.get(cls.language, cls.i18n_info[LanguageType.CHINESE]) return CallInfo(name=lang_info["name"], description=lang_info["description"]) @classmethod @@ -79,8 +79,9 @@ class FactsCall(CoreCall, input_model=FactsInput, output_model=FactsOutput): message=message, ) - - async def _exec(self, input_data: dict[str, Any], language: str = "zh_cn") -> AsyncGenerator[CallOutputChunk, None]: + async def _exec( + self, input_data: dict[str, Any], language: LanguageType = LanguageType.CHINESE + ) -> AsyncGenerator[CallOutputChunk, None]: """执行工具""" data = FactsInput(**input_data) # jinja2 环境 @@ -94,18 +95,24 @@ class FactsCall(CoreCall, input_model=FactsInput, output_model=FactsOutput): # 提取事实信息 facts_tpl = env.from_string(FACTS_PROMPT[language]) facts_prompt = facts_tpl.render(conversation=data.message) - facts_obj: FactsGen = await self._json([ - {"role": "system", "content": "You are a helpful assistant."}, - {"role": "user", "content": facts_prompt}, - ], FactsGen) # type: ignore[arg-type] + facts_obj: FactsGen = await self._json( + [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": facts_prompt}, + ], + FactsGen, + ) # type: ignore[arg-type] # 更新用户画像 domain_tpl = env.from_string(DOMAIN_PROMPT[language]) domain_prompt = domain_tpl.render(conversation=data.message) - domain_list: DomainGen = await self._json([ - {"role": "system", "content": "You are a helpful assistant."}, - {"role": "user", "content": domain_prompt}, - ], DomainGen) # type: ignore[arg-type] + domain_list: DomainGen = await self._json( + [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": domain_prompt}, + ], + DomainGen, + ) # type: ignore[arg-type] for domain in domain_list.keywords: await UserDomainManager.update_user_domain_by_user_sub_and_domain_name(data.user_sub, domain) @@ -118,8 +125,12 @@ class FactsCall(CoreCall, input_model=FactsInput, output_model=FactsOutput): ).model_dump(by_alias=True, exclude_none=True), ) - - async def exec(self, executor: "StepExecutor", input_data: dict[str, Any], language: str = "zh_cn") -> AsyncGenerator[CallOutputChunk, None]: + async def exec( + self, + executor: "StepExecutor", + input_data: dict[str, Any], + language: LanguageType = LanguageType.CHINESE, + ) -> AsyncGenerator[CallOutputChunk, None]: """执行工具""" async for chunk in self._exec(input_data, language=language): content = chunk.content diff --git a/apps/scheduler/call/facts/prompt.py b/apps/scheduler/call/facts/prompt.py index a8b7b0be..02e13439 100644 --- a/apps/scheduler/call/facts/prompt.py +++ b/apps/scheduler/call/facts/prompt.py @@ -2,9 +2,9 @@ """记忆提取工具的提示词""" from textwrap import dedent - +from apps.schemas.enum_var import LanguageType DOMAIN_PROMPT: dict[str, str] = { - "zh_cn": dedent( + LanguageType.CHINESE: dedent( r""" @@ -39,7 +39,7 @@ DOMAIN_PROMPT: dict[str, str] = { """ ), - "en": dedent( + LanguageType.ENGLISH: dedent( r""" @@ -76,8 +76,9 @@ DOMAIN_PROMPT: dict[str, str] = { ), } -FACTS_PROMPT: dict[str, str] = {"zh_cn":dedent( - r""" +FACTS_PROMPT: dict[str, str] = { + LanguageType.CHINESE: dedent( + r""" 从对话中提取关键信息,并将它们组织成独一无二的、易于理解的事实,包含用户偏好、关系、实体等有用信息。 @@ -122,8 +123,8 @@ FACTS_PROMPT: dict[str, str] = {"zh_cn":dedent( """ -), - "en": dedent( + ), + LanguageType.ENGLISH: dedent( r""" @@ -169,5 +170,5 @@ FACTS_PROMPT: dict[str, str] = {"zh_cn":dedent( """ - ) + ), } diff --git a/apps/scheduler/call/graph/graph.py b/apps/scheduler/call/graph/graph.py index 557d67f5..6df9969e 100644 --- a/apps/scheduler/call/graph/graph.py +++ b/apps/scheduler/call/graph/graph.py @@ -11,7 +11,7 @@ from pydantic import Field from apps.scheduler.call.core import CoreCall from apps.scheduler.call.graph.schema import RenderFormat, RenderInput, RenderOutput from apps.scheduler.call.graph.style import RenderStyle -from apps.schemas.enum_var import CallOutputType +from apps.schemas.enum_var import CallOutputType, LanguageType from apps.schemas.scheduler import ( CallError, CallInfo, @@ -26,11 +26,11 @@ class Graph(CoreCall, input_model=RenderInput, output_model=RenderOutput): dataset_key: str = Field(description="图表的数据来源(字段名)", default="") i18n_info: ClassVar[dict[str, dict]] = { - "zh_cn": { + LanguageType.CHINESE: { "name": "图表", "description": "将SQL查询出的数据转换为图表。", }, - "en": { + LanguageType.ENGLISH: { "name": "Chart", "description": "Convert the data queried by SQL into a chart.", }, @@ -44,10 +44,9 @@ class Graph(CoreCall, input_model=RenderInput, output_model=RenderOutput): :return: Call的名称和描述 :rtype: CallInfo """ - lang_info = cls.i18n_info.get(cls.language, cls.i18n_info["zh_cn"]) + lang_info = cls.i18n_info.get(cls.language, cls.i18n_info[LanguageType.CHINESE]) return CallInfo(name=lang_info["name"], description=lang_info["description"]) - async def _init(self, call_vars: CallVars) -> RenderInput: """初始化Render Call,校验参数,读取option模板""" try: @@ -70,8 +69,9 @@ class Graph(CoreCall, input_model=RenderInput, output_model=RenderOutput): data=data, ) - - async def _exec(self, input_data: dict[str, Any], language: str = "zh_cn") -> AsyncGenerator[CallOutputChunk, None]: + async def _exec( + self, input_data: dict[str, Any], language: LanguageType = LanguageType.CHINESE + ) -> AsyncGenerator[CallOutputChunk, None]: """运行Render Call""" data = RenderInput(**input_data) @@ -116,7 +116,6 @@ class Graph(CoreCall, input_model=RenderInput, output_model=RenderOutput): ).model_dump(exclude_none=True, by_alias=True), ) - @staticmethod def _separate_key_value(data: list[dict[str, Any]]) -> list[dict[str, Any]]: """ @@ -133,8 +132,9 @@ class Graph(CoreCall, input_model=RenderInput, output_model=RenderOutput): result.append({"type": key, "value": val}) return result - - def _parse_options(self, column_num: int, chart_style: str, additional_style: str, scale_style: str) -> None: + def _parse_options( + self, column_num: int, chart_style: str, additional_style: str, scale_style: str + ) -> None: """解析LLM做出的图表样式选择""" series_template = {} diff --git a/apps/scheduler/call/llm/llm.py b/apps/scheduler/call/llm/llm.py index 70f0f0e4..ce2deeb5 100644 --- a/apps/scheduler/call/llm/llm.py +++ b/apps/scheduler/call/llm/llm.py @@ -15,7 +15,7 @@ from apps.llm.reasoning import ReasoningLLM from apps.scheduler.call.core import CoreCall from apps.scheduler.call.llm.prompt import LLM_CONTEXT_PROMPT, LLM_DEFAULT_PROMPT from apps.scheduler.call.llm.schema import LLMInput, LLMOutput -from apps.schemas.enum_var import CallOutputType +from apps.schemas.enum_var import CallOutputType, LanguageType from apps.schemas.scheduler import ( CallError, CallInfo, @@ -39,11 +39,11 @@ class LLM(CoreCall, input_model=LLMInput, output_model=LLMOutput): user_prompt: str = Field(description="大模型用户提示词", default=LLM_DEFAULT_PROMPT) i18n_info: ClassVar[dict[str, dict]] = { - "zh_cn": { + LanguageType.CHINESE: { "name": "大模型", "description": "以指定的提示词和上下文信息调用大模型,并获得输出。", }, - "en": { + LanguageType.ENGLISH: { "name": "Foundation Model", "description": "Call the foundation model with specified prompt and context, and obtain the output.", }, @@ -57,7 +57,7 @@ class LLM(CoreCall, input_model=LLMInput, output_model=LLMOutput): :return: Call的名称和描述 :rtype: CallInfo """ - lang_info = cls.i18n_info.get(cls.language, cls.i18n_info["zh_cn"]) + lang_info = cls.i18n_info.get(cls.language, cls.i18n_info[LanguageType.CHINESE]) return CallInfo(name=lang_info["name"], description=lang_info["description"]) async def _prepare_message(self, call_vars: CallVars) -> list[dict[str, Any]]: @@ -115,7 +115,7 @@ class LLM(CoreCall, input_model=LLMInput, output_model=LLMOutput): ) async def _exec( - self, input_data: dict[str, Any], language: str = "zh_cn" + self, input_data: dict[str, Any], language: LanguageType = LanguageType.CHINESE ) -> AsyncGenerator[CallOutputChunk, None]: """运行LLM Call""" data = LLMInput(**input_data) diff --git a/apps/scheduler/call/llm/prompt.py b/apps/scheduler/call/llm/prompt.py index 277fe189..b03891db 100644 --- a/apps/scheduler/call/llm/prompt.py +++ b/apps/scheduler/call/llm/prompt.py @@ -2,6 +2,7 @@ """大模型工具的提示词""" from textwrap import dedent +from apps.schemas.enum_var import LanguageType LLM_CONTEXT_PROMPT = dedent( # r""" @@ -79,7 +80,7 @@ LLM_DEFAULT_PROMPT = dedent( ).strip("\n") LLM_ERROR_PROMPT = { - "zh_cn": dedent( + LanguageType.CHINESE: dedent( r""" 你是一位智能助手,能够根据用户的问题,使用Python工具获取信息,并作出回答。你在使用工具解决回答用户的问题时,发生了错误。 @@ -105,7 +106,7 @@ LLM_ERROR_PROMPT = { 现在,输出你的回答: """ ).strip("\n"), - "en": dedent( + LanguageType.ENGLISH: dedent( r""" You are an intelligent assistant. When using Python tools to answer user questions, an error occurred. diff --git a/apps/scheduler/call/mcp/mcp.py b/apps/scheduler/call/mcp/mcp.py index 9e8ffd22..2fa8ee85 100644 --- a/apps/scheduler/call/mcp/mcp.py +++ b/apps/scheduler/call/mcp/mcp.py @@ -16,7 +16,7 @@ from apps.scheduler.call.mcp.schema import ( MCPOutput, ) from apps.scheduler.mcp import MCPHost, MCPPlanner, MCPSelector -from apps.schemas.enum_var import CallOutputType +from apps.schemas.enum_var import CallOutputType, LanguageType from apps.schemas.mcp import MCPPlanItem from apps.schemas.scheduler import ( CallInfo, @@ -26,28 +26,29 @@ from apps.schemas.scheduler import ( logger = logging.getLogger(__name__) -MCP_GENERATE: dict[str, dict[str, str]] = { +MCP_GENERATE: dict[str, dict[LanguageType, str]] = { "START": { - "zh_cn": "[MCP] 开始生成计划...\n\n\n\n", - "en": "[MCP] Start generating plan...\n\n\n\n", + LanguageType.CHINESE: "[MCP] 开始生成计划...\n\n\n\n", + LanguageType.ENGLISH: "[MCP] Start generating plan...\n\n\n\n", }, "END": { - "zh_cn": "[MCP] 计划生成完成:\n\n{plan_str}\n\n\n\n", - "en": "[MCP] Plan generation completed: \n\n{plan_str}\n\n\n\n", + LanguageType.CHINESE: "[MCP] 计划生成完成:\n\n{plan_str}\n\n\n\n", + LanguageType.ENGLISH: "[MCP] Plan generation completed: \n\n{plan_str}\n\n\n\n", }, } -MCP_SUMMARY: dict[str, dict[str, str]] = { +MCP_SUMMARY: dict[str, dict[LanguageType, str]] = { "START": { - "zh_cn": "[MCP] 正在总结任务结果...\n\n", - "en": "[MCP] Start summarizing task results...\n\n", + LanguageType.CHINESE: "[MCP] 正在总结任务结果...\n\n", + LanguageType.ENGLISH: "[MCP] Start summarizing task results...\n\n", }, "END": { - "zh_cn": "[MCP] 任务完成\n\n---\n\n{answer}\n\n", - "en": "[MCP] Task summary completed\n\n{answer}\n\n", + LanguageType.CHINESE: "[MCP] 任务完成\n\n---\n\n{answer}\n\n", + LanguageType.ENGLISH: "[MCP] Task summary completed\n\n{answer}\n\n", }, } + class MCP(CoreCall, input_model=MCPInput, output_model=MCPOutput): """MCP工具""" @@ -57,11 +58,11 @@ class MCP(CoreCall, input_model=MCPInput, output_model=MCPOutput): to_user: bool = Field(description="是否将结果返回给用户", default=True) i18n_info: ClassVar[dict[str, dict]] = { - "zh_cn": { + LanguageType.CHINESE: { "name": "MCP", "description": "调用MCP Server,执行工具", }, - "en": { + LanguageType.ENGLISH: { "name": "MCP", "description": "Call the MCP Server to execute tools", }, @@ -75,7 +76,7 @@ class MCP(CoreCall, input_model=MCPInput, output_model=MCPOutput): :return: Call的名称和描述 :rtype: CallInfo """ - lang_info = cls.i18n_info.get(cls.language, cls.i18n_info["zh_cn"]) + lang_info = cls.i18n_info.get(cls.language, cls.i18n_info[LanguageType.CHINESE]) return CallInfo(name=lang_info["name"], description=lang_info["description"]) async def _init(self, call_vars: CallVars) -> MCPInput: @@ -97,7 +98,7 @@ class MCP(CoreCall, input_model=MCPInput, output_model=MCPOutput): return MCPInput(avaliable_tools=avaliable_tools, max_steps=self.max_steps) async def _exec( - self, input_data: dict[str, Any], language: str = "zh_cn" + self, input_data: dict[str, Any], language: LanguageType = LanguageType.CHINESE ) -> AsyncGenerator[CallOutputChunk, None]: """执行MCP""" # 生成计划 @@ -137,7 +138,7 @@ class MCP(CoreCall, input_model=MCPInput, output_model=MCPOutput): ) async def _execute_plan_item( - self, plan_item: MCPPlanItem, language: str = "zh_cn" + self, plan_item: MCPPlanItem, language: LanguageType = LanguageType.CHINESE ) -> AsyncGenerator[CallOutputChunk, None]: """执行单个计划项""" # 判断是否为Final diff --git a/apps/scheduler/call/rag/rag.py b/apps/scheduler/call/rag/rag.py index e321ac96..f1f1bf76 100644 --- a/apps/scheduler/call/rag/rag.py +++ b/apps/scheduler/call/rag/rag.py @@ -13,7 +13,7 @@ from apps.common.config import Config from apps.llm.patterns.rewrite import QuestionRewrite from apps.scheduler.call.core import CoreCall from apps.scheduler.call.rag.schema import RAGInput, RAGOutput, SearchMethod -from apps.schemas.enum_var import CallOutputType +from apps.schemas.enum_var import CallOutputType, LanguageType from apps.schemas.scheduler import ( CallError, CallInfo, @@ -38,11 +38,11 @@ class RAG(CoreCall, input_model=RAGInput, output_model=RAGOutput): tokens_limit: int = Field(description="token限制", default=8192) i18n_info: ClassVar[dict[str, dict]] = { - "zh_cn": { + LanguageType.CHINESE: { "name": "知识库", "description": "查询知识库,从文档中获取必要信息", }, - "en": { + LanguageType.ENGLISH: { "name": "Knowledge Base", "description": "Query the knowledge base and obtain necessary information from documents", }, @@ -56,7 +56,7 @@ class RAG(CoreCall, input_model=RAGInput, output_model=RAGOutput): :return: Call的名称和描述 :rtype: CallInfo """ - lang_info = cls.i18n_info.get(cls.language, cls.i18n_info["zh_cn"]) + lang_info = cls.i18n_info.get(cls.language, cls.i18n_info[LanguageType.CHINESE]) return CallInfo(name=lang_info["name"], description=lang_info["description"]) async def _init(self, call_vars: CallVars) -> RAGInput: @@ -75,7 +75,9 @@ class RAG(CoreCall, input_model=RAGInput, output_model=RAGOutput): tokensLimit=self.tokens_limit, ) - async def _exec(self, input_data: dict[str, Any], language:str = "zh_cn") -> AsyncGenerator[CallOutputChunk, None]: + async def _exec( + self, input_data: dict[str, Any], language: LanguageType = LanguageType.CHINESE + ) -> AsyncGenerator[CallOutputChunk, None]: """调用RAG工具""" data = RAGInput(**input_data) question_obj = QuestionRewrite() diff --git a/apps/scheduler/call/search/search.py b/apps/scheduler/call/search/search.py index ead0ed9d..69b5d667 100644 --- a/apps/scheduler/call/search/search.py +++ b/apps/scheduler/call/search/search.py @@ -5,6 +5,7 @@ from typing import Any, ClassVar from apps.scheduler.call.core import CoreCall from apps.scheduler.call.search.schema import SearchInput, SearchOutput +from apps.schemas.enum_var import LanguageType from apps.schemas.scheduler import ( CallError, CallInfo, @@ -17,11 +18,11 @@ class Search(CoreCall, input_model=SearchInput, output_model=SearchOutput): """搜索工具""" i18n_info: ClassVar[dict[str, dict]] = { - "zh_cn": { + LanguageType.CHINESE: { "name": "搜索", "description": "获取搜索引擎的结果。", }, - "en": { + LanguageType.ENGLISH: { "name": "Search", "description": "Get the results of the search engine.", }, @@ -35,7 +36,7 @@ class Search(CoreCall, input_model=SearchInput, output_model=SearchOutput): :return: Call的名称和描述 :rtype: CallInfo """ - lang_info = cls.i18n_info.get(cls.language, cls.i18n_info["zh_cn"]) + lang_info = cls.i18n_info.get(cls.language, cls.i18n_info[LanguageType.CHINESE]) return CallInfo(name=lang_info["name"], description=lang_info["description"]) async def _init(self, call_vars: CallVars) -> SearchInput: diff --git a/apps/scheduler/call/slot/prompt.py b/apps/scheduler/call/slot/prompt.py index e5e7b97b..dbb97ebf 100644 --- a/apps/scheduler/call/slot/prompt.py +++ b/apps/scheduler/call/slot/prompt.py @@ -1,8 +1,9 @@ # Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. """自动参数填充工具的提示词""" +from apps.schemas.enum_var import LanguageType -SLOT_GEN_PROMPT:dict[str, str] = { - "zh_cn": r""" +SLOT_GEN_PROMPT:dict[LanguageType, str] = { + LanguageType.CHINESE: r""" 你是一个可以使用工具的AI助手,正尝试使用工具来完成任务。 目前,你正在生成一个JSON参数对象,以作为调用工具的输入。 @@ -88,7 +89,7 @@ SLOT_GEN_PROMPT:dict[str, str] = { """, - "en": r""" + LanguageType.ENGLISH: r""" You are an AI assistant capable of using tools to complete tasks. Currently, you are generating a JSON parameter object as input for calling a tool. diff --git a/apps/scheduler/call/slot/slot.py b/apps/scheduler/call/slot/slot.py index b32eda10..6332ebe5 100644 --- a/apps/scheduler/call/slot/slot.py +++ b/apps/scheduler/call/slot/slot.py @@ -15,7 +15,7 @@ from apps.scheduler.call.core import CoreCall from apps.scheduler.call.slot.prompt import SLOT_GEN_PROMPT from apps.scheduler.call.slot.schema import SlotInput, SlotOutput from apps.scheduler.slot.slot import Slot as SlotProcessor -from apps.schemas.enum_var import CallOutputType +from apps.schemas.enum_var import CallOutputType, LanguageType from apps.schemas.pool import NodePool from apps.schemas.scheduler import CallInfo, CallOutputChunk, CallVars @@ -33,11 +33,11 @@ class Slot(CoreCall, input_model=SlotInput, output_model=SlotOutput): step_num: int = Field(description="历史步骤数", default=1) i18n_info: ClassVar[dict[str, dict]] = { - "zh_cn": { + LanguageType.CHINESE: { "name": "参数自动填充", "description": "根据步骤历史,自动填充参数", }, - "en": { + LanguageType.ENGLISH: { "name": "Parameter Auto-Fill", "description": "Auto-fill parameters based on step history.", }, @@ -51,11 +51,11 @@ class Slot(CoreCall, input_model=SlotInput, output_model=SlotOutput): :return: Call的名称和描述 :rtype: CallInfo """ - lang_info = cls.i18n_info.get(cls.language, cls.i18n_info["zh_cn"]) + lang_info = cls.i18n_info.get(cls.language, cls.i18n_info[LanguageType.CHINESE]) return CallInfo(name=lang_info["name"], description=lang_info["description"]) async def _llm_slot_fill( - self, remaining_schema: dict[str, Any], language: str = "zh_cn" + self, remaining_schema: dict[str, Any], language: LanguageType = LanguageType.CHINESE ) -> tuple[str, dict[str, Any]]: """使用大模型填充参数;若大模型解析度足够,则直接返回结果""" env = SandboxedEnvironment( @@ -146,7 +146,7 @@ class Slot(CoreCall, input_model=SlotInput, output_model=SlotOutput): ) async def _exec( - self, input_data: dict[str, Any], language: str = "zh_cn" + self, input_data: dict[str, Any], language: LanguageType = LanguageType.CHINESE ) -> AsyncGenerator[CallOutputChunk, None]: """执行参数填充""" data = SlotInput(**input_data) diff --git a/apps/scheduler/call/sql/sql.py b/apps/scheduler/call/sql/sql.py index 4180bc96..357a5899 100644 --- a/apps/scheduler/call/sql/sql.py +++ b/apps/scheduler/call/sql/sql.py @@ -12,7 +12,7 @@ from pydantic import Field from apps.common.config import Config from apps.scheduler.call.core import CoreCall from apps.scheduler.call.sql.schema import SQLInput, SQLOutput -from apps.schemas.enum_var import CallOutputType +from apps.schemas.enum_var import CallOutputType, LanguageType from apps.schemas.scheduler import ( CallError, CallInfo, @@ -22,6 +22,17 @@ from apps.schemas.scheduler import ( logger = logging.getLogger(__name__) +MESSAGE = { + "invaild": { + LanguageType.CHINESE: "SQL查询错误:无法生成有效的SQL语句!", + LanguageType.ENGLISH: "SQL query error: Unable to generate valid SQL statements!", + }, + "fail": { + LanguageType.CHINESE: "SQL查询错误:SQL语句执行失败!", + LanguageType.ENGLISH: "SQL query error: SQL statement execution failed!", + }, +} + class SQL(CoreCall, input_model=SQLInput, output_model=SQLOutput): """SQL工具。用于调用外置的Chat2DB工具的API,获得SQL语句;再在PostgreSQL中执行SQL语句,获得数据。""" @@ -32,11 +43,11 @@ class SQL(CoreCall, input_model=SQLInput, output_model=SQLOutput): use_llm_enhancements: bool = Field(description="是否使用大模型增强", default=False) i18n_info: ClassVar[dict[str, dict]] = { - "zh_cn": { + LanguageType.CHINESE: { "name": "SQL查询", "description": "使用大模型生成SQL语句,用于查询数据库中的结构化数据", }, - "en": { + LanguageType.ENGLISH: { "name": "SQL Query", "description": "Use the foundation model to generate SQL statements to query structured data in the databases", }, @@ -50,10 +61,10 @@ class SQL(CoreCall, input_model=SQLInput, output_model=SQLOutput): :return: Call的名称和描述 :rtype: CallInfo """ - lang_info = cls.i18n_info.get(cls.language, cls.i18n_info["zh_cn"]) + lang_info = cls.i18n_info.get(cls.language, cls.i18n_info[LanguageType.CHINESE]) return CallInfo(name=lang_info["name"], description=lang_info["description"]) - async def _init(self, call_vars: CallVars, language: str = "zh_cn") -> SQLInput: + async def _init(self, call_vars: CallVars, language: LanguageType = LanguageType.CHINESE) -> SQLInput: """初始化SQL工具。""" return SQLInput( question=call_vars.question, @@ -127,25 +138,16 @@ class SQL(CoreCall, input_model=SQLInput, output_model=SQLOutput): return None, None async def _exec( - self, input_data: dict[str, Any], language: str = "zh_cn" + self, input_data: dict[str, Any], language: LanguageType = LanguageType.CHINESE ) -> AsyncGenerator[CallOutputChunk, None]: - """运行SQL工具""" + """运行SQL工具""" data = SQLInput(**input_data) - message = { - "invaild": { - "zh_cn": "SQL查询错误:无法生成有效的SQL语句!", - "en": "SQL query error: Unable to generate valid SQL statements!", - }, - "fail": { - "zh_cn": "SQL查询错误:SQL语句执行失败!", - "en": "SQL query error: SQL statement execution failed!", - } - } + # 生成SQL语句 sql_list = await self._generate_sql(data) if not sql_list: raise CallError( - message=message["invaild"][language], + message=MESSAGE["invaild"][language], data={}, ) @@ -153,7 +155,7 @@ class SQL(CoreCall, input_model=SQLInput, output_model=SQLOutput): sql_exec_results, sql_exec = await self._execute_sql(sql_list) if sql_exec_results is None or sql_exec is None: raise CallError( - message=message["fail"][language], + message=MESSAGE["fail"][language], data={}, ) diff --git a/apps/scheduler/call/suggest/prompt.py b/apps/scheduler/call/suggest/prompt.py index 1e2ecf7d..a9f61d7c 100644 --- a/apps/scheduler/call/suggest/prompt.py +++ b/apps/scheduler/call/suggest/prompt.py @@ -2,9 +2,10 @@ """问题推荐工具的提示词""" from textwrap import dedent +from apps.schemas.enum_var import LanguageType -SUGGEST_PROMPT: dict[str, str] = { - "zh_cn": dedent( +SUGGEST_PROMPT: dict[LanguageType, str] = { + LanguageType.CHINESE: dedent( r""" @@ -93,7 +94,7 @@ SUGGEST_PROMPT: dict[str, str] = { 现在,进行问题生成: """ ), - "en": dedent( + LanguageType.ENGLISH: dedent( r""" diff --git a/apps/scheduler/call/suggest/suggest.py b/apps/scheduler/call/suggest/suggest.py index a722de8c..0d0a93fe 100644 --- a/apps/scheduler/call/suggest/suggest.py +++ b/apps/scheduler/call/suggest/suggest.py @@ -20,7 +20,7 @@ from apps.scheduler.call.suggest.schema import ( SuggestionInput, SuggestionOutput, ) -from apps.schemas.enum_var import CallOutputType +from apps.schemas.enum_var import CallOutputType, LanguageType from apps.schemas.pool import NodePool from apps.schemas.record import RecordContent from apps.schemas.scheduler import ( @@ -50,11 +50,11 @@ class Suggestion(CoreCall, input_model=SuggestionInput, output_model=SuggestionO conversation_id: SkipJsonSchema[str] = Field(description="对话ID", exclude=True) i18n_info: ClassVar[dict[str, dict]] = { - "zh_cn": { + LanguageType.CHINESE: { "name": "问题推荐", "description": "在答案下方显示推荐的下一个问题", }, - "en": { + LanguageType.ENGLISH: { "name": "Question Suggestion", "description": "Display the suggested next question under the answer", }, @@ -68,7 +68,7 @@ class Suggestion(CoreCall, input_model=SuggestionInput, output_model=SuggestionO :return: Call的名称和描述 :rtype: CallInfo """ - lang_info = cls.i18n_info.get(cls.language, cls.i18n_info["zh_cn"]) + lang_info = cls.i18n_info.get(cls.language, cls.i18n_info[LanguageType.CHINESE]) return CallInfo(name=lang_info["name"], description=lang_info["description"]) @classmethod @@ -140,7 +140,9 @@ class Suggestion(CoreCall, input_model=SuggestionInput, output_model=SuggestionO history_questions.append(record_data.question) return history_questions - async def _exec(self, input_data: dict[str, Any], language: str = "zh_cn") -> AsyncGenerator[CallOutputChunk, None]: + async def _exec( + self, input_data: dict[str, Any], language: LanguageType = LanguageType.CHINESE + ) -> AsyncGenerator[CallOutputChunk, None]: """运行问题推荐""" data = SuggestionInput(**input_data) diff --git a/apps/scheduler/call/summary/summary.py b/apps/scheduler/call/summary/summary.py index 87c9e7b3..64fd52e3 100644 --- a/apps/scheduler/call/summary/summary.py +++ b/apps/scheduler/call/summary/summary.py @@ -9,7 +9,7 @@ from pydantic import Field from apps.llm.patterns.executor import ExecutorSummary from apps.scheduler.call.core import CoreCall, DataBase from apps.scheduler.call.summary.schema import SummaryOutput -from apps.schemas.enum_var import CallOutputType +from apps.schemas.enum_var import CallOutputType, LanguageType from apps.schemas.pool import NodePool from apps.schemas.scheduler import ( CallInfo, @@ -22,18 +22,17 @@ if TYPE_CHECKING: from apps.scheduler.executor.step import StepExecutor - class Summary(CoreCall, input_model=DataBase, output_model=SummaryOutput): """总结工具""" context: ExecutorBackground = Field(description="对话上下文") i18n_info: ClassVar[dict[str, dict]] = { - "zh_cn": { + LanguageType.CHINESE: { "name": "理解上下文", "description": "使用大模型,理解对话上下文", }, - "en": { + LanguageType.ENGLISH: { "name": "Context Understanding", "description": "Use the foundation model to understand the conversation context", }, @@ -47,7 +46,7 @@ class Summary(CoreCall, input_model=DataBase, output_model=SummaryOutput): :return: Call的名称和描述 :rtype: CallInfo """ - lang_info = cls.i18n_info.get(cls.language, cls.i18n_info["zh_cn"]) + lang_info = cls.i18n_info.get(cls.language, cls.i18n_info[LanguageType.CHINESE]) return CallInfo(name=lang_info["name"], description=lang_info["description"]) @classmethod @@ -63,13 +62,13 @@ class Summary(CoreCall, input_model=DataBase, output_model=SummaryOutput): await obj._set_input(executor) return obj - async def _init(self, call_vars: CallVars) -> DataBase: """初始化工具,返回输入""" return DataBase() - - async def _exec(self, _input_data: dict[str, Any], language: str = "zh_cn") -> AsyncGenerator[CallOutputChunk, None]: + async def _exec( + self, _input_data: dict[str, Any], language: LanguageType = LanguageType.CHINESE + ) -> AsyncGenerator[CallOutputChunk, None]: """执行工具""" summary_obj = ExecutorSummary() summary = await summary_obj.generate(background=self.context, language=language) @@ -78,8 +77,12 @@ class Summary(CoreCall, input_model=DataBase, output_model=SummaryOutput): yield CallOutputChunk(type=CallOutputType.TEXT, content=summary) - - async def exec(self, executor: "StepExecutor", input_data: dict[str, Any], language= "zh_cn") -> AsyncGenerator[CallOutputChunk, None]: + async def exec( + self, + executor: "StepExecutor", + input_data: dict[str, Any], + language: LanguageType = LanguageType.CHINESE, + ) -> AsyncGenerator[CallOutputChunk, None]: """执行工具""" async for chunk in self._exec(input_data, language): content = chunk.content diff --git a/apps/scheduler/executor/flow.py b/apps/scheduler/executor/flow.py index bea2429b..3b051343 100644 --- a/apps/scheduler/executor/flow.py +++ b/apps/scheduler/executor/flow.py @@ -11,7 +11,7 @@ from pydantic import Field from apps.scheduler.call.llm.prompt import LLM_ERROR_PROMPT from apps.scheduler.executor.base import BaseExecutor from apps.scheduler.executor.step import StepExecutor -from apps.schemas.enum_var import EventType, SpecialCallType, FlowStatus, StepStatus +from apps.schemas.enum_var import EventType, SpecialCallType, FlowStatus, StepStatus, LanguageType from apps.schemas.flow import Flow, Step from apps.schemas.request_data import RequestDataApp from apps.schemas.task import ExecutorState, StepQueueItem @@ -21,13 +21,13 @@ logger = logging.getLogger(__name__) # 开始前的固定步骤 FIXED_STEPS_BEFORE_START = [ { - "zh_cn": Step( + LanguageType.CHINESE: Step( name="理解上下文", description="使用大模型,理解对话上下文", node=SpecialCallType.SUMMARY.value, type=SpecialCallType.SUMMARY.value, ), - "en": Step( + LanguageType.ENGLISH: Step( name="Understand context", description="Use large model to understand the context of the dialogue", node=SpecialCallType.SUMMARY.value, @@ -38,13 +38,13 @@ FIXED_STEPS_BEFORE_START = [ # 结束后的固定步骤 FIXED_STEPS_AFTER_END = [ { - "zh_cn": Step( + LanguageType.CHINESE: Step( name="记忆存储", description="理解对话答案,并存储到记忆中", node=SpecialCallType.FACTS.value, type=SpecialCallType.FACTS.value, ), - "en": Step( + LanguageType.ENGLISH: Step( name="Memory storage", description="Understand the answer of the dialogue and store it in the memory", node=SpecialCallType.FACTS.value, @@ -62,16 +62,17 @@ class FlowExecutor(BaseExecutor): flow_id: str = Field(description="Flow ID") question: str = Field(description="用户输入") post_body_app: RequestDataApp = Field(description="请求体中的app信息") - current_step: StepQueueItem | None = Field( - description="当前执行的步骤", - default=None - ) + current_step: StepQueueItem | None = Field(description="当前执行的步骤", default=None) async def load_state(self) -> None: """从数据库中加载FlowExecutor的状态""" logger.info("[FlowExecutor] 加载Executor状态") # 尝试恢复State - if self.task.state and self.task.state.flow_status != FlowStatus.INIT and self.task.state.flow_status != FlowStatus.UNKNOWN: + if ( + self.task.state + and self.task.state.flow_status != FlowStatus.INIT + and self.task.state.flow_status != FlowStatus.UNKNOWN + ): self.task.context = await TaskManager.get_context_by_task_id(self.task.id) else: # 创建ExecutorState @@ -83,7 +84,7 @@ class FlowExecutor(BaseExecutor): step_status=StepStatus.RUNNING, app_id=str(self.post_body_app.app_id), step_id="start", - step_name="开始" if self.task.language == "zh_cn" else "Start", + step_name="开始" if self.task.language == LanguageType.CHINESE else "Start", ) self.validate_flow_state(self.task) # 是否到达Flow结束终点(变量) @@ -183,7 +184,7 @@ class FlowExecutor(BaseExecutor): self.step_queue.append( StepQueueItem( step_id=str(uuid.uuid4()), - step=step.get(self.task.language, step["zh_cn"]), + step=step.get(self.task.language, step[LanguageType.CHINESE]), enable_filling=False, to_user=False, ) @@ -200,25 +201,31 @@ class FlowExecutor(BaseExecutor): if self.task.state.step_status == StepStatus.ERROR: # type: ignore[arg-type] logger.warning("[FlowExecutor] Executor出错,执行错误处理步骤") self.step_queue.clear() - self.step_queue.appendleft(StepQueueItem( - step_id=str(uuid.uuid4()), - step=Step( - name="错误处理" if self.task.language == "zh_cn" else "Error Handling", - description="错误处理" if self.task.language == "zh_cn" else "Error Handling", - node=SpecialCallType.LLM.value, - type=SpecialCallType.LLM.value, - params={ - "user_prompt": LLM_ERROR_PROMPT[self.task.language].replace( - "{{ error_info }}", - self.task.state.error_info["err_msg"], # type: ignore[arg-type] + self.step_queue.appendleft( + StepQueueItem( + step_id=str(uuid.uuid4()), + step=Step( + name=( + "错误处理" if self.task.language == LanguageType.CHINESE else "Error Handling" ), - }, - ), - enable_filling=False, - to_user=False, - )) + description=( + "错误处理" if self.task.language == LanguageType.CHINESE else "Error Handling" + ), + node=SpecialCallType.LLM.value, + type=SpecialCallType.LLM.value, + params={ + "user_prompt": LLM_ERROR_PROMPT[self.task.language].replace( + "{{ error_info }}", + self.task.state.error_info["err_msg"], # type: ignore[arg-type] + ), + }, + ), + enable_filling=False, + to_user=False, + ) + ) is_error = True - + # 错误处理后结束 self._reached_end = True @@ -244,7 +251,7 @@ class FlowExecutor(BaseExecutor): self.step_queue.append( StepQueueItem( step_id=str(uuid.uuid4()), - step=step.get(self.task.language, step["zh_cn"]), + step=step.get(self.task.language, step[LanguageType.CHINESE]), ) ) await self._step_process() diff --git a/apps/scheduler/mcp/host.py b/apps/scheduler/mcp/host.py index efac1eb4..8e7b26e3 100644 --- a/apps/scheduler/mcp/host.py +++ b/apps/scheduler/mcp/host.py @@ -14,7 +14,7 @@ from apps.llm.function import JsonGenerator from apps.scheduler.mcp.prompt import MEMORY_TEMPLATE from apps.scheduler.pool.mcp.client import MCPClient from apps.scheduler.pool.mcp.pool import MCPPool -from apps.schemas.enum_var import StepStatus +from apps.schemas.enum_var import StepStatus, LanguageType from apps.schemas.mcp import MCPPlanItem, MCPTool from apps.schemas.task import FlowStepHistory from apps.services.task import TaskManager @@ -25,7 +25,14 @@ logger = logging.getLogger(__name__) class MCPHost: """MCP宿主服务""" - def __init__(self, user_sub: str, task_id: str, runtime_id: str, runtime_name: str, language="zh_cn") -> None: + def __init__( + self, + user_sub: str, + task_id: str, + runtime_id: str, + runtime_name: str, + language: LanguageType = LanguageType.CHINESE, + ) -> None: """初始化MCP宿主""" self._user_sub = user_sub self._task_id = task_id @@ -59,7 +66,6 @@ class MCPHost: logger.warning("用户 %s 的MCP %s 没有运行中的实例,请检查环境", self._user_sub, mcp_id) return None - async def assemble_memory(self) -> str: """组装记忆""" task = await TaskManager.get_task_by_task_id(self._task_id) @@ -125,7 +131,6 @@ class MCPHost: return output_data - async def _fill_params(self, tool: MCPTool, query: str) -> dict[str, Any]: """填充工具参数""" # 更清晰的输入·指令,这样可以调用generate @@ -146,7 +151,6 @@ class MCPHost: ) return await json_generator.generate() - async def call_tool(self, tool: MCPTool, plan_item: MCPPlanItem) -> list[dict[str, Any]]: """调用工具""" # 拿到Client diff --git a/apps/scheduler/mcp/plan.py b/apps/scheduler/mcp/plan.py index 6a5e932a..489731b1 100644 --- a/apps/scheduler/mcp/plan.py +++ b/apps/scheduler/mcp/plan.py @@ -8,12 +8,13 @@ from apps.llm.function import JsonGenerator from apps.llm.reasoning import ReasoningLLM from apps.scheduler.mcp.prompt import CREATE_PLAN, FINAL_ANSWER from apps.schemas.mcp import MCPPlan, MCPTool +from apps.schemas.enum_var import LanguageType class MCPPlanner: """MCP 用户目标拆解与规划""" - def __init__(self, user_goal: str, language: str = "zh_cn") -> None: + def __init__(self, user_goal: str, language: LanguageType = LanguageType.CHINESE) -> None: """初始化MCP规划器""" self.user_goal = user_goal self.language = language @@ -26,7 +27,6 @@ class MCPPlanner: self.input_tokens = 0 self.output_tokens = 0 - async def create_plan(self, tool_list: list[MCPTool], max_steps: int = 6) -> MCPPlan: """规划下一步的执行流程,并输出""" # 获取推理结果 @@ -35,7 +35,6 @@ class MCPPlanner: # 解析为结构化数据 return await self._parse_plan_result(result, max_steps) - async def _get_reasoning_plan(self, tool_list: list[MCPTool], max_steps: int) -> str: """获取推理大模型的结果""" # 格式化Prompt @@ -67,7 +66,6 @@ class MCPPlanner: return result - async def _parse_plan_result(self, result: str, max_steps: int) -> MCPPlan: """将推理结果解析为结构化数据""" # 格式化Prompt @@ -86,7 +84,6 @@ class MCPPlanner: plan = await json_generator.generate() return MCPPlan.model_validate(plan) - async def generate_answer(self, plan: MCPPlan, memory: str) -> str: """生成最终回答""" template = self._env.from_string(FINAL_ANSWER[self.language]) diff --git a/apps/scheduler/mcp/prompt.py b/apps/scheduler/mcp/prompt.py index 5942a895..f23d9863 100644 --- a/apps/scheduler/mcp/prompt.py +++ b/apps/scheduler/mcp/prompt.py @@ -2,9 +2,10 @@ """MCP相关的大模型Prompt""" from textwrap import dedent +from apps.schemas.enum_var import LanguageType MCP_SELECT: dict[str, str] = { - "zh_cn": dedent( + LanguageType.CHINESE: dedent( r""" 你是一个乐于助人的智能助手。 你的任务是:根据当前目标,选择最合适的MCP Server。 @@ -65,7 +66,7 @@ MCP_SELECT: dict[str, str] = { """ ), - "en": dedent( + LanguageType.ENGLISH: dedent( r""" You are a helpful intelligent assistant. Your task is to select the most appropriate MCP server based on your current goals. @@ -128,7 +129,7 @@ MCP_SELECT: dict[str, str] = { ), } CREATE_PLAN: dict[str, str] = { - "zh_cn": dedent( + LanguageType.CHINESE: dedent( r""" 你是一个计划生成器。 请分析用户的目标,并生成一个计划。你后续将根据这个计划,一步一步地完成用户的目标。 @@ -229,7 +230,7 @@ CREATE_PLAN: dict[str, str] = { # 计划 """ ), - "en": dedent( + LanguageType.ENGLISH: dedent( r""" You are a plan generator. Please analyze the user's goal and generate a plan. You will then follow this plan to achieve the user's goal step by step. @@ -333,7 +334,7 @@ CREATE_PLAN: dict[str, str] = { ), } EVALUATE_PLAN: dict[str, str] = { - "zh_cn": dedent( + LanguageType.CHINESE: dedent( r""" 你是一个计划评估器。 请根据给定的计划,和当前计划执行的实际情况,分析当前计划是否合理和完整,并生成改进后的计划。 @@ -382,7 +383,7 @@ EVALUATE_PLAN: dict[str, str] = { """ ), - "en": dedent( + LanguageType.ENGLISH: dedent( r""" You are a plan evaluator. Based on the given plan and the actual execution of the current plan, analyze whether the current plan is reasonable and complete, and generate an improved plan. @@ -433,7 +434,7 @@ EVALUATE_PLAN: dict[str, str] = { ), } FINAL_ANSWER: dict[str, str] = { - "zh_cn": dedent( + LanguageType.CHINESE: dedent( r""" 综合理解计划执行结果和背景信息,向用户报告目标的完成情况。 @@ -455,7 +456,7 @@ FINAL_ANSWER: dict[str, str] = { """ ), - "en": dedent( + LanguageType.ENGLISH: dedent( r""" Based on the understanding of the plan execution results and background information, report to the user the completion status of the goal. @@ -479,7 +480,7 @@ FINAL_ANSWER: dict[str, str] = { ), } MEMORY_TEMPLATE: dict[str, str] = { - "zh_cn": dedent( + LanguageType.CHINESE: dedent( r""" {% for ctx in context_list %} - 第{{ loop.index }}步:{{ ctx.step_description }} @@ -489,7 +490,7 @@ MEMORY_TEMPLATE: dict[str, str] = { {% endfor %} """ ), - "en": dedent( + LanguageType.ENGLISH: dedent( r""" {% for ctx in context_list %} - Step {{ loop.index }}: {{ ctx.step_description }} diff --git a/apps/scheduler/mcp/select.py b/apps/scheduler/mcp/select.py index 826bf7a4..db2e49a3 100644 --- a/apps/scheduler/mcp/select.py +++ b/apps/scheduler/mcp/select.py @@ -14,6 +14,7 @@ from apps.llm.reasoning import ReasoningLLM from apps.scheduler.mcp.prompt import ( MCP_SELECT, ) +from apps.schemas.enum_var import LanguageType from apps.schemas.mcp import ( MCPCollection, MCPSelectResult, @@ -48,10 +49,17 @@ class MCPSelector: logger.info("[MCPHelper] 查询MCP Server向量: %s, %s", query, mcp_list) mcp_table = await LanceDB().get_table("mcp") query_embedding = await Embedding.get_embedding([query]) - mcp_vecs = await (await mcp_table.search( - query=query_embedding, - vector_column_name="embedding", - )).where(f"id IN {MCPSelector._assemble_sql(mcp_list)}").limit(5).to_list() + mcp_vecs = ( + await ( + await mcp_table.search( + query=query_embedding, + vector_column_name="embedding", + ) + ) + .where(f"id IN {MCPSelector._assemble_sql(mcp_list)}") + .limit(5) + .to_list() + ) # 拿到名称和description logger.info("[MCPHelper] 查询MCP Server名称和描述: %s", mcp_vecs) @@ -64,19 +72,19 @@ class MCPSelector: logger.warning("[MCPHelper] 查询MCP Server名称和描述失败: %s", mcp_id) continue mcp_data = MCPCollection.model_validate(mcp_data) - llm_mcp_list.extend([{ - "id": mcp_id, - "name": mcp_data.name, - "description": mcp_data.description, - }]) + llm_mcp_list.extend( + [ + { + "id": mcp_id, + "name": mcp_data.name, + "description": mcp_data.description, + } + ] + ) return llm_mcp_list async def _get_mcp_by_llm( - self, - query: str, - mcp_list: list[dict[str, str]], - mcp_ids: list[str], - language + self, query: str, mcp_list: list[dict[str, str]], mcp_ids: list[str], language ) -> MCPSelectResult: """通过LLM选择最合适的MCP Server""" # 初始化jinja2环境 @@ -134,10 +142,7 @@ class MCPSelector: return result async def select_top_mcp( - self, - query: str, - mcp_list: list[str], - language = "zh_cn" + self, query: str, mcp_list: list[str], language: LanguageType = LanguageType.CHINESE ) -> MCPSelectResult: """ 选择最合适的MCP Server @@ -151,14 +156,23 @@ class MCPSelector: return await self._get_mcp_by_llm(query, llm_mcp_list, mcp_list, language) @staticmethod - async def select_top_tool(query: str, mcp_list: list[str], top_n: int = 10, language: str = "zh_cn") -> list[MCPTool]: + async def select_top_tool( + query: str, mcp_list: list[str], top_n: int = 10, language: LanguageType = LanguageType.CHINESE + ) -> list[MCPTool]: """选择最合适的工具""" tool_vector = await LanceDB().get_table("mcp_tool") query_embedding = await Embedding.get_embedding([query]) - tool_vecs = await (await tool_vector.search( - query=query_embedding, - vector_column_name="embedding", - )).where(f"mcp_id IN {MCPSelector._assemble_sql(mcp_list)}").limit(top_n).to_list() + tool_vecs = ( + await ( + await tool_vector.search( + query=query_embedding, + vector_column_name="embedding", + ) + ) + .where(f"mcp_id IN {MCPSelector._assemble_sql(mcp_list)}") + .limit(top_n) + .to_list() + ) # 拿到工具 tool_collection = MongoDB().get_collection("mcp") @@ -167,13 +181,15 @@ class MCPSelector: for tool_vec in tool_vecs: # 到MongoDB里找对应的工具 logger.info("[MCPHelper] 查询MCP Tool名称和描述: %s", tool_vec["mcp_id"]) - tool_data = await tool_collection.aggregate([ - {"$match": {"_id": tool_vec["mcp_id"]}}, - {"$unwind": "$tools"}, - {"$match": {"tools.id": tool_vec["id"]}}, - {"$project": {"_id": 0, "tools": 1}}, - {"$replaceRoot": {"newRoot": "$tools"}}, - ]) + tool_data = await tool_collection.aggregate( + [ + {"$match": {"_id": tool_vec["mcp_id"]}}, + {"$unwind": "$tools"}, + {"$match": {"tools.id": tool_vec["id"]}}, + {"$project": {"_id": 0, "tools": 1}}, + {"$replaceRoot": {"newRoot": "$tools"}}, + ] + ) async for tool in tool_data: tool_obj = MCPTool.model_validate(tool) llm_tool_list.append(tool_obj) diff --git a/apps/scheduler/pool/loader/flow.py b/apps/scheduler/pool/loader/flow.py index 6ea8b687..1b3bb22b 100644 --- a/apps/scheduler/pool/loader/flow.py +++ b/apps/scheduler/pool/loader/flow.py @@ -11,7 +11,7 @@ import yaml from anyio import Path from apps.common.config import Config -from apps.schemas.enum_var import EdgeType +from apps.schemas.enum_var import EdgeType, LanguageType from apps.schemas.flow import AppFlow, Flow from apps.schemas.pool import AppPool from apps.models.vector import FlowPoolVector @@ -26,7 +26,7 @@ BASE_PATH = Path(Config().get_config().deploy.data_dir) / "semantics" / "app" class FlowLoader: """工作流加载器""" - def __init__(self, language: str="zh_cn"): + def __init__(self, language: LanguageType=LanguageType.CHINESE): self.language = language async def _load_yaml_file(self, flow_path: Path) -> dict[str, Any]: @@ -79,12 +79,12 @@ class FlowLoader: logger.error(err) raise ValueError(err) if key == "start": - step["name"] = "开始" if self.language in {"zh", "zh_cn"} else "Start" - step["description"] = "开始节点" if self.language in {"zh", "zh_cn"} else "Start Node" + step["name"] = "开始" if self.language == LanguageType.CHINESE else "Start" + step["description"] = "开始节点" if self.language == LanguageType.CHINESE else "Start Node" step["type"] = "start" elif key == "end": - step["name"] = "结束" if self.language in {"zh", "zh_cn"} else "End" - step["description"] = "结束节点" if self.language in {"zh", "zh_cn"} else "End Node" + step["name"] = "结束" if self.language == LanguageType.CHINESE else "End" + step["description"] = "结束节点" if self.language == LanguageType.CHINESE else "End Node" step["type"] = "end" else: try: diff --git a/apps/scheduler/pool/pool.py b/apps/scheduler/pool/pool.py index 625964bf..bf30d31b 100644 --- a/apps/scheduler/pool/pool.py +++ b/apps/scheduler/pool/pool.py @@ -17,7 +17,7 @@ from apps.scheduler.pool.loader import ( MCPLoader, ServiceLoader, ) -from apps.schemas.enum_var import MetadataType +from apps.schemas.enum_var import MetadataType, LanguageType from apps.schemas.flow import Flow from apps.schemas.pool import AppFlow, CallPool @@ -60,7 +60,6 @@ class Pool: await Path(root_dir + "mcp").unlink(missing_ok=True) await Path(root_dir + "mcp").mkdir(parents=True, exist_ok=True) - @staticmethod async def init() -> None: """ @@ -127,7 +126,6 @@ class Pool: logger.info("[Pool] 载入MCP") await MCPLoader.init() - async def get_flow_metadata(self, app_id: str) -> list[AppFlow]: """从数据库中获取特定App的全部Flow的元数据""" mongo = MongoDB() @@ -145,14 +143,14 @@ class Pool: else: return flow_metadata_list - - async def get_flow(self, app_id: str, flow_id: str, language:str = "zh_cn") -> Flow | None: + async def get_flow( + self, app_id: str, flow_id: str, language: LanguageType = LanguageType.CHINESE + ) -> Flow | None: """从文件系统中获取单个Flow的全部数据""" logger.info("[Pool] 获取工作流 %s", flow_id) flow_loader = FlowLoader(language) return await flow_loader.load(app_id, flow_id) - async def get_call(self, call_id: str) -> Any: """[Exception] 拿到Call的信息""" # 从MongoDB里拿到数据 diff --git a/apps/scheduler/scheduler/scheduler.py b/apps/scheduler/scheduler/scheduler.py index 09e5ddd0..29ac42cc 100644 --- a/apps/scheduler/scheduler/scheduler.py +++ b/apps/scheduler/scheduler/scheduler.py @@ -208,14 +208,14 @@ class Scheduler: logger.error("[Scheduler] 未找到Agent应用") return if app_metadata.llm_id == "empty": - llm = LLM( - _id="empty", - user_sub=self.task.ids.user_sub, - openai_base_url=Config().get_config().llm.endpoint, - openai_api_key=Config().get_config().llm.key, - model_name=Config().get_config().llm.model, - max_tokens=Config().get_config().llm.max_tokens, - ) + llm = LLM( + _id="empty", + user_sub=self.task.ids.user_sub, + openai_base_url=Config().get_config().llm.endpoint, + openai_api_key=Config().get_config().llm.key, + model_name=Config().get_config().llm.model, + max_tokens=Config().get_config().llm.max_tokens, + ) else: llm = await LLMManager.get_llm_by_id( self.task.ids.user_sub, app_metadata.llm_id, diff --git a/apps/schemas/enum_var.py b/apps/schemas/enum_var.py index 3bcabd57..51c276fa 100644 --- a/apps/schemas/enum_var.py +++ b/apps/schemas/enum_var.py @@ -209,3 +209,10 @@ class AgentState(str, Enum): RUNNING = "RUNNING" FINISHED = "FINISHED" ERROR = "ERROR" + +class LanguageType(str, Enum): + """语言类型""" + + CHINESE = "zh_cn" + ENGLISH = "en" + diff --git a/apps/schemas/request_data.py b/apps/schemas/request_data.py index 274fb61f..b472e43a 100644 --- a/apps/schemas/request_data.py +++ b/apps/schemas/request_data.py @@ -7,7 +7,7 @@ from pydantic import BaseModel, Field from apps.common.config import Config from apps.schemas.appcenter import AppData -from apps.schemas.enum_var import CommentType +from apps.schemas.enum_var import CommentType, LanguageType from apps.schemas.flow_topology import FlowItem from apps.schemas.mcp import MCPType from apps.schemas.message import param @@ -44,7 +44,7 @@ class RequestData(BaseModel): question: str = Field(max_length=2000, description="用户输入") conversation_id: str = Field(default="", alias="conversationId", description="聊天ID") group_id: str | None = Field(default=None, alias="groupId", description="问答组ID") - language: str = Field(default="zh", description="语言") + language: LanguageType = Field(default=LanguageType.CHINESE, description="语言") files: list[str] = Field(default=[], description="文件列表") app: RequestDataApp | None = Field(default=None, description="应用") debug: bool = Field(default=False, description="是否调试") diff --git a/apps/schemas/task.py b/apps/schemas/task.py index 230ff8b4..7f68d77c 100644 --- a/apps/schemas/task.py +++ b/apps/schemas/task.py @@ -7,7 +7,7 @@ from typing import Any from pydantic import BaseModel, Field -from apps.schemas.enum_var import FlowStatus, StepStatus +from apps.schemas.enum_var import FlowStatus, StepStatus, LanguageType from apps.schemas.flow import Step from apps.schemas.mcp import MCPPlan @@ -99,7 +99,7 @@ class Task(BaseModel): tokens: TaskTokens = Field(description="Token信息") runtime: TaskRuntime = Field(description="任务运行时数据") created_at: float = Field(default_factory=lambda: round(datetime.now(tz=UTC).timestamp(), 3)) - language: str = Field(description="语言", default="zh") + language: LanguageType = Field(description="语言", default=LanguageType.CHINESE) class StepQueueItem(BaseModel): diff --git a/apps/services/flow.py b/apps/services/flow.py index dbbb5126..8b8801e7 100644 --- a/apps/services/flow.py +++ b/apps/services/flow.py @@ -10,7 +10,7 @@ from apps.common.mongo import MongoDB from apps.scheduler.pool.loader.flow import FlowLoader from apps.scheduler.slot.slot import Slot from apps.schemas.collection import User -from apps.schemas.enum_var import EdgeType, PermissionType +from apps.schemas.enum_var import EdgeType, PermissionType, LanguageType from apps.schemas.flow import Edge, Flow, Step from apps.schemas.flow_topology import ( EdgeItem, @@ -22,6 +22,7 @@ from apps.schemas.flow_topology import ( ) from apps.scheduler.pool.pool import Pool from apps.services.node import NodeManager + logger = logging.getLogger(__name__) @@ -71,7 +72,7 @@ class FlowManager: @staticmethod async def get_node_id_by_service_id( - service_id: str, language: str = "zh_cn" + service_id: str, language: LanguageType = LanguageType.CHINESE ) -> list[NodeMetaDataItem] | None: """ serviceId获取service的接口数据,并将接口转换为节点元数据 @@ -123,7 +124,9 @@ class FlowManager: return nodes_meta_data_items @staticmethod - async def get_service_by_user_id(user_sub: str, language: str = "zh_cn") -> list[NodeServiceItem] | None: + async def get_service_by_user_id( + user_sub: str, language: LanguageType = LanguageType.CHINESE + ) -> list[NodeServiceItem] | None: """ 通过user_id获取用户自己上传的、其他人公开的且收藏的、受保护且有权限访问并收藏的service @@ -166,7 +169,7 @@ class FlowManager: service_items = [ NodeServiceItem( serviceId="", - name="系统" if language == "zh_cn" else "System", + name="系统" if language == LanguageType.CHINESE else "System", type="system", nodeMetaDatas=[], ) @@ -227,7 +230,7 @@ class FlowManager: @staticmethod async def get_flow_by_app_and_flow_id( - app_id: str, flow_id: str, language: str = "zh_cn" + app_id: str, flow_id: str, language: LanguageType = LanguageType.CHINESE ) -> FlowItem | None: # noqa: C901, PLR0911, PLR0912 """ 通过appId flowId获取flow config的路径和focus,并通过flow config的路径获取flow config,并将其转换为flow item。 @@ -372,7 +375,9 @@ class FlowManager: return sorted(edge_list_1) == sorted(edge_list_2) @staticmethod - async def put_flow_by_app_and_flow_id(app_id: str, flow_id: str, flow_item: FlowItem, language: str = "zh_cn") -> FlowItem | None: + async def put_flow_by_app_and_flow_id( + app_id: str, flow_id: str, flow_item: FlowItem, language: LanguageType = LanguageType.CHINESE + ) -> FlowItem | None: """ 存储/更新flow的数据库数据和配置文件 @@ -431,7 +436,7 @@ class FlowManager: flow_config.debug = await FlowManager.is_flow_config_equal(old_flow_config, flow_config) else: flow_config.debug = False - logger.error(f'{flow_config}') + logger.error(f"{flow_config}") await flow_loader.save(app_id, flow_id, flow_config) except Exception: logger.exception("[FlowManager] 存储/更新流失败") @@ -467,7 +472,9 @@ class FlowManager: return flow_id @staticmethod - async def update_flow_debug_by_app_and_flow_id(app_id: str, flow_id: str, *, debug: bool, language: str = "zh_cn") -> bool: + async def update_flow_debug_by_app_and_flow_id( + app_id: str, flow_id: str, *, debug: bool, language: LanguageType = LanguageType.CHINESE + ) -> bool: """ 更新flow的debug状态 diff --git a/apps/services/rag.py b/apps/services/rag.py index 9fc91ad2..5be0ef53 100644 --- a/apps/services/rag.py +++ b/apps/services/rag.py @@ -16,7 +16,7 @@ from apps.llm.reasoning import ReasoningLLM from apps.llm.token import TokenCalculator from apps.schemas.collection import LLM from apps.schemas.config import LLMConfig -from apps.schemas.enum_var import EventType +from apps.schemas.enum_var import EventType, LanguageType from apps.schemas.rag_data import RAGQueryReq from apps.services.session import SessionManager @@ -28,8 +28,8 @@ class RAG: system_prompt: str = "You are a helpful assistant." """系统提示词""" - user_prompt: dict[str, str] = { - "zh_cn" : r""" + user_prompt: dict[LanguageType, str] = { + LanguageType.CHINESE: r""" 你是openEuler社区的智能助手。请结合给出的背景信息, 回答用户的提问,并且基于给出的背景信息在相关句子后进行脚注。 一个例子将在中给出。 @@ -77,7 +77,7 @@ class RAG: {user_question} """, - "en" : r""" + LanguageType.ENGLISH: r""" You are a helpful assistant of openEuler community. Please answer the user's question based on the given background information and add footnotes after the related sentences. An example will be given in . @@ -121,7 +121,7 @@ class RAG: {user_question} - """ + """, } @staticmethod @@ -207,16 +207,18 @@ class RAG: for doc_chunk in doc_chunk_list: if doc_chunk["docId"] not in doc_id_map: doc_cnt += 1 - doc_info_list.append({ - "id": doc_chunk["docId"], - "order": doc_cnt, - "name": doc_chunk.get("docName", ""), - "author": doc_chunk.get("docAuthor", ""), - "extension": doc_chunk.get("docExtension", ""), - "abstract": doc_chunk.get("docAbstract", ""), - "size": doc_chunk.get("docSize", 0), - "created_at": doc_chunk.get("docCreatedAt", round(datetime.now(UTC).timestamp(), 3)), - }) + doc_info_list.append( + { + "id": doc_chunk["docId"], + "order": doc_cnt, + "name": doc_chunk.get("docName", ""), + "author": doc_chunk.get("docAuthor", ""), + "extension": doc_chunk.get("docExtension", ""), + "abstract": doc_chunk.get("docAbstract", ""), + "size": doc_chunk.get("docSize", 0), + "created_at": doc_chunk.get("docCreatedAt", round(datetime.now(UTC).timestamp(), 3)), + } + ) doc_id_map[doc_chunk["docId"]] = doc_cnt doc_index = doc_id_map[doc_chunk["docId"]] if bac_info: @@ -249,7 +251,12 @@ class RAG: @staticmethod async def chat_with_llm_base_on_rag( - user_sub: str, llm: LLM, history: list[dict[str, str]], doc_ids: list[str], data: RAGQueryReq, language: str = "zh_cn" + user_sub: str, + llm: LLM, + history: list[dict[str, str]], + doc_ids: list[str], + data: RAGQueryReq, + language: LanguageType = LanguageType.CHINESE, ) -> AsyncGenerator[str, None]: """获取RAG服务的结果""" reasion_llm = ReasoningLLM( -- Gitee From ed9e9bf517dc316534e332d5bc482d353d0deb3d Mon Sep 17 00:00:00 2001 From: zhanghb <2428333123@qq.com> Date: Mon, 11 Aug 2025 15:02:03 +0800 Subject: [PATCH 10/15] =?UTF-8?q?mcp-agent=E5=9B=BD=E9=99=85=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/scheduler/executor/agent.py | 7 +- apps/scheduler/mcp_agent/host.py | 7 +- apps/scheduler/mcp_agent/plan.py | 22 +- apps/scheduler/mcp_agent/prompt.py | 1180 +++++++++++++++++++++++++--- apps/scheduler/mcp_agent/select.py | 4 +- apps/schemas/task.py | 1 - 6 files changed, 1087 insertions(+), 134 deletions(-) diff --git a/apps/scheduler/executor/agent.py b/apps/scheduler/executor/agent.py index 60394bd7..6df073f5 100644 --- a/apps/scheduler/executor/agent.py +++ b/apps/scheduler/executor/agent.py @@ -10,7 +10,7 @@ from mcp.types import TextContent from apps.llm.patterns.rewrite import QuestionRewrite from apps.llm.reasoning import ReasoningLLM from apps.scheduler.executor.base import BaseExecutor -from apps.schemas.enum_var import EventType, SpecialCallType, FlowStatus, StepStatus +from apps.schemas.enum_var import EventType, SpecialCallType, FlowStatus, StepStatus, LanguageType from apps.scheduler.mcp_agent.host import MCPHost from apps.scheduler.mcp_agent.plan import MCPPlanner from apps.scheduler.mcp_agent.select import FINAL_TOOL_ID, MCPSelector @@ -55,6 +55,11 @@ class MCPAgentExecutor(BaseExecutor): description="推理大模型", ) + def __init__(self): + MCPPlanner.language = self.task.language + MCPHost.language = self.task.language + MCPSelector.language = self.task.language + async def update_tokens(self) -> None: """更新令牌数""" self.task.tokens.input_tokens = self.resoning_llm.input_tokens diff --git a/apps/scheduler/mcp_agent/host.py b/apps/scheduler/mcp_agent/host.py index 85d992c8..dcc211de 100644 --- a/apps/scheduler/mcp_agent/host.py +++ b/apps/scheduler/mcp_agent/host.py @@ -15,7 +15,7 @@ from apps.scheduler.mcp.prompt import MEMORY_TEMPLATE from apps.scheduler.pool.mcp.client import MCPClient from apps.scheduler.pool.mcp.pool import MCPPool from apps.scheduler.mcp_agent.prompt import REPAIR_PARAMS -from apps.schemas.enum_var import StepStatus +from apps.schemas.enum_var import StepStatus, LanguageType from apps.schemas.mcp import MCPPlanItem, MCPTool from apps.schemas.task import Task, FlowStepHistory from apps.services.task import TaskManager @@ -39,12 +39,13 @@ _env.filters['tojson'] = tojson_filter class MCPHost: """MCP宿主服务""" + language:LanguageType = LanguageType.CHINESE @staticmethod async def assemble_memory(task: Task) -> str: """组装记忆""" - return _env.from_string(MEMORY_TEMPLATE).render( + return _env.from_string(MEMORY_TEMPLATE[MCPHost.language]).render( context_list=task.context, ) @@ -73,7 +74,7 @@ class MCPHost: error_message: str = "", params: dict[str, Any] = {}, params_description: str = "") -> dict[str, Any]: llm_query = "请生成修复之后的工具参数" - prompt = _env.from_string(REPAIR_PARAMS).render( + prompt = _env.from_string(REPAIR_PARAMS[MCPHost.language]).render( tool_name=mcp_tool.name, tool_description=mcp_tool.description, input_schema=mcp_tool.input_schema, diff --git a/apps/scheduler/mcp_agent/plan.py b/apps/scheduler/mcp_agent/plan.py index 35c44030..543e4f42 100644 --- a/apps/scheduler/mcp_agent/plan.py +++ b/apps/scheduler/mcp_agent/plan.py @@ -27,6 +27,7 @@ from apps.schemas.mcp import ( MCPPlan, MCPTool ) +from apps.schemas.enum_var import LanguageType from apps.scheduler.slot.slot import Slot _env = SandboxedEnvironment( @@ -39,6 +40,7 @@ _env = SandboxedEnvironment( class MCPPlanner(McpBase): """MCP 用户目标拆解与规划""" + language:LanguageType = LanguageType.CHINESE @staticmethod async def evaluate_goal( @@ -60,7 +62,7 @@ class MCPPlanner(McpBase): goal, tool_list: list[MCPTool], resoning_llm: ReasoningLLM = ReasoningLLM()) -> str: """获取推理大模型的评估结果""" - template = _env.from_string(EVALUATE_GOAL) + template = _env.from_string(EVALUATE_GOAL[MCPPlanner.language]) prompt = template.render( goal=goal, tools=tool_list, @@ -84,7 +86,7 @@ class MCPPlanner(McpBase): @staticmethod async def _get_reasoning_flow_name(user_goal: str, resoning_llm: ReasoningLLM = ReasoningLLM()) -> str: """获取推理大模型的流程名称""" - template = _env.from_string(GENERATE_FLOW_NAME) + template = _env.from_string(GENERATE_FLOW_NAME[MCPPlanner.language]) prompt = template.render(goal=user_goal) result = await MCPPlanner.get_resoning_result(prompt, resoning_llm) return result @@ -96,7 +98,7 @@ class MCPPlanner(McpBase): reasoning_llm: ReasoningLLM = ReasoningLLM()) -> RestartStepIndex: """获取重新规划的步骤索引""" # 获取推理结果 - template = _env.from_string(GET_REPLAN_START_STEP_INDEX) + template = _env.from_string(GET_REPLAN_START_STEP_INDEX[MCPPlanner.language]) prompt = template.render( goal=user_goal, error_message=error_message, @@ -132,7 +134,7 @@ class MCPPlanner(McpBase): """获取推理大模型的结果""" # 格式化Prompt if is_replan: - template = _env.from_string(RECREATE_PLAN) + template = _env.from_string(RECREATE_PLAN[MCPPlanner.language]) prompt = template.render( current_plan=current_plan.model_dump(exclude_none=True, by_alias=True), error_message=error_message, @@ -141,7 +143,7 @@ class MCPPlanner(McpBase): max_num=max_steps, ) else: - template = _env.from_string(CREATE_PLAN) + template = _env.from_string(CREATE_PLAN[MCPPlanner.language]) prompt = template.render( goal=user_goal, tools=tool_list, @@ -179,7 +181,7 @@ class MCPPlanner(McpBase): tool: MCPTool, input_param: dict[str, Any], additional_info: str, resoning_llm: ReasoningLLM) -> str: """获取推理大模型的风险评估结果""" - template = _env.from_string(RISK_EVALUATE) + template = _env.from_string(RISK_EVALUATE[MCPPlanner.language]) prompt = template.render( tool_name=tool.name, tool_description=tool.description, @@ -203,7 +205,7 @@ class MCPPlanner(McpBase): tool: MCPTool, input_param: dict[str, Any], error_message: str, reasoning_llm: ReasoningLLM = ReasoningLLM()) -> str: """获取推理大模型的工具执行错误类型""" - template = _env.from_string(TOOL_EXECUTE_ERROR_TYPE_ANALYSIS) + template = _env.from_string(TOOL_EXECUTE_ERROR_TYPE_ANALYSIS[MCPPlanner.language]) prompt = template.render( goal=user_goal, current_plan=current_plan.model_dump(exclude_none=True, by_alias=True), @@ -241,7 +243,7 @@ class MCPPlanner(McpBase): error_message: str, tool: MCPTool, input_params: dict[str, Any], reasoning_llm: ReasoningLLM = ReasoningLLM()) -> str: """将错误信息转换为工具描述""" - template = _env.from_string(CHANGE_ERROR_MESSAGE_TO_DESCRIPTION) + template = _env.from_string(CHANGE_ERROR_MESSAGE_TO_DESCRIPTION[MCPPlanner.language]) prompt = template.render( error_message=error_message, tool_name=tool.name, @@ -259,7 +261,7 @@ class MCPPlanner(McpBase): error_message: str, reasoning_llm: ReasoningLLM = ReasoningLLM()) -> list[str]: """获取缺失的参数""" slot = Slot(schema=tool.input_schema) - template = _env.from_string(GET_MISSING_PARAMS) + template = _env.from_string(GET_MISSING_PARAMS[MCPPlanner.language]) schema_with_null = slot.add_null_to_basic_types() prompt = template.render( tool_name=tool.name, @@ -278,7 +280,7 @@ class MCPPlanner(McpBase): user_goal: str, plan: MCPPlan, memory: str, resoning_llm: ReasoningLLM = ReasoningLLM()) -> AsyncGenerator[ str, None]: """生成最终回答""" - template = _env.from_string(FINAL_ANSWER) + template = _env.from_string(FINAL_ANSWER[MCPPlanner.language]) prompt = template.render( plan=plan.model_dump(exclude_none=True, by_alias=True), memory=memory, diff --git a/apps/scheduler/mcp_agent/prompt.py b/apps/scheduler/mcp_agent/prompt.py index 365179f6..e29c7a00 100644 --- a/apps/scheduler/mcp_agent/prompt.py +++ b/apps/scheduler/mcp_agent/prompt.py @@ -1,9 +1,11 @@ # Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. """MCP相关的大模型Prompt""" - +from apps.schemas.enum_var import LanguageType from textwrap import dedent -MCP_SELECT = dedent(r""" +MCP_SELECT: dict[LanguageType, str] = { + LanguageType.CHINESE: dedent( + r""" 你是一个乐于助人的智能助手。 你的任务是:根据当前目标,选择最合适的MCP Server。 @@ -61,8 +63,70 @@ MCP_SELECT = dedent(r""" ### 请一步一步思考: -""") -TOOL_SELECT = dedent(r""" + """ + ), + LanguageType.ENGLISH: dedent( + r""" + You are an intelligent assistant who is willing to help. + Your task is: according to the current goal, select the most suitable MCP Server. + + ## Notes when selecting MCP Server: + + 1. Make sure to fully understand the current goal and select the most suitable MCP Server. + 2. Please select from the given MCP Server list, do not generate MCP Server by yourself. + 3. Please first give your reason for selection, then give your selection. + 4. The current goal will be given below, and the MCP Server list will also be given below. + Please put your thinking process in the "Thinking Process" part, and put your selection in the "Selection Result" part. + 5. The selection must be in JSON format, strictly follow the template below, do not output any other content: + + ```json + { + "mcp": "The name of the MCP Server you selected" + } + ``` + 6. The example below is for reference only, do not use the content in the example as the basis for selecting MCP Server. + + ## Example + + ### Goal + + I need an MCP Server to complete a task. + + ### MCP Server List + + - **mcp_1**: "MCP Server 1";Description of MCP Server 1 + - **mcp_2**: "MCP Server 2";Description of MCP Server 2 + + ### Please think step by step: + + Because the current goal needs an MCP Server to complete a task, so select mcp_1. + + ### Selection Result + + ```json + { + "mcp": "mcp_1" + } + ``` + + ## Now start! + ### Goal + + {{goal}} + + ### MCP Server List + + {% for mcp in mcp_list %} + - **{{mcp.id}}**: "{{mcp.name}}";{{mcp.description}} + {% endfor %} + + ### Please think step by step: + """ + ), +} +TOOL_SELECT: dict[LanguageType, str] = { + LanguageType.CHINESE: dedent( + r""" 你是一个乐于助人的智能助手。 你的任务是:根据当前目标,附加信息,选择最合适的MCP工具。 ## 选择MCP工具时的注意事项: @@ -118,9 +182,69 @@ TOOL_SELECT = dedent(r""" {{additional_info}} # 输出 """ - ) + ), + LanguageType.ENGLISH: dedent( + r""" + You are an intelligent assistant who is willing to help. + Your task is: according to the current goal, additional information, select the most suitable MCP tool. + ## Notes when selecting MCP tool: + 1. Make sure to fully understand the current goal and select the MCP tool that can achieve the goal. + 2. Please select from the given MCP tool list, do not generate MCP tool by yourself. + 3. You can select some auxiliary tools, but you must ensure that these tools are related to the current goal. + 4. Note that the returned tool ID must be the ID of the MCP tool, not the name. + 5. Do not select non-existent tools. + Must generate the selection result in the following format, do not output any other content: + ```json + { + "tool_ids": ["tool_id1", "tool_id2", ...] + } + ``` -EVALUATE_GOAL = dedent(r""" + # Example + ## Goal + Optimize MySQL performance + ## MCP Tool List + + - mcp_tool_1 MySQL connection pool tool;used to optimize MySQL connection pool + - mcp_tool_2 MySQL performance tuning tool;used to analyze MySQL performance bottlenecks + - mcp_tool_3 MySQL query optimization tool;used to optimize MySQL query statements + - mcp_tool_4 MySQL index optimization tool;used to optimize MySQL index + - mcp_tool_5 File storage tool;used to store files + - mcp_tool_6 MongoDB tool;used to operate MongoDB database + + ## Additional Information + 1. The current MySQL database version is 8.0.26 + 2. The current MySQL database configuration file path is /etc/my.cnf, and contains the following configuration items + ```json + { + "max_connections": 1000, + "innodb_buffer_pool_size": "1G", + "query_cache_size": "64M" + } + ## Output + ```json + { + "tool_ids": ["mcp_tool_1", "mcp_tool_2", "mcp_tool_3", "mcp_tool_4"] + } + ``` + # Now start! + ## Goal + {{goal}} + ## MCP Tool List + + {% for tool in tools %} + - {{tool.id}} {{tool.name}};{{tool.description}} + {% endfor %} + + ## Additional Information + {{additional_info}} + # Output + """ + ), +} +EVALUATE_GOAL: dict[LanguageType, str] = { + LanguageType.CHINESE: dedent( + r""" 你是一个计划评估器。 请根据用户的目标和当前的工具集合以及一些附加信息,判断基于当前的工具集合,是否能够完成用户的目标。 如果能够完成,请返回`true`,否则返回`false`。 @@ -170,8 +294,65 @@ EVALUATE_GOAL = dedent(r""" # 附加信息 {{additional_info}} -""") -GENERATE_FLOW_NAME = dedent(r""" + """ + ), + LanguageType.ENGLISH: dedent( + r""" + You are a plan evaluator. + Please judge whether the current tool set can complete the user's goal based on the user's goal and the current tool set and some additional information. + If it can be completed, return `true`, otherwise return `false`. + The reasoning process must be clear and understandable, so that people can understand your judgment basis. + Must answer in the following format: + ```json + { + "can_complete": true/false, + "resoning": "Your reasoning process" + } + ``` + + # Example + # Goal + I need to scan the current MySQL database, analyze performance bottlenecks, and optimize it. + + # Tool Set + You can access and use some tools, which will be given in the XML tag. + + - mysql_analyzer Analyze MySQL database performance + - performance_tuner Tune database performance + - Final End step, when executing this step, it means that the plan execution is over, and the result obtained will be the final result. + + + # Additional Information + 1. The current MySQL database version is 8.0.26 + 2. The current MySQL database configuration file path is /etc/my.cnf + + ## + ```json + { + "can_complete": true, + "resoning": "The current tool set contains mysql_analyzer and performance_tuner, which can complete the performance analysis and optimization of MySQL database, so the user's goal can be completed." + } + ``` + + # Goal + {{goal}} + + # Tool Set + + {% for tool in tools %} + - {{tool.id}} {{tool.name}};{{tool.description}} + {% endfor %} + + + # Additional Information + {{additional_info}} + + """ + ), +} +GENERATE_FLOW_NAME: dict[LanguageType, str] = { + LanguageType.CHINESE: dedent( + r""" 你是一个智能助手,你的任务是根据用户的目标,生成一个合适的流程名称。 # 生成流程名称时的注意事项: @@ -189,8 +370,33 @@ GENERATE_FLOW_NAME = dedent(r""" # 目标 {{goal}} # 输出 - """) -GET_REPLAN_START_STEP_INDEX = dedent(r""" + """ + ), + LanguageType.ENGLISH: dedent( + r""" + You are an intelligent assistant, your task is to generate a suitable flow name based on the user's goal. + + # Notes when generating flow names: + 1. The flow name should be concise and clear, accurately expressing the process of achieving the user's goal. + 2. The flow name should include key operations or steps, such as "scan", "analyze", "tune", etc. + 3. The flow name should avoid using overly complex or professional terms, so that users can understand. + 4. The flow name should be as short as possible, less than 20 characters or words. + 5. Only output the flow name, do not output other content. + # Example + # Goal + I need to scan the current MySQL database, analyze performance bottlenecks, and optimize it. + # Output + Scan MySQL database and analyze performance bottlenecks, and optimize it. + # Now start generating the flow name: + # Goal + {{goal}} + # Output + """ + ), +} +GET_REPLAN_START_STEP_INDEX: dict[LanguageType, str] = { + LanguageType.CHINESE: dedent( + r""" 你是一个智能助手,你的任务是根据用户的目标、报错信息和当前计划和历史,获取重新规划的步骤起始索引。 # 样例 @@ -206,7 +412,7 @@ GET_REPLAN_START_STEP_INDEX = dedent(r""" "step_id": "step_1", "content": "生成端口扫描命令", "tool": "command_generator", - "instruction": "生成端口扫描命令:扫描 + "instruction": "生成端口扫描命令:扫描" }, { "step_id": "step_2", @@ -252,9 +458,77 @@ GET_REPLAN_START_STEP_INDEX = dedent(r""" # 历史 {{history}} # 输出 - """) - -CREATE_PLAN = dedent(r""" + """ + ), + LanguageType.ENGLISH: dedent( + r""" + You are an intelligent assistant, your task is to get the starting index of the step to be replanned based on the user's goal, error message, and current plan and history. + + # Example + # Goal + I need to scan the current MySQL database, analyze performance bottlenecks, and optimize it. + # Error message + An error occurred while executing the port scan command: `- bash: curl: command not found`. + # Current plan + ```json + { + "plans": [ + { + "step_id": "step_1", + "content": "Generate port scan command", + "tool": "command_generator", + "instruction": "Generate port scan command: scan" + }, + { + "step_id": "step_2", + "content": "Execute the command generated by Result[0]", + "tool": "command_executor", + "instruction": "Execute port scan command" + } + ] + } + # History + [ + { + id: "0", + task_id: "task_1", + flow_id: "flow_1", + flow_name: "MYSQL Performance Tuning", + flow_status: "RUNNING", + step_id: "step_1", + step_name: "Generate port scan command", + step_description: "Generate port scan command: scan the port of the current MySQL database", + step_status: "FAILED", + input_data: { + "command": "nmap -p 3306 + "target": "localhost" + }, + output_data: { + "error": "- bash: curl: command not found" + } + } + ] + # Output + { + "start_index": 0, + "reasoning": "The first step of the current plan failed, the error message shows that the curl command was not found, which may be because the curl tool was not installed. Therefore, it is necessary to replan from the first step." + } + # Now start getting the starting index of the step to be replanned: + # Goal + {{goal}} + # Error message + {{error_message}} + # Current plan + {{current_plan}} + # History + {{history}} + # Output + """ + ), +} +CREATE_PLAN: dict[LanguageType, str] = { + LanguageType.CHINESE: dedent( + r""" 你是一个计划生成器。 请分析用户的目标,并生成一个计划。你后续将根据这个计划,一步一步地完成用户的目标。 @@ -316,7 +590,8 @@ CREATE_PLAN = dedent(r""" - 在后台运行 - 执行top命令 3. 需要先选择MCP Server, 然后生成Docker命令, 最后执行命令 - ```json + + ```json { "plans": [ { @@ -350,8 +625,112 @@ CREATE_PLAN = dedent(r""" {{goal}} # 计划 -""") -RECREATE_PLAN = dedent(r""" +""" + ), + LanguageType.ENGLISH: dedent( + r""" + You are a plan builder. + Analyze the user's goals and generate a plan. You will then follow this plan, step by step, to achieve the user's goals. + + # A good plan should: + + 1. Be able to successfully achieve the user's goals. + 2. Each step in the plan must use only one tool. + 3. The steps in the plan must have clear and logical progression, without redundant or unnecessary steps. + 4. The last step in the plan must be the Final tool to ensure the plan execution is complete. + + # Things to note when generating a plan: + + - Each plan contains 3 parts: + - Plan content: describes the general content of a single plan step + - Tool ID: must be selected from the tool list below + - Tool instructions: rewrite the user's goal to make it more consistent with the tool's input requirements + - The plan must be generated in the following format, and no additional data should be output: + + ```json + { + "plans": [ + { + "content": "Plan content", + "tool": "Tool ID", + "instruction": "Tool instruction" + } + ] + } + ``` + + - Before generating a plan, please think step by step, analyze the user's goals, and guide your subsequent generation. + The thinking process should be placed in the XML tags. + - In the plan content, you can use "Result[]" to reference the results of the previous plan step. For example: "Result[3]" refers to the result after the third plan is executed. + - There should be no more than {{max_num}} plans, and each plan content should be less than 150 words. + + # Tools + + You can access and use a number of tools, listed within the XML tags. + + + {% for tool in tools %} + - {{tool.id}} {{tool.name}}; {{tool.description}} + {% endfor %} + + + # Example + + # Goal + + Run a new alpine:latest container in the background, mount the host's /root folder to /data, and execute the top command. + + # Plan + + + 1. This goal needs to be completed using Docker. First, you need to select a suitable MCP Server. + 2. The goal can be broken down into the following parts: + - Run the alpine:latest container + - Mount the host directory + - Run in the background + - Execute the top command + 3. You need to select the MCP Server first, then generate the Docker command, and finally execute the command. + + ```json + { + "plans": [ + { + "content": "Select an MCP Server that supports Docker", + "tool": "mcp_selector", + "instruction": "You need an MCP Server that supports running Docker containers" + }, + { + "content": "Use the MCP Server selected in Result[0] to generate Docker commands", + "tool": "command_generator", + "instruction": "Generate Docker commands: run the alpine:latest container in the background, mount /root to /data, and execute the top command" + }, + { + "content": "In the MCP of Result[0] Execute the command generated by Result[1] on the server", + "tool": "command_executor", + "instruction": "Execute Docker command" + }, + { + "content": "Task execution completed, the container is running in the background, the result is Result[2]", + "tool": "Final", + "instruction": "" + } + ] + } + ``` + + # Now start generating the plan: + + # Goal + + {{goal}} + + # Plan + """ + ), +} +RECREATE_PLAN: dict[LanguageType, str] = { + LanguageType.CHINESE: dedent( + r""" 你是一个计划重建器。 请根据用户的目标、当前计划和运行报错,重新生成一个计划。 @@ -498,91 +877,303 @@ RECREATE_PLAN = dedent(r""" {{error_message}} # 重新生成的计划 -""") -RISK_EVALUATE = dedent(r""" - 你是一个工具执行计划评估器。 - 你的任务是根据当前工具的名称、描述和入参以及附加信息,判断当前工具执行的风险并输出提示。 +""" + ), + LanguageType.ENGLISH: dedent( + r""" + You are a plan rebuilder. + Please regenerate a plan based on the user's goals, current plan, and runtime errors. + + # A good plan should: + + 1. Successfully achieve the user's goals. + 2. Each step in the plan must use only one tool. + 3. The steps in the plan must have clear and logical progression, without redundant or unnecessary steps. + 4. Your plan must avoid previous errors and be able to be successfully executed. + 5. The last step in the plan must be the Final tool to ensure that the plan is complete. + + # Things to note when generating a plan: + + - Each plan contains 3 parts: + - Plan content: describes the general content of a single plan step + - Tool ID: must be selected from the tool list below + - Tool instructions: rewrite the user's goal to make it more consistent with the tool's input requirements + - The plan must be generated in the following format, and no additional data should be output: + ```json { - "risk": "low/medium/high", - "reason": "提示信息" - } - ``` - # 样例 - # 工具名称 - mysql_analyzer - # 工具描述 - 分析MySQL数据库性能 - # 工具入参 - { - "host": "192.0.0.1", - "port": 3306, - "username": "root", - "password": "password" + "plans": [ + { + "content": "Plan content", + "tool": "Tool ID", + "instruction": "Tool instruction" + } + ] } - # 附加信息 - 1. 当前MySQL数据库的版本是8.0.26 - 2. 当前MySQL数据库的配置文件路径是/etc/my.cnf,并含有以下配置项 - ```ini - [mysqld] - innodb_buffer_pool_size=1G - innodb_log_file_size=256M ``` - # 输出 + + - Before generating a plan, please think step by step, analyze the user's goals, and guide your subsequent generation. + The thinking process should be placed in the XML tags. + - In the plan content, you can use "Result[]" to reference the results of the previous plan step. For example: "Result[3]" refers to the result after the third plan is executed. + - There should be no more than {{max_num}} plans, and each plan content should be less than 150 words. + + # Example + + # Objective + + Please scan the ports of the machine at 192.168.1.1 to see which ports are open. + # Tools + You can access and use a number of tools, which are listed within the XML tags. + + - command_generator Generates command line instructions + - tool_selector Selects the appropriate tool + - command_executor Executes command line instructions + - Final This is the final step. When this step is reached, the plan execution ends and the result is used as the final result. + # Current plan ```json { - "risk": "中", - "reason": "当前工具将连接到MySQL数据库并分析性能,可能会对数据库性能产生一定影响。请确保在非生产环境中执行此操作。" + "plans": [ + { + "content": "Generate port scan command", + "tool": "command_generator", + "instruction": "Generate port scan command: Scan the open port of 192.168.1.1" + }, + { + "content": "Execute the command generated by Result[0]", + "tool": "command_executor", + "instruction": "Execute the port scan command" + }, + { + "content": "Task execution completed, the port scan result is Result[2]", + "tool": "Final", + "instruction": "" + } + ] } ``` - # 工具 - - {{tool_name}} - {{tool_description}} - - # 工具入参 - {{input_param}} - # 附加信息 - {{additional_info}} - # 输出 - """ - ) -# 根据当前计划和报错信息决定下一步执行,具体计划有需要用户补充工具入参、重计划当前步骤、重计划接下来的所有计划 -TOOL_EXECUTE_ERROR_TYPE_ANALYSIS = dedent(r""" - 你是一个计划决策器。 - 你的任务是根据用户目标、当前计划、当前使用的工具、工具入参和工具运行报错,决定下一步执行的操作。 - 请根据以下规则进行判断: - 1. 仅通过补充工具入参来解决问题的,返回 missing_param; - 2. 需要重计划当前步骤的,返回 decorrect_plan - 3.推理过程必须清晰明了,能够让人理解你的判断依据,并且不超过100字。 - 你的输出要以json格式返回,格式如下: + # Run error + When executing the port scan command, an error occurred: `- bash: curl: command not found`. + # Regenerate the plan + + + 1. This goal requires a network scanning tool. First, select the appropriate network scanning tool. + 2. The goal can be broken down into the following parts: + - Generate the port scanning command + - Execute the port scanning command + 3. However, when executing the port scanning command, an error occurred: `- bash: curl: command not found`. + 4. I adjusted the plan to: + - First, you need to generate a command to check which network scanning tools the current machine supports + - Execute this command to check which network scanning tools the current machine supports + - Then select a network scanning tool from them + - Based on the selected network scanning tool, generate a port scanning command + - Execute the port scanning command + ```json + { + "plans": [ + { + "content": "You need to generate a command to check which network scanning tools the current machine supports", + "tool": "command_generator", + "instruction": "Select which network scanning tools the previous machine supports" + }, + { + "content": "Execute the command generated in Result[0] to check which network scanning tools the current machine supports", + "tool": "command_executor", + "instruction": "Execute the command generated in Result[0]" + }, + { + "content": "Select a network scanning tool from Result[1] to generate a port scanning command", + "tool": "tool_selector", + "instruction": "Select a network scanning tool and generate a port scanning command" + }, + { + "content": "Generate a port scanning command based on the network scanning tool selected in result[2]", + "tool": "command_generator", + "instruction": "Generate a port scanning command: Scan the open ports of 192.168.1.1" + }, + { + "content": "Execute the command generated by Result[3] on the MCP Server of Result[0]", + "tool": "command_executor", + "instruction": "Execute the port scanning command" + }, + { + "content": "Task execution completed, the port scanning result is Result[4]", + "tool": "Final", + "instruction": "" + } + ] + } + ``` + + # Now start to regenerate the plan: + + # Goal + + {{goal}} + + # Tools + + You can access and use some tools, which will be given in the XML tags. + + + {% for tool in tools %} + - {{tool.id}} {{tool.name}}; {{tool.description}} + {% endfor %} + + + # Current plan + {{current_plan}} + + # Run error + {{error_message}} + + # Regenerated plan + """ + ), +} +RISK_EVALUATE: dict[LanguageType, str] = { + LanguageType.CHINESE: dedent( + r""" + 你是一个工具执行计划评估器。 + 你的任务是根据当前工具的名称、描述和入参以及附加信息,判断当前工具执行的风险并输出提示。 + ```json + { + "risk": "low/medium/high", + "reason": "提示信息" + } + ``` + # 样例 + # 工具名称 + mysql_analyzer + # 工具描述 + 分析MySQL数据库性能 + # 工具入参 + { + "host": "192.0.0.1", + "port": 3306, + "username": "root", + "password": "password" + } + # 附加信息 + 1. 当前MySQL数据库的版本是8.0.26 + 2. 当前MySQL数据库的配置文件路径是/etc/my.cnf,并含有以下配置项 + ```ini + [mysqld] + innodb_buffer_pool_size=1G + innodb_log_file_size=256M + ``` + # 输出 + ```json + { + "risk": "中", + "reason": "当前工具将连接到MySQL数据库并分析性能,可能会对数据库性能产生一定影响。请确保在非生产环境中执行此操作。" + } + ``` + # 工具 + + {{tool_name}} + {{tool_description}} + + # 工具入参 + {{input_param}} + # 附加信息 + {{additional_info}} + # 输出 + """ + ), + LanguageType.ENGLISH: dedent( + r""" + You are a tool execution plan evaluator. + Your task is to determine the risk of executing the current tool based on its name, description, input parameters, and additional information, and output a warning. + ```json + { + "risk": "low/medium/high", + "reason": "prompt message" + } + ``` + # Example + # Tool name + mysql_analyzer + # Tool description + Analyzes MySQL database performance + # Tool input + { + "host": "192.0.0.1", + "port": 3306, + "username": "root", + "password": "password" + } + # Additional information + 1. The current MySQL database version is 8.0.26 + 2. The current MySQL database configuration file path is /etc/my.cnf and contains the following configuration items + ```ini + [mysqld] + innodb_buffer_pool_size=1G + innodb_log_file_size=256M + ``` + # Output + ```json + { + "risk": "medium", + "reason": "This tool will connect to a MySQL database and analyze performance, which may impact database performance. This operation should only be performed in a non-production environment." + } + ``` + # Tool + + {{tool_name}} + {{tool_description}} + + # Tool Input Parameters + {{input_param}} + # Additional Information + {{additional_info}} + # Output + + """ + ), +} +# 根据当前计划和报错信息决定下一步执行,具体计划有需要用户补充工具入参、重计划当前步骤、重计划接下来的所有计划 +TOOL_EXECUTE_ERROR_TYPE_ANALYSIS: dict[LanguageType, str] = { + LanguageType.CHINESE: dedent( + r""" + 你是一个计划决策器。 + + 你的任务是根据用户目标、当前计划、当前使用的工具、工具入参和工具运行报错,决定下一步执行的操作。 + 请根据以下规则进行判断: + 1. 仅通过补充工具入参来解决问题的,返回 missing_param; + 2. 需要重计划当前步骤的,返回 decorrect_plan + 3.推理过程必须清晰明了,能够让人理解你的判断依据,并且不超过100字。 + 你的输出要以json格式返回,格式如下: + ```json { "error_type": "missing_param/decorrect_plan, "reason": "你的推理过程" } ``` + # 样例 # 用户目标 我需要扫描当前mysql数据库,分析性能瓶颈, 并调优 # 当前计划 - {"plans": [ - { - "content": "生成端口扫描命令", - "tool": "command_generator", - "instruction": "生成端口扫描命令:扫描192.168.1.1的开放端口" - }, - { - "content": "在执行Result[0]生成的命令", - "tool": "command_executor", - "instruction": "执行端口扫描命令" - }, - { - "content": "任务执行完成,端口扫描结果为Result[2]", - "tool": "Final", - "instruction": "" - } - ]} + { + "plans": [ + { + "content": "生成端口扫描命令", + "tool": "command_generator", + "instruction": "生成端口扫描命令:扫描192.168.1.1的开放端口" + }, + { + "content": "在执行Result[0]生成的命令", + "tool": "command_executor", + "instruction": "执行端口扫描命令" + }, + { + "content": "任务执行完成,端口扫描结果为Result[2]", + "tool": "Final", + "instruction": "" + } + ] + } # 当前使用的工具 command_executor @@ -616,9 +1207,87 @@ TOOL_EXECUTE_ERROR_TYPE_ANALYSIS = dedent(r""" {{error_message}} # 输出 """ - ) + ), + LanguageType.ENGLISH: dedent( + r""" + You are a plan decider. + + Your task is to decide the next action based on the user's goal, the current plan, the tool being used, tool inputs, and tool errors. + Please make your decision based on the following rules: + 1. If the problem can be solved by simply adding tool inputs, return missing_param; + 2. If the current step needs to be replanned, return decorrect_plan. + 3. Your reasoning must be clear and concise, allowing the user to understand your decision. It should not exceed 100 words. + Your output should be returned in JSON format, as follows: + + ```json + { + "error_type": "missing_param/decorrect_plan, + "reason": "Your reasoning" + } + ``` + + # Example + # User Goal + I need to scan the current MySQL database, analyze performance bottlenecks, and optimize it. + # Current Plan + { + "plans": [ + { + "content": "Generate port scan command", + "tool": "command_generator", + "instruction": "Generate port scan command: Scan the open ports of 192.168.1.1" + }, + { + "content": "Execute the command generated by Result[0]", + "tool": "command_executor", + "instruction": "Execute the port scan command" + }, + { + "content": "Task execution completed, the port scan result is Result[2]", + "tool": "Final", + "instruction": "" + } + ] + } + # Currently used tool + + command_executor + Execute command line instructions + + # Tool input parameters + { + "command": "nmap -sS -p--open 192.168.1.1" + } + # Tool running error + When executing the port scan command, an error occurred: `- bash: nmap: command not found`. + # Output + ```json + { + "error_type": "decorrect_plan", + "reason": "The second step of the current plan failed. The error message shows that the nmap command was not found. This may be because the nmap tool is not installed. Therefore, the current step needs to be replanned." + } + ``` + # User goal + {{goal}} + # Current plan + {{current_plan}} + # Currently used tool + + {{tool_name}} + {{tool_description}} + + # Tool input parameters + {{input_param}} + # Tool execution error + {{error_message}} + # Output + """ + ), +} # 将当前程序运行的报错转换为自然语言 -CHANGE_ERROR_MESSAGE_TO_DESCRIPTION = dedent(r""" +CHANGE_ERROR_MESSAGE_TO_DESCRIPTION: dict[LanguageType, str] = { + LanguageType.CHINESE: dedent( + r""" 你是一个智能助手,你的任务是将当前程序运行的报错转换为自然语言描述。 请根据以下规则进行转换: 1. 将报错信息转换为自然语言描述,描述应该简洁明了,能够让人理解报错的原因和影响。 @@ -681,9 +1350,79 @@ CHANGE_ERROR_MESSAGE_TO_DESCRIPTION = dedent(r""" # 报错信息 {{error_message}} # 输出 - """) + """ + ), + LanguageType.ENGLISH: dedent( + r""" + You are an intelligent assistant. Your task is to convert the error message generated by the current program into a natural language description. + Please follow the following rules for conversion: + 1. Convert the error message into a natural language description. The description should be concise and clear, allowing users to understand the cause and impact of the error. + 2. The description should include the specific content of the error and possible solutions. + 3. The description should avoid using overly technical terms so that users can understand it. + 4. The description should be as brief as possible, within 50 words. + 5. Only output the natural language description, do not output other content. + # Example + # Tool Information + + port_scanner + Scan host ports + + { + "type": "object", + "properties": { + "host": { + "type": "string", + "description": "Host address" + }, + "port": { + "type": "integer", + "description": "Port number" + }, + "username": { + "type": "string", + "description": "Username" + }, + "password": { + "type": "string", + "description": "Password" + } + }, + "required": ["host", "port", "username", "password"] + } + + + # Tool input + { + "host": "192.0.0.1", + "port": 3306, + "username": "root", + "password": "password" + } + # Error message + An error occurred while executing the port scan command: `password is not correct`. + # Output + An error occurred while scanning the port: The password is incorrect. Please check that the password you entered is correct and try again. + # Now start converting the error message: + # Tool information + + {{tool_name}} + {{tool_description}} + + {{input_schema}} + + + # Tool input parameters + {{input_params}} + # Error message + {{error_message}} + # Output + """ + ), +} # 获取缺失的参数的json结构体 -GET_MISSING_PARAMS = dedent(r""" +GET_MISSING_PARAMS: dict[LanguageType, str] = { + LanguageType.CHINESE: dedent( + r""" 你是一个工具参数获取器。 你的任务是根据当前工具的名称、描述和入参和入参的schema以及运行报错,将当前缺失的参数设置为null,并输出一个JSON格式的字符串。 ```json @@ -708,42 +1447,126 @@ GET_MISSING_PARAMS = dedent(r""" } # 工具入参schema { - "type": "object", - "properties": { + "type": "object", + "properties": { + "host": { + "anyOf": [ + {"type": "string"}, + {"type": "null"} + ], + "description": "MySQL数据库的主机地址(可以为字符串或null)" + }, + "port": { + "anyOf": [ + {"type": "string"}, + {"type": "null"} + ], + "description": "MySQL数据库的端口号(可以是数字、字符串或null)" + }, + "username": { + "anyOf": [ + {"type": "string"}, + {"type": "null"} + ], + "description": "MySQL数据库的用户名(可以为字符串或null)" + }, + "password": { + "anyOf": [ + {"type": "string"}, + {"type": "null"} + ], + "description": "MySQL数据库的密码(可以为字符串或null)" + } + }, + "required": ["host", "port", "username", "password"] + } + # 运行报错 + 执行端口扫描命令时,出现了错误:`password is not correct`。 + # 输出 + ```json + { + "host": "192.0.0.1", + "port": 3306, + "username": null, + "password": null + } + ``` + # 工具 + + {{tool_name}} + {{tool_description}} + + # 工具入参 + {{input_param}} + # 工具入参schema(部分字段允许为null) + {{input_schema}} + # 运行报错 + {{error_message}} + # 输出 + """ + ), + LanguageType.ENGLISH: dedent( + r""" + You are a tool parameter getter. + Your task is to set missing parameters to null based on the current tool's name, description, input parameters, input parameter schema, and runtime errors, and output a JSON-formatted string. + ```json + { + "host": "Please provide the host address", + "port": "Please provide the port number", + "username": "Please provide the username", + "password": "Please provide the password" + } + ``` + # Example + # Tool Name + mysql_analyzer + # Tool Description + Analyze MySQL database performance + # Tool Input Parameters + { + "host": "192.0.0.1", + "port": 3306, + "username": "root", + "password": "password" + } + # Tool Input Parameter Schema + { + "type": "object", + "properties": { "host": { "anyOf": [ {"type": "string"}, {"type": "null"} ], - "description": "MySQL数据库的主机地址(可以为字符串或null)" + "description": "MySQL database host address (can be a string or null)" }, "port": { "anyOf": [ {"type": "string"}, {"type": "null"} ], - "description": "MySQL数据库的端口号(可以是数字、字符串或null)" + "description": "MySQL database port number (can be a number, a string, or null)" }, "username": { "anyOf": [ {"type": "string"}, {"type": "null"} ], - "description": "MySQL数据库的用户名(可以为字符串或null)" + "description": "MySQL database username (can be a string or null)" }, "password": { "anyOf": [ - {"type": "string"}, - {"type": "null"} - ], - "description": "MySQL数据库的密码(可以为字符串或null)" - } - }, + {"type": "string"}, + {"type": "null"} + ], + "description": "MySQL database password (can be a string or null)" + } + }, "required": ["host", "port", "username", "password"] } - # 运行报错 - 执行端口扫描命令时,出现了错误:`password is not correct`。 - # 输出 + # Run error + When executing the port scan command, an error occurred: `password is not correct`. + # Output ```json { "host": "192.0.0.1", @@ -752,21 +1575,24 @@ GET_MISSING_PARAMS = dedent(r""" "password": null } ``` - # 工具 + # Tool {{tool_name}} {{tool_description}} - # 工具入参 + # Tool input parameters {{input_param}} - # 工具入参schema(部分字段允许为null) + # Tool input parameter schema (some fields can be null) {{input_schema}} - # 运行报错 + # Run error {{error_message}} - # 输出 + # Output """ - ) -REPAIR_PARAMS = dedent(r""" + ), +} +REPAIR_PARAMS: dict[LanguageType, str] = { + LanguageType.CHINESE: dedent( + r""" 你是一个工具参数修复器。 你的任务是根据当前的工具信息、工具入参的schema、工具当前的入参、工具的报错、补充的参数和补充的参数描述,修复当前工具的入参。 @@ -829,7 +1655,7 @@ REPAIR_PARAMS = dedent(r""" {{tool_name}} {{tool_description}} - # 工具入参scheme + # 工具入参schema {{input_schema}} # 工具入参 {{input_param}} @@ -841,8 +1667,88 @@ REPAIR_PARAMS = dedent(r""" {{params_description}} # 输出 """ - ) -FINAL_ANSWER = dedent(r""" + ), + LanguageType.ENGLISH: dedent( + r""" + You are a tool parameter fixer. + Your task is to fix the current tool input parameters based on the current tool information, tool input parameter schema, tool current input parameters, tool error, supplemented parameters, and supplemented parameter descriptions. + + # Example + # Tool information + + mysql_analyzer + Analyze MySQL database performance + + # Tool input parameter schema + { + "type": "object", + "properties": { + "host": { + "type": "string", + "description": "MySQL database host address" + }, + "port": { + "type": "integer", + "description": "MySQL database port number" + }, + "username": { + "type": "string", + "description": "MySQL database username" + }, + "password": { + "type": "string", + "description": "MySQL database password" + } + }, + "required": ["host", "port", "username", "password"] + } + # Current tool input parameters + { + "host": "192.0.0.1", + "port": 3306, + "username": "root", + "password": "password" + } + # Tool error + When executing the port scan command, an error occurred: `password is not correct`. + # Supplementary parameters + { + "username": "admin", + "password": "admin123" + } + # Supplementary parameter description + The user wants to use the admin user and the admin123 password to connect to the MySQL database. + # Output + ```json + { + "host": "192.0.0.1", + "port": 3306, + "username": "admin", + "password": "admin123" + } + ``` + # Tool + + {{tool_name}} + {{tool_description}} + + # Tool input schema + {{input_schema}} + # Tool input parameters + {{input_param}} + # Runtime error + {{error_message}} + # Supplementary parameters + {{params}} + # Supplementary parameter descriptions + {{params_description}} + # Output + """ + ), +} +FINAL_ANSWER: dict[LanguageType, str] = { + LanguageType.CHINESE: dedent( + r""" 综合理解计划执行结果和背景信息,向用户报告目标的完成情况。 # 用户目标 @@ -861,12 +1767,50 @@ FINAL_ANSWER = dedent(r""" # 现在,请根据以上信息,向用户报告目标的完成情况: -""") -MEMORY_TEMPLATE = dedent(r""" + """ + ), + LanguageType.ENGLISH: dedent( + r""" + Comprehensively understand the plan execution results and background information, and report the goal completion status to the user. + + # User Goal + + {{goal}} + + # Plan Execution Status + + To achieve the above goal, you implemented the following plan: + + {{memory}} + + # Additional Background Information: + + {{status}} + + # Now, based on the above information, report the goal completion status to the user: + + """ + ), +} +MEMORY_TEMPLATE: dict[LanguageType, str] = { + LanguageType.CHINESE: dedent( + r""" {% for ctx in context_list %} - 第{{loop.index}}步:{{ctx.step_description}} - - 调用工具 `{{ctx.step_id}}`,并提供参数 `{{ctx.input_data}}` - - 执行状态:{{ctx.status}} - - 得到数据:`{{ctx.output_data}}` + - 调用工具 `{{ctx.step_id}}`,并提供参数 `{{ctx.input_data}}` + - 执行状态:{{ctx.status}} + - 得到数据:`{{ctx.output_data}}` {% endfor %} -""") + """ + ), + LanguageType.ENGLISH: dedent( + r""" + {% for ctx in context_list %} + - Step {{loop.index}}: {{ctx.step_description}} + - Call the tool `{{ctx.step_id}}` and provide the parameter `{{ctx.input_data}}` + - Execution status: {{ctx.status}} + - Receive data: `{{ctx.output_data}}` + {% endfor %} + """ + ), +} diff --git a/apps/scheduler/mcp_agent/select.py b/apps/scheduler/mcp_agent/select.py index 075e08f0..37273689 100644 --- a/apps/scheduler/mcp_agent/select.py +++ b/apps/scheduler/mcp_agent/select.py @@ -24,6 +24,7 @@ from apps.schemas.mcp import ( MCPTool, MCPToolIdsSelectResult ) +from apps.schemas.enum_var import LanguageType from apps.common.config import Config logger = logging.getLogger(__name__) @@ -40,6 +41,7 @@ SUMMARIZE_TOOL_ID = "SUMMARIZE" class MCPSelector(McpBase): """MCP选择器""" + language:LanguageType = LanguageType.CHINESE @staticmethod async def select_top_tool( @@ -49,7 +51,7 @@ class MCPSelector(McpBase): """选择最合适的工具""" random.shuffle(tool_list) max_tokens = reasoning_llm._config.max_tokens - template = _env.from_string(TOOL_SELECT) + template = _env.from_string(TOOL_SELECT[MCPSelector.language]) token_calculator = TokenCalculator() if token_calculator.calculate_token_length( messages=[{"role": "user", "content": template.render( diff --git a/apps/schemas/task.py b/apps/schemas/task.py index 680a43f4..7f68d77c 100644 --- a/apps/schemas/task.py +++ b/apps/schemas/task.py @@ -98,7 +98,6 @@ class Task(BaseModel): state: ExecutorState = Field(description="Flow的状态", default=ExecutorState()) tokens: TaskTokens = Field(description="Token信息") runtime: TaskRuntime = Field(description="任务运行时数据") - language: str = Field(description="语言", default="zh") created_at: float = Field(default_factory=lambda: round(datetime.now(tz=UTC).timestamp(), 3)) language: LanguageType = Field(description="语言", default=LanguageType.CHINESE) -- Gitee From 5b235f2f9e89e38ee1e19bc1ada1e5ba9cdcc7c7 Mon Sep 17 00:00:00 2001 From: zhanghb <2428333123@qq.com> Date: Mon, 11 Aug 2025 15:31:50 +0800 Subject: [PATCH 11/15] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/scheduler/executor/agent.py | 79 +++++++++----- apps/scheduler/mcp_agent/host.py | 21 ++-- apps/scheduler/mcp_agent/plan.py | 162 ++++++++++++++++++++--------- apps/scheduler/mcp_agent/select.py | 76 +++++++++----- 4 files changed, 224 insertions(+), 114 deletions(-) diff --git a/apps/scheduler/executor/agent.py b/apps/scheduler/executor/agent.py index 6df073f5..ed2d1518 100644 --- a/apps/scheduler/executor/agent.py +++ b/apps/scheduler/executor/agent.py @@ -55,11 +55,6 @@ class MCPAgentExecutor(BaseExecutor): description="推理大模型", ) - def __init__(self): - MCPPlanner.language = self.task.language - MCPHost.language = self.task.language - MCPSelector.language = self.task.language - async def update_tokens(self) -> None: """更新令牌数""" self.task.tokens.input_tokens = self.resoning_llm.input_tokens @@ -100,26 +95,36 @@ class MCPAgentExecutor(BaseExecutor): else: error_message = "初始化计划" tools = await MCPSelector.select_top_tool( - self.task.runtime.question, list(self.tools.values()), - additional_info=error_message, top_n=40, reasoning_llm=self.resoning_llm) + self.task.runtime.question, + list(self.tools.values()), + additional_info=error_message, + top_n=40, + reasoning_llm=self.resoning_llm, + language=self.task.language, + ) if is_replan: logger.info("[MCPAgentExecutor] 重新规划流程") if not start_index: - start_index = await MCPPlanner.get_replan_start_step_index(self.task.runtime.question, - self.task.state.error_message, - self.task.runtime.temporary_plans, - self.resoning_llm) + start_index = await MCPPlanner.get_replan_start_step_index( + self.task.runtime.question, + self.task.state.error_message, + self.task.runtime.temporary_plans, + self.resoning_llm, + self.task.language, + ) start_index = start_index.start_index current_plan = MCPPlan(plans=self.task.runtime.temporary_plans.plans[start_index:]) error_message = self.task.state.error_message - temporary_plans = await MCPPlanner.create_plan(self.task.runtime.question, - is_replan=is_replan, - error_message=error_message, - current_plan=current_plan, - tool_list=tools, - max_steps=self.max_steps-start_index-1, - reasoning_llm=self.resoning_llm - ) + temporary_plans = await MCPPlanner.create_plan( + self.task.runtime.question, + is_replan=is_replan, + error_message=error_message, + current_plan=current_plan, + tool_list=tools, + max_steps=self.max_steps - start_index - 1, + reasoning_llm=self.resoning_llm, + language=self.task.language, + ) await self.update_tokens() await self.push_message( EventType.STEP_CANCEL, @@ -134,7 +139,12 @@ class MCPAgentExecutor(BaseExecutor): start_index = 0 logger.error( f"各个字段的类型: {type(self.task.runtime.question)}, {type(tools)}, {type(self.max_steps)}, {type(self.resoning_llm)}") - self.task.runtime.temporary_plans = await MCPPlanner.create_plan(self.task.runtime.question, tool_list=tools, max_steps=self.max_steps, reasoning_llm=self.resoning_llm) + self.task.runtime.temporary_plans = await MCPPlanner.create_plan( + self.task.runtime.question, + tool_list=tools, + max_steps=self.max_steps, + reasoning_llm=self.resoning_llm, + ) for i in range(start_index, len(self.task.runtime.temporary_plans.plans)): self.task.runtime.temporary_plans.plans[i].step_id = str(uuid.uuid4()) @@ -155,7 +165,14 @@ class MCPAgentExecutor(BaseExecutor): params_description = "" tool_id = self.task.runtime.temporary_plans.plans[self.task.state.step_index].tool mcp_tool = self.tools[tool_id] - self.task.state.current_input = await MCPHost._fill_params(mcp_tool, self.task.state.current_input, self.task.state.error_message, params, params_description) + self.task.state.current_input = await MCPHost._fill_params( + mcp_tool, + self.task.state.current_input, + self.task.state.error_message, + params, + params_description, + self.task.language, + ) async def reset_step_to_index(self, start_index: int) -> None: """重置步骤到开始""" @@ -177,7 +194,9 @@ class MCPAgentExecutor(BaseExecutor): # 发送确认消息 tool_id = self.task.runtime.temporary_plans.plans[self.task.state.step_index].tool mcp_tool = self.tools[tool_id] - confirm_message = await MCPPlanner.get_tool_risk(mcp_tool, self.task.state.current_input, "", self.resoning_llm) + confirm_message = await MCPPlanner.get_tool_risk( + mcp_tool, self.task.state.current_input, "", self.resoning_llm, self.task.language + ) await self.update_tokens() await self.push_message(EventType.STEP_WAITING_FOR_START, confirm_message.model_dump( exclude_none=True, by_alias=True)) @@ -274,14 +293,16 @@ class MCPAgentExecutor(BaseExecutor): mcp_tool, self.task.state.current_input, self.task.state.error_message, - self.resoning_llm + self.resoning_llm, + self.task.language ) await self.update_tokens() error_message = MCPPlanner.change_err_message_to_description( error_message=self.task.state.error_message, tool=mcp_tool, input_params=self.task.state.current_input, - reasoning_llm=self.resoning_llm + reasoning_llm=self.resoning_llm, + language=self.task.language ) await self.push_message( EventType.STEP_WAITING_FOR_PARAM, @@ -415,7 +436,8 @@ class MCPAgentExecutor(BaseExecutor): mcp_tool, self.task.state.current_input, self.task.state.error_message, - self.resoning_llm + self.resoning_llm, + self.task.language ) if error_type.type == ErrorType.DECORRECT_PLAN or user_info.auto_execute: await self.plan(is_replan=True) @@ -430,7 +452,8 @@ class MCPAgentExecutor(BaseExecutor): self.task.runtime.question, self.task.runtime.temporary_plans, (await MCPHost.assemble_memory(self.task)), - self.resoning_llm + self.resoning_llm, + self.task.language ): await self.push_message( EventType.TEXT_ADD, @@ -447,7 +470,9 @@ class MCPAgentExecutor(BaseExecutor): # 初始化状态 try: self.task.state.flow_id = str(uuid.uuid4()) - self.task.state.flow_name = await MCPPlanner.get_flow_name(self.task.runtime.question, self.resoning_llm) + self.task.state.flow_name = await MCPPlanner.get_flow_name( + self.task.runtime.question, self.resoning_llm, self.task.language + ) await self.plan(is_replan=False) await self.reset_step_to_index(0) await TaskManager.save_task(self.task.id, self.task) diff --git a/apps/scheduler/mcp_agent/host.py b/apps/scheduler/mcp_agent/host.py index dcc211de..716152d0 100644 --- a/apps/scheduler/mcp_agent/host.py +++ b/apps/scheduler/mcp_agent/host.py @@ -31,21 +31,20 @@ _env = SandboxedEnvironment( def tojson_filter(value): - return json.dumps(value, ensure_ascii=False, separators=(',', ':')) + return json.dumps(value, ensure_ascii=False, separators=(",", ":")) -_env.filters['tojson'] = tojson_filter +_env.filters["tojson"] = tojson_filter class MCPHost: """MCP宿主服务""" - language:LanguageType = LanguageType.CHINESE @staticmethod async def assemble_memory(task: Task) -> str: """组装记忆""" - return _env.from_string(MEMORY_TEMPLATE[MCPHost.language]).render( + return _env.from_string(MEMORY_TEMPLATE[task.language]).render( context_list=task.context, ) @@ -69,12 +68,16 @@ class MCPHost: ) return await json_generator.generate() - async def _fill_params(mcp_tool: MCPTool, - current_input: dict[str, Any], - error_message: str = "", params: dict[str, Any] = {}, - params_description: str = "") -> dict[str, Any]: + async def _fill_params( + mcp_tool: MCPTool, + current_input: dict[str, Any], + error_message: str = "", + params: dict[str, Any] = {}, + params_description: str = "", + language: LanguageType = LanguageType.CHINESE, + ) -> dict[str, Any]: llm_query = "请生成修复之后的工具参数" - prompt = _env.from_string(REPAIR_PARAMS[MCPHost.language]).render( + prompt = _env.from_string(REPAIR_PARAMS[language]).render( tool_name=mcp_tool.name, tool_description=mcp_tool.description, input_schema=mcp_tool.input_schema, diff --git a/apps/scheduler/mcp_agent/plan.py b/apps/scheduler/mcp_agent/plan.py index 543e4f42..bbf79c1e 100644 --- a/apps/scheduler/mcp_agent/plan.py +++ b/apps/scheduler/mcp_agent/plan.py @@ -17,7 +17,7 @@ from apps.scheduler.mcp_agent.prompt import ( TOOL_EXECUTE_ERROR_TYPE_ANALYSIS, CHANGE_ERROR_MESSAGE_TO_DESCRIPTION, GET_MISSING_PARAMS, - FINAL_ANSWER + FINAL_ANSWER, ) from apps.schemas.mcp import ( GoalEvaluationResult, @@ -25,7 +25,7 @@ from apps.schemas.mcp import ( ToolRisk, ToolExcutionErrorType, MCPPlan, - MCPTool + MCPTool, ) from apps.schemas.enum_var import LanguageType from apps.scheduler.slot.slot import Slot @@ -40,16 +40,17 @@ _env = SandboxedEnvironment( class MCPPlanner(McpBase): """MCP 用户目标拆解与规划""" - language:LanguageType = LanguageType.CHINESE @staticmethod async def evaluate_goal( - goal: str, - tool_list: list[MCPTool], - resoning_llm: ReasoningLLM = ReasoningLLM()) -> GoalEvaluationResult: + goal: str, + tool_list: list[MCPTool], + resoning_llm: ReasoningLLM = ReasoningLLM(), + language: LanguageType = LanguageType.CHINESE, + ) -> GoalEvaluationResult: """评估用户目标的可行性""" # 获取推理结果 - result = await MCPPlanner._get_reasoning_evaluation(goal, tool_list, resoning_llm) + result = await MCPPlanner._get_reasoning_evaluation(goal, tool_list, resoning_llm, language) # 解析为结构化数据 evaluation = await MCPPlanner._parse_evaluation_result(result) @@ -59,10 +60,13 @@ class MCPPlanner(McpBase): @staticmethod async def _get_reasoning_evaluation( - goal, tool_list: list[MCPTool], - resoning_llm: ReasoningLLM = ReasoningLLM()) -> str: + goal, + tool_list: list[MCPTool], + resoning_llm: ReasoningLLM = ReasoningLLM(), + language: LanguageType = LanguageType.CHINESE, + ) -> str: """获取推理大模型的评估结果""" - template = _env.from_string(EVALUATE_GOAL[MCPPlanner.language]) + template = _env.from_string(EVALUATE_GOAL[language]) prompt = template.render( goal=goal, tools=tool_list, @@ -78,27 +82,39 @@ class MCPPlanner(McpBase): # 使用GoalEvaluationResult模型解析结果 return GoalEvaluationResult.model_validate(evaluation) - async def get_flow_name(user_goal: str, resoning_llm: ReasoningLLM = ReasoningLLM()) -> str: + async def get_flow_name( + user_goal: str, + resoning_llm: ReasoningLLM = ReasoningLLM(), + language: LanguageType = LanguageType.CHINESE, + ) -> str: """获取当前流程的名称""" - result = await MCPPlanner._get_reasoning_flow_name(user_goal, resoning_llm) + result = await MCPPlanner._get_reasoning_flow_name(user_goal, resoning_llm, language) return result @staticmethod - async def _get_reasoning_flow_name(user_goal: str, resoning_llm: ReasoningLLM = ReasoningLLM()) -> str: + async def _get_reasoning_flow_name( + user_goal: str, + resoning_llm: ReasoningLLM = ReasoningLLM(), + language: LanguageType = LanguageType.CHINESE, + ) -> str: """获取推理大模型的流程名称""" - template = _env.from_string(GENERATE_FLOW_NAME[MCPPlanner.language]) + template = _env.from_string(GENERATE_FLOW_NAME[language]) prompt = template.render(goal=user_goal) result = await MCPPlanner.get_resoning_result(prompt, resoning_llm) return result @staticmethod async def get_replan_start_step_index( - user_goal: str, error_message: str, current_plan: MCPPlan | None = None, - history: str = "", - reasoning_llm: ReasoningLLM = ReasoningLLM()) -> RestartStepIndex: + user_goal: str, + error_message: str, + current_plan: MCPPlan | None = None, + history: str = "", + reasoning_llm: ReasoningLLM = ReasoningLLM(), + language: LanguageType = LanguageType.CHINESE, + ) -> RestartStepIndex: """获取重新规划的步骤索引""" # 获取推理结果 - template = _env.from_string(GET_REPLAN_START_STEP_INDEX[MCPPlanner.language]) + template = _env.from_string(GET_REPLAN_START_STEP_INDEX[language]) prompt = template.render( goal=user_goal, error_message=error_message, @@ -116,25 +132,39 @@ class MCPPlanner(McpBase): @staticmethod async def create_plan( - user_goal: str, is_replan: bool = False, error_message: str = "", current_plan: MCPPlan | None = None, - tool_list: list[MCPTool] = [], - max_steps: int = 6, reasoning_llm: ReasoningLLM = ReasoningLLM()) -> MCPPlan: + user_goal: str, + is_replan: bool = False, + error_message: str = "", + current_plan: MCPPlan | None = None, + tool_list: list[MCPTool] = [], + max_steps: int = 6, + reasoning_llm: ReasoningLLM = ReasoningLLM(), + language: LanguageType = LanguageType.CHINESE, + ) -> MCPPlan: """规划下一步的执行流程,并输出""" # 获取推理结果 - result = await MCPPlanner._get_reasoning_plan(user_goal, is_replan, error_message, current_plan, tool_list, max_steps, reasoning_llm) + result = await MCPPlanner._get_reasoning_plan( + user_goal, is_replan, error_message, current_plan, tool_list, max_steps, reasoning_llm, language + ) # 解析为结构化数据 return await MCPPlanner._parse_plan_result(result, max_steps) @staticmethod async def _get_reasoning_plan( - user_goal: str, is_replan: bool = False, error_message: str = "", current_plan: MCPPlan | None = None, - tool_list: list[MCPTool] = [], - max_steps: int = 10, reasoning_llm: ReasoningLLM = ReasoningLLM()) -> str: + user_goal: str, + is_replan: bool = False, + error_message: str = "", + current_plan: MCPPlan | None = None, + tool_list: list[MCPTool] = [], + max_steps: int = 10, + reasoning_llm: ReasoningLLM = ReasoningLLM(), + language: LanguageType = LanguageType.CHINESE, + ) -> str: """获取推理大模型的结果""" # 格式化Prompt if is_replan: - template = _env.from_string(RECREATE_PLAN[MCPPlanner.language]) + template = _env.from_string(RECREATE_PLAN[language]) prompt = template.render( current_plan=current_plan.model_dump(exclude_none=True, by_alias=True), error_message=error_message, @@ -143,7 +173,7 @@ class MCPPlanner(McpBase): max_num=max_steps, ) else: - template = _env.from_string(CREATE_PLAN[MCPPlanner.language]) + template = _env.from_string(CREATE_PLAN[language]) prompt = template.render( goal=user_goal, tools=tool_list, @@ -164,11 +194,15 @@ class MCPPlanner(McpBase): @staticmethod async def get_tool_risk( - tool: MCPTool, input_parm: dict[str, Any], - additional_info: str = "", resoning_llm: ReasoningLLM = ReasoningLLM()) -> ToolRisk: + tool: MCPTool, + input_parm: dict[str, Any], + additional_info: str = "", + resoning_llm: ReasoningLLM = ReasoningLLM(), + language: LanguageType = LanguageType.CHINESE, + ) -> ToolRisk: """获取MCP工具的风险评估结果""" # 获取推理结果 - result = await MCPPlanner._get_reasoning_risk(tool, input_parm, additional_info, resoning_llm) + result = await MCPPlanner._get_reasoning_risk(tool, input_parm, additional_info, resoning_llm, language) # 解析为结构化数据 risk = await MCPPlanner._parse_risk_result(result) @@ -178,10 +212,14 @@ class MCPPlanner(McpBase): @staticmethod async def _get_reasoning_risk( - tool: MCPTool, input_param: dict[str, Any], - additional_info: str, resoning_llm: ReasoningLLM) -> str: + tool: MCPTool, + input_param: dict[str, Any], + additional_info: str, + resoning_llm: ReasoningLLM, + language: LanguageType = LanguageType.CHINESE, + ) -> str: """获取推理大模型的风险评估结果""" - template = _env.from_string(RISK_EVALUATE[MCPPlanner.language]) + template = _env.from_string(RISK_EVALUATE[language]) prompt = template.render( tool_name=tool.name, tool_description=tool.description, @@ -201,11 +239,16 @@ class MCPPlanner(McpBase): @staticmethod async def _get_reasoning_tool_execute_error_type( - user_goal: str, current_plan: MCPPlan, - tool: MCPTool, input_param: dict[str, Any], - error_message: str, reasoning_llm: ReasoningLLM = ReasoningLLM()) -> str: + user_goal: str, + current_plan: MCPPlan, + tool: MCPTool, + input_param: dict[str, Any], + error_message: str, + reasoning_llm: ReasoningLLM = ReasoningLLM(), + language: LanguageType = LanguageType.CHINESE, + ) -> str: """获取推理大模型的工具执行错误类型""" - template = _env.from_string(TOOL_EXECUTE_ERROR_TYPE_ANALYSIS[MCPPlanner.language]) + template = _env.from_string(TOOL_EXECUTE_ERROR_TYPE_ANALYSIS[language]) prompt = template.render( goal=user_goal, current_plan=current_plan.model_dump(exclude_none=True, by_alias=True), @@ -227,23 +270,33 @@ class MCPPlanner(McpBase): @staticmethod async def get_tool_execute_error_type( - user_goal: str, current_plan: MCPPlan, - tool: MCPTool, input_param: dict[str, Any], - error_message: str, reasoning_llm: ReasoningLLM = ReasoningLLM()) -> ToolExcutionErrorType: + user_goal: str, + current_plan: MCPPlan, + tool: MCPTool, + input_param: dict[str, Any], + error_message: str, + reasoning_llm: ReasoningLLM = ReasoningLLM(), + language: LanguageType = LanguageType.CHINESE, + ) -> ToolExcutionErrorType: """获取MCP工具执行错误类型""" # 获取推理结果 result = await MCPPlanner._get_reasoning_tool_execute_error_type( - user_goal, current_plan, tool, input_param, error_message, reasoning_llm) + user_goal, current_plan, tool, input_param, error_message, reasoning_llm, language + ) error_type = await MCPPlanner._parse_tool_execute_error_type_result(result) # 返回工具执行错误类型 return error_type @staticmethod async def change_err_message_to_description( - error_message: str, tool: MCPTool, input_params: dict[str, Any], - reasoning_llm: ReasoningLLM = ReasoningLLM()) -> str: + error_message: str, + tool: MCPTool, + input_params: dict[str, Any], + reasoning_llm: ReasoningLLM = ReasoningLLM(), + language: LanguageType = LanguageType.CHINESE, + ) -> str: """将错误信息转换为工具描述""" - template = _env.from_string(CHANGE_ERROR_MESSAGE_TO_DESCRIPTION[MCPPlanner.language]) + template = _env.from_string(CHANGE_ERROR_MESSAGE_TO_DESCRIPTION[language]) prompt = template.render( error_message=error_message, tool_name=tool.name, @@ -256,12 +309,15 @@ class MCPPlanner(McpBase): @staticmethod async def get_missing_param( - tool: MCPTool, - input_param: dict[str, Any], - error_message: str, reasoning_llm: ReasoningLLM = ReasoningLLM()) -> list[str]: + tool: MCPTool, + input_param: dict[str, Any], + error_message: str, + reasoning_llm: ReasoningLLM = ReasoningLLM(), + language: LanguageType = LanguageType.CHINESE, + ) -> list[str]: """获取缺失的参数""" slot = Slot(schema=tool.input_schema) - template = _env.from_string(GET_MISSING_PARAMS[MCPPlanner.language]) + template = _env.from_string(GET_MISSING_PARAMS[language]) schema_with_null = slot.add_null_to_basic_types() prompt = template.render( tool_name=tool.name, @@ -277,10 +333,14 @@ class MCPPlanner(McpBase): @staticmethod async def generate_answer( - user_goal: str, plan: MCPPlan, memory: str, resoning_llm: ReasoningLLM = ReasoningLLM()) -> AsyncGenerator[ - str, None]: + user_goal: str, + plan: MCPPlan, + memory: str, + resoning_llm: ReasoningLLM = ReasoningLLM(), + language: LanguageType = LanguageType.CHINESE, + ) -> AsyncGenerator[str, None]: """生成最终回答""" - template = _env.from_string(FINAL_ANSWER[MCPPlanner.language]) + template = _env.from_string(FINAL_ANSWER[language]) prompt = template.render( plan=plan.model_dump(exclude_none=True, by_alias=True), memory=memory, diff --git a/apps/scheduler/mcp_agent/select.py b/apps/scheduler/mcp_agent/select.py index 37273689..fa097175 100644 --- a/apps/scheduler/mcp_agent/select.py +++ b/apps/scheduler/mcp_agent/select.py @@ -17,15 +17,10 @@ from apps.llm.reasoning import ReasoningLLM from apps.llm.token import TokenCalculator from apps.scheduler.mcp_agent.base import McpBase from apps.scheduler.mcp_agent.prompt import TOOL_SELECT -from apps.schemas.mcp import ( - BaseModel, - MCPCollection, - MCPSelectResult, - MCPTool, - MCPToolIdsSelectResult -) +from apps.schemas.mcp import BaseModel, MCPCollection, MCPSelectResult, MCPTool, MCPToolIdsSelectResult from apps.schemas.enum_var import LanguageType from apps.common.config import Config + logger = logging.getLogger(__name__) _env = SandboxedEnvironment( @@ -41,23 +36,33 @@ SUMMARIZE_TOOL_ID = "SUMMARIZE" class MCPSelector(McpBase): """MCP选择器""" - language:LanguageType = LanguageType.CHINESE @staticmethod async def select_top_tool( - goal: str, tool_list: list[MCPTool], - additional_info: str | None = None, top_n: int | None = None, - reasoning_llm: ReasoningLLM | None = None) -> list[MCPTool]: + goal: str, + tool_list: list[MCPTool], + additional_info: str | None = None, + top_n: int | None = None, + reasoning_llm: ReasoningLLM | None = None, + language: LanguageType = LanguageType.CHINESE, + ) -> list[MCPTool]: """选择最合适的工具""" random.shuffle(tool_list) max_tokens = reasoning_llm._config.max_tokens - template = _env.from_string(TOOL_SELECT[MCPSelector.language]) + template = _env.from_string(TOOL_SELECT[language]) token_calculator = TokenCalculator() - if token_calculator.calculate_token_length( - messages=[{"role": "user", "content": template.render( - goal=goal, tools=[], additional_info=additional_info - )}], - pure_text=True) > max_tokens: + if ( + token_calculator.calculate_token_length( + messages=[ + { + "role": "user", + "content": template.render(goal=goal, tools=[], additional_info=additional_info), + } + ], + pure_text=True, + ) + > max_tokens + ): logger.warning("[MCPSelector] 工具选择模板长度超过最大令牌数,无法进行选择") return [] current_index = 0 @@ -68,18 +73,31 @@ class MCPSelector(McpBase): while index < len(tool_list): tool = tool_list[index] tokens = token_calculator.calculate_token_length( - messages=[{"role": "user", "content": template.render( - goal=goal, tools=[tool], - additional_info=additional_info - )}], - pure_text=True + messages=[ + { + "role": "user", + "content": template.render( + goal=goal, tools=[tool], additional_info=additional_info + ), + } + ], + pure_text=True, ) if tokens > max_tokens: continue sub_tools.append(tool) - tokens = token_calculator.calculate_token_length(messages=[{"role": "user", "content": template.render( - goal=goal, tools=sub_tools, additional_info=additional_info)}, ], pure_text=True) + tokens = token_calculator.calculate_token_length( + messages=[ + { + "role": "user", + "content": template.render( + goal=goal, tools=sub_tools, additional_info=additional_info + ), + }, + ], + pure_text=True, + ) if tokens > max_tokens: del sub_tools[-1] break @@ -92,7 +110,10 @@ class MCPSelector(McpBase): schema["properties"]["tool_ids"]["items"] = {} # 将enum添加到items中,限制数组元素的可选值 schema["properties"]["tool_ids"]["items"]["enum"] = [tool.id for tool in sub_tools] - result = await MCPSelector.get_resoning_result(template.render(goal=goal, tools=sub_tools, additional_info="请根据目标选择对应的工具"), reasoning_llm) + result = await MCPSelector.get_resoning_result( + template.render(goal=goal, tools=sub_tools, additional_info="请根据目标选择对应的工具"), + reasoning_llm, + ) result = await MCPSelector._parse_result(result, schema) try: result = MCPToolIdsSelectResult.model_validate(result) @@ -104,8 +125,9 @@ class MCPSelector(McpBase): if top_n is not None: mcp_tools = mcp_tools[:top_n] - mcp_tools.append(MCPTool(id=FINAL_TOOL_ID, name="Final", - description="终止", mcp_id=FINAL_TOOL_ID, input_schema={})) + mcp_tools.append( + MCPTool(id=FINAL_TOOL_ID, name="Final", description="终止", mcp_id=FINAL_TOOL_ID, input_schema={}) + ) # mcp_tools.append(MCPTool(id=SUMMARIZE_TOOL_ID, name="Summarize", # description="总结工具", mcp_id=SUMMARIZE_TOOL_ID, input_schema={})) return mcp_tools -- Gitee From 358e28f32f645ac489b25059b6b14eb2ecf9fd48 Mon Sep 17 00:00:00 2001 From: zhanghb <2428333123@qq.com> Date: Tue, 12 Aug 2025 14:32:48 +0800 Subject: [PATCH 12/15] =?UTF-8?q?=E5=AE=8C=E5=96=84agent=E5=9B=BD=E9=99=85?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/scheduler/executor/agent.py | 1 + apps/scheduler/mcp_agent/host.py | 14 ++++++++++++-- apps/scheduler/scheduler/scheduler.py | 7 ++++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/apps/scheduler/executor/agent.py b/apps/scheduler/executor/agent.py index ed2d1518..3250436f 100644 --- a/apps/scheduler/executor/agent.py +++ b/apps/scheduler/executor/agent.py @@ -144,6 +144,7 @@ class MCPAgentExecutor(BaseExecutor): tool_list=tools, max_steps=self.max_steps, reasoning_llm=self.resoning_llm, + language=self.task.language, ) for i in range(start_index, len(self.task.runtime.temporary_plans.plans)): self.task.runtime.temporary_plans.plans[i].step_id = str(uuid.uuid4()) diff --git a/apps/scheduler/mcp_agent/host.py b/apps/scheduler/mcp_agent/host.py index 716152d0..bd7d94c3 100644 --- a/apps/scheduler/mcp_agent/host.py +++ b/apps/scheduler/mcp_agent/host.py @@ -36,6 +36,16 @@ def tojson_filter(value): _env.filters["tojson"] = tojson_filter +LLM_QUERY = { + "GENERATE_TOOLS": { + LanguageType.CHINESE: "请使用参数生成工具,生成满足以下目标的工具参数:", + LanguageType.ENGLISH: "Please use the parameter to generate the tool, generate the tool parameters that meet the following goals:", + }, + "FIX_TOOLS":{ + LanguageType.CHINESE: "请生成修复之后的工具参数", + LanguageType.ENGLISH: "Please generate the tool parameters after repair" + } +} class MCPHost: """MCP宿主服务""" @@ -52,7 +62,7 @@ class MCPHost: """填充工具参数""" # 更清晰的输入·指令,这样可以调用generate llm_query = rf""" - 请使用参数生成工具,生成满足以下目标的工具参数: + {LLM_QUERY["GENERATE_TOOLS"][task.language]} {query} """ @@ -76,7 +86,7 @@ class MCPHost: params_description: str = "", language: LanguageType = LanguageType.CHINESE, ) -> dict[str, Any]: - llm_query = "请生成修复之后的工具参数" + llm_query = f"{LLM_QUERY['FIX_TOOLS'][language]}" prompt = _env.from_string(REPAIR_PARAMS[language]).render( tool_name=mcp_tool.name, tool_description=mcp_tool.description, diff --git a/apps/scheduler/scheduler/scheduler.py b/apps/scheduler/scheduler/scheduler.py index 0d603f8c..b1f67646 100644 --- a/apps/scheduler/scheduler/scheduler.py +++ b/apps/scheduler/scheduler/scheduler.py @@ -240,7 +240,12 @@ class Scheduler: if background.conversation and self.task.state.flow_status == FlowStatus.INIT: try: question_obj = QuestionRewrite() - post_body.question = await question_obj.generate(history=background.conversation, question=post_body.question, llm=reasion_llm) + post_body.question = await question_obj.generate( + history=background.conversation, + question=post_body.question, + llm=reasion_llm, + language=post_body.language, + ) except Exception: logger.exception("[Scheduler] 问题重写失败") if app_metadata.app_type == AppType.FLOW.value: -- Gitee From dd111a10f4c1e3b33a6420554155ab6e3371691e Mon Sep 17 00:00:00 2001 From: z30057876 Date: Wed, 13 Aug 2025 15:58:28 +0800 Subject: [PATCH 13/15] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E3=80=81=E7=B1=BB=E5=9E=8B=E3=80=81CleanCode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/scheduler/executor/agent.py | 245 ++++++++++++----------------- apps/scheduler/mcp_agent/base.py | 25 +-- apps/scheduler/mcp_agent/host.py | 48 +++--- apps/scheduler/mcp_agent/plan.py | 193 +++++++++++++---------- apps/scheduler/mcp_agent/prompt.py | 69 ++++---- apps/scheduler/mcp_agent/select.py | 32 ++-- apps/scheduler/pool/mcp/client.py | 9 +- apps/scheduler/pool/mcp/install.py | 28 ++-- apps/scheduler/pool/mcp/pool.py | 4 +- apps/schemas/message.py | 6 +- apps/schemas/request_data.py | 4 +- 11 files changed, 327 insertions(+), 336 deletions(-) diff --git a/apps/scheduler/executor/agent.py b/apps/scheduler/executor/agent.py index 4d34b85b..6c80685c 100644 --- a/apps/scheduler/executor/agent.py +++ b/apps/scheduler/executor/agent.py @@ -1,38 +1,32 @@ # Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. """MCP Agent执行器""" -import anyio import logging import uuid -from pydantic import Field -from typing import Any + +import anyio from mcp.types import TextContent -from apps.llm.patterns.rewrite import QuestionRewrite +from pydantic import Field + from apps.llm.reasoning import ReasoningLLM from apps.scheduler.executor.base import BaseExecutor -from apps.schemas.enum_var import EventType, SpecialCallType, FlowStatus, StepStatus from apps.scheduler.mcp_agent.host import MCPHost from apps.scheduler.mcp_agent.plan import MCPPlanner -from apps.scheduler.mcp_agent.select import FINAL_TOOL_ID, MCPSelector -from apps.scheduler.pool.mcp.client import MCPClient +from apps.scheduler.mcp_agent.select import FINAL_TOOL_ID +from apps.scheduler.pool.mcp.pool import MCPPool +from apps.schemas.enum_var import EventType, FlowStatus, StepStatus from apps.schemas.mcp import ( - GoalEvaluationResult, - RestartStepIndex, - ToolRisk, - ErrorType, - ToolExcutionErrorType, - MCPPlan, MCPCollection, MCPTool, - Step + Step, ) -from apps.scheduler.pool.mcp.pool import MCPPool -from apps.schemas.task import ExecutorState, FlowStepHistory, StepQueueItem -from apps.schemas.message import param -from apps.services.task import TaskManager +from apps.schemas.message import FlowParams +from apps.schemas.task import FlowStepHistory from apps.services.appcenter import AppCenterManager from apps.services.mcp_service import MCPServiceManager +from apps.services.task import TaskManager from apps.services.user import UserManager + logger = logging.getLogger(__name__) @@ -46,13 +40,17 @@ class MCPAgentExecutor(BaseExecutor): mcp_list: list[MCPCollection] = Field(description="MCP服务器列表", default=[]) mcp_pool: MCPPool = Field(description="MCP池", default=MCPPool()) tools: dict[str, MCPTool] = Field( - description="MCP工具列表,key为tool_id", default={} + description="MCP工具列表,key为tool_id", + default={}, ) tool_list: list[MCPTool] = Field( - description="MCP工具列表,包含所有MCP工具", default=[] + description="MCP工具列表,包含所有MCP工具", + default=[], ) - params: param | bool | None = Field( - default=None, description="流执行过程中的参数补充", alias="params" + params: FlowParams | bool | None = Field( + default=None, + description="流执行过程中的参数补充", + alias="params", ) resoning_llm: ReasoningLLM = Field( default=ReasoningLLM(), @@ -89,43 +87,53 @@ class MCPAgentExecutor(BaseExecutor): continue self.mcp_list.append(mcp_service) - await self.mcp_pool._init_mcp(mcp_id, self.task.ids.user_sub) + await self.mcp_pool.init_mcp(mcp_id, self.task.ids.user_sub) for tool in mcp_service.tools: self.tools[tool.id] = tool self.tool_list.extend(mcp_service.tools) self.tools[FINAL_TOOL_ID] = MCPTool( - id=FINAL_TOOL_ID, - name="Final Tool", - description="结束流程的工具", - mcp_id="", - input_schema={} + id=FINAL_TOOL_ID, name="Final Tool", description="结束流程的工具", mcp_id="", input_schema={}, + ) + self.tool_list.append( + MCPTool(id=FINAL_TOOL_ID, name="Final Tool", description="结束流程的工具", mcp_id="", input_schema={}), ) - self.tool_list.append(MCPTool(id=FINAL_TOOL_ID, name="Final Tool", - description="结束流程的工具", mcp_id="", input_schema={})) - async def get_tool_input_param(self, is_first: bool) -> None: + async def get_tool_input_param(self, *, is_first: bool) -> None: + """获取工具输入参数""" if is_first: # 获取第一个输入参数 mcp_tool = self.tools[self.task.state.tool_id] - self.task.state.current_input = await MCPHost._get_first_input_params(mcp_tool, self.task.runtime.question, self.task.state.step_description, self.task) + self.task.state.current_input = await MCPHost.get_first_input_params( + mcp_tool, self.task.runtime.question, self.task.state.step_description, self.task + ) else: # 获取后续输入参数 - if isinstance(self.params, param): + if isinstance(self.params, FlowParams): params = self.params.content params_description = self.params.description else: params = {} params_description = "" mcp_tool = self.tools[self.task.state.tool_id] - self.task.state.current_input = await MCPHost._fill_params(mcp_tool, self.task.runtime.question, self.task.state.step_description, self.task.state.current_input, self.task.state.error_message, params, params_description) + self.task.state.current_input = await MCPHost._fill_params( + mcp_tool, + self.task.runtime.question, + self.task.state.step_description, + self.task.state.current_input, + self.task.state.error_message, + params, + params_description, + ) async def confirm_before_step(self) -> None: + """确认前步骤""" # 发送确认消息 mcp_tool = self.tools[self.task.state.tool_id] confirm_message = await MCPPlanner.get_tool_risk(mcp_tool, self.task.state.current_input, "", self.resoning_llm) await self.update_tokens() - await self.push_message(EventType.STEP_WAITING_FOR_START, confirm_message.model_dump( - exclude_none=True, by_alias=True)) + await self.push_message( + EventType.STEP_WAITING_FOR_START, confirm_message.model_dump(exclude_none=True, by_alias=True), + ) await self.push_message(EventType.FLOW_STOP, {}) self.task.state.flow_status = FlowStatus.WAITING self.task.state.step_status = StepStatus.WAITING @@ -142,27 +150,26 @@ class MCPAgentExecutor(BaseExecutor): input_data={}, output_data={}, ex_data=confirm_message.model_dump(exclude_none=True, by_alias=True), - ) + ), ) - async def run_step(self): + async def run_step(self) -> None: """执行步骤""" self.task.state.flow_status = FlowStatus.RUNNING self.task.state.step_status = StepStatus.RUNNING mcp_tool = self.tools[self.task.state.tool_id] - mcp_client = (await self.mcp_pool.get(mcp_tool.mcp_id, self.task.ids.user_sub)) + mcp_client = await self.mcp_pool.get(mcp_tool.mcp_id, self.task.ids.user_sub) try: output_params = await mcp_client.call_tool(mcp_tool.name, self.task.state.current_input) - except anyio.ClosedResourceError as e: - import traceback - logger.error("[MCPAgentExecutor] MCP客户端连接已关闭: %s, 错误: %s", mcp_tool.mcp_id, traceback.format_exc()) + except anyio.ClosedResourceError: + logger.exception("[MCPAgentExecutor] MCP客户端连接已关闭: %s", mcp_tool.mcp_id) await self.mcp_pool.stop(mcp_tool.mcp_id, self.task.ids.user_sub) - await self.mcp_pool._init_mcp(mcp_tool.mcp_id, self.task.ids.user_sub) - logger.error("[MCPAgentExecutor] MCP客户端连接已关闭: %s, 错误: %s", mcp_tool.mcp_id, str(e)) + await self.mcp_pool.init_mcp(mcp_tool.mcp_id, self.task.ids.user_sub) self.task.state.step_status = StepStatus.ERROR return except Exception as e: import traceback + logger.exception("[MCPAgentExecutor] 执行步骤 %s 时发生错误: %s", mcp_tool.name, traceback.format_exc()) self.task.state.step_status = StepStatus.ERROR self.task.state.error_message = str(e) @@ -184,14 +191,8 @@ class MCPAgentExecutor(BaseExecutor): } await self.update_tokens() - await self.push_message( - EventType.STEP_INPUT, - self.task.state.current_input - ) - await self.push_message( - EventType.STEP_OUTPUT, - output_params - ) + await self.push_message(EventType.STEP_INPUT, self.task.state.current_input) + await self.push_message(EventType.STEP_OUTPUT, output_params) self.task.context.append( FlowStepHistory( task_id=self.task.id, @@ -204,7 +205,7 @@ class MCPAgentExecutor(BaseExecutor): flow_status=self.task.state.flow_status, input_data=self.task.state.current_input, output_data=output_params, - ) + ), ) self.task.state.step_status = StepStatus.SUCCESS @@ -212,29 +213,19 @@ class MCPAgentExecutor(BaseExecutor): """生成参数补充""" mcp_tool = self.tools[self.task.state.tool_id] params_with_null = await MCPPlanner.get_missing_param( - mcp_tool, - self.task.state.current_input, - self.task.state.error_message, - self.resoning_llm + mcp_tool, self.task.state.current_input, self.task.state.error_message, self.resoning_llm, ) await self.update_tokens() error_message = await MCPPlanner.change_err_message_to_description( error_message=self.task.state.error_message, tool=mcp_tool, input_params=self.task.state.current_input, - reasoning_llm=self.resoning_llm + reasoning_llm=self.resoning_llm, ) await self.push_message( - EventType.STEP_WAITING_FOR_PARAM, - data={ - "message": error_message, - "params": params_with_null - } - ) - await self.push_message( - EventType.FLOW_STOP, - data={} + EventType.STEP_WAITING_FOR_PARAM, data={"message": error_message, "params": params_with_null}, ) + await self.push_message(EventType.FLOW_STOP, data={}) self.task.state.flow_status = FlowStatus.WAITING self.task.state.step_status = StepStatus.PARAM self.task.context.append( @@ -249,33 +240,25 @@ class MCPAgentExecutor(BaseExecutor): flow_status=self.task.state.flow_status, input_data={}, output_data={}, - ex_data={ - "message": error_message, - "params": params_with_null - } - ) + ex_data={"message": error_message, "params": params_with_null}, + ), ) async def get_next_step(self) -> None: + """获取下一步""" if self.task.state.step_cnt < self.max_steps: self.task.state.step_cnt += 1 history = await MCPHost.assemble_memory(self.task) max_retry = 3 step = None - for i in range(max_retry): + for _ in range(max_retry): step = await MCPPlanner.create_next_step(self.task.runtime.question, history, self.tool_list) - if step.tool_id in self.tools.keys(): + if step.tool_id in self.tools: break - if step is None or step.tool_id not in self.tools.keys(): - step = Step( - tool_id=FINAL_TOOL_ID, - description=FINAL_TOOL_ID - ) + if step is None or step.tool_id not in self.tools: + step = Step(tool_id=FINAL_TOOL_ID, description=FINAL_TOOL_ID) tool_id = step.tool_id - if tool_id == FINAL_TOOL_ID: - step_name = FINAL_TOOL_ID - else: - step_name = self.tools[tool_id].name + step_name = FINAL_TOOL_ID if tool_id == FINAL_TOOL_ID else self.tools[tool_id].name step_description = step.description self.task.state.step_id = str(uuid.uuid4()) self.task.state.tool_id = tool_id @@ -286,16 +269,12 @@ class MCPAgentExecutor(BaseExecutor): else: # 没有下一步了,结束流程 self.task.state.tool_id = FINAL_TOOL_ID - return async def error_handle_after_step(self) -> None: """步骤执行失败后的错误处理""" self.task.state.step_status = StepStatus.ERROR self.task.state.flow_status = FlowStatus.ERROR - await self.push_message( - EventType.FLOW_FAILED, - data={} - ) + await self.push_message(EventType.FLOW_FAILED, data={}) if len(self.task.context) and self.task.context[-1].step_id == self.task.state.step_id: del self.task.context[-1] self.task.context.append( @@ -310,18 +289,18 @@ class MCPAgentExecutor(BaseExecutor): flow_status=self.task.state.flow_status, input_data={}, output_data={}, - ) + ), ) - async def work(self) -> None: + async def work(self) -> None: # noqa: C901, PLR0912, PLR0915 """执行当前步骤""" if self.task.state.step_status == StepStatus.INIT: - await self.push_message( - EventType.STEP_INIT, - data={} - ) + await self.push_message(EventType.STEP_INIT, data={}) await self.get_tool_input_param(is_first=True) user_info = await UserManager.get_userinfo_by_user_sub(self.task.ids.user_sub) + if user_info is None: + logger.error("[MCPAgentExecutor] 用户信息不存在: %s", self.task.ids.user_sub) + return if not user_info.auto_execute: # 等待用户确认 await self.confirm_before_step() @@ -338,14 +317,8 @@ class MCPAgentExecutor(BaseExecutor): else: self.task.state.flow_status = FlowStatus.CANCELLED self.task.state.step_status = StepStatus.CANCELLED - await self.push_message( - EventType.STEP_CANCEL, - data={} - ) - await self.push_message( - EventType.FLOW_CANCEL, - data={} - ) + await self.push_message(EventType.STEP_CANCEL, data={}) + await self.push_message(EventType.FLOW_CANCEL, data={}) if len(self.task.context) and self.task.context[-1].step_id == self.task.state.step_id: self.task.context[-1].step_status = StepStatus.CANCELLED if self.task.state.step_status == StepStatus.PARAM: @@ -363,12 +336,15 @@ class MCPAgentExecutor(BaseExecutor): await self.error_handle_after_step() else: user_info = await UserManager.get_userinfo_by_user_sub(self.task.ids.user_sub) + if user_info is None: + logger.error("[MCPAgentExecutor] 用户信息不存在: %s", self.task.ids.user_sub) + return if user_info.auto_execute: await self.push_message( EventType.STEP_ERROR, data={ "message": self.task.state.error_message, - } + }, ) if len(self.task.context) and self.task.context[-1].step_id == self.task.state.step_id: self.task.context[-1].step_status = StepStatus.ERROR @@ -394,7 +370,7 @@ class MCPAgentExecutor(BaseExecutor): EventType.STEP_ERROR, data={ "message": self.task.state.error_message, - } + }, ) if len(self.task.context) and self.task.context[-1].step_id == self.task.state.step_id: self.task.context[-1].step_status = StepStatus.ERROR @@ -406,18 +382,14 @@ class MCPAgentExecutor(BaseExecutor): await self.get_next_step() async def summarize(self) -> None: + """总结""" async for chunk in MCPPlanner.generate_answer( - self.task.runtime.question, - (await MCPHost.assemble_memory(self.task)), - self.resoning_llm + self.task.runtime.question, (await MCPHost.assemble_memory(self.task)), self.resoning_llm, ): - await self.push_message( - EventType.TEXT_ADD, - data=chunk - ) + await self.push_message(EventType.TEXT_ADD, data=chunk) self.task.runtime.answer += chunk - async def run(self) -> None: + async def run(self) -> None: # noqa: C901 """执行MCP Agent的主逻辑""" # 初始化MCP服务 await self.load_state() @@ -426,32 +398,23 @@ class MCPAgentExecutor(BaseExecutor): # 初始化状态 try: self.task.state.flow_id = str(uuid.uuid4()) - self.task.state.flow_name = await MCPPlanner.get_flow_name(self.task.runtime.question, self.resoning_llm) + self.task.state.flow_name = await MCPPlanner.get_flow_name( + self.task.runtime.question, self.resoning_llm, + ) await TaskManager.save_task(self.task.id, self.task) await self.get_next_step() except Exception as e: - import traceback - logger.error("[MCPAgentExecutor] 初始化失败: %s", traceback.format_exc()) - logger.error("[MCPAgentExecutor] 初始化失败: %s", str(e)) + logger.exception("[MCPAgentExecutor] 初始化失败") self.task.state.flow_status = FlowStatus.ERROR self.task.state.error_message = str(e) - await self.push_message( - EventType.FLOW_FAILED, - data={} - ) + await self.push_message(EventType.FLOW_FAILED, data={}) return self.task.state.flow_status = FlowStatus.RUNNING - await self.push_message( - EventType.FLOW_START, - data={} - ) + await self.push_message(EventType.FLOW_START, data={}) if self.task.state.tool_id == FINAL_TOOL_ID: # 如果已经是最后一步,直接结束 self.task.state.flow_status = FlowStatus.SUCCESS - await self.push_message( - EventType.FLOW_SUCCESS, - data={} - ) + await self.push_message(EventType.FLOW_SUCCESS, data={}) await self.summarize() return try: @@ -465,26 +428,15 @@ class MCPAgentExecutor(BaseExecutor): # 如果已经是最后一步,直接结束 self.task.state.flow_status = FlowStatus.SUCCESS self.task.state.step_status = StepStatus.SUCCESS - await self.push_message( - EventType.FLOW_SUCCESS, - data={} - ) + await self.push_message(EventType.FLOW_SUCCESS, data={}) await self.summarize() except Exception as e: - import traceback - logger.error("[MCPAgentExecutor] 执行过程中发生错误: %s", traceback.format_exc()) - logger.error("[MCPAgentExecutor] 执行过程中发生错误: %s", str(e)) + logger.exception("[MCPAgentExecutor] 执行过程中发生错误") self.task.state.flow_status = FlowStatus.ERROR self.task.state.error_message = str(e) self.task.state.step_status = StepStatus.ERROR - await self.push_message( - EventType.STEP_ERROR, - data={} - ) - await self.push_message( - EventType.FLOW_FAILED, - data={} - ) + await self.push_message(EventType.STEP_ERROR, data={}) + await self.push_message(EventType.FLOW_FAILED, data={}) if len(self.task.context) and self.task.context[-1].step_id == self.task.state.step_id: del self.task.context[-1] self.task.context.append( @@ -499,12 +451,11 @@ class MCPAgentExecutor(BaseExecutor): flow_status=self.task.state.flow_status, input_data={}, output_data={}, - ) + ), ) finally: for mcp_service in self.mcp_list: try: await self.mcp_pool.stop(mcp_service.id, self.task.ids.user_sub) - except Exception as e: - import traceback - logger.error("[MCPAgentExecutor] 停止MCP客户端时发生错误: %s", traceback.format_exc()) + except Exception: + logger.exception("[MCPAgentExecutor] 停止MCP客户端时发生错误") diff --git a/apps/scheduler/mcp_agent/base.py b/apps/scheduler/mcp_agent/base.py index ac3829b4..081a70ab 100644 --- a/apps/scheduler/mcp_agent/base.py +++ b/apps/scheduler/mcp_agent/base.py @@ -1,14 +1,21 @@ -from typing import Any +# Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +"""MCP基类""" + import json -from jsonschema import validate import logging +from typing import Any + +from jsonschema import validate + from apps.llm.function import JsonGenerator from apps.llm.reasoning import ReasoningLLM logger = logging.getLogger(__name__) -class McpBase: +class MCPBase: + """MCP基类""" + @staticmethod async def get_resoning_result(prompt: str, resoning_llm: ReasoningLLM = ReasoningLLM()) -> str: """获取推理结果""" @@ -29,7 +36,7 @@ class McpBase: return result @staticmethod - async def _parse_result(result: str, schema: dict[str, Any], left_str: str = '{', right_str: str = '}') -> str: + async def _parse_result(result: str, schema: dict[str, Any], left_str: str = "{", right_str: str = "}") -> str: """解析推理结果""" left_index = result.find(left_str) right_index = result.rfind(right_str) @@ -41,21 +48,21 @@ class McpBase: flag = False if flag: try: - tmp_js = json.loads(result[left_index:right_index + 1]) + tmp_js = json.loads(result[left_index : right_index + 1]) validate(instance=tmp_js, schema=schema) - except Exception as e: - logger.error("[McpBase] 解析结果失败: %s", e) + except Exception: + logger.exception("[McpBase] 解析结果失败") flag = False if not flag: json_generator = JsonGenerator( "请提取下面内容中的json\n\n", [ {"role": "system", "content": "You are a helpful assistant."}, - {"role": "user", "content": "请提取下面内容中的json\n\n"+result}, + {"role": "user", "content": "请提取下面内容中的json\n\n" + result}, ], schema, ) json_result = await json_generator.generate() else: - json_result = json.loads(result[left_index:right_index + 1]) + json_result = json.loads(result[left_index : right_index + 1]) return json_result diff --git a/apps/scheduler/mcp_agent/host.py b/apps/scheduler/mcp_agent/host.py index f4350676..445ee837 100644 --- a/apps/scheduler/mcp_agent/host.py +++ b/apps/scheduler/mcp_agent/host.py @@ -7,20 +7,14 @@ from typing import Any from jinja2 import BaseLoader from jinja2.sandbox import SandboxedEnvironment -from mcp.types import TextContent -from apps.common.mongo import MongoDB -from apps.llm.reasoning import ReasoningLLM from apps.llm.function import JsonGenerator -from apps.scheduler.mcp_agent.base import McpBase +from apps.llm.reasoning import ReasoningLLM from apps.scheduler.mcp.prompt import MEMORY_TEMPLATE -from apps.scheduler.pool.mcp.client import MCPClient -from apps.scheduler.pool.mcp.pool import MCPPool +from apps.scheduler.mcp_agent.base import MCPBase from apps.scheduler.mcp_agent.prompt import GEN_PARAMS, REPAIR_PARAMS -from apps.schemas.enum_var import StepStatus -from apps.schemas.mcp import MCPPlanItem, MCPTool -from apps.schemas.task import Task, FlowStepHistory -from apps.services.task import TaskManager +from apps.schemas.mcp import MCPTool +from apps.schemas.task import Task logger = logging.getLogger(__name__) @@ -33,25 +27,26 @@ _env = SandboxedEnvironment( def tojson_filter(value): - return json.dumps(value, ensure_ascii=False, separators=(',', ':')) + return json.dumps(value, ensure_ascii=False, separators=(",", ":")) -_env.filters['tojson'] = tojson_filter +_env.filters["tojson"] = tojson_filter -class MCPHost(McpBase): +class MCPHost(MCPBase): """MCP宿主服务""" @staticmethod async def assemble_memory(task: Task) -> str: """组装记忆""" - return _env.from_string(MEMORY_TEMPLATE).render( context_list=task.context, ) - async def _get_first_input_params(mcp_tool: MCPTool, goal: str, current_goal: str, task: Task, - resoning_llm: ReasoningLLM = ReasoningLLM()) -> dict[str, Any]: + async def get_first_input_params( + self, mcp_tool: MCPTool, goal: str, current_goal: str, + task: Task, resoning_llm: ReasoningLLM = ReasoningLLM(), + ) -> dict[str, Any]: """填充工具参数""" # 更清晰的输入·指令,这样可以调用generate prompt = _env.from_string(GEN_PARAMS).render( @@ -63,10 +58,7 @@ class MCPHost(McpBase): background_info=await MCPHost.assemble_memory(task), ) logger.info("[MCPHost] 填充工具参数: %s", prompt) - result = await MCPHost.get_resoning_result( - prompt, - resoning_llm - ) + result = await MCPHost.get_resoning_result(prompt, resoning_llm) # 使用JsonGenerator解析结果 result = await MCPHost._parse_result( result, @@ -74,12 +66,16 @@ class MCPHost(McpBase): ) return result - async def _fill_params(mcp_tool: MCPTool, - goal: str, - current_goal: str, - current_input: dict[str, Any], - error_message: str = "", params: dict[str, Any] = {}, - params_description: str = "") -> dict[str, Any]: + async def _fill_params( + self, + mcp_tool: MCPTool, + goal: str, + current_goal: str, + current_input: dict[str, Any], + error_message: str = "", + params: dict[str, Any] = {}, + params_description: str = "", + ) -> dict[str, Any]: llm_query = "请生成修复之后的工具参数" prompt = _env.from_string(REPAIR_PARAMS).render( tool_name=mcp_tool.name, diff --git a/apps/scheduler/mcp_agent/plan.py b/apps/scheduler/mcp_agent/plan.py index b539482d..3ef84d26 100644 --- a/apps/scheduler/mcp_agent/plan.py +++ b/apps/scheduler/mcp_agent/plan.py @@ -1,41 +1,43 @@ # Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. """MCP 用户目标拆解与规划""" -from typing import Any, AsyncGenerator + +import logging +from collections.abc import AsyncGenerator +from typing import Any + from jinja2 import BaseLoader from jinja2.sandbox import SandboxedEnvironment -import logging + from apps.llm.reasoning import ReasoningLLM -from apps.llm.function import JsonGenerator -from apps.scheduler.mcp_agent.base import McpBase +from apps.scheduler.mcp_agent.base import MCPBase from apps.scheduler.mcp_agent.prompt import ( + CHANGE_ERROR_MESSAGE_TO_DESCRIPTION, + CREATE_PLAN, EVALUATE_GOAL, + FINAL_ANSWER, + GEN_STEP, GENERATE_FLOW_NAME, + GET_MISSING_PARAMS, GET_REPLAN_START_STEP_INDEX, - CREATE_PLAN, + IS_PARAM_ERROR, RECREATE_PLAN, - GEN_STEP, - TOOL_SKIP, RISK_EVALUATE, TOOL_EXECUTE_ERROR_TYPE_ANALYSIS, - IS_PARAM_ERROR, - CHANGE_ERROR_MESSAGE_TO_DESCRIPTION, - GET_MISSING_PARAMS, - FINAL_ANSWER + TOOL_SKIP, ) -from apps.schemas.task import Task +from apps.scheduler.slot.slot import Slot from apps.schemas.mcp import ( GoalEvaluationResult, - RestartStepIndex, - ToolSkip, - ToolRisk, IsParamError, - ToolExcutionErrorType, MCPPlan, + MCPTool, + RestartStepIndex, Step, - MCPPlanItem, - MCPTool + ToolExcutionErrorType, + ToolRisk, + ToolSkip, ) -from apps.scheduler.slot.slot import Slot +from apps.schemas.task import Task _env = SandboxedEnvironment( loader=BaseLoader, @@ -45,36 +47,32 @@ _env = SandboxedEnvironment( ) logger = logging.getLogger(__name__) -class MCPPlanner(McpBase): + +class MCPPlanner(MCPBase): """MCP 用户目标拆解与规划""" @staticmethod async def evaluate_goal( - goal: str, - tool_list: list[MCPTool], - resoning_llm: ReasoningLLM = ReasoningLLM()) -> GoalEvaluationResult: + goal: str, tool_list: list[MCPTool], resoning_llm: ReasoningLLM = ReasoningLLM() + ) -> GoalEvaluationResult: """评估用户目标的可行性""" # 获取推理结果 result = await MCPPlanner._get_reasoning_evaluation(goal, tool_list, resoning_llm) - # 解析为结构化数据 - evaluation = await MCPPlanner._parse_evaluation_result(result) - # 返回评估结果 - return evaluation + return await MCPPlanner._parse_evaluation_result(result) @staticmethod async def _get_reasoning_evaluation( - goal, tool_list: list[MCPTool], - resoning_llm: ReasoningLLM = ReasoningLLM()) -> str: + goal, tool_list: list[MCPTool], resoning_llm: ReasoningLLM = ReasoningLLM() + ) -> str: """获取推理大模型的评估结果""" template = _env.from_string(EVALUATE_GOAL) prompt = template.render( goal=goal, tools=tool_list, ) - result = await MCPPlanner.get_resoning_result(prompt, resoning_llm) - return result + return await MCPPlanner.get_resoning_result(prompt, resoning_llm) @staticmethod async def _parse_evaluation_result(result: str) -> GoalEvaluationResult: @@ -86,22 +84,23 @@ class MCPPlanner(McpBase): async def get_flow_name(user_goal: str, resoning_llm: ReasoningLLM = ReasoningLLM()) -> str: """获取当前流程的名称""" - result = await MCPPlanner._get_reasoning_flow_name(user_goal, resoning_llm) - return result + return await MCPPlanner._get_reasoning_flow_name(user_goal, resoning_llm) @staticmethod async def _get_reasoning_flow_name(user_goal: str, resoning_llm: ReasoningLLM = ReasoningLLM()) -> str: """获取推理大模型的流程名称""" template = _env.from_string(GENERATE_FLOW_NAME) prompt = template.render(goal=user_goal) - result = await MCPPlanner.get_resoning_result(prompt, resoning_llm) - return result + return await MCPPlanner.get_resoning_result(prompt, resoning_llm) @staticmethod async def get_replan_start_step_index( - user_goal: str, error_message: str, current_plan: MCPPlan | None = None, - history: str = "", - reasoning_llm: ReasoningLLM = ReasoningLLM()) -> RestartStepIndex: + user_goal: str, + error_message: str, + current_plan: MCPPlan | None = None, + history: str = "", + reasoning_llm: ReasoningLLM = ReasoningLLM(), + ) -> RestartStepIndex: """获取重新规划的步骤索引""" # 获取推理结果 template = _env.from_string(GET_REPLAN_START_STEP_INDEX) @@ -122,21 +121,33 @@ class MCPPlanner(McpBase): @staticmethod async def create_plan( - user_goal: str, is_replan: bool = False, error_message: str = "", current_plan: MCPPlan | None = None, - tool_list: list[MCPTool] = [], - max_steps: int = 6, reasoning_llm: ReasoningLLM = ReasoningLLM()) -> MCPPlan: + user_goal: str, + is_replan: bool = False, + error_message: str = "", + current_plan: MCPPlan | None = None, + tool_list: list[MCPTool] = [], + max_steps: int = 6, + reasoning_llm: ReasoningLLM = ReasoningLLM(), + ) -> MCPPlan: """规划下一步的执行流程,并输出""" # 获取推理结果 - result = await MCPPlanner._get_reasoning_plan(user_goal, is_replan, error_message, current_plan, tool_list, max_steps, reasoning_llm) + result = await MCPPlanner._get_reasoning_plan( + user_goal, is_replan, error_message, current_plan, tool_list, max_steps, reasoning_llm, + ) # 解析为结构化数据 return await MCPPlanner._parse_plan_result(result, max_steps) @staticmethod async def _get_reasoning_plan( - user_goal: str, is_replan: bool = False, error_message: str = "", current_plan: MCPPlan | None = None, - tool_list: list[MCPTool] = [], - max_steps: int = 10, reasoning_llm: ReasoningLLM = ReasoningLLM()) -> str: + user_goal: str, + is_replan: bool = False, + error_message: str = "", + current_plan: MCPPlan | None = None, + tool_list: list[MCPTool] = [], + max_steps: int = 10, + reasoning_llm: ReasoningLLM = ReasoningLLM(), + ) -> str: """获取推理大模型的结果""" # 格式化Prompt tool_ids = [tool.id for tool in tool_list] @@ -156,8 +167,7 @@ class MCPPlanner(McpBase): tools=tool_list, max_num=max_steps, ) - result = await MCPPlanner.get_resoning_result(prompt, reasoning_llm) - return result + return await MCPPlanner.get_resoning_result(prompt, reasoning_llm) @staticmethod async def _parse_plan_result(result: str, max_steps: int) -> MCPPlan: @@ -171,8 +181,8 @@ class MCPPlanner(McpBase): @staticmethod async def create_next_step( - goal: str, history: str, tools: list[MCPTool], - reasoning_llm: ReasoningLLM = ReasoningLLM()) -> Step: + goal: str, history: str, tools: list[MCPTool], reasoning_llm: ReasoningLLM = ReasoningLLM() + ) -> Step: """创建下一步的执行步骤""" # 获取推理结果 template = _env.from_string(GEN_STEP) @@ -192,12 +202,18 @@ class MCPPlanner(McpBase): @staticmethod async def tool_skip( - task: Task, step_id: str, step_name: str, step_instruction: str, step_content: str, - reasoning_llm: ReasoningLLM = ReasoningLLM()) -> ToolSkip: + task: Task, + step_id: str, + step_name: str, + step_instruction: str, + step_content: str, + reasoning_llm: ReasoningLLM = ReasoningLLM(), + ) -> ToolSkip: """判断当前步骤是否需要跳过""" # 获取推理结果 template = _env.from_string(TOOL_SKIP) from apps.scheduler.mcp_agent.host import MCPHost + history = await MCPHost.assemble_memory(task) prompt = template.render( step_id=step_id, @@ -205,7 +221,7 @@ class MCPPlanner(McpBase): step_instruction=step_instruction, step_content=step_content, history=history, - goal=task.runtime.question + goal=task.runtime.question, ) result = await MCPPlanner.get_resoning_result(prompt, reasoning_llm) @@ -217,22 +233,22 @@ class MCPPlanner(McpBase): @staticmethod async def get_tool_risk( - tool: MCPTool, input_parm: dict[str, Any], - additional_info: str = "", resoning_llm: ReasoningLLM = ReasoningLLM()) -> ToolRisk: + tool: MCPTool, + input_parm: dict[str, Any], + additional_info: str = "", + resoning_llm: ReasoningLLM = ReasoningLLM(), + ) -> ToolRisk: """获取MCP工具的风险评估结果""" # 获取推理结果 result = await MCPPlanner._get_reasoning_risk(tool, input_parm, additional_info, resoning_llm) - # 解析为结构化数据 - risk = await MCPPlanner._parse_risk_result(result) - # 返回风险评估结果 - return risk + return await MCPPlanner._parse_risk_result(result) @staticmethod async def _get_reasoning_risk( - tool: MCPTool, input_param: dict[str, Any], - additional_info: str, resoning_llm: ReasoningLLM) -> str: + tool: MCPTool, input_param: dict[str, Any], additional_info: str, resoning_llm: ReasoningLLM, + ) -> str: """获取推理大模型的风险评估结果""" template = _env.from_string(RISK_EVALUATE) prompt = template.render( @@ -241,8 +257,7 @@ class MCPPlanner(McpBase): input_param=input_param, additional_info=additional_info, ) - result = await MCPPlanner.get_resoning_result(prompt, resoning_llm) - return result + return await MCPPlanner.get_resoning_result(prompt, resoning_llm) @staticmethod async def _parse_risk_result(result: str) -> ToolRisk: @@ -254,9 +269,13 @@ class MCPPlanner(McpBase): @staticmethod async def _get_reasoning_tool_execute_error_type( - user_goal: str, current_plan: MCPPlan, - tool: MCPTool, input_param: dict[str, Any], - error_message: str, reasoning_llm: ReasoningLLM = ReasoningLLM()) -> str: + user_goal: str, + current_plan: MCPPlan, + tool: MCPTool, + input_param: dict[str, Any], + error_message: str, + reasoning_llm: ReasoningLLM = ReasoningLLM(), + ) -> str: """获取推理大模型的工具执行错误类型""" template = _env.from_string(TOOL_EXECUTE_ERROR_TYPE_ANALYSIS) prompt = template.render( @@ -267,8 +286,7 @@ class MCPPlanner(McpBase): input_param=input_param, error_message=error_message, ) - result = await MCPPlanner.get_resoning_result(prompt, reasoning_llm) - return result + return await MCPPlanner.get_resoning_result(prompt, reasoning_llm) @staticmethod async def _parse_tool_execute_error_type_result(result: str) -> ToolExcutionErrorType: @@ -280,22 +298,32 @@ class MCPPlanner(McpBase): @staticmethod async def get_tool_execute_error_type( - user_goal: str, current_plan: MCPPlan, - tool: MCPTool, input_param: dict[str, Any], - error_message: str, reasoning_llm: ReasoningLLM = ReasoningLLM()) -> ToolExcutionErrorType: + user_goal: str, + current_plan: MCPPlan, + tool: MCPTool, + input_param: dict[str, Any], + error_message: str, + reasoning_llm: ReasoningLLM = ReasoningLLM(), + ) -> ToolExcutionErrorType: """获取MCP工具执行错误类型""" # 获取推理结果 result = await MCPPlanner._get_reasoning_tool_execute_error_type( - user_goal, current_plan, tool, input_param, error_message, reasoning_llm) - error_type = await MCPPlanner._parse_tool_execute_error_type_result(result) + user_goal, current_plan, tool, input_param, error_message, reasoning_llm, + ) # 返回工具执行错误类型 - return error_type + return await MCPPlanner._parse_tool_execute_error_type_result(result) + @staticmethod async def is_param_error( - goal: str, history: str, error_message: str, tool: MCPTool, step_description: str, input_params: dict - [str, Any], - reasoning_llm: ReasoningLLM = ReasoningLLM()) -> IsParamError: + goal: str, + history: str, + error_message: str, + tool: MCPTool, + step_description: str, + input_params: dict[str, Any], + reasoning_llm: ReasoningLLM = ReasoningLLM(), + ) -> IsParamError: """判断错误信息是否是参数错误""" tmplate = _env.from_string(IS_PARAM_ERROR) prompt = tmplate.render( @@ -316,8 +344,8 @@ class MCPPlanner(McpBase): @staticmethod async def change_err_message_to_description( - error_message: str, tool: MCPTool, input_params: dict[str, Any], - reasoning_llm: ReasoningLLM = ReasoningLLM()) -> str: + error_message: str, tool: MCPTool, input_params: dict[str, Any], reasoning_llm: ReasoningLLM = ReasoningLLM() + ) -> str: """将错误信息转换为工具描述""" template = _env.from_string(CHANGE_ERROR_MESSAGE_TO_DESCRIPTION) prompt = template.render( @@ -332,9 +360,8 @@ class MCPPlanner(McpBase): @staticmethod async def get_missing_param( - tool: MCPTool, - input_param: dict[str, Any], - error_message: str, reasoning_llm: ReasoningLLM = ReasoningLLM()) -> list[str]: + tool: MCPTool, input_param: dict[str, Any], error_message: str, reasoning_llm: ReasoningLLM = ReasoningLLM() + ) -> list[str]: """获取缺失的参数""" slot = Slot(schema=tool.input_schema) template = _env.from_string(GET_MISSING_PARAMS) @@ -353,8 +380,8 @@ class MCPPlanner(McpBase): @staticmethod async def generate_answer( - user_goal: str, memory: str, resoning_llm: ReasoningLLM = ReasoningLLM()) -> AsyncGenerator[ - str, None]: + user_goal: str, memory: str, resoning_llm: ReasoningLLM = ReasoningLLM() + ) -> AsyncGenerator[str, None]: """生成最终回答""" template = _env.from_string(FINAL_ANSWER) prompt = template.render( diff --git a/apps/scheduler/mcp_agent/prompt.py b/apps/scheduler/mcp_agent/prompt.py index 824ece8a..25dbaff7 100644 --- a/apps/scheduler/mcp_agent/prompt.py +++ b/apps/scheduler/mcp_agent/prompt.py @@ -62,6 +62,7 @@ MCP_SELECT = dedent(r""" ### 请一步一步思考: """) + TOOL_SELECT = dedent(r""" 你是一个乐于助人的智能助手。 你的任务是:根据当前目标,附加信息,选择最合适的MCP工具。 @@ -117,8 +118,7 @@ TOOL_SELECT = dedent(r""" ## 附加信息 {{additional_info}} # 输出 - """ - ) + """) EVALUATE_GOAL = dedent(r""" 你是一个计划评估器。 @@ -142,7 +142,8 @@ EVALUATE_GOAL = dedent(r""" - mysql_analyzer 分析MySQL数据库性能 - performance_tuner 调优数据库性能 - - Final 结束步骤,当执行到这一步时,表示计划执行结束,所得到的结果将作为最终结果。 + - Final 结束步骤,当执行到这一步时,表示计划执行结束,所得到的结果将作为最终结果。\ + # 附加信息 @@ -153,7 +154,8 @@ EVALUATE_GOAL = dedent(r""" ```json { "can_complete": true, - "resoning": "当前的工具集合中包含mysql_analyzer和performance_tuner,能够完成对MySQL数据库的性能分析和调优,因此可以完成用户的目标。" + "resoning": "当前的工具集合中包含mysql_analyzer和performance_tuner,能够完成对MySQL数据库的性能分析和调优,\ +因此可以完成用户的目标。" } ``` @@ -171,6 +173,7 @@ EVALUATE_GOAL = dedent(r""" {{additional_info}} """) + GENERATE_FLOW_NAME = dedent(r""" 你是一个智能助手,你的任务是根据用户的目标,生成一个合适的流程名称。 @@ -190,6 +193,7 @@ GENERATE_FLOW_NAME = dedent(r""" {{goal}} # 输出 """) + GET_REPLAN_START_STEP_INDEX = dedent(r""" 你是一个智能助手,你的任务是根据用户的目标、报错信息和当前计划和历史,获取重新规划的步骤起始索引。 @@ -240,7 +244,8 @@ GET_REPLAN_START_STEP_INDEX = dedent(r""" # 输出 { "start_index": 0, - "reasoning": "当前计划的第一步就失败了,报错信息显示curl命令未找到,可能是因为没有安装curl工具,因此需要从第一步重新规划。" + "reasoning": "当前计划的第一步就失败了,报错信息显示curl命令未找到,可能是因为没有安装curl工具,\ +因此需要从第一步重新规划。" } # 现在开始获取重新规划的步骤起始索引: # 目标 @@ -353,6 +358,7 @@ CREATE_PLAN = dedent(r""" # 计划 """) + RECREATE_PLAN = dedent(r""" 你是一个计划重建器。 请根据用户的目标、当前计划和运行报错,重新生成一个计划。 @@ -402,7 +408,8 @@ RECREATE_PLAN = dedent(r""" - command_generator 生成命令行指令 - tool_selector 选择合适的工具 - command_executor 执行命令行指令 - - Final 结束步骤,当执行到这一步时,表示计划执行结束,所得到的结果将作为最终结果。 + - Final 结束步骤,当执行到这一步时,表示计划执行结束,所得到的结果将作为最终结果。\ + # 当前计划 ```json { @@ -502,6 +509,7 @@ RECREATE_PLAN = dedent(r""" # 重新生成的计划 """) + GEN_STEP = dedent(r""" 你是一个计划生成器。 请根据用户的目标、当前计划和历史,生成一个新的步骤。 @@ -529,7 +537,8 @@ GEN_STEP = dedent(r""" - mcp_tool_1 mysql_analyzer;用于分析数据库性能/description> - mcp_tool_2 文件存储工具;用于存储文件 - mcp_tool_3 mongoDB工具;用于操作MongoDB数据库 - - Final 结束步骤,当执行到这一步时,表示计划执行结束,所得到的结果将作为最终结果。 + - Final 结束步骤,当执行到这一步时,表示计划执行结束,所得到的结果将作为最终结果。\ + # 输出 ```json @@ -560,10 +569,13 @@ GEN_STEP = dedent(r""" - 得到数据:`{"weather": "晴", "temperature": "25°C"}` # 工具 - - mcp_tool_4 maps_geo_planner;将详细的结构化地址转换为经纬度坐标。支持对地标性名胜景区、建筑物名称解析为经纬度坐标 + - mcp_tool_4 maps_geo_planner;将详细的结构化地址转换为经纬度坐标。支持对地标性名胜景区、\ +建筑物名称解析为经纬度坐标 - mcp_tool_5 weather_query;天气查询,用于查询天气信息 - - mcp_tool_6 maps_direction_transit_integrated;根据用户起终点经纬度坐标规划综合各类公共(火车、公交、地铁)交通方式的通勤方案,并且返回通勤方案的数据,跨城场景下必须传起点城市与终点城市 - - Final Final;结束步骤,当执行到这一步时,表示计划执行结束,所得到的结果将作为最终结果。 + - mcp_tool_6 maps_direction_transit_integrated;根据用户起终点经纬度坐标规划综合各类\ +公共交通方式(火车、公交、地铁)的通勤方案,并且返回通勤方案的数据,跨城场景下必须传起点城市与终点城市 + - Final Final;结束步骤,当执行到这一步时,表示计划执行结束,\ +所得到的结果将作为最终结果。 # 输出 ```json @@ -610,7 +622,8 @@ TOOL_SKIP = dedent(r""" - 执行状态:成功 - 得到数据:`{"result": "success"}` 第3步:分析端口扫描结果 - - 调用工具 `mysql_analyzer`,并提供参数 `{"host": "192.168.1.1", "port": 3306, "username": "root", "password": "password"}` + - 调用工具 `mysql_analyzer`,并提供参数 `{"host": "192.168.1.1", "port": 3306, "username": "root",\ + "password": "password"}` - 执行状态:成功 - 得到数据:`{"performance": "good", "bottleneck": "none"}` # 当前步骤 @@ -638,8 +651,8 @@ TOOL_SKIP = dedent(r""" {{step_content}} # 输出 - """ - ) + """) + RISK_EVALUATE = dedent(r""" 你是一个工具执行计划评估器。 你的任务是根据当前工具的名称、描述和入参以及附加信息,判断当前工具执行的风险并输出提示。 @@ -673,7 +686,8 @@ RISK_EVALUATE = dedent(r""" ```json { "risk": "中", - "reason": "当前工具将连接到MySQL数据库并分析性能,可能会对数据库性能产生一定影响。请确保在非生产环境中执行此操作。" + "reason": "当前工具将连接到MySQL数据库并分析性能,可能会对数据库性能产生一定影响。\ +请确保在非生产环境中执行此操作。" } ``` # 工具 @@ -686,8 +700,8 @@ RISK_EVALUATE = dedent(r""" # 附加信息 {{additional_info}} # 输出 - """ - ) + """) + # 根据当前计划和报错信息决定下一步执行,具体计划有需要用户补充工具入参、重计划当前步骤、重计划接下来的所有计划 TOOL_EXECUTE_ERROR_TYPE_ANALYSIS = dedent(r""" 你是一个计划决策器。 @@ -739,7 +753,8 @@ TOOL_EXECUTE_ERROR_TYPE_ANALYSIS = dedent(r""" ```json { "error_type": "decorrect_plan", - "reason": "当前计划的第二步执行失败,报错信息显示nmap命令未找到,可能是因为没有安装nmap工具,因此需要重计划当前步骤。" + "reason": "当前计划的第二步执行失败,报错信息显示nmap命令未找到,可能是因为没有安装nmap工具,\ +因此需要重计划当前步骤。" } ``` # 用户目标 @@ -756,8 +771,8 @@ TOOL_EXECUTE_ERROR_TYPE_ANALYSIS = dedent(r""" # 工具运行报错 {{error_message}} # 输出 - """ - ) + """) + IS_PARAM_ERROR = dedent(r""" 你是一个计划执行专家,你的任务是判断当前的步骤执行失败是否是因为参数错误导致的, 如果是,请返回`true`,否则返回`false`。 @@ -816,8 +831,8 @@ IS_PARAM_ERROR = dedent(r""" # 工具运行报错 {{error_message}} # 输出 - """ - ) + """) + # 将当前程序运行的报错转换为自然语言 CHANGE_ERROR_MESSAGE_TO_DESCRIPTION = dedent(r""" 你是一个智能助手,你的任务是将当前程序运行的报错转换为自然语言描述。 @@ -883,6 +898,7 @@ CHANGE_ERROR_MESSAGE_TO_DESCRIPTION = dedent(r""" {{error_message}} # 输出 """) + # 获取缺失的参数的json结构体 GET_MISSING_PARAMS = dedent(r""" 你是一个工具参数获取器。 @@ -965,8 +981,8 @@ GET_MISSING_PARAMS = dedent(r""" # 运行报错 {{error_message}} # 输出 - """ - ) + """) + GEN_PARAMS = dedent(r""" 你是一个工具参数生成器。 你的任务是根据总的目标、阶段性的目标、工具信息、工具入参的schema和背景信息生成工具的入参。 @@ -1040,8 +1056,7 @@ GEN_PARAMS = dedent(r""" # 背景信息 {{background_info}} # 输出 - """ - ) + """) REPAIR_PARAMS = dedent(r""" 你是一个工具参数修复器。 @@ -1130,8 +1145,8 @@ REPAIR_PARAMS = dedent(r""" # 补充的参数描述 {{params_description}} # 输出 - """ - ) + """) + FINAL_ANSWER = dedent(r""" 综合理解计划执行结果和背景信息,向用户报告目标的完成情况。 diff --git a/apps/scheduler/mcp_agent/select.py b/apps/scheduler/mcp_agent/select.py index 075e08f0..d287b113 100644 --- a/apps/scheduler/mcp_agent/select.py +++ b/apps/scheduler/mcp_agent/select.py @@ -3,28 +3,16 @@ import logging import random + from jinja2 import BaseLoader from jinja2.sandbox import SandboxedEnvironment -from typing import AsyncGenerator, Any -from apps.llm.function import JsonGenerator -from apps.llm.reasoning import ReasoningLLM -from apps.common.lance import LanceDB -from apps.common.mongo import MongoDB -from apps.llm.embedding import Embedding -from apps.llm.function import FunctionLLM from apps.llm.reasoning import ReasoningLLM from apps.llm.token import TokenCalculator -from apps.scheduler.mcp_agent.base import McpBase +from apps.scheduler.mcp_agent.base import MCPBase from apps.scheduler.mcp_agent.prompt import TOOL_SELECT -from apps.schemas.mcp import ( - BaseModel, - MCPCollection, - MCPSelectResult, - MCPTool, - MCPToolIdsSelectResult -) -from apps.common.config import Config +from apps.schemas.mcp import MCPTool, MCPToolIdsSelectResult + logger = logging.getLogger(__name__) _env = SandboxedEnvironment( @@ -38,7 +26,7 @@ FINAL_TOOL_ID = "FIANL" SUMMARIZE_TOOL_ID = "SUMMARIZE" -class MCPSelector(McpBase): +class MCPSelector(MCPBase): """MCP选择器""" @staticmethod @@ -68,9 +56,9 @@ class MCPSelector(McpBase): tokens = token_calculator.calculate_token_length( messages=[{"role": "user", "content": template.render( goal=goal, tools=[tool], - additional_info=additional_info + additional_info=additional_info, )}], - pure_text=True + pure_text=True, ) if tokens > max_tokens: continue @@ -90,7 +78,11 @@ class MCPSelector(McpBase): schema["properties"]["tool_ids"]["items"] = {} # 将enum添加到items中,限制数组元素的可选值 schema["properties"]["tool_ids"]["items"]["enum"] = [tool.id for tool in sub_tools] - result = await MCPSelector.get_resoning_result(template.render(goal=goal, tools=sub_tools, additional_info="请根据目标选择对应的工具"), reasoning_llm) + result = await MCPSelector.get_resoning_result( + template.render( + goal=goal, tools=sub_tools, additional_info="请根据目标选择对应的工具", + ), reasoning_llm, + ) result = await MCPSelector._parse_result(result, schema) try: result = MCPToolIdsSelectResult.model_validate(result) diff --git a/apps/scheduler/pool/mcp/client.py b/apps/scheduler/pool/mcp/client.py index 0ced05e8..562a1056 100644 --- a/apps/scheduler/pool/mcp/client.py +++ b/apps/scheduler/pool/mcp/client.py @@ -129,12 +129,13 @@ class MCPClient: done, pending = await asyncio.wait( [asyncio.create_task(self.ready_sign.wait()), asyncio.create_task(self.error_sign.wait())], - return_when=asyncio.FIRST_COMPLETED + return_when=asyncio.FIRST_COMPLETED, ) if self.error_sign.is_set(): self.status = MCPStatus.ERROR - logger.error("[MCPClient] MCP %s:初始化失败", mcp_id) - raise Exception(f"MCP {mcp_id} 初始化失败") + error = f"MCP {mcp_id} 初始化失败" + logger.error("[MCPClient] %s", error) + raise RuntimeError(error) # 获取工具列表 self.tools = (await self.client.list_tools()).tools @@ -148,5 +149,5 @@ class MCPClient: self.stop_sign.set() try: await self.task - except Exception as e: + except Exception as e: # noqa: BLE001 logger.warning("[MCPClient] MCP %s:停止时发生异常:%s", self.mcp_id, e) diff --git a/apps/scheduler/pool/mcp/install.py b/apps/scheduler/pool/mcp/install.py index 2b15cd69..b694eff3 100644 --- a/apps/scheduler/pool/mcp/install.py +++ b/apps/scheduler/pool/mcp/install.py @@ -1,11 +1,11 @@ # Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. """MCP 安装""" -from asyncio import subprocess -from typing import TYPE_CHECKING import logging -import os import shutil +from asyncio import subprocess +from typing import TYPE_CHECKING + from apps.constants import MCP_PATH if TYPE_CHECKING: @@ -27,31 +27,31 @@ async def install_uvx(mcp_id: str, config: "MCPServerStdioConfig") -> "MCPServer :rtype: MCPServerStdioConfig :raises ValueError: 未找到MCP Server对应的Python包 """ - uv_path = shutil.which('uv') + uv_path = shutil.which("uv") if uv_path is None: error = "[Installer] 未找到uv命令,请先安装uv包管理器: pip install uv" - logging.error(error) - raise Exception(error) + logger.error(error) + raise RuntimeError(error) # 找到包名 package = None for arg in config.args: if not arg.startswith("-"): package = arg break - logger.error(f"[Installer] MCP包名: {package}") + logger.error("[Installer] MCP包名: %s", package) if not package: print("[Installer] 未找到包名") # noqa: T201 return None # 创建文件夹 mcp_path = MCP_PATH / "template" / mcp_id / "project" - logger.error(f"[Installer] MCP安装路径: {mcp_path}") + logger.error("[Installer] MCP安装路径: %s", mcp_path) await mcp_path.mkdir(parents=True, exist_ok=True) # 如果有pyproject.toml文件,则使用sync flag = await (mcp_path / "pyproject.toml").exists() - logger.error(f"[Installer] MCP安装标志: {flag}") + logger.error("[Installer] MCP安装标志: %s", flag) if await (mcp_path / "pyproject.toml").exists(): shell_command = f"{uv_path} venv; {uv_path} sync --index-url https://pypi.tuna.tsinghua.edu.cn/simple --active --no-install-project --no-cache" - logger.error(f"[Installer] MCP安装命令: {shell_command}") + logger.error("[Installer] MCP安装命令: %s", shell_command) pipe = await subprocess.create_subprocess_shell( ( f"{uv_path} venv; " @@ -73,7 +73,7 @@ async def install_uvx(mcp_id: str, config: "MCPServerStdioConfig") -> "MCPServer if "run" not in config.args: config.args = ["run", *config.args] config.auto_install = False - logger.error(f"[Installer] MCP安装配置更新成功: {config}") + logger.error("[Installer] MCP安装配置更新成功: %s", config) return config # 否则,初始化uv项目 @@ -117,11 +117,11 @@ async def install_npx(mcp_id: str, config: "MCPServerStdioConfig") -> "MCPServer :rtype: MCPServerStdioConfig :raises ValueError: 未找到MCP Server对应的npm包 """ - npm_path = shutil.which('npm') + npm_path = shutil.which("npm") if npm_path is None: error = "[Installer] 未找到npm命令,请先安装Node.js和npm" - logging.error(error) - raise Exception(error) + logger.error(error) + raise RuntimeError(error) # 查找package name package = None for arg in config.args: diff --git a/apps/scheduler/pool/mcp/pool.py b/apps/scheduler/pool/mcp/pool.py index bf0320f4..cb76864c 100644 --- a/apps/scheduler/pool/mcp/pool.py +++ b/apps/scheduler/pool/mcp/pool.py @@ -21,7 +21,7 @@ class MCPPool(metaclass=SingletonMeta): """初始化MCP池""" self.pool = {} - async def _init_mcp(self, mcp_id: str, user_sub: str) -> MCPClient | None: + async def init_mcp(self, mcp_id: str, user_sub: str) -> MCPClient | None: """初始化MCP池""" config_path = MCP_USER_PATH / user_sub / mcp_id / "config.json" flag = (await config_path.exists()) @@ -69,7 +69,7 @@ class MCPPool(metaclass=SingletonMeta): return None # 初始化进程 - item = await self._init_mcp(mcp_id, user_sub) + item = await self.init_mcp(mcp_id, user_sub) if item is None: return None diff --git a/apps/schemas/message.py b/apps/schemas/message.py index 5b465ee5..17a569ca 100644 --- a/apps/schemas/message.py +++ b/apps/schemas/message.py @@ -1,16 +1,18 @@ # Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. """队列中的消息结构""" -from typing import Any from datetime import UTC, datetime +from typing import Any + from pydantic import BaseModel, Field from apps.schemas.enum_var import EventType, FlowStatus, StepStatus from apps.schemas.record import RecordMetadata -class param(BaseModel): +class FlowParams(BaseModel): """流执行过程中的参数补充""" + content: dict[str, Any] = Field(default={}, description="流执行过程中的参数补充内容") description: str = Field(default="", description="流执行过程中的参数补充描述") diff --git a/apps/schemas/request_data.py b/apps/schemas/request_data.py index 3fd5a67f..d04f6fd8 100644 --- a/apps/schemas/request_data.py +++ b/apps/schemas/request_data.py @@ -10,7 +10,7 @@ from apps.schemas.appcenter import AppData from apps.schemas.enum_var import CommentType from apps.schemas.flow_topology import FlowItem from apps.schemas.mcp import MCPType -from apps.schemas.message import param +from apps.schemas.message import FlowParams class RequestDataApp(BaseModel): @@ -47,7 +47,7 @@ class RequestData(BaseModel): app: RequestDataApp | None = Field(default=None, description="应用") debug: bool = Field(default=False, description="是否调试") task_id: str | None = Field(default=None, alias="taskId", description="任务ID") - params: param | bool | None = Field(default=None, description="流执行过程中的参数补充", alias="params") + params: FlowParams | bool | None = Field(default=None, description="流执行过程中的参数补充", alias="params") class QuestionBlacklistRequest(BaseModel): -- Gitee From 0df8df8d769dc4206446dd4e45898ab3d0415173 Mon Sep 17 00:00:00 2001 From: zxstty Date: Wed, 13 Aug 2025 16:50:31 +0800 Subject: [PATCH 14/15] fix bug --- apps/scheduler/executor/agent.py | 9 ++++++--- apps/services/mcp_service.py | 7 ++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/apps/scheduler/executor/agent.py b/apps/scheduler/executor/agent.py index 4d34b85b..2b94c5d0 100644 --- a/apps/scheduler/executor/agent.py +++ b/apps/scheduler/executor/agent.py @@ -263,9 +263,12 @@ class MCPAgentExecutor(BaseExecutor): max_retry = 3 step = None for i in range(max_retry): - step = await MCPPlanner.create_next_step(self.task.runtime.question, history, self.tool_list) - if step.tool_id in self.tools.keys(): - break + try: + step = await MCPPlanner.create_next_step(self.task.runtime.question, history, self.tool_list) + if step.tool_id in self.tools.keys(): + break + except Exception as e: + logger.warning("[MCPAgentExecutor] 获取下一步失败,重试中: %s", str(e)) if step is None or step.tool_id not in self.tools.keys(): step = Step( tool_id=FINAL_TOOL_ID, diff --git a/apps/services/mcp_service.py b/apps/services/mcp_service.py index 3c7ab3c5..149d6482 100644 --- a/apps/services/mcp_service.py +++ b/apps/services/mcp_service.py @@ -421,10 +421,11 @@ class MCPServiceManager: if db_service.status == MCPInstallStatus.INSTALLING or db_service.status == MCPInstallStatus.READY: err = "[MCPServiceManager] MCP服务已处于安装中或已准备就绪" raise Exception(err) + await service_collection.update_one( + {"_id": service_id}, + {"$set": {"status": MCPInstallStatus.INSTALLING}}, + ) mcp_config = await MCPLoader.get_config(service_id) await MCPLoader.init_one_template(mcp_id=service_id, config=mcp_config) else: - if db_service.status != MCPInstallStatus.INSTALLING: - err = "[MCPServiceManager] 只能卸载处于安装中的MCP服务" - raise Exception(err) await MCPLoader.cancel_installing_task([service_id]) -- Gitee From 16c507d8f09bbf4bfd38b43fd97e8b351f6fdd03 Mon Sep 17 00:00:00 2001 From: zhanghb <2428333123@qq.com> Date: Wed, 13 Aug 2025 16:53:59 +0800 Subject: [PATCH 15/15] =?UTF-8?q?=E9=80=82=E9=85=8D=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/scheduler/executor/agent.py | 3 ++- apps/scheduler/mcp_agent/plan.py | 15 ++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/apps/scheduler/executor/agent.py b/apps/scheduler/executor/agent.py index a46da890..1513db26 100644 --- a/apps/scheduler/executor/agent.py +++ b/apps/scheduler/executor/agent.py @@ -261,7 +261,7 @@ class MCPAgentExecutor(BaseExecutor): max_retry = 3 step = None for _ in range(max_retry): - step = await MCPPlanner.create_next_step(self.task.runtime.question, history, self.tool_list) + step = await MCPPlanner.create_next_step(self.task.runtime.question, history, self.tool_list, language=self.task.language) if step.tool_id in self.tools: break if step is None or step.tool_id not in self.tools: @@ -370,6 +370,7 @@ class MCPAgentExecutor(BaseExecutor): mcp_tool, self.task.state.step_description, self.task.state.current_input, + language=self.task.language, ) if is_param_error.is_param_error: # 如果是参数错误,生成参数补充 diff --git a/apps/scheduler/mcp_agent/plan.py b/apps/scheduler/mcp_agent/plan.py index e3c8b418..0e97fd2c 100644 --- a/apps/scheduler/mcp_agent/plan.py +++ b/apps/scheduler/mcp_agent/plan.py @@ -194,11 +194,15 @@ class MCPPlanner(MCPBase): @staticmethod async def create_next_step( - goal: str, history: str, tools: list[MCPTool], reasoning_llm: ReasoningLLM = ReasoningLLM() + goal: str, + history: str, + tools: list[MCPTool], + reasoning_llm: ReasoningLLM = ReasoningLLM(), + language: LanguageType = LanguageType.CHINESE, ) -> Step: """创建下一步的执行步骤""" # 获取推理结果 - template = _env.from_string(GEN_STEP) + template = _env.from_string(GEN_STEP[language]) prompt = template.render(goal=goal, history=history, tools=tools) result = await MCPPlanner.get_resoning_result(prompt, reasoning_llm) @@ -221,10 +225,11 @@ class MCPPlanner(MCPBase): step_instruction: str, step_content: str, reasoning_llm: ReasoningLLM = ReasoningLLM(), + language: LanguageType = LanguageType.CHINESE, ) -> ToolSkip: """判断当前步骤是否需要跳过""" # 获取推理结果 - template = _env.from_string(TOOL_SKIP) + template = _env.from_string(TOOL_SKIP[language]) from apps.scheduler.mcp_agent.host import MCPHost history = await MCPHost.assemble_memory(task) @@ -335,7 +340,6 @@ class MCPPlanner(MCPBase): # 返回工具执行错误类型 return await MCPPlanner._parse_tool_execute_error_type_result(result) - @staticmethod async def is_param_error( goal: str, @@ -345,9 +349,10 @@ class MCPPlanner(MCPBase): step_description: str, input_params: dict[str, Any], reasoning_llm: ReasoningLLM = ReasoningLLM(), + language: LanguageType = LanguageType.CHINESE, ) -> IsParamError: """判断错误信息是否是参数错误""" - tmplate = _env.from_string(IS_PARAM_ERROR) + tmplate = _env.from_string(IS_PARAM_ERROR[language]) prompt = tmplate.render( goal=goal, history=history, -- Gitee