Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions .github/workflows/cleanup-stale-prs.yml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions .github/workflows/python-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/web-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ on:

jobs:
build:
name: Web CI / build
runs-on: ubuntu-latest
permissions:
contents: read
Expand Down
18 changes: 17 additions & 1 deletion .github/workflows/zizmor.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -23,4 +37,6 @@ jobs:
persist-credentials: false

- name: Run zizmor
uses: zizmorcore/zizmor-action@195d10ad90f31d8cd6ea1efd6ecc12969ddbe73f # v0.5.1
uses: zizmorcore/zizmor-action@195d10ad90f31d8cd6ea1efd6ecc12969ddbe73f # v0.5.1
with:
args: --ignore insufficient-cooldown
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,18 @@ The iOS sample is a native app utilizing SwiftUI and the [GoogleMapsA2UI(iOS)](h

For more server configration and other detailed setup, refer to the [iOS Sample README](client/ios/README.md).

#### Streaming vs. Non-Streaming Responses (`useStreaming`)

All three sample clients (Web, Android, and iOS) default to **SSE streaming** (`useStreaming = true`) so conversational text and A2UI map components render progressively as they arrive from the agent (`message/stream`).

You can switch between **Streaming** (`message/stream` / `client.sendStream()`) and **Non-Streaming** (`message/send` / `client.send()`) at any time:

* **In the UI**: Toggle the **Streaming** switch/checkbox in the sample app header or control bar.
* **In Code (Default Configuration)**:
* **Web (React)**: Set `DEFAULT_USE_STREAMING = true` (or `false`) in [`client/web/react/src/App.tsx`](client/web/react/src/App.tsx).
* **Android**: Set `DEFAULT_USE_STREAMING = true` (or `false`) in [`ChatRepository.kt`](client/android/app/src/main/java/com/example/maui/data/ChatRepository.kt).
* **iOS**: Set `defaultUseStreaming: Bool = true` (or `false`) in [`ChatService.swift`](client/ios/ChatService.swift).

## Google API Keys

### Google Maps API Key
Expand All @@ -161,7 +173,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
Expand Down
4 changes: 4 additions & 0 deletions agent/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ a static template:
* `A2UI_FALLBACK_MODE=DYNAMIC` ➔ Falls back to full dynamic multi-turn A2UI
component generation.

### Streaming Support (`message/stream` & `message/send`)

The sample server uses `StreamingRequestHandler` by default, enabling both real-time Server-Sent Events (`message/stream`) and single-response JSON-RPC requests (`message/send`). Client applications across Web, Android, and iOS can toggle between streaming and non-streaming modes at runtime using their `useStreaming` toggle without restarting the server.

To run the frontend, follow the instructions in
[../../client/web/react/README.md](../../client/web/react/README.md)

Expand Down
19 changes: 13 additions & 6 deletions agent/python/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import os

from a2a.server.apps import A2AStarletteApplication
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.request_handlers import DefaultRequestHandler # pylint: disable=unused-import
from a2a.server.tasks import InMemoryTaskStore
import click
import dotenv
Expand All @@ -29,6 +29,7 @@
from agent_config import AgentConfig, FallbackMode
from agent_with_grounding import MAUIAgentWithGrounding
from agent_with_templates import MAUIAgentWithTemplates
from streaming_request_handler import StreamingRequestHandler
from agent_executor import MAUIAgentExecutor

dotenv.load_dotenv()
Expand All @@ -43,7 +44,7 @@ class MissingAPIKeyError(Exception):

@click.command()
@click.option("--serverurl", default="")
@click.option("--host", default="0.0.0.0")
@click.option("--host", default="::")
@click.option("--port", default=10002)
@click.option(
"--agent",
Expand All @@ -66,10 +67,14 @@ def main(serverurl, host, port, agent):
" GOOGLE_GENAI_USE_VERTEXAI is not TRUE."
)

base_url = f"http://{host}:{port}"

if serverurl != "":
base_url = serverurl
elif host in ("0.0.0.0", "::"):
base_url = f"http://localhost:{port}"
elif ":" in host and not (host.startswith("[") and host.endswith("]")):
base_url = f"http://[{host}]:{port}"
else:
base_url = f"http://{host}:{port}"

fallback_mode_env = os.getenv("A2UI_FALLBACK_MODE")
if fallback_mode_env:
Expand Down Expand Up @@ -107,9 +112,10 @@ def main(serverurl, host, port, agent):
default_agent=default_agent,
grounding_agent=grounding_agent,
template_agent=template_agent,
ui_agent=ui_agent,
)

