Files
microsoft--semantic-kernel/python/tests/unit/schema/test_schema_builder.py
T
wehub-resource-sync b957a53def
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:21:23 +08:00

458 lines
13 KiB
Python

# Copyright (c) Microsoft. All rights reserved.
import json
from enum import Enum
from typing import Annotated, Any, Optional, Union
from unittest.mock import Mock
import pytest
from semantic_kernel.connectors.utils.structured_output_schema import generate_structured_output_response_format_schema
from semantic_kernel.kernel_pydantic import KernelBaseModel
from semantic_kernel.schema.kernel_json_schema_builder import KernelJsonSchemaBuilder
class ExampleModel(KernelBaseModel):
name: str
age: int
class AnotherModel:
title: str
score: float
class MockClass:
name: str = None
age: int = None
class ModelWithOptionalAttributes:
name: str | None = None
class ModelWithUnionPrimitives:
item: int | str
class EnumTest(Enum):
OPTION_A = "OptionA"
OPTION_B = "OptionB"
OPTION_C = "OptionC"
class MockModel:
__annotations__ = {
"id": int,
"name": str,
"is_active": bool,
"scores": list[int],
"metadata": dict[str, Any],
"tags": set[str],
"coordinates": tuple[int, int],
"status": Union[int, str],
"optional_field": Optional[str],
}
model_fields = {
"id": Mock(description="The ID of the model"),
"name": Mock(description="The name of the model"),
"is_active": Mock(description="Whether the model is active"),
"tags": Mock(description="Tags associated with the model"),
"status": Mock(description="The status of the model, either as an integer or a string"),
"scores": Mock(description="The scores associated with the model"),
"optional_field": Mock(description="An optional field that can be null"),
"metadata": Mock(description="The optional metadata description"),
"coordinates": Mock(description="The coordinates of the model"),
}
class PydanticStep(KernelBaseModel):
explanation: str
output: str
class PydanticReasoning(KernelBaseModel):
steps: list[PydanticStep]
final_answer: str
class NonPydanticStep:
explanation: str
output: str
class NonPydanticReasoning:
steps: list[NonPydanticStep]
final_answer: str
def test_build_with_kernel_base_model():
expected_schema = {
"type": "object",
"properties": {"name": {"type": "string"}, "age": {"type": "integer"}},
"required": ["name", "age"],
}
result = KernelJsonSchemaBuilder.build(ExampleModel)
assert result == expected_schema
def test_build_with_model_with_optional_attributes():
expected_schema = {
"type": "object",
"properties": {"name": {"type": ["string", "null"]}},
}
result = KernelJsonSchemaBuilder.build(ModelWithOptionalAttributes)
assert result == expected_schema
def test_build_with_model_with_union_attributes():
expected_schema = {
"type": "object",
"properties": {"item": {"anyOf": [{"type": "integer"}, {"type": "string"}]}},
"required": ["item"],
}
result = KernelJsonSchemaBuilder.build(ModelWithUnionPrimitives)
assert result == expected_schema
def test_build_with_model_with_annotations():
expected_schema = {
"type": "object",
"properties": {"title": {"type": "string"}, "score": {"type": "number"}},
"required": ["title", "score"],
}
result = KernelJsonSchemaBuilder.build(AnotherModel)
assert result == expected_schema
def test_build_with_primitive_type():
expected_schema = {"type": "string"}
result = KernelJsonSchemaBuilder.build(str)
assert result == expected_schema
result = KernelJsonSchemaBuilder.build("str")
assert result == expected_schema
expected_schema = {"type": "integer"}
result = KernelJsonSchemaBuilder.build(int)
assert result == expected_schema
result = KernelJsonSchemaBuilder.build("int")
assert result == expected_schema
def test_build_with_primitive_type_and_description():
expected_schema = {"type": "string", "description": "A simple string"}
result = KernelJsonSchemaBuilder.build(str, description="A simple string")
assert result == expected_schema
def test_build_model_schema():
expected_schema = {
"type": "object",
"properties": {"name": {"type": "string"}, "age": {"type": "integer"}},
"required": ["name", "age"],
"description": "A model",
}
result = KernelJsonSchemaBuilder.build_model_schema(ExampleModel, description="A model")
assert result == expected_schema
def test_build_from_type_name():
expected_schema = {"type": "string", "description": "A simple string"}
result = KernelJsonSchemaBuilder.build_from_type_name("str", description="A simple string")
assert result == expected_schema
def test_build_from_type_name_with_union():
expected_schema = {
"anyOf": [
{"type": "string", "description": "The value"},
{"type": "integer", "description": "The value"},
]
}
result = KernelJsonSchemaBuilder.build_from_type_name("str, int", description="The value")
assert result == expected_schema
def test_get_json_schema():
expected_schema = {"type": "string"}
result = KernelJsonSchemaBuilder.get_json_schema(str)
assert result == expected_schema
expected_schema = {"type": "integer"}
result = KernelJsonSchemaBuilder.get_json_schema(int)
assert result == expected_schema
def test_build_list():
schema = KernelJsonSchemaBuilder.build(list[str])
assert schema == {"type": "array", "items": {"type": "string"}}
def test_build_list_complex_type():
schema = KernelJsonSchemaBuilder.build(list[MockClass])
assert schema == {
"type": "array",
"items": {
"type": "object",
"properties": {"name": {"type": "string"}, "age": {"type": "integer"}},
"required": ["name", "age"],
},
}
def test_build_dict():
schema = KernelJsonSchemaBuilder.build(dict[str, int])
assert schema == {"type": "object", "additionalProperties": {"type": "integer"}}
def test_build_set():
schema = KernelJsonSchemaBuilder.build(set[int])
assert schema == {"type": "array", "items": {"type": "integer"}}
def test_build_tuple():
schema = KernelJsonSchemaBuilder.build(tuple[int, str])
assert schema == {
"type": "array",
"items": [{"type": "integer"}, {"type": "string"}],
}
def test_build_union():
schema = KernelJsonSchemaBuilder.build(Union[int, str])
assert schema == {"anyOf": [{"type": "integer"}, {"type": "string"}]}
def test_build_optional():
schema = KernelJsonSchemaBuilder.build(Optional[int])
assert schema == {"type": ["integer", "null"]}
def test_build_model_schema_for_many_types():
schema = KernelJsonSchemaBuilder.build(MockModel)
expected = """
{
"type": "object",
"properties":
{
"id":
{
"type": "integer",
"description": "The ID of the model"
},
"name":
{
"type": "string",
"description": "The name of the model"
},
"is_active":
{
"type": "boolean",
"description": "Whether the model is active"
},
"scores":
{
"type": "array",
"items":
{
"type": "integer"
},
"description": "The scores associated with the model"
},
"metadata":
{
"type": "object",
"additionalProperties":
{
"type": "object",
"properties":
{}
},
"description": "The optional metadata description"
},
"tags":
{
"type": "array",
"items":
{
"type": "string"
},
"description": "Tags associated with the model"
},
"coordinates":
{
"type": "array",
"items":
[
{
"type": "integer"
},
{
"type": "integer"
}
],
"description": "The coordinates of the model"
},
"status":
{
"anyOf":
[
{
"type": "integer",
"description": "The status of the model, either as an integer or a string"
},
{
"type": "string",
"description": "The status of the model, either as an integer or a string"
}
]
},
"optional_field": {
"description": "An optional field that can be null",
"type": ["string", "null"]
}
},
"required":
[
"id",
"name",
"is_active",
"scores",
"metadata",
"tags",
"coordinates",
"status"
]
}
"""
expected_schema = json.loads(expected)
assert schema == expected_schema
@pytest.mark.parametrize(
"type_name, expected",
[
("int", {"type": "integer"}),
("str", {"type": "string"}),
("bool", {"type": "boolean"}),
("float", {"type": "number"}),
("list", {"type": "array"}),
("dict", {"type": "object"}),
("object", {"type": "object"}),
("array", {"type": "array"}),
],
)
def test_build_from_many_type_names(type_name, expected):
assert KernelJsonSchemaBuilder.build_from_type_name(type_name) == expected
@pytest.mark.parametrize(
"type_obj, expected",
[
(int, {"type": "integer"}),
(str, {"type": "string"}),
(bool, {"type": "boolean"}),
(float, {"type": "number"}),
],
)
def test_get_json_schema_multiple(type_obj, expected):
assert KernelJsonSchemaBuilder.get_json_schema(type_obj) == expected
class Items(KernelBaseModel):
title: Annotated[str, "Description of the item"]
resource: Annotated[int, "Number of turns required for the item"]
def test_build_complex_type_list():
schema = KernelJsonSchemaBuilder.build(list[Items])
assert schema is not None
def test_enum_schema():
schema = KernelJsonSchemaBuilder.build(EnumTest, "Test Enum Description")
expected_schema = {
"type": "string",
"enum": ["OptionA", "OptionB", "OptionC"],
"description": "Test Enum Description",
}
assert schema == expected_schema
def test_enum_schema_without_description():
schema = KernelJsonSchemaBuilder.build(EnumTest)
expected_schema = {"type": "string", "enum": ["OptionA", "OptionB", "OptionC"]}
assert schema == expected_schema
def test_handle_complex_type():
schema = KernelJsonSchemaBuilder.handle_complex_type(str, "Description")
expected_schema = {"type": "string", "description": "Description"}
assert schema == expected_schema
def test_build_schema_with_pydantic_structured_output():
schema = KernelJsonSchemaBuilder.build(parameter_type=PydanticReasoning, structured_output=True)
structured_output_schema = generate_structured_output_response_format_schema(name="Reasoning", schema=schema)
expected_schema = {
"type": "json_schema",
"json_schema": {
"name": "Reasoning",
"schema": {
"type": "object",
"properties": {
"steps": {
"type": "array",
"items": {
"type": "object",
"properties": {"explanation": {"type": "string"}, "output": {"type": "string"}},
"required": ["explanation", "output"],
"additionalProperties": False,
},
},
"final_answer": {"type": "string"},
},
"required": ["steps", "final_answer"],
"additionalProperties": False,
},
"strict": True,
},
}
assert structured_output_schema == expected_schema
def test_build_schema_with_nonpydantic_structured_output():
schema = KernelJsonSchemaBuilder.build(parameter_type=NonPydanticReasoning, structured_output=True)
structured_output_schema = generate_structured_output_response_format_schema(
name="NonPydanticReasoning", schema=schema
)
expected_schema = {
"type": "json_schema",
"json_schema": {
"name": "NonPydanticReasoning",
"schema": {
"type": "object",
"properties": {
"steps": {
"type": "array",
"items": {
"type": "object",
"properties": {"explanation": {"type": "string"}, "output": {"type": "string"}},
"required": ["explanation", "output"],
"additionalProperties": False,
},
},
"final_answer": {"type": "string"},
},
"required": ["steps", "final_answer"],
"additionalProperties": False,
},
"strict": True,
},
}
assert structured_output_schema == expected_schema