Skip to content

Commit ab9f6bf

Browse files
authored
Merge branch 'main' into main
2 parents 9a2c33a + b7d571b commit ab9f6bf

File tree

12 files changed

+629
-136
lines changed

12 files changed

+629
-136
lines changed

src/google/adk/cli/cli_deploy.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@
6767
"""
6868

6969
_AGENT_ENGINE_APP_TEMPLATE: Final[str] = """
70+
import os
71+
import vertexai
7072
from vertexai.agent_engines import AdkApp
7173
7274
if {is_config_agent}:
@@ -81,9 +83,12 @@
8183
from .agent import {adk_app_object}
8284
8385
if {express_mode}: # Whether or not to use Express Mode
84-
import os
85-
import vertexai
8686
vertexai.init(api_key=os.environ.get("GOOGLE_API_KEY"))
87+
else:
88+
vertexai.init(
89+
project=os.environ.get("GOOGLE_CLOUD_PROJECT"),
90+
location=os.environ.get("GOOGLE_CLOUD_LOCATION"),
91+
)
8792
8893
adk_app = AdkApp(
8994
{adk_app_type}={adk_app_object},
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copyright 2025 Google LLC
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.
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# Copyright 2025 Google LLC
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 dataclasses import dataclass
18+
from enum import Enum
19+
import warnings
20+
21+
from ..utils.env_utils import is_env_enabled
22+
23+
24+
class FeatureName(str, Enum):
25+
"""Feature names."""
26+
27+
JSON_SCHEMA_FOR_FUNC_DECL = "JSON_SCHEMA_FOR_FUNC_DECL"
28+
COMPUTER_USE = "COMPUTER_USE"
29+
30+
31+
class FeatureStage(Enum):
32+
"""Feature lifecycle stages.
33+
34+
Attributes:
35+
WIP: Work in progress, not functioning completely. ADK internal development
36+
only.
37+
EXPERIMENTAL: Feature works but API may change.
38+
STABLE: Production-ready, no breaking changes without MAJOR version bump.
39+
"""
40+
41+
WIP = "wip"
42+
EXPERIMENTAL = "experimental"
43+
STABLE = "stable"
44+
45+
46+
@dataclass
47+
class FeatureConfig:
48+
"""Feature configuration.
49+
50+
Attributes:
51+
stage: The feature stage.
52+
default_on: Whether the feature is enabled by default.
53+
"""
54+
55+
stage: FeatureStage
56+
default_on: bool = False
57+
58+
59+
# Central registry: FeatureName -> FeatureConfig
60+
_FEATURE_REGISTRY: dict[FeatureName, FeatureConfig] = {
61+
FeatureName.JSON_SCHEMA_FOR_FUNC_DECL: FeatureConfig(
62+
FeatureStage.WIP, default_on=False
63+
),
64+
FeatureName.COMPUTER_USE: FeatureConfig(
65+
FeatureStage.EXPERIMENTAL, default_on=True
66+
),
67+
}
68+
69+
# Track which experimental features have already warned (warn only once)
70+
_WARNED_FEATURES: set[FeatureName] = set()
71+
72+
73+
def _get_feature_config(
74+
feature_name: FeatureName,
75+
) -> FeatureConfig | None:
76+
"""Get the stage of a feature from the registry.
77+
78+
Args:
79+
feature_name: The feature name.
80+
81+
Returns:
82+
The feature config from the registry, or None if not found.
83+
"""
84+
return _FEATURE_REGISTRY.get(feature_name, None)
85+
86+
87+
def _register_feature(
88+
feature_name: FeatureName,
89+
config: FeatureConfig,
90+
) -> None:
91+
"""Register a feature with a specific config.
92+
93+
Args:
94+
feature_name: The feature name.
95+
config: The feature config to register.
96+
"""
97+
_FEATURE_REGISTRY[feature_name] = config
98+
99+
100+
def is_feature_enabled(feature_name: FeatureName) -> bool:
101+
"""Check if a feature is enabled at runtime.
102+
103+
This function is used for runtime behavior gating within stable features.
104+
It allows you to conditionally enable new behavior based on feature flags.
105+
106+
Args:
107+
feature_name: The feature name (e.g., FeatureName.RESUMABILITY).
108+
109+
Returns:
110+
True if the feature is enabled, False otherwise.
111+
112+
Example:
113+
```python
114+
def _execute_agent_loop():
115+
if is_feature_enabled(FeatureName.RESUMABILITY):
116+
# New behavior: save checkpoints for resuming
117+
return _execute_with_checkpoints()
118+
else:
119+
# Old behavior: run without checkpointing
120+
return _execute_standard()
121+
```
122+
"""
123+
config = _get_feature_config(feature_name)
124+
if config is None:
125+
raise ValueError(f"Feature {feature_name} is not registered.")
126+
127+
# Check environment variables first (highest priority)
128+
enable_var = f"ADK_ENABLE_{feature_name}"
129+
disable_var = f"ADK_DISABLE_{feature_name}"
130+
if is_env_enabled(enable_var):
131+
if config.stage != FeatureStage.STABLE:
132+
_emit_non_stable_warning_once(feature_name, config.stage)
133+
return True
134+
if is_env_enabled(disable_var):
135+
return False
136+
137+
# Fall back to registry config
138+
if config.stage != FeatureStage.STABLE and config.default_on:
139+
_emit_non_stable_warning_once(feature_name, config.stage)
140+
return config.default_on
141+
142+
143+
def _emit_non_stable_warning_once(
144+
feature_name: FeatureName,
145+
feature_stage: FeatureStage,
146+
) -> None:
147+
"""Emit a warning for a non-stable feature, but only once per feature.
148+
149+
Args:
150+
feature_name: The feature name.
151+
feature_stage: The feature stage.
152+
"""
153+
if feature_name not in _WARNED_FEATURES:
154+
_WARNED_FEATURES.add(feature_name)
155+
full_message = (
156+
f"[{feature_stage.name.upper()}] feature {feature_name} is enabled."
157+
)
158+
warnings.warn(full_message, category=UserWarning, stacklevel=4)

src/google/adk/tools/bigquery/config.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ class BigQueryToolConfig(BaseModel):
6161
change in future versions.
6262
"""
6363

64+
maximum_bytes_billed: Optional[int] = None
65+
"""Maximum number of bytes to bill for a query.
66+
67+
In BigQuery on-demand pricing, charges are rounded up to the nearest MB, with
68+
a minimum 10 MB data processed per table referenced by the query, and with a
69+
minimum 10 MB data processed per query. So this value must be set >=10485760.
70+
"""
71+
6472
max_query_result_rows: int = 50
6573
"""Maximum number of rows to return from a query.
6674
@@ -91,6 +99,19 @@ class BigQueryToolConfig(BaseModel):
9199
locations, see https://cloud.google.com/bigquery/docs/locations.
92100
"""
93101

102+
@field_validator('maximum_bytes_billed')
103+
@classmethod
104+
def validate_maximum_bytes_billed(cls, v):
105+
"""Validate the maximum bytes billed."""
106+
if v and v < 10_485_760:
107+
raise ValueError(
108+
'In BigQuery on-demand pricing, charges are rounded up to the nearest'
109+
' MB, with a minimum 10 MB data processed per table referenced by the'
110+
' query, and with a minimum 10 MB data processed per query. So'
111+
' max_bytes_billed must be set >=10485760.'
112+
)
113+
return v
114+
94115
@field_validator('application_name')
95116
@classmethod
96117
def validate_application_name(cls, v):

src/google/adk/tools/bigquery/query_tool.py

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -152,12 +152,15 @@ def _execute_sql(
152152
return {"status": "SUCCESS", "dry_run_info": dry_run_job.to_api_repr()}
153153

154154
# Finally execute the query, fetch the result, and return it
155+
job_config = bigquery.QueryJobConfig(
156+
connection_properties=bq_connection_properties,
157+
labels=bq_job_labels,
158+
)
159+
if settings.maximum_bytes_billed:
160+
job_config.maximum_bytes_billed = settings.maximum_bytes_billed
155161
row_iterator = bq_client.query_and_wait(
156162
query,
157-
job_config=bigquery.QueryJobConfig(
158-
connection_properties=bq_connection_properties,
159-
labels=bq_job_labels,
160-
),
163+
job_config=job_config,
161164
project=project_id,
162165
max_results=settings.max_query_result_rows,
163166
)
@@ -1090,21 +1093,23 @@ def analyze_contribution(
10901093
"""
10911094

10921095
# Create a session and run the create model query.
1093-
original_write_mode = settings.write_mode
10941096
try:
1095-
if original_write_mode == WriteMode.BLOCKED:
1097+
execute_sql_settings = settings
1098+
if execute_sql_settings.write_mode == WriteMode.BLOCKED:
10961099
raise ValueError("analyze_contribution is not allowed in this session.")
1097-
elif original_write_mode != WriteMode.PROTECTED:
1100+
elif execute_sql_settings.write_mode != WriteMode.PROTECTED:
10981101
# Running create temp model requires a session. So we set the write mode
10991102
# to PROTECTED to run the create model query and job query in the same
11001103
# session.
1101-
settings.write_mode = WriteMode.PROTECTED
1104+
execute_sql_settings = settings.model_copy(
1105+
update={"write_mode": WriteMode.PROTECTED}
1106+
)
11021107

11031108
result = _execute_sql(
11041109
project_id=project_id,
11051110
query=create_model_query,
11061111
credentials=credentials,
1107-
settings=settings,
1112+
settings=execute_sql_settings,
11081113
tool_context=tool_context,
11091114
caller_id="analyze_contribution",
11101115
)
@@ -1115,18 +1120,15 @@ def analyze_contribution(
11151120
project_id=project_id,
11161121
query=get_insights_query,
11171122
credentials=credentials,
1118-
settings=settings,
1123+
settings=execute_sql_settings,
11191124
tool_context=tool_context,
11201125
caller_id="analyze_contribution",
11211126
)
11221127
except Exception as ex: # pylint: disable=broad-except
11231128
return {
11241129
"status": "ERROR",
1125-
"error_details": f"Error during analyze_contribution: {str(ex)}",
1130+
"error_details": f"Error during analyze_contribution: {repr(ex)}",
11261131
}
1127-
finally:
1128-
# Restore the original write mode.
1129-
settings.write_mode == original_write_mode
11301132

11311133
return result
11321134

@@ -1324,21 +1326,23 @@ def detect_anomalies(
13241326
"""
13251327

13261328
# Create a session and run the create model query.
1327-
original_write_mode = settings.write_mode
13281329
try:
1329-
if settings.write_mode == WriteMode.BLOCKED:
1330+
execute_sql_settings = settings
1331+
if execute_sql_settings.write_mode == WriteMode.BLOCKED:
13301332
raise ValueError("anomaly detection is not allowed in this session.")
1331-
elif original_write_mode != WriteMode.PROTECTED:
1333+
elif execute_sql_settings.write_mode != WriteMode.PROTECTED:
13321334
# Running create temp model requires a session. So we set the write mode
13331335
# to PROTECTED to run the create model query and job query in the same
13341336
# session.
1335-
settings.write_mode = WriteMode.PROTECTED
1337+
execute_sql_settings = settings.model_copy(
1338+
update={"write_mode": WriteMode.PROTECTED}
1339+
)
13361340

13371341
result = _execute_sql(
13381342
project_id=project_id,
13391343
query=create_model_query,
13401344
credentials=credentials,
1341-
settings=settings,
1345+
settings=execute_sql_settings,
13421346
tool_context=tool_context,
13431347
caller_id="detect_anomalies",
13441348
)
@@ -1349,17 +1353,14 @@ def detect_anomalies(
13491353
project_id=project_id,
13501354
query=anomaly_detection_query,
13511355
credentials=credentials,
1352-
settings=settings,
1356+
settings=execute_sql_settings,
13531357
tool_context=tool_context,
13541358
caller_id="detect_anomalies",
13551359
)
13561360
except Exception as ex: # pylint: disable=broad-except
13571361
return {
13581362
"status": "ERROR",
1359-
"error_details": f"Error during anomaly detection: {str(ex)}",
1363+
"error_details": f"Error during anomaly detection: {repr(ex)}",
13601364
}
1361-
finally:
1362-
# Restore the original write mode.
1363-
settings.write_mode == original_write_mode
13641365

13651366
return result

0 commit comments

Comments
 (0)