458 lines
13 KiB
Python
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
|