1
+ """
2
+ Test for the fix of the issue where assistant message content is missing
3
+ when tool calls are present in LangGraph/LangChain instrumentation.
4
+
5
+ This test reproduces the issue reported in GitHub where gen_ai.prompt.X.content
6
+ attributes were missing for assistant messages that contained tool_calls.
7
+ """
8
+
9
+ import pytest
10
+ from unittest .mock import Mock
11
+ from langchain_core .messages import AIMessage , HumanMessage , ToolMessage
12
+ from opentelemetry .instrumentation .langchain .span_utils import set_chat_request
13
+ from opentelemetry .semconv_ai import SpanAttributes
14
+
15
+
16
+ def test_assistant_message_with_tool_calls_includes_content ():
17
+ """
18
+ Test that when an assistant message has both content and tool_calls,
19
+ both the content and tool_calls are included in the span attributes.
20
+
21
+ This addresses the issue where content was missing when tool_calls were present.
22
+ """
23
+ # Create a mock span
24
+ mock_span = Mock ()
25
+ mock_span .set_attribute = Mock ()
26
+
27
+ # Create a mock span_holder
28
+ mock_span_holder = Mock ()
29
+ mock_span_holder .request_model = None
30
+
31
+ # Create messages that reproduce the issue:
32
+ # 1. User message
33
+ # 2. Assistant message with BOTH content AND tool_calls
34
+ messages = [[
35
+ HumanMessage (content = "what is the current time? First greet me." ),
36
+ AIMessage (
37
+ content = "Hello! Let me check the current time for you." ,
38
+ tool_calls = [{
39
+ 'id' : 'call_qU7pH3EdQvzwkPyKPOdpgaKA' ,
40
+ 'name' : 'get_current_time' ,
41
+ 'args' : {}
42
+ }]
43
+ ),
44
+ ToolMessage (
45
+ content = "2025-08-15 08:15:21" ,
46
+ tool_call_id = "call_qU7pH3EdQvzwkPyKPOdpgaKA"
47
+ ),
48
+ AIMessage (content = "The current time is 2025-08-15 08:15:21" )
49
+ ]]
50
+
51
+ # Call the function that was previously buggy
52
+ set_chat_request (mock_span , {}, messages , {}, mock_span_holder )
53
+
54
+ # Verify that set_attribute was called with the expected attributes
55
+ call_args = [call [0 ] for call in mock_span .set_attribute .call_args_list ]
56
+
57
+ # Extract all attribute names and values
58
+ attributes = {args [0 ]: args [1 ] for args in call_args }
59
+
60
+ # Check user message (prompt.0)
61
+ assert f"{ SpanAttributes .LLM_PROMPTS } .0.role" in attributes
62
+ assert attributes [f"{ SpanAttributes .LLM_PROMPTS } .0.role" ] == "user"
63
+ assert f"{ SpanAttributes .LLM_PROMPTS } .0.content" in attributes
64
+ assert attributes [f"{ SpanAttributes .LLM_PROMPTS } .0.content" ] == "what is the current time? First greet me."
65
+
66
+ # Check assistant message with tool calls (prompt.1)
67
+ # This is the critical test - BOTH content AND tool_calls should be present
68
+ assert f"{ SpanAttributes .LLM_PROMPTS } .1.role" in attributes
69
+ assert attributes [f"{ SpanAttributes .LLM_PROMPTS } .1.role" ] == "assistant"
70
+
71
+ # The fix should ensure that content is present even when tool_calls exist
72
+ assert f"{ SpanAttributes .LLM_PROMPTS } .1.content" in attributes
73
+ assert attributes [f"{ SpanAttributes .LLM_PROMPTS } .1.content" ] == "Hello! Let me check the current time for you."
74
+
75
+ # Tool calls should also be present
76
+ assert f"{ SpanAttributes .LLM_PROMPTS } .1.tool_calls.0.id" in attributes
77
+ assert attributes [f"{ SpanAttributes .LLM_PROMPTS } .1.tool_calls.0.id" ] == "call_qU7pH3EdQvzwkPyKPOdpgaKA"
78
+ assert f"{ SpanAttributes .LLM_PROMPTS } .1.tool_calls.0.name" in attributes
79
+ assert attributes [f"{ SpanAttributes .LLM_PROMPTS } .1.tool_calls.0.name" ] == "get_current_time"
80
+
81
+ # Check tool message (prompt.2)
82
+ assert f"{ SpanAttributes .LLM_PROMPTS } .2.role" in attributes
83
+ assert attributes [f"{ SpanAttributes .LLM_PROMPTS } .2.role" ] == "tool"
84
+ assert f"{ SpanAttributes .LLM_PROMPTS } .2.content" in attributes
85
+ assert attributes [f"{ SpanAttributes .LLM_PROMPTS } .2.content" ] == "2025-08-15 08:15:21"
86
+ assert f"{ SpanAttributes .LLM_PROMPTS } .2.tool_call_id" in attributes
87
+ assert attributes [f"{ SpanAttributes .LLM_PROMPTS } .2.tool_call_id" ] == "call_qU7pH3EdQvzwkPyKPOdpgaKA"
88
+
89
+ # Check final assistant message (prompt.3)
90
+ assert f"{ SpanAttributes .LLM_PROMPTS } .3.role" in attributes
91
+ assert attributes [f"{ SpanAttributes .LLM_PROMPTS } .3.role" ] == "assistant"
92
+ assert f"{ SpanAttributes .LLM_PROMPTS } .3.content" in attributes
93
+ assert attributes [f"{ SpanAttributes .LLM_PROMPTS } .3.content" ] == "The current time is 2025-08-15 08:15:21"
94
+
95
+
96
+ def test_assistant_message_with_only_tool_calls_no_content ():
97
+ """
98
+ Test that when an assistant message has only tool_calls and no content,
99
+ the tool_calls are still included and no content attribute is set.
100
+ """
101
+ # Create a mock span
102
+ mock_span = Mock ()
103
+ mock_span .set_attribute = Mock ()
104
+
105
+ # Create a mock span_holder
106
+ mock_span_holder = Mock ()
107
+ mock_span_holder .request_model = None
108
+
109
+ # Create message with only tool_calls, no content
110
+ messages = [[
111
+ AIMessage (
112
+ content = "" , # Empty content
113
+ tool_calls = [{
114
+ 'id' : 'call_123' ,
115
+ 'name' : 'some_tool' ,
116
+ 'args' : {'param' : 'value' }
117
+ }]
118
+ )
119
+ ]]
120
+
121
+ # Call the function
122
+ set_chat_request (mock_span , {}, messages , {}, mock_span_holder )
123
+
124
+ # Verify that set_attribute was called with the expected attributes
125
+ call_args = [call [0 ] for call in mock_span .set_attribute .call_args_list ]
126
+
127
+ # Extract all attribute names and values
128
+ attributes = {args [0 ]: args [1 ] for args in call_args }
129
+
130
+ # Check assistant message
131
+ assert f"{ SpanAttributes .LLM_PROMPTS } .0.role" in attributes
132
+ assert attributes [f"{ SpanAttributes .LLM_PROMPTS } .0.role" ] == "assistant"
133
+
134
+ # Content should NOT be set when it's empty (due to _set_span_attribute logic)
135
+ # This is expected behavior to avoid cluttering spans with empty values
136
+ assert f"{ SpanAttributes .LLM_PROMPTS } .0.content" not in attributes
137
+
138
+ # Tool calls should be present
139
+ assert f"{ SpanAttributes .LLM_PROMPTS } .0.tool_calls.0.id" in attributes
140
+ assert attributes [f"{ SpanAttributes .LLM_PROMPTS } .0.tool_calls.0.id" ] == "call_123"
141
+ assert f"{ SpanAttributes .LLM_PROMPTS } .0.tool_calls.0.name" in attributes
142
+ assert attributes [f"{ SpanAttributes .LLM_PROMPTS } .0.tool_calls.0.name" ] == "some_tool"
143
+
144
+
145
+ def test_assistant_message_with_only_content_no_tool_calls ():
146
+ """
147
+ Test that when an assistant message has only content and no tool_calls,
148
+ the content is included and no tool_calls attributes are set.
149
+ """
150
+ # Create a mock span
151
+ mock_span = Mock ()
152
+ mock_span .set_attribute = Mock ()
153
+
154
+ # Create a mock span_holder
155
+ mock_span_holder = Mock ()
156
+ mock_span_holder .request_model = None
157
+
158
+ # Create message with only content, no tool_calls
159
+ messages = [[
160
+ AIMessage (content = "Just a regular response with no tool calls" )
161
+ ]]
162
+
163
+ # Call the function
164
+ set_chat_request (mock_span , {}, messages , {}, mock_span_holder )
165
+
166
+ # Verify that set_attribute was called with the expected attributes
167
+ call_args = [call [0 ] for call in mock_span .set_attribute .call_args_list ]
168
+
169
+ # Extract all attribute names and values
170
+ attributes = {args [0 ]: args [1 ] for args in call_args }
171
+
172
+ # Check assistant message
173
+ assert f"{ SpanAttributes .LLM_PROMPTS } .0.role" in attributes
174
+ assert attributes [f"{ SpanAttributes .LLM_PROMPTS } .0.role" ] == "assistant"
175
+
176
+ # Content should be present
177
+ assert f"{ SpanAttributes .LLM_PROMPTS } .0.content" in attributes
178
+ assert attributes [f"{ SpanAttributes .LLM_PROMPTS } .0.content" ] == "Just a regular response with no tool calls"
179
+
180
+ # No tool call attributes should be present
181
+ tool_call_attributes = [attr for attr in attributes .keys () if "tool_calls" in attr ]
182
+ assert len (tool_call_attributes ) == 0
0 commit comments