Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ dependencies = [
"PyYAML>=6.0.2, <7.0.0", # For APIHubToolset.
"aiosqlite>=0.21.0", # For SQLite database
"anyio>=4.9.0, <5.0.0", # For MCP Session Manager
"authlib>=1.5.1, <2.0.0", # For RestAPI Tool
"authlib>=1.6.6, <2.0.0", # For RestAPI Tool
"click>=8.1.8, <9.0.0", # For CLI tools
"fastapi>=0.115.0, <0.124.0", # FastAPI framework
"google-api-python-client>=2.157.0, <3.0.0", # Google API client discovery
Expand All @@ -46,7 +46,7 @@ dependencies = [
"google-genai>=1.56.0, <2.0.0", # Google GenAI SDK
"graphviz>=0.20.2, <1.0.0", # Graphviz for graph rendering
"jsonschema>=4.23.0, <5.0.0", # Agent Builder config validation
"mcp>=1.10.0, <2.0.0", # For MCP Toolset
"mcp>=1.23.0, <2.0.0", # For MCP Toolset
"opentelemetry-api>=1.37.0, <=1.37.0", # OpenTelemetry - limit upper version for sdk and api to not risk breaking changes from unstable _logs package.
"opentelemetry-exporter-gcp-logging>=1.9.0a0, <2.0.0",
"opentelemetry-exporter-gcp-monitoring>=1.9.0a0, <2.0.0",
Expand Down
62 changes: 46 additions & 16 deletions src/google/adk/cli/cli_tools_click.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,28 +50,44 @@
)


def _apply_feature_overrides(enable_features: tuple[str, ...]) -> None:
def _apply_feature_overrides(
*,
enable_features: tuple[str, ...] = (),
disable_features: tuple[str, ...] = (),
) -> None:
"""Apply feature overrides from CLI flags.
Args:
enable_features: Tuple of feature names to enable.
disable_features: Tuple of feature names to disable.
"""
feature_overrides: dict[str, bool] = {}

for features_str in enable_features:
for feature_name_str in features_str.split(","):
feature_name_str = feature_name_str.strip()
if not feature_name_str:
continue
try:
feature_name = FeatureName(feature_name_str)
override_feature_enabled(feature_name, True)
except ValueError:
valid_names = ", ".join(f.value for f in FeatureName)
click.secho(
f"WARNING: Unknown feature name '{feature_name_str}'. "
f"Valid names are: {valid_names}",
fg="yellow",
err=True,
)
if feature_name_str:
feature_overrides[feature_name_str] = True

for features_str in disable_features:
for feature_name_str in features_str.split(","):
feature_name_str = feature_name_str.strip()
if feature_name_str:
feature_overrides[feature_name_str] = False

# Apply all overrides
for feature_name_str, enabled in feature_overrides.items():
try:
feature_name = FeatureName(feature_name_str)
override_feature_enabled(feature_name, enabled)
except ValueError:
valid_names = ", ".join(f.value for f in FeatureName)
click.secho(
f"WARNING: Unknown feature name '{feature_name_str}'. "
f"Valid names are: {valid_names}",
fg="yellow",
err=True,
)


def feature_options():
Expand All @@ -88,11 +104,25 @@ def decorator(func):
),
multiple=True,
)
@click.option(
"--disable_features",
help=(
"Optional. Comma-separated list of feature names to disable. "
"This provides an alternative to environment variables for "
"disabling features. Example: "
"--disable_features=JSON_SCHEMA_FOR_FUNC_DECL,PROGRESSIVE_SSE_STREAMING"
),
multiple=True,
)
@functools.wraps(func)
def wrapper(*args, **kwargs):
enable_features = kwargs.pop("enable_features", ())
if enable_features:
_apply_feature_overrides(enable_features)
disable_features = kwargs.pop("disable_features", ())
if enable_features or disable_features:
_apply_feature_overrides(
enable_features=enable_features,
disable_features=disable_features,
)
return func(*args, **kwargs)

return wrapper
Expand Down
64 changes: 57 additions & 7 deletions src/google/adk/cli/utils/service_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
from pathlib import Path
from typing import Any
from typing import Optional
from urllib.parse import parse_qsl
from urllib.parse import urlsplit
from urllib.parse import urlunsplit

