diff --git a/.github/workflows/android-ci.yml b/.github/workflows/android-ci.yml new file mode 100644 index 0000000..2905bd5 --- /dev/null +++ b/.github/workflows/android-ci.yml @@ -0,0 +1,48 @@ +# 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. + +name: Android CI + +on: + pull_request: + branches: [ main ] + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + name: Android CI / build + # zizmor: ignore[unpinned-images] + runs-on: ubuntu-24.04 + timeout-minutes: 10 + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - name: Set up JDK 17 + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 + with: + distribution: 'temurin' + java-version: '17' + cache: 'gradle' + + - name: Build and test library + run: | + chmod +x ./gradlew + ./gradlew test assembleRelease --no-daemon + working-directory: client/android/GoogleMapsA2UI diff --git a/.github/workflows/cleanup-stale-prs.yml b/.github/workflows/cleanup-stale-prs.yml new file mode 100644 index 0000000..d238b98 --- /dev/null +++ b/.github/workflows/cleanup-stale-prs.yml @@ -0,0 +1,84 @@ +# 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. + +name: Cleanup Stale Draft PRs + +on: + schedule: + - cron: '0 2 * * *' # Daily at 02:00 UTC + workflow_dispatch: + inputs: + older_than_days: + description: 'Close draft PRs older than N days' + required: false + default: '3' + type: string + dry_run: + description: 'Dry run (simulate without closing PRs or deleting branches)' + required: false + default: false + type: boolean + +permissions: + pull-requests: write + contents: write + +jobs: + cleanup: + name: Cleanup Draft PRs + # zizmor: ignore[unpinned-images] + runs-on: ubuntu-latest + steps: + - name: Close stale draft PRs and delete branches + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + INPUT_DAYS: ${{ inputs.older_than_days }} + INPUT_DRY_RUN: ${{ inputs.dry_run }} + run: | + DAYS="${INPUT_DAYS:-3}" + DRY_RUN="${INPUT_DRY_RUN:-false}" + + echo "Searching for open draft PRs with head branch matching 'test_*' older than $DAYS day(s)..." + + CUTOFF_EPOCH=$(date -d "$DAYS days ago" +%s) + echo "Cutoff timestamp: $CUTOFF_EPOCH ($(date -d "@$CUTOFF_EPOCH" --utc --iso-8601=seconds))" + + PRS_JSON=$(gh pr list --repo "$GH_REPO" --state open --draft --json number,headRefName,updatedAt) + + echo "$PRS_JSON" | jq -c '.[]' | while read -r pr; do + PR_NUMBER=$(echo "$pr" | jq -r '.number') + HEAD_REF=$(echo "$pr" | jq -r '.headRefName') + UPDATED_AT=$(echo "$pr" | jq -r '.updatedAt') + + # Only target Copybara presubmit branches (prefix test_) + if [[ ! "$HEAD_REF" =~ ^test_ ]]; then + echo "Skipping PR #$PR_NUMBER (head branch '$HEAD_REF' does not match 'test_*')" + continue + fi + + PR_EPOCH=$(date -d "$UPDATED_AT" +%s) + if [ "$PR_EPOCH" -lt "$CUTOFF_EPOCH" ]; then + echo "PR #$PR_NUMBER ($HEAD_REF, updated at $UPDATED_AT) is older than $DAYS day(s)." + if [ "$DRY_RUN" = "true" ]; then + echo "[DRY RUN] Would close PR #$PR_NUMBER and delete branch '$HEAD_REF'" + else + echo "Closing PR #$PR_NUMBER and deleting branch '$HEAD_REF'..." + gh pr close "$PR_NUMBER" --repo "$GH_REPO" --comment "Automatically closing stale presubmit draft PR and cleaning up branch." --delete-branch || \ + gh pr close "$PR_NUMBER" --repo "$GH_REPO" --comment "Automatically closing stale presubmit draft PR." + fi + else + echo "Keeping PR #$PR_NUMBER ($HEAD_REF, updated at $UPDATED_AT) - active within $DAYS day(s)." + fi + done diff --git a/.github/workflows/ios-ci.yml b/.github/workflows/ios-ci.yml new file mode 100644 index 0000000..6c8352a --- /dev/null +++ b/.github/workflows/ios-ci.yml @@ -0,0 +1,45 @@ +# 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. + +name: iOS CI + +on: + pull_request: + branches: [ main ] + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + name: iOS CI / build + # Pinned to macos-15 so the bundled Xcode and iOS Simulator lineup stay stable. + # zizmor: ignore[unpinned-images] + runs-on: macos-15 + timeout-minutes: 30 + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - name: Build and test package + run: | + xcodebuild test \ + -scheme GoogleMapsA2UI \ + -destination 'platform=iOS Simulator,name=iPhone 16' \ + -skipPackagePluginValidation \ + CODE_SIGNING_ALLOWED=NO + working-directory: client/ios/GoogleMapsA2UI diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 8ee8f3b..70ba831 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -22,7 +22,10 @@ on: jobs: build: + name: Python CI / build runs-on: ubuntu-latest + permissions: + contents: read steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: @@ -33,7 +36,7 @@ jobs: enable-cache: auto - name: Set up Python - run: uv python install + run: uv python install 3.13 - name: Install dependencies run: uv sync --all-groups @@ -48,3 +51,7 @@ jobs: run: uv run ruff check . working-directory: agent/python_agent continue-on-error: true + + - name: Run unit tests + run: uv run python -m unittest discover -s . -p "test_*.py" + working-directory: agent/python_agent diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fc62731..165d705 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,6 +18,11 @@ on: workflow_dispatch: + inputs: + dry_run: + description: "Run in dry-run mode (no tags, no publish)" + type: boolean + default: true permissions: contents: write @@ -45,7 +50,7 @@ jobs: - name: Install dependencies working-directory: client/web - run: npm ci + run: npm install - name: Setup Node for Publishing uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 @@ -60,5 +65,15 @@ jobs: NODE_AUTH_TOKEN: ${{ secrets.NPM_WOMBAT_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_WOMBAT_TOKEN }} NODE_PATH: ${{ github.workspace }}/client/web/node_modules - run: npx --prefix client/web semantic-release + DRY_RUN: ${{ inputs.dry_run }} + REF_NAME: ${{ github.ref_name }} + run: | + EXTRA_ARGS="" + if [ "$DRY_RUN" != "false" ]; then + EXTRA_ARGS="--dry-run" + fi + if [ "$REF_NAME" != "main" ]; then + EXTRA_ARGS="$EXTRA_ARGS --branches $REF_NAME" + fi + npx --prefix client/web semantic-release $EXTRA_ARGS diff --git a/.github/workflows/web-ci.yml b/.github/workflows/web-ci.yml index a55bc4d..5272949 100644 --- a/.github/workflows/web-ci.yml +++ b/.github/workflows/web-ci.yml @@ -25,6 +25,7 @@ permissions: jobs: build: + name: Web CI / build # zizmor: ignore[unpinned-images] runs-on: ubuntu-24.04 steps: diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index 8c4f481..5f48f4e 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -1,3 +1,17 @@ +# 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. + name: Zizmor on: @@ -24,3 +38,6 @@ jobs: - name: Run zizmor uses: zizmorcore/zizmor-action@195d10ad90f31d8cd6ea1efd6ecc12969ddbe73f # v0.5.1 + with: + args: --ignore insufficient-cooldown + diff --git a/.releaserc.json b/.releaserc.json index 089c315..15c8354 100644 --- a/.releaserc.json +++ b/.releaserc.json @@ -3,7 +3,34 @@ "main" ], "plugins": [ - "@semantic-release/commit-analyzer", + [ + "@semantic-release/commit-analyzer", + { + "preset": "angular", + "releaseRules": [ + { + "breaking": true, + "release": "patch" + }, + { + "type": "feat", + "release": "patch" + }, + { + "type": "fix", + "release": "patch" + }, + { + "type": "perf", + "release": "patch" + }, + { + "type": "refactor", + "release": "patch" + } + ] + } + ], "@semantic-release/release-notes-generator", "@semantic-release/changelog", [ diff --git a/README.md b/README.md index a153511..b847351 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ This package provides the core Python agent implementations for the Agentic UI T * `agent.py`: Contains the `MAUIAgent` class, which handles session management, LLM interaction, and A2UI schema loading. * `agent_with_grounding.py`: Contains `MAUIAgentWithGrounding`, extending the base agent with Vertex Grounding capabilities. +* `streaming_request_handler.py`: Contains `StreamingRequestHandler` (extending `DefaultRequestHandler`), enabling the A2A server to handle both incremental SSE streaming (`message/stream`) and non-streaming (`message/send`) JSON-RPC requests. * `shared/`: Contains schema extensions (e.g., `maps_catalog_extension.json`). * `skills/`: Contains specific skill definitions used by the agents. * `pyproject.toml`: Configuration file for the package, using Hatchling as the build backend. @@ -185,6 +186,25 @@ class MAUIAgentExecutor(AgentExecutor): raise ServerError(error=UnsupportedOperationError()) ``` +#### 3. Enabling Real-Time SSE Streaming (`StreamingRequestHandler`) + +To serve both real-time Server-Sent Events (`message/stream`) and standard single-response requests (`message/send`) from your `A2AStarletteApplication`, pass `StreamingRequestHandler` as the `http_handler`: + +```python +from a2a.server.apps import A2AStarletteApplication +from a2a.server.tasks import InMemoryTaskStore +from streaming_request_handler import StreamingRequestHandler + +request_handler = StreamingRequestHandler( + agent_executor=agent_executor, + task_store=InMemoryTaskStore(), +) +server = A2AStarletteApplication( + agent_card=default_agent.agent_card, + http_handler=request_handler, +) +``` + ## Agentic UI Toolkit Client Libraries This repository provides A2UI client libraries across multiple platforms (Web, Android, and iOS) to render interactive A2UI surfaces and communicate with an A2A agent server. @@ -254,6 +274,28 @@ return ( ); ``` +#### 3. Streaming Responses (`sendStream`) +`A2UIClient` also supports real-time incremental Server-Sent Events (SSE) streaming via `client.sendStream(messageText)` (`message/stream`). You can use a `useStreaming` boolean flag to switch between streaming (`client.sendStream`) and non-streaming (`client.send`): + +```tsx +const useStreaming = true; // Set to false to use non-streaming client.send() + +async function handleSend(messageText: string) { + renderer.addUserMessage(messageText); + + if (useStreaming) { + // Stream incremental text and A2UI component updates via SSE (message/stream) + for await (const chunk of client.sendStream(messageText)) { + renderer.processResponse([chunk]); + } + } else { + // Wait for the full response in a single payload (message/send) + const response = await client.send(messageText); + renderer.processResponse(response); + } +} +``` + #### Local Development To make changes to this package and test them in an application: @@ -297,7 +339,6 @@ Agentic UI Toolkit requires an API Key to use Google Maps Platform products. To Your API Key must have the following APIs enabled in the [Google Cloud Console](https://console.cloud.google.com/apis/credentials): -* Geocoding API * Maps JavaScript API * Places UI Kit * Routes API @@ -312,15 +353,46 @@ To support the use of Grounding Lite within the Python ADK backend, this API Key Your API Key must also be included when loading the Google Maps JavaScript API code. See the [Google Maps Platform Documentation](https://developers.google.com/maps/documentation/javascript/load-maps-js-api) for instructions on how to load the API, including configuring the API Key. -Agentic UI Toolkit requires features available in the Alpha channel. You must use `v=alpha` when loading the Maps JavaScript API. Learn more about versions in the [Google Maps Platform Documentation](https://developers.google.com/maps/documentation/javascript/versions). - -Use of Agentic UI Toolkit requires several [Maps JavaScript API libraries](https://developers.google.com/maps/documentation/javascript/libraries). When loading the Google Maps JavaScript API, you must include the following libraries: +Agentic UI Toolkit requires features available in the Alpha channel. You must +use `v: "alpha"` when loading the Maps JavaScript API. + +MAUI components are able to dynamically load their required libraries, enabling +both the +[Dynamic Library Import](https://developers.google.com/maps/documentation/javascript/load-maps-js-api#dynamic-library-import) +and +[Script Tag](https://developers.google.com/maps/documentation/javascript/load-maps-js-api#use-legacy-tag) +methods of loading the Maps JavaScript API. + +If you use the script tag, do not load it asynchronously (no `async` attribute +or `loading=async` parameter). MAUI components need `google.maps.importLibrary` +to exist when they first render. An asynchronously loaded script tag only +defines it once the full API has downloaded, even if you list the libraries +below in its `libraries` parameter, so components that render before then will +not appear. + +The specific libraries required by MAUI components are: + +* `maps3d` +* `places` +* `routes` + +Alternatively, web apps that do not load the Maps JavaScript API themselves can +let the web client library load it the first time a component renders. Call +`configureMapsJsApi` once from your app's JavaScript or TypeScript entry point +(for example `main.ts` or `app.ts`), before rendering any components. It is a +module export, so it does not go in an `index.html` ` + -