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
2 changes: 1 addition & 1 deletion .github/.release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "1.32.0"
".": "1.33.0"
}
2 changes: 1 addition & 1 deletion .github/release-please-config.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
"last-release-sha": "5e49cfa6567a09e06409b0f380434f12f85a17c9",
"last-release-sha": "88421f80a0b008e90f18401abca4ceec3548f6cd",
"packages": {
".": {
"release-type": "python",
Expand Down
7 changes: 2 additions & 5 deletions .github/scripts/constant.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ limitations under the License.
let CONSTANT_VALUES = {
GLOBALS: {
LABELS: {
STALE: 'stale',
BUG: 'bug',
CORE: 'core',
TOOLS: 'tools',
Expand All @@ -33,9 +32,7 @@ let CONSTANT_VALUES = {
EVAL: 'eval',
TRACING: 'tracing',
WEB: 'web',
WORKFLOW: 'workflow',
REQUEST_CLARIFICATION: 'request clarification',
NEEDS_REVIEW: 'needs review'
WORKFLOW: 'workflow'
},
STATE: { CLOSED: 'closed' }
},
Expand All @@ -52,4 +49,4 @@ let CONSTANT_VALUES = {
}

};
module.exports = CONSTANT_VALUES;
module.exports = CONSTANT_VALUES;
74 changes: 31 additions & 43 deletions .github/scripts/csat.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,59 +13,47 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

const CONSTANT_VALUES = require('./constant');

/**
* Invoked from stale_csat.js and csat.yaml file to post survey link
* in closed issue.
* Invoked from csat.yml workflow file to post survey link
* in closed issues.
* @param {!Object.<string,!Object>} github contains pre defined functions.
* context Information about the workflow run.
* @return {null}
*/
module.exports = async ({ github, context }) => {
const issue = context.payload.issue.html_url;
let baseUrl = '';
// Loop over all ths label present in issue and check if specific label is
// present for survey link.
for (const label of context.payload.issue.labels) {
if (label.name.includes(CONSTANT_VALUES.GLOBALS.LABELS.BUG) ||
label.name.includes(CONSTANT_VALUES.GLOBALS.LABELS.CORE) ||
label.name.includes(CONSTANT_VALUES.GLOBALS.LABELS.TOOLS) ||
label.name.includes(CONSTANT_VALUES.GLOBALS.LABELS.SERVICES) ||
label.name.includes(CONSTANT_VALUES.GLOBALS.LABELS.MODELS) ||
label.name.includes(CONSTANT_VALUES.GLOBALS.LABELS.MCP) ||
label.name.includes(CONSTANT_VALUES.GLOBALS.LABELS.AUTH) ||
label.name.includes(CONSTANT_VALUES.GLOBALS.LABELS.LIVE) ||
label.name.includes(CONSTANT_VALUES.GLOBALS.LABELS.DOCUMENTATION) ||
label.name.includes(CONSTANT_VALUES.GLOBALS.LABELS.GOOD_FIRST_ISSUE) ||
label.name.includes(CONSTANT_VALUES.GLOBALS.LABELS.AGENT_ENGINE) ||
label.name.includes(CONSTANT_VALUES.GLOBALS.LABELS.BQ) ||
label.name.includes(CONSTANT_VALUES.GLOBALS.LABELS.EVAL) ||
label.name.includes(CONSTANT_VALUES.GLOBALS.LABELS.TRACING) ||
label.name.includes(CONSTANT_VALUES.GLOBALS.LABELS.WEB) ||
label.name.includes(CONSTANT_VALUES.GLOBALS.LABELS.WORKFLOW) ||
label.name.includes(CONSTANT_VALUES.GLOBALS.LABELS.REQUEST_CLARIFICATION) ||
label.name.includes(CONSTANT_VALUES.GLOBALS.LABELS.NEEDS_REVIEW)) {
console.log(
`label-${label.name}, posting CSAT survey for issue =${issue}`);
baseUrl = CONSTANT_VALUES.MODULE.CSAT.BASE_URL;

const yesCsat = `<a href="${baseUrl + CONSTANT_VALUES.MODULE.CSAT.SATISFACTION_PARAM +
CONSTANT_VALUES.MODULE.CSAT.YES +
CONSTANT_VALUES.MODULE.CSAT.ISSUEID_PARAM + encodeURIComponent(issue)}"> ${CONSTANT_VALUES.MODULE.CSAT.YES}</a>`;
// Check if any label matches (case-insensitive) the supported CSAT labels.
const supportedLabels = Object.values(CONSTANT_VALUES.GLOBALS.LABELS);
const hasMatchingLabel = context.payload.issue.labels.some(label => {
const name = label.name.toLowerCase();
return supportedLabels.some(supportedLabel => name.includes(supportedLabel));
});

if (hasMatchingLabel) {
console.log(`Posting CSAT survey for issue =${issue}`);
const baseUrl = CONSTANT_VALUES.MODULE.CSAT.BASE_URL;

const yesCsat = `<a href="${baseUrl + CONSTANT_VALUES.MODULE.CSAT.SATISFACTION_PARAM +
CONSTANT_VALUES.MODULE.CSAT.YES +
CONSTANT_VALUES.MODULE.CSAT.ISSUEID_PARAM + encodeURIComponent(issue)}"> ${CONSTANT_VALUES.MODULE.CSAT.YES}</a>`;

const noCsat = `<a href="${baseUrl + CONSTANT_VALUES.MODULE.CSAT.SATISFACTION_PARAM +
CONSTANT_VALUES.MODULE.CSAT.NO +
CONSTANT_VALUES.MODULE.CSAT.ISSUEID_PARAM + encodeURIComponent(issue)}"> ${CONSTANT_VALUES.MODULE.CSAT.NO}</a>`;

const comment = CONSTANT_VALUES.MODULE.CSAT.MSG + '\n' + yesCsat + '\n' +
noCsat + '\n';
const issueNumber = context.issue.number ?? context.payload.issue.number;

const noCsat = `<a href="${baseUrl + CONSTANT_VALUES.MODULE.CSAT.SATISFACTION_PARAM +
CONSTANT_VALUES.MODULE.CSAT.NO +
CONSTANT_VALUES.MODULE.CSAT.ISSUEID_PARAM + encodeURIComponent(issue)}"> ${CONSTANT_VALUES.MODULE.CSAT.NO}</a>`;
const comment = CONSTANT_VALUES.MODULE.CSAT.MSG + '\n' + yesCsat + '\n' +
noCsat + '\n';
let issueNumber = context.issue.number ?? context.payload.issue.number;
await github.rest.issues.createComment({
issue_number: issueNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});
}
await github.rest.issues.createComment({
issue_number: issueNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});
}
};
3 changes: 1 addition & 2 deletions .github/workflows/csat.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ on:
permissions:
contents: read
issues: write
pull-requests: write

jobs:
welcome:
Expand All @@ -18,4 +17,4 @@ jobs:
with:
script: |
const script = require('./.github/scripts/csat.js')
script({github, context})
script({github, context})
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,38 @@
# Changelog

## [1.33.0](https://github.com/google/adk-python/compare/v1.32.0...v1.33.0) (2026-05-08)


### Features

* add BufferableSessionService ([0bc767e](https://github.com/google/adk-python/commit/0bc767e6892742d6290d3445d028f95925187aed))
* **apigee:** allow injecting credentials into ApigeeLlm ([ce578ff](https://github.com/google/adk-python/commit/ce578fffa0dc02b0033f7f5e705b9422cbd6c252))
* Make ADK environment tools truncation limit configurable ([83ae405](https://github.com/google/adk-python/commit/83ae40525aa734f4a3b365614cce43831612a1ec))
* **models:** add get_function_calls and get_function_responses to LlmResponse ([22fae7e](https://github.com/google/adk-python/commit/22fae7e9a09c581f433f3c51ea9a0ab26e689b92))


### Bug Fixes

* catch genai.ClientError when sandbox is missing ([69fa777](https://github.com/google/adk-python/commit/69fa777881b3cb161e5b3dcb005def9a2ad86904)), closes [#5480](https://github.com/google/adk-python/issues/5480)
* double append bug ([f8b4c59](https://github.com/google/adk-python/commit/f8b4c59350fea3319c9e53e29968c56c93c57c99))
* Filter out video events with inline data from being stored in session ([88421f8](https://github.com/google/adk-python/commit/88421f80a0b008e90f18401abca4ceec3548f6cd))
* fix fork detection, correct offload limits, and add response logging in BigQuery plugin ([9d1bb4b](https://github.com/google/adk-python/commit/9d1bb4b4870233e574f5c06ddd2b62a48272398f))
* hot reload agents for adk web ([740557c](https://github.com/google/adk-python/commit/740557c8965305abc75752082bc3ee63d924742f))
* Only append skills to system instruction if ListSkillsTool isn't available ([01f1fc9](https://github.com/google/adk-python/commit/01f1fc9c912a97ff27bb1332a28324f991eae77d))
* prevent state_delta overwrite on function_response-only events ([fc27203](https://github.com/google/adk-python/commit/fc2720378e8997269d30f5439051f5e43d5fa028), [211e2ce](https://github.com/google/adk-python/commit/211e2ceb70ac6b61400559761d1d6548d906a79b)), closes [#3178](https://github.com/google/adk-python/issues/3178)
* Raise a clear actionable error when CustomAuthScheme lacks a registered AuthProvider ([83f9817](https://github.com/google/adk-python/commit/83f981761b963ca51a286cbd004c043567517a3c))
* should use app_name instead of req.app_name ([8286066](https://github.com/google/adk-python/commit/8286066e71e5c07b5b28979b8327d4b330187ddd))
* **simulation:** Add error message when LlmBackedUserSimulator returns empty response ([fb92aad](https://github.com/google/adk-python/commit/fb92aad9c53bb9f6706fb27751d71fcda2419500))
* Update expressmode api call to include default api key param ([e833995](https://github.com/google/adk-python/commit/e8339953911a8b580cfc2d88c7008234a43beece))
* use asyncio.sleep to avoid blocking event loop ([3a1eadc](https://github.com/google/adk-python/commit/3a1eadce66804db08f6520cc11f9c60e81bb9e30))
* Use project and location instead of API key when deploying to agent engine ([398f28f](https://github.com/google/adk-python/commit/398f28feb47d87ec9c4c03dd3e0e7b87a1699e6e))


### Code Refactoring

* adjust computation of workflow.steps metric and add new unit tests ([03d6208](https://github.com/google/adk-python/commit/03d6208aacac8c19adec45ce0dd837f9e3a7f66f))
* remove input.type and output.type attributes from adk metrics ([9559968](https://github.com/google/adk-python/commit/95599683230dd13e5792133f30ade3fe19358d52))

## [1.32.0](https://github.com/google/adk-python/compare/v1.31.0...v1.32.0) (2026-04-30)


Expand Down
2 changes: 1 addition & 1 deletion contributing/samples/hello_world/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ async def check_prime(nums: list[int]) -> str:


root_agent = Agent(
model='projects/adk-cat/locations/us-central1/publishers/google/models/gemini-2.5-flash',
model='gemini-3-flash-preview',
name='hello_world_agent',
description=(
'hello world agent that can roll a dice of 8 sides and check prime'
Expand Down
2 changes: 1 addition & 1 deletion contributing/samples/session_state_agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ async def after_agent_callback(callback_context: CallbackContext):
'Log all users query with `log_query` tool. Must always remind user you'
' cannot answer second query because your setup.'
),
model='gemini-2.5-flash',
model='gemini-3-flash-preview',
before_agent_callback=before_agent_callback,
before_model_callback=before_model_callback,
after_model_callback=after_model_callback,
Expand Down
4 changes: 3 additions & 1 deletion src/google/adk/a2a/converters/request_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
from .part_converter import A2APartToGenAIPartConverter
from .part_converter import convert_a2a_part_to_genai_part

A2A_METADATA_KEY = 'a2a_metadata'


@a2a_experimental
class AgentRunRequest(BaseModel):
Expand Down Expand Up @@ -97,7 +99,7 @@ def convert_a2a_request_to_agent_run_request(

custom_metadata = {}
if request.metadata:
custom_metadata['a2a_metadata'] = request.metadata
custom_metadata[A2A_METADATA_KEY] = request.metadata

output_parts = []
for a2a_part in request.message.parts:
Expand Down
64 changes: 33 additions & 31 deletions src/google/adk/auth/auth_credential.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
from typing import Dict
from typing import List
from typing import Literal
from typing import Optional

from pydantic import alias_generators
from pydantic import BaseModel
Expand All @@ -40,9 +39,9 @@ class BaseModelWithConfig(BaseModel):
class HttpCredentials(BaseModelWithConfig):
"""Represents the secret token value for HTTP authentication, like user name, password, oauth token, etc."""

username: Optional[str] = None
password: Optional[str] = None
token: Optional[str] = None
username: str | None = None
password: str | None = None
token: str | None = None

@classmethod
def model_validate(cls, data: Dict[str, Any]) -> "HttpCredentials":
Expand All @@ -62,40 +61,43 @@ class HttpAuth(BaseModelWithConfig):
# Examples: 'basic', 'bearer'
scheme: str
credentials: HttpCredentials
additional_headers: Optional[Dict[str, str]] = None
additional_headers: Dict[str, str] | None = None


class OAuth2Auth(BaseModelWithConfig):
"""Represents credential value and its metadata for a OAuth2 credential."""

client_id: Optional[str] = None
client_secret: Optional[str] = None
client_id: str | None = None
client_secret: str | None = None
# tool or adk can generate the auth_uri with the state info thus client
# can verify the state
auth_uri: Optional[str] = None
auth_uri: str | None = None
# A unique value generated at the start of the OAuth flow to bind the user's
# session to the authorization request. This value is typically stored with
# user session and passed to backend for validation.
nonce: Optional[str] = None
state: Optional[str] = None
nonce: str | None = None
state: str | None = None
# tool or adk can decide the redirect_uri if they don't want client to decide
redirect_uri: Optional[str] = None
auth_response_uri: Optional[str] = None
auth_code: Optional[str] = None
access_token: Optional[str] = None
refresh_token: Optional[str] = None
id_token: Optional[str] = None
expires_at: Optional[int] = None
expires_in: Optional[int] = None
audience: Optional[str] = None
token_endpoint_auth_method: Optional[
redirect_uri: str | None = None
auth_response_uri: str | None = None
auth_code: str | None = None
access_token: str | None = None
refresh_token: str | None = None
id_token: str | None = None
expires_at: int | None = None
expires_in: int | None = None
audience: str | None = None
code_verifier: str | None = None
code_challenge_method: str | None = None
token_endpoint_auth_method: (
Literal[
"client_secret_basic",
"client_secret_post",
"client_secret_jwt",
"private_key_jwt",
]
] = "client_secret_basic"
| None
) = "client_secret_basic"


class ServiceAccountCredential(BaseModelWithConfig):
Expand Down Expand Up @@ -166,11 +168,11 @@ class ServiceAccount(BaseModelWithConfig):
when ``use_id_token`` is True.
"""

service_account_credential: Optional[ServiceAccountCredential] = None
scopes: Optional[List[str]] = None
use_default_credential: Optional[bool] = False
use_id_token: Optional[bool] = False
audience: Optional[str] = None
service_account_credential: ServiceAccountCredential | None = None
scopes: List[str] | None = None
use_default_credential: bool | None = False
use_id_token: bool | None = False
audience: str | None = None

@model_validator(mode="after")
def _validate_config(self) -> ServiceAccount:
Expand Down Expand Up @@ -275,9 +277,9 @@ class AuthCredential(BaseModelWithConfig):
auth_type: AuthCredentialTypes
# Resource reference for the credential.
# This will be supported in the future.
resource_ref: Optional[str] = None
resource_ref: str | None = None

api_key: Optional[str] = None
http: Optional[HttpAuth] = None
service_account: Optional[ServiceAccount] = None
oauth2: Optional[OAuth2Auth] = None
api_key: str | None = None
http: HttpAuth | None = None
service_account: ServiceAccount | None = None
oauth2: OAuth2Auth | None = None
24 changes: 23 additions & 1 deletion src/google/adk/auth/auth_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from ..sessions.state import State

try:
from authlib.common.security import generate_token
from authlib.integrations.requests_client import OAuth2Session

AUTHLIB_AVAILABLE = True
Expand Down Expand Up @@ -158,6 +159,8 @@ def generate_auth_uri(

auth_scheme = self.auth_config.auth_scheme
auth_credential = self.auth_config.raw_auth_credential
if not auth_credential or not auth_credential.oauth2:
raise ValueError("raw_auth_credential or oauth2 is empty")

if isinstance(auth_scheme, OpenIdConnectWithConfig):
authorization_endpoint = auth_scheme.authorization_endpoint
Expand Down Expand Up @@ -190,19 +193,38 @@ def generate_auth_uri(
auth_credential.oauth2.client_secret,
scope=" ".join(scopes),
redirect_uri=auth_credential.oauth2.redirect_uri,
code_challenge_method=auth_credential.oauth2.code_challenge_method,
)
params = {
"access_type": "offline",
"prompt": "consent",
}
if auth_credential.oauth2.audience:
params["audience"] = auth_credential.oauth2.audience

# If using PKCE with S256, ensure a code_verifier exists.
# If not provided in the credential, generate a cryptographically secure
# random token of 48 characters (OAuth2 recommends 43-128 characters).
code_verifier = auth_credential.oauth2.code_verifier
method = auth_credential.oauth2.code_challenge_method

if method:
if method != "S256":
raise ValueError(
f"Unsupported code_challenge_method: {method}. Only 'S256' is"
" supported."
)
if not code_verifier:
code_verifier = generate_token(48)

uri, state = client.create_authorization_url(
url=authorization_endpoint, **params
url=authorization_endpoint, code_verifier=code_verifier, **params
)

exchanged_auth_credential = auth_credential.model_copy(deep=True)
exchanged_auth_credential.oauth2.auth_uri = uri
exchanged_auth_credential.oauth2.state = state
if code_verifier:
exchanged_auth_credential.oauth2.code_verifier = code_verifier

return exchanged_auth_credential
Loading
Loading