|
12 | 12 | # See the License for the specific language governing permissions and |
13 | 13 | # limitations under the License. |
14 | 14 |
|
| 15 | +from __future__ import annotations |
| 16 | + |
15 | 17 | import copy |
16 | 18 | import functools |
17 | 19 | import json |
18 | 20 | import logging |
19 | 21 | import os |
20 | 22 | import time |
21 | 23 | from typing import Any, AsyncIterator, Awaitable, Iterator, Optional, Union |
| 24 | +from uuid import uuid4 |
22 | 25 |
|
23 | 26 | from google.genai.models import AsyncModels, Models |
| 27 | +from google.genai.models import t as transformers |
24 | 28 | from google.genai.types import ( |
25 | 29 | BlockedReason, |
26 | 30 | Candidate, |
|
45 | 49 | from .custom_semconv import GCP_GENAI_OPERATION_CONFIG |
46 | 50 | from .dict_util import flatten_dict |
47 | 51 | from .flags import is_content_recording_enabled |
| 52 | +from .message import ( |
| 53 | + to_input_messages, |
| 54 | + to_output_message, |
| 55 | + to_system_instruction, |
| 56 | +) |
48 | 57 | from .otel_wrapper import OTelWrapper |
49 | 58 | from .tool_call_wrapper import wrapped as wrapped_tool |
50 | 59 |
|
@@ -143,6 +152,17 @@ def _to_dict(value: object): |
143 | 152 | return json.loads(json.dumps(value)) |
144 | 153 |
|
145 | 154 |
|
| 155 | +def _config_to_system_instruction( |
| 156 | + config: GenerateContentConfigOrDict | None, |
| 157 | +) -> ContentUnion | None: |
| 158 | + if not config: |
| 159 | + return None |
| 160 | + |
| 161 | + if isinstance(config, dict): |
| 162 | + return GenerateContentConfig.model_validate(config).system_instruction |
| 163 | + return config.system_instruction |
| 164 | + |
| 165 | + |
146 | 166 | def _add_request_options_to_span( |
147 | 167 | span, config: Optional[GenerateContentConfigOrDict], allow_list: AllowList |
148 | 168 | ): |
@@ -242,6 +262,7 @@ def __init__( |
242 | 262 | ): |
243 | 263 | self._start_time = time.time_ns() |
244 | 264 | self._otel_wrapper = otel_wrapper |
| 265 | + self._models_object = models_object |
245 | 266 | self._genai_system = _determine_genai_system(models_object) |
246 | 267 | self._genai_request_model = model |
247 | 268 | self._finish_reasons_set = set() |
@@ -290,18 +311,25 @@ def process_request( |
290 | 311 | _add_request_options_to_span( |
291 | 312 | span, config, self._generate_content_config_key_allowlist |
292 | 313 | ) |
293 | | - self._maybe_log_system_instruction(config=config) |
294 | | - self._maybe_log_user_prompt(contents) |
295 | 314 |
|
296 | | - def process_response(self, response: GenerateContentResponse): |
| 315 | + def process_completion( |
| 316 | + self, |
| 317 | + *, |
| 318 | + config: Optional[GenerateContentConfigOrDict], |
| 319 | + request: Union[ContentListUnion, ContentListUnionDict], |
| 320 | + response: GenerateContentResponse, |
| 321 | + ): |
297 | 322 | # TODO: Determine if there are other response properties that |
298 | 323 | # need to be reflected back into the span attributes. |
299 | 324 | # |
300 | 325 | # See also: TODOS.md. |
| 326 | + self._maybe_log_completion_details( |
| 327 | + config=config, request=request, response=response |
| 328 | + ) |
301 | 329 | self._update_finish_reasons(response) |
302 | 330 | self._maybe_update_token_counts(response) |
303 | 331 | self._maybe_update_error_type(response) |
304 | | - self._maybe_log_response(response) |
| 332 | + # self._maybe_log_response(response) |
305 | 333 | self._response_index += 1 |
306 | 334 |
|
307 | 335 | def process_error(self, e: Exception): |
@@ -373,6 +401,45 @@ def _maybe_update_error_type(self, response: GenerateContentResponse): |
373 | 401 | block_reason = response.prompt_feedback.block_reason.name.upper() |
374 | 402 | self._error_type = f"BLOCKED_{block_reason}" |
375 | 403 |
|
| 404 | + def _maybe_log_completion_details( |
| 405 | + self, |
| 406 | + *, |
| 407 | + config: Optional[GenerateContentConfigOrDict], |
| 408 | + request: Union[ContentListUnion, ContentListUnionDict], |
| 409 | + response: GenerateContentResponse, |
| 410 | + ) -> None: |
| 411 | + attributes = { |
| 412 | + gen_ai_attributes.GEN_AI_SYSTEM: self._genai_system, |
| 413 | + } |
| 414 | + |
| 415 | + system_instruction = None |
| 416 | + if system_content := _config_to_system_instruction(config): |
| 417 | + system_instruction = to_system_instruction( |
| 418 | + content=transformers.t_contents(system_content)[0] |
| 419 | + ) |
| 420 | + input_messages = to_input_messages( |
| 421 | + contents=transformers.t_contents(request) |
| 422 | + ) |
| 423 | + output_message = to_output_message( |
| 424 | + candidates=response.candidates or [] |
| 425 | + ) |
| 426 | + |
| 427 | + self._otel_wrapper.log_completion_details( |
| 428 | + system_instructions=system_instruction, |
| 429 | + input_messages=input_messages, |
| 430 | + output_messages=output_message, |
| 431 | + attributes=attributes, |
| 432 | + ) |
| 433 | + |
| 434 | + # Forward looking remote storage refs |
| 435 | + self._otel_wrapper.log_completion_details_refs( |
| 436 | + system_instructions=system_instruction, |
| 437 | + input_messages=input_messages, |
| 438 | + output_messages=output_message, |
| 439 | + attributes=attributes, |
| 440 | + response_id=response.response_id or str(uuid4()), |
| 441 | + ) |
| 442 | + |
376 | 443 | def _maybe_log_system_instruction( |
377 | 444 | self, config: Optional[GenerateContentConfigOrDict] = None |
378 | 445 | ): |
@@ -596,7 +663,9 @@ def instrumented_generate_content( |
596 | 663 | config=helper.wrapped_config(config), |
597 | 664 | **kwargs, |
598 | 665 | ) |
599 | | - helper.process_response(response) |
| 666 | + helper.process_completion( |
| 667 | + config=config, request=contents, response=response |
| 668 | + ) |
600 | 669 | return response |
601 | 670 | except Exception as error: |
602 | 671 | helper.process_error(error) |
@@ -641,7 +710,9 @@ def instrumented_generate_content_stream( |
641 | 710 | config=helper.wrapped_config(config), |
642 | 711 | **kwargs, |
643 | 712 | ): |
644 | | - helper.process_response(response) |
| 713 | + helper.process_completion( |
| 714 | + config=config, request=contents, response=response |
| 715 | + ) |
645 | 716 | yield response |
646 | 717 | except Exception as error: |
647 | 718 | helper.process_error(error) |
@@ -686,7 +757,10 @@ async def instrumented_generate_content( |
686 | 757 | config=helper.wrapped_config(config), |
687 | 758 | **kwargs, |
688 | 759 | ) |
689 | | - helper.process_response(response) |
| 760 | + helper.process_completion( |
| 761 | + config=config, request=contents, response=response |
| 762 | + ) |
| 763 | + |
690 | 764 | return response |
691 | 765 | except Exception as error: |
692 | 766 | helper.process_error(error) |
@@ -744,7 +818,9 @@ async def _response_async_generator_wrapper(): |
744 | 818 | with trace.use_span(span, end_on_exit=True): |
745 | 819 | try: |
746 | 820 | async for response in response_async_generator: |
747 | | - helper.process_response(response) |
| 821 | + helper.process_completion( |
| 822 | + config=config, request=contents, response=response |
| 823 | + ) |
748 | 824 | yield response |
749 | 825 | except Exception as error: |
750 | 826 | helper.process_error(error) |
|
0 commit comments