diff --git a/agent_sdks/kotlin/.gitignore b/agent_sdks/kotlin/.gitignore new file mode 100644 index 000000000..b26b35b0e --- /dev/null +++ b/agent_sdks/kotlin/.gitignore @@ -0,0 +1,128 @@ +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Gradle template +.gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Avoid ignore Gradle wrappper properties +!gradle-wrapper.properties + +# Cache of project +.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +### Kotlin template +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + diff --git a/agent_sdks/kotlin/build.gradle.kts b/agent_sdks/kotlin/build.gradle.kts new file mode 100644 index 000000000..e65b33b0d --- /dev/null +++ b/agent_sdks/kotlin/build.gradle.kts @@ -0,0 +1,99 @@ +/* + * 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 + * + * https://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. + */ + +plugins { + kotlin("jvm") version "2.1.10" + kotlin("plugin.serialization") version "2.1.10" + id("java-library") + id("com.ncorti.ktfmt.gradle") version "0.19.0" + id("org.jetbrains.kotlinx.kover") version "0.9.1" +} + +ktfmt { + googleStyle() +} + +version = "0.1.0" +group = "com.google.a2ui" + +kotlin { + jvmToolchain(21) +} + +repositories { + mavenCentral() +} + +dependencies { + api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") + implementation("com.networknt:json-schema-validator:1.5.1") + implementation("com.fasterxml.jackson.core:jackson-databind:2.17.2") + + // Core Dependencies + api("com.google.adk:google-adk:0.9.0") + api("com.google.adk:google-adk-a2a:0.9.0") + api("io.github.a2asdk:a2a-java-sdk-client:1.0.0.Alpha3") + api("com.google.genai:google-genai:1.43.0") + + testImplementation(kotlin("test")) + testImplementation("io.mockk:mockk:1.13.11") +} + +tasks.test { + useJUnitPlatform() +} + +val copySpecs by tasks.registering(Copy::class) { + val repoRoot = findRepoRoot() + + from(File(repoRoot, "specification/v0_8/json/server_to_client.json")) { + into("com/google/a2ui/assets/0.8") + } + from(File(repoRoot, "specification/v0_8/json/standard_catalog_definition.json")) { + into("com/google/a2ui/assets/0.8") + } + + from(File(repoRoot, "specification/v0_9/json/server_to_client.json")) { + into("com/google/a2ui/assets/0.9") + } + from(File(repoRoot, "specification/v0_9/json/common_types.json")) { + into("com/google/a2ui/assets/0.9") + } + from(File(repoRoot, "specification/v0_9/json/basic_catalog.json")) { + into("com/google/a2ui/assets/0.9") + } + + into(layout.buildDirectory.dir("generated/resources/specs")) +} + +sourceSets { + main { + resources { + srcDir(copySpecs) + } + } +} + +fun findRepoRoot(): File { + var currentDir = project.projectDir + while (currentDir != null) { + if (File(currentDir, ".git").exists()) { + return currentDir + } + currentDir = currentDir.parentFile + } + throw GradleException("Could not find repository root.") +} diff --git a/agent_sdks/kotlin/gradle/wrapper/gradle-wrapper.properties b/agent_sdks/kotlin/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..dbc3ce4a0 --- /dev/null +++ b/agent_sdks/kotlin/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/agent_sdks/kotlin/gradlew b/agent_sdks/kotlin/gradlew new file mode 100755 index 000000000..adff685a0 --- /dev/null +++ b/agent_sdks/kotlin/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# 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 +# +# https://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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/agent_sdks/kotlin/gradlew.bat b/agent_sdks/kotlin/gradlew.bat new file mode 100644 index 000000000..c4bdd3ab8 --- /dev/null +++ b/agent_sdks/kotlin/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/agent_sdks/kotlin/settings.gradle.kts b/agent_sdks/kotlin/settings.gradle.kts new file mode 100644 index 000000000..5a01945cb --- /dev/null +++ b/agent_sdks/kotlin/settings.gradle.kts @@ -0,0 +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 + * + * https://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. + */ + +rootProject.name = "a2ui-agent" diff --git a/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/a2a/A2aHandler.kt b/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/a2a/A2aHandler.kt new file mode 100644 index 000000000..0f6962cd1 --- /dev/null +++ b/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/a2a/A2aHandler.kt @@ -0,0 +1,253 @@ +/* + * 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 + * + * https://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.google.a2ui.a2a + +import com.google.a2ui.core.parser.hasA2uiParts +import com.google.a2ui.core.parser.parseResponseToParts +import com.google.adk.agents.RunConfig +import com.google.adk.events.Event +import com.google.adk.runner.Runner +import com.google.adk.sessions.Session +import com.google.adk.sessions.SessionKey +import com.google.genai.types.Content +import com.google.genai.types.Part +import java.util.Optional +import java.util.UUID +import java.util.logging.Logger +import kotlinx.serialization.json.* + +/** + * Simplifies implementing the Agent-to-App (A2A) protocol for ADK agents without needing heavy + * server dependencies. + */ +class A2aHandler(private val runner: Runner) { + + /** Handles the /.well-known/agent-card.json HTTP GET request. */ + fun handleAgentCardGet( + agentName: String, + serverUrl: String, + supportedCatalogIds: List = emptyList(), + ): Map { + return mapOf( + "name" to agentName, + "url" to serverUrl, + "endpoints" to mapOf("chat" to serverUrl), + "capabilities" to + mapOf( + "streaming" to true, + "extensions" to + listOf( + mapOf( + "uri" to A2uiA2a.A2UI_EXTENSION_URI, + "params" to mapOf("supportedCatalogIds" to supportedCatalogIds), + ) + ), + ), + ) + } + + /** Handles the /a2a HTTP POST JSON-RPC request. */ + @JvmOverloads + fun handleA2aPost( + requestBody: Map, + sessionPreparer: ((Session, Map<*, *>) -> Unit)? = null, + ): Map { + val method = requestBody["method"] as? String + val id = requestBody["id"] ?: "" + val response = mutableMapOf("jsonrpc" to "2.0", "id" to id) + + try { + if (method == "a2a.agent.card.get") { + response["result"] = handleAgentCardGet(runner.appName(), "/a2a") + } else if (method == "a2a.agent.invoke" || method == "message/send") { + val params = requestBody["params"] as? Map<*, *> + val messageMap = params?.get("message") as? Map<*, *> + + if (messageMap == null) { + response["error"] = mapOf("code" to -32602, "message" to "Invalid params") + return response + } + + val contextId = messageMap["contextId"] as? String ?: DEFAULT_CONTEXT_ID + val sessionId = contextId + val userId = A2A_USER_ID + + val content = extractUserContent(messageMap) + val session = getOrCreateSession(userId, sessionId) + + sessionPreparer?.invoke(session, requestBody) + + val events = + runner.runAsync(session, content, RunConfig.builder().build()).toList().blockingGet() + + val allParts = translateEventsToA2aParts(events) + response["result"] = createFinalMessage(contextId, events, allParts) + } else { + response["error"] = mapOf("code" to -32601, "message" to "Method not found") + } + } catch (e: Exception) { + logger.severe(e.message) + response["error"] = mapOf("code" to -32000, "message" to (e.message ?: "Unknown error")) + } + + return response + } + + private fun extractUserContent(messageMap: Map<*, *>): Content { + val partsList = messageMap["parts"] as? List<*> ?: emptyList() + val userText = + partsList + .filterIsInstance>() + .filter { it["kind"] == "text" } + .joinToString(separator = "") { it["text"] as? String ?: "" } + return Content.builder() + .role("user") + .parts(listOf(Part.builder().text(userText).build())) + .build() + } + + private fun getOrCreateSession(userId: String, sessionId: String): Session { + var session = + runner + .sessionService() + .getSession(runner.appName(), userId, sessionId, Optional.empty()) + .blockingGet() + if (session == null) { + session = + runner + .sessionService() + .createSession(SessionKey(runner.appName(), userId, sessionId)) + .blockingGet() + } + return session + } + + private fun translateEventsToA2aParts(events: List<*>): List> { + return events.filterIsInstance().flatMap { event -> + if (event.content().isPresent && event.content().get().parts().isPresent) { + event.content().get().parts().get().flatMap { part -> processPart(part) } + } else { + emptyList() + } + } + } + + private fun processPart(part: Part): List> { + val parsedParts = mutableListOf>() + + val functionCall = part.functionCall().orElse(null) + if (functionCall != null && functionCall.name().orElse(null) == "send_a2ui_json_to_client") { + val argsMap = functionCall.args().orElse(null) as? Map<*, *> + val a2uiJsonStr = argsMap?.get("a2ui_json") as? String + if (a2uiJsonStr != null) { + processA2uiJsonFunctionArg(a2uiJsonStr, parsedParts) + } + } + + val text = part.text().orElse("")?.trim() ?: "" + if (text.isEmpty()) return parsedParts + + if (hasA2uiParts(text)) { + processA2uiTextParts(text, parsedParts) + } else { + parsedParts.add(mapOf("kind" to "text", "text" to text)) + } + + return parsedParts + } + + private fun processA2uiJsonFunctionArg( + a2uiJsonStr: String, + parsedParts: MutableList>, + ) { + try { + val element = Json.parseToJsonElement(a2uiJsonStr) + val data = jsonElementToAny(element) + if (data is Map<*, *> || data is List<*>) { + parsedParts.add( + mapOf( + "kind" to "data", + "metadata" to mapOf(A2uiA2a.MIME_TYPE_KEY to A2uiA2a.A2UI_MIME_TYPE), + "data" to data, + ) + ) + } + } catch (e: Exception) { + logger.severe(e.message) + } + } + + private fun processA2uiTextParts(text: String, parsedParts: MutableList>) { + try { + val responseParts = parseResponseToParts(text) + for (responsePart in responseParts) { + if (responsePart.text.isNotBlank()) { + parsedParts.add(mapOf("kind" to "text", "text" to responsePart.text.trim())) + } + + responsePart.a2uiJson?.forEach { element -> + val data = jsonElementToAny(element) + if (data != null) { + parsedParts.add( + mapOf( + "kind" to "data", + "metadata" to mapOf(A2uiA2a.MIME_TYPE_KEY to A2uiA2a.A2UI_MIME_TYPE), + "data" to data, + ) + ) + } + } + } + } catch (e: Exception) { + logger.severe(e.message) + parsedParts.add(mapOf("kind" to "text", "text" to text)) + } + } + + private fun createFinalMessage( + contextId: String, + events: List<*>, + allParts: List>, + ): Map { + val lastEvent = events.lastOrNull() as? Event + return mapOf( + "messageId" to (lastEvent?.id() ?: UUID.randomUUID().toString()), + "contextId" to contextId, + "role" to "model", + "kind" to "message", + "parts" to allParts, + ) + } + + private fun jsonElementToAny(element: JsonElement): Any? = + when (element) { + is JsonObject -> element.mapValues { jsonElementToAny(it.value) } + is JsonArray -> element.map { jsonElementToAny(it) } + is JsonPrimitive -> { + if (element.isString) element.content + else if (element.booleanOrNull != null) element.booleanOrNull + else if (element.longOrNull != null) element.longOrNull + else if (element.doubleOrNull != null) element.doubleOrNull else null + } + } + + private companion object { + val logger: Logger = Logger.getLogger(A2aHandler::class.java.name) + const val A2A_USER_ID = "a2a-user" + const val DEFAULT_CONTEXT_ID = "default-context" + } +} diff --git a/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/a2a/A2uiA2a.kt b/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/a2a/A2uiA2a.kt new file mode 100644 index 000000000..eaa8f1cbb --- /dev/null +++ b/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/a2a/A2uiA2a.kt @@ -0,0 +1,82 @@ +/* + * 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 + * + * https://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.google.a2ui.a2a + +import com.google.a2ui.core.schema.A2uiConstants +import io.a2a.spec.AgentExtension +import io.a2a.spec.DataPart +import io.a2a.spec.Part +import kotlinx.serialization.json.JsonElement + +/** A2A protocol helpers for A2UI integration. */ +object A2uiA2a { + const val A2UI_EXTENSION_URI = "https://a2ui.org/a2a-extension/a2ui/v0.8" + const val MIME_TYPE_KEY = "mimeType" + const val A2UI_MIME_TYPE = "application/json+a2ui" + + /** Creates an A2A Part containing A2UI data. */ + fun createA2uiPart(a2uiData: JsonElement): Part<*> = + DataPart(a2uiData, mapOf(MIME_TYPE_KEY to A2UI_MIME_TYPE)) + + /** Checks if an A2A Part contains A2UI data. */ + fun isA2uiPart(part: Part<*>): Boolean = + part is DataPart && part.metadata?.get(MIME_TYPE_KEY) == A2UI_MIME_TYPE + + /** Extracts the A2UI data from an A2A Part if present. */ + fun getA2uiData(part: Part<*>): JsonElement? = + if (isA2uiPart(part)) (part as DataPart).data as? JsonElement else null + + /** Creates the A2UI AgentExtension configuration. */ + fun getA2uiAgentExtension( + acceptsInlineCatalogs: Boolean = false, + supportedCatalogIds: List = emptyList(), + ): AgentExtension { + val params = mutableMapOf() + if (acceptsInlineCatalogs) { + params[A2uiConstants.INLINE_CATALOGS_KEY] = true + } + if (supportedCatalogIds.isNotEmpty()) { + params[A2uiConstants.SUPPORTED_CATALOG_IDS_KEY] = supportedCatalogIds + } + + val isSupportRequired = false + return AgentExtension( + A2UI_EXTENSION_URI, + params, + isSupportRequired, + "Provides agent driven UI using the A2UI JSON format.", + ) + } + + /** + * Activates the A2UI extension if requested in the context. + * + * @param requestedExtensions List of extension URIs requested by the client. + * @param addActivatedExtension Callback to register an activated extension. + * @return True if A2UI was activated, false otherwise. + */ + fun tryActivateA2uiExtension( + requestedExtensions: List, + addActivatedExtension: (String) -> Unit, + ): Boolean { + if (A2UI_EXTENSION_URI in requestedExtensions) { + addActivatedExtension(A2UI_EXTENSION_URI) + return true + } + return false + } +} diff --git a/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/adk/a2a_extension/Converters.kt b/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/adk/a2a_extension/Converters.kt new file mode 100644 index 000000000..eef1d9fb6 --- /dev/null +++ b/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/adk/a2a_extension/Converters.kt @@ -0,0 +1,170 @@ +/* + * 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 + * + * https://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.google.a2ui.adk.a2a_extension + +import com.google.a2ui.a2a.A2uiA2a +import com.google.a2ui.core.parser.hasA2uiParts +import com.google.a2ui.core.parser.parseResponseToParts +import com.google.a2ui.core.schema.A2uiCatalog +import com.google.adk.a2a.converters.EventConverter +import com.google.adk.agents.InvocationContext +import com.google.adk.events.Event +import com.google.genai.types.Content +import com.google.genai.types.Part +import io.a2a.spec.Event as A2aEvent +import io.a2a.spec.Message +import io.a2a.spec.Message.Role.ROLE_AGENT +import io.a2a.spec.TaskState +import io.a2a.spec.TaskStatus +import io.a2a.spec.TaskStatusUpdateEvent +import io.a2a.spec.TextPart +import java.time.OffsetDateTime +import java.util.Optional +import java.util.UUID.randomUUID +import java.util.logging.Logger +import kotlinx.serialization.json.JsonElement + +/** A catalog-aware GenAI to A2A part converter. */ +class A2uiPartConverter( + private val catalog: A2uiCatalog, + private val bypassToolCheck: Boolean = false, +) { + private val logger = Logger.getLogger(A2uiPartConverter::class.java.name) + + // Note: Due to missing public API for PartConverter in ADK Java SDK, falling back + // to returning DataParts for A2UI, and omitting standard conversions here. + // Client applications should adapt this integration logic based on actual available converters. + + fun convert(part: Part): List> { + val functionResponse = part.functionResponse().orElse(null) + val isSendA2uiJsonToClientResponse = + functionResponse != null && + functionResponse.name().orElse(null) == SendA2uiToClientToolset.TOOL_NAME + + if (isSendA2uiJsonToClientResponse || bypassToolCheck) { + if (functionResponse == null) return emptyList() + val responseMap = functionResponse.response().orElse(null) as? Map<*, *> + + responseMap?.get(SendA2uiToClientToolset.TOOL_ERROR_KEY)?.let { + logger.warning("A2UI tool call failed: $it") + return emptyList() + } + + return (responseMap?.get(SendA2uiToClientToolset.VALIDATED_A2UI_JSON_KEY) as? JsonElement) + ?.let { listOf(A2uiA2a.createA2uiPart(it)) } ?: emptyList() + } + + val functionCall = part.functionCall().orElse(null) + if ( + functionCall != null && functionCall.name().orElse(null) == SendA2uiToClientToolset.TOOL_NAME + ) { + return emptyList() + } + + val text = part.text().orElse(null) ?: return emptyList() + return if (hasA2uiParts(text)) { + parseResponseToParts(text, catalog.validator).flatMap { responsePart -> + responsePart.a2uiJson?.map { A2uiA2a.createA2uiPart(it) } ?: emptyList() + } + } else { + emptyList() + } + } +} + +/** An event converter that automatically injects the A2UI catalog into part conversion. */ +class A2uiEventConverter( + private val catalogKey: String = "system:a2ui_catalog", + private val bypassToolCheck: Boolean = false, +) { + + fun convert( + event: Event, + invocationContext: InvocationContext, + taskId: String? = null, + contextId: String? = null, + ): List { + val catalog = + invocationContext.session().state()[catalogKey] as? A2uiCatalog ?: return emptyList() + + val converter = A2uiPartConverter(catalog, bypassToolCheck) + val events = mutableListOf() + + // 1. Process Errors + val errorCode = event.errorCode().orElse(null) + if (errorCode != null) { + val errorMessage = event.errorMessage().orElse("An error occurred during processing") + val errorMsgObj = + Message.builder() + .messageId(randomUUID().toString()) + .role(ROLE_AGENT) + .parts(listOf(TextPart(errorMessage))) + .build() + + val status = TaskStatus(TaskState.TASK_STATE_FAILED, errorMsgObj, OffsetDateTime.now()) + + events.add( + TaskStatusUpdateEvent.builder() + .taskId(taskId ?: EventConverter.taskId(event)) + .contextId(contextId ?: EventConverter.contextId(event)) + .status(status) + .build() + ) + } + + // 2. Process Content + val content = event.content().orElse(null) + if (content != null) { + val outputParts = mutableListOf>() + + val genaiParts = content.parts().orElse(emptyList()) ?: emptyList() + for (part in genaiParts) { + val a2uiParts = converter.convert(part) + if (a2uiParts.isNotEmpty()) { + outputParts.addAll(a2uiParts) + } else { + // Fallback to standard GenAI to A2A Part conversion using ADK's internal EventConverter + // helpers + val singleContent = Content.builder().role("model").parts(listOf(part)).build() + val standardParts = EventConverter.contentToParts(Optional.of(singleContent), false) + outputParts.addAll(standardParts) + } + } + + if (outputParts.isNotEmpty()) { + val msgObj = + Message.builder() + .messageId(randomUUID().toString()) + .role(ROLE_AGENT) + .parts(outputParts) + .build() + + val status = TaskStatus(TaskState.TASK_STATE_WORKING, msgObj, OffsetDateTime.now()) + + events.add( + TaskStatusUpdateEvent.builder() + .taskId(taskId ?: EventConverter.taskId(event)) + .contextId(contextId ?: EventConverter.contextId(event)) + .status(status) + .build() + ) + } + } + + return events + } +} diff --git a/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/adk/a2a_extension/SendA2uiToClientToolset.kt b/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/adk/a2a_extension/SendA2uiToClientToolset.kt new file mode 100644 index 000000000..457be882f --- /dev/null +++ b/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/adk/a2a_extension/SendA2uiToClientToolset.kt @@ -0,0 +1,161 @@ +/* + * 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 + * + * https://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.google.a2ui.adk.a2a_extension + +import com.google.a2ui.core.schema.A2uiCatalog +import com.google.adk.agents.ReadonlyContext +import com.google.adk.models.LlmRequest +import com.google.adk.tools.BaseTool +import com.google.adk.tools.BaseToolset +import com.google.adk.tools.ToolContext +import com.google.genai.types.FunctionDeclaration +import com.google.genai.types.Schema +import com.google.genai.types.Type +import io.reactivex.rxjava3.core.Completable +import io.reactivex.rxjava3.core.Flowable +import io.reactivex.rxjava3.core.Single +import java.util.Optional +import java.util.logging.Logger + +typealias A2uiEnabledProvider = (ReadonlyContext) -> Boolean + +typealias A2uiCatalogProvider = (ReadonlyContext) -> A2uiCatalog + +typealias A2uiExamplesProvider = (ReadonlyContext) -> String + +/** + * A toolset that furnishes ADK agents with A2UI (Agent-to-UI) functional tools. + * + * This allows an agent to explicitly decide to render UI components based on the configured schemas + * and current catalog capabilities. Features dynamic enablement, pulling localized components on + * demand. + */ +class SendA2uiToClientToolset +@JvmOverloads +constructor( + private val a2uiEnabled: A2uiEnabledProvider, + private val a2uiCatalog: A2uiCatalogProvider, + private val a2uiExamples: A2uiExamplesProvider, +) : BaseToolset { + + private val logger = Logger.getLogger(SendA2uiToClientToolset::class.java.name) + private val uiTools = listOf(SendA2uiJsonToClientTool()) + + override fun getTools(readonlyContext: ReadonlyContext): Flowable = + if (a2uiEnabled(readonlyContext)) { + logger.info("A2UI is ENABLED, adding ui tools") + Flowable.fromIterable(uiTools) + } else { + logger.info("A2UI is DISABLED, not adding ui tools") + Flowable.empty() + } + + override fun close() { + // Nothing to close + } + + fun getPartConverter(ctx: ReadonlyContext): A2uiPartConverter = + A2uiPartConverter(a2uiCatalog(ctx)) + + inner class SendA2uiJsonToClientTool : + BaseTool( + TOOL_NAME, + "Sends A2UI JSON to the client to render rich UI natively. Always prefer this over returning raw JSON.", + ) { + + override fun declaration(): Optional { + return Optional.of( + FunctionDeclaration.builder() + .name(name()) + .description(description()) + .parameters( + Schema.builder() + .type(Type(Type.Known.OBJECT)) + .properties( + mapOf( + A2UI_JSON_ARG_NAME to + Schema.builder() + .type(Type(Type.Known.STRING)) + .description("The A2UI JSON payload to send to the client.") + .build() + ) + ) + .build() + ) + .build() + ) + } + + override fun processLlmRequest( + llmRequestBuilder: LlmRequest.Builder, + toolContext: ToolContext, + ): Completable = + Completable.fromAction { + val catalog = a2uiCatalog(toolContext) + val instruction = catalog.renderAsLlmInstructions() + val examples = a2uiExamples(toolContext) + + llmRequestBuilder.appendInstructions(listOf(instruction, examples)) + logger.info("Added A2UI schema and examples to system instructions") + } + .andThen(super.processLlmRequest(llmRequestBuilder, toolContext)) + + override fun runAsync( + args: Map, + toolContext: ToolContext, + ): Single> = + Single.fromCallable { + try { + val a2uiJsonStr = + args[A2UI_JSON_ARG_NAME] as? String + ?: throw IllegalArgumentException( + "Failed to call tool $TOOL_NAME because missing required arg $A2UI_JSON_ARG_NAME" + ) + + val catalog = a2uiCatalog(toolContext) + + val a2uiJsonPayload = kotlinx.serialization.json.Json.parseToJsonElement(a2uiJsonStr) + catalog.validator.validate(a2uiJsonPayload) + + logger.info("Validated call to tool $TOOL_NAME with $A2UI_JSON_ARG_NAME") + + // Return the validated JSON so the converter can use it. + mapOf(VALIDATED_A2UI_JSON_KEY to a2uiJsonPayload) + } catch (e: Exception) { + val err = "Failed to call A2UI tool $TOOL_NAME: ${e.message}" + logger.severe(err) + mapOf(TOOL_ERROR_KEY to err) + } + } + } + + companion object { + /** Helper to create a toolset with constant values. */ + @JvmStatic + @JvmOverloads + fun create( + enabled: Boolean, + catalog: A2uiCatalog, + examples: String = "", + ): SendA2uiToClientToolset = SendA2uiToClientToolset({ enabled }, { catalog }, { examples }) + + const val TOOL_NAME = "send_a2ui_json_to_client" + const val VALIDATED_A2UI_JSON_KEY = "validated_a2ui_json" + private const val A2UI_JSON_ARG_NAME = "a2ui_json" + const val TOOL_ERROR_KEY = "error" + } +} diff --git a/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/basic_catalog/BasicCatalogProvider.kt b/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/basic_catalog/BasicCatalogProvider.kt new file mode 100644 index 000000000..d27d13ea8 --- /dev/null +++ b/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/basic_catalog/BasicCatalogProvider.kt @@ -0,0 +1,96 @@ +/* + * 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 + * + * https://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. + */ + +@file:JvmName("BasicCatalogApi") + +package com.google.a2ui.basic_catalog + +import com.google.a2ui.core.schema.A2uiCatalogProvider +import com.google.a2ui.core.schema.A2uiConstants +import com.google.a2ui.core.schema.A2uiVersion +import com.google.a2ui.core.schema.CatalogConfig +import com.google.a2ui.core.schema.SchemaResourceLoader +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive + +/** + * A provider that loads A2UI JSON schemas and specifications from bundled package resources or + * local paths with established fallbacks. + * + * @param version The A2UI conceptual version, e.g., "0.9". + */ +class BundledCatalogProvider(private val version: A2uiVersion) : A2uiCatalogProvider { + + override fun load(): JsonObject { + val specMap = BasicCatalog.BASIC_CATALOG_PATHS[version] ?: emptyMap() + val relPath = specMap[A2uiConstants.CATALOG_SCHEMA_KEY] ?: "" + val filename = relPath.substringAfterLast('/') + + val resource = + SchemaResourceLoader.loadFromBundledResource(version.value, filename)?.toMutableMap() + ?: mutableMapOf() + + if (A2uiConstants.CATALOG_ID_KEY !in resource) { + specMap[A2uiConstants.CATALOG_SCHEMA_KEY]?.let { path -> + val catalogFile = path.replace("/json/", "/") + resource[A2uiConstants.CATALOG_ID_KEY] = + JsonPrimitive(A2uiConstants.BASE_SCHEMA_URL + catalogFile) + } + } + + if ("\$schema" !in resource) { + resource["\$schema"] = JsonPrimitive("https://json-schema.org/draft/2020-12/schema") + } + + return JsonObject(resource) + } +} + +/** Accessor for the built-in basic A2UI Catalog. */ +object BasicCatalog { + /** The standard identifier for the basic catalog. */ + const val BASIC_CATALOG_NAME = "basic" + + /** Paths to bundled standard catalogs for each spec version. */ + @JvmField + val BASIC_CATALOG_PATHS = + mapOf( + A2uiVersion.VERSION_0_8 to + mapOf( + A2uiConstants.CATALOG_SCHEMA_KEY to + "specification/v0_8/json/standard_catalog_definition.json" + ), + A2uiVersion.VERSION_0_9 to + mapOf(A2uiConstants.CATALOG_SCHEMA_KEY to "specification/v0_9/json/basic_catalog.json"), + ) + + /** + * Builds and returns a [CatalogConfig] customized for the basic A2UI catalog. Use this method + * from Java to easily instantiate catalog configurations. + * + * @param version The A2UI schema version. + * @param examplesPath An optional path string to load UI examples. + * @return A catalog configuration object defining how to load the basic schema. + */ + @JvmStatic + @JvmOverloads + fun getConfig(version: A2uiVersion, examplesPath: String? = null): CatalogConfig = + CatalogConfig( + name = BASIC_CATALOG_NAME, + provider = BundledCatalogProvider(version), + examplesPath = examplesPath, + ) +} diff --git a/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/core/InferenceStrategy.kt b/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/core/InferenceStrategy.kt new file mode 100644 index 000000000..ce600136c --- /dev/null +++ b/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/core/InferenceStrategy.kt @@ -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 + * + * https://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.google.a2ui.core + +/** + * Defines the contract for an A2UI inference strategy, responsible for constructing the LLM system + * prompt and providing schema/catalog-driven context to the agent. + */ +interface InferenceStrategy { + /** + * Generates a complete system prompt including the provided descriptions, A2UI JSON schema + * constraints, workflow guidelines, and examples. + * + * @param roleDescription The foundational role or persona for the agent. + * @param workflowDescription Optional workflow instructions to guide agent behavior. + * @param uiDescription Optional UI context or descriptive instruction. + * @param clientUiCapabilities Capabilities reported by the client for targeted schema pruning. + * @param allowedComponents A specific list of component IDs allowed for rendering. + * @param includeSchema Whether to embed the A2UI JSON schema directly in the instructions. + * @param includeExamples Whether to embed few-shot examples in the instructions. + * @param validateExamples Whether to preemptively validate loaded examples against the schema. + * @return A consolidated system prompt string. + */ + fun generateSystemPrompt( + roleDescription: String, + workflowDescription: String = "", + uiDescription: String = "", + clientUiCapabilities: kotlinx.serialization.json.JsonObject? = null, + allowedComponents: List = emptyList(), + includeSchema: Boolean = false, + includeExamples: Boolean = false, + validateExamples: Boolean = false, + ): String +} diff --git a/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/core/parser/Parser.kt b/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/core/parser/Parser.kt new file mode 100644 index 000000000..77839228e --- /dev/null +++ b/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/core/parser/Parser.kt @@ -0,0 +1,81 @@ +/* + * 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 + * + * https://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.google.a2ui.core.parser + +import com.google.a2ui.core.schema.A2uiConstants +import com.google.a2ui.core.schema.A2uiValidator +import java.util.logging.Logger +import kotlinx.serialization.json.JsonElement + +private val logger = Logger.getLogger("com.google.a2ui.core.parser.Parser") + +internal val A2UI_BLOCK_REGEX = + Regex( + "${A2uiConstants.A2UI_OPEN_TAG}(.*?)${A2uiConstants.A2UI_CLOSE_TAG}", + RegexOption.DOT_MATCHES_ALL, + ) + +/** Checks if the given text contains A2UI delimiter tags. */ +fun hasA2uiParts(text: String): Boolean = + text.contains(A2uiConstants.A2UI_OPEN_TAG) && text.contains(A2uiConstants.A2UI_CLOSE_TAG) + +/** Represents a part of the LLM response. */ +data class ResponsePart(val text: String, val a2uiJson: List? = null) + +/** Parses the response text into a list of ResponsePart objects. */ +fun parseResponseToParts(text: String, validator: A2uiValidator? = null): List { + val matches = A2UI_BLOCK_REGEX.findAll(text).toList() + + if (matches.isEmpty()) { + throw IllegalArgumentException( + "A2UI tags '${A2uiConstants.A2UI_OPEN_TAG}' and '${A2uiConstants.A2UI_CLOSE_TAG}' not found in response." + ) + } + + val responseParts = mutableListOf() + var lastEnd = 0 + + for (match in matches) { + val start = match.range.first + val end = match.range.last + 1 + val textPart = text.substring(lastEnd, start).trim() + + val jsonString = match.groupValues[1] + val jsonStringCleaned = sanitizeJsonString(jsonString) + + if (jsonStringCleaned.isEmpty()) { + throw IllegalArgumentException("A2UI JSON part is empty.") + } + + val elements = PayloadFixer.parseAndFix(jsonStringCleaned) + elements.forEach { validator?.validate(it) } + + responseParts.add(ResponsePart(text = textPart, a2uiJson = elements)) + lastEnd = end + } + + val trailingText = text.substring(lastEnd).trim() + if (trailingText.isNotEmpty()) { + responseParts.add(ResponsePart(text = trailingText, a2uiJson = null)) + } + + return responseParts +} + +/** Sanitize LLM output by removing markdown code blocks if present. */ +fun sanitizeJsonString(jsonString: String): String = + jsonString.trim().removePrefix("```json").removePrefix("```").removeSuffix("```").trim() diff --git a/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/core/parser/PayloadFixer.kt b/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/core/parser/PayloadFixer.kt new file mode 100644 index 000000000..85aa68c6e --- /dev/null +++ b/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/core/parser/PayloadFixer.kt @@ -0,0 +1,124 @@ +/* + * 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 + * + * https://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.google.a2ui.core.parser + +import java.util.logging.Logger +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject + +/** Validates and applies autofixes to raw JSON strings. */ +object PayloadFixer { + + private val logger = Logger.getLogger(PayloadFixer::class.java.name) + + /** + * Parses and applies autofixes to a raw JSON string and returns the parsed payload as a list of + * components. + * + * @param payload The raw JSON string from the LLM. + * @return A parsed and potentially fixed payload (array of JsonElements). + */ + fun parseAndFix(payload: String): List { + val normalizedPayload = normalizeSmartQuotes(payload) + return try { + parse(normalizedPayload) + } catch (e: Exception) { + logger.warning("Initial A2UI payload validation failed: ${e.message}") + parse(removeTrailingCommas(normalizedPayload)) + } + } + + /** + * Replaces smart (curly) quotes with standard straight quotes. + * + * @param jsonStr The raw JSON string from the LLM. + * @return A string with curly quotes replaced by straight quotes. + */ + fun normalizeSmartQuotes(jsonStr: String): String { + return jsonStr + .replace('\u201C', '"') // “ + .replace('\u201D', '"') // ” + .replace('\u2018', '\'') // ‘ + .replace('\u2019', '\'') // ’ + } + + private fun parse(payload: String): List = + try { + when (val element = Json.parseToJsonElement(payload)) { + is JsonArray -> element.toList() + is JsonObject -> { + logger.info("Received a single JSON object, wrapping in a list for validation.") + listOf(element) + } + else -> + throw IllegalArgumentException( + "Payload must be a JSON Array or Object, got: ${element::class.simpleName}" + ) + } + } catch (e: Exception) { + logger.severe("Failed to parse JSON: ${e.message}") + throw IllegalArgumentException("Failed to parse JSON: ${e.message}", e) + } + + /** + * Attempts to remove trailing commas from a JSON string. + * + * This implementation is quote-aware and will not modify commas inside strings. + * + * @param jsonStr The raw JSON string from the LLM. + * @return A potentially fixed JSON string. + */ + fun removeTrailingCommas(jsonStr: String): String { + val result = StringBuilder() + var inString = false + var i = 0 + var lastCommaIndex = -1 + + while (i < jsonStr.length) { + val c = jsonStr[i] + + if (c == '"' && (i == 0 || jsonStr[i - 1] != '\\')) { + inString = !inString + } + + if (!inString) { + when { + c == ',' -> lastCommaIndex = result.length + (c == ']' || c == '}') && lastCommaIndex != -1 -> { + val contentBetween = result.substring(lastCommaIndex + 1) + if (contentBetween.isBlank()) { + result.deleteCharAt(lastCommaIndex) + } + lastCommaIndex = -1 + } + !c.isWhitespace() -> lastCommaIndex = -1 + } + } + + result.append(c) + i++ + } + + val fixedJson = result.toString() + if (fixedJson != jsonStr) { + logger.warning("Detected trailing commas in LLM output; applied robust autofix.") + } + return fixedJson + } +} diff --git a/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/core/schema/A2uiConstants.kt b/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/core/schema/A2uiConstants.kt new file mode 100644 index 000000000..1c944b7d6 --- /dev/null +++ b/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/core/schema/A2uiConstants.kt @@ -0,0 +1,55 @@ +/* + * 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 + * + * https://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.google.a2ui.core.schema + +/** Central repository for A2UI protocol constants, aligned with the Python SDK. */ +object A2uiConstants { + const val A2UI_ASSET_PACKAGE = "com.google.a2ui.assets" + const val SERVER_TO_CLIENT_SCHEMA_KEY = "server_to_client" + const val COMMON_TYPES_SCHEMA_KEY = "common_types" + const val CATALOG_SCHEMA_KEY = "catalog" + const val CATALOG_COMPONENTS_KEY = "components" + const val CATALOG_ID_KEY = "catalogId" + const val CATALOG_STYLES_KEY = "styles" + + // Protocol constants + const val SUPPORTED_CATALOG_IDS_KEY = "supportedCatalogIds" + const val INLINE_CATALOGS_KEY = "inlineCatalogs" + + const val A2UI_CLIENT_CAPABILITIES_KEY = "a2uiClientCapabilities" + + const val BASE_SCHEMA_URL = "https://a2ui.org/" + const val INLINE_CATALOG_NAME = "inline" + + const val VERSION_0_8 = "0.8" + const val VERSION_0_9 = "0.9" + + const val A2UI_OPEN_TAG = "" + const val A2UI_CLOSE_TAG = "" + + const val A2UI_SCHEMA_BLOCK_START = "---BEGIN A2UI JSON SCHEMA---" + const val A2UI_SCHEMA_BLOCK_END = "---END A2UI JSON SCHEMA---" + + const val DEFAULT_WORKFLOW_RULES = + """ + The generated response MUST follow these rules: + 1. The response can contain one or more A2UI JSON blocks. + 2. Each A2UI JSON block MUST be wrapped in $A2UI_OPEN_TAG and $A2UI_CLOSE_TAG tags. + 3. Between or around these blocks, you can provide conversational text. + 4. The JSON part MUST be a single, raw JSON object (usually a list of A2UI messages) and MUST validate against the provided A2UI JSON SCHEMA. + """ +} diff --git a/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/core/schema/A2uiSchemaManager.kt b/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/core/schema/A2uiSchemaManager.kt new file mode 100644 index 000000000..c9ef6d74e --- /dev/null +++ b/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/core/schema/A2uiSchemaManager.kt @@ -0,0 +1,230 @@ +/* + * 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 + * + * https://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.google.a2ui.core.schema + +import com.google.a2ui.core.InferenceStrategy +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive + +/** Represents the supported A2UI specification versions. */ +enum class A2uiVersion( + val value: String, + val serverToClientSchemaPath: String, + val commonTypesSchemaPath: String? = null, +) { + VERSION_0_8(A2uiConstants.VERSION_0_8, "server_to_client.json"), + VERSION_0_9(A2uiConstants.VERSION_0_9, "server_to_client.json", "common_types.json"), +} + +/** + * Integrates A2UI capabilities by processing and aggregating [CatalogConfig] schemas to establish + * the grammatical constraints for Agent UI payload generation. + * + * This implementation of [InferenceStrategy] synthesizes the resulting system prompt instructions, + * which contain the precise JSON grammar for the LLM based on the active schemas and requested + * components negotiated between the agent and client. + * + * @param version Framework version (e.g., "0.8" or "0.9"). Delineates internal topology handling + * and resolution strategies. + * @param catalogs List of user-configured catalogs offering UI components to this agent. + * @param acceptsInlineCatalogs Whether this agent permits the client UI to dictate dynamic schema + * payloads rather than relying solely on bundled configurations. + * @param schemaModifiers An optional chain of transformations to perform on all resolved schema + * endpoints. + */ +class A2uiSchemaManager +@JvmOverloads +constructor( + private val version: A2uiVersion, + catalogs: List = emptyList(), + @JvmField val acceptsInlineCatalogs: Boolean = false, + private val schemaModifiers: List<(JsonObject) -> JsonObject> = emptyList(), +) : InferenceStrategy { + + private val serverToClientSchema: JsonObject + private val commonTypesSchema: JsonObject + private val supportedCatalogs = mutableListOf() + private val catalogExamplePaths = mutableMapOf() + + /** + * Identifies the catalogs technically supported by this running agent application. Can be + * utilized to negotiate payload rendering with downstream clients. + */ + val supportedCatalogIds: List + get() = supportedCatalogs.map { it.catalogId } + + init { + serverToClientSchema = + applyModifiers( + SchemaResourceLoader.loadFromBundledResource( + version.value, + version.serverToClientSchemaPath, + ) ?: JsonObject(emptyMap()) + ) + + commonTypesSchema = + version.commonTypesSchemaPath?.let { + applyModifiers( + SchemaResourceLoader.loadFromBundledResource(version.value, it) ?: JsonObject(emptyMap()) + ) + } ?: JsonObject(emptyMap()) + + for (config in catalogs) { + val catalogSchema = applyModifiers(config.provider.load()) + val catalog = + A2uiCatalog( + version = version, + name = config.name, + catalogSchema = catalogSchema, + serverToClientSchema = serverToClientSchema, + commonTypesSchema = commonTypesSchema, + ) + supportedCatalogs.add(catalog) + catalogExamplePaths[catalog.catalogId] = config.examplesPath + } + } + + private fun applyModifiers(schema: JsonObject): JsonObject = + schemaModifiers.fold(schema) { current, modifier -> modifier(current) } + + private fun selectCatalog(clientUiCapabilities: JsonObject?): A2uiCatalog { + check(supportedCatalogs.isNotEmpty()) { "No supported catalogs found." } + + if (clientUiCapabilities == null) return supportedCatalogs.first() + + val inlineCatalogs = + (clientUiCapabilities[A2uiConstants.INLINE_CATALOGS_KEY] as? JsonArray)?.mapNotNull { + it as? JsonObject + } ?: emptyList() + + val clientSupportedCatalogIds = + (clientUiCapabilities[A2uiConstants.SUPPORTED_CATALOG_IDS_KEY] as? JsonArray)?.mapNotNull { + it.jsonPrimitive.content + } ?: emptyList() + + if (!acceptsInlineCatalogs && inlineCatalogs.isNotEmpty()) { + throw IllegalArgumentException( + "Inline catalog '${A2uiConstants.INLINE_CATALOGS_KEY}' is provided in client UI capabilities. However, the agent does not accept inline catalogs." + ) + } + + if (inlineCatalogs.isNotEmpty()) { + // Determine the base catalog: use supportedCatalogIds if provided, + // otherwise fall back to the agent's default catalog. + var baseCatalog = supportedCatalogs.first() + if (clientSupportedCatalogIds.isNotEmpty()) { + val agentSupportedCatalogs = supportedCatalogs.associateBy { it.catalogId } + for (cscid in clientSupportedCatalogIds) { + agentSupportedCatalogs[cscid]?.let { baseCatalog = it } + if (baseCatalog != supportedCatalogs.first()) break + } + } + + val mergedSchemaMap = baseCatalog.catalogSchema.toMutableMap() + val mergedComponents = + mergedSchemaMap[A2uiConstants.CATALOG_COMPONENTS_KEY]?.jsonObject?.toMutableMap() + ?: mutableMapOf() + + for (inlineCatalogSchema in inlineCatalogs) { + val modifiedInline = applyModifiers(inlineCatalogSchema) + val inlineComponents = + modifiedInline[A2uiConstants.CATALOG_COMPONENTS_KEY]?.jsonObject ?: JsonObject(emptyMap()) + mergedComponents.putAll(inlineComponents) + } + + mergedSchemaMap[A2uiConstants.CATALOG_COMPONENTS_KEY] = JsonObject(mergedComponents) + val mergedSchema = JsonObject(mergedSchemaMap) + + return A2uiCatalog( + version = version, + name = A2uiConstants.INLINE_CATALOG_NAME, + catalogSchema = mergedSchema, + serverToClientSchema = serverToClientSchema, + commonTypesSchema = commonTypesSchema, + ) + } + + if (clientSupportedCatalogIds.isEmpty()) return supportedCatalogs.first() + + val agentSupportedCatalogs = supportedCatalogs.associateBy { it.catalogId } + for (cscid in clientSupportedCatalogIds) { + agentSupportedCatalogs[cscid]?.let { + return it + } + } + + throw IllegalArgumentException( + "No client-supported catalog found on the agent side. Agent-supported catalogs are: ${supportedCatalogs.map { it.catalogId }}" + ) + } + + /** + * Resolves the desired catalog based on the client capabilities, returning it with pruned unused + * components. + */ + @JvmOverloads + fun getSelectedCatalog( + clientUiCapabilities: JsonObject? = null, + allowedComponents: List = emptyList(), + ): A2uiCatalog = selectCatalog(clientUiCapabilities).withPrunedComponents(allowedComponents) + + /** Renders LLM examples for a given catalog, loaded from its configured examples path. */ + @JvmOverloads + fun loadExamples(catalog: A2uiCatalog, validate: Boolean = false): String = + catalogExamplePaths[catalog.catalogId]?.let { path -> catalog.loadExamples(path, validate) } + ?: "" + + /** Creates a fully formatted system prompt describing the schema to the LLM model. */ + override fun generateSystemPrompt( + roleDescription: String, + workflowDescription: String, + uiDescription: String, + clientUiCapabilities: JsonObject?, + allowedComponents: List, + includeSchema: Boolean, + includeExamples: Boolean, + validateExamples: Boolean, + ): String { + val parts = mutableListOf(roleDescription) + + val workflow = + if (workflowDescription.isEmpty()) A2uiConstants.DEFAULT_WORKFLOW_RULES + else "${A2uiConstants.DEFAULT_WORKFLOW_RULES}\n$workflowDescription" + parts.add("## Workflow Description:\n$workflow") + + if (uiDescription.isNotEmpty()) { + parts.add("## UI Description:\n$uiDescription") + } + + val selectedCatalog = getSelectedCatalog(clientUiCapabilities, allowedComponents) + + if (includeSchema) { + parts.add(selectedCatalog.renderAsLlmInstructions()) + } + + if (includeExamples) { + val examplesStr = loadExamples(selectedCatalog, validateExamples) + if (examplesStr.isNotEmpty()) { + parts.add("### Examples:\n$examplesStr") + } + } + + return parts.joinToString("\n\n") + } +} diff --git a/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/core/schema/Catalog.kt b/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/core/schema/Catalog.kt new file mode 100644 index 000000000..b499934db --- /dev/null +++ b/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/core/schema/Catalog.kt @@ -0,0 +1,179 @@ +/* + * 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 + * + * https://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. + */ + +@file:JvmName("CatalogApi") + +package com.google.a2ui.core.schema + +import java.io.File +import java.util.logging.Logger +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.jsonPrimitive + +/** + * Configuration for a catalog of components. + * + * A catalog consists of a provider that knows how to load the schema, and optionally a path to + * examples. + */ +data class CatalogConfig( + @JvmField val name: String, + @JvmField val provider: A2uiCatalogProvider, + @JvmField val examplesPath: String? = null, +) { + companion object { + /** Create a [CatalogConfig] using a [FileSystemCatalogProvider]. */ + @JvmStatic + @JvmOverloads + fun fromPath(name: String, catalogPath: String, examplesPath: String? = null): CatalogConfig = + CatalogConfig(name, FileSystemCatalogProvider(catalogPath), examplesPath) + } +} + +/** Represents a processed component catalog with its schema. */ +data class A2uiCatalog( + @JvmField val version: A2uiVersion, + @JvmField val name: String, + @JvmField val serverToClientSchema: JsonObject, + @JvmField val commonTypesSchema: JsonObject, + @JvmField val catalogSchema: JsonObject, +) { + + private val logger = Logger.getLogger(A2uiCatalog::class.java.name) + + val validator: A2uiValidator by lazy { A2uiValidator(this) } + + val catalogId: String + get() { + val idElement = catalogSchema[A2uiConstants.CATALOG_ID_KEY] + require(idElement is JsonPrimitive && idElement.isString) { + "Catalog '$name' missing catalogId" + } + return idElement.content + } + + /** + * Returns a new catalog with only allowed components. + * + * @param allowedComponents List of component names to include. + * @return A copy of the catalog with only allowed components. + */ + fun withPrunedComponents(allowedComponents: List): A2uiCatalog { + if (allowedComponents.isEmpty()) return this + + val schemaCopy = catalogSchema.toMutableMap() + + // Filter components listing + (schemaCopy[A2uiConstants.CATALOG_COMPONENTS_KEY] as? JsonObject)?.let { components -> + schemaCopy[A2uiConstants.CATALOG_COMPONENTS_KEY] = + JsonObject(components.filterKeys { it in allowedComponents }) + } + + // Filter anyComponent oneOf if it exists + (schemaCopy["\$defs"] as? JsonObject)?.let { defsElement -> + (defsElement["anyComponent"] as? JsonObject)?.let { anyCompElement -> + val newAnyComp = pruneAnyComponentOneOf(anyCompElement, allowedComponents) + val newDefs = defsElement.toMutableMap().apply { put("anyComponent", newAnyComp) } + schemaCopy["\$defs"] = JsonObject(newDefs) + } + } + + return copy(catalogSchema = JsonObject(schemaCopy)) + } + + private fun pruneAnyComponentOneOf( + anyCompElement: JsonObject, + allowedComponents: List, + ): JsonObject { + val oneOfElement = anyCompElement["oneOf"] as? JsonArray ?: return anyCompElement + + val filteredOneOf = + oneOfElement.filter { item -> + val ref = (item as? JsonObject)?.get("\$ref")?.jsonPrimitive?.content + if (ref != null && ref.startsWith("#/${A2uiConstants.CATALOG_COMPONENTS_KEY}/")) { + val compName = ref.split("/").last() + compName in allowedComponents + } else { + true // Keep external refs or non-matching refs + } + } + + return JsonObject(anyCompElement + ("oneOf" to JsonArray(filteredOneOf))) + } + + /** Renders the catalog and schema as LLM instructions. */ + fun renderAsLlmInstructions(): String = buildString { + appendLine(A2uiConstants.A2UI_SCHEMA_BLOCK_START) + val jsonFmt = Json + + appendLine("### Server To Client Schema:") + appendLine(jsonFmt.encodeToString(JsonElement.serializer(), serverToClientSchema)) + + if (commonTypesSchema.isNotEmpty()) { + appendLine("\n### Common Types Schema:") + appendLine(jsonFmt.encodeToString(JsonElement.serializer(), commonTypesSchema)) + } + + appendLine("\n### Catalog Schema:") + appendLine(jsonFmt.encodeToString(JsonElement.serializer(), catalogSchema)) + + append("\n${A2uiConstants.A2UI_SCHEMA_BLOCK_END}") + } + + /** Loads and validates examples from a directory. */ + @JvmOverloads + fun loadExamples(path: String?, validate: Boolean = false): String { + if (path.isNullOrEmpty()) return "" + val dir = File(path) + if (!dir.isDirectory) { + logger.warning("Example path $path is not a directory") + return "" + } + + val files = dir.listFiles { _, name -> name.endsWith(".json") } ?: emptyArray() + + return files + .mapNotNull { file -> + val basename = file.nameWithoutExtension + try { + val content = file.readText() + if (validate && !validateExample(file.path, content)) { + null + } else { + "---BEGIN $basename---\n$content\n---END $basename---" + } + } catch (e: Exception) { + logger.warning("Failed to load example ${file.path}: ${e.message}") + null + } + } + .joinToString("\n\n") + } + + private fun validateExample(fullPath: String, content: String): Boolean = + try { + val jsonElement = Json.parseToJsonElement(content) + validator.validate(jsonElement) + true + } catch (e: Exception) { + logger.warning("Failed to validate example $fullPath: ${e.message}") + false + } +} diff --git a/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/core/schema/CatalogProvider.kt b/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/core/schema/CatalogProvider.kt new file mode 100644 index 000000000..26f0e1e7a --- /dev/null +++ b/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/core/schema/CatalogProvider.kt @@ -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 + * + * https://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.google.a2ui.core.schema + +import java.io.File +import java.io.IOException +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject + +/** Abstract base interface for providing A2UI schemas and catalogs. */ +interface A2uiCatalogProvider { + /** + * Loads a catalog definition. + * + * @return The loaded catalog as a JsonObject. + */ + fun load(): JsonObject +} + +/** Loads catalog definition from the local filesystem. */ +class FileSystemCatalogProvider(private val path: String) : A2uiCatalogProvider { + override fun load(): JsonObject { + try { + val file = File(path) + val content = file.readText(Charsets.UTF_8) + return Json.parseToJsonElement(content) as JsonObject + } catch (e: Exception) { + throw IOException("Could not load schema from ${path}: ${e.message}", e) + } + } +} diff --git a/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/core/schema/SchemaModifiers.kt b/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/core/schema/SchemaModifiers.kt new file mode 100644 index 000000000..bfd2a2c4f --- /dev/null +++ b/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/core/schema/SchemaModifiers.kt @@ -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 + * + * https://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.google.a2ui.core.schema + +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull + +/** Standard transformations for A2UI schemas. */ +object SchemaModifiers { + /** + * Recursively removes "additionalProperties: false" constraints from a JSON schema. + * + * This is useful when the agent might generate slightly different properties than defined in a + * strict schema, allowing for more flexible LLM output. + */ + fun removeStrictValidation(schema: JsonObject): JsonObject = + recursiveRemoveStrict(schema) as JsonObject + + private fun recursiveRemoveStrict(element: JsonElement): JsonElement = + when (element) { + is JsonObject -> { + val filtered = + element.filter { (key, value) -> + key != "additionalProperties" || (value as? JsonPrimitive)?.booleanOrNull != false + } + JsonObject(filtered.mapValues { recursiveRemoveStrict(it.value) }) + } + is JsonArray -> JsonArray(element.map { recursiveRemoveStrict(it) }) + else -> element + } +} diff --git a/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/core/schema/SchemaResourceLoader.kt b/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/core/schema/SchemaResourceLoader.kt new file mode 100644 index 000000000..d9f8cf42c --- /dev/null +++ b/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/core/schema/SchemaResourceLoader.kt @@ -0,0 +1,59 @@ +/* + * 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 + * + * https://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. + */ + +@file:JvmName("A2uiUtils") + +package com.google.a2ui.core.schema + +import java.io.IOException +import java.io.InputStream +import java.util.logging.Logger +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive + +/** Common utilities for loading schemas and preparing JSON data. */ +object SchemaResourceLoader { + const val A2UI_ASSET_PACKAGE = A2uiConstants.A2UI_ASSET_PACKAGE + private val logger = Logger.getLogger(SchemaResourceLoader::class.java.name) + + /** Loads a JSON schema from bundled resources or file system. */ + @JvmStatic + fun loadFromBundledResource(version: String, filename: String): JsonObject? { + // Try to load from the bundled package resources + val resourcePath = "/${A2UI_ASSET_PACKAGE.replace('.', '/')}/$version/$filename" + return try { + val stream: InputStream? = SchemaResourceLoader::class.java.getResourceAsStream(resourcePath) + stream?.bufferedReader()?.use { Json.parseToJsonElement(it.readText()) as JsonObject } + ?: run { + logger.fine("Could not find system resource $resourcePath") + throw IOException("Could not load schema $filename for version $version") + } + } catch (e: Exception) { + logger.fine("Could not load '$filename' from package resources: ${e.message}") + throw IOException("Could not load schema $filename for version $version", e) + } + } + + /** + * LLM is instructed to generate a list of messages, so we wrap the bundled schema in an array. + */ + @JvmStatic + fun wrapAsJsonArray(a2uiSchema: JsonObject): JsonObject { + require(a2uiSchema.isNotEmpty()) { "A2UI schema is empty" } + return JsonObject(mapOf("type" to JsonPrimitive("array"), "items" to a2uiSchema)) + } +} diff --git a/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/core/schema/Validator.kt b/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/core/schema/Validator.kt new file mode 100644 index 000000000..209e2571b --- /dev/null +++ b/agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/core/schema/Validator.kt @@ -0,0 +1,589 @@ +/* + * 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 + * + * https://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.google.a2ui.core.schema + +import com.fasterxml.jackson.databind.ObjectMapper +import com.networknt.schema.JsonSchema +import com.networknt.schema.JsonSchemaFactory +import com.networknt.schema.SchemaValidatorsConfig +import com.networknt.schema.SpecVersion +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.jsonPrimitive + +/** + * Responsible for verifying the structural and topological integrity of an A2UI payload. + * + * It utilizes the active [A2uiCatalog] to construct a context-aware JSON schema validator, and + * performs advanced integrity checks including depth-limits, graph topology validations, and + * reference resolutions for rendering capabilities. + * + * @param catalog The localized contextual A2UI catalog utilized for schema validation. + */ +class A2uiValidator(private val catalog: A2uiCatalog) { + private val validator: JsonSchema = buildValidator() + private val mapper = ObjectMapper() + + private fun buildValidator(): JsonSchema = + if (catalog.version == A2uiVersion.VERSION_0_8) build0_8Validator() else build0_9Validator() + + private fun injectAdditionalProperties( + schema: JsonElement, + sourceProperties: Map, + ): Pair> { + val injectedKeys = mutableSetOf() + + fun recursiveInject(obj: JsonElement): JsonElement = + when (obj) { + is JsonObject -> { + val newObj = mutableMapOf() + for ((k, v) in obj) { + if ( + v is JsonObject && v[PROP_ADDITIONAL_PROPERTIES]?.jsonPrimitive?.booleanOrNull == true + ) { + if (sourceProperties.containsKey(k)) { + injectedKeys.add(k) + val newNode = v.toMutableMap() + newNode[PROP_ADDITIONAL_PROPERTIES] = JsonPrimitive(false) + + val existingProps = + newNode[PROP_PROPERTIES] as? JsonObject ?: JsonObject(emptyMap()) + val sourceProps = sourceProperties[k] as? JsonObject ?: JsonObject(emptyMap()) + + newNode[PROP_PROPERTIES] = JsonObject(existingProps + sourceProps) + newObj[k] = JsonObject(newNode) + } else { + newObj[k] = recursiveInject(v) + } + } else { + newObj[k] = recursiveInject(v) + } + } + JsonObject(newObj) + } + is JsonArray -> JsonArray(obj.map { recursiveInject(it) }) + else -> obj + } + + return recursiveInject(schema) to injectedKeys + } + + private fun bundle0_8Schemas(): JsonObject { + if (catalog.serverToClientSchema.isEmpty()) return JsonObject(emptyMap()) + + val sourceProperties = mutableMapOf() + val catalogSchema = catalog.catalogSchema + + if (catalogSchema.isNotEmpty()) { + catalogSchema[A2uiConstants.CATALOG_COMPONENTS_KEY]?.let { + sourceProperties[PROP_COMPONENT] = it + } + catalogSchema[A2uiConstants.CATALOG_STYLES_KEY]?.let { + sourceProperties[A2uiConstants.CATALOG_STYLES_KEY] = it + } + } + + val (bundled) = injectAdditionalProperties(catalog.serverToClientSchema, sourceProperties) + return bundled as JsonObject + } + + private fun build0_8Validator(): JsonSchema { + val bundledSchema = bundle0_8Schemas() + val fullSchema = SchemaResourceLoader.wrapAsJsonArray(bundledSchema).toMutableMap() + fullSchema[KEY_DOLLAR_SCHEMA] = JsonPrimitive(SCHEMA_DRAFT_2020_12) + + val baseUri = + catalog.serverToClientSchema[KEY_DOLLAR_ID]?.jsonPrimitive?.content + ?: A2uiConstants.BASE_SCHEMA_URL + val baseDirUri = baseUri.substringBeforeLast("/") + val commonTypesUri = "$baseDirUri/$FILE_COMMON_TYPES" + + val config = SchemaValidatorsConfig.builder().build() + + val jsonFmt = Json { prettyPrint = false } + + val factory = + JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012)) + .schemaMappers { schemaMappers -> + schemaMappers.mapPrefix(FILE_COMMON_TYPES, commonTypesUri) + } + .build() + + val schemaString = jsonFmt.encodeToString(JsonElement.serializer(), JsonObject(fullSchema)) + return factory.getSchema(schemaString, config) + } + + private fun build0_9Validator(): JsonSchema { + val fullSchema = + SchemaResourceLoader.wrapAsJsonArray(catalog.serverToClientSchema).toMutableMap() + fullSchema[KEY_DOLLAR_SCHEMA] = JsonPrimitive(SCHEMA_DRAFT_2020_12) + + val config = SchemaValidatorsConfig.builder().build() + val factory = + JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012)) + .build() + + val jsonFmt = Json { prettyPrint = false } + val schemaString = jsonFmt.encodeToString(JsonElement.serializer(), JsonObject(fullSchema)) + return factory.getSchema(schemaString, config) + } + + /** + * Parses and validates raw A2UI JSON payload against the designated schema, throwing an + * [IllegalArgumentException] describing any constraint or structural invalidity. + * + * It asserts fundamental strict JSON schemas, recursive depth boundaries, and component reference + * integrity. + * + * @param a2uiJson Raw parsed A2UI response payload element to inspect. + * @throws IllegalArgumentException If validation or referential integrity fail. + */ + fun validate(a2uiJson: JsonElement) { + val messages = a2uiJson as? JsonArray ?: JsonArray(listOf(a2uiJson)) + + // Basic schema validation + val jsonFmt = Json { prettyPrint = false } + val messagesString = jsonFmt.encodeToString(JsonElement.serializer(), messages) + val jsonNode = mapper.readTree(messagesString) + + val errors = validator.validate(jsonNode) + if (errors.isNotEmpty()) { + val msg = buildString { + append("Validation failed:") + for (error in errors) { + append("\n - ${error.message}") + } + } + throw IllegalArgumentException(msg) + } + + // Integrity validation + val rootId = findRootId(messages) + val topologyValidator = A2uiTopologyValidator(catalog, rootId) + val recursionValidator = A2uiRecursionValidator() + + for (message in messages) { + if (message !is JsonObject) continue + + val components = + when { + MSG_SURFACE_UPDATE in message -> + (message[MSG_SURFACE_UPDATE] as? JsonObject)?.get(A2uiConstants.CATALOG_COMPONENTS_KEY) + as? JsonArray + MSG_UPDATE_COMPONENTS in message -> + (message[MSG_UPDATE_COMPONENTS] as? JsonObject)?.get( + A2uiConstants.CATALOG_COMPONENTS_KEY + ) as? JsonArray + else -> null + } + + components?.let { topologyValidator.validate(it) } + + recursionValidator.validate(message) + } + } + + private fun findRootId(messages: JsonArray): String { + for (message in messages) { + if (message is JsonObject && MSG_BEGIN_RENDERING in message) { + val beginRendering = message[MSG_BEGIN_RENDERING] as? JsonObject + val rootObj = beginRendering?.get(ROOT) as? JsonObject + return rootObj?.get(ID)?.jsonPrimitive?.content ?: ROOT + } + } + return ROOT + } + + /** Validates component graph topology, including cycles, orphans, and missing references. */ + private class A2uiTopologyValidator( + private val catalog: A2uiCatalog, + private val rootId: String, + ) { + + fun validate(components: JsonArray) { + val refFieldsMap = extractComponentRefFields() + validateComponentIntegrity(components, refFieldsMap) + validateTopology(components, refFieldsMap) + } + + /** + * Analyzes the catalog schema to identify which fields in each component type act as references + * to other components (either single IDs or lists of IDs). + * + * @return A map where the key is the component type name, and the value is a Pair of: + * - Set of property names that are single component references. + * - Set of property names that are list/collection component references. + */ + private fun extractComponentRefFields(): Map, Set>> { + val refMap = mutableMapOf, Set>>() + + val allComponents = + catalog.catalogSchema[A2uiConstants.CATALOG_COMPONENTS_KEY] as? JsonObject ?: return refMap + + // Heuristically determines if a schema property represents a single ComponentId reference. + fun isComponentIdRef(propSchema: JsonElement): Boolean { + if (propSchema !is JsonObject) return false + val ref = propSchema[KEY_DOLLAR_REF]?.jsonPrimitive?.content ?: "" + if (ref.endsWith(TITLE_COMPONENT_ID) || ref.endsWith(PROP_CHILD) || "/$PROP_CHILD" in ref) + return true + + if ( + propSchema[PROP_TYPE]?.jsonPrimitive?.content == TYPE_STRING && + propSchema[PROP_TITLE]?.jsonPrimitive?.content == TITLE_COMPONENT_ID + ) { + return true + } + + return listOf(COMBINATOR_ONE_OF, COMBINATOR_ANY_OF, COMBINATOR_ALL_OF).any { key -> + (propSchema[key] as? JsonArray)?.any { isComponentIdRef(it) } == true + } + } + + // Heuristically determines if a schema property represents a collection of ComponentIds. + fun isChildListRef(propSchema: JsonElement): Boolean { + if (propSchema !is JsonObject) return false + val ref = propSchema[KEY_DOLLAR_REF]?.jsonPrimitive?.content ?: "" + if ( + ref.endsWith(TITLE_CHILD_LIST) || ref.endsWith(PROP_CHILDREN) || "/$PROP_CHILDREN" in ref + ) + return true + + if (propSchema[PROP_TYPE]?.jsonPrimitive?.content == TYPE_OBJECT) { + val props = propSchema[PROP_PROPERTIES] as? JsonObject + if ( + props != null && + (PROP_EXPLICIT_LIST in props || PROP_TEMPLATE in props || PROP_COMPONENT_ID in props) + ) { + return true + } + } + + if (propSchema[PROP_TYPE]?.jsonPrimitive?.content == TYPE_ARRAY) { + val items = propSchema[PROP_ITEMS] + if (items != null && isComponentIdRef(items)) return true + } + + return listOf(COMBINATOR_ONE_OF, COMBINATOR_ANY_OF, COMBINATOR_ALL_OF).any { key -> + (propSchema[key] as? JsonArray)?.any { isChildListRef(it) } == true + } + } + + // Iterate over all components defined in the catalog to extract their reference fields. + for ((compName, compSchemaElem) in allComponents) { + val singleRefs = mutableSetOf() + val listRefs = mutableSetOf() + + // Recursively inspects properties and combinators to find reference fields. + fun extractFromProps(cs: JsonElement) { + if (cs !is JsonObject) return + val props = cs[PROP_PROPERTIES] as? JsonObject ?: return + for ((propName, propSchema) in props) { + if ( + isComponentIdRef(propSchema) || + propName in listOf(PROP_CHILD, PROP_CONTENT_CHILD, PROP_ENTRY_POINT_CHILD) + ) { + singleRefs.add(propName) + } else if (isChildListRef(propSchema) || propName == PROP_CHILDREN) { + listRefs.add(propName) + } + } + + listOf(COMBINATOR_ALL_OF, COMBINATOR_ONE_OF, COMBINATOR_ANY_OF).forEach { key -> + (cs[key] as? JsonArray)?.forEach { extractFromProps(it) } + } + } + + extractFromProps(compSchemaElem) + if (singleRefs.isNotEmpty() || listRefs.isNotEmpty()) { + refMap[compName] = singleRefs to listRefs + } + } + return refMap + } + + private fun validateComponentIntegrity( + components: JsonArray, + refFieldsMap: Map, Set>>, + ) { + val ids = mutableSetOf() + + for (compElem in components) { + val comp = compElem as? JsonObject ?: continue + val compId = comp[ID]?.jsonPrimitive?.content ?: continue + + if (!ids.add(compId)) { + throw IllegalArgumentException("Duplicate component ID: $compId") + } + } + + if (rootId !in ids) { + throw IllegalArgumentException("Missing root component: No component has id='$rootId'") + } + + for (compElem in components) { + val comp = compElem as? JsonObject ?: continue + for ((refId, fieldName) in getComponentReferences(comp, refFieldsMap)) { + if (refId !in ids) { + val cId = comp[ID]?.jsonPrimitive?.content + throw IllegalArgumentException( + "Component '$cId' references non-existent component '$refId' in field '$fieldName'" + ) + } + } + } + } + + private fun validateTopology( + components: JsonArray, + refFieldsMap: Map, Set>>, + ) { + val adjList = mutableMapOf>() + val allIds = mutableSetOf() + + for (compElem in components) { + val comp = compElem as? JsonObject ?: continue + val compId = comp[ID]?.jsonPrimitive?.content ?: continue + + allIds.add(compId) + val neighbors = adjList.getOrPut(compId) { mutableListOf() } + + for ((refId, fieldName) in getComponentReferences(comp, refFieldsMap)) { + if (refId == compId) { + throw IllegalArgumentException( + "Self-reference detected: Component '$compId' references itself in field '$fieldName'" + ) + } + neighbors.add(refId) + } + } + + val visited = mutableSetOf() + val recursionStack = mutableSetOf() + + fun dfs(nodeId: String, depth: Int) { + if (depth > MAX_GLOBAL_DEPTH) { + throw IllegalArgumentException( + "Global recursion limit exceeded: logical depth > $MAX_GLOBAL_DEPTH" + ) + } + + visited.add(nodeId) + recursionStack.add(nodeId) + + for (neighbor in adjList[nodeId] ?: emptyList()) { + if (neighbor !in visited) { + dfs(neighbor, depth + 1) + } else if (neighbor in recursionStack) { + throw IllegalArgumentException( + "Circular reference detected involving component '$neighbor'" + ) + } + } + + recursionStack.remove(nodeId) + } + + if (rootId in allIds) dfs(rootId, 0) + + val orphans = allIds - visited + if (orphans.isNotEmpty()) { + val firstOrphan = orphans.sorted().first() + throw IllegalArgumentException("Component '$firstOrphan' is not reachable from '$rootId'") + } + } + + private fun getComponentReferences( + component: JsonObject, + refFieldsMap: Map, Set>>, + ): Sequence> = sequence { + if (PROP_COMPONENT in component) { + when (val compVal = component[PROP_COMPONENT]) { + is JsonPrimitive -> { + if (compVal.isString) + yieldAll(getRefsRecursively(compVal.content, component, refFieldsMap)) + } + is JsonObject -> { + for ((cType, cProps) in compVal) { + if (cProps is JsonObject) { + yieldAll(getRefsRecursively(cType, cProps, refFieldsMap)) + } + } + } + else -> {} + } + } + } + + private fun getRefsRecursively( + compType: String, + props: JsonObject, + refFieldsMap: Map, Set>>, + ): Sequence> = sequence { + val (singleRefs, listRefs) = refFieldsMap[compType] ?: (emptySet() to emptySet()) + + for ((key, value) in props) { + when { + key in singleRefs -> { + when { + value is JsonPrimitive && value.isString -> yield(value.content to key) + value is JsonObject && PROP_COMPONENT_ID in value -> { + value[PROP_COMPONENT_ID]?.jsonPrimitive?.content?.let { + yield(it to "$key.$PROP_COMPONENT_ID") + } + } + } + } + key in listRefs -> { + when (value) { + is JsonArray -> { + for (item in value) { + if (item is JsonPrimitive && item.isString) yield(item.content to key) + } + } + is JsonObject -> { + when { + PROP_EXPLICIT_LIST in value -> { + (value[PROP_EXPLICIT_LIST] as? JsonArray)?.forEach { item -> + if (item is JsonPrimitive && item.isString) + yield(item.content to "$key.$PROP_EXPLICIT_LIST") + } + } + PROP_TEMPLATE in value -> { + val template = value[PROP_TEMPLATE] as? JsonObject + template?.get(PROP_COMPONENT_ID)?.jsonPrimitive?.content?.let { + yield(it to "$key.$PROP_TEMPLATE.$PROP_COMPONENT_ID") + } + } + PROP_COMPONENT_ID in value -> { + value[PROP_COMPONENT_ID]?.jsonPrimitive?.content?.let { + yield(it to "$key.$PROP_COMPONENT_ID") + } + } + } + } + else -> {} + } + } + value is JsonArray -> { + for ((idx, item) in value.withIndex()) { + if (item is JsonObject) { + item[PROP_CHILD]?.jsonPrimitive?.content?.let { + yield(it to "$key[$idx].$PROP_CHILD") + } + } + } + } + } + } + } + } + + /** Validates JSON payload recursion depth and functional call depth. */ + private class A2uiRecursionValidator { + fun validate(data: JsonElement) = traverse(data, 0, 0) + + private fun traverse(item: JsonElement, globalDepth: Int, funcDepth: Int) { + if (globalDepth > MAX_GLOBAL_DEPTH) { + throw IllegalArgumentException("Global recursion limit exceeded: Depth > $MAX_GLOBAL_DEPTH") + } + + when (item) { + is JsonArray -> item.forEach { traverse(it, globalDepth + 1, funcDepth) } + is JsonObject -> { + (item[PATH] as? JsonPrimitive) + ?.takeIf { it.isString } + ?.let { pathElem -> + if (!pathElem.content.matches(JSON_POINTER_PATTERN)) { + throw IllegalArgumentException("Invalid JSON Pointer syntax: '${pathElem.content}'") + } + } + + val isFunc = CALL in item && ARGS in item + if (isFunc) { + if (funcDepth >= MAX_FUNC_CALL_DEPTH) { + throw IllegalArgumentException( + "Recursion limit exceeded: $FUNCTION_CALL depth > $MAX_FUNC_CALL_DEPTH" + ) + } + for ((k, v) in item) { + val nextFuncDepth = if (k == ARGS) funcDepth + 1 else funcDepth + traverse(v, globalDepth + 1, nextFuncDepth) + } + } else { + item.values.forEach { traverse(it, globalDepth + 1, funcDepth) } + } + } + else -> {} + } + } + } + + private companion object { + private val JSON_POINTER_PATTERN = Regex("^(?:/(?:[^~/]|~[01])*)*$") + private const val MAX_GLOBAL_DEPTH = 50 + private const val MAX_FUNC_CALL_DEPTH = 5 + + private const val ROOT = "root" + private const val ID = "id" + private const val PATH = "path" + private const val FUNCTION_CALL = "functionCall" + private const val CALL = "call" + private const val ARGS = "args" + + private const val MSG_SURFACE_UPDATE = "surfaceUpdate" + private const val MSG_UPDATE_COMPONENTS = "updateComponents" + private const val MSG_BEGIN_RENDERING = "beginRendering" + + // JSON Schema standard keys + private const val KEY_DOLLAR_SCHEMA = "\$schema" + private const val KEY_DOLLAR_ID = "\$id" + private const val KEY_DOLLAR_REF = "\$ref" + private const val PROP_PROPERTIES = "properties" + private const val PROP_ADDITIONAL_PROPERTIES = "additionalProperties" + private const val PROP_TYPE = "type" + private const val PROP_TITLE = "title" + private const val PROP_ITEMS = "items" + + // JSON Schema combinators + private const val COMBINATOR_ONE_OF = "oneOf" + private const val COMBINATOR_ANY_OF = "anyOf" + private const val COMBINATOR_ALL_OF = "allOf" + + // Types & Drafts + private const val TYPE_STRING = "string" + private const val TYPE_OBJECT = "object" + private const val TYPE_ARRAY = "array" + private const val SCHEMA_DRAFT_2020_12 = "https://json-schema.org/draft/2020-12/schema" + private const val FILE_COMMON_TYPES = "common_types.json" + + // A2UI Component Graph Keys + private const val PROP_COMPONENT = "component" + private const val PROP_CHILD = "child" + private const val PROP_CHILDREN = "children" + private const val PROP_CONTENT_CHILD = "contentChild" + private const val PROP_ENTRY_POINT_CHILD = "entryPointChild" + private const val PROP_COMPONENT_ID = "componentId" + private const val PROP_EXPLICIT_LIST = "explicitList" + private const val PROP_TEMPLATE = "template" + private const val TITLE_COMPONENT_ID = "ComponentId" + private const val TITLE_CHILD_LIST = "ChildList" + } +} diff --git a/agent_sdks/kotlin/src/test/kotlin/com/google/a2ui/a2a/A2aHandlerTest.kt b/agent_sdks/kotlin/src/test/kotlin/com/google/a2ui/a2a/A2aHandlerTest.kt new file mode 100644 index 000000000..349f0d98d --- /dev/null +++ b/agent_sdks/kotlin/src/test/kotlin/com/google/a2ui/a2a/A2aHandlerTest.kt @@ -0,0 +1,291 @@ +/* + * 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 + * + * https://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.google.a2ui.a2a + +import com.google.adk.agents.RunConfig +import com.google.adk.events.Event +import com.google.adk.runner.Runner +import com.google.adk.sessions.BaseSessionService +import com.google.adk.sessions.GetSessionConfig +import com.google.adk.sessions.Session +import com.google.genai.types.Content +import com.google.genai.types.Part +import io.mockk.every +import io.mockk.mockk +import io.reactivex.rxjava3.core.Flowable +import io.reactivex.rxjava3.core.Maybe +import java.util.Optional +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +@Suppress("DEPRECATION", "UNCHECKED_CAST") +class A2aHandlerTest { + + @Test + fun handleA2aPost_replacesSmartQuotesInA2uiJson() { + val mockRunner = mockk(relaxed = true) + val mockSessionService = mockk(relaxed = true) + val mockSession = mockk(relaxed = true) + + every { mockRunner.appName() } returns "test-app" + every { mockRunner.sessionService() } returns mockSessionService + every { + mockSessionService.getSession( + any(), + any(), + any(), + any>(), + ) + } returns Maybe.just(mockSession) + + val smartQuotesJson = + "[\n {\n “beginRendering”: {\n “surfaceId”: “sales-dashboard”\n }\n }\n]" + + val expectedDataObj = mapOf("beginRendering" to mapOf("surfaceId" to "sales-dashboard")) + + val partText = "Here is your chart:\n\n$smartQuotesJson\n\nEnjoy!" + val expectedNormalizedText = "Here is your chart:\n\nEnjoy!" + + val mockPart = mockk(relaxed = true) + every { mockPart.text() } returns Optional.of(partText) + every { mockPart.functionCall() } returns Optional.empty() + + val mockContent = mockk(relaxed = true) + every { mockContent.parts() } returns Optional.of(listOf(mockPart)) + + val mockEvent = mockk(relaxed = true) + every { mockEvent.id() } returns "test-event-id" + every { mockEvent.content() } returns Optional.of(mockContent) + + every { + mockRunner.runAsync(any(), any(), any()) + } returns Flowable.just(mockEvent) + + val handler = A2aHandler(mockRunner) + + val requestBody = + mapOf( + "jsonrpc" to "2.0", + "method" to "message/send", + "id" to 1, + "params" to + mapOf( + "message" to + mapOf( + "contextId" to "test-context", + "parts" to emptyList(), + "metadata" to + mapOf( + "a2uiClientCapabilities" to + mapOf( + "supportedCatalogIds" to + listOf( + "https://a2ui.org/specification/v0_8/standard_catalog_definition.json" + ) + ) + ), + ) + ), + ) + + val response = handler.handleA2aPost(requestBody) + + @Suppress("UNCHECKED_CAST") val result = response["result"] as Map + @Suppress("UNCHECKED_CAST") val parts = result["parts"] as List> + + assertEquals(3, parts.size) + assertEquals("Here is your chart:", parts[0]["text"]) + + val dataPart = parts[1] + assertEquals("data", dataPart["kind"]) + assertEquals("application/json+a2ui", (dataPart["metadata"] as Map)["mimeType"]) + assertEquals(expectedDataObj, dataPart["data"]) + + assertEquals("Enjoy!", parts[2]["text"]) + } + + @Test + fun handleA2aPost_passesRegularTextThrough() { + val mockRunner = mockk(relaxed = true) + val mockSessionService = mockk(relaxed = true) + val mockSession = mockk(relaxed = true) + + every { mockRunner.appName() } returns "test-app" + every { mockRunner.sessionService() } returns mockSessionService + every { + mockSessionService.getSession( + any(), + any(), + any(), + any>(), + ) + } returns Maybe.just(mockSession) + + val partText = "This is a normal conversational turn." + + val mockPart = mockk(relaxed = true) + every { mockPart.text() } returns Optional.of(partText) + every { mockPart.functionCall() } returns Optional.empty() + + val mockContent = mockk(relaxed = true) + every { mockContent.parts() } returns Optional.of(listOf(mockPart)) + + val mockEvent = mockk(relaxed = true) + every { mockEvent.id() } returns "test-event-id" + every { mockEvent.content() } returns Optional.of(mockContent) + + every { + mockRunner.runAsync(any(), any(), any()) + } returns Flowable.just(mockEvent) + + val handler = A2aHandler(mockRunner) + + val requestBody = + mapOf( + "jsonrpc" to "2.0", + "method" to "message/send", + "id" to 1, + "params" to + mapOf( + "message" to + mapOf( + "contextId" to "test-context", + "parts" to emptyList(), + "metadata" to + mapOf( + "a2uiClientCapabilities" to + mapOf( + "supportedCatalogIds" to + listOf( + "https://a2ui.org/specification/v0_8/standard_catalog_definition.json" + ) + ) + ), + ) + ), + ) + + val response = handler.handleA2aPost(requestBody) + + @Suppress("UNCHECKED_CAST") val result = response["result"] as Map + @Suppress("UNCHECKED_CAST") val parts = result["parts"] as List> + + assertEquals(1, parts.size) + assertEquals("text", parts[0]["kind"]) + assertEquals(partText, parts[0]["text"]) + } + + @Test + fun handleA2aPost_gracefullyHandlesInvalidA2uiJson() { + val mockRunner = mockk(relaxed = true) + val mockSessionService = mockk(relaxed = true) + val mockSession = mockk(relaxed = true) + + every { mockRunner.appName() } returns "test-app" + every { mockRunner.sessionService() } returns mockSessionService + every { + mockSessionService.getSession( + any(), + any(), + any(), + any>(), + ) + } returns Maybe.just(mockSession) + + val invalidJson = "[\n {\n “beginRendering”: { MISSING CLOSING BRACKETS..." + + val partText = "Here is an invalid chart:\n\n$invalidJson\n\nOops." + + val mockPart = mockk(relaxed = true) + every { mockPart.text() } returns Optional.of(partText) + every { mockPart.functionCall() } returns Optional.empty() + + val mockContent = mockk(relaxed = true) + every { mockContent.parts() } returns Optional.of(listOf(mockPart)) + + val mockEvent = mockk(relaxed = true) + every { mockEvent.id() } returns "test-event-id" + every { mockEvent.content() } returns Optional.of(mockContent) + + every { + mockRunner.runAsync(any(), any(), any()) + } returns Flowable.just(mockEvent) + + val handler = A2aHandler(mockRunner) + + val requestBody = + mapOf( + "jsonrpc" to "2.0", + "method" to "message/send", + "id" to 1, + "params" to + mapOf( + "message" to + mapOf( + "contextId" to "test-context", + "parts" to emptyList(), + "metadata" to + mapOf( + "a2uiClientCapabilities" to + mapOf( + "supportedCatalogIds" to + listOf( + "https://a2ui.org/specification/v0_8/standard_catalog_definition.json" + ) + ) + ), + ) + ), + ) + + val response = handler.handleA2aPost(requestBody) + + @Suppress("UNCHECKED_CAST") val result = response["result"] as Map + @Suppress("UNCHECKED_CAST") val parts = result["parts"] as List> + + assertEquals(1, parts.size) + assertEquals("text", parts[0]["kind"]) + assertEquals(partText, parts[0]["text"]) + } + + @Test + fun handleA2aPost_invokesSessionPreparerWithFullRequestBody() { + val mockRunner = mockk(relaxed = true) + val mockSessionService = mockk(relaxed = true) + val mockSession = mockk(relaxed = true) + + every { mockRunner.appName() } returns "test-app" + every { mockRunner.sessionService() } returns mockSessionService + every { mockSessionService.getSession(any(), any(), any(), any()) } returns + Maybe.just(mockSession) + + val handler = A2aHandler(mockRunner) + + val requestBody = + mapOf( + "jsonrpc" to "2.0", + "method" to "message/send", + "id" to 1, + "params" to mapOf("message" to mapOf("parts" to emptyList())), + ) + var capturedRequestBody: Map<*, *>? = null + + handler.handleA2aPost(requestBody) { _, rb -> capturedRequestBody = rb } + + assertEquals(requestBody, capturedRequestBody) + } +} diff --git a/agent_sdks/kotlin/src/test/kotlin/com/google/a2ui/a2a/A2uiA2aTest.kt b/agent_sdks/kotlin/src/test/kotlin/com/google/a2ui/a2a/A2uiA2aTest.kt new file mode 100644 index 000000000..e5170a043 --- /dev/null +++ b/agent_sdks/kotlin/src/test/kotlin/com/google/a2ui/a2a/A2uiA2aTest.kt @@ -0,0 +1,60 @@ +/* + * 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 + * + * https://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.google.a2ui.a2a + +import io.a2a.spec.DataPart +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put + +class A2uiA2aTest { + @Test + fun createA2uiPart_setsCorrectMimeType() { + val data = buildJsonObject { put("foo", "bar") } + val part = A2uiA2a.createA2uiPart(data) + assertTrue(part is DataPart) + assertEquals(A2uiA2a.A2UI_MIME_TYPE, (part as DataPart).metadata?.get(A2uiA2a.MIME_TYPE_KEY)) + } + + @Test + fun isA2uiPart_identifiesCorrectPart() { + val data = buildJsonObject { put("foo", "bar") } + val part = A2uiA2a.createA2uiPart(data) + assertTrue(A2uiA2a.isA2uiPart(part)) + } + + @Test + fun tryActivateA2uiExtension_requested_returnsTrue() { + val activated = mutableListOf() + val result = + A2uiA2a.tryActivateA2uiExtension(listOf(A2uiA2a.A2UI_EXTENSION_URI)) { activated.add(it) } + + assertTrue(result) + assertEquals(listOf(A2uiA2a.A2UI_EXTENSION_URI), activated) + } + + @Test + fun tryActivateA2uiExtension_notRequested_returnsFalse() { + val activated = mutableListOf() + val result = A2uiA2a.tryActivateA2uiExtension(listOf("other")) { activated.add(it) } + + kotlin.test.assertFalse(result) + assertTrue(activated.isEmpty()) + } +} diff --git a/agent_sdks/kotlin/src/test/kotlin/com/google/a2ui/adk/a2a_extension/ConvertersTest.kt b/agent_sdks/kotlin/src/test/kotlin/com/google/a2ui/adk/a2a_extension/ConvertersTest.kt new file mode 100644 index 000000000..19e8b39b7 --- /dev/null +++ b/agent_sdks/kotlin/src/test/kotlin/com/google/a2ui/adk/a2a_extension/ConvertersTest.kt @@ -0,0 +1,146 @@ +/* + * 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 + * + * https://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.google.a2ui.adk.a2a_extension + +import com.google.a2ui.core.schema.A2uiCatalog +import com.google.adk.a2a.converters.EventConverter +import com.google.adk.agents.InvocationContext +import com.google.adk.events.Event +import com.google.adk.sessions.Session +import com.google.common.collect.ImmutableList +import com.google.genai.types.Content +import com.google.genai.types.FinishReason +import com.google.genai.types.Part +import io.a2a.spec.TaskState +import io.a2a.spec.TaskStatusUpdateEvent +import io.a2a.spec.TextPart +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import java.util.Optional +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertTrue + +class A2uiEventConverterTest { + + @Test + fun `emits FAILED TaskStatusUpdateEvent when event has errorCode`() { + val session = mockk() + every { session.state() } returns + java.util.concurrent.ConcurrentHashMap( + mapOf("system:a2ui_catalog" to mockk()) + ) + + val context = mockk() + every { context.session() } returns session + + val mockEvent = mockk() + val finishReasonArg = mockk() + every { mockEvent.errorCode() } returns Optional.of(finishReasonArg) + every { mockEvent.errorMessage() } returns Optional.of("Server crash") + every { mockEvent.content() } returns Optional.empty() + every { mockEvent.author() } returns "test_author" + + mockkStatic(EventConverter::class) + every { EventConverter.taskId(mockEvent) } returns "task-1" + every { EventConverter.contextId(mockEvent) } returns "context-1" + + val converter = A2uiEventConverter() + val results = converter.convert(mockEvent, context) + + assertEquals(1, results.size) + val result = results[0] + assertIs(result) + + assertEquals("task-1", result.taskId()) + assertEquals("context-1", result.contextId()) + assertEquals(TaskState.TASK_STATE_FAILED, result.status().state()) + + val msg = result.status().message()!! + assertEquals(1, msg.parts()!!.size) + val part = msg.parts()!![0] as TextPart + assertEquals("Server crash", part.text()) + } + + @Test + fun `emits WORKING TaskStatusUpdateEvent when event has standard content`() { + val catalog = mockk() + val session = mockk() + every { session.state() } returns + java.util.concurrent.ConcurrentHashMap(mapOf("system:a2ui_catalog" to catalog)) + + val context = mockk() + every { context.session() } returns session + + // Using Mockk for GenAI Content and Part + val mockGenaiPart = mockk() + every { mockGenaiPart.functionResponse() } returns Optional.empty() + every { mockGenaiPart.functionCall() } returns Optional.empty() + every { mockGenaiPart.text() } returns Optional.of("Regular text response") + + val mockContent = mockk() + every { mockContent.parts() } returns Optional.of(listOf(mockGenaiPart)) + + val mockEvent = mockk() + every { mockEvent.errorCode() } returns Optional.empty() + every { mockEvent.content() } returns Optional.of(mockContent) + every { mockEvent.author() } returns "test_author" + + mockkStatic(EventConverter::class) + // mock behavior of fallback standard conversion + every { EventConverter.contentToParts(any(), false) } returns + ImmutableList.of(TextPart("Regular text response")) + + every { EventConverter.taskId(mockEvent) } returns "task-123" + every { EventConverter.contextId(mockEvent) } returns "context-123" + + val converter = A2uiEventConverter() + val results = converter.convert(mockEvent, context) + + assertEquals(1, results.size) + val result = results[0] + assertIs(result) + + assertEquals("task-123", result.taskId()) + assertEquals("context-123", result.contextId()) + assertEquals(TaskState.TASK_STATE_WORKING, result.status().state()) + + val msg = result.status().message()!! + assertTrue(msg.parts()!!.isNotEmpty()) + val part = msg.parts()!![0] as TextPart + assertEquals("Regular text response", part.text()) + } + + @Test + fun `returns empty list when catalog is missing from session state`() { + val session = mockk() + every { session.state() } returns + java.util.concurrent.ConcurrentHashMap() // No catalog! + + val context = mockk() + every { context.session() } returns session + + val event = mockk() + + val converter = A2uiEventConverter() + val results = converter.convert(event, context) + + assertTrue(results.isEmpty()) + } +} diff --git a/agent_sdks/kotlin/src/test/kotlin/com/google/a2ui/adk/a2a_extension/SendA2uiToClientToolsetTest.kt b/agent_sdks/kotlin/src/test/kotlin/com/google/a2ui/adk/a2a_extension/SendA2uiToClientToolsetTest.kt new file mode 100644 index 000000000..3b1e86531 --- /dev/null +++ b/agent_sdks/kotlin/src/test/kotlin/com/google/a2ui/adk/a2a_extension/SendA2uiToClientToolsetTest.kt @@ -0,0 +1,106 @@ +/* + * 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 + * + * https://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.google.a2ui.adk.a2a_extension + +import com.google.a2ui.core.schema.A2uiCatalog +import com.google.a2ui.core.schema.A2uiVersion +import com.google.adk.agents.ReadonlyContext +import com.google.adk.tools.ToolContext +import io.mockk.mockk +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class SendA2uiToClientToolsetTest { + + private val catalogSchema = + Json.parseToJsonElement( + """ + { + "catalogId": "dummy", + "components": { + "TestComp": {"type": "object"} + } + } + """ + ) + .jsonObject as JsonObject + + // Minimal permissive serverToClient schema for testing + private val serverToClientSchema = + Json.parseToJsonElement( + """ + { + "type": "object", + "properties": { + "beginRendering": {"type": "object", "properties": {"root": {"type": "object"}}} + } + } + """ + ) + .jsonObject as JsonObject + + private val dummyCatalog = + A2uiCatalog( + version = A2uiVersion.VERSION_0_9, + name = "dummy", + serverToClientSchema = serverToClientSchema, + commonTypesSchema = JsonObject(emptyMap()), + catalogSchema = catalogSchema, + ) + + private val mockContext = mockk(relaxed = true) + private val mockToolContext = mockk(relaxed = true) + + // Removed testsNameAndDescription because BaseTool properties are protected/private in ADK + + @Test + fun execute_validPayload_emitsPayload() { + val toolset = SendA2uiToClientToolset.create(true, dummyCatalog, "") + val tool = toolset.getTools(mockContext).blockingFirst() + + val a2uiJsonStr = """{"beginRendering": {"root": {"component": "TestComp", "id": "1"}}}""" + + val args = mapOf("a2ui_json" to a2uiJsonStr) + val result = tool.runAsync(args, mockToolContext).blockingGet() + + assertNotNull(result[SendA2uiToClientToolset.VALIDATED_A2UI_JSON_KEY]) + val validatedPayload = + result[SendA2uiToClientToolset.VALIDATED_A2UI_JSON_KEY] + as kotlinx.serialization.json.JsonElement + + assertTrue(validatedPayload.toString().contains("beginRendering")) + assertTrue(validatedPayload.toString().contains("TestComp")) + } + + @Test + fun execute_invalidPayload_rejectsPayload() { + val toolset = SendA2uiToClientToolset.create(true, dummyCatalog, "") + val tool = toolset.getTools(mockContext).blockingFirst() + + // Missing a2ui_json argument entirely + val args = mapOf("wrong_arg" to "b") + val result = tool.runAsync(args, mockToolContext).blockingGet() + + assertNotNull(result[SendA2uiToClientToolset.TOOL_ERROR_KEY]) + val errorMsg = result[SendA2uiToClientToolset.TOOL_ERROR_KEY] as String + assertTrue(errorMsg.contains("missing required arg a2ui_json")) + } +} diff --git a/agent_sdks/kotlin/src/test/kotlin/com/google/a2ui/core/parser/ParserTest.kt b/agent_sdks/kotlin/src/test/kotlin/com/google/a2ui/core/parser/ParserTest.kt new file mode 100644 index 000000000..cc12d3a78 --- /dev/null +++ b/agent_sdks/kotlin/src/test/kotlin/com/google/a2ui/core/parser/ParserTest.kt @@ -0,0 +1,107 @@ +/* + * 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 + * + * https://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.google.a2ui.core.parser + +import com.google.a2ui.core.schema.A2uiConstants +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class ParserTest { + + @Test + fun mixedText_extractsA2uiBlocks() { + val mixedText = + """ + Here is some response text. + ${A2uiConstants.A2UI_OPEN_TAG} + {"a": 1} + ${A2uiConstants.A2UI_CLOSE_TAG} + And some trailing output. + """ + .trimIndent() + + val blocks = parseResponseToParts(mixedText) + assertEquals(2, blocks.size) + assertEquals("Here is some response text.", blocks[0].text) + assertEquals("{\"a\":1}", blocks[0].a2uiJson!![0].toString()) + assertEquals("And some trailing output.", blocks[1].text) + assertEquals(null, blocks[1].a2uiJson) + } + + @Test + fun noTagsPresent_throwsException() { + val text = "Just some random { \"key\": null } JSON without tags" + var threw = false + try { + parseResponseToParts(text) + } catch (e: IllegalArgumentException) { + threw = true + } + assertTrue(threw) + } + + @Test + fun textWithTags_returnsTrueForHasA2uiParts() { + assertTrue( + hasA2uiParts("foo\n${A2uiConstants.A2UI_OPEN_TAG}\nbar\n${A2uiConstants.A2UI_CLOSE_TAG}") + ) + assertFalse(hasA2uiParts("No parts here!")) + } + + @Test + fun multipleBlocks_extractsRightBlocks() { + val mixedText = + """ + Prefix + ${A2uiConstants.A2UI_OPEN_TAG}{"first": 1}${A2uiConstants.A2UI_CLOSE_TAG} + Middle + ${A2uiConstants.A2UI_OPEN_TAG} {"second": 2} ${A2uiConstants.A2UI_CLOSE_TAG} + Suffix + """ + .trimIndent() + + val blocks = parseResponseToParts(mixedText) + assertEquals(3, blocks.size) + assertEquals("Prefix", blocks[0].text) + assertEquals("{\"first\":1}", blocks[0].a2uiJson!![0].toString()) + assertEquals("Middle", blocks[1].text) + assertEquals("{\"second\":2}", blocks[1].a2uiJson!![0].toString()) + assertEquals("Suffix", blocks[2].text) + assertEquals(null, blocks[2].a2uiJson) + } + + @Test + fun invalidJson_throwsException() { + val mixedText = + """ + ${A2uiConstants.A2UI_OPEN_TAG} + { invalid_json: + ${A2uiConstants.A2UI_CLOSE_TAG} + """ + .trimIndent() + + var threw = false + try { + parseResponseToParts(mixedText) + } catch (e: Exception) { + threw = true + } + assertTrue(threw) + } +} diff --git a/agent_sdks/kotlin/src/test/kotlin/com/google/a2ui/core/parser/PayloadFixerTest.kt b/agent_sdks/kotlin/src/test/kotlin/com/google/a2ui/core/parser/PayloadFixerTest.kt new file mode 100644 index 000000000..33d9a14b3 --- /dev/null +++ b/agent_sdks/kotlin/src/test/kotlin/com/google/a2ui/core/parser/PayloadFixerTest.kt @@ -0,0 +1,91 @@ +/* + * 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 + * + * https://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.google.a2ui.core.parser + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class PayloadFixerTest { + + @Test + fun arrayWithTrailingCommas_commasRemoved() { + val invalidJson = + """ + [ + {"foo": "bar"}, + {"foo": "baz"}, + ] + """ + .trimIndent() + val result = PayloadFixer.parseAndFix(invalidJson) + assertEquals(2, result.size) + } + + @Test + fun objectWithTrailingCommas_commasRemoved() { + val invalidJson = + """ + { + "foo": "bar", + "foo2": "baz", + } + """ + .trimIndent() + val result = PayloadFixer.parseAndFix(invalidJson) + assertEquals(1, result.size) + } + + @Test + fun edgeCasesWithTrailingCommas_commasRemoved() { + assertEquals("""{"a": 1}""", PayloadFixer.removeTrailingCommas("""{"a": 1,}""")) + assertEquals("""[1, 2, 3]""", PayloadFixer.removeTrailingCommas("""[1, 2, 3,]""")) + assertEquals("""{"a": [1, 2]}""", PayloadFixer.removeTrailingCommas("""{"a": [1, 2,]}""")) + } + + @Test + fun commasInStrings_notRemoved() { + val jsonWithCommasInStrings = """{"text": "Hello, world", "array": ["a,b", "c"]}""" + assertEquals( + jsonWithCommasInStrings, + PayloadFixer.removeTrailingCommas(jsonWithCommasInStrings), + ) + + val trickyJson = """{"text": "Ends with comma,]"}""" + assertEquals(trickyJson, PayloadFixer.removeTrailingCommas(trickyJson)) + } + + @Test + fun validJson_remainsUntouched() { + val validJson = """[{"foo": "bar"}]""" + val result = PayloadFixer.parseAndFix(validJson) + assertEquals(1, result.size) + } + + @Test + fun unrecoverableJson_throwsException() { + val badJson = "not_json_at_all" + assertFailsWith { PayloadFixer.parseAndFix(badJson) } + } + + @Test + fun normalizeSmartQuotes_replacesQuotesCorrectly() { + val input = "“smart” ‘quotes’" + val expected = "\"smart\" 'quotes'" + assertEquals(expected, PayloadFixer.normalizeSmartQuotes(input)) + } +} diff --git a/agent_sdks/kotlin/src/test/kotlin/com/google/a2ui/core/parser/SanitizationTest.kt b/agent_sdks/kotlin/src/test/kotlin/com/google/a2ui/core/parser/SanitizationTest.kt new file mode 100644 index 000000000..ebaeadfe5 --- /dev/null +++ b/agent_sdks/kotlin/src/test/kotlin/com/google/a2ui/core/parser/SanitizationTest.kt @@ -0,0 +1,52 @@ +/* + * 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 + * + * https://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.google.a2ui.core.parser + +import kotlin.test.Test +import kotlin.test.assertEquals + +class SanitizationTest { + @Test + fun sanitizeJsonString_stripsMarkdown() { + val input = + """ + ```json + {"a": 1} + ``` + """ + .trimIndent() + assertEquals("{\"a\": 1}", sanitizeJsonString(input)) + } + + @Test + fun sanitizeJsonString_stripsRawMarkdown() { + val input = + """ + ``` + {"a": 1} + ``` + """ + .trimIndent() + assertEquals("{\"a\": 1}", sanitizeJsonString(input)) + } + + @Test + fun sanitizeJsonString_noMarkdown() { + val input = """{"a": 1}""" + assertEquals("{\"a\": 1}", sanitizeJsonString(input)) + } +} diff --git a/agent_sdks/kotlin/src/test/kotlin/com/google/a2ui/core/schema/A2uiSchemaManagerTest.kt b/agent_sdks/kotlin/src/test/kotlin/com/google/a2ui/core/schema/A2uiSchemaManagerTest.kt new file mode 100644 index 000000000..2ae027c7c --- /dev/null +++ b/agent_sdks/kotlin/src/test/kotlin/com/google/a2ui/core/schema/A2uiSchemaManagerTest.kt @@ -0,0 +1,190 @@ +/* + * 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 + * + * https://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.google.a2ui.core.schema + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject + +class A2uiSchemaManagerTest { + + private val dummyProvider = + object : A2uiCatalogProvider { + override fun load(): JsonObject { + return Json.parseToJsonElement( + """ + { + "catalogId": "test_cat", + "components": { + "TestComp": { + "type": "object" + } + } + } + """ + ) + .jsonObject + } + } + + private val catalogConfig = CatalogConfig("Test", dummyProvider) + + @Test + fun instantiation_localCatalogSet_succeeds() { + val manager = A2uiSchemaManager(A2uiVersion.VERSION_0_9, listOf(catalogConfig)) + assertEquals(listOf("test_cat"), manager.supportedCatalogIds) + } + + @Test + fun getSystemPrompt_promptRequested_returnsInstructions() { + val manager = A2uiSchemaManager(A2uiVersion.VERSION_0_9, listOf(catalogConfig)) + val prompt = + manager.generateSystemPrompt( + roleDescription = "You are a helpful UI agent.", + workflowDescription = "Render simple UI.", + uiDescription = "Use TestComp heavily.", + clientUiCapabilities = null, + allowedComponents = emptyList(), + includeSchema = true, + includeExamples = false, + validateExamples = false, + ) + + assertTrue(prompt.contains("You are a helpful UI agent.")) + assertTrue(prompt.contains("## Workflow Description:\n")) + assertTrue(prompt.contains("Render simple UI.")) + assertTrue(prompt.contains("## UI Description:\nUse TestComp heavily.")) + assertTrue(prompt.contains(A2uiConstants.A2UI_SCHEMA_BLOCK_START)) + assertTrue(prompt.contains("\"test_cat\"")) + } + + @Test + fun getSelectedCatalog_inlineCatalogsCapabilityProvided_returnsInlineCatalog() { + // manager must have acceptsInlineCatalogs = true + val manager = + A2uiSchemaManager( + version = A2uiVersion.VERSION_0_9, + catalogs = listOf(catalogConfig), + acceptsInlineCatalogs = true, + ) + + val inlineCaps = + Json.parseToJsonElement( + """ + { + "inlineCatalogs": [ + { + "components": { + "client_comp": {"type": "object"} + } + } + ] + } + """ + ) + .jsonObject + + val catalog = manager.getSelectedCatalog(inlineCaps, emptyList()) + assertEquals("test_cat", catalog.catalogId) // Base catalog ID is preserved + assertEquals("inline", catalog.name) + assertTrue( + "TestComp" in catalog.catalogSchema[A2uiConstants.CATALOG_COMPONENTS_KEY]!!.jsonObject + ) + assertTrue( + "client_comp" in catalog.catalogSchema[A2uiConstants.CATALOG_COMPONENTS_KEY]!!.jsonObject + ) + } + + @Test + fun getSelectedCatalog_inlineCatalogsDisabled_returnsFallback() { + val manager = + A2uiSchemaManager( + version = A2uiVersion.VERSION_0_9, + catalogs = listOf(catalogConfig), + acceptsInlineCatalogs = false, // Disabled + ) + + val inlineCaps = + Json.parseToJsonElement( + """ + { "inlineCatalogs": [{"catalogId": "inline", "components": {}}] } + """ + ) + .jsonObject + + assertFailsWith("Agent does not accept inline catalogs") { + manager.getSelectedCatalog(inlineCaps, emptyList()) + } + } + + @Test + fun getSelectedCatalog_unsupportedCatalogRequested_throwsException() { + val manager = A2uiSchemaManager(A2uiVersion.VERSION_0_9, listOf(catalogConfig)) + + val caps = + Json.parseToJsonElement( + """ + { "supportedCatalogIds": ["unknown_catalog_id"] } + """ + ) + .jsonObject + + assertFailsWith("No client-supported catalog found") { + manager.getSelectedCatalog(caps, emptyList()) + } + } + + @Test + fun getSelectedCatalog_inlineAndSupportedCapabilitiesProvided_mergesOntoSpecifiedBase() { + val manager = + A2uiSchemaManager( + version = A2uiVersion.VERSION_0_9, + catalogs = listOf(catalogConfig), + acceptsInlineCatalogs = true, + ) + + val caps = + Json.parseToJsonElement( + """ + { + "supportedCatalogIds": ["test_cat"], + "inlineCatalogs": [ + { + "components": { + "client_comp": {"type": "object"} + } + } + ] + } + """ + ) + .jsonObject + + val catalog = manager.getSelectedCatalog(caps, emptyList()) + assertEquals("test_cat", catalog.catalogId) + assertTrue( + "TestComp" in catalog.catalogSchema[A2uiConstants.CATALOG_COMPONENTS_KEY]!!.jsonObject + ) + assertTrue( + "client_comp" in catalog.catalogSchema[A2uiConstants.CATALOG_COMPONENTS_KEY]!!.jsonObject + ) + } +} diff --git a/agent_sdks/kotlin/src/test/kotlin/com/google/a2ui/core/schema/CatalogTest.kt b/agent_sdks/kotlin/src/test/kotlin/com/google/a2ui/core/schema/CatalogTest.kt new file mode 100644 index 000000000..d18dc13d5 --- /dev/null +++ b/agent_sdks/kotlin/src/test/kotlin/com/google/a2ui/core/schema/CatalogTest.kt @@ -0,0 +1,115 @@ +/* + * 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 + * + * https://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.google.a2ui.core.schema + +import java.io.File +import kotlin.test.Test +import kotlin.test.assertTrue +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject + +class CatalogTest { + + private fun createDummyCatalog(): A2uiCatalog { + val serverToClientSchema = Json.parseToJsonElement("""{"s2c": true}""") as JsonObject + val common = Json.parseToJsonElement("""{"common": true}""") as JsonObject + val catalogSchema = + Json.parseToJsonElement( + """ + { + "catalogId": "dummy_catalog", + "components": { + "AllowedComp": {"type": "object"}, + "PrunedComp": {"type": "object"} + }, + "${"$"}defs": { + "anyComponent": { + "oneOf": [ + {"${"$"}ref": "#/components/AllowedComp"}, + {"${"$"}ref": "#/components/PrunedComp"}, + {"${"$"}ref": "https://a2ui.org/other"} + ] + } + } + } + """ + .trimIndent() + ) as JsonObject + + return A2uiCatalog( + version = A2uiVersion.VERSION_0_9, + name = "dummy", + serverToClientSchema = serverToClientSchema, + commonTypesSchema = common, + catalogSchema = catalogSchema, + ) + } + + @Test + fun catalog_rendersAsLlmInstructions() { + val catalog = createDummyCatalog() + val instructions = catalog.renderAsLlmInstructions() + + assertTrue(instructions.startsWith(A2uiConstants.A2UI_SCHEMA_BLOCK_START)) + assertTrue(instructions.endsWith(A2uiConstants.A2UI_SCHEMA_BLOCK_END)) + assertTrue(instructions.contains("### Server To Client Schema:\n{\"s2c\":true}")) + assertTrue(instructions.contains("### Common Types Schema:\n{\"common\":true}")) + assertTrue(instructions.contains("### Catalog Schema:\n{")) + } + + @Test + fun allowlistProvided_prunesComponentsCorrectly() { + val catalog = createDummyCatalog() + val allowed = listOf("AllowedComp") + + val prunedCatalog = catalog.withPrunedComponents(allowed) + + val prunedComponents = prunedCatalog.catalogSchema["components"] as JsonObject + assertTrue("AllowedComp" in prunedComponents) + assertTrue("PrunedComp" !in prunedComponents) + + // Verify oneOf filtering in anyComponent + val anyComponentStr = prunedCatalog.catalogSchema.toString() + assertTrue("#/components/AllowedComp" in anyComponentStr) + assertTrue("#/components/PrunedComp" !in anyComponentStr) + } + + @Test + fun validExamplePath_loadsAndValidatesExamples() { + val dir = + File(System.getProperty("java.io.tmpdir"), "a2ui_examples_${System.currentTimeMillis()}") + dir.mkdirs() + try { + val ex1 = File(dir, "ex1.json").apply { writeText("""{"example": 1}""") } + val ex2 = File(dir, "ex2.txt").apply { writeText("ignore me") } + val ex3 = File(dir, "ex3.json").apply { writeText("invalid_json_here") } + + val catalog = createDummyCatalog() + // We pass false to validate because our dummy validator will fail everything + val examplesContent = catalog.loadExamples(dir.absolutePath, validate = false) + + assertTrue(examplesContent.contains("---BEGIN ex1---")) + assertTrue(examplesContent.contains("{\"example\": 1}")) + // invalid json loads anyway if validation is false + assertTrue(examplesContent.contains("---BEGIN ex3---")) + // txt file ignored + assertTrue(!examplesContent.contains("ignore me")) + } finally { + dir.deleteRecursively() + } + } +} diff --git a/agent_sdks/kotlin/src/test/kotlin/com/google/a2ui/core/schema/SchemaModifiersTest.kt b/agent_sdks/kotlin/src/test/kotlin/com/google/a2ui/core/schema/SchemaModifiersTest.kt new file mode 100644 index 000000000..f4f3645a6 --- /dev/null +++ b/agent_sdks/kotlin/src/test/kotlin/com/google/a2ui/core/schema/SchemaModifiersTest.kt @@ -0,0 +1,69 @@ +/* + * 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 + * + * https://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.google.a2ui.core.schema + +import kotlin.test.Test +import kotlin.test.assertNull +import kotlin.test.assertTrue +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive + +class SchemaModifiersTest { + @Test + fun removeStrictValidation_removesAdditionalPropertiesFalse() { + val schema = + Json.parseToJsonElement( + """ + { + "type": "object", + "properties": { + "a": { "type": "string", "additionalProperties": false } + }, + "additionalProperties": false + } + """ + ) + .jsonObject + + val modified = SchemaModifiers.removeStrictValidation(schema) + + assertNull(modified["additionalProperties"]) + val props = modified["properties"]!!.jsonObject + val aSchema = props["a"]!!.jsonObject + assertNull(aSchema["additionalProperties"]) + } + + @Test + fun removeStrictValidation_keepsAdditionalPropertiesTrue() { + val schema = + Json.parseToJsonElement( + """ + { + "type": "object", + "additionalProperties": true + } + """ + ) + .jsonObject + + val modified = SchemaModifiers.removeStrictValidation(schema) + + assertTrue(modified["additionalProperties"]!!.jsonPrimitive.booleanOrNull!!) + } +} diff --git a/agent_sdks/kotlin/src/test/kotlin/com/google/a2ui/core/schema/ValidatorTest.kt b/agent_sdks/kotlin/src/test/kotlin/com/google/a2ui/core/schema/ValidatorTest.kt new file mode 100644 index 000000000..bac53684c --- /dev/null +++ b/agent_sdks/kotlin/src/test/kotlin/com/google/a2ui/core/schema/ValidatorTest.kt @@ -0,0 +1,219 @@ +/* + * 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 + * + * https://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.google.a2ui.core.schema + +import kotlin.test.Test +import kotlin.test.assertFailsWith +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject + +class ValidatorTest { + + private fun createDummyCatalog(version: A2uiVersion): A2uiCatalog { + val serverToClientSchema = + Json.parseToJsonElement( + if (version == A2uiVersion.VERSION_0_8) { + """{"type": "object", "properties": {"surfaceUpdate": {"type": "object", "properties": {"root": {"type": "object"}}}}}""" + } else { + """{"type": "object", "properties": { + "beginRendering": {"type": "object", "properties": {"root": {"type": "object"}}}, + "updateDataModel": {"type": "object", "additionalProperties": true} + }}""" + } + ) as JsonObject + + val catalogSchema = + Json.parseToJsonElement( + """ + { + "catalogId": "dummy_catalog", + "components": { + "TestComp": {"type": "object", "properties": {"child": {"type": "object"}, "children": {"type": "array"}}} + }, + "${"$"}defs": { + } + } + """ + .trimIndent() + ) as JsonObject + + return A2uiCatalog( + version = version, + name = "dummy", + serverToClientSchema = serverToClientSchema as JsonObject, + commonTypesSchema = Json.parseToJsonElement("{}") as JsonObject, + catalogSchema = catalogSchema, + ) + } + + @Test + fun validate_standardPayload09_validationSucceeds() { + val catalog = createDummyCatalog(A2uiVersion.VERSION_0_9) + val validator = A2uiValidator(catalog) + + val payload = + Json.parseToJsonElement( + """ + { + "beginRendering": { + "root": { + "component": { "componentId": "TestComp" } + } + } + } + """ + .trimIndent() + ) + + // Should not throw + validator.validate(payload) + } + + @Test + fun validate_standardPayload08_validationSucceeds() { + val catalog = createDummyCatalog(A2uiVersion.VERSION_0_8) + val validator = A2uiValidator(catalog) + + val payload = + Json.parseToJsonElement( + """ + { + "surfaceUpdate": { + "root": { + "component": { "componentId": "TestComp" } + } + } + } + """ + .trimIndent() + ) + + // Should not throw + validator.validate(payload) + } + + @Test + fun validate_missingRoot_throwsException() { + val catalog = createDummyCatalog(A2uiVersion.VERSION_0_9) + val validator = A2uiValidator(catalog) + + val payload = + Json.parseToJsonElement( + """ + { + "beginRendering": { + "notRoot": {} + } + } + """ + .trimIndent() + ) + + // Validation against S2C schema will fail because root is missing but may just be ignored if + // not required in my dummy schema. + // Let's actually test A2UI topology logic by putting an invalid component ref for + // `updateComponents`. + + val payloadUpdate = + Json.parseToJsonElement( + """ + { + "updateComponents": { + "components": [ + { + "component": "UnknownComp", + "id": "123" + } + ] + } + } + """ + .trimIndent() + ) + + assertFailsWith { validator.validate(payloadUpdate) } + } + + @Test + fun validate_circularReferences_throwsException() { + val catalog = createDummyCatalog(A2uiVersion.VERSION_0_9) + val validator = A2uiValidator(catalog) + + val payload = + Json.parseToJsonElement( + """ + { + "updateComponents": { + "components": [ + { + "component": "TestComp", + "id": "root", + "children": [ "B" ] + }, + { + "component": "TestComp", + "id": "B", + "children": [ "root" ] + } + ] + } + } + """ + .trimIndent() + ) + + try { + validator.validate(payload) + throw AssertionError("Expected Circular reference exception, got nothing.") + } catch (e: IllegalArgumentException) { + kotlin.test.assertTrue( + e.message!!.contains("Circular reference detected") || + e.message!!.contains("Max function call"), + "Unexpected exception: ${e.message}", + ) + } + } + + @Test + fun validate_globalRecursionLimits_throwsException() { + val catalog = createDummyCatalog(A2uiVersion.VERSION_0_9) + val validator = A2uiValidator(catalog) + + // build deeply nested payload > 50 + var jsonStr = """{"level": 0}""" + for (i in 1..60) { + jsonStr = """{"next": $jsonStr}""" + } + jsonStr = """{"updateDataModel": {"value": $jsonStr}}""" + + val payload = Json.parseToJsonElement(jsonStr) + + // Because the JSON validator might fail before recursion checks, we catch the exception. + // As long as it throws something, we are okay, but ideally we make a permissive schema. + try { + validator.validate(payload) + throw AssertionError("Expected limit exception, got nothing.") + } catch (e: Exception) { + kotlin.test.assertTrue( + e.message!!.contains("Global recursion limit exceeded") || + e.message!!.contains("Validation failed") || + e.message!!.contains("not a valid"), + "Unexpected exception: ${e.message}", + ) + } + } +} diff --git a/samples/agent/adk/orchestrator/README.md b/samples/agent/adk/orchestrator/README.md index b0d79efc2..3f5b1e261 100644 --- a/samples/agent/adk/orchestrator/README.md +++ b/samples/agent/adk/orchestrator/README.md @@ -38,7 +38,7 @@ Subagents are configured using RemoteA2aAgent which translates ADK events to A2A ``` ```bash - cd samples/agent/adk/rizzcharts + cd samples/agent/adk/rizzcharts/python uv run . --port=10005 ``` diff --git a/samples/agent/adk/pyproject.toml b/samples/agent/adk/pyproject.toml index 184fea9c5..ab29e4f9d 100644 --- a/samples/agent/adk/pyproject.toml +++ b/samples/agent/adk/pyproject.toml @@ -17,7 +17,7 @@ url = "https://pypi.org/simple" default = true [tool.uv.workspace] -members = ["contact_lookup", "contact_multiple_surfaces", "orchestrator", "restaurant_finder", "rizzcharts", "mcp_app_proxy"] +members = ["contact_lookup", "contact_multiple_surfaces", "orchestrator", "restaurant_finder", "rizzcharts/python", "mcp_app_proxy"] [tool.uv.sources] a2ui-agent = { path = "../../../agent_sdks/python", editable = true } @@ -40,4 +40,5 @@ pyink-annotation-pragmas = [ [dependency-groups] dev = [ "pyink>=24.10.0", + "pytest>=9.0.2", ] diff --git a/samples/agent/adk/rizzcharts/kotlin/.gitignore b/samples/agent/adk/rizzcharts/kotlin/.gitignore new file mode 100644 index 000000000..b26b35b0e --- /dev/null +++ b/samples/agent/adk/rizzcharts/kotlin/.gitignore @@ -0,0 +1,128 @@ +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Gradle template +.gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Avoid ignore Gradle wrappper properties +!gradle-wrapper.properties + +# Cache of project +.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +### Kotlin template +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + diff --git a/samples/agent/adk/rizzcharts/kotlin/build.gradle.kts b/samples/agent/adk/rizzcharts/kotlin/build.gradle.kts new file mode 100644 index 000000000..989c7b433 --- /dev/null +++ b/samples/agent/adk/rizzcharts/kotlin/build.gradle.kts @@ -0,0 +1,65 @@ +/* + * 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 + * + * https://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. + */ + +plugins { + kotlin("jvm") version "2.3.0" + application + id("com.ncorti.ktfmt.gradle") version "0.19.0" +} + +ktfmt { + googleStyle() +} + +group = "com.google.a2ui.samples" +version = "0.9.0-SNAPSHOT" + +// Configure Java capability +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +repositories { + mavenCentral() + google() +} + +dependencies { + implementation("com.google.a2ui:a2ui-agent") + + // Google ADK + implementation("com.google.adk:google-adk:0.9.0") + + // Default model integration + implementation("com.google.genai:google-genai:1.43.0") + + // Ktor Server + implementation("io.ktor:ktor-server-core:3.4.1") + implementation("io.ktor:ktor-server-netty:3.4.1") + implementation("io.ktor:ktor-server-cors:3.4.1") + implementation("io.ktor:ktor-server-content-negotiation:3.4.1") + implementation("io.ktor:ktor-serialization-jackson:3.4.1") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.17.2") + + // Dotenv + implementation("io.github.cdimascio:dotenv-java:3.0.0") +} + +application { + mainClass.set("com.google.a2ui.samples.rizzcharts.RizzchartsMainKt") +} diff --git a/samples/agent/adk/rizzcharts/kotlin/compile_output.txt b/samples/agent/adk/rizzcharts/kotlin/compile_output.txt new file mode 100644 index 000000000..a7ee18d04 --- /dev/null +++ b/samples/agent/adk/rizzcharts/kotlin/compile_output.txt @@ -0,0 +1,189 @@ +> Task :kotlin:checkKotlinGradlePluginConfigurationErrors SKIPPED +> Task :checkKotlinGradlePluginConfigurationErrors +> Task :kotlin:compileKotlin UP-TO-DATE +> Task :kotlin:compileJava NO-SOURCE + +> Task :compileKotlin FAILED +e: Incompatible classes were found in dependencies. Remove them from the classpath or use '-Xskip-metadata-version-check' to suppress errors +e: file:///usr/local/google/home/jgindin/.gradle/caches/modules-2/files-2.1/com.squareup.okhttp3/okhttp-jvm/5.3.2/f31e8de27feebe1e56d6c9d354a0986b65be0e1d/okhttp-jvm-5.3.2.jar!/META-INF/okhttp.kotlin_moduleModule was compiled with an incompatible version of Kotlin. The binary version of its metadata is 2.2.0, expected version is 1.9.0. +e: file:///usr/local/google/home/jgindin/.gradle/caches/modules-2/files-2.1/com.squareup.okio/okio-jvm/3.16.4/ceb794cf0bbf8d0d20f49aa91ce20db7fd77675d/okio-jvm-3.16.4.jar!/META-INF/okio.kotlin_moduleModule was compiled with an incompatible version of Kotlin. The binary version of its metadata is 2.2.0, expected version is 1.9.0. +e: file:///usr/local/google/home/jgindin/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/2.2.21/fa374a986e128314c3db00a20aae55f72a258511/kotlin-stdlib-2.2.21.jar!/META-INF/kotlin-stdlib-jdk7.kotlin_moduleModule was compiled with an incompatible version of Kotlin. The binary version of its metadata is 2.2.0, expected version is 1.9.0. +e: file:///usr/local/google/home/jgindin/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/2.2.21/fa374a986e128314c3db00a20aae55f72a258511/kotlin-stdlib-2.2.21.jar!/META-INF/kotlin-stdlib-jdk8.kotlin_moduleModule was compiled with an incompatible version of Kotlin. The binary version of its metadata is 2.2.0, expected version is 1.9.0. +e: file:///usr/local/google/home/jgindin/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/2.2.21/fa374a986e128314c3db00a20aae55f72a258511/kotlin-stdlib-2.2.21.jar!/META-INF/kotlin-stdlib.kotlin_moduleModule was compiled with an incompatible version of Kotlin. The binary version of its metadata is 2.2.0, expected version is 1.9.0. +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/agent_sdks/kotlin/build/classes/kotlin/main/META-INF/a2ui-agent.kotlin_moduleModule was compiled with an incompatible version of Kotlin. The binary version of its metadata is 2.1.0, expected version is 1.9.0. +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgent.kt:3:36 Class 'com.google.a2ui.core.schema.A2uiSchemaManager' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.1.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/src/aipg/A2UI/agent_sdks/kotlin/build/classes/kotlin/main/com/google/a2ui/core/schema/A2uiSchemaManager.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgent.kt:8:36 Class 'com.google.a2ui.core.schema.A2uiCatalog' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.1.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/src/aipg/A2UI/agent_sdks/kotlin/build/classes/kotlin/main/com/google/a2ui/core/schema/A2uiCatalog.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgent.kt:9:42 Class 'com.google.a2ui.adk.a2a_extension.SendA2uiToClientToolset' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.1.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/src/aipg/A2UI/agent_sdks/kotlin/build/classes/kotlin/main/com/google/a2ui/adk/a2a_extension/SendA2uiToClientToolset.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgent.kt:14:20 Class 'com.google.a2ui.core.schema.A2uiSchemaManager' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.1.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/src/aipg/A2UI/agent_sdks/kotlin/build/classes/kotlin/main/com/google/a2ui/core/schema/A2uiSchemaManager.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgent.kt:16:42 Class 'com.google.a2ui.core.schema.A2uiCatalog' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.1.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/src/aipg/A2UI/agent_sdks/kotlin/build/classes/kotlin/main/com/google/a2ui/core/schema/A2uiCatalog.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgent.kt:23:41 Class 'com.google.a2ui.core.schema.A2uiSchemaManager' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.1.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/src/aipg/A2UI/agent_sdks/kotlin/build/classes/kotlin/main/com/google/a2ui/core/schema/A2uiSchemaManager.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgent.kt:24:13 Cannot find a parameter with this name: toolName +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgent.kt:25:13 Cannot find a parameter with this name: toolDescription +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgent.kt:27:13 Cannot find a parameter with this name: serverToClientSchema +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgent.kt:28:13 Cannot find a parameter with this name: schemaModifiers +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgent.kt:29:13 Cannot find a parameter with this name: useFunctionCallingForTools +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgent.kt:30:13 Cannot find a parameter with this name: disableInlineCatalogs +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgent.kt:31:13 Cannot find a parameter with this name: suppressBuiltInA2uiInstructions +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgent.kt:31:13 No value passed for parameter 'roleDescription' +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgent.kt:33:16 Unresolved reference: listOf +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgent.kt:34:13 Class 'com.google.a2ui.adk.a2a_extension.SendA2uiToClientToolset' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.1.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/src/aipg/A2UI/agent_sdks/kotlin/build/classes/kotlin/main/com/google/a2ui/adk/a2a_extension/SendA2uiToClientToolset.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgent.kt:41:37 No type arguments expected for class Builder +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgentExecutor.kt:3:36 Class 'com.google.a2ui.core.schema.A2uiSchemaManager' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.1.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/src/aipg/A2UI/agent_sdks/kotlin/build/classes/kotlin/main/com/google/a2ui/core/schema/A2uiSchemaManager.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgentExecutor.kt:11:32 Class 'com.google.a2ui.core.schema.A2uiSchemaManager' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.1.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/src/aipg/A2UI/agent_sdks/kotlin/build/classes/kotlin/main/com/google/a2ui/core/schema/A2uiSchemaManager.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgentExecutor.kt:27:21 Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Session? +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgentExecutor.kt:28:20 Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Session? +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgentExecutor.kt:28:28 Unresolved reference: session.state()["base_url"] +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgentExecutor.kt:28:28 No set method providing array access +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgentExecutor.kt:31:21 Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Session? +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgentExecutor.kt:32:20 Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Session? +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgentExecutor.kt:32:28 Unresolved reference: session.state()[A2UI_ENABLED_KEY] +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgentExecutor.kt:32:28 No set method providing array access +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgentExecutor.kt:35:21 Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Session? +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgentExecutor.kt:36:13 Class 'kotlin.Unit' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.2.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/2.2.21/fa374a986e128314c3db00a20aae55f72a258511/kotlin-stdlib-2.2.21.jar!/kotlin/Unit.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgentExecutor.kt:37:24 Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Session? +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgentExecutor.kt:37:32 Unresolved reference: session.state()[A2UI_CATALOG_KEY] +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgentExecutor.kt:37:32 No set method providing array access +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgentExecutor.kt:37:53 Class 'com.google.a2ui.core.schema.A2uiSchemaManager' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.1.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/src/aipg/A2UI/agent_sdks/kotlin/build/classes/kotlin/main/com/google/a2ui/core/schema/A2uiSchemaManager.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgentExecutor.kt:37:67 Unresolved reference: selectedCatalog +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgentExecutor.kt:39:24 Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Session? +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgentExecutor.kt:39:32 Unresolved reference: session.state()[A2UI_CATALOG_KEY] +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgentExecutor.kt:39:32 No set method providing array access +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgentExecutor.kt:43:21 Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Session? +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgentExecutor.kt:44:13 Class 'kotlin.Unit' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.2.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/2.2.21/fa374a986e128314c3db00a20aae55f72a258511/kotlin-stdlib-2.2.21.jar!/kotlin/Unit.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgentExecutor.kt:45:24 Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Session? +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgentExecutor.kt:45:32 Unresolved reference: session.state()[A2UI_EXAMPLES_KEY] +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgentExecutor.kt:45:32 No set method providing array access +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgentExecutor.kt:45:54 Class 'com.google.a2ui.core.schema.A2uiSchemaManager' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.1.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/src/aipg/A2UI/agent_sdks/kotlin/build/classes/kotlin/main/com/google/a2ui/core/schema/A2uiSchemaManager.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgentExecutor.kt:45:68 Class 'com.google.a2ui.core.schema.A2uiCatalog' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.1.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/src/aipg/A2UI/agent_sdks/kotlin/build/classes/kotlin/main/com/google/a2ui/core/schema/A2uiCatalog.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgentExecutor.kt:45:81 Class 'com.google.a2ui.core.schema.A2uiSchemaManager' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.1.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/src/aipg/A2UI/agent_sdks/kotlin/build/classes/kotlin/main/com/google/a2ui/core/schema/A2uiSchemaManager.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgentExecutor.kt:45:95 Unresolved reference: selectedCatalog +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgentExecutor.kt:47:24 Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Session? +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgentExecutor.kt:47:32 Unresolved reference: session.state()[A2UI_EXAMPLES_KEY] +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgentExecutor.kt:47:32 No set method providing array access +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgentExecutor.kt:51:16 Type mismatch: inferred type is Session? but Session was expected +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:3:38 Class 'com.google.a2ui.basic_catalog.BasicCatalog' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.1.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/src/aipg/A2UI/agent_sdks/kotlin/build/classes/kotlin/main/com/google/a2ui/basic_catalog/BasicCatalog.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:4:36 Class 'com.google.a2ui.core.schema.A2uiSchemaManager' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.1.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/src/aipg/A2UI/agent_sdks/kotlin/build/classes/kotlin/main/com/google/a2ui/core/schema/A2uiSchemaManager.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:5:36 Class 'com.google.a2ui.core.schema.CatalogConfig' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.1.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/src/aipg/A2UI/agent_sdks/kotlin/build/classes/kotlin/main/com/google/a2ui/core/schema/CatalogConfig.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:6:36 Class 'com.google.a2ui.core.schema.Constants' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.1.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/src/aipg/A2UI/agent_sdks/kotlin/build/classes/kotlin/main/com/google/a2ui/core/schema/Constants.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:19:22 Unresolved reference: exitProcess +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:21:2 Class 'kotlin.reflect.KClass' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.2.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/2.2.21/fa374a986e128314c3db00a20aae55f72a258511/kotlin-stdlib-2.2.21.jar!/kotlin/reflect/KClass.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:40:9 Unresolved reference: println +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:42:24 Unresolved reference: mutableMapOf +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:46:9 Class 'kotlin.Unit' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.2.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/2.2.21/fa374a986e128314c3db00a20aae55f72a258511/kotlin-stdlib-2.2.21.jar!/kotlin/Unit.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:48:13 Class 'kotlin.Unit' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.2.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/2.2.21/fa374a986e128314c3db00a20aae55f72a258511/kotlin-stdlib-2.2.21.jar!/kotlin/Unit.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:49:38 Unresolved reference: mapOf +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:49:51 Unresolved reference: to +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:50:20 Class 'kotlin.Unit' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.2.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/2.2.21/fa374a986e128314c3db00a20aae55f72a258511/kotlin-stdlib-2.2.21.jar!/kotlin/Unit.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:51:38 Unresolved reference: mapOf +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:51:54 Unresolved reference: to +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:53:37 Unresolved reference: mapOf +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:53:50 Unresolved reference: to +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:53:71 Unresolved reference: to +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:56:15 Class 'kotlin.Unit' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.2.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/2.2.21/fa374a986e128314c3db00a20aae55f72a258511/kotlin-stdlib-2.2.21.jar!/kotlin/Unit.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:57:33 Unresolved reference: mapOf +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:57:46 Unresolved reference: to +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:57:67 Unresolved reference: to +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:64:37 Class 'com.google.a2ui.core.schema.A2uiSchemaManager' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.1.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/src/aipg/A2UI/agent_sdks/kotlin/build/classes/kotlin/main/com/google/a2ui/core/schema/A2uiSchemaManager.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:69:10 Class 'kotlin.jvm.JvmStatic' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.2.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/2.2.21/fa374a986e128314c3db00a20aae55f72a258511/kotlin-stdlib-2.2.21.jar!/kotlin/jvm/JvmStatic.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:74:49 Unresolved reference: isNullOrEmpty +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:75:28 Class 'kotlin.Unit' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.2.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/2.2.21/fa374a986e128314c3db00a20aae55f72a258511/kotlin-stdlib-2.2.21.jar!/kotlin/Unit.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:76:17 Unresolved reference: exitProcess +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:79:63 Unresolved reference: takeUnless +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:79:76 Unresolved reference: it +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:85:13 Unresolved reference: println +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:87:13 Class 'com.google.a2ui.core.schema.A2uiSchemaManager' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.1.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/src/aipg/A2UI/agent_sdks/kotlin/build/classes/kotlin/main/com/google/a2ui/core/schema/A2uiSchemaManager.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:87:13 Class 'kotlin.Unit' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.2.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/2.2.21/fa374a986e128314c3db00a20aae55f72a258511/kotlin-stdlib-2.2.21.jar!/kotlin/Unit.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:87:29 Class 'com.google.a2ui.core.schema.A2uiSchemaManager' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.1.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/src/aipg/A2UI/agent_sdks/kotlin/build/classes/kotlin/main/com/google/a2ui/core/schema/A2uiSchemaManager.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:88:17 Class 'com.google.a2ui.core.schema.Constants' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.1.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/src/aipg/A2UI/agent_sdks/kotlin/build/classes/kotlin/main/com/google/a2ui/core/schema/Constants.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:89:17 Unresolved reference: listOf +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:90:21 Class 'com.google.a2ui.core.schema.CatalogConfig.Companion' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.1.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/src/aipg/A2UI/agent_sdks/kotlin/build/classes/kotlin/main/com/google/a2ui/core/schema/CatalogConfig$Companion.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:90:21 Class 'com.google.a2ui.core.schema.CatalogConfig' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.1.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/src/aipg/A2UI/agent_sdks/kotlin/build/classes/kotlin/main/com/google/a2ui/core/schema/CatalogConfig.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:90:35 Class 'com.google.a2ui.core.schema.CatalogConfig' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.1.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/src/aipg/A2UI/agent_sdks/kotlin/build/classes/kotlin/main/com/google/a2ui/core/schema/CatalogConfig.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:95:21 Class 'com.google.a2ui.basic_catalog.BasicCatalog' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.1.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/src/aipg/A2UI/agent_sdks/kotlin/build/classes/kotlin/main/com/google/a2ui/basic_catalog/BasicCatalog.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:95:34 Class 'com.google.a2ui.core.schema.CatalogConfig' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.1.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/src/aipg/A2UI/agent_sdks/kotlin/build/classes/kotlin/main/com/google/a2ui/core/schema/CatalogConfig.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:96:25 Class 'com.google.a2ui.core.schema.Constants' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.1.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/src/aipg/A2UI/agent_sdks/kotlin/build/classes/kotlin/main/com/google/a2ui/core/schema/Constants.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:101:17 Unresolved reference: emptyList +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:104:13 Class 'kotlin.Unit' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.2.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/2.2.21/fa374a986e128314c3db00a20aae55f72a258511/kotlin-stdlib-2.2.21.jar!/kotlin/Unit.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:104:21 Class 'com.google.a2ui.core.schema.A2uiSchemaManager' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.1.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/src/aipg/A2UI/agent_sdks/kotlin/build/classes/kotlin/main/com/google/a2ui/core/schema/A2uiSchemaManager.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:107:17 Class 'com.google.a2ui.core.schema.A2uiSchemaManager' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.1.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/src/aipg/A2UI/agent_sdks/kotlin/build/classes/kotlin/main/com/google/a2ui/core/schema/A2uiSchemaManager.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:108:30 Unresolved reference: session +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:109:30 Unresolved reference: session +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:109:121 Class 'com.google.a2ui.core.schema.A2uiCatalog' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.1.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/src/aipg/A2UI/agent_sdks/kotlin/build/classes/kotlin/main/com/google/a2ui/core/schema/A2uiCatalog.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:110:30 Unresolved reference: session +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:113:13 Class 'kotlin.Unit' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.2.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/2.2.21/fa374a986e128314c3db00a20aae55f72a258511/kotlin-stdlib-2.2.21.jar!/kotlin/Unit.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:115:57 Unresolved reference: java +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt:116:13 Unresolved reference: println +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsTools.kt:6:20 Unresolved reference: mapOf +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsTools.kt:7:13 Class 'kotlin.Pair' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.2.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/2.2.21/fa374a986e128314c3db00a20aae55f72a258511/kotlin-stdlib-2.2.21.jar!/kotlin/Pair.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsTools.kt:8:13 Class 'kotlin.Pair' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.2.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/2.2.21/fa374a986e128314c3db00a20aae55f72a258511/kotlin-stdlib-2.2.21.jar!/kotlin/Pair.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsTools.kt:9:13 Class 'kotlin.Pair' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.2.0, but the compiler version 1.9.0 can read versions up to 2.0.0. +The class is loaded from /usr/local/google/home/jgindin/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/2.2.21/fa374a986e128314c3db00a20aae55f72a258511/kotlin-stdlib-2.2.21.jar!/kotlin/Pair.class +e: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsTools.kt:16:59 Unresolved reference: isEmpty + +[Incubating] Problems report is available at: file:///usr/local/google/home/jgindin/src/aipg/A2UI/samples/agent/adk/rizzcharts-kotlin/build/reports/problems/problems-report.html + +FAILURE: Build failed with an exception. + +* What went wrong: +Execution failed for task ':compileKotlin'. +> A failure occurred while executing org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction + > Compilation error. See log for more details + +* Try: +> Run with --stacktrace option to get the stack trace. +> Run with --info or --debug option to get more log output. +> Run with --scan to get full insights from a Build Scan (powered by Develocity). +> Get more help at https://help.gradle.org. + +Deprecated Gradle features were used in this build, making it incompatible with Gradle 10. + +You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins. + +For more on this, please refer to https://docs.gradle.org/9.4.0/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation. + +BUILD FAILED in 3s +3 actionable tasks: 2 executed, 1 up-to-date diff --git a/samples/agent/adk/rizzcharts/kotlin/gradle/wrapper/gradle-wrapper.properties b/samples/agent/adk/rizzcharts/kotlin/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..dbc3ce4a0 --- /dev/null +++ b/samples/agent/adk/rizzcharts/kotlin/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/samples/agent/adk/rizzcharts/kotlin/gradlew b/samples/agent/adk/rizzcharts/kotlin/gradlew new file mode 100755 index 000000000..adff685a0 --- /dev/null +++ b/samples/agent/adk/rizzcharts/kotlin/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# 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 +# +# https://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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/samples/agent/adk/rizzcharts/kotlin/gradlew.bat b/samples/agent/adk/rizzcharts/kotlin/gradlew.bat new file mode 100644 index 000000000..c4bdd3ab8 --- /dev/null +++ b/samples/agent/adk/rizzcharts/kotlin/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/samples/agent/adk/rizzcharts/kotlin/kotlin rizzcharts.iml b/samples/agent/adk/rizzcharts/kotlin/kotlin rizzcharts.iml new file mode 100644 index 000000000..8021953ed --- /dev/null +++ b/samples/agent/adk/rizzcharts/kotlin/kotlin rizzcharts.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/samples/agent/adk/rizzcharts/kotlin/settings.gradle.kts b/samples/agent/adk/rizzcharts/kotlin/settings.gradle.kts new file mode 100644 index 000000000..2e2445535 --- /dev/null +++ b/samples/agent/adk/rizzcharts/kotlin/settings.gradle.kts @@ -0,0 +1,25 @@ +/* + * 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 + * + * https://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. + */ + +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + } +} + +rootProject.name = "rizzcharts-kotlin" +includeBuild("../../../../../agent_sdks/kotlin") diff --git a/samples/agent/adk/rizzcharts/kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgent.kt b/samples/agent/adk/rizzcharts/kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgent.kt new file mode 100644 index 000000000..c125fecfd --- /dev/null +++ b/samples/agent/adk/rizzcharts/kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgent.kt @@ -0,0 +1,108 @@ +/* + * 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 + * + * https://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.google.a2ui.samples.rizzcharts + +import com.google.a2ui.adk.a2a_extension.SendA2uiToClientToolset +import com.google.a2ui.core.schema.A2uiCatalog +import com.google.a2ui.core.schema.A2uiSchemaManager +import com.google.adk.agents.Instruction +import com.google.adk.agents.LlmAgent +import com.google.adk.agents.ReadonlyContext +import com.google.adk.models.BaseLlm + +class RizzchartsAgent( + baseUrl: String, + llm: BaseLlm, + schemaManager: A2uiSchemaManager, + uiEnabledChecker: (ReadonlyContext) -> Boolean, + catalogChecker: (ReadonlyContext) -> A2uiCatalog, + examplesChecker: (ReadonlyContext) -> String, +) : + LlmAgent( + builder() + .name("Ecommerce Dashboard Agent") + .description("An agent that lets sales managers request sales data.") + .model(llm) + .disallowTransferToParent(false) + .disallowTransferToPeers(true) + .instruction( + Instruction.Static( + schemaManager.generateSystemPrompt( + ROLE_DESCRIPTION, + WORKFLOW_DESCRIPTION, + UI_DESCRIPTION, + null, + emptyList(), + false, + false, + false, + ) + ) + ) + .tools( + listOf( + RizzchartsTools.GetStoreSalesTool(), + RizzchartsTools.GetSalesDataTool(), + SendA2uiToClientToolset( + a2uiEnabled = uiEnabledChecker, + a2uiCatalog = catalogChecker, + a2uiExamples = examplesChecker, + ), + ) + ) + ) { + + // AgentCard logic not strictly needed since A2AServerApplication is stripped. + fun getAgentCard(): Any? { + return null + } + + companion object { + const val ROLE_DESCRIPTION = + "You are an expert A2UI Ecommerce Dashboard analyst. Your primary function is to translate user requests for ecommerce data into A2UI JSON payloads to display charts and visualizations. You MUST use the `send_a2ui_json_to_client` tool with the `a2ui_json` argument set to the A2UI JSON payload to send to the client.\n" + + const val WORKFLOW_DESCRIPTION = + "Your task is to analyze the user's request, fetch the necessary data, select the correct generic template, and send the corresponding A2UI JSON payload.\n\n" + + "1. **Analyze the Request:** Determine the user's intent (Visual Chart vs. Geospatial Map).\n" + + " * \"show my sales breakdown by product category for q3\" -> **Intent:** Chart.\n" + + " * \"show revenue trends yoy by month\" -> **Intent:** Chart.\n" + + " * \"were there any outlier stores in the northeast region\" -> **Intent:** Map.\n\n" + + "2. **Fetch Data:** Select and use the appropriate tool to retrieve the necessary data.\n" + + " * Use **`get_sales_data`** for general sales, revenue, and product category trends (typically for Charts).\n" + + " * Use **`get_store_sales`** for regional performance, store locations, and geospatial outliers (typically for Maps).\n\n" + + "3. **Select Example:** Based on the intent, choose the correct example block to use as your template.\n" + + " * **Intent** (Chart/Data Viz) -> Use `---BEGIN chart---`.\n" + + " * **Intent** (Map/Geospatial) -> Use `---BEGIN map---`.\n\n" + + "4. **Construct the JSON Payload:**\n" + + " * Use the **entire** JSON array from the chosen example as the base value for the `a2ui_json` argument.\n" + + " * **Generate a new `surfaceId`:** You MUST generate a new, unique `surfaceId` for this request (e.g., `sales_breakdown_q3_surface`, `regional_outliers_northeast_surface`). This new ID must be used for the `surfaceId` in all three messages within the JSON array (`beginRendering`, `surfaceUpdate`, `dataModelUpdate`).\n" + + " * **Update the title Text:** You MUST update the `literalString` value for the `Text` component (the component with `id: \"page_header\"`) to accurately reflect the specific user query. For example, if the user asks for \"Q3\" sales, update the generic template text to \"Q3 2025 Sales by Product Category\".\n" + + " * Ensure the generated JSON perfectly matches the A2UI specification. It will be validated against the json_schema and rejected if it does not conform. \n" + + " * If you get an error in the tool response apologize to the user and let them know they should try again.\n\n" + + "5. **Call the Tool:** Call the `send_a2ui_json_to_client` tool with the fully constructed `a2ui_json` payload.\n" + + const val UI_DESCRIPTION = + "**Core Objective:** To provide a dynamic and interactive dashboard by constructing UI surfaces with the appropriate visualization components based on user queries.\n\n" + + "**Key Components & Examples:**\n\n" + + "You will be provided a schema that defines the A2UI message structure and two key generic component templates for displaying data.\n\n" + + "1. **Charts:** Used for requests about sales breakdowns, revenue performance, comparisons, or trends.\n" + + " * **Template:** Use the JSON from `---BEGIN chart---`.\n" + + "2. **Maps:** Used for requests about regional data, store locations, geography-based performance, or regional outliers.\n" + + " * **Template:** Use the JSON from `---BEGIN map---`.\n\n" + + "You will also use layout components like `Column` (as the `root`) and `Text` (to provide a title).\n" + } +} diff --git a/samples/agent/adk/rizzcharts/kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgentExecutor.kt b/samples/agent/adk/rizzcharts/kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgentExecutor.kt new file mode 100644 index 000000000..0dbc9823e --- /dev/null +++ b/samples/agent/adk/rizzcharts/kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsAgentExecutor.kt @@ -0,0 +1,87 @@ +/* + * 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 + * + * https://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.google.a2ui.samples.rizzcharts + +import com.fasterxml.jackson.databind.ObjectMapper +import com.google.a2ui.a2a.A2uiA2a +import com.google.a2ui.core.schema.A2uiSchemaManager +import com.google.adk.sessions.Session + +class RizzchartsAgentExecutor( + private val baseUrl: String, + private val schemaManager: A2uiSchemaManager, +) { + companion object { + const val A2UI_ENABLED_KEY = "a2ui_enabled" + const val A2UI_CATALOG_KEY = "a2ui_catalog" + const val A2UI_EXAMPLES_KEY = "a2ui_examples" + + private val objectMapper = ObjectMapper() + } + + fun prepareSession(session: Session, rawRequestBody: Map<*, *>) { + @Suppress("UNCHECKED_CAST") + val requestBody = objectMapper.convertValue(rawRequestBody, Map::class.java) as Map + val params = requestBody["params"] as? Map<*, *> + val messageMap = params?.get("message") as? Map<*, *> + val messageMetadata = messageMap?.get("metadata") as? Map<*, *> + val rootMetadata = requestBody["metadata"] as? Map<*, *> + val metadata = messageMetadata ?: rootMetadata + val capabilitiesNode = metadata?.get("a2uiClientCapabilities") + var capabilities = capabilitiesNode as? Map<*, *> + + if (capabilities == null) { + val history = params?.get("history") as? List<*> + if (history != null) { + for (msg in history) { + val msgMap = msg as? Map<*, *> + val msgMeta = msgMap?.get("metadata") as? Map<*, *> + val msgCaps = msgMeta?.get("a2uiClientCapabilities") as? Map<*, *> + if (msgCaps != null) { + capabilities = msgCaps + break + } + } + } + } + + val requestedExtensions = + ((params?.get("extensions") ?: requestBody["extensions"]) as? List<*>)?.filterIsInstance< + String + >() ?: emptyList() + + var useUi = A2uiA2a.tryActivateA2uiExtension(requestedExtensions) { /* no-op */ } + if (!useUi) { + useUi = capabilitiesNode != null || capabilities != null + } + + if (!session.state().containsKey("base_url")) { + session.state()["base_url"] = baseUrl + } + + session.state()[A2UI_ENABLED_KEY] = useUi + + if (useUi) { + session.state()[A2UI_CATALOG_KEY] = schemaManager.getSelectedCatalog() + session.state()[A2UI_EXAMPLES_KEY] = + schemaManager.loadExamples(schemaManager.getSelectedCatalog()) + } else { + session.state().remove(A2UI_CATALOG_KEY) + session.state().remove(A2UI_EXAMPLES_KEY) + } + } +} diff --git a/samples/agent/adk/rizzcharts/kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt b/samples/agent/adk/rizzcharts/kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt new file mode 100644 index 000000000..0577e4cea --- /dev/null +++ b/samples/agent/adk/rizzcharts/kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsMain.kt @@ -0,0 +1,143 @@ +/* + * 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 + * + * https://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.google.a2ui.samples.rizzcharts + +import com.google.a2ui.a2a.A2aHandler +import com.google.a2ui.basic_catalog.BasicCatalog +import com.google.a2ui.core.schema.A2uiSchemaManager +import com.google.a2ui.core.schema.A2uiVersion +import com.google.a2ui.core.schema.CatalogConfig +import com.google.adk.agents.ReadonlyContext +import com.google.adk.models.Gemini +import com.google.adk.runner.InMemoryRunner +import com.google.adk.runner.Runner +import io.ktor.http.HttpMethod +import io.ktor.serialization.jackson.jackson +import io.ktor.server.application.install +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty +import io.ktor.server.plugins.contentnegotiation.ContentNegotiation +import io.ktor.server.plugins.cors.routing.CORS +import io.ktor.server.request.receive +import io.ktor.server.response.respond +import io.ktor.server.routing.get +import io.ktor.server.routing.post +import io.ktor.server.routing.routing +import kotlin.system.exitProcess + +lateinit var schemaManager: A2uiSchemaManager +lateinit var agent: RizzchartsAgent +lateinit var runner: Runner +lateinit var agentExecutor: RizzchartsAgentExecutor +lateinit var a2aHandler: A2aHandler + +fun main(args: Array) { + val dotenv = io.github.cdimascio.dotenv.Dotenv.configure().ignoreIfMissing().load() + val apiKey = dotenv["GEMINI_API_KEY"] ?: System.getenv("GEMINI_API_KEY") + val useVertexAi = + dotenv["GOOGLE_GENAI_USE_VERTEXAI"] ?: System.getenv("GOOGLE_GENAI_USE_VERTEXAI") + + if (useVertexAi != "TRUE" && apiKey.isNullOrEmpty()) { + System.err.println( + "Error: GEMINI_API_KEY environment variable not set and GOOGLE_GENAI_USE_VERTEXAI is not TRUE." + ) + exitProcess(1) + } + + val envModel = dotenv["LITELLM_MODEL"] ?: System.getenv("LITELLM_MODEL") + val liteLlmModel = envModel?.takeUnless { it.isEmpty() } ?: "gemini-2.5-flash" + + val host = "localhost" + val port = 10002 + val baseUrl = "http://$host:$port" + + println("Starting Kotlin Rizzcharts Server (Ktor)...") + + schemaManager = + A2uiSchemaManager( + A2uiVersion.VERSION_0_8, + listOf( + CatalogConfig.fromPath( + "rizzcharts", + "../catalog_schemas/0.8/rizzcharts_catalog_definition.json", + "../examples/rizzcharts_catalog/0.8", + ), + BasicCatalog.getConfig(A2uiVersion.VERSION_0_8, "../examples/standard_catalog/0.8"), + ), + true, + java.util.Collections.emptyList(), + ) + + agent = + RizzchartsAgent( + baseUrl, + Gemini(liteLlmModel, apiKey), + schemaManager, + { ctx: ReadonlyContext -> + val isEnabled = + ctx.state().getOrDefault(RizzchartsAgentExecutor.A2UI_ENABLED_KEY, false) as Boolean + println("A2UI: Evaluating a2uiEnabled. isEnabled: $isEnabled, ctx.state(): " + ctx.state()) + isEnabled + }, + { ctx: ReadonlyContext -> + ctx.state()[RizzchartsAgentExecutor.A2UI_CATALOG_KEY] + as com.google.a2ui.core.schema.A2uiCatalog + }, + { ctx: ReadonlyContext -> ctx.state()[RizzchartsAgentExecutor.A2UI_EXAMPLES_KEY] as String }, + ) + + runner = InMemoryRunner(agent) + agentExecutor = RizzchartsAgentExecutor(baseUrl, schemaManager) + a2aHandler = A2aHandler(runner) + + embeddedServer(Netty, port = port, host = host) { + install(CORS) { + anyHost() + allowMethod(HttpMethod.Options) + allowMethod(HttpMethod.Get) + allowMethod(HttpMethod.Post) + allowMethod(HttpMethod.Put) + allowMethod(HttpMethod.Delete) + allowMethod(HttpMethod.Patch) + allowHeader("Content-Type") + allowHeader("Authorization") + allowCredentials = true + } + install(ContentNegotiation) { jackson() } + routing { + get("/.well-known/agent-card.json") { + val response = + a2aHandler.handleAgentCardGet( + runner.appName(), + "http://localhost:10002", + listOf( + "https://a2ui.org/specification/v0_8/standard_catalog_definition.json", + "https://github.com/google/A2UI/blob/main/samples/agent/adk/rizzcharts/rizzcharts_catalog_definition.json", + ), + ) + call.respond(response) + } + + post("/") { + val requestBody = call.receive>() + val response = a2aHandler.handleA2aPost(requestBody, agentExecutor::prepareSession) + call.respond(response) + } + } + } + .start(wait = true) +} diff --git a/samples/agent/adk/rizzcharts/kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsTools.kt b/samples/agent/adk/rizzcharts/kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsTools.kt new file mode 100644 index 000000000..76b93f0b9 --- /dev/null +++ b/samples/agent/adk/rizzcharts/kotlin/src/main/kotlin/com/google/a2ui/samples/rizzcharts/RizzchartsTools.kt @@ -0,0 +1,166 @@ +/* + * 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 + * + * https://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.google.a2ui.samples.rizzcharts + +import com.google.adk.tools.BaseTool +import com.google.adk.tools.ToolContext +import com.google.genai.types.FunctionDeclaration +import com.google.genai.types.Schema +import com.google.genai.types.Type +import io.reactivex.rxjava3.core.Single +import java.util.Optional + +class RizzchartsTools { + + class GetStoreSalesTool : + BaseTool( + "get_store_sales", + "Gets individual store sales. Args: region: the region to filter by.", + ) { + + override fun declaration(): Optional { + return Optional.of( + FunctionDeclaration.builder() + .name(name()) + .description(description()) + .parameters( + Schema.builder() + .type(Type(Type.Known.OBJECT)) + .properties( + mapOf( + "region" to + Schema.builder() + .type(Type(Type.Known.STRING)) + .description("the region to filter by") + .build() + ) + ) + .build() + ) + .build() + ) + } + + override fun runAsync( + args: Map, + toolContext: ToolContext, + ): Single> { + return Single.fromCallable { + mapOf( + "center" to mapOf("lat" to 34.0, "lng" to -118.2437), + "zoom" to 10, + "locations" to + listOf( + mapOf( + "lat" to 34.0195, + "lng" to -118.4912, + "name" to "Santa Monica Branch", + "description" to "High traffic coastal location.", + "outlier_reason" to "Yes, 15% sales over baseline", + "background" to "#4285F4", + "borderColor" to "#FFFFFF", + "glyphColor" to "#FFFFFF", + ), + mapOf("lat" to 34.0488, "lng" to -118.2518, "name" to "Downtown Flagship"), + mapOf("lat" to 34.1016, "lng" to -118.3287, "name" to "Hollywood Boulevard Store"), + mapOf("lat" to 34.1478, "lng" to -118.1445, "name" to "Pasadena Location"), + mapOf("lat" to 33.7701, "lng" to -118.1937, "name" to "Long Beach Outlet"), + mapOf("lat" to 34.0736, "lng" to -118.4004, "name" to "Beverly Hills Boutique"), + ), + ) + } + } + } + + class GetSalesDataTool : + BaseTool( + "get_sales_data", + "Gets the sales data. Args: time_period: the time period to filter by.", + ) { + + override fun declaration(): Optional { + return Optional.of( + FunctionDeclaration.builder() + .name(name()) + .description(description()) + .parameters( + Schema.builder() + .type(Type(Type.Known.OBJECT)) + .properties( + mapOf( + "time_period" to + Schema.builder() + .type(Type(Type.Known.STRING)) + .description("the time period to filter by") + .build() + ) + ) + .build() + ) + .build() + ) + } + + override fun runAsync( + args: Map, + toolContext: ToolContext, + ): Single> { + return Single.fromCallable { + mapOf( + "sales_data" to + listOf( + mapOf( + "label" to "Apparel", + "value" to 41, + "drillDown" to + listOf( + mapOf("label" to "Tops", "value" to 31), + mapOf("label" to "Bottoms", "value" to 38), + mapOf("label" to "Outerwear", "value" to 20), + mapOf("label" to "Footwear", "value" to 11), + ), + ), + mapOf( + "label" to "Home Goods", + "value" to 15, + "drillDown" to + listOf( + mapOf("label" to "Pillow", "value" to 8), + mapOf("label" to "Coffee Maker", "value" to 16), + mapOf("label" to "Area Rug", "value" to 3), + mapOf("label" to "Bath Towels", "value" to 14), + ), + ), + mapOf( + "label" to "Electronics", + "value" to 28, + "drillDown" to + listOf( + mapOf("label" to "Phones", "value" to 25), + mapOf("label" to "Laptops", "value" to 27), + mapOf("label" to "TVs", "value" to 21), + mapOf("label" to "Other", "value" to 27), + ), + ), + mapOf("label" to "Health & Beauty", "value" to 10), + mapOf("label" to "Other", "value" to 6), + ) + ) + } + } + } +} diff --git a/samples/agent/adk/rizzcharts/kotlin/src/main/resources/application.properties b/samples/agent/adk/rizzcharts/kotlin/src/main/resources/application.properties new file mode 100644 index 000000000..224a2eeaa --- /dev/null +++ b/samples/agent/adk/rizzcharts/kotlin/src/main/resources/application.properties @@ -0,0 +1 @@ +server.port=10002 diff --git a/samples/agent/adk/rizzcharts/.env.example b/samples/agent/adk/rizzcharts/python/.env.example similarity index 100% rename from samples/agent/adk/rizzcharts/.env.example rename to samples/agent/adk/rizzcharts/python/.env.example diff --git a/samples/agent/adk/rizzcharts/README.md b/samples/agent/adk/rizzcharts/python/README.md similarity index 97% rename from samples/agent/adk/rizzcharts/README.md rename to samples/agent/adk/rizzcharts/python/README.md index b158d72a2..bbb490030 100644 --- a/samples/agent/adk/rizzcharts/README.md +++ b/samples/agent/adk/rizzcharts/python/README.md @@ -13,7 +13,7 @@ This sample uses the Agent Development Kit (ADK) along with the A2A protocol to 1. Navigate to the samples directory: ```bash - cd samples/agent/adk/rizzcharts + cd samples/agent/adk/rizzcharts/python ``` 2. Create an environment file with your API key: @@ -37,4 +37,4 @@ All operational data received from an external agent—including its AgentCard, Similarly, any UI definition or data stream received must be treated as untrusted. Malicious agents could attempt to spoof legitimate interfaces to deceive users (phishing), inject malicious scripts via property values (XSS), or generate excessive layout complexity to degrade client performance (DoS). If your application supports optional embedded content (such as iframes or web views), additional care must be taken to prevent exposure to malicious external sites. -Developer Responsibility: Failure to properly validate data and strictly sandbox rendered content can introduce severe vulnerabilities. Developers are responsible for implementing appropriate security measures—such as input sanitization, Content Security Policies (CSP), strict isolation for optional embedded content, and secure credential handling—to protect their systems and users. \ No newline at end of file +Developer Responsibility: Failure to properly validate data and strictly sandbox rendered content can introduce severe vulnerabilities. Developers are responsible for implementing appropriate security measures—such as input sanitization, Content Security Policies (CSP), strict isolation for optional embedded content, and secure credential handling—to protect their systems and users. diff --git a/samples/agent/adk/rizzcharts/__init__.py b/samples/agent/adk/rizzcharts/python/__init__.py similarity index 100% rename from samples/agent/adk/rizzcharts/__init__.py rename to samples/agent/adk/rizzcharts/python/__init__.py diff --git a/samples/agent/adk/rizzcharts/__main__.py b/samples/agent/adk/rizzcharts/python/__main__.py similarity index 100% rename from samples/agent/adk/rizzcharts/__main__.py rename to samples/agent/adk/rizzcharts/python/__main__.py diff --git a/samples/agent/adk/rizzcharts/agent.py b/samples/agent/adk/rizzcharts/python/agent.py similarity index 97% rename from samples/agent/adk/rizzcharts/agent.py rename to samples/agent/adk/rizzcharts/python/agent.py index 313408eab..6cf1d02ad 100644 --- a/samples/agent/adk/rizzcharts/agent.py +++ b/samples/agent/adk/rizzcharts/python/agent.py @@ -147,13 +147,13 @@ def _build_schema_manager(self, version: str) -> A2uiSchemaManager: CatalogConfig.from_path( name="rizzcharts", catalog_path=( - f"catalog_schemas/{version}/rizzcharts_catalog_definition.json" + f"../catalog_schemas/{version}/rizzcharts_catalog_definition.json" ), - examples_path=f"examples/rizzcharts_catalog/{version}", + examples_path=f"../examples/rizzcharts_catalog/{version}", ), BasicCatalog.get_config( version=version, - examples_path=f"examples/standard_catalog/{version}", + examples_path=f"../examples/standard_catalog/{version}", ), ], accepts_inline_catalogs=True, diff --git a/samples/agent/adk/rizzcharts/agent_executor.py b/samples/agent/adk/rizzcharts/python/agent_executor.py similarity index 100% rename from samples/agent/adk/rizzcharts/agent_executor.py rename to samples/agent/adk/rizzcharts/python/agent_executor.py diff --git a/samples/agent/adk/rizzcharts/prompt_builder.py b/samples/agent/adk/rizzcharts/python/prompt_builder.py similarity index 95% rename from samples/agent/adk/rizzcharts/prompt_builder.py rename to samples/agent/adk/rizzcharts/python/prompt_builder.py index 75225e2b9..6999426a8 100644 --- a/samples/agent/adk/rizzcharts/prompt_builder.py +++ b/samples/agent/adk/rizzcharts/python/prompt_builder.py @@ -30,11 +30,11 @@ CatalogConfig.from_path( name="rizzcharts", catalog_path="rizzcharts_catalog_definition.json", - examples_path=f"examples/rizzcharts_catalog/{version}", + examples_path=f"../examples/rizzcharts_catalog/{version}", ), BasicCatalog.get_config( version=version, - examples_path=f"examples/standard_catalog/{version}", + examples_path=f"../examples/standard_catalog/{version}", ), ], accepts_inline_catalogs=True, diff --git a/samples/agent/adk/rizzcharts/pyproject.toml b/samples/agent/adk/rizzcharts/python/pyproject.toml similarity index 94% rename from samples/agent/adk/rizzcharts/pyproject.toml rename to samples/agent/adk/rizzcharts/python/pyproject.toml index 65f9d57da..72de47511 100644 --- a/samples/agent/adk/rizzcharts/pyproject.toml +++ b/samples/agent/adk/rizzcharts/python/pyproject.toml @@ -44,4 +44,4 @@ url = "https://pypi.org/simple" default = true [tool.uv.sources] -a2ui-agent = { path = "../../../../agent_sdks/python", editable = true } +a2ui-agent = { path = "../../../../../agent_sdks/python", editable = true } diff --git a/samples/agent/adk/rizzcharts/tools.py b/samples/agent/adk/rizzcharts/python/tools.py similarity index 100% rename from samples/agent/adk/rizzcharts/tools.py rename to samples/agent/adk/rizzcharts/python/tools.py diff --git a/samples/agent/adk/uv.lock b/samples/agent/adk/uv.lock index c0bb82750..0f1da45d1 100644 --- a/samples/agent/adk/uv.lock +++ b/samples/agent/adk/uv.lock @@ -17,7 +17,10 @@ members = [ ] [manifest.dependency-groups] -dev = [{ name = "pyink", specifier = ">=24.10.0" }] +dev = [ + { name = "pyink", specifier = ">=24.10.0" }, + { name = "pytest", specifier = ">=9.0.2" }, +] [[package]] name = "a2a-sdk" @@ -1408,6 +1411,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -2035,6 +2047,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + [[package]] name = "propcache" version = "0.4.1" @@ -2329,6 +2350,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -2548,7 +2585,7 @@ wheels = [ [[package]] name = "rizzcharts" version = "0.1.0" -source = { editable = "rizzcharts" } +source = { editable = "rizzcharts/python" } dependencies = [ { name = "a2a-sdk" }, { name = "a2ui-agent" }, diff --git a/samples/client/angular/README.md b/samples/client/angular/README.md index 96970563a..4e33e66d5 100644 --- a/samples/client/angular/README.md +++ b/samples/client/angular/README.md @@ -7,7 +7,7 @@ These are sample implementations of A2UI in Angular. 1. [nodejs](https://nodejs.org/en) 2. [uv](https://docs.astral.sh/uv/getting-started/installation/) -NOTE: [For the rizzcharts app](../../agent/adk/rizzcharts/), you will need GoogleMap API ([How to get the API key](https://developers.google.com/maps/documentation/javascript/get-api-key)) to display Google Map custome components. Please refer to [Rizzcharts README](./projects/rizzcharts/README.md) +NOTE: [For the rizzcharts app](../../agent/adk/rizzcharts/python/), you will need GoogleMap API ([How to get the API key](https://developers.google.com/maps/documentation/javascript/get-api-key)) to display Google Map custome components. Please refer to [Rizzcharts README](./projects/rizzcharts/README.md) ## Running @@ -30,7 +30,7 @@ Here are the instructions if you want to do each step manually. 3. Run the relevant A2A server: * [For the restaurant app](../../agent/adk/restaurant_finder/) * [For the contact app](../../agent/adk/contact_lookup/) - * [For the rizzcharts app](../../agent/adk/rizzcharts/) + * [For the rizzcharts app](../../agent/adk/rizzcharts/python/) * [For the orchestrator app](../../agent/adk/orchestrator/) 4. Run the relevant app: * `npm start -- restaurant` diff --git a/samples/client/angular/projects/rizzcharts/README.md b/samples/client/angular/projects/rizzcharts/README.md index 10fbbd794..ebb991c8b 100644 --- a/samples/client/angular/projects/rizzcharts/README.md +++ b/samples/client/angular/projects/rizzcharts/README.md @@ -12,7 +12,7 @@ These are sample implementations of A2UI in Angular. 1. Update the `src/environments/environment.ts` file with your Google Maps API key. 2. Build the shared dependencies by running `npm i`, then `npm run build` in the `renderers/web_core` directory 3. Install the dependencies: `npm i` -4. Run the A2A server for the [rizzcharts agent](../../../../agent/adk/rizzcharts/) +4. Run the A2A server for the [rizzcharts agent](../../../../agent/adk/rizzcharts/python/) 5. Run the relevant app: * `npm start -- rizzcharts` -6. Open http://localhost:4200/ \ No newline at end of file +6. Open http://localhost:4200/