opentelemetry-instrumentation-vertexai 2.0b0__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.

@@ -39,58 +39,135 @@ API
39
39
  ---
40
40
  """
41
41
 
42
+ from __future__ import annotations
43
+
42
44
  from typing import Any, Collection
43
45
 
44
46
  from wrapt import (
45
47
  wrap_function_wrapper, # type: ignore[reportUnknownVariableType]
46
48
  )
47
49
 
48
- from opentelemetry._events import get_event_logger
50
+ from opentelemetry._logs import get_logger
51
+ from opentelemetry.instrumentation._semconv import (
52
+ _OpenTelemetrySemanticConventionStability,
53
+ _OpenTelemetryStabilitySignalType,
54
+ _StabilityMode,
55
+ )
49
56
  from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
57
+ from opentelemetry.instrumentation.utils import unwrap
50
58
  from opentelemetry.instrumentation.vertexai.package import _instruments
51
- from opentelemetry.instrumentation.vertexai.patch import (
52
- generate_content_create,
53
- )
59
+ from opentelemetry.instrumentation.vertexai.patch import MethodWrappers
54
60
  from opentelemetry.instrumentation.vertexai.utils import is_content_enabled
55
61
  from opentelemetry.semconv.schemas import Schemas
56
62
  from opentelemetry.trace import get_tracer
63
+ from opentelemetry.util.genai.completion_hook import load_completion_hook
64
+
65
+
66
+ def _methods_to_wrap(
67
+ method_wrappers: MethodWrappers,
68
+ ):
69
+ # This import is very slow, do it lazily in case instrument() is not called
70
+ # pylint: disable=import-outside-toplevel
71
+ from google.cloud.aiplatform_v1.services.prediction_service import (
72
+ async_client,
73
+ client,
74
+ )
75
+ from google.cloud.aiplatform_v1beta1.services.prediction_service import (
76
+ async_client as async_client_v1beta1,
77
+ )
78
+ from google.cloud.aiplatform_v1beta1.services.prediction_service import (
79
+ client as client_v1beta1,
80
+ )
81
+
82
+ for client_class in (
83
+ client.PredictionServiceClient,
84
+ client_v1beta1.PredictionServiceClient,
85
+ ):
86
+ yield (
87
+ client_class,
88
+ client_class.generate_content.__name__, # type: ignore[reportUnknownMemberType]
89
+ method_wrappers.generate_content,
90
+ )
91
+
92
+ for client_class in (
93
+ async_client.PredictionServiceAsyncClient,
94
+ async_client_v1beta1.PredictionServiceAsyncClient,
95
+ ):
96
+ yield (
97
+ client_class,
98
+ client_class.generate_content.__name__, # type: ignore[reportUnknownMemberType]
99
+ method_wrappers.agenerate_content,
100
+ )
57
101
 
58
102
 
59
103
  class VertexAIInstrumentor(BaseInstrumentor):
104
+ def __init__(self) -> None:
105
+ super().__init__()
106
+ self._methods_to_unwrap: list[tuple[Any, str]] = []
107
+
60
108
  def instrumentation_dependencies(self) -> Collection[str]:
61
109
  return _instruments
62
110
 
63
111
  def _instrument(self, **kwargs: Any):
64
112
  """Enable VertexAI instrumentation."""
113
+ completion_hook = (
114
+ kwargs.get("completion_hook") or load_completion_hook()
115
+ )
116
+ sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
117
+ _OpenTelemetryStabilitySignalType.GEN_AI,
118
+ )
65
119
  tracer_provider = kwargs.get("tracer_provider")
120
+ schema = (
121
+ Schemas.V1_28_0.value
122
+ if sem_conv_opt_in_mode == _StabilityMode.DEFAULT
123
+ else Schemas.V1_36_0.value
124
+ )
66
125
  tracer = get_tracer(
67
126
  __name__,
68
127
  "",
69
128
  tracer_provider,
70
- schema_url=Schemas.V1_28_0.value,
129
+ schema_url=schema,
71
130
  )
72
- event_logger_provider = kwargs.get("event_logger_provider")
73
- event_logger = get_event_logger(
131
+ logger_provider = kwargs.get("logger_provider")
132
+ logger = get_logger(
74
133
  __name__,
75
134
  "",
76
- schema_url=Schemas.V1_28_0.value,
77
- event_logger_provider=event_logger_provider,
78
- )
79
-
80
- wrap_function_wrapper(
81
- module="google.cloud.aiplatform_v1beta1.services.prediction_service.client",
82
- name="PredictionServiceClient.generate_content",
83
- wrapper=generate_content_create(
84
- tracer, event_logger, is_content_enabled()
85
- ),
135
+ logger_provider=logger_provider,
136
+ schema_url=schema,
86
137
  )
87
- wrap_function_wrapper(
88
- module="google.cloud.aiplatform_v1.services.prediction_service.client",
89
- name="PredictionServiceClient.generate_content",
90
- wrapper=generate_content_create(
91
- tracer, event_logger, is_content_enabled()
92
- ),
138
+ sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
139
+ _OpenTelemetryStabilitySignalType.GEN_AI,
93
140
  )
141
+ if sem_conv_opt_in_mode == _StabilityMode.DEFAULT:
142
+ # Type checker now knows sem_conv_opt_in_mode is a Literal[_StabilityMode.DEFAULT]
143
+ method_wrappers = MethodWrappers(
144
+ tracer,
145
+ logger,
146
+ is_content_enabled(sem_conv_opt_in_mode),
147
+ sem_conv_opt_in_mode,
148
+ completion_hook,
149
+ )
150
+ elif sem_conv_opt_in_mode == _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL:
151
+ # Type checker now knows it's the other literal
152
+ method_wrappers = MethodWrappers(
153
+ tracer,
154
+ logger,
155
+ is_content_enabled(sem_conv_opt_in_mode),
156
+ sem_conv_opt_in_mode,
157
+ completion_hook,
158
+ )
159
+ else:
160
+ raise RuntimeError(f"{sem_conv_opt_in_mode} mode not supported")
161
+ for client_class, method_name, wrapper in _methods_to_wrap(
162
+ method_wrappers
163
+ ):
164
+ wrap_function_wrapper(
165
+ client_class,
166
+ name=method_name,
167
+ wrapper=wrapper,
168
+ )
169
+ self._methods_to_unwrap.append((client_class, method_name))
94
170
 
95
171
  def _uninstrument(self, **kwargs: Any) -> None:
96
- """TODO: implemented in later PR"""
172
+ for client_class, method_name in self._methods_to_unwrap:
173
+ unwrap(client_class, method_name)
@@ -12,6 +12,8 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ # type: ignore[reportUnknownDeprecated]
16
+
15
17
  """
