Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions python/semantic_kernel/schema/kernel_json_schema_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import sys
import types
from enum import Enum
from typing import Any, Union, get_args, get_origin, get_type_hints
from typing import Any, Literal, Union, get_args, get_origin, get_type_hints

from semantic_kernel.const import PARSED_ANNOTATION_UNION_DELIMITER
from semantic_kernel.exceptions.function_exceptions import FunctionInvalidParameterConfiguration
Expand Down Expand Up @@ -196,12 +196,23 @@ def handle_complex_type(
if structured_output:
schema["additionalProperties"] = False
return schema
if origin is Literal:
values = [arg.value if isinstance(arg, Enum) else arg for arg in args]
value_types = {"null" if v is None else TYPE_MAPPING.get(type(v), "string") for v in values}
ordered_types = sorted(value_types, key=lambda t: (t == "null", t))
schema = {"type": ordered_types[0] if len(ordered_types) == 1 else ordered_types, "enum": values}
if description:
schema["description"] = description
return schema
if origin in {Union, types.UnionType}:
# Handle Optional[T] (Union[T, None]) by making schema nullable
if len(args) == 2 and type(None) in args:
non_none_type = args[0] if args[1] is type(None) else args[1]
schema = cls.build(non_none_type, structured_output=structured_output)
schema["type"] = [schema["type"], "null"]
schema_types = schema["type"] if isinstance(schema["type"], list) else [schema["type"]]
schema["type"] = schema_types if "null" in schema_types else [*schema_types, "null"]
if "enum" in schema and None not in schema["enum"]:
schema["enum"] = [*schema["enum"], None]
if description:
schema["description"] = description
if structured_output:
Expand Down
46 changes: 45 additions & 1 deletion python/tests/unit/schema/test_schema_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import json
from enum import Enum
from typing import Annotated, Any, Optional, Union
from typing import Annotated, Any, Literal, Optional, Union
from unittest.mock import Mock

import pytest
Expand Down Expand Up @@ -455,3 +455,47 @@ def test_build_schema_with_nonpydantic_structured_output():
}

assert structured_output_schema == expected_schema


class LiteralModel(KernelBaseModel):
level: Literal["low", "high"]
maybe: Literal["a", "b"] | None = None


@pytest.mark.parametrize(
("literal_type", "expected"),
[
(Literal["fast", "slow"], {"type": "string", "enum": ["fast", "slow"]}),
(Literal[1, 2], {"type": "integer", "enum": [1, 2]}),
(Literal[True], {"type": "boolean", "enum": [True]}),
(Literal["a", 1], {"type": ["integer", "string"], "enum": ["a", 1]}),
(Optional[Literal["fast", "slow"]], {"type": ["string", "null"], "enum": ["fast", "slow", None]}), # noqa: UP045
(Literal["fast", "slow"] | None, {"type": ["string", "null"], "enum": ["fast", "slow", None]}),
(Optional[Literal["a", None]], {"type": ["string", "null"], "enum": ["a", None]}), # noqa: UP045
(list[Literal["a", "b"]], {"type": "array", "items": {"type": "string", "enum": ["a", "b"]}}),
],
)
def test_build_literal(literal_type, expected):
assert KernelJsonSchemaBuilder.build(literal_type) == expected


def test_build_literal_with_description():
schema = KernelJsonSchemaBuilder.build(Literal["fast", "slow"], "speed mode")
assert schema == {"type": "string", "enum": ["fast", "slow"], "description": "speed mode"}


def test_build_literal_enum_member():
schema = KernelJsonSchemaBuilder.build(Literal[EnumTest.OPTION_A])
assert schema == {"type": "string", "enum": ["OptionA"]}


def test_build_model_with_literal_fields():
schema = KernelJsonSchemaBuilder.build(LiteralModel)
assert schema == {
"type": "object",
"properties": {
"level": {"type": "string", "enum": ["low", "high"]},
"maybe": {"type": ["string", "null"], "enum": ["a", "b", None]},
},
"required": ["level"],
}
Loading