Function-calling with an OpenAPI specification

互联网的很大一部分由 RESTful API 提供支持。让 GPT 能够调用它们,可以开启无限的可能性。本笔记本演示了如何智能地使用 GPT 调用 API。它利用 OpenAPI 规范和链式函数调用。

OpenAPI 规范 (OAS) 是一个通用的标准,用于以机器可读和可解释的格式描述 RESTful API 的详细信息。它使人类和计算机都能理解服务的各项功能,并可用于向 GPT 展示如何调用 API。

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

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

我们建议在继续之前先熟悉函数调用

!pip install -q jsonref # 用于解析 OpenAPI 规范中的 $ref
!pip install -q openai
 [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 [0m [33m
 [0m
 [1m[ [0m [34;49mnotice [0m [1;39;49m] [0m [39;49m A new release of pip is available:  [0m [31;49m23.2.1 [0m [39;49m ->  [0m [32;49m23.3.1 [0m
 [1m[ [0m [34;49mnotice [0m [1;39;49m] [0m [39;49m To update, run:  [0m [32;49mpip install --upgrade pip [0m
 [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 [0m [33m
 [0m
 [1m[ [0m [34;49mnotice [0m [1;39;49m] [0m [39;49m A new release of pip is available:  [0m [31;49m23.2.1 [0m [39;49m ->  [0m [32;49m23.3.1 [0m
 [1m[ [0m [34;49mnotice [0m [1;39;49m] [0m [39;49m To update, run:  [0m [32;49mpip install --upgrade pip [0m
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 规范转换为函数定义

我们这里使用的示例 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()) # 加载 jsonref 很重要,如下文所述

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. 解析 JSON 引用。
            spec = jsonref.replace_refs(spec_with_ref)

            # 2. 提取函数名称。
            function_name = spec.get("operationId")

            # 3. 提取描述和参数。
            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 调用这些函数

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

需要注意的是,聊天补全 API 不会执行函数;相反,它会生成您可以在自己的代码中用于调用函数的 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 可以用来智能调用它们的函数规范,并展示了如何将它们链接在一起以执行复杂的操作。

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