-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Update the root Otel span to be an invoke_agent span #45033
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -60,21 +60,18 @@ def set_request_id_to_context_var(self, request): | |||||||||||||||||||||||||
| request_context.set(ctx) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| def set_run_context_to_context_var(self, run_context): | ||||||||||||||||||||||||||
| agent_id, agent_name = "", "" | ||||||||||||||||||||||||||
| agent_obj = run_context.get_agent_id_object() | ||||||||||||||||||||||||||
| if agent_obj: | ||||||||||||||||||||||||||
| agent_name = getattr(agent_obj, "name", "") | ||||||||||||||||||||||||||
| agent_version = getattr(agent_obj, "version", "") | ||||||||||||||||||||||||||
| agent_id = f"{agent_name}:{agent_version}" | ||||||||||||||||||||||||||
| agent_name, agent_id = get_agent_name(run_context) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| res = { | ||||||||||||||||||||||||||
| "azure.ai.agentserver.response_id": run_context.response_id or "", | ||||||||||||||||||||||||||
| "azure.ai.agentserver.conversation_id": run_context.conversation_id or "", | ||||||||||||||||||||||||||
| "azure.ai.agentserver.streaming": str(run_context.stream or False), | ||||||||||||||||||||||||||
| "gen_ai.operation.name": "invoke_agent", | ||||||||||||||||||||||||||
| "gen_ai.agent.id": agent_id, | ||||||||||||||||||||||||||
| "gen_ai.agent.name": agent_name, | ||||||||||||||||||||||||||
| "gen_ai.agent.name": agent_name or "", | ||||||||||||||||||||||||||
| "gen_ai.provider.name": "AzureAI Hosted Agents", | ||||||||||||||||||||||||||
| "gen_ai.response.id": run_context.response_id or "", | ||||||||||||||||||||||||||
| "gen_ai.conversation.id": run_context.conversation_id or "", | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| ctx = request_context.get() or {} | ||||||||||||||||||||||||||
| ctx.update(res) | ||||||||||||||||||||||||||
|
|
@@ -87,10 +84,12 @@ async def runs_endpoint(request): | |||||||||||||||||||||||||
| # Set up tracing context and span | ||||||||||||||||||||||||||
| context = request.state.agent_run_context | ||||||||||||||||||||||||||
| ctx = request_context.get() | ||||||||||||||||||||||||||
| agent_name, _ = get_agent_name(context) | ||||||||||||||||||||||||||
| span_name = f"invoke_agent {agent_name}" if agent_name else "invoke_agent" | ||||||||||||||||||||||||||
| with self.tracer.start_as_current_span( | ||||||||||||||||||||||||||
| name=f"HostedAgents-{context.response_id}", | ||||||||||||||||||||||||||
| name=span_name, | ||||||||||||||||||||||||||
| attributes=ctx, | ||||||||||||||||||||||||||
| kind=trace.SpanKind.SERVER, | ||||||||||||||||||||||||||
| kind=trace.SpanKind.CLIENT, | ||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||
| logger.info("Start processing CreateResponse request:") | ||||||||||||||||||||||||||
|
|
@@ -304,6 +303,30 @@ def setup_otlp_exporter(self, endpoint, provider): | |||||||||||||||||||||||||
| logger.info(f"Tracing setup with OTLP exporter: {endpoint}") | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| def get_agent_name(agent_run_context: AgentRunContext) -> tuple: | ||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||
| Extract the agent name and agent id from an AgentRunContext. | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| :param agent_run_context: The AgentRunContext instance to extract from. | ||||||||||||||||||||||||||
| :type agent_run_context: AgentRunContext | ||||||||||||||||||||||||||
| :return: Tuple of (agent_name, agent_id). Agent id is formatted as "name:version". | ||||||||||||||||||||||||||
|
Comment on lines
+308
to
+312
|
||||||||||||||||||||||||||
| Extract the agent name and agent id from an AgentRunContext. | |
| :param agent_run_context: The AgentRunContext instance to extract from. | |
| :type agent_run_context: AgentRunContext | |
| :return: Tuple of (agent_name, agent_id). Agent id is formatted as "name:version". | |
| Extract the agent name and agent ID from an AgentRunContext. | |
| :param agent_run_context: The AgentRunContext instance to extract from. | |
| :type agent_run_context: AgentRunContext | |
| :return: Tuple of (agent_name, agent_id). Agent ID is formatted as "name:version". |
Copilot
AI
Feb 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The docstring states "Agent id is formatted as 'name:version'" but should be "Agent ID is formatted as 'name:version'" for consistency with standard capitalization of "ID" in technical documentation.
| :return: Tuple of (agent_name, agent_id). Agent id is formatted as "name:version". | |
| :return: Tuple of (agent_name, agent_id). Agent ID is formatted as "name:version". |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,152 @@ | ||
| #!/usr/bin/env python3 | ||
| # --------------------------------------------------------- | ||
| # Copyright (c) Microsoft Corporation. All rights reserved. | ||
| # --------------------------------------------------------- | ||
| """Unit tests for get_agent_name function.""" | ||
|
|
||
| import sys | ||
| from pathlib import Path | ||
| from unittest.mock import MagicMock | ||
|
|
||
| import pytest | ||
|
|
||
| # Add the project root to the path | ||
| project_root = Path(__file__).parent.parent | ||
| sys.path.insert(0, str(project_root)) | ||
|
|
||
| from azure.ai.agentserver.core.server.base import get_agent_name | ||
| from azure.ai.agentserver.core.server.common.agent_run_context import AgentRunContext | ||
|
|
||
|
|
||
| class TestGetAgentName: | ||
| """Test suite for get_agent_name function.""" | ||
|
|
||
| def test_get_agent_name_with_valid_agent_object(self): | ||
| """Test get_agent_name with valid agent object.""" | ||
| # Create a mock agent object | ||
| mock_agent = MagicMock() | ||
| mock_agent.name = "TestAgent" | ||
| mock_agent.version = "1.0.0" | ||
|
|
||
| # Create a mock AgentRunContext | ||
| mock_context = MagicMock(spec=AgentRunContext) | ||
| mock_context.get_agent_id_object.return_value = mock_agent | ||
|
|
||
| # Call get_agent_name | ||
| agent_name, agent_id = get_agent_name(mock_context) | ||
|
|
||
| # Assert results | ||
| assert agent_name == "TestAgent" | ||
| assert agent_id == "TestAgent:1.0.0" | ||
|
|
||
| def test_get_agent_name_with_no_version(self): | ||
| """Test get_agent_name when agent has no version.""" | ||
| # Create a mock agent object with no version | ||
| mock_agent = MagicMock() | ||
| mock_agent.name = "TestAgent" | ||
| mock_agent.version = "" | ||
|
|
||
| # Create a mock AgentRunContext | ||
| mock_context = MagicMock(spec=AgentRunContext) | ||
| mock_context.get_agent_id_object.return_value = mock_agent | ||
|
|
||
| # Call get_agent_name | ||
| agent_name, agent_id = get_agent_name(mock_context) | ||
|
|
||
| # Assert results | ||
| assert agent_name == "TestAgent" | ||
| assert agent_id == "TestAgent:" | ||
|
|
||
| def test_get_agent_name_with_none_agent_object(self): | ||
| """Test get_agent_name when agent object is None.""" | ||
| # Create a mock AgentRunContext that returns None | ||
| mock_context = MagicMock(spec=AgentRunContext) | ||
| mock_context.get_agent_id_object.return_value = None | ||
|
|
||
| # Call get_agent_name | ||
| agent_name, agent_id = get_agent_name(mock_context) | ||
|
|
||
| # Assert results | ||
| assert agent_name is None | ||
| assert agent_id == "" | ||
|
|
||
| def test_get_agent_name_with_none_context(self): | ||
| """Test get_agent_name with None context.""" | ||
| # Call get_agent_name with None | ||
| agent_name, agent_id = get_agent_name(None) | ||
|
|
||
| # Assert results | ||
| assert agent_name is None | ||
| assert agent_id == "" | ||
|
|
||
| def test_get_agent_name_with_missing_name_attribute(self): | ||
| """Test get_agent_name when agent object has no name attribute.""" | ||
| # Create a mock agent object without name | ||
| mock_agent = MagicMock() | ||
| mock_agent.name = None | ||
| mock_agent.version = "1.0.0" | ||
|
|
||
| # Create a mock AgentRunContext | ||
| mock_context = MagicMock(spec=AgentRunContext) | ||
| mock_context.get_agent_id_object.return_value = mock_agent | ||
|
|
||
| # Call get_agent_name | ||
| agent_name, agent_id = get_agent_name(mock_context) | ||
|
|
||
| # Assert results - agent_id should not be set if name is None | ||
| assert agent_name is None | ||
| assert agent_id == "" | ||
|
|
||
| def test_get_agent_name_with_empty_name(self): | ||
| """Test get_agent_name when agent name is empty string.""" | ||
| # Create a mock agent object with empty name | ||
| mock_agent = MagicMock() | ||
| mock_agent.name = "" | ||
| mock_agent.version = "1.0.0" | ||
|
|
||
| # Create a mock AgentRunContext | ||
| mock_context = MagicMock(spec=AgentRunContext) | ||
| mock_context.get_agent_id_object.return_value = mock_agent | ||
|
|
||
| # Call get_agent_name | ||
| agent_name, agent_id = get_agent_name(mock_context) | ||
|
|
||
| # Assert results - agent_id should not be set if name is empty | ||
| assert agent_name == "" | ||
| assert agent_id == "" | ||
|
|
||
| def test_get_agent_name_return_type(self): | ||
| """Test that get_agent_name returns a tuple.""" | ||
| # Create a mock agent object | ||
| mock_agent = MagicMock() | ||
| mock_agent.name = "TestAgent" | ||
| mock_agent.version = "2.0.0" | ||
|
|
||
| # Create a mock AgentRunContext | ||
| mock_context = MagicMock(spec=AgentRunContext) | ||
| mock_context.get_agent_id_object.return_value = mock_agent | ||
|
|
||
| # Call get_agent_name | ||
| result = get_agent_name(mock_context) | ||
|
|
||
| # Assert result is a tuple of length 2 | ||
| assert isinstance(result, tuple) | ||
| assert len(result) == 2 | ||
|
|
||
| def test_get_agent_name_with_special_characters(self): | ||
| """Test get_agent_name with special characters in agent name and version.""" | ||
| # Create a mock agent object with special characters | ||
| mock_agent = MagicMock() | ||
| mock_agent.name = "Test-Agent_v2" | ||
| mock_agent.version = "2.0.0-beta.1" | ||
|
|
||
| # Create a mock AgentRunContext | ||
| mock_context = MagicMock(spec=AgentRunContext) | ||
| mock_context.get_agent_id_object.return_value = mock_agent | ||
|
|
||
| # Call get_agent_name | ||
| agent_name, agent_id = get_agent_name(mock_context) | ||
|
|
||
| # Assert results | ||
| assert agent_name == "Test-Agent_v2" | ||
| assert agent_id == "Test-Agent_v2:2.0.0-beta.1" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The span kind has been changed from SERVER to CLIENT, but this endpoint is receiving HTTP requests, which indicates it should use SpanKind.SERVER according to OpenTelemetry semantic conventions. SpanKind.CLIENT is typically used for outbound calls the service makes, not for handling incoming requests. This is a server endpoint (runs_endpoint) handling incoming POST requests to /runs or /responses. Unless this represents the client-side invocation of an agent (not the HTTP server receiving the request), this should remain SpanKind.SERVER.