Skip to content
Merged
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
15 changes: 14 additions & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
# 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.
version: 2
updates:
- package-ecosystem: "npm"
Expand All @@ -20,4 +33,4 @@ updates:
interval: "weekly"
open-pull-requests-limit: 0
cooldown:
default-days: 7
default-days: 7
2 changes: 1 addition & 1 deletion .github/workflows/zizmor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ jobs:
persist-credentials: false

- name: Run zizmor
uses: zizmorcore/zizmor-action@195d10ad90f31d8cd6ea1efd6ecc12969ddbe73f # v0.5.1
uses: zizmorcore/zizmor-action@195d10ad90f31d8cd6ea1efd6ecc12969ddbe73f # v0.5.1
41 changes: 41 additions & 0 deletions agent/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,47 @@ variables to be set:
This will automatically resolve dependencies, install them in a local
virtual environment, and start the A2A server on port 10002.

## Agent Modes & Configuration

The sample server supports 3 agent backends configured via `--agent` or the
`A2UI_DEFAULT_AGENT` environment variable:

Agent Mode | CLI Flag / Env Option | Description
:------------------------------- | :---------------------------------------------------- | :----------
**Template Agent (Recommended)** | `--agent TEMPLATE`<br>`A2UI_DEFAULT_AGENT=TEMPLATE` | Low-latency template agent with fast intent classification (`LOCAL_SEARCH`, `DIRECTIONS`) and structured parameter merging.
**Base Agent** | `--agent BASE`<br>`A2UI_DEFAULT_AGENT=BASE` | Standard dynamic UI agent generating unconstrained A2UI component trees.
**Grounding Agent** | `--agent GROUNDING`<br>`A2UI_DEFAULT_AGENT=GROUNDING` | Vertex AI Maps Grounding agent.

### Running with Template Agent

```bash
A2UI_DEFAULT_AGENT=TEMPLATE \
GEMINI_API_KEY="<YOUR_KEY>" \
GOOGLE_MAPS_API_KEY="<YOUR_KEY>" \
uv run python __main__.py --host 127.0.0.1 --port 10002
```

### Multi-Agent Query Prefixes

You can test specific agent implementations against a running server using
prompt prefixes:

* `[TEMPLATE] <query>` ➔ Routes directly to `MAUIAgentWithTemplates` (e.g.
`[TEMPLATE] Coffee shops near Pike Place`).
* `[GROUNDING] <query>` ➔ Routes directly to `MAUIAgentWithGrounding` (e.g.
`[GROUNDING] Hotels in Bellevue`).
* `<query>` (no prefix) ➔ Routes to the configured default agent.

### Fallback Modes

Set `A2UI_FALLBACK_MODE` to control behavior when a query cannot be fulfilled by
a static template:

* `A2UI_FALLBACK_MODE=TEXT` (Default) ➔ Fast grounded plain text / markdown
response with Grounding Lite tool assistance.
* `A2UI_FALLBACK_MODE=DYNAMIC` ➔ Falls back to full dynamic multi-turn A2UI
component generation.

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

Expand Down
59 changes: 51 additions & 8 deletions agent/python/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,17 @@
from a2a.server.apps import A2AStarletteApplication
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.tasks import InMemoryTaskStore
from a2a.types import AgentCapabilities, AgentCard, AgentSkill
import click
import dotenv
from starlette.middleware.cors import CORSMiddleware
from starlette.responses import RedirectResponse
from starlette.staticfiles import StaticFiles
import uvicorn

from python_agent.agent import MAUIAgent
from python_agent.agent_with_grounding import MAUIAgentWithGrounding
from agent import MAUIAgent
from agent_config import AgentConfig, FallbackMode
from agent_with_grounding import MAUIAgentWithGrounding
from agent_with_templates import MAUIAgentWithTemplates
from agent_executor import MAUIAgentExecutor

dotenv.load_dotenv()
Expand All @@ -43,7 +45,18 @@ class MissingAPIKeyError(Exception):
@click.option("--serverurl", default="")
@click.option("--host", default="0.0.0.0")
@click.option("--port", default=10002)
def main(serverurl, host, port):
@click.option(
"--agent",
default="MAUIAgent",
show_default=True,
envvar="A2UI_DEFAULT_AGENT",
help=(
"Agent to use as default. Accepts class name (e.g., 'MAUIAgent',"
" 'MAUIAgentWithTemplates', 'MAUIAgentWithGrounding') or shorthand"
" ('BASE', 'TEMPLATE', 'GROUNDING')."
),
)
def main(serverurl, host, port, agent):
try:
# Check for API key only if Vertex AI is not configured
if not os.getenv("GOOGLE_GENAI_USE_VERTEXAI") == "TRUE":
Expand All @@ -58,27 +71,57 @@ def main(serverurl, host, port):
if serverurl != "":
base_url = serverurl

