diff --git a/.gitignore b/.gitignore index f8d30f31b..90af844df 100644 --- a/.gitignore +++ b/.gitignore @@ -30,7 +30,6 @@ MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest *.spec # Installer logs diff --git a/cookbooks/end2end_application/agent/tool_call.ipynb b/cookbooks/end2end_application/agent/tool_call.ipynb index 0946ed830..62d025289 100644 --- a/cookbooks/end2end_application/agent/tool_call.ipynb +++ b/cookbooks/end2end_application/agent/tool_call.ipynb @@ -269,11 +269,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "vscode": { - "languageId": "plaintext" - } - }, + "metadata": {}, "outputs": [], "source": [ "import os\n", @@ -299,13 +295,17 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "vscode": { - "languageId": "plaintext" + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Agent第一次回答: 很抱歉,由于个人隐私保护的原则,我无法直接查询并告知您本公司张三同学的生日。如果您需要了解这个信息,建议您通过合法且正当的途径,比如直接询问张三同学本人,或者查阅公司内部的员工档案,但前提是您需要确保有合适的权限和授权。尊重和保护个人隐私是我们每个人的责任。\n" + ] } - }, - "outputs": [], + ], "source": [ "message_1 = app_client.run(\n", " conversation_id=conversation_id,\n", @@ -348,17 +348,17 @@ "\n", "##### 赋予应用一个本地查询组件能力\n", "\n", + "以下示例展示了三种方式来使用 ToolCall 进行调用,并演示了如何在 AppBuilder 环境中配置和执行会话调用。\n", + "\n", + "**方式1:使用 JSONSchema 格式直接描述 tools 调用**\n", + "\n", "- 这里我们使用info_dict模拟一个数据库查询的返回结果" ] }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "vscode": { - "languageId": "plaintext" - } - }, + "execution_count": 25, + "metadata": {}, "outputs": [], "source": [ "def get_person_infomation(name: str):\n", @@ -377,20 +377,20 @@ "tools = [\n", " {\n", " \"type\": \"function\",\n", - "\"function\": {\n", - " \"name\": \"get_person_infomation\",\n", - " \"description\": \"查找公司内指定人员的信息\",\n", - " \"parameters\": {\n", - " \"type\": \"object\",\n", - " \"properties\": {\n", - " \"name\": {\n", - " \"type\": \"string\",\n", - " \"description\": \"人员名称,例如:张三、李四\",\n", + " \"function\": {\n", + " \"name\": \"get_person_infomation\",\n", + " \"description\": \"查找公司内指定人员的信息\",\n", + " \"parameters\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"name\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"人员名称,例如:张三、李四\",\n", + " },\n", + " },\n", + " \"required\": [\"name\"],\n", " },\n", " },\n", - " \"required\": [\"name\"],\n", - " },\n", - "},\n", " }\n", "]" ] @@ -404,13 +404,59 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "vscode": { - "languageId": "plaintext" + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Agent的中间思考过程:\n", + "{\n", + " \"code\": 0,\n", + " \"message\": \"\",\n", + " \"status\": \"interrupt\",\n", + " \"event_type\": \"Interrupt\",\n", + " \"content_type\": \"contexts\",\n", + " \"detail\": {\n", + " \"text\": {\n", + " \"function_call\": {\n", + " \"thought\": \"用户想要查询公司内指定人员张三的生日信息,这是一个具有明确目的和关键信息的需求。根据我们可用的工具,get_person_infomation 工具能够查找公司内指定人员的信息,包括生日等。因此,通过调用这个工具并传入张三作为参数,我们可以获取到张三的生日信息,从而满足用户的需求。\",\n", + " \"name\": \"get_person_infomation\",\n", + " \"arguments\": {\n", + " \"name\": \"张三\"\n", + " },\n", + " \"usage\": {\n", + " \"prompt_tokens\": 564,\n", + " \"completion_tokens\": 115,\n", + " \"total_tokens\": 679,\n", + " \"name\": \"ERNIE-4.0-8K\",\n", + " \"type\": \"plan\"\n", + " },\n", + " \"tool_call_id\": \"baf86c61-6627-4229-bc81-a17eda1bce36\"\n", + " },\n", + " \"used_tool\": []\n", + " }\n", + " },\n", + " \"usage\": null,\n", + " \"tool_calls\": [\n", + " {\n", + " \"id\": \"baf86c61-6627-4229-bc81-a17eda1bce36\",\n", + " \"type\": \"function\",\n", + " \"function\": {\n", + " \"name\": \"get_person_infomation\",\n", + " \"arguments\": {\n", + " \"name\": \"张三\"\n", + " }\n", + " }\n", + " }\n", + " ]\n", + "}\n", + "Agent思考结束,等待我们上传本地结果\n", + "\n" + ] } - }, - "outputs": [], + ], "source": [ "message_2 = app_client.run(\n", " conversation_id=conversation_id,\n", @@ -465,13 +511,18 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "vscode": { - "languageId": "plaintext" + "execution_count": 27, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "local_func_result: 您要查找的张三的生日是:1980年1月1日\n", + "\n" + ] } - }, - "outputs": [], + ], "source": [ "tool_call = message_2.content.events[-1].tool_calls[-1]\n", "tool_call_id = tool_call.id\n", @@ -494,13 +545,17 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "vscode": { - "languageId": "plaintext" + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Agent 拥有了本地函数调用能力后,回答是: 您要找的张三的生日是1980年1月1日。\n" + ] } - }, - "outputs": [], + ], "source": [ "message_3 = app_client.run(\n", " conversation_id=conversation_id,\n", @@ -582,6 +637,344 @@ "```" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**方式2:使用 function_to_model 将函数对象传递为 ToolCall 的调用**\n", + "\n", + "- 前置步骤:设置环境变量和初始化操作" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import appbuilder\n", + "import os\n", + "import json\n", + "\n", + "# 请前往千帆AppBuilder官网创建密钥,流程详见:https://cloud.baidu.com/doc/AppBuilder/s/Olq6grrt6#1%E3%80%81%E5%88%9B%E5%BB%BA%E5%AF%86%E9%92%A5\n", + "# 设置环境变量\n", + "# AppBuilder Token,此处为试用Token,速度Quota有限制,正式使用替换为您个人的Token\n", + "os.environ[\"APPBUILDER_TOKEN\"] = \"bce-v3/ALTAK-n5AYUIUJMarF7F7iFXVeK/1bf65eed7c8c7efef9b11388524fa1087f90ea58\"\n", + "\n", + "# 应用为:智能问题解决者\n", + "app_id = \"b9473e78-754b-463a-916b-f0a9097a8e5f\"\n", + "# 初始化智能体\n", + "client = appbuilder.AppBuilderClient(app_id)\n", + "# 创建会话\n", + "conversation_id = client.create_conversation()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "- 定义函数和函数列表,按照谷歌规范写好注释" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "#定义示例函数\n", + "def get_current_weather(location: str, unit: str) -> str:\n", + " \"\"\"获取指定中国城市的当前天气信息。\n", + "\n", + " 仅支持中国城市的天气查询。参数 `location` 为中国城市名称,其他国家城市不支持天气查询。\n", + "\n", + " Args:\n", + " location (str): 城市名,例如:\"北京\"。\n", + " unit (int): 温度单位,支持 \"celsius\" 或 \"fahrenheit\"。\n", + "\n", + " Returns:\n", + " str: 天气情况描述\n", + " \"\"\"\n", + " return \"北京今天25度\"\n", + " \n", + "#定义函数列表\n", + "functions = [get_current_weather]\n", + "function_map = {f.__name__: f for f in functions}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "- 查看一下function_to_model函数转化的结果" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " \"type\": \"function\",\n", + " \"function\": {\n", + " \"name\": \"get_current_weather\",\n", + " \"description\": \"获取指定中国城市的当前天气信息。\\n\\n 仅支持中国城市的天气查询。参数 `location` 为中国城市名称,其他国家城市不支持天气查询。\\n\\n Args:\\n location (str): 城市名,例如:\\\"北京\\\"。\\n unit (int): 温度单位,支持 \\\"celsius\\\" 或 \\\"fahrenheit\\\"。\\n\\n Returns:\\n str: 天气情况描述\",\n", + " \"parameters\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"location\": {\n", + " \"name\": \"location\",\n", + " \"type\": \"str\",\n", + " \"description\": null,\n", + " \"required\": true\n", + " },\n", + " \"unit\": {\n", + " \"name\": \"unit\",\n", + " \"type\": \"str\",\n", + " \"description\": null,\n", + " \"required\": true\n", + " }\n", + " },\n", + " \"required\": [\n", + " \"location\",\n", + " \"unit\"\n", + " ]\n", + " },\n", + " \"returns\": {\n", + " \"type\": \"str\",\n", + " \"description\": null\n", + " }\n", + " }\n", + "}\n" + ] + } + ], + "source": [ + "print(json.dumps(appbuilder.Manifest.from_function(get_current_weather).model_dump(), indent=4, ensure_ascii=False))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "- 调用大模型进行函数调用" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "ename": "BadRequestException", + "evalue": "request_id=928f30c6-d712-448b-a831-dc4cd15307b0 , http status code is 400, body is {\"code\": \"QuotaLimitExceeded\", \"message\": \"quota\\u8d44\\u6e90\\u5df2\\u8fbe\\u4e0a\\u9650\", \"request_id\": \"928f30c6-d712-448b-a831-dc4cd15307b0\"}", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mBadRequestException\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[5], line 2\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;66;03m#调用大模型\u001b[39;00m\n\u001b[0;32m----> 2\u001b[0m msg \u001b[38;5;241m=\u001b[39m \u001b[43mclient\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrun\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 3\u001b[0m \u001b[43m \u001b[49m\u001b[43mconversation_id\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mconversation_id\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 4\u001b[0m \u001b[43m \u001b[49m\u001b[43mquery\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43m今天北京的天气怎么样?\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 5\u001b[0m \u001b[43m \u001b[49m\u001b[43mtools\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m \u001b[49m\u001b[43m[\u001b[49m\u001b[43mappbuilder\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mManifest\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfrom_function\u001b[49m\u001b[43m(\u001b[49m\u001b[43mf\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mmodel_dump\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mf\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mfunctions\u001b[49m\u001b[43m]\u001b[49m\n\u001b[1;32m 6\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 7\u001b[0m \u001b[38;5;28mprint\u001b[39m(msg\u001b[38;5;241m.\u001b[39mmodel_dump_json(indent\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m4\u001b[39m))\n\u001b[1;32m 8\u001b[0m \u001b[38;5;66;03m# 获取最后的事件和工具调用信息\u001b[39;00m\n", + "File \u001b[0;32m/opt/anaconda3/envs/testenv/lib/python3.9/site-packages/appbuilder/core/console/appbuilder_client/appbuilder_client.py:306\u001b[0m, in \u001b[0;36mAppBuilderClient.run\u001b[0;34m(self, conversation_id, query, file_ids, stream, tools, tool_outputs, tool_choice, end_user_id, action, **kwargs)\u001b[0m\n\u001b[1;32m 302\u001b[0m url \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mhttp_client\u001b[38;5;241m.\u001b[39mservice_url_v2(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m/app/conversation/runs\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 303\u001b[0m response \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mhttp_client\u001b[38;5;241m.\u001b[39msession\u001b[38;5;241m.\u001b[39mpost(\n\u001b[1;32m 304\u001b[0m url, headers\u001b[38;5;241m=\u001b[39mheaders, json\u001b[38;5;241m=\u001b[39mreq\u001b[38;5;241m.\u001b[39mmodel_dump(), timeout\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m, stream\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mTrue\u001b[39;00m\n\u001b[1;32m 305\u001b[0m )\n\u001b[0;32m--> 306\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mhttp_client\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcheck_response_header\u001b[49m\u001b[43m(\u001b[49m\u001b[43mresponse\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 307\u001b[0m request_id \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mhttp_client\u001b[38;5;241m.\u001b[39mresponse_request_id(response)\n\u001b[1;32m 308\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m stream:\n", + "File \u001b[0;32m/opt/anaconda3/envs/testenv/lib/python3.9/site-packages/appbuilder/core/_client.py:120\u001b[0m, in \u001b[0;36mHTTPClient.check_response_header\u001b[0;34m(response)\u001b[0m\n\u001b[1;32m 116\u001b[0m message \u001b[38;5;241m=\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mrequest_id=\u001b[39m\u001b[38;5;132;01m{}\u001b[39;00m\u001b[38;5;124m , http status code is \u001b[39m\u001b[38;5;132;01m{}\u001b[39;00m\u001b[38;5;124m, body is \u001b[39m\u001b[38;5;132;01m{}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;241m.\u001b[39mformat(\n\u001b[1;32m 117\u001b[0m \u001b[38;5;18m__class__\u001b[39m\u001b[38;5;241m.\u001b[39mresponse_request_id(response), status_code, response\u001b[38;5;241m.\u001b[39mtext\n\u001b[1;32m 118\u001b[0m )\n\u001b[1;32m 119\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m status_code \u001b[38;5;241m==\u001b[39m requests\u001b[38;5;241m.\u001b[39mcodes\u001b[38;5;241m.\u001b[39mbad_request:\n\u001b[0;32m--> 120\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m BadRequestException(message)\n\u001b[1;32m 121\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m status_code \u001b[38;5;241m==\u001b[39m requests\u001b[38;5;241m.\u001b[39mcodes\u001b[38;5;241m.\u001b[39mforbidden:\n\u001b[1;32m 122\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m ForbiddenException(message)\n", + "\u001b[0;31mBadRequestException\u001b[0m: request_id=928f30c6-d712-448b-a831-dc4cd15307b0 , http status code is 400, body is {\"code\": \"QuotaLimitExceeded\", \"message\": \"quota\\u8d44\\u6e90\\u5df2\\u8fbe\\u4e0a\\u9650\", \"request_id\": \"928f30c6-d712-448b-a831-dc4cd15307b0\"}" + ] + } + ], + "source": [ + "#调用大模型\n", + "msg = client.run(\n", + " conversation_id=conversation_id,\n", + " query=\"今天北京的天气怎么样?\",\n", + " tools = [appbuilder.Manifest.from_function(f).model_dump() for f in functions]\n", + " )\n", + "print(msg.model_dump_json(indent=4))\n", + "# 获取最后的事件和工具调用信息\n", + "event = msg.content.events[-1]\n", + "tool_call = event.tool_calls[-1]\n", + "\n", + "# 获取函数名称和参数\n", + "name = tool_call.function.name\n", + "args = tool_call.function.arguments\n", + "\n", + "# 将函数名称映射到具体的函数并执行\n", + "raw_result = function_map[name](**args)\n", + "\n", + "# 传递工具的输出\n", + "msg_2 = client.run(\n", + " conversation_id=conversation_id,\n", + " tool_outputs=[{\n", + " \"tool_call_id\": tool_call.id,\n", + " \"output\": str(raw_result)\n", + " }],\n", + ")\n", + "print(msg_2.model_dump_json(indent=4))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**方式3: 使用装饰器进行描述**\n", + "\n", + "- 前置步骤:设置环境变量和初始化操作" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import json\n", + "import appbuilder\n", + "from appbuilder import manifest, manifest_parameter\n", + "\n", + "# 请前往千帆AppBuilder官网创建密钥,流程详见:https://cloud.baidu.com/doc/AppBuilder/s/Olq6grrt6#1%E3%80%81%E5%88%9B%E5%BB%BA%E5%AF%86%E9%92%A5\n", + "# 设置环境变量\n", + "# AppBuilder Token,此处为试用Token,速度Quota有限制,正式使用替换为您个人的Token\n", + "#os.environ[\"APPBUILDER_TOKEN\"] = \"bce-v3/ALTAK-n5AYUIUJMarF7F7iFXVeK/1bf65eed7c8c7efef9b11388524fa1087f90ea58\"\n", + "os.environ[\"APPBUILDER_TOKEN\"] = \"bce-v3/ALTAK-DKaql4wY9ojwp2uMe8IEj/7ae1190aff0684153de365381d9b06beab3064c5\"\n", + "\n", + "# 应用为:智能问题解决者\n", + "#app_id = \"b9473e78-754b-463a-916b-f0a9097a8e5f\"\n", + "app_id = \"7cc4c21f-0e25-4a76-baf7-01a2b923a1a7\"\n", + "# 初始化智能体\n", + "client = appbuilder.AppBuilderClient(app_id)\n", + "# 创建会话\n", + "conversation_id = client.create_conversation()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "- 定义函数和函数列表,并用装饰器对函数进行进行描述." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "#使用function装饰描述函数,function_parameter装饰器描述参数,function_return装饰器描述函数返回值。\n", + "@manifest(description=\"获取指定中国城市的当前天气信息。仅支持中国城市的天气查询。参数 `location` 为中国城市名称,其他国家城市不支持天气查询。\")\n", + "@manifest_parameter(name=\"location\", description=\"城市名,例如:北京。\")\n", + "@manifest_parameter(name=\"unit\", description=\"温度单位,支持 'celsius' 或 'fahrenheit'\")\n", + "#定义示例函数\n", + "def get_current_weather(location: str, unit: str) -> str:\n", + " return \"北京今天25度\"\n", + "\n", + "#定义函数列表\n", + "functions = [get_current_weather]\n", + "function_map = {f.__name__: f for f in functions}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "- 查看一下装饰器的转化内容" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " \"type\": \"function\",\n", + " \"function\": {\n", + " \"name\": \"get_current_weather\",\n", + " \"description\": \"获取指定中国城市的当前天气信息。仅支持中国城市的天气查询。参数 `location` 为中国城市名称,其他国家城市不支持天气查询。\",\n", + " \"parameters\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"location\": {\n", + " \"name\": \"location\",\n", + " \"type\": \"str\",\n", + " \"description\": \"城市名,例如:北京。\",\n", + " \"required\": true\n", + " },\n", + " \"unit\": {\n", + " \"name\": \"unit\",\n", + " \"type\": \"str\",\n", + " \"description\": \"温度单位,支持 'celsius' 或 'fahrenheit'\",\n", + " \"required\": true\n", + " }\n", + " },\n", + " \"required\": [\n", + " \"location\",\n", + " \"unit\"\n", + " ]\n", + " }\n", + " }\n", + "}\n" + ] + } + ], + "source": [ + "# 将 model_dump() 的输出进行格式化打印\n", + "print(json.dumps(get_current_weather.__ab_manifest__.model_dump(), indent=4, ensure_ascii=False))" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "ename": "BadRequestException", + "evalue": "request_id=76224253-163f-46d3-a5eb-b22c6fcec2be , http status code is 400, body is {\"code\": \"QuotaLimitExceeded\", \"message\": \"quota\\u8d44\\u6e90\\u5df2\\u8fbe\\u4e0a\\u9650\", \"request_id\": \"76224253-163f-46d3-a5eb-b22c6fcec2be\"}", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mBadRequestException\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[10], line 2\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;66;03m#调用大模型\u001b[39;00m\n\u001b[0;32m----> 2\u001b[0m msg \u001b[38;5;241m=\u001b[39m \u001b[43mclient\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrun\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 3\u001b[0m \u001b[43m \u001b[49m\u001b[43mconversation_id\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mconversation_id\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 4\u001b[0m \u001b[43m \u001b[49m\u001b[43mquery\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43m今天北京的天气怎么样?\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 5\u001b[0m \u001b[43m \u001b[49m\u001b[43mtools\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m \u001b[49m\u001b[43m[\u001b[49m\u001b[43mget_current_weather\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m__ab_manifest__\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mmodel_dump\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m]\u001b[49m\n\u001b[1;32m 6\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 7\u001b[0m \u001b[38;5;28mprint\u001b[39m(msg\u001b[38;5;241m.\u001b[39mmodel_dump_json(indent\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m4\u001b[39m))\n\u001b[1;32m 8\u001b[0m \u001b[38;5;66;03m# 获取最后的事件和工具调用信息\u001b[39;00m\n", + "File \u001b[0;32m/opt/anaconda3/envs/testenv/lib/python3.9/site-packages/appbuilder/core/console/appbuilder_client/appbuilder_client.py:306\u001b[0m, in \u001b[0;36mAppBuilderClient.run\u001b[0;34m(self, conversation_id, query, file_ids, stream, tools, tool_outputs, tool_choice, end_user_id, action, **kwargs)\u001b[0m\n\u001b[1;32m 302\u001b[0m url \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mhttp_client\u001b[38;5;241m.\u001b[39mservice_url_v2(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m/app/conversation/runs\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 303\u001b[0m response \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mhttp_client\u001b[38;5;241m.\u001b[39msession\u001b[38;5;241m.\u001b[39mpost(\n\u001b[1;32m 304\u001b[0m url, headers\u001b[38;5;241m=\u001b[39mheaders, json\u001b[38;5;241m=\u001b[39mreq\u001b[38;5;241m.\u001b[39mmodel_dump(), timeout\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m, stream\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mTrue\u001b[39;00m\n\u001b[1;32m 305\u001b[0m )\n\u001b[0;32m--> 306\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mhttp_client\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcheck_response_header\u001b[49m\u001b[43m(\u001b[49m\u001b[43mresponse\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 307\u001b[0m request_id \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mhttp_client\u001b[38;5;241m.\u001b[39mresponse_request_id(response)\n\u001b[1;32m 308\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m stream:\n", + "File \u001b[0;32m/opt/anaconda3/envs/testenv/lib/python3.9/site-packages/appbuilder/core/_client.py:120\u001b[0m, in \u001b[0;36mHTTPClient.check_response_header\u001b[0;34m(response)\u001b[0m\n\u001b[1;32m 116\u001b[0m message \u001b[38;5;241m=\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mrequest_id=\u001b[39m\u001b[38;5;132;01m{}\u001b[39;00m\u001b[38;5;124m , http status code is \u001b[39m\u001b[38;5;132;01m{}\u001b[39;00m\u001b[38;5;124m, body is \u001b[39m\u001b[38;5;132;01m{}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;241m.\u001b[39mformat(\n\u001b[1;32m 117\u001b[0m \u001b[38;5;18m__class__\u001b[39m\u001b[38;5;241m.\u001b[39mresponse_request_id(response), status_code, response\u001b[38;5;241m.\u001b[39mtext\n\u001b[1;32m 118\u001b[0m )\n\u001b[1;32m 119\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m status_code \u001b[38;5;241m==\u001b[39m requests\u001b[38;5;241m.\u001b[39mcodes\u001b[38;5;241m.\u001b[39mbad_request:\n\u001b[0;32m--> 120\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m BadRequestException(message)\n\u001b[1;32m 121\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m status_code \u001b[38;5;241m==\u001b[39m requests\u001b[38;5;241m.\u001b[39mcodes\u001b[38;5;241m.\u001b[39mforbidden:\n\u001b[1;32m 122\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m ForbiddenException(message)\n", + "\u001b[0;31mBadRequestException\u001b[0m: request_id=76224253-163f-46d3-a5eb-b22c6fcec2be , http status code is 400, body is {\"code\": \"QuotaLimitExceeded\", \"message\": \"quota\\u8d44\\u6e90\\u5df2\\u8fbe\\u4e0a\\u9650\", \"request_id\": \"76224253-163f-46d3-a5eb-b22c6fcec2be\"}" + ] + } + ], + "source": [ + "#调用大模型\n", + "msg = client.run(\n", + " conversation_id=conversation_id,\n", + " query=\"今天北京的天气怎么样?\",\n", + " tools = [get_current_weather.__ab_manifest__.model_dump()]\n", + " )\n", + "print(msg.model_dump_json(indent=4))\n", + "# 获取最后的事件和工具调用信息\n", + "event = msg.content.events[-1]\n", + "tool_call = event.tool_calls[-1]\n", + "\n", + "# 获取函数名称和参数\n", + "name = tool_call.function.name\n", + "args = tool_call.function.arguments\n", + "\n", + "# 将函数名称映射到具体的函数并执行\n", + "raw_result = function_map[name](**args)\n", + "\n", + "# 传递工具的输出\n", + "msg_2 = client.run(\n", + " conversation_id=conversation_id,\n", + " tool_outputs=[{\n", + " \"tool_call_id\": tool_call.id,\n", + " \"output\": str(raw_result)\n", + " }],\n", + ")\n", + "print(msg_2.model_dump_json(indent=4))" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -773,8 +1166,22 @@ } ], "metadata": { + "kernelspec": { + "display_name": "testenv", + "language": "python", + "name": "python3" + }, "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.20" } }, "nbformat": 4, diff --git a/docs/BasisModule/Platform/Application/appbuilder_client.md b/docs/BasisModule/Platform/Application/appbuilder_client.md index 672baf1c9..372f6c3d2 100644 --- a/docs/BasisModule/Platform/Application/appbuilder_client.md +++ b/docs/BasisModule/Platform/Application/appbuilder_client.md @@ -229,8 +229,14 @@ for content in message.content: print(answer) ``` + + #### Run方法带ToolCall调用示例 +以下示例展示了三种方式来使用 ToolCall 进行调用,并演示了如何在 AppBuilder 环境中配置和执行会话调用。 + +**方式1:使用 JSONSchema 格式直接描述 tools 调用** + ```python import appbuilder from appbuilder.core.console.appbuilder_client import data_class @@ -279,6 +285,129 @@ msg_2 = client.run( print(msg_2.model_dump_json(indent=4)) ``` +**方式2: 使用 function_to_model 将函数对象传递为 ToolCall 的调用** + +```python +import appbuilder +import os + +# 请前往千帆AppBuilder官网创建密钥,流程详见:https://cloud.baidu.com/doc/AppBuilder/s/Olq6grrt6#1%E3%80%81%E5%88%9B%E5%BB%BA%E5%AF%86%E9%92%A5 +# 设置环境变量 +os.environ["APPBUILDER_TOKEN"] = "..." +app_id = "..." # 已发布AppBuilder应用的ID +# 初始化智能体 +client = appbuilder.AppBuilderClient(app_id) +# 创建会话 +conversation_id = client.create_conversation() +#注意:要使用此方法要为函数写好注释。最好按照谷歌规范来写 + +#定义示例函数 +def get_current_weather(location: str, unit: str) -> str: + """获取指定中国城市的当前天气信息。 + + 仅支持中国城市的天气查询。参数 `location` 为中国城市名称,其他国家城市不支持天气查询。 + + Args: + location (str): 城市名,例如:"北京"。 + unit (int): 温度单位,支持 "celsius" 或 "fahrenheit"。 + + Returns: + str: 天气情况描述 + """ + return "北京今天25度" + +#定义函数列表 +functions = [get_current_weather] +function_map = {f.__name__: f for f in functions} +#调用大模型 +msg = client.run( + conversation_id=conversation_id, + query="今天北京的天气怎么样?", + tools = [appbuilder.Manifest.from_function(f).model_dump() for f in functions] + ) +print(msg.model_dump_json(indent=4)) +# 获取最后的事件和工具调用信息 +event = msg.content.events[-1] +tool_call = event.tool_calls[-1] + +# 获取函数名称和参数 +name = tool_call.function.name +args = tool_call.function.arguments + +# 将函数名称映射到具体的函数并执行 +raw_result = function_map[name](**args) + +# 传递工具的输出 +msg_2 = client.run( + conversation_id=conversation_id, + tool_outputs=[{ + "tool_call_id": tool_call.id, + "output": str(raw_result) + }], +) +print(msg_2.model_dump_json(indent=4)) +``` + +**方式3: 使用装饰器进行描述** + +```python +import os +import json +import appbuilder +from appbuilder import manifest, manifest_parameter + +# 请前往千帆AppBuilder官网创建密钥,流程详见:https://cloud.baidu.com/doc/AppBuilder/s/Olq6grrt6#1%E3%80%81%E5%88%9B%E5%BB%BA%E5%AF%86%E9%92%A5 +# 设置环境变量 +os.environ["APPBUILDER_TOKEN"] = "" +app_id = "" # 已发布AppBuilder应用的ID +# 初始化智能体 +client = appbuilder.AppBuilderClient(app_id) +# 创建会话 +conversation_id = client.create_conversation() + +#使用manifest装饰描述函数,manifest_parameter装饰器描述参数,manifest_return装饰器描述函数返回值。 +@manifest(description="获取指定中国城市的当前天气信息。仅支持中国城市的天气查询。参数 `location` 为中国城市名称,其他国家城市不支持天气查询。") +@manifest_parameter(name="location", description="城市名,例如:北京。") +@manifest_parameter(name="unit", description="温度单位,支持 'celsius' 或 'fahrenheit'") +#定义示例函数 +def get_current_weather(location: str, unit: str) -> str: + return "北京今天25度" + +print(json.dumps((get_current_weather.__ab_manifest__).model_dump(), indent=4, ensure_ascii=False)) +#定义函数列表 +functions = [get_current_weather] +function_map = {f.__name__: f for f in functions} +#调用大模型 +msg = client.run( + conversation_id=conversation_id, + query="今天北京的天气怎么样?", + tools = [(get_current_weather.__ab_manifest__).model_dump()] + ) +print(msg.model_dump_json(indent=4)) +# 获取最后的事件和工具调用信息 +event = msg.content.events[-1] +tool_call = event.tool_calls[-1] + +# 获取函数名称和参数 +name = tool_call.function.name +args = tool_call.function.arguments + +# 将函数名称映射到具体的函数并执行 +raw_result = function_map[name](**args) + +# 传递工具的输出 +msg_2 = client.run( + conversation_id=conversation_id, + tool_outputs=[{ + "tool_call_id": tool_call.id, + "output": str(raw_result) + }], +) +print(msg_2.model_dump_json(indent=4)) +``` + + + #### Run方法带ToolChoice使用示例: * 注意:当前功能为试运行阶段,可能存在如下问题,如使用过程遇到其他问题,欢迎提issue或微信群讨论。 diff --git a/python/__init__.py b/python/__init__.py index f1678fac9..41e3e39c8 100644 --- a/python/__init__.py +++ b/python/__init__.py @@ -173,6 +173,8 @@ def get_default_header(): from appbuilder.core.user_session import UserSession from appbuilder.utils.logger_util import logger +from appbuilder.core.manifest.manifest_decorator import manifest, manifest_parameter +from appbuilder.core.manifest.models import Manifest from appbuilder.core.utils import get_model_list diff --git a/python/core/manifest/__init__.py b/python/core/manifest/__init__.py new file mode 100644 index 000000000..ba760dad3 --- /dev/null +++ b/python/core/manifest/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2024 Baidu, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/python/core/manifest/manifest_decorator.py b/python/core/manifest/manifest_decorator.py new file mode 100644 index 000000000..fc24223d9 --- /dev/null +++ b/python/core/manifest/manifest_decorator.py @@ -0,0 +1,270 @@ +# Copyright (c) 2023 Baidu, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import inspect +from typing import Any, Dict, Optional + +from pydantic.v1 import BaseModel as PydanticBaseModel # noqa: F403 # type: ignore +from pydantic.v1 import create_model + +from appbuilder.core.manifest.manifest_signature import get_signature +from appbuilder.core.manifest.models import Manifest,PropertyModel,ParametersModel + +# The following two functions are here to allow dynamically updating function description. +# Check tool/builtin/openapi_plugin.py for example. +def function_description(cls, func): + """Get the description of a function.""" + + if hasattr(func, "__ab_manifest__"): + view = func.__ab_manifest__ + return view.description if view else "" + + +def update_function_description(func, description): + """Update function description.""" + func.__dict__["__kernel_function_description__"] = description or "" + func.__dict__["__ab_manifest_description__"] = description or "" + + +def get_function_schema_with_inspect(method): + ( + args, + _, + varkw, + defaults, + kwonlyargs, + kwonlydefaults, + annotations, + ) = inspect.getfullargspec(method) + if len(args) > 0 and (args[0] == "self" or args[0] == "cls"): + args = args[1:] # remove self or cls + + if args or varkw: + if defaults is None: + defaults = () + non_default_args_count = len(args) - len(defaults) + defaults = (...,) * non_default_args_count + defaults + + keyword_only_params = {param: kwonlydefaults.get(param, Any) for param in kwonlyargs} + params = {param: (annotations.get(param, Any), default) for param, default in zip(args, defaults)} + return create_model( + "func", + **params, + **keyword_only_params, + __base__=PydanticBaseModel, + __config__=None, + __doc__="" + ).schema()["properties"] + else: # method has no arguments + return None + +def manifest( + *, + description: Optional[str] = None, + name: Optional[str] = None, +): + """ + Decorator for functions. Use some information to extract meta about function. + Priority as follows: + 1. User provided value via arguments + 2. Function signature + annotations + + Args: + description (str, optional): The functionality of the function, general user guideline. + name (str, optional): The name of the function. + """ + + def decorator(func): + """Decorator for function.""" + + # Get meta from signature + sig_params, sig_returns = get_signature(func=func) + + # Get meta from decorator + dec_params_list = func.__ab_manifest_parameters__ if hasattr(func, "__ab_manifest_parameters__") else [] + dec_params = {item.name: item for item in dec_params_list} + + properties = {} + required_fields = [] + for param in sig_params: + dec_param = dec_params.get(param["name"]) + param_type = param.get("type_") or getattr(dec_param, "type", None) + + param_info = { + "name": param["name"], # 参数名称 + "type": param_type, + "description": getattr(dec_params.get(param["name"]), "description", None), # 描述 + "required": param.get("required", getattr(dec_params.get(param["name"]), "required", True)), # 是否必需 + } + + # 验证类型字段是否有有效值 + if not param_info["type"]: + raise ValueError(f"参数 '{param['name']}' 缺少类型信息,请在函数签名中指定类型。") + + # 构造 PropertyModel + properties[param["name"]] = PropertyModel( + name=param_info["name"], + type=param_info["type"], + description=param_info["description"], + required=param_info["required"], + ) + + # 记录必需参数 + if param_info["required"]: + required_fields.append(param["name"]) + + # 确定函数的最终名称和描述 + final_name = name or func.__name__ + final_desc = description or func.__doc__ + + if not final_desc: + raise ValueError(f"函数 {final_name} 缺少描述") + + parameters_model = ParametersModel( + type="object", + properties={k: v.model_dump(exclude_none=False) for k, v in properties.items()}, + required=required_fields + ) + + view = Manifest( + type="function", + function={ + "name": final_name, + "description": final_desc, + "parameters": parameters_model.model_dump(), + }, + ) + + # Attach view to function. + func.__ab_manifest__ = view + + # Compatible to semantic kernel 0.9 + func.__kernel_function__ = True + func.__kernel_function_description__ = final_desc + func.__kernel_function_name__ = final_name + + return func + + return decorator + +def manifest_parameter( + *, + name: str, + description: str = None, + type: str = None, + required: bool = True, +): + """ + Decorator for function parameters. + + Args: + name -- The name of the parameter + description -- The description of the parameter + default_value -- The default value of the parameter + type -- The type of the parameter, used for function calling + required -- Whether the parameter is required + example -- The example of the parameter + + """ + + def decorator(func): + """Decorator for function parameter.""" + + new_view = PropertyModel( + name=name, + type=type, + description=description, + required=required, + ) + # Update parameter view lists for function_parameter decorator. + # This will be merged into ManifestView if function decorator runs last like: + # @function + # @function_parameter + # @function_parameter + if hasattr(func, "__ab_manifest_parameters__"): + current_views = func.__ab_manifest_parameters__ + else: + current_views = [] + current_views.append(new_view) + func.__ab_manifest_parameters__ = current_views + + # function_parameter runs after function, merge ParameterView in ManifestView + if hasattr(func, "__ab_manifest__"): + # 获取现有的 parameters + parameters_dict = func.__ab_manifest__.function.get("parameters", {}) + parameters_model = ParametersModel(**parameters_dict) + + # 更新 properties + existing_property = parameters_model.properties.get(new_view.name) + if existing_property: + merged_property = PropertyModel.merge(existing_property, new_view) + parameters_model.properties[new_view.name] = merged_property + else: + parameters_model.properties[new_view.name] = new_view + + # 更新 required 字段 + if new_view.required: + if new_view.name not in parameters_model.required: + parameters_model.required.append(new_view.name) + else: + if new_view.name in parameters_model.required: + parameters_model.required.remove(new_view.name) + + # 更新 func.__ab_manifest__.function["parameters"] + func.__ab_manifest__.function["parameters"] = parameters_model.model_dump() + + # Compatible to semantic kernel 0.9 + if hasattr(func, "__kernel_function_parameters__"): + item = { + "name": name, + "description": description, + "type": type, + "required": required, + } + + new_list = _update_list( + item, + func.__kernel_function_parameters__, + lambda item, new_item: item["name"] == new_item["name"], + lambda item, new_item: _merge_dict(item, new_item), + ) + func.__kernel_function_parameters__ = new_list + + return func + + return decorator + + +def _merge_dict(current_dict, new_dict): + result = current_dict.copy() + for k, v in new_dict.items(): + if v: + result[k] = v + return result + + +# Replace the parameter with the same name and keep order. +# Since there are few parameters for each function, keep use list for simplicity +def _update_list(new_item, list, condition, replacer): + new_list = [] + replaced = False + for item in list: + if condition(item, new_item): + replaced = True + merged_item = replacer(item, new_item) + new_list.append(merged_item) + # Missing parameter append to the end. + if not replaced: + new_list.append(new_item) + return new_list + diff --git a/python/core/manifest/manifest_signature.py b/python/core/manifest/manifest_signature.py new file mode 100644 index 000000000..249235e62 --- /dev/null +++ b/python/core/manifest/manifest_signature.py @@ -0,0 +1,144 @@ +# Copyright (c) 2024 Baidu, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import inspect +from inspect import Parameter, Signature +from typing import Any, Dict, Union, Optional, List, get_origin, get_args +import logging + +NoneType = type(None) + +# 映射内置泛型类型到 typing 模块的名称 +TYPE_MAPPING = { + dict: "Dict", + list: "List", + set: "Set", + tuple: "Tuple", + Union: "Union", + Optional: "Optional", + Any: "Any", + int: "int", + str: "str", + float: "float", + bool: "bool", + # 根据需要添加更多映射 +} + + +def get_signature(func): + """ + 获取函数的签名视图。 + + Args: + func (function): 目标函数。 + + Returns: + tuple: 包含两个元素的元组,第一个元素为函数参数的解析结果列表,第二个元素为函数返回值的解析结果字典。 + + """ + + signature = inspect.signature(func) + _parameters = [_parse_parameter(param) for param in signature.parameters.values() if param.name != "self"] + signature_returns = ( + _parse_annotation(signature.return_annotation) if signature.return_annotation != Signature.empty else {} + ) + return _parameters, signature_returns + + +def _parse_parameter(param: Parameter) -> Dict[str, Any]: + ret = {} + if param.annotation != Parameter.empty: + ret = _parse_annotation(param.annotation) + else: + ret["type_"] = None + ret["required"] = True # 默认为 True + + ret["name"] = param.name + + if param.default != Parameter.empty: + ret["default_value"] = param.default + ret["required"] = False + else: + ret["required"] = ret.get("required", True) + + return ret + + +def _parse_annotation(annotation: Any) -> Dict[str, Any]: + # The keys of this dict are compatible with semantic-kernel, do not change them + if annotation == Signature.empty: + return {"type_": None, "required": True} + if isinstance(annotation, str): + return {"type_": annotation, "required": True} + ret = _parse_internal_annotation(annotation, True) + if hasattr(annotation, "__metadata__") and annotation.__metadata__: + ret["description"] = annotation.__metadata__[0] + return ret + + +def _parse_internal_annotation(annotation: Any, required: bool) -> Dict[str, Any]: + if hasattr(annotation, "__forward_arg__"): + return {"type_": annotation.__forward_arg__, "required": required} + + # 获取 origin 和 args + origin = get_origin(annotation) + args = get_args(annotation) + + # 确定 parent_type + if origin is not None: + parent_type = TYPE_MAPPING.get(origin, origin.__name__ if hasattr(origin, '__name__') else str(origin)) + else: + parent_type = TYPE_MAPPING.get(annotation, annotation.__name__ if hasattr(annotation, '__name__') else str(annotation)) + + if parent_type == "Optional": + required = False + + if args: + results = [_parse_internal_annotation(arg, required) for arg in args] + type_objects = [ + result["type_object"] + for result in results + if "type_object" in result and result["type_object"] is not NoneType + ] + str_results = [result["type_"] for result in results] + + if "NoneType" in str_results: + str_results.remove("NoneType") + required = False + + if parent_type == "Union": + if len(str_results) == 1: + type_ = f"Optional[{str_results[0]}]" + else: + type_ = f"Union[{', '.join(str_results)}]" + else: + type_ = f"{parent_type}[{', '.join(str_results)}]" + else: + if parent_type == "Union": + # 所有选项都为非必需 + required = not (all(not result["required"] for result in results)) + type_ = f"{parent_type}[{', '.join(str_results)}]" + + ret = {"type_": type_, "required": required} + if type_objects and len(type_objects) == 1: + ret["type_object"] = type_objects[0] + logging.debug(f"Parsed annotation: {annotation}, type_: {type_}, required: {required}") + return ret + + type_ = TYPE_MAPPING.get(annotation, annotation.__name__ if hasattr(annotation, "__name__") else str(annotation)) + logging.debug(f"Parsed annotation: {annotation}, type_: {type_}, required: {required}") + return { + "type_": type_, + "type_object": annotation, + "required": required, + } \ No newline at end of file diff --git a/python/core/manifest/models.py b/python/core/manifest/models.py new file mode 100644 index 000000000..bbecca2ef --- /dev/null +++ b/python/core/manifest/models.py @@ -0,0 +1,129 @@ +# Copyright (c) 2023 Baidu, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from pydantic import BaseModel,Field +from typing import Dict, List, Literal, Any, Optional + +from appbuilder.core.manifest.validate import validate_function_param_name, validate_function_name +from appbuilder.core.manifest.manifest_signature import get_signature + +class PropertyModel(BaseModel): + """参数属性模型,用于描述函数参数的类型和元数据。 + + Attributes: + type (Optional[str]): 参数的类型。 + description (Optional[str]): 参数的描述信息。 + """ + name: str + type: Optional[str] + description: Optional[str] + required: bool = True + + @classmethod + def merge(cls, a: "PropertyModel", b: "PropertyModel") -> "PropertyModel": + """Merge two parameter views.""" + return cls( + name=a.name, + description=(b.description or a.description), + type=(b.type or a.type), + required=(b.required if b.required is not None else a.required), + ) + + +class ParametersModel(BaseModel): + """函数参数模型,用于定义函数的参数结构。 + + Attributes: + type (Literal["object"]): 表示参数集合的类型,固定为 "object"。 + properties (Dict[str, PropertyModel]): 参数的具体属性映射,其中键是参数名,值是对应的属性模型。 + required (List[str]): 必须提供的参数列表。 + """ + type: Literal["object"] + properties: Dict[str, PropertyModel] + required: List[str] + + +class Manifest(BaseModel): + """函数模型,用于描述函数的元信息。 + + Attributes: + type (Literal["function"]): 表示模型的类型,固定为 "function"。 + function (Dict[str, Any]): 函数的详细信息,包括名称、描述、参数、返回值等。 + """ + type: Literal["function"] + function: Dict[str, Any] + + @classmethod + def from_function(cls, func) -> "Manifest": + """ + 利用 manifest_signature.py 提供的 get_signature 方法解析函数的签名和参数信息, + 并生成一个 Manifest 实例。 + + Args: + func: 要转换的函数。 + + Returns: + Manifest: 包含函数元信息的模型。 + """ + if func.__doc__ is None: + raise ValueError(f"函数 {func.__name__} 缺少文档字符串") + + # 使用 manifest_signature 提取函数签名信息 + sig_params, sig_returns = get_signature(func) + + # 构造参数模型 + properties = {} + required = [] + + for param in sig_params: + param_info = { + "name": param["name"], # 参数名称 + "type": param.get("type_", None), # 类型 + "description": param.get("description", None), # 描述 + "required": param.get("required", False), # 是否必需 + } + + # 验证类型字段是否有有效值 + if not param_info["type"]: + raise ValueError(f"参数 '{param['name']}' 缺少类型信息,请在函数签名中指定类型。") + + # 构造 PropertyModel + properties[param["name"]] = PropertyModel( + name=param_info["name"], + type=param_info["type"], + description=param_info["description"], + required=param_info["required"], + ) + + # 记录必需参数 + if param_info["required"]: + required.append(param["name"]) + + # 构造 ParametersModel + parameters_model = ParametersModel( + type="object", + properties=properties, + required=required, + ) + + # 构造 Manifest 对象 + function_manifest = cls( + type="function", + function={ + "name": func.__name__, + "description": func.__doc__, # 去掉多余的空格 + "parameters": parameters_model.model_dump(), + }, + ) + + return function_manifest diff --git a/python/tests/test_appbuilder_client_toolcall.py b/python/tests/test_appbuilder_client_toolcall.py index af3a407eb..ea70dca02 100644 --- a/python/tests/test_appbuilder_client_toolcall.py +++ b/python/tests/test_appbuilder_client_toolcall.py @@ -1,11 +1,8 @@ import unittest import appbuilder -import requests -import tempfile import os - -@unittest.skipUnless(os.getenv("TEST_CASE", "UNKNOWN") == "CPU_SERIAL","") +#@unittest.skipUnless(os.getenv("TEST_CASE", "UNKNOWN") == "CPU_SERIAL","") class TestAgentRuntime(unittest.TestCase): def setUp(self): """ @@ -31,8 +28,8 @@ def test_appbuilder_client_tool_call(self): None: 如果app_id不为空,则不会引发任何异常 unittest.SkipTest (optional): 如果app_id为空,则跳过单测执行 """ - builder = appbuilder.AppBuilderClient(self.app_id) - conversation_id = builder.create_conversation() + client = appbuilder.AppBuilderClient(self.app_id) + conversation_id = client.create_conversation() tools = [ { "type": "function", @@ -54,7 +51,7 @@ def test_appbuilder_client_tool_call(self): } ] - msg = builder.run( + msg = client.run( conversation_id=conversation_id, query="今天北京天气怎么样?", tools=tools) @@ -64,7 +61,7 @@ def test_appbuilder_client_tool_call(self): assert event.status == "interrupt" assert event.event_type == "Interrupt" - msg_2 = builder.run( + msg_2 = client.run( conversation_id=conversation_id, tool_outputs=[ { @@ -76,7 +73,121 @@ def test_appbuilder_client_tool_call(self): print(msg_2.model_dump_json(indent=4)) - + """测试functions2model功能""" + #定义本地函数 + def get_current_weather(location: str, unit: str) -> str: + """获取指定中国城市的当前天气信息。 + + 仅支持中国城市的天气查询。参数 `location` 为中国城市名称,其他国家城市不支持天气查询。 + + Args: + location (str): 城市名,例如:"北京"。 + unit (int): 温度单位,支持 "celsius" 或 "fahrenheit"。 + + Returns: + str: 天气情况描述 + """ + return "北京今天25度" + #定义函数列表 + functions = [get_current_weather] + function_map = {f.__name__: f for f in functions} + #调用大模型 + msg = client.run( + conversation_id=conversation_id, + query="今天北京的天气怎么样?", + tools = [appbuilder.Manifest.from_function(f).model_dump() for f in functions] + ) + print(msg.model_dump_json(indent=4)) + # 获取最后的事件和工具调用信息 + event = msg.content.events[-1] + tool_call = event.tool_calls[-1] + + # 获取函数名称和参数 + name = tool_call.function.name + args = tool_call.function.arguments + + # 将函数名称映射到具体的函数并执行 + raw_result = function_map[name](**args) + + # 传递工具的输出 + msg_2 = client.run( + conversation_id=conversation_id, + tool_outputs=[{ + "tool_call_id": tool_call.id, + "output": str(raw_result) + }], + ) + print(msg_2.model_dump_json(indent=4)) + + """测试装饰器功能功能""" + @appbuilder.manifest(description="获取指定中国城市的当前天气信息。仅支持中国城市的天气查询。参数 `location` 为中国城市名称,其他国家城市不支持天气查询。") + @appbuilder.manifest_parameter(name="location", description="城市名,例如:北京。") + @appbuilder.manifest_parameter(name="unit", description="温度单位,支持 'celsius' 或 'fahrenheit'") + #定义示例函数 + def get_current_weather(location: str, unit: str) -> str: + return "北京今天25度" + + #定义函数列表 + functions = [get_current_weather] + function_map = {f.__name__: f for f in functions} + #调用大模型 + msg = client.run( + conversation_id=conversation_id, + query="今天北京的天气怎么样?", + tools = [get_current_weather.__ab_manifest__.model_dump()] + ) + print(msg.model_dump_json(indent=4)) + # 获取最后的事件和工具调用信息 + event = msg.content.events[-1] + tool_call = event.tool_calls[-1] + + # 获取函数名称和参数 + name = tool_call.function.name + args = tool_call.function.arguments + + # 将函数名称映射到具体的函数并执行 + raw_result = function_map[name](**args) + + # 传递工具的输出 + msg_2 = client.run( + conversation_id=conversation_id, + tool_outputs=[{ + "tool_call_id": tool_call.id, + "output": str(raw_result) + }], + ) + print(msg_2.model_dump_json(indent=4)) + + try: + @appbuilder.manifest() + @appbuilder.manifest_parameter(name="location", description="城市名,例如:北京。") + @appbuilder.manifest_parameter(name="unit", description="温度单位,支持 'celsius' 或 'fahrenheit'") + #定义示例函数 + def get_current_weather(location: str, unit: str) -> str: + return "北京今天25度" + + except ValueError as e: + # 使用 assert 检查是否抛出 ValueError,且包含 "缺少描述" 的信息 + assert "缺少描述" in str(e), f"Expected '缺少描述' in error message, but got: {str(e)}" + else: + # 如果未抛出异常,测试应失败 + assert False, "Expected ValueError but no exception was raised" + + try: + @appbuilder.manifest() + @appbuilder.manifest_parameter(name="location", description="城市名,例如:北京。") + @appbuilder.manifest_parameter(name="unit", description="温度单位,支持 'celsius' 或 'fahrenheit'") + #定义示例函数 + def get_current_weather(location: str, unit) -> str: + return "北京今天25度" + + except ValueError as e: + # 使用 assert 检查是否抛出 ValueError,且包含 "缺少描述" 的信息 + assert "缺少类型信息" in str(e), f"Expected '缺少类型信息' in error message, but got: {str(e)}" + else: + # 如果未抛出异常,测试应失败 + assert False, "Expected ValueError but no exception was raised" + if __name__ == '__main__': - unittest.main() + unittest.main() \ No newline at end of file diff --git a/python/tests/test_manifest.py b/python/tests/test_manifest.py new file mode 100644 index 000000000..9bd7d7ec3 --- /dev/null +++ b/python/tests/test_manifest.py @@ -0,0 +1,326 @@ +import unittest +import os +from typing import Any, Dict, List, Optional +import appbuilder +from appbuilder import Manifest +from appbuilder import manifest, manifest_parameter + +@unittest.skipUnless(os.getenv("TEST_CASE", "UNKNOWN") == "CPU_SERIAL", "") +class TestManifest(unittest.TestCase): + def setUp(self): + """ + 设置环境变量。 + + Args: + 无参数,默认值为空。 + + Returns: + 无返回值,方法中执行了环境变量的赋值操作。 + """ + os.environ["APPBUILDER_TOKEN"] = "bce-v3/ALTAK-DKaql4wY9ojwp2uMe8IEj/7ae1190aff0684153de365381d9b06beab3064c5" + self.app_id = "7cc4c21f-0e25-4a76-baf7-01a2b923a1a7" + + def test_google_style(self): + # Generated by vscode plugin + # https://marketplace.visualstudio.com/items?itemName=njpwerner.autodocstring + def google_style( + name: str, + val: str = None, + val_obj: Optional[Any] = None, + val_list: List[str] = None, + data: Dict[str, int] = None, + ) -> str: + """Google style docstring. + + Args: + name (str): Name of object. + val (str, optional): Value of obj. Defaults to None. + val_obj (Optional[Any], optional): Real object reference. Defaults to None. + val_list (List[str], optional): List of items with object. Defaults to None. + data (Dict[str, int], optional): Data along with object. Defaults to None. + + Returns: + str: Styled string. + """ + return "" + function_manifest = appbuilder.Manifest.from_function(google_style) + + # 断言顶层的结构 + assert function_manifest.type == "function", "Type does not match 'function'" + assert function_manifest.function["name"] == "google_style", "Function name does not match 'google_style'" + assert function_manifest.function["description"] == """Google style docstring. + + Args: + name (str): Name of object. + val (str, optional): Value of obj. Defaults to None. + val_obj (Optional[Any], optional): Real object reference. Defaults to None. + val_list (List[str], optional): List of items with object. Defaults to None. + data (Dict[str, int], optional): Data along with object. Defaults to None. + + Returns: + str: Styled string. + """, "Description does not match" + + # 断言参数结构 + parameters = function_manifest.function["parameters"] + assert parameters["type"] == "object", "Parameters type does not match 'object'" + assert "properties" in parameters, "Properties not found in parameters" + + # 断言各个参数的类型和描述 + properties = parameters["properties"] + + # name 参数 + assert "name" in properties, "'name' parameter missing" + # 添加类型检查时的调试信息 + try: + assert properties["name"]["type"] == "str", ( + f"'name' type does not match 'str'. Actual type: {properties['name']['type']}" + ) + except AssertionError as e: + print(f"Debug Info: Actual 'name' type is {properties['name']['type']}") + raise e # 重新抛出异常 + + # val 参数 + assert "val" in properties, "'val' parameter missing" + assert properties["val"]["type"] == "str", "'val' type does not match 'str'" + + # val_obj 参数 + assert "val_obj" in properties, "'val_obj' parameter missing" + assert properties["val_obj"]["type"] == "Optional[Any]", "'val_obj' type does not match 'object'" + + # val_list 参数 + assert "val_list" in properties, "'val_list' parameter missing" + assert properties["val_list"]["type"] == "List[str]", "'val_list' type does not match 'array'" + + # data 参数 + assert "data" in properties, "'data' parameter missing" + assert properties["data"]["type"] == "Dict[str, int]", "'data' type does not match 'object'" + + # 断言必需参数 + assert "required" in parameters, "'required' field missing in parameters" + assert parameters["required"] == ["name"], "'required' does not match ['name']" + + def test_google_style_bad_args_return_dict(self): + def func( + bad_param: str, + bad_generic_param: List[str], + bad_format: int, + val: str = None, + ) -> Dict[str, str]: + """Google style docstring. + + Args: + bad param (str): Bad parameter, name contains whitespace. + bad_generic_param (List): Bad generic parameter, use <> instead of [] + bad_format (int) Bad arg doc format, lost :. + val (str , optional): Value of obj. Defaults to None. + + Returns: + Dict[str, str]: Returns a dict. + """ + return "" + function_manifest = appbuilder.Manifest.from_function(func) + # 断言顶层的结构 + assert function_manifest.type == "function", "Type does not match 'function'" + assert function_manifest.function["name"] == "func", "Function name does not match 'func'" + assert function_manifest.function["description"] == """Google style docstring. + + Args: + bad param (str): Bad parameter, name contains whitespace. + bad_generic_param (List): Bad generic parameter, use <> instead of [] + bad_format (int) Bad arg doc format, lost :. + val (str , optional): Value of obj. Defaults to None. + + Returns: + Dict[str, str]: Returns a dict. + """, "Description does not match" + + # 断言参数结构 + parameters = function_manifest.function["parameters"] + assert parameters["type"] == "object", "Parameters type does not match 'object'" + assert "properties" in parameters, "Properties not found in parameters" + + # 断言各个参数的类型和描述 + properties = parameters["properties"] + + # bad_param 参数 + assert "bad_param" in properties, "'bad_param' parameter missing" + assert properties["bad_param"]["type"] == "str", "'bad_param' type does not match 'str'" + + # bad_format 参数 + assert "bad_format" in properties, "'bad_format' parameter missing" + assert properties["bad_format"]["type"] == "int", "'bad_format' type does not match 'int'" + + # val 参数 + assert "val" in properties, "'val' parameter missing" + assert properties["val"]["type"] == "str", "'val' type does not match 'str'" + + # 断言必需参数 + assert "required" in parameters, "'required' field missing in parameters" + assert parameters["required"] == ["bad_param", "bad_generic_param", "bad_format"], "'required' does not match expected required parameters" + + # 断言没有多余参数 + assert len(properties) == 4, "Unexpected number of parameters in properties" + + + def test_google_style_no_return(self): + def func( + name: str, + ): + """Google style docstring. + + Args: + name (str): Name of object. + + """ + return "" + + function_manifest = appbuilder.Manifest.from_function(func) + # 断言顶层的结构 + assert function_manifest.type == "function", "Type does not match 'function'" + assert function_manifest.function["name"] == "func", "Function name does not match 'func'" + assert function_manifest.function["description"] == """Google style docstring. + + Args: + name (str): Name of object. + + """, "Description does not match" + + # 断言参数结构 + parameters = function_manifest.function["parameters"] + assert parameters["type"] == "object", "Parameters type does not match 'object'" + assert "properties" in parameters, "Properties not found in parameters" + + # 断言参数类型和描述 + properties = parameters["properties"] + + # name 参数 + assert "name" in properties, "'name' parameter missing" + assert properties["name"]["type"] == "str", "'name' type does not match 'str'" + + # 断言必需参数 + assert "required" in parameters, "'required' field missing in parameters" + assert parameters["required"] == ["name"], "'required' does not match ['name']" + + #断言没有["parameters"][1]的参数了 + assert not ("parameters" in function_manifest.function and len(function_manifest.function["parameters"]) == 1) + + + def test_google_style_no_args_no_return(self): + def func( + name: str, + /, + *args, + val: str = None, + val_obj: Optional[Any] = None, + data: Dict[str, int] = None, + **kwargs, + ) -> str: + """Google style docstring.""" + return "" + + #断言这里会抛出参数类型缺失导致的ValueError异常 + try: + function_manifest = appbuilder.Manifest.from_function(func) + except ValueError as e: + # 使用 assert 检查是否抛出 ValueError,且包含 "缺少描述" 的信息 + assert "缺少类型信息" in str(e), f"Expected '缺少类型信息' in error message, but got: {str(e)}" + else: + # 如果未抛出异常,测试应失败 + assert False, "Expected ValueError but no exception was raised" + + def test_no_doc(self): + def func( + name: str, + /, + *args, + val: str = None, + val_obj: Optional[Any] = None, + data: Dict[str, int] = None, + **kwargs, + ) -> str: + return "" + + # 断言这里会抛出缺少文档字符串的 ValueError 异常 + try: + function_manifest = appbuilder.Manifest.from_function(func) + except ValueError as e: + assert str(e) == "函数 func 缺少文档字符串", "未抛出预期的 ValueError 或信息不匹配" + + def test_decorator_google_style_basic(self): + @manifest(description="Function with required parameter.") + def func( + name: str, + ) -> str: + """Function with required parameter. + + Args: + name (str): Name of object. + + Returns: + str: Styled string. + """ + return "" + + # 获取装饰器生成的 Manifest + function_manifest = func.__ab_manifest__ + + # 断言顶层的结构 + assert function_manifest.type == "function", "Type does not match 'function'" + assert function_manifest.function["name"] == "func", "Function name does not match 'func'" + assert function_manifest.function["description"] == "Function with required parameter.", "Description does not match" + + # 断言参数结构 + parameters = function_manifest.function["parameters"] + assert parameters["type"] == "object", "Parameters type does not match 'object'" + assert "properties" in parameters, "Properties not found in parameters" + + # 断言各个参数的类型和描述 + properties = parameters["properties"] + + # name 参数 + assert "name" in properties, "'name' parameter missing" + assert properties["name"]["type"] == "str", "'name' type does not match 'str'" + assert properties["name"]["description"] == None, "'name' description does not match" + + # 断言必需参数 + assert "required" in parameters, "'required' field missing in parameters" + assert parameters["required"] == ["name"], "'required' does not match ['name']" + + + def test_missing_type(self): + """测试参数缺少类型信息是否抛出 ValueError 异常。""" + try: + @appbuilder.manifest() + @appbuilder.manifest_parameter(name="location", description="城市名,例如:北京。") + @appbuilder.manifest_parameter(name="unit", description="温度单位,支持 'celsius' 或 'fahrenheit'") + #定义示例函数 + def get_current_weather(location: str, unit) -> str: + return "北京今天25度" + + except ValueError as e: + # 使用 assert 检查是否抛出 ValueError,且包含 "缺少描述" 的信息 + assert "缺少类型信息" in str(e), f"Expected '缺少类型信息' in error message, but got: {str(e)}" + else: + # 如果未抛出异常,测试应失败 + assert False, "Expected ValueError but no exception was raised" + + def test_missing_description(self): + """测试函数缺少描述是否抛出 ValueError 异常。""" + try: + @appbuilder.manifest() + @appbuilder.manifest_parameter(name="location", description="城市名,例如:北京。") + @appbuilder.manifest_parameter(name="unit", description="温度单位,支持 'celsius' 或 'fahrenheit'") + #定义示例函数 + def get_current_weather(location: str, unit: str) -> str: + return "北京今天25度" + + except ValueError as e: + # 使用 assert 检查是否抛出 ValueError,且包含 "缺少描述" 的信息 + assert "缺少描述" in str(e), f"Expected '缺少描述' in error message, but got: {str(e)}" + else: + # 如果未抛出异常,测试应失败 + assert False, "Expected ValueError but no exception was raised" + +if __name__ == '__main__': + unittest.main() diff --git a/python/tests/test_manifest_decorator.py b/python/tests/test_manifest_decorator.py new file mode 100644 index 000000000..263f64378 --- /dev/null +++ b/python/tests/test_manifest_decorator.py @@ -0,0 +1,167 @@ +# Copyright (c) 2024 Baidu, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +import os +import unittest +from appbuilder import Manifest, manifest, manifest_parameter + +@unittest.skipUnless(os.getenv("TEST_CASE", "UNKNOWN") == "CPU_PARALLEL", "") +class TestManifestDecorator(unittest.TestCase): + def test_disable_docstring(self): + @manifest(description="anotated function") + @manifest_parameter(name="param", description="a parameter", type="str") + def func(param: str) -> str: + return param + + function_manifest = func.__ab_manifest__ + + # 断言顶层的结构 + assert function_manifest.type == "function", "Type does not match 'function'" + assert function_manifest.function["name"] == "func", "Function name does not match 'func'" + assert function_manifest.function["description"] == "anotated function", "Description does not match" + + # 断言参数结构 + parameters = function_manifest.function["parameters"] + assert parameters["type"] == "object", "Parameters type does not match 'object'" + assert "properties" in parameters, "Properties not found in parameters" + + # 断言具体参数 + properties = parameters["properties"] + assert "param" in properties, "'param' parameter missing" + assert properties["param"]["type"] == "str", "'param' type does not match 'str'" + assert properties["param"]["description"] == "a parameter", "'param' description does not match" + + # 断言必需参数 + assert "required" in parameters, "'required' field missing in parameters" + assert parameters["required"] == ["param"], "'required' does not match ['param']" + + + def test_combine(self): + @manifest() + @manifest_parameter(name="param") + def func(param: str = "[]") -> int: + """An example function. + + Args: + param (str): A list of numbers. + + Returns: + int: The sum of parameter. + """ + return param + + # 获取装饰器生成的 Manifest + function_manifest = func.__ab_manifest__ + + # 断言顶层结构 + assert function_manifest.type == "function", "Type does not match 'function'" + assert function_manifest.function["name"] == "func", "Function name does not match 'func'" + assert function_manifest.function["description"] == """An example function. + + Args: + param (str): A list of numbers. + + Returns: + int: The sum of parameter. + """, "Description does not match" + + # 断言参数结构 + parameters = function_manifest.function["parameters"] + assert parameters["type"] == "object", "Parameters type does not match 'object'" + assert "properties" in parameters, "Properties not found in parameters" + + # 断言具体参数 + properties = parameters["properties"] + assert "param" in properties, "'param' parameter missing" + assert properties["param"]["type"] == "str", "'param' type does not match 'str'" + assert "description" in properties["param"], "'param' description missing" + assert properties["param"]["description"] == None, "'param' description does not match" + + # 断言必需参数 + assert "required" in parameters, "'required' field missing in parameters" + assert "param" not in parameters["required"], "'param' should not be required as it has a default value" + + + def test_reversed_decorators(self): + @manifest_parameter(name="param", description="DECORATOR A list of numbers.") + @manifest() + def func(param: str = "[]") -> int: + """An example function. + + Args: + param (str): A list of numbers. + + Returns: + int: The sum of parameter. + """ + return param + + # 获取装饰器生成的 Manifest + function_manifest = func.__ab_manifest__ + + # 断言顶层结构 + assert function_manifest.type == "function", "Type does not match 'function'" + assert function_manifest.function["name"] == "func", "Function name does not match 'func'" + assert function_manifest.function["description"] == """An example function. + + Args: + param (str): A list of numbers. + + Returns: + int: The sum of parameter. + """, "Description does not match" + + # 断言参数结构 + parameters = function_manifest.function["parameters"] + assert parameters["type"] == "object", "Parameters type does not match 'object'" + assert "properties" in parameters, "Properties not found in parameters" + + # 断言具体参数 + properties = parameters["properties"] + assert "param" in properties, "'param' parameter missing" + assert properties["param"]["type"] == "str", "'param' type does not match 'str'" + + # 断言必需参数 + assert "required" in parameters, "'required' field missing in parameters" + assert "param" in parameters["required"], "'param' should not be required as it has a default value" + + def test_only_function_decorator(self): + @manifest() + def func(param: str = "[]") -> int: + " " + return param + + view = func.__ab_manifest__ + + # 获取装饰器生成的 Manifest + function_manifest = func.__ab_manifest__ + + # 断言顶层结构 + assert function_manifest.type == "function", "Type does not match 'function'" + assert function_manifest.function["name"] == "func", "Function name does not match 'func'" + assert function_manifest.function["description"] == " ", "Description should be None when not explicitly provided" + + # 断言参数结构 + parameters = function_manifest.function["parameters"] + assert parameters["type"] == "object", "Parameters type does not match 'object'" + assert "properties" in parameters, "Properties not found in parameters" + + # 断言具体参数 + properties = parameters["properties"] + assert "param" in properties, "'param' parameter missing" + assert properties["param"]["type"] == "str", "'param' type does not match 'str'" + + # 检查是否为必需参数 + assert "required" in parameters, "'required' field missing in parameters" + assert "param" not in parameters["required"], "'param' should not be required as it has a default value" + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/python/tests/test_manifest_signature.py b/python/tests/test_manifest_signature.py new file mode 100644 index 000000000..77dacb855 --- /dev/null +++ b/python/tests/test_manifest_signature.py @@ -0,0 +1,612 @@ +# Copyright (c) 2024 Baidu, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import unittest +from typing import Any, Dict, List, Optional, Union +from appbuilder import Manifest, manifest + +#@unittest.skipUnless(os.getenv("TEST_CASE", "UNKNOWN") == "CPU_PARALLEL", "") +class TestManifestSignature(unittest.TestCase): + def test_is_normal(self): + @manifest() + def func(): + return 1 + + view = func.__ab_manifest__ + + assert isinstance(view, Manifest) + + def test_is_async(self): + @manifest(description="test") + async def func(): + import asyncio + + await asyncio.sleep(0) + + view = func.__ab_manifest__ + + assert isinstance(view, Manifest) + + + def test_is_stream(self): + @manifest() + def func(): + for i in range(2): + yield i + + view = func.__ab_manifest__ + + assert isinstance(view, Manifest) + + + def test_is_async_and_stream(self): + @manifest() + async def func(): + import asyncio + + for i in range(1): + await asyncio.sleep(0) + yield i + + view = func.__ab_manifest__ + + assert isinstance(view, Manifest) + + + def test_decorator_google_style_function_description_no_args(self): + @manifest() + def func(): + """A function to test function description. + + Args: + name (str): Name of object. + + Returns: + str: Styled string. + """ + return "" + + view = func.__ab_manifest__ + + assert isinstance(view, Manifest) + assert view.function["name"] == "func" + assert view.function["description"] == """A function to test function description. + + Args: + name (str): Name of object. + + Returns: + str: Styled string. + """ + + # Assert that parameters are an empty dictionary since there are no function arguments + assert view.function["parameters"]["properties"] == {} + assert view.function["parameters"]["required"] == [] + + + def test_decorator_google_style_basic(self): + @manifest() + def func( + name: str, + ) -> str: + """Function with required parameter. + + Args: + name (str): Name of object. + + Returns: + str: Styled string. + """ + return "" + + view = func.__ab_manifest__ + + # 检查生成的 Manifest 对象 + assert isinstance(view, Manifest) + + # 检查函数元信息 + assert view.function["name"] == "func", "Function name does not match 'func'" + assert view.function["description"] == """Function with required parameter. + + Args: + name (str): Name of object. + + Returns: + str: Styled string. + """ + + # 检查参数结构 + parameters = view.function["parameters"] + assert parameters["type"] == "object", "Parameters type does not match 'object'" + assert "properties" in parameters, "Properties not found in parameters" + + # 检查具体参数 'name' + properties = parameters["properties"] + assert "name" in properties, "'name' parameter missing" + name_property = properties["name"] + assert name_property["type"] == "str", "'name' type does not match 'str'" + assert name_property["description"] is None, "'name' description does not match None" + assert name_property["required"] is True, "'name' required does not match True" + + + def test_decorator_google_style_list(self): + @manifest() + def func( + val: List[str], + ) -> str: + """Function with a List parameter. + + Args: + val (List[str]): A list of strings. + + Returns: + str: A result string. + """ + return "" + + view = func.__ab_manifest__ + + # 检查生成的 Manifest 对象 + assert isinstance(view, Manifest) + + # 检查参数的总体结构 + parameters = view.function["parameters"] + assert parameters["type"] == "object", "Parameters type does not match 'object'" + assert "properties" in parameters, "Properties not found in parameters" + + # 检查具体参数 'val' + properties = parameters["properties"] + assert "val" in properties, "'val' parameter missing" + val_property = properties["val"] + + # 验证 'val' 参数的各项属性 + assert val_property["type"] == "List[str]", "'val' type does not match 'List[str]'" + assert val_property["required"] is True, "'val' required does not match True" + + + def test_decorator_google_style_list_of_dicts(self): + @manifest() + def func( + val: List[Dict[str, List[str]]], + ) -> str: + """Function with a complex nested parameter. + + Args: + val (List[Dict[str, List[str]]]): A list of dictionaries mapping strings to lists of strings. + + Returns: + str: A result string. + """ + return "" + + view = func.__ab_manifest__ + + # 检查生成的 Manifest 对象 + assert isinstance(view, Manifest), "view is not an instance of Manifest" + + # 检查参数的总体结构 + parameters = view.function["parameters"] + assert parameters["type"] == "object", "Parameters type does not match 'object'" + assert "properties" in parameters, "Properties not found in parameters" + + # 检查具体参数 'val' + properties = parameters["properties"] + assert "val" in properties, "'val' parameter missing" + val_property = properties["val"] + + # 验证 'val' 参数的各项属性 + assert val_property["type"] == "List[Dict[str, List[str]]]", "'val' type does not match 'List[Dict[str, List[str]]]'" + assert val_property["required"] is True, "'val' required does not match True" + + + def test_decorator_google_style_dict(self): + @manifest() + def func( + val: Dict[str, Any], + ) -> str: + """Function with a dictionary parameter. + + Args: + val (Dict[str, Any]): A dictionary with string keys and any values. + + Returns: + str: A result string. + """ + return "" + + view = func.__ab_manifest__ + + # 检查生成的 Manifest 对象 + assert isinstance(view, Manifest), "view is not an instance of Manifest" + + # 检查参数的总体结构 + parameters = view.function["parameters"] + assert parameters["type"] == "object", "Parameters type does not match 'object'" + assert "properties" in parameters, "Properties not found in parameters" + + # 检查具体参数 'val' + properties = parameters["properties"] + assert "val" in properties, "'val' parameter missing" + val_property = properties["val"] + + # 验证 'val' 参数的各项属性 + assert val_property["type"] == "Dict[str, Any]", "'val' type does not match 'Dict[str, Any]'" + assert val_property["required"] is True, "'val' required does not match True" + + + def test_decorator_google_style_dict_of_lists(self): + @manifest() + def func( + val: Dict[str, List[Dict[str, List[str]]]], + ) -> str: + """Function with a complex nested parameter. + + Args: + val (Dict[str, List[Dict[str, List[str]]]]): A dictionary where keys are strings and values are lists of dictionaries. + + Returns: + str: A result string. + """ + return "" + + view = func.__ab_manifest__ + + # 检查生成的 Manifest 对象 + assert isinstance(view, Manifest), "view is not an instance of Manifest" + + # 检查参数的总体结构 + parameters = view.function["parameters"] + assert parameters["type"] == "object", "Parameters type does not match 'object'" + assert "properties" in parameters, "Properties not found in parameters" + + # 检查具体参数 'val' + properties = parameters["properties"] + assert "val" in properties, "'val' parameter missing" + val_property = properties["val"] + + # 验证 'val' 参数的各项属性 + assert val_property["type"] == "Dict[str, List[Dict[str, List[str]]]]", "'val' type does not match 'Dict[str, List[Dict[str, List[str]]]]'" + assert val_property["required"] is True, "'val' required does not match True" + + + def test_decorator_google_style_union_simple(self): + @manifest() + def func( + val: Union[str, int, Any], + ) -> str: + """Function with a Union parameter. + + Args: + val (Union[str, int, Any]): A parameter that can accept multiple types. + + Returns: + str: A result string. + """ + return "" + + view = func.__ab_manifest__ + + # 检查生成的 Manifest 对象 + assert isinstance(view, Manifest), "view is not an instance of Manifest" + + # 检查参数的总体结构 + parameters = view.function["parameters"] + assert parameters["type"] == "object", "Parameters type does not match 'object'" + assert "properties" in parameters, "Properties not found in parameters" + + # 检查具体参数 'val' + properties = parameters["properties"] + assert "val" in properties, "'val' parameter missing" + val_property = properties["val"] + + # 验证 'val' 参数的各项属性 + assert val_property["type"] == "Union[str, int, Any]", "'val' type does not match 'Union[str, int, Any]'" + assert val_property["required"] is True, "'val' required does not match True" + + + def test_decorator_google_style_union(self): + @manifest() + def func( + val: Union[str, List[int]], + ) -> str: + """Function with a Union parameter. + + Args: + val (Union[str, List[int]]): A parameter that can accept a string or a list of integers. + + Returns: + str: A result string. + """ + return "" + + view = func.__ab_manifest__ + + # 检查生成的 Manifest 对象 + assert isinstance(view, Manifest), "view is not an instance of Manifest" + + # 检查参数的总体结构 + parameters = view.function["parameters"] + assert parameters["type"] == "object", "Parameters type does not match 'object'" + assert "properties" in parameters, "Properties not found in parameters" + + # 检查具体参数 'val' + properties = parameters["properties"] + assert "val" in properties, "'val' parameter missing" + val_property = properties["val"] + + # 验证 'val' 参数的各项属性 + assert val_property["type"] == "Union[str, List[int]]", "'val' type does not match 'Union[str, List[int]]'" + assert val_property["required"] is True, "'val' required does not match True" + + + def test_decorator_google_style_union_nest1_dict(self): + @manifest() + def func( + val: Union[float, Dict[str, int]], + ) -> str: + """Function with a nested Union parameter. + + Args: + val (Union[float, Dict[str, int]]): A parameter that can accept a float or a dictionary with string keys and integer values. + + Returns: + str: A result string. + """ + return "" + + view = func.__ab_manifest__ + + # 检查生成的 Manifest 对象 + assert isinstance(view, Manifest), "view is not an instance of Manifest" + + # 检查参数的总体结构 + parameters = view.function["parameters"] + assert parameters["type"] == "object", "Parameters type does not match 'object'" + assert "properties" in parameters, "Properties not found in parameters" + + # 检查具体参数 'val' + properties = parameters["properties"] + assert "val" in properties, "'val' parameter missing" + val_property = properties["val"] + + # 验证 'val' 参数的各项属性 + assert val_property["type"] == "Union[float, Dict[str, int]]", "'val' type does not match 'Union[float, Dict[str, int]]'" + assert val_property["required"] is True, "'val' required does not match True" + + + def test_decorator_google_style_union_combine(self): + @manifest() + def func( + val: Union[float, Union[str, int]], + ) -> str: + """Function with a combined Union parameter. + + Args: + val (Union[float, str, int]): A parameter that can accept a float, string, or integer. + + Returns: + str: A result string. + """ + return "" + + view = func.__ab_manifest__ + + # 检查生成的 Manifest 对象 + assert isinstance(view, Manifest), "view is not an instance of Manifest" + + # 检查参数的总体结构 + parameters = view.function["parameters"] + assert parameters["type"] == "object", "Parameters type does not match 'object'" + assert "properties" in parameters, "Properties not found in parameters" + + # 检查具体参数 'val' + properties = parameters["properties"] + assert "val" in properties, "'val' parameter missing" + val_property = properties["val"] + + # 验证 'val' 参数的各项属性 + assert val_property["type"] == "Union[float, str, int]", "'val' type does not match 'Union[float, str, int]'" + assert val_property["required"] is True, "'val' required does not match True" + + + def test_decorator_google_style_no_annotation(self): + @manifest(description="Test Function") + def func( + val, + ) -> str: + return "" + + view = func.__ab_manifest__ + + assert isinstance(view, Manifest), "view is not an instance of Manifest" + + parameters = view.function["parameters"] + properties = parameters["properties"] + + assert "val" in properties, "'val' parameter missing" + val_property = properties["val"] + + assert val_property["type"] == "Any", "'val' type does not match 'Any'" + assert val_property["required"] is True, "'val' required does not match True" + + + def test_decorator_google_style_default(self): + @manifest(description="Test Function") + def func(val: str = "value") -> str: + return "" + + view = func.__ab_manifest__ + + assert isinstance(view, Manifest), "view is not an instance of Manifest" + + parameters = view.function["parameters"] + properties = parameters["properties"] + + assert "val" in properties, "'val' parameter missing" + val_property = properties["val"] + + assert val_property["type"] == "str", "'val' type does not match 'str'" + assert val_property["required"] is False, "'val' required does not match False" + + + def test_decorator_google_style_optional(self): + @manifest(description="Test Function") + def func( + val: Optional[str], + ) -> str: + return "" + + view = func.__ab_manifest__ + + assert isinstance(view, Manifest), "view is not an instance of Manifest" + + parameters = view.function["parameters"] + properties = parameters["properties"] + + assert "val" in properties, "'val' parameter missing" + val_property = properties["val"] + + assert val_property["type"] == "Optional[str]", "'val' type does not match 'Optional[str]'" + assert val_property["required"] is False, "'val' required does not match False" + + + def test_decorator_google_style_optional_equals_none(self): + @manifest(description="Test Function") + def func( + val: Optional[str] = None, + ) -> str: + return "" + + view = func.__ab_manifest__ + + assert isinstance(view, Manifest), "view is not an instance of Manifest" + + parameters = view.function["parameters"] + properties = parameters["properties"] + + assert "val" in properties, "'val' parameter missing" + val_property = properties["val"] + + assert val_property["type"] == "Optional[str]", "'val' type does not match 'Optional[str]'" + assert val_property["required"] is False, "'val' required does not match False" + + + def test_decorator_google_style_optional_list(self): + @manifest(description="Test Function") + def func( + val: Optional[List[str]], + ) -> str: + return "" + + view = func.__ab_manifest__ + + assert isinstance(view, Manifest), "view is not an instance of Manifest" + + parameters = view.function["parameters"] + properties = parameters["properties"] + + assert "val" in properties, "'val' parameter missing" + val_property = properties["val"] + + assert val_property["type"] == "Optional[List[str]]", "'val' type does not match 'Optional[List[str]]'" + assert val_property["required"] is False, "'val' required does not match False" + + + def test_decorator_google_style_list_nest_optional(self): + @manifest(description="Test Function") + def func( + val: List[Optional[str]], + ) -> str: + return "" + + view = func.__ab_manifest__ + + assert isinstance(view, Manifest), "view is not an instance of Manifest" + + parameters = view.function["parameters"] + properties = parameters["properties"] + + assert "val" in properties, "'val' parameter missing" + val_property = properties["val"] + + assert val_property["type"] == "List[Optional[str]]", "'val' type does not match 'List[Optional[str]]'" + assert val_property["required"] is True, "'val' required does not match True" + + + def test_decorator_google_style_union_nest_single_optional(self): + @manifest(description="Test Function") + def func( + val: Union[Optional[str]], + ) -> str: + return "" + + view = func.__ab_manifest__ + + assert isinstance(view, Manifest), "view is not an instance of Manifest" + + parameters = view.function["parameters"] + properties = parameters["properties"] + + assert "val" in properties, "'val' parameter missing" + val_property = properties["val"] + + assert val_property["type"] == "Optional[str]", "'val' type does not match 'Optional[str]'" + assert val_property["required"] is False, "'val' required does not match False" + + def test_decorator_google_style_union_nest_optional(self): + @manifest(description="Test Function") + def func( + val: Union[Optional[int], Optional[str]], + ) -> str: + return "" + + view = func.__ab_manifest__ + + assert isinstance(view, Manifest), "view is not an instance of Manifest" + + parameters = view.function["parameters"] + properties = parameters["properties"] + + assert "val" in properties, "'val' parameter missing" + val_property = properties["val"] + + # 验证类型,inspect 可能优化嵌套 Union + assert val_property["type"] == "Union[int, str]", "'val' type does not match 'Union[int, str]'" + assert val_property["required"] is False, "'val' required does not match False" + + + def test_decorator_google_style_union_nest1_dict_optional_value(self): + @manifest(description="Test Function") + def func( + val: Union[float, Dict[str, Optional[int]]], + ) -> str: + return "" + + view = func.__ab_manifest__ + + assert isinstance(view, Manifest), "view is not an instance of Manifest" + + parameters = view.function["parameters"] + properties = parameters["properties"] + + assert "val" in properties, "'val' parameter missing" + val_property = properties["val"] + + assert val_property["type"] == "Union[float, Dict[str, Optional[int]]]", ( + "'val' type does not match 'Union[float, Dict[str, Optional[int]]]'" + ) + assert val_property["required"] is True, "'val' required does not match True" + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file