使用 OpenAPI 规范的函数调用

,
2023 年 10 月 15 日
在 Github 上打开

互联网的很大一部分是由 RESTful API 驱动的。赋予 GPT 调用它们的能力开启了无限可能。本笔记本演示了如何使用 GPT 智能地调用 API。它利用 OpenAPI 规范和链式函数调用。

OpenAPI 规范 (OAS) 是描述 RESTful API 细节的通用标准,其格式可供机器读取和解释。它使人类和计算机都能理解服务的功能,并且可以用来向 GPT 展示如何调用 API。

本笔记本分为两个主要部分

  1. 如何将示例 OpenAPI 规范转换为聊天完成 API 的函数定义列表。
  2. 如何使用聊天完成 API 根据用户指令智能地调用这些函数。

我们建议您先熟悉 函数调用,然后再继续。

!pip install -q jsonref # for resolving $ref's in the OpenAPI spec
!pip install -q openai
DEPRECATION: 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

[notice] A new release of pip is available: 23.2.1 -> 23.3.1
[notice] To update, run: pip install --upgrade pip
DEPRECATION: 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

[notice] A new release of pip is available: 23.2.1 -> 23.3.1
[notice] To update, run: pip install --upgrade pip
import os
import json
import jsonref
from openai import OpenAI
import requests
from pprint import pp

client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY", "<your OpenAI API key if not set as env var>"))

我们在此处使用的示例 OpenAPI 规范是使用 gpt-4 创建的。我们将把这个示例规范转换为一组函数定义,这些定义可以提供给聊天完成 API。该模型根据提供的用户指令生成一个 JSON 对象,其中包含调用这些函数所需的参数。

在我们继续之前,让我们检查一下这个生成的规范。OpenAPI 规范包括有关 API 端点、它们支持的操作、它们接受的参数、它们可以处理的请求以及它们返回的响应的详细信息。该规范以 JSON 格式定义。

规范中的端点包括以下操作:

  • 列出所有事件
  • 创建新事件
  • 按 ID 检索事件
  • 按 ID 删除事件
  • 按 ID 更新事件名称

规范中的每个操作都有一个 operationId,当我们将规范解析为函数规范时,我们将使用它作为函数名。该规范还包括定义每个操作参数的数据类型和结构的模式。

您可以在这里查看模式

with open('./data/example_events_openapi.json', 'r') as f:
    openapi_spec = jsonref.loads(f.read()) # it's important to load with jsonref, as explained below

display(openapi_spec)
{'openapi': '3.0.0',
 'info': {'version': '1.0.0',
  'title': 'Event Management API',
  'description': 'An API for managing event data'},
 'paths': {'/events': {'get': {'summary': 'List all events',
    'operationId': 'listEvents',
    'responses': {'200': {'description': 'A list of events',
      'content': {'application/json': {'schema': {'type': 'array',
         'items': {'type': 'object',
          'properties': {'id': {'type': 'string'},
           'name': {'type': 'string'},
           'date': {'type': 'string', 'format': 'date-time'},
           'location': {'type': 'string'}},
          'required': ['name', 'date', 'location']}}}}}}},
   'post': {'summary': 'Create a new event',
    'operationId': 'createEvent',
    'requestBody': {'required': True,
     'content': {'application/json': {'schema': {'type': 'object',
        'properties': {'id': {'type': 'string'},
         'name': {'type': 'string'},
         'date': {'type': 'string', 'format': 'date-time'},
         'location': {'type': 'string'}},
        'required': ['name', 'date', 'location']}}}},
    'responses': {'201': {'description': 'The event was created',
      'content': {'application/json': {'schema': {'type': 'object',
         'properties': {'id': {'type': 'string'},
          'name': {'type': 'string'},
          'date': {'type': 'string', 'format': 'date-time'},
          'location': {'type': 'string'}},
         'required': ['name', 'date', 'location']}}}}}}},
  '/events/{id}': {'get': {'summary': 'Retrieve an event by ID',
    'operationId': 'getEventById',
    'parameters': [{'name': 'id',
      'in': 'path',
      'required': True,
      'schema': {'type': 'string'}}],
    'responses': {'200': {'description': 'The event',
      'content': {'application/json': {'schema': {'type': 'object',
         'properties': {'id': {'type': 'string'},
          'name': {'type': 'string'},
          'date': {'type': 'string', 'format': 'date-time'},
          'location': {'type': 'string'}},
         'required': ['name', 'date', 'location']}}}}}},
   'delete': {'summary': 'Delete an event by ID',
    'operationId': 'deleteEvent',
    'parameters': [{'name': 'id',
      'in': 'path',
      'required': True,
      'schema': {'type': 'string'}}],
    'responses': {'204': {'description': 'The event was deleted'}}},
   'patch': {'summary': "Update an event's details by ID",
    'operationId': 'updateEventDetails',
    'parameters': [{'name': 'id',
      'in': 'path',
      'required': True,
      'schema': {'type': 'string'}}],
    'requestBody': {'required': True,
     'content': {'application/json': {'schema': {'type': 'object',
        'properties': {'name': {'type': 'string'},
         'date': {'type': 'string', 'format': 'date-time'},
         'location': {'type': 'string'}},
        'required': ['name', 'date', 'location']}}}},
    'responses': {'200': {'description': "The event's details were updated",
      'content': {'application/json': {'schema': {'type': 'object',
         'properties': {'id': {'type': 'string'},
          'name': {'type': 'string'},
          'date': {'type': 'string', 'format': 'date-time'},
          'location': {'type': 'string'}},
         'required': ['name', 'date', 'location']}}}}}}}},
 'components': {'schemas': {'Event': {'type': 'object',
    'properties': {'id': {'type': 'string'},
     'name': {'type': 'string'},
     'date': {'type': 'string', 'format': 'date-time'},
     'location': {'type': 'string'}},
    'required': ['name', 'date', 'location']}}}}

