Skip to content

Commit b1dd2dc

Browse files
fix(ai): add mapping for gen_ai message roles (#4884)
- Add a constant that contains the allowed message roles according to OTEL and a mapping - Apply that mapping to all gen_ai integrations - We will track input roles that do not conform to expectations via a Sentry issue in agent monitoring to make sure we continually update the mappings --------- Co-authored-by: Ivana Kellyer <[email protected]>
1 parent a879d82 commit b1dd2dc

File tree

13 files changed

+525
-31
lines changed

13 files changed

+525
-31
lines changed

sentry_sdk/ai/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from .utils import (
2+
set_data_normalized,
3+
GEN_AI_MESSAGE_ROLE_MAPPING,
4+
GEN_AI_MESSAGE_ROLE_REVERSE_MAPPING,
5+
normalize_message_role,
6+
normalize_message_roles,
7+
) # noqa: F401

sentry_sdk/ai/utils.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,26 @@
1010
from sentry_sdk.utils import logger
1111

1212

13+
class GEN_AI_ALLOWED_MESSAGE_ROLES:
14+
SYSTEM = "system"
15+
USER = "user"
16+
ASSISTANT = "assistant"
17+
TOOL = "tool"
18+
19+
20+
GEN_AI_MESSAGE_ROLE_REVERSE_MAPPING = {
21+
GEN_AI_ALLOWED_MESSAGE_ROLES.SYSTEM: ["system"],
22+
GEN_AI_ALLOWED_MESSAGE_ROLES.USER: ["user", "human"],
23+
GEN_AI_ALLOWED_MESSAGE_ROLES.ASSISTANT: ["assistant", "ai"],
24+
GEN_AI_ALLOWED_MESSAGE_ROLES.TOOL: ["tool", "tool_call"],
25+
}
26+
27+
GEN_AI_MESSAGE_ROLE_MAPPING = {}
28+
for target_role, source_roles in GEN_AI_MESSAGE_ROLE_REVERSE_MAPPING.items():
29+
for source_role in source_roles:
30+
GEN_AI_MESSAGE_ROLE_MAPPING[source_role] = target_role
31+
32+
1333
def _normalize_data(data, unpack=True):
1434
# type: (Any, bool) -> Any
1535
# convert pydantic data (e.g. OpenAI v1+) to json compatible format
@@ -40,6 +60,34 @@ def set_data_normalized(span, key, value, unpack=True):
4060
span.set_data(key, json.dumps(normalized))
4161

4262

63+
def normalize_message_role(role):
64+
# type: (str) -> str
65+
"""
66+
Normalize a message role to one of the 4 allowed gen_ai role values.
67+
Maps "ai" -> "assistant" and keeps other standard roles unchanged.
68+
"""
69+
return GEN_AI_MESSAGE_ROLE_MAPPING.get(role, role)
70+
71+
72+
def normalize_message_roles(messages):
73+
# type: (list[dict[str, Any]]) -> list[dict[str, Any]]
74+
"""
75+
Normalize roles in a list of messages to use standard gen_ai role values.
76+
Creates a deep copy to avoid modifying the original messages.
77+
"""
78+
normalized_messages = []
79+
for message in messages:
80+
if not isinstance(message, dict):
81+
normalized_messages.append(message)
82+
continue
83+
normalized_message = message.copy()
84+
if "role" in message:
85+
normalized_message["role"] = normalize_message_role(message["role"])
86+
normalized_messages.append(normalized_message)
87+
88+
return normalized_messages
89+
90+
4391
def get_start_span_function():
4492
# type: () -> Callable[..., Any]
4593
current_span = sentry_sdk.get_current_span()

sentry_sdk/integrations/anthropic.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33

44
import sentry_sdk
55
from sentry_sdk.ai.monitoring import record_token_usage
6-
from sentry_sdk.ai.utils import set_data_normalized, get_start_span_function
6+
from sentry_sdk.ai.utils import (
7+
set_data_normalized,
8+
normalize_message_roles,
9+
get_start_span_function,
10+
)
711
from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS
812
from sentry_sdk.integrations import _check_minimum_version, DidNotEnable, Integration
913
from sentry_sdk.scope import should_send_default_pii
@@ -140,8 +144,12 @@ def _set_input_data(span, kwargs, integration):
140144
else:
141145
normalized_messages.append(message)
142146

147+
role_normalized_messages = normalize_message_roles(normalized_messages)
143148
set_data_normalized(
144-
span, SPANDATA.GEN_AI_REQUEST_MESSAGES, normalized_messages, unpack=False
149+
span,
150+
SPANDATA.GEN_AI_REQUEST_MESSAGES,
151+
role_normalized_messages,
152+
unpack=False,
145153
)
146154

147155
set_data_normalized(

sentry_sdk/integrations/langchain.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@
44

55
import sentry_sdk
66
from sentry_sdk.ai.monitoring import set_ai_pipeline_name
7-
from sentry_sdk.ai.utils import set_data_normalized, get_start_span_function
7+
from sentry_sdk.ai.utils import (
8+
GEN_AI_ALLOWED_MESSAGE_ROLES,
9+
normalize_message_roles,
10+
set_data_normalized,
11+
get_start_span_function,
12+
)
813
from sentry_sdk.consts import OP, SPANDATA
914
from sentry_sdk.integrations import DidNotEnable, Integration
1015
from sentry_sdk.scope import should_send_default_pii
@@ -209,8 +214,18 @@ def on_llm_start(
209214
_set_tools_on_span(span, all_params.get("tools"))
210215

211216
if should_send_default_pii() and self.include_prompts:
217+
normalized_messages = [
218+
{
219+
"role": GEN_AI_ALLOWED_MESSAGE_ROLES.USER,
220+
"content": {"type": "text", "text": prompt},
221+
}
222+
for prompt in prompts
223+
]
212224
set_data_normalized(
213-
span, SPANDATA.GEN_AI_REQUEST_MESSAGES, prompts, unpack=False
225+
span,
226+
SPANDATA.GEN_AI_REQUEST_MESSAGES,
227+
normalized_messages,
228+
unpack=False,
214229
)
215230

216231
def on_chat_model_start(self, serialized, messages, *, run_id, **kwargs):
@@ -262,6 +277,8 @@ def on_chat_model_start(self, serialized, messages, *, run_id, **kwargs):
262277
normalized_messages.append(
263278
self._normalize_langchain_message(message)
264279
)
280+
normalized_messages = normalize_message_roles(normalized_messages)
281+
265282
set_data_normalized(
266283
span,
267284
SPANDATA.GEN_AI_REQUEST_MESSAGES,
@@ -740,8 +757,12 @@ def new_invoke(self, *args, **kwargs):
740757
and should_send_default_pii()
741758
and integration.include_prompts
742759
):
760+
normalized_messages = normalize_message_roles([input])
743761
set_data_normalized(
744-
span, SPANDATA.GEN_AI_REQUEST_MESSAGES, [input], unpack=False
762+
span,
763+
SPANDATA.GEN_AI_REQUEST_MESSAGES,
764+
normalized_messages,
765+
unpack=False,
745766
)
746767

747768
output = result.get("output")
@@ -791,8 +812,12 @@ def new_stream(self, *args, **kwargs):
791812
and should_send_default_pii()
792813
and integration.include_prompts
793814
):
815+
normalized_messages = normalize_message_roles([input])
794816
set_data_normalized(
795-
span, SPANDATA.GEN_AI_REQUEST_MESSAGES, [input], unpack=False
817+
span,
818+
SPANDATA.GEN_AI_REQUEST_MESSAGES,
819+
normalized_messages,
820+
unpack=False,
796821
)
797822

798823
# Run the agent

sentry_sdk/integrations/langgraph.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from typing import Any, Callable, List, Optional
33

44
import sentry_sdk
5-
from sentry_sdk.ai.utils import set_data_normalized
5+
from sentry_sdk.ai.utils import set_data_normalized, normalize_message_roles
66
from sentry_sdk.consts import OP, SPANDATA
77
from sentry_sdk.integrations import DidNotEnable, Integration
88
from sentry_sdk.scope import should_send_default_pii
@@ -180,10 +180,11 @@ def new_invoke(self, *args, **kwargs):
180180
):
181181
input_messages = _parse_langgraph_messages(args[0])
182182
if input_messages:
183+
normalized_input_messages = normalize_message_roles(input_messages)
183184
set_data_normalized(
184185
span,
185186
SPANDATA.GEN_AI_REQUEST_MESSAGES,
186-
input_messages,
187+
normalized_input_messages,
187188
unpack=False,
188189
)
189190

@@ -230,10 +231,11 @@ async def new_ainvoke(self, *args, **kwargs):
230231
):
231232
input_messages = _parse_langgraph_messages(args[0])
232233
if input_messages:
234+
normalized_input_messages = normalize_message_roles(input_messages)
233235
set_data_normalized(
234236
span,
235237
SPANDATA.GEN_AI_REQUEST_MESSAGES,
236-
input_messages,
238+
normalized_input_messages,
237239
unpack=False,
238240
)
239241

