@@ -649,3 +649,302 @@ def test_emit_llo_attributes_with_session_id_and_other_attributes(self):
649649 self .assertNotIn ("other.attribute" , emitted_event .attributes )
650650 self .assertNotIn ("gen_ai.prompt" , emitted_event .attributes )
651651 self .assertNotIn ("gen_ai.completion" , emitted_event .attributes )
652+
653+ def test_emit_llo_attributes_otel_genai_patterns (self ):
654+ """
655+ Test the new GenAI patterns from Strands SDK that follow OTel GenAI Semantic Convention.
656+ """
657+ attributes = {
658+ "gen_ai.user.message" : "What is machine learning?" ,
659+ "gen_ai.assistant.message" : (
660+ "Machine learning is a subset of AI that enables computers to learn patterns from data."
661+ ),
662+ "gen_ai.system.message" : "You are a helpful AI assistant specializing in technology topics." ,
663+ "gen_ai.tool.message" : "Searching knowledge base for machine learning definitions..." ,
664+ "gen_ai.choice" : "Best answer: Machine learning uses algorithms to find patterns in data." ,
665+ }
666+
667+ span = self ._create_mock_span (attributes )
668+ span .end_time = 1234567899
669+ span .instrumentation_scope = MagicMock ()
670+ span .instrumentation_scope .name = "strands.genai.scope"
671+
672+ self .llo_handler ._emit_llo_attributes (span , attributes )
673+
674+ self .event_logger_mock .emit .assert_called_once ()
675+ emitted_event = self .event_logger_mock .emit .call_args [0 ][0 ]
676+
677+ self .assertEqual (emitted_event .name , "strands.genai.scope" )
678+ self .assertEqual (emitted_event .timestamp , span .end_time )
679+
680+ event_body = emitted_event .body
681+ self .assertIn ("input" , event_body )
682+ self .assertIn ("output" , event_body )
683+
684+ # Check input messages (system, user, and tool messages)
685+ input_messages = event_body ["input" ]["messages" ]
686+ self .assertEqual (len (input_messages ), 3 )
687+
688+ input_contents = [msg ["content" ] for msg in input_messages ]
689+ self .assertIn ("What is machine learning?" , input_contents )
690+ self .assertIn ("You are a helpful AI assistant specializing in technology topics." , input_contents )
691+ self .assertIn ("Searching knowledge base for machine learning definitions..." , input_contents )
692+
693+ # Verify roles for input messages
694+ user_msg = next ((msg for msg in input_messages if msg ["content" ] == "What is machine learning?" ), None )
695+ self .assertIsNotNone (user_msg )
696+ self .assertEqual (user_msg ["role" ], "user" )
697+
698+ system_msg = next ((msg for msg in input_messages if "helpful AI assistant" in msg ["content" ]), None )
699+ self .assertIsNotNone (system_msg )
700+ self .assertEqual (system_msg ["role" ], "system" )
701+
702+ tool_msg = next ((msg for msg in input_messages if "Searching knowledge base" in msg ["content" ]), None )
703+ self .assertIsNotNone (tool_msg )
704+ self .assertEqual (tool_msg ["role" ], "user" )
705+
706+ # Check output messages (assistant message and choice)
707+ output_messages = event_body ["output" ]["messages" ]
708+ self .assertEqual (len (output_messages ), 2 )
709+
710+ output_contents = [msg ["content" ] for msg in output_messages ]
711+ self .assertIn (
712+ "Machine learning is a subset of AI that enables computers to learn patterns from data." , output_contents
713+ )
714+ self .assertIn ("Best answer: Machine learning uses algorithms to find patterns in data." , output_contents )
715+
716+ # Verify all output messages have assistant role
717+ for msg in output_messages :
718+ self .assertEqual (msg ["role" ], "assistant" )
719+
720+ def test_emit_llo_attributes_genai_user_message_only (self ):
721+ """
722+ Test event generation with only gen_ai.user.message attribute.
723+ """
724+ attributes = {
725+ "gen_ai.user.message" : "Hello, how are you today?" ,
726+ }
727+
728+ span = self ._create_mock_span (attributes )
729+ span .end_time = 1234567899
730+ span .instrumentation_scope = MagicMock ()
731+ span .instrumentation_scope .name = "test.scope"
732+
733+ self .llo_handler ._emit_llo_attributes (span , attributes )
734+
735+ self .event_logger_mock .emit .assert_called_once ()
736+ emitted_event = self .event_logger_mock .emit .call_args [0 ][0 ]
737+
738+ event_body = emitted_event .body
739+ self .assertIn ("input" , event_body )
740+ self .assertNotIn ("output" , event_body )
741+
742+ input_messages = event_body ["input" ]["messages" ]
743+ self .assertEqual (len (input_messages ), 1 )
744+ self .assertEqual (input_messages [0 ]["content" ], "Hello, how are you today?" )
745+ self .assertEqual (input_messages [0 ]["role" ], "user" )
746+
747+ def test_emit_llo_attributes_genai_assistant_message_only (self ):
748+ """
749+ Test event generation with only gen_ai.assistant.message attribute.
750+ """
751+ attributes = {
752+ "gen_ai.assistant.message" : "I'm doing well, thank you for asking!" ,
753+ }
754+
755+ span = self ._create_mock_span (attributes )
756+ span .end_time = 1234567899
757+ span .instrumentation_scope = MagicMock ()
758+ span .instrumentation_scope .name = "test.scope"
759+
760+ self .llo_handler ._emit_llo_attributes (span , attributes )
761+
762+ self .event_logger_mock .emit .assert_called_once ()
763+ emitted_event = self .event_logger_mock .emit .call_args [0 ][0 ]
764+
765+ event_body = emitted_event .body
766+ self .assertNotIn ("input" , event_body )
767+ self .assertIn ("output" , event_body )
768+
769+ output_messages = event_body ["output" ]["messages" ]
770+ self .assertEqual (len (output_messages ), 1 )
771+ self .assertEqual (output_messages [0 ]["content" ], "I'm doing well, thank you for asking!" )
772+ self .assertEqual (output_messages [0 ]["role" ], "assistant" )
773+
774+ def test_emit_llo_attributes_genai_system_message_only (self ):
775+ """
776+ Test event generation with only gen_ai.system.message attribute.
777+ """
778+ attributes = {
779+ "gen_ai.system.message" : "You are a creative writing assistant. Help users write stories." ,
780+ }
781+
782+ span = self ._create_mock_span (attributes )
783+ span .end_time = 1234567899
784+ span .instrumentation_scope = MagicMock ()
785+ span .instrumentation_scope .name = "test.scope"
786+
787+ self .llo_handler ._emit_llo_attributes (span , attributes )
788+
789+ self .event_logger_mock .emit .assert_called_once ()
790+ emitted_event = self .event_logger_mock .emit .call_args [0 ][0 ]
791+
792+ event_body = emitted_event .body
793+ self .assertIn ("input" , event_body )
794+ self .assertNotIn ("output" , event_body )
795+
796+ input_messages = event_body ["input" ]["messages" ]
797+ self .assertEqual (len (input_messages ), 1 )
798+ self .assertEqual (
799+ input_messages [0 ]["content" ], "You are a creative writing assistant. Help users write stories."
800+ )
801+ self .assertEqual (input_messages [0 ]["role" ], "system" )
802+
803+ def test_emit_llo_attributes_genai_tool_message_only (self ):
804+ """
805+ Test event generation with only gen_ai.tool.message attribute.
806+ """
807+ attributes = {
808+ "gen_ai.tool.message" : "Executing search function with query: 'latest news'" ,
809+ }
810+
811+ span = self ._create_mock_span (attributes )
812+ span .end_time = 1234567899
813+ span .instrumentation_scope = MagicMock ()
814+ span .instrumentation_scope .name = "test.scope"
815+
816+ self .llo_handler ._emit_llo_attributes (span , attributes )
817+
818+ self .event_logger_mock .emit .assert_called_once ()
819+ emitted_event = self .event_logger_mock .emit .call_args [0 ][0 ]
820+
821+ event_body = emitted_event .body
822+ self .assertIn ("input" , event_body )
823+ self .assertNotIn ("output" , event_body )
824+
825+ input_messages = event_body ["input" ]["messages" ]
826+ self .assertEqual (len (input_messages ), 1 )
827+ self .assertEqual (input_messages [0 ]["content" ], "Executing search function with query: 'latest news'" )
828+ self .assertEqual (input_messages [0 ]["role" ], "user" )
829+
830+ def test_emit_llo_attributes_genai_choice_only (self ):
831+ """
832+ Test event generation with only gen_ai.choice attribute.
833+ """
834+ attributes = {
835+ "gen_ai.choice" : "Selected option: Continue with the current approach." ,
836+ }
837+
838+ span = self ._create_mock_span (attributes )
839+ span .end_time = 1234567899
840+ span .instrumentation_scope = MagicMock ()
841+ span .instrumentation_scope .name = "test.scope"
842+
843+ self .llo_handler ._emit_llo_attributes (span , attributes )
844+
845+ self .event_logger_mock .emit .assert_called_once ()
846+ emitted_event = self .event_logger_mock .emit .call_args [0 ][0 ]
847+
848+ event_body = emitted_event .body
849+ self .assertNotIn ("input" , event_body )
850+ self .assertIn ("output" , event_body )
851+
852+ output_messages = event_body ["output" ]["messages" ]
853+ self .assertEqual (len (output_messages ), 1 )
854+ self .assertEqual (output_messages [0 ]["content" ], "Selected option: Continue with the current approach." )
855+ self .assertEqual (output_messages [0 ]["role" ], "assistant" )
856+
857+ def test_emit_llo_attributes_from_span_events (self ):
858+ """
859+ Test that LLO attributes are collected from span events when event names match LLO patterns.
860+ """
861+ # Create span with normal attributes
862+ span_attributes = {"normal.attribute" : "value" }
863+
864+ # Create span events where event names are LLO patterns
865+ user_event = MagicMock ()
866+ user_event .name = "gen_ai.user.message"
867+ user_event .attributes = {"content" : "What is the weather today?" , "other" : "metadata" }
868+ user_event .timestamp = 1234567890
869+
870+ assistant_event = MagicMock ()
871+ assistant_event .name = "gen_ai.assistant.message"
872+ assistant_event .attributes = {"content" : "It's sunny and 75°F" , "confidence" : 0.95 }
873+ assistant_event .timestamp = 1234567891
874+
875+ system_event = MagicMock ()
876+ system_event .name = "gen_ai.system.message"
877+ system_event .attributes = {"content" : "You are a weather assistant" }
878+ system_event .timestamp = 1234567892
879+
880+ span = self ._create_mock_span (span_attributes )
881+ span .events = [user_event , assistant_event , system_event ]
882+ span .end_time = 1234567899
883+ span .instrumentation_scope = MagicMock ()
884+ span .instrumentation_scope .name = "event.patterns.scope"
885+
886+ # Collect LLO attributes and emit event
887+ all_llo_attrs = self .llo_handler ._collect_llo_attributes_from_span (span )
888+ self .llo_handler ._emit_llo_attributes (span , all_llo_attrs )
889+
890+ # Should emit event because LLO attributes were collected from span events
891+ self .event_logger_mock .emit .assert_called_once ()
892+ emitted_event = self .event_logger_mock .emit .call_args [0 ][0 ]
893+
894+ event_body = emitted_event .body
895+ self .assertIn ("input" , event_body )
896+ self .assertIn ("output" , event_body )
897+
898+ # Check input messages (user and system)
899+ input_messages = event_body ["input" ]["messages" ]
900+ self .assertEqual (len (input_messages ), 2 )
901+
902+ # User message should contain all event attributes as content
903+ user_msg = next ((msg for msg in input_messages if msg ["role" ] == "user" ), None )
904+ self .assertIsNotNone (user_msg )
905+ self .assertEqual (user_msg ["content" ], {"content" : "What is the weather today?" , "other" : "metadata" })
906+
907+ # System message should contain all event attributes as content
908+ system_msg = next ((msg for msg in input_messages if msg ["role" ] == "system" ), None )
909+ self .assertIsNotNone (system_msg )
910+ self .assertEqual (system_msg ["content" ], {"content" : "You are a weather assistant" })
911+
912+ # Check output messages (assistant)
913+ output_messages = event_body ["output" ]["messages" ]
914+ self .assertEqual (len (output_messages ), 1 )
915+ self .assertEqual (output_messages [0 ]["role" ], "assistant" )
916+ self .assertEqual (output_messages [0 ]["content" ], {"content" : "It's sunny and 75°F" , "confidence" : 0.95 })
917+
918+ def test_filter_span_events_removes_llo_pattern_events (self ):
919+ """
920+ Test that span events are completely removed when their names match LLO patterns.
921+ """
922+ # Create span events
923+ normal_event = MagicMock ()
924+ normal_event .name = "normal.event"
925+ normal_event .attributes = {"data" : "keep this" }
926+
927+ llo_event1 = MagicMock ()
928+ llo_event1 .name = "gen_ai.user.message"
929+ llo_event1 .attributes = {"content" : "remove this event" }
930+
931+ llo_event2 = MagicMock ()
932+ llo_event2 .name = "gen_ai.assistant.message"
933+ llo_event2 .attributes = {"content" : "remove this too" }
934+
935+ another_normal_event = MagicMock ()
936+ another_normal_event .name = "another.normal.event"
937+ another_normal_event .attributes = {"info" : "keep this too" }
938+
939+ span = self ._create_mock_span ({})
940+ span .events = [normal_event , llo_event1 , llo_event2 , another_normal_event ]
941+
942+ self .llo_handler ._filter_span_events (span )
943+
944+ # Should only have normal events left
945+ self .assertEqual (len (span ._events ), 2 )
946+ remaining_names = [event .name for event in span ._events ]
947+ self .assertIn ("normal.event" , remaining_names )
948+ self .assertIn ("another.normal.event" , remaining_names )
949+ self .assertNotIn ("gen_ai.user.message" , remaining_names )
950+ self .assertNotIn ("gen_ai.assistant.message" , remaining_names )
0 commit comments