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
41 changes: 41 additions & 0 deletions src/google/adk/cli/cli_tools_click.py
Original file line number Diff line number Diff line change
Expand Up @@ -1553,6 +1553,47 @@ def cli_deploy_cloud_run(
click.secho(f"Deploy failed: {e}", fg="red", err=True)


@main.group()
def migrate():
"""ADK migration commands."""
pass


@migrate.command("session", cls=HelpfulCommand)
@click.option(
"--source_db_url",
required=True,
help=(
"SQLAlchemy URL of source database in database session service, e.g."
" sqlite:///source.db."
),
)
@click.option(
"--dest_db_url",
required=True,
help=(
"SQLAlchemy URL of destination database in database session service,"
" e.g. sqlite:///dest.db."
),
)
@click.option(
"--log_level",
type=LOG_LEVELS,
default="INFO",
help="Optional. Set the logging level",
)
def cli_migrate_session(
*, source_db_url: str, dest_db_url: str, log_level: str
):
"""Migrates a session database to the latest schema version."""
logs.setup_adk_logger(getattr(logging, log_level.upper()))
try:
migration_runner.upgrade(source_db_url, dest_db_url)
click.secho("Migration check and upgrade process finished.", fg="green")
except Exception as e:
click.secho(f"Migration failed: {e}", fg="red", err=True)


@deploy.command("agent_engine")
@click.option(
"--api_key",
Expand Down
75 changes: 75 additions & 0 deletions src/google/adk/features/_feature_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@

from __future__ import annotations

from contextlib import contextmanager
from dataclasses import dataclass
from enum import Enum
from typing import Generator
import warnings

from ..utils.env_utils import is_env_enabled
Expand All @@ -24,17 +26,23 @@
class FeatureName(str, Enum):
"""Feature names."""

AUTHENTICATED_FUNCTION_TOOL = "AUTHENTICATED_FUNCTION_TOOL"
BASE_AUTHENTICATED_TOOL = "BASE_AUTHENTICATED_TOOL"
BIG_QUERY_TOOLSET = "BIG_QUERY_TOOLSET"
BIG_QUERY_TOOL_CONFIG = "BIG_QUERY_TOOL_CONFIG"
BIGTABLE_TOOL_SETTINGS = "BIGTABLE_TOOL_SETTINGS"
BIGTABLE_TOOLSET = "BIGTABLE_TOOLSET"
COMPUTER_USE = "COMPUTER_USE"
GOOGLE_CREDENTIALS_CONFIG = "GOOGLE_CREDENTIALS_CONFIG"
GOOGLE_TOOL = "GOOGLE_TOOL"
JSON_SCHEMA_FOR_FUNC_DECL = "JSON_SCHEMA_FOR_FUNC_DECL"
PROGRESSIVE_SSE_STREAMING = "PROGRESSIVE_SSE_STREAMING"
PUBSUB_TOOL_CONFIG = "PUBSUB_TOOL_CONFIG"
PUBSUB_TOOLSET = "PUBSUB_TOOLSET"
SPANNER_TOOLSET = "SPANNER_TOOLSET"
SPANNER_TOOL_SETTINGS = "SPANNER_TOOL_SETTINGS"
TOOL_CONFIG = "TOOL_CONFIG"
TOOL_CONFIRMATION = "TOOL_CONFIRMATION"


class FeatureStage(Enum):
Expand Down Expand Up @@ -67,6 +75,12 @@ class FeatureConfig:

# Central registry: FeatureName -> FeatureConfig
_FEATURE_REGISTRY: dict[FeatureName, FeatureConfig] = {
FeatureName.AUTHENTICATED_FUNCTION_TOOL: FeatureConfig(
FeatureStage.EXPERIMENTAL, default_on=True
),
FeatureName.BASE_AUTHENTICATED_TOOL: FeatureConfig(
FeatureStage.EXPERIMENTAL, default_on=True
),
FeatureName.BIG_QUERY_TOOLSET: FeatureConfig(
FeatureStage.EXPERIMENTAL, default_on=True
),
Expand All @@ -76,6 +90,9 @@ class FeatureConfig:
FeatureName.BIGTABLE_TOOL_SETTINGS: FeatureConfig(
FeatureStage.EXPERIMENTAL, default_on=True
),
FeatureName.BIGTABLE_TOOLSET: FeatureConfig(
FeatureStage.EXPERIMENTAL, default_on=True
),
FeatureName.COMPUTER_USE: FeatureConfig(
FeatureStage.EXPERIMENTAL, default_on=True
),
Expand All @@ -91,6 +108,9 @@ class FeatureConfig:
FeatureName.PROGRESSIVE_SSE_STREAMING: FeatureConfig(
FeatureStage.EXPERIMENTAL, default_on=True
),
FeatureName.PUBSUB_TOOL_CONFIG: FeatureConfig(
FeatureStage.EXPERIMENTAL, default_on=True
),
FeatureName.PUBSUB_TOOLSET: FeatureConfig(
FeatureStage.EXPERIMENTAL, default_on=True
),
Expand All @@ -100,6 +120,12 @@ class FeatureConfig:
FeatureName.SPANNER_TOOL_SETTINGS: FeatureConfig(
FeatureStage.EXPERIMENTAL, default_on=True
),
FeatureName.TOOL_CONFIG: FeatureConfig(
FeatureStage.EXPERIMENTAL, default_on=True
),
FeatureName.TOOL_CONFIRMATION: FeatureConfig(
FeatureStage.EXPERIMENTAL, default_on=True
),
}

# Track which experimental features have already warned (warn only once)
Expand Down Expand Up @@ -240,3 +266,52 @@ def _emit_non_stable_warning_once(
f"[{feature_stage.name.upper()}] feature {feature_name} is enabled."
)
warnings.warn(full_message, category=UserWarning, stacklevel=4)


@contextmanager
def temporary_feature_override(
feature_name: FeatureName,
enabled: bool,
) -> Generator[None, None, None]:
"""Temporarily override a feature's enabled state within a context.

This context manager is useful for testing or temporarily enabling/disabling
a feature within a specific scope. The original state is restored when the
context exits.

Args:
feature_name: The feature name to override.
enabled: Whether the feature should be enabled.

Yields:
None

Example:
```python
from google.adk.features import FeatureName, temporary_feature_override

# Temporarily enable a feature for testing
with temporary_feature_override(FeatureName.JSON_SCHEMA_FOR_FUNC_DECL, True):
# Feature is enabled here
result = some_function_that_checks_feature()
# Feature is restored to original state here
```
"""
config = _get_feature_config(feature_name)
if config is None:
raise ValueError(f"Feature {feature_name} is not registered.")

# Save the original override state
had_override = feature_name in _FEATURE_OVERRIDES
original_value = _FEATURE_OVERRIDES.get(feature_name)

# Apply the temporary override
_FEATURE_OVERRIDES[feature_name] = enabled
try:
yield
finally:
# Restore the original state
if had_override:
_FEATURE_OVERRIDES[feature_name] = original_value
else:
_FEATURE_OVERRIDES.pop(feature_name, None)
18 changes: 16 additions & 2 deletions src/google/adk/models/lite_llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,8 @@ async def _content_to_message_param(
A litellm Message, a list of litellm Messages.
"""

tool_messages = []
tool_messages: list[Message] = []
non_tool_parts: list[types.Part] = []
for part in content.parts:
if part.function_response:
response = part.function_response.response
Expand All @@ -477,9 +478,22 @@ async def _content_to_message_param(
content=response_content,
)
)
if tool_messages:
else:
non_tool_parts.append(part)

if tool_messages and not non_tool_parts:
return tool_messages if len(tool_messages) > 1 else tool_messages[0]

if tool_messages and non_tool_parts:
follow_up = await _content_to_message_param(
types.Content(role=content.role, parts=non_tool_parts),
provider=provider,
)
follow_up_messages = (
follow_up if isinstance(follow_up, list) else [follow_up]
)
return tool_messages + follow_up_messages

# Handle user or assistant messages
role = _to_litellm_role(content.role)

Expand Down
33 changes: 19 additions & 14 deletions src/google/adk/runners.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,15 +153,21 @@ def __init__(
"""Initializes the Runner.

Developers should provide either an `app` instance or both `app_name` and
`agent`. Providing a mix of `app` and `app_name`/`agent` will result in a
`ValueError`. Providing `app` is the recommended way to create a runner.
`agent`. When `app` is provided, `app_name` can optionally override the
app's name (useful for deployment scenarios like Agent Engine where the
resource name differs from the app's identifier). However, `agent` should
not be provided when `app` is provided. Providing `app` is the recommended
way to create a runner.

Args:
app: An optional `App` instance. If provided, `app_name` and `agent`
should not be specified.
app: An optional `App` instance. If provided, `agent` should not be
specified. `app_name` can optionally override `app.name`.
app_name: The application name of the runner. Required if `app` is not
provided.
agent: The root agent to run. Required if `app` is not provided.
provided. If `app` is provided, this can optionally override `app.name`
(e.g., for deployment scenarios where a resource name differs from the
app identifier).
agent: The root agent to run. Required if `app` is not provided. Should
not be provided when `app` is provided.
plugins: Deprecated. A list of plugins for the runner. Please use the
`app` argument to provide plugins instead.
artifact_service: The artifact service for the runner.
Expand All @@ -171,8 +177,8 @@ def __init__(
plugin_close_timeout: The timeout in seconds for plugin close methods.

Raises:
ValueError: If `app` is provided along with `app_name` or `plugins`, or
if `app` is not provided but either `app_name` or `agent` is missing.
ValueError: If `app` is provided along with `agent` or `plugins`, or if
`app` is not provided but either `app_name` or `agent` is missing.
"""
self.app = app
(
Expand Down Expand Up @@ -213,7 +219,8 @@ def _validate_runner_params(

Args:
app: An optional `App` instance.
app_name: The application name of the runner.
app_name: The application name of the runner. Can override app.name when
app is provided.
agent: The root agent to run.
plugins: A list of plugins for the runner.

Expand All @@ -232,18 +239,16 @@ def _validate_runner_params(
)

if app:
if app_name:
raise ValueError(
'When app is provided, app_name should not be provided.'
)
if agent:
raise ValueError('When app is provided, agent should not be provided.')
if plugins:
raise ValueError(
'When app is provided, plugins should not be provided and should be'
' provided in the app instead.'
)
app_name = app.name
# Allow app_name to override app.name (useful for deployment scenarios
# like Agent Engine where resource names differ from app identifiers)
app_name = app_name or app.name
agent = app.root_agent
plugins = app.plugins
context_cache_config = app.context_cache_config
Expand Down
51 changes: 36 additions & 15 deletions src/google/adk/tools/agent_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@

from . import _automatic_function_calling_util
from ..agents.common_configs import AgentRefConfig
from ..features import FeatureName
from ..features import is_feature_enabled
from ..memory.in_memory_memory_service import InMemoryMemoryService
from ..utils.context_utils import Aclosing
from ._forwarding_artifact_service import ForwardingArtifactService
Expand Down Expand Up @@ -82,29 +84,48 @@ def _get_declaration(self) -> types.FunctionDeclaration:
# Override the description with the agent's description
result.description = self.agent.description
else:
result = types.FunctionDeclaration(
parameters=types.Schema(
type=types.Type.OBJECT,
properties={
'request': types.Schema(
type=types.Type.STRING,
),
},
required=['request'],
),
description=self.agent.description,
name=self.name,
)
if is_feature_enabled(FeatureName.JSON_SCHEMA_FOR_FUNC_DECL):
result = types.FunctionDeclaration(
name=self.name,
description=self.agent.description,
parameters_json_schema={
'type': 'object',
'properties': {
'request': {'type': 'string'},
},
'required': ['request'],
},
)
else:
result = types.FunctionDeclaration(
parameters=types.Schema(
type=types.Type.OBJECT,
properties={
'request': types.Schema(
type=types.Type.STRING,
),
},
required=['request'],
),
description=self.agent.description,
name=self.name,
)

# Set response schema for non-GEMINI_API variants
if self._api_variant != GoogleLLMVariant.GEMINI_API:
# Determine response type based on agent's output schema
if isinstance(self.agent, LlmAgent) and self.agent.output_schema:
# Agent has structured output schema - response is an object
result.response = types.Schema(type=types.Type.OBJECT)
if is_feature_enabled(FeatureName.JSON_SCHEMA_FOR_FUNC_DECL):
result.response_json_schema = {'type': 'object'}
else:
result.response = types.Schema(type=types.Type.OBJECT)
else:
# Agent returns text - response is a string
result.response = types.Schema(type=types.Type.STRING)
if is_feature_enabled(FeatureName.JSON_SCHEMA_FOR_FUNC_DECL):
result.response_json_schema = {'type': 'string'}
else:
result.response = types.Schema(type=types.Type.STRING)

result.name = self.name
return result
Expand Down
6 changes: 3 additions & 3 deletions src/google/adk/tools/authenticated_function_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import logging
from typing import Any
from typing import Callable
from typing import Dict
from typing import Optional
from typing import Union

Expand All @@ -27,14 +26,15 @@
from ..auth.auth_credential import AuthCredential
from ..auth.auth_tool import AuthConfig
from ..auth.credential_manager import CredentialManager
from ..utils.feature_decorator import experimental
from ..features import experimental
from ..features import FeatureName
from .function_tool import FunctionTool
from .tool_context import ToolContext

logger = logging.getLogger("google_adk." + __name__)


@experimental
@experimental(FeatureName.AUTHENTICATED_FUNCTION_TOOL)
class AuthenticatedFunctionTool(FunctionTool):
"""A FunctionTool that handles authentication before the actual tool logic
gets called. Functions can accept a special `credential` argument which is the
Expand Down
Loading
Loading