Skip to content

Commit 8fd4289

Browse files
nirgaclaude
andauthored
fix(vertexai): add missing role attributes when handling images (#3347)
Co-authored-by: Claude <[email protected]>
1 parent cf784d8 commit 8fd4289

File tree

2 files changed

+222
-0
lines changed

2 files changed

+222
-0
lines changed

packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,11 @@ async def set_input_attributes(span, args):
214214
processed_content = await _process_vertexai_argument(argument, span)
215215

216216
if processed_content:
217+
_set_span_attribute(
218+
span,
219+
f"{SpanAttributes.LLM_PROMPTS}.{arg_index}.role",
220+
"user"
221+
)
217222
_set_span_attribute(
218223
span,
219224
f"{SpanAttributes.LLM_PROMPTS}.{arg_index}.content",
@@ -233,6 +238,11 @@ def set_input_attributes_sync(span, args):
233238
processed_content = _process_vertexai_argument_sync(argument, span)
234239

235240
if processed_content:
241+
_set_span_attribute(
242+
span,
243+
f"{SpanAttributes.LLM_PROMPTS}.{arg_index}.role",
244+
"user"
245+
)
236246
_set_span_attribute(
237247
span,
238248
f"{SpanAttributes.LLM_PROMPTS}.{arg_index}.content",
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
import json
2+
import pytest
3+
from unittest.mock import Mock, patch
4+
from opentelemetry.instrumentation.vertexai.span_utils import (
5+
set_input_attributes,
6+
set_input_attributes_sync,
7+
)
8+
from opentelemetry.semconv_ai import SpanAttributes
9+
10+
11+
class TestRoleAttributes:
12+
"""Test cases for role attribute handling in VertexAI instrumentation"""
13+
14+
def setup_method(self):
15+
"""Setup test fixtures"""
16+
self.mock_span = Mock()
17+
self.mock_span.is_recording.return_value = True
18+
self.mock_span.context.trace_id = "test_trace_id"
19+
self.mock_span.context.span_id = "test_span_id"
20+
self.span_attributes = {}
21+
22+
# Mock set_attribute to capture the attributes
23+
def capture_attribute(key, value):
24+
self.span_attributes[key] = value
25+
26+
self.mock_span.set_attribute = capture_attribute
27+
28+
@patch("opentelemetry.instrumentation.vertexai.span_utils.should_send_prompts")
29+
@pytest.mark.asyncio
30+
async def test_async_role_attribute_for_string_args(self, mock_should_send_prompts):
31+
"""Test that role='user' is set for string arguments in async function"""
32+
mock_should_send_prompts.return_value = True
33+
34+
# Test with string arguments
35+
args = ["Hello, world!"]
36+
await set_input_attributes(self.mock_span, args)
37+
38+
# Verify role attribute is set
39+
assert f"{SpanAttributes.LLM_PROMPTS}.0.role" in self.span_attributes
40+
assert self.span_attributes[f"{SpanAttributes.LLM_PROMPTS}.0.role"] == "user"
41+
42+
# Verify content is also set
43+
assert f"{SpanAttributes.LLM_PROMPTS}.0.content" in self.span_attributes
44+
expected_content = json.dumps([{"type": "text", "text": "Hello, world!"}])
45+
assert self.span_attributes[f"{SpanAttributes.LLM_PROMPTS}.0.content"] == expected_content
46+
47+
@patch("opentelemetry.instrumentation.vertexai.span_utils.should_send_prompts")
48+
@pytest.mark.asyncio
49+
async def test_async_role_attribute_for_multiple_args(self, mock_should_send_prompts):
50+
"""Test that role='user' is set for multiple arguments in async function"""
51+
mock_should_send_prompts.return_value = True
52+
53+
# Test with multiple string arguments
54+
args = ["First message", "Second message"]
55+
await set_input_attributes(self.mock_span, args)
56+
57+
# Verify role attributes are set for both arguments
58+
assert f"{SpanAttributes.LLM_PROMPTS}.0.role" in self.span_attributes
59+
assert self.span_attributes[f"{SpanAttributes.LLM_PROMPTS}.0.role"] == "user"
60+
61+
assert f"{SpanAttributes.LLM_PROMPTS}.1.role" in self.span_attributes
62+
assert self.span_attributes[f"{SpanAttributes.LLM_PROMPTS}.1.role"] == "user"
63+
64+
@patch("opentelemetry.instrumentation.vertexai.span_utils.should_send_prompts")
65+
@pytest.mark.asyncio
66+
async def test_async_role_attribute_with_mixed_content(self, mock_should_send_prompts):
67+
"""Test role attribute with mixed content types (text + image placeholders)"""
68+
mock_should_send_prompts.return_value = True
69+
70+
# Test with list containing mixed content
71+
args = [["Text content", "More text"]]
72+
await set_input_attributes(self.mock_span, args)
73+
74+
# Verify role attribute is set
75+
assert f"{SpanAttributes.LLM_PROMPTS}.0.role" in self.span_attributes
76+
assert self.span_attributes[f"{SpanAttributes.LLM_PROMPTS}.0.role"] == "user"
77+
78+
@patch("opentelemetry.instrumentation.vertexai.span_utils.should_send_prompts")
79+
def test_sync_role_attribute_for_string_args(self, mock_should_send_prompts):
80+
"""Test that role='user' is set for string arguments in sync function"""
81+
mock_should_send_prompts.return_value = True
82+
83+
# Test with string arguments
84+
args = ["Hello, world!"]
85+
set_input_attributes_sync(self.mock_span, args)
86+
87+
# Verify role attribute is set
88+
assert f"{SpanAttributes.LLM_PROMPTS}.0.role" in self.span_attributes
89+
assert self.span_attributes[f"{SpanAttributes.LLM_PROMPTS}.0.role"] == "user"
90+
91+
# Verify content is also set
92+
assert f"{SpanAttributes.LLM_PROMPTS}.0.content" in self.span_attributes
93+
expected_content = json.dumps([{"type": "text", "text": "Hello, world!"}])
94+
assert self.span_attributes[f"{SpanAttributes.LLM_PROMPTS}.0.content"] == expected_content
95+
96+
@patch("opentelemetry.instrumentation.vertexai.span_utils.should_send_prompts")
97+
def test_sync_role_attribute_for_multiple_args(self, mock_should_send_prompts):
98+
"""Test that role='user' is set for multiple arguments in sync function"""
99+
mock_should_send_prompts.return_value = True
100+
101+
# Test with multiple string arguments
102+
args = ["First message", "Second message"]
103+
set_input_attributes_sync(self.mock_span, args)
104+
105+
# Verify role attributes are set for both arguments
106+
assert f"{SpanAttributes.LLM_PROMPTS}.0.role" in self.span_attributes
107+
assert self.span_attributes[f"{SpanAttributes.LLM_PROMPTS}.0.role"] == "user"
108+
109+
assert f"{SpanAttributes.LLM_PROMPTS}.1.role" in self.span_attributes
110+
assert self.span_attributes[f"{SpanAttributes.LLM_PROMPTS}.1.role"] == "user"
111+
112+
@patch("opentelemetry.instrumentation.vertexai.span_utils.should_send_prompts")
113+
def test_sync_role_attribute_with_mixed_content(self, mock_should_send_prompts):
114+
"""Test role attribute with mixed content types in sync function"""
115+
mock_should_send_prompts.return_value = True
116+
117+
# Test with list containing mixed content
118+
args = [["Text content", "More text"]]
119+
set_input_attributes_sync(self.mock_span, args)
120+
121+
# Verify role attribute is set
122+
assert f"{SpanAttributes.LLM_PROMPTS}.0.role" in self.span_attributes
123+
assert self.span_attributes[f"{SpanAttributes.LLM_PROMPTS}.0.role"] == "user"
124+
125+
@patch("opentelemetry.instrumentation.vertexai.span_utils.should_send_prompts")
126+
@pytest.mark.asyncio
127+
async def test_async_no_role_when_prompts_disabled(self, mock_should_send_prompts):
128+
"""Test that no role attributes are set when prompts are disabled"""
129+
mock_should_send_prompts.return_value = False
130+
131+
args = ["Hello, world!"]
132+
await set_input_attributes(self.mock_span, args)
133+
134+
# Verify no attributes are set when prompts are disabled
135+
assert len(self.span_attributes) == 0
136+
137+
@patch("opentelemetry.instrumentation.vertexai.span_utils.should_send_prompts")
138+
def test_sync_no_role_when_prompts_disabled(self, mock_should_send_prompts):
139+
"""Test that no role attributes are set when prompts are disabled in sync function"""
140+
mock_should_send_prompts.return_value = False
141+
142+
args = ["Hello, world!"]
143+
set_input_attributes_sync(self.mock_span, args)
144+
145+
# Verify no attributes are set when prompts are disabled
146+
assert len(self.span_attributes) == 0
147+
148+
@patch("opentelemetry.instrumentation.vertexai.span_utils.should_send_prompts")
149+
@pytest.mark.asyncio
150+
async def test_async_no_role_when_span_not_recording(self, mock_should_send_prompts):
151+
"""Test that no role attributes are set when span is not recording"""
152+
mock_should_send_prompts.return_value = True
153+
self.mock_span.is_recording.return_value = False
154+
155+
args = ["Hello, world!"]
156+
await set_input_attributes(self.mock_span, args)
157+
158+
# Verify no attributes are set when span is not recording
159+
assert len(self.span_attributes) == 0
160+
161+
@patch("opentelemetry.instrumentation.vertexai.span_utils.should_send_prompts")
162+
def test_sync_no_role_when_span_not_recording(self, mock_should_send_prompts):
163+
"""Test that no role attributes are set when span is not recording in sync function"""
164+
mock_should_send_prompts.return_value = True
165+
self.mock_span.is_recording.return_value = False
166+
167+
args = ["Hello, world!"]
168+
set_input_attributes_sync(self.mock_span, args)
169+
170+
# Verify no attributes are set when span is not recording
171+
assert len(self.span_attributes) == 0
172+
173+
@patch("opentelemetry.instrumentation.vertexai.span_utils.should_send_prompts")
174+
@pytest.mark.asyncio
175+
async def test_async_role_attribute_for_image_content(self, mock_should_send_prompts):
176+
"""Test that role='user' is set when processing arguments that contain images"""
177+
mock_should_send_prompts.return_value = True
178+
179+
# Create a mock image-like object (simplified for testing)
180+
mock_image_part = Mock()
181+
mock_image_part.mime_type = "image/jpeg"
182+
183+
# Test with mixed content including mock image
184+
args = [["Hello with image", mock_image_part]]
185+
await set_input_attributes(self.mock_span, args)
186+
187+
# Verify role attribute is set even when content includes images
188+
assert f"{SpanAttributes.LLM_PROMPTS}.0.role" in self.span_attributes
189+
assert self.span_attributes[f"{SpanAttributes.LLM_PROMPTS}.0.role"] == "user"
190+
191+
# Verify content is also set
192+
assert f"{SpanAttributes.LLM_PROMPTS}.0.content" in self.span_attributes
193+
194+
@patch("opentelemetry.instrumentation.vertexai.span_utils.should_send_prompts")
195+
def test_sync_role_attribute_for_image_content(self, mock_should_send_prompts):
196+
"""Test that role='user' is set when processing arguments that contain images in sync function"""
197+
mock_should_send_prompts.return_value = True
198+
199+
# Create a mock image-like object (simplified for testing)
200+
mock_image_part = Mock()
201+
mock_image_part.mime_type = "image/jpeg"
202+
203+
# Test with mixed content including mock image
204+
args = [["Hello with image", mock_image_part]]
205+
set_input_attributes_sync(self.mock_span, args)
206+
207+
# Verify role attribute is set even when content includes images
208+
assert f"{SpanAttributes.LLM_PROMPTS}.0.role" in self.span_attributes
209+
assert self.span_attributes[f"{SpanAttributes.LLM_PROMPTS}.0.role"] == "user"
210+
211+
# Verify content is also set
212+
assert f"{SpanAttributes.LLM_PROMPTS}.0.content" in self.span_attributes

0 commit comments

Comments
 (0)