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
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ def kernel_function(
if you set `{"include_in_function_choices": False}` in the annotation, that parameter will not be included in
the representation of the function towards LLM's or MCP Servers. If you do set this and the parameter is required
but you do not set it in a invoke level arguments, the function will raise an error.
During auto function calling, a tool call from the model that sets such a parameter is rejected as an
unexpected argument, so the value can only come from invoke level arguments or the parameter default.

After the annotations, it checks recursively until it reaches the lowest level, and it combines
the types into a single comma-separated string, a forwardRef is also supported.
Expand Down
6 changes: 5 additions & 1 deletion python/semantic_kernel/kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,10 +376,14 @@ async def invoke_function_call(
required_param_names = {
param.name for param in function_to_call.parameters if param.name is not None and param.is_required
}
# Parameters hidden from the model (include_in_function_choices=False) can only be set by the host.
model_param_names = {
param.name for param in function_to_call.parameters if param.include_in_function_choices
}
received_param_names = set(parsed_args or {})

missing_params = required_param_names - received_param_names
unexpected_params = received_param_names - {param.name for param in function_to_call.parameters}
unexpected_params = received_param_names - model_param_names

if missing_params or unexpected_params:
msg_parts = []
Expand Down
58 changes: 57 additions & 1 deletion python/tests/unit/kernel/test_kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from copy import deepcopy
from dataclasses import dataclass
from pathlib import Path
from typing import Union
from typing import Annotated, Union
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
Expand Down Expand Up @@ -682,6 +682,62 @@ async def test_invoke_function_call_with_missing_or_unexpected_args(kernel: Kern
), "Expected fallback message not found in chat history."


@pytest.mark.parametrize(
"model_arguments",
[{"value": "visible", "scope": "model-value"}, '{"value": "visible", "scope": "model-value"}'],
ids=["dict", "json"],
)
async def test_invoke_function_call_rejects_model_supplied_hidden_parameter(kernel: Kernel, model_arguments):
calls: list[str] = []

class TestPlugin:
@kernel_function
def echo(
self,
value: str,
scope: Annotated[str, "Host supplied", {"include_in_function_choices": False}] = "default",
) -> str:
calls.append(scope)
return f"{value}:{scope}"

kernel.add_plugin(TestPlugin(), plugin_name="test")
chat_history = ChatHistory()

result = await kernel.invoke_function_call(
function_call=FunctionCallContent(name="test-echo", arguments=model_arguments),
chat_history=chat_history,
arguments=KernelArguments(scope="host-value"),
function_behavior=FunctionChoiceBehavior.Auto(),
)

assert result is None
assert calls == []
assert "Received unexpected argument(s): ['scope']" in chat_history.messages[-1].items[0].result


async def test_invoke_function_call_keeps_host_supplied_hidden_parameter(kernel: Kernel):
class TestPlugin:
@kernel_function
def echo(
self,
value: str,
scope: Annotated[str, "Host supplied", {"include_in_function_choices": False}] = "default",
) -> str:
return f"{value}:{scope}"

kernel.add_plugin(TestPlugin(), plugin_name="test")
chat_history = ChatHistory()

await kernel.invoke_function_call(
function_call=FunctionCallContent(name="test-echo", arguments={"value": "visible"}),
chat_history=chat_history,
arguments=KernelArguments(scope="host-value"),
function_behavior=FunctionChoiceBehavior.Auto(),
)

assert chat_history.messages[-1].items[0].result == "visible:host-value"


async def test_invoke_function_call_with_filters_blocks_unallowed_function(kernel: Kernel):
"""Verify that when function_behavior has filters, an unallowed function is blocked."""
tool_call_mock = MagicMock(spec=FunctionCallContent)
Expand Down
Loading