2023年10月15日

使用OpenAPI规范进行函数调用

,

互联网的大部分功能都由RESTful API驱动。赋予GPT调用这些API的能力将开启无限可能。本笔记本展示了如何利用GPT智能调用API,它结合了OpenAPI规范与链式函数调用功能。

OpenAPI Specification (OAS) 是一个被广泛接受的标准,用于以机器可读和解释的格式描述RESTful API的细节。它使人类和计算机都能理解服务的能力,并可用于向GPT展示如何调用API。

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

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

我们建议在继续之前先熟悉function-calling

!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。

如需了解更多关于函数调用的信息,请参阅我们的专用函数调用指南

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集成以执行实际操作,以及改进错误处理和验证以确保指令可行且函数调用成功。