Repository navigation
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
The documentation incorrectly promises that auto_invoke=False always disables invocation, omitting the explicit attempt-count override and NoneInvoke exception.
1 open issue
What changed in this PR
This PR aligns Python’s dictionary-based function-choice configuration with the existing string and factory defaults, enabling automatic tool invocation for auto and required.
Changes:
- Defaults
auto_invokebased on the behavior type while preserving explicit settings. - Documents the new default.
- Adds regression tests for dictionary parsing, overrides, and
PromptExecutionSettingsintegration.
| File | Description |
|---|---|
python/tests/unit/connectors/ai/test_prompt_execution_settings.py |
Tests default invocation and explicit opt-out through settings parsing. |
python/tests/unit/connectors/ai/test_function_choice_behavior.py |
Tests behavior defaults, overrides, and consistency with string parsing. |
python/semantic_kernel/connectors/ai/function_choice_behavior.py |
Updates the dictionary default and its documentation. |
Review effort: Balanced
|
|
||
| The default for `auto_invoke` matches `from_string` and the classmethods: | ||
| auto-invocation is enabled for the "auto" and "required" types and disabled | ||
| for "none". An explicit `auto_invoke` key in the dict always wins. |
There was a problem hiding this comment.
Correct — both exceptions are real, and the docstring is now precise (2b12e2b):
An explicit `auto_invoke` key overrides that default, with two exceptions:
* an explicit `maximum_auto_invoke_attempts` in the same dict wins, because the
classmethods only fill the count in when it is absent -- so
`{"type": "auto", "auto_invoke": False, "maximum_auto_invoke_attempts": 3}`
still auto-invokes. Leave the count out when `auto_invoke=False` is meant to
disable invocation for "auto" and "required".
* the "none" behavior ignores `auto_invoke` entirely: `NoneInvoke()` always sets
the attempt count to 0, whatever the dict says.
While there I also made from_dict() copy the input (data = dict(data)): it pop()s type, auto_invoke, functions and filters, so the caller's config dict came back emptied. Behavior is otherwise unchanged.
…sable invocation from_dict() lets an explicit maximum_auto_invoke_attempts win over auto_invoke, and the none behavior ignores auto_invoke entirely. Spell both out, and copy the input dict so from_dict() does not strip the caller's keys.
Muhammad Abdullah Butt (1AbdullahButt)
left a comment
There was a problem hiding this comment.
Looks correct. Here's what I checked.
- The default.
auto_invoke = data.pop("auto_invoke", behavior_type != "none")(function_choice_behavior.py:209) passesTruetoAuto()/Required(). Those thensetdefaultthe attempt count toDEFAULT_MAX_AUTO_INVOKE_ATTEMPTSand 1. That matchesfrom_string("auto")andfrom_string("required").NoneInvoke()ignores the flag and sets 0, as the docstring says. - Existing callers. The only caller in the package is
PromptExecutionSettings.parse_function_choice_behavior(prompt_execution_settings.py:49). The existingfrom_dicttests and thefunction_choice_yaml/defined_function.yamlsample all passmaximum_auto_invoke_attemptsexplicitly. So onmainthey already auto-invoked throughsetdefault, and this change doesn't move them. Only dicts without a count change behaviour, which is the bug. data = dict(data). It's a shallow copy, sofiltersandfunctionsare still the caller's objects. Nothing here mutates them (_combine_filter_dictsbuilds a new dict), so that's fine.git merge-treeagainstmainis clean.
I didn't run the tests.
Two notes:
- This turns on function execution for any existing
{"type": "auto"}or{"type": "required"}settings dict without a count, including YAML prompt templates written that way. Before, those only advertised the tools. That's the intended fix, but it is a behaviour change that runs code. A line in the release notes would help, pointing toauto_invoke: falseas the way to keep advertise-only. type_map[behavior_type]is case-sensitive, whilefrom_stringlowercases (data.lower()). So{"type": "Auto"}raisesKeyErrorwhile"Auto"as a string works. That's not from this PR, butbehavior_type = data.pop("type", "auto").lower()would line the two up, since this PR is about making them agree.
Reviewed with help from Claude Code.

Motivation and Context
FunctionChoiceBehavior.from_dictis the parser used whenever afunction_choice_behavioris supplied as a dictionary, e.g.PromptExecutionSettings(function_choice_behavior={"type": "auto"})(seePromptExecutionSettings.parse_function_choice_behavior). It currently popsauto_invokewith a hard default ofFalse:Consequences on the current
main:FunctionChoiceBehavior.from_dict({"type": "auto"})->maximum_auto_invoke_attempts == 0, soauto_invoke_kernel_functionsisFalse: tools are advertised to the model but are never invoked.FunctionChoiceBehavior.from_dict({"type": "required"})-> also0attempts, so the model is forced to emit a tool call that the SDK then never executes.FunctionChoiceBehavior.from_string("auto")-> 5 attempts,FunctionChoiceBehavior.Auto()-> 5 attempts,FunctionChoiceBehavior.Required()-> 1 attempt.So the dict notation silently downgrades "auto"/"required" to tool-call-advertisement-only mode unless the caller knows about the undocumented
auto_invokekey. An agent configured from a serialized settings dict (or any flow that round-trips settings through dictionaries) stops calling tools with no error and no warning.Description
FunctionChoiceBehavior.from_dict, defaultauto_invoketobehavior_type != "none"instead ofFalse, so the dict form defaults exactly likefrom_stringand theAuto/Requiredclassmethods.NoneInvokekeeps auto-invocation off, and an explicitauto_invokekey in the dict still wins (both directions).from_dictto state the default.test_from_dict_defaults_auto_invoke_consistently(parametrized over auto/required/none/explicit overrides),test_from_dict_matches_from_string_for_auto,PromptExecutionSettingstests covering the dict parse path used byparse_function_choice_behavior.No existing behavior changes for callers that pass
auto_invokeexplicitly or passmaximum_auto_invoke_attemptsin the dict (the existing parametrizedfrom_dicttests keep passing unchanged).Validation
Red -> green evidence (Python 3.13, Windows):
pytest tests/unit/connectors/ai/test_function_choice_behavior.py tests/unit/connectors/ai/test_prompt_execution_settings.py:4 failed, 38 passed— the four failures are exactly the new implicit-default cases ({"type": "auto"}-> expected 5 got 0,{"type": "required"}-> expected 1 got 0,from_dictvsfrom_stringmismatch, and the settings-level dict parse).42 passed.439 passed(kernel, agents, function-choice, settings, chat-completion agent suites) and218 passed(open_ai connector suite), plus the previously passing suite around the change stayed green.Contribution Checklist
AI assistance
This change was prepared with the help of an AI coding agent (code analysis, patch drafting, and test writing); the final code, tests, and validation were reviewed by a human before submission.