fallback_mode_env = os.getenv("A2UI_FALLBACK_MODE")
if fallback_mode_env:
config = AgentConfig(fallback_mode=FallbackMode(fallback_mode_env))
else:
config = AgentConfig()
logger.info(f"Using fallback_mode: {config.fallback_mode}")

ui_agent = MAUIAgent(base_url=base_url)
grounding_agent = MAUIAgentWithGrounding(base_url=base_url)
template_agent = MAUIAgentWithTemplates(base_url=base_url, config=config)

agent_map = {
"MAUIAGENT": ui_agent,
"BASE": ui_agent,
"MAUIAGENTWITHGROUNDING": grounding_agent,
"GROUNDING": grounding_agent,
"MAUIAGENTWITHTEMPLATES": template_agent,
"TEMPLATE": template_agent,
}

normalized_agent = agent.upper()
if normalized_agent not in agent_map:
raise ValueError(
f"Unknown agent: {agent}. Expected one of {list(agent_map.keys())}"
)

default_agent = agent_map[normalized_agent]
logger.info(
f"--- SERVER: Binding {default_agent.__class__.__name__} as default"
" agent ---"
)

agent_executor = MAUIAgentExecutor(
default_agent=ui_agent, grounding_agent=grounding_agent
default_agent=default_agent,
grounding_agent=grounding_agent,
template_agent=template_agent,
)

request_handler = DefaultRequestHandler(
agent_executor=agent_executor,
task_store=InMemoryTaskStore(),
)
server = A2AStarletteApplication(
agent_card=ui_agent.agent_card, http_handler=request_handler
agent_card=default_agent.agent_card, http_handler=request_handler
)
import uvicorn

app = server.build()

