Add Request body (#2507)

This still doesn't handle the following

- non-JSON media types
- anyOf, allOf, oneOf's

And doesn't emit the typescript definitions for referred types yet, but
that can be saved for a separate PR.

Also, we could have better support for Swagger 2.0 specs and OpenAPI
3.0.3 (can use the same lib for the latter) recommend offline conversion
for now.
doc
William FH 1 year ago committed by GitHub
parent 13d1df2140
commit f240651bd8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -38,7 +38,15 @@
"execution_count": 2,
"id": "0831271b",
"metadata": {},
"outputs": [],
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"Attempting to load an OpenAPI 3.0.1 spec. This may result in degraded performance. Convert your OpenAPI spec to 3.1.* spec for better support.\n"
]
}
],
"source": [
"spec = OpenAPISpec.from_url(\"https://www.klarna.com/us/shopping/public/openai/v0/api-docs/\")"
]
@ -91,16 +99,32 @@
{
"cell_type": "code",
"execution_count": 5,
"id": "c5f27406",
"id": "788a7cef",
"metadata": {},
"outputs": [],
"source": [
"chain = OpenAPIEndpointChain.from_api_operation(operation, OpenAI(), requests=Requests(), verbose=True)"
"llm = OpenAI() # Load a Language Model"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "c5f27406",
"metadata": {},
"outputs": [],
"source": [
"chain = OpenAPIEndpointChain.from_api_operation(\n",
" operation, \n",
" llm, \n",
" requests=Requests(), \n",
" verbose=True,\n",
" return_intermediate_steps=True # Return request and response text\n",
")"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "23652053",
"metadata": {
"scrolled": false
@ -162,13 +186,15 @@
"\u001b[0m\n",
"\n",
"\u001b[1m> Finished chain.\u001b[0m\n",
"\u001b[32;1m\u001b[1;3m{\"q\": \"shirt\", \"max_price\": null}\u001b[0m\n",
"\u001b[36;1m\u001b[1;3m{\"products\":[{\"name\":\"Burberry Check Poplin Shirt\",\"url\":\"https://www.klarna.com/us/shopping/pl/cl10001/3201810981/Clothing/Burberry-Check-Poplin-Shirt/?utm_source=openai&ref-site=openai_plugin\",\"price\":\"$360.00\",\"attributes\":[\"Material:Cotton\",\"Target Group:Man\",\"Color:Gray,Blue,Beige\",\"Properties:Pockets\",\"Pattern:Checkered\"]},{\"name\":\"Burberry Vintage Check Cotton Shirt - Beige\",\"url\":\"https://www.klarna.com/us/shopping/pl/cl359/3200280807/Children-s-Clothing/Burberry-Vintage-Check-Cotton-Shirt-Beige/?utm_source=openai&ref-site=openai_plugin\",\"price\":\"$196.30\",\"attributes\":[\"Material:Cotton,Elastane\",\"Color:Beige\",\"Model:Boy\",\"Pattern:Checkered\"]},{\"name\":\"Burberry Somerton Check Shirt - Camel\",\"url\":\"https://www.klarna.com/us/shopping/pl/cl10001/3201112728/Clothing/Burberry-Somerton-Check-Shirt-Camel/?utm_source=openai&ref-site=openai_plugin\",\"price\":\"$450.00\",\"attributes\":[\"Material:Elastane/Lycra/Spandex,Cotton\",\"Target Group:Man\",\"Color:Beige\"]},{\"name\":\"Calvin Klein Slim Fit Oxford Dress Shirt\",\"url\":\"https://www.klarna.com/us/shopping/pl/cl10001/3201839169/Clothing/Calvin-Klein-Slim-Fit-Oxford-Dress-Shirt/?utm_source=openai&ref-site=openai_plugin\",\"price\":\"$24.91\",\"attributes\":[\"Material:Cotton\",\"Target Group:Man\",\"Color:Gray,White,Blue,Black\",\"Pattern:Solid Color\"]},{\"name\":\"Magellan Outdoors Laguna Madre Solid Short Sleeve Fishing Shirt\",\"url\":\"https://www.klarna.com/us/shopping/pl/cl10001/3203102142/Clothing/Magellan-Outdoors-Laguna-Madre-Solid-Short-Sleeve-Fishing-Shirt/?utm_source=openai&ref-site=openai_plugin\",\"price\":\"$19.99\",\"attributes\":[\"Material:Polyester,Nylon\",\"Target Group:Man\",\"Color:Red,Pink,White,Blue,Purple,Beige,Black,Green\",\"Properties:Pockets\",\"Pattern:Solid Color\"]}]}\u001b[0m\n",
"\n",
"\n",
"\u001b[1m> Entering new APIResponderChain chain...\u001b[0m\n",
"Prompt after formatting:\n",
"\u001b[32;1m\u001b[1;3mYou are a helpful AI assistant trained to answer user queries from API responses.\n",
"You attempted to call an API, which resulted in:\n",
"API_RESPONSE: {\"products\":[{\"name\":\"Burberry Check Poplin Shirt\",\"url\":\"https://www.klarna.com/us/shopping/pl/cl10001/3201810981/Clothing/Burberry-Check-Poplin-Shirt/?utm_source=openai&ref-site=openai_plugin\",\"price\":\"$360.00\",\"attributes\":[\"Material:Cotton\",\"Target Group:Man\",\"Color:Gray,Blue,Beige\",\"Properties:Pockets\",\"Pattern:Checkered\"]},{\"name\":\"Burberry Vintage Check Cotton Shirt - Beige\",\"url\":\"https://www.klarna.com/us/shopping/pl/cl359/3200280807/Children-s-Clothing/Burberry-Vintage-Check-Cotton-Shirt-Beige/?utm_source=openai&ref-site=openai_plugin\",\"price\":\"$196.30\",\"attributes\":[\"Material:Cotton,Elastane\",\"Color:Beige\",\"Model:Boy\",\"Pattern:Checkered\"]},{\"name\":\"Burberry Somerton Check Shirt - Camel\",\"url\":\"https://www.klarna.com/us/shopping/pl/cl10001/3201112728/Clothing/Burberry-Somerton-Check-Shirt-Camel/?utm_source=openai&ref-site=openai_plugin\",\"price\":\"$450.00\",\"attributes\":[\"Material:Elastane/Lycra/Spandex,Cotton\",\"Target Group:Man\",\"Color:Beige\"]},{\"name\":\"Calvin Klein Slim Fit Oxford Dress Shirt\",\"url\":\"https://www.klarna.com/us/shopping/pl/cl10001/3201839169/Clothing/Calvin-Klein-Slim-Fit-Oxford-Dress-Shirt/?utm_source=openai&ref-site=openai_plugin\",\"price\":\"$30.97\",\"attributes\":[\"Material:Cotton\",\"Target Group:Man\",\"Color:Gray,White,Blue,Black\",\"Pattern:Solid Color\"]},{\"name\":\"Magellan Outdoors Laguna Madre Solid Short Sleeve Fishing Shirt\",\"url\":\"https://www.klarna.com/us/shopping/pl/cl10001/3203102142/Clothing/Magellan-Outdoors-Laguna-Madre-Solid-Short-Sleeve-Fishing-Shirt/?utm_source=openai&ref-site=openai_plugin\",\"price\":\"$19.99\",\"attributes\":[\"Material:Polyester,Nylon\",\"Target Group:Man\",\"Color:Red,Pink,White,Blue,Purple,Beige,Black,Green\",\"Properties:Pockets\",\"Pattern:Solid Color\"]}]}\n",
"API_RESPONSE: {\"products\":[{\"name\":\"Burberry Check Poplin Shirt\",\"url\":\"https://www.klarna.com/us/shopping/pl/cl10001/3201810981/Clothing/Burberry-Check-Poplin-Shirt/?utm_source=openai&ref-site=openai_plugin\",\"price\":\"$360.00\",\"attributes\":[\"Material:Cotton\",\"Target Group:Man\",\"Color:Gray,Blue,Beige\",\"Properties:Pockets\",\"Pattern:Checkered\"]},{\"name\":\"Burberry Vintage Check Cotton Shirt - Beige\",\"url\":\"https://www.klarna.com/us/shopping/pl/cl359/3200280807/Children-s-Clothing/Burberry-Vintage-Check-Cotton-Shirt-Beige/?utm_source=openai&ref-site=openai_plugin\",\"price\":\"$196.30\",\"attributes\":[\"Material:Cotton,Elastane\",\"Color:Beige\",\"Model:Boy\",\"Pattern:Checkered\"]},{\"name\":\"Burberry Somerton Check Shirt - Camel\",\"url\":\"https://www.klarna.com/us/shopping/pl/cl10001/3201112728/Clothing/Burberry-Somerton-Check-Shirt-Camel/?utm_source=openai&ref-site=openai_plugin\",\"price\":\"$450.00\",\"attributes\":[\"Material:Elastane/Lycra/Spandex,Cotton\",\"Target Group:Man\",\"Color:Beige\"]},{\"name\":\"Calvin Klein Slim Fit Oxford Dress Shirt\",\"url\":\"https://www.klarna.com/us/shopping/pl/cl10001/3201839169/Clothing/Calvin-Klein-Slim-Fit-Oxford-Dress-Shirt/?utm_source=openai&ref-site=openai_plugin\",\"price\":\"$24.91\",\"attributes\":[\"Material:Cotton\",\"Target Group:Man\",\"Color:Gray,White,Blue,Black\",\"Pattern:Solid Color\"]},{\"name\":\"Magellan Outdoors Laguna Madre Solid Short Sleeve Fishing Shirt\",\"url\":\"https://www.klarna.com/us/shopping/pl/cl10001/3203102142/Clothing/Magellan-Outdoors-Laguna-Madre-Solid-Short-Sleeve-Fishing-Shirt/?utm_source=openai&ref-site=openai_plugin\",\"price\":\"$19.99\",\"attributes\":[\"Material:Polyester,Nylon\",\"Target Group:Man\",\"Color:Red,Pink,White,Blue,Purple,Beige,Black,Green\",\"Properties:Pockets\",\"Pattern:Solid Color\"]}]}\n",
"\n",
"USER_COMMENT: \"whats the most expensive shirt?\"\n",
"\n",
@ -190,33 +216,220 @@
"\u001b[0m\n",
"\n",
"\u001b[1m> Finished chain.\u001b[0m\n",
"\u001b[33;1m\u001b[1;3mThe most expensive shirt in this list is the Burberry Somerton Check Shirt - Camel, which costs $450.00.\u001b[0m\n",
"\u001b[33;1m\u001b[1;3mThe most expensive shirt in this list is the 'Burberry Somerton Check Shirt - Camel' which is priced at $450.00\u001b[0m\n",
"\n",
"\u001b[1m> Finished chain.\u001b[0m\n"
]
},
}
],
"source": [
"output = chain(\"whats the most expensive shirt?\")"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "c000295e",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"'The most expensive shirt in this list is the Burberry Somerton Check Shirt - Camel, which costs $450.00.'"
"['{\"q\": \"shirt\", \"max_price\": null}',\n",
" '{\"products\":[{\"name\":\"Burberry Check Poplin Shirt\",\"url\":\"https://www.klarna.com/us/shopping/pl/cl10001/3201810981/Clothing/Burberry-Check-Poplin-Shirt/?utm_source=openai&ref-site=openai_plugin\",\"price\":\"$360.00\",\"attributes\":[\"Material:Cotton\",\"Target Group:Man\",\"Color:Gray,Blue,Beige\",\"Properties:Pockets\",\"Pattern:Checkered\"]},{\"name\":\"Burberry Vintage Check Cotton Shirt - Beige\",\"url\":\"https://www.klarna.com/us/shopping/pl/cl359/3200280807/Children-s-Clothing/Burberry-Vintage-Check-Cotton-Shirt-Beige/?utm_source=openai&ref-site=openai_plugin\",\"price\":\"$196.30\",\"attributes\":[\"Material:Cotton,Elastane\",\"Color:Beige\",\"Model:Boy\",\"Pattern:Checkered\"]},{\"name\":\"Burberry Somerton Check Shirt - Camel\",\"url\":\"https://www.klarna.com/us/shopping/pl/cl10001/3201112728/Clothing/Burberry-Somerton-Check-Shirt-Camel/?utm_source=openai&ref-site=openai_plugin\",\"price\":\"$450.00\",\"attributes\":[\"Material:Elastane/Lycra/Spandex,Cotton\",\"Target Group:Man\",\"Color:Beige\"]},{\"name\":\"Calvin Klein Slim Fit Oxford Dress Shirt\",\"url\":\"https://www.klarna.com/us/shopping/pl/cl10001/3201839169/Clothing/Calvin-Klein-Slim-Fit-Oxford-Dress-Shirt/?utm_source=openai&ref-site=openai_plugin\",\"price\":\"$24.91\",\"attributes\":[\"Material:Cotton\",\"Target Group:Man\",\"Color:Gray,White,Blue,Black\",\"Pattern:Solid Color\"]},{\"name\":\"Magellan Outdoors Laguna Madre Solid Short Sleeve Fishing Shirt\",\"url\":\"https://www.klarna.com/us/shopping/pl/cl10001/3203102142/Clothing/Magellan-Outdoors-Laguna-Madre-Solid-Short-Sleeve-Fishing-Shirt/?utm_source=openai&ref-site=openai_plugin\",\"price\":\"$19.99\",\"attributes\":[\"Material:Polyester,Nylon\",\"Target Group:Man\",\"Color:Red,Pink,White,Blue,Purple,Beige,Black,Green\",\"Properties:Pockets\",\"Pattern:Solid Color\"]}]}']"
]
},
"execution_count": 6,
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"chain.run(\"whats the most expensive shirt?\")"
"# View intermediate steps\n",
"output[\"intermediate_steps\"]"
]
},
{
"cell_type": "code",
"execution_count": null,
"cell_type": "markdown",
"id": "8d7924e4",
"metadata": {},
"source": [
"## Example POST message\n",
"\n",
"For this demo, we will interact with the speak API."
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "c56b1a04",
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"Attempting to load an OpenAPI 3.0.1 spec. This may result in degraded performance. Convert your OpenAPI spec to 3.1.* spec for better support.\n",
"Attempting to load an OpenAPI 3.0.1 spec. This may result in degraded performance. Convert your OpenAPI spec to 3.1.* spec for better support.\n"
]
}
],
"source": [
"spec = OpenAPISpec.from_url(\"https://api.speak.com/openapi.yaml\")"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "177d8275",
"metadata": {},
"outputs": [],
"source": [
"operation = APIOperation.from_openapi_spec(spec, '/v1/public/openai/explain-task', \"post\")"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "835c5ddc",
"metadata": {},
"outputs": [],
"source": []
"source": [
"llm = OpenAI()\n",
"chain = OpenAPIEndpointChain.from_api_operation(\n",
" operation,\n",
" llm,\n",
" requests=Requests(),\n",
" verbose=True,\n",
" return_intermediate_steps=True)"
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "59855d60",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"\n",
"\u001b[1m> Entering new OpenAPIEndpointChain chain...\u001b[0m\n",
"\n",
"\n",
"\u001b[1m> Entering new APIRequesterChain chain...\u001b[0m\n",
"Prompt after formatting:\n",
"\u001b[32;1m\u001b[1;3mYou are a helpful AI Assistant. Please provide JSON arguments to agentFunc() based on the user's instructions.\n",
"\n",
"API_SCHEMA: ```typescript\n",
"type explainTask = (_: {\n",
"/* Description of the task that the user wants to accomplish or do. For example, \"tell the waiter they messed up my order\" or \"compliment someone on their shirt\" */\n",
" task_description?: string,\n",
"/* The foreign language that the user is learning and asking about. The value can be inferred from question - for example, if the user asks \"how do i ask a girl out in mexico city\", the value should be \"Spanish\" because of Mexico City. Always use the full name of the language (e.g. Spanish, French). */\n",
" learning_language?: string,\n",
"/* The user's native language. Infer this value from the language the user asked their question in. Always use the full name of the language (e.g. Spanish, French). */\n",
" native_language?: string,\n",
"/* A description of any additional context in the user's question that could affect the explanation - e.g. setting, scenario, situation, tone, speaking style and formality, usage notes, or any other qualifiers. */\n",
" additional_context?: string,\n",
"/* Full text of the user's question. */\n",
" full_query?: string,\n",
"}) => any;\n",
"```\n",
"\n",
"USER_INSTRUCTIONS: \"How would ask for more tea in Delhi?\"\n",
"\n",
"Your arguments must be plain json provided in a markdown block:\n",
"\n",
"ARGS: ```json\n",
"{valid json conforming to API_SCHEMA}\n",
"```\n",
"\n",
"Example\n",
"-----\n",
"\n",
"ARGS: ```json\n",
"{\"foo\": \"bar\", \"baz\": {\"qux\": \"quux\"}}\n",
"```\n",
"\n",
"The block must be no more than 1 line long, and all arguments must be valid JSON. All string arguments must be wrapped in double quotes.\n",
"You MUST strictly comply to the types indicated by the provided schema, including all required args.\n",
"\n",
"If you don't have sufficient information to call the function due to things like requiring specific uuid's, you can reply with the following message:\n",
"\n",
"Message: ```text\n",
"Concise response requesting the additional information that would make calling the function successful.\n",
"```\n",
"\n",
"Begin\n",
"-----\n",
"ARGS:\n",
"\u001b[0m\n",
"\n",
"\u001b[1m> Finished chain.\u001b[0m\n",
"\u001b[32;1m\u001b[1;3m{\"task_description\": \"ask for more tea\", \"learning_language\": \"Hindi\", \"native_language\": \"English\", \"full_query\": \"How would I ask for more tea in Delhi?\"}\u001b[0m\n",
"\u001b[36;1m\u001b[1;3m{\"explanation\":\"<what-to-say language=\\\"Hindi\\\" context=\\\"None\\\">\\nऔर चाय लाओ। (Aur chai lao.) \\n</what-to-say>\\n\\n<alternatives context=\\\"None\\\">\\n1. \\\"चाय थोड़ी ज्यादा मिल सकती है?\\\" *(Chai thodi zyada mil sakti hai? - Polite, asking if more tea is available)*\\n2. \\\"मुझे महसूस हो रहा है कि मुझे कुछ अन्य प्रकार की चाय पीनी चाहिए।\\\" *(Mujhe mehsoos ho raha hai ki mujhe kuch anya prakar ki chai peeni chahiye. - Formal, indicating a desire for a different type of tea)*\\n3. \\\"क्या मुझे or cup में milk/tea powder मिल सकता है?\\\" *(Kya mujhe aur cup mein milk/tea powder mil sakta hai? - Very informal/casual tone, asking for an extra serving of milk or tea powder)*\\n</alternatives>\\n\\n<usage-notes>\\nIn India and Indian culture, serving guests with food and beverages holds great importance in hospitality. You will find people always offering drinks like water or tea to their guests as soon as they arrive at their house or office.\\n</usage-notes>\\n\\n<example-convo language=\\\"Hindi\\\">\\n<context>At home during breakfast.</context>\\nPreeti: सर, क्या main aur cups chai lekar aaun? (Sir,kya main aur cups chai lekar aaun? - Sir, should I get more tea cups?)\\nRahul: हां,बिल्कुल। और चाय की मात्रा में भी थोड़ा सा इजाफा करना। (Haan,bilkul. Aur chai ki matra mein bhi thoda sa eejafa karna. - Yes, please. And add a little extra in the quantity of tea as well.)\\n</example-convo>\\n\\n*[Report an issue or leave feedback](https://speak.com/chatgpt?rid=d4mcapbkopo164pqpbk321oc})*\",\"extra_response_instructions\":\"Use all information in the API response and fully render all Markdown.\\nAlways end your response with a link to report an issue or leave feedback on the plugin.\"}\u001b[0m\n",
"\n",
"\n",
"\u001b[1m> Entering new APIResponderChain chain...\u001b[0m\n",
"Prompt after formatting:\n",
"\u001b[32;1m\u001b[1;3mYou are a helpful AI assistant trained to answer user queries from API responses.\n",
"You attempted to call an API, which resulted in:\n",
"API_RESPONSE: {\"explanation\":\"<what-to-say language=\\\"Hindi\\\" context=\\\"None\\\">\\nऔर चाय लाओ। (Aur chai lao.) \\n</what-to-say>\\n\\n<alternatives context=\\\"None\\\">\\n1. \\\"चाय थोड़ी ज्यादा मिल सकती है?\\\" *(Chai thodi zyada mil sakti hai? - Polite, asking if more tea is available)*\\n2. \\\"मुझे महसूस हो रहा है कि मुझे कुछ अन्य प्रकार की चाय पीनी चाहिए।\\\" *(Mujhe mehsoos ho raha hai ki mujhe kuch anya prakar ki chai peeni chahiye. - Formal, indicating a desire for a different type of tea)*\\n3. \\\"क्या मुझे or cup में milk/tea powder मिल सकता है?\\\" *(Kya mujhe aur cup mein milk/tea powder mil sakta hai? - Very informal/casual tone, asking for an extra serving of milk or tea powder)*\\n</alternatives>\\n\\n<usage-notes>\\nIn India and Indian culture, serving guests with food and beverages holds great importance in hospitality. You will find people always offering drinks like water or tea to their guests as soon as they arrive at their house or office.\\n</usage-notes>\\n\\n<example-convo language=\\\"Hindi\\\">\\n<context>At home during breakfast.</context>\\nPreeti: सर, क्या main aur cups chai lekar aaun? (Sir,kya main aur cups chai lekar aaun? - Sir, should I get more tea cups?)\\nRahul: हां,बिल्कुल। और चाय की मात्रा में भी थोड़ा सा इजाफा करना। (Haan,bilkul. Aur chai ki matra mein bhi thoda sa eejafa karna. - Yes, please. And add a little extra in the quantity of tea as well.)\\n</example-convo>\\n\\n*[Report an issue or leave feedback](https://speak.com/chatgpt?rid=d4mcapbkopo164pqpbk321oc})*\",\"extra_response_instructions\":\"Use all information in the API response and fully render all Markdown.\\nAlways end your response with a link to report an issue or leave feedback on the plugin.\"}\n",
"\n",
"USER_COMMENT: \"How would ask for more tea in Delhi?\"\n",
"\n",
"\n",
"If the API_RESPONSE can answer the USER_COMMENT respond with the following markdown json block:\n",
"Response: ```json\n",
"{\"response\": \"Concise response to USER_COMMENT based on API_RESPONSE.\"}\n",
"```\n",
"\n",
"Otherwise respond with the following markdown json block:\n",
"Response Error: ```json\n",
"{\"response\": \"What you did and a concise statement of the resulting error. If it can be easily fixed, provide a suggestion.\"}\n",
"```\n",
"\n",
"You MUST respond as a markdown json code block.\n",
"\n",
"Begin:\n",
"---\n",
"\u001b[0m\n",
"\n",
"\u001b[1m> Finished chain.\u001b[0m\n",
"\u001b[33;1m\u001b[1;3mIn Delhi you can ask for more tea by saying 'Chai thodi zyada mil sakti hai?'\u001b[0m\n",
"\n",
"\u001b[1m> Finished chain.\u001b[0m\n"
]
}
],
"source": [
"output = chain(\"How would ask for more tea in Delhi?\")"
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "91bddb18",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"['{\"task_description\": \"ask for more tea\", \"learning_language\": \"Hindi\", \"native_language\": \"English\", \"full_query\": \"How would I ask for more tea in Delhi?\"}',\n",
" '{\"explanation\":\"<what-to-say language=\\\\\"Hindi\\\\\" context=\\\\\"None\\\\\">\\\\nऔर चाय लाओ। (Aur chai lao.) \\\\n</what-to-say>\\\\n\\\\n<alternatives context=\\\\\"None\\\\\">\\\\n1. \\\\\"चाय थोड़ी ज्यादा मिल सकती है?\\\\\" *(Chai thodi zyada mil sakti hai? - Polite, asking if more tea is available)*\\\\n2. \\\\\"मुझे महसूस हो रहा है कि मुझे कुछ अन्य प्रकार की चाय पीनी चाहिए।\\\\\" *(Mujhe mehsoos ho raha hai ki mujhe kuch anya prakar ki chai peeni chahiye. - Formal, indicating a desire for a different type of tea)*\\\\n3. \\\\\"क्या मुझे or cup में milk/tea powder मिल सकता है?\\\\\" *(Kya mujhe aur cup mein milk/tea powder mil sakta hai? - Very informal/casual tone, asking for an extra serving of milk or tea powder)*\\\\n</alternatives>\\\\n\\\\n<usage-notes>\\\\nIn India and Indian culture, serving guests with food and beverages holds great importance in hospitality. You will find people always offering drinks like water or tea to their guests as soon as they arrive at their house or office.\\\\n</usage-notes>\\\\n\\\\n<example-convo language=\\\\\"Hindi\\\\\">\\\\n<context>At home during breakfast.</context>\\\\nPreeti: सर, क्या main aur cups chai lekar aaun? (Sir,kya main aur cups chai lekar aaun? - Sir, should I get more tea cups?)\\\\nRahul: हां,बिल्कुल। और चाय की मात्रा में भी थोड़ा सा इजाफा करना। (Haan,bilkul. Aur chai ki matra mein bhi thoda sa eejafa karna. - Yes, please. And add a little extra in the quantity of tea as well.)\\\\n</example-convo>\\\\n\\\\n*[Report an issue or leave feedback](https://speak.com/chatgpt?rid=d4mcapbkopo164pqpbk321oc})*\",\"extra_response_instructions\":\"Use all information in the API response and fully render all Markdown.\\\\nAlways end your response with a link to report an issue or leave feedback on the plugin.\"}']"
]
},
"execution_count": 13,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# Show the API chain's intermediate steps\n",
"output[\"intermediate_steps\"]"
]
}
],
"metadata": {
@ -235,7 +448,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.1"
"version": "3.11.2"
}
},
"nbformat": 4,