sentry_sdk/integrations/openai.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import sentry_sdk
44
from sentry_sdk import consts
55
from sentry_sdk.ai.monitoring import record_token_usage
6-
from sentry_sdk.ai.utils import set_data_normalized
6+
from sentry_sdk.ai.utils import set_data_normalized, normalize_message_roles
77
from sentry_sdk.consts import SPANDATA
88
from sentry_sdk.integrations import DidNotEnable, Integration
99
from sentry_sdk.scope import should_send_default_pii
@@ -182,8 +182,9 @@ def _set_input_data(span, kwargs, operation, integration):
182182
and should_send_default_pii()
183183
and integration.include_prompts
184184
):
185+
normalized_messages = normalize_message_roles(messages)
185186
set_data_normalized(
186-
span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages, unpack=False
187+
span, SPANDATA.GEN_AI_REQUEST_MESSAGES, normalized_messages, unpack=False
187188
)
188189

189190
# Input attributes: Common

sentry_sdk/integrations/openai_agents/spans/invoke_agent.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import sentry_sdk
2-
from sentry_sdk.ai.utils import get_start_span_function, set_data_normalized
2+
from sentry_sdk.ai.utils import (
3+
get_start_span_function,
4+
set_data_normalized,
5+
normalize_message_roles,
6+
)
37
from sentry_sdk.consts import OP, SPANDATA
48
from sentry_sdk.scope import should_send_default_pii
59
from sentry_sdk.utils import safe_serialize
@@ -56,8 +60,12 @@ def invoke_agent_span(context, agent, kwargs):
5660
)
5761

