-
Notifications
You must be signed in to change notification settings - Fork 9.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Chained functions using OpenAPI spec (#790)
- Loading branch information
1 parent
3feb142
commit ac345dd
Showing
2 changed files
with
10 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
{"cells":[{"attachments":{},"cell_type":"markdown","metadata":{"cell_id":"8317718bcd8b475e9b336514241d679e","deepnote_cell_type":"text-cell-h1","formattedRanges":[]},"source":["# Chained Functions Using OpenAPI Specification"]},{"attachments":{},"cell_type":"markdown","metadata":{"cell_id":"6f810c4e582c41a1bd3bdfc4263dee89","deepnote_cell_type":"text-cell-p","formattedRanges":[]},"source":["The majority of the tools we use over the internet are powered by RESTful APIs. Giving GPT the power to call them opens up a world of possibilities. This notebook demonstrates how GPTs can be used to intelligently call APIs. It leverages OpenAPI specifications and chained function calls. \n","\n","The [OpenAPI Specification (OAS)](https://swagger.io/specification/) is a universally accepted standard for describing the details of RESTful APIs in a format that machines can read and interpret. It enables both humans and computers to understand the capabilities of a service without direct access to the source code.\n","\n","This notebook is divided into two main sections: \n","\n","1. The first section involves converting a sample OpenAPI spec into a list of function definitions along with their expected arguments. This is done by parsing the OpenAPI spec and extracting the necessary information. \n","2. The second section involves taking the list of functions generated in the first step, along with a user instruction, and executing the functions sequentially. This is done by feeding the function definitions and the user instruction to the Chat completion API, which generates the JSON objects for function calls."]},{"cell_type":"code","execution_count":null,"metadata":{"cell_id":"bf983b3e199d4ea6a2718e58a141bd88","deepnote_cell_type":"code","deepnote_to_be_reexecuted":false,"execution_millis":10617,"execution_start":1697419508239,"source_hash":"d6b9a6d3"},"outputs":[],"source":["!pip install -q jsonref\n","!pip install -q openai"]},{"cell_type":"code","execution_count":2,"metadata":{"cell_id":"12fb12583cc74b7c842a1b4656b94f47","deepnote_cell_type":"code","deepnote_to_be_reexecuted":false,"execution_millis":10,"execution_start":1697419706563,"source_hash":"750280cb"},"outputs":[],"source":["import os\n","import json\n","import jsonref\n","import openai\n","import requests\n","from pprint import pp\n","\n","openai.api_key = os.environ[\"OPENAI_API_KEY\"]"]},{"attachments":{},"cell_type":"markdown","metadata":{"cell_id":"76ae868e66b14cc48c9c447302ea268e","deepnote_cell_type":"text-cell-h2","formattedRanges":[]},"source":["## Converting OpenAPI Specifications into OpenAI Functions"]},{"attachments":{},"cell_type":"markdown","metadata":{"cell_id":"9cf167d2d5fe4d7eadb69ba18db4a696","deepnote_cell_type":"text-cell-p","formattedRanges":[]},"source":["The example OpenAPI spec we use here was created using `gpt-4`. Here we will transform the spec into a set of function definitions that can be supplied to the chat completion API. The model, based on the provided user instructions, generates a JSON object containing the necessary arguments to call these functions.\n","\n","Here's a brief overview of the dialogue that led to its creation:\n","\n","```\n","User: Let's generate a fake Swagger openai.json for a \"froge\" character database.\n","Assistant: Provides a draft Swagger specification.\n","User: Use OpenAPI spec and also add endpoints for getting and updating the name for a froge and getting froge by name.\n","Assistant: Updates the specification to OpenAPI 3.0.0, adds endpoints for updating a Froge's name, and getting a Froge by name.\n","User: Could you also add a function name as operationId to it?\n","Assistant: Adds operationId for function names to each endpoint.\n","```\n","\n","Before we proceed, let's inspect this generated spec. OpenAPI specs include details about the API's endpoints, the operations they support, the parameters they accept, the requests they can handle, and the responses they return. The spec is defined in JSON format.\n","\n","The endpoints in the spec include operations for:\n","\n","- listing all \"froge\" characters\n","- creating a new \"froge\" character\n","- retrieving a \"froge\" character by ID\n","- deleting a \"froge\" character by ID\n","- updating a \"froge\" character's name by ID\n","- retrieving a \"froge\" character by name.\n","\n","Each operation in the spec has an operationId, a unique string used to identify an operation, which we will use as the function name when we parse the spec into function specifications. The spec also includes schemas that define the data types and structures of the parameters for each operation. These schemas will be used to validate the input parameters when we call the functions.\n","\n","You can see the schema here:\n"]},{"cell_type":"code","execution_count":3,"metadata":{"cell_id":"176efbfea4b546d28d9d9966342f286a","deepnote_cell_type":"code","deepnote_to_be_reexecuted":false,"execution_millis":231,"execution_start":1697419710160,"source_hash":"1ce3848"},"outputs":[{"data":{"text/plain":["{'openapi': '3.0.0',\n"," 'info': {'version': '1.0.0',\n"," 'title': 'Froge Character API',\n"," 'description': 'An API for managing froge character data'},\n"," 'paths': {'/froge': {'get': {'summary': 'List all froge characters',\n"," 'operationId': 'listFroges',\n"," 'responses': {'200': {'description': 'A list of froge characters',\n"," 'content': {'application/json': {'schema': {'type': 'array',\n"," 'items': {'type': 'object',\n"," 'properties': {'id': {'type': 'string'},\n"," 'name': {'type': 'string'},\n"," 'age': {'type': 'integer'}},\n"," 'required': ['name', 'age']}}}}}}},\n"," 'post': {'summary': 'Create a new froge character',\n"," 'operationId': 'createFroge',\n"," 'requestBody': {'required': True,\n"," 'content': {'application/json': {'schema': {'type': 'object',\n"," 'properties': {'id': {'type': 'string'},\n"," 'name': {'type': 'string'},\n"," 'age': {'type': 'integer'}},\n"," 'required': ['name', 'age']}}}},\n"," 'responses': {'201': {'description': 'The froge character was created',\n"," 'content': {'application/json': {'schema': {'type': 'object',\n"," 'properties': {'id': {'type': 'string'},\n"," 'name': {'type': 'string'},\n"," 'age': {'type': 'integer'}},\n"," 'required': ['name', 'age']}}}}}}},\n"," '/froge/{id}': {'get': {'summary': 'Retrieve a froge character by ID',\n"," 'operationId': 'getFrogeById',\n"," 'parameters': [{'name': 'id',\n"," 'in': 'path',\n"," 'required': True,\n"," 'schema': {'type': 'string'}}],\n"," 'responses': {'200': {'description': 'The froge character',\n"," 'content': {'application/json': {'schema': {'type': 'object',\n"," 'properties': {'id': {'type': 'string'},\n"," 'name': {'type': 'string'},\n"," 'age': {'type': 'integer'}},\n"," 'required': ['name', 'age']}}}}}},\n"," 'delete': {'summary': 'Delete a froge character by ID',\n"," 'operationId': 'deleteFroge',\n"," 'parameters': [{'name': 'id',\n"," 'in': 'path',\n"," 'required': True,\n"," 'schema': {'type': 'string'}}],\n"," 'responses': {'204': {'description': 'The froge character was deleted'}}},\n"," 'patch': {'summary': \"Update a froge character's name by ID\",\n"," 'operationId': 'updateFrogeName',\n"," 'parameters': [{'name': 'id',\n"," 'in': 'path',\n"," 'required': True,\n"," 'schema': {'type': 'string'}}],\n"," 'requestBody': {'required': True,\n"," 'content': {'application/json': {'schema': {'type': 'object',\n"," 'properties': {'name': {'type': 'string'}},\n"," 'required': ['name']}}}},\n"," 'responses': {'200': {'description': \"The froge character's name was updated\",\n"," 'content': {'application/json': {'schema': {'type': 'object',\n"," 'properties': {'id': {'type': 'string'},\n"," 'name': {'type': 'string'},\n"," 'age': {'type': 'integer'}},\n"," 'required': ['name', 'age']}}}}}}},\n"," '/froge/name/{name}': {'get': {'summary': 'Retrieve a froge character by name',\n"," 'operationId': 'getFrogeByName',\n"," 'parameters': [{'name': 'name',\n"," 'in': 'path',\n"," 'required': True,\n"," 'schema': {'type': 'string'}}],\n"," 'responses': {'200': {'description': 'The froge character',\n"," 'content': {'application/json': {'schema': {'type': 'object',\n"," 'properties': {'id': {'type': 'string'},\n"," 'name': {'type': 'string'},\n"," 'age': {'type': 'integer'}},\n"," 'required': ['name', 'age']}}}}}}}},\n"," 'components': {'schemas': {'Froge': {'type': 'object',\n"," 'properties': {'id': {'type': 'string'},\n"," 'name': {'type': 'string'},\n"," 'age': {'type': 'integer'}},\n"," 'required': ['name', 'age']}}}}"]},"metadata":{},"output_type":"display_data"}],"source":["url = \"https://gist.githubusercontent.com/shyamal-anadkat/d44674a87778796222bdb8fa9158ad47/raw/030d173d53c55d806a93976705cf1c5f9e9c5240/frogeapi.json\"\n","openapi_spec = jsonref.loads(requests.get(url).content)\n","\n","display(openapi_spec)"]},{"attachments":{},"cell_type":"markdown","metadata":{"cell_id":"e3e39ad4ac854299bf62b5f7bb1bef45","deepnote_cell_type":"text-cell-p","formattedRanges":[]},"source":["Now that we have a good understanding of the OpenAPI spec, we can proceed to parse it into function specifications.\n","\n","The main objective of `parse_functions` is to generate a list of functions, where each function is represented as a dictionary containing the following keys:\n","- 'name': This corresponds to the operation identifier of the API endpoint as defined in the OpenAPI specification.\n","- 'description': This is a brief description or summary of the function, providing an overview of what the function does.\n","- 'parameters': This is a schema that defines the expected input parameters for the function. It provides information about the type of each parameter, whether it is required or optional, and other related details.\n","\n","The output of this function is a list of such dictionaries, each representing a function defined in the OpenAPI specification."]},{"cell_type":"code","execution_count":4,"metadata":{"cell_id":"adbb17ca8a2a4fa2aa3f0213a0e211b6","deepnote_cell_type":"code","deepnote_to_be_reexecuted":false,"execution_millis":333,"execution_start":1697419853135,"source_hash":"ad112cb1"},"outputs":[{"name":"stdout","output_type":"stream","text":["{'name': 'listFroges',\n"," 'description': 'List all froge characters',\n"," 'parameters': {'type': 'object', 'properties': {}}}\n","\n","{'name': 'createFroge',\n"," 'description': 'Create a new froge character',\n"," 'parameters': {'type': 'object',\n"," 'properties': {'requestBody': {'type': 'object',\n"," 'properties': {'id': {'type': 'string'},\n"," 'name': {'type': 'string'},\n"," 'age': {'type': 'integer'}},\n"," 'required': ['name', 'age']}}}}\n","\n","{'name': 'getFrogeById',\n"," 'description': 'Retrieve a froge character by ID',\n"," 'parameters': {'type': 'object',\n"," 'properties': {'parameters': {'type': 'object',\n"," 'properties': {'id': {'type': 'string'}}}}}}\n","\n","{'name': 'deleteFroge',\n"," 'description': 'Delete a froge character by ID',\n"," 'parameters': {'type': 'object',\n"," 'properties': {'parameters': {'type': 'object',\n"," 'properties': {'id': {'type': 'string'}}}}}}\n","\n","{'name': 'updateFrogeName',\n"," 'description': \"Update a froge character's name by ID\",\n"," 'parameters': {'type': 'object',\n"," 'properties': {'requestBody': {'type': 'object',\n"," 'properties': {'name': {'type': 'string'}},\n"," 'required': ['name']},\n"," 'parameters': {'type': 'object',\n"," 'properties': {'id': {'type': 'string'}}}}}}\n","\n","{'name': 'getFrogeByName',\n"," 'description': 'Retrieve a froge character by name',\n"," 'parameters': {'type': 'object',\n"," 'properties': {'parameters': {'type': 'object',\n"," 'properties': {'name': {'type': 'string'}}}}}}\n","\n"]}],"source":["def parse_functions(openapi_spec): \n","\n"," # Initialize an empty list to store the function specifications\n"," functions = []\n","\n"," # Iterate through the paths and methods specified in the OpenAPI spec\n"," for path, methods in openapi_spec[\"paths\"].items():\n"," for method, spec_with_ref in methods.items():\n"," # Replace any JSON references in the spec\n"," spec = jsonref.replace_refs(spec_with_ref)\n","\n"," # Extract the operationId which will be used as the function name\n"," function_name = spec.get(\"operationId\")\n"," \n"," # Gather the description, request body and parameters from the spec\n"," desc = spec.get(\"description\") or spec.get(\"summary\", \"\")\n"," req_body = spec.get(\"requestBody\", {}).get(\"content\", {}).get(\"application/json\", {}).get(\"schema\")\n"," params = spec.get(\"parameters\", [])\n"," \n"," # Initialize an empty schema\n"," schema = {\"type\": \"object\", \"properties\": {}}\n"," \n"," # If a request body is defined, add it to the schema\n"," if req_body:\n"," schema[\"properties\"][\"requestBody\"] = req_body\n"," \n"," # If parameters are defined, add them to the schema\n"," if params:\n"," param_properties = {param[\"name\"]: param[\"schema\"] for param in params if \"schema\" in param}\n"," schema[\"properties\"][\"parameters\"] = {\"type\": \"object\", \"properties\": param_properties}\n"," \n"," # Append the function specification to the list of functions\n"," functions.append({\"name\": function_name, \"description\": desc, \"parameters\": schema})\n","\n"," # Return the list of function specifications\n"," return functions\n","\n","# Parse the OpenAPI spec to get the function specifications\n","functions = parse_functions(openapi_spec)\n","\n","\n","for function in functions:\n"," pp(function)\n"," print()\n"]},{"attachments":{},"cell_type":"markdown","metadata":{"cell_id":"f03f1aacdade4ed9a422797d3cf79fbb","deepnote_cell_type":"text-cell-h2","formattedRanges":[]},"source":["## Orchestrating Sequential Function Calls\n"]},{"attachments":{},"cell_type":"markdown","metadata":{"cell_id":"08712696b5fd4cafac7b4b496ee67c5a","deepnote_cell_type":"text-cell-p","formattedRanges":[]},"source":["Now that we have these function definitions that we have derived from the OpenAPI spec, we can leverage them to orchestrate a series of calls to the `gpt-3.5-turbo-16k-0613` model. \n","\n","The model will determine the sequence of functions to call based on the user's input and the available function specifications. \n","\n","It's important to note that the chat completions API does not execute the function; instead, it generates the JSON that you can use to call the function in your own code."]},{"cell_type":"code","execution_count":5,"metadata":{"cell_id":"b8f7d0f157264694b958008f93aabf3f","deepnote_cell_type":"code","deepnote_to_be_reexecuted":false,"execution_millis":6442,"execution_start":1697419907347,"source_hash":"ac9ad493"},"outputs":[{"name":"stdout","output_type":"stream","text":["\n","Function call #: 1\n","{'name': 'listFroges',\n"," 'arguments': '{}'}\n","\n","Function call #: 2\n","{'name': 'createFroge',\n"," 'arguments': '{\\n'\n"," ' \"requestBody\": {\\n'\n"," ' \"id\": \"1234\",\\n'\n"," ' \"name\": \"dalle3\",\\n'\n"," ' \"age\": 2\\n'\n"," ' }\\n'\n"," '}'}\n","\n","Function call #: 3\n","{'name': 'deleteFroge',\n"," 'arguments': '{\\n \"parameters\": {\\n \"id\": \"2456\"\\n }\\n}'}\n","\n","Message:\n","Actions:\n","1. Retrieved all the froges.\n","2. Created a new froge named \"dalle3\" with an age of 2 and a random numerical id.\n","3. Deleted the froge with id 2456.\n"]}],"source":["SYSTEM_PROMPT = \"\"\"\n","You are a helpful assistant. \n","Respond to the following prompt by using function_call and then summarize actions. \n","Ask for clarification if a user request is ambiguous.\n","\"\"\"\n","\n","USER_PROMPT = \"\"\"\n","Instruction: Get all the froges. \n","Then create a new 2-year old froge named dalle3, with a random numerical id. \n","Then delete froge with id 2456.\n","\"\"\"\n","\n","messages = [\n"," {\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n"," {\"role\": \"user\", \"content\": USER_PROMPT}\n","]\n","\n","# Maximum number of chained calls allowed to prevent infinite or lengthy loops\n","MAX_CHAINED_CALLS = 5\n","\n","def get_openai_response(functions, messages):\n"," return openai.ChatCompletion.create(\n"," model='gpt-3.5-turbo-16k-0613',\n"," functions=functions,\n"," function_call=\"auto\", # \"auto\" means the model can pick between generating a message or calling a function.\n"," temperature=0,\n"," messages=messages\n"," )\n","\n","def process_chained_calls(functions, messages):\n"," stack = 0\n"," while stack < MAX_CHAINED_CALLS:\n"," response = get_openai_response(functions, messages)\n"," message = response[\"choices\"][0][\"message\"]\n"," \n"," if message.get(\"function_call\"):\n"," print(f\"\\nFunction call #: {stack + 1}\")\n"," pp(message[\"function_call\"])\n"," messages.append(message)\n"," stack += 1\n"," else:\n"," print(\"\\nMessage:\")\n"," print(message[\"content\"])\n"," break\n"," \n"," if stack >= MAX_CHAINED_CALLS:\n"," print(f\"Reached max chained function calls: {MAX_CHAINED_CALLS}\")\n","\n","process_chained_calls(functions, messages)"]},{"attachments":{},"cell_type":"markdown","metadata":{},"source":["\n","### Conclusion\n","\n","We have demonstrated how to convert OpenAPI specs into function specifications that can be given to GPT for it to intelligently call them, and shown how these can be chained together to perform complex operations.\n","\n","Possible extensions of this system could include handling more complex user instructions that require conditional logic or looping, integrating with real APIs to perform actual operations, and improving error handling and validation to ensure the instructions are feasible and the function calls are successful."]}],"metadata":{"deepnote":{},"deepnote_execution_queue":[],"deepnote_notebook_id":"84d101406ec34e36a9cf96d0c0c25a7d","kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.9.13"},"orig_nbformat":2},"nbformat":4,"nbformat_minor":0} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters