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