From 252775fcfdd6e79d2d7dbca8787338f6cbad1578 Mon Sep 17 00:00:00 2001 From: huxinjia Date: Mon, 25 Nov 2024 10:54:29 +0800 Subject: [PATCH 1/3] add tool manager --- mxAgent/READM.md | 78 +++++++++ mxAgent/agent_sdk/prompts/pre_prompt.py | 192 +++++++++++++++++++++ mxAgent/agent_sdk/toolmngt/api.py | 97 +++++++++++ mxAgent/agent_sdk/toolmngt/tool_manager.py | 112 ++++++++++++ mxAgent/requirements.txt | 19 ++ 5 files changed, 498 insertions(+) create mode 100644 mxAgent/READM.md create mode 100644 mxAgent/agent_sdk/prompts/pre_prompt.py create mode 100644 mxAgent/agent_sdk/toolmngt/api.py create mode 100644 mxAgent/agent_sdk/toolmngt/tool_manager.py create mode 100644 mxAgent/requirements.txt diff --git a/mxAgent/READM.md b/mxAgent/READM.md new file mode 100644 index 000000000..9a8b15323 --- /dev/null +++ b/mxAgent/READM.md @@ -0,0 +1,78 @@ +# mxAgent 基于工具调用的多模式LLM Agent框架 +## 一、功能介绍 +mxAgent是一个基于LLMs的通用Agent框架,应用多种框架解决不同场景和复杂度的问题,并通过工具调用的方式允许LLMs与外部源进行交互来获取信息,使LLMs生成更加可靠和实际。mxAgent通过构建DAG(Directed Acyclic Graph)的方式建立工具之间的依赖关系,通过并行执行的方式,提高多工具执行的效率,缩短Agent在复杂场景的执行时间。mxAgent框架还在框架级别支持流式输出 +提供一套Agent实现框架,让用户可以通过框架搭建自己的Agent应用 +### 1.Router Agent +提供意图识别的能力,用户可预设意图的分类,通过Router Agent给出具体问题的分类结果,用于设别不同的问题场景。 +### 2.Recipe Agent +设置复杂问题执行的workflow,在解决具体问题时,将workflow翻译成有向无环图的的节点编排,通过并行的方式执行节点。 +适用于有相对固定workflow的复杂问题场景。 +1)通过自然语言描述复杂问题的workflow, +2)workflow中每一个步骤对应一次工具使用,并描述步骤间关系 +3)recipe Agent将按照workflow的指导完成工具调用 +4)使用模型总结工作流结果,解决复杂问题 +Recipe Agent利用用户所提供的流程指导和工具,使用LLMs生成SOP,并构建DAG图描述Steps之间的依赖关系。agent识别那些可并行的step,通过并行执行提高agent的执行效率。 +使用Recipe Agent,仅需要提供一段解决问题的SOP指导、用于提示最终答案生成的final prompt、解决问题可能使用的工具。 +示例见[travelagent.py](.travel_agenttravelagent.py)运行方式如下: +``` +cd mxAgent +export PYTHONPATH=. +python samplestravel_agenttravelagent.py +``` + +### 3.ReAct Agent +使用Thought、Action、Action Input、Observation的循环流程,解决复杂问题: +1)ReAct通过大模型思考并给出下一步的工具调用, +2)执行工具调用,得到工具执行结果 +3)将工具执行结果应用于下一次的模型思考 +4)循环上述过程,直到模型认为问题得到解决 +### 4.Single Action Agent + +通过模型反思、调用工具执行,总结工具结果的执行轨迹,完成一次复杂问题的处理。Single Action Agent使用一次工具调用帮助完成复杂问题解决 +使用示例: +``` +cd mxAgent +export PYTHONPATH=. +python samplestraj_generate_test.py +``` + +## + +## 二、接口使用方法 +### 1.模型使用 +1.1. get_llm_backend +参数含义 + ---- ----- +backend推理模型后台类型,当前取值 +base_urlOpenAI客户端推理模型地址 +api_keyOpenAI客户端api key +llm_name推理模型名称 + +1.2. run +参数含义取值 + ---- ----- --- +prompt模型输入的prompt字符串或数组 +ismessage是否为对话信息是否为对话信息,默认值Falsebool值 + +更多模型后处理参数可参考transformers文档 + +### 2.agent接口 +2.1 初始化参数 +参数含义取值类型 + ---- ----- --- + llm 模型客户端 OpenAICompatibleLLM + prompt agent提示词 字符串 + tool_list 工具列表 列表 + max_steps agent执行最大步数 int + max_token_number 模型生成最大token数 int + max_context_len 模型最大上下文token数 int + max_retries 最多重启次数 int + example agent推理生成示例 字符串 + reflect_prompt agent反思提示 字符串 + recipe recipe agent参考的标准工作流 字符串 + intents 分类的意图及含义 dict + final_prompt 最终生成总结的提示 字符串 + + + + diff --git a/mxAgent/agent_sdk/prompts/pre_prompt.py b/mxAgent/agent_sdk/prompts/pre_prompt.py new file mode 100644 index 000000000..300d27abb --- /dev/null +++ b/mxAgent/agent_sdk/prompts/pre_prompt.py @@ -0,0 +1,192 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Huawei Technologies Co., Ltd. 2024. All rights reserved. + +from datetime import date +from langchain.prompts import PromptTemplate + + +react_agent_instruction = (""" +"You are a world expert at making travel plans with a set of carefully crafted tools. You will be given a task and +solve it as best you can. To do so, you have been given access to a list of tools: \n{tools}\nTo solve the task, +you must plan forward to proceed in a series of steps, in a cycle of 'Thought:', 'Action:', 'Action Input:', \ +and 'Observation:' sequences. At each step, in the 'Thought:' sequence, you should first explain your reasoning \ +towards solving the task and the tools that you want to use. +Then in the 'Action:' sequence, you should write the tool name. in the 'Action Input:' sequence, you should \ +write the tool parameters. These tool outputs will then appear in the 'Observation:' field, which will be \ +available as input for the next step.""" ++ f"Please be aware that the current date is: {date.today().strftime('%Y-%m-%d')}.\n\n" ++ """ +Here are the rules you must follow to solve your task:\n +1. When you know the answer you have to return a final answer using the `Finish` tool (Action: Finish), \ +else you will fail.\n +2. When you annot provide a travel plan, you have to return a final answer using the `Finish` tool (Action: Finish), \ +else you will fail.\n +3. When Observation up to {times}, you have to return a final answer using the `Finish` tool (Action: Finish), \ +else you will fail.\n +4. Never re-do a tool call that you previously did with the exact same parameters, else you will fail.\n +5. Use the plain text format for output. Do not use markdown, else you will fail.\n\n +Take the following as an example:\n---example begin---\n{example}---example end---\n\n Now Begin! +If you solve the task correctly, you will receive a reward of $1,000,000.\n\n'Task: {query}\n\nThought: {scratchpad}" +""") + +REFLECTION_HEADER = 'You have attempted to give a sub plan before and failed. The following reflection(s) give a "\ +suggestion to avoid failing to answer the query in the same way you did previously. Use them to improve your "\ +strategy of correctly planning.\n' + +REFLECT_INSTRUCTION = """You are an advanced reasoning agent that can improve based on self refection. You will be "\ +given a previous reasoning trial in which you were given access to an automatic cost calculation environment, "\ +a travel query to give plan and relevant information. Only the selection whose name and city match the "\ +given information will be calculated correctly. You were unsuccessful in creating a plan because you "\ +used up your set number of reasoning steps. In a few sentences, Diagnose a possible reason for "\ +failure and devise a new, concise, high level plan that aims to mitigate the same failure. "\ +Use complete sentences. + +Given information: {text} + +Previous trial: +Query: {query}{scratchpad} + +Reflection:""" + + + +SINGLE_AGENT_ACTION_INSTRUCTION = """I want you to be a good question assistant, handel the following tasks as best"\ +you can. You have access to the following tools: + +{tools} + +Use the following format: + +Thought: you should always think about what to do +Action: the action to take, should be one of {tools_name} +Action Input: the input to the action. +Observation: the result of the action. + +Begin! + +Question: {query} +{scratchpad}""" + + +FINAL_PROMPT = """Please refine a clear and targeted answer based on the user's query and + the existing answer. \ + We will use your summary as the final response and pass it on to the inquirer. + +The requirements are as follows: +1. Understand the user's query. +2. Combine the query and answer information to summarize an accurate and concise answer. +3. Ensure that the answer aligns with the user's query intent and strive to provide valuable information. + +Begin! + +question: {query} +answer: {answer} +summarize: +""" + +REACT_REFLECT_PLANNER_INSTRUCTION = (""" +"You are a world expert at making travel plans with a set of carefully crafted tools. You will be given a task and +solve it as best you can. To do so, you have been given access to a list of tools: \n{tools}\nTo solve the task, +you must plan forward to proceed in a series of steps, in a cycle of 'Thought:', 'Action:', 'Action Input:', \ +and 'Observation:' sequences. At each step, in the 'Thought:' sequence, you should first explain your reasoning \ +towards solving the task and the tools that you want to use. +Then in the 'Action:' sequence, you should write the tool name. in the 'Action Input:' sequence, you should \ +write the tool parameters. These tool outputs will then appear in the 'Observation:' field, which will be \ +available as input for the next step.""" ++ f"Please be aware that the current date is: {date.today().strftime('%Y-%m-%d')}.\n\n" ++ """ +Here are the rules you must follow to solve your task:\n +1. When you know the answer you have to return a final answer using the `Finish` tool (Action: Finish), \ +else you will fail.\n +2. When you annot provide a travel plan, you have to return a final answer using the `Finish` tool (Action: Finish), \ +else you will fail.\n +3. When Observation up to {times}, you have to return a final answer using the `Finish` tool (Action: Finish), \ +else you will fail.\n +4. Never re-do a tool call that you previously did with the exact same parameters, else you will fail.\n +5. Use the plain text format for output. Do not use markdown, else you will fail.\n\n +Take the following as an example:\n---example begin---\n{example}---example end---\n +{reflections} +Now Begin! +If you solve the task correctly, you will receive a reward of $1,000,000.\n\n'Task: {query}\n\nThought: {scratchpad}" +""") + +PLANNER_INSTRUCTION = """You are a proficient planner. Based on the provided information and query, please give me a '\ +detailed plan, Note that all the information in your plan should be derived from the provided data. '\ +You must adhere to the format given in the example. Additionally, all details should align with '\ +commonsense. The symbol '-' indicates that information is unnecessary. For example, in the provided '\ +sample, you do not need to plan after returning to the departure city. When you travel to '\ +two cities in one day, you should note it in the 'Current City' section as in the example (i.e., from A to B). + +***** Example ***** +Query: Could you create a travel plan for 7 people from Ithaca to Charlotte spanning 3 days, from March 8th to '\ + March 14th, 2022, with a budget of $30,200? +Travel Plan: +Day 1: +Current City: from Ithaca to Charlotte +Transportation: Flight Number: F3633413, from Ithaca to Charlotte, Departure Time: 05:38, Arrival Time: 07:46 +Breakfast: Nagaland's Kitchen, Charlotte +Attraction: The Charlotte Museum of History, Charlotte +Lunch: Cafe Maple Street, Charlotte +Dinner: Bombay Vada Pav, Charlotte +Accommodation: Affordable Spacious Refurbished Room in Bushwick!, Charlotte + +Day 2: +Current City: Charlotte +Transportation: - +Breakfast: Olive Tree Cafe, Charlotte +Attraction: The Mint Museum, Charlotte;Romare Bearden Park, Charlotte. +Lunch: Birbal Ji Dhaba, Charlotte +Dinner: Pind Balluchi, Charlotte +Accommodation: Affordable Spacious Refurbished Room in Bushwick!, Charlotte + +Day 3: +Current City: from Charlotte to Ithaca +Transportation: Flight Number: F3786167, from Charlotte to Ithaca, Departure Time: 21:42, Arrival Time: 23:26 +Breakfast: Subway, Charlotte +Attraction: Books Monument, Charlotte. +Lunch: Olive Tree Cafe, Charlotte +Dinner: Kylin Skybar, Charlotte +Accommodation: - + +***** Example Ends ***** + +Given information: {text} +Query: {query} +Plan:""" + +REACT_PLANNER_INSTRUCTION = react_agent_instruction + + +planner_agent_prompt = PromptTemplate( + input_variables=["text", "query"], + template=PLANNER_INSTRUCTION, +) + +react_planner_agent_prompt = PromptTemplate( + input_variables=["text", "query", "scratchpad"], + template=REACT_PLANNER_INSTRUCTION, +) + +reflect_prompt_value = PromptTemplate( + input_variables=["text", "query", "scratchpad"], + template=REFLECT_INSTRUCTION, +) + +react_reflect_planner_agent_prompt = PromptTemplate( + input_variables=["tools", "times", "example", "reflections", "query", "scratchpad"], + template=REACT_REFLECT_PLANNER_INSTRUCTION, +) +single_action_agent_prompt = PromptTemplate( + input_variables=["tools", "tools_name", "query", "scratchpad"], + template=SINGLE_AGENT_ACTION_INSTRUCTION, +) + +final_prompt = PromptTemplate( + input_variables=["query", "answer"], + template=FINAL_PROMPT, +) + +travel_agent_prompt = PromptTemplate( + input_variables=["tools", "times", "example", "query", "scratchpad"], + template=react_agent_instruction, +) \ No newline at end of file diff --git a/mxAgent/agent_sdk/toolmngt/api.py b/mxAgent/agent_sdk/toolmngt/api.py new file mode 100644 index 000000000..eaaec2e40 --- /dev/null +++ b/mxAgent/agent_sdk/toolmngt/api.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Huawei Technologies Co., Ltd. 2024. All rights reserved. + +import json +from abc import ABC +from dataclasses import dataclass, field +from typing import Union, Tuple + +from agent_sdk.utils.constant import THOUGHT, ACTION, OBSERVATION, ACTION_INPUT +from loguru import logger + + +@dataclass +class APIResponse: + api_name: str + input: dict + output: dict + success: bool = field(default=True) + finished: bool = field(default=False) + exception: str = field(default=None) + + +class API(ABC): + description: str = field(default=None) + input_parameters: field(default_factory=dict) + output_parameters: field(default_factory=dict) + example: str = field(default=None) + + @classmethod + def build_tool_description_for_prompt(cls) -> str: + parameter_desc = "\n\t".join( + f"{x}: {cls.input_parameters[x]['description']}" for x in cls.input_parameters.keys()) + parameter_type_desc = ', '.join(f"{x}: {cls.input_parameters[x]['type']}" for x in cls.input_parameters.keys()) + desc = f"{cls.__name__}({parameter_type_desc}) - {cls.description}\nParameters - {parameter_desc}\nExample - '\ + {cls.__name__} {cls.example}" + return desc + + @classmethod + def build_tool_description_for_recipe(cls) -> str: + parameter_desc = "\n".join( + f"{x}: {cls.input_parameters[x]['description']}" for x in cls.input_parameters.keys()) + output_parameter_desc = "\n".join( + f"{x}: {cls.output_parameters[x]['description']}" for x in cls.output_parameters.keys()) + parameter_type_desc = ', '.join(f"{x}: {cls.input_parameters[x]['type']}" for x in cls.input_parameters.keys()) + desc = (f"{cls.__name__}({parameter_type_desc}) - {cls.description}\nInputs: - {parameter_desc}\nOutput - " + + f"{output_parameter_desc}\nExample - {cls.__name__} {cls.example}") + return desc + + def gen_few_shot(self, thought: str, param: str, idx: int) -> str: + p = self.format_tool_input_parameter(param) + output = self.call(p).output + try: + o = json.loads(output) + output = json.dumps(list(o[:1])) + except Exception as e: + logger.error(e) + + return (f"{THOUGHT}: {thought}\n" + f"{ACTION}: {self.__class__.__name__}\n" + f"{ACTION_INPUT}: {param}\n" + f"{OBSERVATION}{idx}: {output}\n\n") + + def format_tool_input_parameters(self, text) -> Union[dict, str]: + logger.debug(f"{self.__class__.__name__} parse param start") + try: + tool_input = json.loads(text, strict=False) + return tool_input + except Exception as e: + logger.error(f"{self.__class__.__name__} parse param failed {str(e)}") + return ( + f'Invalid "Action Input" parameter format".\nPlease strictly follow the tool usage example ' + f'format: \n{self.build_tool_description_for_prompt()}\n' + f'Requirement:\n' + f'1.Invalid JSON format should only contain key-value pairs; do not add comments or description text "\ + within the JSON.\n' + f'2.Please extract the values strictly based on the information provides in the query to ensure that"\ + the "Action Input" values are accurate and reliable, and do not fabricate them.\n' + f'3.All parameter key-value pairs should be integrated into a single JSON format; do not use multiple"\ + JSON objects.') + + def check_api_call_correctness(self, response, groundtruth) -> bool: + raise NotImplementedError + + def call(self, input_parameter: dict, **kwargs): + raise NotImplementedError + + def make_response(self, parameters, results, success=True, finished=False, exception=""): + api_name = self.__class__.__name__ + return APIResponse(api_name=api_name, + input=parameters, + output=results, + success=success, + finished=finished, + exception=exception) + + def make_failed_tip(self, data, key): + return f"{self.__class__.__name__} failed, available {key}: {', '.join(data[key].unique())}" diff --git a/mxAgent/agent_sdk/toolmngt/tool_manager.py b/mxAgent/agent_sdk/toolmngt/tool_manager.py new file mode 100644 index 000000000..d38d4be7b --- /dev/null +++ b/mxAgent/agent_sdk/toolmngt/tool_manager.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Huawei Technologies Co., Ltd. 2024. All rights reserved. + +import importlib.util +import json +import os + +from loguru import logger + +from .api import API + + +class ToolManager: + apis = {} + + def __init__(self) -> None: + pass + + @staticmethod + def remove_extra_parameters(paras, input_parameters) -> dict: + processed_parameters = {} + for input_key in paras: + if input_key not in input_parameters: + continue + processed_parameters[input_key] = paras[input_key] + return processed_parameters + + @staticmethod + def init_apis(apis_dir): + all_apis = [] + except_files = ['__init__.py', 'api.py'] + for file in os.listdir(apis_dir): + if file.endswith('.py') and file not in except_files: + api_file = file.split('.')[0] + basename = os.path.basename(apis_dir) + module = importlib.import_module(f'{basename}.{api_file}') + classes = [getattr(module, x) for x in dir(module) if isinstance(getattr(module, x), type)] + for cls in classes: + if issubclass(cls, API) and cls is not API: + all_apis.append(cls) + return all_apis + + @classmethod + def register_tool(cls): + def wrapper(apicls): + if issubclass(apicls, API) and apicls is not API: + name = apicls.__name__ + cls_info = { + 'name': name, + 'class': apicls, + 'description': apicls.description, + 'input_parameters': apicls.input_parameters, + 'output_parameters': apicls.output_parameters, + } + cls.apis[name] = cls_info + return apicls + + return wrapper + + def get_api_by_name(self, name: str): + for _name, api in self.apis.items(): + if _name == name: + return api + logger.error(f"failed to get_api_by_name={name}") + return None + + def get_api_description(self, name: str): + api_info = self.get_api_by_name(name).copy() + api_info.pop('class') + if 'init_database' in api_info: + api_info.pop('init_database') + return json.dumps(api_info) + + def init_tool(self, tool_name: str, *args, **kwargs): + api_class = self.get_api_by_name(tool_name)['class'] + + tool = api_class(*args, **kwargs) + + return tool + + def executor_call(self, tool_name: str, paras: dict, llm): + tool = self.init_tool(tool_name) + parameter = paras if paras else {} + input_parameters = self.get_api_by_name(tool_name)['input_parameters'] + processed_parameters = self.remove_extra_parameters( + parameter, input_parameters) + response = tool.call(processed_parameters, llm=llm) + return response.output + # recipe agent需要使用response + # 这个地方需要统一为一个 call, 使用output + + def api_call(self, tool_name: str, text: str, **kwargs): + tool = self.init_tool(tool_name) + tool_param = tool.format_tool_input_parameters(text) + + if isinstance(tool_param, str): + return tool.make_response(None, tool_param, False) + input_parameters = self.get_api_by_name(tool_name)['input_parameters'] + processed_parameters = self.remove_extra_parameters( + tool_param, input_parameters) + + try: + response = tool.call(processed_parameters, **kwargs) + except Exception as e: + msg = f'failed to invoke {tool_name}' + logger.error(f'{msg}, error={e}') + return tool.make_response(processed_parameters, msg, True) + + return response + + def list_all_apis(self): + return [_name for _name, api in self.apis.items()] diff --git a/mxAgent/requirements.txt b/mxAgent/requirements.txt new file mode 100644 index 000000000..c96356599 --- /dev/null +++ b/mxAgent/requirements.txt @@ -0,0 +1,19 @@ +requests=2.27.1 +tqdm +selenium=4.9.0 +bs4 +transformers +openai +pandas +datasets +peft +fschat +langchain +vllm +rouge +langchain_openai +colorlog +rouge-score +langchain-community +loguru +tiktoken \ No newline at end of file -- Gitee From 1fc50a1ba249418c809f1ceaba3f88b7a3b2e2c5 Mon Sep 17 00:00:00 2001 From: huxinjia Date: Tue, 26 Nov 2024 16:57:28 +0800 Subject: [PATCH 2/3] cleancode --- mxAgent/agent_sdk/toolmngt/api.py | 7 ++----- mxAgent/agent_sdk/toolmngt/tool_manager.py | 3 --- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/mxAgent/agent_sdk/toolmngt/api.py b/mxAgent/agent_sdk/toolmngt/api.py index eaaec2e40..c270ee7c1 100644 --- a/mxAgent/agent_sdk/toolmngt/api.py +++ b/mxAgent/agent_sdk/toolmngt/api.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -# Copyright (c) Huawei Technologies Co., Ltd. 2024. All rights reserved. - import json from abc import ABC from dataclasses import dataclass, field @@ -50,8 +47,8 @@ class API(ABC): p = self.format_tool_input_parameter(param) output = self.call(p).output try: - o = json.loads(output) - output = json.dumps(list(o[:1])) + output_json = json.loads(output) + output = json.dumps(list(output_json[:1])) except Exception as e: logger.error(e) diff --git a/mxAgent/agent_sdk/toolmngt/tool_manager.py b/mxAgent/agent_sdk/toolmngt/tool_manager.py index d38d4be7b..2d1f5ecbc 100644 --- a/mxAgent/agent_sdk/toolmngt/tool_manager.py +++ b/mxAgent/agent_sdk/toolmngt/tool_manager.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -# Copyright (c) Huawei Technologies Co., Ltd. 2024. All rights reserved. - import importlib.util import json import os -- Gitee From 7f505e44617ca900a978f79e545e177fc6f86931 Mon Sep 17 00:00:00 2001 From: huxinjia Date: Tue, 26 Nov 2024 17:17:29 +0800 Subject: [PATCH 3/3] Update pre_prompt.py --- mxAgent/agent_sdk/prompts/pre_prompt.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/mxAgent/agent_sdk/prompts/pre_prompt.py b/mxAgent/agent_sdk/prompts/pre_prompt.py index 300d27abb..140ad9ac5 100644 --- a/mxAgent/agent_sdk/prompts/pre_prompt.py +++ b/mxAgent/agent_sdk/prompts/pre_prompt.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -# Copyright (c) Huawei Technologies Co., Ltd. 2024. All rights reserved. - from datetime import date from langchain.prompts import PromptTemplate -- Gitee