Skip to content

Latest commit

 

History

History
314 lines (233 loc) · 9.47 KB

tool_use.md

File metadata and controls

314 lines (233 loc) · 9.47 KB

Tool Use conventions

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.

Defining and using a tool, a typical use case

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.

Naming and describing a tool

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.

Describing a tool with annotations

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.

Returning ToolResult back to the ToolUse requesting model

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.

Using tools for obtaining structured output

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]

Specifying tool properties

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

Injecting tool dependencies

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.

Tool naming conventions

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.

See NormalizeToolNameTest.kt

The tool result returned to the model can represent an object

TBD

tool use suspended function

TBD

Built In tools

TBD