Skip to content

Commit da8643d

Browse files
authored
Fix MCP serialization for telemetry. Add unit tests. (#42739)
* Fix MCP serialization for telemetry. Add unit tests. * Fix
1 parent 1526205 commit da8643d

File tree

9 files changed

+1079
-636
lines changed

9 files changed

+1079
-636
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
* 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)).
1414
* Fix the issue with `RunStepOpenAPIToolCall` logging [issue](https://github.com/Azure/azure-sdk-for-python/issues/42645).
15+
* Fix the issue with `RunStepMcpToolCall` logging [issue](https://github.com/Azure/azure-sdk-for-python/issues/42689).
1516

1617
### Sample updates
1718

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_ac2e010ef5"
5+
"Tag": "python/ai/azure-ai-agents_28d79451bb"
66
}

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
RunStepDeltaChunk,
3131
RunStepError,
3232
RunStepFunctionToolCall,
33+
RunStepMcpToolCall,
3334
RunStepOpenAPIToolCall,
3435
RunStepToolCallDetails,
3536
RunStepCodeInterpreterToolCall,
@@ -448,6 +449,15 @@ def _process_tool_calls(self, step: RunStep) -> List[Dict[str, Any]]:
448449
"type": t.type,
449450
'function': t.as_dict().get('function', {})
450451
}
452+
elif isinstance(t, RunStepMcpToolCall):
453+
tool_call = {
454+
"id": t.id,
455+
"type": t.type,
456+
"arguments": t.arguments,
457+
"name": t.name,
458+
"output": t.output,
459+
"server_label": t.server_label or ""
460+
}
451461
else:
452462
tool_details = t.as_dict()[t.type]
453463

@@ -2066,7 +2076,10 @@ def on_thread_message(self, message: "ThreadMessage") -> None: # type: ignore[f
20662076
else:
20672077
retval = super().on_thread_message(message) # pylint: disable=assignment-from-none # type: ignore
20682078

2069-
# Message status may be in progress, even if the thread.message.completed event has arrived.
2079+
# TODO: Workaround for issue where message.status may be IN_PROGRESS even after a thread.message.completed event has arrived.
2080+
# See work item 4636616 and 4636299 for details.
2081+
# When the work item is resolved, change this code back to:
2082+
# if message.status in {MessageStatus.COMPLETED, MessageStatus.INCOMPLETE}
20702083
if message.status in {MessageStatus.COMPLETED, MessageStatus.INCOMPLETE} or (message.status == MessageStatus.IN_PROGRESS and message.content):
20712084
self.last_message = message
20722085

@@ -2204,7 +2217,10 @@ async def on_thread_message(self, message: "ThreadMessage") -> None: # type: ig
22042217
else:
22052218
retval = await super().on_thread_message(message) # type: ignore
22062219

2207-
# Message status may be in progress, even if the thread.message.completed event has arrived.
2220+
# TODO: Workaround for issue where message.status may be IN_PROGRESS even after a thread.message.completed event has arrived.
2221+
# See work item 4636616 and 4636299 for details.
2222+
# When the work item is resolved, change this code back to:
2223+
# if message.status in {MessageStatus.COMPLETED, MessageStatus.INCOMPLETE}
22082224
if message.status in {MessageStatus.COMPLETED, MessageStatus.INCOMPLETE} or (message.status == MessageStatus.IN_PROGRESS and message.content):
22092225
self.last_message = message
22102226

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

Lines changed: 34 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -21,51 +21,44 @@ def check_span_attributes(self, span, attributes):
2121
for attribute_name in span.attributes.keys():
2222
# Check if the attribute name exists in the input attributes
2323
if attribute_name not in attribute_dict:
24-
print("Attribute name " + attribute_name + " not in attribute dictionary")
25-
return False
24+
raise AssertionError("Attribute name " + attribute_name + " not in attribute dictionary")
2625

