@@ -649,3 +649,302 @@ def test_emit_llo_attributes_with_session_id_and_other_attributes(self):
649
649
self .assertNotIn ("other.attribute" , emitted_event .attributes )
650
650
self .assertNotIn ("gen_ai.prompt" , emitted_event .attributes )
651
651
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