{"cells": [{"attachments": {}, "cell_type": "markdown", "metadata": {"cell_id": "8317718bcd8b475e9b336514241d679e", "deepnote_cell_type": "text-cell-h1", "formattedRanges": []}, "source": ["# 使用OpenAPI规范调用函数\n"]}, {"attachments": {}, "cell_type": "markdown", "metadata": {"cell_id": "6f810c4e582c41a1bd3bdfc4263dee89", "deepnote_cell_type": "text-cell-p", "formattedRanges": []}, "source": ["许多互联网服务都是由RESTful API提供支持的。赋予GPT调用这些API的能力打开了无限的可能性。本笔记演示了如何利用GPT智能调用API。它利用了OpenAPI规范和链式函数调用。\n", "\n", "[OpenAPI规范(OAS)](https://swagger.io/specification/)是一种被普遍接受的标准,用于以机器可读和解释的格式描述RESTful API的细节。它使人类和计算机都能理解一个服务的能力,并且可以被利用来展示GPT如何调用API。\n", "\n", "本笔记分为两个主要部分:\n", "\n", "1. 如何将一个示例OpenAPI规范转换为聊天完成API的函数定义列表。\n", "2. 如何使用聊天完成API根据用户指令智能调用这些函数。\n", "\n", "我们建议在继续之前先熟悉[如何使用聊天模型调用函数](./How_to_call_functions_with_chat_models.ipynb)。\n"]}, {"cell_type": "code", "execution_count": 1, "metadata": {"cell_id": "bf983b3e199d4ea6a2718e58a141bd88", "deepnote_cell_type": "code", "deepnote_to_be_reexecuted": false, "execution_millis": 10617, "execution_start": 1697419508239, "source_hash": "d6b9a6d3"}, "outputs": [{"name": "stdout", "output_type": "stream", "text": ["\u001b[33mDEPRECATION: textract 1.6.5 has a non-standard dependency specifier extract-msg<=0.29.*. pip 23.3 will enforce this behaviour change. A possible replacement is to upgrade to a newer version of textract or contact the author to suggest that they release a version with a conforming dependency specifiers. Discussion can be found at https://github.com/pypa/pip/issues/12063\u001b[0m\u001b[33m\n", "\u001b[0m\n", "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m23.2.1\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m23.3.1\u001b[0m\n", "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip install --upgrade pip\u001b[0m\n", "\u001b[33mDEPRECATION: textract 1.6.5 has a non-standard dependency specifier extract-msg<=0.29.*. pip 23.3 will enforce this behaviour change. A possible replacement is to upgrade to a newer version of textract or contact the author to suggest that they release a version with a conforming dependency specifiers. Discussion can be found at https://github.com/pypa/pip/issues/12063\u001b[0m\u001b[33m\n", "\u001b[0m\n", "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m23.2.1\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m23.3.1\u001b[0m\n", "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip install --upgrade pip\u001b[0m\n"]}], "source": ["!pip install -q jsonref # 用于解析OpenAPI规范中的$ref引用\n", "!pip install -q openai\n"]}, {"cell_type": "code", "execution_count": 7, "metadata": {"cell_id": "12fb12583cc74b7c842a1b4656b94f47", "deepnote_cell_type": "code", "deepnote_to_be_reexecuted": false, "execution_millis": 10, "execution_start": 1697419706563, "source_hash": "750280cb"}, "outputs": [], "source": ["import os\n", "import json\n", "import jsonref\n", "from openai import OpenAI\n", "import requests\n", "from pprint import pp\n", "\n", "client = OpenAI(api_key=os.environ.get(\"OPENAI_API_KEY\", \"\"))\n"]}, {"attachments": {}, "cell_type": "markdown", "metadata": {"cell_id": "76ae868e66b14cc48c9c447302ea268e", "deepnote_cell_type": "text-cell-h2", "formattedRanges": []}, "source": ["## 如何将OpenAPI规范转换为函数定义\n"]}, {"attachments": {}, "cell_type": "markdown", "metadata": {"cell_id": "9cf167d2d5fe4d7eadb69ba18db4a696", "deepnote_cell_type": "text-cell-p", "formattedRanges": []}, "source": ["我们在这里使用的示例OpenAPI规范是使用`gpt-4`创建的。我们将把这个示例规范转换成一组函数定义,可以提供给聊天完成API。基于提供的用户指令,模型会生成一个包含调用这些函数所需参数的JSON对象。\n", "\n", "在我们继续之前,让我们检查一下生成的规范。OpenAPI规范包括关于API端点、它们支持的操作、它们接受的参数、它们可以处理的请求以及它们返回的响应的详细信息。该规范以JSON格式定义。\n", "\n", "规范中的端点包括以下操作:\n", "\n", "- 列出所有事件\n", "- 创建新事件\n", "- 通过ID检索事件\n", "- 通过ID删除事件\n", "- 通过ID更新事件名称\n", "\n", "规范中的每个操作都有一个`operationId`,我们将在将规范解析为函数规范时将其用作函数名。规范还包括定义每个操作的参数的数据类型和结构的模式。\n", "\n", "您可以在这里看到模式:\n"]}, {"cell_type": "code", "execution_count": 3, "metadata": {"cell_id": "176efbfea4b546d28d9d9966342f286a", "deepnote_cell_type": "code", "deepnote_to_be_reexecuted": false, "execution_millis": 231, "execution_start": 1697419710160, "source_hash": "1ce3848"}, "outputs": [{"data": {"text/plain": ["{'openapi': '3.0.0',\n", " 'info': {'version': '1.0.0',\n", " 'title': 'Event Management API',\n", " 'description': 'An API for managing event data'},\n", " 'paths': {'/events': {'get': {'summary': 'List all events',\n", " 'operationId': 'listEvents',\n", " 'responses': {'200': {'description': 'A list of events',\n", " 'content': {'application/json': {'schema': {'type': 'array',\n", " 'items': {'type': 'object',\n", " 'properties': {'id': {'type': 'string'},\n", " 'name': {'type': 'string'},\n", " 'date': {'type': 'string', 'format': 'date-time'},\n", " 'location': {'type': 'string'}},\n", " 'required': ['name', 'date', 'location']}}}}}}},\n", " 'post': {'summary': 'Create a new event',\n", " 'operationId': 'createEvent',\n", " 'requestBody': {'required': True,\n", " 'content': {'application/json': {'schema': {'type': 'object',\n", " 'properties': {'id': {'type': 'string'},\n", " 'name': {'type': 'string'},\n", " 'date': {'type': 'string', 'format': 'date-time'},\n", " 'location': {'type': 'string'}},\n", " 'required': ['name', 'date', 'location']}}}},\n", " 'responses': {'201': {'description': 'The event was created',\n", " 'content': {'application/json': {'schema': {'type': 'object',\n", " 'properties': {'id': {'type': 'string'},\n", " 'name': {'type': 'string'},\n", " 'date': {'type': 'string', 'format': 'date-time'},\n", " 'location': {'type': 'string'}},\n", " 'required': ['name', 'date', 'location']}}}}}}},\n", " '/events/{id}': {'get': {'summary': 'Retrieve an event by ID',\n", " 'operationId': 'getEventById',\n", " 'parameters': [{'name': 'id',\n", " 'in': 'path',\n", " 'required': True,\n", " 'schema': {'type': 'string'}}],\n", " 'responses': {'200': {'description': 'The event',\n", " 'content': {'application/json': {'schema': {'type': 'object',\n", " 'properties': {'id': {'type': 'string'},\n", " 'name': {'type': 'string'},\n", " 'date': {'type': 'string', 'format': 'date-time'},\n", " 'location': {'type': 'string'}},\n", " 'required': ['name', 'date', 'location']}}}}}},\n", " 'delete': {'summary': 'Delete an event by ID',\n", " 'operationId': 'deleteEvent',\n", " 'parameters': [{'name': 'id',\n", " 'in': 'path',\n", " 'required': True,\n", " 'schema': {'type': 'string'}}],\n", " 'responses': {'204': {'description': 'The event was deleted'}}},\n", " 'patch': {'summary': \"Update an event's details by ID\",\n", " 'operationId': 'updateEventDetails',\n", " 'parameters': [{'name': 'id',\n", " 'in': 'path',\n", " 'required': True,\n", " 'schema': {'type': 'string'}}],\n", " 'requestBody': {'required': True,\n", " 'content': {'application/json': {'schema': {'type': 'object',\n", " 'properties': {'name': {'type': 'string'},\n", " 'date': {'type': 'string', 'format': 'date-time'},\n", " 'location': {'type': 'string'}},\n", " 'required': ['name', 'date', 'location']}}}},\n", " 'responses': {'200': {'description': \"The event's details were updated\",\n", " 'content': {'application/json': {'schema': {'type': 'object',\n", " 'properties': {'id': {'type': 'string'},\n", " 'name': {'type': 'string'},\n", " 'date': {'type': 'string', 'format': 'date-time'},\n", " 'location': {'type': 'string'}},\n", " 'required': ['name', 'date', 'location']}}}}}}}},\n", " 'components': {'schemas': {'Event': {'type': 'object',\n", " 'properties': {'id': {'type': 'string'},\n", " 'name': {'type': 'string'},\n", " 'date': {'type': 'string', 'format': 'date-time'},\n", " 'location': {'type': 'string'}},\n", " 'required': ['name', 'date', 'location']}}}}"]}, "metadata": {}, "output_type": "display_data"}], "source": ["with open('./data/example_events_openapi.json', 'r') as f:\n", " openapi_spec = jsonref.loads(f.read()) # 使用jsonref进行加载非常重要,具体原因如下所述。\n", "\n", "display(openapi_spec)\n"]}, {"attachments": {}, "cell_type": "markdown", "metadata": {"cell_id": "e3e39ad4ac854299bf62b5f7bb1bef45", "deepnote_cell_type": "text-cell-p", "formattedRanges": []}, "source": ["现在我们对OpenAPI规范有了很好的理解,我们可以继续将其解析为函数规范。\n", "\n", "我们可以编写一个简单的`openapi_to_functions`函数来生成一个定义列表,其中每个函数都表示为一个包含以下键的字典:\n", "\n", "- `name`:这对应于在OpenAPI规范中定义的API端点的操作标识符。\n", "- `description`:这是函数的简要描述或总结,提供了函数的概述。\n", "- `parameters`:这是定义函数期望的输入参数的模式。它提供有关每个参数的类型、是否为必需或可选以及其他相关详细信息。\n", "\n", "对于模式中定义的每个端点,我们需要执行以下操作:\n", "\n", "1. **解析JSON引用**:在OpenAPI规范中,通常使用JSON引用(也称为$ref)来避免重复。这些引用指向在多个位置使用的定义。例如,如果多个API端点返回相同的对象结构,那么可以定义一次该结构,然后在需要时引用它。我们需要解析和替换这些引用为它们指向的内容。\n", "\n", "2. **提取函数的名称**:我们将简单地使用operationId作为函数名称。或者,我们可以使用端点路径和操作作为函数名称。\n", "\n", "3. **提取描述和参数**:我们将遍历`description`、`summary`、`requestBody`和`parameters`字段,以填充函数的描述和参数。\n", "\n", "这里是实现:\n"]}, {"cell_type": "code", "execution_count": 9, "metadata": {"cell_id": "adbb17ca8a2a4fa2aa3f0213a0e211b6", "deepnote_cell_type": "code", "deepnote_to_be_reexecuted": false, "execution_millis": 333, "execution_start": 1697419853135, "source_hash": "ad112cb1"}, "outputs": [{"name": "stdout", "output_type": "stream", "text": ["{'type': 'function',\n", " 'function': {'name': 'listEvents',\n", " 'description': 'List all events',\n", " 'parameters': {'type': 'object', 'properties': {}}}}\n", "\n", "{'type': 'function',\n", " 'function': {'name': 'createEvent',\n", " 'description': 'Create a new event',\n", " 'parameters': {'type': 'object',\n", " 'properties': {'requestBody': {'type': 'object',\n", " 'properties': {'id': {'type': 'string'},\n", " 'name': {'type': 'string'},\n", " 'date': {'type': 'string',\n", " 'format': 'date-time'},\n", " 'location': {'type': 'string'}},\n", " 'required': ['name',\n", " 'date',\n", " 'location']}}}}}\n", "\n", "{'type': 'function',\n", " 'function': {'name': 'getEventById',\n", " 'description': 'Retrieve an event by ID',\n", " 'parameters': {'type': 'object',\n", " 'properties': {'parameters': {'type': 'object',\n", " 'properties': {'id': {'type': 'string'}}}}}}}\n", "\n", "{'type': 'function',\n", " 'function': {'name': 'deleteEvent',\n", " 'description': 'Delete an event by ID',\n", " 'parameters': {'type': 'object',\n", " 'properties': {'parameters': {'type': 'object',\n", " 'properties': {'id': {'type': 'string'}}}}}}}\n", "\n", "{'type': 'function',\n", " 'function': {'name': 'updateEventDetails',\n", " 'description': \"Update an event's details by ID\",\n", " 'parameters': {'type': 'object',\n", " 'properties': {'requestBody': {'type': 'object',\n", " 'properties': {'name': {'type': 'string'},\n", " 'date': {'type': 'string',\n", " 'format': 'date-time'},\n", " 'location': {'type': 'string'}},\n", " 'required': ['name',\n", " 'date',\n", " 'location']},\n", " 'parameters': {'type': 'object',\n", " 'properties': {'id': {'type': 'string'}}}}}}}\n", "\n"]}], "source": ["def openapi_to_functions(openapi_spec):\n", " functions = []\n", "\n", " for path, methods in openapi_spec[\"paths\"].items():\n", " for method, spec_with_ref in methods.items():\n", " # 1. 解析JSON引用。\n", " spec = jsonref.replace_refs(spec_with_ref)\n", "\n", " # 2. 为功能提取一个名称。\n", " function_name = spec.get(\"operationId\")\n", "\n", " # 3. 提取描述和参数。\n", " desc = spec.get(\"description\") or spec.get(\"summary\", \"\")\n", "\n", " schema = {\"type\": \"object\", \"properties\": {}}\n", "\n", " req_body = (\n", " spec.get(\"requestBody\", {})\n", " .get(\"content\", {})\n", " .get(\"application/json\", {})\n", " .get(\"schema\")\n", " )\n", " if req_body:\n", " schema[\"properties\"][\"requestBody\"] = req_body\n", "\n", " params = spec.get(\"parameters\", [])\n", " if params:\n", " param_properties = {\n", " param[\"name\"]: param[\"schema\"]\n", " for param in params\n", " if \"schema\" in param\n", " }\n", " schema[\"properties\"][\"parameters\"] = {\n", " \"type\": \"object\",\n", " \"properties\": param_properties,\n", " }\n", "\n", " functions.append(\n", " {\"type\": \"function\", \"function\": {\"name\": function_name, \"description\": desc, \"parameters\": schema}}\n", " )\n", "\n", " return functions\n", "\n", "\n", "functions = openapi_to_functions(openapi_spec)\n", "\n", "for function in functions:\n", " pp(function)\n", " print()\n", "\n"]}, {"attachments": {}, "cell_type": "markdown", "metadata": {"cell_id": "f03f1aacdade4ed9a422797d3cf79fbb", "deepnote_cell_type": "text-cell-h2", "formattedRanges": []}, "source": ["## 如何使用GPT 调用这些函数\n"]}, {"attachments": {}, "cell_type": "markdown", "metadata": {"cell_id": "08712696b5fd4cafac7b4b496ee67c5a", "deepnote_cell_type": "text-cell-p", "formattedRanges": []}, "source": ["现在我们有了这些函数定义,我们可以利用GPT根据用户输入智能地调用它们。\n", "\n", "需要注意的是,聊天完成API不会执行函数;相反,它会生成JSON,您可以在自己的代码中使用该JSON来调用函数。\n", "\n", "有关调用函数的更多信息,请参考我们专门的[调用函数指南](./How_to_call_functions_with_chat_models.ipynb)。\n"]}, {"cell_type": "code", "execution_count": 23, "metadata": {"cell_id": "b8f7d0f157264694b958008f93aabf3f", "deepnote_cell_type": "code", "deepnote_to_be_reexecuted": false, "execution_millis": 6442, "execution_start": 1697419907347, "source_hash": "ac9ad493"}, "outputs": [{"name": "stdout", "output_type": "stream", "text": ["ChatCompletionMessage(content=None, role='assistant', function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_jmlvEyMRMvOtB80adX9RbqIV', function=Function(arguments='{}', name='listEvents'), type='function')])\n", "\n", ">> Function call #: 1\n", "\n", "[ChatCompletionMessageToolCall(id='call_jmlvEyMRMvOtB80adX9RbqIV', function=Function(arguments='{}', name='listEvents'), type='function')]\n", "ChatCompletionMessage(content=None, role='assistant', function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_OOPOY7IHMq3T7Ib71JozlUQJ', function=Function(arguments='{\\n \"requestBody\": {\\n \"id\": \"1234\",\\n \"name\": \"AGI Party\",\\n \"date\": \"2022-12-31\",\\n \"location\": \"New York\"\\n }\\n}', name='createEvent'), type='function')])\n", "\n", ">> Function call #: 2\n", "\n", "[ChatCompletionMessageToolCall(id='call_OOPOY7IHMq3T7Ib71JozlUQJ', function=Function(arguments='{\\n \"requestBody\": {\\n \"id\": \"1234\",\\n \"name\": \"AGI Party\",\\n \"date\": \"2022-12-31\",\\n \"location\": \"New York\"\\n }\\n}', name='createEvent'), type='function')]\n", "ChatCompletionMessage(content=None, role='assistant', function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_Kxluu3fJSOsZNNCn3JIlWAAM', function=Function(arguments='{\\n \"parameters\": {\\n \"id\": \"2456\"\\n }\\n}', name='deleteEvent'), type='function')])\n", "\n", ">> Function call #: 3\n", "\n", "[ChatCompletionMessageToolCall(id='call_Kxluu3fJSOsZNNCn3JIlWAAM', function=Function(arguments='{\\n \"parameters\": {\\n \"id\": \"2456\"\\n }\\n}', name='deleteEvent'), type='function')]\n", "ChatCompletionMessage(content='Here are the actions I performed:\\n\\n1. Retrieved all the events.\\n2. Created a new event named \"AGI Party\" with the ID \"1234\", scheduled for December 31, 2022, in New York.\\n3. Deleted the event with the ID \"2456\".', role='assistant', function_call=None, tool_calls=None)\n", "\n", ">> Function call #: 4\n", "\n", "None\n", "\n", ">> Message:\n", "\n", "Here are the actions I performed:\n", "\n", "1. Retrieved all the events.\n", "2. Created a new event named \"AGI Party\" with the ID \"1234\", scheduled for December 31, 2022, in New York.\n", "3. Deleted the event with the ID \"2456\".\n"]}], "source": ["SYSTEM_MESSAGE = \"\"\"\n", "您是一个有帮助的助手。\n", "请使用function_call来回应以下提示,然后总结行动。\n", "如果用户请求不明确,请请求澄清。\n", "\"\"\"\n", "\n", "# 防止无限或长时间循环的最大允许函数调用次数\n", "MAX_CALLS = 5\n", "\n", "\n", "def get_openai_response(functions, messages):\n", " return client.chat.completions.create(\n", " model=\"gpt-3.5-turbo-16k\",\n", " tools=functions,\n", " tool_choice=\"auto\", # \"auto\" means the model can pick between generating a message or calling a function.\n", " temperature=0,\n", " messages=messages,\n", " )\n", "\n", "\n", "def process_user_instruction(functions, instruction):\n", " num_calls = 0\n", " messages = [\n", " {\"content\": SYSTEM_MESSAGE, \"role\": \"system\"},\n", " {\"content\": instruction, \"role\": \"user\"},\n", " ]\n", "\n", " while num_calls < MAX_CALLS:\n", " response = get_openai_response(functions, messages)\n", " message = response.choices[0].message\n", " print(message)\n", " try:\n", " print(f\"\\n>> Function call #: {num_calls + 1}\\n\")\n", " pp(message.tool_calls)\n", " messages.append(message)\n", "\n", " # For the sake of this example, we'll simply add a message to simulate success.\n", " # Normally, you'd want to call the function here, and append the results to messages.\n", " messages.append(\n", " {\n", " \"role\": \"tool\",\n", " \"content\": \"success\",\n", " \"tool_call_id\": message.tool_calls[0].id,\n", " }\n", " )\n", "\n", " num_calls += 1\n", " except:\n", " print(\"\\n>> Message:\\n\")\n", " print(message.content)\n", " break\n", "\n", " if num_calls >= MAX_CALLS:\n", " print(f\"Reached max chained function calls: {MAX_CALLS}\")\n", "\n", "\n", "USER_INSTRUCTION = \"\"\"\n", "指令:获取所有事件。\n", "然后创建一个名为AGI Party的新事件。\n", "然后删除ID为2456的事件。\n", "\"\"\"\n", "\n", "process_user_instruction(functions, USER_INSTRUCTION)\n", "\n"]}, {"attachments": {}, "cell_type": "markdown", "metadata": {}, "source": ["### 结论\n", "\n", "我们已经演示了如何将OpenAPI规范转换为函数规范,以便将其提供给GPT以便智能地调用它们,并展示了如何将它们链接在一起以执行复杂操作。\n", "\n", "该系统的可能扩展包括处理需要条件逻辑或循环的更复杂用户指令,与真实API集成以执行实际操作,以及改进错误处理和验证以确保指令可行且函数调用成功。\n"]}], "metadata": {"deepnote": {}, "deepnote_execution_queue": [], "deepnote_notebook_id": "84d101406ec34e36a9cf96d0c0c25a7d", "kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, "language_info": {"codemirror_mode": {"name": "ipython", "version": 3}, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.5"}, "orig_nbformat": 2}, "nbformat": 4, "nbformat_minor": 0}