trulens-providers-google 2.2.3__tar.gz

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.
@@ -0,0 +1,28 @@
1
+ Metadata-Version: 2.1
2
+ Name: trulens-providers-google
3
+ Version: 2.2.3
4
+ Summary: Library to systematically track and evaluate LLM based applications.
5
+ Home-page: https://trulens.org/
6
+ License: MIT
7
+ Author: Snowflake Inc.
8
+ Author-email: ml-observability-wg-dl@snowflake.com
9
+ Requires-Python: >=3.9,<4.0
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Requires-Dist: google-auth (>=2.20.0)
20
+ Requires-Dist: google-genai (>=1.27.0)
21
+ Requires-Dist: trulens-core (>=2.0.0,<3.0.0)
22
+ Requires-Dist: trulens-feedback (>=2.0.0,<3.0.0)
23
+ Project-URL: Documentation, https://trulens.org/getting_started/
24
+ Project-URL: Repository, https://github.com/truera/trulens
25
+ Description-Content-Type: text/markdown
26
+
27
+ # trulens-providers-google
28
+
@@ -0,0 +1 @@
1
+ # trulens-providers-google
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ build-backend = "poetry.core.masonry.api"
3
+ requires = [
4
+ "poetry-core",
5
+ ]
6
+
7
+ [tool.poetry]
8
+ name = "trulens-providers-google"
9
+ version = "2.2.3"
10
+ description = "Library to systematically track and evaluate LLM based applications."
11
+ authors = [
12
+ "Snowflake Inc. <ml-observability-wg-dl@snowflake.com>",
13
+ ]
14
+ license = "MIT"
15
+ readme = "README.md"
16
+ packages = [
17
+ { include = "trulens" },
18
+ ]
19
+ homepage = "https://trulens.org/"
20
+ documentation = "https://trulens.org/getting_started/"
21
+ repository = "https://github.com/truera/trulens"
22
+ classifiers = [
23
+ "Programming Language :: Python :: 3",
24
+ "Operating System :: OS Independent",
25
+ "Development Status :: 3 - Alpha",
26
+ "License :: OSI Approved :: MIT License",
27
+ ]
28
+
29
+ [tool.poetry.dependencies]
30
+ python = "^3.9"
31
+ trulens-core = { version = "^2.0.0" }
32
+ trulens-feedback = { version = "^2.0.0" }
33
+ google-genai = ">=1.27.0"
34
+ google-auth = ">=2.20.0"
35
+
36
+ [tool.poetry.group.dev.dependencies]
37
+ trulens-core = { path = "../../core" }
38
+ trulens-feedback = { path = "../../feedback" }
@@ -0,0 +1,22 @@
1
+ """
2
+ !!! note "Additional Dependency Required"
3
+
4
+ To use this module, you must have the `trulens-providers-google` package installed.
5
+
6
+ ```bash
7
+ pip install trulens-providers-google
8
+ ```
9
+ """
10
+
11
+ # WARNING: This file does not follow the no-init aliases import standard.
12
+
13
+ from importlib.metadata import version
14
+
15
+ from trulens.core.utils import imports as import_utils
16
+ from trulens.providers.google.provider import Google
17
+
18
+ __version__ = version(
19
+ import_utils.safe_importlib_package_name(__package__ or __name__)
20
+ )
21
+
22
+ __all__ = ["Google"]
@@ -0,0 +1,178 @@
1
+ import inspect
2
+ import logging
3
+ import os
4
+ import pprint
5
+ from typing import (
6
+ Any,
7
+ Callable,
8
+ ClassVar,
9
+ Dict,
10
+ List,
11
+ Optional,
12
+ Tuple,
13
+ )
14
+
15
+ from trulens.core.feedback import endpoint as core_endpoint
16
+ from trulens.otel.semconv.trace import SpanAttributes
17
+
18
+ from google import genai
19
+ from google.auth.credentials import Credentials
20
+ from google.genai import Client
21
+ from google.genai.types import GenerateContentResponse
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ pp = pprint.PrettyPrinter()
26
+
27
+
28
+ def _get_env_api_key() -> Optional[str]:
29
+ """Gets the API key from environment variables, prioritizing GOOGLE_API_KEY.
30
+ Returns:
31
+ The API key string if found, otherwise None. Empty string is considered
32
+ invalid.
33
+ """
34
+ env_google_api_key = os.environ.get("GOOGLE_API_KEY", None)
35
+ env_gemini_api_key = os.environ.get("GEMINI_API_KEY", None)
36
+ if env_google_api_key and env_gemini_api_key:
37
+ logger.warning(
38
+ "Both GOOGLE_API_KEY and GEMINI_API_KEY are set. Using GOOGLE_API_KEY."
39
+ )
40
+
41
+ return env_google_api_key or env_gemini_api_key or None
42
+
43
+
44
+ class GoogleCostComputer:
45
+ @staticmethod
46
+ def handle_response(response: Any) -> Dict[str, Any]:
47
+ usage = response.usage_metadata
48
+ return {
49
+ SpanAttributes.COST.NUM_TOKENS: usage.total_token_count or 0,
50
+ SpanAttributes.COST.NUM_PROMPT_TOKENS: usage.prompt_token_count
51
+ or 0,
52
+ SpanAttributes.COST.NUM_COMPLETION_TOKENS: usage.candidates_token_count
53
+ or 0,
54
+ SpanAttributes.COST.NUM_REASONING_TOKENS: usage.thoughts_token_count
55
+ or 0,
56
+ # TODO: Check the cost computation functionality
57
+ # SpanAttributes.COST.COST: completion_cost(response),
58
+ SpanAttributes.COST.CURRENCY: "USD",
59
+ SpanAttributes.COST.MODEL: response.model_version,
60
+ }
61
+
62
+
63
+ class GoogleCallback(core_endpoint.EndpointCallback):
64
+ _FIELDS_MAP: ClassVar[List[Tuple[str, str]]] = [
65
+ ("cost", "total_cost"),
66
+ ("n_tokens", "total_tokens"),
67
+ ("n_successful_requests", "successful_requests"),
68
+ ("n_prompt_tokens", "prompt_tokens"),
69
+ ("n_completion_tokens", "completion_tokens"),
70
+ ]
71
+
72
+ def handle_generation(self, response: Any):
73
+ """Get the usage information from GoogleGenAI LLM function response's usage_metadata field."""
74
+ response_dict = response
75
+ if isinstance(response, GenerateContentResponse):
76
+ response_dict = response.to_json_dict()
77
+
78
+ usage = response_dict.get("usage_metadata")
79
+ super().handle_generation(response_dict)
80
+ self.cost.n_successful_requests += 1
81
+
82
+ for cost_field, google_field in [
83
+ ("n_tokens", "total_token_count"),
84
+ ("n_prompt_tokens", "prompt_token_count"),
85
+ ("n_completion_tokens", "candidates_token_count"),
86
+ ("n_reasoning_tokens", "thoughts_token_count"),
87
+ ]:
88
+ setattr(
89
+ self.cost,
90
+ cost_field,
91
+ getattr(self.cost, cost_field, 0) + usage.get(google_field, 0),
92
+ )
93
+
94
+ # TODO: missing code for cost calculation
95
+
96
+
97
+ class GoogleEndpoint(core_endpoint.Endpoint):
98
+ client: Optional["Client"] = None
99
+ vertexai: Optional[bool] = None
100
+ api_key: Optional[str] = None
101
+ credentials: Optional["Credentials"] = None
102
+ project: Optional[str] = None
103
+ location: Optional[str] = None
104
+
105
+ def __init__(
106
+ self,
107
+ client: Optional["Client"] = None,
108
+ vertexai: Optional[bool] = None,
109
+ api_key: Optional[str] = None,
110
+ credentials: Optional["Credentials"] = None,
111
+ project: Optional[str] = None,
112
+ location: Optional[str] = None,
113
+ **kwargs: dict,
114
+ ):
115
+ actual_client_instance = client
116
+ if not actual_client_instance:
117
+ if vertexai:
118
+ actual_client_instance = genai.Client(
119
+ vertexai=vertexai,
120
+ credentials=credentials,
121
+ project=project,
122
+ location=location,
123
+ )
124
+ else:
125
+ actual_client_instance = genai.Client(
126
+ api_key=api_key or _get_env_api_key()
127
+ )
128
+
129
+ kwargs_for_super = {
130
+ # These are the fields declared in GoogleEndpoint
131
+ "client": actual_client_instance,
132
+ "vertexai": vertexai,
133
+ "api_key": api_key,
134
+ "credentials": credentials,
135
+ "project": project,
136
+ "location": location,
137
+ # This is a kwarg your Endpoint base class expects,
138
+ # and it's also set for the parent.
139
+ "callback_class": GoogleCallback,
140
+ **kwargs, # Pass through any other arbitrary kwargs
141
+ }
142
+
143
+ super().__init__(**kwargs_for_super)
144
+
145
+ def handle_wrapped_call(
146
+ self,
147
+ func: Callable[..., Any],
148
+ bindings: inspect.BoundArguments,
149
+ response: Any,
150
+ callback: Optional[core_endpoint.EndpointCallback],
151
+ ) -> Any:
152
+ try:
153
+ if isinstance(response, GenerateContentResponse):
154
+ response_dict = response.to_json_dict()
155
+ except Exception as e:
156
+ logger.error(f"Error occurred while parsing response: {e}")
157
+ raise e
158
+
159
+ if (
160
+ isinstance(response_dict, dict)
161
+ and "usage_metadata" in response_dict
162
+ ):
163
+ candidate = response_dict["candidates"][0]
164
+ if (
165
+ "finish_reason" in candidate
166
+ and candidate["finish_reason"] == "STOP"
167
+ ):
168
+ self.global_callback.handle_generation(response=response_dict)
169
+
170
+ if callback is not None:
171
+ callback.handle_generation(response=response_dict)
172
+ else:
173
+ logger.warning(
174
+ "Unrecognized Google content response format. It did not have usage information:\n%s",
175
+ pp.pformat(response_dict),
176
+ )
177
+
178
+ return response
@@ -0,0 +1,174 @@
1
+ import logging
2
+ from typing import ClassVar, Dict, Optional, Sequence, Type, Union
3
+
4
+ import pydantic
5
+ from trulens.feedback import llm_provider
6
+ from trulens.providers.google import endpoint as google_endpoint
7
+
8
+ from google.auth.credentials import Credentials
9
+ from google.genai import Client
10
+ from google.genai.types import GenerateContentConfig
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class Google(llm_provider.LLMProvider):
16
+ """Google provides access to Google's generative models via the Gemini Developer API
17
+ or Vertex AI, depending on the configuration.
18
+
19
+ For more details, see the official Gemini documentation.
20
+
21
+ Examples:
22
+
23
+ === "Connecting with a Gemini Developer API client"
24
+ ```python
25
+ from google import genai
26
+ from trulens.providers.google import Google
27
+
28
+ google_client = genai.Client(api_key="GOOGLE_API_KEY")
29
+ provider = Google(client=google_client)
30
+ ```
31
+
32
+ === "Connecting with a Vertex AI client"
33
+ ```python
34
+ from google import genai
35
+ from trulens.providers.google import Google
36
+
37
+ PROJECT_ID = "your_project_id"
38
+ LOCATION = "us-central1"
39
+
40
+ vertex_client = genai.Client(vertexai=True, project=PROJECT_ID, location=LOCATION)
41
+ provider = Google(client=vertex_client)
42
+ ```
43
+
44
+ === "Using only an API key (Gemini Developer API)"
45
+ ```python
46
+ from trulens.providers.google import Google
47
+
48
+ provider = Google(api_key="GOOGLE_API_KEY")
49
+ ```
50
+
51
+ === "Using Vertex AI configuration directly"
52
+ ```python
53
+ from trulens.providers.google import Google
54
+
55
+ PROJECT_ID = "your_project_id"
56
+ LOCATION = "us-central1"
57
+
58
+ provider = Google(vertexai=True, project=PROJECT_ID, location=LOCATION)
59
+ ```
60
+
61
+ Args:
62
+ model_engine: Model engine to use. Defaults to `"gemini-2.5-flash"`.
63
+ api_key: API key for authenticating with the Gemini Developer API. If not provided,
64
+ the key will be read from the environment variable `GOOGLE_API_KEY` or `GEMINI_API_KEY`, if available.
65
+ vertexai: Whether to use Vertex AI endpoints. Set to `True` to use Vertex AI instead of the Gemini Developer API. Defaults to `False`.
66
+ credentials: Credentials to authenticate with Vertex AI. If not provided, default application credentials are used.
67
+ project: Google Cloud project ID used for billing and quota when using Vertex AI. Can be set via environment variables.
68
+ location: Region to send Vertex AI API requests to (e.g., `"us-central1"`). Can also be set via environment variables.
69
+ """
70
+
71
+ DEFAULT_MODEL_ENGINE: ClassVar[str] = "gemini-2.5-flash"
72
+
73
+ def __init__(
74
+ self,
75
+ endpoint=None,
76
+ client: Optional["Client"] = None,
77
+ vertexai: Optional[bool] = None,
78
+ api_key: Optional[str] = None,
79
+ credentials: Optional["Credentials"] = None,
80
+ project: Optional[str] = None,
81
+ location: Optional[str] = None,
82
+ model_engine: Optional[str] = None,
83
+ **kwargs: Dict,
84
+ ):
85
+ if model_engine is None:
86
+ model_engine = self.DEFAULT_MODEL_ENGINE
87
+
88
+ self_kwargs = dict()
89
+ self_kwargs.update(**kwargs)
90
+ self_kwargs["model_engine"] = model_engine
91
+
92
+ endpoint = google_endpoint.GoogleEndpoint(
93
+ client=client,
94
+ vertexai=vertexai,
95
+ api_key=api_key,
96
+ credentials=credentials,
97
+ project=project,
98
+ location=location,
99
+ **kwargs,
100
+ )
101
+ self_kwargs["endpoint"] = endpoint
102
+ super().__init__(**self_kwargs)
103
+
104
+ def _create_chat_completion(
105
+ self,
106
+ prompt: Optional[str] = None,
107
+ messages: Optional[Sequence[Dict]] = None,
108
+ response_format: Optional[Type[pydantic.BaseModel]] = None,
109
+ **kwargs,
110
+ ) -> Optional[Union[str, pydantic.BaseModel]]:
111
+ contents = []
112
+ system_instruction = ""
113
+ if messages is not None:
114
+ for message in messages:
115
+ if message["role"] == "system":
116
+ system_instruction = message["content"]
117
+ elif message["role"] == "user":
118
+ # TODO: Add multi-modal (text + image) handling here for Google models
119
+ contents.append({
120
+ "parts": [{"text": message["content"]}],
121
+ "role": "user",
122
+ })
123
+ else:
124
+ logger.warning(
125
+ f"Ignoring role '{message['role']}' — only 'system' and 'user' are supported."
126
+ )
127
+ elif prompt is not None:
128
+ contents.append({
129
+ "parts": [{"text": prompt}],
130
+ "role": "user",
131
+ })
132
+ else:
133
+ raise ValueError("`prompt` or `messages` must be specified.")
134
+ config_kwargs = dict(**kwargs)
135
+
136
+ # Ensure seed is set if response_format is not used
137
+ if response_format is None and "seed" not in config_kwargs:
138
+ config_kwargs["seed"] = 123
139
+
140
+ # Add optional fields based on conditions
141
+ if response_format is not None and self._structured_output_supported():
142
+ config_kwargs.update({
143
+ "response_mime_type": "application/json",
144
+ "response_schema": response_format,
145
+ })
146
+
147
+ if system_instruction:
148
+ config_kwargs["system_instruction"] = system_instruction
149
+
150
+ response = self.endpoint.client.models.generate_content(
151
+ model=self.model_engine,
152
+ contents=contents,
153
+ config=GenerateContentConfig(**config_kwargs),
154
+ )
155
+
156
+ if response_format:
157
+ return response.parsed
158
+ return response.text
159
+
160
+ def _structured_output_supported(self) -> bool:
161
+ """Whether the provider supports structured output.
162
+ For more details: https://ai.google.dev/gemini-api/docs/models
163
+ """
164
+ # Models with only output audio do not support structured output
165
+ # generation (very logical)
166
+ audio_only_output_models = [
167
+ "gemini-2.5-pro-preview-tts",
168
+ "gemini-2.5-flash-preview-tts",
169
+ "gemini-2.5-flash-preview-native-audio-dialog",
170
+ "gemini-2.5-flash-exp-native-audio-thinking-dialog",
171
+ ]
172
+ if self.model_engine in audio_only_output_models:
173
+ return False
174
+ return True