16
18
  Factories for event types described in
17
19
  https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-events.md#system-event.
@@ -25,7 +27,7 @@ from __future__ import annotations
25
27
  from dataclasses import asdict, dataclass
26
28
  from typing import Any, Iterable, Literal
27
29
 
28
- from opentelemetry._events import Event
30
+ from opentelemetry._logs import LogRecord
29
31
  from opentelemetry.semconv._incubating.attributes import gen_ai_attributes
30
32
  from opentelemetry.util.types import AnyValue
31
33
 
@@ -34,7 +36,7 @@ def user_event(
34
36
  *,
35
37
  role: str = "user",
36
38
  content: AnyValue = None,
37
- ) -> Event:
39
+ ) -> LogRecord:
38
40
  """Creates a User event
39
41
  https://github.com/open-telemetry/semantic-conventions/blob/v1.28.0/docs/gen-ai/gen-ai-events.md#user-event
40
42
  """
@@ -43,8 +45,8 @@ def user_event(
43
45
  }
44
46
  if content is not None:
45
47
  body["content"] = content
46
- return Event(
47
- name="gen_ai.user.message",
48
+ return LogRecord(
49
+ event_name="gen_ai.user.message",
48
50
  attributes={
49
51
  gen_ai_attributes.GEN_AI_SYSTEM: gen_ai_attributes.GenAiSystemValues.VERTEX_AI.value,
50
52
  },
@@ -56,7 +58,7 @@ def assistant_event(
56
58
  *,
57
59
  role: str = "assistant",
58
60
  content: AnyValue = None,
59
- ) -> Event:
61
+ ) -> LogRecord:
60
62
  """Creates an Assistant event
61
63
  https://github.com/open-telemetry/semantic-conventions/blob/v1.28.0/docs/gen-ai/gen-ai-events.md#assistant-event
62
64
  """
@@ -65,8 +67,8 @@ def assistant_event(
65
67
  }
66
68
  if content is not None:
67
69
  body["content"] = content
68
- return Event(
69
- name="gen_ai.assistant.message",
70
+ return LogRecord(
71
+ event_name="gen_ai.assistant.message",
70
72
  attributes={
71
73
  gen_ai_attributes.GEN_AI_SYSTEM: gen_ai_attributes.GenAiSystemValues.VERTEX_AI.value,
72
74
  },
@@ -78,7 +80,7 @@ def system_event(
78
80
  *,
79
81
  role: str = "system",
80
82
  content: AnyValue = None,
81
- ) -> Event:
83
+ ) -> LogRecord:
82
84
  """Creates a System event
83
85
  https://github.com/open-telemetry/semantic-conventions/blob/v1.28.0/docs/gen-ai/gen-ai-events.md#system-event
84
86
  """
@@ -87,8 +89,8 @@ def system_event(
87
89
  }
88
90
  if content is not None:
89
91
  body["content"] = content
90
- return Event(
91
- name="gen_ai.system.message",
92
+ return LogRecord(
93
+ event_name="gen_ai.system.message",
92
94
  attributes={
93
95
  gen_ai_attributes.GEN_AI_SYSTEM: gen_ai_attributes.GenAiSystemValues.VERTEX_AI.value,
94
96
  },
@@ -101,7 +103,7 @@ def tool_event(
101
103
  role: str | None,
102
104
  id_: str,
103
105
  content: AnyValue = None,
104
- ) -> Event:
106
+ ) -> LogRecord:
105
107
  """Creates a Tool message event
106
108
  https://github.com/open-telemetry/semantic-conventions/blob/v1.28.0/docs/gen-ai/gen-ai-events.md#event-gen_aitoolmessage
107
109
  """
@@ -114,8 +116,8 @@ def tool_event(
114
116
  }
115
117
  if content is not None:
116
118
  body["content"] = content
117
- return Event(
118
- name="gen_ai.tool.message",
119
+ return LogRecord(
120
+ event_name="gen_ai.tool.message",
119
121
  attributes={
120
122
  gen_ai_attributes.GEN_AI_SYSTEM: gen_ai_attributes.GenAiSystemValues.VERTEX_AI.value,
121
123
  },
@@ -156,7 +158,7 @@ def choice_event(
156
158
  index: int,
157
159
  message: ChoiceMessage,
158
160
  tool_calls: Iterable[ChoiceToolCall] = (),
159
- ) -> Event:
161
+ ) -> LogRecord:
160
162
  """Creates a choice event, which describes the Gen AI response message.
161
163
  https://github.com/open-telemetry/semantic-conventions/blob/v1.28.0/docs/gen-ai/gen-ai-events.md#event-gen_aichoice
162
164
  """
@@ -172,8 +174,8 @@ def choice_event(
172
174
  if tool_calls_list:
173
175
  body["tool_calls"] = tool_calls_list
174
176
 
175
- return Event(
176
- name="gen_ai.choice",
177
+ return LogRecord(
178
+ event_name="gen_ai.choice",
177
179
  attributes={
178
180
  gen_ai_attributes.GEN_AI_SYSTEM: gen_ai_attributes.GenAiSystemValues.VERTEX_AI.value,
179
181
  },
@@ -14,16 +14,28 @@
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
+ from contextlib import contextmanager
18
+ from dataclasses import asdict
17
19
  from typing import (
18
20
  TYPE_CHECKING,
19
21
  Any,
22
+ Awaitable,
20
23
  Callable,
24
+ Literal,
21
25
  MutableSequence,
26
+ Union,
27
+ cast,
28
+ overload,
22
29
  )
23
30
 
24
- from opentelemetry._events import EventLogger
31
+ from opentelemetry._logs import Logger, LogRecord
32
+ from opentelemetry.instrumentation._semconv import (
33
+ _StabilityMode,
34
+ )
25
35
  from opentelemetry.instrumentation.vertexai.utils import (
26
36
  GenerateContentParams,
37
+ _map_finish_reason,
38
+ convert_content_to_message_parts,
27
39
  get_genai_request_attributes,
28
40
  get_genai_response_attributes,
29
41
  get_server_attributes,
@@ -31,7 +43,17 @@ from opentelemetry.instrumentation.vertexai.utils import (
31
43
  request_to_events,
32
44
  response_to_events,
33
45
  )
46
+ from opentelemetry.semconv._incubating.attributes import (
47
+ gen_ai_attributes as GenAI,
48
+ )
34
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
35
57
 
36
58
  if TYPE_CHECKING:
37
59
  from google.cloud.aiplatform_v1.services.prediction_service import client
@@ -87,17 +109,148 @@ def _extract_params(
87
109
  )
88
110
 
89
111
 
90
- def generate_content_create(
91
- tracer: Tracer, event_logger: EventLogger, capture_content: bool
92
- ):
93
- """Wrap the `generate_content` method of the `GenerativeModel` class to trace it."""
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: ...
94
126
 
95
- def traced_method(
96
- wrapped: Callable[
97
- ...,
98
- prediction_service.GenerateContentResponse
99
- | prediction_service_v1beta1.GenerateContentResponse,
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],
100
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,
101
254
  instance: client.PredictionServiceClient
102
255
  | client_v1beta1.PredictionServiceClient,
103
256
  args: Any,
@@ -106,12 +259,13 @@ def generate_content_create(
106
259
  params = _extract_params(*args, **kwargs)
107
260
  api_endpoint: str = instance.api_endpoint # type: ignore[reportUnknownMemberType]
108
261
  span_attributes = {
109
- **get_genai_request_attributes(params),
262
+ **get_genai_request_attributes(False, params),
110
263
  **get_server_attributes(api_endpoint),
111
264
  }
112
265
 
113
266
  span_name = get_span_name(span_attributes)
114
- with tracer.start_as_current_span(
267
+
268
+ with self.tracer.start_as_current_span(
115
269
  name=span_name,
116
270
  kind=SpanKind.CLIENT,
117
271
  attributes=span_attributes,
@@ -119,24 +273,99 @@ def generate_content_create(
119
273
  for event in request_to_events(
120
274
  params=params, capture_content=capture_content
121
275
  ):
122
- event_logger.emit(event)
276
+ self.logger.emit(event)
123
277
 
124
278
  # TODO: set error.type attribute
125
279
  # https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-spans.md
126
- response = wrapped(*args, **kwargs)
127
- # TODO: handle streaming
128
- # if is_streaming(kwargs):
129
- # return StreamWrapper(
130
- # result, span, event_logger, capture_content
131
- # )
132
-
133
- if span.is_recording():
134
- span.set_attributes(get_genai_response_attributes(response))
135
- for event in response_to_events(
136
- response=response, capture_content=capture_content
137
- ):
138
- event_logger.emit(event)
139
280
 
140
- return response
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)
141
335
 
142
- return traced_method
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)
@@ -12,27 +12,35 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ # type: ignore[reportUnknownDeprecated]
16
+
15
17
  from __future__ import annotations
16
18
 
19
+ import logging
17
20
  import re
18
21
  from dataclasses import dataclass
19
22
  from os import environ
20
23
  from typing import (
21
24
  TYPE_CHECKING,
22
25
  Iterable,
26
+ Literal,
23
27
  Mapping,
24
28
  Sequence,
29
+ Union,
25
30
  cast,
31
+ overload,
26
32
  )
27
33
  from urllib.parse import urlparse
28
34
 
29
35
  from google.protobuf import json_format
30
36
 
31
- from opentelemetry._events import Event
37
+ from opentelemetry._logs import LogRecord
38
+ from opentelemetry.instrumentation._semconv import (
39
+ _StabilityMode,
40
+ )
32
41
  from opentelemetry.instrumentation.vertexai.events import (
33
42
  ChoiceMessage,
34
43
  ChoiceToolCall,
35
- FinishReason,
36
44
  assistant_event,
37
45
  choice_event,
38
46
  system_event,
@@ -43,6 +51,15 @@ from opentelemetry.semconv._incubating.attributes import (
43
51
  gen_ai_attributes as GenAIAttributes,
44
52
  )
45
53
  from opentelemetry.semconv.attributes import server_attributes
54
+ from opentelemetry.util.genai.types import (
55
+ ContentCapturingMode,
56
+ FinishReason,
57
+ MessagePart,
58
+ Text,
59
+ ToolCall,
60
+ ToolCallResponse,
61
+ )
62
+ from opentelemetry.util.genai.utils import get_content_capturing_mode
46
63
  from opentelemetry.util.types import AnyValue, AttributeValue
47
64
 
48
65
  if TYPE_CHECKING:
@@ -103,7 +120,8 @@ def get_server_attributes(
103
120
  }
104
121
 
105
122
 
106
- def get_genai_request_attributes(
123
+ def get_genai_request_attributes( # pylint: disable=too-many-branches
124
+ use_latest_semconvs: bool,
107
125
  params: GenerateContentParams,
108
126
  operation_name: GenAIAttributes.GenAiOperationNameValues = GenAIAttributes.GenAiOperationNameValues.CHAT,
109
127
  ):
@@ -111,9 +129,12 @@ def get_genai_request_attributes(
111
129
  generation_config = params.generation_config
112
130
  attributes: dict[str, AttributeValue] = {
113
131
  GenAIAttributes.GEN_AI_OPERATION_NAME: operation_name.value,
114
- GenAIAttributes.GEN_AI_SYSTEM: GenAIAttributes.GenAiSystemValues.VERTEX_AI.value,
115
132
  GenAIAttributes.GEN_AI_REQUEST_MODEL: model,
116
133
  }
134
+ if not use_latest_semconvs:
135
+ attributes[GenAIAttributes.GEN_AI_SYSTEM] = (
136
+ GenAIAttributes.GenAiSystemValues.VERTEX_AI.value
137
+ )
117
138
 
118
139
  if not generation_config:
119
140
  return attributes
@@ -125,6 +146,8 @@ def get_genai_request_attributes(
125
146
  generation_config.temperature
126
147
  )
127
148
  if "top_p" in generation_config:
149
+ # There is also a top_k parameter ( The maximum number of tokens to consider when sampling.),
150
+ # but no semconv yet exists for it.
128
151
  attributes[GenAIAttributes.GEN_AI_REQUEST_TOP_P] = (
129
152
  generation_config.top_p
130
153
  )
@@ -140,30 +163,43 @@ def get_genai_request_attributes(
140
163
  attributes[GenAIAttributes.GEN_AI_REQUEST_FREQUENCY_PENALTY] = (
141
164
  generation_config.frequency_penalty
142
165
  )
143
- # Uncomment once GEN_AI_REQUEST_SEED is released in 1.30
144
- # https://github.com/open-telemetry/semantic-conventions/pull/1710
145
- # if "seed" in generation_config:
146
- # attributes[GenAIAttributes.GEN_AI_REQUEST_SEED] = (
147
- # generation_config.seed
148
- # )
149
166
  if "stop_sequences" in generation_config:
150
167
  attributes[GenAIAttributes.GEN_AI_REQUEST_STOP_SEQUENCES] = (
151
168
  generation_config.stop_sequences
152
169
  )
170
+ if use_latest_semconvs:
171
+ if "seed" in generation_config:
172
+ attributes[GenAIAttributes.GEN_AI_REQUEST_SEED] = (
173
+ generation_config.seed
174
+ )
175
+ if "candidate_count" in generation_config:
176
+ attributes[GenAIAttributes.GEN_AI_REQUEST_CHOICE_COUNT] = (
177
+ generation_config.candidate_count
178
+ )
179
+ if "response_mime_type" in generation_config:
180
+ if generation_config.response_mime_type == "text/plain":
181
+ attributes[GenAIAttributes.GEN_AI_OUTPUT_TYPE] = "text"
182
+ elif generation_config.response_mime_type == "application/json":
183
+ attributes[GenAIAttributes.GEN_AI_OUTPUT_TYPE] = "json"
184
+ else:
185
+ attributes[GenAIAttributes.GEN_AI_OUTPUT_TYPE] = (
186
+ generation_config.response_mime_type
187
+ )
153
188
 
154
189
  return attributes
155
190
 
156
191
 
157
192
  def get_genai_response_attributes(
158
193
  response: prediction_service.GenerateContentResponse
159
- | prediction_service_v1beta1.GenerateContentResponse,
194
+ | prediction_service_v1beta1.GenerateContentResponse
195
+ | None,
160
196
  ) -> dict[str, AttributeValue]:
197
+ if not response:
198
+ return {}
161
199
  finish_reasons: list[str] = [
162
200
  _map_finish_reason(candidate.finish_reason)
163
201
  for candidate in response.candidates
164
202
  ]
165
- # TODO: add gen_ai.response.id once available in the python client
166
- # https://github.com/open-telemetry/opentelemetry-python-contrib/issues/3246
167
203
  return {
168
204
  GenAIAttributes.GEN_AI_RESPONSE_MODEL: response.model_version,
169
205
  GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS: finish_reasons,
@@ -186,12 +222,29 @@ OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = (
186
222
  )
187
223
 
188
224
 
189
- def is_content_enabled() -> bool:
190
- capture_content = environ.get(
191
- OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, "false"
192
- )
225
+ @overload
226
+ def is_content_enabled(
227
+ mode: Literal[_StabilityMode.GEN_AI_LATEST_EXPERIMENTAL],
228
+ ) -> ContentCapturingMode: ...
229
+
230
+
231
+ @overload
232
+ def is_content_enabled(mode: Literal[_StabilityMode.DEFAULT]) -> bool: ...
233
+
234
+
235
+ def is_content_enabled(
236
+ mode: Union[
237
+ Literal[_StabilityMode.DEFAULT],
238
+ Literal[_StabilityMode.GEN_AI_LATEST_EXPERIMENTAL],
239
+ ],
240
+ ) -> Union[bool, ContentCapturingMode]:
241
+ if mode == _StabilityMode.DEFAULT:
242
+ capture_content = environ.get(
243
+ OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, "false"
244
+ )
193
245
 
194
- return capture_content.lower() == "true"
246
+ return capture_content.lower() == "true"
247
+ return get_content_capturing_mode()
195
248
 
196
249
 
197
250
  def get_span_name(span_attributes: Mapping[str, AttributeValue]) -> str:
@@ -204,7 +257,7 @@ def get_span_name(span_attributes: Mapping[str, AttributeValue]) -> str:
204
257
 
205
258
  def request_to_events(
206
259
  *, params: GenerateContentParams, capture_content: bool
207
- ) -> Iterable[Event]:
260
+ ) -> Iterable[LogRecord]:
208
261
  # System message
209
262
  if params.system_instruction:
210
263
  request_content = _parts_to_any_value(
@@ -256,12 +309,72 @@ def request_to_events(
256
309
  yield user_event(role=content.role, content=request_content)
257
310
 
258
311
 
312
+ @dataclass
313
+ class BlobPart:
314
+ data: bytes
315
+ mime_type: str
316
+ type: Literal["blob"] = "blob"
317
+
318
+
319
+ @dataclass
320
+ class FileDataPart:
321
+ mime_type: str
322
+ uri: str
323
+ type: Literal["file_data"] = "file_data"
324
+
325
+ class Config:
326
+ extra = "allow"
327
+
328
+
329
+ def convert_content_to_message_parts(
330
+ content: content.Content | content_v1beta1.Content,
331
+ ) -> list[MessagePart]:
332
+ parts: MessagePart = []
333
+ for idx, part in enumerate(content.parts):
334
+ if "function_response" in part:
335
+ part = part.function_response
336
+ parts.append(
337
+ ToolCallResponse(
338
+ id=f"{part.name}_{idx}",
339
+ response=json_format.MessageToDict(part._pb.response), # type: ignore[reportUnknownMemberType]
340
+ )
341
+ )
342
+ elif "function_call" in part:
343
+ part = part.function_call
344
+ parts.append(
345
+ ToolCall(
346
+ id=f"{part.name}_{idx}",
347
+ name=part.name,
348
+ arguments=json_format.MessageToDict(
349
+ part._pb.args, # type: ignore[reportUnknownMemberType]
350
+ ),
351
+ )
352
+ )
353
+ elif "text" in part:
354
+ parts.append(Text(content=part.text))
355
+ elif "inline_data" in part:
356
+ part = part.inline_data
357
+ parts.append(
358
+ BlobPart(mime_type=part.mime_type or "", data=part.data or b"")
359
+ )
360
+ elif "file_data" in part:
361
+ part = part.file_data
362
+ parts.append(
363
+ FileDataPart(
364
+ mime_type=part.mime_type or "", uri=part.file_uri or ""
365
+ )
366
+ )
367
+ else:
368
+ logging.warning("Unknown part dropped from telemetry %s", part)
369
+ return parts
370
+
371
+
259
372
  def response_to_events(
260
373
  *,
261
374
  response: prediction_service.GenerateContentResponse
262
375
  | prediction_service_v1beta1.GenerateContentResponse,
263
376
  capture_content: bool,
264
- ) -> Iterable[Event]:
377
+ ) -> Iterable[LogRecord]:
265
378
  for candidate in response.candidates:
266
379
  tool_calls = _extract_tool_calls(
267
380
  candidate=candidate, capture_content=capture_content
@@ -319,7 +432,9 @@ def _parts_to_any_value(
319
432
  return [
320
433
  cast(
321
434
  "dict[str, AnyValue]",
322
- type(part).to_dict(part, including_default_value_fields=False), # type: ignore[reportUnknownMemberType]
435
+ type(part).to_dict( # type: ignore[reportUnknownMemberType]
436
+ part, always_print_fields_with_no_presence=False
437
+ ),
323
438
  )
324
439
  for part in parts
325
440
  ]
@@ -12,4 +12,4 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- __version__ = "2.0b0"
15
+ __version__ = "2.1b0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opentelemetry-instrumentation-vertexai
3
- Version: 2.0b0
3
+ Version: 2.1b0
4
4
  Summary: OpenTelemetry Official VertexAI instrumentation
5
5
  Project-URL: Homepage, https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation-genai/opentelemetry-instrumentation-vertexai
6
6
  Project-URL: Repository, https://github.com/open-telemetry/opentelemetry-python-contrib
@@ -12,15 +12,15 @@ Classifier: Intended Audience :: Developers
12
12
  Classifier: License :: OSI Approved :: Apache Software License
13
13
  Classifier: Programming Language :: Python
14
14
  Classifier: Programming Language :: Python :: 3
15
- Classifier: Programming Language :: Python :: 3.8
16
15
  Classifier: Programming Language :: Python :: 3.9
17
16
  Classifier: Programming Language :: Python :: 3.10
18
17
  Classifier: Programming Language :: Python :: 3.11
19
18
  Classifier: Programming Language :: Python :: 3.12
20
- Requires-Python: >=3.8
21
- Requires-Dist: opentelemetry-api~=1.28
22
- Requires-Dist: opentelemetry-instrumentation~=0.49b0
23
- Requires-Dist: opentelemetry-semantic-conventions~=0.49b0
19
+ Requires-Python: >=3.9
20
+ Requires-Dist: opentelemetry-api~=1.37
21
+ Requires-Dist: opentelemetry-instrumentation~=0.58b0
22
+ Requires-Dist: opentelemetry-semantic-conventions~=0.58b0
23
+ Requires-Dist: opentelemetry-util-genai<0.3b0,>=0.2b0
24
24
  Provides-Extra: instruments
25
25
  Requires-Dist: google-cloud-aiplatform>=1.64; extra == 'instruments'
26
26
  Description-Content-Type: text/x-rst
@@ -0,0 +1,12 @@
1
+ opentelemetry/instrumentation/vertexai/__init__.py,sha256=h2ikRd_hLXscLwbacyfCY5fdFYH76iMxkHHr8r1AeCU,6010
2
+ opentelemetry/instrumentation/vertexai/events.py,sha256=EHp1uqJ-vMKJ5jVxRgHNyboN51JUYEPcXcT3yV5rcRE,5274
3
+ opentelemetry/instrumentation/vertexai/package.py,sha256=CFLAAZb6L_fDNfJgpW-cXjhiQjwGLAuxhdAjMNt3jPM,638
4
+ opentelemetry/instrumentation/vertexai/patch.py,sha256=_YZpMo14k-ccy8VcNbl9rG7VsztLorcS12an6e8wJx8,13543
5
+ opentelemetry/instrumentation/vertexai/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ opentelemetry/instrumentation/vertexai/utils.py,sha256=CxOypyBGjpuaKX4lijomyGBupiViC_TpYHgiCTR2nGE,15001
7
+ opentelemetry/instrumentation/vertexai/version.py,sha256=tlQVIB6SsAnJ7uZmREAk0QDaATSCgbgYwdtY4G8GX9M,607
8
+ opentelemetry_instrumentation_vertexai-2.1b0.dist-info/METADATA,sha256=tXZDreIfz0gB64b5zYAdufFr3WdjRL6hB_1k4ynrrL4,3946
9
+ opentelemetry_instrumentation_vertexai-2.1b0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
+ opentelemetry_instrumentation_vertexai-2.1b0.dist-info/entry_points.txt,sha256=aAbxWr7zIDuYms-m-ea5GEV2rqyx7xPT8FWr2umrCmU,100
11
+ opentelemetry_instrumentation_vertexai-2.1b0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
12
+ opentelemetry_instrumentation_vertexai-2.1b0.dist-info/RECORD,,
@@ -1,12 +0,0 @@
1
- opentelemetry/instrumentation/vertexai/__init__.py,sha256=DrASu5cA6RjJU9fX6z-T4Oi_DfG5bmZi49mX4D7k_us,3188
2
- opentelemetry/instrumentation/vertexai/events.py,sha256=0PlFioS1I_hnvelEwFAOMxwiLBQqQpq9ADSZa8yxF_c,5161
3
- opentelemetry/instrumentation/vertexai/package.py,sha256=CFLAAZb6L_fDNfJgpW-cXjhiQjwGLAuxhdAjMNt3jPM,638
4
- opentelemetry/instrumentation/vertexai/patch.py,sha256=UIgMrsT4Qhr6bFRUn2vca_ntQgR7MGUMRlVZEsgMvDA,4769
5
- opentelemetry/instrumentation/vertexai/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- opentelemetry/instrumentation/vertexai/utils.py,sha256=CJwxZIH-9CmnCcbc9TXsG9W59NaJdU_cHDVTdK-6jCU,11546
7
- opentelemetry/instrumentation/vertexai/version.py,sha256=3DvzQveBD-YdMIJDP5YIVXzqInnizLBgk8mSkEdl7CA,607
8
- opentelemetry_instrumentation_vertexai-2.0b0.dist-info/METADATA,sha256=G6fPgJcQPWVvSTSeD1oC-ssN6V7_Rql_30B1Q3BaW3I,3942
9
- opentelemetry_instrumentation_vertexai-2.0b0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
- opentelemetry_instrumentation_vertexai-2.0b0.dist-info/entry_points.txt,sha256=aAbxWr7zIDuYms-m-ea5GEV2rqyx7xPT8FWr2umrCmU,100
11
- opentelemetry_instrumentation_vertexai-2.0b0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
12
- opentelemetry_instrumentation_vertexai-2.0b0.dist-info/RECORD,,