Python: Azure Responses client (#311)

* Azure Responses client

* Fix a change made in the wrong place

* allow api_version and token_endpoint to use env vars

* Add getting started sample

* add responses deployment name env var

* update azure clients to use defaults for api_version and token_endpoint

* make tests more reliable

---------

Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
This commit is contained in:
peterychang
2025-08-06 10:18:38 -04:00
committed by GitHub
Unverified
parent 5a1de491d4
commit f43939d803
12 changed files with 555 additions and 75 deletions
@@ -5,6 +5,7 @@ import importlib.metadata
from ._assistants_client import AzureAssistantsClient
from ._chat_client import AzureChatClient
from ._entra_id_authentication import get_entra_auth_token
from ._responses_client import AzureResponsesClient
from ._shared import AzureOpenAISettings
try:
@@ -16,6 +17,7 @@ __all__ = [
"AzureAssistantsClient",
"AzureChatClient",
"AzureOpenAISettings",
"AzureResponsesClient",
"__version__",
"get_entra_auth_token",
]
@@ -10,7 +10,6 @@ from pydantic import SecretStr, ValidationError
from pydantic.networks import AnyUrl
from ._shared import (
DEFAULT_AZURE_TOKEN_ENDPOINT,
AzureOpenAISettings,
)
@@ -75,10 +74,11 @@ class AzureAssistantsClient(OpenAIAssistantsClient):
base_url=AnyUrl(base_url) if base_url else None,
endpoint=AnyUrl(endpoint) if endpoint else None,
chat_deployment_name=deployment_name,
api_version=api_version or self.DEFAULT_AZURE_API_VERSION,
api_version=api_version,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
token_endpoint=token_endpoint or DEFAULT_AZURE_TOKEN_ENDPOINT,
token_endpoint=token_endpoint,
default_api_version=self.DEFAULT_AZURE_API_VERSION,
)
except ValidationError as ex:
raise ServiceInitializationError("Failed to create Azure OpenAI settings.", ex) from ex
@@ -26,8 +26,6 @@ from pydantic import SecretStr, ValidationError
from pydantic.networks import AnyUrl
from ._shared import (
DEFAULT_AZURE_API_VERSION,
DEFAULT_AZURE_TOKEN_ENDPOINT,
AzureOpenAIConfigBase,
AzureOpenAISettings,
)
@@ -87,16 +85,18 @@ class AzureChatClient(AzureOpenAIConfigBase, OpenAIChatClientBase):
base_url=AnyUrl(base_url) if base_url else None,
endpoint=AnyUrl(endpoint) if endpoint else None,
chat_deployment_name=deployment_name,
api_version=api_version or DEFAULT_AZURE_API_VERSION,
api_version=api_version,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
token_endpoint=token_endpoint or DEFAULT_AZURE_TOKEN_ENDPOINT,
token_endpoint=token_endpoint,
)
except ValidationError as exc:
raise ServiceInitializationError(f"Failed to validate settings: {exc}") from exc
if not azure_openai_settings.chat_deployment_name:
raise ServiceInitializationError("chat_deployment_name is required.")
if not azure_openai_settings.api_version:
raise ServiceInitializationError("api_version is required.")
super().__init__(
deployment_name=azure_openai_settings.chat_deployment_name,
@@ -0,0 +1,131 @@
# Copyright (c) Microsoft. All rights reserved.
from collections.abc import Mapping
from typing import Any, ClassVar
from urllib.parse import urljoin
from agent_framework import use_tool_calling
from agent_framework.exceptions import ServiceInitializationError
from agent_framework.openai._responses_client import OpenAIResponsesClientBase
from agent_framework.openai._shared import OpenAIModelTypes
from agent_framework.telemetry import use_telemetry
from openai.lib.azure import AsyncAzureADTokenProvider, AsyncAzureOpenAI
from pydantic import SecretStr, ValidationError
from pydantic.networks import AnyUrl
from ._shared import (
AzureOpenAIConfigBase,
AzureOpenAISettings,
)
@use_telemetry
@use_tool_calling
class AzureResponsesClient(AzureOpenAIConfigBase, OpenAIResponsesClientBase):
"""Azure Responses completion class."""
MODEL_PROVIDER_NAME: ClassVar[str] = "azure_openai" # type: ignore[reportIncompatibleVariableOverride, misc]
def __init__(
self,
api_key: str | None = None,
deployment_name: str | None = None,
endpoint: str | None = None,
base_url: str | None = None,
api_version: str | None = None,
ad_token: str | None = None,
ad_token_provider: AsyncAzureADTokenProvider | None = None,
token_endpoint: str | None = None,
default_headers: Mapping[str, str] | None = None,
async_client: AsyncAzureOpenAI | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
instruction_role: str | None = None,
) -> None:
"""Initialize an AzureResponses service.
Args:
api_key: The optional api key. If provided, will override the value in the
env vars or .env file.
deployment_name: The optional deployment. If provided, will override the value
(responses_deployment_name) in the env vars or .env file.
endpoint: The optional deployment endpoint. If provided will override the value
in the env vars or .env file.
base_url: The optional deployment base_url. If provided will override the value
in the env vars or .env file. Currently, the base_url must end with "/openai/v1/"
api_version: The optional deployment api version. If provided will override the value
in the env vars or .env file. Currently, the api_version must be "preview".
ad_token: The Azure Active Directory token. (Optional)
ad_token_provider: The Azure Active Directory token provider. (Optional)
token_endpoint: The token endpoint to request an Azure token. (Optional)
default_headers: The default headers mapping of string keys to
string values for HTTP requests. (Optional)
async_client: An existing client to use. (Optional)
env_file_path: Use the environment settings file as a fallback to using env vars.
env_file_encoding: The encoding of the environment settings file, defaults to 'utf-8'.
instruction_role: The role to use for 'instruction' messages, for example, summarization
prompts could use `developer` or `system`. (Optional)
"""
try:
# Filter out any None values from the arguments
azure_openai_settings = AzureOpenAISettings(
api_key=SecretStr(api_key) if api_key else None,
base_url=AnyUrl(base_url) if base_url else None,
endpoint=AnyUrl(endpoint) if endpoint else None,
responses_deployment_name=deployment_name,
api_version=api_version,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
token_endpoint=token_endpoint,
default_api_version="preview",
)
# TODO(peterychang): This is a temporary hack to ensure that the base_url is set correctly
# while this feature is in preview.
# But we should only do this if we're on azure. Private deployments may not need this.
if (
not azure_openai_settings.base_url
and azure_openai_settings.endpoint
and str(azure_openai_settings.endpoint).rstrip("/").endswith("openai.azure.com")
):
azure_openai_settings.base_url = AnyUrl(urljoin(str(azure_openai_settings.endpoint), "/openai/v1/"))
except ValidationError as exc:
raise ServiceInitializationError(f"Failed to validate settings: {exc}") from exc
if not azure_openai_settings.responses_deployment_name:
raise ServiceInitializationError("responses_deployment_name is required.")
if not azure_openai_settings.api_version:
raise ServiceInitializationError("api_version is required.")
super().__init__(
deployment_name=azure_openai_settings.responses_deployment_name,
endpoint=azure_openai_settings.endpoint,
base_url=azure_openai_settings.base_url,
api_version=azure_openai_settings.api_version,
api_key=azure_openai_settings.api_key.get_secret_value() if azure_openai_settings.api_key else None,
ad_token=ad_token,
ad_token_provider=ad_token_provider,
token_endpoint=azure_openai_settings.token_endpoint,
default_headers=default_headers,
ai_model_type=OpenAIModelTypes.RESPONSE,
client=async_client,
instruction_role=instruction_role,
)
@classmethod
def from_dict(cls, settings: dict[str, Any]) -> "AzureResponsesClient":
"""Initialize an Open AI service from a dictionary of settings.
Args:
settings: A dictionary of settings for the service.
"""
return AzureResponsesClient(
api_key=settings.get("api_key"),
deployment_name=settings.get("deployment_name"),
endpoint=settings.get("endpoint"),
base_url=settings.get("base_url"),
api_version=settings.get("api_version"),
ad_token=settings.get("ad_token"),
ad_token_provider=settings.get("ad_token_provider"),
default_headers=settings.get("default_headers"),
env_file_path=settings.get("env_file_path"),
)
@@ -1,7 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
import logging
import sys
from collections.abc import Awaitable, Callable, Mapping
from copy import copy
from typing import Any, ClassVar, Final
@@ -11,10 +11,15 @@ from agent_framework.exceptions import ServiceInitializationError
from agent_framework.openai._shared import OpenAIHandler, OpenAIModelTypes
from agent_framework.telemetry import USER_AGENT_KEY
from openai.lib.azure import AsyncAzureOpenAI
from pydantic import ConfigDict, SecretStr, validate_call
from pydantic import ConfigDict, SecretStr, model_validator, validate_call
from ._entra_id_authentication import get_entra_auth_token
if sys.version_info >= (3, 11):
from typing import Self # pragma: no cover
else:
from typing_extensions import Self # pragma: no cover
logger: logging.Logger = logging.getLogger(__name__)
@@ -94,11 +99,15 @@ class AzureOpenAISettings(AFBaseSettings):
your resource from the Azure portal, the endpoint should end in openai.azure.com.
If both base_url and endpoint are supplied, base_url will be used.
(Env var AZURE_OPENAI_ENDPOINT)
api_version: The API version to use. The default value is "2024-02-01".
api_version: The API version to use. The default value is `default_api_version`.
(Env var AZURE_OPENAI_API_VERSION)
token_endpoint: The token endpoint to use to retrieve the authentication token.
The default value is "https://cognitiveservices.azure.com/.default".
The default value is `default_token_endpoint`.
(Env var AZURE_OPENAI_TOKEN_ENDPOINT)
default_api_version: The default API version to use if not specified.
The default value is "2024-10-21".
default_token_endpoint: The default token endpoint to use if not specified.
The default value is "https://cognitiveservices.azure.com/.default".
Parameters:
env_file_path: The path to the .env file to load settings from.
@@ -118,8 +127,10 @@ class AzureOpenAISettings(AFBaseSettings):
endpoint: HttpsUrl | None = None
base_url: HttpsUrl | None = None
api_key: SecretStr | None = None
api_version: str = DEFAULT_AZURE_API_VERSION
token_endpoint: str = DEFAULT_AZURE_TOKEN_ENDPOINT
api_version: str | None = None
token_endpoint: str | None = None
default_api_version: str = DEFAULT_AZURE_API_VERSION
default_token_endpoint: str = DEFAULT_AZURE_TOKEN_ENDPOINT
def get_azure_openai_auth_token(self, token_endpoint: str | None = None) -> str | None:
"""Retrieve a Microsoft Entra Auth Token for a given token endpoint for the use with Azure OpenAI.
@@ -143,6 +154,12 @@ class AzureOpenAISettings(AFBaseSettings):
raise ServiceInitializationError("Please provide a token endpoint to retrieve the authentication token.")
return get_entra_auth_token(endpoint_to_use)
@model_validator(mode="after")
def _validate_fields(self) -> Self:
self.api_version = self.api_version or self.default_api_version
self.token_endpoint = self.token_endpoint or self.default_token_endpoint
return self
class AzureOpenAIConfigBase(OpenAIHandler):
"""Internal class for configuring a connection to an Azure OpenAI service."""
+1
View File
@@ -32,6 +32,7 @@ def azure_openai_unit_test_env(monkeypatch, exclude_list, override_env_param_dic
env_vars = {
"AZURE_OPENAI_ENDPOINT": "https://test-endpoint.com",
"AZURE_OPENAI_CHAT_DEPLOYMENT_NAME": "test_chat_deployment",
"AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME": "test_chat_deployment",
"AZURE_OPENAI_TEXT_DEPLOYMENT_NAME": "test_text_deployment",
"AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME": "test_embedding_deployment",
"AZURE_OPENAI_TEXT_TO_IMAGE_DEPLOYMENT_NAME": "test_text_to_image_deployment",
@@ -0,0 +1,287 @@
# Copyright (c) Microsoft. All rights reserved.
import os
from typing import Annotated
import pytest
from agent_framework import ChatClient, ChatMessage, ChatResponse, ChatResponseUpdate, TextContent, ai_function
from agent_framework.azure import AzureResponsesClient
from agent_framework.exceptions import ServiceInitializationError, ServiceResponseException
from pydantic import BaseModel
skip_if_azure_integration_tests_disabled = pytest.mark.skipif(
os.getenv("RUN_INTEGRATION_TESTS", "false").lower() != "true"
or os.getenv("AZURE_OPENAI_ENDPOINT", "") in ("", "https://test-endpoint.com"),
reason="No real AZURE_OPENAI_ENDPOINT provided; skipping integration tests."
if os.getenv("RUN_INTEGRATION_TESTS", "false").lower() == "true"
else "Integration tests are disabled.",
)
class OutputStruct(BaseModel):
"""A structured output for testing purposes."""
location: str
weather: str
@ai_function
async def get_weather(location: Annotated[str, "The location as a city name"]) -> str:
"""Get the current weather in a given location."""
# Implementation of the tool to get weather
return f"The current weather in {location} is sunny."
def test_init(azure_openai_unit_test_env: dict[str, str]) -> None:
# Test successful initialization
azure_responses_client = AzureResponsesClient()
assert azure_responses_client.ai_model_id == azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"]
assert isinstance(azure_responses_client, ChatClient)
def test_init_validation_fail() -> None:
# Test successful initialization
with pytest.raises(ServiceInitializationError):
AzureResponsesClient(api_key="34523", deployment_name={"test": "dict"}) # type: ignore
def test_init_ai_model_id_constructor(azure_openai_unit_test_env: dict[str, str]) -> None:
# Test successful initialization
ai_model_id = "test_model_id"
azure_responses_client = AzureResponsesClient(deployment_name=ai_model_id)
assert azure_responses_client.ai_model_id == ai_model_id
assert isinstance(azure_responses_client, ChatClient)
def test_init_with_default_header(azure_openai_unit_test_env: dict[str, str]) -> None:
default_headers = {"X-Unit-Test": "test-guid"}
# Test successful initialization
azure_responses_client = AzureResponsesClient(
default_headers=default_headers,
)
assert azure_responses_client.ai_model_id == azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"]
assert isinstance(azure_responses_client, ChatClient)
# Assert that the default header we added is present in the client's default headers
for key, value in default_headers.items():
assert key in azure_responses_client.client.default_headers
assert azure_responses_client.client.default_headers[key] == value
@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"]], indirect=True)
def test_init_with_empty_model_id(azure_openai_unit_test_env: dict[str, str]) -> None:
with pytest.raises(ServiceInitializationError):
AzureResponsesClient(
env_file_path="test.env",
)
def test_serialize(azure_openai_unit_test_env: dict[str, str]) -> None:
default_headers = {"X-Unit-Test": "test-guid"}
settings = {
"ai_model_id": azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"],
"api_key": azure_openai_unit_test_env["AZURE_OPENAI_API_KEY"],
"default_headers": default_headers,
}
azure_responses_client = AzureResponsesClient.from_dict(settings)
dumped_settings = azure_responses_client.to_dict()
assert dumped_settings["ai_model_id"] == azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"]
assert dumped_settings["api_key"] == azure_openai_unit_test_env["AZURE_OPENAI_API_KEY"]
# Assert that the default header we added is present in the dumped_settings default headers
for key, value in default_headers.items():
assert key in dumped_settings["default_headers"]
assert dumped_settings["default_headers"][key] == value
# Assert that the 'User-Agent' header is not present in the dumped_settings default headers
assert "User-Agent" not in dumped_settings["default_headers"]
@skip_if_azure_integration_tests_disabled
async def test_azure_responses_client_response() -> None:
"""Test azure responses client responses."""
azure_responses_client = AzureResponsesClient()
assert isinstance(azure_responses_client, ChatClient)
messages: list[ChatMessage] = []
messages.append(
ChatMessage(
role="user",
text="Emily and David, two passionate scientists, met during a research expedition to Antarctica. "
"Bonded by their love for the natural world and shared curiosity, they uncovered a "
"groundbreaking phenomenon in glaciology that could potentially reshape our understanding "
"of climate change.",
)
)
messages.append(ChatMessage(role="user", text="who are Emily and David?"))
# Test that the client can be used to get a response
response = await azure_responses_client.get_response(messages=messages)
assert response is not None
assert isinstance(response, ChatResponse)
assert "scientists" in response.text
messages.clear()
messages.append(ChatMessage(role="user", text="The weather in New York is sunny"))
messages.append(ChatMessage(role="user", text="What is the weather in New York?"))
# Test that the client can be used to get a response
response = await azure_responses_client.get_response(
messages=messages,
response_format=OutputStruct,
)
assert response is not None
assert isinstance(response, ChatResponse)
output = OutputStruct.model_validate_json(response.text)
assert output.location == "New York"
assert "sunny" in output.weather.lower()
@skip_if_azure_integration_tests_disabled
async def test_azure_responses_client_response_tools() -> None:
"""Test azure responses client tools."""
azure_responses_client = AzureResponsesClient()
assert isinstance(azure_responses_client, ChatClient)
messages: list[ChatMessage] = []
messages.append(ChatMessage(role="user", text="What is the weather in New York?"))
# Test that the client can be used to get a response
response = await azure_responses_client.get_response(
messages=messages,
tools=[get_weather],
tool_choice="auto",
)
assert response is not None
assert isinstance(response, ChatResponse)
assert "sunny" in response.text
messages.clear()
messages.append(ChatMessage(role="user", text="What is the weather in Seattle?"))
# Test that the client can be used to get a response
response = await azure_responses_client.get_response(
messages=messages,
tools=[get_weather],
tool_choice="auto",
response_format=OutputStruct,
)
assert response is not None
assert isinstance(response, ChatResponse)
output = OutputStruct.model_validate_json(response.text)
assert "Seattle" in output.location
assert "sunny" in output.weather.lower()
@skip_if_azure_integration_tests_disabled
async def test_azure_responses_client_streaming() -> None:
"""Test Azure azure responses client streaming responses."""
azure_responses_client = AzureResponsesClient()
assert isinstance(azure_responses_client, ChatClient)
messages: list[ChatMessage] = []
messages.append(
ChatMessage(
role="user",
text="Emily and David, two passionate scientists, met during a research expedition to Antarctica. "
"Bonded by their love for the natural world and shared curiosity, they uncovered a "
"groundbreaking phenomenon in glaciology that could potentially reshape our understanding "
"of climate change.",
)
)
messages.append(ChatMessage(role="user", text="who are Emily and David?"))
# Test that the client can be used to get a response
response = azure_responses_client.get_streaming_response(messages=messages)
full_message: str = ""
async for chunk in response:
assert chunk is not None
assert isinstance(chunk, ChatResponseUpdate)
for content in chunk.contents:
if isinstance(content, TextContent) and content.text:
full_message += content.text
assert "scientists" in full_message
messages.clear()
messages.append(ChatMessage(role="user", text="The weather in Seattle is sunny"))
messages.append(ChatMessage(role="user", text="What is the weather in Seattle?"))
# This is currently broken. See https://github.com/azure/azure-python/issues/2305
with pytest.raises(ServiceResponseException):
response = azure_responses_client.get_streaming_response(
messages=messages,
response_format=OutputStruct,
)
full_message = ""
async for chunk in response:
assert chunk is not None
assert isinstance(chunk, ChatResponseUpdate)
for content in chunk.contents:
if isinstance(content, TextContent) and content.text:
full_message += content.text
output = OutputStruct.model_validate_json(full_message)
assert "Seattle" in output.location
assert "sunny" in output.weather.lower()
@skip_if_azure_integration_tests_disabled
async def test_azure_responses_client_streaming_tools() -> None:
"""Test azure responses client streaming tools."""
azure_responses_client = AzureResponsesClient()
assert isinstance(azure_responses_client, ChatClient)
messages: list[ChatMessage] = [ChatMessage(role="user", text="What is the weather in Seattle?")]
# Test that the client can be used to get a response
response = azure_responses_client.get_streaming_response(
messages=messages,
tools=[get_weather],
tool_choice="auto",
)
full_message: str = ""
async for chunk in response:
assert chunk is not None
assert isinstance(chunk, ChatResponseUpdate)
for content in chunk.contents:
if isinstance(content, TextContent) and content.text:
full_message += content.text
assert "sunny" in full_message
messages.clear()
messages.append(ChatMessage(role="user", text="What is the weather in Seattle?"))
# This is currently broken. See https://github.com/azure/azure-python/issues/2305
with pytest.raises(ServiceResponseException):
response = azure_responses_client.get_streaming_response(
messages=messages,
tools=[get_weather],
tool_choice="auto",
response_format=OutputStruct,
)
full_message = ""
async for chunk in response:
assert chunk is not None
assert isinstance(chunk, ChatResponseUpdate)
for content in chunk.contents:
if isinstance(content, TextContent) and content.text:
full_message += content.text
output = OutputStruct.model_validate_json(full_message)
assert "Seattle" in output.location
assert "sunny" in output.weather.lower()
@@ -9,6 +9,7 @@ _IMPORTS = [
"AzureAssistantsClient",
"AzureChatClient",
"AzureOpenAISettings",
"AzureResponsesClient",
"get_entra_auth_token",
"__version__",
]
@@ -4,6 +4,7 @@ from agent_framework_azure import (
AzureAssistantsClient,
AzureChatClient,
AzureOpenAISettings,
AzureResponsesClient,
__version__,
get_entra_auth_token,
)
@@ -12,6 +13,7 @@ __all__ = [
"AzureAssistantsClient",
"AzureChatClient",
"AzureOpenAISettings",
"AzureResponsesClient",
"__version__",
"get_entra_auth_token",
]
@@ -52,68 +52,7 @@ __all__ = ["OpenAIResponsesClient"]
# region ResponsesClient
@use_telemetry
@use_tool_calling
class OpenAIResponsesClient(OpenAIConfigBase, ChatClientBase, OpenAIHandler):
"""OpenAI Responses client class."""
MODEL_PROVIDER_NAME: ClassVar[str] = "openai" # type: ignore[reportIncompatibleVariableOverride, misc]
def __init__(
self,
ai_model_id: str | None = None,
api_key: str | None = None,
org_id: str | None = None,
default_headers: Mapping[str, str] | None = None,
async_client: AsyncOpenAI | None = None,
instruction_role: str | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
"""Initialize an OpenAIChatCompletion service.
Args:
ai_model_id: OpenAI model name, see
https://platform.openai.com/docs/models
api_key: The optional API key to use. If provided will override,
the env vars or .env file value.
org_id: The optional org ID to use. If provided will override,
the env vars or .env file value.
default_headers: The default headers mapping of string keys to
string values for HTTP requests. (Optional)
async_client: An existing client to use. (Optional)
instruction_role: The role to use for 'instruction' messages, for example,
"system" or "developer". If not provided, the default is "system".
env_file_path: Use the environment settings file as a fallback
to environment variables. (Optional)
env_file_encoding: The encoding of the environment settings file. (Optional)
"""
try:
openai_settings = OpenAISettings(
api_key=SecretStr(api_key) if api_key else None,
org_id=org_id,
responses_model_id=ai_model_id,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
except ValidationError as ex:
raise ServiceInitializationError("Failed to create OpenAI settings.", ex) from ex
if not async_client and not openai_settings.api_key:
raise ServiceInitializationError("The OpenAI API key is required.")
if not openai_settings.responses_model_id:
raise ServiceInitializationError("The OpenAI model ID is required.")
super().__init__(
ai_model_id=openai_settings.responses_model_id,
api_key=openai_settings.api_key.get_secret_value() if openai_settings.api_key else None,
org_id=openai_settings.org_id,
ai_model_type=OpenAIModelTypes.RESPONSE,
default_headers=default_headers,
client=async_client,
instruction_role=instruction_role,
)
class OpenAIResponsesClientBase(OpenAIHandler, ChatClientBase):
def _filter_options(self, **kwargs: Any) -> dict[str, Any]:
"""Filter options for the responses call."""
# The responses call does not support all the options that the chat completion call does.
@@ -544,6 +483,69 @@ class OpenAIResponsesClient(OpenAIConfigBase, ChatClientBase, OpenAIHandler):
"logprobs": getattr(output, "logprobs", None),
}
@use_telemetry
@use_tool_calling
class OpenAIResponsesClient(OpenAIConfigBase, OpenAIResponsesClientBase):
"""OpenAI Responses client class."""
MODEL_PROVIDER_NAME: ClassVar[str] = "openai" # type: ignore[reportIncompatibleVariableOverride, misc]
def __init__(
self,
ai_model_id: str | None = None,
api_key: str | None = None,
org_id: str | None = None,
default_headers: Mapping[str, str] | None = None,
async_client: AsyncOpenAI | None = None,
instruction_role: str | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
"""Initialize an OpenAIChatCompletion service.
Args:
ai_model_id: OpenAI model name, see
https://platform.openai.com/docs/models
api_key: The optional API key to use. If provided will override,
the env vars or .env file value.
org_id: The optional org ID to use. If provided will override,
the env vars or .env file value.
default_headers: The default headers mapping of string keys to
string values for HTTP requests. (Optional)
async_client: An existing client to use. (Optional)
instruction_role: The role to use for 'instruction' messages, for example,
"system" or "developer". If not provided, the default is "system".
env_file_path: Use the environment settings file as a fallback
to environment variables. (Optional)
env_file_encoding: The encoding of the environment settings file. (Optional)
"""
try:
openai_settings = OpenAISettings(
api_key=SecretStr(api_key) if api_key else None,
org_id=org_id,
responses_model_id=ai_model_id,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
except ValidationError as ex:
raise ServiceInitializationError("Failed to create OpenAI settings.", ex) from ex
if not async_client and not openai_settings.api_key:
raise ServiceInitializationError("The OpenAI API key is required.")
if not openai_settings.responses_model_id:
raise ServiceInitializationError("The OpenAI model ID is required.")
super().__init__(
ai_model_id=openai_settings.responses_model_id,
api_key=openai_settings.api_key.get_secret_value() if openai_settings.api_key else None,
org_id=openai_settings.org_id,
ai_model_type=OpenAIModelTypes.RESPONSE,
default_headers=default_headers,
client=async_client,
instruction_role=instruction_role,
)
@classmethod
def from_dict(cls, settings: dict[str, Any]) -> "OpenAIResponsesClient":
"""Initialize an Open AI service from a dictionary of settings.
@@ -0,0 +1,36 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from random import randint
from typing import Annotated
from agent_framework.azure import AzureResponsesClient
from pydantic import Field
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
) -> str:
"""Get the weather for a given location."""
conditions = ["sunny", "cloudy", "rainy", "stormy"]
return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."
async def main() -> None:
client = AzureResponsesClient()
message = "What's the weather in Amsterdam and in Paris?"
stream = False
print(f"User: {message}")
if stream:
print("Assistant: ", end="")
async for chunk in client.get_streaming_response(message, tools=get_weather):
if str(chunk):
print(str(chunk), end="")
print("")
else:
response = await client.get_response(message, tools=get_weather)
print(f"Assistant: {response}")
if __name__ == "__main__":
asyncio.run(main())