The ability of using tools (OpenAI calls it "function calling") is crucial for building AI agents. This SDK is trying to make tool use as simple as possible, using idiomatic Kotlin to achieve this goal.
Consider the following code:
// first we define the input which LLM will provide to us
@Serializable
@SerialName("get_weather")
class GetWeather(val location: String)
// then we define:
// 1. the tool using this input
// 2. what happens when it is executed locally
val getWeatherTool = Tool<GetWeather> {
"15 degrees at $location"
}
Note
The 15 degrees at $location
is just for the sake of example, in real-life implementation it should be obtained with a call to some weather API.
Once the tool is defined, it can be used when building the message request:
val anthropic = Anthropic()
val response = anthropic.messages.create {
+Message { +"What is the weather like in San Francisco?" }
tools += getWeatherTool
}
// process the response
The JSON Schema of the tool input will be generated under the hood using xemantic-ai-tool-schema library.
If the tool is defined with @SerialName
, then provided name will be used as a tool name. Otherwise, autogenerated class name will be assigned, which defaults to fully qualified class name of the tool input class (e.g. GetWeather
class). Anthropic API imposes further restrictions on the tool name - see adjustments described in the Tool naming conventions section.
The tool can be also named explicitly and additionally described:
@Serializable
class GetWeather(val location: String)
val tool = Tool<GetWeather>(
name = "get_weather",
description = "Get the current weather in a given location"
) {
"15 degrees at $location"
}
val anthropic = Anthropic()
// subsequent steps involving the tool
This time the get_weather
name will be used as a tool name in the underlying API call and tool description will be attached.
Annotations offer more convenient way of describing a tool, and work also for tool input properties.
@Description("Get the current weather in a given location")
class GetWeather(
@Description("The city and state, e.g. San Francisco, CA")
val location: String
)
val tool = Tool<GetWeather> {
"15 degrees at $location"
}
// subsequent steps involving the tool
Note
The @Description
annotation implies @Serializable
, which therefore can be omitted
Refer to xemantic-ai-tool-schema for full documentation of the JSON schema controlling annotations.
Note
If the @Description
annotation is specified for the tool input class (GetWeather
), then it will be removed from the tool input JSON schema used in API calls and used as a tool description instead.
Let's expand our example:
@SerialName("get_weather")
@Description("Get the current weather in a given location")
class GetWeather(
@Description("The city and state, e.g. San Francisco, CA")
val location: String
)
val tool = Tool<GetWeather> {
"15 degrees"
}
val weatherTools = listOf(tool)
val anthropic = Anthropic()
val conversation = mutableListOf<Message>()
conversation += Message { +"What is the weather like in San Francisco?" }
val response1 = anthropic.messages.create {
messages = conversation
tools = weatherTools
}
conversation += response // we need to add LLM response to the conversation
// the response also contains tool use requests
println(response1.text) // there might be additional textual content from the model
if (response1.stopReasom != StopReason.TOOL_USE) {
throw Exception("We are expecting LLM to decide to use GetWeather tool")
}
conversation += response1.useTools() // will create a message containing tool results
val response2 = anthropic.messages.create {
messages = conversation
tools = weatherTools
// even if we are not intending to use a tool, the definition must be attached
}
println(response2.text) // full response from the model after tool result has been submitted
Note
The TOOL_USE
stop reason is usually handled as so-called "Agent Loop", which either ends if the StopReason
is other than TOOL_USE
or after predefined amount of iterations.
Sometimes we just want to use a tool to obtain a structured output from the model, for example when interpreting images or parsing documents.
@Description("Describes a person mentioned in the document")
data class Person(
val name: String,
val surname: String,
val email: String
)
@SerialName("people")
@Description("Extract the list of people mentioned in the document, who are also it's authors or editors")
data class People(val people: List<Person>)
val peopleTools = listOf(Tool<People>())
val anthropic = Anthropic()
val response = anthropic.messages.create {
+Message {
+Document("foo.pdf")
+"Extract document authors"
}
tools = peopleTools
toolChoice = ToolChoice.Tool<People>() // we are forcing LLM to use this tool
}
val people = response.toolUse.input<People>()
It will return an instance of People
populated with the list of authors.
Note
This time we are using toolChoice
as it is forcing LLM to use this exact tool unconditionally, instead of inferring possible tool use from the context. We are also not calling response.useTools()
, because after parsing we are not going to pass the tool result back to the model.
Let's take a more complex example, where we are using the same People
class twice, but in different roles.
@Description("Describes a person mentioned in the document")
data class Person(
val name: String,
val surname: String,
val email: String
)
data class People(val people: List<Person>)
val peopleTools = listOf(
Tool<People>(
name = "extract_authors",
description = "Extracts the list of authors of this documents"
),
Tool<People>(
name = "extract_recipients",
description = "Extracts the list of recipients of this documents"
)
)
val anthropic = Antropic()
val conversation = mutableListOf<Message>()
conversation += Message {
+Document("foo.pdf")
+"Extract in order: 1. document authors, 2. document recipients"
}
val response1 = anthropic.messages.create {
messages = conversation
tools = peopleTools
toolChoice = ToolChoice.Tool("extract_authors")
}
conversation += response1
val response2 = anthropic.messages.create {
messages = conversation
tools = peopleTools
toolChoice = ToolChoice.Tool("extract_recipients")
}
conversation += response1
val authors = response.toolUses[0]
val recipients = response.toolUses[1]
A tool can also have properties:
@Description("Get the current weather in a given location")
class GetWeather(
@Description("The city and state, e.g. San Francisco, CA")
val location: String
)
val tools = list(
Tool<GetWeather>(
builder = {
cacheControl = CacheControl.Ephemeral()
}
) {
"15 degrees at $location"
}
)
val anthropic = Anthropic {
}
// subsequent steps involving the tool
Tools can be provided with dependencies, for example singleton services providing some facilities, like HTTP client to connect to call the API or DB connection pool to access the database.
@AnthropicTool("query_database")
@Description("Executes SQL on the database")
data class QueryDatabase(val sql: String)
fun Connection.queryDatabase(sql: String) {
prepareStatement(sql).use { statement ->
statement.executeQuery().use { resultSet ->
resultSet.toString()
}
}
}
val dataSource = initDataSource()
val client = Anthropic {
tool<QueryDatabase> {
dataSource.connection.use {
it.queryDatabase(sql)
}
}
}
val response = client.messages.create {
+Message { +"Select all the users who never logged into the the system" }
singleTool<QueryDatabase>()
}
val tool = response.content.filterIsInstance<ToolUse>().first()
val toolResult = tool.use()
println(toolResult)
After the DatabaseQueryTool
is decoded from the API response, it can be processed
by the lambda function passed to the tool definition. In case of the example above,
the lambda will use the dataSource
as a dependency.
More sophisticated code examples targeting various Kotlin platforms can be found in the anthropic-sdk-kotlin-demo project.
In case of inferring tool name from the Kotlin class name, due to name restrictions in Anthropic API, the following transformations will be applied:
- trailing
$
will be removed - all the
.
(dot) and$
(dollar sign) in the name will be replaced with_
(underscore) - the length will be truncated to max 64 characters.
TBD
TBD
TBD