from ...artifacts.base_artifact_service import BaseArtifactService
from ...memory.base_memory_service import BaseMemoryService
Expand All @@ -42,6 +45,41 @@
_KUBERNETES_HOST_ENV = "KUBERNETES_SERVICE_HOST"


def _redact_uri_for_log(uri: str) -> str:
"""Returns a safe-to-log representation of a URI.

Redacts user info (username/password) and query parameter values.
"""
if not uri or not uri.strip():
return "<empty>"
sanitized = uri.replace("\r", "\\r").replace("\n", "\\n")
if "://" not in sanitized:
return "<scheme-missing>"
try:
parsed = urlsplit(sanitized)
except ValueError:
return "<unparseable>"

if not parsed.scheme:
return "<scheme-missing>"

netloc = parsed.netloc
if "@" in netloc:
_, netloc = netloc.rsplit("@", 1)

if parsed.query:
try:
redacted_pairs = parse_qsl(parsed.query, keep_blank_values=True)
except ValueError:
query = "<redacted>"
else:
query = "&".join(f"{key}=<redacted>" for key, _ in redacted_pairs)
else:
query = ""

return urlunsplit((parsed.scheme, netloc, parsed.path, query, ""))


def _is_cloud_run() -> bool:
"""Returns True when running in Cloud Run."""
return bool(os.environ.get(_CLOUD_RUN_SERVICE_ENV))
Expand Down Expand Up @@ -148,7 +186,10 @@ def create_session_service_from_options(
kwargs.update(session_db_kwargs)

if session_service_uri:
logger.info("Using session service URI: %s", session_service_uri)
logger.info(
"Using session service URI: %s",
_redact_uri_for_log(session_service_uri),
)
service = registry.create_session_service(session_service_uri, **kwargs)
if service is not None:
return service
Expand All @@ -162,7 +203,7 @@ def create_session_service_from_options(
fallback_kwargs.pop("agents_dir", None)
logger.info(
"Falling back to DatabaseSessionService for URI: %s",
session_service_uri,
_redact_uri_for_log(session_service_uri),
)
return DatabaseSessionService(db_url=session_service_uri, **fallback_kwargs)

Expand Down Expand Up @@ -208,13 +249,18 @@ def create_memory_service_from_options(
registry = get_service_registry()

if memory_service_uri:
logger.info("Using memory service URI: %s", memory_service_uri)
logger.info(
"Using memory service URI: %s", _redact_uri_for_log(memory_service_uri)
)
service = registry.create_memory_service(
memory_service_uri,
agents_dir=str(base_path),
)
if service is None:
raise ValueError(f"Unsupported memory service URI: {memory_service_uri}")
raise ValueError(
"Unsupported memory service URI: %s"
% _redact_uri_for_log(memory_service_uri)
)
return service

logger.info("Using in-memory memory service")
Expand All @@ -235,19 +281,23 @@ def create_artifact_service_from_options(
registry = get_service_registry()

if artifact_service_uri:
logger.info("Using artifact service URI: %s", artifact_service_uri)
logger.info(
"Using artifact service URI: %s",
_redact_uri_for_log(artifact_service_uri),
)
service = registry.create_artifact_service(
artifact_service_uri,
agents_dir=str(base_path),
)
if service is None:
if strict_uri:
raise ValueError(
f"Unsupported artifact service URI: {artifact_service_uri}"
"Unsupported artifact service URI: %s"
% _redact_uri_for_log(artifact_service_uri)
)
return _create_in_memory_artifact_service(
"Unsupported artifact service URI: %s, falling back to in-memory",
artifact_service_uri,
_redact_uri_for_log(artifact_service_uri),
)
return service

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
from typing_extensions import override

from ..agents.invocation_context import InvocationContext
from ..utils.feature_decorator import experimental
from .base_code_executor import BaseCodeExecutor
from .code_execution_utils import CodeExecutionInput
from .code_execution_utils import CodeExecutionResult
Expand All @@ -32,7 +31,6 @@
logger = logging.getLogger('google_adk.' + __name__)


@experimental
class AgentEngineSandboxCodeExecutor(BaseCodeExecutor):
"""A code executor that uses Agent Engine Code Execution Sandbox to execute code.

Expand Down
Loading
Loading