Files
semantic-kernel/python/tests/unit/schema/test_schema_builder.py
Eduard van Valkenburg e0042afa80 Python: graduate filters, add exception during addition and some cleanup (#9856)
### Motivation and Context

<!-- Thank you for your contribution to the semantic-kernel repo!
Please help reviewers and future users, providing the following
information:
  1. Why is this change required?
  2. What problem does it solve?
  3. What scenario does it contribute to?
  4. If it fixes an open issue, please link to the issue here.
-->
This PR graduates the filters.

This includes some updates to the docstrings.
And adds a specific Exception for errors during adding or removing of
filters.

Closes #9838 
Fixes #9641 

### Description

<!-- Describe your changes, the overall approach, the underlying design.
These notes will help understanding how your code works. Thanks! -->

### Contribution Checklist

<!-- Before submitting this PR, please make sure: -->

- [x] The code builds clean without any errors or warnings
- [x] The PR follows the [SK Contribution
Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md)
and the [pre-submission formatting
script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts)
raises no violations
- [x] All unit tests pass, and I have added new tests where possible
- [x] I didn't break anyone 😄
2024-12-03 17:40:01 +00: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