From 947f2bf642db46f42f4646f4168976b38ae7df05 Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Wed, 10 Sep 2025 10:57:37 +0200 Subject: [PATCH] Python: added user agents to foundry and azure openai (#658) * added user agents to foundry and azure openai * improvement * improvement for disabled setup --- .../azure/agent_framework_azure/_shared.py | 6 +++-- .../agent_framework_foundry/_chat_client.py | 26 ++++++++++++++----- .../foundry/tests/test_foundry_chat_client.py | 18 +++++++++---- .../main/agent_framework/telemetry.py | 11 +++++++- .../samples/getting_started/test_telemetry.py | 0 5 files changed, 47 insertions(+), 14 deletions(-) delete mode 100644 python/tests/samples/getting_started/test_telemetry.py diff --git a/python/packages/azure/agent_framework_azure/_shared.py b/python/packages/azure/agent_framework_azure/_shared.py index b4beba5da0..6f3a6a72fd 100644 --- a/python/packages/azure/agent_framework_azure/_shared.py +++ b/python/packages/azure/agent_framework_azure/_shared.py @@ -9,7 +9,7 @@ from typing import Any, ClassVar, Final from agent_framework._pydantic import AFBaseSettings, HTTPsUrl from agent_framework.exceptions import ServiceInitializationError from agent_framework.openai._shared import OpenAIBase -from agent_framework.telemetry import USER_AGENT_KEY +from agent_framework.telemetry import APP_INFO, USER_AGENT_KEY, prepend_agent_framework_to_user_agent from azure.core.credentials import TokenCredential from openai.lib.azure import AsyncAzureOpenAI from pydantic import ConfigDict, SecretStr, model_validator, validate_call @@ -212,7 +212,9 @@ class AzureOpenAIConfigMixin(OpenAIBase): """ # Merge APP_INFO into the headers if it exists merged_headers = dict(copy(default_headers)) if default_headers else {} - + if APP_INFO: + merged_headers.update(APP_INFO) + merged_headers = prepend_agent_framework_to_user_agent(merged_headers) if not client: # If the client is None, the api_key is none, the ad_token is none, and the ad_token_provider is none, # then we will attempt to get the ad_token using the default endpoint specified in the Azure OpenAI diff --git a/python/packages/foundry/agent_framework_foundry/_chat_client.py b/python/packages/foundry/agent_framework_foundry/_chat_client.py index 130eeebc47..811daed8b8 100644 --- a/python/packages/foundry/agent_framework_foundry/_chat_client.py +++ b/python/packages/foundry/agent_framework_foundry/_chat_client.py @@ -28,7 +28,7 @@ from agent_framework import ( ) from agent_framework._pydantic import AFBaseSettings from agent_framework.exceptions import ServiceInitializationError, ServiceResponseException -from agent_framework.telemetry import use_telemetry +from agent_framework.telemetry import prepend_agent_framework_to_user_agent, use_telemetry from azure.ai.agents.models import ( AgentsNamedToolChoice, AgentsNamedToolChoiceType, @@ -95,6 +95,8 @@ class FoundrySettings(AFBaseSettings): TFoundryChatClient = TypeVar("TFoundryChatClient", bound="FoundryChatClient") +HEADERS = prepend_agent_framework_to_user_agent() + @use_telemetry @use_tool_calling @@ -267,7 +269,11 @@ class FoundryChatClient(BaseChatClient): raise ServiceInitializationError("Model deployment name is required for agent creation.") agent_name = self.agent_name - args = {"model": self.ai_model_deployment_name, "name": agent_name} + args = { + "model": self.ai_model_deployment_name, + "name": agent_name, + "headers": HEADERS, + } if run_options: if "tools" in run_options: args["tools"] = run_options["tools"] @@ -303,7 +309,11 @@ class FoundryChatClient(BaseChatClient): if thread_run is not None and tool_run_id is not None and tool_run_id == thread_run.id and tool_outputs: # There's an active run and we have tool results to submit, so submit the results. await self.client.agents.runs.submit_tool_outputs_stream( # type: ignore[reportUnknownMemberType] - thread_run.thread_id, tool_run_id, tool_outputs=tool_outputs, event_handler=handler + thread_run.thread_id, + tool_run_id, + tool_outputs=tool_outputs, + event_handler=handler, + headers=HEADERS, ) # Pass the handler to the stream to continue processing stream = handler # type: ignore @@ -316,6 +326,7 @@ class FoundryChatClient(BaseChatClient): stream = await self.client.agents.runs.stream( # type: ignore[reportUnknownMemberType] final_thread_id, agent_id=agent_id, + headers=HEADERS, **run_options, ) @@ -326,7 +337,9 @@ class FoundryChatClient(BaseChatClient): if thread_id is None: return None - async for run in self.client.agents.runs.list(thread_id=thread_id, limit=1, order=ListSortOrder.DESCENDING): + async for run in self.client.agents.runs.list( + thread_id=thread_id, limit=1, order=ListSortOrder.DESCENDING, headers=HEADERS + ): # type: ignore[reportUnknownMemberType] if run.status not in [ RunStatus.COMPLETED, RunStatus.CANCELLED, @@ -346,13 +359,14 @@ class FoundryChatClient(BaseChatClient): messages=run_options["additional_messages"], tool_resources=run_options.get("tool_resources"), metadata=run_options.get("metadata"), + headers=HEADERS, ) run_options["additional_messages"] = [] return thread.id if thread_run is not None: # There was an active run; we need to cancel it before starting a new run. - await self.client.agents.runs.cancel(thread_id, thread_run.id) + await self.client.agents.runs.cancel(thread_id, thread_run.id, headers=HEADERS) return thread_id @@ -474,7 +488,7 @@ class FoundryChatClient(BaseChatClient): async def _cleanup_agent_if_needed(self) -> None: """Clean up the agent if we created it.""" if self._should_delete_agent and self.agent_id is not None: - await self.client.agents.delete_agent(self.agent_id) + await self.client.agents.delete_agent(self.agent_id, headers=HEADERS) self.agent_id = None self._should_delete_agent = False diff --git a/python/packages/foundry/tests/test_foundry_chat_client.py b/python/packages/foundry/tests/test_foundry_chat_client.py index 5696af9d81..967b178ede 100644 --- a/python/packages/foundry/tests/test_foundry_chat_client.py +++ b/python/packages/foundry/tests/test_foundry_chat_client.py @@ -24,6 +24,7 @@ from agent_framework import ( UriContent, ai_function, ) +from agent_framework import __version__ as AF_VERSION from agent_framework.exceptions import ServiceInitializationError from azure.ai.agents.models import ( RequiredFunctionToolCall, @@ -316,9 +317,10 @@ async def test_foundry_chat_client_cleanup_agent_if_needed_should_delete( ) await chat_client._cleanup_agent_if_needed() # type: ignore - # Verify agent deletion was called - mock_ai_project_client.agents.delete_agent.assert_called_once_with("agent-to-delete") + mock_ai_project_client.agents.delete_agent.assert_called_once_with( + "agent-to-delete", headers={"User-Agent": f"agent-framework-python/{AF_VERSION}"} + ) assert not chat_client._should_delete_agent # type: ignore @@ -359,7 +361,9 @@ async def test_foundry_chat_client_aclose(mock_ai_project_client: MagicMock) -> await chat_client.close() # Verify agent deletion was called - mock_ai_project_client.agents.delete_agent.assert_called_once_with("agent-to-delete") + mock_ai_project_client.agents.delete_agent.assert_called_once_with( + "agent-to-delete", headers={"User-Agent": f"agent-framework-python/{AF_VERSION}"} + ) async def test_foundry_chat_client_async_context_manager(mock_ai_project_client: MagicMock) -> None: @@ -373,7 +377,9 @@ async def test_foundry_chat_client_async_context_manager(mock_ai_project_client: pass # Just test that we can enter and exit # Verify cleanup was called on exit - mock_ai_project_client.agents.delete_agent.assert_called_once_with("agent-to-delete") + mock_ai_project_client.agents.delete_agent.assert_called_once_with( + "agent-to-delete", headers={"User-Agent": f"agent-framework-python/{AF_VERSION}"} + ) def test_foundry_chat_client_create_run_options_basic(mock_ai_project_client: MagicMock) -> None: @@ -600,7 +606,9 @@ async def test_foundry_chat_client_prepare_thread_cancels_active_run(mock_ai_pro result = await chat_client._prepare_thread("test-thread", mock_thread_run, run_options) # type: ignore assert result == "test-thread" - mock_ai_project_client.agents.runs.cancel.assert_called_once_with("test-thread", "run_123") + mock_ai_project_client.agents.runs.cancel.assert_called_once_with( + "test-thread", "run_123", headers={"User-Agent": f"agent-framework-python/{AF_VERSION}"} + ) def test_foundry_chat_client_create_function_call_contents_basic(mock_ai_project_client: MagicMock) -> None: diff --git a/python/packages/main/agent_framework/telemetry.py b/python/packages/main/agent_framework/telemetry.py index dd909837a2..b52708fac3 100644 --- a/python/packages/main/agent_framework/telemetry.py +++ b/python/packages/main/agent_framework/telemetry.py @@ -165,15 +165,24 @@ HTTP_USER_AGENT: Final[str] = "agent-framework-python" AGENT_FRAMEWORK_USER_AGENT = f"{HTTP_USER_AGENT}/{version_info}" # type: ignore[has-type] -def prepend_agent_framework_to_user_agent(headers: dict[str, Any]) -> dict[str, Any]: +def prepend_agent_framework_to_user_agent(headers: dict[str, Any] | None = None) -> dict[str, Any]: """Prepend "agent-framework" to the User-Agent in the headers. + When user agent telemetry is disabled, through the AZURE_TELEMETRY_DISABLED environment variable, + the User-Agent header will not include the agent-framework information, it will be sent back as is, + or as a empty dict when None is passed. + Args: headers: The existing headers dictionary. Returns: + A new dict with "User-Agent" set to "agent-framework-python/{version}" if headers is None. The modified headers dictionary with "agent-framework-python/{version}" prepended to the User-Agent. """ + if not IS_TELEMETRY_ENABLED: + return headers or {} + if not headers: + return {USER_AGENT_KEY: AGENT_FRAMEWORK_USER_AGENT} headers[USER_AGENT_KEY] = ( f"{AGENT_FRAMEWORK_USER_AGENT} {headers[USER_AGENT_KEY]}" if USER_AGENT_KEY in headers diff --git a/python/tests/samples/getting_started/test_telemetry.py b/python/tests/samples/getting_started/test_telemetry.py deleted file mode 100644 index e69de29bb2..0000000000