{"cells":[{"attachments":{},"cell_type":"markdown","metadata":{"cell_id":"8317718bcd8b475e9b336514241d679e","deepnote_cell_type":"text-cell-h1","formattedRanges":[]},"source":["# Function-calling with an OpenAPI specification\n"]},{"attachments":{},"cell_type":"markdown","metadata":{"cell_id":"6f810c4e582c41a1bd3bdfc4263dee89","deepnote_cell_type":"text-cell-p","formattedRanges":[]},"source":["Much of the internet is powered by RESTful APIs. Giving GPT the ability 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, and it can be leveraged to show GPT how to call APIs.\n","\n","This notebook is divided into two main sections:\n","\n","1. How to convert a sample OpenAPI specification into a list of function definitions for the chat completions API.\n","2. How to use the chat completions API to intelligently invoke these functions based on user instructions.\n","\n","We recommend familiariazing yourself with [function-calling](./How_to_call_functions_with_chat_models.ipynb) before proceding.\n"]},{"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 # for resolving $ref's in the OpenAPI spec\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":["## How to convert an OpenAPI specification into function definitions\n"]},{"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`. We will transform this sample 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","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 events\n","- Creating a new event\n","- Retrieving an event by ID\n","- Deleting an event by ID\n","- Updating an event name by ID\n","\n","Each operation in the spec has an `operationId`, 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.\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': 'Event Management API',\n"," 'description': 'An API for managing event data'},\n"," 'paths': {'/events': {'get': {'summary': 'List all events',\n"," 'operationId': 'listEvents',\n"," 'responses': {'200': {'description': 'A list of events',\n"," 'content': {'application/json': {'schema': {'type': 'array',\n"," 'items': {'type': 'object',\n"," 'properties': {'id': {'type': 'string'},\n"," 'name': {'type': 'string'},\n"," 'date': {'type': 'string', 'format': 'date-time'},\n"," 'location': {'type': 'string'}},\n"," 'required': ['name', 'date', 'location']}}}}}}},\n"," 'post': {'summary': 'Create a new event',\n"," 'operationId': 'createEvent',\n"," 'requestBody': {'required': True,\n"," 'content': {'application/json': {'schema': {'type': 'object',\n"," 'properties': {'id': {'type': 'string'},\n"," 'name': {'type': 'string'},\n"," 'date': {'type': 'string', 'format': 'date-time'},\n"," 'location': {'type': 'string'}},\n"," 'required': ['name', 'date', 'location']}}}},\n"," 'responses': {'201': {'description': 'The event was created',\n"," 'content': {'application/json': {'schema': {'type': 'object',\n"," 'properties': {'id': {'type': 'string'},\n"," 'name': {'type': 'string'},\n"," 'date': {'type': 'string', 'format': 'date-time'},\n"," 'location': {'type': 'string'}},\n"," 'required': ['name', 'date', 'location']}}}}}}},\n"," '/events/{id}': {'get': {'summary': 'Retrieve an event by ID',\n"," 'operationId': 'getEventById',\n"," 'parameters': [{'name': 'id',\n"," 'in': 'path',\n"," 'required': True,\n"," 'schema': {'type': 'string'}}],\n"," 'responses': {'200': {'description': 'The event',\n"," 'content': {'application/json': {'schema': {'type': 'object',\n"," 'properties': {'id': {'type': 'string'},\n"," 'name': {'type': 'string'},\n"," 'date': {'type': 'string', 'format': 'date-time'},\n"," 'location': {'type': 'string'}},\n"," 'required': ['name', 'date', 'location']}}}}}},\n"," 'delete': {'summary': 'Delete an event by ID',\n"," 'operationId': 'deleteEvent',\n"," 'parameters': [{'name': 'id',\n"," 'in': 'path',\n"," 'required': True,\n"," 'schema': {'type': 'string'}}],\n"," 'responses': {'204': {'description': 'The event was deleted'}}},\n"," 'patch': {'summary': \"Update an event's details by ID\",\n"," 'operationId': 'updateEventDetails',\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"," 'date': {'type': 'string', 'format': 'date-time'},\n"," 'location': {'type': 'string'}},\n"," 'required': ['name', 'date', 'location']}}}},\n"," 'responses': {'200': {'description': \"The event's details were updated\",\n"," 'content': {'application/json': {'schema': {'type': 'object',\n"," 'properties': {'id': {'type': 'string'},\n"," 'name': {'type': 'string'},\n"," 'date': {'type': 'string', 'format': 'date-time'},\n"," 'location': {'type': 'string'}},\n"," 'required': ['name', 'date', 'location']}}}}}}}},\n"," 'components': {'schemas': {'Event': {'type': 'object',\n"," 'properties': {'id': {'type': 'string'},\n"," 'name': {'type': 'string'},\n"," 'date': {'type': 'string', 'format': 'date-time'},\n"," 'location': {'type': 'string'}},\n"," 'required': ['name', 'date', 'location']}}}}"]},"metadata":{},"output_type":"display_data"}],"source":["with open('./data/example_events_openapi.json', 'r') as f:\n"," openapi_spec = jsonref.loads(f.read()) # it's important to load with jsonref, as explained below\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","We can write a simple `openapi_to_functions` function to generate a list of definitions, where each function is represented as a dictionary containing the following keys:\n","\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","For each of the endpoints defined in the schema, we need to do the following:\n","\n","1. **Resolve JSON references**: In an OpenAPI specification, it's common to use JSON references (also known as $ref) to avoid duplication. These references point to definitions that are used in multiple places. For example, if multiple API endpoints return the same object structure, that structure can be defined once and then referenced wherever it's needed. We need to resolve and replace these references with the content they point to.\n","\n","2. **Extract a name for the functions:** We will simply use the operationId as the function name. Alternatively, we could use the endpoint path and operation as the function name.\n","\n","3. **Extract a description and parameters:** We will iterate through the `description`, `summary`, `requestBody` and `parameters` fields to populate the function's description and parameters.\n","\n","Here's the implementation:\n"]},{"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': 'listEvents',\n"," 'description': 'List all events',\n"," 'parameters': {'type': 'object', 'properties': {}}}\n","\n","{'name': 'createEvent',\n"," 'description': 'Create a new event',\n"," 'parameters': {'type': 'object',\n"," 'properties': {'requestBody': {'type': 'object',\n"," 'properties': {'id': {'type': 'string'},\n"," 'name': {'type': 'string'},\n"," 'date': {'type': 'string',\n"," 'format': 'date-time'},\n"," 'location': {'type': 'string'}},\n"," 'required': ['name',\n"," 'date',\n"," 'location']}}}}\n","\n","{'name': 'getEventById',\n"," 'description': 'Retrieve an event by ID',\n"," 'parameters': {'type': 'object',\n"," 'properties': {'parameters': {'type': 'object',\n"," 'properties': {'id': {'type': 'string'}}}}}}\n","\n","{'name': 'deleteEvent',\n"," 'description': 'Delete an event by ID',\n"," 'parameters': {'type': 'object',\n"," 'properties': {'parameters': {'type': 'object',\n"," 'properties': {'id': {'type': 'string'}}}}}}\n","\n","{'name': 'updateEventDetails',\n"," 'description': \"Update an event's details by ID\",\n"," 'parameters': {'type': 'object',\n"," 'properties': {'requestBody': {'type': 'object',\n"," 'properties': {'name': {'type': 'string'},\n"," 'date': {'type': 'string',\n"," 'format': 'date-time'},\n"," 'location': {'type': 'string'}},\n"," 'required': ['name',\n"," 'date',\n"," 'location']},\n"," 'parameters': {'type': 'object',\n"," 'properties': {'id': {'type': 'string'}}}}}}\n","\n"]}],"source":["def openapi_to_functions(openapi_spec):\n"," functions = []\n","\n"," for path, methods in openapi_spec[\"paths\"].items():\n"," for method, spec_with_ref in methods.items():\n"," # 1. Resolve JSON references.\n"," spec = jsonref.replace_refs(spec_with_ref)\n","\n"," # 2. Extract a name for the functions.\n"," function_name = spec.get(\"operationId\")\n","\n"," # 3. Extract a description and parameters.\n"," desc = spec.get(\"description\") or spec.get(\"summary\", \"\")\n","\n"," schema = {\"type\": \"object\", \"properties\": {}}\n","\n"," req_body = (\n"," spec.get(\"requestBody\", {})\n"," .get(\"content\", {})\n"," .get(\"application/json\", {})\n"," .get(\"schema\")\n"," )\n"," if req_body:\n"," schema[\"properties\"][\"requestBody\"] = req_body\n","\n"," params = spec.get(\"parameters\", [])\n"," if params:\n"," param_properties = {\n"," param[\"name\"]: param[\"schema\"]\n"," for param in params\n"," if \"schema\" in param\n"," }\n"," schema[\"properties\"][\"parameters\"] = {\n"," \"type\": \"object\",\n"," \"properties\": param_properties,\n"," }\n","\n"," functions.append(\n"," {\"name\": function_name, \"description\": desc, \"parameters\": schema}\n"," )\n","\n"," return functions\n","\n","\n","functions = openapi_to_functions(openapi_spec)\n","\n","for function in functions:\n"," pp(function)\n"," print()"]},{"attachments":{},"cell_type":"markdown","metadata":{"cell_id":"f03f1aacdade4ed9a422797d3cf79fbb","deepnote_cell_type":"text-cell-h2","formattedRanges":[]},"source":["## How to call these functions with GPT\n"]},{"attachments":{},"cell_type":"markdown","metadata":{"cell_id":"08712696b5fd4cafac7b4b496ee67c5a","deepnote_cell_type":"text-cell-p","formattedRanges":[]},"source":["Now that we have these function definitions, we can leverage GPT to call them intelligently based on user inputs.\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.\n","\n","For more information on function-calling, refer to our dedicated [function-calling guide](./How_to_call_functions_with_chat_models.ipynb).\n"]},{"cell_type":"code","execution_count":14,"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","\n","{'name': 'listEvents',\n"," 'arguments': '{}'}\n","\n"," >> Function call #: 2\n","\n","{'name': 'createEvent',\n"," 'arguments': '{\\n'\n"," ' \"requestBody\": {\\n'\n"," ' \"id\": \"1234\",\\n'\n"," ' \"name\": \"AGI Party\",\\n'\n"," ' \"date\": \"2022-12-31\",\\n'\n"," ' \"location\": \"New York\"\\n'\n"," ' }\\n'\n"," '}'}\n","\n"," >> Function call #: 3\n","\n","{'name': 'deleteEvent',\n"," 'arguments': '{\\n \"parameters\": {\\n \"id\": \"2456\"\\n }\\n}'}\n","\n",">> Message:\n","\n","Actions summary:\n","1. Retrieved all the events successfully.\n","2. Created a new event named \"AGI Party\" with ID 1234, scheduled for December 31, 2022, in New York.\n","3. Deleted the event with ID 2456.\n"]}],"source":["SYSTEM_MESSAGE = \"\"\"\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","# Maximum number of function calls allowed to prevent infinite or lengthy loops\n","MAX_CALLS = 5\n","\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","\n","def process_user_instruction(functions, instruction):\n"," num_calls = 0\n"," messages = [\n"," {\"content\": SYSTEM_MESSAGE, \"role\": \"system\"},\n"," {\"content\": instruction, \"role\": \"user\"},\n"," ]\n","\n"," while num_calls < MAX_CALLS:\n"," response = get_openai_response(functions, messages)\n"," message = response[\"choices\"][0][\"message\"]\n","\n"," if message.get(\"function_call\"):\n"," print(f\"\\n>> Function call #: {num_calls + 1}\\n\")\n"," pp(message[\"function_call\"])\n"," messages.append(message)\n","\n"," # For the sake of this example, we'll simply add a message to simulate success.\n"," # Normally, you'd want to call the function here, and append the results to messages.\n"," messages.append(\n"," {\n"," \"role\": \"function\",\n"," \"content\": \"success\",\n"," \"name\": message[\"function_call\"][\"name\"],\n"," }\n"," )\n","\n"," num_calls += 1\n"," else:\n"," print(\"\\n>> Message:\\n\")\n"," print(message[\"content\"])\n"," break\n","\n"," if num_calls >= MAX_CALLS:\n"," print(f\"Reached max chained function calls: {MAX_CALLS}\")\n","\n","\n","USER_INSTRUCTION = \"\"\"\n","Instruction: Get all the events. \n","Then create a new event named AGI Party.\n","Then delete event with id 2456.\n","\"\"\"\n","\n","process_user_instruction(functions, USER_INSTRUCTION)"]},{"attachments":{},"cell_type":"markdown","metadata":{},"source":["### 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.\n"]}],"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}