mirror of
https://github.com/hwchase17/langchain.git
synced 2026-04-03 19:04:23 +00:00
- **Description:** This PR is intended to improve the generation of
payloads for OpenAI functions when converting from an OpenAPI spec file.
The solution is to recursively resolve `$refs`.
Currently when converting OpenAPI specs into OpenAI functions using
`openapi_spec_to_openai_fn`, if the schemas have nested references, the
generated functions contain `$ref` that causes the LLM to generate
payloads with an incorrect schema.
For example, for the for OpenAPI spec:
```
text = """
{
"openapi": "3.0.3",
"info": {
"title": "Swagger Petstore - OpenAPI 3.0",
"termsOfService": "http://swagger.io/terms/",
"contact": {
"email": "apiteam@swagger.io"
},
"license": {
"name": "Apache 2.0",
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
},
"version": "1.0.11"
},
"externalDocs": {
"description": "Find out more about Swagger",
"url": "http://swagger.io"
},
"servers": [
{
"url": "https://petstore3.swagger.io/api/v3"
}
],
"tags": [
{
"name": "pet",
"description": "Everything about your Pets",
"externalDocs": {
"description": "Find out more",
"url": "http://swagger.io"
}
},
{
"name": "store",
"description": "Access to Petstore orders",
"externalDocs": {
"description": "Find out more about our store",
"url": "http://swagger.io"
}
},
{
"name": "user",
"description": "Operations about user"
}
],
"paths": {
"/pet": {
"post": {
"tags": [
"pet"
],
"summary": "Add a new pet to the store",
"description": "Add a new pet to the store",
"operationId": "addPet",
"requestBody": {
"description": "Create a new pet in the store",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Pet"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful operation",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Pet"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"Tag": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"model_type": {
"type": "number"
}
}
},
"Category": {
"type": "object",
"required": [
"model",
"year",
"age"
],
"properties": {
"year": {
"type": "integer",
"format": "int64",
"example": 1
},
"model": {
"type": "string",
"example": "Ford"
},
"age": {
"type": "integer",
"example": 42
}
}
},
"Pet": {
"required": [
"name"
],
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64",
"example": 10
},
"name": {
"type": "string",
"example": "doggie"
},
"category": {
"$ref": "#/components/schemas/Category"
},
"tags": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Tag"
}
},
"status": {
"type": "string",
"description": "pet status in the store",
"enum": [
"available",
"pending",
"sold"
]
}
}
}
}
}
}
"""
```
Executing:
```
spec = OpenAPISpec.from_text(text)
pet_openai_functions, pet_callables = openapi_spec_to_openai_fn(spec)
response = model.invoke("Create a pet named Scott", functions=pet_openai_functions)
```
`pet_open_functions` contains unresolved `$refs`:
```
[
{
"name": "addPet",
"description": "Add a new pet to the store",
"parameters": {
"type": "object",
"properties": {
"json": {
"properties": {
"id": {
"type": "integer",
"schema_format": "int64",
"example": 10
},
"name": {
"type": "string",
"example": "doggie"
},
"category": {
"ref": "#/components/schemas/Category"
},
"tags": {
"items": {
"ref": "#/components/schemas/Tag"
},
"type": "array"
},
"status": {
"type": "string",
"enum": [
"available",
"pending",
"sold"
],
"description": "pet status in the store"
}
},
"type": "object",
"required": [
"name",
"photoUrls"
]
}
}
}
}
]
```
and the generated JSON has an incorrect schema (e.g. category is filled
with `id` and `name` instead of `model`, `year` and `age`:
```
{
"id": 1,
"name": "Scott",
"category": {
"id": 1,
"name": "Dogs"
},
"tags": [
{
"id": 1,
"name": "tag1"
}
],
"status": "available"
}
```
With this change, the generated JSON by the LLM becomes,
`pet_openai_functions` becomes:
```
[
{
"name": "addPet",
"description": "Add a new pet to the store",
"parameters": {
"type": "object",
"properties": {
"json": {
"properties": {
"id": {
"type": "integer",
"schema_format": "int64",
"example": 10
},
"name": {
"type": "string",
"example": "doggie"
},
"category": {
"properties": {
"year": {
"type": "integer",
"schema_format": "int64",
"example": 1
},
"model": {
"type": "string",
"example": "Ford"
},
"age": {
"type": "integer",
"example": 42
}
},
"type": "object",
"required": [
"model",
"year",
"age"
]
},
"tags": {
"items": {
"properties": {
"id": {
"type": "integer",
"schema_format": "int64"
},
"model_type": {
"type": "number"
}
},
"type": "object"
},
"type": "array"
},
"status": {
"type": "string",
"enum": [
"available",
"pending",
"sold"
],
"description": "pet status in the store"
}
},
"type": "object",
"required": [
"name"
]
}
}
}
}
]
```
and the JSON generated by the LLM is:
```
{
"id": 1,
"name": "Scott",
"category": {
"year": 2022,
"model": "Dog",
"age": 42
},
"tags": [
{
"id": 1,
"model_type": 1
}
],
"status": "available"
}
```
which has the intended schema.
- **Twitter handle:**: @brunoalvisio
---------
Co-authored-by: Harrison Chase <hw.chase.17@gmail.com>
94 lines
2.7 KiB
Python
94 lines
2.7 KiB
Python
from pathlib import Path
|
|
|
|
import pytest
|
|
from langchain.chains.openai_functions.openapi import openapi_spec_to_openai_fn
|
|
|
|
from langchain_community.utilities.openapi import ( # noqa: E402 # ignore: community-import
|
|
OpenAPISpec,
|
|
)
|
|
|
|
EXPECTED_OPENAI_FUNCTIONS_HEADER_PARAM = [
|
|
{
|
|
"name": "showPetById",
|
|
"description": "Info for a specific pet",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"headers": {
|
|
"type": "object",
|
|
"properties": {
|
|
"header_param": {
|
|
"type": "string",
|
|
"description": "A header param",
|
|
}
|
|
},
|
|
"required": ["header_param"],
|
|
}
|
|
},
|
|
},
|
|
}
|
|
]
|
|
|
|
|
|
@pytest.mark.requires("openapi_pydantic")
|
|
def test_header_param() -> None:
|
|
spec = OpenAPISpec.from_file(
|
|
Path(__file__).parent.parent
|
|
/ "data"
|
|
/ "openapi_specs"
|
|
/ "openapi_spec_header_param.json",
|
|
)
|
|
|
|
openai_functions, _ = openapi_spec_to_openai_fn(spec)
|
|
|
|
assert openai_functions == EXPECTED_OPENAI_FUNCTIONS_HEADER_PARAM
|
|
|
|
|
|
EXPECTED_OPENAI_FUNCTIONS_NESTED_REF = [
|
|
{
|
|
"name": "addPet",
|
|
"description": "Add a new pet to the store",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"json": {
|
|
"properties": {
|
|
"id": {
|
|
"type": "integer",
|
|
"schema_format": "int64",
|
|
"example": 10,
|
|
},
|
|
"name": {"type": "string", "example": "doggie"},
|
|
"tags": {
|
|
"items": {
|
|
"properties": {
|
|
"id": {"type": "integer", "schema_format": "int64"},
|
|
"model_type": {"type": "number"},
|
|
},
|
|
"type": "object",
|
|
},
|
|
"type": "array",
|
|
},
|
|
},
|
|
"type": "object",
|
|
"required": ["name"],
|
|
}
|
|
},
|
|
},
|
|
}
|
|
]
|
|
|
|
|
|
@pytest.mark.requires("openapi_pydantic")
|
|
def test_nested_ref_in_openapi_spec() -> None:
|
|
spec = OpenAPISpec.from_file(
|
|
Path(__file__).parent.parent
|
|
/ "data"
|
|
/ "openapi_specs"
|
|
/ "openapi_spec_nested_ref.json",
|
|
)
|
|
|
|
openai_functions, _ = openapi_spec_to_openai_fn(spec)
|
|
|
|
assert openai_functions == EXPECTED_OPENAI_FUNCTIONS_NESTED_REF
|