Skip to content

Commit e164c54

Browse files
committed
VertexAI emit user events
1 parent ec3c51d commit e164c54

File tree

6 files changed

+315
-8
lines changed

6 files changed

+315
-8
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""
16+
Factories for event types described in
17+
https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-events.md#system-event.
18+
19+
Hopefully this code can be autogenerated by Weaver once Gen AI semantic conventions are
20+
schematized in YAML and the Weaver tool supports it.
21+
"""
22+
23+
from opentelemetry._events import Event
24+
from opentelemetry.semconv._incubating.attributes import gen_ai_attributes
25+
from opentelemetry.util.types import AnyValue
26+
27+
28+
def user_event(
29+
*,
30+
role: str = "user",
31+
content: AnyValue = None,
32+
) -> Event:
33+
"""Creates a User event
34+
https://github.com/open-telemetry/semantic-conventions/blob/v1.28.0/docs/gen-ai/gen-ai-events.md#user-event
35+
"""
36+
body: dict[str, AnyValue] = {
37+
"role": role,
38+
}
39+
if content is not None:
40+
body["content"] = content
41+
return Event(
42+
name="gen_ai.user.message",
43+
attributes={
44+
gen_ai_attributes.GEN_AI_SYSTEM: gen_ai_attributes.GenAiSystemValues.VERTEX_AI.value,
45+
},
46+
body=body,
47+
)
48+
49+
50+
def assistant_event(
51+
*,
52+
role: str = "model",
53+
content: AnyValue = None,
54+
) -> Event:
55+
"""Creates an Assistant event
56+
https://github.com/open-telemetry/semantic-conventions/blob/v1.28.0/docs/gen-ai/gen-ai-events.md#assistant-event
57+
"""
58+
# TODO: add tool_calls once instrumentation supports it
59+
60+
return Event(
61+
name="gen_ai.assistant.message",
62+
attributes={
63+
gen_ai_attributes.GEN_AI_SYSTEM: gen_ai_attributes.GenAiSystemValues.VERTEX_AI.value,
64+
},
65+
body={
66+
"role": role,
67+
"content": content,
68+
},
69+
)

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
GenerateContentParams,
2727
get_genai_request_attributes,
2828
get_span_name,
29+
request_to_events,
2930
)
3031
from opentelemetry.trace import SpanKind, Tracer
3132

@@ -107,13 +108,12 @@ def traced_method(
107108
name=span_name,
108109
kind=SpanKind.CLIENT,
109110
attributes=span_attributes,
110-
) as _span:
111-
# TODO: emit request events
112-
# if span.is_recording():
113-
# for message in kwargs.get("messages", []):
114-
# event_logger.emit(
115-
# message_to_event(message, capture_content)
116-
# )
111+
) as span:
112+
if span.is_recording():
113+
for event in request_to_events(
114+
params=params, capture_content=capture_content
115+
):
116+
event_logger.emit(event)
117117

118118
# TODO: set error.type attribute
119119
# https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-spans.md

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

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,22 @@
1616

1717
import re
1818
from dataclasses import dataclass
19+
from enum import Enum
1920
from os import environ
2021
from typing import (
2122
TYPE_CHECKING,
23+
Iterable,
2224
Mapping,
2325
Sequence,
26+
cast,
2427
)
2528

29+
from opentelemetry._events import Event
30+
from opentelemetry.instrumentation.vertexai.events import user_event
2631
from opentelemetry.semconv._incubating.attributes import (
2732
gen_ai_attributes as GenAIAttributes,
2833
)
29-
from opentelemetry.util.types import AttributeValue
34+
from opentelemetry.util.types import AnyValue, AttributeValue
3035

3136
if TYPE_CHECKING:
3237
from google.cloud.aiplatform_v1.types import content, tool
@@ -137,3 +142,21 @@ def get_span_name(span_attributes: Mapping[str, AttributeValue]) -> str:
137142
if not model:
138143
return f"{name}"
139144
return f"{name} {model}"
145+
146+
147+
def request_to_events(
148+
*, params: GenerateContentParams, capture_content: bool
149+
) -> Iterable[Event]:
150+
for content in params.contents or []:
151+
if content.role == "model":
152+
# TODO: handle assistant message
153+
pass
154+
# Assume user event but role should be "user"
155+
else:
156+
request_content = None
157+
if capture_content:
158+
request_content = [
159+
cast(dict[str, AnyValue], type(part).to_dict(part)) # type: ignore[reportUnknownMemberType]
160+
for part in content.parts
161+
]
162+
yield user_event(role=content.role, content=request_content)
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
interactions:
2+
- request:
3+
body: |-
4+
{
5+
"contents": [
6+
{
7+
"role": "invalid_role",
8+
"parts": [
9+
{
10+
"text": "Say this is a test"
11+
}
12+
]
13+
}
14+
]
15+
}
16+
headers:
17+
Accept:
18+
- '*/*'
19+
Accept-Encoding:
20+
- gzip, deflate
21+
Connection:
22+
- keep-alive
23+
Content-Length:
24+
- '149'
25+
Content-Type:
26+
- application/json
27+
User-Agent:
28+
- python-requests/2.32.3
29+
method: POST
30+
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
31+
response:
32+
body:
33+
string: |-
34+
{
35+
"error": {
36+
"code": 400,
37+
"message": "Please use a valid role: user, model.",
38+
"status": "INVALID_ARGUMENT",
39+
"details": []
40+
}
41+
}
42+
headers:
43+
Content-Type:
44+
- application/json; charset=UTF-8
45+
Transfer-Encoding:
46+
- chunked
47+
Vary:
48+
- Origin
49+
- X-Origin
50+
- Referer
51+
content-length:
52+
- '416'
53+
status:
54+
code: 400
55+
message: Bad Request
56+
version: 1
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
interactions:
2+
- request:
3+
body: |-
4+
{
5+
"contents": [
6+
{
7+
"role": "user",
8+
"parts": [
9+
{
10+
"text": "Say this is a test"
11+
}
12+
]
13+
}
14+
]
15+
}
16+
headers:
17+
Accept:
18+
- '*/*'
19+
Accept-Encoding:
20+
- gzip, deflate
21+
Connection:
22+
- keep-alive
23+
Content-Length:
24+
- '141'
25+
Content-Type:
26+
- application/json
27+
User-Agent:
28+
- python-requests/2.32.3
29+
method: POST
30+
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
31+
response:
32+
body:
33+
string: |-
34+
{
35+
"candidates": [
36+
{
37+
"content": {
38+
"role": "model",
39+
"parts": [
40+
{
41+
"text": "Okay, I understand. I'm ready for your test. Please proceed.\n"
42+
}
43+
]
44+
},
45+
"finishReason": 1,
46+
"avgLogprobs": -0.005519990466142956
47+
}
48+
],
49+
"usageMetadata": {
50+
"promptTokenCount": 5,
51+
"candidatesTokenCount": 19,
52+
"totalTokenCount": 24
53+
},
54+
"modelVersion": "gemini-1.5-flash-002"
55+
}
56+
headers:
57+
Content-Type:
58+
- application/json; charset=UTF-8
59+
Transfer-Encoding:
60+
- chunked
61+
Vary:
62+
- Origin
63+
- X-Origin
64+
- Referer
65+
content-length:
66+
- '453'
67+
status:
68+
code: 200
69+
message: OK
70+
version: 1

0 commit comments

Comments
 (0)