Skip to content

Commit af0b090

Browse files
Align message serialization to OTel spec: remove version wrapper
Remove the version/messages wrapper envelope from serialized message payloads. Messages now serialize as a plain JSON array per OTel gen-ai semantic conventions, matching the .NET SDK (Agent365-dotnet#253). Changes: - Remove A365_MESSAGE_SCHEMA_VERSION constant and version field from InputMessages/OutputMessages dataclasses - Serialize as plain JSON array instead of {version, messages} object - Make OutputMessage.finish_reason default to 'stop' (required per spec) - Update fallback serialization to also use array format - Add TestSerializationFormat test class ensuring all paths produce arrays - Update all unit and integration tests to expect array format Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 41775eb commit af0b090

12 files changed

Lines changed: 362 additions & 251 deletions

File tree

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

Lines changed: 23 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"""Conversion and serialization helpers for OTEL gen-ai message format.
55
66
Provides normalization from plain ``list[str]`` (backward compat) to the
7-
versioned wrapper format, and a non-throwing ``serialize_messages`` function.
7+
structured array format, and a non-throwing ``serialize_messages`` function.
88
"""
99

1010
from __future__ import annotations
@@ -16,7 +16,6 @@
1616
from typing import Union
1717

1818
from .models.messages import (
19-
A365_MESSAGE_SCHEMA_VERSION,
2019
ChatMessage,
2120
InputMessages,
2221
InputMessagesParam,
@@ -40,7 +39,7 @@ def is_string_list(
4039
def is_wrapped_messages(
4140
param: Union[InputMessagesParam, OutputMessagesParam],
4241
) -> bool:
43-
"""Return ``True`` when *param* is a versioned wrapper."""
42+
"""Return ``True`` when *param* is a structured message container."""
4443
return isinstance(param, (InputMessages, OutputMessages))
4544

4645

@@ -71,7 +70,7 @@ def to_output_messages(messages: list[str]) -> list[OutputMessage]:
7170

7271

7372
def normalize_input_messages(param: InputMessagesParam) -> InputMessages:
74-
"""Normalize an ``InputMessagesParam`` to a versioned ``InputMessages`` wrapper.
73+
"""Normalize an ``InputMessagesParam`` to an ``InputMessages`` instance.
7574
7675
- ``str`` → wrapped in a single-element list, then converted.
7776
- ``list[str]`` → converted to ``ChatMessage`` list and wrapped.
@@ -85,7 +84,7 @@ def normalize_input_messages(param: InputMessagesParam) -> InputMessages:
8584

8685

8786
def normalize_output_messages(param: OutputMessagesParam) -> OutputMessages:
88-
"""Normalize an ``OutputMessagesParam`` to a versioned ``OutputMessages`` wrapper.
87+
"""Normalize an ``OutputMessagesParam`` to an ``OutputMessages`` instance.
8988
9089
- ``str`` → wrapped in a single-element list, then converted.
9190
- ``list[str]`` → converted to ``OutputMessage`` list and wrapped.
@@ -114,37 +113,34 @@ def _message_dict_factory(items: list[tuple[str, object]]) -> dict[str, object]:
114113
def serialize_messages(
115114
wrapper: Union[InputMessages, OutputMessages],
116115
) -> str:
117-
"""Serialize a versioned message wrapper to JSON.
116+
"""Serialize a message container to a JSON array.
118117
119-
The output is the full wrapper object:
120-
``{"version":"0.1.0","messages":[...]}``.
118+
The output is a plain JSON array of message objects per OTel gen-ai
119+
semantic conventions: ``[{"role":"user","parts":[...]}]``.
121120
122121
The try/except ensures telemetry recording is non-throwing even when
123122
message parts contain non-JSON-serializable values.
124123
"""
125124
try:
126-
return json.dumps(
127-
asdict(wrapper, dict_factory=_message_dict_factory),
128-
default=str,
129-
ensure_ascii=False,
130-
)
125+
messages_dicts = [
126+
asdict(msg, dict_factory=_message_dict_factory) for msg in wrapper.messages
127+
]
128+
return json.dumps(messages_dicts, default=str, ensure_ascii=False)
131129
except Exception:
132130
logger.warning("Failed to serialize messages; using fallback.", exc_info=True)
133131
messages = getattr(wrapper, "messages", [])
134132
count = len(messages) if isinstance(messages, list) else 0
135133
noun = "message" if count == 1 else "messages"
136-
fallback = {
137-
"version": A365_MESSAGE_SCHEMA_VERSION,
138-
"messages": [
139-
{
140-
"role": MessageRole.SYSTEM.value,
141-
"parts": [
142-
{
143-
"type": "text",
144-
"content": f"[serialization failed: {count} {noun}]",
145-
}
146-
],
147-
}
148-
],
149-
}
134+
fallback = [
135+
{
136+
"role": MessageRole.SYSTEM.value,
137+
"parts": [
138+
{
139+
"type": "text",
140+
"content": f"[serialization failed: {count} {noun}]",
141+
}
142+
],
143+
"finish_reason": "error",
144+
}
145+
]
150146
return json.dumps(fallback, ensure_ascii=False)

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

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -175,33 +175,37 @@ class ChatMessage:
175175

176176
@dataclass
177177
class OutputMessage(ChatMessage):
178-
"""An output message produced by a model (OTEL gen-ai semantic conventions)."""
178+
"""An output message produced by a model (OTEL gen-ai semantic conventions).
179179
180-
finish_reason: str | None = None
180+
``finish_reason`` defaults to ``"stop"`` per OTel spec (required field).
181+
"""
182+
183+
finish_reason: str = "stop"
181184

182185

183186
# ---------------------------------------------------------------------------
184-
# Versioned wrappers
187+
# Message containers
185188
# ---------------------------------------------------------------------------
186189

187-
A365_MESSAGE_SCHEMA_VERSION: str = "0.1.0"
188-
"""Schema version embedded in serialized message payloads."""
189-
190190

191191
@dataclass
192192
class InputMessages:
193-
"""Versioned wrapper for input messages."""
193+
"""Represents the list of input messages sent to the model.
194+
195+
Serializes as a plain JSON array per OTel gen-ai semantic conventions.
196+
"""
194197

195198
messages: list[ChatMessage] = field(default_factory=list)
196-
version: str = field(default=A365_MESSAGE_SCHEMA_VERSION, init=False)
197199

198200

199201
@dataclass
200202
class OutputMessages:
201-
"""Versioned wrapper for output messages."""
203+
"""Represents the list of output messages generated by the model.
204+
205+
Serializes as a plain JSON array per OTel gen-ai semantic conventions.
206+
"""
202207

203208
messages: list[OutputMessage] = field(default_factory=list)
204-
version: str = field(default=A365_MESSAGE_SCHEMA_VERSION, init=False)
205209

206210

207211
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)