Skip to content

Commit 4e7d80b

Browse files
authored
trace update for system instructions (#45138)
* trace update for system instructions * update changelog * update to re-trigger ci * update to re-trigger ci
1 parent 7e801a6 commit 4e7d80b

6 files changed

Lines changed: 134 additions & 130 deletions

File tree

sdk/ai/azure-ai-projects/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
* Tracing: response generation span names changed from "responses {model_name}" to "chat {model_name}" for model calls and from "responses {agent_name}" to "invoke_agent {agent_name}" for agent calls.
2727
* Tracing: response generation operation names changed from "responses" to "chat" for model calls and from "responses" to "invoke_agent" for agent calls.
2828
* Tracing: response generation uses gen_ai.input.messages and gen_ai.output.messages attributes directly under the span instead of events.
29-
* Tracing: agent creation uses gen_ai.system.instructions attribute directly under the span instead of an event.
29+
* Tracing: agent creation uses gen_ai.system_instructions attribute directly under the span instead of an event. Note that the attribute name is gen_ai.system_instructions not gen_ai.system.instructions.
3030
* Tracing: "gen_ai.provider.name" attribute value changed to "microsoft.foundry".
3131
* Tracing: the format of the function tool call related traces in input and output messages changed to {"type": "tool_call", "id": "...", "name": "...", "arguments": {...}} and {"type": "tool_call_response", "id": "...", "result": "..."}
3232

sdk/ai/azure-ai-projects/azure/ai/projects/telemetry/_ai_project_instrumentor.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
GEN_AI_OUTPUT_MESSAGES,
3535
GEN_AI_PROVIDER_NAME,
3636
GEN_AI_SYSTEM_MESSAGE,
37+
GEN_AI_SYSTEM_INSTRUCTION_EVENT,
3738
GEN_AI_THREAD_ID,
3839
GEN_AI_THREAD_RUN_ID,
3940
GEN_AI_USAGE_INPUT_TOKENS,
@@ -633,9 +634,10 @@ def _add_instructions_event(
633634
return
634635

635636
content_array: List[Dict[str, Any]] = []
636-
if _trace_agents_content:
637-
# Add instructions if provided
638-
if instructions:
637+
638+
# Add instructions if provided
639+
if instructions:
640+
if _trace_agents_content:
639641
# Combine instructions if both exist
640642
if additional_instructions:
641643
combined_text = f"{instructions} {additional_instructions}"
@@ -644,9 +646,13 @@ def _add_instructions_event(
644646

645647
# Use optimized format with consistent "content" field
646648
content_array.append({"type": "text", "content": combined_text})
649+
else:
650+
# Content recording disabled, but indicate that text instructions exist
651+
content_array.append({"type": "text"})
647652

648-
# Add response schema if provided
649-
if response_schema is not None:
653+
# Add response schema if provided
654+
if response_schema is not None:
655+
if _trace_agents_content:
650656
# Convert schema to JSON string if it's a dict/object
651657
if isinstance(response_schema, dict):
652658
schema_str = json.dumps(response_schema, ensure_ascii=False)
@@ -663,6 +669,9 @@ def _add_instructions_event(
663669
schema_str = str(response_schema)
664670

665671
content_array.append({"type": "response_schema", "content": schema_str})
672+
else:
673+
# Content recording disabled, but indicate that response schema exists
674+
content_array.append({"type": "response_schema"})
666675

667676
use_events = _get_use_message_events()
668677

@@ -671,14 +680,11 @@ def _add_instructions_event(
671680
attributes = self._create_event_attributes(agent_id=agent_id, thread_id=thread_id)
672681
# Store as JSON array directly without outer wrapper
673682
attributes[GEN_AI_EVENT_CONTENT] = json.dumps(content_array, ensure_ascii=False)
674-
span.span_instance.add_event(name=GEN_AI_SYSTEM_MESSAGE, attributes=attributes)
683+
span.span_instance.add_event(name=GEN_AI_SYSTEM_INSTRUCTION_EVENT, attributes=attributes)
675684
else:
676685
# Use attributes for instructions tracing
677-
# System instructions use the same attribute name as the event
678-
message_json = json.dumps(
679-
[{"role": "system", "parts": content_array}] if content_array else [{"role": "system"}],
680-
ensure_ascii=False,
681-
)
686+
# System instructions format: array of content objects without role/parts wrapper
687+
message_json = json.dumps(content_array, ensure_ascii=False)
682688
if span and span.span_instance.is_recording:
683689
span.add_attribute(GEN_AI_SYSTEM_MESSAGE, message_json)
684690

sdk/ai/azure-ai-projects/azure/ai/projects/telemetry/_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
GEN_AI_REQUEST_RESPONSE_FORMAT = "gen_ai.request.response_format"
4646
GEN_AI_USAGE_INPUT_TOKENS = "gen_ai.usage.input_tokens"
4747
GEN_AI_USAGE_OUTPUT_TOKENS = "gen_ai.usage.output_tokens"
48-
GEN_AI_SYSTEM_MESSAGE = "gen_ai.system.instructions"
48+
GEN_AI_SYSTEM_MESSAGE = "gen_ai.system_instructions"
4949
GEN_AI_EVENT_CONTENT = "gen_ai.event.content"
5050
GEN_AI_RUN_STEP_START_TIMESTAMP = "gen_ai.run_step.start.timestamp"
5151
GEN_AI_RUN_STEP_END_TIMESTAMP = "gen_ai.run_step.end.timestamp"

sdk/ai/azure-ai-projects/tests/agents/telemetry/test_ai_agents_instrumentor.py

Lines changed: 54 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -239,13 +239,8 @@ def _test_agent_creation_with_tracing_content_recording_enabled_impl(self, use_e
239239
expected_system_message = json.dumps(
240240
[
241241
{
242-
"role": "system",
243-
"parts": [
244-
{
245-
"type": "text",
246-
"content": "You are a helpful AI assistant. Be polite and provide accurate information.",
247-
}
248-
],
242+
"type": "text",
243+
"content": "You are a helpful AI assistant. Be polite and provide accurate information.",
249244
}
250245
],
251246
ensure_ascii=False,
@@ -279,15 +274,12 @@ def _test_agent_creation_with_tracing_content_recording_enabled_impl(self, use_e
279274
system_message_json = span.attributes[GEN_AI_SYSTEM_MESSAGE]
280275
system_message = json.loads(system_message_json)
281276

282-
# Verify structure
277+
# Verify structure (new format without role/parts wrapper)
283278
assert isinstance(system_message, list)
284279
assert len(system_message) == 1
285-
assert system_message[0]["role"] == "system"
286-
assert "parts" in system_message[0]
287-
assert len(system_message[0]["parts"]) == 1
288-
assert system_message[0]["parts"][0]["type"] == "text"
280+
assert system_message[0]["type"] == "text"
289281
assert (
290-
system_message[0]["parts"][0]["content"]
282+
system_message[0]["content"]
291283
== "You are a helpful AI assistant. Be polite and provide accurate information."
292284
)
293285

@@ -349,25 +341,27 @@ def _test_agent_creation_with_tracing_content_recording_disabled_impl(self, use_
349341
]
350342

351343
# When using attributes (regardless of content recording), add system message attribute
352-
# When content recording is disabled, it will have empty content (just role)
344+
# When content recording is disabled, it will have type indicator without content
353345
if not use_events:
354346
from azure.ai.projects.telemetry._utils import GEN_AI_SYSTEM_MESSAGE
355347
import json
356348

357-
# Empty system message (no parts, just role)
358-
expected_system_message = json.dumps([{"role": "system"}], ensure_ascii=False)
349+
# Empty system message (type indicator only, no content)
350+
expected_system_message = json.dumps([{"type": "text"}], ensure_ascii=False)
359351
expected_attributes.append((GEN_AI_SYSTEM_MESSAGE, expected_system_message))
360352

361353
attributes_match = GenAiTraceVerifier().check_span_attributes(span, expected_attributes)
362354
assert attributes_match == True
363355

364356
if use_events:
357+
import json
358+
365359
expected_events = [
366360
{
367361
"name": GEN_AI_SYSTEM_INSTRUCTION_EVENT,
368362
"attributes": {
369363
GEN_AI_PROVIDER_NAME: AGENTS_PROVIDER,
370-
GEN_AI_EVENT_CONTENT: "[]",
364+
GEN_AI_EVENT_CONTENT: json.dumps([{"type": "text"}]),
371365
},
372366
}
373367
]
@@ -383,12 +377,11 @@ def _test_agent_creation_with_tracing_content_recording_disabled_impl(self, use_
383377

384378
system_message_json = span.attributes[GEN_AI_SYSTEM_MESSAGE]
385379
system_message = json.loads(system_message_json)
386-
# Should have empty content (just role, no parts)
380+
# Should have type indicator when content recording is disabled
387381
assert isinstance(system_message, list)
388382
assert len(system_message) == 1
389-
assert system_message[0]["role"] == "system"
390-
# No parts field when content recording is disabled
391-
assert "parts" not in system_message[0]
383+
assert system_message[0]["type"] == "text"
384+
assert "content" not in system_message[0]
392385

393386
@pytest.mark.usefixtures("instrument_without_content")
394387
@servicePreparer()
@@ -594,21 +587,19 @@ def _test_agent_with_structured_output_with_instructions_impl(
594587
expected_system_msg = json.dumps(
595588
[
596589
{
597-
"role": "system",
598-
"parts": [
599-
{
600-
"type": "text",
601-
"content": "You are a helpful assistant that extracts person information.",
602-
},
603-
{"type": "response_schema", "content": json.dumps(test_schema)},
604-
],
605-
}
590+
"type": "text",
591+
"content": "You are a helpful assistant that extracts person information.",
592+
},
593+
{"type": "response_schema", "content": json.dumps(test_schema)},
606594
],
607595
ensure_ascii=False,
608596
)
609597
else:
610-
# When content recording disabled, attribute has empty structure
611-
expected_system_msg = json.dumps([{"role": "system"}], ensure_ascii=False)
598+
# When content recording disabled, type indicators without content
599+
expected_system_msg = json.dumps(
600+
[{"type": "text"}, {"type": "response_schema"}],
601+
ensure_ascii=False,
602+
)
612603
expected_attributes.append((GEN_AI_SYSTEM_MESSAGE, expected_system_msg))
613604

614605
attributes_match = GenAiTraceVerifier().check_span_attributes(span, expected_attributes)
@@ -634,7 +625,12 @@ def _test_agent_with_structured_output_with_instructions_impl(
634625
assert "name" in schema_obj["properties"]
635626
assert "age" in schema_obj["properties"]
636627
else:
637-
assert len(event_content) == 0 # Empty when content recording disabled
628+
# Type indicators without content when content recording disabled
629+
assert len(event_content) == 2
630+
assert event_content[0]["type"] == "text"
631+
assert "content" not in event_content[0]
632+
assert event_content[1]["type"] == "response_schema"
633+
assert "content" not in event_content[1]
638634
else:
639635
# When using attributes, verify attribute
640636
from azure.ai.projects.telemetry._utils import GEN_AI_SYSTEM_MESSAGE
@@ -646,25 +642,26 @@ def _test_agent_with_structured_output_with_instructions_impl(
646642
system_message = json.loads(system_message_json)
647643

648644
assert isinstance(system_message, list)
649-
assert len(system_message) == 1
650-
assert system_message[0]["role"] == "system"
651645

652646
if content_recording_enabled:
653-
assert "parts" in system_message[0]
654-
assert len(system_message[0]["parts"]) == 2
647+
assert len(system_message) == 2
655648

656649
# Check instruction part
657-
assert system_message[0]["parts"][0]["type"] == "text"
658-
assert "helpful assistant" in system_message[0]["parts"][0]["content"]
650+
assert system_message[0]["type"] == "text"
651+
assert "helpful assistant" in system_message[0]["content"]
659652

660653
# Check schema part
661-
assert system_message[0]["parts"][1]["type"] == "response_schema"
662-
schema_obj = json.loads(system_message[0]["parts"][1]["content"])
654+
assert system_message[1]["type"] == "response_schema"
655+
schema_obj = json.loads(system_message[1]["content"])
663656
assert schema_obj["type"] == "object"
664657
assert "name" in schema_obj["properties"]
665658
else:
666-
# When content recording disabled, no parts field
667-
assert "parts" not in system_message[0]
659+
# When content recording disabled, type indicators without content
660+
assert len(system_message) == 2
661+
assert system_message[0]["type"] == "text"
662+
assert "content" not in system_message[0]
663+
assert system_message[1]["type"] == "response_schema"
664+
assert "content" not in system_message[1]
668665

669666
@pytest.mark.usefixtures("instrument_with_content")
670667
@servicePreparer()
@@ -776,12 +773,12 @@ def _test_agent_with_structured_output_without_instructions_impl(
776773

777774
if content_recording_enabled:
778775
expected_system_msg = json.dumps(
779-
[{"role": "system", "parts": [{"type": "response_schema", "content": json.dumps(test_schema)}]}],
776+
[{"type": "response_schema", "content": json.dumps(test_schema)}],
780777
ensure_ascii=False,
781778
)
782779
else:
783-
# When content recording disabled, attribute has empty structure
784-
expected_system_msg = json.dumps([{"role": "system"}], ensure_ascii=False)
780+
# When content recording disabled, type indicator without content
781+
expected_system_msg = json.dumps([{"type": "response_schema"}], ensure_ascii=False)
785782
expected_attributes.append((GEN_AI_SYSTEM_MESSAGE, expected_system_msg))
786783

787784
attributes_match = GenAiTraceVerifier().check_span_attributes(span, expected_attributes)
@@ -804,7 +801,10 @@ def _test_agent_with_structured_output_without_instructions_impl(
804801
assert schema_obj["type"] == "object"
805802
assert "result" in schema_obj["properties"]
806803
else:
807-
assert len(event_content) == 0 # Empty when content recording disabled
804+
# Type indicator without content when content recording disabled
805+
assert len(event_content) == 1
806+
assert event_content[0]["type"] == "response_schema"
807+
assert "content" not in event_content[0]
808808
else:
809809
# When using attributes, verify attribute
810810
from azure.ai.projects.telemetry._utils import GEN_AI_SYSTEM_MESSAGE
@@ -816,21 +816,20 @@ def _test_agent_with_structured_output_without_instructions_impl(
816816
system_message = json.loads(system_message_json)
817817

818818
assert isinstance(system_message, list)
819-
assert len(system_message) == 1
820-
assert system_message[0]["role"] == "system"
821819

822820
if content_recording_enabled:
823-
assert "parts" in system_message[0]
824-
assert len(system_message[0]["parts"]) == 1
821+
assert len(system_message) == 1
825822

826823
# Check schema part
827-
assert system_message[0]["parts"][0]["type"] == "response_schema"
828-
schema_obj = json.loads(system_message[0]["parts"][0]["content"])
824+
assert system_message[0]["type"] == "response_schema"
825+
schema_obj = json.loads(system_message[0]["content"])
829826
assert schema_obj["type"] == "object"
830827
assert "result" in schema_obj["properties"]
831828
else:
832-
# When content recording disabled, no parts field
833-
assert "parts" not in system_message[0]
829+
# When content recording disabled, type indicator without content
830+
assert len(system_message) == 1
831+
assert system_message[0]["type"] == "response_schema"
832+
assert "content" not in system_message[0]
834833

835834
@pytest.mark.usefixtures("instrument_with_content")
836835
@servicePreparer()

0 commit comments

Comments
 (0)