Python: Support structured outputs with Azure AI inference chat completion. (#12183)

### Motivation and Context

The Azure AI inference SDK released support for structured outputs for
chat completions (for models that support it). We had not brought this
into SK. This PR adds functionality and the ability to run the
structured output samples that exist today.

<!-- 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.
-->

### Description

Adds functionality to handle structured outputs for the Azure AI
Inference Chat Completion calls.
- Fixes a bug within AzureAIInferenceChatCompletion where the wrong
isinstance check type is used. This prevented adding tool calls during
streaming invocation. Also properly add the `ai_model_id` from the
connector during message construction during streaming invocation,
- Adds unit tests for AzureAIInferenceChatCompletion structured outputs
- Closes #11952

<!-- Describe your changes, the overall approach, the underlying design.
These notes will help understanding how your code works. Thanks! -->

### 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
- [X] I didn't break anyone 😄
This commit is contained in:
Evan Mattson
2025-05-21 09:29:03 +00:00
committed by GitHub
parent 318e09232b
commit c084b067de
6 changed files with 348 additions and 166 deletions
@@ -1,107 +1,95 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import json
from pydantic import BaseModel, ConfigDict
from samples.concepts.setup.chat_completion_services import Services, get_chat_completion_service_and_request_settings
from semantic_kernel import Kernel
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
from semantic_kernel.connectors.ai.open_ai.services.azure_chat_completion import AzureChatCompletion
from semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion import OpenAIChatCompletion
from semantic_kernel.contents import ChatHistory
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
from semantic_kernel.connectors.ai import FunctionChoiceBehavior
from semantic_kernel.contents import ChatHistory, StreamingChatMessageContent
###################################################################
# The following sample demonstrates how to create a chat #
# completion call that assists users in solving math problems. #
# The bot guides the user step-by-step through the solution #
# process using a structured output format based on either a #
# Pydantic model or a non-Pydantic model. #
###################################################################
"""
The following sample demonstrates how to create a chat
completion call that assists users in solving math problems.
The bot guides the user step-by-step through the solution
process using a structured output format based on either a
Pydantic model or a non-Pydantic model.
###################################################################
# NOTE: If using Azure OpenAI the the following is required:
# - access to gpt-4o-2024-08-06
# - the 2024-08-01-preview API version
# - if using a token instead of an API KEY, you must have the
# `Cognitive Services OpenAI Contributor` role assigned to your
# Azure AD user.
# - flip the `use_azure_openai` flag to `True`
###################################################################
use_azure_openai = False
NOTE: If using Azure OpenAI the the following is required:
- access to gpt-4o-2024-08-06
- the 2024-08-01-preview API version
- if using a token instead of an API KEY, you must have the
`Cognitive Services OpenAI Contributor` role assigned to your
Azure AD user.
- flip the `use_azure_openai` flag to `True`
"""
system_message = """
You are a helpful math tutor. Guide the user through the solution step by step.
"""
"""
Define the Pydantic model that represents the
structured output from the OpenAI service. This model will be
used to parse the structured output from the OpenAI service,
and ensure that the model correctly outputs the schema based
on the Pydantic model.
"""
###################################################################
# OPTION 1: Define the Pydantic model that represents the
# structured output from the OpenAI service. This model will be
# used to parse the structured output from the OpenAI service,
# and ensure that the model correctly outputs the schema based
# on the Pydantic model.
from semantic_kernel.kernel_pydantic import KernelBaseModel # noqa: E402
# Note: The `extra=forbid` means to forbid extra fields during model initialization
# It is required to ensure that the model is strict and does not
# accept any extra fields that are not defined in the model.
class Step(KernelBaseModel):
class Step(BaseModel):
model_config = ConfigDict(extra="forbid")
explanation: str
output: str
class Reasoning(KernelBaseModel):
class Reasoning(BaseModel):
model_config = ConfigDict(extra="forbid")
steps: list[Step]
final_answer: str
###################################################################
# OPTION 2: Define a non-Pydantic model that should represent the
# structured output from the OpenAI service. This model will be
# converted to the proper JSON Schema and sent to the LLM.
# Uncomment the follow lines and comment out the Pydantic model
# above to use this option.
# class Step:
# explanation: str
# output: str
# class Reasoning:
# steps: list[Step]
# final_answer: str
###################################################################
kernel = Kernel()
service_id = "structured-output"
if use_azure_openai:
chat_service = AzureChatCompletion(
service_id=service_id,
)
else:
chat_service = OpenAIChatCompletion(
service_id=service_id,
)
kernel.add_service(chat_service)
# You can select from the following chat completion services:
# Note: the model must allow for structured outputs.
# - Services.OPENAI
# - Services.AZURE_OPENAI
# - Services.AZURE_AI_INFERENCE
# - Services.ANTHROPIC
# - Services.BEDROCK
# - Services.GOOGLE_AI
# - Services.MISTRAL_AI
# - Services.OLLAMA
# - Services.ONNX
# - Services.VERTEX_AI
# - Services.DEEPSEEK
# Please make sure you have configured your environment correctly for the selected chat completion service.
chat_completion_service, request_settings = get_chat_completion_service_and_request_settings(Services.AZURE_OPENAI)
kernel.add_service(chat_completion_service)
req_settings = kernel.get_prompt_execution_settings_from_service_id(service_id=service_id)
req_settings.max_tokens = 2000
req_settings.temperature = 0.7
req_settings.top_p = 0.8
req_settings.function_choice_behavior = FunctionChoiceBehavior.Auto(filters={"excluded_plugins": ["chat"]})
request_settings.max_tokens = 2000
request_settings.temperature = 0.7
request_settings.top_p = 0.8
request_settings.function_choice_behavior = FunctionChoiceBehavior.Auto(filters={"excluded_plugins": ["chat"]})
# NOTE: This is the key setting in this example that tells the OpenAI service
# to return structured output based on the Pydantic model Reasoning.
req_settings.response_format = Reasoning
request_settings.response_format = Reasoning
chat_function = kernel.add_function(
prompt=system_message + """{{$chat_history}}""",
function_name="chat",
plugin_name="chat",
prompt_execution_settings=req_settings,
prompt_execution_settings=request_settings,
)
history = ChatHistory()
@@ -127,9 +115,23 @@ async def main():
chat_function,
chat_history=history,
)
reasoned_result = Reasoning.model_validate_json(result.value[0].content)
print(f"Mosscap:> {reasoned_result}")
history.add_assistant_message(str(result))
reasoned_result = Reasoning.model_validate(json.loads(result.value[0].content))
print(f"{reasoned_result.model_dump_json(indent=4)}")
history.add_assistant_message(str(result))
"""
Sample Output:
{
"steps": [
{
"explanation": "User requested the current weather condition in Paris, so I utilized the 'weather-get_weather_for_city' function to retrieve the data.",
"output": "The weather in Paris is 60 degrees Fahrenheit and rainy."
}
],
"final_answer": "The current weather in Paris is 60 degrees Fahrenheit and rainy."
}
""" # noqa: E501
if __name__ == "__main__":
@@ -1,39 +1,35 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import json
from functools import reduce
from typing import Annotated
from samples.concepts.setup.chat_completion_services import Services, get_chat_completion_service_and_request_settings
from semantic_kernel import Kernel
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
from semantic_kernel.connectors.ai.open_ai.services.azure_chat_completion import AzureChatCompletion
from semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion import OpenAIChatCompletion
from semantic_kernel.contents import ChatHistory
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
from semantic_kernel.functions.kernel_function_decorator import kernel_function
from semantic_kernel.connectors.ai import FunctionChoiceBehavior
from semantic_kernel.contents import ChatHistory, FunctionResultContent, StreamingChatMessageContent
from semantic_kernel.functions import kernel_function
###################################################################
# The following sample demonstrates how to create a chat #
# completion call that assists users in solving a question #
# using a Semantic Kernel Plugin and function calling problems. #
# The chat plugin guides the user step-by-step through the #
# solution process using a structured output format based on #
# either a Pydantic model or a non-Pydantic model #
###################################################################
"""
The following sample demonstrates how to create a chat
completion call that assists users in solving a question
using a Semantic Kernel Plugin and function calling problems.
The chat plugin guides the user step-by-step through the
solution process using a structured output format based on
a Pydantic model.
###################################################################
# NOTE: If using Azure OpenAI the the following is required:
# - access to gpt-4o-2024-08-06
# - the 2024-08-01-preview API version
# - if using a token instead of an API KEY, you must have the
# `Cognitive Services OpenAI Contributor` role assigned to your
# Azure AD user.
# - flip the `use_azure_openai` flag to `True`
###################################################################
use_azure_openai = True
NOTE: If using Azure OpenAI the the following is required:
- access to gpt-4o-2024-08-06
- the 2024-08-01-preview API version
- if using a token instead of an API KEY, you must have the
`Cognitive Services OpenAI Contributor` role assigned to your
Azure AD user.
- flip the `use_azure_openai` flag to `True`
"""
system_message = """
You are a helpful math tutor. Guide the user through the solution step by step.
You are a helpful assistant who provides answers to the user's questions in structured JSON format.
"""
@@ -60,84 +56,69 @@ class WeatherPlugin:
return "31 and snowing"
###################################################################
# OPTION 1: Define the Pydantic model that represents the
# structured output from the OpenAI service. This model will be
# used to parse the structured output from the OpenAI service,
# and ensure that the model correctly outputs the schema based
# on the Pydantic model.
from semantic_kernel.kernel_pydantic import KernelBaseModel # noqa: E402
"""
Define the Pydantic model that represents the
structured output from the OpenAI service. This model will be
used to parse the structured output from the OpenAI service,
and ensure that the model correctly outputs the schema based
on the Pydantic model.
"""
from pydantic import BaseModel, ConfigDict # noqa: E402
# Note: The `extra=forbid` means to forbid extra fields during model initialization
# It is required to ensure that the model is strict and does not
# accept any extra fields that are not defined in the model.
class Step(KernelBaseModel):
class Step(BaseModel):
model_config = ConfigDict(extra="forbid")
explanation: str
output: str
class Reasoning(KernelBaseModel):
class Reasoning(BaseModel):
model_config = ConfigDict(extra="forbid")
steps: list[Step]
final_answer: str
###################################################################
# OPTION 2: Define a non-Pydantic model that should represent the
# structured output from the OpenAI service. This model will be
# converted to the proper JSON Schema and sent to the LLM.
# Uncomment the follow lines and comment out the Pydantic model
# above to use this option.
# class Step:
# explanation: str
# output: str
# class Reasoning:
# steps: list[Step]
# final_answer: str
###################################################################
kernel = Kernel()
service_id = "structured-output"
if use_azure_openai:
chat_service = AzureChatCompletion(
service_id=service_id,
)
else:
chat_service = OpenAIChatCompletion(
service_id=service_id,
)
kernel.add_service(chat_service)
# You can select from the following chat completion services:
# Note: the model must allow for structured outputs.
# - Services.OPENAI
# - Services.AZURE_OPENAI
# - Services.AZURE_AI_INFERENCE
# - Services.ANTHROPIC
# - Services.BEDROCK
# - Services.GOOGLE_AI
# - Services.MISTRAL_AI
# - Services.OLLAMA
# - Services.ONNX
# - Services.VERTEX_AI
# - Services.DEEPSEEK
# Please make sure you have configured your environment correctly for the selected chat completion service.
chat_completion_service, request_settings = get_chat_completion_service_and_request_settings(Services.AZURE_OPENAI)
kernel.add_service(chat_completion_service)
kernel.add_plugin(WeatherPlugin(), plugin_name="weather")
req_settings = kernel.get_prompt_execution_settings_from_service_id(service_id=service_id)
req_settings.max_tokens = 2000
req_settings.temperature = 0.7
req_settings.top_p = 0.8
req_settings.function_choice_behavior = FunctionChoiceBehavior.Auto(filters={"excluded_plugins": ["chat"]})
# NOTE: This is the key setting in this example that tells the OpenAI service
# to return structured output based on the Pydantic model Reasoning.
req_settings.response_format = Reasoning
request_settings.response_format = Reasoning
request_settings.function_choice_behavior = FunctionChoiceBehavior.Auto(filters={"excluded_plugins": ["chat"]})
chat_function = kernel.add_function(
prompt=system_message + """{{$chat_history}}""",
function_name="chat",
plugin_name="chat",
prompt_execution_settings=req_settings,
prompt_execution_settings=request_settings,
)
history = ChatHistory()
history.add_user_message("Using the available plugin, what is the weather in Paris?")
history.add_user_message("What is the weather in Paris?")
async def main():
stream = True
stream = False
if stream:
answer = kernel.invoke_stream(
chat_function,
@@ -146,17 +127,38 @@ async def main():
print("Mosscap:> ", end="")
result_content: list[StreamingChatMessageContent] = []
async for message in answer:
result_content.append(message[0])
print(str(message[0]), end="", flush=True)
if not any(isinstance(item, FunctionResultContent | FunctionResultContent) for item in message[0].items):
print(str(message[0]), end="", flush=True)
result_content.append(message[0])
print()
if result_content:
result = "".join([str(content) for content in result_content])
full_response: StreamingChatMessageContent = reduce(lambda x, y: x + y, result_content)
reasoned_result = Reasoning.model_validate(json.loads(full_response.content))
print("Result formatted in JSON:")
print(f"{reasoned_result.model_dump_json(indent=4)}")
history.add_assistant_message(str(full_response))
else:
result = await kernel.invoke(
chat_function,
chat_history=history,
)
print(f"Mosscap:> {result}")
history.add_assistant_message(str(result))
reasoned_result = Reasoning.model_validate(json.loads(result.value[0].content))
print(f"{reasoned_result.model_dump_json(indent=4)}")
history.add_assistant_message(str(result))
"""
Sample Output:
{
"steps": [
{
"explanation": "User requested the current weather condition in Paris, so I utilized the 'weather-get_weather_for_city' function to retrieve the data.",
"output": "The weather in Paris is 60 degrees Fahrenheit and rainy."
}
],
"final_answer": "The current weather in Paris is 60 degrees Fahrenheit and rainy."
}
""" # noqa: E501
if __name__ == "__main__":
@@ -2,9 +2,10 @@
from typing import Annotated, Any, Literal
from pydantic import Field
from pydantic import BaseModel, Field, model_validator
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.exceptions.service_exceptions import ServiceInvalidExecutionSettingsError
from semantic_kernel.utils.feature_stage_decorator import experimental
@@ -30,6 +31,12 @@ class AzureAIInferencePromptExecutionSettings(PromptExecutionSettings):
class AzureAIInferenceChatPromptExecutionSettings(AzureAIInferencePromptExecutionSettings):
"""Azure AI Inference Chat Prompt Execution Settings."""
response_format: (
dict[Literal["type"], Literal["text", "json_object"]] | dict[str, Any] | type[BaseModel] | type | None
) = None
structured_json_response: Annotated[
bool, Field(description="Do not set this manually. It is set by the service.")
] = False
tools: Annotated[
list[dict[str, Any]] | None,
Field(
@@ -45,6 +52,39 @@ class AzureAIInferenceChatPromptExecutionSettings(AzureAIInferencePromptExecutio
),
] = None
@model_validator(mode="before")
def validate_response_format_and_set_flag(cls, values: Any) -> Any:
"""Validate the response_format and set structured_json_response accordingly."""
if not isinstance(values, dict):
return values
response_format = values.get("response_format", None)
if response_format is None:
return values
if isinstance(response_format, dict):
if response_format.get("type") == "json_object":
return values
if response_format.get("type") == "json_schema":
json_schema = response_format.get("json_schema")
if isinstance(json_schema, dict):
values["structured_json_response"] = True
return values
raise ServiceInvalidExecutionSettingsError(
"If response_format has type 'json_schema', 'json_schema' must be a valid dictionary."
)
if isinstance(response_format, type):
if issubclass(response_format, BaseModel):
values["structured_json_response"] = True
else:
values["structured_json_response"] = True
else:
raise ServiceInvalidExecutionSettingsError(
"response_format must be a dictionary, a subclass of BaseModel, a Python class/type, or None"
)
return values
@experimental
class AzureAIInferenceEmbeddingPromptExecutionSettings(PromptExecutionSettings):
@@ -5,11 +5,6 @@ import sys
from collections.abc import AsyncGenerator, Callable
from typing import TYPE_CHECKING, Any, ClassVar
if sys.version_info >= (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 (
AsyncStreamingChatCompletions,
@@ -17,9 +12,12 @@ from azure.ai.inference.models import (
ChatCompletions,
ChatCompletionsToolCall,
ChatRequestMessage,
JsonSchemaFormat,
StreamingChatChoiceUpdate,
StreamingChatCompletionsUpdate,
StreamingChatResponseToolCallUpdate,
)
from pydantic import BaseModel
from semantic_kernel.connectors.ai.azure_ai_inference import AzureAIInferenceChatPromptExecutionSettings
from semantic_kernel.connectors.ai.azure_ai_inference.services.azure_ai_inference_base import (
@@ -42,12 +40,18 @@ 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 ServiceInvalidExecutionSettingsError
from semantic_kernel.schema.kernel_json_schema_builder import KernelJsonSchemaBuilder
from semantic_kernel.utils.feature_stage_decorator import experimental
if TYPE_CHECKING:
from semantic_kernel.connectors.ai.function_call_choice_configuration import FunctionCallChoiceConfiguration
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
logger: logging.Logger = logging.getLogger(__name__)
@@ -134,12 +138,14 @@ class AzureAIInferenceChatCompletion(ChatCompletionClientBase, AzureAIInferenceB
assert isinstance(self.client, ChatCompletionsClient) # nosec
with AzureAIInferenceTracing():
settings_dict = settings.prepare_settings_dict()
self._handle_structured_output(settings, settings_dict)
response: ChatCompletions = await self.client.complete(
messages=self._prepare_chat_history_for_request(chat_history),
# The model id will be ignored by the service if the endpoint serves only one model (i.e. MaaS)
model=self.ai_model_id,
model_extras=settings.extra_parameters,
**settings.prepare_settings_dict(),
**settings_dict,
)
response_metadata = self._get_metadata_from_response(response)
@@ -158,13 +164,15 @@ class AzureAIInferenceChatCompletion(ChatCompletionClientBase, AzureAIInferenceB
assert isinstance(self.client, ChatCompletionsClient) # nosec
with AzureAIInferenceTracing():
settings_dict = settings.prepare_settings_dict()
self._handle_structured_output(settings, settings_dict)
response: AsyncStreamingChatCompletions = await self.client.complete(
stream=True,
# The model id will be ignored by the service if the endpoint serves only one model (i.e. MaaS)
model=self.ai_model_id,
messages=self._prepare_chat_history_for_request(chat_history),
model_extras=settings.extra_parameters,
**settings.prepare_settings_dict(),
**settings_dict,
)
async for chunk in response:
@@ -223,6 +231,37 @@ class AzureAIInferenceChatCompletion(ChatCompletionClientBase, AzureAIInferenceB
return chat_request_messages
def _handle_structured_output(
self, request_settings: AzureAIInferenceChatPromptExecutionSettings, settings: dict[str, Any]
) -> None:
response_format = getattr(request_settings, "response_format", None)
if getattr(request_settings, "structured_json_response", False) and response_format:
# Case 1: response_format is a Pydantic BaseModel type
if isinstance(response_format, type) and issubclass(response_format, BaseModel):
schema = response_format.model_json_schema()
settings["response_format"] = JsonSchemaFormat(
name=response_format.__name__,
schema=schema,
description=f"Schema for {response_format.__name__}",
strict=True,
)
# Case 2: response_format is a type but not a subclass of BaseModel
elif isinstance(response_format, type):
generated_schema = KernelJsonSchemaBuilder.build(parameter_type=response_format, structured_output=True)
assert generated_schema is not None # nosec
settings["response_format"] = JsonSchemaFormat(
name=response_format.__name__,
schema=generated_schema,
description=f"Schema for {response_format.__name__}",
strict=True,
)
# Case 3: response_format is already a JsonSchemaFormat instance, pass it
elif isinstance(response_format, JsonSchemaFormat):
settings["response_format"] = response_format
# Case 4: response_format is a dictionary (legacy), create JsonSchemaFormat from dict
elif isinstance(response_format, dict):
settings["response_format"] = JsonSchemaFormat(**response_format)
# endregion
# region Non-streaming
@@ -301,7 +340,7 @@ class AzureAIInferenceChatCompletion(ChatCompletionClientBase, AzureAIInferenceB
)
if choice.delta.tool_calls:
for tool_call in choice.delta.tool_calls:
if isinstance(tool_call, ChatCompletionsToolCall):
if isinstance(tool_call, StreamingChatResponseToolCallUpdate):
items.append(
FunctionCallContent(
id=tool_call.id,
@@ -312,13 +351,14 @@ class AzureAIInferenceChatCompletion(ChatCompletionClientBase, AzureAIInferenceB
)
return StreamingChatMessageContent(
role=AuthorRole(choice.delta.role) if choice.delta.role else AuthorRole.ASSISTANT,
role=(AuthorRole(choice.delta.role) if choice.delta and choice.delta.role else AuthorRole.ASSISTANT),
items=items,
choice_index=choice.index,
inner_content=chunk,
finish_reason=FinishReason(choice.finish_reason) if choice.finish_reason else None,
metadata=metadata,
function_invoke_attempt=function_invoke_attempt,
ai_model_id=self.ai_model_id,
)
# endregion
@@ -15,6 +15,7 @@ from azure.ai.inference.models import (
FunctionCall,
StreamingChatChoiceUpdate,
StreamingChatCompletionsUpdate,
StreamingChatResponseToolCallUpdate,
)
from azure.core.credentials import AzureKeyCredential
@@ -249,7 +250,7 @@ def mock_azure_ai_inference_streaming_chat_completion_response_with_tool_call(mo
delta=ChatResponseMessage(
role="assistant",
tool_calls=[
ChatCompletionsToolCall(
StreamingChatResponseToolCallUpdate(
id="test_id",
function=FunctionCall(
name="getLightStatus",
@@ -4,8 +4,9 @@ from unittest.mock import AsyncMock, patch
import pytest
from azure.ai.inference.aio import ChatCompletionsClient
from azure.ai.inference.models import UserMessage
from azure.ai.inference.models import JsonSchemaFormat, UserMessage
from azure.core.credentials import AzureKeyCredential
from pydantic import BaseModel, Field
from semantic_kernel.connectors.ai.azure_ai_inference import (
AzureAIInferenceChatCompletion,
@@ -335,6 +336,55 @@ async def test_azure_ai_inference_chat_completion_with_function_choice_behavior_
assert responses[0].content == "Hello"
class MockResponseModel(BaseModel):
a: int = Field(..., description="The a field")
b: str = Field(..., description="The b field")
@pytest.mark.parametrize(
"azure_ai_inference_service",
[AzureAIInferenceChatCompletion.__name__],
indirect=True,
)
@patch.object(ChatCompletionsClient, "complete", new_callable=AsyncMock)
async def test_azure_ai_inference_response_format_json_schema(
mock_complete,
azure_ai_inference_service,
model_id,
chat_history: ChatHistory,
mock_azure_ai_inference_chat_completion_response,
):
chat_history.add_user_message("Return an object with fields a and b.")
settings = AzureAIInferenceChatPromptExecutionSettings(
response_format=MockResponseModel,
structured_json_response=True,
)
mock_complete.return_value = mock_azure_ai_inference_chat_completion_response
_ = await azure_ai_inference_service.get_chat_message_contents(chat_history=chat_history, settings=settings)
assert mock_complete.call_count == 1
kwargs = mock_complete.call_args.kwargs
assert "response_format" in kwargs
response_format = kwargs["response_format"]
assert isinstance(response_format, JsonSchemaFormat)
assert response_format.name == "MockResponseModel"
assert response_format.strict is True
schema = response_format.schema
assert schema["title"] == "MockResponseModel"
assert "properties" in schema
assert "a" in schema["properties"]
assert schema["properties"]["a"]["type"] == "integer"
assert "b" in schema["properties"]
assert schema["properties"]["b"]["type"] == "string"
assert kwargs["messages"][0].content == "Return an object with fields a and b."
assert kwargs["model"] == model_id
# endregion chat completion
@@ -594,4 +644,51 @@ async def test_azure_ai_inference_streaming_chat_completion_with_function_choice
)
class MockStreamingResponseModel(BaseModel):
foo: float = Field(..., description="Foo value")
bar: bool = Field(..., description="Bar value")
@pytest.mark.parametrize(
"azure_ai_inference_service",
[AzureAIInferenceChatCompletion.__name__],
indirect=True,
)
@patch.object(ChatCompletionsClient, "complete", new_callable=AsyncMock)
async def test_azure_ai_inference_streaming_response_format_json_schema(
mock_complete,
azure_ai_inference_service,
model_id,
chat_history: ChatHistory,
mock_azure_ai_inference_streaming_chat_completion_response,
):
chat_history.add_user_message("Stream a response with foo and bar.")
settings = AzureAIInferenceChatPromptExecutionSettings(
response_format=MockStreamingResponseModel,
structured_json_response=True,
)
mock_complete.return_value = mock_azure_ai_inference_streaming_chat_completion_response
messages = []
async for chunk in azure_ai_inference_service.get_streaming_chat_message_contents(chat_history, settings):
messages.extend(chunk)
assert mock_complete.call_count == 1
kwargs = mock_complete.call_args.kwargs
assert "response_format" in kwargs
response_format = kwargs["response_format"]
assert isinstance(response_format, JsonSchemaFormat)
assert response_format.name == "MockStreamingResponseModel"
assert response_format.strict is True
schema = response_format.schema
assert schema["title"] == "MockStreamingResponseModel"
assert "foo" in schema["properties"]
assert schema["properties"]["foo"]["type"] == "number"
assert "bar" in schema["properties"]
assert schema["properties"]["bar"]["type"] == "boolean"
assert kwargs["stream"] is True
assert kwargs["model"] == model_id
# endregion streaming chat completion