Merge branch 'main' into fix/anthropic-nested-schema-type-conversion

This commit is contained in:
Rohit Yanamadala
2026-02-19 11:45:29 -08:00
committed by GitHub
72 changed files with 6587 additions and 1671 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
{
".": "1.24.1"
".": "1.25.1"
}
+1
View File
@@ -1,5 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
"last-release-sha": "9f7d5b3f1476234e552b783415527cc4bac55b39",
"packages": {
".": {
"release-type": "python",
+3 -3
View File
@@ -1,5 +1,5 @@
# Cherry-picks a commit from main to the release/candidate branch.
# Use this to include bug fixes in an in-progress release.
# Step 3 (optional): Cherry-picks a commit from main to the release/candidate branch.
# Use between step 1 and step 4 to include bug fixes in an in-progress release.
name: "Release: Cherry-pick"
on:
@@ -42,5 +42,5 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
run: |
gh workflow run release-please.yml --repo ${{ github.repository }}
gh workflow run release-please.yml --repo ${{ github.repository }} --ref release/candidate
echo "Triggered Release Please workflow"
+3 -3
View File
@@ -1,5 +1,5 @@
# Starts the release process by creating a release/candidate branch.
# Triggers release-please to generate a changelog PR.
# Step 1: Starts the release process by creating a release/candidate branch.
# Generates a changelog PR for review (step 2).
name: "Release: Cut"
on:
@@ -42,5 +42,5 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
run: |
gh workflow run release-please.yml --repo ${{ github.repository }}
gh workflow run release-please.yml --repo ${{ github.repository }} --ref release/candidate
echo "Triggered Release Please workflow"
+26 -9
View File
@@ -1,5 +1,5 @@
# Triggers when release-please PR is merged to release/candidate.
# Creates the final release/v{version} branch and deletes the candidate branch.
# Step 4: Triggers when the changelog PR is merged to release/candidate.
# Records last-release-sha and renames release/candidate to release/v{version}.
name: "Release: Finalize"
on:
@@ -33,6 +33,8 @@ jobs:
if: steps.check.outputs.is_release_pr == 'true'
with:
ref: release/candidate
token: ${{ secrets.RELEASE_PAT }}
fetch-depth: 0
- name: Extract version from manifest
if: steps.check.outputs.is_release_pr == 'true'
@@ -42,18 +44,33 @@ jobs:
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Extracted version: $VERSION"
- name: Create release branch
- name: Configure git identity from RELEASE_PAT
if: steps.check.outputs.is_release_pr == 'true'
env:
GH_TOKEN: ${{ secrets.RELEASE_PAT }}
run: |
git checkout -b "release/v${{ steps.version.outputs.version }}"
git push origin "release/v${{ steps.version.outputs.version }}"
echo "Created branch: release/v${{ steps.version.outputs.version }}"
USER_JSON=$(gh api user)
git config user.name "$(echo "$USER_JSON" | jq -r '.login')"
git config user.email "$(echo "$USER_JSON" | jq -r '.id')+$(echo "$USER_JSON" | jq -r '.login')@users.noreply.github.com"
- name: Delete release/candidate branch
- name: Record last-release-sha for release-please
if: steps.check.outputs.is_release_pr == 'true'
run: |
git push origin --delete release/candidate
echo "Deleted branch: release/candidate"
git fetch origin main
CUT_SHA=$(git merge-base origin/main HEAD)
echo "Release was cut from main at: $CUT_SHA"
jq --arg sha "$CUT_SHA" '. + {"last-release-sha": $sha}' \
.github/release-please-config.json > tmp.json && mv tmp.json .github/release-please-config.json
git add .github/release-please-config.json
git commit -m "chore: update last-release-sha for next release"
git push origin release/candidate
- name: Rename release/candidate to release/v{version}
if: steps.check.outputs.is_release_pr == 'true'
run: |
VERSION="v${{ steps.version.outputs.version }}"
git push origin "release/candidate:refs/heads/release/$VERSION" ":release/candidate"
echo "Renamed release/candidate to release/$VERSION"
- name: Update PR label to tagged
if: steps.check.outputs.is_release_pr == 'true'
+15 -1
View File
@@ -1,5 +1,5 @@
# Runs release-please to create/update a PR with version bump and changelog.
# Triggered by pushes to release/candidate or manually.
# Triggered automatically by step 1 (cut) or step 3 (cherry-pick).
name: "Release: Please"
on:
@@ -18,11 +18,25 @@ jobs:
if: "!startsWith(github.event.head_commit.message, 'chore(release')"
runs-on: ubuntu-latest
steps:
- name: Check if release/candidate still exists
id: check
env:
GH_TOKEN: ${{ github.token }}
run: |
if gh api repos/${{ github.repository }}/branches/release/candidate --silent 2>/dev/null; then
echo "exists=true" >> $GITHUB_OUTPUT
else
echo "release/candidate branch no longer exists, skipping"
echo "exists=false" >> $GITHUB_OUTPUT
fi
- uses: actions/checkout@v4
if: steps.check.outputs.exists == 'true'
with:
ref: release/candidate
- uses: googleapis/release-please-action@v4
if: steps.check.outputs.exists == 'true'
with:
token: ${{ secrets.RELEASE_PAT }}
config-file: .github/release-please-config.json
+2 -2
View File
@@ -1,5 +1,5 @@
# Builds and publishes the package to PyPI from a release branch.
# Creates a merge-back PR to sync release changes to main.
# Step 6: Builds and publishes the package to PyPI from a release/v{version} branch.
# Creates a merge-back PR (step 7) to sync release changes to main.
name: "Release: Publish to PyPi"
on:
+6
View File
@@ -1,5 +1,11 @@
# Changelog
## [1.25.1](https://github.com/google/adk-python/compare/v1.25.0...v1.25.1) (2026-02-18)
### Bug Fixes
* Fix pickling lock errors in McpSessionManager ([4e2d615](https://github.com/google/adk-python/commit/4e2d6159ae3552954aaae295fef3e09118502898))
## [1.25.0](https://github.com/google/adk-python/compare/v1.24.1...v1.25.0) (2026-02-11)
### Features
+3 -1
View File
@@ -38,7 +38,9 @@ BIGQUERY_AGENT_NAME = "adk_sample_bigquery_agent"
# tool read-only) or PROTECTED (only allows writes in the anonymous dataset of a
# BigQuery session) write mode.
tool_config = BigQueryToolConfig(
write_mode=WriteMode.ALLOWED, application_name=BIGQUERY_AGENT_NAME
write_mode=WriteMode.ALLOWED,
application_name=BIGQUERY_AGENT_NAME,
max_query_result_rows=50,
)
if CREDENTIALS_TYPE == AuthCredentialTypes.OAUTH2:
@@ -0,0 +1,15 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# 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.
from . import agent
@@ -0,0 +1,27 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# 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.
"""LiteLLM sample agent for SSE text streaming."""
from __future__ import annotations
from google.adk import Agent
from google.adk.models.lite_llm import LiteLlm
root_agent = Agent(
name='litellm_streaming_agent',
model=LiteLlm(model='gemini/gemini-2.5-flash'),
description='A LiteLLM agent used for streaming text responses.',
instruction='You are a verbose assistant',
)
@@ -0,0 +1,107 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# 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.
"""Runs the LiteLLM streaming sample with SSE enabled."""
from __future__ import annotations
import asyncio
from dotenv import load_dotenv
from google.adk.agents.run_config import RunConfig
from google.adk.agents.run_config import StreamingMode
from google.adk.cli.utils import logs
from google.adk.runners import InMemoryRunner
from google.genai import types
from . import agent
load_dotenv(override=True)
logs.log_to_tmp_folder()
async def _run_prompt(
*,
runner: InMemoryRunner,
user_id: str,
session_id: str,
prompt: str,
) -> None:
"""Runs one prompt and prints partial chunks in real time."""
content = types.Content(
role='user',
parts=[types.Part.from_text(text=prompt)],
)
print(f'User: {prompt}')
print('Agent: ', end='', flush=True)
saw_text = False
saw_partial_text = False
# For `adk web`, enable the `Streaming` toggle in the UI to get
# partial SSE responses similar to this script.
async for event in runner.run_async(
user_id=user_id,
session_id=session_id,
new_message=content,
run_config=RunConfig(streaming_mode=StreamingMode.SSE),
):
if not event.content:
continue
text = ''.join(part.text for part in event.content.parts if part.text)
if not text:
continue
if event.partial:
print(text, end='', flush=True)
saw_text = True
saw_partial_text = True
continue
# With SSE mode, ADK emits a final aggregated event after partial chunks.
if not saw_partial_text:
print(text, end='', flush=True)
saw_text = True
if saw_text:
print()
else:
print('(no text response)')
print('------------------------------------')
async def main() -> None:
app_name = 'litellm_streaming_demo'
user_id = 'user_1'
runner = InMemoryRunner(agent=agent.root_agent, app_name=app_name)
session = await runner.session_service.create_session(
app_name=app_name,
user_id=user_id,
)
prompts = [
'Write an essay about the roman empire',
'Now summarize the essay into one sentence.',
]
for prompt in prompts:
await _run_prompt(
runner=runner,
user_id=user_id,
session_id=session.id,
prompt=prompt,
)
if __name__ == '__main__':
asyncio.run(main())
+1 -3
View File
@@ -52,9 +52,7 @@ root_agent = Agent(
model="gemini-2.5-flash",
name="skill_user_agent",
description="An agent that can use specialized skills.",
instruction=(
"You are a helpful assistant that can leverage skills to perform tasks."
),
instruction=skill_toolset.DEFAULT_SKILL_SYSTEM_INSTRUCTION,
tools=[
my_skill_toolset,
],
+2 -1
View File
@@ -56,7 +56,7 @@ dependencies = [
"opentelemetry-resourcedetector-gcp>=1.9.0a0, <2.0.0",
"opentelemetry-sdk>=1.36.0, <1.40.0",
"pyarrow>=14.0.0",
"pydantic>=2.0, <3.0.0", # For data validation/models
"pydantic>=2.7.0, <3.0.0", # For data validation/models
"python-dateutil>=2.9.0.post0, <3.0.0", # For Vertext AI Session Service
"python-dotenv>=1.0.0, <2.0.0", # To manage environment variables
"requests>=2.32.4, <3.0.0",
@@ -108,6 +108,7 @@ community = [
eval = [
# go/keep-sorted start
"Jinja2>=3.1.4,<4.0.0", # For eval template rendering
"google-cloud-aiplatform[evaluation]>=1.100.0",
"pandas>=2.2.3",
"rouge-score>=0.1.2",
+2 -1
View File
@@ -15,8 +15,9 @@
from __future__ import annotations
from . import version
from .agents.context import Context
from .agents.llm_agent import Agent
from .runners import Runner
__version__ = version.__version__
__all__ = ["Agent", "Runner"]
__all__ = ["Agent", "Context", "Runner"]
+2
View File
@@ -13,6 +13,7 @@
# limitations under the License.
from .base_agent import BaseAgent
from .context import Context
from .invocation_context import InvocationContext
from .live_request_queue import LiveRequest
from .live_request_queue import LiveRequestQueue
@@ -27,6 +28,7 @@ from .sequential_agent import SequentialAgent
__all__ = [
'Agent',
'BaseAgent',
'Context',
'LlmAgent',
'LoopAgent',
'McpInstructionProvider',
+4 -235
View File
@@ -14,240 +14,9 @@
from __future__ import annotations
from collections.abc import Mapping
from collections.abc import Sequence
from typing import Any
from typing import Optional
from typing import TYPE_CHECKING
from typing_extensions import override
from .context import Context
# Keep ReadonlyContext for backward compatibility
from .readonly_context import ReadonlyContext
if TYPE_CHECKING:
from google.genai import types
from ..artifacts.base_artifact_service import ArtifactVersion
from ..auth.auth_credential import AuthCredential
from ..auth.auth_tool import AuthConfig
from ..events.event import Event
from ..events.event_actions import EventActions
from ..sessions.state import State
from .invocation_context import InvocationContext
class CallbackContext(ReadonlyContext):
"""The context of various callbacks within an agent run."""
def __init__(
self,
invocation_context: InvocationContext,
*,
event_actions: Optional[EventActions] = None,
) -> None:
super().__init__(invocation_context)
from ..events.event_actions import EventActions
from ..sessions.state import State
self._event_actions = event_actions or EventActions()
self._state = State(
value=invocation_context.session.state,
delta=self._event_actions.state_delta,
)
@property
@override
def state(self) -> State:
"""The delta-aware state of the current session.
For any state change, you can mutate this object directly,
e.g. `ctx.state['foo'] = 'bar'`
"""
return self._state
async def load_artifact(
self, filename: str, version: Optional[int] = None
) -> Optional[types.Part]:
"""Loads an artifact attached to the current session.
Args:
filename: The filename of the artifact.
version: The version of the artifact. If None, the latest version will be
returned.
Returns:
The artifact.
"""
if self._invocation_context.artifact_service is None:
raise ValueError("Artifact service is not initialized.")
return await self._invocation_context.artifact_service.load_artifact(
app_name=self._invocation_context.app_name,
user_id=self._invocation_context.user_id,
session_id=self._invocation_context.session.id,
filename=filename,
version=version,
)
async def save_artifact(
self,
filename: str,
artifact: types.Part,
custom_metadata: Optional[dict[str, Any]] = None,
) -> int:
"""Saves an artifact and records it as delta for the current session.
Args:
filename: The filename of the artifact.
artifact: The artifact to save.
custom_metadata: Custom metadata to associate with the artifact.
Returns:
The version of the artifact.
"""
if self._invocation_context.artifact_service is None:
raise ValueError("Artifact service is not initialized.")
version = await self._invocation_context.artifact_service.save_artifact(
app_name=self._invocation_context.app_name,
user_id=self._invocation_context.user_id,
session_id=self._invocation_context.session.id,
filename=filename,
artifact=artifact,
custom_metadata=custom_metadata,
)
self._event_actions.artifact_delta[filename] = version
return version
async def get_artifact_version(
self, filename: str, version: Optional[int] = None
) -> Optional[ArtifactVersion]:
"""Gets artifact version info.
Args:
filename: The filename of the artifact.
version: The version of the artifact. If None, the latest version will be
returned.
Returns:
The artifact version info.
"""
if self._invocation_context.artifact_service is None:
raise ValueError("Artifact service is not initialized.")
return await self._invocation_context.artifact_service.get_artifact_version(
app_name=self._invocation_context.app_name,
user_id=self._invocation_context.user_id,
session_id=self._invocation_context.session.id,
filename=filename,
version=version,
)
async def list_artifacts(self) -> list[str]:
"""Lists the filenames of the artifacts attached to the current session."""
if self._invocation_context.artifact_service is None:
raise ValueError("Artifact service is not initialized.")
return await self._invocation_context.artifact_service.list_artifact_keys(
app_name=self._invocation_context.app_name,
user_id=self._invocation_context.user_id,
session_id=self._invocation_context.session.id,
)
async def save_credential(self, auth_config: AuthConfig) -> None:
"""Saves a credential to the credential service.
Args:
auth_config: The authentication configuration containing the credential.
"""
if self._invocation_context.credential_service is None:
raise ValueError("Credential service is not initialized.")
await self._invocation_context.credential_service.save_credential(
auth_config, self
)
async def load_credential(
self, auth_config: AuthConfig
) -> Optional[AuthCredential]:
"""Loads a credential from the credential service.
Args:
auth_config: The authentication configuration for the credential.
Returns:
The loaded credential, or None if not found.
"""
if self._invocation_context.credential_service is None:
raise ValueError("Credential service is not initialized.")
return await self._invocation_context.credential_service.load_credential(
auth_config, self
)
def get_auth_response(
self, auth_config: AuthConfig
) -> Optional[AuthCredential]:
"""Gets the auth response credential from session state.
This method retrieves an authentication credential that was previously
stored in session state after a user completed an OAuth flow or other
authentication process.
Args:
auth_config: The authentication configuration for the credential.
Returns:
The auth credential from the auth response, or None if not found.
"""
from ..auth.auth_handler import AuthHandler
return AuthHandler(auth_config).get_auth_response(self.state)
async def add_session_to_memory(self) -> None:
"""Triggers memory generation for the current session.
This method saves the current session's events to the memory service,
enabling the agent to recall information from past interactions.
Raises:
ValueError: If memory service is not available.
Example:
```python
async def my_after_agent_callback(callback_context: CallbackContext):
# Save conversation to memory at the end of each interaction
await callback_context.add_session_to_memory()
```
"""
if self._invocation_context.memory_service is None:
raise ValueError(
"Cannot add session to memory: memory service is not available."
)
await self._invocation_context.memory_service.add_session_to_memory(
self._invocation_context.session
)
async def add_events_to_memory(
self,
*,
events: Sequence[Event],
custom_metadata: Mapping[str, object] | None = None,
) -> None:
"""Adds an explicit list of events to the memory service.
Uses this callback's current session identifiers as memory scope.
Args:
events: Explicit events to add to memory.
custom_metadata: Optional standard metadata for memory generation.
Raises:
ValueError: If memory service is not available.
"""
if self._invocation_context.memory_service is None:
raise ValueError(
"Cannot add events to memory: memory service is not available."
)
await self._invocation_context.memory_service.add_events_to_memory(
app_name=self._invocation_context.session.app_name,
user_id=self._invocation_context.session.user_id,
session_id=self._invocation_context.session.id,
events=events,
custom_metadata=custom_metadata,
)
# CallbackContext is unified into Context
CallbackContext = Context
+40 -1
View File
@@ -32,6 +32,7 @@ if TYPE_CHECKING:
from ..events.event import Event
from ..events.event_actions import EventActions
from ..memory.base_memory_service import SearchMemoryResponse
from ..memory.memory_entry import MemoryEntry
from ..sessions.state import State
from ..tools.tool_confirmation import ToolConfirmation
from .invocation_context import InvocationContext
@@ -76,11 +77,21 @@ class Context(ReadonlyContext):
"""The function call id of the current tool call."""
return self._function_call_id
@function_call_id.setter
def function_call_id(self, value: str | None) -> None:
"""Sets the function call id of the current tool call."""
self._function_call_id = value
@property
def tool_confirmation(self) -> ToolConfirmation | None:
"""The tool confirmation of the current tool call."""
return self._tool_confirmation
@tool_confirmation.setter
def tool_confirmation(self, value: ToolConfirmation | None) -> None:
"""Sets the tool confirmation of the current tool call."""
self._tool_confirmation = value
@property
@override
def state(self) -> State:
@@ -335,7 +346,8 @@ class Context(ReadonlyContext):
Args:
events: Explicit events to add to memory.
custom_metadata: Optional standard metadata for memory generation.
custom_metadata: Optional metadata forwarded to the configured memory
service. Supported keys are implementation-specific.
Raises:
ValueError: If memory service is not available.
@@ -352,6 +364,33 @@ class Context(ReadonlyContext):
custom_metadata=custom_metadata,
)
async def add_memory(
self,
*,
memories: Sequence[MemoryEntry],
custom_metadata: Mapping[str, object] | None = None,
) -> None:
"""Adds explicit memory items directly to the memory service.
Uses this callback's current session identifiers as memory scope.
Args:
memories: Explicit memory items to add.
custom_metadata: Optional metadata forwarded to the configured memory
service. Supported keys are implementation-specific.
Raises:
ValueError: If memory service is not available.
"""
if self._invocation_context.memory_service is None:
raise ValueError("Cannot add memory: memory service is not available.")
await self._invocation_context.memory_service.add_memory(
app_name=self._invocation_context.session.app_name,
user_id=self._invocation_context.session.user_id,
memories=memories,
custom_metadata=custom_metadata,
)
async def search_memory(self, query: str) -> SearchMemoryResponse:
"""Searches the memory of the current user.
+1
View File
@@ -79,6 +79,7 @@ class OAuth2Auth(BaseModelWithConfig):
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
@@ -107,8 +107,10 @@ def update_credential_with_tokens(
auth_credential: The authentication credential to update.
tokens: The OAuth2Token object containing new token information.
"""
if auth_credential.oauth2 and tokens:
auth_credential.oauth2.access_token = tokens.get("access_token")
auth_credential.oauth2.refresh_token = tokens.get("refresh_token")
auth_credential.oauth2.id_token = tokens.get("id_token")
auth_credential.oauth2.expires_at = (
int(tokens.get("expires_at")) if tokens.get("expires_at") else None
)

Some files were not shown because too many files have changed in this diff Show More