Skip to content

Commit 5e540a4

Browse files
authored
Fix exception when tracing agents_client.threads.create(messages=[ThreadMessageOptions(....)]) (#42812)
1 parent f40049c commit 5e540a4

File tree

7 files changed

+213
-121
lines changed

7 files changed

+213
-121
lines changed

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@
88

99
### Features Added
1010

11-
- Added static merge_resources method to `McpTool` with accompanying sample.
11+
- Added static `merge_resources` method to `McpTool` with accompanying sample.
1212

1313
### Bugs Fixed
1414

15-
* Fix the issue with logging Agent message, when the message has "in progress" status (related to [issue](https://github.com/Azure/azure-sdk-for-python/issues/42645)).
16-
* Fix the issue with `RunStepOpenAPIToolCall` logging [issue](https://github.com/Azure/azure-sdk-for-python/issues/42645).
15+
- Fix issue with tracing an Agent message, when the message has "in progress" status (related to [GitHub Issue 42645](https://github.com/Azure/azure-sdk-for-python/issues/42645)).
16+
- Fix issue with tracing `RunStepOpenAPIToolCall` ([GitHub issue 42645](https://github.com/Azure/azure-sdk-for-python/issues/42645)).
17+
- Fix issue when `.threads.create(messages=[ThreadMessageOptions(...])` is called on the `AgentsClient`, when tracing is enabled ([GitHub issue 42805](https://github.com/Azure/azure-sdk-for-python/issues/42805))
1718

1819
### Sample updates
1920

sdk/ai/azure-ai-agents/assets.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
"AssetsRepo": "Azure/azure-sdk-assets",
33
"AssetsRepoPrefixPath": "python",
44
"TagPrefix": "python/ai/azure-ai-agents",
5-
"Tag": "python/ai/azure-ai-agents_65510bf995"
5+
"Tag": "python/ai/azure-ai-agents_f5906767ec"
66
}

sdk/ai/azure-ai-agents/azure/ai/agents/telemetry/_ai_agents_instrumentor.py

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
MessageAttachment,
2727
MessageDeltaChunk,
2828
MessageIncompleteDetails,
29+
MessageInputContentBlock,
30+
MessageInputTextBlock,
2931
RunStep,
3032
RunStepDeltaChunk,
3133
RunStepError,
@@ -36,6 +38,7 @@
3638
RunStepCodeInterpreterToolCall,
3739
RunStepBingGroundingToolCall,
3840
ThreadMessage,
41+
ThreadMessageOptions,
3942
ThreadRun,
4043
ToolDefinition,
4144
ToolOutput,
@@ -369,31 +372,42 @@ def _create_event_attributes(
369372
def add_thread_message_event(
370373
self,
371374
span,
372-
message: ThreadMessage,
375+
message: Union[ThreadMessage, ThreadMessageOptions],
373376
usage: Optional[_models.RunStepCompletionUsage] = None,
374377
) -> None:
375-
content_body = {}
378+
379+
content_body: Optional[Union[str, Dict[str, Any]]] = None
376380
if _trace_agents_content:
377-
for content in message.content:
378-
typed_content = content.get(content.type, None)
379-
if typed_content:
380-
content_details = {"value": self._get_field(typed_content, "value")}
381-
annotations = self._get_field(typed_content, "annotations")
382-
if annotations:
383-
content_details["annotations"] = [a.as_dict() for a in annotations]
384-
content_body[content.type] = content_details
381+
if isinstance(message, ThreadMessage):
382+
for content in message.content:
383+
typed_content = content.get(content.type, None)
384+
if typed_content:
385+
content_details = {"value": self._get_field(typed_content, "value")}
386+
annotations = self._get_field(typed_content, "annotations")
387+
if annotations:
388+
content_details["annotations"] = [a.as_dict() for a in annotations]
389+
content_body = {}
390+
content_body[content.type] = content_details
391+
elif isinstance(message, ThreadMessageOptions):
392+
if isinstance(message.content, str):
393+
content_body = message.content
394+
elif isinstance(message.content, List):
395+
for block in message.content:
396+
if isinstance(block, MessageInputTextBlock):
397+
content_body = block.text
398+
break
385399

386400
self._add_message_event(
387401
span,
388402
self._get_role(message.role),
389403
content_body,
390-
attachments=message.attachments,
391-
thread_id=message.thread_id,
392-
agent_id=message.agent_id,
393-
message_id=message.id,
394-
thread_run_id=message.run_id,
395-
message_status=message.status,
396-
incomplete_details=message.incomplete_details,
404+
attachments=getattr(message, "attachments", None),
405+
thread_id=getattr(message, "thread_id", None),
406+
agent_id=getattr(message, "agent_id", None),
407+
message_id=getattr(message, "id", None),
408+
thread_run_id=getattr(message, "run_id", None),
409+
message_status=getattr(message, "status", None),
410+
incomplete_details=getattr(message, "incomplete_details", None),
397411
usage=usage,
398412
)
399413

@@ -563,7 +577,7 @@ def _add_message_event(
563577
self,
564578
span,
565579
role: str,
566-
content: Any,
580+
content: Optional[Union[str, dict[str, Any], List[MessageInputContentBlock]]] = None,
567581
attachments: Any = None, # Optional[List[MessageAttachment]] or dict
568582
thread_id: Optional[str] = None,
569583
agent_id: Optional[str] = None,
@@ -575,9 +589,15 @@ def _add_message_event(
575589
) -> None:
576590
# TODO document new fields
577591

578-
event_body = {}
592+
event_body: dict[str, Any]= {}
579593
if _trace_agents_content:
580-
event_body["content"] = content
594+
if isinstance(content, List):
595+
for block in content:
596+
if isinstance(block, MessageInputTextBlock):
597+
event_body["content"] = block.text
598+
break
599+
else:
600+
event_body["content"] = content
581601
if attachments:
582602
event_body["attachments"] = []
583603
for attachment in attachments:
@@ -809,7 +829,7 @@ def start_create_agent_span(
809829
def start_create_thread_span(
810830
self,
811831
server_address: Optional[str] = None,
812-
messages: Optional[List[ThreadMessage]] = None,
832+
messages: Optional[Union[List[ThreadMessage], List[ThreadMessageOptions]]] = None,
813833
_tool_resources: Optional[ToolResources] = None,
814834
) -> "Optional[AbstractSpan]":
815835
span = start_span(OperationName.CREATE_THREAD, server_address=server_address)
@@ -1487,7 +1507,7 @@ def start_create_message_span(
14871507
self,
14881508
server_address: Optional[str] = None,
14891509
thread_id: Optional[str] = None,
1490-
content: Optional[str] = None,
1510+
content: Optional[Union[str, List[MessageInputContentBlock]]] = None,
14911511
role: Optional[Union[str, MessageRole]] = None,
14921512
attachments: Optional[List[MessageAttachment]] = None,
14931513
) -> "Optional[AbstractSpan]":

sdk/ai/azure-ai-agents/samples/sample_agents_basics.py

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,24 +27,6 @@
2727
from azure.identity import DefaultAzureCredential
2828
from azure.ai.agents.models import ListSortOrder
2929

30-
import sys
31-
import logging
32-
33-
# Enable detailed console logs across Azure libraries
34-
azure_logger = logging.getLogger("azure")
35-
azure_logger.setLevel(logging.DEBUG)
36-
azure_logger.addHandler(logging.StreamHandler(stream=sys.stdout))
37-
38-
# Exclude details logs for network calls associated with getting Entra ID token
39-
identity_logger = logging.getLogger("azure.identity")
40-
identity_logger.setLevel(logging.ERROR)
41-
42-
# Make sure regular (redacted) detailed azure.core logs are not shown, as we are about to
43-
# turn on non-redacted logs by passing 'logging_enable=True' to the client constructor
44-
# (which are implemented as a separate logging policy)
45-
logger = logging.getLogger("azure.core.pipeline.policies.http_logging_policy")
46-
logger.setLevel(logging.ERROR)
47-
4830
# Pass in 'logging_enable=True' to your client constructor for un-redacted logs
4931
project_client = AIProjectClient(
5032
endpoint=os.environ["PROJECT_ENDPOINT"], credential=DefaultAzureCredential(), logging_enable=True

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

Lines changed: 77 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
McpTool,
1919
MessageDeltaChunk,
2020
MessageDeltaTextContent,
21+
MessageInputTextBlock,
2122
OpenApiAnonymousAuthDetails,
2223
OpenApiTool,
2324
RequiredMcpToolCall,
@@ -28,6 +29,7 @@
2829
RunStepToolCallDetails,
2930
SubmitToolApprovalAction,
3031
ThreadMessage,
32+
ThreadMessageOptions,
3133
ThreadRun,
3234
Tool,
3335
ToolApproval,
@@ -36,26 +38,22 @@
3638
from azure.ai.agents.telemetry._ai_agents_instrumentor import _AIAgentsInstrumentorPreview
3739
from azure.ai.agents.telemetry import AIAgentsInstrumentor, _utils
3840
from azure.core.settings import settings
39-
from memory_trace_exporter import MemoryTraceExporter
4041
from gen_ai_trace_verifier import GenAiTraceVerifier
41-
from opentelemetry import trace
42-
from opentelemetry.sdk.trace import TracerProvider
43-
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
4442
from azure.ai.agents import AgentsClient
4543

4644
from devtools_testutils import (
4745
recorded_by_proxy,
4846
)
4947

5048
from test_agents_client_base import agentClientPreparer
51-
from test_ai_instrumentor_base import TestAiAgentsInstrumentorBase
49+
from test_ai_instrumentor_base import TestAiAgentsInstrumentorBase, MessageCreationMode, CONTENT_TRACING_ENV_VARIABLE
5250

53-
CONTENT_TRACING_ENV_VARIABLE = "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"
5451
settings.tracing_implementation = "OpenTelemetry"
5552
_utils._span_impl_type = settings.tracing_implementation()
5653

5754

5855
class TestAiAgentsInstrumentor(TestAiAgentsInstrumentorBase):
56+
5957
"""Tests for AI agents instrumentor."""
6058

6159
@pytest.fixture(scope="function")
@@ -72,19 +70,6 @@ def instrument_without_content(self):
7270
yield
7371
self.cleanup()
7472

75-
def setup_telemetry(self):
76-
trace._TRACER_PROVIDER = TracerProvider()
77-
self.exporter = MemoryTraceExporter()
78-
span_processor = SimpleSpanProcessor(self.exporter)
79-
trace.get_tracer_provider().add_span_processor(span_processor)
80-
AIAgentsInstrumentor().instrument()
81-
82-
def cleanup(self):
83-
self.exporter.shutdown()
84-
AIAgentsInstrumentor().uninstrument()
85-
trace._TRACER_PROVIDER = None
86-
os.environ.pop(CONTENT_TRACING_ENV_VARIABLE, None)
87-
8873
# helper function: create client and using environment variables
8974
def create_client(self, **kwargs):
9075
# fetch environment variables
@@ -227,13 +212,38 @@ def set_env_var(var_name, value):
227212
@agentClientPreparer()
228213
@recorded_by_proxy
229214
def test_agent_chat_with_tracing_content_recording_enabled(self, **kwargs):
215+
# Note: The proper way to invoke the same test over and over again with different parameter values is to use @pytest.mark.parametrize. However,
216+
# this does not work together with @recorded_by_proxy. So we call the helper function 4 times instead in a single recorded test.
217+
self._agent_chat_with_tracing_content_recording_enabled(message_creation_mode=MessageCreationMode.MESSAGE_CREATE_STR, **kwargs)
218+
self._agent_chat_with_tracing_content_recording_enabled(message_creation_mode=MessageCreationMode.MESSAGE_CREATE_INPUT_TEXT_BLOCK, **kwargs)
219+
self._agent_chat_with_tracing_content_recording_enabled(message_creation_mode=MessageCreationMode.THREAD_CREATE_STR, **kwargs)
220+
self._agent_chat_with_tracing_content_recording_enabled(message_creation_mode=MessageCreationMode.THREAD_CREATE_INPUT_TEXT_BLOCK, **kwargs)
221+
222+
def _agent_chat_with_tracing_content_recording_enabled(self, message_creation_mode: MessageCreationMode, **kwargs):
223+
self.cleanup()
224+
os.environ.update({CONTENT_TRACING_ENV_VARIABLE: "True"})
225+
self.setup_telemetry()
230226
assert True == AIAgentsInstrumentor().is_content_recording_enabled()
231227
assert True == AIAgentsInstrumentor().is_instrumented()
232228

233229
client = self.create_client(**kwargs)
234230
agent = client.create_agent(model="gpt-4o-mini", name="my-agent", instructions="You are helpful agent")
235-
thread = client.threads.create()
236-
client.messages.create(thread_id=thread.id, role="user", content="Hello, tell me a joke")
231+
user_content = "Hello, tell me a joke"
232+
233+
# Test 4 different patterns of thread & message creation
234+
if message_creation_mode == MessageCreationMode.MESSAGE_CREATE_STR:
235+
thread = client.threads.create()
236+
client.messages.create(thread_id=thread.id, role="user", content=user_content)
237+
elif message_creation_mode == MessageCreationMode.MESSAGE_CREATE_INPUT_TEXT_BLOCK:
238+
thread = client.threads.create()
239+
client.messages.create(thread_id=thread.id, role="user", content=[MessageInputTextBlock(text=user_content)])
240+
elif message_creation_mode == MessageCreationMode.THREAD_CREATE_STR:
241+
thread = client.threads.create(messages=[ThreadMessageOptions(role="user", content=user_content)])
242+
elif message_creation_mode == MessageCreationMode.THREAD_CREATE_INPUT_TEXT_BLOCK:
243+
thread = client.threads.create(messages=[ThreadMessageOptions(role="user", content=[MessageInputTextBlock(text=user_content)])])
244+
else:
245+
assert False, f"Unknown message creation mode: {message_creation_mode}"
246+
237247
run = client.runs.create(thread_id=thread.id, agent_id=agent.id)
238248

239249
while run.status in ["queued", "in_progress", "requires_action"]:
@@ -250,6 +260,7 @@ def test_agent_chat_with_tracing_content_recording_enabled(self, **kwargs):
250260
assert len(messages) > 1
251261
client.close()
252262

263+
# ------------------------- Validate "create_agent" span ---------------------------------
253264
self.exporter.force_flush()
254265
spans = self.exporter.get_spans_by_name("create_agent my-agent")
255266
assert len(spans) == 1
@@ -277,6 +288,7 @@ def test_agent_chat_with_tracing_content_recording_enabled(self, **kwargs):
277288
events_match = GenAiTraceVerifier().check_span_events(span, expected_events)
278289
assert events_match == True
279290

291+
# ------------------------- Validate "create_thread" span ---------------------------------
280292
spans = self.exporter.get_spans_by_name("create_thread")
281293
assert len(spans) == 1
282294
span = spans[0]
@@ -289,32 +301,48 @@ def test_agent_chat_with_tracing_content_recording_enabled(self, **kwargs):
289301
attributes_match = GenAiTraceVerifier().check_span_attributes(span, expected_attributes)
290302
assert attributes_match == True
291303

292-
spans = self.exporter.get_spans_by_name("create_message")
293-
assert len(spans) == 1
294-
span = spans[0]
295-
expected_attributes = [
296-
("gen_ai.system", "az.ai.agents"),
297-
("gen_ai.operation.name", "create_message"),
298-
("server.address", ""),
299-
("gen_ai.thread.id", ""),
300-
("gen_ai.message.id", ""),
301-
]
302-
attributes_match = GenAiTraceVerifier().check_span_attributes(span, expected_attributes)
303-
assert attributes_match == True
304-
305-
expected_events = [
306-
{
307-
"name": "gen_ai.user.message",
308-
"attributes": {
309-
"gen_ai.system": "az.ai.agents",
310-
"gen_ai.thread.id": "*",
311-
"gen_ai.event.content": '{"content": "Hello, tell me a joke", "role": "user"}',
312-
},
313-
}
314-
]
315-
events_match = GenAiTraceVerifier().check_span_events(span, expected_events)
316-
assert events_match == True
317-
304+
if message_creation_mode in (MessageCreationMode.THREAD_CREATE_STR, MessageCreationMode.THREAD_CREATE_INPUT_TEXT_BLOCK):
305+
expected_events = [
306+
{
307+
"name": "gen_ai.user.message",
308+
"attributes": {
309+
"gen_ai.system": "az.ai.agents",
310+
"gen_ai.event.content": '{"content": "Hello, tell me a joke", "role": "user"}',
311+
},
312+
}
313+
]
314+
events_match = GenAiTraceVerifier().check_span_events(span, expected_events)
315+
assert events_match == True
316+
317+
# ------------------------- Validate "create_message" span ---------------------------------
318+
if message_creation_mode in (MessageCreationMode.MESSAGE_CREATE_STR, MessageCreationMode.MESSAGE_CREATE_INPUT_TEXT_BLOCK):
319+
spans = self.exporter.get_spans_by_name("create_message")
320+
assert len(spans) == 1
321+
span = spans[0]
322+
expected_attributes = [
323+
("gen_ai.system", "az.ai.agents"),
324+
("gen_ai.operation.name", "create_message"),
325+
("server.address", ""),
326+
("gen_ai.thread.id", ""),
327+
("gen_ai.message.id", ""),
328+
]
329+
attributes_match = GenAiTraceVerifier().check_span_attributes(span, expected_attributes)
330+
assert attributes_match == True
331+
332+
expected_events = [
333+
{
334+
"name": "gen_ai.user.message",
335+
"attributes": {
336+
"gen_ai.system": "az.ai.agents",
337+
"gen_ai.thread.id": "*",
338+
"gen_ai.event.content": '{"content": "Hello, tell me a joke", "role": "user"}',
339+
},
340+
}
341+
]
342+
events_match = GenAiTraceVerifier().check_span_events(span, expected_events)
343+
assert events_match == True
344+
345+
# ------------------------- Validate "start_thread_run" span ---------------------------------
318346
spans = self.exporter.get_spans_by_name("start_thread_run")
319347
assert len(spans) == 1
320348
span = spans[0]
@@ -332,6 +360,7 @@ def test_agent_chat_with_tracing_content_recording_enabled(self, **kwargs):
332360
attributes_match = GenAiTraceVerifier().check_span_attributes(span, expected_attributes)
333361
assert attributes_match == True
334362

363+
# ------------------------- Validate "get_thread_run" span ---------------------------------
335364
spans = self.exporter.get_spans_by_name("get_thread_run")
336365
assert len(spans) >= 1
337366
span = spans[-1]
@@ -350,6 +379,7 @@ def test_agent_chat_with_tracing_content_recording_enabled(self, **kwargs):
350379
attributes_match = GenAiTraceVerifier().check_span_attributes(span, expected_attributes)
351380
assert attributes_match == True
352381

382+
# ------------------------- Validate "list_messages" span ---------------------------------
353383
spans = self.exporter.get_spans_by_name("list_messages")
354384
assert len(spans) == 2
355385
span = spans[0]

0 commit comments

Comments
 (0)