opentelemetry-instrumentation-vertexai 0.40.14__py3-none-any.whl → 0.41.0__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.
- opentelemetry/instrumentation/vertexai/__init__.py +85 -119
- opentelemetry/instrumentation/vertexai/config.py +1 -0
- opentelemetry/instrumentation/vertexai/event_emitter.py +164 -0
- opentelemetry/instrumentation/vertexai/event_models.py +41 -0
- opentelemetry/instrumentation/vertexai/span_utils.py +89 -0
- opentelemetry/instrumentation/vertexai/utils.py +14 -0
- opentelemetry/instrumentation/vertexai/version.py +1 -1
- {opentelemetry_instrumentation_vertexai-0.40.14.dist-info → opentelemetry_instrumentation_vertexai-0.41.0.dist-info}/METADATA +2 -2
- opentelemetry_instrumentation_vertexai-0.41.0.dist-info/RECORD +11 -0
- opentelemetry_instrumentation_vertexai-0.40.14.dist-info/RECORD +0 -8
- {opentelemetry_instrumentation_vertexai-0.40.14.dist-info → opentelemetry_instrumentation_vertexai-0.41.0.dist-info}/WHEEL +0 -0
- {opentelemetry_instrumentation_vertexai-0.40.14.dist-info → opentelemetry_instrumentation_vertexai-0.41.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,26 +1,34 @@
|
|
|
1
1
|
"""OpenTelemetry Vertex AI instrumentation"""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
import os
|
|
5
4
|
import types
|
|
6
5
|
from typing import Collection
|
|
7
|
-
from opentelemetry.instrumentation.vertexai.config import Config
|
|
8
|
-
from opentelemetry.instrumentation.vertexai.utils import dont_throw
|
|
9
|
-
from wrapt import wrap_function_wrapper
|
|
10
6
|
|
|
11
7
|
from opentelemetry import context as context_api
|
|
12
|
-
from opentelemetry.
|
|
13
|
-
from opentelemetry.trace.status import Status, StatusCode
|
|
14
|
-
|
|
8
|
+
from opentelemetry._events import get_event_logger
|
|
15
9
|
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
|
16
10
|
from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY, unwrap
|
|
17
|
-
|
|
11
|
+
from opentelemetry.instrumentation.vertexai.config import Config
|
|
12
|
+
from opentelemetry.instrumentation.vertexai.event_emitter import (
|
|
13
|
+
emit_prompt_events,
|
|
14
|
+
emit_response_events,
|
|
15
|
+
)
|
|
16
|
+
from opentelemetry.instrumentation.vertexai.span_utils import (
|
|
17
|
+
set_input_attributes,
|
|
18
|
+
set_model_input_attributes,
|
|
19
|
+
set_model_response_attributes,
|
|
20
|
+
set_response_attributes,
|
|
21
|
+
)
|
|
22
|
+
from opentelemetry.instrumentation.vertexai.utils import dont_throw, should_emit_events
|
|
23
|
+
from opentelemetry.instrumentation.vertexai.version import __version__
|
|
18
24
|
from opentelemetry.semconv_ai import (
|
|
19
25
|
SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY,
|
|
20
|
-
SpanAttributes,
|
|
21
26
|
LLMRequestTypeValues,
|
|
27
|
+
SpanAttributes,
|
|
22
28
|
)
|
|
23
|
-
from opentelemetry.
|
|
29
|
+
from opentelemetry.trace import SpanKind, get_tracer
|
|
30
|
+
from opentelemetry.trace.status import Status, StatusCode
|
|
31
|
+
from wrapt import wrap_function_wrapper
|
|
24
32
|
|
|
25
33
|
logger = logging.getLogger(__name__)
|
|
26
34
|
|
|
@@ -114,12 +122,6 @@ WRAPPED_METHODS = [
|
|
|
114
122
|
]
|
|
115
123
|
|
|
116
124
|
|
|
117
|
-
def should_send_prompts():
|
|
118
|
-
return (
|
|
119
|
-
os.getenv("TRACELOOP_TRACE_CONTENT") or "true"
|
|
120
|
-
).lower() == "true" or context_api.get_value("override_enable_content_tracing")
|
|
121
|
-
|
|
122
|
-
|
|
123
125
|
def is_streaming_response(response):
|
|
124
126
|
return isinstance(response, types.GeneratorType)
|
|
125
127
|
|
|
@@ -128,81 +130,18 @@ def is_async_streaming_response(response):
|
|
|
128
130
|
return isinstance(response, types.AsyncGeneratorType)
|
|
129
131
|
|
|
130
132
|
|
|
131
|
-
def _set_span_attribute(span, name, value):
|
|
132
|
-
if value is not None:
|
|
133
|
-
if value != "":
|
|
134
|
-
span.set_attribute(name, value)
|
|
135
|
-
return
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
def _set_input_attributes(span, args, kwargs, llm_model):
|
|
139
|
-
if should_send_prompts() and args is not None and len(args) > 0:
|
|
140
|
-
prompt = ""
|
|
141
|
-
for arg in args:
|
|
142
|
-
if isinstance(arg, str):
|
|
143
|
-
prompt = f"{prompt}{arg}\n"
|
|
144
|
-
elif isinstance(arg, list):
|
|
145
|
-
for subarg in arg:
|
|
146
|
-
prompt = f"{prompt}{subarg}\n"
|
|
147
|
-
|
|
148
|
-
_set_span_attribute(
|
|
149
|
-
span,
|
|
150
|
-
f"{SpanAttributes.LLM_PROMPTS}.0.user",
|
|
151
|
-
prompt,
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
_set_span_attribute(span, SpanAttributes.LLM_REQUEST_MODEL, llm_model)
|
|
155
|
-
_set_span_attribute(
|
|
156
|
-
span, f"{SpanAttributes.LLM_PROMPTS}.0.user", kwargs.get("prompt")
|
|
157
|
-
)
|
|
158
|
-
_set_span_attribute(
|
|
159
|
-
span, SpanAttributes.LLM_REQUEST_TEMPERATURE, kwargs.get("temperature")
|
|
160
|
-
)
|
|
161
|
-
_set_span_attribute(
|
|
162
|
-
span, SpanAttributes.LLM_REQUEST_MAX_TOKENS, kwargs.get("max_output_tokens")
|
|
163
|
-
)
|
|
164
|
-
_set_span_attribute(span, SpanAttributes.LLM_REQUEST_TOP_P, kwargs.get("top_p"))
|
|
165
|
-
_set_span_attribute(span, SpanAttributes.LLM_TOP_K, kwargs.get("top_k"))
|
|
166
|
-
_set_span_attribute(
|
|
167
|
-
span, SpanAttributes.LLM_PRESENCE_PENALTY, kwargs.get("presence_penalty")
|
|
168
|
-
)
|
|
169
|
-
_set_span_attribute(
|
|
170
|
-
span, SpanAttributes.LLM_FREQUENCY_PENALTY, kwargs.get("frequency_penalty")
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
return
|
|
174
|
-
|
|
175
|
-
|
|
176
133
|
@dont_throw
|
|
177
|
-
def
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
)
|
|
186
|
-
_set_span_attribute(
|
|
187
|
-
span,
|
|
188
|
-
SpanAttributes.LLM_USAGE_COMPLETION_TOKENS,
|
|
189
|
-
token_usage.candidates_token_count,
|
|
190
|
-
)
|
|
191
|
-
_set_span_attribute(
|
|
192
|
-
span,
|
|
193
|
-
SpanAttributes.LLM_USAGE_PROMPT_TOKENS,
|
|
194
|
-
token_usage.prompt_token_count,
|
|
195
|
-
)
|
|
196
|
-
|
|
197
|
-
_set_span_attribute(span, f"{SpanAttributes.LLM_COMPLETIONS}.0.role", "assistant")
|
|
198
|
-
_set_span_attribute(
|
|
199
|
-
span,
|
|
200
|
-
f"{SpanAttributes.LLM_COMPLETIONS}.0.content",
|
|
201
|
-
generation_text,
|
|
202
|
-
)
|
|
134
|
+
def handle_streaming_response(span, event_logger, llm_model, response, token_usage):
|
|
135
|
+
set_model_response_attributes(span, llm_model, token_usage)
|
|
136
|
+
if should_emit_events():
|
|
137
|
+
emit_response_events(response, event_logger)
|
|
138
|
+
else:
|
|
139
|
+
set_response_attributes(span, llm_model, response)
|
|
140
|
+
if span.is_recording():
|
|
141
|
+
span.set_status(Status(StatusCode.OK))
|
|
203
142
|
|
|
204
143
|
|
|
205
|
-
def _build_from_streaming_response(span, response, llm_model):
|
|
144
|
+
def _build_from_streaming_response(span, event_logger, response, llm_model):
|
|
206
145
|
complete_response = ""
|
|
207
146
|
token_usage = None
|
|
208
147
|
for item in response:
|
|
@@ -213,13 +152,15 @@ def _build_from_streaming_response(span, response, llm_model):
|
|
|
213
152
|
|
|
214
153
|
yield item_to_yield
|
|
215
154
|
|
|
216
|
-
|
|
155
|
+
handle_streaming_response(
|
|
156
|
+
span, event_logger, llm_model, complete_response, token_usage
|
|
157
|
+
)
|
|
217
158
|
|
|
218
159
|
span.set_status(Status(StatusCode.OK))
|
|
219
160
|
span.end()
|
|
220
161
|
|
|
221
162
|
|
|
222
|
-
async def _abuild_from_streaming_response(span, response, llm_model):
|
|
163
|
+
async def _abuild_from_streaming_response(span, event_logger, response, llm_model):
|
|
223
164
|
complete_response = ""
|
|
224
165
|
token_usage = None
|
|
225
166
|
async for item in response:
|
|
@@ -230,34 +171,39 @@ async def _abuild_from_streaming_response(span, response, llm_model):
|
|
|
230
171
|
|
|
231
172
|
yield item_to_yield
|
|
232
173
|
|
|
233
|
-
|
|
174
|
+
handle_streaming_response(span, event_logger, llm_model, response, token_usage)
|
|
234
175
|
|
|
235
176
|
span.set_status(Status(StatusCode.OK))
|
|
236
177
|
span.end()
|
|
237
178
|
|
|
238
179
|
|
|
239
180
|
@dont_throw
|
|
240
|
-
def _handle_request(span, args, kwargs, llm_model):
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
181
|
+
def _handle_request(span, event_logger, args, kwargs, llm_model):
|
|
182
|
+
set_model_input_attributes(span, kwargs, llm_model)
|
|
183
|
+
if should_emit_events():
|
|
184
|
+
emit_prompt_events(args, event_logger)
|
|
185
|
+
else:
|
|
186
|
+
set_input_attributes(span, args)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _handle_response(span, event_logger, response, llm_model):
|
|
190
|
+
set_model_response_attributes(span, llm_model, response.usage_metadata)
|
|
191
|
+
if should_emit_events():
|
|
192
|
+
emit_response_events(response, event_logger)
|
|
193
|
+
else:
|
|
194
|
+
set_response_attributes(
|
|
195
|
+
span, llm_model, response.candidates[0].text if response.candidates else ""
|
|
250
196
|
)
|
|
251
|
-
|
|
197
|
+
if span.is_recording():
|
|
252
198
|
span.set_status(Status(StatusCode.OK))
|
|
253
199
|
|
|
254
200
|
|
|
255
201
|
def _with_tracer_wrapper(func):
|
|
256
202
|
"""Helper for providing tracer for wrapper functions."""
|
|
257
203
|
|
|
258
|
-
def _with_tracer(tracer, to_wrap):
|
|
204
|
+
def _with_tracer(tracer, event_logger, to_wrap):
|
|
259
205
|
def wrapper(wrapped, instance, args, kwargs):
|
|
260
|
-
return func(tracer, to_wrap, wrapped, instance, args, kwargs)
|
|
206
|
+
return func(tracer, event_logger, to_wrap, wrapped, instance, args, kwargs)
|
|
261
207
|
|
|
262
208
|
return wrapper
|
|
263
209
|
|
|
@@ -265,7 +211,7 @@ def _with_tracer_wrapper(func):
|
|
|
265
211
|
|
|
266
212
|
|
|
267
213
|
@_with_tracer_wrapper
|
|
268
|
-
async def _awrap(tracer, to_wrap, wrapped, instance, args, kwargs):
|
|
214
|
+
async def _awrap(tracer, event_logger, to_wrap, wrapped, instance, args, kwargs):
|
|
269
215
|
"""Instruments and calls every function defined in TO_WRAP."""
|
|
270
216
|
if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY) or context_api.get_value(
|
|
271
217
|
SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY
|
|
@@ -283,29 +229,33 @@ async def _awrap(tracer, to_wrap, wrapped, instance, args, kwargs):
|
|
|
283
229
|
name,
|
|
284
230
|
kind=SpanKind.CLIENT,
|
|
285
231
|
attributes={
|
|
286
|
-
SpanAttributes.LLM_SYSTEM: "
|
|
232
|
+
SpanAttributes.LLM_SYSTEM: "Google",
|
|
287
233
|
SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.COMPLETION.value,
|
|
288
234
|
},
|
|
289
235
|
)
|
|
290
236
|
|
|
291
|
-
_handle_request(span, args, kwargs, llm_model)
|
|
237
|
+
_handle_request(span, event_logger, args, kwargs, llm_model)
|
|
292
238
|
|
|
293
239
|
response = await wrapped(*args, **kwargs)
|
|
294
240
|
|
|
295
241
|
if response:
|
|
296
242
|
if is_streaming_response(response):
|
|
297
|
-
return _build_from_streaming_response(
|
|
243
|
+
return _build_from_streaming_response(
|
|
244
|
+
span, event_logger, response, llm_model
|
|
245
|
+
)
|
|
298
246
|
elif is_async_streaming_response(response):
|
|
299
|
-
return _abuild_from_streaming_response(
|
|
247
|
+
return _abuild_from_streaming_response(
|
|
248
|
+
span, event_logger, response, llm_model
|
|
249
|
+
)
|
|
300
250
|
else:
|
|
301
|
-
_handle_response(span, response, llm_model)
|
|
251
|
+
_handle_response(span, event_logger, response, llm_model)
|
|
302
252
|
|
|
303
253
|
span.end()
|
|
304
254
|
return response
|
|
305
255
|
|
|
306
256
|
|
|
307
257
|
@_with_tracer_wrapper
|
|
308
|
-
def _wrap(tracer, to_wrap, wrapped, instance, args, kwargs):
|
|
258
|
+
def _wrap(tracer, event_logger, to_wrap, wrapped, instance, args, kwargs):
|
|
309
259
|
"""Instruments and calls every function defined in TO_WRAP."""
|
|
310
260
|
if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY) or context_api.get_value(
|
|
311
261
|
SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY
|
|
@@ -323,22 +273,26 @@ def _wrap(tracer, to_wrap, wrapped, instance, args, kwargs):
|
|
|
323
273
|
name,
|
|
324
274
|
kind=SpanKind.CLIENT,
|
|
325
275
|
attributes={
|
|
326
|
-
SpanAttributes.LLM_SYSTEM: "
|
|
276
|
+
SpanAttributes.LLM_SYSTEM: "Google",
|
|
327
277
|
SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.COMPLETION.value,
|
|
328
278
|
},
|
|
329
279
|
)
|
|
330
280
|
|
|
331
|
-
_handle_request(span, args, kwargs, llm_model)
|
|
281
|
+
_handle_request(span, event_logger, args, kwargs, llm_model)
|
|
332
282
|
|
|
333
283
|
response = wrapped(*args, **kwargs)
|
|
334
284
|
|
|
335
285
|
if response:
|
|
336
286
|
if is_streaming_response(response):
|
|
337
|
-
return _build_from_streaming_response(
|
|
287
|
+
return _build_from_streaming_response(
|
|
288
|
+
span, event_logger, response, llm_model
|
|
289
|
+
)
|
|
338
290
|
elif is_async_streaming_response(response):
|
|
339
|
-
return _abuild_from_streaming_response(
|
|
291
|
+
return _abuild_from_streaming_response(
|
|
292
|
+
span, event_logger, response, llm_model
|
|
293
|
+
)
|
|
340
294
|
else:
|
|
341
|
-
_handle_response(span, response, llm_model)
|
|
295
|
+
_handle_response(span, event_logger, response, llm_model)
|
|
342
296
|
|
|
343
297
|
span.end()
|
|
344
298
|
return response
|
|
@@ -347,9 +301,10 @@ def _wrap(tracer, to_wrap, wrapped, instance, args, kwargs):
|
|
|
347
301
|
class VertexAIInstrumentor(BaseInstrumentor):
|
|
348
302
|
"""An instrumentor for VertextAI's client library."""
|
|
349
303
|
|
|
350
|
-
def __init__(self, exception_logger=None):
|
|
304
|
+
def __init__(self, exception_logger=None, use_legacy_attributes=True):
|
|
351
305
|
super().__init__()
|
|
352
306
|
Config.exception_logger = exception_logger
|
|
307
|
+
Config.use_legacy_attributes = use_legacy_attributes
|
|
353
308
|
|
|
354
309
|
def instrumentation_dependencies(self) -> Collection[str]:
|
|
355
310
|
return _instruments
|
|
@@ -357,6 +312,17 @@ class VertexAIInstrumentor(BaseInstrumentor):
|
|
|
357
312
|
def _instrument(self, **kwargs):
|
|
358
313
|
tracer_provider = kwargs.get("tracer_provider")
|
|
359
314
|
tracer = get_tracer(__name__, __version__, tracer_provider)
|
|
315
|
+
|
|
316
|
+
event_logger = None
|
|
317
|
+
|
|
318
|
+
if should_emit_events():
|
|
319
|
+
event_logger_provider = kwargs.get("event_logger_provider")
|
|
320
|
+
event_logger = get_event_logger(
|
|
321
|
+
__name__,
|
|
322
|
+
__version__,
|
|
323
|
+
event_logger_provider=event_logger_provider,
|
|
324
|
+
)
|
|
325
|
+
|
|
360
326
|
for wrapped_method in WRAPPED_METHODS:
|
|
361
327
|
wrap_package = wrapped_method.get("package")
|
|
362
328
|
wrap_object = wrapped_method.get("object")
|
|
@@ -366,9 +332,9 @@ class VertexAIInstrumentor(BaseInstrumentor):
|
|
|
366
332
|
wrap_package,
|
|
367
333
|
f"{wrap_object}.{wrap_method}",
|
|
368
334
|
(
|
|
369
|
-
_awrap(tracer, wrapped_method)
|
|
335
|
+
_awrap(tracer, event_logger, wrapped_method)
|
|
370
336
|
if wrapped_method.get("is_async")
|
|
371
|
-
else _wrap(tracer, wrapped_method)
|
|
337
|
+
else _wrap(tracer, event_logger, wrapped_method)
|
|
372
338
|
),
|
|
373
339
|
)
|
|
374
340
|
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
from dataclasses import asdict
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Union
|
|
4
|
+
|
|
5
|
+
from opentelemetry._events import Event
|
|
6
|
+
from opentelemetry.instrumentation.vertexai.event_models import (
|
|
7
|
+
ChoiceEvent,
|
|
8
|
+
MessageEvent,
|
|
9
|
+
)
|
|
10
|
+
from opentelemetry.instrumentation.vertexai.utils import (
|
|
11
|
+
dont_throw,
|
|
12
|
+
should_emit_events,
|
|
13
|
+
should_send_prompts,
|
|
14
|
+
)
|
|
15
|
+
from opentelemetry.semconv._incubating.attributes import (
|
|
16
|
+
gen_ai_attributes as GenAIAttributes,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from vertexai.generative_models import GenerationResponse
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Roles(Enum):
|
|
23
|
+
USER = "user"
|
|
24
|
+
ASSISTANT = "assistant"
|
|
25
|
+
SYSTEM = "system"
|
|
26
|
+
TOOL = "tool"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
VALID_MESSAGE_ROLES = {role.value for role in Roles}
|
|
30
|
+
"""The valid roles for naming the message event."""
|
|
31
|
+
|
|
32
|
+
EVENT_ATTRIBUTES = {
|
|
33
|
+
GenAIAttributes.GEN_AI_SYSTEM: GenAIAttributes.GenAiSystemValues.VERTEX_AI.value
|
|
34
|
+
}
|
|
35
|
+
"""The attributes to be used for the event."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _parse_vertex_finish_reason(reason):
|
|
39
|
+
if reason is None:
|
|
40
|
+
return "unknown"
|
|
41
|
+
|
|
42
|
+
finish_reason_map = {
|
|
43
|
+
0: "unspecified",
|
|
44
|
+
1: "stop",
|
|
45
|
+
2: "max_tokens",
|
|
46
|
+
3: "safety",
|
|
47
|
+
4: "recitation",
|
|
48
|
+
5: "other",
|
|
49
|
+
6: "blocklist",
|
|
50
|
+
7: "prohibited_content",
|
|
51
|
+
8: "spii",
|
|
52
|
+
9: "malformed_function_call",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if hasattr(reason, "value"):
|
|
56
|
+
reason_value = reason.value
|
|
57
|
+
else:
|
|
58
|
+
reason_value = reason
|
|
59
|
+
|
|
60
|
+
return finish_reason_map.get(reason_value, "unknown")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dont_throw
|
|
64
|
+
def emit_prompt_events(args, event_logger):
|
|
65
|
+
prompt = ""
|
|
66
|
+
if args is not None and len(args) > 0:
|
|
67
|
+
for arg in args:
|
|
68
|
+
if isinstance(arg, str):
|
|
69
|
+
prompt = f"{prompt}{arg}\n"
|
|
70
|
+
elif isinstance(arg, list):
|
|
71
|
+
for subarg in arg:
|
|
72
|
+
prompt = f"{prompt}{subarg}\n"
|
|
73
|
+
emit_event(MessageEvent(content=prompt, role=Roles.USER.value), event_logger)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def emit_response_events(response, event_logger):
|
|
77
|
+
if isinstance(response, str):
|
|
78
|
+
emit_event(
|
|
79
|
+
ChoiceEvent(
|
|
80
|
+
index=0,
|
|
81
|
+
message={"content": response, "role": Roles.ASSISTANT.value},
|
|
82
|
+
finish_reason="unknown",
|
|
83
|
+
),
|
|
84
|
+
event_logger,
|
|
85
|
+
)
|
|
86
|
+
elif isinstance(response, GenerationResponse):
|
|
87
|
+
for candidate in response.candidates:
|
|
88
|
+
emit_event(
|
|
89
|
+
ChoiceEvent(
|
|
90
|
+
index=candidate.index,
|
|
91
|
+
message={
|
|
92
|
+
"content": candidate.text,
|
|
93
|
+
"role": Roles.ASSISTANT.value,
|
|
94
|
+
},
|
|
95
|
+
finish_reason=_parse_vertex_finish_reason(candidate.finish_reason),
|
|
96
|
+
),
|
|
97
|
+
event_logger,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def emit_event(event: Union[MessageEvent, ChoiceEvent], event_logger) -> None:
|
|
102
|
+
"""
|
|
103
|
+
Emit an event to the OpenTelemetry SDK.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
event: The event to emit.
|
|
107
|
+
"""
|
|
108
|
+
if not should_emit_events() or event_logger is None:
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
if isinstance(event, MessageEvent):
|
|
112
|
+
_emit_message_event(event, event_logger)
|
|
113
|
+
elif isinstance(event, ChoiceEvent):
|
|
114
|
+
_emit_choice_event(event, event_logger)
|
|
115
|
+
else:
|
|
116
|
+
raise TypeError("Unsupported event type")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _emit_message_event(event: MessageEvent, event_logger) -> None:
|
|
120
|
+
body = asdict(event)
|
|
121
|
+
|
|
122
|
+
if event.role in VALID_MESSAGE_ROLES:
|
|
123
|
+
name = "gen_ai.{}.message".format(event.role)
|
|
124
|
+
# According to the semantic conventions, the role is conditionally required if available
|
|
125
|
+
# and not equal to the "role" in the message name. So, remove the role from the body if
|
|
126
|
+
# it is the same as the in the event name.
|
|
127
|
+
body.pop("role", None)
|
|
128
|
+
else:
|
|
129
|
+
name = "gen_ai.user.message"
|
|
130
|
+
|
|
131
|
+
# According to the semantic conventions, only the assistant role has tool call
|
|
132
|
+
if event.role != Roles.ASSISTANT.value and event.tool_calls is not None:
|
|
133
|
+
del body["tool_calls"]
|
|
134
|
+
elif event.tool_calls is None:
|
|
135
|
+
del body["tool_calls"]
|
|
136
|
+
|
|
137
|
+
if not should_send_prompts():
|
|
138
|
+
del body["content"]
|
|
139
|
+
if body.get("tool_calls") is not None:
|
|
140
|
+
for tool_call in body["tool_calls"]:
|
|
141
|
+
tool_call["function"].pop("arguments", None)
|
|
142
|
+
|
|
143
|
+
event_logger.emit(Event(name=name, body=body, attributes=EVENT_ATTRIBUTES))
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _emit_choice_event(event: ChoiceEvent, event_logger) -> None:
|
|
147
|
+
body = asdict(event)
|
|
148
|
+
if event.message["role"] == Roles.ASSISTANT.value:
|
|
149
|
+
# According to the semantic conventions, the role is conditionally required if available
|
|
150
|
+
# and not equal to "assistant", so remove the role from the body if it is "assistant".
|
|
151
|
+
body["message"].pop("role", None)
|
|
152
|
+
|
|
153
|
+
if event.tool_calls is None:
|
|
154
|
+
del body["tool_calls"]
|
|
155
|
+
|
|
156
|
+
if not should_send_prompts():
|
|
157
|
+
body["message"].pop("content", None)
|
|
158
|
+
if body.get("tool_calls") is not None:
|
|
159
|
+
for tool_call in body["tool_calls"]:
|
|
160
|
+
tool_call["function"].pop("arguments", None)
|
|
161
|
+
|
|
162
|
+
event_logger.emit(
|
|
163
|
+
Event(name="gen_ai.choice", body=body, attributes=EVENT_ATTRIBUTES)
|
|
164
|
+
)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any, List, Literal, Optional, TypedDict
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class _FunctionToolCall(TypedDict):
|
|
6
|
+
function_name: str
|
|
7
|
+
arguments: Optional[dict[str, Any]]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ToolCall(TypedDict):
|
|
11
|
+
"""Represents a tool call in the AI model."""
|
|
12
|
+
|
|
13
|
+
id: str
|
|
14
|
+
function: _FunctionToolCall
|
|
15
|
+
type: Literal["function"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CompletionMessage(TypedDict):
|
|
19
|
+
"""Represents a message in the AI model."""
|
|
20
|
+
|
|
21
|
+
content: Any
|
|
22
|
+
role: str = "assistant"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class MessageEvent:
|
|
27
|
+
"""Represents an input event for the AI model."""
|
|
28
|
+
|
|
29
|
+
content: Any
|
|
30
|
+
role: str = "user"
|
|
31
|
+
tool_calls: Optional[List[ToolCall]] = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class ChoiceEvent:
|
|
36
|
+
"""Represents a completion event for the AI model."""
|
|
37
|
+
|
|
38
|
+
index: int
|
|
39
|
+
message: CompletionMessage
|
|
40
|
+
finish_reason: str = "unknown"
|
|
41
|
+
tool_calls: Optional[List[ToolCall]] = None
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from opentelemetry.instrumentation.vertexai.utils import dont_throw, should_send_prompts
|
|
2
|
+
from opentelemetry.semconv_ai import SpanAttributes
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _set_span_attribute(span, name, value):
|
|
6
|
+
if value is not None:
|
|
7
|
+
if value != "":
|
|
8
|
+
span.set_attribute(name, value)
|
|
9
|
+
return
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dont_throw
|
|
13
|
+
def set_input_attributes(span, args):
|
|
14
|
+
if not span.is_recording():
|
|
15
|
+
return
|
|
16
|
+
if should_send_prompts() and args is not None and len(args) > 0:
|
|
17
|
+
prompt = ""
|
|
18
|
+
for arg in args:
|
|
19
|
+
if isinstance(arg, str):
|
|
20
|
+
prompt = f"{prompt}{arg}\n"
|
|
21
|
+
elif isinstance(arg, list):
|
|
22
|
+
for subarg in arg:
|
|
23
|
+
prompt = f"{prompt}{subarg}\n"
|
|
24
|
+
|
|
25
|
+
_set_span_attribute(
|
|
26
|
+
span,
|
|
27
|
+
f"{SpanAttributes.LLM_PROMPTS}.0.user",
|
|
28
|
+
prompt,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dont_throw
|
|
33
|
+
def set_model_input_attributes(span, kwargs, llm_model):
|
|
34
|
+
if not span.is_recording():
|
|
35
|
+
return
|
|
36
|
+
_set_span_attribute(span, SpanAttributes.LLM_REQUEST_MODEL, llm_model)
|
|
37
|
+
_set_span_attribute(
|
|
38
|
+
span, f"{SpanAttributes.LLM_PROMPTS}.0.user", kwargs.get("prompt")
|
|
39
|
+
)
|
|
40
|
+
_set_span_attribute(
|
|
41
|
+
span, SpanAttributes.LLM_REQUEST_TEMPERATURE, kwargs.get("temperature")
|
|
42
|
+
)
|
|
43
|
+
_set_span_attribute(
|
|
44
|
+
span, SpanAttributes.LLM_REQUEST_MAX_TOKENS, kwargs.get("max_output_tokens")
|
|
45
|
+
)
|
|
46
|
+
_set_span_attribute(span, SpanAttributes.LLM_REQUEST_TOP_P, kwargs.get("top_p"))
|
|
47
|
+
_set_span_attribute(span, SpanAttributes.LLM_TOP_K, kwargs.get("top_k"))
|
|
48
|
+
_set_span_attribute(
|
|
49
|
+
span, SpanAttributes.LLM_PRESENCE_PENALTY, kwargs.get("presence_penalty")
|
|
50
|
+
)
|
|
51
|
+
_set_span_attribute(
|
|
52
|
+
span, SpanAttributes.LLM_FREQUENCY_PENALTY, kwargs.get("frequency_penalty")
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dont_throw
|
|
57
|
+
def set_response_attributes(span, llm_model, generation_text):
|
|
58
|
+
if not span.is_recording() or not should_send_prompts():
|
|
59
|
+
return
|
|
60
|
+
_set_span_attribute(span, f"{SpanAttributes.LLM_COMPLETIONS}.0.role", "assistant")
|
|
61
|
+
_set_span_attribute(
|
|
62
|
+
span,
|
|
63
|
+
f"{SpanAttributes.LLM_COMPLETIONS}.0.content",
|
|
64
|
+
generation_text,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dont_throw
|
|
69
|
+
def set_model_response_attributes(span, llm_model, token_usage):
|
|
70
|
+
if not span.is_recording():
|
|
71
|
+
return
|
|
72
|
+
_set_span_attribute(span, SpanAttributes.LLM_RESPONSE_MODEL, llm_model)
|
|
73
|
+
|
|
74
|
+
if token_usage:
|
|
75
|
+
_set_span_attribute(
|
|
76
|
+
span,
|
|
77
|
+
SpanAttributes.LLM_USAGE_TOTAL_TOKENS,
|
|
78
|
+
token_usage.total_token_count,
|
|
79
|
+
)
|
|
80
|
+
_set_span_attribute(
|
|
81
|
+
span,
|
|
82
|
+
SpanAttributes.LLM_USAGE_COMPLETION_TOKENS,
|
|
83
|
+
token_usage.candidates_token_count,
|
|
84
|
+
)
|
|
85
|
+
_set_span_attribute(
|
|
86
|
+
span,
|
|
87
|
+
SpanAttributes.LLM_USAGE_PROMPT_TOKENS,
|
|
88
|
+
token_usage.prompt_token_count,
|
|
89
|
+
)
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import os
|
|
2
3
|
import traceback
|
|
3
4
|
|
|
5
|
+
from opentelemetry import context as context_api
|
|
4
6
|
from opentelemetry.instrumentation.vertexai.config import Config
|
|
5
7
|
|
|
8
|
+
TRACELOOP_TRACE_CONTENT = "TRACELOOP_TRACE_CONTENT"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def should_send_prompts():
|
|
12
|
+
return (
|
|
13
|
+
os.getenv(TRACELOOP_TRACE_CONTENT) or "true"
|
|
14
|
+
).lower() == "true" or context_api.get_value("override_enable_content_tracing")
|
|
15
|
+
|
|
6
16
|
|
|
7
17
|
def dont_throw(func):
|
|
8
18
|
"""
|
|
@@ -27,3 +37,7 @@ def dont_throw(func):
|
|
|
27
37
|
Config.exception_logger(e)
|
|
28
38
|
|
|
29
39
|
return wrapper
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def should_emit_events():
|
|
43
|
+
return not Config.use_legacy_attributes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.41.0"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: opentelemetry-instrumentation-vertexai
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.41.0
|
|
4
4
|
Summary: OpenTelemetry Vertex AI instrumentation
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Author: Gal Kleinman
|
|
@@ -17,7 +17,7 @@ Provides-Extra: instruments
|
|
|
17
17
|
Requires-Dist: opentelemetry-api (>=1.28.0,<2.0.0)
|
|
18
18
|
Requires-Dist: opentelemetry-instrumentation (>=0.50b0)
|
|
19
19
|
Requires-Dist: opentelemetry-semantic-conventions (>=0.50b0)
|
|
20
|
-
Requires-Dist: opentelemetry-semantic-conventions-ai (==0.4.
|
|
20
|
+
Requires-Dist: opentelemetry-semantic-conventions-ai (==0.4.10)
|
|
21
21
|
Project-URL: Repository, https://github.com/traceloop/openllmetry/tree/main/packages/opentelemetry-instrumentation-vertexai
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
opentelemetry/instrumentation/vertexai/__init__.py,sha256=3-sM6HhS3T3mL9Y_HkVzBJ861ilCn3ZietOFJ_iklo4,11192
|
|
2
|
+
opentelemetry/instrumentation/vertexai/config.py,sha256=aaFMaCpJS9eAxkHIP88wvlc8B1tKspysmXdULfhWGSA,75
|
|
3
|
+
opentelemetry/instrumentation/vertexai/event_emitter.py,sha256=v-f7-oWbLi1IcRjpsoPst0XhocqCKvaEovc_0Er0PDo,5043
|
|
4
|
+
opentelemetry/instrumentation/vertexai/event_models.py,sha256=PCfCGxrrArwZqR-4wFcXrhwQq0sBMAxmSrpC4PUMtaM,876
|
|
5
|
+
opentelemetry/instrumentation/vertexai/span_utils.py,sha256=8TMnqbzo0OA8JnO1qVESZfXVRLbKh8Ds1G4Z47k3R9E,2811
|
|
6
|
+
opentelemetry/instrumentation/vertexai/utils.py,sha256=Rj-TT_GQFhfi1F1rugvDRFxl4Xo4D-rOYJojOK8iblI,1172
|
|
7
|
+
opentelemetry/instrumentation/vertexai/version.py,sha256=WqzeTKVe9Q7QeiOa2KhoieBiGxzHK-Gb2TvnyeIJEKk,23
|
|
8
|
+
opentelemetry_instrumentation_vertexai-0.41.0.dist-info/METADATA,sha256=DZ12PLsT5hlWwmRHaY20My0n39gg5GgJeG5GLFled-4,2183
|
|
9
|
+
opentelemetry_instrumentation_vertexai-0.41.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
10
|
+
opentelemetry_instrumentation_vertexai-0.41.0.dist-info/entry_points.txt,sha256=HbacwtKx_31YuUruZKYKWOiTGnRw3YaazUKF3TPbzDc,114
|
|
11
|
+
opentelemetry_instrumentation_vertexai-0.41.0.dist-info/RECORD,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
opentelemetry/instrumentation/vertexai/__init__.py,sha256=wK_JzqcaH7JJqDAPfT2rpjYnA-PcSay0cpZNFAZnF9Q,11900
|
|
2
|
-
opentelemetry/instrumentation/vertexai/config.py,sha256=CtypZov_ytI9nSrfN9lWnjcufbAR9sfkXRA0OstDEUw,42
|
|
3
|
-
opentelemetry/instrumentation/vertexai/utils.py,sha256=xQHvL1ZiTkjV8LS5DibI1dzoA-gnOLeixPrfeVIqlYA,809
|
|
4
|
-
opentelemetry/instrumentation/vertexai/version.py,sha256=TzmqqRPz5JsMF0vCMChofQC_r_x0W9P-JB4K5rRCvtE,24
|
|
5
|
-
opentelemetry_instrumentation_vertexai-0.40.14.dist-info/METADATA,sha256=YKwFIQyf3hJuvHU3dlGJRvbfKtD13-23lcQN3Ss0MbE,2183
|
|
6
|
-
opentelemetry_instrumentation_vertexai-0.40.14.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
7
|
-
opentelemetry_instrumentation_vertexai-0.40.14.dist-info/entry_points.txt,sha256=HbacwtKx_31YuUruZKYKWOiTGnRw3YaazUKF3TPbzDc,114
|
|
8
|
-
opentelemetry_instrumentation_vertexai-0.40.14.dist-info/RECORD,,
|
|
File without changes
|