Skip to content
Open
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
53 changes: 48 additions & 5 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_config import AgentConfig, FallbackMode
from python_agent.agent_with_grounding import MAUIAgentWithGrounding
from python_agent.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,21 +71,51 @@ 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()

Expand Down
17 changes: 16 additions & 1 deletion 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 @@ -35,6 +36,7 @@
from a2ui.a2a.extension import try_activate_a2ui_extension
from python_agent.agent import MAUIAgent
from python_agent.agent_with_grounding import MAUIAgentWithGrounding
from python_agent.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
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
22 changes: 15 additions & 7 deletions client/ios/ChatService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ import Foundation
import GoogleMapsA2UI

protocol ChatServiceProtocol {
func sendMessage(text: String, isVertex: Bool) async throws -> AsyncThrowingStream<
ParsedA2AEvent, Swift.Error
>
func sendMessage(text: String, agentType: AgentType) async throws
-> AsyncThrowingStream<
ParsedA2AEvent, Swift.Error
>
func sendAction(jsonString: String) async throws -> AsyncThrowingStream<
ParsedA2AEvent, Swift.Error
>
Expand Down Expand Up @@ -67,12 +68,19 @@ actor ChatService: ChatServiceProtocol {
private var useSSEProtocol = false
private let contextID = UUID().uuidString

func sendMessage(text: String, isVertex: Bool) async throws -> AsyncThrowingStream<
ParsedA2AEvent, Swift.Error
> {
func sendMessage(text: String, agentType: AgentType) async throws
-> AsyncThrowingStream<
ParsedA2AEvent, Swift.Error
>
{
var serverText = text
if isVertex {
switch agentType {
case .vertex:
serverText = "[GROUNDING] \(text)"
case .template:
serverText = "[TEMPLATE] \(text)"
case .lite:
break
}
let payload: [String: Any] = ["text": serverText]
return try await callPythonServer(userMessage: payload)
Expand Down
8 changes: 4 additions & 4 deletions client/ios/ChatView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ struct ChatView: View {
}
}

GroundingSelector(selection: $viewModel.selectedGroundingType)
AgentSelector(selection: $viewModel.selectedAgentType)

Divider()

Expand Down Expand Up @@ -212,12 +212,12 @@ struct LoadingBubble: View {
}
}

struct GroundingSelector: View {
@Binding var selection: GroundingType
struct AgentSelector: View {
@Binding var selection: AgentType

var body: some View {
VStack(alignment: .leading, spacing: 12) {
ForEach(GroundingType.allCases) { type in
ForEach(AgentType.allCases) { type in
Button(action: {
withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
selection = type
Expand Down
7 changes: 4 additions & 3 deletions client/ios/ChatViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class ChatViewModel: ObservableObject {
@Published private(set) var messages: [ChatMessage] = []
@Published private(set) var isLoading: Bool = false
@Published var webViewToScrollID: UUID?
@Published var selectedGroundingType: GroundingType = .lite
@Published var selectedAgentType: AgentType = .lite

private let chatService: ChatServiceProtocol
private let googleMapsApiKey = "$GOOGLE_MAPS_API_KEY"
Expand All @@ -39,13 +39,14 @@ class ChatViewModel: ObservableObject {

/// Sends a text message to the server.
func sendMessage(text: String) {
let isVertex = (selectedGroundingType == .vertex)
let agentType = selectedAgentType
addMessage(.text(content: text, isUser: true))

Task {
isLoading = true
do {
let stream = try await chatService.sendMessage(text: text, isVertex: isVertex)
let stream = try await chatService.sendMessage(
text: text, agentType: agentType)
for try await part in stream {
handleParsedEvent(part)
}
Expand Down
3 changes: 2 additions & 1 deletion client/ios/Models.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@
import Foundation
import SwiftUI

enum GroundingType: String, CaseIterable, Identifiable {
enum AgentType: String, CaseIterable, Identifiable {
case lite = "Grounding Lite (MCP)"
case vertex = "Grounding with Google Maps (Vertex)"
case template = "Template Agent"

var id: Self { self }
}
Expand Down
Loading