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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,7 @@ data class InvocationContext(
functionResponse =
FunctionResponse(
name = tool.name,
response = toFinalResponseMap(toolResult),
response = toResponseMapPreservingNulls(toolResult),
id = toolContext.functionCallId,
)
)
Expand Down Expand Up @@ -642,6 +642,24 @@ data class InvocationContext(
@OptIn(FrameworkInternalApi::class)
private fun toTraceJson(payload: Any?): JsonElement = anyToJsonElement(payload)

/**
* Like [toFinalResponseMap] but preserves null values in the response dict (e.g. `{result:
* null}`); only entries with non-String keys are dropped. The genai `FunctionResponse` allows
* null values, so the function-response *event* keeps them. (The after-tool callback path still
* receives the null-dropped [toFinalResponseMap] to preserve its non-null `Map<String, Any>`
* contract.)
*
* TODO(b/536808100): unify with [toFinalResponseMap] so the callback path preserves nulls too.
*/
private fun toResponseMapPreservingNulls(payload: Any?): Map<String, Any?> =
if (payload !is Map<*, *>) {
mapOf(BaseTool.RESULT_KEY to payload)
} else {
payload.entries
.mapNotNull { entry -> (entry.key as? String)?.let { key -> key to entry.value } }
.toMap()
}

private fun safeCastToMapStringAny(value: Any?): Map<String, Any> {
if (value !is Map<*, *>) return emptyMap()
return value.entries
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,10 @@ internal class LlmAgentTurn(
partial = response.partial,
interrupted = response.interrupted,
cacheMetadata = response.cacheMetadata,
modelVersion = response.modelVersion,
avgLogProbs = response.avgLogprobs,
groundingMetadata = response.groundingMetadata,
citationMetadata = response.citationMetadata,
)
.populateClientFunctionCallId()

Expand Down
3 changes: 2 additions & 1 deletion core/src/commonMain/kotlin/com/google/adk/kt/events/Event.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import com.google.adk.kt.types.GroundingMetadata
import com.google.adk.kt.types.UsageMetadata
import kotlin.time.Clock
import kotlinx.serialization.Contextual
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

/**
Expand Down Expand Up @@ -72,7 +73,7 @@ data class Event(
val errorMessage: String? = null,
val finishReason: FinishReason? = null,
val usageMetadata: UsageMetadata? = null,
val avgLogProbs: Double? = null,
@SerialName("avgLogprobs") val avgLogProbs: Double? = null,
val interrupted: Boolean = false,
val branch: String? = null,
val groundingMetadata: GroundingMetadata? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import com.google.genai.kotlin.types.GenerationConfigRoutingConfigManualRoutingM
import com.google.genai.kotlin.types.GoogleMaps as GenAiGoogleMaps
import com.google.genai.kotlin.types.GoogleSearch as GenAiGoogleSearch
import com.google.genai.kotlin.types.GroundingChunk as GenAiGroundingChunk
import com.google.genai.kotlin.types.GroundingChunkMaps as GenAiGroundingChunkMaps
import com.google.genai.kotlin.types.GroundingChunkRetrievedContext as GenAiGroundingChunkRetrievedContext
import com.google.genai.kotlin.types.GroundingChunkWeb as GenAiGroundingChunkWeb
import com.google.genai.kotlin.types.GroundingMetadata as GenAiGroundingMetadata
Expand Down Expand Up @@ -467,10 +468,11 @@ internal fun GenerateContentResponse.toGenaiSdk(): GenAiGenerateContentResponse
/** Converts a [GenAiGroundingMetadata] from the GenAI SDK to an ADK [GroundingMetadata]. */
internal fun GenAiGroundingMetadata.fromGenaiSdk(): GroundingMetadata =
GroundingMetadata(
imageSearchQueries = imageSearchQueries ?: emptyList(),
imageSearchQueries = imageSearchQueries,
groundingChunks = groundingChunks?.map { it.fromGenaiSdk() },
groundingSupports = groundingSupports?.map { it.fromGenaiSdk() },
webSearchQueries = webSearchQueries,
retrievalQueries = retrievalQueries,
searchEntryPoint = searchEntryPoint?.fromGenaiSdk(),
retrievalMetadata = retrievalMetadata?.fromGenaiSdk(),
)
Expand All @@ -482,18 +484,36 @@ internal fun GroundingMetadata.toGenaiSdk(): GenAiGroundingMetadata =
groundingChunks = groundingChunks?.map { it.toGenaiSdk() },
groundingSupports = groundingSupports?.map { it.toGenaiSdk() },
webSearchQueries = webSearchQueries,
retrievalQueries = retrievalQueries,
searchEntryPoint = searchEntryPoint?.toGenaiSdk(),
retrievalMetadata = retrievalMetadata?.toGenaiSdk(),
)

// --- GroundingChunk ---
/** Converts a [GenAiGroundingChunk] from the GenAI SDK to an ADK [GroundingChunk]. */
internal fun GenAiGroundingChunk.fromGenaiSdk(): GroundingChunk =
GroundingChunk(web = web?.fromGenaiSdk(), retrievedContext = retrievedContext?.fromGenaiSdk())
GroundingChunk(
web = web?.fromGenaiSdk(),
retrievedContext = retrievedContext?.fromGenaiSdk(),
maps = maps?.fromGenaiSdk(),
)

/** Converts an ADK [GroundingChunk] to a [GenAiGroundingChunk] for the GenAI SDK. */
internal fun GroundingChunk.toGenaiSdk(): GenAiGroundingChunk =
GenAiGroundingChunk(web = web?.toGenaiSdk(), retrievedContext = retrievedContext?.toGenaiSdk())
GenAiGroundingChunk(
web = web?.toGenaiSdk(),
retrievedContext = retrievedContext?.toGenaiSdk(),
maps = maps?.toGenaiSdk(),
)

// --- GroundingChunkMaps ---
/** Converts a [GenAiGroundingChunkMaps] from the GenAI SDK to an ADK [GroundingChunkMaps]. */
internal fun GenAiGroundingChunkMaps.fromGenaiSdk(): GroundingChunkMaps =
GroundingChunkMaps(uri = uri, title = title, placeId = placeId)

/** Converts an ADK [GroundingChunkMaps] to a [GenAiGroundingChunkMaps] for the GenAI SDK. */
internal fun GroundingChunkMaps.toGenaiSdk(): GenAiGroundingChunkMaps =
GenAiGroundingChunkMaps(uri = uri, title = title, placeId = placeId)

// --- GroundingChunkWeb ---
/** Converts a [GenAiGroundingChunkWeb] from the GenAI SDK to an ADK [GroundingChunkWeb]. */
Expand Down Expand Up @@ -761,6 +781,7 @@ internal fun GenAiGenerateContentResponseUsageMetadata.fromGenaiSdk(): UsageMeta
cachedContentTokenCount = cachedContentTokenCount,
promptTokensDetails = promptTokensDetails?.map { it.fromGenaiSdk() },
candidatesTokensDetails = candidatesTokensDetails?.map { it.fromGenaiSdk() },
toolUsePromptTokensDetails = toolUsePromptTokensDetails?.map { it.fromGenaiSdk() },
)