app.add_middleware(
CORSMiddleware,
allow_origin_regex=r"http://localhost:\d+",
allow_origin_regex=r"http://(localhost|127\.0\.0\.1):\d+",
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
Expand Down
21 changes: 18 additions & 3 deletions agent/python/agent_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# limitations under the License.

import logging
from typing import Optional

from a2a.server.agent_execution import AgentExecutor, RequestContext
from a2a.server.events import EventQueue
Expand All @@ -33,8 +34,9 @@
from a2a.utils.errors import ServerError

from a2ui.a2a.extension import try_activate_a2ui_extension
from python_agent.agent import MAUIAgent
from python_agent.agent_with_grounding import MAUIAgentWithGrounding
from agent import MAUIAgent
from agent_with_grounding import MAUIAgentWithGrounding
from agent_with_templates import MAUIAgentWithTemplates

logger = logging.getLogger(__name__)

Expand All @@ -43,10 +45,14 @@ class MAUIAgentExecutor(AgentExecutor):
"""MAUI AgentExecutor Example."""

def __init__(
self, default_agent: MAUIAgent, grounding_agent: MAUIAgentWithGrounding
self,
default_agent: MAUIAgent,
grounding_agent: MAUIAgentWithGrounding,
template_agent: Optional[MAUIAgentWithTemplates] = None,
):
self._default_agent = default_agent
self._grounding_agent = grounding_agent
self._template_agent = template_agent

async def execute(
self,
Expand Down Expand Up @@ -95,6 +101,15 @@ async def execute(
)
agent_to_use = self._grounding_agent
query = query[len("[GROUNDING]") :].strip()
elif query.startswith("[TEMPLATE]"):
if not self._template_agent:
raise UnsupportedOperationError("Template Agent is not configured.")
logger.info(
"--- AGENT_EXECUTOR: Prefix [TEMPLATE] detected. Using Template"
" Agent. ---"
)
agent_to_use = self._template_agent
query = query[len("[TEMPLATE]") :].strip()
else:
logger.info(
"--- AGENT_EXECUTOR: No prefix detected. Using Default Agent. ---"
Expand Down
6 changes: 3 additions & 3 deletions agent/python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ description = "Sample agent consuming MAUI packages"
requires-python = ">=3.13"
dependencies = [
"maui-a2ui-python",
"google-adk[a2a,extensions]>=1.28.0,<2.0.0",
"a2a-sdk[http-server]>=0.3.0",
"google-genai>=1.64.0",
"google-adk[a2a,extensions,mcp]>=2.0.0",
"a2a-sdk[http-server]>=0.3.0,<1.0.0",
"google-genai>=2.0.0",
"jsonschema>=4.0.0"
]

Expand Down
20 changes: 19 additions & 1 deletion agent/python/setup.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,18 @@
#!/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.

# Check if path argument is provided
if [ -z "$1" ]; then
Expand All @@ -18,6 +32,10 @@ fi

# Replace path and uncomment line if needed
# Using | as delimiter for sed to handle slashes in paths
sed -i '' "s|^ *#* *maui-a2ui-python = { path = [^,}]*|maui-a2ui-python = { path = \"$MAUI_PATH\"|" "$FILE"
if [[ "$OSTYPE" == "darwin"* ]]; then
sed -i '' "s|^ *#* *maui-a2ui-python = { path = [^,}]*|maui-a2ui-python = { path = \"$MAUI_PATH\"|" "$FILE"
else
sed -i "s|^ *#* *maui-a2ui-python = { path = [^,}]*|maui-a2ui-python = { path = \"$MAUI_PATH\"|" "$FILE"
fi

echo "Updated $FILE with path: $MAUI_PATH"
23 changes: 23 additions & 0 deletions client/android/app/src/main/java/com/example/maui/AgentType.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// 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.
//

package com.example.maui

enum class AgentType {
LITE,
VERTEX,
TEMPLATE,
}
20 changes: 13 additions & 7 deletions client/android/app/src/main/java/com/example/maui/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,19 @@ class MainActivity : AppCompatActivity() {
buttonSend.setOnClickListener {
val messageText = editTextMessage.text.toString().trim()
if (messageText.isNotEmpty()) {
val radioGroundingVertex =
findViewById<android.widget.RadioButton>(R.id.radioGroundingVertex)
viewModel.sendMessage(
messageText,
radioGroundingVertex.isChecked,
switchCannedServer.isChecked,
)
val radioAgentVertex = findViewById<android.widget.RadioButton>(R.id.radioAgentVertex)
val radioAgentTemplate = findViewById<android.widget.RadioButton>(R.id.radioAgentTemplate)

val agentType =
if (radioAgentVertex.isChecked) {
com.example.maui.AgentType.VERTEX
} else if (radioAgentTemplate.isChecked) {
com.example.maui.AgentType.TEMPLATE
} else {
com.example.maui.AgentType.LITE
}

viewModel.sendMessage(messageText, agentType, switchCannedServer.isChecked)
editTextMessage.text.clear()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,21 @@ class ChatViewModel(
resourceLogger.startLogging(viewModelScope)
}

fun sendMessage(text: String, isGrounding: Boolean = false, bypassCanned: Boolean = false) {
fun sendMessage(
text: String,
agentType: com.example.maui.AgentType = com.example.maui.AgentType.LITE,
bypassCanned: Boolean = false,
) {
currentRequestJob?.cancel()
currentAgentTextIndex = null
currentAgentA2UIIndex = null
val serverMessageText = if (isGrounding) "[GROUNDING] $text" else text
val serverMessageText =
when (agentType) {
com.example.maui.AgentType.VERTEX -> "[GROUNDING] $text"
com.example.maui.AgentType.TEMPLATE -> "[TEMPLATE] $text"
com.example.maui.AgentType.LITE -> text
}

addMessage(ChatMessage.Text(text, true))
val jsonObject =
JSONObject().apply {
Expand Down
14 changes: 10 additions & 4 deletions client/android/app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
android:id="@+id/promptsLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/groundingRadioGroup"
android:layout_above="@+id/agentRadioGroup"
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="8dp">
Expand All @@ -53,7 +53,7 @@
</LinearLayout>

<RadioGroup
android:id="@+id/groundingRadioGroup"
android:id="@+id/agentRadioGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/inputLayout"
Expand All @@ -63,17 +63,23 @@
android:background="?android:attr/windowBackground">

<RadioButton
android:id="@+id/radioGroundingLite"
android:id="@+id/radioAgentLite"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Grounding Lite (MCP)"
android:checked="true" />

<RadioButton
android:id="@+id/radioGroundingVertex"
android:id="@+id/radioAgentVertex"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Grounding with Google Maps (Vertex)" />

<RadioButton
android:id="@+id/radioAgentTemplate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Template Agent" />
</RadioGroup>

<LinearLayout
Expand Down
Loading
Loading