diff --git a/python/semantic_kernel/connectors/ai/azure_ai_inference/azure_ai_inference_prompt_execution_settings.py b/python/semantic_kernel/connectors/ai/azure_ai_inference/azure_ai_inference_prompt_execution_settings.py index f64646dcf0c7..804ddfd80267 100644 --- a/python/semantic_kernel/connectors/ai/azure_ai_inference/azure_ai_inference_prompt_execution_settings.py +++ b/python/semantic_kernel/connectors/ai/azure_ai_inference/azure_ai_inference_prompt_execution_settings.py @@ -1,6 +1,6 @@ # Copyright (c) Microsoft. All rights reserved. -from typing import Literal +from typing import Any, Literal from pydantic import Field @@ -30,6 +30,9 @@ class AzureAIInferencePromptExecutionSettings(PromptExecutionSettings): class AzureAIInferenceChatPromptExecutionSettings(AzureAIInferencePromptExecutionSettings): """Azure AI Inference Chat Prompt Execution Settings.""" + tools: list[dict[str, Any]] | None = Field(None, max_length=64) + tool_choice: str | None = None + @experimental_class class AzureAIInferenceEmbeddingPromptExecutionSettings(PromptExecutionSettings): diff --git a/python/semantic_kernel/connectors/ai/azure_ai_inference/services/azure_ai_inference_chat_completion.py b/python/semantic_kernel/connectors/ai/azure_ai_inference/services/azure_ai_inference_chat_completion.py index 5d39d3953e65..4ebf2bbc7d19 100644 --- a/python/semantic_kernel/connectors/ai/azure_ai_inference/services/azure_ai_inference_chat_completion.py +++ b/python/semantic_kernel/connectors/ai/azure_ai_inference/services/azure_ai_inference_chat_completion.py @@ -1,24 +1,25 @@ # Copyright (c) Microsoft. All rights reserved. +import asyncio import logging +import sys from collections.abc import AsyncGenerator +from functools import reduce from typing import Any +if sys.version >= "3.12": + from typing import override # pragma: no cover +else: + from typing_extensions import override # pragma: no cover + from azure.ai.inference.aio import ChatCompletionsClient from azure.ai.inference.models import ( - AssistantMessage, AsyncStreamingChatCompletions, ChatChoice, ChatCompletions, + ChatCompletionsFunctionToolCall, ChatRequestMessage, - ImageContentItem, - ImageDetailLevel, - ImageUrl, StreamingChatChoiceUpdate, - SystemMessage, - TextContentItem, - ToolMessage, - UserMessage, ) from azure.core.credentials import AzureKeyCredential from pydantic import ValidationError @@ -28,26 +29,26 @@ AzureAIInferenceSettings, ) from semantic_kernel.connectors.ai.azure_ai_inference.services.azure_ai_inference_base import AzureAIInferenceBase +from semantic_kernel.connectors.ai.azure_ai_inference.services.utils import MESSAGE_CONVERTERS from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase +from semantic_kernel.connectors.ai.function_calling_utils import update_settings_from_function_call_configuration +from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior from semantic_kernel.contents.chat_history import ChatHistory from semantic_kernel.contents.chat_message_content import ChatMessageContent from semantic_kernel.contents.function_call_content import FunctionCallContent -from semantic_kernel.contents.image_content import ImageContent from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent from semantic_kernel.contents.streaming_text_content import StreamingTextContent from semantic_kernel.contents.text_content import TextContent from semantic_kernel.contents.utils.author_role import AuthorRole from semantic_kernel.contents.utils.finish_reason import FinishReason -from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError +from semantic_kernel.exceptions.service_exceptions import ( + ServiceInitializationError, + ServiceInvalidExecutionSettingsError, +) +from semantic_kernel.functions.kernel_arguments import KernelArguments +from semantic_kernel.kernel import Kernel from semantic_kernel.utils.experimental_decorator import experimental_class -_MESSAGE_CONVERTER: dict[AuthorRole, Any] = { - AuthorRole.SYSTEM: SystemMessage, - AuthorRole.USER: UserMessage, - AuthorRole.ASSISTANT: AssistantMessage, - AuthorRole.TOOL: ToolMessage, -} - logger: logging.Logger = logging.getLogger(__name__) @@ -106,6 +107,7 @@ def __init__( client=client, ) + # region Non-streaming async def get_chat_message_contents( self, chat_history: ChatHistory, @@ -122,8 +124,46 @@ async def get_chat_message_contents( Returns: A list of chat message contents. """ + if ( + settings.function_choice_behavior is None + or not settings.function_choice_behavior.auto_invoke_kernel_functions + ): + return await self._send_chat_request(chat_history, settings) + + kernel: Kernel = kwargs.get("kernel") + arguments: KernelArguments = kwargs.get("arguments") + self._verify_function_choice_behavior(settings, kernel, arguments) + self._configure_function_choice_behavior(settings, kernel) + + for request_index in range(settings.function_choice_behavior.maximum_auto_invoke_attempts): + completions = await self._send_chat_request(chat_history, settings) + chat_history.add_message(message=completions[0]) + function_calls = [item for item in chat_history.messages[-1].items if isinstance(item, FunctionCallContent)] + if (fc_count := len(function_calls)) == 0: + return completions + + results = await self._invoke_function_calls( + function_calls=function_calls, + chat_history=chat_history, + kernel=kernel, + arguments=arguments, + function_call_count=fc_count, + request_index=request_index, + function_behavior=settings.function_choice_behavior, + ) + + if any(result.terminate for result in results if result is not None): + return completions + else: + # do a final call without auto function calling + return await self._send_chat_request(chat_history, settings) + + async def _send_chat_request( + self, chat_history: ChatHistory, settings: AzureAIInferenceChatPromptExecutionSettings + ) -> list[ChatMessageContent]: + """Send a chat request to the Azure AI Inference service.""" response: ChatCompletions = await self.client.complete( - messages=self._format_chat_history(chat_history), + messages=self._prepare_chat_history_for_request(chat_history), model_extras=settings.extra_parameters, **settings.prepare_settings_dict(), ) @@ -131,53 +171,6 @@ async def get_chat_message_contents( return [self._create_chat_message_content(response, choice, response_metadata) for choice in response.choices] - async def get_streaming_chat_message_contents( - self, - chat_history: ChatHistory, - settings: AzureAIInferenceChatPromptExecutionSettings, - **kwargs: Any, - ) -> AsyncGenerator[list[StreamingChatMessageContent], Any]: - """Get streaming chat message contents from the Azure AI Inference service. - - Args: - chat_history: A list of chats in a chat_history object. - settings: Settings for the request. - kwargs: Optional arguments. - - Returns: - A list of chat message contents. - """ - response: AsyncStreamingChatCompletions = await self.client.complete( - stream=True, - messages=self._format_chat_history(chat_history), - model_extras=settings.extra_parameters, - **settings.prepare_settings_dict(), - ) - - async for chunk in response: - if len(chunk.choices) == 0: - continue - chunk_metadata = self._get_metadata_from_response(chunk) - yield [ - self._create_streaming_chat_message_content(chunk, choice, chunk_metadata) for choice in chunk.choices - ] - - def _get_metadata_from_response(self, response: ChatCompletions | AsyncStreamingChatCompletions) -> dict[str, Any]: - """Get metadata from the response. - - Args: - response: The response from the service. - - Returns: - A dictionary containing metadata. - """ - return { - "id": response.id, - "model": response.model, - "created": response.created, - "usage": response.usage, - } - def _create_chat_message_content( self, response: ChatCompletions, choice: ChatChoice, metadata: dict[str, Any] ) -> ChatMessageContent: @@ -218,6 +211,102 @@ def _create_chat_message_content( metadata=metadata, ) + # endregion + + # region Streaming + async def get_streaming_chat_message_contents( + self, + chat_history: ChatHistory, + settings: AzureAIInferenceChatPromptExecutionSettings, + **kwargs: Any, + ) -> AsyncGenerator[list[StreamingChatMessageContent], Any]: + """Get streaming chat message contents from the Azure AI Inference service. + + Args: + chat_history: A list of chats in a chat_history object. + settings: Settings for the request. + kwargs: Optional arguments. + + Returns: + A list of chat message contents. + """ + if ( + settings.function_choice_behavior is None + or not settings.function_choice_behavior.auto_invoke_kernel_functions + ): + # No auto invoke is required. + async_generator = self._send_chat_streaming_request(chat_history, settings) + else: + # Auto invoke is required. + async_generator = self._get_streaming_chat_message_contents_auto_invoke(chat_history, settings, **kwargs) + + async for messages in async_generator: + yield messages + + async def _get_streaming_chat_message_contents_auto_invoke( + self, + chat_history: ChatHistory, + settings: AzureAIInferenceChatPromptExecutionSettings, + **kwargs: Any, + ) -> AsyncGenerator[list[StreamingChatMessageContent], Any]: + """Get streaming chat message contents from the Azure AI Inference service with auto invoking functions.""" + kernel: Kernel = kwargs.get("kernel") + arguments: KernelArguments = kwargs.get("arguments") + self._verify_function_choice_behavior(settings, kernel, arguments) + self._configure_function_choice_behavior(settings, kernel) + request_attempts = settings.function_choice_behavior.maximum_auto_invoke_attempts + + for request_index in range(request_attempts): + all_messages: list[StreamingChatMessageContent] = [] + function_call_returned = False + async for messages in self._send_chat_streaming_request(chat_history, settings): + for message in messages: + if message: + all_messages.append(message) + if any(isinstance(item, FunctionCallContent) for item in message.items): + function_call_returned = True + yield messages + + if not function_call_returned: + # Response doesn't contain any function calls. No need to proceed to the next request. + return + + full_completion: StreamingChatMessageContent = reduce(lambda x, y: x + y, all_messages) + function_calls = [item for item in full_completion.items if isinstance(item, FunctionCallContent)] + chat_history.add_message(message=full_completion) + + results = await self._invoke_function_calls( + function_calls=function_calls, + chat_history=chat_history, + kernel=kernel, + arguments=arguments, + function_call_count=len(function_calls), + request_index=request_index, + function_behavior=settings.function_choice_behavior, + ) + + if any(result.terminate for result in results if result is not None): + return + + async def _send_chat_streaming_request( + self, chat_history: ChatHistory, settings: AzureAIInferenceChatPromptExecutionSettings + ) -> AsyncGenerator[list[StreamingChatMessageContent], Any]: + """Send a streaming chat request to the Azure AI Inference service.""" + response: AsyncStreamingChatCompletions = await self.client.complete( + stream=True, + messages=self._prepare_chat_history_for_request(chat_history), + model_extras=settings.extra_parameters, + **settings.prepare_settings_dict(), + ) + + async for chunk in response: + if len(chunk.choices) == 0: + continue + chunk_metadata = self._get_metadata_from_response(chunk) + yield [ + self._create_streaming_chat_message_content(chunk, choice, chunk_metadata) for choice in chunk.choices + ] + def _create_streaming_chat_message_content( self, chunk: AsyncStreamingChatCompletions, @@ -246,14 +335,15 @@ def _create_streaming_chat_message_content( ) if choice.delta.tool_calls: for tool_call in choice.delta.tool_calls: - items.append( - FunctionCallContent( - id=tool_call.id, - index=choice.index, - name=tool_call.function.name, - arguments=tool_call.function.arguments, + if isinstance(tool_call, ChatCompletionsFunctionToolCall): + items.append( + FunctionCallContent( + id=tool_call.id, + index=choice.index, + name=tool_call.function.name, + arguments=tool_call.function.arguments, + ) ) - ) return StreamingChatMessageContent( role=AuthorRole(choice.delta.role) if choice.delta.role else AuthorRole.ASSISTANT, @@ -264,43 +354,99 @@ def _create_streaming_chat_message_content( metadata=metadata, ) - def _format_chat_history(self, chat_history: ChatHistory) -> list[ChatRequestMessage]: - """Format the chat history to the expected objects for the client. - - Args: - chat_history: The chat history. + # endregion - Returns: - A list of formatted chat history. - """ + @override + def _prepare_chat_history_for_request( + self, + chat_history: ChatHistory, + role_key: str = "role", + content_key: str = "content", + ) -> list[ChatRequestMessage]: chat_request_messages: list[ChatRequestMessage] = [] for message in chat_history.messages: - if message.role != AuthorRole.USER or not any(isinstance(item, ImageContent) for item in message.items): - chat_request_messages.append(_MESSAGE_CONVERTER[message.role](content=message.content)) + if message.role not in MESSAGE_CONVERTERS: + logger.warning( + "Unsupported author role in chat history while formatting for Azure AI Inference: {message.role}" + ) continue - # If it's a user message and there are any image items in the message, we need to create a list of - # content items, otherwise we need to just pass in the content as a string or it will error. - contentItems = [] - for item in message.items: - if isinstance(item, TextContent): - contentItems.append(TextContentItem(text=item.text)) - elif isinstance(item, ImageContent) and (item.data_uri or item.uri): - contentItems.append( - ImageContentItem( - image_url=ImageUrl(url=item.data_uri or str(item.uri), detail=ImageDetailLevel.Auto) - ) - ) - else: - logger.warning( - "Unsupported item type in User message while formatting chat history for Azure AI" - f" Inference: {type(item)}" - ) - chat_request_messages.append(_MESSAGE_CONVERTER[message.role](content=contentItems)) + chat_request_messages.append(MESSAGE_CONVERTERS[message.role](message)) return chat_request_messages + def _get_metadata_from_response(self, response: ChatCompletions | AsyncStreamingChatCompletions) -> dict[str, Any]: + """Get metadata from the response. + + Args: + response: The response from the service. + + Returns: + A dictionary containing metadata. + """ + return { + "id": response.id, + "model": response.model, + "created": response.created, + "usage": response.usage, + } + + def _verify_function_choice_behavior( + self, + settings: AzureAIInferenceChatPromptExecutionSettings, + kernel: Kernel, + arguments: KernelArguments, + ): + """Verify the function choice behavior.""" + if settings.function_choice_behavior is not None: + if kernel is None: + raise ServiceInvalidExecutionSettingsError("Kernel is required for tool calls.") + if arguments is None and settings.function_choice_behavior.auto_invoke_kernel_functions: + raise ServiceInvalidExecutionSettingsError("Kernel arguments are required for auto tool calls.") + if settings.extra_parameters is not None and settings.extra_parameters.get("n", 1) > 1: + # Currently only OpenAI models allow multiple completions but the Azure AI Inference service + # does not expose the functionality directly. If users want to have more than 1 responses, they + # need to configure `extra_parameters` with a key of "n" and a value greater than 1. + raise ServiceInvalidExecutionSettingsError( + "Auto invocation of tool calls may only be used with a single completion." + ) + + def _configure_function_choice_behavior( + self, settings: AzureAIInferenceChatPromptExecutionSettings, kernel: Kernel + ): + """Configure the function choice behavior to include the kernel functions.""" + settings.function_choice_behavior.configure( + kernel=kernel, update_settings_callback=update_settings_from_function_call_configuration, settings=settings + ) + + async def _invoke_function_calls( + self, + function_calls: list[FunctionCallContent], + chat_history: ChatHistory, + kernel: Kernel, + arguments: KernelArguments, + function_call_count: int, + request_index: int, + function_behavior: FunctionChoiceBehavior, + ): + """Invoke function calls.""" + logger.info(f"processing {function_call_count} tool calls in parallel.") + + return await asyncio.gather( + *[ + kernel.invoke_function_call( + function_call=function_call, + chat_history=chat_history, + arguments=arguments, + function_call_count=function_call_count, + request_index=request_index, + function_behavior=function_behavior, + ) + for function_call in function_calls + ], + ) + def get_prompt_execution_settings_class( self, ) -> AzureAIInferenceChatPromptExecutionSettings: diff --git a/python/semantic_kernel/connectors/ai/azure_ai_inference/services/utils.py b/python/semantic_kernel/connectors/ai/azure_ai_inference/services/utils.py new file mode 100644 index 000000000000..33b1b04d631b --- /dev/null +++ b/python/semantic_kernel/connectors/ai/azure_ai_inference/services/utils.py @@ -0,0 +1,135 @@ +# Copyright (c) Microsoft. All rights reserved. + +import logging +from collections.abc import Callable + +from azure.ai.inference.models import ( + AssistantMessage, + ChatCompletionsFunctionToolCall, + ChatRequestMessage, + FunctionCall, + ImageContentItem, + ImageDetailLevel, + ImageUrl, + SystemMessage, + TextContentItem, + ToolMessage, + UserMessage, +) + +from semantic_kernel.contents.chat_message_content import ChatMessageContent +from semantic_kernel.contents.function_call_content import FunctionCallContent +from semantic_kernel.contents.function_result_content import FunctionResultContent +from semantic_kernel.contents.image_content import ImageContent +from semantic_kernel.contents.text_content import TextContent +from semantic_kernel.contents.utils.author_role import AuthorRole + +logger: logging.Logger = logging.getLogger(__name__) + + +def _format_system_message(message: ChatMessageContent) -> SystemMessage: + """Format a system message to the expected object for the client. + + Args: + message: The system message. + + Returns: + The formatted system message. + """ + return SystemMessage(content=message.content) + + +def _format_user_message(message: ChatMessageContent) -> UserMessage: + """Format a user message to the expected object for the client. + + If there are any image items in the message, we need to create a list of content items, + otherwise we need to just pass in the content as a string or it will error. + + Args: + message: The user message. + + Returns: + The formatted user message. + """ + if not any(isinstance(item, (ImageContent)) for item in message.items): + return UserMessage(content=message.content) + + contentItems = [] + for item in message.items: + if isinstance(item, TextContent): + contentItems.append(TextContentItem(text=item.text)) + elif isinstance(item, ImageContent) and (item.data_uri or item.uri): + contentItems.append( + ImageContentItem(image_url=ImageUrl(url=item.data_uri or str(item.uri), detail=ImageDetailLevel.Auto)) + ) + else: + logger.warning( + "Unsupported item type in User message while formatting chat history for Azure AI" + f" Inference: {type(item)}" + ) + + return UserMessage(content=contentItems) + + +def _format_assistant_message(message: ChatMessageContent) -> AssistantMessage: + """Format an assistant message to the expected object for the client. + + Args: + message: The assistant message. + + Returns: + The formatted assistant message. + """ + contentItems = [] + toolCalls = [] + + for item in message.items: + if isinstance(item, TextContent): + contentItems.append(TextContentItem(text=item.text)) + elif isinstance(item, FunctionCallContent): + toolCalls.append( + ChatCompletionsFunctionToolCall( + id=item.id, function=FunctionCall(name=item.name, arguments=item.arguments) + ) + ) + else: + logger.warning( + "Unsupported item type in Assistant message while formatting chat history for Azure AI" + f" Inference: {type(item)}" + ) + + # tollCalls cannot be an empty list, so we need to set it to None if it is empty + return AssistantMessage(content=contentItems, tool_calls=toolCalls if toolCalls else None) + + +def _format_tool_message(message: ChatMessageContent) -> ToolMessage: + """Format a tool message to the expected object for the client. + + Args: + message: The tool message. + + Returns: + The formatted tool message. + """ + if len(message.items) != 1: + logger.warning( + "Unsupported number of items in Tool message while formatting chat history for Azure AI" + f" Inference: {len(message.items)}" + ) + + if not isinstance(message.items[0], FunctionResultContent): + logger.warning( + "Unsupported item type in Tool message while formatting chat history for Azure AI" + f" Inference: {type(message.items[0])}" + ) + + # The API expects the result to be a string, so we need to convert it to a string + return ToolMessage(content=str(message.items[0].result), tool_call_id=message.items[0].id) + + +MESSAGE_CONVERTERS: dict[AuthorRole, Callable[[ChatMessageContent], ChatRequestMessage]] = { + AuthorRole.SYSTEM: _format_system_message, + AuthorRole.USER: _format_user_message, + AuthorRole.ASSISTANT: _format_assistant_message, + AuthorRole.TOOL: _format_tool_message, +} diff --git a/python/semantic_kernel/connectors/ai/chat_completion_client_base.py b/python/semantic_kernel/connectors/ai/chat_completion_client_base.py index ab92d29fd65f..21332e7359b7 100644 --- a/python/semantic_kernel/connectors/ai/chat_completion_client_base.py +++ b/python/semantic_kernel/connectors/ai/chat_completion_client_base.py @@ -59,7 +59,7 @@ def _prepare_chat_history_for_request( chat_history: "ChatHistory", role_key: str = "role", content_key: str = "content", - ) -> list[dict[str, str | None]]: + ) -> Any: """Prepare the chat history for a request. Allowing customization of the key names for role/author, and optionally overriding the role. @@ -68,12 +68,14 @@ def _prepare_chat_history_for_request( They require a "tool_call_id" and (function) "name" key, and the "metadata" key should be removed. The "encoding" key should also be removed. + Override this method to customize the formatting of the chat history for a request. + Args: chat_history (ChatHistory): The chat history to prepare. role_key (str): The key name for the role/author. content_key (str): The key name for the content/message. Returns: - List[Dict[str, Optional[str]]]: The prepared chat history. + prepared_chat_history (Any): The prepared chat history for a request. """ return [message.to_dict(role_key=role_key, content_key=content_key) for message in chat_history.messages] diff --git a/python/semantic_kernel/connectors/ai/function_calling_utils.py b/python/semantic_kernel/connectors/ai/function_calling_utils.py index 70704093141f..e9ebb64d6f35 100644 --- a/python/semantic_kernel/connectors/ai/function_calling_utils.py +++ b/python/semantic_kernel/connectors/ai/function_calling_utils.py @@ -1,31 +1,23 @@ # Copyright (c) Microsoft. All rights reserved. -import logging -from typing import TYPE_CHECKING, Any +from typing import Any -from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.open_ai_prompt_execution_settings import ( - OpenAIChatPromptExecutionSettings, -) +from semantic_kernel.connectors.ai.function_choice_behavior import FunctionCallChoiceConfiguration +from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata -if TYPE_CHECKING: - from semantic_kernel.connectors.ai.function_choice_behavior import ( - FunctionCallChoiceConfiguration, - ) - from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.open_ai_prompt_execution_settings import ( - OpenAIChatPromptExecutionSettings, - ) - -logger = logging.getLogger(__name__) - def update_settings_from_function_call_configuration( - function_choice_configuration: "FunctionCallChoiceConfiguration", - settings: "OpenAIChatPromptExecutionSettings", + function_choice_configuration: FunctionCallChoiceConfiguration, + settings: PromptExecutionSettings, type: str, ) -> None: """Update the settings from a FunctionChoiceConfiguration.""" - if function_choice_configuration.available_functions: + if ( + function_choice_configuration.available_functions + and hasattr(settings, "tool_choice") + and hasattr(settings, "tools") + ): settings.tool_choice = type settings.tools = [ kernel_function_metadata_to_function_call_format(f) diff --git a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py index 5047b1c0901b..4bdb95b8d62b 100644 --- a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py +++ b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py @@ -14,12 +14,8 @@ from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase from semantic_kernel.connectors.ai.function_call_behavior import FunctionCallBehavior -from semantic_kernel.connectors.ai.function_calling_utils import ( - update_settings_from_function_call_configuration, -) -from semantic_kernel.connectors.ai.function_choice_behavior import ( - FunctionChoiceBehavior, -) +from semantic_kernel.connectors.ai.function_calling_utils import update_settings_from_function_call_configuration +from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.open_ai_prompt_execution_settings import ( OpenAIChatPromptExecutionSettings, ) @@ -33,10 +29,7 @@ from semantic_kernel.contents.text_content import TextContent from semantic_kernel.contents.utils.author_role import AuthorRole from semantic_kernel.contents.utils.finish_reason import FinishReason -from semantic_kernel.exceptions import ( - ServiceInvalidExecutionSettingsError, - ServiceInvalidResponseError, -) +from semantic_kernel.exceptions import ServiceInvalidExecutionSettingsError, ServiceInvalidResponseError from semantic_kernel.filters.auto_function_invocation.auto_function_invocation_context import ( AutoFunctionInvocationContext, ) diff --git a/python/tests/integration/completions/test_chat_completions.py b/python/tests/integration/completions/test_chat_completions.py index caeeef177615..e4af42884843 100644 --- a/python/tests/integration/completions/test_chat_completions.py +++ b/python/tests/integration/completions/test_chat_completions.py @@ -395,6 +395,58 @@ def services() -> dict[str, tuple[ChatCompletionClientBase, type[PromptExecution ["house", "germany"], id="azure_ai_inference_image_input_file", ), + pytest.param( + "azure_ai_inference", + { + "function_choice_behavior": FunctionChoiceBehavior.Auto( + auto_invoke=True, filters={"excluded_plugins": ["chat"]} + ) + }, + [ + ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="What is 3+345?")]), + ], + ["348"], + id="azure_ai_inference_tool_call_auto", + ), + pytest.param( + "azure_ai_inference", + { + "function_choice_behavior": FunctionChoiceBehavior.Auto( + auto_invoke=False, filters={"excluded_plugins": ["chat"]} + ) + }, + [ + ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="What is 3+345?")]), + ], + ["348"], + id="azure_ai_inference_tool_call_non_auto", + ), + pytest.param( + "azure_ai_inference", + {}, + [ + [ + ChatMessageContent( + role=AuthorRole.USER, + items=[TextContent(text="What was our 2024 revenue?")], + ), + ChatMessageContent( + role=AuthorRole.ASSISTANT, + items=[ + FunctionCallContent( + id="fin", name="finance-search", arguments='{"company": "contoso", "year": 2024}' + ) + ], + ), + ChatMessageContent( + role=AuthorRole.TOOL, + items=[FunctionResultContent(id="fin", name="finance-search", result="1.2B")], + ), + ], + ], + ["1.2"], + id="azure_ai_inference_tool_call_flow", + ), pytest.param( "mistral_ai", {}, diff --git a/python/tests/unit/connectors/open_ai/services/test_open_ai_chat_completion_base.py b/python/tests/unit/connectors/open_ai/services/test_open_ai_chat_completion_base.py index 38ac7313a121..a1eef6d81831 100644 --- a/python/tests/unit/connectors/open_ai/services/test_open_ai_chat_completion_base.py +++ b/python/tests/unit/connectors/open_ai/services/test_open_ai_chat_completion_base.py @@ -243,7 +243,7 @@ async def test_process_tool_calls_with_continuation_on_malformed_arguments(): ai_model_id="test_model_id", service_id="test", client=MagicMock(spec=AsyncOpenAI) ) - with patch("semantic_kernel.connectors.ai.function_calling_utils.logger", autospec=True): + with patch("semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion_base.logger", autospec=True): await chat_completion_base._process_function_call( tool_call_mock, chat_history_mock,