From d91105ded33d900f0ae1bf756e2796711edc0be3 Mon Sep 17 00:00:00 2001 From: Charanvardhan Date: Sat, 4 Apr 2026 02:32:20 -0400 Subject: [PATCH 1/2] Python: Fix FoundryAgent telemetry gaps for name and model (Fixes #5088) Resolves gen_ai.agent.name rendering as UUID and gen_ai.request.model rendering as 'unknown' by falling back to the client agent definition and lazily resolving the Azure AI Projects SDK request model on first fetch. --- .../foundry/agent_framework_foundry/_agent.py | 31 +++++++++++++++- .../tests/foundry/test_foundry_agent.py | 35 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/python/packages/foundry/agent_framework_foundry/_agent.py b/python/packages/foundry/agent_framework_foundry/_agent.py index a499abf6f4e..1e96e092316 100644 --- a/python/packages/foundry/agent_framework_foundry/_agent.py +++ b/python/packages/foundry/agent_framework_foundry/_agent.py @@ -214,6 +214,11 @@ def _get_agent_reference(self) -> dict[str, str]: ref["version"] = self.agent_version return ref + @property + def model(self) -> str: + """Get the lazily fetched model name, or 'unknown' if not yet fetched.""" + return getattr(self, "_fetched_model", "unknown") + @override def as_agent( self, @@ -269,6 +274,28 @@ async def _prepare_options( **kwargs: Any, ) -> dict[str, Any]: """Prepare options for the Responses API, injecting agent reference and validating tools.""" + # Lazily fetch the model name if not already cached + if not hasattr(self, "_fetched_model"): + try: + agent = await self.project_client.agents.get_agent(self.agent_name) + self._fetched_model = getattr(agent, "model", "unknown") + + # Try to update the current OpenTelemetry span directly + try: + from opentelemetry import trace + from agent_framework.observability import OtelAttr + + current_span = trace.get_current_span() + if current_span and current_span.is_recording(): + current_span.set_attribute(OtelAttr.REQUEST_MODEL, self._fetched_model) + span_name_parts = current_span.name.split(" ", 1) + if len(span_name_parts) > 0: + current_span.update_name(f"{span_name_parts[0]} {self._fetched_model}") + except ImportError: + pass + except Exception: + self._fetched_model = "unknown" + # Validate tools — only FunctionTool allowed tools = options.get("tools", []) if tools: @@ -543,11 +570,13 @@ def __init__( client = actual_client_type(**client_kwargs) + resolved_name = name if name is not None else getattr(client, "agent_name", None) + super().__init__( client=client, # type: ignore[arg-type] instructions=instructions, id=id, - name=name, + name=resolved_name, description=description, tools=tools, # type: ignore[arg-type] default_options=cast(FoundryAgentOptionsT | None, default_options), diff --git a/python/packages/foundry/tests/foundry/test_foundry_agent.py b/python/packages/foundry/tests/foundry/test_foundry_agent.py index 09a31f941b7..1429533588d 100644 --- a/python/packages/foundry/tests/foundry/test_foundry_agent.py +++ b/python/packages/foundry/tests/foundry/test_foundry_agent.py @@ -465,3 +465,38 @@ async def test_foundry_agent_custom_client_run() -> None: assert isinstance(response, AgentResponse) assert response.text is not None assert "response test" in response.text.lower() + + +async def test_foundry_agent_telemetry_defaults() -> None: + """Test that agent name acts as a fallback and _prepare_options lazily gets model.""" + mock_project = MagicMock() + mock_openai = MagicMock() + mock_project.get_openai_client.return_value = mock_openai + + # Mock agents getter + mock_agent_instance = MagicMock() + mock_agent_instance.model = "gpt-telemetry-test" + mock_project.agents.get_agent = AsyncMock(return_value=mock_agent_instance) + + agent = FoundryAgent( + project_client=mock_project, + agent_name="my-telemetry-agent", + name=None # Explicitly None to test fallback + ) + + assert agent.name == "my-telemetry-agent" + assert getattr(agent.client, "model", "unknown") == "unknown" + + # Call prepare_options to trigger lazy load + with patch( + "agent_framework_openai._chat_client.RawOpenAIChatClient._prepare_options", + new_callable=AsyncMock, + return_value={}, + ): + await agent.client._prepare_options( + messages=[Message(role="user", contents="hi")], + options={}, + ) + + assert agent.client.model == "gpt-telemetry-test" + mock_project.agents.get_agent.assert_called_once_with("my-telemetry-agent") From a94ca207368c61996149a831c37e2576d8820c2a Mon Sep 17 00:00:00 2001 From: Charanvardhan Date: Sat, 4 Apr 2026 11:51:15 -0400 Subject: [PATCH 2/2] Address Copilot PR review comments --- .../foundry/agent_framework_foundry/_agent.py | 13 +++++++++---- .../foundry/tests/foundry/test_foundry_agent.py | 8 ++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/python/packages/foundry/agent_framework_foundry/_agent.py b/python/packages/foundry/agent_framework_foundry/_agent.py index 1e96e092316..939c05ed841 100644 --- a/python/packages/foundry/agent_framework_foundry/_agent.py +++ b/python/packages/foundry/agent_framework_foundry/_agent.py @@ -219,6 +219,10 @@ def model(self) -> str: """Get the lazily fetched model name, or 'unknown' if not yet fetched.""" return getattr(self, "_fetched_model", "unknown") + @model.setter + def model(self, value: str) -> None: + pass + @override def as_agent( self, @@ -279,8 +283,11 @@ async def _prepare_options( try: agent = await self.project_client.agents.get_agent(self.agent_name) self._fetched_model = getattr(agent, "model", "unknown") + except Exception: + self._fetched_model = "unknown" - # Try to update the current OpenTelemetry span directly + # Try to update the current OpenTelemetry span directly + if hasattr(self, "_fetched_model") and self._fetched_model != "unknown": try: from opentelemetry import trace from agent_framework.observability import OtelAttr @@ -291,10 +298,8 @@ async def _prepare_options( span_name_parts = current_span.name.split(" ", 1) if len(span_name_parts) > 0: current_span.update_name(f"{span_name_parts[0]} {self._fetched_model}") - except ImportError: + except Exception: pass - except Exception: - self._fetched_model = "unknown" # Validate tools — only FunctionTool allowed tools = options.get("tools", []) diff --git a/python/packages/foundry/tests/foundry/test_foundry_agent.py b/python/packages/foundry/tests/foundry/test_foundry_agent.py index 1429533588d..ca0f19276fc 100644 --- a/python/packages/foundry/tests/foundry/test_foundry_agent.py +++ b/python/packages/foundry/tests/foundry/test_foundry_agent.py @@ -472,7 +472,7 @@ async def test_foundry_agent_telemetry_defaults() -> None: mock_project = MagicMock() mock_openai = MagicMock() mock_project.get_openai_client.return_value = mock_openai - + # Mock agents getter mock_agent_instance = MagicMock() mock_agent_instance.model = "gpt-telemetry-test" @@ -483,10 +483,10 @@ async def test_foundry_agent_telemetry_defaults() -> None: agent_name="my-telemetry-agent", name=None # Explicitly None to test fallback ) - + assert agent.name == "my-telemetry-agent" assert getattr(agent.client, "model", "unknown") == "unknown" - + # Call prepare_options to trigger lazy load with patch( "agent_framework_openai._chat_client.RawOpenAIChatClient._prepare_options", @@ -497,6 +497,6 @@ async def test_foundry_agent_telemetry_defaults() -> None: messages=[Message(role="user", contents="hi")], options={}, ) - + assert agent.client.model == "gpt-telemetry-test" mock_project.agents.get_agent.assert_called_once_with("my-telemetry-agent")