opentelemetry-instrumentation-vertexai 0.47.3__py3-none-any.whl → 2.1b0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of opentelemetry-instrumentation-vertexai might be problematic. Click here for more details.

@@ -0,0 +1,190 @@
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
+ # type: ignore[reportUnknownDeprecated]
16
+
17
+ """
18
+ Factories for event types described in
19
+ https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-events.md#system-event.
20
+
21
+ Hopefully this code can be autogenerated by Weaver once Gen AI semantic conventions are
22
+ schematized in YAML and the Weaver tool supports it.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from dataclasses import asdict, dataclass
28
+ from typing import Any, Iterable, Literal
29
+
30
+ from opentelemetry._logs import LogRecord
31
+ from opentelemetry.semconv._incubating.attributes import gen_ai_attributes
32
+ from opentelemetry.util.types import AnyValue
33
+
34
+
35
+ def user_event(
36
+ *,
37
+ role: str = "user",
38
+ content: AnyValue = None,
39
+ ) -> LogRecord:
40
+ """Creates a User event
41
+ https://github.com/open-telemetry/semantic-conventions/blob/v1.28.0/docs/gen-ai/gen-ai-events.md#user-event
42
+ """
43
+ body: dict[str, AnyValue] = {
44
+ "role": role,
45
+ }
46
+ if content is not None:
47
+ body["content"] = content
48
+ return LogRecord(
49
+ event_name="gen_ai.user.message",
50
+ attributes={
51
+ gen_ai_attributes.GEN_AI_SYSTEM: gen_ai_attributes.GenAiSystemValues.VERTEX_AI.value,
52
+ },
53
+ body=body,
54
+ )
55
+
56
+
57
+ def assistant_event(
58
+ *,
59
+ role: str = "assistant",
60
+ content: AnyValue = None,
61
+ ) -> LogRecord:
62
+ """Creates an Assistant event
63
+ https://github.com/open-telemetry/semantic-conventions/blob/v1.28.0/docs/gen-ai/gen-ai-events.md#assistant-event
64
+ """
65
+ body: dict[str, AnyValue] = {
66
+ "role": role,
67
+ }
68
+ if content is not None:
69
+ body["content"] = content
70
+ return LogRecord(
71
+ event_name="gen_ai.assistant.message",
72
+ attributes={
73
+ gen_ai_attributes.GEN_AI_SYSTEM: gen_ai_attributes.GenAiSystemValues.VERTEX_AI.value,
74
+ },
75
+ body=body,
76
+ )
77
+
78
+
79
+ def system_event(
80
+ *,
81
+ role: str = "system",
82
+ content: AnyValue = None,
83
+ ) -> LogRecord:
84
+ """Creates a System event
85
+ https://github.com/open-telemetry/semantic-conventions/blob/v1.28.0/docs/gen-ai/gen-ai-events.md#system-event
86
+ """
87
+ body: dict[str, AnyValue] = {
88
+ "role": role,
89
+ }
90
+ if content is not None:
91
+ body["content"] = content
92
+ return LogRecord(
93
+ event_name="gen_ai.system.message",
94
+ attributes={
95
+ gen_ai_attributes.GEN_AI_SYSTEM: gen_ai_attributes.GenAiSystemValues.VERTEX_AI.value,
96
+ },
97
+ body=body,
98
+ )
99
+
100
+
101
+ def tool_event(
102
+ *,
103
+ role: str | None,
104
+ id_: str,
105
+ content: AnyValue = None,
106
+ ) -> LogRecord:
107
+ """Creates a Tool message event
108
+ https://github.com/open-telemetry/semantic-conventions/blob/v1.28.0/docs/gen-ai/gen-ai-events.md#event-gen_aitoolmessage
109
+ """
110
+ if not role:
111
+ role = "tool"
112
+
113
+ body: dict[str, AnyValue] = {
114
+ "role": role,
115
+ "id": id_,
116
+ }
117
+ if content is not None:
118
+ body["content"] = content
119
+ return LogRecord(
120
+ event_name="gen_ai.tool.message",
121
+ attributes={
122
+ gen_ai_attributes.GEN_AI_SYSTEM: gen_ai_attributes.GenAiSystemValues.VERTEX_AI.value,
123
+ },
124
+ body=body,
125
+ )
126
+
127
+
128
+ @dataclass
129
+ class ChoiceMessage:
130
+ """The message field for a gen_ai.choice event"""
131
+
132
+ content: AnyValue = None
133
+ role: str = "assistant"
134
+
135
+
136
+ @dataclass
137
+ class ChoiceToolCall:
138
+ """The tool_calls field for a gen_ai.choice event"""
139
+
140
+ @dataclass
141
+ class Function:
142
+ name: str
143
+ arguments: AnyValue = None
144
+
145
+ function: Function
146
+ id: str
147
+ type: Literal["function"] = "function"
148
+
149
+
150
+ FinishReason = Literal[
151
+ "content_filter", "error", "length", "stop", "tool_calls"
152
+ ]
153
+
154
+
155
+ def choice_event(
156
+ *,
157
+ finish_reason: FinishReason | str,
158
+ index: int,
159
+ message: ChoiceMessage,
160
+ tool_calls: Iterable[ChoiceToolCall] = (),
161
+ ) -> LogRecord:
162
+ """Creates a choice event, which describes the Gen AI response message.
163
+ https://github.com/open-telemetry/semantic-conventions/blob/v1.28.0/docs/gen-ai/gen-ai-events.md#event-gen_aichoice
164
+ """
165
+ body: dict[str, AnyValue] = {
166
+ "finish_reason": finish_reason,
167
+ "index": index,
168
+ "message": _asdict_filter_nulls(message),
169
+ }
170
+
171
+ tool_calls_list = [
172
+ _asdict_filter_nulls(tool_call) for tool_call in tool_calls
173
+ ]
174
+ if tool_calls_list:
175
+ body["tool_calls"] = tool_calls_list
176
+
177
+ return LogRecord(
178
+ event_name="gen_ai.choice",
179
+ attributes={
180
+ gen_ai_attributes.GEN_AI_SYSTEM: gen_ai_attributes.GenAiSystemValues.VERTEX_AI.value,
181
+ },
182
+ body=body,
183
+ )
184
+
185
+
186
+ def _asdict_filter_nulls(instance: Any) -> dict[str, AnyValue]:
187
+ return asdict(
188
+ instance,
189
+ dict_factory=lambda kvs: {k: v for (k, v) in kvs if v is not None},
190
+ )
@@ -0,0 +1,16 @@
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
+ _instruments = ("google-cloud-aiplatform >= 1.64",)
@@ -0,0 +1,371 @@
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
+ from __future__ import annotations
16
+
17
+ from contextlib import contextmanager
18
+ from dataclasses import asdict
19
+ from typing import (
20
+ TYPE_CHECKING,
21
+ Any,
22
+ Awaitable,
23
+ Callable,
24
+ Literal,
25
+ MutableSequence,
26
+ Union,
27
+ cast,
28
+ overload,
29
+ )
30
+
31
+ from opentelemetry._logs import Logger, LogRecord
32
+ from opentelemetry.instrumentation._semconv import (
33
+ _StabilityMode,
34
+ )
35
+ from opentelemetry.instrumentation.vertexai.utils import (
36
+ GenerateContentParams,
37
+ _map_finish_reason,
38
+ convert_content_to_message_parts,
39
+ get_genai_request_attributes,
40
+ get_genai_response_attributes,
41
+ get_server_attributes,
42
+ get_span_name,
43
+ request_to_events,
44
+ response_to_events,
45
+ )
46
+ from opentelemetry.semconv._incubating.attributes import (
47
+ gen_ai_attributes as GenAI,
48
+ )
49
+ from opentelemetry.trace import SpanKind, Tracer
50
+ from opentelemetry.util.genai.completion_hook import CompletionHook
51
+ from opentelemetry.util.genai.types import (
52
+ ContentCapturingMode,
53
+ InputMessage,
54
+ OutputMessage,
55
+ )
56
+ from opentelemetry.util.genai.utils import gen_ai_json_dumps
57
+
58
+ if TYPE_CHECKING:
59
+ from google.cloud.aiplatform_v1.services.prediction_service import client
60
+ from google.cloud.aiplatform_v1.types import (
61
+ content,
62
+ prediction_service,
63
+ )
64
+ from google.cloud.aiplatform_v1beta1.services.prediction_service import (
65
+ client as client_v1beta1,
66
+ )
67
+ from google.cloud.aiplatform_v1beta1.types import (
68
+ content as content_v1beta1,
69
+ )
70
+ from google.cloud.aiplatform_v1beta1.types import (
71
+ prediction_service as prediction_service_v1beta1,
72
+ )
73
+
74
+
75
+ # Use parameter signature from
76
+ # https://github.com/googleapis/python-aiplatform/blob/v1.76.0/google/cloud/aiplatform_v1/services/prediction_service/client.py#L2088
77
+ # to handle named vs positional args robustly
78
+ def _extract_params(
79
+ request: prediction_service.GenerateContentRequest
80
+ | prediction_service_v1beta1.GenerateContentRequest
81
+ | dict[Any, Any]
82
+ | None = None,
83
+ *,
84
+ model: str | None = None,
85
+ contents: MutableSequence[content.Content]
86
+ | MutableSequence[content_v1beta1.Content]
87
+ | None = None,
88
+ **_kwargs: Any,
89
+ ) -> GenerateContentParams:
90
+ # Request vs the named parameters are mututally exclusive or the RPC will fail
91
+ if not request:
92
+ return GenerateContentParams(
93
+ model=model or "",
94
+ contents=contents,
95
+ )
96
+
97
+ if isinstance(request, dict):
98
+ return GenerateContentParams(**request)
99
+
100
+ return GenerateContentParams(
101
+ model=request.model,
102
+ contents=request.contents,
103
+ system_instruction=request.system_instruction,
104
+ tools=request.tools,
105
+ tool_config=request.tool_config,
106
+ labels=request.labels,
107
+ safety_settings=request.safety_settings,
108
+ generation_config=request.generation_config,
109
+ )
110
+
111
+
112
+ # For details about GEN_AI_LATEST_EXPERIMENTAL stability mode see
113
+ # https://github.com/open-telemetry/semantic-conventions/blob/v1.37.0/docs/gen-ai/gen-ai-agent-spans.md?plain=1#L18-L37
114
+ class MethodWrappers:
115
+ @overload
116
+ def __init__(
117
+ self,
118
+ tracer: Tracer,
119
+ logger: Logger,
120
+ capture_content: ContentCapturingMode,
121
+ sem_conv_opt_in_mode: Literal[
122
+ _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL
123
+ ],
124
+ completion_hook: CompletionHook,
125
+ ) -> None: ...
126
+
127
+ @overload
128
+ def __init__(
129
+ self,
130
+ tracer: Tracer,
131
+ logger: Logger,
132
+ capture_content: bool,
133
+ sem_conv_opt_in_mode: Literal[_StabilityMode.DEFAULT],
134
+ completion_hook: CompletionHook,
135
+ ) -> None: ...
136
+
137
+ def __init__(
138
+ self,
139
+ tracer: Tracer,
140
+ logger: Logger,
141
+ capture_content: Union[bool, ContentCapturingMode],
142
+ sem_conv_opt_in_mode: Union[
143
+ Literal[_StabilityMode.DEFAULT],
144
+ Literal[_StabilityMode.GEN_AI_LATEST_EXPERIMENTAL],
145
+ ],
146
+ completion_hook: CompletionHook,
147
+ ) -> None:
148
+ self.tracer = tracer
149
+ self.logger = logger
150
+ self.capture_content = capture_content
151
+ self.sem_conv_opt_in_mode = sem_conv_opt_in_mode
152
+ self.completion_hook = completion_hook
153
+
154
+ @contextmanager
155
+ def _with_new_instrumentation(
156
+ self,
157
+ capture_content: ContentCapturingMode,
158
+ instance: client.PredictionServiceClient
159
+ | client_v1beta1.PredictionServiceClient,
160
+ args: Any,
161
+ kwargs: Any,
162
+ ):
163
+ params = _extract_params(*args, **kwargs)
164
+ request_attributes = get_genai_request_attributes(True, params)
165
+ with self.tracer.start_as_current_span(
166
+ name=f"{GenAI.GenAiOperationNameValues.CHAT.value} {request_attributes.get(GenAI.GEN_AI_REQUEST_MODEL, '')}".strip(),
167
+ kind=SpanKind.CLIENT,
168
+ ) as span:
169
+
170
+ def handle_response(
171
+ response: prediction_service.GenerateContentResponse
172
+ | prediction_service_v1beta1.GenerateContentResponse
173
+ | None,
174
+ ) -> None:
175
+ event = LogRecord(
176
+ event_name="gen_ai.client.inference.operation.details",
177
+ )
178
+ attributes = (
179
+ get_server_attributes(instance.api_endpoint) # type: ignore[reportUnknownMemberType]
180
+ | request_attributes
181
+ | get_genai_response_attributes(response)
182
+ )
183
+ system_instructions, inputs, outputs = [], [], []
184
+ if params.system_instruction:
185
+ system_instructions = convert_content_to_message_parts(
186
+ params.system_instruction
187
+ )
188
+ if params.contents:
189
+ inputs = [
190
+ InputMessage(
191
+ role=content.role,
192
+ parts=convert_content_to_message_parts(content),
193
+ )
194
+ for content in params.contents
195
+ ]
196
+ if response:
197
+ outputs = [
198
+ OutputMessage(
199
+ finish_reason=_map_finish_reason(
200
+ candidate.finish_reason
201
+ ),
202
+ role=candidate.content.role,
203
+ parts=convert_content_to_message_parts(
204
+ candidate.content
205
+ ),
206
+ )
207
+ for candidate in response.candidates
208
+ ]
209
+ self.completion_hook.on_completion(
210
+ inputs=inputs,
211
+ outputs=outputs,
212
+ system_instruction=system_instructions,
213
+ span=span,
214
+ log_record=event,
215
+ )
216
+ content_attributes = {
217
+ k: [asdict(x) for x in v]
218
+ for k, v in [
219
+ (
220
+ GenAI.GEN_AI_SYSTEM_INSTRUCTIONS,
221
+ system_instructions,
222
+ ),
223
+ (GenAI.GEN_AI_INPUT_MESSAGES, inputs),
224
+ (GenAI.GEN_AI_OUTPUT_MESSAGES, outputs),
225
+ ]
226
+ if v
227
+ }
228
+ if span.is_recording():
229
+ span.set_attributes(attributes)
230
+ if capture_content in (
231
+ ContentCapturingMode.SPAN_AND_EVENT,
232
+ ContentCapturingMode.SPAN_ONLY,
233
+ ):
234
+ span.set_attributes(
235
+ {
236
+ k: gen_ai_json_dumps(v)
237
+ for k, v in content_attributes.items()
238
+ }
239
+ )
240
+ event.attributes = attributes
241
+ if capture_content in (
242
+ ContentCapturingMode.SPAN_AND_EVENT,
243
+ ContentCapturingMode.EVENT_ONLY,
244
+ ):
245
+ event.attributes |= content_attributes
246
+ self.logger.emit(event)
247
+
248
+ yield handle_response
249
+
250
+ @contextmanager
251
+ def _with_default_instrumentation(
252
+ self,
253
+ capture_content: bool,
254
+ instance: client.PredictionServiceClient
255
+ | client_v1beta1.PredictionServiceClient,
256
+ args: Any,
257
+ kwargs: Any,
258
+ ):
259
+ params = _extract_params(*args, **kwargs)
260
+ api_endpoint: str = instance.api_endpoint # type: ignore[reportUnknownMemberType]
261
+ span_attributes = {
262
+ **get_genai_request_attributes(False, params),
263
+ **get_server_attributes(api_endpoint),
264
+ }
265
+
266
+ span_name = get_span_name(span_attributes)
267
+
268
+ with self.tracer.start_as_current_span(
269
+ name=span_name,
270
+ kind=SpanKind.CLIENT,
271
+ attributes=span_attributes,
272
+ ) as span:
273
+ for event in request_to_events(
274
+ params=params, capture_content=capture_content
275
+ ):
276
+ self.logger.emit(event)
277
+
278
+ # TODO: set error.type attribute
279
+ # https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-spans.md
280
+
281
+ def handle_response(
282
+ response: prediction_service.GenerateContentResponse
283
+ | prediction_service_v1beta1.GenerateContentResponse,
284
+ ) -> None:
285
+ if span.is_recording():
286
+ # When streaming, this is called multiple times so attributes would be
287
+ # overwritten. In practice, it looks the API only returns the interesting
288
+ # attributes on the last streamed response. However, I couldn't find
289
+ # documentation for this and setting attributes shouldn't be too expensive.
290
+ span.set_attributes(
291
+ get_genai_response_attributes(response)
292
+ )
293
+
294
+ for event in response_to_events(
295
+ response=response, capture_content=capture_content
296
+ ):
297
+ self.logger.emit(event)
298
+
299
+ yield handle_response
300
+
301
+ def generate_content(
302
+ self,
303
+ wrapped: Callable[
304
+ ...,
305
+ prediction_service.GenerateContentResponse
306
+ | prediction_service_v1beta1.GenerateContentResponse,
307
+ ],
308
+ instance: client.PredictionServiceClient
309
+ | client_v1beta1.PredictionServiceClient,
310
+ args: Any,
311
+ kwargs: Any,
312
+ ) -> (
313
+ prediction_service.GenerateContentResponse
314
+ | prediction_service_v1beta1.GenerateContentResponse
315
+ ):
316
+ if self.sem_conv_opt_in_mode == _StabilityMode.DEFAULT:
317
+ capture_content_bool = cast(bool, self.capture_content)
318
+ with self._with_default_instrumentation(
319
+ capture_content_bool, instance, args, kwargs
320
+ ) as handle_response:
321
+ response = wrapped(*args, **kwargs)
322
+ handle_response(response)
323
+ return response
324
+ else:
325
+ capture_content = cast(ContentCapturingMode, self.capture_content)
326
+ with self._with_new_instrumentation(
327
+ capture_content, instance, args, kwargs
328
+ ) as handle_response:
329
+ response = None
330
+ try:
331
+ response = wrapped(*args, **kwargs)
332
+ return response
333
+ finally:
334
+ handle_response(response)
335
+
336
+ async def agenerate_content(
337
+ self,
338
+ wrapped: Callable[
339
+ ...,
340
+ Awaitable[
341
+ prediction_service.GenerateContentResponse
342
+ | prediction_service_v1beta1.GenerateContentResponse
343
+ ],
344
+ ],
345
+ instance: client.PredictionServiceClient
346
+ | client_v1beta1.PredictionServiceClient,
347
+ args: Any,
348
+ kwargs: Any,
349
+ ) -> (
350
+ prediction_service.GenerateContentResponse
351
+ | prediction_service_v1beta1.GenerateContentResponse
352
+ ):
353
+ if self.sem_conv_opt_in_mode == _StabilityMode.DEFAULT:
354
+ capture_content_bool = cast(bool, self.capture_content)
355
+ with self._with_default_instrumentation(
356
+ capture_content_bool, instance, args, kwargs
357
+ ) as handle_response:
358
+ response = await wrapped(*args, **kwargs)
359
+ handle_response(response)
360
+ return response
361
+ else:
362
+ capture_content = cast(ContentCapturingMode, self.capture_content)
363
+ with self._with_new_instrumentation(
364
+ capture_content, instance, args, kwargs
365
+ ) as handle_response:
366
+ response = None
367
+ try:
368
+ response = await wrapped(*args, **kwargs)
369
+ return response
370
+ finally:
371
+ handle_response(response)
File without changes