diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f266a80..ad53b5f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -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" @@ -20,4 +33,4 @@ updates: interval: "weekly" open-pull-requests-limit: 0 cooldown: - default-days: 7 + default-days: 7 \ No newline at end of file diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index 8c4f481..1d45775 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -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 \ No newline at end of file diff --git a/agent/python/__main__.py b/agent/python/__main__.py index 9c49718..79e509e 100644 --- a/agent/python/__main__.py +++ b/agent/python/__main__.py @@ -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() @@ -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": @@ -58,11 +71,42 @@ 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( @@ -70,9 +114,8 @@ def main(serverurl, host, port): 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() diff --git a/agent/python/agent_executor.py b/agent/python/agent_executor.py index 073c652..51a3d52 100644 --- a/agent/python/agent_executor.py +++ b/agent/python/agent_executor.py @@ -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 @@ -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__) @@ -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, @@ -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. ---" diff --git a/client/android/app/src/main/java/com/example/maui/AgentType.kt b/client/android/app/src/main/java/com/example/maui/AgentType.kt new file mode 100644 index 0000000..fff6fcc --- /dev/null +++ b/client/android/app/src/main/java/com/example/maui/AgentType.kt @@ -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, +} diff --git a/client/android/app/src/main/java/com/example/maui/MainActivity.kt b/client/android/app/src/main/java/com/example/maui/MainActivity.kt index be93eeb..1b44f26 100644 --- a/client/android/app/src/main/java/com/example/maui/MainActivity.kt +++ b/client/android/app/src/main/java/com/example/maui/MainActivity.kt @@ -128,13 +128,19 @@ class MainActivity : AppCompatActivity() { buttonSend.setOnClickListener { val messageText = editTextMessage.text.toString().trim() if (messageText.isNotEmpty()) { - val radioGroundingVertex = - findViewById(R.id.radioGroundingVertex) - viewModel.sendMessage( - messageText, - radioGroundingVertex.isChecked, - switchCannedServer.isChecked, - ) + val radioAgentVertex = findViewById(R.id.radioAgentVertex) + val radioAgentTemplate = findViewById(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() } } diff --git a/client/android/app/src/main/java/com/example/maui/ui/ChatViewModel.kt b/client/android/app/src/main/java/com/example/maui/ui/ChatViewModel.kt index d6f4396..5a1b746 100644 --- a/client/android/app/src/main/java/com/example/maui/ui/ChatViewModel.kt +++ b/client/android/app/src/main/java/com/example/maui/ui/ChatViewModel.kt @@ -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 { diff --git a/client/android/app/src/main/res/layout/activity_main.xml b/client/android/app/src/main/res/layout/activity_main.xml index 60472d5..7891a55 100644 --- a/client/android/app/src/main/res/layout/activity_main.xml +++ b/client/android/app/src/main/res/layout/activity_main.xml @@ -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"> @@ -53,7 +53,7 @@ + + 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 > @@ -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) diff --git a/client/ios/ChatView.swift b/client/ios/ChatView.swift index 598b223..aca9758 100644 --- a/client/ios/ChatView.swift +++ b/client/ios/ChatView.swift @@ -85,7 +85,7 @@ struct ChatView: View { } } - GroundingSelector(selection: $viewModel.selectedGroundingType) + AgentSelector(selection: $viewModel.selectedAgentType) Divider() @@ -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 diff --git a/client/ios/ChatViewModel.swift b/client/ios/ChatViewModel.swift index c38709b..e6bf452 100644 --- a/client/ios/ChatViewModel.swift +++ b/client/ios/ChatViewModel.swift @@ -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" @@ -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) } diff --git a/client/ios/Models.swift b/client/ios/Models.swift index 0361325..41ca1a7 100644 --- a/client/ios/Models.swift +++ b/client/ios/Models.swift @@ -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 } }