跳到主要内容

使用OpenAPI规范调用函数

nbviewer

许多互联网服务都是由RESTful API提供支持的。赋予GPT调用这些API的能力打开了无限的可能性。本笔记演示了如何利用GPT智能调用API。它利用了OpenAPI规范和链式函数调用。

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

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

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

我们建议在继续之前先熟悉如何使用聊天模型调用函数

!pip install -q jsonref # 用于解析OpenAPI规范中的$ref引用
!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规范转换为函数定义

我们在这里使用的示例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,您可以在自己的代码中使用该JSON来调用函数。

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

SYSTEM_MESSAGE = """
您是一个有帮助的助手。
请使用function_call来回应以下提示,然后总结行动。
如果用户请求不明确,请请求澄清。
"""

# 防止无限或长时间循环的最大允许函数调用次数
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 = """
指令:获取所有事件。
然后创建一个名为AGI Party的新事件。
然后删除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集成以执行实际操作,以及改进错误处理和验证以确保指令可行且函数调用成功。