@@ -817,3 +817,144 @@ def test_langchain_integration_with_langchain_core_only(sentry_init, capture_eve
817817 assert llm_span ["data" ]["gen_ai.usage.total_tokens" ] == 25
818818 assert llm_span ["data" ]["gen_ai.usage.input_tokens" ] == 10
819819 assert llm_span ["data" ]["gen_ai.usage.output_tokens" ] == 15
820+
821+
822+ def test_langchain_message_role_mapping (sentry_init , capture_events ):
823+ """Test that message roles are properly normalized in langchain integration."""
824+ global llm_type
825+ llm_type = "openai-chat"
826+
827+ sentry_init (
828+ integrations = [LangchainIntegration (include_prompts = True )],
829+ traces_sample_rate = 1.0 ,
830+ send_default_pii = True ,
831+ )
832+ events = capture_events ()
833+
834+ prompt = ChatPromptTemplate .from_messages (
835+ [
836+ ("system" , "You are a helpful assistant" ),
837+ ("human" , "{input}" ),
838+ MessagesPlaceholder (variable_name = "agent_scratchpad" ),
839+ ]
840+ )
841+
842+ global stream_result_mock
843+ stream_result_mock = Mock (
844+ side_effect = [
845+ [
846+ ChatGenerationChunk (
847+ type = "ChatGenerationChunk" ,
848+ message = AIMessageChunk (content = "Test response" ),
849+ ),
850+ ]
851+ ]
852+ )
853+
854+ llm = MockOpenAI (
855+ model_name = "gpt-3.5-turbo" ,
856+ temperature = 0 ,
857+ openai_api_key = "badkey" ,
858+ )
859+ agent = create_openai_tools_agent (llm , [get_word_length ], prompt )
860+ agent_executor = AgentExecutor (agent = agent , tools = [get_word_length ], verbose = True )
861+
862+ # Test input that should trigger message role normalization
863+ test_input = "Hello, how are you?"
864+
865+ with start_transaction ():
866+ list (agent_executor .stream ({"input" : test_input }))
867+
868+ assert len (events ) > 0
869+ tx = events [0 ]
870+ assert tx ["type" ] == "transaction"
871+
872+ # Find spans with gen_ai operation that should have message data
873+ gen_ai_spans = [
874+ span for span in tx .get ("spans" , []) if span .get ("op" , "" ).startswith ("gen_ai" )
875+ ]
876+
877+ # Check if any span has message data with normalized roles
878+ message_data_found = False
879+ for span in gen_ai_spans :
880+ span_data = span .get ("data" , {})
881+ if SPANDATA .GEN_AI_REQUEST_MESSAGES in span_data :
882+ message_data_found = True
883+ messages_data = span_data [SPANDATA .GEN_AI_REQUEST_MESSAGES ]
884+
885+ # Parse the message data (might be JSON string)
886+ if isinstance (messages_data , str ):
887+ import json
888+
889+ try :
890+ messages = json .loads (messages_data )
891+ except json .JSONDecodeError :
892+ # If not valid JSON, skip this assertion
893+ continue
894+ else :
895+ messages = messages_data
896+
897+ # Verify that the input message is present and contains the test input
898+ assert isinstance (messages , list )
899+ assert len (messages ) > 0
900+
901+ # The test input should be in one of the messages
902+ input_found = False
903+ for msg in messages :
904+ if isinstance (msg , dict ) and test_input in str (msg .get ("content" , "" )):
905+ input_found = True
906+ break
907+ elif isinstance (msg , str ) and test_input in msg :
908+ input_found = True
909+ break
910+
911+ assert input_found , (
912+ f"Test input '{ test_input } ' not found in messages: { messages } "
913+ )
914+ break
915+
916+ # The message role mapping functionality is primarily tested through the normalization
917+ # that happens in the integration code. The fact that we can capture and process
918+ # the messages without errors indicates the role mapping is working correctly.
919+ assert message_data_found , "No span found with gen_ai request messages data"
920+
921+
922+ def test_langchain_message_role_normalization_units ():
923+ """Test the message role normalization functions directly."""
924+ from sentry_sdk .ai .utils import normalize_message_role , normalize_message_roles
925+
926+ # Test individual role normalization
927+ assert normalize_message_role ("ai" ) == "assistant"
928+ assert normalize_message_role ("human" ) == "user"
929+ assert normalize_message_role ("tool_call" ) == "tool"
930+ assert normalize_message_role ("system" ) == "system"
931+ assert normalize_message_role ("user" ) == "user"
932+ assert normalize_message_role ("assistant" ) == "assistant"
933+ assert normalize_message_role ("tool" ) == "tool"
934+
935+ # Test unknown role (should remain unchanged)
936+ assert normalize_message_role ("unknown_role" ) == "unknown_role"
937+
938+ # Test message list normalization
939+ test_messages = [
940+ {"role" : "human" , "content" : "Hello" },
941+ {"role" : "ai" , "content" : "Hi there!" },
942+ {"role" : "tool_call" , "content" : "function_call" },
943+ {"role" : "system" , "content" : "You are helpful" },
944+ {"content" : "Message without role" },
945+ "string message" ,
946+ ]
947+
948+ normalized = normalize_message_roles (test_messages )
949+
950+ # Verify the original messages are not modified
951+ assert test_messages [0 ]["role" ] == "human" # Original unchanged
952+ assert test_messages [1 ]["role" ] == "ai" # Original unchanged
953+
954+ # Verify the normalized messages have correct roles
955+ assert normalized [0 ]["role" ] == "user" # human -> user
956+ assert normalized [1 ]["role" ] == "assistant" # ai -> assistant
957+ assert normalized [2 ]["role" ] == "tool" # tool_call -> tool
958+ assert normalized [3 ]["role" ] == "system" # system unchanged
959+ assert "role" not in normalized [4 ] # Message without role unchanged
960+ assert normalized [5 ] == "string message" # String message unchanged
0 commit comments