mirror of
https://github.com/microsoft/semantic-kernel.git
synced 2026-10-12 11:19:56 +00:00
### 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 😄
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
|