Skip to content

Commit fa89295

Browse files
committed
fix(ai): truncate long message histories
1 parent 87f8f39 commit fa89295

File tree

2 files changed

+529
-0
lines changed

2 files changed

+529
-0
lines changed

sentry_sdk/ai/message_utils.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import json
2+
from typing import TYPE_CHECKING
3+
4+
if TYPE_CHECKING:
5+
from typing import Any, Dict, List, Optional
6+
7+
try:
8+
from sentry_sdk.serializer import serialize
9+
except ImportError:
10+
# Fallback for cases where sentry_sdk isn't fully importable
11+
def serialize(obj, **kwargs):
12+
# type: (Any, **Any) -> Any
13+
return obj
14+
15+
16+
MAX_GEN_AI_MESSAGE_BYTES = 30_000 # 300KB
17+
18+
19+
def truncate_messages_by_size(messages, max_bytes=MAX_GEN_AI_MESSAGE_BYTES):
20+
# type: (List[Dict[str, Any]], int) -> List[Dict[str, Any]]
21+
"""
22+
Truncate messages by removing the oldest ones until the serialized size is within limits.
23+
24+
This function prioritizes keeping the most recent messages while ensuring the total
25+
serialized size stays under the specified byte limit. It uses the Sentry serializer
26+
to get accurate size estimates that match what will actually be sent.
27+
28+
:param messages: List of message objects (typically with 'role' and 'content' keys)
29+
:param max_bytes: Maximum allowed size in bytes for the serialized messages
30+
:returns: Truncated list of messages that fits within the size limit
31+
"""
32+
if not messages:
33+
return messages
34+
35+
truncated_messages = list(messages)
36+
37+
while truncated_messages:
38+
serialized = serialize(truncated_messages, is_vars=False)
39+
serialized_json = json.dumps(serialized, separators=(",", ":"))
40+
current_size = len(serialized_json.encode("utf-8"))
41+
42+
if current_size <= max_bytes:
43+
break
44+
45+
truncated_messages.pop(0)
46+
47+
return truncated_messages
48+
49+
50+
def serialize_gen_ai_messages(messages, max_bytes=MAX_GEN_AI_MESSAGE_BYTES):
51+
# type: (Optional[List[Dict[str, Any]]], int) -> Optional[str]
52+
"""
53+
Serialize and truncate gen_ai messages for storage in spans.
54+
55+
This function handles the complete workflow of:
56+
1. Truncating messages to fit within size limits
57+
2. Serializing them using Sentry's serializer
58+
3. Converting to JSON string for storage
59+
60+
:param messages: List of message objects or None
61+
:param max_bytes: Maximum allowed size in bytes for the serialized messages
62+
:returns: JSON string of serialized messages or None if input was None/empty
63+
"""
64+
if not messages:
65+
return None
66+
truncated_messages = truncate_messages_by_size(messages, max_bytes)
67+
if not truncated_messages:
68+
return None
69+
serialized_messages = serialize(truncated_messages, is_vars=False)
70+
71+
return json.dumps(serialized_messages, separators=(",", ":"))
72+
73+
74+
def get_messages_metadata(original_messages, truncated_messages):
75+
# type: (List[Dict[str, Any]], List[Dict[str, Any]]) -> Dict[str, Any]
76+
"""
77+
Generate metadata about message truncation for debugging/monitoring.
78+
79+
:param original_messages: The original list of messages
80+
:param truncated_messages: The truncated list of messages
81+
:returns: Dictionary with metadata about the truncation
82+
"""
83+
original_count = len(original_messages) if original_messages else 0
84+
truncated_count = len(truncated_messages) if truncated_messages else 0
85+
86+
metadata = {
87+
"original_count": original_count,
88+
"truncated_count": truncated_count,
89+
"messages_removed": original_count - truncated_count,
90+
"was_truncated": original_count != truncated_count,
91+
}
92+
93+
return metadata
94+
95+
96+
def truncate_and_serialize_messages(messages, max_bytes=MAX_GEN_AI_MESSAGE_BYTES):
97+
# type: (Optional[List[Dict[str, Any]]], int) -> Dict[str, Any]
98+
"""
99+
One-stop function for gen_ai integrations to truncate and serialize messages.
100+
101+
This is the main function that gen_ai integrations should use. It handles the
102+
complete workflow and returns both the serialized data and metadata.
103+
104+
Example usage:
105+
from sentry_sdk.ai.message_utils import truncate_and_serialize_messages
106+
107+
result = truncate_and_serialize_messages(messages)
108+
if result['serialized_data']:
109+
span.set_data('gen_ai.request.messages', result['serialized_data'])
110+
if result['metadata']['was_truncated']:
111+
# Log warning about truncation if desired
112+
pass
113+
114+
:param messages: List of message objects or None
115+
:param max_bytes: Maximum allowed size in bytes for the serialized messages
116+
:returns: Dictionary containing 'serialized_data', 'metadata', and 'original_size'
117+
"""
118+
if not messages:
119+
return {
120+
"serialized_data": None,
121+
"metadata": get_messages_metadata([], []),
122+
"original_size": 0,
123+
}
124+
125+
original_serialized = serialize(messages, is_vars=False)
126+
original_json = json.dumps(original_serialized, separators=(",", ":"))
127+
original_size = len(original_json.encode("utf-8"))
128+
129+
truncated_messages = truncate_messages_by_size(messages, max_bytes)
130+
serialized_data = serialize_gen_ai_messages(truncated_messages, max_bytes)
131+
metadata = get_messages_metadata(messages, truncated_messages)
132+
133+
return {
134+
"serialized_data": serialized_data,
135+
"metadata": metadata,
136+
"original_size": original_size,
137+
}

0 commit comments

Comments
 (0)