diff --git a/.github/release-please.yml b/.github/release-please.yml
new file mode 100644
index 0000000000..65cfbe96ec
--- /dev/null
+++ b/.github/release-please.yml
@@ -0,0 +1,5 @@
+releaseType: python
+handleGHRelease: true
+bumpMinorPreMajor: false
+extraFiles:
+ - src/google/adk/version.py
\ No newline at end of file
diff --git a/.github/release-trigger.yml b/.github/release-trigger.yml
new file mode 100644
index 0000000000..7fe3622578
--- /dev/null
+++ b/.github/release-trigger.yml
@@ -0,0 +1 @@
+enabled: true
\ No newline at end of file
diff --git a/.github/workflows/check-file-contents.yml b/.github/workflows/check-file-contents.yml
new file mode 100644
index 0000000000..780ff9f62e
--- /dev/null
+++ b/.github/workflows/check-file-contents.yml
@@ -0,0 +1,113 @@
+# Copyright 2025 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.
+
+name: "Check file contents"
+
+on:
+ pull_request:
+ paths:
+ - '**.py'
+
+jobs:
+ check-file-contents:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 2
+
+ - name: Check for logger pattern in all changed Python files
+ run: |
+ git fetch origin ${{ github.base_ref }}
+ CHANGED_FILES=$(git diff --diff-filter=ACMR --name-only origin/${{ github.base_ref }}...HEAD | grep -E '\.py$' || true)
+ if [ -n "$CHANGED_FILES" ]; then
+ echo "Changed Python files to check:"
+ echo "$CHANGED_FILES"
+ echo ""
+
+ # Check for 'logger = logging.getLogger(__name__)' in changed .py files.
+ # The grep command will exit with a non-zero status code if the pattern is not found.
+ # We invert the exit code with ! so the step succeeds if the pattern is NOT found.
+ set +e
+ FILES_WITH_FORBIDDEN_LOGGER=$(grep -lE 'logger = logging\.getLogger\(__name__\)' $CHANGED_FILES)
+ GREP_EXIT_CODE=$?
+ set -e
+
+ # grep exits with 0 if matches are found, 1 if no matches are found.
+ # A non-zero exit code other than 1 indicates an error.
+ if [ $GREP_EXIT_CODE -eq 0 ]; then
+ echo "❌ Found forbidden use of 'logger = logging.getLogger(__name__)'. Please use 'logger = logging.getLogger('google_adk.' + __name__)' instead."
+ echo "The following files contain the forbidden pattern:"
+ echo "$FILES_WITH_FORBIDDEN_LOGGER"
+ exit 1
+ elif [ $GREP_EXIT_CODE -eq 1 ]; then
+ echo "✅ No instances of 'logger = logging.getLogger(__name__)' found in changed Python files."
+ fi
+ else
+ echo "✅ No relevant Python files found."
+ fi
+
+ - name: Check for import pattern in certain changed Python files
+ run: |
+ git fetch origin ${{ github.base_ref }}
+ CHANGED_FILES=$(git diff --diff-filter=ACMR --name-only origin/${{ github.base_ref }}...HEAD | grep -E '\.py$' | grep -v -E '__init__.py$|version.py$|tests/.*|contributing/samples/' || true)
+ if [ -n "$CHANGED_FILES" ]; then
+ echo "Changed Python files to check:"
+ echo "$CHANGED_FILES"
+ echo ""
+
+ # Use grep -L to find files that DO NOT contain the pattern.
+ # This command will output a list of non-compliant files.
+ FILES_MISSING_IMPORT=$(grep -L 'from __future__ import annotations' $CHANGED_FILES)
+
+ # Check if the list of non-compliant files is empty
+ if [ -z "$FILES_MISSING_IMPORT" ]; then
+ echo "✅ All modified Python files include 'from __future__ import annotations'."
+ exit 0
+ else
+ echo "❌ The following files are missing 'from __future__ import annotations':"
+ echo "$FILES_MISSING_IMPORT"
+ echo "This import is required to allow forward references in type annotations without quotes."
+ exit 1
+ fi
+ else
+ echo "✅ No relevant Python files found."
+ fi
+
+ - name: Check for import from cli package in certain changed Python files
+ run: |
+ git fetch origin ${{ github.base_ref }}
+ CHANGED_FILES=$(git diff --diff-filter=ACMR --name-only origin/${{ github.base_ref }}...HEAD | grep -E '\.py$' | grep -v -E 'cli/.*|tests/.*|contributing/samples/' || true)
+ if [ -n "$CHANGED_FILES" ]; then
+ echo "Changed Python files to check:"
+ echo "$CHANGED_FILES"
+ echo ""
+
+ set +e
+ FILES_WITH_FORBIDDEN_IMPORT=$(grep -lE '^from.*cli.*import.*$' $CHANGED_FILES)
+ GREP_EXIT_CODE=$?
+ set -e
+
+ if [[ $GREP_EXIT_CODE -eq 0 ]]; then
+ echo "❌ Do not import from the cli package outside of the cli package. If you need to reuse the code elsewhere, please move the code outside of the cli package."
+ echo "The following files contain the forbidden pattern:"
+ echo "$FILES_WITH_FORBIDDEN_IMPORT"
+ exit 1
+ else
+ echo "✅ No instances of importing from the cli package found in relevant changed Python files."
+ fi
+ else
+ echo "✅ No relevant Python files found."
+ fi
\ No newline at end of file
diff --git a/.github/workflows/isort.yml b/.github/workflows/isort.yml
new file mode 100644
index 0000000000..e1a087742c
--- /dev/null
+++ b/.github/workflows/isort.yml
@@ -0,0 +1,69 @@
+# Copyright 2025 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.
+
+name: Check sorting of imports
+
+on:
+ pull_request:
+ paths:
+ - '**.py'
+ - 'pyproject.toml'
+
+jobs:
+ isort-check:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 2
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.x'
+
+ - name: Install isort
+ run: |
+ pip install isort
+
+ - name: Run isort on changed files
+ id: run_isort
+ run: |
+ git fetch origin ${{ github.base_ref }}
+ CHANGED_FILES=$(git diff --diff-filter=ACMR --name-only origin/${{ github.base_ref }}...HEAD | grep -E '\.py$' || true)
+ if [ -n "$CHANGED_FILES" ]; then
+ echo "Changed Python files:"
+ echo "$CHANGED_FILES"
+ echo ""
+ FORMATTED_FILES=$(echo "$CHANGED_FILES" | tr '\n' ' ')
+
+ # Run isort --check
+ set +e
+ isort --check $CHANGED_FILES
+ RESULT=$?
+ set -e
+ if [ $RESULT -ne 0 ]; then
+ echo ""
+ echo "❌ isort check failed!"
+ echo "👉 To fix import order, run locally:"
+ echo ""
+ echo " isort $FORMATTED_FILES"
+ echo ""
+ exit $RESULT
+ fi
+ else
+ echo "No Python files changed. Skipping isort check."
+ fi
diff --git a/.github/workflows/pyink.yml b/.github/workflows/pyink.yml
index c79ae4b40f..ef9e72e453 100644
--- a/.github/workflows/pyink.yml
+++ b/.github/workflows/pyink.yml
@@ -17,8 +17,7 @@ name: Check Pyink Formatting
on:
pull_request:
paths:
- - 'src/**/*.py'
- - 'tests/**/*.py'
+ - '**.py'
- 'pyproject.toml'
jobs:
@@ -28,6 +27,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
+ with:
+ fetch-depth: 2
- name: Set up Python
uses: actions/setup-python@v5
@@ -38,36 +39,31 @@ jobs:
run: |
pip install pyink
- - name: Detect changed Python files
- id: detect_changes
+ - name: Run pyink on changed files
+ id: run_pyink
run: |
git fetch origin ${{ github.base_ref }}
CHANGED_FILES=$(git diff --diff-filter=ACMR --name-only origin/${{ github.base_ref }}...HEAD | grep -E '\.py$' || true)
- echo "CHANGED_FILES=${CHANGED_FILES}" >> $GITHUB_ENV
-
- - name: Run pyink on changed files
- if: env.CHANGED_FILES != ''
- run: |
- echo "Changed Python files:"
- echo "$CHANGED_FILES"
-
- # Run pyink --check
- set +e
- pyink --check --config pyproject.toml $CHANGED_FILES
- RESULT=$?
- set -e
-
- if [ $RESULT -ne 0 ]; then
- echo ""
- echo "❌ Pyink formatting check failed!"
- echo "👉 To fix formatting, run locally:"
- echo ""
- echo " pyink --config pyproject.toml $CHANGED_FILES"
+ if [ -n "$CHANGED_FILES" ]; then
+ echo "Changed Python files:"
+ echo "$CHANGED_FILES"
echo ""
- exit $RESULT
- fi
+ FORMATTED_FILES=$(echo "$CHANGED_FILES" | tr '\n' ' ')
- - name: No changed Python files detected
- if: env.CHANGED_FILES == ''
- run: |
- echo "No Python files changed. Skipping pyink check."
+ # Run pyink --check
+ set +e
+ pyink --check --diff --config pyproject.toml $CHANGED_FILES
+ RESULT=$?
+ set -e
+ if [ $RESULT -ne 0 ]; then
+ echo ""
+ echo "❌ Pyink formatting check failed!"
+ echo "👉 To fix formatting, run locally:"
+ echo ""
+ echo " pyink --config pyproject.toml $FORMATTED_FILES"
+ echo ""
+ exit $RESULT
+ fi
+ else
+ echo "No Python files changed. Skipping pyink check."
+ fi
diff --git a/.github/workflows/python-unit-tests.yml b/.github/workflows/python-unit-tests.yml
index b3689ed9a5..a504fde0d0 100644
--- a/.github/workflows/python-unit-tests.yml
+++ b/.github/workflows/python-unit-tests.yml
@@ -50,5 +50,4 @@ jobs:
source .venv/bin/activate
pytest tests/unittests \
--ignore=tests/unittests/artifacts/test_artifact_service.py \
- --ignore=tests/unittests/tools/google_api_tool/test_googleapi_to_openapi_converter.py
-
+ --ignore=tests/unittests/tools/google_api_tool/test_googleapi_to_openapi_converter.py
diff --git a/.gitignore b/.gitignore
index 589329ebab..6fb068d485 100644
--- a/.gitignore
+++ b/.gitignore
@@ -96,4 +96,4 @@ site/
Thumbs.db
*.bak
*.tmp
-*.temp
\ No newline at end of file
+*.temp
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8afca246ce..48e23b3a82 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,97 @@
# Changelog
+## [1.2.1](https://github.com/google/adk-python/compare/v1.2.0...v1.2.1) (2025-06-04)
+
+
+### Bug Fixes
+
+* Import deprecated from typing_extensions ([068df04](https://github.com/google/adk-python/commit/068df04bcef694725dd36e09f4476b5e67f1b456))
+
+
+## [1.2.0](https://github.com/google/adk-python/compare/v1.1.1...v1.2.0) (2025-06-04)
+
+
+### Features
+
+* Add agent engine as a deployment option to the ADK CLI ([2409c3e](https://github.com/google/adk-python/commit/2409c3ef192262c80f5328121f6dc4f34265f5cf))
+* Add an option to use gcs artifact service in adk web. ([8d36dbd](https://github.com/google/adk-python/commit/8d36dbda520b1c0dec148e1e1d84e36ddcb9cb95))
+* Add index tracking to handle parallel tool call using litellm ([05f4834](https://github.com/google/adk-python/commit/05f4834759c9b1f0c0af9d89adb7b81ea67d82c8))
+* Add sortByColumn functionality to List Operation ([af95dd2](https://github.com/google/adk-python/commit/af95dd29325865ec30a1945b98e65e457760e003))
+* Add implementation for `get_eval_case`, `update_eval_case` and `delete_eval_case` for the local eval sets manager. ([a7575e0](https://github.com/google/adk-python/commit/a7575e078a564af6db3f42f650e94ebc4f338918))
+* Expose more config of VertexAiSearchTool from latest Google GenAI SDK ([2b5c89b](https://github.com/google/adk-python/commit/2b5c89b3a94e82ea4a40363ea8de33d9473d7cf0))
+* New Agent Visualization ([da4bc0e](https://github.com/google/adk-python/commit/da4bc0efc0dd96096724559008205854e97c3fd1))
+* Set the max width and height of view image dialog to be 90% ([98a635a](https://github.com/google/adk-python/commit/98a635afee399f64e0a813d681cd8521fbb49500))
+* Support Langchain StructuredTool for Langchain tool ([7e637d3](https://github.com/google/adk-python/commit/7e637d3fa05ca3e43a937e7158008d2b146b1b81))
+* Support Langchain tools that has run_manager in _run args and don't have args_schema populated ([3616bb5](https://github.com/google/adk-python/commit/3616bb5fc4da90e79eb89039fb5e302d6a0a14ec))
+* Update for anthropic models ([16f7d98](https://github.com/google/adk-python/commit/16f7d98acf039f21ec8a99f19eabf0ef4cb5268c))
+* Use bigquery scope by default in bigquery credentials. ([ba5b80d](https://github.com/google/adk-python/commit/ba5b80d5d774ff5fdb61bd43b7849057da2b4edf))
+* Add jira_agent adk samples code which connect Jira cloud ([8759a25](https://github.com/google/adk-python/commit/8759a2525170edb2f4be44236fa646a93ba863e6))
+* Render HTML artifact in chat window ([5c2ad32](https://github.com/google/adk-python/commit/5c2ad327bf4262257c3bc91010c3f8c303d3a5f5))
+* Add export to json button in the chat window ([fc3e374](https://github.com/google/adk-python/commit/fc3e374c86c4de87b4935ee9c56b6259f00e8ea2))
+* Add tooltip to the export session button ([2735942](https://github.com/google/adk-python/commit/273594215efe9dbed44d4ef85e6234bd7ba7b7ae))
+
+
+### Bug Fixes
+
+* Add adk icon for UI ([2623c71](https://github.com/google/adk-python/commit/2623c710868d832b6d5119f38e22d82adb3de66b))
+* Add cache_ok option to remove sa warning. ([841e10a](https://github.com/google/adk-python/commit/841e10ae353e0b1b3d020a26d6cac6f37981550e))
+* Add support for running python main function in UnsafeLocalCodeExecutor when the code has an if __name__ == "__main__" statement. ([95e33ba](https://github.com/google/adk-python/commit/95e33baf57e9c267a758e08108cde76adf8af69b))
+* Adk web not working on some env for windows, fixes https://github.com/google/adk-web/issues/34 ([daac8ce](https://github.com/google/adk-python/commit/daac8cedfe6d894f77ea52784f0a6d19003b2c00))
+* Assign empty inputSchema to MCP tool when converting an ADK tool that wraps a function which takes no parameters. ([2a65c41](https://github.com/google/adk-python/commit/2a65c4118bb2aa97f2a13064db884bd63c14a5f7))
+* Call all tools in parallel calls during partial authentication ([0e72efb](https://github.com/google/adk-python/commit/0e72efb4398ce6a5d782bcdcb770b2473eb5af2e))
+* Continue fetching events if there are multiple pages. ([6506302](https://github.com/google/adk-python/commit/65063023a5a7cb6cd5db43db14a411213dc8acf5))
+* Do not convert "false" value to dict ([60ceea7](https://github.com/google/adk-python/commit/60ceea72bde2143eb102c60cf33b365e1ab07d8f))
+* Enhance agent loader exception handler and expose precise error information ([7b51ae9](https://github.com/google/adk-python/commit/7b51ae97245f6990c089183734aad41fe59b3330))
+* Ensure function description is copied when ignoring parameters ([7fdc6b4](https://github.com/google/adk-python/commit/7fdc6b4417e5cf0fbc72d3117531914353d3984a))
+* Filter memory by app_name and user_id. ([db4bc98](https://github.com/google/adk-python/commit/db4bc9809c7bb6b0d261973ca7cfd87b392694be))
+* Fix filtering by user_id for vertex ai session service listing ([9d4ca4e](https://github.com/google/adk-python/commit/9d4ca4ed44cf10bc87f577873faa49af469acc25))
+* fix parameter schema generation for gemini ([5a67a94](https://github.com/google/adk-python/commit/5a67a946d2168b80dd6eba008218468c2db2e74e))
+* Handle non-indexed function call chunks with incremental fallback index ([b181cbc](https://github.com/google/adk-python/commit/b181cbc8bc629d1c9bfd50054e47a0a1b04f7410))
+* Handles function tool parsing corner case where type hints are stored as strings. ([a8a2074](https://github.com/google/adk-python/commit/a8a20743f92cd63c3d287a3d503c1913dd5ad5ae))
+* Introduce PreciseTimestamp to fix mysql datetime precision issue. ([841e10a](https://github.com/google/adk-python/commit/841e10ae353e0b1b3d020a26d6cac6f37981550e))
+* match arg case in errors ([b226a06](https://github.com/google/adk-python/commit/b226a06c0bf798f85a53c591ad12ee582703af6d))
+* ParallelAgent should only append to its immediate sub-agent, not transitive descendants ([ec8bc73](https://github.com/google/adk-python/commit/ec8bc7387c84c3f261c44cedfe76eb1f702e7b17))
+* Relax openapi spec to gemini schema conversion to tolerate more cases ([b1a74d0](https://github.com/google/adk-python/commit/b1a74d099fae44d41750b79e58455282d919dd78))
+* Remove labels from config when using API key from Google AI Studio to call model ([5d29716](https://github.com/google/adk-python/commit/5d297169d08a2d0ea1a07641da2ac39fa46b68a4))
+* **sample:** Correct text artifact saving in artifact_save_text sample ([5c6001d](https://github.com/google/adk-python/commit/5c6001d90fe6e1d15a2db6b30ecf9e7b6c26eee4))
+* Separate thinking from text parts in streaming mode ([795605a](https://github.com/google/adk-python/commit/795605a37e1141e37d86c9b3fa484a3a03e7e9a6))
+* Simplify content for ollama provider ([eaee49b](https://github.com/google/adk-python/commit/eaee49bc897c20231ecacde6855cccfa5e80d849))
+* Timeout issues for mcpstdio server when mcp tools are incorrect. ([45ef668](https://github.com/google/adk-python/commit/45ef6684352e3c8082958bece8610df60048f4a3))
+* **transfer_to_agent:** update docstring for clarity and accuracy ([854a544](https://github.com/google/adk-python/commit/854a5440614590c2a3466cf652688ba57d637205))
+* Update unit test code for test_connection ([b0403b2](https://github.com/google/adk-python/commit/b0403b2d98b2776d15475f6b525409670e2841fc))
+* Use inspect.cleandoc on function docstrings in generate_function_declaration. ([f7cb666](https://github.com/google/adk-python/commit/f7cb66620be843b8d9f3d197d6e8988e9ee0dfca))
+* Restore errors path ([32c5ffa](https://github.com/google/adk-python/commit/32c5ffa8ca5e037f41ff345f9eecf5b26f926ea1))
+* Unused import for deprecated ([ccd05e0](https://github.com/google/adk-python/commit/ccd05e0b00d0327186e3b1156f1b0216293efe21))
+* Prevent JSON parsing errors and preserve non-ascii characters in telemetry ([d587270](https://github.com/google/adk-python/commit/d587270327a8de9f33b3268de5811ac756959850))
+* Raise HTTPException when running evals in fast_api if google-adk[eval] is not installed ([1de5c34](https://github.com/google/adk-python/commit/1de5c340d8da1cedee223f6f5a8c90070a9f0298))
+* Fix typos in README for sample bigquery_agent and oauth_calendar_agent ([9bdd813](https://github.com/google/adk-python/commit/9bdd813be15935af5c5d2a6982a2391a640cab23))
+* Make tool_call one span for telemetry and renamed to execute_tool ([999a7fe](https://github.com/google/adk-python/commit/999a7fe69d511b1401b295d23ab3c2f40bccdc6f))
+* Use media type in chat window. Remove isArtifactImage and isArtifactAudio reference ([1452dac](https://github.com/google/adk-python/commit/1452dacfeb6b9970284e1ddeee6c4f3cb56781f8))
+* Set output_schema correctly for LiteLllm ([6157db7](https://github.com/google/adk-python/commit/6157db77f2fba4a44d075b51c83bff844027a147))
+* Update pending event dialog style ([1db601c](https://github.com/google/adk-python/commit/1db601c4bd90467b97a2f26fe9d90d665eb3c740))
+* Remove the gap between event holder and image ([63822c3](https://github.com/google/adk-python/commit/63822c3fa8b0bdce2527bd0d909c038e2b66dd98))
+
+
+### Documentation
+
+* Adds a sample agent to illustrate state usage via `callbacks`. ([18fbe3c](https://github.com/google/adk-python/commit/18fbe3cbfc9f2af97e4b744ec0a7552331b1d8e3))
+* Fix typos in documentation ([7aaf811](https://github.com/google/adk-python/commit/7aaf8116169c210ceda35c649b5b49fb65bbb740))
+* Change eval_dataset to eval_dataset_file_path_or_dir ([62d7bf5](https://github.com/google/adk-python/commit/62d7bf58bb1c874caaf3c56a614500ae3b52f215))
+* Fix broken link to A2A example ([0d66a78](https://github.com/google/adk-python/commit/0d66a7888b68380241b92f7de394a06df5a0cc06))
+* Fix typo in envs.py ([bd588bc](https://github.com/google/adk-python/commit/bd588bce50ccd0e70b96c7291db035a327ad4d24))
+* Updates CONTRIBUTING.md to refine setup process using uv. ([04e07b4](https://github.com/google/adk-python/commit/04e07b4a1451123272641a256c6af1528ea6523e))
+* Create and update project documentation including README.md and CONTRIBUTING.md ([f180331](https://github.com/google/adk-python/commit/f1803312c6a046f94c23cfeaed3e8656afccf7c3))
+* Rename the root agent in the example to match the example name ([94c0aca](https://github.com/google/adk-python/commit/94c0aca685f1dfa4edb44caaedc2de25cc0caa41))
+* ADK: add section comment ([349a414](https://github.com/google/adk-python/commit/349a414120fbff0937966af95864bd683f063d08))
+
+
+### Chore
+
+* Miscellaneous changes ([0724a83](https://github.com/google/adk-python/commit/0724a83aa9cda00c1b228ed47a5baa7527bb4a0a), [a9dcc58](https://github.com/google/adk-python/commit/a9dcc588ad63013d063dbe37095c0d2e870142c3), [ac52eab](https://github.com/google/adk-python/commit/ac52eab88eccafa451be7584e24aea93ff15f3f3), [a0714b8](https://github.com/google/adk-python/commit/a0714b8afc55461f315ede8451b17aad18d698dd))
+* Enable release-please workflow ([57d99aa](https://github.com/google/adk-python/commit/57d99aa7897fb229f41c2a08034606df1e1e6064))
+* Added unit test coverage for local_eval_sets_manager.py ([174afb3](https://github.com/google/adk-python/commit/174afb3975bdc7e5f10c26f3eebb17d2efa0dd59))
+* Extract common options for `adk web` and `adk api_server` ([01965bd](https://github.com/google/adk-python/commit/01965bdd74a9dbdb0ce91a924db8dee5961478b8))
+
## 1.1.1
### Features
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e9ae04f61d..c0f3d0069f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,8 +2,7 @@
We'd love to accept your patches and contributions to this project.
-## Table of Contents
-
+- [How to contribute](#how-to-contribute)
- [Before you begin](#before-you-begin)
- [Sign our Contributor License Agreement](#sign-our-contributor-license-agreement)
- [Review our community guidelines](#review-our-community-guidelines)
@@ -12,16 +11,16 @@ We'd love to accept your patches and contributions to this project.
- [Requirement for PRs](#requirement-for-prs)
- [Large or Complex Changes](#large-or-complex-changes)
- [Testing Requirements](#testing-requirements)
- - [Unit Tests](#unit-tests)
- - [End-to-End (E2E) Tests](#manual-end-to-end-e2e-tests)
+ - [Unit Tests](#unit-tests)
+ - [Manual End-to-End (E2E) Tests](#manual-end-to-end-e2e-tests)
- [Documentation](#documentation)
- [Development Setup](#development-setup)
-- [Code reviews](#code-reviews)
+ - [Code reviews](#code-reviews)
-## Before you begin
+# Before you begin
-### Sign our Contributor License Agreement
+## Sign our Contributor License Agreement
Contributions to this project must be accompanied by a
[Contributor License Agreement](https://cla.developers.google.com/about) (CLA).
@@ -35,38 +34,38 @@ was for a different project), you probably don't need to do it again.
Visit to see your current agreements or to
sign a new one.
-### Review our community guidelines
+## Review our community guidelines
This project follows
[Google's Open Source Community Guidelines](https://opensource.google/conduct/).
-## Contribution workflow
+# Contribution workflow
-### Finding Issues to Work On
+## Finding Issues to Work On
- Browse issues labeled **`good first issue`** (newcomer-friendly) or **`help wanted`** (general contributions).
- For other issues, please kindly ask before contributing to avoid duplication.
-### Requirement for PRs
+## Requirement for PRs
- All PRs, other than small documentation or typo fixes, should have a Issue assoicated. If not, please create one.
- Small, focused PRs. Keep changes minimal—one concern per PR.
- For bug fixes or features, please provide logs or screenshot after the fix is applied to help reviewers better understand the fix.
- Please include a `testing plan` section in your PR to talk about how you will test. This will save time for PR review. See `Testing Requirements` section for more details.
-### Large or Complex Changes
+## Large or Complex Changes
For substantial features or architectural revisions:
- Open an Issue First: Outline your proposal, including design considerations and impact.
- Gather Feedback: Discuss with maintainers and the community to ensure alignment and avoid duplicate work
-### Testing Requirements
+## Testing Requirements
To maintain code quality and prevent regressions, all code changes must include comprehensive tests and verifiable end-to-end (E2E) evidence.
-#### Unit Tests
+### Unit Tests
Please add or update unit tests for your change. Please include a summary of passed `pytest` results.
@@ -80,7 +79,7 @@ Requirements for unit tests:
- Free of external dependencies (use mocks or fixtures as needed).
- **Quality:** Aim for high readability and maintainability; include docstrings or comments for complex scenarios.
-#### Manual End-to-End (E2E) Tests
+### Manual End-to-End (E2E) Tests
Manual E2E tests ensure integrated flows work as intended. Your tests should cover all scenarios. Sometimes, it's also good to ensure relevant functionality is not impacted.
@@ -97,23 +96,34 @@ Depending on your change:
- Include the command used and console output showing test results.
- Highlight sections of the log that directly relate to your change.
-### Documentation
+## Documentation
For any changes that impact user-facing documentation (guides, API reference, tutorials), please open a PR in the [adk-docs](https://github.com/google/adk-docs) repository to update relevant part before or alongside your code PR.
-### Development Setup
+## Development Setup
1. **Clone the repository:**
```shell
- git clone git@github.com:google/adk-python.git
+ gh repo clone google/adk-python
cd adk-python
```
-2. **Create and activate a virtual environment:**
+
+2. **Install uv:**
+
+ Check out [uv installation guide](https://docs.astral.sh/uv/getting-started/installation/).
+
+3. **Create and activate a virtual environment:**
+
+ **NOTE**: ADK supports Python 3.9+. Python 3.11 and above is strongly recommended.
+
+ Create a workspace venv using uv.
```shell
- python -m venv .venv
+ uv venv --python "python3.11" ".venv"
```
+ Activate the workspace venv.
+
```shell
source .venv/bin/activate
```
@@ -123,39 +133,77 @@ For any changes that impact user-facing documentation (guides, API reference, tu
source .\.venv\Scripts\activate
```
-3. **Install dependencies:**
+4. **Install dependencies:**
```shell
- pip install uv
uv sync --all-extras
```
-4. **Run unit tests:**
+
+ **NOTE**: for convenience, installing all extra deps as a starting point.
+
+5. **Run unit tests:**
```shell
- uv run pytest ./tests/unittests
+ pytest ./tests/unittests
```
-5. **Run pyink to format codebase:**
+
+ NOTE: for accurately repro test failure, only include `test` and `eval` as
+ extra dependencies.
+
+ ```shell
+ uv sync --extra test --extra eval
+ pytest ./tests/unittests
+ ```
+
+6. **Auto-format the code:**
+
+ **NOTE**: We use `isort` and `pyink` for styles. Use the included
+ autoformat.sh to auto-format.
```shell
- uv run pyink --config pyproject.toml ./src
+ ./autoformat.sh
```
-6. **Build the package**
+7. **Build the wheel file:**
+
```shell
uv build
```
-7. **Local Testing**
- Have a simple testing folder setup as mentioned in the [quickstart](https://google.github.io/adk-docs/get-started/quickstart/)
- then install the local package with changes after building it using the below command to test the changes.
+8. **Test the locally built wheel file:**
+ Have a simple testing folder setup as mentioned in the
+ [quickstart](https://google.github.io/adk-docs/get-started/quickstart/).
+
+ Then following below steps to test your changes:
+
+ Create a clean venv and activate it:
+
+ ```shell
+ VENV_PATH=~/venvs/adk-quickstart
+ ```
```shell
- uv pip install
+ command -v deactivate >/dev/null 2>&1 && deactivate
+ ```
- [eg]: uv pip install /dist/google_adk-0.4.0-py3-none-any.whl
+ ```shell
+ rm -rf $VENV_PATH \
+ && python3 -m venv $VENV_PATH \
+ && source $VENV_PATH/bin/activate
```
-### Code reviews
+ Install the locally built wheel file:
+
+ ```shell
+ pip install dist/google_adk--py3-none-any.whl
+ ```
+
+## Contributing Resources
+
+[Contributing folder](https://github.com/google/adk-python/tree/main/contributing/samples) has resources that is helpful for contributors.
+
+
+## Code reviews
All submissions, including submissions by project members, require review. We
use GitHub pull requests for this purpose. Consult
diff --git a/README.md b/README.md
index 3edc7c8f06..d117602d55 100644
--- a/README.md
+++ b/README.md
@@ -43,8 +43,8 @@ Agent Development Kit (ADK) is a flexible and modular framework for developing a
## 🤖 Agent2Agent (A2A) Protocol and ADK Integration
For remote agent-to-agent communication, ADK integrates with the
-[A2A protocol](https://github.com/google/A2A/).
-See this [example](https://github.com/google/A2A/tree/main/samples/python/agents/google_adk)
+[A2A protocol](https://github.com/google-a2a/A2A/).
+See this [example](https://github.com/google-a2a/a2a-samples/tree/main/samples/python/agents/google_adk)
for how they can work together.
## 🚀 Installation
diff --git a/autoformat.sh b/autoformat.sh
index 0c9a1c735e..2e439a879a 100755
--- a/autoformat.sh
+++ b/autoformat.sh
@@ -41,6 +41,13 @@ echo '---------------------------------------'
isort tests/
echo 'All done! ✨ 🍰 ✨'
+echo '---------------------------------------'
+echo '| Organizing imports for contributing/...'
+echo '---------------------------------------'
+
+isort contributing/
+echo 'All done! ✨ 🍰 ✨'
+
echo '---------------------------------------'
echo '| Auto-formatting src/...'
echo '---------------------------------------'
@@ -52,3 +59,9 @@ echo '| Auto-formatting tests/...'
echo '---------------------------------------'
find -L tests/ -type f -name "*.py" -exec pyink --config pyproject.toml {} +
+
+echo '---------------------------------------'
+echo '| Auto-formatting contributing/...'
+echo '---------------------------------------'
+
+find -L contributing/ -type f -name "*.py" -exec pyink --config pyproject.toml {} +
diff --git a/contributing/samples/README.md b/contributing/samples/README.md
new file mode 100644
index 0000000000..a2abab60cf
--- /dev/null
+++ b/contributing/samples/README.md
@@ -0,0 +1,9 @@
+# Contributing Resources
+
+This folder host resources for ADK contributors, for example, testing samples etc.
+
+# Samples
+
+Samples folder host samples to test different features. The samples are usually minimal and simplistic to test one or a few scenarios.
+
+**Note**: This is different from the [google/adk-samples](https://github.com/google/adk-samples) repo, which hosts more complex e2e samples for customers to use or modify directly.
diff --git a/contributing/samples/adk_triaging_agent/agent.py b/contributing/samples/adk_triaging_agent/agent.py
new file mode 100644
index 0000000000..2720e5b463
--- /dev/null
+++ b/contributing/samples/adk_triaging_agent/agent.py
@@ -0,0 +1,118 @@
+# Copyright 2025 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.
+
+import os
+import random
+import time
+
+from google.adk import Agent
+from google.adk.tools.tool_context import ToolContext
+from google.genai import types
+import requests
+
+# Read the PAT from the environment variable
+GITHUB_TOKEN = os.getenv("GITHUB_TOKEN") # Ensure you've set this in your shell
+if not GITHUB_TOKEN:
+ raise ValueError("GITHUB_TOKEN environment variable not set")
+
+# Repository information
+OWNER = "google"
+REPO = "adk-python"
+
+# Base URL for the GitHub API
+BASE_URL = "https://api.github.com"
+
+# Headers including the Authorization header
+headers = {
+ "Authorization": f"token {GITHUB_TOKEN}",
+ "Accept": "application/vnd.github.v3+json",
+}
+
+
+def list_issues(per_page: int):
+ """
+ Generator to list all issues for the repository by handling pagination.
+
+ Args:
+ per_page: number of pages to return per page.
+
+ """
+ state = "open"
+ # only process the 1st page for testing for now
+ page = 1
+ results = []
+ url = ( # :contentReference[oaicite:16]{index=16}
+ f"{BASE_URL}/repos/{OWNER}/{REPO}/issues"
+ )
+ # Warning: let's only handle max 10 issues at a time to avoid bad results
+ params = {"state": state, "per_page": per_page, "page": page}
+ response = requests.get(url, headers=headers, params=params)
+ response.raise_for_status() # :contentReference[oaicite:17]{index=17}
+ issues = response.json()
+ if not issues:
+ return []
+ for issue in issues:
+ # Skip pull requests (issues API returns PRs as well)
+ if "pull_request" in issue:
+ continue
+ results.append(issue)
+ return results
+
+
+def add_label_to_issue(issue_number: str, label: str):
+ """
+ Add the specified label to the given issue number.
+
+ Args:
+ issue_number: issue number of the Github issue, in string foramt.
+ label: label to assign
+ """
+ url = f"{BASE_URL}/repos/{OWNER}/{REPO}/issues/{issue_number}/labels"
+ payload = [label]
+ response = requests.post(url, headers=headers, json=payload)
+ response.raise_for_status()
+ return response.json()
+
+
+root_agent = Agent(
+ model="gemini-2.5-pro-preview-05-06",
+ name="adk_triaging_assistant",
+ description="Triage ADK issues.",
+ instruction="""
+ You are a Github adk-python repo triaging bot. You will help get issues, and label them.
+ Here are the rules for labeling:
+ - If the user is asking about documentation-related questions, label it with "documentation".
+ - If it's about session, memory services, label it with "services"
+ - If it's about UI/web, label it with "question"
+ - If it's related to tools, label it with "tools"
+ - If it's about agent evalaution, then label it with "eval".
+ - If it's about streaming/live, label it with "live".
+ - If it's about model support(non-Gemini, like Litellm, Ollama, OpenAI models), label it with "models".
+ - If it's about tracing, label it with "tracing".
+ - If it's agent orchestration, agent definition, label it with "core".
+ - If you can't find a appropriate labels for the issue, return the issues to user to decide.
+ """,
+ tools=[
+ list_issues,
+ add_label_to_issue,
+ ],
+ generate_content_config=types.GenerateContentConfig(
+ safety_settings=[
+ types.SafetySetting( # avoid false alarm about rolling dice.
+ category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
+ threshold=types.HarmBlockThreshold.OFF,
+ ),
+ ]
+ ),
+)
diff --git a/contributing/samples/artifact_save_text/agent.py b/contributing/samples/artifact_save_text/agent.py
index 1f62dcca83..53a7f300d8 100755
--- a/contributing/samples/artifact_save_text/agent.py
+++ b/contributing/samples/artifact_save_text/agent.py
@@ -19,8 +19,12 @@
async def log_query(tool_context: ToolContext, query: str):
- """Roll a die with the specified number of sides."""
- await tool_context.save_artifact('query', types.Part(text=query))
+ """Saves the provided query string as a 'text/plain' artifact named 'query'."""
+ query_bytes = query.encode('utf-8')
+ artifact_part = types.Part(
+ inline_data=types.Blob(mime_type='text/plain', data=query_bytes)
+ )
+ await tool_context.save_artifact('query', artifact_part)
root_agent = Agent(
diff --git a/contributing/samples/bigquery/agent.py b/contributing/samples/bigquery/agent.py
index 81a0a18bdd..39663e063b 100644
--- a/contributing/samples/bigquery/agent.py
+++ b/contributing/samples/bigquery/agent.py
@@ -19,7 +19,6 @@
from google.adk.tools.bigquery import BigQueryToolset
import google.auth
-
RUN_WITH_ADC = False
@@ -36,7 +35,6 @@
credentials_config = BigQueryCredentialsConfig(
client_id=os.getenv("OAUTH_CLIENT_ID"),
client_secret=os.getenv("OAUTH_CLIENT_SECRET"),
- scopes=["https://www.googleapis.com/auth/bigquery"],
)
bigquery_toolset = BigQueryToolset(credentials_config=credentials_config)
diff --git a/contributing/samples/bigquery_agent/README.md b/contributing/samples/bigquery_agent/README.md
index 81a4c2bf7f..c7dc7fd8bf 100644
--- a/contributing/samples/bigquery_agent/README.md
+++ b/contributing/samples/bigquery_agent/README.md
@@ -1,8 +1,8 @@
-# OAuth Sample
+# BigQuery Sample
## Introduction
-This sample tests and demos the OAuth support in ADK via two tools:
+This sample tests and demos the BigQuery support in ADK via two tools:
* 1. bigquery_datasets_list:
@@ -28,14 +28,14 @@ This sample tests and demos the OAuth support in ADK via two tools:
* 1. Follow https://developers.google.com/identity/protocols/oauth2#1.-obtain-oauth-2.0-credentials-from-the-dynamic_data.setvar.console_name. to get your client id and client secret.
Be sure to choose "web" as your client type.
-* 2. Configure your .env file to add two variables:
+* 2. Configure your `.env` file to add two variables:
- * GOOGLE_CLIENT_ID={your client id}
- * GOOGLE_CLIENT_SECRET={your client secret}
+ * OAUTH_CLIENT_ID={your client id}
+ * OAUTH_CLIENT_SECRET={your client secret}
- Note: done't create a separate .env , instead put it to the same .env file that stores your Vertex AI or Dev ML credentials
+ Note: don't create a separate `.env` file , instead put it to the same `.env` file that stores your Vertex AI or Dev ML credentials
-* 3. Follow https://developers.google.com/identity/protocols/oauth2/web-server#creatingcred to add http://localhost/dev-ui to "Authorized redirect URIs".
+* 3. Follow https://developers.google.com/identity/protocols/oauth2/web-server#creatingcred to add http://localhost/dev-ui/ to "Authorized redirect URIs".
Note: localhost here is just a hostname that you use to access the dev ui, replace it with the actual hostname you use to access the dev ui.
diff --git a/contributing/samples/generate_image/agent.py b/contributing/samples/generate_image/agent.py
index d501fb2ab8..1d0fa6b1bd 100644
--- a/contributing/samples/generate_image/agent.py
+++ b/contributing/samples/generate_image/agent.py
@@ -12,12 +12,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-from google.genai import Client
-from google.genai import types
-
from google.adk import Agent
from google.adk.tools import load_artifacts
from google.adk.tools import ToolContext
+from google.genai import Client
+from google.genai import types
# Only Vertex AI supports image generation for now.
client = Client()
diff --git a/contributing/samples/google_search_agent/agent.py b/contributing/samples/google_search_agent/agent.py
index 9ec1c88cf1..4056f1ef59 100644
--- a/contributing/samples/google_search_agent/agent.py
+++ b/contributing/samples/google_search_agent/agent.py
@@ -12,10 +12,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-from google.genai import Client
-
from google.adk import Agent
from google.adk.tools import google_search
+from google.genai import Client
# Only Vertex AI supports image generation for now.
client = Client()
diff --git a/contributing/samples/hello_world/agent.py b/contributing/samples/hello_world/agent.py
index b7b8ce1a9f..36d2ef0732 100755
--- a/contributing/samples/hello_world/agent.py
+++ b/contributing/samples/hello_world/agent.py
@@ -65,9 +65,10 @@ async def check_prime(nums: list[int]) -> str:
else f"{', '.join(str(num) for num in primes)} are prime numbers."
)
+
root_agent = Agent(
model='gemini-2.0-flash',
- name='data_processing_agent',
+ name='hello_world_agent',
description=(
'hello world agent that can roll a dice of 8 sides and check prime'
' numbers.'
diff --git a/contributing/samples/hello_world_anthropic/__init__.py b/contributing/samples/hello_world_anthropic/__init__.py
new file mode 100644
index 0000000000..7d5bb0b1c6
--- /dev/null
+++ b/contributing/samples/hello_world_anthropic/__init__.py
@@ -0,0 +1,16 @@
+# Copyright 2025 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
diff --git a/contributing/samples/hello_world_anthropic/agent.py b/contributing/samples/hello_world_anthropic/agent.py
new file mode 100644
index 0000000000..bafe7fa1b6
--- /dev/null
+++ b/contributing/samples/hello_world_anthropic/agent.py
@@ -0,0 +1,90 @@
+# Copyright 2025 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.
+
+
+import random
+
+from google.adk import Agent
+from google.adk.models.anthropic_llm import Claude
+
+
+def roll_die(sides: int) -> int:
+ """Roll a die and return the rolled result.
+
+ Args:
+ sides: The integer number of sides the die has.
+
+ Returns:
+ An integer of the result of rolling the die.
+ """
+ return random.randint(1, sides)
+
+
+async def check_prime(nums: list[int]) -> str:
+ """Check if a given list of numbers are prime.
+
+ Args:
+ nums: The list of numbers to check.
+
+ Returns:
+ A str indicating which number is prime.
+ """
+ primes = set()
+ for number in nums:
+ number = int(number)
+ if number <= 1:
+ continue
+ is_prime = True
+ for i in range(2, int(number**0.5) + 1):
+ if number % i == 0:
+ is_prime = False
+ break
+ if is_prime:
+ primes.add(number)
+ return (
+ "No prime numbers found."
+ if not primes
+ else f"{', '.join(str(num) for num in primes)} are prime numbers."
+ )
+
+
+root_agent = Agent(
+ model=Claude(model="claude-3-5-sonnet-v2@20241022"),
+ name="hello_world_agent",
+ description=(
+ "hello world agent that can roll a dice of 8 sides and check prime"
+ " numbers."
+ ),
+ instruction="""
+ You roll dice and answer questions about the outcome of the dice rolls.
+ You can roll dice of different sizes.
+ You can use multiple tools in parallel by calling functions in parallel(in one request and in one round).
+ It is ok to discuss previous dice roles, and comment on the dice rolls.
+ When you are asked to roll a die, you must call the roll_die tool with the number of sides. Be sure to pass in an integer. Do not pass in a string.
+ You should never roll a die on your own.
+ When checking prime numbers, call the check_prime tool with a list of integers. Be sure to pass in a list of integers. You should never pass in a string.
+ You should not check prime numbers before calling the tool.
+ When you are asked to roll a die and check prime numbers, you should always make the following two function calls:
+ 1. You should first call the roll_die tool to get a roll. Wait for the function response before calling the check_prime tool.
+ 2. After you get the function response from roll_die tool, you should call the check_prime tool with the roll_die result.
+ 2.1 If user asks you to check primes based on previous rolls, make sure you include the previous rolls in the list.
+ 3. When you respond, you must include the roll_die result from step 1.
+ You should always perform the previous 3 steps when asking for a roll and checking prime numbers.
+ You should not rely on the previous history on prime results.
+ """,
+ tools=[
+ roll_die,
+ check_prime,
+ ],
+)
diff --git a/contributing/samples/hello_world_anthropic/main.py b/contributing/samples/hello_world_anthropic/main.py
new file mode 100644
index 0000000000..923ec22a1d
--- /dev/null
+++ b/contributing/samples/hello_world_anthropic/main.py
@@ -0,0 +1,76 @@
+# Copyright 2025 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.
+
+
+import asyncio
+import time
+
+import agent
+from dotenv import load_dotenv
+from google.adk import Runner
+from google.adk.artifacts import InMemoryArtifactService
+from google.adk.cli.utils import logs
+from google.adk.sessions import InMemorySessionService
+from google.adk.sessions import Session
+from google.genai import types
+
+load_dotenv(override=True)
+logs.log_to_tmp_folder()
+
+
+async def main():
+ app_name = 'my_app'
+ user_id_1 = 'user1'
+ session_service = InMemorySessionService()
+ artifact_service = InMemoryArtifactService()
+ runner = Runner(
+ app_name=app_name,
+ agent=agent.root_agent,
+ artifact_service=artifact_service,
+ session_service=session_service,
+ )
+ session_11 = await session_service.create_session(
+ app_name=app_name, user_id=user_id_1
+ )
+
+ async def run_prompt(session: Session, new_message: str):
+ content = types.Content(
+ role='user', parts=[types.Part.from_text(text=new_message)]
+ )
+ print('** User says:', content.model_dump(exclude_none=True))
+ async for event in runner.run_async(
+ user_id=user_id_1,
+ session_id=session.id,
+ new_message=content,
+ ):
+ if event.content.parts and event.content.parts[0].text:
+ print(f'** {event.author}: {event.content.parts[0].text}')
+
+ start_time = time.time()
+ print('Start time:', start_time)
+ print('------------------------------------')
+ await run_prompt(session_11, 'Hi, introduce yourself.')
+ await run_prompt(
+ session_11,
+ 'Run the following request 10 times: roll a die with 100 sides and check'
+ ' if it is prime',
+ )
+ end_time = time.time()
+ print('------------------------------------')
+ print('End time:', end_time)
+ print('Total time:', end_time - start_time)
+
+
+if __name__ == '__main__':
+ asyncio.run(main())
diff --git a/contributing/samples/human_in_loop/main.py b/contributing/samples/human_in_loop/main.py
index 6beb9c8b34..f3f542fa38 100644
--- a/contributing/samples/human_in_loop/main.py
+++ b/contributing/samples/human_in_loop/main.py
@@ -13,25 +13,23 @@
# limitations under the License.
import asyncio
+import os
+from typing import Any
+from typing import Union
import agent
from dotenv import load_dotenv
-from typing import Any
-from typing import Union
from google.adk.agents import Agent
from google.adk.events import Event
from google.adk.runners import Runner
-from google.adk.tools import LongRunningFunctionTool
from google.adk.sessions import InMemorySessionService
+from google.adk.tools import LongRunningFunctionTool
from google.genai import types
-
-import os
from opentelemetry import trace
from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter
from opentelemetry.sdk.trace import export
from opentelemetry.sdk.trace import TracerProvider
-
load_dotenv(override=True)
APP_NAME = "human_in_the_loop"
diff --git a/contributing/samples/jira_agent/README.md b/contributing/samples/jira_agent/README.md
new file mode 100644
index 0000000000..23bd8b3b5a
--- /dev/null
+++ b/contributing/samples/jira_agent/README.md
@@ -0,0 +1,25 @@
+This agent connects to the Jira Cloud using Google Application Integration workflow and Integrations Connector
+
+**Instructions to connect to an agent:**
+
+**Use Integration Connectors**
+
+Connect your agent to enterprise applications using [Integration Connectors](https://cloud.google.com/integration-connectors/docs/overview).
+
+**Steps:**
+
+1. To use a connector from Integration Connectors, you need to [provision](https://console.cloud.google.com/) Application Integration in the same region as your connection by clicking on "QUICK SETUP" button.
+Google Cloud Tools
+
+
+2. Go to [Connection Tool]((https://console.cloud.google.com/)) template from the template library and click on "USE TEMPLATE" button.
+
+
+3. Fill the Integration Name as **ExecuteConnection** (It is mandatory to use this integration name only) and select the region same as the connection region. Click on "CREATE".
+
+4. Publish the integration by using the "PUBLISH" button on the Application Integration Editor.
+
+
+**References:**
+
+https://google.github.io/adk-docs/tools/google-cloud-tools/#application-integration-tools
diff --git a/contributing/samples/jira_agent/__init__.py b/contributing/samples/jira_agent/__init__.py
new file mode 100644
index 0000000000..c48963cdc7
--- /dev/null
+++ b/contributing/samples/jira_agent/__init__.py
@@ -0,0 +1,15 @@
+# Copyright 2025 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
diff --git a/contributing/samples/jira_agent/agent.py b/contributing/samples/jira_agent/agent.py
new file mode 100644
index 0000000000..12dc266313
--- /dev/null
+++ b/contributing/samples/jira_agent/agent.py
@@ -0,0 +1,53 @@
+# Copyright 2025 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 google.adk.agents import Agent
+
+from .tools import jira_tool
+
+root_agent = Agent(
+ model='gemini-2.0-flash-001',
+ name='jira_connector_agent',
+ description='This agent helps search issues in JIRA',
+ instruction="""
+ To start with, greet the user
+ First, you will be given a description of what you can do.
+ You the jira agent, who can help the user by fetching the jira issues based on the user query inputs
+
+ If an User wants to display all issues, then output only Key, Description, Summary, Status fields in a **clear table format** with key information. Example given below. Separate each line.
+ Example: {"key": "PROJ-123", "description": "This is a description", "summary": "This is a summary", "status": "In Progress"}
+
+ If an User wants to fetch on one specific key then use the LIST operation to fetch all Jira issues. Then filter locally to display only filtered result as per User given key input.
+ - **User query:** "give me the details of SMP-2"
+ - Output only Key, Description, Summary, Status fields in a **clear table format** with key information.
+ - **Output:** {"key": "PROJ-123", "description": "This is a description", "summary": "This is a summary", "status": "In Progress"}
+
+ Example scenarios:
+ - **User query:** "Can you show me all Jira issues with status `Done`?"
+ - **Output:** {"key": "PROJ-123", "description": "This is a description", "summary": "This is a summary", "status": "In Progress"}
+
+ - **User query:** "can you give details of SMP-2?"
+ - **Output:** {"key": "PROJ-123", "description": "This is a description", "summary": "This is a summary", "status": "In Progress"}
+
+ - **User query:** "Show issues with summary containing 'World'"
+ - **Output:** {"key": "PROJ-123", "description": "This is a description", "summary": "World", "status": "In Progress"}
+
+ - **User query:** "Show issues with description containing 'This is example task 3'"
+ - **Output:** {"key": "PROJ-123", "description": "This is example task 3", "summary": "World", "status": "In Progress"}
+
+ **Important Notes:**
+ - I currently support only **GET** and **LIST** operations.
+ """,
+ tools=jira_tool.get_tools(),
+)
diff --git a/contributing/samples/jira_agent/image-app-intg-editor.png b/contributing/samples/jira_agent/image-app-intg-editor.png
new file mode 100644
index 0000000000..87f34951ef
Binary files /dev/null and b/contributing/samples/jira_agent/image-app-intg-editor.png differ
diff --git a/contributing/samples/jira_agent/image-application-integration.png b/contributing/samples/jira_agent/image-application-integration.png
new file mode 100644
index 0000000000..c92131498c
Binary files /dev/null and b/contributing/samples/jira_agent/image-application-integration.png differ
diff --git a/contributing/samples/jira_agent/image-connection-tool.png b/contributing/samples/jira_agent/image-connection-tool.png
new file mode 100644
index 0000000000..8a844e39b8
Binary files /dev/null and b/contributing/samples/jira_agent/image-connection-tool.png differ
diff --git a/contributing/samples/jira_agent/tools.py b/contributing/samples/jira_agent/tools.py
new file mode 100644
index 0000000000..f03c5ed106
--- /dev/null
+++ b/contributing/samples/jira_agent/tools.py
@@ -0,0 +1,33 @@
+# Copyright 2025 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 google.adk.tools.application_integration_tool.application_integration_toolset import ApplicationIntegrationToolset
+
+jira_tool = ApplicationIntegrationToolset(
+ project="your-gcp-project-id", # replace with your GCP project ID
+ location="your-regions", # replace your regions
+ connection="your-integration-connection-name", # replace with your connection name
+ entity_operations={
+ "Issues": ["GET", "LIST"],
+ },
+ actions=[
+ "get_issue_by_key",
+ ],
+ tool_name="jira_conversation_tool",
+ tool_instructions="""
+
+ This tool is to call an integration to search for issues in JIRA
+
+ """,
+)
diff --git a/contributing/samples/langchain_structured_tool_agent/__init__.py b/contributing/samples/langchain_structured_tool_agent/__init__.py
new file mode 100644
index 0000000000..c48963cdc7
--- /dev/null
+++ b/contributing/samples/langchain_structured_tool_agent/__init__.py
@@ -0,0 +1,15 @@
+# Copyright 2025 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
diff --git a/contributing/samples/langchain_structured_tool_agent/agent.py b/contributing/samples/langchain_structured_tool_agent/agent.py
new file mode 100644
index 0000000000..e9e3d232aa
--- /dev/null
+++ b/contributing/samples/langchain_structured_tool_agent/agent.py
@@ -0,0 +1,49 @@
+# Copyright 2025 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.
+
+"""
+This agent aims to test the Langchain tool with Langchain's StructuredTool
+"""
+from google.adk.agents import Agent
+from google.adk.tools.langchain_tool import LangchainTool
+from langchain_core.tools.structured import StructuredTool
+from pydantic import BaseModel
+
+
+def add(x, y) -> int:
+ return x + y
+
+
+class AddSchema(BaseModel):
+ x: int
+ y: int
+
+
+test_langchain_tool = StructuredTool.from_function(
+ add,
+ name="add",
+ description="Adds two numbers",
+ args_schema=AddSchema,
+)
+
+root_agent = Agent(
+ model="gemini-2.0-flash-001",
+ name="test_app",
+ description="A helpful assistant for user questions.",
+ instruction=(
+ "You are a helpful assistant for user questions, you have access to a"
+ " tool that adds two numbers."
+ ),
+ tools=[LangchainTool(tool=test_langchain_tool)],
+)
diff --git a/contributing/samples/langchain_youtube_search_agent/README.md b/contributing/samples/langchain_youtube_search_agent/README.md
new file mode 100644
index 0000000000..e87ca59420
--- /dev/null
+++ b/contributing/samples/langchain_youtube_search_agent/README.md
@@ -0,0 +1,8 @@
+# Langchain Youtube Search Agent
+
+This agent utilize the Lanchain YoutubeSearchTool to search youtubes.
+You need to install below dependencies:
+
+```python
+uv pip install youtube_search
+```
diff --git a/contributing/samples/langchain_youtube_search_agent/__init__.py b/contributing/samples/langchain_youtube_search_agent/__init__.py
new file mode 100644
index 0000000000..c48963cdc7
--- /dev/null
+++ b/contributing/samples/langchain_youtube_search_agent/__init__.py
@@ -0,0 +1,15 @@
+# Copyright 2025 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
diff --git a/contributing/samples/langchain_youtube_search_agent/agent.py b/contributing/samples/langchain_youtube_search_agent/agent.py
new file mode 100644
index 0000000000..70d7b1e9d3
--- /dev/null
+++ b/contributing/samples/langchain_youtube_search_agent/agent.py
@@ -0,0 +1,36 @@
+# Copyright 2025 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 google.adk.agents import LlmAgent
+from google.adk.tools.langchain_tool import LangchainTool
+from langchain_community.tools import YouTubeSearchTool
+
+# Instantiate the tool
+langchain_yt_tool = YouTubeSearchTool()
+
+# Wrap the tool in the LangchainTool class from ADK
+adk_yt_tool = LangchainTool(
+ tool=langchain_yt_tool,
+)
+
+root_agent = LlmAgent(
+ name="youtube_search_agent",
+ model="gemini-2.0-flash", # Replace with the actual model name
+ instruction="""
+ Ask customer to provide singer name, and the number of videos to search.
+ """,
+ description="Help customer to search for a video on Youtube.",
+ tools=[adk_yt_tool],
+ output_key="youtube_search_output",
+)
diff --git a/contributing/samples/langchain_youtube_search_agent/requirements.txt b/contributing/samples/langchain_youtube_search_agent/requirements.txt
new file mode 100644
index 0000000000..31eedf6f7c
--- /dev/null
+++ b/contributing/samples/langchain_youtube_search_agent/requirements.txt
@@ -0,0 +1 @@
+youtube_search
diff --git a/contributing/samples/mcp_sse_agent/filesystem_server.py b/contributing/samples/mcp_sse_agent/filesystem_server.py
index 9a04f9755f..cda4f0a968 100644
--- a/contributing/samples/mcp_sse_agent/filesystem_server.py
+++ b/contributing/samples/mcp_sse_agent/filesystem_server.py
@@ -16,6 +16,7 @@
import os
from pathlib import Path
import sys
+
from mcp.server.fastmcp import FastMCP
# Create an MCP server with a name
diff --git a/contributing/samples/mcp_stdio_notion_agent/README.md b/contributing/samples/mcp_stdio_notion_agent/README.md
new file mode 100644
index 0000000000..f53bd2f03f
--- /dev/null
+++ b/contributing/samples/mcp_stdio_notion_agent/README.md
@@ -0,0 +1,20 @@
+# Notion MCP Agent
+
+This is an agent that is using Notion MCP tool to call Notion API. And it demonstrate how to pass in the Notion API key.
+
+Follow below instruction to use it:
+
+* Follow the installation instruction in below page to get an API key for Notion API:
+https://www.npmjs.com/package/@notionhq/notion-mcp-server
+
+* Set the environment variable `NOTION_API_KEY` to the API key you obtained in the previous step.
+
+```bash
+export NOTION_API_KEY=
+```
+
+* Run the agent in ADK Web UI
+
+* Send below queries:
+ * What can you do for me ?
+ * Seach `XXXX` in my pages.
diff --git a/contributing/samples/mcp_stdio_notion_agent/__init__.py b/contributing/samples/mcp_stdio_notion_agent/__init__.py
new file mode 100755
index 0000000000..c48963cdc7
--- /dev/null
+++ b/contributing/samples/mcp_stdio_notion_agent/__init__.py
@@ -0,0 +1,15 @@
+# Copyright 2025 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
diff --git a/contributing/samples/mcp_stdio_notion_agent/agent.py b/contributing/samples/mcp_stdio_notion_agent/agent.py
new file mode 100644
index 0000000000..bfb385a1bc
--- /dev/null
+++ b/contributing/samples/mcp_stdio_notion_agent/agent.py
@@ -0,0 +1,48 @@
+# Copyright 2025 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.
+
+import json
+import os
+
+from dotenv import load_dotenv
+from google.adk.agents.llm_agent import LlmAgent
+from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset
+from google.adk.tools.mcp_tool.mcp_toolset import StdioServerParameters
+
+load_dotenv()
+
+NOTION_API_KEY = os.getenv("NOTION_API_KEY")
+NOTION_HEADERS = json.dumps({
+ "Authorization": f"Bearer {NOTION_API_KEY}",
+ "Notion-Version": "2022-06-28",
+})
+
+root_agent = LlmAgent(
+ model="gemini-2.0-flash",
+ name="notion_agent",
+ instruction=(
+ "You are my workspace assistant. "
+ "Use the provided tools to read, search, comment on, "
+ "or create Notion pages. Ask clarifying questions when unsure."
+ ),
+ tools=[
+ MCPToolset(
+ connection_params=StdioServerParameters(
+ command="npx",
+ args=["-y", "@notionhq/notion-mcp-server"],
+ env={"OPENAPI_MCP_HEADERS": NOTION_HEADERS},
+ )
+ )
+ ],
+)
diff --git a/contributing/samples/mcp_stdio_server_agent/agent.py b/contributing/samples/mcp_stdio_server_agent/agent.py
index a14ab43955..fe8b75c218 100755
--- a/contributing/samples/mcp_stdio_server_agent/agent.py
+++ b/contributing/samples/mcp_stdio_server_agent/agent.py
@@ -16,8 +16,9 @@
import os
from google.adk.agents.llm_agent import LlmAgent
+from google.adk.tools.mcp_tool import StdioConnectionParams
from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset
-from google.adk.tools.mcp_tool.mcp_toolset import StdioServerParameters
+from mcp import StdioServerParameters
_allowed_path = os.path.dirname(os.path.abspath(__file__))
@@ -31,13 +32,16 @@
""",
tools=[
MCPToolset(
- connection_params=StdioServerParameters(
- command='npx',
- args=[
- '-y', # Arguments for the command
- '@modelcontextprotocol/server-filesystem',
- _allowed_path,
- ],
+ connection_params=StdioConnectionParams(
+ server_params=StdioServerParameters(
+ command='npx',
+ args=[
+ '-y', # Arguments for the command
+ '@modelcontextprotocol/server-filesystem',
+ _allowed_path,
+ ],
+ ),
+ timeout=5,
),
# don't want agent to do write operation
# you can also do below
diff --git a/contributing/samples/mcp_streamablehttp_agent/filesystem_server.py b/contributing/samples/mcp_streamablehttp_agent/filesystem_server.py
index 51f8482cfc..9e822f232b 100644
--- a/contributing/samples/mcp_streamablehttp_agent/filesystem_server.py
+++ b/contributing/samples/mcp_streamablehttp_agent/filesystem_server.py
@@ -16,6 +16,7 @@
import os
from pathlib import Path
import sys
+
from mcp.server.fastmcp import FastMCP
# Create an MCP server with a name
diff --git a/contributing/samples/oauth_calendar_agent/README.md b/contributing/samples/oauth_calendar_agent/README.md
index e914fbd434..aaefd6d08b 100644
--- a/contributing/samples/oauth_calendar_agent/README.md
+++ b/contributing/samples/oauth_calendar_agent/README.md
@@ -21,14 +21,14 @@ This sample tests and demos the OAuth support in ADK via two tools:
* 1. Follow https://developers.google.com/identity/protocols/oauth2#1.-obtain-oauth-2.0-credentials-from-the-dynamic_data.setvar.console_name. to get your client id and client secret.
Be sure to choose "web" as your client type.
-* 2. Configure your .env file to add two variables:
+* 2. Configure your `.env` file to add two variables:
- * GOOGLE_CLIENT_ID={your client id}
- * GOOGLE_CLIENT_SECRET={your client secret}
+ * OAUTH_CLIENT_ID={your client id}
+ * OAUTH_CLIENT_SECRET={your client secret}
- Note: done't create a separate .env , instead put it to the same .env file that stores your Vertex AI or Dev ML credentials
+ Note: don't create a separate `.env` file , instead put it to the same `.env` file that stores your Vertex AI or Dev ML credentials
-* 3. Follow https://developers.google.com/identity/protocols/oauth2/web-server#creatingcred to add http://localhost/dev-ui to "Authorized redirect URIs".
+* 3. Follow https://developers.google.com/identity/protocols/oauth2/web-server#creatingcred to add http://localhost/dev-ui/ to "Authorized redirect URIs".
Note: localhost here is just a hostname that you use to access the dev ui, replace it with the actual hostname you use to access the dev ui.
diff --git a/contributing/samples/session_state_agent/README.md b/contributing/samples/session_state_agent/README.md
new file mode 100644
index 0000000000..bec0536487
--- /dev/null
+++ b/contributing/samples/session_state_agent/README.md
@@ -0,0 +1,66 @@
+# Sample Agent to demo session state persistence.
+
+## Lifecycle of session state
+
+After assigning a state using the context object (e.g.
+`tool_context.state['log_query_var'] = 'log_query_var_value'`):
+
+* The state is available for use in a later callback.
+* Once the resulting event is processed by the runner and appneded in the
+ session, the state will be also persisted in the session.
+
+This sample agent is for demonstrating the aforementioned behavior.
+
+## Run the agent
+
+Run below command:
+
+```bash
+$ adk run contributing/samples/session_state_agent --replay contributing/samples/session_state_agent/input.json
+```
+
+And you should see below output:
+
+```bash
+[user]: hello world!
+===================== In before_agent_callback ==============================
+** Asserting keys are cached in context: ['before_agent_callback_state_key'] pass ✅
+** Asserting keys are already persisted in session: [] pass ✅
+** Asserting keys are not persisted in session yet: ['before_agent_callback_state_key'] pass ✅
+============================================================
+===================== In before_model_callback ==============================
+** Asserting keys are cached in context: ['before_agent_callback_state_key', 'before_model_callback_state_key'] pass ✅
+** Asserting keys are already persisted in session: ['before_agent_callback_state_key'] pass ✅
+** Asserting keys are not persisted in session yet: ['before_model_callback_state_key'] pass ✅
+============================================================
+===================== In after_model_callback ==============================
+** Asserting keys are cached in context: ['before_agent_callback_state_key', 'before_model_callback_state_key', 'after_model_callback_state_key'] pass ✅
+** Asserting keys are already persisted in session: ['before_agent_callback_state_key'] pass ✅
+** Asserting keys are not persisted in session yet: ['before_model_callback_state_key', 'after_model_callback_state_key'] pass ✅
+============================================================
+[root_agent]: Hello! How can I help you verify something today?
+
+===================== In after_agent_callback ==============================
+** Asserting keys are cached in context: ['before_agent_callback_state_key', 'before_model_callback_state_key', 'after_model_callback_state_key', 'after_agent_callback_state_key'] pass ✅
+** Asserting keys are already persisted in session: ['before_agent_callback_state_key', 'before_model_callback_state_key', 'after_model_callback_state_key'] pass ✅
+** Asserting keys are not persisted in session yet: ['after_agent_callback_state_key'] pass ✅
+============================================================
+```
+
+## Detailed Explanation
+
+As rule of thumb, to read and write session state, user should assume the
+state is available after writing via the context object
+(`tool_context`, `callback_context` or `readonly_context`).
+
+### Current Behavior
+
+The current behavior of pesisting states are:
+
+* for `before_agent_callback`: state delta will be persisted after all callbacks are processed.
+* for `before_model_callback`: state delta will be persisted with the final LlmResponse,
+ aka. after `after_model_callback` is processed.
+* for `after_model_callback`: state delta will be persisted together with the event of LlmResponse.
+* for `after_agent_callback`: state delta will be persisted after all callbacks are processed.
+
+**NOTE**: the current behavior is considered implementation detail and may be changed later. **DO NOT** rely on it.
diff --git a/contributing/samples/session_state_agent/__init__.py b/contributing/samples/session_state_agent/__init__.py
new file mode 100644
index 0000000000..02c597e11e
--- /dev/null
+++ b/contributing/samples/session_state_agent/__init__.py
@@ -0,0 +1 @@
+from . import agent
diff --git a/contributing/samples/session_state_agent/agent.py b/contributing/samples/session_state_agent/agent.py
new file mode 100644
index 0000000000..a4ed704e96
--- /dev/null
+++ b/contributing/samples/session_state_agent/agent.py
@@ -0,0 +1,180 @@
+# Copyright 2025 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.
+
+"""The agent to demo the session state lifecycle.
+
+This agent illustrate how session state will be cached in context and persisted
+in session state.
+"""
+
+
+import logging
+from typing import Optional
+
+from google.adk.agents.callback_context import CallbackContext
+from google.adk.agents.llm_agent import Agent
+from google.adk.models.llm_request import LlmRequest
+from google.adk.models.llm_response import LlmResponse
+from google.genai import types
+
+logger = logging.getLogger('google_adk.' + __name__)
+
+
+async def assert_session_values(
+ ctx: CallbackContext,
+ title: str,
+ *,
+ keys_in_ctx_session: Optional[list[str]] = None,
+ keys_in_service_session: Optional[list[str]] = None,
+ keys_not_in_service_session: Optional[list[str]] = None,
+):
+ session_in_ctx = ctx._invocation_context.session
+ session_in_service = (
+ await ctx._invocation_context.session_service.get_session(
+ app_name=session_in_ctx.app_name,
+ user_id=session_in_ctx.user_id,
+ session_id=session_in_ctx.id,
+ )
+ )
+ assert session_in_service is not None
+
+ print(f'===================== {title} ==============================')
+ print(
+ f'** Asserting keys are cached in context: {keys_in_ctx_session}', end=' '
+ )
+ for key in keys_in_ctx_session or []:
+ assert key in session_in_ctx.state
+ print('\033[92mpass ✅\033[0m')
+
+ print(
+ '** Asserting keys are already persisted in session:'
+ f' {keys_in_service_session}',
+ end=' ',
+ )
+ for key in keys_in_service_session or []:
+ assert key in session_in_service.state
+ print('\033[92mpass ✅\033[0m')
+
+ print(
+ '** Asserting keys are not persisted in session yet:'
+ f' {keys_not_in_service_session}',
+ end=' ',
+ )
+ for key in keys_not_in_service_session or []:
+ assert key not in session_in_service.state
+ print('\033[92mpass ✅\033[0m')
+ print('============================================================')
+
+
+async def before_agent_callback(
+ callback_context: CallbackContext,
+) -> Optional[types.Content]:
+ if 'before_agent_callback_state_key' in callback_context.state:
+ return types.ModelContent('Sorry, I can only reply once.')
+
+ callback_context.state['before_agent_callback_state_key'] = (
+ 'before_agent_callback_state_value'
+ )
+
+ await assert_session_values(
+ callback_context,
+ 'In before_agent_callback',
+ keys_in_ctx_session=['before_agent_callback_state_key'],
+ keys_in_service_session=[],
+ keys_not_in_service_session=['before_agent_callback_state_key'],
+ )
+
+
+async def before_model_callback(
+ callback_context: CallbackContext, llm_request: LlmRequest
+):
+ callback_context.state['before_model_callback_state_key'] = (
+ 'before_model_callback_state_value'
+ )
+
+ await assert_session_values(
+ callback_context,
+ 'In before_model_callback',
+ keys_in_ctx_session=[
+ 'before_agent_callback_state_key',
+ 'before_model_callback_state_key',
+ ],
+ keys_in_service_session=['before_agent_callback_state_key'],
+ keys_not_in_service_session=['before_model_callback_state_key'],
+ )
+
+
+async def after_model_callback(
+ callback_context: CallbackContext, llm_response: LlmResponse
+):
+ callback_context.state['after_model_callback_state_key'] = (
+ 'after_model_callback_state_value'
+ )
+
+ await assert_session_values(
+ callback_context,
+ 'In after_model_callback',
+ keys_in_ctx_session=[
+ 'before_agent_callback_state_key',
+ 'before_model_callback_state_key',
+ 'after_model_callback_state_key',
+ ],
+ keys_in_service_session=[
+ 'before_agent_callback_state_key',
+ ],
+ keys_not_in_service_session=[
+ 'before_model_callback_state_key',
+ 'after_model_callback_state_key',
+ ],
+ )
+
+
+async def after_agent_callback(callback_context: CallbackContext):
+ callback_context.state['after_agent_callback_state_key'] = (
+ 'after_agent_callback_state_value'
+ )
+
+ await assert_session_values(
+ callback_context,
+ 'In after_agent_callback',
+ keys_in_ctx_session=[
+ 'before_agent_callback_state_key',
+ 'before_model_callback_state_key',
+ 'after_model_callback_state_key',
+ 'after_agent_callback_state_key',
+ ],
+ keys_in_service_session=[
+ 'before_agent_callback_state_key',
+ 'before_model_callback_state_key',
+ 'after_model_callback_state_key',
+ ],
+ keys_not_in_service_session=[
+ 'after_agent_callback_state_key',
+ ],
+ )
+
+
+root_agent = Agent(
+ name='root_agent',
+ description='a verification agent.',
+ instruction=(
+ 'Log all users query with `log_query` tool. Must always remind user you'
+ ' cannot answer second query because your setup.'
+ ),
+ model='gemini-2.0-flash-001',
+ before_agent_callback=before_agent_callback,
+ before_model_callback=before_model_callback,
+ after_model_callback=after_model_callback,
+ after_agent_callback=after_agent_callback,
+)
diff --git a/contributing/samples/session_state_agent/input.json b/contributing/samples/session_state_agent/input.json
new file mode 100644
index 0000000000..6f76f166b1
--- /dev/null
+++ b/contributing/samples/session_state_agent/input.json
@@ -0,0 +1,4 @@
+{
+ "state": {},
+ "queries": ["hello world!"]
+}
diff --git a/contributing/samples/simple_sequential_agent/agent.py b/contributing/samples/simple_sequential_agent/agent.py
index afd7ebea9a..9ec0b35a95 100644
--- a/contributing/samples/simple_sequential_agent/agent.py
+++ b/contributing/samples/simple_sequential_agent/agent.py
@@ -88,7 +88,7 @@ def check_prime(nums: list[int]) -> str:
)
root_agent = SequentialAgent(
- name="code_pipeline_agent",
+ name="simple_sequential_agent",
sub_agents=[roll_agent, prime_agent],
# The agents will run in the order provided: roll_agent -> prime_agent
)
diff --git a/contributing/samples/telemetry/agent.py b/contributing/samples/telemetry/agent.py
new file mode 100755
index 0000000000..62497300d6
--- /dev/null
+++ b/contributing/samples/telemetry/agent.py
@@ -0,0 +1,110 @@
+# Copyright 2025 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.
+
+import random
+
+from google.adk import Agent
+from google.adk.planners import BuiltInPlanner
+from google.adk.planners import PlanReActPlanner
+from google.adk.tools.tool_context import ToolContext
+from google.genai import types
+
+
+def roll_die(sides: int, tool_context: ToolContext) -> int:
+ """Roll a die and return the rolled result.
+
+ Args:
+ sides: The integer number of sides the die has.
+
+ Returns:
+ An integer of the result of rolling the die.
+ """
+ result = random.randint(1, sides)
+ if not 'rolls' in tool_context.state:
+ tool_context.state['rolls'] = []
+
+ tool_context.state['rolls'] = tool_context.state['rolls'] + [result]
+ return result
+
+
+async def check_prime(nums: list[int]) -> str:
+ """Check if a given list of numbers are prime.
+
+ Args:
+ nums: The list of numbers to check.
+
+ Returns:
+ A str indicating which number is prime.
+ """
+ primes = set()
+ for number in nums:
+ number = int(number)
+ if number <= 1:
+ continue
+ is_prime = True
+ for i in range(2, int(number**0.5) + 1):
+ if number % i == 0:
+ is_prime = False
+ break
+ if is_prime:
+ primes.add(number)
+ return (
+ 'No prime numbers found.'
+ if not primes
+ else f"{', '.join(str(num) for num in primes)} are prime numbers."
+ )
+
+
+root_agent = Agent(
+ model='gemini-2.0-flash',
+ name='data_processing_agent',
+ description=(
+ 'hello world agent that can roll a dice of 8 sides and check prime'
+ ' numbers.'
+ ),
+ instruction="""
+ You roll dice and answer questions about the outcome of the dice rolls.
+ You can roll dice of different sizes.
+ You can use multiple tools in parallel by calling functions in parallel(in one request and in one round).
+ It is ok to discuss previous dice roles, and comment on the dice rolls.
+ When you are asked to roll a die, you must call the roll_die tool with the number of sides. Be sure to pass in an integer. Do not pass in a string.
+ You should never roll a die on your own.
+ When checking prime numbers, call the check_prime tool with a list of integers. Be sure to pass in a list of integers. You should never pass in a string.
+ You should not check prime numbers before calling the tool.
+ When you are asked to roll a die and check prime numbers, you should always make the following two function calls:
+ 1. You should first call the roll_die tool to get a roll. Wait for the function response before calling the check_prime tool.
+ 2. After you get the function response from roll_die tool, you should call the check_prime tool with the roll_die result.
+ 2.1 If user asks you to check primes based on previous rolls, make sure you include the previous rolls in the list.
+ 3. When you respond, you must include the roll_die result from step 1.
+ You should always perform the previous 3 steps when asking for a roll and checking prime numbers.
+ You should not rely on the previous history on prime results.
+ """,
+ tools=[
+ roll_die,
+ check_prime,
+ ],
+ # planner=BuiltInPlanner(
+ # thinking_config=types.ThinkingConfig(
+ # include_thoughts=True,
+ # ),
+ # ),
+ generate_content_config=types.GenerateContentConfig(
+ safety_settings=[
+ types.SafetySetting( # avoid false alarm about rolling dice.
+ category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
+ threshold=types.HarmBlockThreshold.OFF,
+ ),
+ ]
+ ),
+)
diff --git a/contributing/samples/telemetry/main.py b/contributing/samples/telemetry/main.py
new file mode 100755
index 0000000000..de08c82dc2
--- /dev/null
+++ b/contributing/samples/telemetry/main.py
@@ -0,0 +1,111 @@
+# Copyright 2025 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.
+
+import asyncio
+import os
+import time
+
+import agent
+from dotenv import load_dotenv
+from google.adk.agents.run_config import RunConfig
+from google.adk.runners import InMemoryRunner
+from google.adk.sessions import Session
+from google.genai import types
+from opentelemetry import trace
+from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter
+from opentelemetry.sdk.trace import export
+from opentelemetry.sdk.trace import TracerProvider
+
+load_dotenv(override=True)
+
+
+async def main():
+ app_name = 'my_app'
+ user_id_1 = 'user1'
+ runner = InMemoryRunner(
+ agent=agent.root_agent,
+ app_name=app_name,
+ )
+ session_11 = await runner.session_service.create_session(
+ app_name=app_name, user_id=user_id_1
+ )
+
+ async def run_prompt(session: Session, new_message: str):
+ content = types.Content(
+ role='user', parts=[types.Part.from_text(text=new_message)]
+ )
+ print('** User says:', content.model_dump(exclude_none=True))
+ async for event in runner.run_async(
+ user_id=user_id_1,
+ session_id=session.id,
+ new_message=content,
+ ):
+ if event.content.parts and event.content.parts[0].text:
+ print(f'** {event.author}: {event.content.parts[0].text}')
+
+ async def run_prompt_bytes(session: Session, new_message: str):
+ content = types.Content(
+ role='user',
+ parts=[
+ types.Part.from_bytes(
+ data=str.encode(new_message), mime_type='text/plain'
+ )
+ ],
+ )
+ print('** User says:', content.model_dump(exclude_none=True))
+ async for event in runner.run_async(
+ user_id=user_id_1,
+ session_id=session.id,
+ new_message=content,
+ run_config=RunConfig(save_input_blobs_as_artifacts=True),
+ ):
+ if event.content.parts and event.content.parts[0].text:
+ print(f'** {event.author}: {event.content.parts[0].text}')
+
+ start_time = time.time()
+ print('Start time:', start_time)
+ print('------------------------------------')
+ await run_prompt(session_11, 'Hi')
+ await run_prompt(session_11, 'Roll a die with 100 sides')
+ await run_prompt(session_11, 'Roll a die again with 100 sides.')
+ await run_prompt(session_11, 'What numbers did I got?')
+ await run_prompt_bytes(session_11, 'Hi bytes')
+ print(
+ await runner.artifact_service.list_artifact_keys(
+ app_name=app_name, user_id=user_id_1, session_id=session_11.id
+ )
+ )
+ end_time = time.time()
+ print('------------------------------------')
+ print('End time:', end_time)
+ print('Total time:', end_time - start_time)
+
+
+if __name__ == '__main__':
+
+ provider = TracerProvider()
+ project_id = os.environ.get('GOOGLE_CLOUD_PROJECT')
+ if not project_id:
+ raise ValueError('GOOGLE_CLOUD_PROJECT environment variable is not set.')
+ print('Tracing to project', project_id)
+ processor = export.BatchSpanProcessor(
+ CloudTraceSpanExporter(project_id=project_id)
+ )
+ provider.add_span_processor(processor)
+ trace.set_tracer_provider(provider)
+
+ asyncio.run(main())
+
+ provider.force_flush()
+ print('Done tracing to project', project_id)
diff --git a/pyproject.toml b/pyproject.toml
index fd72b367ee..158025c7e9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -25,26 +25,27 @@ classifiers = [ # List of https://pypi.org/classifiers/
]
dependencies = [
# go/keep-sorted start
- "authlib>=1.5.1", # For RestAPI Tool
- "click>=8.1.8", # For CLI tools
- "fastapi>=0.115.0", # FastAPI framework
- "google-api-python-client>=2.157.0", # Google API client discovery
- "google-cloud-aiplatform>=1.87.0", # For VertexAI integrations, e.g. example store.
- "google-cloud-secret-manager>=2.22.0", # Fetching secrets in RestAPI Tool
- "google-cloud-speech>=2.30.0", # For Audio Transcription
- "google-cloud-storage>=2.18.0, <3.0.0", # For GCS Artifact service
- "google-genai>=1.14.0", # Google GenAI SDK
- "graphviz>=0.20.2", # Graphviz for graph rendering
- "mcp>=1.8.0;python_version>='3.10'", # For MCP Toolset
- "opentelemetry-api>=1.31.0", # OpenTelemetry
+ "authlib>=1.5.1", # For RestAPI Tool
+ "click>=8.1.8", # For CLI tools
+ "fastapi>=0.115.0", # FastAPI framework
+ "google-api-python-client>=2.157.0", # Google API client discovery
+ "google-cloud-aiplatform[agent_engines]>=1.95.1", # For VertexAI integrations, e.g. example store.
+ "google-cloud-secret-manager>=2.22.0", # Fetching secrets in RestAPI Tool
+ "google-cloud-speech>=2.30.0", # For Audio Transcription
+ "google-cloud-storage>=2.18.0, <3.0.0", # For GCS Artifact service
+ "google-genai>=1.17.0", # Google GenAI SDK
+ "graphviz>=0.20.2", # Graphviz for graph rendering
+ "mcp>=1.8.0;python_version>='3.10'", # For MCP Toolset
+ "opentelemetry-api>=1.31.0", # OpenTelemetry
"opentelemetry-exporter-gcp-trace>=1.9.0",
"opentelemetry-sdk>=1.31.0",
- "pydantic>=2.0, <3.0.0", # For data validation/models
- "python-dotenv>=1.0.0", # To manage environment variables
- "PyYAML>=6.0.2", # For APIHubToolset.
- "sqlalchemy>=2.0", # SQL database ORM
- "tzlocal>=5.3", # Time zone utilities
- "uvicorn>=0.34.0", # ASGI server for FastAPI
+ "pydantic>=2.0, <3.0.0", # For data validation/models
+ "python-dotenv>=1.0.0", # To manage environment variables
+ "PyYAML>=6.0.2", # For APIHubToolset.
+ "sqlalchemy>=2.0", # SQL database ORM
+ "tzlocal>=5.3", # Time zone utilities
+ "typing-extensions>=4.5, <5",
+ "uvicorn>=0.34.0", # ASGI server for FastAPI
# go/keep-sorted end
]
dynamic = ["version"]
@@ -83,7 +84,7 @@ test = [
"anthropic>=0.43.0", # For anthropic model tests
"langchain-community>=0.3.17",
"langgraph>=0.2.60", # For LangGraphAgent
- "litellm>=1.63.11", # For LiteLLM tests
+ "litellm>=1.71.2", # For LiteLLM tests
"llama-index-readers-file>=0.4.0", # For retrieval tests
"pytest-asyncio>=0.25.0",
@@ -145,6 +146,7 @@ exclude = ['src/**/*.sh']
[tool.flit.module]
name = "google.adk"
+include = ["py.typed"]
[tool.isort]
profile = "google"
diff --git a/src/google/adk/agents/base_agent.py b/src/google/adk/agents/base_agent.py
index 18a5de473d..bdc10ac3a0 100644
--- a/src/google/adk/agents/base_agent.py
+++ b/src/google/adk/agents/base_agent.py
@@ -246,8 +246,6 @@ def _create_invocation_context(
) -> InvocationContext:
"""Creates a new invocation context for this agent."""
invocation_context = parent_context.model_copy(update={'agent': self})
- if parent_context.branch:
- invocation_context.branch = f'{parent_context.branch}.{self.name}'
return invocation_context
@property
diff --git a/src/google/adk/agents/invocation_context.py b/src/google/adk/agents/invocation_context.py
index 46ec6635ea..f703715358 100644
--- a/src/google/adk/agents/invocation_context.py
+++ b/src/google/adk/agents/invocation_context.py
@@ -39,9 +39,9 @@ class LlmCallsLimitExceededError(Exception):
class _InvocationCostManager(BaseModel):
"""A container to keep track of the cost of invocation.
- While we don't expected the metrics captured here to be a direct
- representatative of monetary cost incurred in executing the current
- invocation, but they, in someways have an indirect affect.
+ While we don't expect the metrics captured here to be a direct
+ representative of monetary cost incurred in executing the current
+ invocation, they in some ways have an indirect effect.
"""
_number_of_llm_calls: int = 0
diff --git a/src/google/adk/agents/parallel_agent.py b/src/google/adk/agents/parallel_agent.py
index 61ca41bd95..427128cec5 100644
--- a/src/google/adk/agents/parallel_agent.py
+++ b/src/google/adk/agents/parallel_agent.py
@@ -26,14 +26,20 @@
from .base_agent import BaseAgent
-def _set_branch_for_current_agent(
- current_agent: BaseAgent, invocation_context: InvocationContext
-):
+def _create_branch_ctx_for_sub_agent(
+ agent: BaseAgent,
+ sub_agent: BaseAgent,
+ invocation_context: InvocationContext,
+) -> InvocationContext:
+ """Create isolated branch for every sub-agent."""
+ invocation_context = invocation_context.model_copy()
+ branch_suffix = f"{agent.name}.{sub_agent.name}"
invocation_context.branch = (
- f"{invocation_context.branch}.{current_agent.name}"
+ f"{invocation_context.branch}.{branch_suffix}"
if invocation_context.branch
- else current_agent.name
+ else branch_suffix
)
+ return invocation_context
async def _merge_agent_run(
@@ -90,8 +96,12 @@ class ParallelAgent(BaseAgent):
async def _run_async_impl(
self, ctx: InvocationContext
) -> AsyncGenerator[Event, None]:
- _set_branch_for_current_agent(self, ctx)
- agent_runs = [agent.run_async(ctx) for agent in self.sub_agents]
+ agent_runs = [
+ sub_agent.run_async(
+ _create_branch_ctx_for_sub_agent(self, sub_agent, ctx)
+ )
+ for sub_agent in self.sub_agents
+ ]
async for event in _merge_agent_run(agent_runs):
yield event
diff --git a/src/google/adk/agents/sequential_agent.py b/src/google/adk/agents/sequential_agent.py
index 91b001382b..845dd5ac11 100644
--- a/src/google/adk/agents/sequential_agent.py
+++ b/src/google/adk/agents/sequential_agent.py
@@ -27,7 +27,7 @@
class SequentialAgent(BaseAgent):
- """A shell agent that run its sub-agents in sequence."""
+ """A shell agent that runs its sub-agents in sequence."""
@override
async def _run_async_impl(
@@ -43,11 +43,11 @@ async def _run_live_impl(
) -> AsyncGenerator[Event, None]:
"""Implementation for live SequentialAgent.
- Compared to non-live case, live agents process a continous streams of audio
- or video, so it doesn't have a way to tell if it's finished and should pass
- to next agent or not. So we introduce a task_completed() function so the
+ Compared to the non-live case, live agents process a continuous stream of audio
+ or video, so there is no way to tell if it's finished and should pass
+ to the next agent or not. So we introduce a task_completed() function so the
model can call this function to signal that it's finished the task and we
- can move on to next agent.
+ can move on to the next agent.
Args:
ctx: The invocation context of the agent.
@@ -66,10 +66,10 @@ def task_completed():
# Use function name to dedupe.
if task_completed.__name__ not in sub_agent.tools:
sub_agent.tools.append(task_completed)
- sub_agent.instruction += f"""If you finished the user' request
- according to its description, call {task_completed.__name__} function
+ sub_agent.instruction += f"""If you finished the user's request
+ according to its description, call the {task_completed.__name__} function
to exit so the next agents can take over. When calling this function,
- do not generate any text other than the function call.'"""
+ do not generate any text other than the function call."""
for sub_agent in self.sub_agents:
async for event in sub_agent.run_live(ctx):
diff --git a/src/google/adk/approval/approval_policy.py b/src/google/adk/approval/approval_policy.py
index d3799c9dc1..1a0dada26b 100644
--- a/src/google/adk/approval/approval_policy.py
+++ b/src/google/adk/approval/approval_policy.py
@@ -11,6 +11,7 @@
Policies determine what actions on what resources require approval before a tool can be executed.
"""
+from __future__ import annotations
from typing import Any, Callable, Optional
from pydantic import BaseModel
diff --git a/src/google/adk/auth/auth_preprocessor.py b/src/google/adk/auth/auth_preprocessor.py
index 8ad30b72c3..0c964ed967 100644
--- a/src/google/adk/auth/auth_preprocessor.py
+++ b/src/google/adk/auth/auth_preprocessor.py
@@ -100,23 +100,24 @@ async def run_async(
function_calls = event.get_function_calls()
if not function_calls:
continue
- for function_call in function_calls:
- function_response_event = None
- if function_call.id in tools_to_resume:
- function_response_event = await functions.handle_function_calls_async(
- invocation_context,
- event,
- {
- tool.name: tool
- for tool in await agent.canonical_tools(
- ReadonlyContext(invocation_context)
- )
- },
- # there could be parallel function calls that require auth
- # auth response would be a dict keyed by function call id
- tools_to_resume,
- )
- if function_response_event:
+
+ if any([
+ function_call.id in tools_to_resume
+ for function_call in function_calls
+ ]):
+ if function_response_event := await functions.handle_function_calls_async(
+ invocation_context,
+ event,
+ {
+ tool.name: tool
+ for tool in await agent.canonical_tools(
+ ReadonlyContext(invocation_context)
+ )
+ },
+ # there could be parallel function calls that require auth
+ # auth response would be a dict keyed by function call id
+ tools_to_resume,
+ ):
yield function_response_event
return
return
diff --git a/src/google/adk/cli/agent_graph.py b/src/google/adk/cli/agent_graph.py
index 18e1eaef5e..a0b8a467df 100644
--- a/src/google/adk/cli/agent_graph.py
+++ b/src/google/adk/cli/agent_graph.py
@@ -20,6 +20,9 @@
import graphviz
from ..agents import BaseAgent
+from ..agents import LoopAgent
+from ..agents import ParallelAgent
+from ..agents import SequentialAgent
from ..agents.llm_agent import LlmAgent
from ..tools.agent_tool import AgentTool
from ..tools.base_tool import BaseTool
@@ -35,14 +38,39 @@
retrieval_tool_module_loaded = True
-async def build_graph(graph, agent: BaseAgent, highlight_pairs):
+async def build_graph(
+ graph: graphviz.Digraph,
+ agent: BaseAgent,
+ highlight_pairs,
+ parent_agent=None,
+):
+ """
+ Build a graph of the agent and its sub-agents.
+ Args:
+ graph: The graph to build on.
+ agent: The agent to build the graph for.
+ highlight_pairs: A list of pairs of nodes to highlight.
+ parent_agent: The parent agent of the current agent. This is specifically used when building Workflow Agents to directly connect a node to nodes inside a Workflow Agent.
+
+ Returns:
+ None
+ """
dark_green = '#0F5223'
light_green = '#69CB87'
light_gray = '#cccccc'
+ white = '#ffffff'
def get_node_name(tool_or_agent: Union[BaseAgent, BaseTool]):
if isinstance(tool_or_agent, BaseAgent):
- return tool_or_agent.name
+ # Added Workflow Agent checks for different agent types
+ if isinstance(tool_or_agent, SequentialAgent):
+ return tool_or_agent.name + f' (Sequential Agent)'
+ elif isinstance(tool_or_agent, LoopAgent):
+ return tool_or_agent.name + f' (Loop Agent)'
+ elif isinstance(tool_or_agent, ParallelAgent):
+ return tool_or_agent.name + f' (Parallel Agent)'
+ else:
+ return tool_or_agent.name
elif isinstance(tool_or_agent, BaseTool):
return tool_or_agent.name
else:
@@ -73,6 +101,7 @@ def get_node_caption(tool_or_agent: Union[BaseAgent, BaseTool]):
def get_node_shape(tool_or_agent: Union[BaseAgent, BaseTool]):
if isinstance(tool_or_agent, BaseAgent):
return 'ellipse'
+
elif retrieval_tool_module_loaded and isinstance(
tool_or_agent, BaseRetrievalTool
):
@@ -89,32 +118,131 @@ def get_node_shape(tool_or_agent: Union[BaseAgent, BaseTool]):
)
return 'cylinder'
+ def should_build_agent_cluster(tool_or_agent: Union[BaseAgent, BaseTool]):
+ if isinstance(tool_or_agent, BaseAgent):
+ if isinstance(tool_or_agent, SequentialAgent):
+ return True
+ elif isinstance(tool_or_agent, LoopAgent):
+ return True
+ elif isinstance(tool_or_agent, ParallelAgent):
+ return True
+ else:
+ return False
+ elif retrieval_tool_module_loaded and isinstance(
+ tool_or_agent, BaseRetrievalTool
+ ):
+ return False
+ elif isinstance(tool_or_agent, FunctionTool):
+ return False
+ elif isinstance(tool_or_agent, BaseTool):
+ return False
+ else:
+ logger.warning(
+ 'Unsupported tool, type: %s, obj: %s',
+ type(tool_or_agent),
+ tool_or_agent,
+ )
+ return False
+
+ def build_cluster(child: graphviz.Digraph, agent: BaseAgent, name: str):
+ if isinstance(agent, LoopAgent) and parent_agent:
+ # Draw the edge from the parent agent to the first sub-agent
+ draw_edge(parent_agent.name, agent.sub_agents[0].name)
+ length = len(agent.sub_agents)
+ currLength = 0
+ # Draw the edges between the sub-agents
+ for sub_agent_int_sequential in agent.sub_agents:
+ build_graph(child, sub_agent_int_sequential, highlight_pairs)
+ # Draw the edge between the current sub-agent and the next one
+ # If it's the last sub-agent, draw an edge to the first one to indicating a loop
+ draw_edge(
+ agent.sub_agents[currLength].name,
+ agent.sub_agents[
+ 0 if currLength == length - 1 else currLength + 1
+ ].name,
+ )
+ currLength += 1
+ elif isinstance(agent, SequentialAgent) and parent_agent:
+ # Draw the edge from the parent agent to the first sub-agent
+ draw_edge(parent_agent.name, agent.sub_agents[0].name)
+ length = len(agent.sub_agents)
+ currLength = 0
+
+ # Draw the edges between the sub-agents
+ for sub_agent_int_sequential in agent.sub_agents:
+ build_graph(child, sub_agent_int_sequential, highlight_pairs)
+ # Draw the edge between the current sub-agent and the next one
+ # If it's the last sub-agent, don't draw an edge to avoid a loop
+ draw_edge(
+ agent.sub_agents[currLength].name,
+ agent.sub_agents[currLength + 1].name,
+ ) if currLength != length - 1 else None
+ currLength += 1
+
+ elif isinstance(agent, ParallelAgent) and parent_agent:
+ # Draw the edge from the parent agent to every sub-agent
+ for sub_agent in agent.sub_agents:
+ build_graph(child, sub_agent, highlight_pairs)
+ draw_edge(parent_agent.name, sub_agent.name)
+ else:
+ for sub_agent in agent.sub_agents:
+ build_graph(child, sub_agent, highlight_pairs)
+ draw_edge(agent.name, sub_agent.name)
+
+ child.attr(
+ label=name,
+ style='rounded',
+ color=white,
+ fontcolor=light_gray,
+ )
+
def draw_node(tool_or_agent: Union[BaseAgent, BaseTool]):
name = get_node_name(tool_or_agent)
shape = get_node_shape(tool_or_agent)
caption = get_node_caption(tool_or_agent)
+ asCluster = should_build_agent_cluster(tool_or_agent)
+ child = None
if highlight_pairs:
for highlight_tuple in highlight_pairs:
if name in highlight_tuple:
- graph.node(
- name,
- caption,
- style='filled,rounded',
- fillcolor=dark_green,
- color=dark_green,
- shape=shape,
- fontcolor=light_gray,
- )
+ # if in highlight, draw highlight node
+ if asCluster:
+ cluster = graphviz.Digraph(
+ name='cluster_' + name
+ ) # adding "cluster_" to the name makes the graph render as a cluster subgraph
+ build_cluster(cluster, agent, name)
+ graph.subgraph(cluster)
+ else:
+ graph.node(
+ name,
+ caption,
+ style='filled,rounded',
+ fillcolor=dark_green,
+ color=dark_green,
+ shape=shape,
+ fontcolor=light_gray,
+ )
return
- # if not in highlight, draw non-highliht node
- graph.node(
- name,
- caption,
- shape=shape,
- style='rounded',
- color=light_gray,
- fontcolor=light_gray,
- )
+ # if not in highlight, draw non-highlight node
+ if asCluster:
+
+ cluster = graphviz.Digraph(
+ name='cluster_' + name
+ ) # adding "cluster_" to the name makes the graph render as a cluster subgraph
+ build_cluster(cluster, agent, name)
+ graph.subgraph(cluster)
+
+ else:
+ graph.node(
+ name,
+ caption,
+ shape=shape,
+ style='rounded',
+ color=light_gray,
+ fontcolor=light_gray,
+ )
+
+ return
def draw_edge(from_name, to_name):
if highlight_pairs:
@@ -126,12 +254,26 @@ def draw_edge(from_name, to_name):
graph.edge(from_name, to_name, color=light_green, dir='back')
return
# if no need to highlight, color gray
- graph.edge(from_name, to_name, arrowhead='none', color=light_gray)
+ if should_build_agent_cluster(agent):
+
+ graph.edge(
+ from_name,
+ to_name,
+ color=light_gray,
+ )
+ else:
+ graph.edge(from_name, to_name, arrowhead='none', color=light_gray)
draw_node(agent)
for sub_agent in agent.sub_agents:
- await build_graph(graph, sub_agent, highlight_pairs)
- draw_edge(agent.name, sub_agent.name)
+
+ build_graph(graph, sub_agent, highlight_pairs, agent)
+ if not should_build_agent_cluster(
+ sub_agent
+ ) and not should_build_agent_cluster(
+ agent
+ ): # This is to avoid making a node for a Workflow Agent
+ draw_edge(agent.name, sub_agent.name)
if isinstance(agent, LlmAgent):
for tool in await agent.canonical_tools():
draw_node(tool)
diff --git a/src/google/adk/cli/browser/assets/ADK-512-color.svg b/src/google/adk/cli/browser/assets/ADK-512-color.svg
new file mode 100644
index 0000000000..77a606aa8d
--- /dev/null
+++ b/src/google/adk/cli/browser/assets/ADK-512-color.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/google/adk/cli/browser/index.html b/src/google/adk/cli/browser/index.html
index c4a7ed9eaf..97ebf041e3 100644
--- a/src/google/adk/cli/browser/index.html
+++ b/src/google/adk/cli/browser/index.html
@@ -23,11 +23,11 @@
-
+
-
+