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.
- opentelemetry/instrumentation/vertexai/__init__.py +150 -343
- opentelemetry/instrumentation/vertexai/events.py +190 -0
- opentelemetry/instrumentation/vertexai/package.py +16 -0
- opentelemetry/instrumentation/vertexai/patch.py +371 -0
- opentelemetry/instrumentation/vertexai/py.typed +0 -0
- opentelemetry/instrumentation/vertexai/utils.py +445 -29
- opentelemetry/instrumentation/vertexai/version.py +15 -1
- opentelemetry_instrumentation_vertexai-2.1b0.dist-info/METADATA +106 -0
- opentelemetry_instrumentation_vertexai-2.1b0.dist-info/RECORD +12 -0
- {opentelemetry_instrumentation_vertexai-0.47.3.dist-info → opentelemetry_instrumentation_vertexai-2.1b0.dist-info}/WHEEL +1 -1
- opentelemetry_instrumentation_vertexai-2.1b0.dist-info/entry_points.txt +2 -0
- opentelemetry_instrumentation_vertexai-2.1b0.dist-info/licenses/LICENSE +201 -0
- opentelemetry/instrumentation/vertexai/config.py +0 -9
- opentelemetry/instrumentation/vertexai/event_emitter.py +0 -164
- opentelemetry/instrumentation/vertexai/event_models.py +0 -41
- opentelemetry/instrumentation/vertexai/span_utils.py +0 -310
- opentelemetry_instrumentation_vertexai-0.47.3.dist-info/METADATA +0 -58
- opentelemetry_instrumentation_vertexai-0.47.3.dist-info/RECORD +0 -11
- opentelemetry_instrumentation_vertexai-0.47.3.dist-info/entry_points.txt +0 -3
|
@@ -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
|