2726
attribute_value = attribute_dict[attribute_name]
2827
if isinstance(attribute_value, list):
2928
# Check if the attribute value in the span matches the provided list
3029
if span.attributes[attribute_name] != attribute_value:
31-
print(
30+
raise AssertionError(
3231
"Attribute value list "
3332
+ str(span.attributes[attribute_name])
3433
+ " does not match with "
3534
+ str(attribute_value)
3635
)
37-
return False
3836
elif isinstance(attribute_value, tuple):
3937
# Check if the attribute value in the span matches the provided list
4038
if span.attributes[attribute_name] != attribute_value:
41-
print(
39+
raise AssertionError(
4240
"Attribute value tuple "
4341
+ str(span.attributes[attribute_name])
4442
+ " does not match with "
4543
+ str(attribute_value)
4644
)
47-
return False
4845
else:
4946
# Check if the attribute value matches the provided value
5047
if attribute_value == "+":
5148
if not isinstance(span.attributes[attribute_name], numbers.Number):
52-
print("Attribute value " + str(span.attributes[attribute_name]) + " is not a number")
53-
return False
49+
raise AssertionError("Attribute value " + str(span.attributes[attribute_name]) + " is not a number")
5450
if span.attributes[attribute_name] < 0:
55-
print("Attribute value " + str(span.attributes[attribute_name]) + " is negative")
56-
return False
51+
raise AssertionError("Attribute value " + str(span.attributes[attribute_name]) + " is negative")
5752
elif attribute_value != "" and span.attributes[attribute_name] != attribute_value:
58-
print(
53+
raise AssertionError(
5954
"Attribute value "
6055
+ str(span.attributes[attribute_name])
6156
+ " does not match with "
6257
+ str(attribute_value)
6358
)
64-
return False
6559
# Check if the attribute value in the span is not empty when the provided value is ""
6660
elif attribute_value == "" and not span.attributes[attribute_name]:
67-
print("Expected non-empty attribute value")
68-
return False
61+
raise AssertionError("Expected non-empty attribute value")
6962

7063
return True
7164

@@ -76,63 +69,53 @@ def check_decorator_span_attributes(self, span: Span, attributes: List[tuple]) -
7669
# Ensure all required attributes are present in the span
7770
for attribute_name in attribute_dict.keys():
7871
if attribute_name not in span.attributes:
79-
print("Required attribute name " + attribute_name + " not found in span attributes")
80-
return False
72+
raise AssertionError("Required attribute name " + attribute_name + " not found in span attributes")
8173

8274
for attribute_name in span.attributes.keys():
8375
# Check if the attribute name exists in the input attributes
8476
if attribute_name not in attribute_dict:
85-
print("Attribute name " + attribute_name + " not in attribute dictionary")
86-
return False
77+
raise AssertionError("Attribute name " + attribute_name + " not in attribute dictionary")
8778

8879
attribute_value = attribute_dict[attribute_name]
8980
span_value = span.attributes[attribute_name]
9081

9182
if isinstance(attribute_value, (list, tuple)):
9283
# Convert both to lists for comparison
9384
if list(span_value) != list(attribute_value):
94-
print(
85+
raise AssertionError(
9586
"Attribute value list/tuple " + str(span_value) + " does not match with " + str(attribute_value)
9687
)
97-
return False
9888
elif isinstance(attribute_value, dict):
9989
# Check if both are dictionaries and compare them
10090
if not isinstance(span_value, dict) or span_value != attribute_value:
101-
print("Attribute value dict " + str(span_value) + " does not match with " + str(attribute_value))
102-
return False
91+
raise AssertionError("Attribute value dict " + str(span_value) + " does not match with " + str(attribute_value))
10392
else:
10493
# Check if the attribute value matches the provided value
10594
if attribute_value == "+":
10695
if not isinstance(span_value, numbers.Number):
107-
print("Attribute value " + str(span_value) + " is not a number")
108-
return False
96+
raise AssertionError("Attribute value " + str(span_value) + " is not a number")
10997
if span_value < 0:
110-
print("Attribute value " + str(span_value) + " is negative")
111-
return False
98+
raise AssertionError("Attribute value " + str(span_value) + " is negative")
11299
elif attribute_value != "" and span_value != attribute_value:
113-
print("Attribute value " + str(span_value) + " does not match with " + str(attribute_value))
114-
return False
100+
raise AssertionError("Attribute value " + str(span_value) + " does not match with " + str(attribute_value))
115101
# Check if the attribute value in the span is not empty when the provided value is ""
116102
elif attribute_value == "" and not span_value:
117-
print("Expected non-empty attribute value")
118-
return False
103+
raise AssertionError("Expected non-empty attribute value")
119104

120105
return True
121106

122107
def is_valid_json(self, my_string):
123108
try:
124-
json_object = json.loads(my_string)
109+
json.loads(my_string)
125110
except ValueError as e1:
126111
return False
127112
except TypeError as e2:
128113
return False
129114
return True
130115

131116
def check_json_string(self, expected_json, actual_json):
132-
if self.is_valid_json(expected_json) and self.is_valid_json(actual_json):
133-
return self.check_event_attributes(json.loads(expected_json), json.loads(actual_json))
134-
else:
135-
return False
117+
assert self.is_valid_json(expected_json) and self.is_valid_json(actual_json)
118+
return self.check_event_attributes(json.loads(expected_json), json.loads(actual_json))
136119

137120
def check_event_attributes(self, expected_dict, actual_dict):
138121
if set(expected_dict.keys()) != set(actual_dict.keys()):
@@ -144,56 +127,39 @@ def check_event_attributes(self, expected_dict, actual_dict):
144127
actual_val = json.dumps(actual_dict)
145128
else:
146129
actual_val = actual_dict
147-
print("check_event_attributes: keys do not match: " + expected_val + "!=" + actual_val)
148-
return False
130+
raise AssertionError("check_event_attributes: keys do not match: " + expected_val + "!=" + actual_val)
149131
for key, expected_val in expected_dict.items():
150132
if key not in actual_dict:
151-
print("check_event_attributes: key not found")
152-
return False
133+
raise AssertionError(f"check_event_attributes: key {key} not found in actuals")
153134
actual_val = actual_dict[key]
154135

155136
if self.is_valid_json(expected_val):
156137
if not self.is_valid_json(actual_val):
157-
print("check_event_attributes: actual_val is not valid json")
158-
return False
159-
if not self.check_json_string(expected_val, actual_val):
160-
print("check_event_attributes: check_json_string failed")
161-
return False
138+
raise AssertionError(f"check_event_attributes: actual_val for {key} is not valid json")
139+
self.check_json_string(expected_val, actual_val)
162140
elif isinstance(expected_val, dict):
163141
if not isinstance(actual_val, dict):
164-
print("check_event_attributes: actual_val is not dict")
165-
return False
166-
if not self.check_event_attributes(expected_val, actual_val):
167-
print("check_event_attributes: check_event_attributes failed")
168-
return False
142+
raise AssertionError(f"check_event_attributes: actual_val for {key} is not dict")
143+
self.check_event_attributes(expected_val, actual_val)
169144
elif isinstance(expected_val, list):
170145
if not isinstance(actual_val, list):
171-
print("check_event_attributes: actual_val is not list")
172-
return False
146+
raise AssertionError(f"check_event_attributes: actual_val for {key} is not list")
173147
if len(expected_val) != len(actual_val):
174-
print("check_event_attributes: list lengths do not match")
175-
return False
148+
raise AssertionError(f"check_event_attributes: list lengths do not match for key {key}: expected {len(expected_val)}, actual {len(actual_val)}")
176149
for expected_list, actual_list in zip(expected_val, actual_val):
177-
if not self.check_event_attributes(expected_list, actual_list):
178-
print("check_event_attributes: check_event_attributes for list failed")
179-
return False
150+
self.check_event_attributes(expected_list, actual_list)
180151
elif isinstance(expected_val, str) and expected_val == "*":
181152
if actual_val == "":
182-
print("check_event_attributes: actual_val is empty")
183-
return False
153+
raise AssertionError(f"check_event_attributes: actual_val for {key} is empty")
184154
elif isinstance(expected_val, str) and expected_val == "+":
185-
if not isinstance(actual_val, numbers.Number):
186-
return False
187-
if actual_val < 0:
188-
return False
155+
assert isinstance(actual_val, numbers.Number), f"The {key} is not a number."
156+
assert actual_val > 0, f"The {key} is <0 {actual_val}"
189157
elif expected_val != actual_val:
190158
if isinstance(expected_val, dict):
191159
expected_val = json.dumps(expected_val)
192160
if isinstance(actual_val, dict):
193161
actual_val = json.dumps(actual_val)
194-
print("check_event_attributes: values do not match: " + expected_val + "!=" + actual_val)
195-
return False
196-
return True
162+
raise AssertionError(f"check_event_attributes: values do not match for key {key}: {expected_val} != {actual_val}")
197163

198164
def check_span_events(self, span, expected_events):
199165
print("Checking span: " + span.name)
@@ -202,17 +168,13 @@ def check_span_events(self, span, expected_events):
202168
for expected_event in expected_events:
203169
for actual_event in span_events:
204170
if expected_event["name"] == actual_event.name:
205-
if not self.check_event_attributes(expected_event["attributes"], actual_event.attributes._dict):
206-
print("Event attributes do not match")
207-
return False
171+
self.check_event_attributes(expected_event["attributes"], actual_event.attributes._dict)
208172
span_events.remove(actual_event) # Remove the matched event from the span_events
209173
break
210174
else:
211-
print("check_span_events: event not found")
212-
return False # If no match found for an expected event
175+
raise AssertionError("check_span_events: event not found")
213176

214177
if len(span_events) > 0: # If there are any additional events in the span_events
215-
print("check_span_events: unexpected event found")
216-
return False
178+
raise AssertionError("check_span_events: unexpected event found")
217179

218180
return True

0 commit comments

Comments
 (0)