Skip to content

Commit 9691281

Browse files
Python: update ToolCallBehavior and rename to FunctionCallBehavior to match dotnet and extended capabilities (microsoft#5919)
### Motivation and Context <!-- Thank you for your contribution to the semantic-kernel repo! Please help reviewers and future users, providing the following information: 1. Why is this change required? 2. What problem does it solve? 3. What scenario does it contribute to? 4. If it fixes an open issue, please link to the issue here. --> Updated ToolCallBehavior to match the approach in dotnet Closes microsoft#5727 Closes microsoft#5447 Closes microsoft#5414 ### Description <!-- Describe your changes, the overall approach, the underlying design. These notes will help understanding how your code works. Thanks! --> Extends ToolCallBehavior class with fields: - enable_kernel_functions - max_use_attempts - allow_any_request_kernel_function Added subclasses of TCB: - KernelFunctions, the configure_options set tools to all functions in the kernel and tool_choice to auto - EnabledFunctions, created with a filter dict, sets tool_choice to auto and tools to the filtered list - RequiredFunction, created with a function fully qualified name (plugin-function), sets tool_choice to that name and adds the definition of just that tool to tools. Methods: - configure_options(kernel, exection_settings) - This sets the execution settings depending on the field in toolcallbehavior - Does nothing in the default ToolCallBehavior class, so there you have to manually set tools and tool_choice ClassMethods: - AutoInvokeKernelFunctions, returns KernelFunctions class with max_auto_invoke_attempts set to 5 (default) - EnabelKernelFunctions, return KernelFunctions but with max_auto_invoke_attempts set to 0, disabling auto invoke, but it might return toolcalls from the model - EnableFunctions, takes the filter and a auto_invoke params and returns a EnabledFunctions class, if auto_invoke == True then it will auto invoke, otherwise it wont. - RequiredFunctions, returns RequiredFunction class with either max_invoke_attempts 0 or 1 depending on auto_invoke param Changed OpenAIChatPromptExecutionSettings to have a field called tool_call_behavior instead of max_invoke_attempts and auto_invoke_tool_calls fields. Some changes in openai_chat_completion_base to handle this. ### Contribution Checklist <!-- Before submitting this PR, please make sure: --> - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [x] All unit tests pass, and I have added new tests where possible - [ ] I didn't break anyone 😄 --------- Co-authored-by: Evan Mattson <evmattso@microsoft.com> Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
1 parent c28c7cc commit 9691281

25 files changed

Lines changed: 750 additions & 367 deletions

‎python/samples/concepts/auto_function_calling/chat_gpt_api_function_calling.py‎

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@
66
from typing import TYPE_CHECKING, List
77

88
from semantic_kernel import Kernel
9-
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion, OpenAIChatPromptExecutionSettings
10-
from semantic_kernel.connectors.ai.open_ai.utils import get_tool_call_object
9+
from semantic_kernel.connectors.ai.function_call_behavior import FunctionCallBehavior
10+
from semantic_kernel.connectors.ai.open_ai import (
11+
OpenAIChatCompletion,
12+
OpenAIChatPromptExecutionSettings,
13+
)
1114
from semantic_kernel.contents import ChatHistory
1215
from semantic_kernel.contents.chat_message_content import ChatMessageContent
1316
from semantic_kernel.contents.function_call_content import FunctionCallContent
@@ -71,10 +74,9 @@
7174
max_tokens=2000,
7275
temperature=0.7,
7376
top_p=0.8,
74-
tool_choice="auto",
75-
tools=get_tool_call_object(kernel, {"exclude_plugin": ["ChatBot"]}),
76-
auto_invoke_kernel_functions=True,
77-
max_auto_invoke_attempts=3,
77+
function_call_behavior=FunctionCallBehavior.EnableFunctions(
78+
auto_invoke=True, filters={"included_plugins": ["math"]}
79+
),
7880
)
7981

