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.
- trulens_providers_google-2.2.3/PKG-INFO +28 -0
- trulens_providers_google-2.2.3/README.md +1 -0
- trulens_providers_google-2.2.3/pyproject.toml +38 -0
- trulens_providers_google-2.2.3/trulens/providers/google/__init__.py +22 -0
- trulens_providers_google-2.2.3/trulens/providers/google/endpoint.py +178 -0
- trulens_providers_google-2.2.3/trulens/providers/google/provider.py +174 -0
- trulens_providers_google-2.2.3/trulens/providers/google/py.typed +0 -0
|
@@ -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
|
|
File without changes
|