Python: Azure chat client (#185)

* updated openai, fcc works, with sample

* reduced files in openai

* Add azure chat client

* fix tests

* Update python/packages/main/tests/unit/test_openai_chat_completion_base.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update python/packages/azure/agent_framework/azure/__init__.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update python/packages/azure/agent_framework/azure/_azure_openai_settings.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* PR comments

* fix bad merge

* disable tests for now

* actually disable tests for azure

* fix tests, align test files with merge changes

* update code for new project structure

* PR comments

* add streaming integration tests. Fix flakiness

---------

Co-authored-by: eavanvalkenburg <github@vanvalkenburg.eu>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
peterychang
2025-07-15 14:38:09 -04:00
committed by GitHub
Unverified
parent fa96f74ee9
commit f0dc661c3e
22 changed files with 2162 additions and 33 deletions
@@ -10,7 +10,6 @@ except importlib.metadata.PackageNotFoundError:
__version__ = "0.0.0" # Fallback for development mode
_IMPORTS = {
"get_logger": "._logging",
"AFBaseModel": "._pydantic",
"AFBaseSettings": "._pydantic",
"Agent": "._agents",
@@ -18,39 +17,41 @@ _IMPORTS = {
"AgentRunResponseUpdate": "._types",
"AgentThread": "._agents",
"AITool": "._tools",
"ai_function": "._tools",
"AIFunction": "._tools",
"AIContent": "._types",
"AIContents": "._types",
"ChatClient": "._clients",
"ChatClientAgent": "._agents",
"ChatClientAgentThread": "._agents",
"ChatClientAgentThreadType": "._agents",
"ChatClientBase": "._clients",
"ChatFinishReason": "._types",
"ChatMessage": "._types",
"ChatOptions": "._types",
"ChatResponse": "._types",
"ChatResponseUpdate": "._types",
"ChatRole": "._types",
"ChatToolMode": "._types",
"DataContent": "._types",
"EmbeddingGenerator": "._clients",
"ErrorContent": "._types",
"FunctionCallContent": "._types",
"FunctionResultContent": "._types",
"GeneratedEmbeddings": "._types",
"HttpsUrl": "._pydantic",
"InputGuardrail": ".guard_rails",
"OutputGuardrail": ".guard_rails",
"SpeechToTextOptions": "._types",
"StructuredResponse": "._types",
"TextContent": "._types",
"TextReasoningContent": "._types",
"DataContent": "._types",
"TextToSpeechOptions": "._types",
"UriContent": "._types",
"UsageContent": "._types",
"UsageDetails": "._types",
"FunctionCallContent": "._types",
"FunctionResultContent": "._types",
"ChatFinishReason": "._types",
"ChatMessage": "._types",
"ChatResponse": "._types",
"StructuredResponse": "._types",
"ChatResponseUpdate": "._types",
"ChatRole": "._types",
"ErrorContent": "._types",
"GeneratedEmbeddings": "._types",
"ChatOptions": "._types",
"ChatToolMode": "._types",
"ChatClient": "._clients",
"ChatClientBase": "._clients",
"ai_function": "._tools",
"get_logger": "._logging",
"use_tool_calling": "._clients",
"EmbeddingGenerator": "._clients",
"InputGuardrail": ".guard_rails",
"OutputGuardrail": ".guard_rails",
"TextToSpeechOptions": "._types",
"SpeechToTextOptions": "._types",
}
@@ -4,7 +4,7 @@ from . import __version__ # type: ignore[attr-defined]
from ._agents import Agent, AgentThread, ChatClientAgent, ChatClientAgentThread, ChatClientAgentThreadType
from ._clients import ChatClient, ChatClientBase, EmbeddingGenerator, use_tool_calling
from ._logging import get_logger
from ._pydantic import AFBaseModel, AFBaseSettings
from ._pydantic import AFBaseModel, AFBaseSettings, HttpsUrl
from ._tools import AIFunction, AITool, ai_function
from ._types import (
AgentRunResponse,
@@ -63,6 +63,7 @@ __all__ = [
"FunctionCallContent",
"FunctionResultContent",
"GeneratedEmbeddings",
"HttpsUrl",
"InputGuardrail",
"OutputGuardrail",
"SpeechToTextOptions",
@@ -112,7 +112,14 @@ def _tool_call_non_streaming(func: TInnerGetResponse) -> TInnerGetResponse:
for attempt_idx in range(getattr(self, "__maximum_iterations_per_request", 10)):
response = await func(self, messages=messages, chat_options=chat_options)
# if there are function calls, we will handle them first
function_calls = [it for it in response.messages[0].contents if isinstance(it, FunctionCallContent)]
function_results = {
it.call_id for it in response.messages[0].contents if isinstance(it, FunctionResultContent)
}
function_calls = [
it
for it in response.messages[0].contents
if isinstance(it, FunctionCallContent) and it.call_id not in function_results
]
if function_calls:
# Run all function calls concurrently
results = await asyncio.gather(*[
@@ -1,11 +1,14 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Any, ClassVar, TypeVar
from typing import Annotated, Any, ClassVar, TypeVar
from pydantic import BaseModel, ConfigDict, Field
from pydantic import BaseModel, ConfigDict, Field, UrlConstraints
from pydantic.networks import AnyUrl
from pydantic_settings import BaseSettings, SettingsConfigDict
HttpsUrl = Annotated[AnyUrl, UrlConstraints(max_length=2083, allowed_schemes=["https"])]
class AFBaseModel(BaseModel):
"""Base class for all pydantic models in the Agent Framework."""
+19 -2
View File
@@ -16,7 +16,16 @@ from collections.abc import (
)
from typing import Annotated, Any, ClassVar, Generic, Literal, TypeVar, overload
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, ValidationError, field_validator, model_validator
from pydantic import (
BaseModel,
ConfigDict,
Field,
PrivateAttr,
ValidationError,
field_validator,
model_serializer,
model_validator,
)
from ._pydantic import AFBaseModel
from ._tools import AITool, ai_function
@@ -1398,6 +1407,11 @@ class ChatToolMode(AFBaseModel):
return self.mode == other.mode and self.required_function_name == other.required_function_name
return False
@model_serializer
def serialize_model(self) -> str:
"""Serializes the ChatToolMode to just the mode string."""
return self.mode
ChatToolMode.AUTO = ChatToolMode(mode="auto") # type: ignore[assignment]
ChatToolMode.REQUIRED_ANY = ChatToolMode(mode="required") # type: ignore[assignment]
@@ -1496,10 +1510,13 @@ class ChatOptions(AFBaseModel):
Dictionary of settings for provider.
"""
default_exclude = {"additional_properties"}
# No tool choice if no tools are defined
if self.tools is None or len(self.tools) == 0:
default_exclude.add("tool_choice")
merged_exclude = default_exclude if exclude is None else default_exclude | set(exclude)
settings = self.model_dump(exclude_none=True, by_alias=by_alias, exclude=merged_exclude)
settings = {k: v for k, v in settings.items() if v and not isinstance(v, dict)}
settings = {k: v for k, v in settings.items() if v}
settings.update(self.additional_properties)
for key in merged_exclude:
settings.pop(key, None)
@@ -7,6 +7,8 @@ PACKAGE_EXTRA = "azure"
_IMPORTS = {
"__version__": "agent_framework_azure",
"AzureChatClient": "agent_framework_azure",
"get_entra_auth_token": "agent_framework_azure",
}
@@ -46,6 +46,12 @@ class ServiceContentFilterException(ServiceResponseException):
pass
class ServiceInvalidAuthError(ServiceException):
"""An error occurred while authenticating the service."""
pass
class ServiceInvalidExecutionSettingsError(ServiceResponseException):
"""An error occurred while validating the execution settings of the service."""
@@ -2,9 +2,11 @@
from ._chat_client import OpenAIChatClient
from ._shared import OpenAISettings
from ._shared import OpenAIHandler, OpenAIModelTypes, OpenAISettings
__all__ = [
"OpenAIChatClient",
"OpenAIHandler",
"OpenAIModelTypes",
"OpenAISettings",
]
@@ -239,11 +239,11 @@ class OpenAIChatClientBase(OpenAIHandler, ChatClientBase):
function_call = self._openai_content_parser(content)
if "tool_calls" not in args:
args["tool_calls"] = []
args["tool_calls"].append(function_call)
args["tool_calls"].append(function_call) # type: ignore
case _:
if "content" not in args:
args["content"] = []
args["content"].append(self._openai_content_parser(content))
args["content"].append(self._openai_content_parser(content)) # type: ignore
if "content" in args or "tool_calls" in args:
all_messages.append(args)
return all_messages