From 3eb26632cea20c1b5d0d03c0351263daa5349329 Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Tue, 30 Sep 2025 21:33:12 +0200 Subject: [PATCH] Python: fix string parsing in azure openai client (#1023) * fix string parsing in azure openai client * test fix * changed setup func name and check if there is a conn string * redid parsing in safe way --- .../agent_framework_azure_ai/_chat_client.py | 14 +++++++++++--- .../agent_framework/azure/_assistants_client.py | 10 +++++----- .../core/agent_framework/azure/_chat_client.py | 10 +++++----- .../agent_framework/azure/_responses_client.py | 16 ++++++++-------- .../core/tests/azure/test_azure_chat_client.py | 1 - .../agents/azure_ai/azure_ai_with_hosted_mcp.py | 2 +- .../azure_ai/azure_ai_with_multiple_tools.py | 2 +- .../azure_ai_agent_observability.py | 2 +- .../azure_ai_chat_client_with_observability.py | 2 +- 9 files changed, 33 insertions(+), 26 deletions(-) diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py b/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py index 0f5ba1ba94..b64ea62d3e 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py @@ -83,7 +83,7 @@ from azure.ai.agents.models import ( from azure.ai.projects.aio import AIProjectClient from azure.ai.projects.models import ConnectionType from azure.core.credentials_async import AsyncTokenCredential -from azure.core.exceptions import HttpResponseError +from azure.core.exceptions import HttpResponseError, ResourceNotFoundError from pydantic import BaseModel, Field, PrivateAttr, ValidationError if sys.version_info >= (3, 11): @@ -216,17 +216,25 @@ class AzureAIAgentClient(BaseChatClient): ) self._should_close_client = should_close_client - async def setup_observability(self) -> None: + async def setup_azure_ai_observability(self, enable_sensitive_data: bool | None = None) -> None: """Use this method to setup tracing in your Azure AI Project. This will take the connection string from the project project_client. It will override any connection string that is set in the environment variables. It will disable any OTLP endpoint that might have been set. """ + try: + conn_string = await self.project_client.telemetry.get_application_insights_connection_string() + except ResourceNotFoundError: + logger.warning( + "No Application Insights connection string found for the Azure AI Project, " + "please call setup_observability() manually." + ) + return from agent_framework.observability import setup_observability setup_observability( - applicationinsights_connection_string=await self.project_client.telemetry.get_application_insights_connection_string(), # noqa: E501 + applicationinsights_connection_string=conn_string, enable_sensitive_data=enable_sensitive_data ) async def __aenter__(self) -> "Self": diff --git a/python/packages/core/agent_framework/azure/_assistants_client.py b/python/packages/core/agent_framework/azure/_assistants_client.py index e22d188a49..3668ae58b6 100644 --- a/python/packages/core/agent_framework/azure/_assistants_client.py +++ b/python/packages/core/agent_framework/azure/_assistants_client.py @@ -4,8 +4,7 @@ from collections.abc import Mapping from typing import TYPE_CHECKING, Any, ClassVar from openai.lib.azure import AsyncAzureADTokenProvider, AsyncAzureOpenAI -from pydantic import SecretStr, ValidationError -from pydantic.networks import AnyUrl +from pydantic import ValidationError from ..exceptions import ServiceInitializationError from ..openai import OpenAIAssistantsClient @@ -72,9 +71,10 @@ class AzureOpenAIAssistantsClient(OpenAIAssistantsClient): """ try: 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, + # pydantic settings will see if there is a value, if not, will try the env var or .env file + api_key=api_key, # type: ignore + base_url=base_url, # type: ignore + endpoint=endpoint, # type: ignore chat_deployment_name=deployment_name, api_version=api_version, env_file_path=env_file_path, diff --git a/python/packages/core/agent_framework/azure/_chat_client.py b/python/packages/core/agent_framework/azure/_chat_client.py index 53500e9726..5bdfa707ec 100644 --- a/python/packages/core/agent_framework/azure/_chat_client.py +++ b/python/packages/core/agent_framework/azure/_chat_client.py @@ -10,8 +10,7 @@ from azure.core.credentials import TokenCredential from openai.lib.azure import AsyncAzureADTokenProvider, AsyncAzureOpenAI from openai.types.chat.chat_completion import Choice from openai.types.chat.chat_completion_chunk import Choice as ChunkChoice -from pydantic import SecretStr, ValidationError -from pydantic.networks import AnyUrl +from pydantic import ValidationError from agent_framework import ( ChatResponse, @@ -92,9 +91,10 @@ class AzureOpenAIChatClient(AzureOpenAIConfigMixin, OpenAIBaseChatClient): 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, + # pydantic settings will see if there is a value, if not, will try the env var or .env file + api_key=api_key, # type: ignore + base_url=base_url, # type: ignore + endpoint=endpoint, # type: ignore chat_deployment_name=deployment_name, api_version=api_version, env_file_path=env_file_path, diff --git a/python/packages/core/agent_framework/azure/_responses_client.py b/python/packages/core/agent_framework/azure/_responses_client.py index 19dc4b3fbb..d5ef19f927 100644 --- a/python/packages/core/agent_framework/azure/_responses_client.py +++ b/python/packages/core/agent_framework/azure/_responses_client.py @@ -6,8 +6,7 @@ from urllib.parse import urljoin from azure.core.credentials import TokenCredential from openai.lib.azure import AsyncAzureADTokenProvider, AsyncAzureOpenAI -from pydantic import SecretStr, ValidationError -from pydantic.networks import AnyUrl +from pydantic import ValidationError from agent_framework import use_chat_middleware, use_function_invocation from agent_framework.exceptions import ServiceInitializationError @@ -71,11 +70,11 @@ class AzureOpenAIResponsesClient(AzureOpenAIConfigMixin, OpenAIBaseResponsesClie 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, + # pydantic settings will see if there is a value, if not, will try the env var or .env file + api_key=api_key, # type: ignore + base_url=base_url, # type: ignore + endpoint=endpoint, # type: ignore responses_deployment_name=deployment_name, api_version=api_version, env_file_path=env_file_path, @@ -89,9 +88,10 @@ class AzureOpenAIResponsesClient(AzureOpenAIConfigMixin, OpenAIBaseResponsesClie if ( not azure_openai_settings.base_url and azure_openai_settings.endpoint - and str(azure_openai_settings.endpoint).rstrip("/").endswith("openai.azure.com") + and azure_openai_settings.endpoint.host + and azure_openai_settings.endpoint.host.endswith(".openai.azure.com") ): - azure_openai_settings.base_url = AnyUrl(urljoin(str(azure_openai_settings.endpoint), "/openai/v1/")) + azure_openai_settings.base_url = urljoin(str(azure_openai_settings.endpoint), "/openai/v1/") # type: ignore except ValidationError as exc: raise ServiceInitializationError(f"Failed to validate settings: {exc}") from exc diff --git a/python/packages/core/tests/azure/test_azure_chat_client.py b/python/packages/core/tests/azure/test_azure_chat_client.py index 5b4af83f29..e7c7cb5046 100644 --- a/python/packages/core/tests/azure/test_azure_chat_client.py +++ b/python/packages/core/tests/azure/test_azure_chat_client.py @@ -131,7 +131,6 @@ def test_serialize(azure_openai_unit_test_env: dict[str, str]) -> None: azure_chat_client = AzureOpenAIChatClient.from_dict(settings) dumped_settings = azure_chat_client.to_dict() assert dumped_settings["ai_model_id"] == settings["deployment_name"] - assert str(settings["endpoint"]) in str(dumped_settings["base_url"]) assert str(settings["deployment_name"]) in str(dumped_settings["base_url"]) assert settings["api_key"] == dumped_settings["api_key"] assert settings["api_version"] == dumped_settings["api_version"] diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_hosted_mcp.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_hosted_mcp.py index 3a96a7f2cc..2c3517bcaa 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_hosted_mcp.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_hosted_mcp.py @@ -38,7 +38,7 @@ async def main() -> None: AzureAIAgentClient(async_credential=credential) as chat_client, ): # enable azure-ai observability - await chat_client.setup_observability() + await chat_client.setup_azure_ai_observability() agent = chat_client.create_agent( name="DocsAgent", instructions="You are a helpful assistant that can help with microsoft documentation questions.", diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_multiple_tools.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_multiple_tools.py index 89eccdc0df..c7b5c88382 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_multiple_tools.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_multiple_tools.py @@ -50,7 +50,7 @@ async def main() -> None: AzureAIAgentClient(async_credential=credential) as chat_client, ): # enable azure-ai observability - await chat_client.setup_observability() + await chat_client.setup_azure_ai_observability() agent = chat_client.create_agent( name="DocsAgent", instructions="You are a helpful assistant that can help with microsoft documentation questions.", diff --git a/python/samples/getting_started/observability/azure_ai_agent_observability.py b/python/samples/getting_started/observability/azure_ai_agent_observability.py index 514042a0c8..d30b7569c7 100644 --- a/python/samples/getting_started/observability/azure_ai_agent_observability.py +++ b/python/samples/getting_started/observability/azure_ai_agent_observability.py @@ -47,7 +47,7 @@ async def main(): # This will enable tracing and configure the application to send telemetry data to the # Application Insights instance attached to the Azure AI project. # This will override any existing configuration. - await client.setup_observability() + await client.setup_azure_ai_observability() questions = ["What's the weather in Amsterdam?", "and in Paris, and which is better?", "Why is the sky blue?"] diff --git a/python/samples/getting_started/observability/azure_ai_chat_client_with_observability.py b/python/samples/getting_started/observability/azure_ai_chat_client_with_observability.py index 24203616a5..2dafa753da 100644 --- a/python/samples/getting_started/observability/azure_ai_chat_client_with_observability.py +++ b/python/samples/getting_started/observability/azure_ai_chat_client_with_observability.py @@ -68,7 +68,7 @@ async def main() -> None: # This will enable tracing and configure the application to send telemetry data to the # Application Insights instance attached to the Azure AI project. # This will override any existing configuration. - await client.setup_observability() + await client.setup_azure_ai_observability() with get_tracer().start_as_current_span( name="Foundry Telemetry from Agent Framework", kind=SpanKind.CLIENT