许多互联网服务都是由RESTful API提供支持的。赋予GPT调用这些API的能力打开了无限的可能性。本笔记演示了如何利用GPT智能调用API。它利用了OpenAPI规范和链式函数调用。
OpenAPI规范(OAS)是一种被普遍接受的标准,用于以机器可读和解释的格式描述RESTful API的细节。它使人类和计算机都能理解一个服务的能力,并且可以被利用来展示GPT如何调用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规范是使用gpt-4
创建的。我们将把这个示例规范转换成一组函数定义,可以提供给聊天完成API。基于提供的用户指令,模型会生成一个包含调用这些函数所需参数的JSON对象。
在我们继续之前,让我们检查一下生成的规范。OpenAPI规范包括关于API端点、它们支持的操作、它们接受的参数、它们可以处理的请求以及它们返回的响应的详细信息。该规范以JSON格式定义。
规范中的端点包括以下操作:
规范中的每个操作都有一个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
:这是定义函数期望的输入参数的模式。它提供有关每个参数的类型、是否为必需或可选以及其他相关详细信息。对于模式中定义的每个端点,我们需要执行以下操作:
解析JSON引用:在OpenAPI规范中,通常使用JSON引用(也称为$ref)来避免重复。这些引用指向在多个位置使用的定义。例如,如果多个API端点返回相同的对象结构,那么可以定义一次该结构,然后在需要时引用它。我们需要解析和替换这些引用为它们指向的内容。
提取函数的名称:我们将简单地使用operationId作为函数名称。或者,我们可以使用端点路径和操作作为函数名称。
提取描述和参数:我们将遍历description
、summary
、requestBody
和parameters
字段,以填充函数的描述和参数。
这里是实现:
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根据用户输入智能地调用它们。
需要注意的是,聊天完成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集成以执行实际操作,以及改进错误处理和验证以确保指令可行且函数调用成功。