mirror of
https://github.com/open-webui/mcpo
synced 2025-06-26 18:26:58 +00:00
feat: support AnyOf
and Null
This commit is contained in:
parent
393f2e724b
commit
603327c853
@ -1,6 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from typing import Any, List, Dict
|
from typing import Any, List, Dict, Union
|
||||||
|
|
||||||
from mcpo.utils.main import _process_schema_property
|
from mcpo.utils.main import _process_schema_property
|
||||||
|
|
||||||
@ -64,6 +64,17 @@ def test_process_simple_number():
|
|||||||
assert result_field.default == expected_field.default
|
assert result_field.default == expected_field.default
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_null():
|
||||||
|
schema = {"type": "null"}
|
||||||
|
expected_type = None
|
||||||
|
expected_field = Field(default=..., description="")
|
||||||
|
result_type, result_field = _process_schema_property(
|
||||||
|
_model_cache, schema, "test", "prop", True
|
||||||
|
)
|
||||||
|
assert result_type == expected_type
|
||||||
|
assert result_field.default == expected_field.default
|
||||||
|
|
||||||
|
|
||||||
def test_process_unknown_type():
|
def test_process_unknown_type():
|
||||||
schema = {"type": "unknown"}
|
schema = {"type": "unknown"}
|
||||||
expected_type = Any
|
expected_type = Any
|
||||||
@ -226,8 +237,11 @@ def test_model_caching():
|
|||||||
assert len(_model_cache) == 2 # Only two unique models created
|
assert len(_model_cache) == 2 # Only two unique models created
|
||||||
|
|
||||||
|
|
||||||
def test_multi_type_property():
|
def test_multi_type_property_with_list():
|
||||||
schema = {"type": ["string", "number"], "description": "A property with multiple types"}
|
schema = {
|
||||||
|
"type": ["string", "number"],
|
||||||
|
"description": "A property with multiple types",
|
||||||
|
}
|
||||||
expected_field = Field(default=..., description="A property with multiple types")
|
expected_field = Field(default=..., description="A property with multiple types")
|
||||||
result_type, result_field = _process_schema_property(
|
result_type, result_field = _process_schema_property(
|
||||||
_model_cache, schema, "test", "multi_type", True
|
_model_cache, schema, "test", "multi_type", True
|
||||||
@ -243,3 +257,56 @@ def test_multi_type_property():
|
|||||||
# Check field properties
|
# Check field properties
|
||||||
assert result_field.default == expected_field.default
|
assert result_field.default == expected_field.default
|
||||||
assert result_field.description == expected_field.description
|
assert result_field.description == expected_field.description
|
||||||
|
|
||||||
|
|
||||||
|
def test_multi_type_property_with_any_of():
|
||||||
|
schema = {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The name of the function to call",
|
||||||
|
},
|
||||||
|
"arguments": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The arguments to pass to the function, as a JSON string",
|
||||||
|
"default": "{}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["name"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"function_id": {
|
||||||
|
"type": "int",
|
||||||
|
"description": "The id of the function to call",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["function_id"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["auto", "none"],
|
||||||
|
"description": "Control function calling behavior",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"description": "A property with multiple types",
|
||||||
|
}
|
||||||
|
result_type, result_field = _process_schema_property(
|
||||||
|
_model_cache, schema, "test", "multi_type", True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if the resulting type is a Union
|
||||||
|
assert result_type.__origin__ == Union
|
||||||
|
|
||||||
|
# Check if the Union has the correct number of types
|
||||||
|
assert len(result_type.__args__) == 3
|
||||||
|
assert len(result_type.__args__[0].model_fields) == 2
|
||||||
|
assert len(result_type.__args__[1].model_fields) == 1
|
||||||
|
assert result_type.__args__[2] is str
|
||||||
|
|
||||||
|
# assert result_field parameter config
|
||||||
|
assert result_field.description == "A property with multiple types"
|
||||||
|
@ -3,7 +3,14 @@ from pydantic import create_model, Field
|
|||||||
from pydantic.fields import FieldInfo
|
from pydantic.fields import FieldInfo
|
||||||
from mcp import ClientSession, types
|
from mcp import ClientSession, types
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from mcp.types import CallToolResult, PARSE_ERROR, INVALID_REQUEST, METHOD_NOT_FOUND, INVALID_PARAMS, INTERNAL_ERROR
|
from mcp.types import (
|
||||||
|
CallToolResult,
|
||||||
|
PARSE_ERROR,
|
||||||
|
INVALID_REQUEST,
|
||||||
|
METHOD_NOT_FOUND,
|
||||||
|
INVALID_PARAMS,
|
||||||
|
INTERNAL_ERROR,
|
||||||
|
)
|
||||||
from mcp.shared.exceptions import McpError
|
from mcp.shared.exceptions import McpError
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@ -58,6 +65,21 @@ def _process_schema_property(
|
|||||||
default_value = ... if is_required else prop_schema.get("default", None)
|
default_value = ... if is_required else prop_schema.get("default", None)
|
||||||
pydantic_field = Field(default=default_value, description=prop_desc)
|
pydantic_field = Field(default=default_value, description=prop_desc)
|
||||||
|
|
||||||
|
# Handle the case where prop_type is missing but 'anyOf' key exists
|
||||||
|
# In this case, use data type from 'anyOf' to determine the type hint
|
||||||
|
if "anyOf" in prop_schema:
|
||||||
|
type_hints = []
|
||||||
|
for i, schema_option in enumerate(prop_schema["anyOf"]):
|
||||||
|
type_hint, _ = _process_schema_property(
|
||||||
|
_model_cache,
|
||||||
|
schema_option,
|
||||||
|
f"{model_name_prefix}_{prop_name}",
|
||||||
|
f"choice_{i}",
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
type_hints.append(type_hint)
|
||||||
|
return Union[tuple(type_hints)], pydantic_field
|
||||||
|
|
||||||
# Handle the case where prop_type is a list of types, e.g. ['string', 'number']
|
# Handle the case where prop_type is a list of types, e.g. ['string', 'number']
|
||||||
if isinstance(prop_type, list):
|
if isinstance(prop_type, list):
|
||||||
# Create a Union of all the types
|
# Create a Union of all the types
|
||||||
@ -127,6 +149,8 @@ def _process_schema_property(
|
|||||||
return bool, pydantic_field
|
return bool, pydantic_field
|
||||||
elif prop_type == "number":
|
elif prop_type == "number":
|
||||||
return float, pydantic_field
|
return float, pydantic_field
|
||||||
|
elif prop_type == "null":
|
||||||
|
return None, pydantic_field
|
||||||
else:
|
else:
|
||||||
return Any, pydantic_field
|
return Any, pydantic_field
|
||||||
|
|
||||||
@ -174,7 +198,9 @@ def get_tool_handler(session, endpoint_name, form_model_name, model_fields):
|
|||||||
)
|
)
|
||||||
|
|
||||||
response_data = process_tool_response(result)
|
response_data = process_tool_response(result)
|
||||||
final_response = response_data[0] if len(response_data) == 1 else response_data
|
final_response = (
|
||||||
|
response_data[0] if len(response_data) == 1 else response_data
|
||||||
|
)
|
||||||
return final_response
|
return final_response
|
||||||
|
|
||||||
except McpError as e:
|
except McpError as e:
|
||||||
@ -222,7 +248,9 @@ def get_tool_handler(session, endpoint_name, form_model_name, model_fields):
|
|||||||
)
|
)
|
||||||
|
|
||||||
response_data = process_tool_response(result)
|
response_data = process_tool_response(result)
|
||||||
final_response = response_data[0] if len(response_data) == 1 else response_data
|
final_response = (
|
||||||
|
response_data[0] if len(response_data) == 1 else response_data
|
||||||
|
)
|
||||||
return final_response
|
return final_response
|
||||||
|
|
||||||
except McpError as e:
|
except McpError as e:
|
||||||
|
Loading…
Reference in New Issue
Block a user