Building an AI Banking Assistant with Koog
Open on GitHub Download .ipynb
In this tutorial we’ll build a small banking assistant using Koog agents in Kotlin. You’ll learn how to: - Define domain models and sample data - Expose capability-focused tools for money transfers and transaction analytics - Classify user intent (Transfer vs Analytics) - Orchestrate calls in two styles: 1) a graph/subgraph strategy 2) “agents as tools”
By the end, you’ll be able to route free-form user requests to the right tools and produce helpful, auditable responses.
Setup & Dependencies
We’ll use the Kotlin Notebook kernel. Make sure your Koog artifacts are resolvable from Maven Central
and your LLM provider key is available via OPENAI_API_KEY
.
%useLatestDescriptors
%use datetime
// uncomment this for using koog from Maven Central
// %use koog
import ai.koog.prompt.executor.llms.all.simpleOpenAIExecutor
val apiKey = System.getenv("OPENAI_API_KEY") ?: error("Please set OPENAI_API_KEY environment variable")
val openAIExecutor = simpleOpenAIExecutor(apiKey)
Defining the System Prompt
A well-crafted system prompt helps the AI understand its role and constraints. This prompt will guide all our agents' behavior.
val bankingAssistantSystemPrompt = """
|You are a banking assistant interacting with a user (userId=123).
|Your goal is to understand the user's request and determine whether it can be fulfilled using the available tools.
|
|If the task can be accomplished with the provided tools, proceed accordingly,
|at the end of the conversation respond with: "Task completed successfully."
|If the task cannot be performed with the tools available, respond with: "Can't perform the task."
""".trimMargin()
Domain model & Sample data
First, let's define our domain models and sample data. We'll use Kotlin's data classes with serialization support.
import kotlinx.serialization.Serializable
@Serializable
data class Contact(
val id: Int,
val name: String,
val surname: String? = null,
val phoneNumber: String
)
val contactList = listOf(
Contact(100, "Alice", "Smith", "+1 415 555 1234"),
Contact(101, "Bob", "Johnson", "+49 151 23456789"),
Contact(102, "Charlie", "Williams", "+36 20 123 4567"),
Contact(103, "Daniel", "Anderson", "+46 70 123 45 67"),
Contact(104, "Daniel", "Garcia", "+34 612 345 678"),
)
val contactById = contactList.associateBy(Contact::id)
Tools: Money Transfer
Tools should be pure and predictable.
We model two “soft contracts”:
- chooseRecipient
returns candidates when ambiguity is detected.
- sendMoney
supports a confirmed
flag. If false
, it asks the agent to confirm with the user.
import ai.koog.agents.core.tools.annotations.LLMDescription
import ai.koog.agents.core.tools.annotations.Tool
import ai.koog.agents.core.tools.reflect.ToolSet
@LLMDescription("Tools for money transfer operations.")
class MoneyTransferTools : ToolSet {
@Tool
@LLMDescription(
"""
Returns the list of contacts for the given user.
The user in this demo is always userId=123.
"""
)
fun getContacts(
@LLMDescription("The unique identifier of the user whose contact list is requested.") userId: Int
): String = buildString {
contactList.forEach { c ->
appendLine("${c.id}: ${c.name} ${c.surname ?: ""} (${c.phoneNumber})")
}
}.trimEnd()
@Tool
@LLMDescription("Returns the current balance (demo value).")
fun getBalance(
@LLMDescription("The unique identifier of the user.") userId: Int
): String = "Balance: 200.00 EUR"
@Tool
@LLMDescription("Returns the default user currency (demo value).")
fun getDefaultCurrency(
@LLMDescription("The unique identifier of the user.") userId: Int
): String = "EUR"
@Tool
@LLMDescription("Returns a demo FX rate between two ISO currencies (e.g. EUR→USD).")
fun getExchangeRate(
@LLMDescription("Base currency (e.g., EUR).") from: String,
@LLMDescription("Target currency (e.g., USD).") to: String
): String = when (from.uppercase() to to.uppercase()) {
"EUR" to "USD" -> "1.10"
"EUR" to "GBP" -> "0.86"
"GBP" to "EUR" -> "1.16"
"USD" to "EUR" -> "0.90"
else -> "No information about exchange rate available."
}
@Tool
@LLMDescription(
"""
Returns a ranked list of possible recipients for an ambiguous name.
The agent should ask the user to pick one and then use the selected contact id.
"""
)
fun chooseRecipient(
@LLMDescription("An ambiguous or partial contact name.") confusingRecipientName: String
): String {
val matches = contactList.filter { c ->
c.name.contains(confusingRecipientName, ignoreCase = true) ||
(c.surname?.contains(confusingRecipientName, ignoreCase = true) ?: false)
}
if (matches.isEmpty()) {
return "No candidates found for '$confusingRecipientName'. Use getContacts and ask the user to choose."
}
return matches.mapIndexed { idx, c ->
"${idx + 1}. ${c.id}: ${c.name} ${c.surname ?: ""} (${c.phoneNumber})"
}.joinToString("\n")
}
@Tool
@LLMDescription(
"""
Sends money from the user to a contact.
If confirmed=false, return "REQUIRES_CONFIRMATION" with a human-readable summary.
The agent should confirm with the user before retrying with confirmed=true.
"""
)
fun sendMoney(
@LLMDescription("Sender user id.") senderId: Int,
@LLMDescription("Amount in sender's default currency.") amount: Double,
@LLMDescription("Recipient contact id.") recipientId: Int,
@LLMDescription("Short purpose/description.") purpose: String,
@LLMDescription("Whether the user already confirmed this transfer.") confirmed: Boolean = false
): String {
val recipient = contactById[recipientId] ?: return "Invalid recipient."
val summary = "Transfer €%.2f to %s %s (%s) for \"%s\"."
.format(amount, recipient.name, recipient.surname ?: "", recipient.phoneNumber, purpose)
if (!confirmed) {
return "REQUIRES_CONFIRMATION: $summary"
}
// In a real system this is where you'd call a payment API.
return "Money was sent. $summary"
}
}
Creating Your First Agent
Now let's create an agent that uses our money transfer tools. An agent combines an LLM with tools to accomplish tasks.
import ai.koog.agents.core.agent.AIAgent
import ai.koog.agents.core.tools.ToolRegistry
import ai.koog.agents.core.tools.reflect.asTools
import ai.koog.agents.ext.tool.AskUser
import ai.koog.prompt.executor.clients.openai.OpenAIModels
import kotlinx.coroutines.runBlocking
val transferAgent = AIAgent(
executor = openAIExecutor,
llmModel = OpenAIModels.Reasoning.GPT4oMini,
systemPrompt = bankingAssistantSystemPrompt,
temperature = 0.0, // Use deterministic responses for financial operations
toolRegistry = ToolRegistry {
tool(AskUser)
tools(MoneyTransferTools().asTools())
}
)
// Test the agent with various scenarios
println("Banking Assistant started")
val message = "Send 25 euros to Daniel for dinner at the restaurant."
// Other test messages you can try:
// - "Send 50 euros to Alice for the concert tickets"
// - "What's my current balance?"
// - "Transfer 100 euros to Bob for the shared vacation expenses"
runBlocking {
val result = transferAgent.run(message)
result
}
Banking Assistant started
There are two contacts named Daniel. Please confirm which one you would like to send money to:
1. Daniel Anderson (+46 70 123 45 67)
2. Daniel Garcia (+34 612 345 678)
Please confirm the transfer of €25.00 to Daniel Garcia (+34 612 345 678) for "Dinner at the restaurant".
Task completed successfully.
Adding Transaction Analytics
Let's expand our assistant's capabilities with transaction analysis tools. First, we'll define the transaction domain model.
@Serializable
enum class TransactionCategory(val title: String) {
FOOD_AND_DINING("Food & Dining"),
SHOPPING("Shopping"),
TRANSPORTATION("Transportation"),
ENTERTAINMENT("Entertainment"),
GROCERIES("Groceries"),
HEALTH("Health"),
UTILITIES("Utilities"),
HOME_IMPROVEMENT("Home Improvement");
companion object {
fun fromString(value: String): TransactionCategory? =
entries.find { it.title.equals(value, ignoreCase = true) }
fun availableCategories(): String =
entries.joinToString(", ") { it.title }
}
}
@Serializable
data class Transaction(
val merchant: String,
val amount: Double,
val category: TransactionCategory,
val date: LocalDateTime
)
Sample transaction data
val transactionAnalysisPrompt = """
Today is 2025-05-22.
Available categories for transactions: ${TransactionCategory.availableCategories()}
"""
val sampleTransactions = listOf(
Transaction("Starbucks", 5.99, TransactionCategory.FOOD_AND_DINING, LocalDateTime(2025, 5, 22, 8, 30, 0, 0)),
Transaction("Amazon", 129.99, TransactionCategory.SHOPPING, LocalDateTime(2025, 5, 22, 10, 15, 0, 0)),
Transaction(
"Shell Gas Station",
45.50,
TransactionCategory.TRANSPORTATION,
LocalDateTime(2025, 5, 21, 18, 45, 0, 0)
),
Transaction("Netflix", 15.99, TransactionCategory.ENTERTAINMENT, LocalDateTime(2025, 5, 21, 12, 0, 0, 0)),
Transaction("AMC Theaters", 32.50, TransactionCategory.ENTERTAINMENT, LocalDateTime(2025, 5, 20, 19, 30, 0, 0)),
Transaction("Whole Foods", 89.75, TransactionCategory.GROCERIES, LocalDateTime(2025, 5, 20, 16, 20, 0, 0)),
Transaction("Target", 67.32, TransactionCategory.SHOPPING, LocalDateTime(2025, 5, 20, 14, 30, 0, 0)),
Transaction("CVS Pharmacy", 23.45, TransactionCategory.HEALTH, LocalDateTime(2025, 5, 19, 11, 25, 0, 0)),
Transaction("Subway", 12.49, TransactionCategory.FOOD_AND_DINING, LocalDateTime(2025, 5, 19, 13, 15, 0, 0)),
Transaction("Spotify Premium", 9.99, TransactionCategory.ENTERTAINMENT, LocalDateTime(2025, 5, 19, 14, 15, 0, 0)),
Transaction("AT&T", 85.00, TransactionCategory.UTILITIES, LocalDateTime(2025, 5, 18, 9, 0, 0, 0)),
Transaction("Home Depot", 156.78, TransactionCategory.HOME_IMPROVEMENT, LocalDateTime(2025, 5, 18, 15, 45, 0, 0)),
Transaction("Amazon", 129.99, TransactionCategory.SHOPPING, LocalDateTime(2025, 5, 17, 10, 15, 0, 0)),
Transaction("Starbucks", 5.99, TransactionCategory.FOOD_AND_DINING, LocalDateTime(2025, 5, 17, 8, 30, 0, 0)),
Transaction("Whole Foods", 89.75, TransactionCategory.GROCERIES, LocalDateTime(2025, 5, 16, 16, 20, 0, 0)),
Transaction("CVS Pharmacy", 23.45, TransactionCategory.HEALTH, LocalDateTime(2025, 5, 15, 11, 25, 0, 0)),
Transaction("AT&T", 85.00, TransactionCategory.UTILITIES, LocalDateTime(2025, 5, 14, 9, 0, 0, 0)),
Transaction("Xbox Game Pass", 14.99, TransactionCategory.ENTERTAINMENT, LocalDateTime(2025, 5, 14, 16, 45, 0, 0)),
Transaction("Aldi", 76.45, TransactionCategory.GROCERIES, LocalDateTime(2025, 5, 13, 17, 30, 0, 0)),
Transaction("Chipotle", 15.75, TransactionCategory.FOOD_AND_DINING, LocalDateTime(2025, 5, 13, 12, 45, 0, 0)),
Transaction("Best Buy", 299.99, TransactionCategory.SHOPPING, LocalDateTime(2025, 5, 12, 14, 20, 0, 0)),
Transaction("Olive Garden", 89.50, TransactionCategory.FOOD_AND_DINING, LocalDateTime(2025, 5, 12, 19, 15, 0, 0)),
Transaction("Whole Foods", 112.34, TransactionCategory.GROCERIES, LocalDateTime(2025, 5, 11, 10, 30, 0, 0)),
Transaction("Old Navy", 45.99, TransactionCategory.SHOPPING, LocalDateTime(2025, 5, 11, 13, 45, 0, 0)),
Transaction("Panera Bread", 18.25, TransactionCategory.FOOD_AND_DINING, LocalDateTime(2025, 5, 10, 11, 30, 0, 0)),
Transaction("Costco", 245.67, TransactionCategory.GROCERIES, LocalDateTime(2025, 5, 10, 15, 20, 0, 0)),
Transaction("Five Guys", 22.50, TransactionCategory.FOOD_AND_DINING, LocalDateTime(2025, 5, 9, 18, 30, 0, 0)),
Transaction("Macy's", 156.78, TransactionCategory.SHOPPING, LocalDateTime(2025, 5, 9, 14, 15, 0, 0)),
Transaction("Hulu Plus", 12.99, TransactionCategory.ENTERTAINMENT, LocalDateTime(2025, 5, 8, 20, 0, 0, 0)),
Transaction("Whole Foods", 94.23, TransactionCategory.GROCERIES, LocalDateTime(2025, 5, 8, 16, 45, 0, 0)),
Transaction("Texas Roadhouse", 78.90, TransactionCategory.FOOD_AND_DINING, LocalDateTime(2025, 5, 8, 19, 30, 0, 0)),
Transaction("Walmart", 167.89, TransactionCategory.SHOPPING, LocalDateTime(2025, 5, 7, 11, 20, 0, 0)),
Transaction("Chick-fil-A", 14.75, TransactionCategory.FOOD_AND_DINING, LocalDateTime(2025, 5, 7, 12, 30, 0, 0)),
Transaction("Aldi", 82.45, TransactionCategory.GROCERIES, LocalDateTime(2025, 5, 6, 15, 45, 0, 0)),
Transaction("TJ Maxx", 67.90, TransactionCategory.SHOPPING, LocalDateTime(2025, 5, 6, 13, 20, 0, 0)),
Transaction("P.F. Chang's", 95.40, TransactionCategory.FOOD_AND_DINING, LocalDateTime(2025, 5, 5, 19, 15, 0, 0)),
Transaction("Whole Foods", 78.34, TransactionCategory.GROCERIES, LocalDateTime(2025, 5, 4, 14, 30, 0, 0)),
Transaction("H&M", 89.99, TransactionCategory.SHOPPING, LocalDateTime(2025, 5, 3, 16, 20, 0, 0)),
Transaction("Red Lobster", 112.45, TransactionCategory.FOOD_AND_DINING, LocalDateTime(2025, 5, 2, 18, 45, 0, 0)),
Transaction("Whole Foods", 67.23, TransactionCategory.GROCERIES, LocalDateTime(2025, 5, 2, 11, 30, 0, 0)),
Transaction("Marshalls", 123.45, TransactionCategory.SHOPPING, LocalDateTime(2025, 5, 1, 15, 20, 0, 0)),
Transaction(
"Buffalo Wild Wings",
45.67,
TransactionCategory.FOOD_AND_DINING,
LocalDateTime(2025, 5, 1, 19, 30, 0, 0)
),
Transaction("Aldi", 145.78, TransactionCategory.GROCERIES, LocalDateTime(2025, 5, 1, 10, 15, 0, 0))
)
Transaction Analysis Tools
@LLMDescription("Tools for analyzing transaction history")
class TransactionAnalysisTools : ToolSet {
@Tool
@LLMDescription(
"""
Retrieves transactions filtered by userId, category, start date, and end date.
All parameters are optional. If no parameters are provided, all transactions are returned.
Dates should be in the format YYYY-MM-DD.
"""
)
fun getTransactions(
@LLMDescription("The ID of the user whose transactions to retrieve.")
userId: String? = null,
@LLMDescription("The category to filter transactions by (e.g., 'Food & Dining').")
category: String? = null,
@LLMDescription("The start date to filter transactions by, in the format YYYY-MM-DD.")
startDate: String? = null,
@LLMDescription("The end date to filter transactions by, in the format YYYY-MM-DD.")
endDate: String? = null
): String {
var filteredTransactions = sampleTransactions
// Validate userId (in production, this would query a real database)
if (userId != null && userId != "123") {
return "No transactions found for user $userId."
}
// Apply category filter
category?.let { cat ->
val categoryEnum = TransactionCategory.fromString(cat)
?: return "Invalid category: $cat. Available: ${TransactionCategory.availableCategories()}"
filteredTransactions = filteredTransactions.filter { it.category == categoryEnum }
}
// Apply date range filters
startDate?.let { date ->
val startDateTime = parseDate(date, startOfDay = true)
filteredTransactions = filteredTransactions.filter { it.date >= startDateTime }
}
endDate?.let { date ->
val endDateTime = parseDate(date, startOfDay = false)
filteredTransactions = filteredTransactions.filter { it.date <= endDateTime }
}
if (filteredTransactions.isEmpty()) {
return "No transactions found matching the specified criteria."
}
return filteredTransactions.joinToString("\n") { transaction ->
"${transaction.date}: ${transaction.merchant} - " +
"$${transaction.amount} (${transaction.category.title})"
}
}
@Tool
@LLMDescription("Calculates the sum of an array of double numbers.")
fun sumArray(
@LLMDescription("Comma-separated list of double numbers to sum (e.g., '1.5,2.3,4.7').")
numbers: String
): String {
val numbersList = numbers.split(",")
.mapNotNull { it.trim().toDoubleOrNull() }
val sum = numbersList.sum()
return "Sum: $%.2f".format(sum)
}
// Helper function to parse dates
private fun parseDate(dateStr: String, startOfDay: Boolean): LocalDateTime {
val parts = dateStr.split("-").map { it.toInt() }
require(parts.size == 3) { "Invalid date format. Use YYYY-MM-DD" }
return if (startOfDay) {
LocalDateTime(parts[0], parts[1], parts[2], 0, 0, 0, 0)
} else {
LocalDateTime(parts[0], parts[1], parts[2], 23, 59, 59, 999999999)
}
}
}
val analysisAgent = AIAgent(
executor = openAIExecutor,
llmModel = OpenAIModels.Reasoning.GPT4oMini,
systemPrompt = "$bankingAssistantSystemPrompt\n$transactionAnalysisPrompt",
temperature = 0.0,
toolRegistry = ToolRegistry {
tools(TransactionAnalysisTools().asTools())
}
)
println("Transaction Analysis Assistant started")
val analysisMessage = "How much have I spent on restaurants this month?"
// Other queries to try:
// - "What's my maximum check at a restaurant this month?"
// - "How much did I spend on groceries in the first week of May?"
// - "What's my total spending on entertainment in May?"
// - "Show me all transactions from last week"
runBlocking {
val result = analysisAgent.run(analysisMessage)
result
}
Transaction Analysis Assistant started
You have spent a total of $517.64 on restaurants this month.
Task completed successfully.
Building an Agent with Graph
Now let's combine our specialized agents into a graph agent that can route requests to the appropriate handler.
Request Classification
First, we need a way to classify incoming requests:
import ai.koog.agents.ext.agent.SerializableSubgraphResult
import kotlinx.serialization.SerialName
@SerialName("UserRequestType")
@Serializable
@LLMDescription("Type of user request: Transfer or Analytics")
enum class RequestType {
Transfer,
Analytics
}
@Serializable
@LLMDescription("The bank request that was classified by the agent.")
data class ClassifiedBankRequest(
@LLMDescription("Type of request: Transfer or Analytics")
val requestType: RequestType,
@LLMDescription("Actual request to be performed by the banking application")
val userRequest: String
) : SerializableSubgraphResult<ClassifiedBankRequest> {
override fun getSerializer() = serializer()
}
Shared tool registry
import ai.koog.agents.ext.agent.ProvideStringSubgraphResult
// Create a comprehensive tool registry for the multi-agent system
val toolRegistry = ToolRegistry {
tool(AskUser) // Allow agents to ask for clarification
tools(MoneyTransferTools().asTools())
tools(TransactionAnalysisTools().asTools())
tool(ProvideStringSubgraphResult)
}
Agent Strategy
Now we'll create a strategy that orchestrates multiple nodes:
import ai.koog.agents.core.dsl.builder.forwardTo
import ai.koog.agents.core.dsl.builder.strategy
import ai.koog.agents.core.dsl.extension.*
import ai.koog.agents.ext.agent.subgraphWithTask
import ai.koog.prompt.structure.StructureFixingParser
val strategy = strategy<String, String>("banking assistant") {
// Subgraph for classifying user requests
val classifyRequest by subgraph<String, ClassifiedBankRequest>(
tools = listOf(AskUser)
) {
// Use structured output to ensure proper classification
val requestClassification by nodeLLMRequestStructured<ClassifiedBankRequest>(
examples = listOf(
ClassifiedBankRequest(
requestType = RequestType.Transfer,
userRequest = "Send 25 euros to Daniel for dinner at the restaurant."
),
ClassifiedBankRequest(
requestType = RequestType.Analytics,
userRequest = "Provide transaction overview for the last month"
)
),
fixingParser = StructureFixingParser(
fixingModel = OpenAIModels.CostOptimized.GPT4oMini,
retries = 2,
)
)
val callLLM by nodeLLMRequest()
val callAskUserTool by nodeExecuteTool()
// Define the flow
edge(nodeStart forwardTo requestClassification)
edge(
requestClassification forwardTo nodeFinish
onCondition { it.isSuccess }
transformed { it.getOrThrow().structure }
)
edge(
requestClassification forwardTo callLLM
onCondition { it.isFailure }
transformed { "Failed to understand the user's intent" }
)
edge(callLLM forwardTo callAskUserTool onToolCall { true })
edge(
callLLM forwardTo callLLM onAssistantMessage { true }
transformed { "Please call `${AskUser.name}` tool instead of chatting" }
)
edge(callAskUserTool forwardTo requestClassification
transformed { it.result.toString() })
}
// Subgraph for handling money transfers
val transferMoney by subgraphWithTask<ClassifiedBankRequest>(
tools = MoneyTransferTools().asTools() + AskUser,
llmModel = OpenAIModels.Chat.GPT4o // Use more capable model for transfers
) { request ->
"""
$bankingAssistantSystemPrompt
Specifically, you need to help with the following request:
${request.userRequest}
""".trimIndent()
}
// Subgraph for transaction analysis
val transactionAnalysis by subgraphWithTask<ClassifiedBankRequest>(
tools = TransactionAnalysisTools().asTools() + AskUser,
) { request ->
"""
$bankingAssistantSystemPrompt
$transactionAnalysisPrompt
Specifically, you need to help with the following request:
${request.userRequest}
""".trimIndent()
}
// Connect the subgraphs
edge(nodeStart forwardTo classifyRequest)
edge(classifyRequest forwardTo transferMoney
onCondition { it.requestType == RequestType.Transfer })
edge(classifyRequest forwardTo transactionAnalysis
onCondition { it.requestType == RequestType.Analytics })
// Route results to finish node
edge(transferMoney forwardTo nodeFinish transformed { it.result })
edge(transactionAnalysis forwardTo nodeFinish transformed { it.result })
}
import ai.koog.agents.core.agent.config.AIAgentConfig
import ai.koog.prompt.dsl.prompt
val agentConfig = AIAgentConfig(
prompt = prompt(id = "banking assistant") {
system("$bankingAssistantSystemPrompt\n$transactionAnalysisPrompt")
},
model = OpenAIModels.Chat.GPT4o,
maxAgentIterations = 50 // Allow for complex multi-step operations
)
val agent = AIAgent<String, String>(
promptExecutor = openAIExecutor,
strategy = strategy,
agentConfig = agentConfig,
toolRegistry = toolRegistry,
)
Run graph agent
println("Banking Assistant started")
val testMessage = "Send 25 euros to Daniel for dinner at the restaurant."
// Test various scenarios:
// Transfer requests:
// - "Send 50 euros to Alice for the concert tickets"
// - "Transfer 100 to Bob for groceries"
// - "What's my current balance?"
//
// Analytics requests:
// - "How much have I spent on restaurants this month?"
// - "What's my maximum check at a restaurant this month?"
// - "How much did I spend on groceries in the first week of May?"
// - "What's my total spending on entertainment in May?"
runBlocking {
val result = agent.run(testMessage)
"Result: $result"
}
Banking Assistant started
I found multiple contacts with the name Daniel. Please choose the correct one:
1. Daniel Anderson (+46 70 123 45 67)
2. Daniel Garcia (+34 612 345 678)
Please specify the number of the correct recipient.
Please confirm if you would like to proceed with sending €25 to Daniel Garcia for "dinner at the restaurant."
Result: Task completed successfully.
Agent Composition — Using Agents as Tools
Koog allows you to use agents as tools within other agents, enabling powerful composition patterns.
import ai.koog.agents.core.agent.asTool
import ai.koog.agents.core.tools.ToolParameterDescriptor
import ai.koog.agents.core.tools.ToolParameterType
val classifierAgent = AIAgent(
executor = openAIExecutor,
llmModel = OpenAIModels.Reasoning.GPT4oMini,
toolRegistry = ToolRegistry {
tool(AskUser)
// Convert agents into tools
tool(
transferAgent.asTool(
agentName = "transferMoney",
agentDescription = "Transfers money and handles all related operations",
inputDescriptor = ToolParameterDescriptor(
name = "request",
description = "Transfer request from the user",
type = ToolParameterType.String
)
)
)
tool(
analysisAgent.asTool(
agentName = "analyzeTransactions",
agentDescription = "Performs analytics on user transactions",
inputDescriptor = ToolParameterDescriptor(
name = "request",
description = "Transaction analytics request",
type = ToolParameterType.String
)
)
)
},
systemPrompt = "$bankingAssistantSystemPrompt\n$transactionAnalysisPrompt"
)
Run composed agent
println("Banking Assistant started")
val composedMessage = "Send 25 euros to Daniel for dinner at the restaurant."
runBlocking {
val result = classifierAgent.run(composedMessage)
"Result: $result"
}
Banking Assistant started
There are two contacts named Daniel. Please confirm which one you would like to send money to:
1. Daniel Anderson (+46 70 123 45 67)
2. Daniel Garcia (+34 612 345 678)
Please confirm the transfer of €25.00 to Daniel Anderson (+46 70 123 45 67) for "Dinner at the restaurant".
Result: Can't perform the task.
Summary
In this tutorial, you've learned how to:
- Create LLM-powered tools with clear descriptions that help the AI understand when and how to use them
- Build single-purpose agents that combine LLMs with tools to accomplish specific tasks
- Implement graph agent using strategies and subgraphs for complex workflows
- Compose agents by using them as tools within other agents
- Handle user interactions including confirmations and disambiguation
Best Practices
- Clear tool descriptions: Write detailed LLMDescription annotations to help the AI understand tool usage
- Idiomatic Kotlin: Use Kotlin features like data classes, extension functions, and scope functions
- Error handling: Always validate inputs and provide meaningful error messages
- User experience: Include confirmation steps for critical operations like money transfers
- Modularity: Separate concerns into different tools and agents for better maintainability