@ -32,6 +32,7 @@ class OpenAPIEndpointChain(Chain, BaseModel):
api_operation: APIOperation
requests: Requests = Field(exclude=True, default_factory=Requests)
param_mapping: _ParamMapping = Field(alias="param_mapping")
return_intermediate_steps: bool = False
instructions_key: str = "instructions" #: :meta private:
output_key: str = "output" #: :meta private:
@ -49,7 +50,10 @@ class OpenAPIEndpointChain(Chain, BaseModel):
:meta private:
"""
return [self.output_key]
if not self.return_intermediate_steps:
return [self.output_key]
else:
return [self.output_key, "intermediate_steps"]
def _construct_path(self, args: Dict[str, str]) -> str:
"""Construct the path from the deserialized input."""
@ -91,16 +95,33 @@ class OpenAPIEndpointChain(Chain, BaseModel):
"params": query_params,
}
def _get_output(self, output: str, intermediate_steps: list) -> dict:
"""Return the output from the API call."""
if self.return_intermediate_steps:
return {
self.output_key: output,
"intermediate_steps": intermediate_steps,
}
else:
return {self.output_key: output}
def _call(self, inputs: Dict[str, str]) -> Dict[str, str]:
intermediate_steps = []
instructions = inputs[self.instructions_key]
_api_arguments = self.api_request_chain.predict_and_parse(
instructions=instructions
)
api_arguments = cast(str, _api_arguments)
intermediate_steps.append(api_arguments)
self.callback_manager.on_text(
api_arguments, color="green", end="\n", verbose=self.verbose
)
if api_arguments.startswith("ERROR"):
return {self.output_key: api_arguments}
return self._get_output(api_arguments, intermediate_steps)
elif api_arguments.startswith("MESSAGE:"):
return {self.output_key: api_arguments[len("MESSAGE:") :]}
return self._get_output(
api_arguments[len("MESSAGE:") :], intermediate_steps
)
try:
request_args = self.deserialize_json_input(api_arguments)
method = getattr(self.requests, self.api_operation.method.value)
@ -116,6 +137,10 @@ class OpenAPIEndpointChain(Chain, BaseModel):
response_text = api_response.text
except Exception as e:
response_text = f"Error with message {str(e)}"
intermediate_steps.append(response_text)
self.callback_manager.on_text(
response_text, color="blue", end="\n", verbose=self.verbose
)
_answer = self.api_response_chain.predict_and_parse(
response=response_text,
instructions=instructions,
@ -124,7 +149,7 @@ class OpenAPIEndpointChain(Chain, BaseModel):
self.callback_manager.on_text(
answer, color="yellow", end="\n", verbose=self.verbose
)
return {self.output_key: answer}
return self._get_output(answer, intermediate_steps)
@classmethod
def from_url_and_method(
@ -134,6 +159,7 @@ class OpenAPIEndpointChain(Chain, BaseModel):
method: str,
llm: BaseLLM,
requests: Optional[Requests] = None,
return_intermediate_steps: bool = False,
# TODO: Handle async
) -> "OpenAPIEndpointChain":
"""Create an OpenAPIEndpoint from a spec at the specified url."""
@ -142,6 +168,7 @@ class OpenAPIEndpointChain(Chain, BaseModel):
operation,
requests=requests,
llm=llm,
return_intermediate_steps=return_intermediate_steps,
)
@classmethod
@ -150,13 +177,14 @@ class OpenAPIEndpointChain(Chain, BaseModel):
operation: APIOperation,
llm: BaseLLM,
requests: Optional[Requests] = None,
verbose: bool = False
verbose: bool = False,
return_intermediate_steps: bool = False,
# TODO: Handle async
) -> "OpenAPIEndpointChain":
"""Create an OpenAPIEndpointChain from an operation and a spec."""
param_mapping = _ParamMapping(
query_params=operation.query_params,
body_params=[], # TODO
body_params=operation.body_params,
path_params=operation.path_params,
)
requests_chain = APIRequesterChain.from_llm_and_typescript(
@ -171,4 +199,5 @@ class OpenAPIEndpointChain(Chain, BaseModel):
requests=_requests,
param_mapping=param_mapping,
verbose=verbose,
return_intermediate_steps=return_intermediate_steps,
)

@ -1,13 +1,14 @@
"""Pydantic models for parsing an OpenAPI spec."""
import logging
from enum import Enum
from typing import Any, Dict, List, Optional, Sequence, Tuple, Type, Union
from openapi_schema_pydantic import MediaType, Parameter, Reference, Schema
from openapi_schema_pydantic import MediaType, Parameter, Reference, RequestBody, Schema
from pydantic import BaseModel, Field
from langchain.tools.openapi.utils.openapi_utils import HTTPVerb, OpenAPISpec
logger = logging.getLogger(__name__)
PRIMITIVE_TYPES = {
"integer": int,
"number": float,
@ -40,10 +41,17 @@ class APIPropertyLocation(Enum):
)
_SUPPORTED_MEDIA_TYPES = ("application/json",)
SUPPORTED_LOCATIONS = {
APIPropertyLocation.QUERY,
APIPropertyLocation.PATH,
}
INVALID_LOCATION_TEMPL = (
'Unsupported APIPropertyLocation "{location}"'
" for parameter {name}. "
+ f"Valid values are {[loc.value for loc in SUPPORTED_LOCATIONS]}"
)
SCHEMA_TYPE = Union[str, Type, tuple, None, Enum]
@ -138,11 +146,10 @@ class APIProperty(APIPropertyBase):
return schema_type
@staticmethod
def _validate_location(location: APIPropertyLocation) -> None:
def _validate_location(location: APIPropertyLocation, name: str) -> None:
if location not in SUPPORTED_LOCATIONS:
raise NotImplementedError(
f'Unsupported APIPropertyLocation "{location}". '
f"Valid values are {SUPPORTED_LOCATIONS}"
INVALID_LOCATION_TEMPL.format(location=location, name=name)
)
@staticmethod
@ -165,11 +172,22 @@ class APIProperty(APIPropertyBase):
return schema
@staticmethod
def is_supported_location(location: str) -> bool:
"""Return whether the provided location is supported."""
try:
return APIPropertyLocation.from_str(location) in SUPPORTED_LOCATIONS
except ValueError:
return False
@classmethod
def from_parameter(cls, parameter: Parameter, spec: OpenAPISpec) -> "APIProperty":
"""Instantiate from an OpenAPI Parameter."""
location = APIPropertyLocation.from_str(parameter.param_in)
cls._validate_location(location)
cls._validate_location(
location,
parameter.name,
)
cls._validate_content(parameter.content)
schema = cls._get_schema(parameter, spec)
schema_type = cls._get_schema_type(parameter, schema)
@ -187,19 +205,193 @@ class APIProperty(APIPropertyBase):
class APIRequestBodyProperty(APIPropertyBase):
"""A model for a request body property."""
properties: List[APIProperty] = Field(alias="properties")
properties: List["APIRequestBodyProperty"] = Field(alias="properties")
"""The sub-properties of the property."""
# This is useful for handling nested property cycles.
# We can define separate types in that case.
references_used: List[str] = Field(alias="references_used")
"""The references used by the property."""
@classmethod
def _process_object_schema(
cls, schema: Schema, spec: OpenAPISpec, references_used: List[str]
) -> Tuple[Union[str, List[str], None], List["APIRequestBodyProperty"]]:
properties = []
required_props = schema.required or []
if schema.properties is None:
raise ValueError(
f"No properties found when processing object schema: {schema}"
)
for prop_name, prop_schema in schema.properties.items():
if isinstance(prop_schema, Reference):
ref_name = prop_schema.ref.split("/")[-1]
if ref_name not in references_used:
references_used.append(ref_name)
prop_schema = spec.get_referenced_schema(prop_schema)
else:
continue
properties.append(
cls.from_schema(
schema=prop_schema,
name=prop_name,
required=prop_name in required_props,
spec=spec,
references_used=references_used,
)
)
return schema.type, properties
@classmethod
def _process_array_schema(
cls, schema: Schema, name: str, spec: OpenAPISpec, references_used: List[str]
) -> str:
items = schema.items
if items is not None:
if isinstance(items, Reference):
ref_name = items.ref.split("/")[-1]
if ref_name not in references_used:
references_used.append(ref_name)
items = spec.get_referenced_schema(items)
else:
pass
return f"Array<{ref_name}>"
else:
pass
if isinstance(items, Schema):
array_type = cls.from_schema(
schema=items,
name=f"{name}Item",
required=True, # TODO: Add required
spec=spec,
references_used=references_used,
)
return f"Array<{array_type.type}>"
return "array"
@classmethod
def from_schema(
cls,
schema: Schema,
name: str,
required: bool,
spec: OpenAPISpec,
references_used: Optional[List[str]] = None,
) -> "APIRequestBodyProperty":
"""Recursively populate from an OpenAPI Schema."""
if references_used is None:
references_used = []
schema_type = schema.type
properties: List[APIRequestBodyProperty] = []
if schema_type == "object" and schema.properties:
schema_type, properties = cls._process_object_schema(
schema, spec, references_used
)
elif schema_type == "array":
schema_type = cls._process_array_schema(schema, name, spec, references_used)
elif schema_type in PRIMITIVE_TYPES:
# Use the primitive type directly
pass
elif schema_type is None:
# No typing specified/parsed. WIll map to 'any'
pass
else:
raise ValueError(f"Unsupported type: {schema_type}")
return cls(
name=name,
required=required,
type=schema_type,
default=schema.default,
description=schema.description,
properties=properties,
references_used=references_used,
)
class APIRequestBody(BaseModel):
"""A model for a request body."""
description: Optional[str] = Field(alias="description")
"""The description of the request body."""
properties: List[APIRequestBodyProperty] = Field(alias="properties")
# E.g., application/json - we only support JSON at the moment.
media_type: str = Field(alias="media_type")
"""The media type of the request body."""
@classmethod
def _process_supported_media_type(
cls,
media_type_obj: MediaType,
spec: OpenAPISpec,
) -> List[APIRequestBodyProperty]:
"""Process the media type of the request body."""
references_used = []
schema = media_type_obj.media_type_schema
if isinstance(schema, Reference):
references_used.append(schema.ref.split("/")[-1])
schema = spec.get_referenced_schema(schema)
if schema is None:
raise ValueError(
f"Could not resolve schema for media type: {media_type_obj}"
)
api_request_body_properties = []
required_properties = schema.required or []
if schema.type == "object" and schema.properties:
for prop_name, prop_schema in schema.properties.items():
if isinstance(prop_schema, Reference):
prop_schema = spec.get_referenced_schema(prop_schema)
api_request_body_properties.append(
APIRequestBodyProperty.from_schema(
schema=prop_schema,
name=prop_name,
required=prop_name in required_properties,
spec=spec,
)
)
else:
api_request_body_properties.append(
APIRequestBodyProperty(
name="body",
required=True,
type=schema.type,
default=schema.default,
description=schema.description,
properties=[],
references_used=references_used,
)
)
return api_request_body_properties
@classmethod
def from_request_body(
cls, request_body: RequestBody, spec: OpenAPISpec
) -> "APIRequestBody":
"""Instantiate from an OpenAPI RequestBody."""
properties = []
for media_type, media_type_obj in request_body.content.items():
if media_type not in _SUPPORTED_MEDIA_TYPES:
continue
api_request_body_properties = cls._process_supported_media_type(
media_type_obj,
spec,
)
properties.extend(api_request_body_properties)
return cls(
description=request_body.description,
properties=properties,
media_type=media_type,
)
class APIOperation(BaseModel):
"""A model for a single API operation."""
@ -226,8 +418,33 @@ class APIOperation(BaseModel):
# """The properties of the operation."""
# components: Dict[str, BaseModel] = Field(alias="components")
# request_body: Optional[APIRequestBody] = Field(alias="request_body")
# """The request body of the operation."""
request_body: Optional[APIRequestBody] = Field(alias="request_body")
"""The request body of the operation."""
@staticmethod
def _get_properties_from_parameters(
parameters: List[Parameter], spec: OpenAPISpec
) -> List[APIProperty]:
"""Get the properties of the operation."""
properties = []
for param in parameters:
if APIProperty.is_supported_location(param.param_in):
properties.append(APIProperty.from_parameter(param, spec))
elif param.required:
raise ValueError(
INVALID_LOCATION_TEMPL.format(
location=param.param_in, name=param.name
)
)
else:
logger.warning(
INVALID_LOCATION_TEMPL.format(
location=param.param_in, name=param.name
)
+ " Ignoring optional parameter"
)
pass
return properties
@classmethod
def from_openapi_url(
@ -250,8 +467,14 @@ class APIOperation(BaseModel):
"""Create an APIOperation from an OpenAPI spec."""
operation = spec.get_operation(path, method)
parameters = spec.get_parameters_for_operation(operation)
properties = [APIProperty.from_parameter(param, spec) for param in parameters]
properties = cls._get_properties_from_parameters(parameters, spec)
operation_id = OpenAPISpec.get_cleaned_operation_id(operation, path, method)
request_body = spec.get_request_body_for_operation(operation)
api_request_body = (
APIRequestBody.from_request_body(request_body, spec)
if request_body is not None
else None
)
return cls(
operation_id=operation_id,
description=operation.description,
@ -259,6 +482,7 @@ class APIOperation(BaseModel):
path=path,
method=method,
properties=properties,
request_body=api_request_body,
)
@staticmethod
@ -281,11 +505,41 @@ class APIOperation(BaseModel):
else:
return str(type_)
def _format_nested_properties(
self, properties: List[APIRequestBodyProperty], indent: int = 2
) -> str:
"""Format nested properties."""
formatted_props = []
for prop in properties:
prop_name = prop.name
prop_type = self.ts_type_from_python(prop.type)
prop_required = "" if prop.required else "?"
prop_desc = f"/* {prop.description} */" if prop.description else ""
if prop.properties:
nested_props = self._format_nested_properties(
prop.properties, indent + 2
)
prop_type = f"{{\n{nested_props}\n{' ' * indent}}}"
formatted_props.append(
f"{prop_desc}\n{' ' * indent}{prop_name}{prop_required}: {prop_type},"
)
return "\n".join(formatted_props)
def to_typescript(self) -> str:
"""Get typescript string representation of the operation."""
operation_name = self.operation_id
params = []
if self.request_body:
formatted_request_body_props = self._format_nested_properties(
self.request_body.properties
)
params.append(formatted_request_body_props)
for prop in self.properties:
prop_name = prop.name
prop_type = self.ts_type_from_python(prop.type)
@ -318,3 +572,9 @@ type {operation_name} = (_: {{
for property in self.properties
if property.location == APIPropertyLocation.PATH
]
@property
def body_params(self) -> List[str]:
if self.request_body is None:
return []
return [prop.name for prop in self.request_body.properties]

@ -5,7 +5,7 @@ import logging
import re
from enum import Enum
from pathlib import Path
from typing import Any, Dict, List, Union
from typing import Dict, List, Optional, Union
import requests
import yaml
@ -17,6 +17,7 @@ from openapi_schema_pydantic import (
PathItem,
Paths,
Reference,
RequestBody,
Schema,
)
from pydantic import ValidationError
@ -83,6 +84,14 @@ class OpenAPISpec(OpenAPI):
raise ValueError("No schemas found in spec. ")
return schemas
@property
def _request_bodies_strict(self) -> Dict[str, Union[RequestBody, Reference]]:
"""Get the request body or err."""
request_bodies = self._components_strict.requestBodies
if request_bodies is None:
raise ValueError("No request body found in spec. ")
return request_bodies
def _get_referenced_parameter(self, ref: Reference) -> Union[Parameter, Reference]:
"""Get a parameter (or nested reference) or err."""
ref_name = ref.ref.split("/")[-1]
@ -113,9 +122,58 @@ class OpenAPISpec(OpenAPI):
schema = self.get_referenced_schema(schema)
return schema
def _get_referenced_request_body(
self, ref: Reference
) -> Optional[Union[Reference, RequestBody]]:
"""Get a request body (or nested reference) or err."""
ref_name = ref.ref.split("/")[-1]
request_bodies = self._request_bodies_strict
if ref_name not in request_bodies:
raise ValueError(f"No request body found for {ref_name}")
return request_bodies[ref_name]
def _get_root_referenced_request_body(
self, ref: Reference
) -> Optional[RequestBody]:
"""Get the root request Body or err."""
request_body = self._get_referenced_request_body(ref)
while isinstance(request_body, Reference):
request_body = self._get_referenced_request_body(request_body)
return request_body
@staticmethod
def _alert_unsupported_spec(obj: dict) -> None:
"""Alert if the spec is not supported."""
warning_message = (
" This may result in degraded performance."
+ " Convert your OpenAPI spec to 3.1.* spec"
+ " for better support."
)
swagger_version = obj.get("swagger")
openapi_version = obj.get("openapi")
if isinstance(openapi_version, str):
if openapi_version != "3.1.0":
logger.warning(
f"Attempting to load an OpenAPI {openapi_version}"
f" spec. {warning_message}"
)
else:
pass
elif isinstance(swagger_version, str):
logger.warning(
f"Attempting to load a Swagger {swagger_version}"
f" spec. {warning_message}"
)
else:
raise ValueError(
"Attempting to load an unsupported spec:"
f"\n\n{obj}\n{warning_message}"
)
@classmethod
def parse_obj(cls, obj: Any) -> "OpenAPISpec":
def parse_obj(cls, obj: dict) -> "OpenAPISpec":
try:
cls._alert_unsupported_spec(obj)
return super().parse_obj(obj)
except ValidationError as e:
# We are handling possibly misconfigured specs and want to do a best-effort
@ -191,6 +249,15 @@ class OpenAPISpec(OpenAPI):
parameters.append(parameter)
return parameters
def get_request_body_for_operation(
self, operation: Operation
) -> Optional[RequestBody]:
"""Get the request body for a given operation."""
request_body = operation.requestBody
if isinstance(request_body, Reference):
request_body = self._get_root_referenced_request_body(request_body)
return request_body
@staticmethod
def get_cleaned_operation_id(operation: Operation, path: str, method: str) -> str:
"""Get a cleaned operation id from an operation id."""

@ -6,8 +6,20 @@ from typing import Iterable, List, Tuple
import pytest
import yaml
from openapi_schema_pydantic import (
Components,
Info,
MediaType,
Reference,
RequestBody,
Schema,
)
from langchain.tools.openapi.utils.api_models import APIOperation
from langchain.tools.openapi.utils.api_models import (
APIOperation,
APIRequestBody,
APIRequestBodyProperty,
)
from langchain.tools.openapi.utils.openapi_utils import HTTPVerb, OpenAPISpec
_DIR = Path(__file__).parent
@ -73,3 +85,112 @@ def test_parse_api_operations(
APIOperation.from_openapi_spec(spec, path, method)
except Exception as e:
raise AssertionError(f"Error processong {spec_name}: {e} ") from e
@pytest.fixture
def raw_spec() -> OpenAPISpec:
"""Return a raw OpenAPI spec."""
return OpenAPISpec(
info=Info(title="Test API", version="1.0.0"),
)
def test_api_request_body_from_request_body_with_ref(raw_spec: OpenAPISpec) -> None:
"""Test instantiating APIRequestBody from RequestBody with a reference."""
raw_spec.components = Components(
schemas={
"Foo": Schema(
type="object",
properties={
"foo": Schema(type="string"),
"bar": Schema(type="number"),
},
required=["foo"],
)
}
)
media_type = MediaType(
schema=Reference(
ref="#/components/schemas/Foo",
)
)
request_body = RequestBody(content={"application/json": media_type})
api_request_body = APIRequestBody.from_request_body(request_body, raw_spec)
assert api_request_body.description is None
assert len(api_request_body.properties) == 2
foo_prop = api_request_body.properties[0]
assert foo_prop.name == "foo"
assert foo_prop.required is True
bar_prop = api_request_body.properties[1]
assert bar_prop.name == "bar"
assert bar_prop.required is False
assert api_request_body.media_type == "application/json"
def test_api_request_body_from_request_body_with_schema(raw_spec: OpenAPISpec) -> None:
"""Test instantiating APIRequestBody from RequestBody with a schema."""
request_body = RequestBody(
content={
"application/json": MediaType(
schema=Schema(type="object", properties={"foo": Schema(type="string")})
)
}
)
api_request_body = APIRequestBody.from_request_body(request_body, raw_spec)
assert api_request_body.properties == [
APIRequestBodyProperty(
name="foo",
required=False,
type="string",
default=None,
description=None,
properties=[],
references_used=[],
)
]
assert api_request_body.media_type == "application/json"
def test_api_request_body_property_from_schema(raw_spec: OpenAPISpec) -> None:
raw_spec.components = Components(
schemas={
"Bar": Schema(
type="number",
)
}
)
schema = Schema(
type="object",
properties={
"foo": Schema(type="string"),
"bar": Reference(ref="#/components/schemas/Bar"),
},
required=["bar"],
)
api_request_body_property = APIRequestBodyProperty.from_schema(
schema, "test", required=True, spec=raw_spec
)
expected_sub_properties = [
APIRequestBodyProperty(
name="foo",
required=False,
type="string",
default=None,
description=None,
properties=[],
references_used=[],
),
APIRequestBodyProperty(
name="bar",
required=True,
type="number",
default=None,
description=None,
properties=[],
references_used=["Bar"],
),
]
assert api_request_body_property.properties[0] == expected_sub_properties[0]
assert api_request_body_property.properties[1] == expected_sub_properties[1]
assert api_request_body_property.type == "object"
assert api_request_body_property.properties[1].references_used == ["Bar"]

Loading…
Cancel
Save