8082
history = ChatHistory()
@@ -119,7 +121,9 @@ async def handle_streaming(
119121
print("Mosscap:> ", end="")
120122
streamed_chunks: List[StreamingChatMessageContent] = []
121123
async for message in response:
122-
if not execution_settings.auto_invoke_kernel_functions:
124+
if not execution_settings.function_call_behavior.auto_invoke_kernel_functions and isinstance(
125+
message[0], FunctionCallContent
126+
):
123127
streamed_chunks.append(message[0])
124128
else:
125129
print(str(message[0]), end="")
@@ -148,7 +152,7 @@ async def chat() -> bool:
148152
arguments["user_input"] = user_input
149153
arguments["chat_history"] = history
150154

151-
stream = True
155+
stream = False
152156
if stream:
153157
await handle_streaming(kernel, chat_function, arguments=arguments)
154158
else:
@@ -157,7 +161,9 @@ async def chat() -> bool:
157161
# If tools are used, and auto invoke tool calls is False, the response will be of type
158162
# ChatMessageContent with information about the tool calls, which need to be sent
159163
# back to the model to get the final response.
160-
if not execution_settings.auto_invoke_kernel_functions:
164+
if not execution_settings.function_call_behavior.auto_invoke_kernel_functions and isinstance(
165+
result.value[0], FunctionCallContent
166+
):
161167
print_tool_calls(result.value[0])
162168
return True
163169

‎python/samples/concepts/on_your_data/azure_chat_gpt_with_data_api_function_calling.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
import os
66

77
import semantic_kernel as sk
8+
from semantic_kernel.connectors.ai.function_call_behavior import FunctionCallBehavior
89
from semantic_kernel.connectors.ai.open_ai import (
910
AzureAISearchDataSource,
1011
AzureChatCompletion,
1112
AzureChatPromptExecutionSettings,
1213
ExtraBody,
1314
)
14-
from semantic_kernel.connectors.ai.open_ai.utils import get_tool_call_object
1515
from semantic_kernel.contents import ChatHistory
1616
from semantic_kernel.core_plugins import TimePlugin
1717
from semantic_kernel.functions import KernelArguments
@@ -85,9 +85,9 @@
8585
# calling the chat, you could add a overloaded version of the settings here,
8686
# to enable or disable function calling or set the function calling to a specific plugin.
8787
# see the openai_function_calling example for how to use this with a unrelated function definition
88-
filter = {"exclude_plugin": ["ChatBot"]}
89-
req_settings.tools = get_tool_call_object(kernel, filter)
90-
req_settings.auto_invoke_kernel_functions = True
88+
req_settings.function_call_behavior = FunctionCallBehavior.EnableFunctions(
89+
auto_invoke=True, filters={"excluded_plugins": ["ChatBot"]}
90+
)
9191

9292
arguments = KernelArguments(settings=req_settings)
9393

‎python/samples/concepts/plugins/openai_function_calling_with_custom_plugin.py‎

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010
else:
1111
from typing_extensions import Annotated
1212

13+
from semantic_kernel.connectors.ai.function_call_behavior import FunctionCallBehavior
1314
from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion, OpenAIChatCompletion
1415
from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.open_ai_prompt_execution_settings import (
1516
OpenAIChatPromptExecutionSettings,
1617
)
17-
from semantic_kernel.connectors.ai.open_ai.utils import get_tool_call_object
1818
from semantic_kernel.contents.chat_history import ChatHistory
1919
from semantic_kernel.contents.function_call_content import FunctionCallContent
2020
from semantic_kernel.core_plugins.time_plugin import TimePlugin
@@ -74,9 +74,9 @@ async def main():
7474
settings: OpenAIChatPromptExecutionSettings = kernel.get_prompt_execution_settings_from_service_id(
7575
service_id=service_id
7676
)
77-
settings.auto_invoke_kernel_functions = True
78-
settings.tool_choice = "auto"
79-
settings.tools = get_tool_call_object(kernel, filter={})
77+
settings.function_call_behavior = FunctionCallBehavior.EnableFunctions(
78+
auto_invoke=True, filters={"include_plugin": ["weather", "time"]}
79+
)
8080

8181
print(
8282
await kernel.invoke_prompt(
@@ -92,9 +92,9 @@ async def main():
9292
settings: OpenAIChatPromptExecutionSettings = kernel.get_prompt_execution_settings_from_service_id(
9393
service_id=service_id
9494
)
95-
settings.auto_invoke_kernel_functions = True
96-
settings.tool_choice = "auto"
97-
settings.tools = get_tool_call_object(kernel, filter={})
95+
settings.function_call_behavior = FunctionCallBehavior.EnableFunctions(
96+
auto_invoke=True, filters={"include_plugin": ["weather", "time"]}
97+
)
9898

9999
result = kernel.invoke_prompt_stream(
100100
function_name="prompt_test",
@@ -115,8 +115,9 @@ async def main():
115115
settings: OpenAIChatPromptExecutionSettings = kernel.get_prompt_execution_settings_from_service_id(
116116
service_id=service_id
117117
)
118-
settings.auto_invoke_kernel_functions = False
119-
settings.tools = get_tool_call_object(kernel, filter={})
118+
settings.function_call_behavior = FunctionCallBehavior.EnableFunctions(
119+
auto_invoke=True, filters={"include_plugin": ["weather", "time"]}
120+
)
120121
chat_history.add_user_message(
121122
"Given the current time of day and weather, what is the likely color of the sky in Boston?"
122123
)

‎python/samples/demos/booking_restaurant/restaurant_booking.py‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
OpenAIChatPromptExecutionSettings,
1313
)
1414
from semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion import OpenAIChatCompletion
15-
from semantic_kernel.connectors.ai.open_ai.utils import get_tool_call_object
1615
from semantic_kernel.contents.chat_history import ChatHistory
1716
from semantic_kernel.functions.kernel_arguments import KernelArguments
1817
from semantic_kernel.kernel import Kernel
@@ -56,9 +55,7 @@
5655
settings.max_tokens = 2000
5756
settings.temperature = 0.1
5857
settings.top_p = 0.8
59-
settings.auto_invoke_kernel_functions = True
60-
settings.tool_choice = "auto"
61-
settings.tools = get_tool_call_object(kernel, {"exclude_plugin": ["ChatBot"]})
58+
settings.function_call_behavior.enable_functions(auto_invoke=True, filters={"exclude_plugin": ["ChatBot"]})
6259

6360
chat_history = ChatHistory(
6461
system_message="When responding to the user's request to book a table, include the reservation ID."
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
# Copyright (c) Microsoft. All rights reserved.
2+
from __future__ import annotations
3+
4+
from typing import TYPE_CHECKING, Callable, Literal
5+
6+
from pydantic.dataclasses import dataclass
7+
8+
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata
9+
from semantic_kernel.kernel_pydantic import KernelBaseModel
10+
11+
if TYPE_CHECKING:
12+
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
13+
from semantic_kernel.kernel import Kernel
14+
15+
DEFAULT_MAX_AUTO_INVOKE_ATTEMPTS = 5
16+
17+
18+
@dataclass
19+
class FunctionCallConfiguration:
20+
"""Class that holds the configured functions for function calling."""
21+
22+
available_functions: list["KernelFunctionMetadata"] | None = None
23+
required_functions: list["KernelFunctionMetadata"] | None = None
24+
25+
26+
class FunctionCallBehavior(KernelBaseModel):
27+
"""Class that controls function calling behavior.
28+
29+
Args:
30+
enable_kernel_functions (bool): Enable kernel functions.
31+
max_auto_invoke_attempts (int): The maximum number of auto invoke attempts.
32+
33+
Attributes:
34+
enable_kernel_functions (bool): Enable kernel functions.
35+
max_auto_invoke_attempts (int): The maximum number of auto invoke attempts.
36+
37+
Properties:
38+
auto_invoke_kernel_functions: Check if the kernel functions should be auto-invoked.
39+
Determined as max_auto_invoke_attempts > 0.
40+
41+
Methods:
42+
configure: Configures the settings for the function call behavior,
43+
the default version in this class, does nothing, use subclasses for different behaviors.
44+
45+
Class methods:
46+
AutoInvokeKernelFunctions: Returns KernelFunctions class with auto_invoke enabled, all functions.
47+
EnableKernelFunctions: Returns KernelFunctions class with auto_invoke disabled, all functions.
48+
EnableFunctions: Set the enable kernel functions flag, filtered functions, auto_invoke optional.
49+
RequiredFunction: Set the required function flag, auto_invoke optional.
50+
51+
"""
52+
53+
enable_kernel_functions: bool = True
54+
max_auto_invoke_attempts: int = DEFAULT_MAX_AUTO_INVOKE_ATTEMPTS
55+
56+
@property
57+
def auto_invoke_kernel_functions(self):
58+
"""Check if the kernel functions should be auto-invoked."""
59+
return self.max_auto_invoke_attempts > 0
60+
61+
@auto_invoke_kernel_functions.setter
62+
def auto_invoke_kernel_functions(self, value: bool):
63+
"""Set the auto_invoke_kernel_functions flag."""
64+
if not value:
65+
self.max_auto_invoke_attempts = 0
66+
else:
67+
if self.max_auto_invoke_attempts == 0:
68+
self.max_auto_invoke_attempts = DEFAULT_MAX_AUTO_INVOKE_ATTEMPTS
69+
70+
def configure(
71+
self,
72+
kernel: "Kernel",
73+
update_settings_callback: Callable[..., None],
74+
settings: "PromptExecutionSettings",
75+
) -> None:
76+
"""Configures the settings for the function call behavior.
77+
78+
Using the base ToolCallBehavior means that you manually have to set tool_choice and tools.
79+
80+
For different behaviors, use the subclasses of ToolCallBehavior:
81+
KernelFunctions (all functions in the Kernel)
82+
EnabledFunctions (filtered set of functions from the Kernel)
83+
RequiredFunction (a single function)
84+
85+
By default the update_settings_callback is called with FunctionCallConfiguration,
86+
which contains a list of available functions or a list of required functions, it also
87+
takes the PromptExecutionSettings object.
88+
89+
It should update the prompt execution settings with the available functions or required functions.
90+
91+
Alternatively you can override this class and add your own logic in the configure method.
92+
"""
93+
return
94+
95+
@classmethod
96+
def AutoInvokeKernelFunctions(cls) -> "KernelFunctions":
97+
"""Returns KernelFunctions class with auto_invoke enabled."""
98+
return KernelFunctions(max_auto_invoke_attempts=DEFAULT_MAX_AUTO_INVOKE_ATTEMPTS)
99+
100+
@classmethod
101+
def EnableKernelFunctions(cls) -> "KernelFunctions":
102+
"""Returns KernelFunctions class with auto_invoke disabled.
103+
104+
Function calls are enabled in this case, just not invoked.
105+
"""
106+
return KernelFunctions(max_auto_invoke_attempts=0)
107+
108+
@classmethod
109+
def EnableFunctions(
110+
cls,
111+
auto_invoke: bool = False,
112+
*,
113+
filters: dict[
114+
Literal["excluded_plugins", "included_plugins", "excluded_functions", "included_functions"], list[str]
115+
],
116+
) -> "EnabledFunctions":
117+
"""Set the enable kernel functions flag."""
118+
return EnabledFunctions(
119+
filters=filters, max_auto_invoke_attempts=DEFAULT_MAX_AUTO_INVOKE_ATTEMPTS if auto_invoke else 0
120+
)
121+
122+
@classmethod
123+
def RequiredFunction(
124+
cls,
125+
auto_invoke: bool = False,
126+
*,
127+
function_fully_qualified_name: str,
128+
) -> "RequiredFunction":
129+
"""Set the required function flag."""
130+
return RequiredFunction(
131+
function_fully_qualified_name=function_fully_qualified_name,
132+
max_auto_invoke_attempts=1 if auto_invoke else 0,
133+
)
134+
135+
136+
class KernelFunctions(FunctionCallBehavior):
137+
"""Function call behavior for making all kernel functions available for tool calls."""
138+
139+
def configure(
140+
self,
141+
kernel: "Kernel",
142+
update_settings_callback: Callable[..., None],
143+
settings: "PromptExecutionSettings",
144+
) -> None:
145+
"""Set the options for the tool call behavior in the settings."""
146+
if self.enable_kernel_functions:
147+
update_settings_callback(
148+
FunctionCallConfiguration(available_functions=kernel.get_full_list_of_function_metadata()), settings
149+
)
150+
151+
152+
class EnabledFunctions(FunctionCallBehavior):
153+
"""Function call behavior for making a filtered set of functions available for tool calls."""
154+
155+
filters: dict[
156+
Literal["excluded_plugins", "included_plugins", "excluded_functions", "included_functions"], list[str]
157+
]
158+
159+
def configure(
160+
self,
161+
kernel: "Kernel",
162+
update_settings_callback: Callable[..., None],
163+
settings: "PromptExecutionSettings",
164+
) -> None:
165+
"""Set the options for the tool call behavior in the settings."""
166+
if self.enable_kernel_functions:
167+
update_settings_callback(
168+
FunctionCallConfiguration(available_functions=kernel.get_list_of_function_metadata(self.filters)),
169+
settings,
170+
)
171+
172+
173+
class RequiredFunction(FunctionCallBehavior):
174+
"""Function call behavior for making a single function available for tool calls."""
175+
176+
function_fully_qualified_name: str
177+
178+
def configure(
179+
self,
180+
kernel: "Kernel",
181+
update_settings_callback: Callable[..., None],
182+
settings: "PromptExecutionSettings",
183+
) -> None:
184+
"""Set the options for the tool call behavior in the settings."""
185+
if not self.enable_kernel_functions:
186+
return
187+
# since using this always calls this single function, we do not want to allow repeated calls
188+
# TODO: reevaluate when other models support function calling then OpenAI.
189+
if self.max_auto_invoke_attempts > 1:
190+
self.max_auto_invoke_attempts = 1
191+
update_settings_callback(
192+
FunctionCallConfiguration(
193+
required_functions=kernel.get_list_of_function_metadata(
194+
{"included_functions": [self.function_fully_qualified_name]}
195+
)
196+
),
197+
settings,
198+
)

0 commit comments

Comments
 (0)