From 1e0f5b17931af9ff9933bc92a89b4796a5363aa1 Mon Sep 17 00:00:00 2001 From: M-Hietala <78813398+M-Hietala@users.noreply.github.com> Date: Wed, 26 Nov 2025 13:55:27 -0600 Subject: [PATCH 1/3] add tracing support for structured output agent creation --- sdk/ai/azure-ai-projects/CHANGELOG.md | 1 + .../telemetry/_ai_project_instrumentor.py | 50 ++- .../telemetry/test_ai_agents_instrumentor.py | 318 +++++++++++++++++ .../test_ai_agents_instrumentor_async.py | 326 ++++++++++++++++++ 4 files changed, 685 insertions(+), 10 deletions(-) diff --git a/sdk/ai/azure-ai-projects/CHANGELOG.md b/sdk/ai/azure-ai-projects/CHANGELOG.md index 57cea638acae..76b7d3adc4c8 100644 --- a/sdk/ai/azure-ai-projects/CHANGELOG.md +++ b/sdk/ai/azure-ai-projects/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features Added * The package now takes dependency on openai and azure-identity packages. No need to install them separately. +* Tracing: support for tracing the schema when agent with structured output is created. ### Breaking changes 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 25e439f82874..8b46c98663a0 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 @@ -454,21 +454,38 @@ def _add_instructions_event( additional_instructions: Optional[str], agent_id: Optional[str] = None, thread_id: Optional[str] = None, + response_schema: Optional[Any] = None, ) -> None: - # Early return if no instructions to trace - if not instructions: + # Early return if no instructions AND no response schema to trace + if not instructions and response_schema is None: return content_array: List[Dict[str, Any]] = [] if _trace_agents_content: - # Combine instructions if both exist - if additional_instructions: - combined_text = f"{instructions} {additional_instructions}" - else: - combined_text = instructions + # Add instructions if provided + if instructions: + # Combine instructions if both exist + if additional_instructions: + combined_text = f"{instructions} {additional_instructions}" + else: + combined_text = instructions + + # Use optimized format with consistent "content" field + content_array.append({"type": "text", "content": combined_text}) + + # Add response schema if provided + if response_schema is not None: + # Convert schema to JSON string if it's a dict/object + if isinstance(response_schema, dict): + schema_str = json.dumps(response_schema, ensure_ascii=False) + elif hasattr(response_schema, "__dict__"): + # Handle model objects by converting to dict first + schema_dict = {k: v for k, v in response_schema.__dict__.items() if not k.startswith("_")} + schema_str = json.dumps(schema_dict, ensure_ascii=False) + else: + schema_str = str(response_schema) - # Use optimized format with consistent "content" field - content_array.append({"type": "text", "content": combined_text}) + content_array.append({"type": "response_schema", "content": schema_str}) attributes = self._create_event_attributes(agent_id=agent_id, thread_id=thread_id) # Store as JSON array directly without outer wrapper @@ -536,8 +553,21 @@ def start_create_agent_span( # pylint: disable=too-many-locals if agent_type: span.add_attribute("gen_ai.agent.type", agent_type) + # Extract response schema from text parameter if available + response_schema = None + if response_format and text: + # Extract schema from text.format.schema if available + if hasattr(text, "format"): + format_info = getattr(text, "format", None) + if format_info and hasattr(format_info, "schema"): + response_schema = getattr(format_info, "schema", None) + elif isinstance(text, dict): + format_info = text.get("format") + if format_info and isinstance(format_info, dict): + response_schema = format_info.get("schema") + # Add instructions event (if instructions exist) - self._add_instructions_event(span, instructions, None) + self._add_instructions_event(span, instructions, None, response_schema=response_schema) # Add workflow event if workflow type agent (always add event, but only include YAML content if content recording enabled) if workflow_yaml is not None: 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 51f35257f342..af93341751d0 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 @@ -504,3 +504,321 @@ def test_workflow_agent_creation_with_tracing_content_recording_disabled(self, * # When content recording is disabled, event should be an empty array assert isinstance(event_content, list) assert len(event_content) == 0 + + @pytest.mark.usefixtures("instrument_with_content") + @servicePreparer() + @recorded_by_proxy + def test_agent_with_structured_output_with_instructions_content_recording_enabled(self, **kwargs): + """Test agent creation with structured output and instructions, content recording enabled.""" + self.cleanup() + os.environ.update({CONTENT_TRACING_ENV_VARIABLE: "True"}) + self.setup_telemetry() + assert True == AIProjectInstrumentor().is_content_recording_enabled() + assert True == AIProjectInstrumentor().is_instrumented() + + from azure.ai.projects.models import ResponseTextFormatConfigurationJsonSchema + + with self.create_client(operation_group="tracing", **kwargs) as project_client: + + model = self.test_agents_params["model_deployment_name"] + + # Define a JSON schema for structured output + test_schema = { + "type": "object", + "properties": { + "name": {"type": "string"}, + "age": {"type": "number"}, + }, + "required": ["name", "age"], + } + + agent_definition = PromptAgentDefinition( + model=model, + instructions="You are a helpful assistant that extracts person information.", + text=PromptAgentDefinitionText( + format=ResponseTextFormatConfigurationJsonSchema( + name="PersonInfo", + schema=test_schema, + ) + ), + ) + + agent = project_client.agents.create_version(agent_name="structured-agent", definition=agent_definition) + version = agent.version + + project_client.agents.delete_version(agent_name=agent.name, agent_version=agent.version) + + # Validate span + self.exporter.force_flush() + spans = self.exporter.get_spans_by_name("create_agent structured-agent") + assert len(spans) == 1 + span = spans[0] + + expected_attributes = [ + (GEN_AI_PROVIDER_NAME, AZURE_AI_AGENTS_PROVIDER), + (GEN_AI_OPERATION_NAME, "create_agent"), + (SERVER_ADDRESS, ""), + (GEN_AI_REQUEST_MODEL, model), + ("gen_ai.request.response_format", "json_schema"), + (GEN_AI_AGENT_NAME, "structured-agent"), + (GEN_AI_AGENT_ID, "structured-agent:" + str(version)), + (GEN_AI_AGENT_VERSION, str(version)), + (GEN_AI_AGENT_TYPE, AGENT_TYPE_PROMPT), + ] + attributes_match = GenAiTraceVerifier().check_span_attributes(span, expected_attributes) + assert attributes_match == True + + # Verify event contains both instructions and schema + events = span.events + assert len(events) == 1 + instruction_event = events[0] + assert instruction_event.name == GEN_AI_SYSTEM_INSTRUCTION_EVENT + + import json + + event_content = json.loads(instruction_event.attributes[GEN_AI_EVENT_CONTENT]) + assert isinstance(event_content, list) + assert len(event_content) == 2 # Both instructions and schema + + # Check instructions content + assert event_content[0]["type"] == "text" + assert "helpful assistant" in event_content[0]["content"] + + # Check schema content + assert event_content[1]["type"] == "response_schema" + schema_str = event_content[1]["content"] + schema_obj = json.loads(schema_str) + assert schema_obj["type"] == "object" + assert "name" in schema_obj["properties"] + assert "age" in schema_obj["properties"] + + @pytest.mark.usefixtures("instrument_without_content") + @servicePreparer() + @recorded_by_proxy + def test_agent_with_structured_output_with_instructions_content_recording_disabled(self, **kwargs): + """Test agent creation with structured output and instructions, content recording disabled.""" + self.cleanup() + os.environ.update({CONTENT_TRACING_ENV_VARIABLE: "False"}) + self.setup_telemetry() + assert False == AIProjectInstrumentor().is_content_recording_enabled() + assert True == AIProjectInstrumentor().is_instrumented() + + from azure.ai.projects.models import ResponseTextFormatConfigurationJsonSchema + + with self.create_client(operation_group="agents", **kwargs) as project_client: + + model = self.test_agents_params["model_deployment_name"] + + test_schema = { + "type": "object", + "properties": { + "name": {"type": "string"}, + "age": {"type": "number"}, + }, + "required": ["name", "age"], + } + + agent_definition = PromptAgentDefinition( + model=model, + instructions="You are a helpful assistant that extracts person information.", + text=PromptAgentDefinitionText( + format=ResponseTextFormatConfigurationJsonSchema( + name="PersonInfo", + schema=test_schema, + ) + ), + ) + + agent = project_client.agents.create_version(agent_name="structured-agent", definition=agent_definition) + version = agent.version + + project_client.agents.delete_version(agent_name=agent.name, agent_version=agent.version) + + # Validate span + self.exporter.force_flush() + spans = self.exporter.get_spans_by_name("create_agent structured-agent") + assert len(spans) == 1 + span = spans[0] + + expected_attributes = [ + (GEN_AI_PROVIDER_NAME, AZURE_AI_AGENTS_PROVIDER), + (GEN_AI_OPERATION_NAME, "create_agent"), + (SERVER_ADDRESS, ""), + (GEN_AI_REQUEST_MODEL, model), + ("gen_ai.request.response_format", "json_schema"), + (GEN_AI_AGENT_NAME, "structured-agent"), + (GEN_AI_AGENT_ID, "structured-agent:" + str(version)), + (GEN_AI_AGENT_VERSION, str(version)), + (GEN_AI_AGENT_TYPE, AGENT_TYPE_PROMPT), + ] + attributes_match = GenAiTraceVerifier().check_span_attributes(span, expected_attributes) + assert attributes_match == True + + # When content recording is disabled, event should be empty + events = span.events + assert len(events) == 1 + instruction_event = events[0] + assert instruction_event.name == GEN_AI_SYSTEM_INSTRUCTION_EVENT + + import json + + event_content = json.loads(instruction_event.attributes[GEN_AI_EVENT_CONTENT]) + assert isinstance(event_content, list) + assert len(event_content) == 0 # Empty when content recording disabled + + @pytest.mark.usefixtures("instrument_with_content") + @servicePreparer() + @recorded_by_proxy + def test_agent_with_structured_output_without_instructions_content_recording_enabled(self, **kwargs): + """Test agent creation with structured output but NO instructions, content recording enabled.""" + self.cleanup() + os.environ.update({CONTENT_TRACING_ENV_VARIABLE: "True"}) + self.setup_telemetry() + assert True == AIProjectInstrumentor().is_content_recording_enabled() + assert True == AIProjectInstrumentor().is_instrumented() + + from azure.ai.projects.models import ResponseTextFormatConfigurationJsonSchema + + with self.create_client(operation_group="tracing", **kwargs) as project_client: + + model = self.test_agents_params["model_deployment_name"] + + test_schema = { + "type": "object", + "properties": { + "result": {"type": "string"}, + }, + "required": ["result"], + } + + agent_definition = PromptAgentDefinition( + model=model, + # No instructions provided + text=PromptAgentDefinitionText( + format=ResponseTextFormatConfigurationJsonSchema( + name="Result", + schema=test_schema, + ) + ), + ) + + agent = project_client.agents.create_version( + agent_name="no-instructions-agent", definition=agent_definition + ) + version = agent.version + + project_client.agents.delete_version(agent_name=agent.name, agent_version=agent.version) + + # Validate span + self.exporter.force_flush() + spans = self.exporter.get_spans_by_name("create_agent no-instructions-agent") + assert len(spans) == 1 + span = spans[0] + + expected_attributes = [ + (GEN_AI_PROVIDER_NAME, AZURE_AI_AGENTS_PROVIDER), + (GEN_AI_OPERATION_NAME, "create_agent"), + (SERVER_ADDRESS, ""), + (GEN_AI_REQUEST_MODEL, model), + ("gen_ai.request.response_format", "json_schema"), + (GEN_AI_AGENT_NAME, "no-instructions-agent"), + (GEN_AI_AGENT_ID, "no-instructions-agent:" + str(version)), + (GEN_AI_AGENT_VERSION, str(version)), + (GEN_AI_AGENT_TYPE, AGENT_TYPE_PROMPT), + ] + attributes_match = GenAiTraceVerifier().check_span_attributes(span, expected_attributes) + assert attributes_match == True + + # Event should be created with just the schema (no instructions) + events = span.events + assert len(events) == 1 + instruction_event = events[0] + assert instruction_event.name == GEN_AI_SYSTEM_INSTRUCTION_EVENT + + import json + + event_content = json.loads(instruction_event.attributes[GEN_AI_EVENT_CONTENT]) + assert isinstance(event_content, list) + assert len(event_content) == 1 # Only schema, no instructions + + # Check schema content + assert event_content[0]["type"] == "response_schema" + schema_str = event_content[0]["content"] + schema_obj = json.loads(schema_str) + assert schema_obj["type"] == "object" + assert "result" in schema_obj["properties"] + + @pytest.mark.usefixtures("instrument_without_content") + @servicePreparer() + @recorded_by_proxy + def test_agent_with_structured_output_without_instructions_content_recording_disabled(self, **kwargs): + """Test agent creation with structured output but NO instructions, content recording disabled.""" + self.cleanup() + os.environ.update({CONTENT_TRACING_ENV_VARIABLE: "False"}) + self.setup_telemetry() + assert False == AIProjectInstrumentor().is_content_recording_enabled() + assert True == AIProjectInstrumentor().is_instrumented() + + from azure.ai.projects.models import ResponseTextFormatConfigurationJsonSchema + + with self.create_client(operation_group="agents", **kwargs) as project_client: + + model = self.test_agents_params["model_deployment_name"] + + test_schema = { + "type": "object", + "properties": { + "result": {"type": "string"}, + }, + "required": ["result"], + } + + agent_definition = PromptAgentDefinition( + model=model, + # No instructions provided + text=PromptAgentDefinitionText( + format=ResponseTextFormatConfigurationJsonSchema( + name="Result", + schema=test_schema, + ) + ), + ) + + agent = project_client.agents.create_version( + agent_name="no-instructions-agent", definition=agent_definition + ) + version = agent.version + + project_client.agents.delete_version(agent_name=agent.name, agent_version=agent.version) + + # Validate span + self.exporter.force_flush() + spans = self.exporter.get_spans_by_name("create_agent no-instructions-agent") + assert len(spans) == 1 + span = spans[0] + + expected_attributes = [ + (GEN_AI_PROVIDER_NAME, AZURE_AI_AGENTS_PROVIDER), + (GEN_AI_OPERATION_NAME, "create_agent"), + (SERVER_ADDRESS, ""), + (GEN_AI_REQUEST_MODEL, model), + ("gen_ai.request.response_format", "json_schema"), + (GEN_AI_AGENT_NAME, "no-instructions-agent"), + (GEN_AI_AGENT_ID, "no-instructions-agent:" + str(version)), + (GEN_AI_AGENT_VERSION, str(version)), + (GEN_AI_AGENT_TYPE, AGENT_TYPE_PROMPT), + ] + attributes_match = GenAiTraceVerifier().check_span_attributes(span, expected_attributes) + assert attributes_match == True + + # Event should be created with empty content due to content recording disabled + events = span.events + assert len(events) == 1 + instruction_event = events[0] + assert instruction_event.name == GEN_AI_SYSTEM_INSTRUCTION_EVENT + + import json + + event_content = json.loads(instruction_event.attributes[GEN_AI_EVENT_CONTENT]) + assert isinstance(event_content, list) + assert len(event_content) == 0 # Empty because content recording is disabled diff --git a/sdk/ai/azure-ai-projects/tests/agents/telemetry/test_ai_agents_instrumentor_async.py b/sdk/ai/azure-ai-projects/tests/agents/telemetry/test_ai_agents_instrumentor_async.py index cea68f6037ef..0d554d5f14a0 100644 --- a/sdk/ai/azure-ai-projects/tests/agents/telemetry/test_ai_agents_instrumentor_async.py +++ b/sdk/ai/azure-ai-projects/tests/agents/telemetry/test_ai_agents_instrumentor_async.py @@ -382,3 +382,329 @@ async def test_workflow_agent_creation_with_tracing_content_recording_disabled(s # When content recording is disabled, event should be an empty array assert isinstance(event_content, list) assert len(event_content) == 0 + + @pytest.mark.usefixtures("instrument_with_content") + @servicePreparer() + @recorded_by_proxy_async + async def test_agent_with_structured_output_with_instructions_content_recording_enabled(self, **kwargs): + """Test agent creation with structured output and instructions, content recording enabled (async).""" + self.cleanup() + os.environ.update({CONTENT_TRACING_ENV_VARIABLE: "True"}) + self.setup_telemetry() + assert True == AIProjectInstrumentor().is_content_recording_enabled() + assert True == AIProjectInstrumentor().is_instrumented() + + from azure.ai.projects.models import ResponseTextFormatConfigurationJsonSchema + + project_client = self.create_async_client(operation_group="tracing", **kwargs) + + async with project_client: + model = self.test_agents_params["model_deployment_name"] + + # Define a JSON schema for structured output + test_schema = { + "type": "object", + "properties": { + "name": {"type": "string"}, + "age": {"type": "number"}, + }, + "required": ["name", "age"], + } + + agent_definition = PromptAgentDefinition( + model=model, + instructions="You are a helpful assistant that extracts person information.", + text=PromptAgentDefinitionText( + format=ResponseTextFormatConfigurationJsonSchema( + name="PersonInfo", + schema=test_schema, + ) + ), + ) + + agent = await project_client.agents.create_version( + agent_name="structured-agent-async", definition=agent_definition + ) + version = agent.version + + await project_client.agents.delete_version(agent_name=agent.name, agent_version=agent.version) + + # Validate span + self.exporter.force_flush() + spans = self.exporter.get_spans_by_name("create_agent structured-agent-async") + assert len(spans) == 1 + span = spans[0] + + expected_attributes = [ + (GEN_AI_PROVIDER_NAME, AZURE_AI_AGENTS_PROVIDER), + (GEN_AI_OPERATION_NAME, "create_agent"), + (SERVER_ADDRESS, ""), + (GEN_AI_REQUEST_MODEL, model), + ("gen_ai.request.response_format", "json_schema"), + (GEN_AI_AGENT_NAME, "structured-agent-async"), + (GEN_AI_AGENT_ID, "structured-agent-async:" + str(version)), + (GEN_AI_AGENT_VERSION, str(version)), + (GEN_AI_AGENT_TYPE, AGENT_TYPE_PROMPT), + ] + attributes_match = GenAiTraceVerifier().check_span_attributes(span, expected_attributes) + assert attributes_match == True + + # Verify event contains both instructions and schema + events = span.events + assert len(events) == 1 + instruction_event = events[0] + assert instruction_event.name == GEN_AI_SYSTEM_INSTRUCTION_EVENT + + import json + + event_content = json.loads(instruction_event.attributes[GEN_AI_EVENT_CONTENT]) + assert isinstance(event_content, list) + assert len(event_content) == 2 # Both instructions and schema + + # Check instructions content + assert event_content[0]["type"] == "text" + assert "helpful assistant" in event_content[0]["content"] + + # Check schema content + assert event_content[1]["type"] == "response_schema" + schema_str = event_content[1]["content"] + schema_obj = json.loads(schema_str) + assert schema_obj["type"] == "object" + assert "name" in schema_obj["properties"] + assert "age" in schema_obj["properties"] + + @pytest.mark.usefixtures("instrument_without_content") + @servicePreparer() + @recorded_by_proxy_async + async def test_agent_with_structured_output_with_instructions_content_recording_disabled(self, **kwargs): + """Test agent creation with structured output and instructions, content recording disabled (async).""" + self.cleanup() + os.environ.update({CONTENT_TRACING_ENV_VARIABLE: "False"}) + self.setup_telemetry() + assert False == AIProjectInstrumentor().is_content_recording_enabled() + assert True == AIProjectInstrumentor().is_instrumented() + + from azure.ai.projects.models import ResponseTextFormatConfigurationJsonSchema + + project_client = self.create_async_client(operation_group="agents", **kwargs) + + async with project_client: + model = self.test_agents_params["model_deployment_name"] + + test_schema = { + "type": "object", + "properties": { + "name": {"type": "string"}, + "age": {"type": "number"}, + }, + "required": ["name", "age"], + } + + agent_definition = PromptAgentDefinition( + model=model, + instructions="You are a helpful assistant that extracts person information.", + text=PromptAgentDefinitionText( + format=ResponseTextFormatConfigurationJsonSchema( + name="PersonInfo", + schema=test_schema, + ) + ), + ) + + agent = await project_client.agents.create_version( + agent_name="structured-agent-async", definition=agent_definition + ) + version = agent.version + + await project_client.agents.delete_version(agent_name=agent.name, agent_version=agent.version) + + # Validate span + self.exporter.force_flush() + spans = self.exporter.get_spans_by_name("create_agent structured-agent-async") + assert len(spans) == 1 + span = spans[0] + + expected_attributes = [ + (GEN_AI_PROVIDER_NAME, AZURE_AI_AGENTS_PROVIDER), + (GEN_AI_OPERATION_NAME, "create_agent"), + (SERVER_ADDRESS, ""), + (GEN_AI_REQUEST_MODEL, model), + ("gen_ai.request.response_format", "json_schema"), + (GEN_AI_AGENT_NAME, "structured-agent-async"), + (GEN_AI_AGENT_ID, "structured-agent-async:" + str(version)), + (GEN_AI_AGENT_VERSION, str(version)), + (GEN_AI_AGENT_TYPE, AGENT_TYPE_PROMPT), + ] + attributes_match = GenAiTraceVerifier().check_span_attributes(span, expected_attributes) + assert attributes_match == True + + # When content recording is disabled, event should be empty + events = span.events + assert len(events) == 1 + instruction_event = events[0] + assert instruction_event.name == GEN_AI_SYSTEM_INSTRUCTION_EVENT + + import json + + event_content = json.loads(instruction_event.attributes[GEN_AI_EVENT_CONTENT]) + assert isinstance(event_content, list) + assert len(event_content) == 0 # Empty when content recording disabled + + @pytest.mark.usefixtures("instrument_with_content") + @servicePreparer() + @recorded_by_proxy_async + async def test_agent_with_structured_output_without_instructions_content_recording_enabled(self, **kwargs): + """Test agent creation with structured output but NO instructions, content recording enabled (async).""" + self.cleanup() + os.environ.update({CONTENT_TRACING_ENV_VARIABLE: "True"}) + self.setup_telemetry() + assert True == AIProjectInstrumentor().is_content_recording_enabled() + assert True == AIProjectInstrumentor().is_instrumented() + + from azure.ai.projects.models import ResponseTextFormatConfigurationJsonSchema + + project_client = self.create_async_client(operation_group="tracing", **kwargs) + + async with project_client: + model = self.test_agents_params["model_deployment_name"] + + test_schema = { + "type": "object", + "properties": { + "result": {"type": "string"}, + }, + "required": ["result"], + } + + agent_definition = PromptAgentDefinition( + model=model, + # No instructions provided + text=PromptAgentDefinitionText( + format=ResponseTextFormatConfigurationJsonSchema( + name="Result", + schema=test_schema, + ) + ), + ) + + agent = await project_client.agents.create_version( + agent_name="no-instructions-agent-async", definition=agent_definition + ) + version = agent.version + + await project_client.agents.delete_version(agent_name=agent.name, agent_version=agent.version) + + # Validate span + self.exporter.force_flush() + spans = self.exporter.get_spans_by_name("create_agent no-instructions-agent-async") + assert len(spans) == 1 + span = spans[0] + + expected_attributes = [ + (GEN_AI_PROVIDER_NAME, AZURE_AI_AGENTS_PROVIDER), + (GEN_AI_OPERATION_NAME, "create_agent"), + (SERVER_ADDRESS, ""), + (GEN_AI_REQUEST_MODEL, model), + ("gen_ai.request.response_format", "json_schema"), + (GEN_AI_AGENT_NAME, "no-instructions-agent-async"), + (GEN_AI_AGENT_ID, "no-instructions-agent-async:" + str(version)), + (GEN_AI_AGENT_VERSION, str(version)), + (GEN_AI_AGENT_TYPE, AGENT_TYPE_PROMPT), + ] + attributes_match = GenAiTraceVerifier().check_span_attributes(span, expected_attributes) + assert attributes_match == True + + # Event should be created with just the schema (no instructions) + events = span.events + assert len(events) == 1 + instruction_event = events[0] + assert instruction_event.name == GEN_AI_SYSTEM_INSTRUCTION_EVENT + + import json + + event_content = json.loads(instruction_event.attributes[GEN_AI_EVENT_CONTENT]) + assert isinstance(event_content, list) + assert len(event_content) == 1 # Only schema, no instructions + + # Check schema content + assert event_content[0]["type"] == "response_schema" + schema_str = event_content[0]["content"] + schema_obj = json.loads(schema_str) + assert schema_obj["type"] == "object" + assert "result" in schema_obj["properties"] + + @pytest.mark.usefixtures("instrument_without_content") + @servicePreparer() + @recorded_by_proxy_async + async def test_agent_with_structured_output_without_instructions_content_recording_disabled(self, **kwargs): + """Test agent creation with structured output but NO instructions, content recording disabled (async).""" + self.cleanup() + os.environ.update({CONTENT_TRACING_ENV_VARIABLE: "False"}) + self.setup_telemetry() + assert False == AIProjectInstrumentor().is_content_recording_enabled() + assert True == AIProjectInstrumentor().is_instrumented() + + from azure.ai.projects.models import ResponseTextFormatConfigurationJsonSchema + + project_client = self.create_async_client(operation_group="agents", **kwargs) + + async with project_client: + model = self.test_agents_params["model_deployment_name"] + + test_schema = { + "type": "object", + "properties": { + "result": {"type": "string"}, + }, + "required": ["result"], + } + + agent_definition = PromptAgentDefinition( + model=model, + # No instructions provided + text=PromptAgentDefinitionText( + format=ResponseTextFormatConfigurationJsonSchema( + name="Result", + schema=test_schema, + ) + ), + ) + + agent = await project_client.agents.create_version( + agent_name="no-instructions-agent-async", definition=agent_definition + ) + version = agent.version + + await project_client.agents.delete_version(agent_name=agent.name, agent_version=agent.version) + + # Validate span + self.exporter.force_flush() + spans = self.exporter.get_spans_by_name("create_agent no-instructions-agent-async") + assert len(spans) == 1 + span = spans[0] + + expected_attributes = [ + (GEN_AI_PROVIDER_NAME, AZURE_AI_AGENTS_PROVIDER), + (GEN_AI_OPERATION_NAME, "create_agent"), + (SERVER_ADDRESS, ""), + (GEN_AI_REQUEST_MODEL, model), + ("gen_ai.request.response_format", "json_schema"), + (GEN_AI_AGENT_NAME, "no-instructions-agent-async"), + (GEN_AI_AGENT_ID, "no-instructions-agent-async:" + str(version)), + (GEN_AI_AGENT_VERSION, str(version)), + (GEN_AI_AGENT_TYPE, AGENT_TYPE_PROMPT), + ] + attributes_match = GenAiTraceVerifier().check_span_attributes(span, expected_attributes) + assert attributes_match == True + + # Event should be created with empty content due to content recording disabled + events = span.events + assert len(events) == 1 + instruction_event = events[0] + assert instruction_event.name == GEN_AI_SYSTEM_INSTRUCTION_EVENT + + import json + + event_content = json.loads(instruction_event.attributes[GEN_AI_EVENT_CONTENT]) + assert isinstance(event_content, list) + assert len(event_content) == 0 # Empty because content recording is disabled From 457839ea9a2484ddb64f82bc2227026629fcaf21 Mon Sep 17 00:00:00 2001 From: M-Hietala <78813398+M-Hietala@users.noreply.github.com> Date: Wed, 26 Nov 2025 13:56:21 -0600 Subject: [PATCH 2/3] updating assets.json --- sdk/ai/azure-ai-projects/assets.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/ai/azure-ai-projects/assets.json b/sdk/ai/azure-ai-projects/assets.json index 0ee7b5bbbdd2..5c8a802be574 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_6895b440ac" + "Tag": "python/ai/azure-ai-projects_354ab8e34b" } From cf1207a2d17432631651a3939157f4e4c8560c88 Mon Sep 17 00:00:00 2001 From: M-Hietala <78813398+M-Hietala@users.noreply.github.com> Date: Wed, 26 Nov 2025 14:50:52 -0600 Subject: [PATCH 3/3] a change to re-trigger ci build --- sdk/ai/azure-ai-projects/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/ai/azure-ai-projects/CHANGELOG.md b/sdk/ai/azure-ai-projects/CHANGELOG.md index 76b7d3adc4c8..a2e21e0b3163 100644 --- a/sdk/ai/azure-ai-projects/CHANGELOG.md +++ b/sdk/ai/azure-ai-projects/CHANGELOG.md @@ -5,7 +5,7 @@ ### Features Added * The package now takes dependency on openai and azure-identity packages. No need to install them separately. -* Tracing: support for tracing the schema when agent with structured output is created. +* Tracing: support for tracing the schema when agent is created with structured output definition. ### Breaking changes