现在我们对 OpenAPI 规范有了很好的理解,我们可以继续将其解析为函数规范。

我们可以编写一个简单的 openapi_to_functions 函数来生成定义列表,其中每个函数都表示为一个包含以下键的字典

  • name:这对应于 OpenAPI 规范中定义的 API 端点的操作标识符。
  • description:这是函数的简要描述或摘要,概述了函数的功能。
  • parameters:这是一个模式,定义了函数的预期输入参数。它提供了有关每个参数类型、它是必需还是可选以及其他相关详细信息的信息。

对于模式中定义的每个端点,我们需要执行以下操作

  1. 解析 JSON 引用:在 OpenAPI 规范中,通常使用 JSON 引用(也称为 $ref)来避免重复。这些引用指向在多个位置使用的定义。例如,如果多个 API 端点返回相同的对象结构,则可以定义一次该结构,然后在需要它的任何地方引用它。我们需要解析这些引用,并用它们指向的内容替换它们。

  2. 提取函数名称: 我们将简单地使用 operationId 作为函数名。或者,我们可以使用端点路径和操作作为函数名。

  3. 提取描述和参数: 我们将遍历 descriptionsummaryrequestBodyparameters 字段来填充函数的描述和参数。

这是实现

def openapi_to_functions(openapi_spec):
    functions = []

    for path, methods in openapi_spec["paths"].items():
        for method, spec_with_ref in methods.items():
            # 1. Resolve JSON references.
            spec = jsonref.replace_refs(spec_with_ref)

            # 2. Extract a name for the functions.
            function_name = spec.get("operationId")

            # 3. Extract a description and parameters.
            desc = spec.get("description") or spec.get("summary", "")

            schema = {"type": "object", "properties": {}}

            req_body = (
                spec.get("requestBody", {})
                .get("content", {})
                .get("application/json", {})
                .get("schema")
            )
            if req_body:
                schema["properties"]["requestBody"] = req_body

            params = spec.get("parameters", [])
            if params:
                param_properties = {
                    param["name"]: param["schema"]
                    for param in params
                    if "schema" in param
                }
                schema["properties"]["parameters"] = {
                    "type": "object",
                    "properties": param_properties,
                }

            functions.append(
                {"type": "function", "function": {"name": function_name, "description": desc, "parameters": schema}}
            )

    return functions


functions = openapi_to_functions(openapi_spec)

for function in functions:
    pp(function)
    print()
{'type': 'function',
 'function': {'name': 'listEvents',
              'description': 'List all events',
              'parameters': {'type': 'object', 'properties': {}}}}

{'type': 'function',
 'function': {'name': 'createEvent',
              'description': 'Create a new event',
              'parameters': {'type': 'object',
                             'properties': {'requestBody': {'type': 'object',
                                                            'properties': {'id': {'type': 'string'},
                                                                           'name': {'type': 'string'},
                                                                           'date': {'type': 'string',
                                                                                    'format': 'date-time'},
                                                                           'location': {'type': 'string'}},
                                                            'required': ['name',
                                                                         'date',
                                                                         'location']}}}}}