/**
Expand All @@ -777,6 +798,7 @@ internal fun UsageMetadata.toGenaiSdk(): GenAiGenerateContentResponseUsageMetada
cachedContentTokenCount = cachedContentTokenCount,
promptTokensDetails = promptTokensDetails?.map { it.toGenaiSdk() },
candidatesTokensDetails = candidatesTokensDetails?.map { it.toGenaiSdk() },
toolUsePromptTokensDetails = toolUsePromptTokensDetails?.map { it.toGenaiSdk() },
)

// --- Part ---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,6 @@ data class GroundingChunk(
val web: GroundingChunkWeb? = null,
/** A chunk sourced from a retrieval (RAG) context. */
val retrievedContext: GroundingChunkRetrievedContext? = null,
/** A chunk sourced from Google Maps grounding. */
val maps: GroundingChunkMaps? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.adk.kt.types

import kotlinx.serialization.Serializable

/** A grounding chunk sourced from Google Maps. */
@Serializable
data class GroundingChunkMaps(
/** The URI of the place on Google Maps. */
val uri: String? = null,
/** The title (name) of the place. */
val title: String? = null,
/** The Google Maps place id (e.g. `places/ChIJ...`). */
val placeId: String? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,16 @@ import kotlinx.serialization.Serializable
/** Metadata returned to client when grounding is enabled. */
@Serializable
data class GroundingMetadata(
val imageSearchQueries: List<String> = emptyList(),
/** The image search queries used to retrieve grounding sources. */
val imageSearchQueries: List<String>? = null,
/** The cited source chunks that ground the response. */
val groundingChunks: List<GroundingChunk>? = null,
/** Maps response segments to the grounding chunks that support them. */
val groundingSupports: List<GroundingSupport>? = null,
/** The web search queries used to retrieve grounding sources. */
val webSearchQueries: List<String>? = null,
/** The retrieval queries used to retrieve grounding sources. */
val retrievalQueries: List<String>? = null,
/** The Google Search entry point for rendering search suggestions. */
val searchEntryPoint: SearchEntryPoint? = null,
/** Metadata about the retrieval step. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,8 @@ data class UsageMetadata(
val promptTokensDetails: List<ModalityTokenCount>? = null,
/** A per-modality breakdown of the candidates token count. */
val candidatesTokensDetails: List<ModalityTokenCount>? = null,
/** A per-modality breakdown of the tool-use prompt token count. */
val toolUsePromptTokensDetails: List<ModalityTokenCount>? = null,
/** The traffic type for the request (e.g. "ON_DEMAND", "PROVISIONED_THROUGHPUT"). */
val trafficType: String? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ class GenaiConvertersTest {
val adkGroundingMetadata =
GroundingMetadata(
webSearchQueries = listOf("kotlin coroutines"),
retrievalQueries = listOf("kotlin release date"),
groundingChunks =
listOf(
GroundingChunk(
Expand All @@ -376,7 +377,15 @@ class GenaiConvertersTest {
title = "Example",
domain = "example.com",
)
)
),
GroundingChunk(
maps =
GroundingChunkMaps(
uri = "https://maps.google.com/?cid=1",
title = "Googleplex",
placeId = "places/ABC",
)
),
),
groundingSupports =
listOf(
Expand All @@ -392,7 +401,8 @@ class GenaiConvertersTest {

val genaiGroundingMetadata = adkGroundingMetadata.toGenaiSdk()
assertEquals(listOf("kotlin coroutines"), genaiGroundingMetadata.webSearchQueries)
assertEquals("example.com", genaiGroundingMetadata.groundingChunks?.single()?.web?.domain)
assertEquals("example.com", genaiGroundingMetadata.groundingChunks?.get(0)?.web?.domain)
assertEquals("places/ABC", genaiGroundingMetadata.groundingChunks?.get(1)?.maps?.placeId)

val convertedBack = genaiGroundingMetadata.fromGenaiSdk()
assertEquals(adkGroundingMetadata, convertedBack)
Expand Down Expand Up @@ -655,6 +665,8 @@ class GenaiConvertersTest {
listOf(ModalityTokenCount(modality = MediaModality.TEXT, tokenCount = 1)),
candidatesTokensDetails =
listOf(ModalityTokenCount(modality = MediaModality.IMAGE, tokenCount = 2)),
toolUsePromptTokensDetails =
listOf(ModalityTokenCount(modality = MediaModality.TEXT, tokenCount = 7)),
)
val genaiUsageMetadata = adkUsageMetadata.toGenaiSdk()
assertEquals(1, genaiUsageMetadata.promptTokenCount)
Expand Down
7 changes: 7 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,10 @@ project(":google-adk-kotlin-litertlm").projectDir = file("litertlm")
include(":google-adk-kotlin-examples-android")

project(":google-adk-kotlin-examples-android").projectDir = file("examples/android")

// copybara:strip_begin(internal)
// Internal-only module (config API + conformance harness), stripped from the public GitHub mirror.
include(":google-adk-kotlin-internal-conformance")

project(":google-adk-kotlin-internal-conformance").projectDir =
file("internal/conformance") // copybara:strip_end
2 changes: 1 addition & 1 deletion webserver/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

plugins {
kotlin("jvm")
id("application")
id("java-library")
id("maven-publish")
}
Expand Down Expand Up @@ -44,6 +43,7 @@ sourceSets {
dependencies {
implementation(project(":google-adk-kotlin-core"))
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.coroutines.core)

implementation(libs.graphviz.java)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@
package com.google.adk.kt.webserver

import com.google.adk.kt.artifacts.ArtifactService
import com.google.adk.kt.plugins.Plugin
import com.google.adk.kt.runners.Runner
import com.google.adk.kt.sessions.SessionService
import com.google.adk.kt.telemetry.TelemetryConfig
import com.google.adk.kt.webserver.AdkWebServer.StatusAwareLogger
import com.google.adk.kt.webserver.loaders.AgentLoader
import com.google.adk.kt.webserver.models.VersionInfo
import com.google.adk.kt.webserver.routes.appRoutes
import com.google.adk.kt.webserver.routes.artifactRoutes
import com.google.adk.kt.webserver.routes.debugRoutes
Expand All @@ -32,8 +34,10 @@ import com.google.adk.kt.webserver.routes.sessionRoutes
import com.google.adk.kt.webserver.routes.staticRoutes
import com.google.adk.kt.webserver.telemetry.ApiServerSpanExporter
import com.google.adk.kt.webserver.telemetry.OpenTelemetryConfig
import com.google.gson.GsonBuilder
import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
import io.ktor.serialization.gson.gson
import io.ktor.server.application.Application
Expand All @@ -46,6 +50,7 @@ import io.ktor.server.plugins.callloging.CallLogging
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.request.httpMethod
import io.ktor.server.request.uri
import io.ktor.server.response.respond
import io.ktor.server.response.respondText
import io.ktor.server.routing.get
import io.ktor.server.routing.routing
Expand All @@ -68,6 +73,8 @@ class AdkWebServer(
private val agentLoader: AgentLoader,
private val apiServerSpanExporter: ApiServerSpanExporter,
private val captureMessageContent: Boolean = false,
private val plugins: List<Plugin> = emptyList(),
private val gsonConfigurer: (GsonBuilder.() -> Unit)? = null,
) {
@Deprecated(
message = "Use constructor without runner",
Expand Down Expand Up @@ -96,6 +103,9 @@ class AdkWebServer(

companion object {
private val logger = LoggerFactory.getLogger(AdkWebServer::class.java)

/** The ADK Kotlin version reported by the `/version` endpoint. */
fun adkVersion(): String = com.google.adk.kt.VERSION
}

private var server: ApplicationEngine? = null
Expand All @@ -111,6 +121,8 @@ class AdkWebServer(
agentLoader,
apiServerSpanExporter,
captureMessageContent,
plugins,
gsonConfigurer,
)
}
.start(wait = wait)
Expand All @@ -133,7 +145,7 @@ class AdkWebServer(
}

override fun read(reader: JsonReader): Instant? {
if (reader.peek() == com.google.gson.stream.JsonToken.NULL) {
if (reader.peek() == JsonToken.NULL) {
reader.nextNull()
return null
}
Expand All @@ -158,6 +170,8 @@ fun Application.adkModule(
agentLoader: AgentLoader,
apiServerSpanExporter: ApiServerSpanExporter,
captureMessageContent: Boolean = false,
plugins: List<Plugin> = emptyList(),
gsonConfigurer: (GsonBuilder.() -> Unit)? = null,
) {
install(CallLogging) {
level = Level.INFO
Expand All @@ -173,6 +187,8 @@ fun Application.adkModule(
gson {
setPrettyPrinting()
registerTypeAdapter(Instant::class.java, AdkWebServer.InstantTypeAdapter())
// Callers can register extra Gson type adapters here.
gsonConfigurer?.invoke(this)
}
}

Expand All @@ -199,12 +215,21 @@ fun Application.adkModule(

routing {
get("/api/health") { call.respondText("OK") }
get("/version") {
call.respond(
VersionInfo(
version = AdkWebServer.adkVersion(),
language = "kotlin",
languageVersion = System.getProperty("java.version", "unknown"),
)
)
}
appRoutes(agentLoader)
artifactRoutes(artifactService)
debugRoutes(apiServerSpanExporter)
evalRoutes()
graphRoutes(agentLoader, sessionService)
runRoutes(agentLoader, sessionService, artifactService)
runRoutes(agentLoader, sessionService, artifactService, plugins)
sessionRoutes(sessionService)
staticRoutes(this@adkModule)
}
Expand Down
Loading
Loading