chore: import upstream snapshot with attribution
Continuous Integration / Pre-commit Linter (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.10) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.11) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.12) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.13) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.10) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.11) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.12) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.13) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.14) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Waiting to run
Copybara PR Handler / close-imported-pr (push) Waiting to run
Continuous Integration / Pre-commit Linter (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.10) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.11) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.12) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.13) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.10) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.11) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.12) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.13) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.14) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Waiting to run
Copybara PR Handler / close-imported-pr (push) Waiting to run
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Checks that newly-added Python files under src/google/adk/ have a '_' prefix.
|
||||
|
||||
ADK is private-by-default: a newly-added Python file under src/google/adk/ must
|
||||
have a '_'-prefixed basename. To make it public, add the symbol to the package
|
||||
__init__.py / __all__ instead. See
|
||||
.agents/skills/adk-style/references/visibility.md.
|
||||
|
||||
Newly-added files are detected by diffing the working tree against a baseline
|
||||
source tree (e.g. an origin/main checkout), so it works in a checkout that has
|
||||
no local git history:
|
||||
|
||||
python scripts/check_new_py_files.py --baseline-dir /path/to/origin-main
|
||||
|
||||
Exit codes: 0 = ok, 1 = violation(s) found, 2 = usage/setup error.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
_PACKAGE_RELPATH = os.path.join('src', 'google', 'adk')
|
||||
|
||||
_VIOLATION_LINE = "Error: New Python file '{path}' must have a '_' prefix."
|
||||
|
||||
_GUIDANCE = (
|
||||
'All new Python files in src/google/adk/ must be private by default.\n'
|
||||
'To expose a public interface, use __init__.py and list public symbols'
|
||||
' in __all__.\n'
|
||||
'See .agents/skills/adk-style/references/visibility.md for details.'
|
||||
)
|
||||
|
||||
# Subtrees that may exist in the working tree but are intentionally absent from
|
||||
# the baseline tree; ignore them so the diff does not report them as newly
|
||||
# added.
|
||||
_IGNORED_PREFIXES = (
|
||||
'src/google/adk/internal/',
|
||||
'src/google/adk/v1/',
|
||||
'src/google/adk/platform/internal/',
|
||||
)
|
||||
|
||||
|
||||
def find_py_files(root: str) -> set[str]:
|
||||
"""Returns root-relative paths of every *.py under <root>/src/google/adk.
|
||||
|
||||
Each path includes the src/google/adk/ prefix (e.g.
|
||||
'src/google/adk/agents/foo.py'). Symlinks are followed so that a src/google/adk
|
||||
tree assembled from symlinked subdirectories is walked correctly.
|
||||
"""
|
||||
package_root = os.path.join(root, _PACKAGE_RELPATH)
|
||||
found: set[str] = set()
|
||||
for dirpath, _, filenames in os.walk(package_root, followlinks=True):
|
||||
for name in filenames:
|
||||
if name.endswith('.py'):
|
||||
abs_path = os.path.join(dirpath, name)
|
||||
found.add(os.path.relpath(abs_path, root))
|
||||
return found
|
||||
|
||||
|
||||
def _should_check(relpath: str) -> bool:
|
||||
"""Returns False for paths under an ignored prefix."""
|
||||
return not any(relpath.startswith(prefix) for prefix in _IGNORED_PREFIXES)
|
||||
|
||||
|
||||
def added_py_files(new_root: str, baseline_root: str) -> set[str]:
|
||||
"""Returns .py files present in new_root but not in baseline_root.
|
||||
|
||||
Paths under _IGNORED_PREFIXES are skipped: they may exist in the working tree
|
||||
but are intentionally absent from the baseline, so a plain diff would
|
||||
otherwise report them as newly added.
|
||||
"""
|
||||
added = find_py_files(new_root) - find_py_files(baseline_root)
|
||||
return {path for path in added if _should_check(path)}
|
||||
|
||||
|
||||
def find_violations(added: set[str]) -> list[str]:
|
||||
"""Returns the sorted added files whose basename does not start with '_'."""
|
||||
return sorted(
|
||||
path for path in added if not os.path.basename(path).startswith('_')
|
||||
)
|
||||
|
||||
|
||||
def _has_package_dir(root: str) -> bool:
|
||||
return os.path.isdir(os.path.join(root, _PACKAGE_RELPATH))
|
||||
|
||||
|
||||
def _parse_args(argv: list[str]) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
'--baseline-dir',
|
||||
required=True,
|
||||
help='Baseline source tree to diff against (an origin/main checkout).',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--new-dir',
|
||||
default='.',
|
||||
help='New source tree to check (default: current directory).',
|
||||
)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
args = _parse_args(argv)
|
||||
for label, root in (('baseline', args.baseline_dir), ('new', args.new_dir)):
|
||||
if not _has_package_dir(root):
|
||||
print(
|
||||
f'Error: {label} tree has no {_PACKAGE_RELPATH} directory: {root}',
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 2
|
||||
|
||||
violations = find_violations(added_py_files(args.new_dir, args.baseline_dir))
|
||||
for path in violations:
|
||||
print(_VIOLATION_LINE.format(path=path), file=sys.stderr)
|
||||
if violations:
|
||||
print(_GUIDANCE, file=sys.stderr)
|
||||
return 1 if violations else 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
Executable
+52
@@ -0,0 +1,52 @@
|
||||
#!/bin/bash
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
exit_code=0
|
||||
|
||||
get_added_files() {
|
||||
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
git diff --cached --name-only --diff-filter=A
|
||||
elif jj root >/dev/null 2>&1; then
|
||||
jj diff --summary 2>/dev/null | awk '/^A / {print $2}'
|
||||
elif hg root >/dev/null 2>&1; then
|
||||
hg status --added --no-status 2>/dev/null
|
||||
elif g4 info >/dev/null 2>&1; then
|
||||
g4 opened 2>/dev/null | awk '/ - add / {print $1}' | sed 's/#.*//'
|
||||
elif p4 info >/dev/null 2>&1; then
|
||||
p4 opened 2>/dev/null | awk '/ - add / {print $1}' | sed 's/#.*//'
|
||||
fi
|
||||
}
|
||||
|
||||
while read -r file; do
|
||||
# Check if file is not empty (happens if no new files)
|
||||
if [[ -n "$file" ]]; then
|
||||
# Match only files in the package source (src/google/adk/) to avoid false
|
||||
# positives in environments (e.g., monorepos) where the entire repository
|
||||
# root is nested under a 'google/adk/' directory structure.
|
||||
if [[ "$file" == */src/google/adk/*.py ]] || [[ "$file" == src/google/adk/*.py ]]; then
|
||||
filename=$(basename "$file")
|
||||
if [[ ! "$filename" == _* ]]; then
|
||||
echo "Error: New Python file '$file' must have a '_' prefix."
|
||||
echo "All new Python files in src/google/adk/ must be private by default."
|
||||
echo "To expose a public interface, use __init__.py and list public symbols in __all__."
|
||||
echo "See .agents/skills/adk-style/references/visibility.md for details."
|
||||
exit_code=1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done < <(get_added_files)
|
||||
|
||||
exit $exit_code
|
||||
Executable
+182
@@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Runs compliance checks on ADK source files.
|
||||
|
||||
This script is used as a pre-commit hook and in CI to enforce coding standards.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
# Legacy files that are temporarily excluded from the mTLS check.
|
||||
# Do not add new files to this list. All new code must support mTLS.
|
||||
_EXCLUDED_FROM_MTLS = {
|
||||
'contributing/samples/environment_and_skills/e2b_environment/agent.py',
|
||||
'contributing/samples/integrations/bigquery_mcp/agent.py',
|
||||
'contributing/samples/integrations/bigtable/agent.py',
|
||||
'contributing/samples/integrations/data_agent/agent.py',
|
||||
'contributing/samples/integrations/gcp_auth/agent.py',
|
||||
'contributing/samples/integrations/gcs/agent.py',
|
||||
'contributing/samples/integrations/gcs_admin/agent.py',
|
||||
'contributing/samples/integrations/integration_connector_euc_agent/agent.py',
|
||||
'contributing/samples/integrations/oauth_calendar_agent/agent.py',
|
||||
'contributing/samples/integrations/spanner/agent.py',
|
||||
'contributing/samples/integrations/spanner_admin/agent.py',
|
||||
'contributing/samples/integrations/spanner_rag_agent/agent.py',
|
||||
'contributing/samples/mcp/mcp_service_account_agent/agent.py',
|
||||
'contributing/samples/models/interactions_api/main.py',
|
||||
'contributing/samples/multimodal/static_non_text_content/agent.py',
|
||||
'src/google/adk/auth/auth_credential.py',
|
||||
'src/google/adk/integrations/api_registry/api_registry.py',
|
||||
'src/google/adk/integrations/bigquery/bigquery_credentials.py',
|
||||
'src/google/adk/integrations/bigquery/data_insights_tool.py',
|
||||
'src/google/adk/integrations/bigquery/metadata_tool.py',
|
||||
'src/google/adk/integrations/gcs/gcs_credentials.py',
|
||||
'src/google/adk/plugins/bigquery_agent_analytics_plugin.py',
|
||||
'src/google/adk/tools/_google_credentials.py',
|
||||
'src/google/adk/tools/apihub_tool/clients/apihub_client.py',
|
||||
'src/google/adk/tools/application_integration_tool/application_integration_toolset.py',
|
||||
'src/google/adk/tools/application_integration_tool/clients/connections_client.py',
|
||||
'src/google/adk/tools/application_integration_tool/clients/integration_client.py',
|
||||
'src/google/adk/tools/bigtable/bigtable_credentials.py',
|
||||
'src/google/adk/tools/data_agent/credentials.py',
|
||||
'src/google/adk/tools/data_agent/data_agent_tool.py',
|
||||
'src/google/adk/tools/google_api_tool/google_api_toolset.py',
|
||||
'src/google/adk/tools/google_api_tool/googleapi_to_openapi_converter.py',
|
||||
'src/google/adk/tools/mcp_tool/mcp_session_manager.py',
|
||||
'src/google/adk/tools/openapi_tool/auth/auth_helpers.py',
|
||||
'src/google/adk/tools/openapi_tool/auth/credential_exchangers/service_account_exchanger.py',
|
||||
'src/google/adk/tools/pubsub/pubsub_credentials.py',
|
||||
'src/google/adk/tools/spanner/spanner_credentials.py',
|
||||
'tests/integration/test_managed_agent.py',
|
||||
'tests/unittests/auth/test_credential_manager.py',
|
||||
'tests/unittests/cli/utils/test_gcp_utils.py',
|
||||
'tests/unittests/flows/llm_flows/test_functions_request_euc.py',
|
||||
'tests/unittests/integrations/api_registry/test_api_registry.py',
|
||||
'tests/unittests/integrations/bigquery/test_bigquery_credentials.py',
|
||||
'tests/unittests/tools/apihub_tool/clients/test_apihub_client.py',
|
||||
'tests/unittests/tools/application_integration_tool/clients/test_connections_client.py',
|
||||
'tests/unittests/tools/application_integration_tool/clients/test_integration_client.py',
|
||||
'tests/unittests/tools/application_integration_tool/test_application_integration_toolset.py',
|
||||
'tests/unittests/tools/data_agent/test_data_agent_tool.py',
|
||||
'tests/unittests/tools/google_api_tool/test_docs_batchupdate.py',
|
||||
'tests/unittests/tools/google_api_tool/test_google_api_toolset.py',
|
||||
'tests/unittests/tools/google_api_tool/test_googleapi_to_openapi_converter.py',
|
||||
'tests/unittests/tools/openapi_tool/auth/credential_exchangers/test_service_account_exchanger.py',
|
||||
'tests/unittests/tools/openapi_tool/openapi_spec_parser/test_openapi_toolset.py',
|
||||
'tests/unittests/tools/openapi_tool/openapi_spec_parser/test_rest_api_tool.py',
|
||||
'tests/unittests/tools/spanner/test_spanner_credentials.py',
|
||||
'tests/unittests/tools/test_base_google_credentials_manager.py',
|
||||
'tests/unittests/tools/test_google_tool.py',
|
||||
'tests/unittests/workflow/utils/test_workflow_hitl_utils.py',
|
||||
}
|
||||
|
||||
|
||||
def check_logger(content: str) -> bool:
|
||||
# Forbidden: getLogger(__name__) without the 'google_adk.' prefix.
|
||||
pattern = re.compile(r'logger\s*=\s*logging\.getLogger\(__name__\)')
|
||||
return not pattern.search(content)
|
||||
|
||||
|
||||
def check_future_annotations(content: str, filename: str) -> bool:
|
||||
# Exclude: __init__.py, version.py, tests/, contributing/samples/
|
||||
if (
|
||||
filename.endswith('__init__.py')
|
||||
or filename.endswith('version.py')
|
||||
or 'tests/' in filename
|
||||
or 'contributing/samples/' in filename
|
||||
):
|
||||
return True
|
||||
return 'from __future__ import annotations' in content
|
||||
|
||||
|
||||
def check_cli_import(content: str, filename: str) -> bool:
|
||||
# Exclude: cli/, apihub_toolset.py, tests/, contributing/samples/
|
||||
if (
|
||||
'cli/' in filename
|
||||
or filename.endswith('apihub_toolset.py')
|
||||
or 'tests/' in filename
|
||||
or 'contributing/samples/' in filename
|
||||
):
|
||||
return True
|
||||
# Pattern: ^from.*\bcli\b.*import.*$ (multiline)
|
||||
pattern = re.compile(r'^from.*\bcli\b.*import.*$', re.MULTILINE)
|
||||
return not pattern.search(content)
|
||||
|
||||
|
||||
def check_mtls(content: str, filename: str) -> bool:
|
||||
if filename in _EXCLUDED_FROM_MTLS:
|
||||
return True
|
||||
# Pattern for googleapis: https?://[a-zA-Z0-9.-]+\.googleapis\.com
|
||||
endpoint_pattern = re.compile(r'https?://[a-zA-Z0-9.-]+\.googleapis\.com')
|
||||
if endpoint_pattern.search(content):
|
||||
return '.mtls.googleapis.com' in content
|
||||
return True
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('files', nargs='*', help='Files to check')
|
||||
args = parser.parse_args()
|
||||
|
||||
failed = False
|
||||
for f in args.files:
|
||||
# Skip directories if they are passed accidentally
|
||||
if not os.path.isfile(f):
|
||||
continue
|
||||
try:
|
||||
with open(f, 'r', encoding='utf-8') as file:
|
||||
content = file.read()
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
print(f'Error reading {f}: {e}')
|
||||
continue
|
||||
|
||||
# Run checks
|
||||
if not check_logger(content):
|
||||
print(
|
||||
f"❌ {f}: Found forbidden use of 'logger ="
|
||||
" logging.getLogger(__name__)'. Please use 'logger ="
|
||||
' logging.getLogger("google_adk." + __name__)\' instead.'
|
||||
)
|
||||
failed = True
|
||||
|
||||
if not check_future_annotations(content, f):
|
||||
print(f"❌ {f}: Missing 'from __future__ import annotations'.")
|
||||
failed = True
|
||||
|
||||
if not check_cli_import(content, f):
|
||||
print(
|
||||
f'❌ {f}: Do not import from the cli package outside of the cli'
|
||||
' package.'
|
||||
)
|
||||
failed = True
|
||||
|
||||
if not check_mtls(content, f):
|
||||
print(
|
||||
f'❌ {f}: Found hardcoded googleapis.com endpoints without mTLS'
|
||||
' support.'
|
||||
)
|
||||
failed = True
|
||||
|
||||
if failed:
|
||||
sys.exit(1)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,327 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Curate the newest CHANGELOG.md release section during a release cut.
|
||||
|
||||
Runs as a post-step after release-please in the "Release: Cut" workflow and
|
||||
commits the result back to the release PR branch. It does three things to the
|
||||
newest version section, in order:
|
||||
|
||||
1. Deterministic cleanup of the entries (no model): unescape HTML entities
|
||||
(``>=`` -> ``>=``), de-link accidental ``@mentions`` that release-please
|
||||
auto-linked from a commit subject, drop duplicate entries (the same change
|
||||
landed under several commits), and lowercase the leading word so entries
|
||||
read as consistent imperative phrases.
|
||||
2. Draft a short "Highlights" block with Gemini and place it above the fold, so
|
||||
a reader grasps the release in a handful of bullets.
|
||||
3. For large releases, collapse the full categorized list under a ``<details>``
|
||||
fold so the notes read short while remaining a complete record.
|
||||
|
||||
Every step is best-effort. If the model is unavailable the Highlights fall back
|
||||
to a template; the deterministic passes never call the network. The file is only
|
||||
rewritten when something changed, so it is safe to re-run on each release-please
|
||||
regenerate (idempotent). The release manager edits the result in the PR before
|
||||
merging.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import html
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
_HIGHLIGHTS_HEADER = "### Highlights"
|
||||
_DETAILS_SUMMARY = "<summary>All changes</summary>"
|
||||
|
||||
# Matches a release header line, e.g. "## [2.4.0](https://...) (2026-06-29)".
|
||||
_VERSION_RE = re.compile(r"^## \[")
|
||||
# Matches a category header, e.g. "### Features", "### Bug Fixes".
|
||||
_SUBSECTION_RE = re.compile(r"^### ")
|
||||
# Matches a changelog entry bullet.
|
||||
_ENTRY_RE = re.compile(r"^\s*\* ")
|
||||
# Trailing " ([abc1234](url))..." on an entry; stripped only to build the dedupe
|
||||
# key so two commits with the same subject collapse to one.
|
||||
_TRAILER_RE = re.compile(r"\s*\(\[[0-9a-f]{6,}\]\(.*$")
|
||||
# An accidental "[@name](https://github.com/name)" auto-link, produced when a
|
||||
# commit subject contained a bare "@name" (e.g. "... in @node decorator").
|
||||
_MENTION_RE = re.compile(r"\[@([\w-]+)\]\(https://github\.com/\1\)")
|
||||
# "* " then an optional bold "**scope:** " prefix, then the first word and rest.
|
||||
_LEAD_RE = re.compile(
|
||||
r"(?P<head>\s*\* (?:\*\*[^*]+\*\* )?)(?P<first>\w+)(?P<rest>.*)", re.S
|
||||
)
|
||||
|
||||
# Inserted verbatim when the model is unavailable, so the release manager has a
|
||||
# scaffold to fill in by hand. Mirrors the format the model is asked to produce.
|
||||
_TEMPLATE = """### Highlights
|
||||
|
||||
<one sentence describing the theme of this release>
|
||||
|
||||
* **<Feature>**: <what it unlocks for the user, in one line>. (<commit>)
|
||||
* **<Feature>**: <user benefit>. (<commit>)
|
||||
|
||||
#### Breaking changes
|
||||
|
||||
* **<what changed>**: <how to migrate, in one line>. (<commit>)
|
||||
"""
|
||||
|
||||
_PROMPT = """\
|
||||
You are drafting the "Highlights" section of an ADK (Agent Development Kit)
|
||||
Python release changelog.
|
||||
|
||||
Below is the auto-generated changelog for the new version, grouped by type
|
||||
(Features, Bug Fixes, etc.). Each entry ends with a commit hash link.
|
||||
|
||||
Write a short Highlights section so a reader can grasp the release at a glance:
|
||||
- Start with ONE sentence describing the theme of the release.
|
||||
- Then 2-5 bullets, each leading with the user-facing benefit rather than the
|
||||
implementation, formatted as
|
||||
"* **<Area>**: <benefit in one line>. (<commit link>)".
|
||||
- Reuse the exact commit links from the entries you summarize.
|
||||
- Pick only the few changes that matter most to users. Ignore pure refactors,
|
||||
chores, and trivial docs.
|
||||
- If there are breaking changes, add a "#### Breaking changes" subsection after
|
||||
the bullets, each with a one-line migration note.
|
||||
|
||||
Output ONLY the markdown body. Do NOT include the "### Highlights" header and do
|
||||
NOT wrap the output in code fences.
|
||||
|
||||
Changelog for the new version:
|
||||
|
||||
{changelog}
|
||||
"""
|
||||
|
||||
|
||||
def _find_latest_section(lines: list[str]) -> tuple[int, int] | None:
|
||||
"""Returns the [start, end) line span of the newest release section.
|
||||
|
||||
start is the index of the "## [" header; end is the index of the next "## ["
|
||||
header or len(lines). Returns None if no release header is present.
|
||||
"""
|
||||
start = None
|
||||
for i, line in enumerate(lines):
|
||||
if _VERSION_RE.match(line):
|
||||
start = i
|
||||
break
|
||||
if start is None:
|
||||
return None
|
||||
end = len(lines)
|
||||
for j in range(start + 1, len(lines)):
|
||||
if _VERSION_RE.match(lines[j]):
|
||||
end = j
|
||||
break
|
||||
return start, end
|
||||
|
||||
|
||||
def _latest_section_text(text: str) -> str | None:
|
||||
"""Returns the text of the newest release section, or None if absent."""
|
||||
lines = text.splitlines(keepends=True)
|
||||
span = _find_latest_section(lines)
|
||||
if span is None:
|
||||
return None
|
||||
start, end = span
|
||||
return "".join(lines[start:end]).strip("\n") + "\n"
|
||||
|
||||
|
||||
def _normalize_entry(line: str) -> str:
|
||||
"""Applies deterministic, meaning-preserving fixes to a single entry line."""
|
||||
s = html.unescape(line) # >= -> >=, & -> &, etc.
|
||||
s = _MENTION_RE.sub(r"`@\1`", s) # de-link an accidental @mention
|
||||
m = _LEAD_RE.match(s)
|
||||
if m:
|
||||
first = m.group("first")
|
||||
# Lowercase a plain leading word ("Fix" -> "fix") but leave acronyms and
|
||||
# camelCase/proper nouns intact ("OAuth", "GPU", "iOS", "A2A").
|
||||
if not any(c.isupper() for c in first[1:]):
|
||||
first = first[0].lower() + first[1:]
|
||||
s = f"{m.group('head')}{first}{m.group('rest')}"
|
||||
return s
|
||||
|
||||
|
||||
def _dedupe_key(line: str) -> str:
|
||||
"""Key for detecting the same change landed under multiple commits."""
|
||||
core = _TRAILER_RE.sub("", line) # drop the "([hash](url))" trailer
|
||||
return re.sub(r"\s+", " ", core).strip().lower()
|
||||
|
||||
|
||||
def _normalize_body(lines: list[str]) -> list[str]:
|
||||
"""Normalizes and de-duplicates entry bullets; passes other lines through."""
|
||||
seen: set[str] = set()
|
||||
out: list[str] = []
|
||||
for line in lines:
|
||||
if _ENTRY_RE.match(line):
|
||||
norm = _normalize_entry(line)
|
||||
key = _dedupe_key(norm)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
out.append(norm)
|
||||
else:
|
||||
out.append(line)
|
||||
return out
|
||||
|
||||
|
||||
def _count_entries(lines: list[str]) -> int:
|
||||
return sum(1 for line in lines if _ENTRY_RE.match(line))
|
||||
|
||||
|
||||
def _wrap_in_details(body_lines: list[str]) -> str:
|
||||
"""Collapses the categorized list under a <details> fold."""
|
||||
inner = "".join(body_lines).strip("\n")
|
||||
return f"<details>\n{_DETAILS_SUMMARY}\n\n{inner}\n\n</details>\n"
|
||||
|
||||
|
||||
def _draft_highlights(section_text: str, *, model: str) -> str | None:
|
||||
"""Drafts the Highlights body with Gemini, or None if unavailable."""
|
||||
api_key = os.environ.get("GOOGLE_API_KEY")
|
||||
if not api_key:
|
||||
print("GOOGLE_API_KEY not set; skipping model drafting.")
|
||||
return None
|
||||
try:
|
||||
from google import genai
|
||||
|
||||
client = genai.Client(api_key=api_key)
|
||||
response = client.models.generate_content(
|
||||
model=model,
|
||||
contents=_PROMPT.format(changelog=section_text),
|
||||
)
|
||||
body = (response.text or "").strip()
|
||||
return body or None
|
||||
# The release must never fail because drafting failed (missing dependency,
|
||||
# network/API error, quota); fall back to the template in every case.
|
||||
except Exception as e: # pylint: disable=broad-exception-caught
|
||||
print(f"Highlights drafting failed ({e!r}); falling back to template.")
|
||||
return None
|
||||
|
||||
|
||||
def _build_block(body: str) -> str:
|
||||
"""Wraps a model-drafted body in the Highlights header."""
|
||||
body = body.strip()
|
||||
if body.startswith(_HIGHLIGHTS_HEADER):
|
||||
body = body[len(_HIGHLIGHTS_HEADER) :].lstrip("\n")
|
||||
return f"{_HIGHLIGHTS_HEADER}\n\n{body}\n"
|
||||
|
||||
|
||||
def curate(text: str, *, model: str, fold_threshold: int) -> str:
|
||||
"""Returns CHANGELOG text with the newest release section curated."""
|
||||
lines = text.splitlines(keepends=True)
|
||||
span = _find_latest_section(lines)
|
||||
if span is None:
|
||||
print("No release section found; leaving CHANGELOG unchanged.")
|
||||
return text
|
||||
start, end = span
|
||||
|
||||
section = lines[start:end]
|
||||
if any(line.strip() == _HIGHLIGHTS_HEADER for line in section) or any(
|
||||
_DETAILS_SUMMARY in line for line in section
|
||||
):
|
||||
print("Section already curated; leaving CHANGELOG unchanged.")
|
||||
return text
|
||||
|
||||
# Split the section into its header (## [..] + blank lines) and the
|
||||
# categorized body (### Features ... through the end of the section).
|
||||
first_sub = None
|
||||
for i in range(start + 1, end):
|
||||
if _SUBSECTION_RE.match(lines[i]):
|
||||
first_sub = i
|
||||
break
|
||||
|
||||
if first_sub is None:
|
||||
# No categorized entries (rare): only add Highlights.
|
||||
header = section
|
||||
body_norm: list[str] = []
|
||||
model_input = ""
|
||||
else:
|
||||
header = lines[start:first_sub]
|
||||
body_norm = _normalize_body(lines[first_sub:end])
|
||||
model_input = "".join(body_norm)
|
||||
|
||||
drafted = _draft_highlights(model_input, model=model) if model_input else None
|
||||
if drafted is None:
|
||||
highlights = _TEMPLATE
|
||||
print("Inserted Highlights template.")
|
||||
else:
|
||||
highlights = _build_block(drafted)
|
||||
print("Inserted model-drafted Highlights.")
|
||||
|
||||
parts: list[str] = list(header)
|
||||
if parts and parts[-1].strip():
|
||||
parts.append("\n")
|
||||
parts.append(highlights.rstrip("\n") + "\n\n")
|
||||
|
||||
if body_norm:
|
||||
if _count_entries(body_norm) > fold_threshold:
|
||||
parts.append(_wrap_in_details(body_norm))
|
||||
print(f"Folded {_count_entries(body_norm)} entries under <details>.")
|
||||
else:
|
||||
parts.append("".join(body_norm).strip("\n") + "\n")
|
||||
|
||||
new_section = "".join(parts).rstrip("\n") + "\n\n"
|
||||
return "".join(lines[:start]) + new_section + "".join(lines[end:])
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--changelog",
|
||||
default="CHANGELOG.md",
|
||||
help="Path to the changelog file to curate.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--model",
|
||||
default=os.environ.get("CHANGELOG_CURATION_MODEL", "gemini-2.5-flash"),
|
||||
help="Gemini model used to draft the Highlights.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--fold-threshold",
|
||||
type=int,
|
||||
default=int(os.environ.get("CHANGELOG_FOLD_THRESHOLD", "12")),
|
||||
help=(
|
||||
"Collapse the full list under a <details> fold when the release has"
|
||||
" more than this many entries. Set very high to never fold."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--section-out",
|
||||
default=None,
|
||||
help=(
|
||||
"If set, write the curated newest release section to this path, for"
|
||||
" use as the PR description body. Written even when the changelog"
|
||||
" file is otherwise unchanged."
|
||||
),
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
with open(args.changelog, encoding="utf-8") as f:
|
||||
text = f.read()
|
||||
updated = curate(text, model=args.model, fold_threshold=args.fold_threshold)
|
||||
|
||||
if args.section_out:
|
||||
section = _latest_section_text(updated)
|
||||
if section is not None:
|
||||
with open(args.section_out, "w", encoding="utf-8") as f:
|
||||
f.write(section)
|
||||
print(f"Wrote latest section to {args.section_out}.")
|
||||
|
||||
if updated == text:
|
||||
return 0
|
||||
with open(args.changelog, "w", encoding="utf-8") as f:
|
||||
f.write(updated)
|
||||
print(f"Updated {args.changelog}.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Executable
+158
@@ -0,0 +1,158 @@
|
||||
#!/bin/bash
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
|
||||
# This script is to update sessions DB that is created in previous ADK version,
|
||||
# to schema that current ADK version use. The sample usage is in the samples/migrate_session_db.
|
||||
#
|
||||
# Usage:
|
||||
# ./db_migration.sh "sqlite:///%(here)s/sessions.db" "google.adk.sessions.database_session_service"
|
||||
# ./db_migration.sh "postgresql://user:pass@localhost/mydb" "google.adk.sessions.database_session_service"
|
||||
# First argument is the sessions DB url.
|
||||
# Second argument is the model import path.
|
||||
|
||||
# --- Configuration ---
|
||||
ALEMBIC_DIR="alembic"
|
||||
INI_FILE="alembic.ini"
|
||||
ENV_FILE="${ALEMBIC_DIR}/env.py"
|
||||
|
||||
# --- Functions ---
|
||||
print_usage() {
|
||||
echo "Usage: $0 <sqlalchemy_url> <model_import_path>"
|
||||
echo " <sqlalchemy_url>: The full SQLAlchemy connection string."
|
||||
echo " <model_import_path>: The Python import path to your models (e.g., my_project.models)"
|
||||
echo ""
|
||||
echo "Example:"
|
||||
echo " $0 \"sqlite:///%(here)s/sessions.db\" \"google.adk.sessions.database_session_service\""
|
||||
}
|
||||
|
||||
# --- Argument Validation ---
|
||||
if [ "$#" -ne 2 ]; then
|
||||
print_usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DB_URL=$1
|
||||
MODEL_PATH=$2
|
||||
|
||||
echo "Setting up Alembic..."
|
||||
echo " Database URL: ${DB_URL}"
|
||||
echo " Model Path: ${MODEL_PATH}"
|
||||
echo ""
|
||||
|
||||
# --- Safety Check ---
|
||||
if [ -f "$INI_FILE" ] || [ -d "$ALEMBIC_DIR" ]; then
|
||||
echo "Error: 'alembic.ini' or 'alembic/' directory already exists."
|
||||
echo "Please remove them before running this script."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- 1. Run alembic init ---
|
||||
echo "Running 'alembic init ${ALEMBIC_DIR}'..."
|
||||
alembic init ${ALEMBIC_DIR}
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: 'alembic init' failed. Is alembic installed?"
|
||||
exit 1
|
||||
fi
|
||||
echo "Initialization complete."
|
||||
echo ""
|
||||
|
||||
# --- 2. Set sqlalchemy.url in alembic.ini ---
|
||||
echo "Configuring ${INI_FILE}..."
|
||||
# Use a different delimiter (#) for sed to avoid escaping slashes in the URL
|
||||
sed -i.bak "s#sqlalchemy.url = driver://user:pass@localhost/dbname#sqlalchemy.url = ${DB_URL}#" "${INI_FILE}"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: Failed to set sqlalchemy.url in ${INI_FILE}."
|
||||
exit 1
|
||||
fi
|
||||
echo " Set sqlalchemy.url"
|
||||
|
||||
# --- 3. Set target_metadata in alembic/env.py ---
|
||||
echo "Configuring ${ENV_FILE}..."
|
||||
|
||||
# Edit 1: Uncomment and replace the model import line
|
||||
sed -i.bak "s/# from myapp import mymodel/from ${MODEL_PATH} import Base/" "${ENV_FILE}"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: Failed to set model import in ${ENV_FILE}."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Edit 2: Set the target_metadata to use the imported Base
|
||||
sed -i.bak "s/target_metadata = None/target_metadata = Base.metadata/" "${ENV_FILE}"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: Failed to set target_metadata in ${ENV_FILE}."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo " Set target_metadata"
|
||||
echo ""
|
||||
|
||||
# --- 4. Clean up backup files ---
|
||||
echo "Cleaning up backup files..."
|
||||
rm "${INI_FILE}.bak"
|
||||
rm "${ENV_FILE}.bak"
|
||||
|
||||
# --- 5. Run alembic stamp head ---
|
||||
echo "Running 'alembic stamp head'..."
|
||||
alembic stamp head
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: 'alembic stamp head' failed."
|
||||
exit 1
|
||||
fi
|
||||
echo "stamping complete."
|
||||
echo ""
|
||||
|
||||
# --- 6. Run alembic upgrade ---
|
||||
echo "Running 'alembic revision --autogenerate'..."
|
||||
alembic revision --autogenerate -m "ADK session DB upgrade"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: 'alembic revision' failed."
|
||||
exit 1
|
||||
fi
|
||||
echo "revision complete."
|
||||
echo ""
|
||||
|
||||
# --- 7. Add import statement to version files ---
|
||||
echo "Adding import statement to version files..."
|
||||
for f in ${ALEMBIC_DIR}/versions/*.py; do
|
||||
if [ -f "$f" ]; then
|
||||
# Check if the first line is already the import statement
|
||||
FIRST_LINE=$(head -n 1 "$f")
|
||||
IMPORT_STATEMENT="import ${MODEL_PATH}"
|
||||
if [ "$FIRST_LINE" != "$IMPORT_STATEMENT" ]; then
|
||||
echo "Adding import to $f"
|
||||
sed -i.bak "1s|^|${IMPORT_STATEMENT}\n|" "$f"
|
||||
rm "${f}.bak"
|
||||
else
|
||||
echo "Import already exists in $f"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
echo "Import statements added."
|
||||
echo ""
|
||||
|
||||
# --- 8. Run alembic upgrade ---
|
||||
echo "running 'alembic upgrade'..."
|
||||
alembic upgrade head
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: 'alembic upgrade' failed. "
|
||||
exit 1
|
||||
fi
|
||||
echo "upgrade complete."
|
||||
echo ""
|
||||
|
||||
echo "---"
|
||||
echo "✅ ADK session DB is Updated!"
|
||||
@@ -0,0 +1,67 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Script to generate AgentConfig.json from AgentConfig."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
from google.adk.agents.agent_config import AgentConfig
|
||||
from pydantic.json_schema import GenerateJsonSchema
|
||||
from pydantic.json_schema import PydanticInvalidForJsonSchema
|
||||
|
||||
|
||||
class CustomGenerateJsonSchema(GenerateJsonSchema):
|
||||
"""Custom schema generator that handles invalid types by falling back."""
|
||||
|
||||
def handle_invalid_for_json_schema(self, schema, error_info):
|
||||
try:
|
||||
return super().handle_invalid_for_json_schema(schema, error_info)
|
||||
except PydanticInvalidForJsonSchema:
|
||||
# Return a fallback schema instead of failing
|
||||
return {
|
||||
"type": "object",
|
||||
"description": f"Fallback for invalid schema: {error_info}",
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
"""Generates the AgentConfig.json schema."""
|
||||
# Use the custom generator to avoid failing on httpx.Client
|
||||
schema = AgentConfig.model_json_schema(
|
||||
schema_generator=CustomGenerateJsonSchema
|
||||
)
|
||||
|
||||
# Find the repo root relative to this file.
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
repo_root = os.path.dirname(script_dir)
|
||||
|
||||
output_path = os.path.join(
|
||||
repo_root, "src/google/adk/agents/config_schemas/AgentConfig.json"
|
||||
)
|
||||
|
||||
# Ensure directory exists
|
||||
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||||
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
json.dump(schema, f, indent=2)
|
||||
f.write("\n")
|
||||
|
||||
print(f"Successfully generated {output_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user