{'type': 'function',
 'function': {'name': 'getEventById',
              'description': 'Retrieve an event by ID',
              'parameters': {'type': 'object',
                             'properties': {'parameters': {'type': 'object',
                                                           'properties': {'id': {'type': 'string'}}}}}}}

{'type': 'function',
 'function': {'name': 'deleteEvent',
              'description': 'Delete an event by ID',
              'parameters': {'type': 'object',
                             'properties': {'parameters': {'type': 'object',
                                                           'properties': {'id': {'type': 'string'}}}}}}}

{'type': 'function',
 'function': {'name': 'updateEventDetails',
              'description': "Update an event's details by ID",
              'parameters': {'type': 'object',
                             'properties': {'requestBody': {'type': 'object',
                                                            'properties': {'name': {'type': 'string'},
                                                                           'date': {'type': 'string',
                                                                                    'format': 'date-time'},
                                                                           'location': {'type': 'string'}},
                                                            'required': ['name',
                                                                         'date',
                                                                         'location']},
                                            'parameters': {'type': 'object',
                                                           'properties': {'id': {'type': 'string'}}}}}}}

现在我们有了这些函数定义,我们可以利用 GPT 根据用户输入智能地调用它们。

重要的是要注意,聊天完成 API 不会执行函数;相反,它会生成 JSON,您可以使用该 JSON 在自己的代码中调用函数。

有关函数调用的更多信息,请参阅我们的专用 函数调用指南

SYSTEM_MESSAGE = """
You are a helpful assistant.
Respond to the following prompt by using function_call and then summarize actions.
Ask for clarification if a user request is ambiguous.
"""

# Maximum number of function calls allowed to prevent infinite or lengthy loops
MAX_CALLS = 5


def get_openai_response(functions, messages):
    return client.chat.completions.create(
        model="gpt-3.5-turbo-16k",
        tools=functions,
        tool_choice="auto",  # "auto" means the model can pick between generating a message or calling a function.
        temperature=0,
        messages=messages,
    )


def process_user_instruction(functions, instruction):
    num_calls = 0
    messages = [
        {"content": SYSTEM_MESSAGE, "role": "system"},
        {"content": instruction, "role": "user"},
    ]

    while num_calls < MAX_CALLS:
        response = get_openai_response(functions, messages)
        message = response.choices[0].message
        print(message)
        try:
            print(f"\n>> Function call #: {num_calls + 1}\n")
            pp(message.tool_calls)
            messages.append(message)

            # For the sake of this example, we'll simply add a message to simulate success.
            # Normally, you'd want to call the function here, and append the results to messages.
            messages.append(
                {
                    "role": "tool",
                    "content": "success",
                    "tool_call_id": message.tool_calls[0].id,
                }
            )

            num_calls += 1
        except:
            print("\n>> Message:\n")
            print(message.content)
            break

    if num_calls >= MAX_CALLS:
        print(f"Reached max chained function calls: {MAX_CALLS}")


USER_INSTRUCTION = """
Instruction: Get all the events.
Then create a new event named AGI Party.
Then delete event with id 2456.
"""

process_user_instruction(functions, USER_INSTRUCTION)
ChatCompletionMessage(content=None, role='assistant', function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_jmlvEyMRMvOtB80adX9RbqIV', function=Function(arguments='{}', name='listEvents'), type='function')])

>> Function call #: 1

[ChatCompletionMessageToolCall(id='call_jmlvEyMRMvOtB80adX9RbqIV', function=Function(arguments='{}', name='listEvents'), type='function')]
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')])

>> Function call #: 2

[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')]
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')])

>> Function call #: 3

[ChatCompletionMessageToolCall(id='call_Kxluu3fJSOsZNNCn3JIlWAAM', function=Function(arguments='{\n  "parameters": {\n    "id": "2456"\n  }\n}', name='deleteEvent'), type='function')]
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)

>> Function call #: 4

None

>> Message:

Here are the actions I performed:

1. Retrieved all the events.
2. Created a new event named "AGI Party" with the ID "1234", scheduled for December 31, 2022, in New York.
3. Deleted the event with the ID "2456".

结论

我们已经演示了如何将 OpenAPI 规范转换为函数规范,这些规范可以提供给 GPT,以便 GPT 智能地调用它们,并展示了如何将这些规范链接在一起以执行复杂操作。

此系统的可能扩展包括处理需要条件逻辑或循环的更复杂的用户指令,与真实 API 集成以执行实际操作,以及改进错误处理和验证,以确保指令可行且函数调用成功。