Skip to content

Python: default dict function_choice_behavior to auto-invoking for auto and required - #14752

Open
DZH166 wants to merge 2 commits into
microsoft:mainfrom
DZH166:codex/fcb-from-dict-auto-invoke-20261108
Open

DZH166 wants to merge 2 commits into
microsoft:mainfrom
DZH166:codex/fcb-from-dict-auto-invoke-20261108

Conversation

@DZH166

@DZH166 DZH166 commented Nov 8, 2026

Copy link
Copy Markdown

Motivation and Context

FunctionChoiceBehavior.from_dict is the parser used whenever a function_choice_behavior is supplied as a dictionary, e.g. PromptExecutionSettings(function_choice_behavior={"type": "auto"}) (see PromptExecutionSettings.parse_function_choice_behavior). It currently pops auto_invoke with a hard default of False:

auto_invoke = data.pop("auto_invoke", False)

Consequences on the current main:

  • FunctionChoiceBehavior.from_dict({"type": "auto"}) -> maximum_auto_invoke_attempts == 0, so auto_invoke_kernel_functions is False: tools are advertised to the model but are never invoked.
  • FunctionChoiceBehavior.from_dict({"type": "required"}) -> also 0 attempts, so the model is forced to emit a tool call that the SDK then never executes.
  • This contradicts every other way of requesting the same behavior:
    • 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_invoke key. 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

  • In FunctionChoiceBehavior.from_dict, default auto_invoke to behavior_type != "none" instead of False, so the dict form defaults exactly like from_string and the Auto/Required classmethods. NoneInvoke keeps auto-invocation off, and an explicit auto_invoke key in the dict still wins (both directions).
  • Extended the docstring of from_dict to state the default.
  • Added regression tests:
    • test_from_dict_defaults_auto_invoke_consistently (parametrized over auto/required/none/explicit overrides),
    • test_from_dict_matches_from_string_for_auto,
    • two PromptExecutionSettings tests covering the dict parse path used by parse_function_choice_behavior.

No existing behavior changes for callers that pass auto_invoke explicitly or pass maximum_auto_invoke_attempts in the dict (the existing parametrized from_dict tests keep passing unchanged).

Validation

Red -> green evidence (Python 3.13, Windows):

  • Red (before the fix), 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_dict vs from_string mismatch, and the settings-level dict parse).
  • Green (after the fix): 42 passed.
  • Broader regression run after the fix: 439 passed (kernel, agents, function-choice, settings, chat-completion agent suites) and 218 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.

@DZH166
DZH166 requested a review from a team as a code owner November 8, 2026 15:45
Copilot AI balanced review requested due to automatic review settings November 8, 2026 15:45
@DZH166
DZH166 temporarily deployed to github-app-auth November 8, 2026 15:45 — with GitHub Actions Inactive
@semantic-kernel-automation semantic-kernel-automation Bot added the python Pull requests for the Python Semantic Kernel label Nov 8, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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_invoke based on the behavior type while preserving explicit settings.
  • Documents the new default.
  • Adds regression tests for dictionary parsing, overrides, and PromptExecutionSettings integration.
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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.
@DZH166
DZH166 temporarily deployed to github-app-auth November 8, 2026 16:24 — with GitHub Actions Inactive

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Looks correct. Here's what I checked.

  • The default. auto_invoke = data.pop("auto_invoke", behavior_type != "none") (function_choice_behavior.py:209) passes True to Auto()/Required(). Those then setdefault the attempt count to DEFAULT_MAX_AUTO_INVOKE_ATTEMPTS and 1. That matches from_string("auto") and from_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 existing from_dict tests and the function_choice_yaml/defined_function.yaml sample all pass maximum_auto_invoke_attempts explicitly. So on main they already auto-invoked through setdefault, 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, so filters and functions are still the caller's objects. Nothing here mutates them (_combine_filter_dicts builds a new dict), so that's fine.
  • git merge-tree against main is 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 to auto_invoke: false as the way to keep advertise-only.
  • type_map[behavior_type] is case-sensitive, while from_string lowercases (data.lower()). So {"type": "Auto"} raises KeyError while "Auto" as a string works. That's not from this PR, but behavior_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.

This branch was previously deployed

1 inactive deployment
github-app-auth — 2b12e2bb Deployed Nov 8, 2026 by DZH166 via add_label #29632
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Pull requests for the Python Semantic Kernel

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants