Skip to content

Commit 9b3f2a6

Browse files
committed
feat: refactor ToolCall to ToolCallRequest and enhance type definitions
1 parent b277b72 commit 9b3f2a6

3 files changed

Lines changed: 136 additions & 91 deletions

File tree

  • instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai
  • util/opentelemetry-util-genai

instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai/utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
FinishReason,
5757
MessagePart,
5858
Text,
59-
ToolCall,
59+
ToolCallRequest,
6060
ToolCallResponse,
6161
)
6262
from opentelemetry.util.genai.utils import get_content_capturing_mode
@@ -341,7 +341,7 @@ def convert_content_to_message_parts(
341341
elif "function_call" in part:
342342
part = part.function_call
343343
parts.append(
344-
ToolCall(
344+
ToolCallRequest(
345345
id=f"{part.name}_{idx}",
346346
name=part.name,
347347
arguments=json_format.MessageToDict(

util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -42,22 +42,30 @@ class ContentCapturingMode(Enum):
4242

4343

4444
@dataclass()
45-
class ToolCall:
46-
"""Represents a tool call with dual usage: message part and execution tracking.
45+
class ToolCallRequest:
46+
"""Represents a tool call requested by the model
4747
48-
This type serves two purposes as defined in OpenTelemetry semantic conventions:
48+
This model is specified as part of semconv in `GenAI messages Python models - ToolCallRequestPart
49+
<https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/non-normative/models.ipynb>`__.
50+
"""
51+
52+
arguments: Any
53+
name: str
54+
id: str | None
55+
type: Literal["tool_call"] = "tool_call"
4956

50-
1. Message Part (ToolCallRequestPart):
51-
Represents a tool call requested by the model as part of a message.
52-
Reference: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/non-normative/models.ipynb
5357

54-
2. Tool Execution (execute_tool spans):
55-
Represents the actual execution of a tool call, tracked via spans and metrics.
56-
Reference: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-spans.md#execute-tool-span
58+
@dataclass()
59+
class ToolCall(ToolCallRequest):
60+
"""Represents a tool call for execution tracking with spans and metrics.
61+
62+
This type extends ToolCallRequest with additional fields for tracking tool execution
63+
per the execute_tool span semantic conventions.
64+
65+
Reference: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-spans.md#execute-tool-span
5766
58-
The execution-only fields (tool_type, tool_description, tool_result, error_type)
59-
are used when tracking tool execution via spans, but are typically not present when
60-
this type is used as a message part in InputMessage or OutputMessage.
67+
For simple message parts (tool calls requested by the model), consider using
68+
ToolCallRequest instead to avoid unnecessary execution-tracking fields.
6169
6270
Semantic convention attributes for execute_tool spans:
6371
- gen_ai.operation.name: "execute_tool" (Required)
@@ -70,17 +78,7 @@ class ToolCall:
7078
- error.type: Error type if operation failed (Conditionally Required)
7179
"""
7280

73-
# Fields used for both message part and execution tracking:
74-
# gen_ai.tool.name - Name of the tool
75-
name: str
76-
# gen_ai.tool.call.arguments - Arguments passed to the tool (Opt-In, may contain sensitive data)
77-
arguments: Any = None
78-
# gen_ai.tool.call.id - Unique identifier for the tool call
79-
id: str | None = None
80-
# Message part type identifier
81-
type: Literal["tool_call"] = "tool_call"
82-
83-
# Execution-only fields (used for execute_tool spans, not typically in messages):
81+
# Execution-only fields (used for execute_tool spans):
8482
# gen_ai.tool.type - Tool type: "function", "extension", or "datastore"
8583
tool_type: str | None = None
8684
# gen_ai.tool.description - Description of what the tool does
@@ -194,7 +192,15 @@ class GenericToolDefinition:
194192
ToolDefinition = Union[FunctionToolDefinition, GenericToolDefinition]
195193

196194
MessagePart = Union[
197-
Text, ToolCall, ToolCallResponse, Blob, File, Uri, Reasoning, Any
195+
Text,
196+
ToolCallRequest,
197+
ToolCall,
198+
ToolCallResponse,
199+
Blob,
200+
File,
201+
Uri,
202+
Reasoning,
203+
Any,
198204
]
199205

200206

util/opentelemetry-util-genai/tests/test_toolcall.py

Lines changed: 104 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -12,114 +12,153 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
"""Tests for Enhanced ToolCall Type Definition"""
15+
"""Tests for ToolCallRequest and ToolCall types"""
1616

1717
import pytest
1818

1919
from opentelemetry.util.genai.types import (
2020
InputMessage,
2121
OutputMessage,
2222
ToolCall,
23+
ToolCallRequest,
2324
)
2425

2526

26-
def test_toolcall_backward_compatibility():
27-
"""Test backward compatibility as message part"""
28-
tc = ToolCall(
27+
def test_toolcallrequest_basic():
28+
"""Test basic ToolCallRequest instantiation"""
29+
tcr = ToolCallRequest(arguments=None, name="get_weather", id=None)
30+
assert tcr.name == "get_weather"
31+
assert tcr.type == "tool_call"
32+
assert tcr.arguments is None
33+
assert tcr.id is None
34+
35+
36+
def test_toolcallrequest_with_all_fields():
37+
"""Test ToolCallRequest with all fields"""
38+
tcr = ToolCallRequest(
2939
name="get_weather",
3040
arguments={"location": "Paris"},
3141
id="call_123",
3242
)
33-
assert tc.name == "get_weather"
34-
assert tc.arguments == {"location": "Paris"}
35-
assert tc.id == "call_123"
36-
assert tc.type == "tool_call"
43+
assert tcr.name == "get_weather"
44+
assert tcr.arguments == {"location": "Paris"}
45+
assert tcr.id == "call_123"
46+
assert tcr.type == "tool_call"
3747

3848

39-
def test_toolcall_in_message():
40-
"""Test ToolCall works as message part in InputMessage"""
41-
tc = ToolCall(name="get_weather", arguments={"location": "Paris"})
42-
msg = InputMessage(role="user", parts=[tc])
49+
def test_toolcallrequest_in_message():
50+
"""Test ToolCallRequest works as message part"""
51+
tcr = ToolCallRequest(
52+
arguments={"location": "Paris"}, name="get_weather", id=None
53+
)
54+
msg = InputMessage(role="user", parts=[tcr])
4355
assert len(msg.parts) == 1
44-
assert msg.parts[0] == tc
56+
assert msg.parts[0] == tcr
57+
58+
59+
def test_toolcall_inherits_from_toolcallrequest():
60+
"""Test that ToolCall inherits from ToolCallRequest"""
61+
tc = ToolCall(arguments=None, name="get_weather", id=None)
62+
assert isinstance(tc, ToolCallRequest)
63+
assert isinstance(tc, ToolCall)
64+
4565

66+
def test_toolcall_has_execution_fields():
67+
"""Test ToolCall has execution-only fields"""
68+
tc = ToolCall(arguments=None, name="get_weather", id=None)
69+
assert hasattr(tc, "tool_type")
70+
assert hasattr(tc, "tool_description")
71+
assert hasattr(tc, "tool_result")
72+
assert hasattr(tc, "error_type")
4673

47-
def test_toolcall_full_lifecycle():
48-
"""Test complete tool call lifecycle with all fields"""
49-
# Start with tool call request
74+
75+
def test_toolcall_execution_fields_default_none():
76+
"""Test ToolCall execution fields default to None"""
77+
tc = ToolCall(arguments=None, name="get_weather", id=None)
78+
assert tc.tool_type is None
79+
assert tc.tool_description is None
80+
assert tc.tool_result is None
81+
assert tc.error_type is None
82+
83+
84+
def test_toolcall_with_execution_fields():
85+
"""Test ToolCall with execution fields set"""
5086
tc = ToolCall(
5187
name="get_weather",
52-
arguments={"location": "Paris", "units": "metric"},
53-
id="call_abc123",
88+
arguments={"location": "Paris"},
89+
id="call_123",
5490
tool_type="function",
55-
tool_description="Retrieves current weather for a location",
91+
tool_description="Get current weather",
92+
tool_result={"temp": 20, "condition": "sunny"},
5693
)
57-
58-
# Simulate successful execution - set result
59-
tc.tool_result = {"temperature": 15, "condition": "cloudy"}
60-
6194
assert tc.name == "get_weather"
6295
assert tc.tool_type == "function"
63-
assert tc.tool_result is not None
64-
assert tc.error_type is None
96+
assert tc.tool_description == "Get current weather"
97+
assert tc.tool_result == {"temp": 20, "condition": "sunny"}
6598

66-
# Simulate failed execution - set error
67-
tc_failed = ToolCall(
68-
name="get_weather",
99+
100+
def test_toolcall_with_error():
101+
"""Test ToolCall with error_type set"""
102+
tc = ToolCall(
69103
arguments={"location": "Invalid"},
70-
id="call_xyz789",
71-
tool_type="function",
104+
name="get_weather",
105+
id=None,
106+
error_type="InvalidLocationError",
72107
)
73-
tc_failed.error_type = "InvalidLocationError"
74-
75-
assert tc_failed.error_type == "InvalidLocationError"
76-
assert tc_failed.tool_result is None
108+
assert tc.error_type == "InvalidLocationError"
109+
assert tc.tool_result is None
77110

78111

79-
def test_toolcall_with_output_message():
80-
"""Test ToolCall in OutputMessage (backward compatibility)"""
112+
def test_toolcall_backward_compatibility():
113+
"""Test ToolCall still works as message part (backward compatibility)"""
81114
tc = ToolCall(
82115
name="get_weather",
83116
arguments={"location": "Paris"},
84117
id="call_123",
85118
)
86-
msg = OutputMessage(
119+
# Should work in messages
120+
msg = InputMessage(role="user", parts=[tc])
121+
assert len(msg.parts) == 1
122+
123+
# Should work in output messages
124+
out_msg = OutputMessage(
87125
role="assistant", parts=[tc], finish_reason="tool_calls"
88126
)
127+
assert len(out_msg.parts) == 1
89128

90-
assert len(msg.parts) == 1
91-
assert msg.parts[0].name == "get_weather"
92-
assert msg.finish_reason == "tool_calls"
93129

130+
def test_toolcallrequest_no_execution_fields():
131+
"""Test that ToolCallRequest doesn't have execution fields"""
132+
tcr = ToolCallRequest(arguments=None, name="get_weather", id=None)
133+
# ToolCallRequest should only have message part fields
134+
assert not hasattr(tcr, "tool_type")
135+
assert not hasattr(tcr, "tool_description")
136+
assert not hasattr(tcr, "tool_result")
137+
assert not hasattr(tcr, "error_type")
94138

95-
def test_toolcall_field_values():
96-
"""Test that ToolCall fields can be set and retrieved correctly"""
139+
140+
def test_mixed_types_in_message():
141+
"""Test using both ToolCallRequest and ToolCall in messages"""
142+
tcr = ToolCallRequest(arguments=None, name="simple_tool", id=None)
97143
tc = ToolCall(
98-
name="get_weather",
99-
id="call_123",
100-
tool_type="function",
101-
tool_description="Weather tool",
102-
arguments={"location": "Paris"},
103-
tool_result={"temp": 20},
144+
arguments=None, name="complex_tool", id=None, tool_type="function"
104145
)
105146

106-
# Verify all field values are set correctly
107-
assert tc.name == "get_weather"
108-
assert tc.id == "call_123"
109-
assert tc.tool_type == "function"
110-
assert tc.tool_description == "Weather tool"
111-
assert tc.arguments == {"location": "Paris"}
112-
assert tc.tool_result == {"temp": 20}
113-
assert tc.error_type is None
114-
115-
# Verify these fields map to semantic convention attributes:
116-
# - name -> gen_ai.tool.name
117-
# - id -> gen_ai.tool.call.id
118-
# - tool_type -> gen_ai.tool.type
119-
# - tool_description -> gen_ai.tool.description
120-
# - arguments -> gen_ai.tool.call.arguments (Opt-In)
121-
# - tool_result -> gen_ai.tool.call.result (Opt-In)
122-
# - error_type -> error.type
147+
msg = InputMessage(role="user", parts=[tcr, tc])
148+
assert len(msg.parts) == 2
149+
assert isinstance(msg.parts[0], ToolCallRequest)
150+
assert isinstance(msg.parts[1], ToolCall)
151+
# ToolCall is also a ToolCallRequest
152+
assert isinstance(msg.parts[1], ToolCallRequest)
153+
154+
155+
def test_toolcall_tool_type_values():
156+
"""Test valid tool_type values"""
157+
for tool_type in ["function", "extension", "datastore"]:
158+
tc = ToolCall(
159+
arguments=None, name="test", id=None, tool_type=tool_type
160+
)
161+
assert tc.tool_type == tool_type
123162

124163

125164
if __name__ == "__main__":

0 commit comments

Comments
 (0)