Files
Shahin Saadati 8da5856bc5 Add Kotlin snippets to the custom tools page, including toolset filtering (#2114)
* Add Kotlin snippet for filtering a toolset's tools

The Toolsets section had no Kotlin. ToolFilter and ToolPredicate arrived in
adk-kotlin 0.7.0 and give Kotlin something the other languages do not have: a
filter that receives the ReadonlyContext, so a toolset's tool list can depend on
session state or the current user.

The Python tab in the Simple Math Toolset example only gestures at this, in a
commented-out branch inside get_tools(). This snippet implements it, showing all
three states: no filter selects everything, allowList selects by name, and a
Predicate decides per invocation.

Placed in its own subsection rather than as a fourth tab on the Simple Math
Toolset example. That group is one worked example explained by five bullets --
an agent, a greet tool, name prefixing, a tool_context.state write and close() --
and a filtering snippet satisfies none of them. Kotlin cannot satisfy the prefix
bullet at all, since BaseTool.name is a val and adk-kotlin has no prefix
mechanism. A tab there would have left readers with four bullets that do not
describe the code above them.

Transcluded, so CI compiles and lints it. Verified beyond compiling: the tools
are generated by KSP, filtering returns the expected sets for all three cases,
and the tool bodies run -- addNumbers(7,3) -> {result=10}.

* feat: add Kotlin code snippets for tool definition and usage examples to ADK documentation

* refactor: move doc snippet markers above imports in Kotlin tool examples

* Match ExternalApprovalTool to the 0.8.0 BaseTool.run signature

adk-kotlin 0.8.0 widened `BaseTool.run`'s args from `Map<String, Any>` to
`Map<String, Any?>`, so the override in this snippet overrides nothing and the
class no longer implements its abstract member. `compileKotlin` fails with
"'run' overrides nothing" at MultiAgentExample.kt:61.

The break arrived on main with the 0.8.0 bump in #2143, not from this branch.
It surfaces here because the snippet runner builds the whole examples project,
while that PR's own check only compiled the files it changed - and it changed
no .kt files at all. Fixing it here because it blocks this PR; it is one
character and unrelated to the toolset filtering content.

* Correct the tool badges and drop the exclusivity from the filter heading

Four things from joefernandez's review:

- Heading shortened to "Filter tools in toolsets" as suggested.
- That shorter heading is conceptual, and the subsection carries a Kotlin-only
  badge, which would repeat the exclusivity claim his b/548652184 is about.
  Python, Java and TypeScript all filter toolsets by name or by a
  context-aware predicate on BaseToolset, so the section now says so in a
  sentence and the badge's title scopes the version to the Kotlin ToolFilter
  API rather than to filtering as a concept.
- Page badge Kotlin v0.7.0 -> v0.1.0, and the Toolsets badge likewise. @Tool,
  ToolContext and Toolset all exist at the v0.1.0 tag; only ToolFilter is new
  in v0.7.0, and function-tools.md already carries v0.1.0.
- The Toolsets badge's bare "Java" span now reads v0.3.0, the first adk-java
  release containing BaseToolset (added in a211ac4c, tagged v0.3.0).
2026-08-19 10:32:11 -07:00

123 lines
4.1 KiB
Kotlin

/*
* 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.examples.tools.overview
// --8<-- [start:init]
import com.google.adk.kt.agents.Instruction
import com.google.adk.kt.agents.LlmAgent
import com.google.adk.kt.agents.ReadonlyContext
import com.google.adk.kt.annotations.Param
import com.google.adk.kt.annotations.Tool
import com.google.adk.kt.models.Gemini
import com.google.adk.kt.tools.BaseTool
import com.google.adk.kt.tools.ToolContext
import com.google.adk.kt.tools.ToolFilter
import com.google.adk.kt.tools.Toolset
import com.google.adk.kt.tools.isToolSelected
/** The individual tools, exposed by the @Tool annotation. */
class MathTools {
/**
* Adds two integer numbers.
*/
@Tool
fun addNumbers(
@Param("The first number.") a: Int,
@Param("The second number.") b: Int,
context: ToolContext,
): Map<String, Any> {
// Example: recording something in the session state.
context.actions.stateDelta["last_math_operation"] = "addition"
return mapOf("status" to "success", "result" to a + b)
}
/**
* Subtracts the second number from the first.
*/
@Tool
fun subtractNumbers(
@Param("The first number.") a: Int,
@Param("The second number.") b: Int,
): Map<String, Any> = mapOf("status" to "success", "result" to a - b)
}
/** An individual tool, defined outside any toolset. */
class GreetTools {
/**
* Greets the user.
*/
@Tool
fun greetUser(
@Param("The name of the user to greet.") name: String,
): Map<String, String> {
println("Tool: greetUser called with name=$name")
return mapOf("greeting" to "Hello, $name!")
}
}
/**
* A toolset that narrows what it exposes with an optional [ToolFilter].
*
* A null filter selects every tool, so the filter is genuinely optional.
*/
class SimpleMathToolset(private val filter: ToolFilter? = null) : Toolset {
private val tools = MathTools().generatedTools()
override suspend fun getTools(readonlyContext: ReadonlyContext?): List<BaseTool> =
tools.filter { filter.isToolSelected(it, readonlyContext) }
/** Releases anything the toolset holds. There is nothing to release here. */
override fun close() {}
}
/**
* An agent using both an individual tool and a toolset. Kotlin keeps the two
* apart: individual tools go in `tools`, toolsets in `toolsets`.
*/
val calculatorAgent =
LlmAgent(
name = "calculator_agent",
model = Gemini(name = "gemini-flash-latest"),
instruction =
Instruction(
"You are a helpful calculator and greeter. Use greetUser for " +
"greetings. Use addNumbers to add and subtractNumbers to " +
"subtract. Announce the state of 'last_math_operation' if it is set.",
),
tools = GreetTools().generatedTools(),
toolsets = listOf(SimpleMathToolset()),
)
// --8<-- [end:init]
// --8<-- [start:filter]
// SimpleMathToolset, defined above, applies its optional ToolFilter inside
// getTools(). Passing one narrows what the same toolset exposes.
// Expose a fixed subset by name.
val addOnlyMath = SimpleMathToolset(ToolFilter.allowList("addNumbers"))
// Or decide per invocation. The predicate receives the ReadonlyContext, so the
// tool list can depend on session state, the user, or anything else on it.
val contextAwareMath =
SimpleMathToolset(
ToolFilter.Predicate { tool, context ->
tool.name == "addNumbers" || context?.state?.get("enable_advanced_math") == true
},
)
// --8<-- [end:filter]