Skip to content

Commit 3c2649b

Browse files
authored
simpler function tool trace (#45083)
* simpler function tool call trace format * test fix * updating test recordings * fix based on review comment] * updating test recordings * updated tests
1 parent fae0494 commit 3c2649b

6 files changed

Lines changed: 992 additions & 246 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
* Tracing: response generation uses gen_ai.input.messages and gen_ai.output.messages attributes directly under the span instead of events.
2929
* Tracing: agent creation uses gen_ai.system.instructions attribute directly under the span instead of an event.
3030
* Tracing: "gen_ai.provider.name" attribute value changed to "microsoft.foundry".
31+
* 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": "..."}
3132

3233
## 2.0.0b3 (2026-01-06)
3334

sdk/ai/azure-ai-projects/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-projects",
5-
"Tag": "python/ai/azure-ai-projects_8d150306ae"
5+
"Tag": "python/ai/azure-ai-projects_6896cca62c"
66
}

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

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
SPAN_NAME_CHAT,
5959
SPAN_NAME_INVOKE_AGENT,
6060
_get_use_message_events,
61+
_get_use_simple_tool_format,
6162
start_span,
6263
RESPONSES_PROVIDER,
6364
)
@@ -171,6 +172,18 @@ def is_binary_data_enabled(self) -> bool:
171172
"""
172173
return self._impl.is_binary_data_enabled()
173174

175+
def is_simple_tool_format_enabled(self) -> bool:
176+
"""This function gets the simple tool format value.
177+
178+
When enabled, function tool calls use a simplified OTEL-compliant format:
179+
tool_call: {"type": "tool_call", "id": "...", "name": "...", "arguments": {...}}
180+
tool_call_response: {"type": "tool_call_response", "id": "...", "result": "..."}
181+
182+
:return: A bool value indicating whether simple tool format is enabled.
183+
:rtype: bool
184+
"""
185+
return _get_use_simple_tool_format()
186+
174187

175188
class _ResponsesInstrumentorPreview: # pylint: disable=too-many-instance-attributes,too-many-statements,too-many-public-methods
176189
"""
@@ -703,9 +716,21 @@ def _add_tool_message_events( # pylint: disable=too-many-branches
703716
output_value = {k: v for k, v in output_value.items() if k != "image_url"}
704717
tool_output["output"] = output_value
705718

706-
# Add to parts array (type "tool_call_output" wraps the tool output)
707-
# Always include type and id, even when content recording is disabled
708-
parts.append({"type": "tool_call_output", "content": tool_output})
719+
# Add to parts array
720+
# Check if simple tool format is enabled for function_call_output types
721+
if _get_use_simple_tool_format() and item_type == "function_call_output":
722+
# Build simplified OTEL-compliant format:
723+
# {"type": "tool_call_response", "id": "...", "result": "..."}
724+
simple_response: Dict[str, Any] = {"type": "tool_call_response"}
725+
if "id" in tool_output:
726+
simple_response["id"] = tool_output["id"]
727+
if _trace_responses_content and "output" in tool_output:
728+
simple_response["result"] = tool_output["output"]
729+
parts.append(simple_response)
730+
else:
731+
# Original nested format (type "tool_call_output" wraps the tool output)
732+
# Always include type and id, even when content recording is disabled
733+
parts.append({"type": "tool_call_output", "content": tool_output})
709734
except Exception: # pylint: disable=broad-exception-caught
710735
# Skip items that can't be processed
711736
logger.debug(
@@ -1044,8 +1069,25 @@ def _emit_tool_call_event(
10441069
conversation_id: Optional[str] = None,
10451070
) -> None:
10461071
"""Helper to emit a single tool call event or attribute."""
1047-
# Wrap tool call in parts array
1048-
parts = [{"type": "tool_call", "content": tool_call}]
1072+
# Check if simple tool format is enabled for function_call types
1073+
if _get_use_simple_tool_format() and tool_call.get("type") == "function_call":
1074+
# Build simplified OTEL-compliant format:
1075+
# {"type": "tool_call", "id": "...", "name": "...", "arguments": {...}}
1076+
simple_tool_call: Dict[str, Any] = {"type": "tool_call"}
1077+
if "id" in tool_call:
1078+
simple_tool_call["id"] = tool_call["id"]
1079+
# Extract name and arguments from nested function object if content recording enabled
1080+
if _trace_responses_content and "function" in tool_call:
1081+
func = tool_call["function"]
1082+
if "name" in func:
1083+
simple_tool_call["name"] = func["name"]
1084+
if "arguments" in func:
1085+
simple_tool_call["arguments"] = func["arguments"]
1086+
parts = [simple_tool_call]
1087+
else:
1088+
# Original nested format
1089+
parts = [{"type": "tool_call", "content": tool_call}]
1090+
10491091
content_array = [{"role": "assistant", "parts": parts}]
10501092

10511093
if _get_use_message_events():
@@ -1069,9 +1111,21 @@ def _emit_tool_output_event(
10691111
conversation_id: Optional[str] = None,
10701112
) -> None:
10711113
"""Helper to emit a single tool output event or attribute."""
1072-
# Wrap tool output in parts array
1073-
# Tool outputs are inputs TO the model (from tool execution), so use role "tool"
1074-
parts = [{"type": "tool_call_output", "content": tool_output}]
1114+
# Check if simple tool format is enabled for function_call_output types
1115+
if _get_use_simple_tool_format() and tool_output.get("type") == "function_call_output":
1116+
# Build simplified OTEL-compliant format:
1117+
# {"type": "tool_call_response", "id": "...", "result": "..."}
1118+
simple_tool_response: Dict[str, Any] = {"type": "tool_call_response"}
1119+
if "id" in tool_output:
1120+
simple_tool_response["id"] = tool_output["id"]
1121+
# Add result only if content recording is enabled
1122+
if _trace_responses_content and "output" in tool_output:
1123+
simple_tool_response["result"] = tool_output["output"]
1124+
parts = [simple_tool_response]
1125+
else:
1126+
# Original nested format
1127+
parts = [{"type": "tool_call_output", "content": tool_output}]
1128+
10751129
content_array = [{"role": "tool", "parts": parts}]
10761130

10771131
if _get_use_message_events():

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,33 @@
121121
# Set to True for event-based, False for attribute-based (default)
122122
_use_message_events = False
123123

124+
# Configuration: Controls whether function tool calls use simplified OTEL-compliant format
125+
# When True (default), function_call and function_call_output use a simpler structure:
126+
# tool_call: {"type": "tool_call", "id": "...", "name": "...", "arguments": {...}}
127+
# tool_call_response: {"type": "tool_call_response", "id": "...", "result": "..."}
128+
# When False, the full nested structure is used
129+
_use_simple_tool_format = True
130+
131+
132+
def _get_use_simple_tool_format() -> bool:
133+
"""Get the current tool format mode (simple vs nested). Internal use only.
134+
135+
:return: True if using simple OTEL-compliant format, False if using nested format
136+
:rtype: bool
137+
"""
138+
return _use_simple_tool_format
139+
140+
141+
def _set_use_simple_tool_format(use_simple: bool) -> None:
142+
"""
143+
Set the tool format mode at runtime. Internal use only.
144+
145+
:param use_simple: True to use simple OTEL-compliant format, False for nested format
146+
:type use_simple: bool
147+
"""
148+
global _use_simple_tool_format # pylint: disable=global-statement
149+
_use_simple_tool_format = use_simple
150+
124151

125152
def _get_use_message_events() -> bool:
126153
"""Get the current message tracing mode (events vs attributes). Internal use only.

0 commit comments

Comments
 (0)