diff --git a/.gitignore b/.gitignore index c62e334e6e0f521a7f889c54a758a5956a99c0ea..7e0cc625cc5b39fb43670412f5355ebb6be011d5 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 6165dc455aae038aeb1ad9bcd0840b418c3dc8e8..05d2f8fd4ac8297817849be05866fb0bf40c985a 100644 --- a/apps/llm/function.py +++ b/apps/llm/function.py @@ -85,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} @@ -124,7 +126,9 @@ class FunctionLLM: 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 @@ -184,14 +188,16 @@ 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] return await self.process_response(response.message.content or "") @@ -266,7 +272,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 = [ diff --git a/apps/llm/patterns/core.py b/apps/llm/patterns/core.py index 4ef8133a9fed1b1e62f1ceb578c6bdb5a93b12a5..f80c275dcefe9aeb0b69e54f419e31f2a3a07bc3 100644 --- a/apps/llm/patterns/core.py +++ b/apps/llm/patterns/core.py @@ -3,22 +3,26 @@ from abc import ABC, abstractmethod from textwrap import dedent +from apps.schemas.enum_var import LanguageType class CorePattern(ABC): """基础大模型范式抽象类""" - system_prompt: str = "" + system_prompt: dict[LanguageType, str] = {} """系统提示词""" - user_prompt: str = "" + user_prompt: dict[LanguageType, str] = {} """用户提示词""" input_tokens: int = 0 """输入Token数量""" output_tokens: int = 0 """输出Token数量""" - - def __init__(self, system_prompt: str | None = None, user_prompt: str | None = None) -> None: + def __init__( + self, + system_prompt: dict[LanguageType, str] | None = None, + user_prompt: dict[LanguageType, str] | None = None, + ) -> None: """ 检查是否已经自定义了Prompt;有的话就用自定义的;同时对Prompt进行空格清除 @@ -35,8 +39,9 @@ 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()} + + 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 f872fd2ac8d691b4079756a56a6107d6b6556585..31d096ea132c525b5f860954668a10c5f6fa201c 100644 --- a/apps/llm/patterns/executor.py +++ b/apps/llm/patterns/executor.py @@ -1,12 +1,12 @@ # 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 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,7 +14,8 @@ if TYPE_CHECKING: class ExecutorThought(CorePattern): """通过大模型生成Executor的思考内容""" - user_prompt: str = r""" + user_prompt: dict[LanguageType, str] = { + LanguageType.CHINESE: r""" 你是一个可以使用工具的智能助手。 @@ -44,10 +45,46 @@ class ExecutorThought(CorePattern): 请综合以上信息,再次一步一步地进行思考,并给出见解和行动: - """ + """, + LanguageType.ENGLISH: 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: dict[LanguageType, str] | None = None, + user_prompt: dict[LanguageType, 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: LanguageType = kwargs.get("language", LanguageType.CHINESE) 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[LanguageType, str] = { + LanguageType.CHINESE: r""" 根据给定的对话记录和关键事实,生成一个三句话背景总结。这个总结将用于后续对话的上下文理解。 @@ -105,10 +147,36 @@ class ExecutorSummary(CorePattern): 现在,请开始生成背景总结: - """ + """, + 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. + + 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: dict[LanguageType, str] | None = None, + user_prompt: dict[LanguageType, 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", LanguageType.CHINESE) 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 0b0381ff40a0e6632fd204c9efcf834a13c4711f..ac833f515419e5229b6f3a685a4642edd141aaed 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,7 +26,8 @@ class Facts(CorePattern): system_prompt: str = "You are a helpful assistant." """系统提示词(暂不使用)""" - user_prompt: str = r""" + user_prompt: dict[LanguageType, str] = { + LanguageType.CHINESE: r""" 从对话中提取关键信息,并将它们组织成独一无二的、易于理解的事实,包含用户偏好、关系、实体等有用信息。 @@ -63,21 +65,65 @@ class Facts(CorePattern): {conversation} - """ - """用户提示词""" + """, + LanguageType.ENGLISH: 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: 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) -> list[str]: # noqa: ANN003 """事实提取""" conversation = convert_context_to_prompt(kwargs["conversation"]) + language = kwargs.get("language", LanguageType.CHINESE) + 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 ef78d92667d30fbd3b26d55ba4d87961181f3d48..ec4c3d39263c5ff2450a8fd9c69abe1704fbfb1b 100644 --- a/apps/llm/patterns/rewoo.py +++ b/apps/llm/patterns/rewoo.py @@ -3,12 +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: str = r""" + system_prompt: dict[LanguageType, str] = { + LanguageType.CHINESE: r""" 你是一个计划生成器。对于给定的目标,**制定一个简单的计划**,该计划可以逐步生成合适的命令行参数和标志。 你会收到一个"命令前缀",这是已经确定和生成的命令部分。你需要基于这个前缀使用标志和参数来完成命令。 @@ -41,10 +44,54 @@ class InitPlan(CorePattern): 示例结束 让我们开始! - """ + """, + 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. + + 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[LanguageType, str] = { + LanguageType.CHINESE: r""" 任务:{instruction} 前缀:`{binary_name} {subcmd_name}` 用法:`{subcmd_usage}`。这是一个Python模板字符串。OPTS是所有标志的占位符。参数必须是 {argument_list} 其中之一。 @@ -52,10 +99,25 @@ class InitPlan(CorePattern): "{subcmd_description}"。 请生成相应的计划。 - """ + """, + 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 \ + 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: dict[LanguageType, str] | None = None, + user_prompt: dict[LanguageType, str] | None = None, + ) -> None: """处理Prompt""" super().__init__(system_prompt, user_prompt) @@ -64,6 +126,7 @@ class InitPlan(CorePattern): spec = kwargs["spec"] binary_name = kwargs["binary_name"] subcmd_name = kwargs["subcmd_name"] + 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] @@ -73,16 +136,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 +164,8 @@ class InitPlan(CorePattern): class PlanEvaluator(CorePattern): """计划评估器""" - system_prompt: str = r""" + system_prompt: dict[LanguageType, str] = { + LanguageType.CHINESE: r""" 你是一个计划评估器。你的任务是评估给定的计划是否合理和完整。 一个好的计划应该: @@ -115,29 +182,64 @@ class PlanEvaluator(CorePattern): 请回复: "VALID" - 如果计划良好且完整 "INVALID: <原因>" - 如果计划有问题,请解释原因 - """ + """, + 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: + 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[LanguageType, str] = { + LanguageType.CHINESE: r""" 任务:{instruction} 计划:{plan} 评估计划并回复"VALID"或"INVALID: <原因>"。 - """ + """, + LanguageType.ENGLISH: 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: 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", LanguageType.CHINESE) 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 +255,8 @@ class PlanEvaluator(CorePattern): class RePlanner(CorePattern): """重新规划器""" - system_prompt: str = r""" + system_prompt: dict[LanguageType, str] = { + LanguageType.CHINESE: r""" 你是一个计划重新规划器。当计划被评估为无效时,你需要生成一个新的、改进的计划。 新计划应该: @@ -167,31 +270,64 @@ class RePlanner(CorePattern): - 包含带有适当参数的工具使用 - 保持步骤简洁和重点突出 - 以"Final"步骤结束 - """ + """, + 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: + 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[LanguageType, str] = { + LanguageType.CHINESE: r""" 任务:{instruction} 原始计划:{plan} 评估:{evaluation} 生成一个新的、改进的计划,解决评估中提到的所有问题。 - """ + """, + LanguageType.ENGLISH: 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: 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", LanguageType.CHINESE) 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 15d52ab288b4c964e81ee290894bd29a590bbab2..6cf36e47baa4692814a91ccae743bce215173bf9 100644 --- a/apps/llm/patterns/rewrite.py +++ b/apps/llm/patterns/rewrite.py @@ -4,11 +4,13 @@ import logging from pydantic import BaseModel, Field +from textwrap import dedent 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__) @@ -21,7 +23,8 @@ class QuestionRewriteResult(BaseModel): class QuestionRewrite(CorePattern): """问题补全与重写""" - system_prompt: str = r""" + system_prompt: dict[LanguageType, str] = { + LanguageType.CHINESE: r""" 根据历史对话,推断用户的实际意图并补全用户的提问内容,历史对话被包含在标签中,用户意图被包含在标签中。 @@ -72,26 +75,87 @@ class QuestionRewrite(CorePattern): {question} + """, + 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. + 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[LanguageType, str] = { + LanguageType.CHINESE: r""" 请输出补全后的问题 - """ + """, + LanguageType.ENGLISH: 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", LanguageType.CHINESE) 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 +177,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/llm/patterns/select.py b/apps/llm/patterns/select.py index a6c496bdd0ef79631bbdc41935e717ca3e3668ea..1aaa10dfb9a26c2590ca5a181e0fe6a01e8818bc 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,10 +19,14 @@ logger = logging.getLogger(__name__) class Select(CorePattern): """通过投票选择最佳答案""" - system_prompt: str = "You are a helpful assistant." + system_prompt: dict[LanguageType, str] = { + LanguageType.CHINESE: "你是一个有用的助手。", + LanguageType.ENGLISH: "You are a helpful assistant.", + } """系统提示词""" - user_prompt: str = r""" + user_prompt: dict[LanguageType, str] = { + LanguageType.CHINESE: r""" 根据历史对话(包括工具调用结果)和用户问题,从给出的选项列表中,选出最符合要求的那一项。 @@ -71,7 +76,63 @@ class Select(CorePattern): 让我们一步一步思考。 - """ + """, + LanguageType.ENGLISH: 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 +147,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[LanguageType, 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 +183,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 +190,7 @@ class Select(CorePattern): result_list = [] background = kwargs.get("background", "无背景信息。") + 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"]) @@ -141,7 +204,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 888fbd04f67c0ad2ad9482db8d37016e0d71447d..462b10ab48c9239ed3648313c0999774916bb824 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, RequestDataApp 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 @@ -65,6 +66,7 @@ async def init_task(post_body: RequestData, user_sub: str, session_id: str) -> T post_body.conversation_id = task.ids.conversation_id post_body.language = task.language post_body.question = task.runtime.question + task.language = post_body.language return task @@ -140,6 +142,7 @@ async def chat( session_id: Annotated[str, Depends(get_session)], ) -> StreamingResponse: """LLM流式对话接口""" + post_body.language = LanguageType.CHINESE if post_body.language in {"zh", LanguageType.CHINESE} else LanguageType.ENGLISH # 前端 Flow-Debug 传输为“zh" # 问题黑名单检测 if post_body.question is not None and not await QuestionBlacklistManager.check_blacklisted_questions(input_question=post_body.question): # 用户扣分 diff --git a/apps/routers/flow.py b/apps/routers/flow.py index 68c0c9f3bd2fbdb434164d64ac4c3d1cf31a376b..7dbf68ef058017b8fe1af07769023220ba541a22 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 @@ -36,6 +37,11 @@ router = APIRouter( ], ) +""" +get_services, get_flow, put_flow 三个函数需要前端传入 language 参数,已验证可行 +""" + + @router.get( "/service", @@ -46,9 +52,10 @@ router = APIRouter( ) async def get_services( user_sub: Annotated[str, Depends(get_user)], + language: LanguageType = Query(LanguageType.CHINESE, 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, @@ -74,7 +81,7 @@ async def get_services( 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")], + flow_id: Annotated[str, Query(alias="flowId")] ) -> JSONResponse: """获取流拓扑结构""" if not await AppManager.validate_user_app_access(user_sub, app_id): @@ -120,7 +127,7 @@ async def put_flow( user_sub: Annotated[str, Depends(get_user)], app_id: Annotated[str, Query(alias="appId")], flow_id: Annotated[str, Query(alias="flowId")], - put_body: Annotated[PutFlowReq, Body(...)], + put_body: Annotated[PutFlowReq, Body(...)] ) -> 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 e1891f7259b72a2fa03228f6289c54da6297b958..66aa838690f25fc9dbc52e35cb1850f2ba4189ba 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 @@ -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, @@ -59,10 +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={}) + i18n_info: ClassVar[dict[str, dict]] = { + LanguageType.CHINESE: { + "name": "API调用", + "description": "向某一个API接口发送HTTP请求,获取数据", + }, + LanguageType.ENGLISH: { + "name": "API Call", + "description": "Send an HTTP request to an API to obtain data", + }, + } + @classmethod def info(cls) -> CallInfo: - """返回Call的名称和描述""" - return CallInfo(name="API调用", description="向某一个API接口发送HTTP请求,获取数据。") + """ + 返回Call的名称和描述 + + :return: Call的名称和描述 + :rtype: CallInfo + """ + 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调用工具""" @@ -99,8 +116,10 @@ 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: LanguageType = LanguageType.CHINESE + ) -> AsyncGenerator[CallOutputChunk, None]: + """调用API,然后返回LLM解析后的数据""" self._client = httpx.AsyncClient(timeout=self.timeout) input_obj = APIInput.model_validate(input_data) try: @@ -112,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 01ac7106fbdae673d12488a49d831f1d7e7fc13d..5574eadb2ae3de3cfa6df0ce3873df92d480a1cd 100644 --- a/apps/scheduler/call/choice/choice.py +++ b/apps/scheduler/call/choice/choice.py @@ -5,21 +5,21 @@ import ast import copy import logging from collections.abc import AsyncGenerator -from typing import Any +from typing import Any, ClassVar 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.enum_var import CallOutputType, LanguageType +from apps.schemas.parameters import Type from apps.schemas.scheduler import ( CallError, CallInfo, @@ -34,15 +34,38 @@ 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]] = { + LanguageType.CHINESE: { + "name": "判断", + "description": "使用大模型或使用程序做出判断", + }, + LanguageType.ENGLISH: { + "name": "Choice", + "description": "Use a large model or a program to make a decision", + }, + } @classmethod def info(cls) -> CallInfo: - """返回Call的名称和描述""" - return CallInfo(name="选择器", description="使用大模型或使用程序做出判断") + """ + 返回Call的名称和描述 - async def _prepare_message(self, call_vars: CallVars) -> list[dict[str, Any]]: + :return: Call的名称和描述 + :rtype: CallInfo + """ + 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: + """统一处理 ValueError 异常抛出""" + logger.warning(msg) + raise ValueError(msg) + + async def _prepare_message(self, call_vars: CallVars) -> list[ChoiceBranch]: # noqa: C901, PLR0912, PLR0915 """替换choices中的系统变量""" valid_choices = [] @@ -50,8 +73,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 +83,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 +158,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 +171,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: LanguageType ) -> AsyncGenerator[CallOutputChunk, None]: """执行Choice工具""" # 解析输入数据 diff --git a/apps/scheduler/call/choice/condition_handler.py b/apps/scheduler/call/choice/condition_handler.py index 261809ad57fdd6d8c5267c59a7b87404dd7befa5..3c1354c9e4f244463e6431b0768b578a0d6fa0c3 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,16 +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 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 """ 判断列表类型的条件。 @@ -256,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 """ 判断字典类型的条件。 @@ -296,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 d97a0c8d7e9efcef255ced76c8d1d6b39dfed241..955322705031604531cc14f6fe7a4a30935d5989 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): diff --git a/apps/scheduler/call/core.py b/apps/scheduler/call/core.py index 5bed80309fbdb993aacc378ab0319ed291f1ffb3..16db721550a41c73d2dc2b5fda14ce29821b5438 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 @@ -52,7 +53,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 +71,12 @@ class CoreCall(BaseModel): exclude=True, frozen=True, ) + language: ClassVar[SkipJsonSchema[LanguageType]] = Field( + description="语言", + default=LanguageType.CHINESE, + exclude=True, + ) + i18n_info: ClassVar[SkipJsonSchema[dict[str, dict]]] = {} to_user: bool = Field(description="是否需要将输出返回给用户", default=False) @@ -76,7 +85,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,9 +192,14 @@ 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: LanguageType = LanguageType.CHINESE, + ) -> AsyncGenerator[CallOutputChunk, None]: """Call类实例的执行方法""" - async for chunk in self._exec(input_data): + 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 5865bc7e804491a7d6aa41fa251dc8c5d9c77dc6..590a24b9372cc492670323161c9e0d7e87c83e11 100644 --- a/apps/scheduler/call/empty.py +++ b/apps/scheduler/call/empty.py @@ -5,7 +5,7 @@ from collections.abc import AsyncGenerator from typing import Any from apps.scheduler.call.core import CoreCall, DataBase -from apps.schemas.enum_var import CallOutputType +from apps.schemas.enum_var import CallOutputType, LanguageType from apps.schemas.scheduler import CallInfo, CallOutputChunk, CallVars @@ -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: LanguageType = LanguageType.CHINESE) -> AsyncGenerator[CallOutputChunk, None]: """ 执行Call diff --git a/apps/scheduler/call/facts/facts.py b/apps/scheduler/call/facts/facts.py index 2b9df0c6f8f1002e76bda24eb099f47a6084b2af..2bf35aba501b5f6e1c524abd8d8ae7e04bb7425d 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 @@ -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 @@ -30,10 +30,27 @@ class FactsCall(CoreCall, input_model=FactsInput, output_model=FactsOutput): answer: str = Field(description="用户输入") + i18n_info: ClassVar[dict[str, dict]] = { + LanguageType.CHINESE: { + "name": "提取事实", + "description": "从对话上下文和文档片段中提取事实。", + }, + LanguageType.ENGLISH: { + "name": "Fact Extraction", + "description": "Extract facts from the conversation context and document snippets.", + }, + } + @classmethod def info(cls) -> CallInfo: - """返回Call的名称和描述""" - return CallInfo(name="提取事实", description="从对话上下文和文档片段中提取事实。") + """ + 返回Call的名称和描述 + + :return: Call的名称和描述 + :rtype: CallInfo + """ + lang_info = cls.i18n_info.get(cls.language, cls.i18n_info[LanguageType.CHINESE]) + return CallInfo(name=lang_info["name"], description=lang_info["description"]) @classmethod async def instance(cls, executor: "StepExecutor", node: NodePool | None, **kwargs: Any) -> Self: @@ -62,7 +79,9 @@ 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: LanguageType = LanguageType.CHINESE + ) -> AsyncGenerator[CallOutputChunk, None]: """执行工具""" data = FactsInput(**input_data) # jinja2 环境 @@ -74,20 +93,26 @@ 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."}, - {"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) + 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) @@ -100,9 +125,14 @@ 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: LanguageType = LanguageType.CHINESE, + ) -> 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 b2b2513f2c28feb4d19f5fb4ee3eba1bd616a4dc..02e134391fa33d3dbe3b3e71cc63947acc739777 100644 --- a/apps/scheduler/call/facts/prompt.py +++ b/apps/scheduler/call/facts/prompt.py @@ -2,8 +2,10 @@ """记忆提取工具的提示词""" from textwrap import dedent - -DOMAIN_PROMPT: str = dedent(r""" +from apps.schemas.enum_var import LanguageType +DOMAIN_PROMPT: dict[str, str] = { + LanguageType.CHINESE: dedent( + r""" 根据对话上文,提取推荐系统所需的关键词标签,要求: @@ -35,8 +37,48 @@ DOMAIN_PROMPT: str = dedent(r""" {% endfor %} -""") -FACTS_PROMPT: str = dedent(r""" +""" + ), + LanguageType.ENGLISH: 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] = { + LanguageType.CHINESE: dedent( + r""" 从对话中提取关键信息,并将它们组织成独一无二的、易于理解的事实,包含用户偏好、关系、实体等有用信息。 @@ -80,4 +122,53 @@ FACTS_PROMPT: str = dedent(r""" {% endfor %} -""") +""" + ), + LanguageType.ENGLISH: 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 c2728f17913fcd0e8343168f2b508dbe6006fd6e..6df9969ea248234c27c982ad769125f11efb346c 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 @@ -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, @@ -25,12 +25,27 @@ class Graph(CoreCall, input_model=RenderInput, output_model=RenderOutput): dataset_key: str = Field(description="图表的数据来源(字段名)", default="") + i18n_info: ClassVar[dict[str, dict]] = { + LanguageType.CHINESE: { + "name": "图表", + "description": "将SQL查询出的数据转换为图表。", + }, + LanguageType.ENGLISH: { + "name": "Chart", + "description": "Convert the data queried by SQL into a chart.", + }, + } @classmethod def info(cls) -> CallInfo: - """返回Call的名称和描述""" - return CallInfo(name="图表", description="将SQL查询出的数据转换为图表") + """ + 返回Call的名称和描述 + :return: Call的名称和描述 + :rtype: CallInfo + """ + 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模板""" @@ -54,8 +69,9 @@ class Graph(CoreCall, input_model=RenderInput, output_model=RenderOutput): data=data, ) - - async def _exec(self, input_data: dict[str, Any]) -> 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) @@ -100,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]]: """ @@ -117,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 6a679dce98af6164211edd16b2fa38714d899f31..ce2deeb5f214cd8e1645b124812a06175a6cbdbd 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 @@ -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, @@ -38,12 +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]] = { + LanguageType.CHINESE: { + "name": "大模型", + "description": "以指定的提示词和上下文信息调用大模型,并获得输出。", + }, + LanguageType.ENGLISH: { + "name": "Foundation Model", + "description": "Call the foundation model with specified prompt and context, and obtain the output.", + }, + } @classmethod def info(cls) -> CallInfo: - """返回Call的名称和描述""" - return CallInfo(name="大模型", description="以指定的提示词和上下文信息调用大模型,并获得输出。") + """ + 返回Call的名称和描述 + :return: Call的名称和描述 + :rtype: CallInfo + """ + 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]]: """准备消息""" @@ -57,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: @@ -93,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]) -> AsyncGenerator[CallOutputChunk, None]: + async def _exec( + self, input_data: dict[str, Any], language: LanguageType = LanguageType.CHINESE + ) -> 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 0f227dcaa618b11a2c888f55a61fa51f349d7d8b..b03891dbdac51673a575412ba6512fc62c8a0556 100644 --- a/apps/scheduler/call/llm/prompt.py +++ b/apps/scheduler/call/llm/prompt.py @@ -2,16 +2,34 @@ """大模型工具的提示词""" from textwrap import dedent +from apps.schemas.enum_var import LanguageType 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 +39,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 +75,13 @@ LLM_DEFAULT_PROMPT = dedent( {{ context }} - - 现在,输出你的回答: - """, + Now, please output your answer: + """ ).strip("\n") -LLM_ERROR_PROMPT = dedent( - r""" + +LLM_ERROR_PROMPT = { + LanguageType.CHINESE: dedent( + r""" 你是一位智能助手,能够根据用户的问题,使用Python工具获取信息,并作出回答。你在使用工具解决回答用户的问题时,发生了错误。 你的任务是:分析工具(Python程序)的异常信息,分析造成该异常可能的原因,并以通俗易懂的方式,将原因告知用户。 @@ -67,8 +104,36 @@ LLM_ERROR_PROMPT = dedent( 现在,输出你的回答: - """, -).strip("\n") + """ + ).strip("\n"), + LanguageType.ENGLISH: 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 bd0257b49fb6ea24fbbc429cb11f7b8e6be364b9..2fa8ee8593a43a56c5178f19394bc8d372cfb405 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 @@ -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,6 +26,28 @@ from apps.schemas.scheduler import ( logger = logging.getLogger(__name__) +MCP_GENERATE: dict[str, dict[LanguageType, str]] = { + "START": { + LanguageType.CHINESE: "[MCP] 开始生成计划...\n\n\n\n", + LanguageType.ENGLISH: "[MCP] Start generating plan...\n\n\n\n", + }, + "END": { + 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[LanguageType, str]] = { + "START": { + LanguageType.CHINESE: "[MCP] 正在总结任务结果...\n\n", + LanguageType.ENGLISH: "[MCP] Start summarizing task results...\n\n", + }, + "END": { + 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工具""" @@ -35,6 +57,17 @@ 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]] = { + LanguageType.CHINESE: { + "name": "MCP", + "description": "调用MCP Server,执行工具", + }, + LanguageType.ENGLISH: { + "name": "MCP", + "description": "Call the MCP Server to execute tools", + }, + } + @classmethod def info(cls) -> CallInfo: """ @@ -43,12 +76,15 @@ class MCP(CoreCall, input_model=MCPInput, output_model=MCPOutput): :return: Call的名称和描述 :rtype: CallInfo """ - return CallInfo(name="MCP", description="调用MCP Server,执行工具") + 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: """初始化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 +97,33 @@ 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: LanguageType = LanguageType.CHINESE + ) -> 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]: """生成执行计划""" # 开始提示 - yield self._create_output("[MCP] 开始生成计划...\n\n\n\n", MCPMessageType.PLAN_BEGIN) + yield self._create_output(MCP_GENERATE["START"][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) # 输出计划 @@ -94,12 +132,14 @@ class MCP(CoreCall, input_model=MCPInput, output_model=MCPOutput): plan_str += f"[+] {plan_item.content}; {plan_item.tool}[{plan_item.instruction}]\n\n" yield self._create_output( - f"[MCP] 计划生成完成:\n\n{plan_str}\n\n\n\n", + 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) -> AsyncGenerator[CallOutputChunk, None]: + async def _execute_plan_item( + self, plan_item: MCPPlanItem, language: LanguageType = LanguageType.CHINESE + ) -> AsyncGenerator[CallOutputChunk, None]: """执行单个计划项""" # 判断是否为Final if plan_item.tool == "Final": @@ -120,7 +160,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 +176,21 @@ 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]: """生成总结""" # 提示开始总结 yield self._create_output( - "[MCP] 正在总结任务结果...\n\n", + MCP_SUMMARY["START"][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()) # 输出结果 yield self._create_output( - f"[MCP] 任务完成\n\n---\n\n{answer}\n\n", + MCP_SUMMARY["END"][language].format(answer=answer), MCPMessageType.FINISH_END, data=MCPOutput( message=answer, @@ -166,8 +206,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 e27327d8ad4d01387eeeb4f1644c056d32bc0dbe..f1f1bf7618b3a1aaabac872e9b2203428cefe136 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 @@ -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, @@ -37,10 +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]] = { + LanguageType.CHINESE: { + "name": "知识库", + "description": "查询知识库,从文档中获取必要信息", + }, + LanguageType.ENGLISH: { + "name": "Knowledge Base", + "description": "Query the knowledge base and obtain necessary information from documents", + }, + } + @classmethod def info(cls) -> CallInfo: - """返回Call的名称和描述""" - return CallInfo(name="知识库", description="查询知识库,从文档中获取必要信息") + """ + 返回Call的名称和描述 + + :return: Call的名称和描述 + :rtype: CallInfo + """ + 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: """初始化RAG工具""" @@ -58,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]) -> 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 73d21d7b9956a19b6eaaf23467b4286d7fbb3d79..69b5d6671bab7624913fdd53e887e1cd7eaf2c4d 100644 --- a/apps/scheduler/call/search/search.py +++ b/apps/scheduler/call/search/search.py @@ -1,10 +1,11 @@ """搜索工具""" 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 +from apps.schemas.enum_var import LanguageType from apps.schemas.scheduler import ( CallError, CallInfo, @@ -16,18 +17,32 @@ from apps.schemas.scheduler import ( class Search(CoreCall, input_model=SearchInput, output_model=SearchOutput): """搜索工具""" + i18n_info: ClassVar[dict[str, dict]] = { + LanguageType.CHINESE: { + "name": "搜索", + "description": "获取搜索引擎的结果。", + }, + LanguageType.ENGLISH: { + "name": "Search", + "description": "Get the results of the search engine.", + }, + } + @classmethod def info(cls) -> CallInfo: - """返回Call的名称和描述""" - return CallInfo(name="搜索", description="获取搜索引擎的结果") + """ + 返回Call的名称和描述 + :return: Call的名称和描述 + :rtype: CallInfo + """ + 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: """初始化工具""" 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 e5650a4c5764a4ab739c6b66ad00838597c134a3..dbb97ebfbe3c917707028069ae38409753073009 100644 --- a/apps/scheduler/call/slot/prompt.py +++ b/apps/scheduler/call/slot/prompt.py @@ -1,7 +1,9 @@ # Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. """自动参数填充工具的提示词""" +from apps.schemas.enum_var import LanguageType -SLOT_GEN_PROMPT = r""" +SLOT_GEN_PROMPT:dict[LanguageType, str] = { + LanguageType.CHINESE: r""" 你是一个可以使用工具的AI助手,正尝试使用工具来完成任务。 目前,你正在生成一个JSON参数对象,以作为调用工具的输入。 @@ -17,6 +19,7 @@ SLOT_GEN_PROMPT = r""" 3. 只输出JSON对象,不要输出任何解释说明,不要输出任何其他内容。 4. 如果JSON Schema中描述的JSON字段是可选的,则可以不输出该字段。 5. example中仅为示例,不要照搬example中的内容,不要将example中的内容作为输出。 + 6. 优先使用与用户问题相同的语言回答,但如果用户明确要求回答语言,则遵循用户的要求。 @@ -85,4 +88,91 @@ SLOT_GEN_PROMPT = r""" {{schema}} - """ + """, + 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. + 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 d24e1661e9dd7facc4bf590b24409052b6ec9104..6332ebe53aae9f563098459c329b8e9f17c4a584 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 @@ -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 @@ -32,12 +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]] = { + LanguageType.CHINESE: { + "name": "参数自动填充", + "description": "根据步骤历史,自动填充参数", + }, + LanguageType.ENGLISH: { + "name": "Parameter Auto-Fill", + "description": "Auto-fill parameters based on step history.", + }, + } + @classmethod def info(cls) -> CallInfo: - """返回Call的名称和描述""" - return CallInfo(name="参数自动填充", description="根据步骤历史,自动填充参数") - - async def _llm_slot_fill(self, remaining_schema: dict[str, Any]) -> tuple[str, dict[str, Any]]: + """ + 返回Call的名称和描述 + + :return: Call的名称和描述 + :rtype: CallInfo + """ + 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: LanguageType = LanguageType.CHINESE + ) -> tuple[str, dict[str, Any]]: """使用大模型填充参数;若大模型解析度足够,则直接返回结果""" env = SandboxedEnvironment( loader=BaseLoader(), @@ -45,21 +64,24 @@ 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."}, - {"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, + ), + }, ] # 使用大模型进行尝试 @@ -108,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: @@ -123,7 +145,9 @@ 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: LanguageType = LanguageType.CHINESE + ) -> AsyncGenerator[CallOutputChunk, None]: """执行参数填充""" data = SlotInput(**input_data) @@ -137,7 +161,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 3e24301de508e06adf5cfdbf24b3d8ca37c0cc27..357a58994f831ee5e1a970a9bd7cda2662f8db19 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 @@ -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,29 +22,54 @@ 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语句,获得数据。""" 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) + i18n_info: ClassVar[dict[str, dict]] = { + LanguageType.CHINESE: { + "name": "SQL查询", + "description": "使用大模型生成SQL语句,用于查询数据库中的结构化数据", + }, + LanguageType.ENGLISH: { + "name": "SQL Query", + "description": "Use the foundation model to generate SQL statements to query structured data in the databases", + }, + } @classmethod def info(cls) -> CallInfo: - """返回Call的名称和描述""" - return CallInfo(name="SQL查询", description="使用大模型生成SQL语句,用于查询数据库中的结构化数据") + """ + 返回Call的名称和描述 + :return: Call的名称和描述 + :rtype: CallInfo + """ + 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) -> SQLInput: + async def _init(self, call_vars: CallVars, language: LanguageType = LanguageType.CHINESE) -> SQLInput: """初始化SQL工具。""" return SQLInput( question=call_vars.question, ) - async def _generate_sql(self, data: SQLInput) -> list[dict[str, Any]]: """生成SQL语句列表""" post_data = { @@ -82,7 +107,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 +137,17 @@ class SQL(CoreCall, input_model=SQLInput, output_model=SQLOutput): return None, None - - async def _exec(self, input_data: dict[str, Any]) -> AsyncGenerator[CallOutputChunk, None]: - """运行SQL工具""" + async def _exec( + self, input_data: dict[str, Any], language: LanguageType = LanguageType.CHINESE + ) -> AsyncGenerator[CallOutputChunk, None]: + """运行SQL工具""" data = SQLInput(**input_data) # 生成SQL语句 sql_list = await self._generate_sql(data) if not sql_list: raise CallError( - message="SQL查询错误:无法生成有效的SQL语句!", + message=MESSAGE["invaild"][language], data={}, ) @@ -130,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="SQL查询错误:SQL语句执行失败!", + message=MESSAGE["fail"][language], data={}, ) diff --git a/apps/scheduler/call/suggest/prompt.py b/apps/scheduler/call/suggest/prompt.py index abc5e7d186500f917b4610b9c0e893dec7ef3c12..a9f61d7c1c5df6bc7fcee488d6ab05781b685d72 100644 --- a/apps/scheduler/call/suggest/prompt.py +++ b/apps/scheduler/call/suggest/prompt.py @@ -2,8 +2,11 @@ """问题推荐工具的提示词""" from textwrap import dedent +from apps.schemas.enum_var import LanguageType -SUGGEST_PROMPT = dedent(r""" +SUGGEST_PROMPT: dict[LanguageType, str] = { + LanguageType.CHINESE: dedent( + r""" 根据提供的对话和附加信息(用户倾向、历史问题列表、工具信息等),生成三个预测问题。 @@ -89,4 +92,98 @@ SUGGEST_PROMPT = dedent(r""" 现在,进行问题生成: -""") +""" + ), + LanguageType.ENGLISH: 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 1788fa0f4a8ede3af38264c9bb4a82628018086b..0d0a93fe08c05e0074365cd5087293b28fdbebde 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 @@ -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 ( @@ -42,16 +42,34 @@ 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) + i18n_info: ClassVar[dict[str, dict]] = { + LanguageType.CHINESE: { + "name": "问题推荐", + "description": "在答案下方显示推荐的下一个问题", + }, + LanguageType.ENGLISH: { + "name": "Question Suggestion", + "description": "Display the suggested next question under the answer", + }, + } + @classmethod def info(cls) -> CallInfo: - """返回Call的名称和描述""" - return CallInfo(name="问题推荐", description="在答案下方显示推荐的下一个问题") + """ + 返回Call的名称和描述 + :return: Call的名称和描述 + :rtype: CallInfo + """ + lang_info = cls.i18n_info.get(cls.language, cls.i18n_info[LanguageType.CHINESE]) + return CallInfo(name=lang_info["name"], description=lang_info["description"]) @classmethod async def instance(cls, executor: "StepExecutor", node: NodePool | None, **kwargs: Any) -> Self: @@ -77,7 +95,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 +126,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 +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]) -> AsyncGenerator[CallOutputChunk, None]: + async def _exec( + self, input_data: dict[str, Any], language: LanguageType = LanguageType.CHINESE + ) -> AsyncGenerator[CallOutputChunk, None]: """运行问题推荐""" data = SuggestionInput(**input_data) @@ -141,7 +158,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 +188,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 +203,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 +221,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 b605204e179246f915d561c0884fd277712faf33..64fd52e347adb555c734126de91bc4b8ddc65bc5 100644 --- a/apps/scheduler/call/summary/summary.py +++ b/apps/scheduler/call/summary/summary.py @@ -2,14 +2,14 @@ """总结上下文工具""" from collections.abc import AsyncGenerator -from typing import TYPE_CHECKING, Any, Self +from typing import TYPE_CHECKING, Any, Self, ClassVar 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,16 +22,32 @@ 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]] = { + LanguageType.CHINESE: { + "name": "理解上下文", + "description": "使用大模型,理解对话上下文", + }, + LanguageType.ENGLISH: { + "name": "Context Understanding", + "description": "Use the foundation model to understand the conversation context", + }, + } + @classmethod def info(cls) -> CallInfo: - """返回Call的名称和描述""" - return CallInfo(name="理解上下文", description="使用大模型,理解对话上下文") + """ + 返回Call的名称和描述 + + :return: Call的名称和描述 + :rtype: CallInfo + """ + lang_info = cls.i18n_info.get(cls.language, cls.i18n_info[LanguageType.CHINESE]) + return CallInfo(name=lang_info["name"], description=lang_info["description"]) @classmethod async def instance(cls, executor: "StepExecutor", node: NodePool | None, **kwargs: Any) -> Self: @@ -46,25 +62,29 @@ 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]) -> 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) + 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: LanguageType = LanguageType.CHINESE, + ) -> 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/agent.py b/apps/scheduler/executor/agent.py index 9f714cc7fa0b36d88f7153e5c0ace540c00ef267..af6247372be2563bbbac5cc8a50b5926b4aa1dc1 100644 --- a/apps/scheduler/executor/agent.py +++ b/apps/scheduler/executor/agent.py @@ -1,38 +1,33 @@ # 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.schemas.enum_var import 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 -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 +41,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 +88,56 @@ 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, + self.task.language, + ) 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) + 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)) + 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 +154,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 +195,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 +209,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 @@ -215,26 +220,21 @@ 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 = 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 - ) - await self.push_message( - EventType.STEP_WAITING_FOR_PARAM, - data={ - "message": error_message, - "params": params_with_null - } + reasoning_llm=self.resoning_llm, + language=self.task.language, ) 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,14 +249,12 @@ 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) @@ -264,7 +262,7 @@ class MCPAgentExecutor(BaseExecutor): step = None for i in range(max_retry): try: - 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.keys(): break except Exception as e: @@ -275,10 +273,7 @@ class MCPAgentExecutor(BaseExecutor): 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 @@ -289,16 +284,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( @@ -313,18 +304,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() @@ -341,14 +332,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: @@ -366,12 +351,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 @@ -405,6 +393,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: # 如果是参数错误,生成参数补充 @@ -414,9 +403,12 @@ 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: + 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 self.task.context[-1].output_data = { "message": self.task.state.error_message, @@ -443,18 +435,17 @@ 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.resoning_llm, + self.task.language, ): - 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() @@ -463,32 +454,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, self.task.language + ) 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: @@ -502,26 +484,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( @@ -536,12 +507,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/executor/flow.py b/apps/scheduler/executor/flow.py index ebd4da8ecb6e55c5220a9cd4e0f63c72842c9828..3b0513432d2d7f7d14bd18e1f20c06eb2a071161 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 @@ -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, - ), + { + LanguageType.CHINESE: Step( + name="理解上下文", + description="使用大模型,理解对话上下文", + node=SpecialCallType.SUMMARY.value, + type=SpecialCallType.SUMMARY.value, + ), + LanguageType.ENGLISH: 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, - ), + { + LanguageType.CHINESE: Step( + name="记忆存储", + description="理解对话答案,并存储到记忆中", + node=SpecialCallType.FACTS.value, + type=SpecialCallType.FACTS.value, + ), + LanguageType.ENGLISH: 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, + ), + } ] @@ -46,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: + 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 @@ -67,7 +84,7 @@ 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 == LanguageType.CHINESE else "Start", ) self.validate_flow_state(self.task) # 是否到达Flow结束终点(变量) @@ -164,12 +181,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[LanguageType.CHINESE]), + enable_filling=False, + to_user=False, + ) + ) await self._step_process() # 插入首个步骤 @@ -182,24 +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="错误处理", - description="错误处理", - node=SpecialCallType.LLM.value, - type=SpecialCallType.LLM.value, - params={ - "user_prompt": LLM_ERROR_PROMPT.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 @@ -222,10 +248,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[LanguageType.CHINESE]), + ) + ) await self._step_process() # FlowStop需要返回总时间,需要倒推最初的开始时间(当前时间减去当前已用总时间) diff --git a/apps/scheduler/executor/step.py b/apps/scheduler/executor/step.py index 5a95e407989f4dfc9c26c2157ccbec5d3ecdad21..7c3808c2d6d051289f257d742f184956fad96d4c 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 8e11083900c20c29fcaf71b6535f3b442fc2313a..8e7b26e38e2eba24b9b9c735b93e6a5a12407970 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,10 +25,18 @@ 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: LanguageType = LanguageType.CHINESE, + ) -> 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 @@ -72,7 +80,7 @@ class MCPHost: continue context_list.append(context) - return self._env.from_string(MEMORY_TEMPLATE).render( + return self._env.from_string(MEMORY_TEMPLATE[self.language]).render( context_list=context_list, ) diff --git a/apps/scheduler/mcp/plan.py b/apps/scheduler/mcp/plan.py index cd4f5975eea3f023a92626966081c2d1eb33bdb7..489731b14e5414893159ed4f8c5953f7df85df75 100644 --- a/apps/scheduler/mcp/plan.py +++ b/apps/scheduler/mcp/plan.py @@ -8,14 +8,16 @@ 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) -> None: + def __init__(self, user_goal: str, language: LanguageType = LanguageType.CHINESE) -> None: """初始化MCP规划器""" self.user_goal = user_goal + self.language = language self._env = SandboxedEnvironment( loader=BaseLoader, autoescape=True, @@ -25,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: """规划下一步的执行流程,并输出""" # 获取推理结果 @@ -34,11 +35,10 @@ 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 - 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, @@ -66,7 +66,6 @@ class MCPPlanner: return result - async def _parse_plan_result(self, result: str, max_steps: int) -> MCPPlan: """将推理结果解析为结构化数据""" # 格式化Prompt @@ -85,10 +84,9 @@ 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) + 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 b906fb8f75dfafb2546aaef6eb36370645c57598..29721b31c92a84d875052f3097af6e3e5c6db82d 100644 --- a/apps/scheduler/mcp/prompt.py +++ b/apps/scheduler/mcp/prompt.py @@ -2,8 +2,11 @@ """MCP相关的大模型Prompt""" from textwrap import dedent +from apps.schemas.enum_var import LanguageType -MCP_SELECT = dedent(r""" +MCP_SELECT: dict[str, str] = { + LanguageType.CHINESE: dedent( + r""" 你是一个乐于助人的智能助手。 你的任务是:根据当前目标,选择最合适的MCP Server。 @@ -61,8 +64,73 @@ MCP_SELECT = dedent(r""" ### 请一步一步思考: -""") -CREATE_PLAN = dedent(r""" +""" + ), + 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. + + ## 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] = { + LanguageType.CHINESE: dedent( + r""" 你是一个计划生成器。 请分析用户的目标,并生成一个计划。你后续将根据这个计划,一步一步地完成用户的目标。 @@ -94,8 +162,7 @@ CREATE_PLAN = dedent(r""" } ``` - - 在生成计划之前,请一步一步思考,解析用户的目标,并指导你接下来的生成。\ -思考过程应放置在 XML标签中。 + - 在生成计划之前,请一步一步思考,解析用户的目标,并指导你接下来的生成。思考过程应按步骤顺序放置在 XML标签中。 - 计划内容中,可以使用"Result[]"来引用之前计划步骤的结果。例如:"Result[3]"表示引用第三条计划执行后的结果。 - 计划不得多于{{ max_num }}条,且每条计划内容应少于150字。 @@ -107,8 +174,7 @@ CREATE_PLAN = dedent(r""" {% for tool in tools %} - {{ tool.id }}{{tool.name}};{{ tool.description }} {% endfor %} - - Final结束步骤,当执行到这一步时,\ -表示计划执行结束,所得到的结果将作为最终结果。 + - Final结束步骤,当执行到这一步时,表示计划执行结束,所得到的结果将作为最终结果。 # 样例 @@ -163,8 +229,114 @@ CREATE_PLAN = dedent(r""" {{goal}} # 计划 -""") -EVALUATE_PLAN = dedent(r""" +""" + ), + 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. + + # 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] = { + LanguageType.CHINESE: dedent( + r""" 你是一个计划评估器。 请根据给定的计划,和当前计划执行的实际情况,分析当前计划是否合理和完整,并生成改进后的计划。 @@ -210,8 +382,61 @@ EVALUATE_PLAN = dedent(r""" # 现在开始评估计划: -""") -FINAL_ANSWER = dedent(r""" +""" + ), + 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. + + # 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] = { + LanguageType.CHINESE: dedent( + r""" 综合理解计划执行结果和背景信息,向用户报告目标的完成情况。 # 用户目标 @@ -230,12 +455,50 @@ FINAL_ANSWER = dedent(r""" # 现在,请根据以上信息,向用户报告目标的完成情况: -""") -MEMORY_TEMPLATE = dedent(r""" +""" + ), + 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. + + # 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] = { + LanguageType.CHINESE: dedent( + r""" {% for ctx in context_list %} - 第{{ loop.index }}步:{{ ctx.step_description }} - 调用工具 `{{ ctx.step_name }}`,并提供参数 `{{ ctx.input_data|tojson }}`。 - 执行状态:{{ ctx.step_status }} - 得到数据:`{{ ctx.output_data|tojson }}` {% endfor %} -""") +""" + ), + LanguageType.ENGLISH: 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 f3d6e0d4ae3aea9508d48f33499ac2bcf3de98a9..db2e49a3ee96b07f9e44dd328119d34443966fdf 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,18 +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], + self, query: str, mcp_list: list[dict[str, str]], mcp_ids: list[str], language ) -> MCPSelectResult: """通过LLM选择最合适的MCP Server""" # 初始化jinja2环境 @@ -85,7 +94,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, @@ -133,9 +142,7 @@ class MCPSelector: return result async def select_top_mcp( - self, - query: str, - mcp_list: list[str], + self, query: str, mcp_list: list[str], language: LanguageType = LanguageType.CHINESE ) -> MCPSelectResult: """ 选择最合适的MCP Server @@ -146,17 +153,26 @@ 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: 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") @@ -165,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/mcp_agent/base.py b/apps/scheduler/mcp_agent/base.py index ac3829b418be54bfc5df6ae3d61db82fe0fc129e..b83606854490d3305071dfd87cea854efcdeebec 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 a06104f3b624a0338b87377b628937a7edc00dc9..efdedc02286b6128e4cdc38d251ba195f4d82746 100644 --- a/apps/scheduler/mcp_agent/host.py +++ b/apps/scheduler/mcp_agent/host.py @@ -7,20 +7,15 @@ 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 +from apps.schemas.enum_var import LanguageType logger = logging.getLogger(__name__) @@ -33,29 +28,39 @@ _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 +LLM_QUERY_FIX = { + LanguageType.CHINESE: "请生成修复之后的工具参数", + LanguageType.ENGLISH: "Please generate the tool parameters after repair", +} -class MCPHost(McpBase): + +class MCPHost(MCPBase): """MCP宿主服务""" @staticmethod async def assemble_memory(task: Task) -> str: """组装记忆""" - return _env.from_string(MEMORY_TEMPLATE).render( + return _env.from_string(MEMORY_TEMPLATE[task.language]).render( context_list=task.context, ) @staticmethod - 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( + 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( + prompt = _env.from_string(GEN_PARAMS[task.language]).render( tool_name=mcp_tool.name, tool_description=mcp_tool.description, goal=goal, @@ -63,10 +68,7 @@ class MCPHost(McpBase): input_schema=mcp_tool.input_schema, background_info=await MCPHost.assemble_memory(task), ) - 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, @@ -75,14 +77,18 @@ class MCPHost(McpBase): return result @staticmethod - 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]: - llm_query = "请生成修复之后的工具参数" - prompt = _env.from_string(REPAIR_PARAMS).render( + 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 = "", + language: LanguageType = LanguageType.CHINESE, + ) -> dict[str, Any]: + llm_query = LLM_QUERY_FIX[language] + prompt = _env.from_string(REPAIR_PARAMS[language]).render( tool_name=mcp_tool.name, goal=goal, current_goal=current_goal, diff --git a/apps/scheduler/mcp_agent/plan.py b/apps/scheduler/mcp_agent/plan.py index aaacc84423b8e425a9033d2a313eb2bc505f5fe5..45c096117e1cc9449f7c61698ef70542b105fb12 100644 --- a/apps/scheduler/mcp_agent/plan.py +++ b/apps/scheduler/mcp_agent/plan.py @@ -1,41 +1,44 @@ # 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.schemas.enum_var import LanguageType +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, @@ -46,36 +49,31 @@ _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(), language: LanguageType = LanguageType.CHINESE, + ) -> GoalEvaluationResult: """评估用户目标的可行性""" # 获取推理结果 - result = await MCPPlanner._get_reasoning_evaluation(goal, tool_list, resoning_llm) - - # 解析为结构化数据 - evaluation = await MCPPlanner._parse_evaluation_result(result) + result = await MCPPlanner._get_reasoning_evaluation(goal, tool_list, resoning_llm, language) # 返回评估结果 - 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(), language: LanguageType = LanguageType.CHINESE, + ) -> str: """获取推理大模型的评估结果""" - template = _env.from_string(EVALUATE_GOAL) + template = _env.from_string(EVALUATE_GOAL[language]) 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: @@ -85,27 +83,38 @@ 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) - return result + + return await MCPPlanner._get_reasoning_flow_name(user_goal, resoning_llm, language) @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) + 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 + 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(), + language: LanguageType = LanguageType.CHINESE, + ) -> RestartStepIndex: """获取重新规划的步骤索引""" # 获取推理结果 - template = _env.from_string(GET_REPLAN_START_STEP_INDEX) + template = _env.from_string(GET_REPLAN_START_STEP_INDEX[language]) prompt = template.render( goal=user_goal, error_message=error_message, @@ -123,26 +132,40 @@ 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 tool_ids = [tool.id for tool in tool_list] if is_replan: - template = _env.from_string(RECREATE_PLAN) + 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, @@ -151,14 +174,13 @@ class MCPPlanner(McpBase): max_num=max_steps, ) else: - template = _env.from_string(CREATE_PLAN) + template = _env.from_string(CREATE_PLAN[language]) prompt = template.render( goal=user_goal, 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: @@ -172,11 +194,15 @@ 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(), + 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) @@ -198,12 +224,19 @@ 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(), + 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) prompt = template.render( step_id=step_id, @@ -211,7 +244,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) @@ -223,32 +256,38 @@ 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) - - # 解析为结构化数据 - risk = await MCPPlanner._parse_risk_result(result) + result = await MCPPlanner._get_reasoning_risk( + tool, input_parm, additional_info, resoning_llm, language + ) # 返回风险评估结果 - 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, + language: LanguageType = LanguageType.CHINESE, + ) -> str: """获取推理大模型的风险评估结果""" - template = _env.from_string(RISK_EVALUATE) + template = _env.from_string(RISK_EVALUATE[language]) prompt = template.render( tool_name=tool.name, tool_description=tool.description, 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: @@ -260,11 +299,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) + 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), @@ -273,8 +317,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: @@ -286,24 +329,35 @@ 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) - error_type = await MCPPlanner._parse_tool_execute_error_type_result(result) + user_goal, current_plan, tool, input_param, error_message, reasoning_llm, language + ) # 返回工具执行错误类型 - 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(), + 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, @@ -322,10 +376,14 @@ 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(), + language: LanguageType = LanguageType.CHINESE, + ) -> str: """将错误信息转换为工具描述""" - template = _env.from_string(CHANGE_ERROR_MESSAGE_TO_DESCRIPTION) + template = _env.from_string(CHANGE_ERROR_MESSAGE_TO_DESCRIPTION[language]) prompt = template.render( error_message=error_message, tool_name=tool.name, @@ -338,12 +396,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) + template = _env.from_string(GET_MISSING_PARAMS[language]) schema_with_null = slot.add_null_to_basic_types() prompt = template.render( tool_name=tool.name, @@ -359,10 +420,13 @@ 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(), + language: LanguageType = LanguageType.CHINESE, + ) -> AsyncGenerator[str, None]: """生成最终回答""" - template = _env.from_string(FINAL_ANSWER) + template = _env.from_string(FINAL_ANSWER[language]) prompt = template.render( memory=memory, goal=user_goal, diff --git a/apps/scheduler/mcp_agent/prompt.py b/apps/scheduler/mcp_agent/prompt.py index 824ece8a35dfb3b02afb0e5bd5602d6f5d0d1342..59abd383858e386f32fe7d0ec214cfbbf39b0996 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,16 +63,78 @@ 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工具时的注意事项: 1. 确保充分理解当前目标,选择实现目标所需的MCP工具。 - 2. 不要选择不存在的工具。 + 2. 请在给定的MCP工具列表中选择,不要自己生成MCP工具。 3. 可以选择一些辅助工具,但必须确保这些工具与当前目标相关。 4. 注意,返回的工具ID必须是MCP工具的ID,而不是名称。 - 5. 可以多选择一些工具,用于应对不同的情况。 + 5. 不要选择不存在的工具。 必须按照以下格式生成选择结果,不要输出任何其他内容: ```json { @@ -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""" 你是一个计划生成器。 请分析用户的目标,并生成一个计划。你后续将根据这个计划,一步一步地完成用户的目标。 @@ -263,9 +537,7 @@ CREATE_PLAN = dedent(r""" 1. 能够成功完成用户的目标 2. 计划中的每一个步骤必须且只能使用一个工具。 3. 计划中的步骤必须具有清晰和逻辑的步骤,没有冗余或不必要的步骤。 - 4. 不要选择不存在的工具。 - 5. 计划中的最后一步必须是Final工具,以确保计划执行结束。 - 6. 生成的计划必须要覆盖用户的目标,当然需要考虑一些意外情况,可以有一定的冗余步骤。 + 4. 计划中的最后一步必须是Final工具,以确保计划执行结束。 # 生成计划时的注意事项: @@ -318,7 +590,8 @@ CREATE_PLAN = dedent(r""" - 在后台运行 - 执行top命令 3. 需要先选择MCP Server, 然后生成Docker命令, 最后执行命令 - ```json + + ```json { "plans": [ { @@ -327,17 +600,17 @@ CREATE_PLAN = dedent(r""" "instruction": "需要一个支持Docker容器运行的MCP Server" }, { - "content": "使用第一步选择的MCP Server,生成Docker命令", + "content": "使用Result[0]中选择的MCP Server,生成Docker命令", "tool": "command_generator", "instruction": "生成Docker命令:在后台运行alpine:latest容器,挂载/root到/data,执行top命令" }, { - "content": "执行第二步生成的Docker命令", + "content": "在Result[0]的MCP Server上执行Result[1]生成的命令", "tool": "command_executor", "instruction": "执行Docker命令" }, { - "content": "任务执行完成,容器已在后台运行", + "content": "任务执行完成,容器已在后台运行,结果为Result[2]", "tool": "Final", "instruction": "" } @@ -352,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""" 你是一个计划重建器。 请根据用户的目标、当前计划和运行报错,重新生成一个计划。 @@ -363,9 +740,8 @@ RECREATE_PLAN = dedent(r""" 2. 计划中的每一个步骤必须且只能使用一个工具。 3. 计划中的步骤必须具有清晰和逻辑的步骤,没有冗余或不必要的步骤。 4. 你的计划必须避免之前的错误,并且能够成功执行。 - 5. 不要选择不存在的工具。 - 6. 计划中的最后一步必须是Final工具,以确保计划执行结束。 - 7. 生成的计划必须要覆盖用户的目标,当然需要考虑一些意外情况,可以有一定的冗余步骤。 + 5. 计划中的最后一步必须是Final工具,以确保计划执行结束。 + # 生成计划时的注意事项: - 每一条计划包含3个部分: @@ -501,74 +877,229 @@ RECREATE_PLAN = dedent(r""" {{error_message}} # 重新生成的计划 -""") -GEN_STEP = 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. - # 一个好的计划步骤应该: - 1.使用最适合的工具来完成当前步骤。 - 2.能够基于当前的计划和历史,完成阶段性的任务。 - 3.不要选择不存在的工具。 - 4.如果你认为当前已经达成了用户的目标,可以直接返回Final工具,表示计划执行结束。 + # 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: - # 样例 1 - # 目标 - 我需要扫描当前mysql数据库,分析性能瓶颈, 并调优,我的ip是192.168.1.1,数据库端口是3306,用户名是root,密码是password - # 历史记录 - 第1步:生成端口扫描命令 - - 调用工具 `command_generator`,并提供参数 `帮我生成一个mysql端口扫描命令` - - 执行状态:成功 - - 得到数据:`{"command": "nmap -sS -p--open 192.168.1.1"}` - 第2步:执行端口扫描命令 - - 调用工具 `command_executor`,并提供参数 `{"command": "nmap -sS -p--open 192.168.1.1"}` - - 执行状态:成功 - - 得到数据:`{"result": "success"}` - # 工具 - - - mcp_tool_1 mysql_analyzer;用于分析数据库性能/description> - - mcp_tool_2 文件存储工具;用于存储文件 - - mcp_tool_3 mongoDB工具;用于操作MongoDB数据库 - - Final 结束步骤,当执行到这一步时,表示计划执行结束,所得到的结果将作为最终结果。 - - # 输出 ```json { - "tool_id": "mcp_tool_1", // 选择的工具ID - "description": "扫描ip为192.168.1.1的MySQL数据库,端口为3306,用户名为root,密码为password的数据库性能", + "plans": [ + { + "content": "Plan content", + "tool": "Tool ID", + "instruction": "Tool instruction" + } + ] } ``` - # 样例二 - # 目标 - 计划从杭州到北京的旅游计划 - # 历史记录 - 第1步:将杭州转换为经纬度坐标 - - 调用工具 `maps_geo_planner`,并提供参数 `{"city_from": "杭州", "address": "西湖"}` - - 执行状态:成功 - - 得到数据:`{"location": "123.456, 78.901"}` - 第2步:查询杭州的天气 - - 调用工具 `weather_query`,并提供参数 `{"location": "123.456, 78.901"}` - - 执行状态:成功 - - 得到数据:`{"weather": "晴", "temperature": "25°C"}` - 第3步:将北京转换为经纬度坐标 - - 调用工具 `maps_geo_planner`,并提供参数 `{"city_from": "北京", "address": "天安门"}` - - 执行状态:成功 - - 得到数据:`{"location": "123.456, 78.901"}` - 第4步:查询北京的天气 - - 调用工具 `weather_query`,并提供参数 `{"location": "123.456, 78.901"}` - - 执行状态:成功 - - 得到数据:`{"weather": "晴", "temperature": "25°C"}` - # 工具 + + - 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. + + # 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. - - mcp_tool_4 maps_geo_planner;将详细的结构化地址转换为经纬度坐标。支持对地标性名胜景区、建筑物名称解析为经纬度坐标 - - mcp_tool_5 weather_query;天气查询,用于查询天气信息 - - mcp_tool_6 maps_direction_transit_integrated;根据用户起终点经纬度坐标规划综合各类公共(火车、公交、地铁)交通方式的通勤方案,并且返回通勤方案的数据,跨城场景下必须传起点城市与终点城市 - - Final Final;结束步骤,当执行到这一步时,表示计划执行结束,所得到的结果将作为最终结果。 - - # 输出 + - 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 { - "tool_id": "mcp_tool_6", // 选择的工具ID + "plans": [ + { + "content": "Generate port scan command", + "tool": "command_generator", + "instruction": "Generate port scan command: Scan open ports on 192.168.1.1" + }, + { + "content": "Execute the command generated in the first step", + "tool": "command_executor", + "instruction": "Execute the port scan command" + }, + { + "content": "Task execution completed", + "tool": "Final", + "instruction": "" + } + ] + } + ``` + # 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: + - 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 + - Generate a port scanning command based on the selected network scanning tool + - 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 current machine supports" + + }, + { + "content": "Execute the command generated in the first step to check which network scanning tools the current machine supports", + "tool": "command_executor", + "instruction": "Execute the command generated in the first step" + + }, + { + "content": "Select a network scanning tool from the results of the second step and generate a port scanning command", + "tool": "tool_selector", + "instruction": "Select a network scanning tool and generate a port scanning command" + + }, + { + "content": "Generate a port scan command based on the network scanning tool selected in step 3", + "tool": "command_generator", + "instruction": "Generate a port scan command: Scan the open ports on 192.168.1.1" + }, + { + "content": "Execute the port scan command generated in step 4", + "tool": "command_executor", + "instruction": "Execute the port scan command" + }, + { + "content": "Task execution completed", + "tool": "Final", + "instruction": "" + } + ] + } + ``` + + # Now start regenerating the plan: + + # Goal + + {{goal}} + + # Tools + + You can access and use a number of tools, which are listed within the XML tags. + + + {% for tool in tools %} + - {{tool.id}} {{tool.name}}; {{tool.description}} + {% endfor %} + + + # Current plan + {{current_plan}} + + # Run error + {{error_message}} + + # Regenerated plan + """ + ), +} +GEN_STEP: dict[LanguageType, str] = { + LanguageType.CHINESE: dedent( + r""" + 你是一个计划生成器。 + 请根据用户的目标、当前计划和历史,生成一个新的步骤。 + + # 一个好的计划步骤应该: + 1.使用最适合的工具来完成当前步骤。 + 2.能够基于当前的计划和历史,完成阶段性的任务。 + 3.不要选择不存在的工具。 + 4.如果你认为当前已经达成了用户的目标,可以直接返回Final工具,表示计划执行结束。 + + # 样例 1 + # 目标 + 我需要扫描当前mysql数据库,分析性能瓶颈, 并调优,我的ip是192.168.1.1,数据库端口是3306,用户名是root,密码是password + # 历史记录 + 第1步:生成端口扫描命令 + - 调用工具 `command_generator`,并提供参数 `帮我生成一个mysql端口扫描命令` + - 执行状态:成功 + - 得到数据:`{"command": "nmap -sS -p--open 192.168.1.1"}` + 第2步:执行端口扫描命令 + - 调用工具 `command_executor`,并提供参数 `{"command": "nmap -sS -p--open 192.168.1.1"}` + - 执行状态:成功 + - 得到数据:`{"result": "success"}` + # 工具 + + - mcp_tool_1 mysql_analyzer;用于分析数据库性能/description> + - mcp_tool_2 文件存储工具;用于存储文件 + - mcp_tool_3 mongoDB工具;用于操作MongoDB数据库 + - Final 结束步骤,当执行到这一步时,表示计划执行结束,所得到的结果将作为最终结果。 + + # 输出 + ```json + { + "tool_id": "mcp_tool_1", // 选择的工具ID + "description": "扫描ip为192.168.1.1的MySQL数据库,端口为3306,用户名为root,密码为password的数据库性能", + } + ``` + # 样例二 + # 目标 + 计划从杭州到北京的旅游计划 + # 历史记录 + 第1步:将杭州转换为经纬度坐标 + - 调用工具 `maps_geo_planner`,并提供参数 `{"city_from": "杭州", "address": "西湖"}` + - 执行状态:成功 + - 得到数据:`{"location": "123.456, 78.901"}` + 第2步:查询杭州的天气 + - 调用工具 `weather_query`,并提供参数 `{"location": "123.456, 78.901"}` + - 执行状态:成功 + - 得到数据:`{"weather": "晴", "temperature": "25°C"}` + 第3步:将北京转换为经纬度坐标 + - 调用工具 `maps_geo_planner`,并提供参数 `{"city_from": "北京", "address": "天安门"}` + - 执行状态:成功 + - 得到数据:`{"location": "123.456, 78.901"}` + 第4步:查询北京的天气 + - 调用工具 `weather_query`,并提供参数 `{"location": "123.456, 78.901"}` + - 执行状态:成功 + - 得到数据:`{"weather": "晴", "temperature": "25°C"}` + # 工具 + + - mcp_tool_4 maps_geo_planner;将详细的结构化地址转换为经纬度坐标。支持对地标性名胜景区、建筑物名称解析为经纬度坐标 + - mcp_tool_5 weather_query;天气查询,用于查询天气信息 + - mcp_tool_6 maps_direction_transit_integrated;根据用户起终点经纬度坐标规划综合各类公共(火车、公交、地铁)交通方式的通勤方案,并且返回通勤方案的数据,跨城场景下必须传起点城市与终点城市 + - Final Final;结束步骤,当执行到这一步时,表示计划执行结束,所得到的结果将作为最终结果。 + + # 输出 + ```json + { + "tool_id": "mcp_tool_6", // 选择的工具ID "description": "规划从杭州到北京的综合公共交通方式的通勤方案" } ``` @@ -583,9 +1114,97 @@ GEN_STEP = dedent(r""" - {{tool.id}} {{tool.name}};{{tool.description}} {% endfor %} -""") +""" + ), + LanguageType.ENGLISH: dedent( + r""" + You are a plan generator. + Please generate a new step based on the user's goal, current plan, and history. + + # A good plan step should: + 1. Use the most appropriate tool for the current step. + 2. Complete the tasks at each stage based on the current plan and history. + 3. Do not select a tool that does not exist. + 4. If you believe the user's goal has been achieved, return to the Final tool to complete the plan execution. + + # Example 1 + # Objective + I need to scan the current MySQL database, analyze performance bottlenecks, and optimize it. My IP address is 192.168.1.1, the database port is 3306, my username is root, and my password is password. + # History + Step 1: Generate a port scan command + - Call the `command_generator` tool and provide the `help me generate a MySQL port scan command` parameter. + - Execution status: Success. + - Result: `{"command": "nmap -sS -p --open 192.168.1.1"}` + Step 2: Execute the port scan command + - Call the `command_executor` tool and provide the `{"command": "nmap -sS -p --open 192.168.1.1"}` parameter. + - Execution status: Success. + - Result: `{"result": "success"}` + # Tools + + - mcp_tool_1 mysql_analyzer; used for analyzing database performance. + - mcp_tool_2 File storage tool; used for storing files. + - mcp_tool_3 MongoDB tool; used for operating MongoDB databases. + - Final This step completes the plan execution and the result is used as the final result. + + # Output + ```json + { + "tool_id": "mcp_tool_1", // Selected tool ID + "description": "Scan the database performance of the MySQL database with IP address 192.168.1.1, port 3306, username root, and password password", + } + ``` + # Example 2 + # Objective + Plan a trip from Hangzhou to Beijing + # History + Step 1: Convert Hangzhou to latitude and longitude coordinates + - Call the `maps_geo_planner` tool and provide `{"city_from": "Hangzhou", "address": "West Lake"}` + - Execution status: Success + - Result: `{"location": "123.456, 78.901"}` + Step 2: Query the weather in Hangzhou + - Call the `weather_query` tool and provide `{"location": "123.456, 78.901"}` + - Execution Status: Success + - Result: `{"weather": "Sunny", "temperature": "25°C"}` + Step 3: Convert Beijing to latitude and longitude coordinates + - Call the `maps_geo_planner` tool and provide `{"city_from": "Beijing", "address": "Tiananmen"}` + - Execution Status: Success + - Result: `{"location": "123.456, 78.901"}` + Step 4: Query the weather in Beijing + - Call the `weather_query` tool and provide `{"location": "123.456, 78.901"}` + - Execution Status: Success + - Result: `{"weather": "Sunny", "temperature": "25°C"}` + # Tools + + - mcp_tool_4 maps_geo_planner; Converts a detailed structured address into longitude and latitude coordinates. Supports parsing landmarks, scenic spots, and building names into longitude and latitude coordinates. + - mcp_tool_5 weather_query; Weather query, used to query weather information. + - mcp_tool_6 maps_direction_transit_integrated; Plans a commuting plan based on the user's starting and ending longitude and latitude coordinates, integrating various public transportation modes (train, bus, subway), and returns the commuting plan data. For cross-city scenarios, both the starting and ending cities must be provided. + - Final Final; Final step. When this step is reached, plan execution is complete, and the resulting result is used as the final result. + + # Output + ```json + { + "tool_id": "mcp_tool_6", // Selected tool ID + "description": "Plan a comprehensive public transportation commute from Hangzhou to Beijing" + } + ``` + # Now start generating steps: + # Goal + {{goal}} + # History + {{history}} + # Tools + + {% for tool in tools %} + - {{tool.id}} {{tool.name}};{{tool.description}} + {% endfor %} + + """ + ), +} -TOOL_SKIP = dedent(r""" +TOOL_SKIP: dict[LanguageType, str] = { + LanguageType.CHINESE: dedent( + r""" 你是一个计划执行器。 你的任务是根据当前的计划和用户目标,判断当前步骤是否需要跳过。 如果需要跳过,请返回`true`,否则返回`false`。 @@ -639,8 +1258,67 @@ TOOL_SKIP = dedent(r""" # 输出 """ - ) -RISK_EVALUATE = dedent(r""" + ), + LanguageType.ENGLISH: dedent( + r""" + You are a plan executor. + Your task is to determine whether the current step should be skipped based on the current plan and the user's goal. + If skipping is required, return `true`; otherwise, return `false`. + The answer must follow the following format: + ```json + { + "skip": true/false, + } + ``` + Note: + 1. Be cautious in your judgment and only decide whether to skip the current step when there is sufficient context in the historical messages. + # Example + # User Goal + I need to scan the current MySQL database, analyze performance bottlenecks, and optimize it. + # History + Step 1: Generate a port scan command + - Call the `command_generator` tool with `{"command": "nmap -sS -p--open 192.168.1.1"}` + - Execution Status: Success + - Result: `{"command": "nmap -sS -p--open 192.168.1.1"}` + Step 2: Execute the port scan command + - Call the `command_executor` tool with `{"command": "nmap -sS -p--open 192.168.1.1"}` + - Execution Status: Success + - Result: `{"result": "success"}` + Step 3: Analyze the port scan results + - Call the `mysql_analyzer` tool with `{"host": "192.168.1.1", "port": 3306, "username": "root", "password": "password"}` + - Execution status: Success + - Result: `{"performance": "good", "bottleneck": "none"}` + # Current step + + step_4 + command_generator + Generate MySQL performance tuning commands + Generate MySQL performance tuning commands: Tune MySQL database performance + + # Output + ```json + { + "skip": true + } + ``` + # User goal + {{goal}} + # History + {{history}} + # Current step + + {{step_id}} + {{step_name}} + {{step_instruction}} + {{step_content}} + + # output + """ + ), +} +RISK_EVALUATE: dict[LanguageType, str] = { + LanguageType.CHINESE: dedent( + r""" 你是一个工具执行计划评估器。 你的任务是根据当前工具的名称、描述和入参以及附加信息,判断当前工具执行的风险并输出提示。 ```json @@ -677,58 +1355,116 @@ RISK_EVALUATE = dedent(r""" } ``` # 工具 - < tool > - < name > {{tool_name}} < /name > - < description > {{tool_description}} < /description > - < / tool > + + {{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 = dedent(r""" +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": "" + } + ] + } # 当前使用的工具 - < tool > - < name > command_executor < /name > - < description > 执行命令行指令 < /description > - < / tool > + + command_executor + 执行命令行指令 + # 工具入参 { "command": "nmap -sS -p--open 192.168.1.1" @@ -747,18 +1483,97 @@ TOOL_EXECUTE_ERROR_TYPE_ANALYSIS = dedent(r""" # 当前计划 {{current_plan}} # 当前使用的工具 - < tool > - < name > {{tool_name}} < /name > - < description > {{tool_description}} < /description > - < / tool > + + {{tool_name}} + {{tool_description}} + # 工具入参 {{input_param}} # 工具运行报错 {{error_message}} # 输出 """ - ) -IS_PARAM_ERROR = dedent(r""" + ), + 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 + """ + ), +} + +IS_PARAM_ERROR: dict[LanguageType, str] = { + LanguageType.CHINESE: dedent( + r""" 你是一个计划执行专家,你的任务是判断当前的步骤执行失败是否是因为参数错误导致的, 如果是,请返回`true`,否则返回`false`。 必须按照以下格式回答: @@ -817,9 +1632,74 @@ IS_PARAM_ERROR = dedent(r""" {{error_message}} # 输出 """ - ) + ), + LanguageType.ENGLISH: dedent( + r""" + You are a plan execution expert. Your task is to determine whether the current step execution failure is due to parameter errors. + If so, return `true`; otherwise, return `false`. + The answer must be in the following format: + ```json + { + "is_param_error": true/false, + } + ``` + # Example + # User Goal + I need to scan the current MySQL database, analyze performance bottlenecks, and optimize it. + # History + Step 1: Generate a port scan command + - Call the `command_generator` tool and provide `{"command": "nmap -sS -p--open 192.168.1.1"}` + - Execution Status: Success + - Result: `{"command": "nmap -sS -p--open 192.168.1.1"}` + Step 2: Execute the port scan command + - Call the `command_executor` tool and provide `{"command": "nmap -sS -p--open 192.168.1.1"}` + - Execution Status: Success + - Result: `{"result": "success"}` + # Current step + + step_3 + mysql_analyzer + Analyze MySQL database performance + + # Tool input parameters + { + "host": "192.0.0.1", + "port": 3306, + "username": "root", + "password": "password" + } + # Tool execution error + When executing the MySQL performance analysis command, an error occurred: `host is not correct`. + + # Output + ```json + { + "is_param_error": true + } + ``` + # User goal + {{goal}} + # History + {{history}} + # Current step + + {{step_id}} + {{step_name}} + {{step_instruction}} + + # Tool input parameters + {{input_param}} + # Tool error + {{error_message}} + # Output + """ + ), +} + # 将当前程序运行的报错转换为自然语言 -CHANGE_ERROR_MESSAGE_TO_DESCRIPTION = dedent(r""" +CHANGE_ERROR_MESSAGE_TO_DESCRIPTION: dict[LanguageType, str] = { + LanguageType.CHINESE: dedent( + r""" 你是一个智能助手,你的任务是将当前程序运行的报错转换为自然语言描述。 请根据以下规则进行转换: 1. 将报错信息转换为自然语言描述,描述应该简洁明了,能够让人理解报错的原因和影响。 @@ -829,10 +1709,10 @@ CHANGE_ERROR_MESSAGE_TO_DESCRIPTION = dedent(r""" 5. 只输出自然语言描述,不要输出其他内容。 # 样例 # 工具信息 - < tool > - < name > port_scanner < /name > - < description > 扫描主机端口 < /description > - < input_schema > + + port_scanner + 扫描主机端口 + { "type": "object", "properties": { @@ -850,13 +1730,13 @@ CHANGE_ERROR_MESSAGE_TO_DESCRIPTION = dedent(r""" }, "password": { "type": "string", - "description": "密码" + "description": "密码" } }, "required": ["host", "port", "username", "password"] } - < /input_schema > - < / tool > + + # 工具入参 { "host": "192.0.0.1", @@ -870,21 +1750,91 @@ CHANGE_ERROR_MESSAGE_TO_DESCRIPTION = dedent(r""" 扫描端口时发生错误:密码不正确。请检查输入的密码是否正确,并重试。 # 现在开始转换报错信息: # 工具信息 - < tool > - < name > {{tool_name}} < /name > - < description > {{tool_description}} < /description > - < input_schema > + + {{tool_name}} + {{tool_description}} + {{input_schema}} - < /input_schema > - < / tool > + + # 工具入参 {{input_params}} # 报错信息 {{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 @@ -909,42 +1859,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", @@ -953,21 +1987,25 @@ GET_MISSING_PARAMS = dedent(r""" "password": null } ``` - # 工具 - < tool > - < name > {{tool_name}} < /name > - < description > {{tool_description}} < /description > - < / tool > - # 工具入参 + # 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 """ - ) -GEN_PARAMS = dedent(r""" + ), +} + +GEN_PARAMS: dict[LanguageType, str] = { + LanguageType.CHINESE: dedent( + r""" 你是一个工具参数生成器。 你的任务是根据总的目标、阶段性的目标、工具信息、工具入参的schema和背景信息生成工具的入参。 注意: @@ -1041,21 +2079,100 @@ GEN_PARAMS = dedent(r""" {{background_info}} # 输出 """ - ) + ), + LanguageType.ENGLISH: dedent( + r""" + You are a tool parameter generator. + Your task is to generate tool input parameters based on the overall goal, phased goals, tool information, tool input parameter schema, and background information. + Note: + 1. The generated parameters must conform to the tool input parameter schema. + 2. The overall goal, phased goals, and background information must be fully understood and used to generate tool input parameters. + 3. The generated parameters must conform to the phased goals. + + # Example + # Tool Information + < tool > + < name >mysql_analyzer < /name > + < description > Analyze MySQL Database Performance < /description > + < / tool > + # Overall Goal + I need to scan the current MySQL database, analyze performance bottlenecks, and optimize it. The IP address is 192.168.1.1, the port is 3306, the username is root, and the password is password. + # Current Phase Goal + I need to connect to the MySQL database, analyze performance bottlenecks, and optimize it. # Tool input 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"] + } + # Background information + Step 1: Generate a port scan command + - Call the `command_generator` tool and provide the `Help me generate a MySQL port scan command` parameter + - Execution status: Success + - Received data: `{"command": "nmap -sS -p --open 192.168.1.1"}` + + Step 2: Execute the port scan command + - Call the `command_executor` tool and provide the parameters `{"command": "nmap -sS -p --open 192.168.1.1"}` + - Execution status: Success + - Received data: `{"result": "success"}` + # Output + ```json + { + "host": "192.168.1.1", + "port": 3306, + "username": "root", + "password": "password" + } + ``` + # Tool + < tool > + < name > {{tool_name}} < /name > + < description > {{tool_description}} < /description > + < / tool > + # Overall goal + {{goal}} + # Current stage goal + {{current_goal}} + # Tool input scheme + {{input_schema}} + # Background information + {{background_info}} + # Output + """ + ), +} -REPAIR_PARAMS = dedent(r""" +REPAIR_PARAMS: dict[LanguageType, str] = { + LanguageType.CHINESE: dedent( + r""" 你是一个工具参数修复器。 你的任务是根据当前的工具信息、目标、工具入参的schema、工具当前的入参、工具的报错、补充的参数和补充的参数描述,修复当前工具的入参。 - + 注意: 1.最终修复的参数要符合目标和工具入参的schema。 - + # 样例 # 工具信息 - < tool > - < name > mysql_analyzer < /name > - < description > 分析MySQL数据库性能 < /description > - < / tool > + + mysql_analyzer + 分析MySQL数据库性能 + # 总目标 我需要扫描当前mysql数据库,分析性能瓶颈, 并调优 # 当前阶段目标 @@ -1109,10 +2226,10 @@ REPAIR_PARAMS = dedent(r""" } ``` # 工具 - < tool > - < name > {{tool_name}} < /name > - < description > {{tool_description}} < /description > - < / tool > + + {{tool_name}} + {{tool_description}} + # 总目标 {{goal}} # 当前阶段目标 @@ -1121,8 +2238,6 @@ REPAIR_PARAMS = dedent(r""" {{input_schema}} # 工具入参 {{input_param}} - # 工具描述 - {{tool_description}} # 运行报错 {{error_message}} # 补充的参数 @@ -1131,8 +2246,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""" 综合理解计划执行结果和背景信息,向用户报告目标的完成情况。 # 用户目标 @@ -1145,15 +2340,57 @@ FINAL_ANSWER = dedent(r""" {{memory}} + # 其他背景信息: + + {{status}} # 现在,请根据以上信息,向用户报告目标的完成情况: -""") -MEMORY_TEMPLATE = dedent(r""" - {% for ctx in context_list % } + """ + ), + 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.step_status}} - - 得到数据:`{{ctx.output_data}}` - {% endfor % } -""") + - 调用工具 `{{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 075e08f00cb2b5a2976d5372c623193b04c42412..aea6cd11f260e261a0ebdbd94452aa08789cda34 100644 --- a/apps/scheduler/mcp_agent/select.py +++ b/apps/scheduler/mcp_agent/select.py @@ -3,28 +3,17 @@ 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 +from apps.schemas.enum_var import LanguageType + logger = logging.getLogger(__name__) _env = SandboxedEnvironment( @@ -38,24 +27,35 @@ FINAL_TOOL_ID = "FIANL" SUMMARIZE_TOOL_ID = "SUMMARIZE" -class MCPSelector(McpBase): +class MCPSelector(MCPBase): """MCP选择器""" @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) + 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 @@ -66,18 +66,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 @@ -90,7 +103,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) @@ -102,8 +118,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 diff --git a/apps/scheduler/pool/loader/flow.py b/apps/scheduler/pool/loader/flow.py index 57344d40a5c1aba0454a7f00fffcfbf5a93ee221..eeb856408e06579a9286f115716637f5439fc488 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 NodeType,EdgeType, LanguageType from apps.schemas.flow import AppFlow, Flow from apps.schemas.pool import AppPool from apps.models.vector import FlowPoolVector @@ -26,7 +26,6 @@ BASE_PATH = Path(Config().get_config().deploy.data_dir) / "semantics" / "app" class FlowLoader: """工作流加载器""" - async def _load_yaml_file(self, flow_path: Path) -> dict[str, Any]: """从YAML文件加载工作流配置""" try: @@ -68,7 +67,6 @@ class FlowLoader: return {} else: return flow_yaml - 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) @@ -77,25 +75,18 @@ class FlowLoader: err = f"[FlowLoader] 步骤名称不能以下划线开头:{key}" logger.error(err) raise ValueError(err) - if key == "start": - step["name"] = "开始" - step["description"] = "开始节点" - step["type"] = "start" - elif key == "end": - step["name"] = "结束" - step["description"] = "结束节点" - step["type"] = "end" - else: - try: - step["type"] = await NodeManager.get_node_call_id(step["node"]) - except ValueError as e: - logger.warning("[FlowLoader] 获取节点call_id失败:%s,错误信息:%s", step["node"], e) - step["type"] = "Empty" - step["name"] = ( - (await NodeManager.get_node_name(step["node"])) - if "name" not in step or step["name"] == "" - else step["name"] - ) + if step["type"]==NodeType.START.value or step["type"]==NodeType.END.value: + continue + try: + step["type"] = await NodeManager.get_node_call_id(step["node"]) + except ValueError as e: + logger.warning("[FlowLoader] 获取节点call_id失败:%s,错误信息:%s", step["node"], e) + step["type"] = "Empty" + step["name"] = ( + (await NodeManager.get_node_name(step["node"])) + if "name" not in step or step["name"] == "" + else step["name"] + ) return flow_yaml async def load(self, app_id: str, flow_id: str) -> Flow | None: diff --git a/apps/scheduler/pool/mcp/client.py b/apps/scheduler/pool/mcp/client.py index 68f84ffa07201ae518737edab847700031ddbec3..cba9bf5cb08d6e5c1cd8819588e5313de0f9cc11 100644 --- a/apps/scheduler/pool/mcp/client.py +++ b/apps/scheduler/pool/mcp/client.py @@ -130,12 +130,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 @@ -149,5 +150,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 02392b366e17b59110ce6a467e63daa081e8a427..a4b1ab8f2ef74db019a94d875c47aff609123c83 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("-") and arg != "run": 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 bf0320f429a9ef45864ba6548b1bb28e3d874b59..cb76864c68823d3f3a0ce69a36b446c7de3747c0 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/scheduler/pool/pool.py b/apps/scheduler/pool/pool.py index ead552fc2994150137121b103ff013d8ea4d66a5..b4ba3eeadd64a0e9421ada6f7ec91ed733528682 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 @@ -146,7 +146,9 @@ class Pool: else: 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 + ) -> Flow | None: """从文件系统中获取单个Flow的全部数据""" logger.info("[Pool] 获取工作流 %s", flow_id) flow_loader = FlowLoader() diff --git a/apps/scheduler/scheduler/message.py b/apps/scheduler/scheduler/message.py index d43ba7fb891d2b1ed7ecdb3378a8ad8e0e512286..8ccead7f3b2ec3d808c4de7d3083d749a3b80fdf 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 @@ -60,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): + 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 b524d249a6f55caf779a473ef39db35a463525b9..14b49be58d2edbb757c2eaa94966f871fd1f3d70 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: @@ -265,12 +270,12 @@ class Scheduler: self.task = flow_chooser.task logger.info("[Scheduler] 获取工作流定义") flow_data = await Pool().get_flow(app_info.app_id, flow_id) - + # 如果flow_data为空,则直接返回 if not flow_data: logger.error("[Scheduler] 未找到工作流定义") return - + # 初始化Executor logger.info("[Scheduler] 初始化Executor") flow_exec = FlowExecutor( diff --git a/apps/schemas/enum_var.py b/apps/schemas/enum_var.py index 3ae7c42528d27c3e0d17970ba5e959db8e1bed44..f76e75028253e430766777bdb169cf5d77db40cc 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/message.py b/apps/schemas/message.py index 5b465ee54321bbd4c649753911025bff41840186..17a569ca2cf710b206ddbc1afb655bf4675af7fd 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 3fd5a67fa29af6e1685d55285ee1ebd458cabce2..7a4d6bb69be8ef8e3feb07dce750843bfef6b1ed 100644 --- a/apps/schemas/request_data.py +++ b/apps/schemas/request_data.py @@ -7,10 +7,11 @@ 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 +from apps.schemas.message import FlowParams + class RequestDataApp(BaseModel): @@ -42,12 +43,12 @@ class RequestData(BaseModel): question: str | None = Field(default=None, max_length=2000, description="用户输入") conversation_id: str | None = Field(default=None, 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="是否调试") 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): diff --git a/apps/schemas/task.py b/apps/schemas/task.py index d3e7b036c35f8ecf53a204b8b8a28e4600738184..2bd292b67322aa586d557546f28703d2b3e11469 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,8 +99,8 @@ 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) class StepQueueItem(BaseModel): diff --git a/apps/services/flow.py b/apps/services/flow.py index 4d682e5a85677b3a526ec9d6d2db521c238357b4..cedd58260b3a2befbffb84d5feab29d56c8f6298 100644 --- a/apps/services/flow.py +++ b/apps/services/flow.py @@ -4,12 +4,13 @@ import logging from pymongo import ASCENDING +from pydantic import BaseModel 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, @@ -19,7 +20,9 @@ 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 +68,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: LanguageType = LanguageType.CHINESE + ) -> list[NodeMetaDataItem] | None: """ serviceId获取service的接口数据,并将接口转换为节点元数据 @@ -91,15 +96,26 @@ class FlowManager: except Exception: logger.exception("[FlowManager] generate_from_schema 失败") continue + + if service_id == "": + 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"], - description=node_pool_record["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] 获取节点元数据失败") @@ -108,7 +124,9 @@ 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: LanguageType = LanguageType.CHINESE + ) -> list[NodeServiceItem] | None: """ 通过user_id获取用户自己上传的、其他人公开的且收藏的、受保护且有权限访问并收藏的service @@ -148,7 +166,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 == LanguageType.CHINESE else "System", + type="system", + nodeMetaDatas=[], + ) + ] service_items += [ NodeServiceItem( serviceId=record["_id"], @@ -160,7 +185,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 +229,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 + ) -> FlowItem | None: # noqa: C901, PLR0911, PLR0912 """ 通过appId flowId获取flow config的路径和focus,并通过flow config的路径获取flow config,并将其转换为flow item。 @@ -271,8 +300,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) @@ -335,7 +363,6 @@ class FlowManager: tuple(sorted((k, str(v)) for k, v in step.params.items())), ) step_list_2.append(step_tuple) - # 排序后比较 if sorted(step_list_1) != sorted(step_list_2): return False @@ -348,9 +375,7 @@ class FlowManager: @staticmethod async def put_flow_by_app_and_flow_id( - app_id: str, - flow_id: str, - flow_item: FlowItem, + app_id: str, flow_id: str, flow_item: FlowItem ) -> FlowItem | None: """ 存储/更新flow的数据库数据和配置文件 @@ -410,7 +435,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] 存储/更新流失败") @@ -446,7 +471,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) -> bool: + async def update_flow_debug_by_app_and_flow_id( + app_id: str, flow_id: str, *, debug: bool + ) -> bool: """ 更新flow的debug状态 diff --git a/apps/services/rag.py b/apps/services/rag.py index b50db3b791c3fd5ff4349bd297130b5e19df25dc..1fffbee60ace62ea8700656071132a3adb86b73c 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,59 +28,106 @@ 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[LanguageType, str] = { + LanguageType.CHINESE: 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} - - """ + """, + 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 . + 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. Your response should not exceed 250 words. + + + + + + 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 +185,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: @@ -172,31 +227,40 @@ 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: LanguageType = LanguageType.CHINESE, ) -> AsyncGenerator[str, None]: """获取RAG服务的结果""" reasion_llm = ReasoningLLM( @@ -210,13 +274,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, { @@ -225,7 +293,7 @@ class RAG: }, { "role": "user", - "content": RAG.user_prompt.format( + "content": RAG.user_prompt[language].format( bac_info=bac_info, user_question=data.query, ), @@ -269,8 +337,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( diff --git a/deploy/chart/euler_copilot/configs/deepinsight/.env b/deploy/chart/euler_copilot/configs/deepinsight/.env new file mode 100644 index 0000000000000000000000000000000000000000..b753d5c3ed98685de3c1fde1cb272f166487cd2e --- /dev/null +++ b/deploy/chart/euler_copilot/configs/deepinsight/.env @@ -0,0 +1,41 @@ +# .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 + +# 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 + +# 默认监听地址和端口 +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/templates/deepinsight-web/deepinsight-web-config.yaml b/deploy/chart/euler_copilot/templates/deepinsight-web/deepinsight-web-config.yaml new file mode 100644 index 0000000000000000000000000000000000000000..2981fce549fe08b721e01488dc846d18b42f908a --- /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 0000000000000000000000000000000000000000..1e645538e37820832c51bd40fbd05740dde0c0c3 --- /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 0000000000000000000000000000000000000000..0a0506ae2158e29c1f165841ae218a1dce96d085 --- /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 0000000000000000000000000000000000000000..0553728dd5ab9de4f62f523c4d0fcfe9aaa09e23 --- /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 1793023e13f014cac11ac2939106f14688620c8b..da421edaa5bd30f2c55fdf1cf7ee8bcdcc1df507 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 c20c045dd686f0c1607467f5c680821afe7e7994..3bb939b64f4d88d0c5174ac6e7f591f38606f897 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 diff --git a/deploy/scripts/2-install-tools/install_tools.sh b/deploy/scripts/2-install-tools/install_tools.sh index 804c735da93d2b2c5e69fca808886a2d13390b55..8536d036514b1d814fb13265fc911c3fb4f45c65 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 0 + 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 af351df511465898cb7095f2368d9c14c6753e95..cf878866cbdc5c103f2e52ae2d3feabcca8b451d 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 "$@"