Skip to content
Open
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
7 changes: 5 additions & 2 deletions python/semantic_kernel/functions/kernel_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,12 +451,15 @@ def as_agent_framework_tool(
fields = {}
for param in self.parameters:
if param.include_in_function_choices:
if param.default_value is not None:
# Requiredness is explicit metadata: None can be a legitimate
# default for optional parameters and must remain a Pydantic default.
if param.is_required:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

param.is_required does not distinguish x: str | None from x: str | None = None: the decorator marks both optional. With a nullable annotation but no Python default, this branch now creates Field(default=None), and tool.input_model() accepts the missing required argument. I reproduced this with a decorated def f(x: str | None). Preserve the missing-default distinction from the callable signature (or metadata) and add a regression for nullable-but-required parameters.

fields[param.name] = (param.type_, Field(description=param.description))
else:
fields[param.name] = (
param.type_,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preserving the None default also needs a nullable field annotation. For the new test function, metadata stores label as type_="str"; the generated model therefore advertises a string-only field with default null, and tool.input_model(label=None) raises ValidationError. The current test checks the default object but never validates an explicit None. Please retain the nullable annotation and cover both omitted and explicitly-null values.

Field(description=param.description, default=param.default_value),
)
fields[param.name] = (param.type_, Field(description=param.description))
input_model = create_model("InputModel", **fields) # type: ignore
Comment thread
ump45nose marked this conversation as resolved.
Resolved

async def wrapper(*args: Any, **kwargs: Any) -> Any:
Expand Down
22 changes: 22 additions & 0 deletions python/tests/unit/functions/test_kernel_function_from_method.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,28 @@ def decorated_function() -> None:
assert len(native_function.parameters) == 0


def test_as_agent_framework_tool_preserves_optional_parameter_default(monkeypatch):
class MockAIFunction:
def __init__(self, *, input_model: Any, **kwargs: Any):
self.input_model = input_model

monkeypatch.setitem(__import__("sys").modules, "agent_framework", Mock(AIFunction=MockAIFunction))

@kernel_function()
def function(count: int = 5, label: str | None = None) -> int:
return count

native_function = KernelFunction.from_method(method=function, plugin_name="MockPlugin")
tool = native_function.as_agent_framework_tool(kernel=Mock())
count_field = tool.input_model.model_fields["count"]
label_field = tool.input_model.model_fields["label"]

assert count_field.default == 5
assert not count_field.is_required()
assert label_field.default is None
assert not label_field.is_required()


def test_init_method_is_none():
with pytest.raises(FunctionInitializationError):
KernelFunction.from_method(method=None, plugin_name="MockPlugin")
Expand Down
Loading