From f8c05acde623576cc043dd00a67c2cca440978bd Mon Sep 17 00:00:00 2001 From: M-Hietala <78813398+M-Hietala@users.noreply.github.com> Date: Fri, 17 Jul 2026 16:04:56 -0500 Subject: [PATCH] adding support for external agent creation --- sdk/ai/azure-ai-projects/assets.json | 2 +- .../telemetry/_ai_project_instrumentor.py | 23 +++++++ .../azure/ai/projects/telemetry/_utils.py | 1 + .../telemetry/test_ai_agents_instrumentor.py | 63 ++++++++++++++++++- 4 files changed, 87 insertions(+), 2 deletions(-) diff --git a/sdk/ai/azure-ai-projects/assets.json b/sdk/ai/azure-ai-projects/assets.json index 19f1f7e5f9d3..6330c2aaf2c4 100644 --- a/sdk/ai/azure-ai-projects/assets.json +++ b/sdk/ai/azure-ai-projects/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "python", "TagPrefix": "python/ai/azure-ai-projects", - "Tag": "python/ai/azure-ai-projects_875c4f833f" + "Tag": "python/ai/azure-ai-projects_7959346120" } diff --git a/sdk/ai/azure-ai-projects/azure/ai/projects/telemetry/_ai_project_instrumentor.py b/sdk/ai/azure-ai-projects/azure/ai/projects/telemetry/_ai_project_instrumentor.py index c23a994b795e..a9141c487df0 100644 --- a/sdk/ai/azure-ai-projects/azure/ai/projects/telemetry/_ai_project_instrumentor.py +++ b/sdk/ai/azure-ai-projects/azure/ai/projects/telemetry/_ai_project_instrumentor.py @@ -48,6 +48,7 @@ AGENT_TYPE_PROMPT, AGENT_TYPE_WORKFLOW, AGENT_TYPE_HOSTED, + AGENT_TYPE_EXTERNAL, AGENT_TYPE_UNKNOWN, GEN_AI_AGENT_TYPE, ERROR_MESSAGE, @@ -901,9 +902,20 @@ def _create_agent_span_from_parameters( hosted_cpu = definition.get("cpu") hosted_memory = definition.get("memory") hosted_image = definition.get("image") + # Also check container_configuration for image + if not hosted_image: + container_config = definition.get("container_configuration") + if container_config: + if hasattr(container_config, "image"): + hosted_image = getattr(container_config, "image", None) + elif isinstance(container_config, dict): + hosted_image = container_config.get("image") hosted_protocol = None hosted_protocol_version = None + # Check both old and new field names for protocol versions container_protocol_versions = definition.get("container_protocol_versions") + if not container_protocol_versions: + container_protocol_versions = definition.get("protocol_versions") if container_protocol_versions and len(container_protocol_versions) > 0: # Extract protocol and version from first entry protocol_record = container_protocol_versions[0] @@ -920,10 +932,21 @@ def _create_agent_span_from_parameters( # Determine agent type from definition # Check for hosted agent first (most specific - has container/image configuration) agent_type = None + # Extract kind discriminator for external agents + kind = definition.get("kind") + if isinstance(kind, str): + kind = kind.lower() + elif hasattr(kind, "value"): + kind = str(kind.value).lower() + else: + kind = None + if hosted_image or hosted_cpu or hosted_memory: agent_type = AGENT_TYPE_HOSTED elif workflow_yaml: agent_type = AGENT_TYPE_WORKFLOW + elif kind == "external" or definition.get("otel_agent_id") is not None: + agent_type = AGENT_TYPE_EXTERNAL elif instructions or model: # Prompt agent - identified by having instructions and/or a model. # Note: An agent with only a model (no instructions) is treated as a prompt agent, diff --git a/sdk/ai/azure-ai-projects/azure/ai/projects/telemetry/_utils.py b/sdk/ai/azure-ai-projects/azure/ai/projects/telemetry/_utils.py index 47047e2720c3..a2c4e71806b1 100644 --- a/sdk/ai/azure-ai-projects/azure/ai/projects/telemetry/_utils.py +++ b/sdk/ai/azure-ai-projects/azure/ai/projects/telemetry/_utils.py @@ -106,6 +106,7 @@ AGENT_TYPE_PROMPT = "prompt" AGENT_TYPE_WORKFLOW = "workflow" AGENT_TYPE_HOSTED = "hosted" +AGENT_TYPE_EXTERNAL = "external" AGENT_TYPE_UNKNOWN = "unknown" # Span name prefixes for responses API operations diff --git a/sdk/ai/azure-ai-projects/tests/agents/telemetry/test_ai_agents_instrumentor.py b/sdk/ai/azure-ai-projects/tests/agents/telemetry/test_ai_agents_instrumentor.py index cbee3236f66e..dc7990c871aa 100644 --- a/sdk/ai/azure-ai-projects/tests/agents/telemetry/test_ai_agents_instrumentor.py +++ b/sdk/ai/azure-ai-projects/tests/agents/telemetry/test_ai_agents_instrumentor.py @@ -18,7 +18,7 @@ ) from azure.ai.projects.telemetry import AIProjectInstrumentor, _utils from azure.core.settings import settings -from azure.ai.projects.models import PromptAgentDefinition, PromptAgentDefinitionTextOptions +from azure.ai.projects.models import PromptAgentDefinition, PromptAgentDefinitionTextOptions, ExternalAgentDefinition from azure.ai.projects.telemetry._utils import ( GEN_AI_AGENT_ID, GEN_AI_AGENT_NAME, @@ -34,6 +34,7 @@ AGENTS_PROVIDER, AGENT_TYPE_PROMPT, AGENT_TYPE_WORKFLOW, + AGENT_TYPE_EXTERNAL, _set_use_message_events, ) @@ -1097,3 +1098,63 @@ def test_agent_with_structured_output_without_instructions_content_recording_dis self._test_agent_with_structured_output_without_instructions_impl( use_events=False, content_recording_enabled=False, **kwargs ) + + # ---- External agent creation tests ---- + + def _test_external_agent_creation_impl(self, content_recording_enabled: bool, **kwargs): + """Implementation for external agent creation test. + + :param content_recording_enabled: Whether content recording is enabled. + :type content_recording_enabled: bool + """ + self.cleanup() + os.environ.update({CONTENT_TRACING_ENV_VARIABLE: "True" if content_recording_enabled else "False"}) + self.setup_telemetry() + assert content_recording_enabled == AIProjectInstrumentor().is_content_recording_enabled() + assert True == AIProjectInstrumentor().is_instrumented() + + operation_group = "tracing" if content_recording_enabled else "agents" + with self.create_client(operation_group=operation_group, allow_preview=True, **kwargs) as project_client: + + agent = project_client.agents.create_version( + agent_name="my-external-agent", + definition=ExternalAgentDefinition(otel_agent_id="external-service-agent-001"), + ) + version = agent.version + + project_client.agents.delete_version(agent_name=agent.name, agent_version=agent.version) + print("Deleted external agent") + + # ------------------------- Validate "create_agent" span --------------------------------- + self.exporter.force_flush() + spans = self.exporter.get_spans_by_name("create_agent my-external-agent") + assert len(spans) == 1 + span = spans[0] + expected_attributes = [ + (GEN_AI_PROVIDER_NAME, AGENTS_PROVIDER), + (GEN_AI_OPERATION_NAME, "create_agent"), + (SERVER_ADDRESS, ""), + (GEN_AI_AGENT_NAME, "my-external-agent"), + (GEN_AI_AGENT_ID, "my-external-agent:" + str(version)), + (GEN_AI_AGENT_VERSION, str(version)), + (GEN_AI_AGENT_TYPE, AGENT_TYPE_EXTERNAL), + ] + attributes_match = GenAiTraceVerifier().check_span_attributes(span, expected_attributes) + assert attributes_match == True + + # External agents have no instructions, no model, no tools — so no events or content attributes + assert len(span.events) == 0 + + @pytest.mark.usefixtures("instrument_with_content") + @servicePreparer() + @recorded_by_proxy + def test_external_agent_creation_with_tracing_content_recording_enabled(self, **kwargs): + """Test external agent creation with content recording enabled.""" + self._test_external_agent_creation_impl(content_recording_enabled=True, **kwargs) + + @pytest.mark.usefixtures("instrument_without_content") + @servicePreparer() + @recorded_by_proxy + def test_external_agent_creation_with_tracing_content_recording_disabled(self, **kwargs): + """Test external agent creation with content recording disabled.""" + self._test_external_agent_creation_impl(content_recording_enabled=False, **kwargs)