Python: added user agents to foundry and azure openai (#658)

* added user agents to foundry and azure openai

* improvement

* improvement for disabled setup
This commit is contained in:
Eduard van Valkenburg
2025-09-10 10:57:37 +02:00
committed by GitHub
Unverified
parent 3b81164a6d
commit 947f2bf642
5 changed files with 47 additions and 14 deletions
@@ -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
@@ -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
@@ -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:
@@ -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