5862
if len(messages) > 0:
63+
normalized_messages = normalize_message_roles(messages)
5964
set_data_normalized(
60-
span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages, unpack=False
65+
span,
66+
SPANDATA.GEN_AI_REQUEST_MESSAGES,
67+
normalized_messages,
68+
unpack=False,
6169
)
6270

6371
_set_agent_data(span, agent)

sentry_sdk/integrations/openai_agents/utils.py

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import sentry_sdk
2-
from sentry_sdk.ai.utils import set_data_normalized
2+
from sentry_sdk.ai.utils import (
3+
GEN_AI_ALLOWED_MESSAGE_ROLES,
4+
normalize_message_roles,
5+
set_data_normalized,
6+
normalize_message_role,
7+
)
38
from sentry_sdk.consts import SPANDATA, SPANSTATUS, OP
49
from sentry_sdk.integrations import DidNotEnable
510
from sentry_sdk.scope import should_send_default_pii
@@ -94,35 +99,47 @@ def _set_input_data(span, get_response_kwargs):
9499
# type: (sentry_sdk.tracing.Span, dict[str, Any]) -> None
95100
if not should_send_default_pii():
96101
return
102+
request_messages = []
97103

98-
messages_by_role = {
99-
"system": [],
100-
"user": [],
101-
"assistant": [],
102-
"tool": [],
103-
} # type: (dict[str, list[Any]])
104104
system_instructions = get_response_kwargs.get("system_instructions")
105105
if system_instructions:
106-
messages_by_role["system"].append({"type": "text", "text": system_instructions})
106+
request_messages.append(
107+
{
108+
"role": GEN_AI_ALLOWED_MESSAGE_ROLES.SYSTEM,
109+
"content": [{"type": "text", "text": system_instructions}],
110+
}
111+
)
107112

108113
for message in get_response_kwargs.get("input", []):
109114
if "role" in message:
110-
messages_by_role[message.get("role")].append(
111-
{"type": "text", "text": message.get("content")}
115+
normalized_role = normalize_message_role(message.get("role"))
116+
request_messages.append(
117+
{
118+
"role": normalized_role,
119+
"content": [{"type": "text", "text": message.get("content")}],
120+
}
112121
)
113122
else:
114123
if message.get("type") == "function_call":
115-
messages_by_role["assistant"].append(message)
124+
request_messages.append(
125+
{
126+
"role": GEN_AI_ALLOWED_MESSAGE_ROLES.ASSISTANT,
127+
"content": [message],
128+
}
129+
)
116130
elif message.get("type") == "function_call_output":
117-
messages_by_role["tool"].append(message)
118-
119-
request_messages = []
120-
for role, messages in messages_by_role.items():
121-
if len(messages) > 0:
122-
request_messages.append({"role": role, "content": messages})
131+
request_messages.append(
132+
{
133+
"role": GEN_AI_ALLOWED_MESSAGE_ROLES.TOOL,
134+
"content": [message],
135+
}
136+
)
123137

124138
set_data_normalized(
125-
span, SPANDATA.GEN_AI_REQUEST_MESSAGES, request_messages, unpack=False
139+
span,
140+
SPANDATA.GEN_AI_REQUEST_MESSAGES,
141+
normalize_message_roles(request_messages),
142+
unpack=False,
126143
)
127144

128145

0 commit comments

Comments
 (0)