Skip to content

Commit 5033da7

Browse files
committed
Emit system and assistant events
1 parent 6509727 commit 5033da7

File tree

5 files changed

+248
-10
lines changed

5 files changed

+248
-10
lines changed

instrumentation-genai/opentelemetry-instrumentation-vertexai/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
([#3192](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3192))
1212
- Initial VertexAI instrumentation
1313
([#3123](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3123))
14-
- VertexAI emit user events
14+
- VertexAI emit user, system, and assistant events
1515
([#3203](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3203))

instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai/events.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,47 @@ def user_event(
4545
},
4646
body=body,
4747
)
48+
49+
50+
def assistant_event(
51+
*,
52+
role: str = "assistant",
53+
content: AnyValue = None,
54+
) -> Event:
55+
"""Creates a User event
56+
https://github.com/open-telemetry/semantic-conventions/blob/v1.28.0/docs/gen-ai/gen-ai-events.md#assistant-event
57+
"""
58+
body: dict[str, AnyValue] = {
59+
"role": role,
60+
}
61+
if content is not None:
62+
body["content"] = content
63+
return Event(
64+
name="gen_ai.assistant.message",
65+
attributes={
66+
gen_ai_attributes.GEN_AI_SYSTEM: gen_ai_attributes.GenAiSystemValues.VERTEX_AI.value,
67+
},
68+
body=body,
69+
)
70+
71+
72+
def system_event(
73+
*,
74+
role: str = "system",
75+
content: AnyValue = None,
76+
) -> Event:
77+
"""Creates a User event
78+
https://github.com/open-telemetry/semantic-conventions/blob/v1.28.0/docs/gen-ai/gen-ai-events.md#system-event
79+
"""
80+
body: dict[str, AnyValue] = {
81+
"role": role,
82+
}
83+
if content is not None:
84+
body["content"] = content
85+
return Event(
86+
name="gen_ai.system.message",
87+
attributes={
88+
gen_ai_attributes.GEN_AI_SYSTEM: gen_ai_attributes.GenAiSystemValues.VERTEX_AI.value,
89+
},
90+
body=body,
91+
)

instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai/utils.py

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@
2626
)
2727

2828
from opentelemetry._events import Event
29-
from opentelemetry.instrumentation.vertexai.events import user_event
29+
from opentelemetry.instrumentation.vertexai.events import (
30+
assistant_event,
31+
system_event,
32+
user_event,
33+
)
3034
from opentelemetry.semconv._incubating.attributes import (
3135
gen_ai_attributes as GenAIAttributes,
3236
)
@@ -146,16 +150,41 @@ def get_span_name(span_attributes: Mapping[str, AttributeValue]) -> str:
146150
def request_to_events(
147151
*, params: GenerateContentParams, capture_content: bool
148152
) -> Iterable[Event]:
153+
# System message
154+
if params.system_instruction:
155+
request_content = _parts_to_any_value(
156+
capture_content=capture_content,
157+
parts=params.system_instruction.parts,
158+
)
159+
yield system_event(
160+
role=params.system_instruction.role, content=request_content
161+
)
162+
149163
for content in params.contents or []:
164+
# Assistant message
150165
if content.role == "model":
151-
# TODO: handle assistant message
152-
pass
166+
request_content = _parts_to_any_value(
167+
capture_content=capture_content, parts=content.parts
168+
)
169+
170+
yield assistant_event(role=content.role, content=request_content)
153171
# Assume user event but role should be "user"
154172
else:
155-
request_content = None
156-
if capture_content:
157-
request_content = [
158-
cast(dict[str, AnyValue], type(part).to_dict(part)) # type: ignore[reportUnknownMemberType]
159-
for part in content.parts
160-
]
173+
request_content = _parts_to_any_value(
174+
capture_content=capture_content, parts=content.parts
175+
)
161176
yield user_event(role=content.role, content=request_content)
177+
178+
179+
def _parts_to_any_value(
180+
*,
181+
capture_content: bool,
182+
parts: Sequence[content.Part] | Sequence[content_v1beta1.Part],
183+
) -> list[dict[str, AnyValue]] | None:
184+
if not capture_content:
185+
return None
186+
187+
return [
188+
cast(dict[str, AnyValue], type(part).to_dict(part)) # type: ignore[reportUnknownMemberType]
189+
for part in parts
190+
]
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
interactions:
2+
- request:
3+
body: |-
4+
{
5+
"contents": [
6+
{
7+
"role": "user",
8+
"parts": [
9+
{
10+
"text": "My name is OpenTelemetry"
11+
}
12+
]
13+
},
14+
{
15+
"role": "model",
16+
"parts": [
17+
{
18+
"text": "Hello OpenTelemetry!"
19+
}
20+
]
21+
},
22+
{
23+
"role": "user",
24+
"parts": [
25+
{
26+
"text": "Address me by name and say this is a test"
27+
}
28+
]
29+
}
30+
],
31+
"systemInstruction": {
32+
"role": "user",
33+
"parts": [
34+
{
35+
"text": "You are a clever language model"
36+
}
37+
]
38+
}
39+
}
40+
headers:
41+
Accept:
42+
- '*/*'
43+
Accept-Encoding:
44+
- gzip, deflate
45+
Connection:
46+
- keep-alive
47+
Content-Length:
48+
- '548'
49+
Content-Type:
50+
- application/json
51+
User-Agent:
52+
- python-requests/2.32.3
53+
method: POST
54+
uri: https://us-central1-aiplatform.googleapis.com/v1/projects/fake-project/locations/us-central1/publishers/google/models/gemini-1.5-flash-002:generateContent?%24alt=json%3Benum-encoding%3Dint
55+
response:
56+
body:
57+
string: |-
58+
{
59+
"candidates": [
60+
{
61+
"content": {
62+
"role": "model",
63+
"parts": [
64+
{
65+
"text": "OpenTelemetry, this is a test.\n"
66+
}
67+
]
68+
},
69+
"finishReason": 1,
70+
"avgLogprobs": -1.1655389850299496e-06
71+
}
72+
],
73+
"usageMetadata": {
74+
"promptTokenCount": 25,
75+
"candidatesTokenCount": 9,
76+
"totalTokenCount": 34
77+
},
78+
"modelVersion": "gemini-1.5-flash-002"
79+
}
80+
headers:
81+
Content-Type:
82+
- application/json; charset=UTF-8
83+
Transfer-Encoding:
84+
- chunked
85+
Vary:
86+
- Origin
87+
- X-Origin
88+
- Referer
89+
content-length:
90+
- '422'
91+
status:
92+
code: 200
93+
message: OK
94+
version: 1

instrumentation-genai/opentelemetry-instrumentation-vertexai/tests/test_chat_completions.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,3 +259,74 @@ def assert_span_error(span: ReadableSpan) -> None:
259259
# Records exception event
260260
error_events = [e for e in span.events if e.name == "exception"]
261261
assert error_events != []
262+
263+
264+
@pytest.mark.vcr
265+
def test_generate_content_all_input_events(
266+
log_exporter: InMemoryLogExporter,
267+
instrument_with_content: VertexAIInstrumentor,
268+
):
269+
model = GenerativeModel(
270+
"gemini-1.5-flash-002",
271+
system_instruction=Part.from_text("You are a clever language model"),
272+
)
273+
model.generate_content(
274+
[
275+
Content(
276+
role="user", parts=[Part.from_text("My name is OpenTelemetry")]
277+
),
278+
Content(
279+
role="model", parts=[Part.from_text("Hello OpenTelemetry!")]
280+
),
281+
Content(
282+
role="user",
283+
parts=[
284+
Part.from_text("Address me by name and say this is a test")
285+
],
286+
),
287+
],
288+
)
289+
290+
# Emits a system event, 2 users events, and a assistant event
291+
logs = log_exporter.get_finished_logs()
292+
assert len(logs) == 4
293+
system_log, user_log1, assistant_log, user_log2 = [
294+
log_data.log_record for log_data in logs
295+
]
296+
297+
assert system_log.attributes == {
298+
"gen_ai.system": "vertex_ai",
299+
"event.name": "gen_ai.system.message",
300+
}
301+
assert system_log.body == {
302+
"content": [{"text": "You are a clever language model"}],
303+
# The API only allows user and model, so system instruction is considered a user role
304+
"role": "user",
305+
}
306+
307+
assert user_log1.attributes == {
308+
"gen_ai.system": "vertex_ai",
309+
"event.name": "gen_ai.user.message",
310+
}
311+
assert user_log1.body == {
312+
"content": [{"text": "My name is OpenTelemetry"}],
313+
"role": "user",
314+
}
315+
316+
assert assistant_log.attributes == {
317+
"gen_ai.system": "vertex_ai",
318+
"event.name": "gen_ai.assistant.message",
319+
}
320+
assert assistant_log.body == {
321+
"content": [{"text": "Hello OpenTelemetry!"}],
322+
"role": "model",
323+
}
324+
325+
assert user_log2.attributes == {
326+
"gen_ai.system": "vertex_ai",
327+
"event.name": "gen_ai.user.message",
328+
}
329+
assert user_log2.body == {
330+
"content": [{"text": "Address me by name and say this is a test"}],
331+
"role": "user",
332+
}

0 commit comments

Comments
 (0)