Skip to content

Commit fc33978

Browse files
committed
feat(ai): add single message truncation
1 parent 659bd84 commit fc33978

File tree

2 files changed

+112
-4
lines changed

2 files changed

+112
-4
lines changed

sentry_sdk/ai/utils.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,30 @@ def get_start_span_function():
101101
return sentry_sdk.start_span if transaction_exists else sentry_sdk.start_transaction
102102

103103

104+
def _truncate_single_message(message, max_bytes):
105+
# type: (Dict[str, Any], int) -> Dict[str, Any]
106+
"""
107+
Truncate a single message to fit within max_bytes.
108+
If the message is too large, truncate the content field.
109+
"""
110+
if not isinstance(message, dict) or "content" not in message:
111+
return message
112+
content = message.get("content", "")
113+
114+
if not isinstance(content, str) or len(content) <= max_bytes:
115+
return message
116+
117+
overhead_message = message.copy()
118+
overhead_message["content"] = ""
119+
overhead_size = len(
120+
json.dumps(overhead_message, separators=(",", ":")).encode("utf-8")
121+
)
122+
123+
available_content_bytes = max_bytes - overhead_size - 20
124+
message["content"] = content[:available_content_bytes] + "..."
125+
return message
126+
127+
104128
def _find_truncation_index(messages, max_bytes):
105129
# type: (List[Dict[str, Any]], int) -> int
106130
"""
@@ -120,14 +144,20 @@ def _find_truncation_index(messages, max_bytes):
120144

121145
def truncate_messages_by_size(messages, max_bytes=MAX_GEN_AI_MESSAGE_BYTES):
122146
# type: (List[Dict[str, Any]], int) -> Tuple[List[Dict[str, Any]], int]
123-
serialized_json = json.dumps(messages, separators=(",", ":"))
147+
messages_with_truncated_content = [
148+
_truncate_single_message(msg, max_bytes) for msg in messages
149+
]
150+
151+
serialized_json = json.dumps(messages_with_truncated_content, separators=(",", ":"))
124152
current_size = len(serialized_json.encode("utf-8"))
125153

126154
if current_size <= max_bytes:
127-
return messages, 0
155+
return messages_with_truncated_content, 0
128156

129-
truncation_index = _find_truncation_index(messages, max_bytes)
130-
return messages[truncation_index:], truncation_index
157+
truncation_index = _find_truncation_index(
158+
messages_with_truncated_content, max_bytes
159+
)
160+
return messages_with_truncated_content[truncation_index:], truncation_index
131161

132162

133163
def truncate_and_annotate_messages(

tests/test_ai_monitoring.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,84 @@ def test_progressive_truncation(self, large_messages):
278278
assert current_count >= 1
279279
prev_count = current_count
280280

281+
def test_individual_message_truncation(self):
282+
large_content = "This is a very long message. " * 1000
283+
284+
messages = [
285+
{"role": "system", "content": "You are a helpful assistant."},
286+
{"role": "user", "content": large_content},
287+
]
288+
289+
result, truncation_index = truncate_messages_by_size(
290+
messages, max_bytes=MAX_GEN_AI_MESSAGE_BYTES
291+
)
292+
293+
assert len(result) > 0
294+
295+
total_size = len(json.dumps(result, separators=(",", ":")).encode("utf-8"))
296+
assert total_size <= MAX_GEN_AI_MESSAGE_BYTES
297+
298+
for msg in result:
299+
msg_size = len(json.dumps(msg, separators=(",", ":")).encode("utf-8"))
300+
assert msg_size <= MAX_GEN_AI_MESSAGE_BYTES
301+
302+
# If the last message is too large, the system message is not present
303+
system_msgs = [m for m in result if m.get("role") == "system"]
304+
assert len(system_msgs) == 0
305+
306+
# Confirm the user message is truncated with '...'
307+
user_msgs = [m for m in result if m.get("role") == "user"]
308+
assert len(user_msgs) == 1
309+
assert user_msgs[0]["content"].endswith("...")
310+
assert len(user_msgs[0]["content"]) < len(large_content)
311+
312+
def test_combined_individual_and_array_truncation(self):
313+
huge_content = "X" * 25000
314+
medium_content = "Y" * 5000
315+
316+
messages = [
317+
{"role": "system", "content": medium_content},
318+
{"role": "user", "content": huge_content},
319+
{"role": "assistant", "content": medium_content},
320+
{"role": "user", "content": "small"},
321+
]
322+
323+
result, truncation_index = truncate_messages_by_size(
324+
messages, max_bytes=MAX_GEN_AI_MESSAGE_BYTES
325+
)
326+
327+
assert len(result) > 0
328+
329+
total_size = len(json.dumps(result, separators=(",", ":")).encode("utf-8"))
330+
assert total_size <= MAX_GEN_AI_MESSAGE_BYTES
331+
332+
for msg in result:
333+
msg_size = len(json.dumps(msg, separators=(",", ":")).encode("utf-8"))
334+
assert msg_size <= MAX_GEN_AI_MESSAGE_BYTES
335+
336+
# The last user "small" message should always be present and untruncated
337+
last_user_msgs = [
338+
m for m in result if m.get("role") == "user" and m["content"] == "small"
339+
]
340+
assert len(last_user_msgs) == 1
341+
342+
# If the huge message is present, it must be truncated
343+
for user_msg in [
344+
m for m in result if m.get("role") == "user" and "X" in m["content"]
345+
]:
346+
assert user_msg["content"].endswith("...")
347+
assert len(user_msg["content"]) < len(huge_content)
348+
349+
# The medium messages, if present, should not be truncated
350+
for expected_role in ["system", "assistant"]:
351+
role_msgs = [m for m in result if m.get("role") == expected_role]
352+
if role_msgs:
353+
assert role_msgs[0]["content"].startswith("Y")
354+
assert len(role_msgs[0]["content"]) <= len(medium_content)
355+
assert not role_msgs[0]["content"].endswith("...") or len(
356+
role_msgs[0]["content"]
357+
) == len(medium_content)
358+
281359

282360
class TestTruncateAndAnnotateMessages:
283361
def test_no_truncation_returns_list(self, sample_messages):

0 commit comments

Comments
 (0)