request_handler = DefaultRequestHandler(
request_handler = StreamingRequestHandler(
agent_executor=agent_executor,
task_store=InMemoryTaskStore(),
)
Expand All @@ -121,10 +127,11 @@ def main(serverurl, host, port, agent):

app.add_middleware(
CORSMiddleware,
allow_origin_regex=r"http://(localhost|127\.0\.0\.1):\d+",
allow_origin_regex=r"https?://.*",
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
allow_private_network=True,
)

logger.info(f"Starting A2A server on {host}:{port}")
Expand Down
10 changes: 9 additions & 1 deletion agent/python/agent_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,12 @@ def __init__(
default_agent: MAUIAgent,
grounding_agent: MAUIAgentWithGrounding,
template_agent: Optional[MAUIAgentWithTemplates] = None,
ui_agent: Optional[MAUIAgent] = None,
):
self._default_agent = default_agent
self._grounding_agent = grounding_agent
self._template_agent = template_agent
self._ui_agent = ui_agent or default_agent

async def execute(
self,
Expand Down Expand Up @@ -94,7 +96,13 @@ async def execute(

# Interpret prefix and choose agent
agent_to_use = self._default_agent
if query.startswith("[GROUNDING]"):
if query.startswith("[MCP]"):
logger.info(
"--- AGENT_EXECUTOR: Prefix [MCP] detected. Using UI (MCP) Agent. ---"
)
agent_to_use = self._ui_agent
query = query[len("[MCP]") :].strip()
elif query.startswith("[GROUNDING]"):
logger.info(
"--- AGENT_EXECUTOR: Prefix [GROUNDING] detected. Using Grounding"
" Agent. ---"
Expand Down
4 changes: 4 additions & 0 deletions client/android/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ In `app/src/main/java/com/example/maui/MainActivity.kt`, verify the flags match
* `DeviceType.PHYSICAL`: Use when testing on real Android devices. *(Note: If using a local Demo Server on a physical device, run `adb reverse tcp:10002 tcp:10002`)*
* `DeviceType.EMULATOR`: Use when testing on emulators.

#### Streaming vs. Non-Streaming (`useStreaming`)
* By default, the app uses SSE streaming (`ChatRepository.DEFAULT_USE_STREAMING = true` in `ChatRepository.kt`, sending JSON-RPC `message/stream`).
* You can toggle between streaming (`message/stream`) and non-streaming (`message/send`) at runtime using the **Streaming** switch in the control bar (`MainActivity.kt`), or change `DEFAULT_USE_STREAMING` in `ChatRepository.kt`.

### 4. Build and Run the App

1. Build and install the app (Debug version):
Expand Down
21 changes: 17 additions & 4 deletions client/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ android {

buildFeatures {
buildConfig true
compose true
}

signingConfigs {
Expand Down Expand Up @@ -74,17 +75,29 @@ kotlin {
}

dependencies {
def composeBom = platform('androidx.compose:compose-bom:2024.02.00')
implementation composeBom
androidTestImplementation composeBom
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-graphics'
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'androidx.compose.material3:material3'
implementation 'androidx.activity:activity-compose:1.8.2'
debugImplementation 'androidx.compose.ui:ui-tooling'
debugImplementation 'androidx.compose.ui:ui-test-manifest'
def lifecycleVersion = '2.8.7'
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-runtime-compose:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"

implementation 'androidx.tracing:tracing-ktx:1.2.0'
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7'
implementation 'com.squareup.okhttp3:okhttp:5.3.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
implementation 'com.google.android.libraries.mapsplatform.a2ui:GoogleMapsA2UI:0.1.0'
}
2 changes: 0 additions & 2 deletions client/android/app/src/androidTest/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.maui.test">



<instrumentation
android:name="androidx.test.runner.AndroidJUnitRunner"
android:targetPackage="com.example.maui"
Expand Down
Loading
Loading