|
| 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 | +"""This module defines the generic hooks for GenAI content uploading |
| 16 | +
|
| 17 | +The hooks are specified as part of semconv in `Uploading content to external storage |
| 18 | +<https://github.com/open-telemetry/semantic-conventions/blob/v1.37.0/docs/gen-ai/gen-ai-spans.md#uploading-content-to-external-storage>`__. |
| 19 | +
|
| 20 | +This module defines the `UploadHook` type that custom implementations should implement, and a |
| 21 | +`load_upload_hook` function to load it from an entry point. |
| 22 | +""" |
| 23 | + |
| 24 | +from __future__ import annotations |
| 25 | + |
| 26 | +import logging |
| 27 | +from os import environ |
| 28 | +from typing import Any, Protocol, cast, runtime_checkable |
| 29 | + |
| 30 | +from opentelemetry._logs import LogRecord |
| 31 | +from opentelemetry.trace import Span |
| 32 | +from opentelemetry.util._importlib_metadata import ( |
| 33 | + entry_points, # pyright: ignore[reportUnknownVariableType] |
| 34 | +) |
| 35 | +from opentelemetry.util.genai import types |
| 36 | +from opentelemetry.util.genai.environment_variables import ( |
| 37 | + OTEL_INSTRUMENTATION_GENAI_UPLOAD_HOOK, |
| 38 | +) |
| 39 | + |
| 40 | +_logger = logging.getLogger(__name__) |
| 41 | + |
| 42 | + |
| 43 | +@runtime_checkable |
| 44 | +class UploadHook(Protocol): |
| 45 | + """A hook to upload GenAI content to an external storage. |
| 46 | +
|
| 47 | + This is the interface for a hook that can be |
| 48 | + used to upload GenAI content to an external storage. The hook is a |
| 49 | + callable that takes the inputs, outputs, and system instruction of a |
| 50 | + GenAI interaction, as well as the span and log record associated with |
| 51 | + it. |
| 52 | +
|
| 53 | + The hook can be used to upload the content to any external storage, |
| 54 | + such as a database, a file system, or a cloud storage service. |
| 55 | +
|
| 56 | + The span and log_record arguments should be provided based on the content capturing mode |
| 57 | + :func:`~opentelemetry.util.genai.utils.get_content_capturing_mode`. |
| 58 | +
|
| 59 | + Args: |
| 60 | + inputs: The inputs of the GenAI interaction. |
| 61 | + outputs: The outputs of the GenAI interaction. |
| 62 | + system_instruction: The system instruction of the GenAI |
| 63 | + interaction. |
| 64 | + span: The span associated with the GenAI interaction. |
| 65 | + log_record: The event log associated with the GenAI |
| 66 | + interaction. |
| 67 | + """ |
| 68 | + |
| 69 | + def upload( |
| 70 | + self, |
| 71 | + *, |
| 72 | + inputs: list[types.InputMessage], |
| 73 | + outputs: list[types.OutputMessage], |
| 74 | + system_instruction: list[types.MessagePart], |
| 75 | + span: Span | None = None, |
| 76 | + log_record: LogRecord | None = None, |
| 77 | + ) -> None: ... |
| 78 | + |
| 79 | + |
| 80 | +class _NoOpUploadHook(UploadHook): |
| 81 | + def upload(self, **kwargs: Any) -> None: |
| 82 | + return None |
| 83 | + |
| 84 | + |
| 85 | +def load_upload_hook() -> UploadHook: |
| 86 | + """Load the upload hook from entry point or return a noop implementation |
| 87 | +
|
| 88 | + This function loads an upload hook from the entry point group |
| 89 | + ``opentelemetry_genai_upload_hook`` with name coming from |
| 90 | + :envvar:`OTEL_INSTRUMENTATION_GENAI_UPLOAD_HOOK`. If one can't be found, returns a no-op |
| 91 | + implementation. |
| 92 | + """ |
| 93 | + hook_name = environ.get(OTEL_INSTRUMENTATION_GENAI_UPLOAD_HOOK, None) |
| 94 | + if not hook_name: |
| 95 | + return _NoOpUploadHook() |
| 96 | + |
| 97 | + for entry_point in entry_points(group="opentelemetry_genai_upload_hook"): # pyright: ignore[reportUnknownVariableType] |
| 98 | + name = cast(str, entry_point.name) # pyright: ignore[reportUnknownMemberType] |
| 99 | + try: |
| 100 | + if hook_name != name: |
| 101 | + continue |
| 102 | + |
| 103 | + hook = entry_point.load()() # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType] |
| 104 | + if not isinstance(hook, UploadHook): |
| 105 | + _logger.debug("%s is not a valid UploadHook. Using noop", name) |
| 106 | + continue |
| 107 | + |
| 108 | + _logger.debug("Using UploadHook %s", name) |
| 109 | + return hook |
| 110 | + |
| 111 | + except Exception: # pylint: disable=broad-except |
| 112 | + _logger.exception( |
| 113 | + "UploadHook %s configuration failed. Using noop", name |
| 114 | + ) |
| 115 | + |
| 116 | + return _NoOpUploadHook() |
| 117 | + |
| 118 | + |
| 119 | +__all__ = ["UploadHook", "load_upload_hook"] |
0 commit comments