Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions docs/tools-custom/function-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,11 @@ it's None) into the content of the `FunctionResponse` sent back to the LLM.
with the response. For more details on using the Resume feature, see [Resume
stopped agents](/runtime/resume/).

In **Kotlin**, the runner resolves the invocation from the function
response's own call ID, so you do not need to pass `invocationId` to
`runAsync`. A response whose ID matches no function call in the session
throws instead.

??? Tip "Applies to only Java ADK"

When passing `ToolContext` with Function Tools, ensure that one of the
Expand Down Expand Up @@ -679,6 +684,12 @@ it's None) into the content of the `FunctionResponse` sent back to the LLM.
--8<-- "examples/java/snippets/src/main/java/tools/LongRunningFunctionExample.java:full_code"
```

=== "Kotlin"

```kotlin
--8<-- "examples/kotlin/snippets/tools/function-tools/LongRunningTool.kt:call_reimbursement_tool"
```

??? "Python complete example: File Processing Simulation"

```python
Expand All @@ -694,6 +705,15 @@ it's None) into the content of the `FunctionResponse` sent back to the LLM.
incoming FunctionResponse stream (progress vs. completion) for user updates.
- **Final return**: The function returns the final result dictionary, which is
sent in the concluding FunctionResponse to indicate completion.
- **Kotlin has no `LongRunningFunctionTool` class**: Annotate the function with
`@Tool(isLongRunning = true)`, or pass `isLongRunning = true` to a `BaseTool`
subclass.
- **Kotlin turn count**: The tool above returns a value rather than
`Unit`, so non-resumable apps send that placeholder to the model and call it a
second time, ending turn 1 in an interim reply. A resumable app pauses on the
function call with no second model call. Returning `Unit` suppresses the
placeholder response entirely, ending the turn on the function call in either
mode.

## Agent-as-a-Tool {#agent-tool}

Expand Down
81 changes: 81 additions & 0 deletions examples/kotlin/snippets/tools/function-tools/LongRunningTool.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@ import com.google.adk.kt.agents.Instruction
import com.google.adk.kt.agents.LlmAgent
import com.google.adk.kt.annotations.Param
import com.google.adk.kt.annotations.Tool
import com.google.adk.kt.events.Event
import com.google.adk.kt.models.Gemini
import com.google.adk.kt.runners.Runner
import com.google.adk.kt.types.Content
import com.google.adk.kt.types.FunctionResponse
import com.google.adk.kt.types.Part
import com.google.adk.kt.types.Role
import kotlinx.coroutines.flow.toList

// --8<-- [start:long_running_tool]
data class ReimbursementApproval(
Expand Down Expand Up @@ -47,3 +54,77 @@ fun main() {
)
}
// --8<-- [end:long_running_tool]

// --8<-- [start:call_reimbursement_tool]

/**
* Drives the approval from the client side.
*
* `askForApproval` is long running, so its return value is only a `pending`
* placeholder: the real decision arrives out of band and is handed back on a
* later turn as a `FunctionResponse`.
*
* What turn 1 looks like depends on the app. Because this tool returns a value
* rather than `Unit`, a non-resumable app emits the placeholder as a function
* response and calls the model a second time, so the user sees an interim reply
* before the decision exists. A resumable app pauses on the function call
* instead, with no second model call.
*/
suspend fun approveReimbursement(
runner: Runner,
userId: String,
sessionId: String,
) {
val firstTurn =
runner
.runAsync(
userId = userId,
sessionId = sessionId,
newMessage = Content.fromText(Role.USER, "Please reimburse 200 USD for meals."),
).toList()
firstTurn.printText()

// A pending call is one whose id the event also lists in longRunningToolIds.
val pendingCall =
firstTurn.firstNotNullOfOrNull { event ->
event.functionCalls().firstOrNull { it.id != null && it.id in event.longRunningToolIds }
}
if (pendingCall == null) {
println("The model answered without calling the tool; nothing to approve.")
return
}

// The id is not optional bookkeeping: the framework matches the response to
// the original call by id, and a missing or unknown one throws rather than
// degrading. It also tells a resumable app which invocation to resume, so no
// invocationId argument is needed here.
val approval =
Content(
role = Role.USER,
parts =
listOf(
Part(
functionResponse =
FunctionResponse(
name = pendingCall.name,
id = pendingCall.id,
response = mapOf("status" to "approved", "approver" to "Sean Zhou"),
),
),
),
)

runner
.runAsync(
userId = userId,
sessionId = sessionId,
newMessage = approval,
).toList()
.printText()
}

private fun List<Event>.printText() =
forEach { event ->
event.content?.parts?.forEach { part -> part.text?.let(::println) }
}
// --8<-- [end:call_reimbursement_tool]
Loading