mirror of https://github.com/langgenius/dify.git
fix: fix OpenAPI Schema Import Pydantic Validation Errors for Complex Default Values (#27159)
Co-authored-by: Alain <yinxulai@hoymail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
bebb4ffbaa
commit
26ff59172e
|
|
@ -76,7 +76,7 @@ class PluginParameter(BaseModel):
|
||||||
auto_generate: PluginParameterAutoGenerate | None = None
|
auto_generate: PluginParameterAutoGenerate | None = None
|
||||||
template: PluginParameterTemplate | None = None
|
template: PluginParameterTemplate | None = None
|
||||||
required: bool = False
|
required: bool = False
|
||||||
default: Union[float, int, str] | None = None
|
default: Union[float, int, str, bool] | None = None
|
||||||
min: Union[float, int] | None = None
|
min: Union[float, int] | None = None
|
||||||
max: Union[float, int] | None = None
|
max: Union[float, int] | None = None
|
||||||
precision: int | None = None
|
precision: int | None = None
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,11 @@ class ApiBasedToolSchemaParser:
|
||||||
root = root[ref]
|
root = root[ref]
|
||||||
interface["operation"]["parameters"][i] = root
|
interface["operation"]["parameters"][i] = root
|
||||||
for parameter in interface["operation"]["parameters"]:
|
for parameter in interface["operation"]["parameters"]:
|
||||||
|
# Handle complex type defaults that are not supported by PluginParameter
|
||||||
|
default_value = None
|
||||||
|
if "schema" in parameter and "default" in parameter["schema"]:
|
||||||
|
default_value = ApiBasedToolSchemaParser._sanitize_default_value(parameter["schema"]["default"])
|
||||||
|
|
||||||
tool_parameter = ToolParameter(
|
tool_parameter = ToolParameter(
|
||||||
name=parameter["name"],
|
name=parameter["name"],
|
||||||
label=I18nObject(en_US=parameter["name"], zh_Hans=parameter["name"]),
|
label=I18nObject(en_US=parameter["name"], zh_Hans=parameter["name"]),
|
||||||
|
|
@ -72,9 +77,7 @@ class ApiBasedToolSchemaParser:
|
||||||
required=parameter.get("required", False),
|
required=parameter.get("required", False),
|
||||||
form=ToolParameter.ToolParameterForm.LLM,
|
form=ToolParameter.ToolParameterForm.LLM,
|
||||||
llm_description=parameter.get("description"),
|
llm_description=parameter.get("description"),
|
||||||
default=parameter["schema"]["default"]
|
default=default_value,
|
||||||
if "schema" in parameter and "default" in parameter["schema"]
|
|
||||||
else None,
|
|
||||||
placeholder=I18nObject(
|
placeholder=I18nObject(
|
||||||
en_US=parameter.get("description", ""), zh_Hans=parameter.get("description", "")
|
en_US=parameter.get("description", ""), zh_Hans=parameter.get("description", "")
|
||||||
),
|
),
|
||||||
|
|
@ -134,6 +137,11 @@ class ApiBasedToolSchemaParser:
|
||||||
required = body_schema.get("required", [])
|
required = body_schema.get("required", [])
|
||||||
properties = body_schema.get("properties", {})
|
properties = body_schema.get("properties", {})
|
||||||
for name, property in properties.items():
|
for name, property in properties.items():
|
||||||
|
# Handle complex type defaults that are not supported by PluginParameter
|
||||||
|
default_value = ApiBasedToolSchemaParser._sanitize_default_value(
|
||||||
|
property.get("default", None)
|
||||||
|
)
|
||||||
|
|
||||||
tool = ToolParameter(
|
tool = ToolParameter(
|
||||||
name=name,
|
name=name,
|
||||||
label=I18nObject(en_US=name, zh_Hans=name),
|
label=I18nObject(en_US=name, zh_Hans=name),
|
||||||
|
|
@ -144,12 +152,11 @@ class ApiBasedToolSchemaParser:
|
||||||
required=name in required,
|
required=name in required,
|
||||||
form=ToolParameter.ToolParameterForm.LLM,
|
form=ToolParameter.ToolParameterForm.LLM,
|
||||||
llm_description=property.get("description", ""),
|
llm_description=property.get("description", ""),
|
||||||
default=property.get("default", None),
|
default=default_value,
|
||||||
placeholder=I18nObject(
|
placeholder=I18nObject(
|
||||||
en_US=property.get("description", ""), zh_Hans=property.get("description", "")
|
en_US=property.get("description", ""), zh_Hans=property.get("description", "")
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# check if there is a type
|
# check if there is a type
|
||||||
typ = ApiBasedToolSchemaParser._get_tool_parameter_type(property)
|
typ = ApiBasedToolSchemaParser._get_tool_parameter_type(property)
|
||||||
if typ:
|
if typ:
|
||||||
|
|
@ -197,6 +204,22 @@ class ApiBasedToolSchemaParser:
|
||||||
|
|
||||||
return bundles
|
return bundles
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _sanitize_default_value(value):
|
||||||
|
"""
|
||||||
|
Sanitize default values for PluginParameter compatibility.
|
||||||
|
Complex types (list, dict) are converted to None to avoid validation errors.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: The default value from OpenAPI schema
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None for complex types (list, dict), otherwise the original value
|
||||||
|
"""
|
||||||
|
if isinstance(value, (list, dict)):
|
||||||
|
return None
|
||||||
|
return value
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_tool_parameter_type(parameter: dict) -> ToolParameter.ToolParameterType | None:
|
def _get_tool_parameter_type(parameter: dict) -> ToolParameter.ToolParameterType | None:
|
||||||
parameter = parameter or {}
|
parameter = parameter or {}
|
||||||
|
|
@ -217,7 +240,11 @@ class ApiBasedToolSchemaParser:
|
||||||
return ToolParameter.ToolParameterType.STRING
|
return ToolParameter.ToolParameterType.STRING
|
||||||
elif typ == "array":
|
elif typ == "array":
|
||||||
items = parameter.get("items") or parameter.get("schema", {}).get("items")
|
items = parameter.get("items") or parameter.get("schema", {}).get("items")
|
||||||
return ToolParameter.ToolParameterType.FILES if items and items.get("format") == "binary" else None
|
if items and items.get("format") == "binary":
|
||||||
|
return ToolParameter.ToolParameterType.FILES
|
||||||
|
else:
|
||||||
|
# For regular arrays, return ARRAY type instead of None
|
||||||
|
return ToolParameter.ToolParameterType.ARRAY
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -109,3 +109,83 @@ def test_parse_openapi_to_tool_bundle_properties_all_of(app):
|
||||||
assert tool_bundles[0].parameters[0].llm_description == "desc prop1"
|
assert tool_bundles[0].parameters[0].llm_description == "desc prop1"
|
||||||
# TODO: support enum in OpenAPI
|
# TODO: support enum in OpenAPI
|
||||||
# assert set(tool_bundles[0].parameters[0].options) == {"option1", "option2", "option3"}
|
# assert set(tool_bundles[0].parameters[0].options) == {"option1", "option2", "option3"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_openapi_to_tool_bundle_default_value_type_casting(app):
|
||||||
|
"""
|
||||||
|
Test that default values are properly cast to match parameter types.
|
||||||
|
This addresses the issue where array default values like [] cause validation errors
|
||||||
|
when parameter type is inferred as string/number/boolean.
|
||||||
|
"""
|
||||||
|
openapi = {
|
||||||
|
"openapi": "3.0.0",
|
||||||
|
"info": {"title": "Test API", "version": "1.0.0"},
|
||||||
|
"servers": [{"url": "https://example.com"}],
|
||||||
|
"paths": {
|
||||||
|
"/product/create": {
|
||||||
|
"post": {
|
||||||
|
"operationId": "createProduct",
|
||||||
|
"summary": "Create a product",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"categories": {
|
||||||
|
"description": "List of category identifiers",
|
||||||
|
"default": [],
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"description": "Product name",
|
||||||
|
"default": "Default Product",
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"price": {"description": "Product price", "default": 0.0, "type": "number"},
|
||||||
|
"available": {
|
||||||
|
"description": "Product availability",
|
||||||
|
"default": True,
|
||||||
|
"type": "boolean",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {"200": {"description": "Default Response"}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
with app.test_request_context():
|
||||||
|
tool_bundles = ApiBasedToolSchemaParser.parse_openapi_to_tool_bundle(openapi)
|
||||||
|
|
||||||
|
assert len(tool_bundles) == 1
|
||||||
|
bundle = tool_bundles[0]
|
||||||
|
assert len(bundle.parameters) == 4
|
||||||
|
|
||||||
|
# Find parameters by name
|
||||||
|
params_by_name = {param.name: param for param in bundle.parameters}
|
||||||
|
|
||||||
|
# Check categories parameter (array type with [] default)
|
||||||
|
categories_param = params_by_name["categories"]
|
||||||
|
assert categories_param.type == "array" # Will be detected by _get_tool_parameter_type
|
||||||
|
assert categories_param.default is None # Array default [] is converted to None
|
||||||
|
|
||||||
|
# Check name parameter (string type with string default)
|
||||||
|
name_param = params_by_name["name"]
|
||||||
|
assert name_param.type == "string"
|
||||||
|
assert name_param.default == "Default Product"
|
||||||
|
|
||||||
|
# Check price parameter (number type with number default)
|
||||||
|
price_param = params_by_name["price"]
|
||||||
|
assert price_param.type == "number"
|
||||||
|
assert price_param.default == 0.0
|
||||||
|
|
||||||
|
# Check available parameter (boolean type with boolean default)
|
||||||
|
available_param = params_by_name["available"]
|
||||||
|
assert available_param.type == "boolean"
|
||||||
|
assert available_param.default is True
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue