Skip to content

Commit 302c3e2

Browse files
add support for execute tool input and output types
1 parent 064f7dd commit 302c3e2

10 files changed

Lines changed: 221 additions & 53 deletions

File tree

libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@
4747
TextPart,
4848
ToolCallRequestPart,
4949
ToolCallResponsePart,
50+
ToolInputMessage,
51+
ToolInputMessages,
52+
ToolOutputMessage,
53+
ToolOutputMessages,
5054
UriPart,
5155
)
5256
from .models.response import Response
@@ -122,6 +126,10 @@
122126
"OutputMessages",
123127
"InputMessagesParam",
124128
"OutputMessagesParam",
129+
"ToolInputMessage",
130+
"ToolOutputMessage",
131+
"ToolInputMessages",
132+
"ToolOutputMessages",
125133
# Utility functions
126134
"extract_context_from_headers",
127135
"get_traceparent",

libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/execute_tool_scope.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
GEN_AI_CONVERSATION_ID_KEY,
1313
GEN_AI_TOOL_ARGS_KEY,
1414
GEN_AI_TOOL_CALL_ID_KEY,
15+
GEN_AI_TOOL_CALL_RESULT_KEY,
1516
GEN_AI_TOOL_DESCRIPTION_KEY,
1617
GEN_AI_TOOL_NAME_KEY,
1718
GEN_AI_TOOL_TYPE_KEY,
@@ -21,6 +22,8 @@
2122
USER_ID_KEY,
2223
USER_NAME_KEY,
2324
)
25+
from .message_utils import serialize_messages
26+
from .models.messages import ToolInputMessages, ToolOutputMessages
2427
from .models.user_details import UserDetails
2528
from .opentelemetry_scope import OpenTelemetryScope
2629
from .request import Request
@@ -108,7 +111,8 @@ def __init__(
108111
endpoint = details.endpoint
109112

110113
self.set_tag_maybe(GEN_AI_TOOL_NAME_KEY, tool_name)
111-
self.set_tag_maybe(GEN_AI_TOOL_ARGS_KEY, arguments)
114+
if arguments is not None:
115+
self.record_tool_input(arguments)
112116
self.set_tag_maybe(GEN_AI_TOOL_TYPE_KEY, tool_type)
113117
self.set_tag_maybe(GEN_AI_TOOL_CALL_ID_KEY, tool_call_id)
114118
self.set_tag_maybe(GEN_AI_TOOL_DESCRIPTION_KEY, description)
@@ -134,13 +138,18 @@ def __init__(
134138
validate_and_normalize_ip(user_details.user_client_ip),
135139
)
136140

137-
def record_response(self, response: str) -> None:
138-
"""Records response information for telemetry tracking.
141+
def record_tool_input(self, messages: ToolInputMessages) -> None:
142+
"""Record the tool input for telemetry tracking.
139143
140-
Note: This method is intentionally a no-op as GEN_AI_EVENT_CONTENT was removed.
141-
The method is kept for interface compatibility.
144+
Args:
145+
messages: A ToolInputMessages wrapper containing tool call requests
146+
"""
147+
self.set_tag_maybe(GEN_AI_TOOL_ARGS_KEY, serialize_messages(messages))
148+
149+
def record_tool_output(self, messages: ToolOutputMessages) -> None:
150+
"""Record the tool output for telemetry tracking.
142151
143152
Args:
144-
response: The response to record
153+
messages: A ToolOutputMessages wrapper containing tool call responses
145154
"""
146-
pass
155+
self.set_tag_maybe(GEN_AI_TOOL_CALL_RESULT_KEY, serialize_messages(messages))

libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/message_utils.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import json
1313
import logging
1414
from dataclasses import asdict
15+
from enum import Enum
1516
from typing import Union
1617

1718
from .models.messages import (
@@ -24,6 +25,8 @@
2425
OutputMessages,
2526
OutputMessagesParam,
2627
TextPart,
28+
ToolInputMessages,
29+
ToolOutputMessages,
2730
)
2831

2932
logger = logging.getLogger(__name__)
@@ -37,10 +40,10 @@ def is_string_list(
3740

3841

3942
def is_wrapped_messages(
40-
param: Union[InputMessagesParam, OutputMessagesParam],
43+
param: Union[InputMessagesParam, OutputMessagesParam, ToolInputMessages, ToolOutputMessages],
4144
) -> bool:
42-
"""Return ``True`` when *param* is a versioned wrapper (``InputMessages`` or ``OutputMessages``)."""
43-
return isinstance(param, (InputMessages, OutputMessages))
45+
"""Return ``True`` when *param* is a versioned wrapper."""
46+
return isinstance(param, (InputMessages, OutputMessages, ToolInputMessages, ToolOutputMessages))
4447

4548

4649
# ---------------------------------------------------------------------------
@@ -51,15 +54,15 @@ def is_wrapped_messages(
5154
def to_input_messages(messages: list[str]) -> list[ChatMessage]:
5255
"""Convert plain input strings into OTEL ``ChatMessage`` objects."""
5356
return [
54-
ChatMessage(role=MessageRole.USER.value, parts=[TextPart(content=content)])
57+
ChatMessage(role=MessageRole.USER, parts=[TextPart(content=content)])
5558
for content in messages
5659
]
5760

5861

5962
def to_output_messages(messages: list[str]) -> list[OutputMessage]:
6063
"""Convert plain output strings into OTEL ``OutputMessage`` objects."""
6164
return [
62-
OutputMessage(role=MessageRole.ASSISTANT.value, parts=[TextPart(content=content)])
65+
OutputMessage(role=MessageRole.ASSISTANT, parts=[TextPart(content=content)])
6366
for content in messages
6467
]
6568

@@ -97,11 +100,16 @@ def normalize_output_messages(param: OutputMessagesParam) -> OutputMessages:
97100

98101

99102
def _message_dict_factory(items: list[tuple[str, object]]) -> dict[str, object]:
100-
"""Custom dict factory for ``dataclasses.asdict`` that drops ``None`` values."""
101-
return {k: v for k, v in items if v is not None}
103+
"""Custom dict factory for ``dataclasses.asdict``.
104+
105+
Drops ``None`` values and converts enum members to their string value.
106+
"""
107+
return {k: (v.value if isinstance(v, Enum) else v) for k, v in items if v is not None}
102108

103109

104-
def serialize_messages(wrapper: Union[InputMessages, OutputMessages]) -> str:
110+
def serialize_messages(
111+
wrapper: Union[InputMessages, OutputMessages, ToolInputMessages, ToolOutputMessages],
112+
) -> str:
105113
"""Serialize a versioned message wrapper to JSON.
106114
107115
The output is the full wrapper object:

libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/models/messages.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ class GenericPart:
168168
class ChatMessage:
169169
"""An input message sent to a model (OTEL gen-ai semantic conventions)."""
170170

171-
role: str
171+
role: MessageRole
172172
parts: list[MessagePart] = field(default_factory=list)
173173
name: str | None = None
174174

@@ -180,6 +180,24 @@ class OutputMessage(ChatMessage):
180180
finish_reason: str | None = None
181181

182182

183+
@dataclass
184+
class ToolInputMessage:
185+
"""A tool input message representing a tool call request."""
186+
187+
role: MessageRole
188+
parts: list[ToolCallRequestPart] = field(default_factory=list)
189+
name: str | None = None
190+
191+
192+
@dataclass
193+
class ToolOutputMessage:
194+
"""A tool output message representing a tool call response."""
195+
196+
role: MessageRole
197+
parts: list[ToolCallResponsePart] = field(default_factory=list)
198+
name: str | None = None
199+
200+
183201
# ---------------------------------------------------------------------------
184202
# Versioned wrappers
185203
# ---------------------------------------------------------------------------
@@ -204,6 +222,22 @@ class OutputMessages:
204222
version: str = field(default=A365_MESSAGE_SCHEMA_VERSION, init=False)
205223

206224

225+
@dataclass
226+
class ToolInputMessages:
227+
"""Versioned wrapper for tool input messages."""
228+
229+
messages: list[ToolInputMessage] = field(default_factory=list)
230+
version: str = field(default=A365_MESSAGE_SCHEMA_VERSION, init=False)
231+
232+
233+
@dataclass
234+
class ToolOutputMessages:
235+
"""Versioned wrapper for tool output messages."""
236+
237+
messages: list[ToolOutputMessage] = field(default_factory=list)
238+
version: str = field(default=A365_MESSAGE_SCHEMA_VERSION, init=False)
239+
240+
207241
# ---------------------------------------------------------------------------
208242
# Parameter type aliases (backward-compatible union types)
209243
# ---------------------------------------------------------------------------

libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/tool_call_details.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,21 @@
33

44
# Data class for tool call details.
55

6+
from __future__ import annotations
7+
68
from dataclasses import dataclass
7-
from urllib.parse import ParseResult
9+
10+
from .models.messages import ToolInputMessages
11+
from .models.service_endpoint import ServiceEndpoint
812

913

1014
@dataclass
1115
class ToolCallDetails:
1216
"""Details of a tool call made by an agent in the system."""
1317

1418
tool_name: str
15-
arguments: str | None = None
19+
arguments: ToolInputMessages | None = None
1620
tool_call_id: str | None = None
1721
description: str | None = None
1822
tool_type: str | None = None
19-
endpoint: ParseResult | None = None
23+
endpoint: ServiceEndpoint | None = None

tests/observability/core/test_custom_start_end_time.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@
2121
get_tracer_provider,
2222
)
2323
from microsoft_agents_a365.observability.core.config import _telemetry_manager
24+
from microsoft_agents_a365.observability.core.models.messages import (
25+
MessageRole,
26+
ToolCallRequestPart,
27+
ToolInputMessage,
28+
ToolInputMessages,
29+
)
2430
from microsoft_agents_a365.observability.core.opentelemetry_scope import OpenTelemetryScope
2531
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
2632
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
@@ -46,7 +52,14 @@ def setUpClass(cls):
4652
)
4753
cls.tool_details = ToolCallDetails(
4854
tool_name="test_tool",
49-
arguments='{"arg": "value"}',
55+
arguments=ToolInputMessages(
56+
messages=[
57+
ToolInputMessage(
58+
role=MessageRole.ASSISTANT,
59+
parts=[ToolCallRequestPart(name="test_tool", arguments={"arg": "value"})],
60+
)
61+
]
62+
),
5063
tool_call_id="call-123",
5164
)
5265

tests/observability/core/test_execute_tool_scope.py

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@
2323
CHANNEL_LINK_KEY,
2424
CHANNEL_NAME_KEY,
2525
)
26+
from microsoft_agents_a365.observability.core.models.messages import (
27+
MessageRole,
28+
ToolCallRequestPart,
29+
ToolInputMessage,
30+
ToolInputMessages,
31+
)
2632
from microsoft_agents_a365.observability.core.opentelemetry_scope import OpenTelemetryScope
2733
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
2834
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
@@ -50,7 +56,19 @@ def setUpClass(cls):
5056
)
5157
cls.tool_details = ToolCallDetails(
5258
tool_name="weather_tool",
53-
arguments='{"location": "Seattle", "units": "metric"}',
59+
arguments=ToolInputMessages(
60+
messages=[
61+
ToolInputMessage(
62+
role=MessageRole.ASSISTANT,
63+
parts=[
64+
ToolCallRequestPart(
65+
name="weather_tool",
66+
arguments={"location": "Seattle", "units": "metric"},
67+
)
68+
],
69+
)
70+
]
71+
),
5472
tool_call_id="call-123",
5573
description="Get current weather information for a location",
5674
)
@@ -79,14 +97,22 @@ def tearDown(self):
7997

8098
self.span_exporter.clear()
8199

82-
def test_record_response_method_exists(self):
83-
"""Test that record_response method exists on ExecuteToolScope."""
100+
def test_record_tool_input_method_exists(self):
101+
"""Test that record_tool_input method exists on ExecuteToolScope."""
102+
scope = ExecuteToolScope.start(Request(), self.tool_details, self.agent_details)
103+
104+
if scope is not None:
105+
self.assertTrue(hasattr(scope, "record_tool_input"))
106+
self.assertTrue(callable(scope.record_tool_input))
107+
scope.dispose()
108+
109+
def test_record_tool_output_method_exists(self):
110+
"""Test that record_tool_output method exists on ExecuteToolScope."""
84111
scope = ExecuteToolScope.start(Request(), self.tool_details, self.agent_details)
85112

86113
if scope is not None:
87-
# Test that the method exists
88-
self.assertTrue(hasattr(scope, "record_response"))
89-
self.assertTrue(callable(scope.record_response))
114+
self.assertTrue(hasattr(scope, "record_tool_output"))
115+
self.assertTrue(callable(scope.record_tool_output))
90116
scope.dispose()
91117

92118
def test_request_metadata_set_on_span(self):

0 commit comments

Comments
 (0)