Skip to content

Commit bc89fb2

Browse files
damianmomotgooglecopybara-github
authored andcommitted
feat: accept Java List/Map params in @tool and add a Java function-tool example
Add FunctionToolDemoAgentJava, a Java port of FunctionToolDemoAgent, in the Kotlin examples module so KSP processes its @tool methods at compile time. The @tool processor now also accepts java.util.List/Map (which reach KSP as the kotlin.collections.Mutable* variants), so collection-typed tools work from Java. PiperOrigin-RevId: 967080670
1 parent e554455 commit bc89fb2

3 files changed

Lines changed: 177 additions & 10 deletions

File tree

examples/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ val jdkVersion = providers.gradleProperty("jdkVersion").getOrElse("17").toInt()
2727

2828
kotlin { jvmToolchain(maxOf(21, jdkVersion)) }
2929

30-
sourceSets { main { java.srcDirs("src/main/kotlin") } }
30+
sourceSets { main { java.srcDirs("src/main/kotlin", "src/main/java") } }
3131

3232
dependencies {
3333
implementation(project(":google-adk-kotlin-a2a"))
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.adk.kt.examples.interop;
18+
19+
import com.google.adk.kt.agents.BaseAgent;
20+
import com.google.adk.kt.agents.LlmAgent;
21+
import com.google.adk.kt.annotations.Param;
22+
import com.google.adk.kt.annotations.Tool;
23+
import com.google.adk.kt.models.Gemini;
24+
import com.google.adk.kt.tools.ToolContext;
25+
import java.util.ArrayList;
26+
import java.util.LinkedHashMap;
27+
import java.util.List;
28+
import java.util.Locale;
29+
import java.util.Map;
30+
import java.util.concurrent.ThreadLocalRandom;
31+
32+
/**
33+
* Java port of {@code FunctionToolDemoAgent.kt} exposing {@code @Tool} methods from a pure-Java
34+
* service. It sits in the Kotlin examples module because {@code @Tool} runs through KSP at compile
35+
* time; the sample's data classes are modeled as {@link Map}s.
36+
*/
37+
public final class FunctionToolDemoAgentJava {
38+
39+
/** Tea status enum, ported directly from the Kotlin enum. */
40+
public enum TeaStatus {
41+
HOT,
42+
COLD,
43+
NOT_AVAILABLE,
44+
NEARLY_BUT_NOT_QUITE_ENTIRELY_UNLIKE_TEA,
45+
}
46+
47+
/** A mock service whose {@code @Tool}-annotated methods are exposed to the LLM. */
48+
public static final class HitchhikersGuideService {
49+
50+
@Tool(
51+
description =
52+
"Retrieves the Answer to the Ultimate Question of Life, the Universe, and Everything.")
53+
public String getAnswerToEverything(
54+
@Param(
55+
description =
56+
"The question to ask Deep Thought, e.g., 'What is the answer to life?'")
57+
String question) {
58+
System.out.println(">>> Deep Thought [JAVA]: Calculating answer for '" + question + "'...");
59+
String q = question.toLowerCase(Locale.ROOT);
60+
if (q.contains("life") && q.contains("universe")) {
61+
return "The answer to the Ultimate Question of Life, the Universe, and Everything is 42.";
62+
}
63+
return "I don't know that. I only know the answer to the Ultimate Question.";
64+
}
65+
66+
@Tool(description = "Calculates the improbability of a given event.")
67+
public String calculateImprobability(
68+
@Param(description = "The event, e.g. 'A cup of tea materializing'") String event,
69+
@Param(description = "Desired level of improbability") Double level) {
70+
System.out.println(
71+
">>> Improbability Drive [JAVA]: Engaging for " + event + " at level " + level + "...");
72+
double improbability = ThreadLocalRandom.current().nextDouble() * 1000;
73+
return "The improbability of '" + event + "' is approximately " + improbability + " to 1.";
74+
}
75+
76+
/** Uses {@link Map}s where the Kotlin sample used data classes. */
77+
@Tool(description = "Gets the status of the Infinite Improbability Drive at given coordinates.")
78+
public Map<String, Object> getDriveStatus(
79+
@Param(description = "Galactic coordinates as an object with numeric x, y, z, time")
80+
Map<String, Object> coordinates) {
81+
System.out.println(">>> Heart of Gold [JAVA]: Checking drive status at " + coordinates + ".");
82+
List<String> sideEffects = new ArrayList<>();
83+
sideEffects.add("Whales and petunias materializing");
84+
sideEffects.add("Reality alteration");
85+
Map<String, Object> report = new LinkedHashMap<>();
86+
report.put("locationName", "Sector ZZ9 Plural Z Alpha");
87+
report.put("improbabilityLevel", ThreadLocalRandom.current().nextDouble() * 1e6);
88+
report.put("sideEffects", sideEffects);
89+
report.put("teaStatus", TeaStatus.NEARLY_BUT_NOT_QUITE_ENTIRELY_UNLIKE_TEA.name());
90+
return report;
91+
}
92+
93+
@Tool(description = "Gets bulk guide entries. Demonstrates a List parameter and Map return.")
94+
public Map<String, Object> getBulkGuideEntries(
95+
@Param(description = "List of guide entries to look up") List<String> entries) {
96+
System.out.println(">>> The Guide [JAVA]: Looking up bulk entries for " + entries + "...");
97+
Map<String, Object> result = new LinkedHashMap<>();
98+
for (String entry : entries) {
99+
Map<String, Object> report = new LinkedHashMap<>();
100+
report.put("locationName", entry);
101+
report.put("improbabilityLevel", 42.0);
102+
report.put("sideEffects", new ArrayList<String>());
103+
report.put("teaStatus", TeaStatus.NOT_AVAILABLE.name());
104+
result.put(entry, report);
105+
}
106+
return result;
107+
}
108+
109+
@Tool(
110+
description = "Submits a request for tea. Demonstrates context injection and enum params.")
111+
public String submitTeaRequest(
112+
ToolContext context,
113+
@Param(description = "The person requesting tea") String requester,
114+
@Param(description = "The desired status of the tea") TeaStatus status) {
115+
System.out.println(
116+
">>> Nutri-Matic [JAVA]: Submitting "
117+
+ status
118+
+ " tea for "
119+
+ requester
120+
+ "... (Call ID: "
121+
+ context.getFunctionCallId()
122+
+ ")");
123+
return "Successfully submitted request for " + status + " tea for " + requester + ".";
124+
}
125+
126+
@Tool(description = "Retrieves an entry from The Hitchhiker's Guide for a specific edition.")
127+
public String getHistoricalGuideEntry(
128+
@Param(description = "The name of the entry (e.g. 'Babel Fish')") String entryName,
129+
@Param(description = "The edition of the guide (e.g. 'Standard', 'Premium')")
130+
String edition) {
131+
System.out.println(
132+
">>> The Guide [JAVA]: Looking up " + entryName + " in the " + edition + " edition...");
133+
return switch (entryName.toLowerCase(Locale.ROOT)) {
134+
case "babel fish" ->
135+
"The Babel fish is small, yellow, and leech-like. (Edition: " + edition + ")";
136+
case "vogon" ->
137+
"Vogons are one of the most unpleasant races in the Galaxy. (Edition: " + edition + ")";
138+
default ->
139+
"Entry for '" + entryName + "' not found. Mostly harmless. (Edition: " + edition + ")";
140+
};
141+
}
142+
}
143+
144+
public static final BaseAgent rootAgent =
145+
LlmAgent.builder()
146+
.name("hitchhikers_guide_bot")
147+
.model(new Gemini("gemini-3.1-flash-lite"))
148+
.instruction(
149+
"""
150+
You are a helpful assistant themed around "The Hitchhiker's Guide to the Galaxy".
151+
Use the available tools as requested to showcase their capabilities.
152+
Be witty, slightly sarcastic, and concise, in the style of the Guide. Don't Panic.\
153+
""")
154+
// KSP-generated accessor; a static *Kt method from Java.
155+
.tools(
156+
FunctionToolDemoAgentJava_HitchhikersGuideService_GeneratedToolsKt.generatedTools(
157+
new HitchhikersGuideService()))
158+
.build();
159+
160+
private FunctionToolDemoAgentJava() {}
161+
}

processor/src/jvmMain/kotlin/com/google/adk/kt/compiler/ksp/FunctionToolGenerator.kt

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -245,14 +245,14 @@ class FunctionToolGenerator(
245245
return null
246246
}
247247
}
248-
typeNameString == LIST_QUALIFIED_NAME -> {
248+
typeNameString in LIST_QUALIFIED_NAMES -> {
249249
if (
250250
!buildListParameter(paramName, paramType, isRequired, executeFun, param, mutableSetOf())
251251
) {
252252
return null
253253
}
254254
}
255-
typeNameString == MAP_QUALIFIED_NAME -> {
255+
typeNameString in MAP_QUALIFIED_NAMES -> {
256256
if (!buildMapParameter(paramName, paramType, isRequired, executeFun, mutableSetOf())) {
257257
return null
258258
}
@@ -436,7 +436,7 @@ class FunctionToolGenerator(
436436
return false
437437
}
438438
}
439-
cpTypeNameString == LIST_QUALIFIED_NAME -> {
439+
cpTypeNameString in LIST_QUALIFIED_NAMES -> {
440440
if (
441441
!buildListParameter(
442442
"${paramName}_${cpName}",
@@ -453,7 +453,7 @@ class FunctionToolGenerator(
453453
return false
454454
}
455455
}
456-
cpTypeNameString == MAP_QUALIFIED_NAME -> {
456+
cpTypeNameString in MAP_QUALIFIED_NAMES -> {
457457
if (
458458
!buildMapParameter(
459459
"${paramName}_${cpName}",
@@ -658,7 +658,7 @@ class FunctionToolGenerator(
658658
typeName in PRIMITIVE_OR_STRING_QUALIFIED_NAMES -> valueExpr
659659
typeDeclaration?.classKind == ClassKind.ENUM_CLASS ->
660660
if (type.isMarkedNullable) "${valueExpr}?.name" else "${valueExpr}.name"
661-
typeName == LIST_QUALIFIED_NAME -> {
661+
typeName in LIST_QUALIFIED_NAMES -> {
662662
val listTypeArg = type.arguments.firstOrNull()?.type?.resolve()
663663
if (listTypeArg != null) {
664664
// `Any` element: pass the list through; the wire layer handles arbitrary `Any?`
@@ -681,7 +681,7 @@ class FunctionToolGenerator(
681681
valueExpr
682682
}
683683
}
684-
typeName == MAP_QUALIFIED_NAME -> {
684+
typeName in MAP_QUALIFIED_NAMES -> {
685685
val mapValueTypeArg = type.arguments.getOrNull(1)?.type?.resolve()
686686
if (mapValueTypeArg != null) {
687687
// `Any` value: pass the map through; the wire layer handles arbitrary `Any?` values.
@@ -859,7 +859,7 @@ class FunctionToolGenerator(
859859
val qualifiedName = type.declaration.qualifiedName?.asString()
860860
val typeDeclaration = type.declaration as? KSClassDeclaration
861861
return when {
862-
qualifiedName == LIST_QUALIFIED_NAME -> {
862+
qualifiedName in LIST_QUALIFIED_NAMES -> {
863863
val element = type.arguments.firstOrNull()?.type?.resolve() ?: return false
864864
element.declaration.qualifiedName?.asString() != ANY_QUALIFIED_NAME &&
865865
describesFaithfully(element, visited)
@@ -946,8 +946,8 @@ class FunctionToolGenerator(
946946
typeString == INT_QUALIFIED_NAME -> "INTEGER"
947947
typeString == DOUBLE_QUALIFIED_NAME || typeString == FLOAT_QUALIFIED_NAME -> "NUMBER"
948948
typeString == BOOLEAN_QUALIFIED_NAME -> "BOOLEAN"
949-
typeString == LIST_QUALIFIED_NAME -> "ARRAY"
950-
typeString == MAP_QUALIFIED_NAME -> "OBJECT"
949+
typeString in LIST_QUALIFIED_NAMES -> "ARRAY"
950+
typeString in MAP_QUALIFIED_NAMES -> "OBJECT"
951951
typeDeclaration?.classKind == ClassKind.ENUM_CLASS -> "STRING"
952952
typeDeclaration?.isDataClass() == true -> "OBJECT"
953953
else -> "STRING"
@@ -1080,6 +1080,12 @@ class FunctionToolGenerator(
10801080
private val UNIT_QUALIFIED_NAME = Unit::class.qualifiedName
10811081
private val LIST_QUALIFIED_NAME = List::class.qualifiedName
10821082
private val MAP_QUALIFIED_NAME = Map::class.qualifiedName
1083+
// Hardcoded because Mutable*::class.qualifiedName resolves to the read-only name at runtime.
1084+
private const val MUTABLE_LIST_QUALIFIED_NAME = "kotlin.collections.MutableList"
1085+
private const val MUTABLE_MAP_QUALIFIED_NAME = "kotlin.collections.MutableMap"
1086+
// Java collections reach KSP as the mutable variants, so accept both forms.
1087+
private val LIST_QUALIFIED_NAMES = setOf(LIST_QUALIFIED_NAME, MUTABLE_LIST_QUALIFIED_NAME)
1088+
private val MAP_QUALIFIED_NAMES = setOf(MAP_QUALIFIED_NAME, MUTABLE_MAP_QUALIFIED_NAME)
10831089
private val ANY_QUALIFIED_NAME = Any::class.qualifiedName
10841090
private val TOOL_CONTEXT_QUALIFIED_NAME = ToolContext::class.qualifiedName
10851091
private val PRIMITIVE_OR_STRING_QUALIFIED_NAMES =

0 commit comments

Comments
 (0)