1111)
1212from sentry_sdk .ai .monitoring import ai_track
1313from sentry_sdk .ai .utils import (
14- MAX_GEN_AI_MESSAGE_BYTES ,
1514 MAX_SINGLE_MESSAGE_CONTENT_CHARS ,
1615 set_data_normalized ,
1716 truncate_and_annotate_messages ,
18- truncate_messages_by_size ,
19- _find_truncation_index ,
2017 parse_data_uri ,
2118 redact_blob_message_parts ,
2219 get_modality_from_mime_type ,
@@ -222,127 +219,7 @@ def large_messages():
222219 ]
223220
224221
225- class TestTruncateMessagesBySize :
226- def test_no_truncation_needed (self , sample_messages ):
227- """Test that messages under the limit are not truncated"""
228- result , truncation_index = truncate_messages_by_size (
229- sample_messages , max_bytes = MAX_GEN_AI_MESSAGE_BYTES
230- )
231- assert len (result ) == len (sample_messages )
232- assert result == sample_messages
233- assert truncation_index == 0
234-
235- def test_truncation_removes_oldest_first (self , large_messages ):
236- """Test that oldest messages are removed first during truncation"""
237- small_limit = 3000
238- result , truncation_index = truncate_messages_by_size (
239- large_messages , max_bytes = small_limit
240- )
241- assert len (result ) < len (large_messages )
242-
243- assert result [- 1 ] == large_messages [- 1 ]
244- assert truncation_index == len (large_messages ) - len (result )
245-
246- def test_empty_messages_list (self ):
247- """Test handling of empty messages list"""
248- result , truncation_index = truncate_messages_by_size (
249- [], max_bytes = MAX_GEN_AI_MESSAGE_BYTES // 500
250- )
251- assert result == []
252- assert truncation_index == 0
253-
254- def test_find_truncation_index (
255- self ,
256- ):
257- """Test that the truncation index is found correctly"""
258- # when represented in JSON, these are each 7 bytes long
259- messages = ["A" * 5 , "B" * 5 , "C" * 5 , "D" * 5 , "E" * 5 ]
260- truncation_index = _find_truncation_index (messages , 20 )
261- assert truncation_index == 3
262- assert messages [truncation_index :] == ["D" * 5 , "E" * 5 ]
263-
264- messages = ["A" * 5 , "B" * 5 , "C" * 5 , "D" * 5 , "E" * 5 ]
265- truncation_index = _find_truncation_index (messages , 40 )
266- assert truncation_index == 0
267- assert messages [truncation_index :] == [
268- "A" * 5 ,
269- "B" * 5 ,
270- "C" * 5 ,
271- "D" * 5 ,
272- "E" * 5 ,
273- ]
274-
275- def test_progressive_truncation (self , large_messages ):
276- """Test that truncation works progressively with different limits"""
277- limits = [
278- MAX_GEN_AI_MESSAGE_BYTES // 5 ,
279- MAX_GEN_AI_MESSAGE_BYTES // 10 ,
280- MAX_GEN_AI_MESSAGE_BYTES // 25 ,
281- MAX_GEN_AI_MESSAGE_BYTES // 100 ,
282- MAX_GEN_AI_MESSAGE_BYTES // 500 ,
283- ]
284- prev_count = len (large_messages )
285-
286- for limit in limits :
287- result = truncate_messages_by_size (large_messages , max_bytes = limit )
288- current_count = len (result )
289-
290- assert current_count <= prev_count
291- assert current_count >= 1
292- prev_count = current_count
293-
294- def test_single_message_truncation (self ):
295- large_content = "This is a very long message. " * 10_000
296-
297- messages = [
298- {"role" : "system" , "content" : "You are a helpful assistant." },
299- {"role" : "user" , "content" : large_content },
300- ]
301-
302- result , truncation_index = truncate_messages_by_size (
303- messages , max_single_message_chars = MAX_SINGLE_MESSAGE_CONTENT_CHARS
304- )
305-
306- assert len (result ) == 1
307- assert (
308- len (result [0 ]["content" ].rstrip ("..." )) <= MAX_SINGLE_MESSAGE_CONTENT_CHARS
309- )
310-
311- # If the last message is too large, the system message is not present
312- system_msgs = [m for m in result if m .get ("role" ) == "system" ]
313- assert len (system_msgs ) == 0
314-
315- # Confirm the user message is truncated with '...'
316- user_msgs = [m for m in result if m .get ("role" ) == "user" ]
317- assert len (user_msgs ) == 1
318- assert user_msgs [0 ]["content" ].endswith ("..." )
319- assert len (user_msgs [0 ]["content" ]) < len (large_content )
320-
321-
322222class TestTruncateAndAnnotateMessages :
323- def test_no_truncation_returns_list (self , sample_messages ):
324- class MockSpan :
325- def __init__ (self ):
326- self .span_id = "test_span_id"
327- self .data = {}
328-
329- def set_data (self , key , value ):
330- self .data [key ] = value
331-
332- class MockScope :
333- def __init__ (self ):
334- self ._gen_ai_original_message_count = {}
335-
336- span = MockSpan ()
337- scope = MockScope ()
338- result = truncate_and_annotate_messages (sample_messages , span , scope )
339-
340- assert isinstance (result , list )
341- assert not isinstance (result , AnnotatedValue )
342- assert len (result ) == len (sample_messages )
343- assert result == sample_messages
344- assert span .span_id not in scope ._gen_ai_original_message_count
345-
346223 def test_truncation_sets_metadata_on_scope (self , large_messages ):
347224 class MockSpan :
348225 def __init__ (self ):
@@ -361,7 +238,7 @@ def __init__(self):
361238 scope = MockScope ()
362239 original_count = len (large_messages )
363240 result = truncate_and_annotate_messages (
364- large_messages , span , scope , max_bytes = small_limit
241+ large_messages , span , scope , max_single_message_chars = small_limit
365242 )
366243
367244 assert isinstance (result , list )
@@ -388,7 +265,7 @@ def __init__(self):
388265 scope = MockScope ()
389266
390267 result = truncate_and_annotate_messages (
391- large_messages , span , scope , max_bytes = small_limit
268+ large_messages , span , scope , max_single_message_chars = small_limit
392269 )
393270
394271 assert scope ._gen_ai_original_message_count [span .span_id ] == original_count
@@ -415,6 +292,47 @@ def __init__(self):
415292 result = truncate_and_annotate_messages (None , span , scope )
416293 assert result is None
417294
295+ def test_single_message_truncation (self , large_messages ):
296+ class MockSpan :
297+ def __init__ (self ):
298+ self .span_id = "test_span_id"
299+ self .data = {}
300+
301+ def set_data (self , key , value ):
302+ self .data [key ] = value
303+
304+ class MockScope :
305+ def __init__ (self ):
306+ self ._gen_ai_original_message_count = {}
307+
308+ large_content = "This is a very long message. " * 10_000
309+
310+ messages = [
311+ {"role" : "system" , "content" : "You are a helpful assistant." },
312+ {"role" : "user" , "content" : large_content },
313+ ]
314+
315+ span = MockSpan ()
316+ scope = MockScope ()
317+ result = truncate_and_annotate_messages (
318+ messages ,
319+ span ,
320+ scope ,
321+ max_single_message_chars = MAX_SINGLE_MESSAGE_CONTENT_CHARS ,
322+ )
323+ assert result is not None
324+
325+ assert len (result ) == 1
326+ assert (
327+ len (result [0 ]["content" ].rstrip ("..." )) <= MAX_SINGLE_MESSAGE_CONTENT_CHARS
328+ )
329+
330+ # Confirm the user message is truncated with '...'
331+ user_msgs = [m for m in result if m .get ("role" ) == "user" ]
332+ assert len (user_msgs ) == 1
333+ assert user_msgs [0 ]["content" ].endswith ("..." )
334+ assert len (user_msgs [0 ]["content" ]) < len (large_content )
335+
418336 def test_truncated_messages_newest_first (self , large_messages ):
419337 class MockSpan :
420338 def __init__ (self ):
@@ -432,7 +350,7 @@ def __init__(self):
432350 span = MockSpan ()
433351 scope = MockScope ()
434352 result = truncate_and_annotate_messages (
435- large_messages , span , scope , max_bytes = small_limit
353+ large_messages , span , scope , max_single_message_chars = small_limit
436354 )
437355
438356 assert isinstance (result , list )
@@ -500,15 +418,12 @@ class MockScope:
500418 def __init__ (self ):
501419 self ._gen_ai_original_message_count = {}
502420
503- small_limit = 3000
504421 span = MockSpan ()
505422 scope = MockScope ()
506423 original_count = len (large_messages )
507424
508425 # Simulate what integrations do
509- truncated_messages = truncate_and_annotate_messages (
510- large_messages , span , scope , max_bytes = small_limit
511- )
426+ truncated_messages = truncate_and_annotate_messages (large_messages , span , scope )
512427 span .set_data (SPANDATA .GEN_AI_REQUEST_MESSAGES , truncated_messages )
513428
514429 # Verify metadata was set on scope
@@ -557,14 +472,11 @@ class MockScope:
557472 def __init__ (self ):
558473 self ._gen_ai_original_message_count = {}
559474
560- small_limit = 3000
561475 span = MockSpan ()
562476 scope = MockScope ()
563477 original_message_count = len (large_messages )
564478
565- truncated_messages = truncate_and_annotate_messages (
566- large_messages , span , scope , max_bytes = small_limit
567- )
479+ truncated_messages = truncate_and_annotate_messages (large_messages , span , scope )
568480
569481 assert len (truncated_messages ) < original_message_count
570482
0 commit comments