Files
Lavi Nigam 33b60275d3 fix(tools): Correct async handling in LRO and HTIL sessions (#374)
The LRO (Long-Running Operation) and HTIL (Human-in-the-Loop) tools were failing to execute properly due to an unhandled promise.

The `session_service.create_session` method is asynchronous but was not being awaited, causing race conditions. This change adds the required `await` to ensure proper execution flow.

Additionally, the related documentation has been updated to reflect the corrected code.
2025-06-06 18:21:10 +09:00

12 KiB
Raw Permalink Blame History

Function tools

What are function tools?

When out-of-the-box tools don't fully meet specific requirements, developers can create custom function tools. This allows for tailored functionality, such as connecting to proprietary databases or implementing unique algorithms.

For example, a function tool, "myfinancetool", might be a function that calculates a specific financial metric. ADK also supports long running functions, so if that calculation takes a while, the agent can continue working on other tasks.

ADK offers several ways to create functions tools, each suited to different levels of complexity and control:

  1. Function Tool
  2. Long Running Function Tool
  3. Agents-as-a-Tool

1. Function Tool

Transforming a function into a tool is a straightforward way to integrate custom logic into your agents. In fact, when you assign a function to an agent’s tools list, the framework will automatically wrap it as a Function Tool for you. This approach offers flexibility and quick integration.

Parameters

Define your function parameters using standard JSON-serializable types (e.g., string, integer, list, dictionary). It's important to avoid setting default values for parameters, as the language model (LLM) does not currently support interpreting them.

Return Type

The preferred return type for a Function Tool is a dictionary in Python or Map in Java. This allows you to structure the response with key-value pairs, providing context and clarity to the LLM. If your function returns a type other than a dictionary, the framework automatically wraps it into a dictionary with a single key named "result".

Strive to make your return values as descriptive as possible. For example, instead of returning a numeric error code, return a dictionary with an "error_message" key containing a human-readable explanation. Remember that the LLM, not a piece of code, needs to understand the result. As a best practice, include a "status" key in your return dictionary to indicate the overall outcome (e.g., "success", "error", "pending"), providing the LLM with a clear signal about the operation's state.

Docstring / Source code comments

The docstring (or comments above) your function serve as the tool's description and is sent to the LLM. Therefore, a well-written and comprehensive docstring is crucial for the LLM to understand how to use the tool effectively. Clearly explain the purpose of the function, the meaning of its parameters, and the expected return values.

??? "Example"

=== "Python"

    This tool is a python function which obtains the Stock price of a given Stock ticker/ symbol.

    <u>Note</u>: You need to `pip install yfinance` library before using this tool.

    ```py
    --8<-- "examples/python/snippets/tools/function-tools/func_tool.py"
    ```

    The return value from this tool will be wrapped into a dictionary.

    ```json
    {"result": "$123"}
    ```

=== "Java"

    This tool retrieves the mocked value of a stock price.

    ```java
    --8<-- "examples/java/snippets/src/main/java/tools/StockPriceAgent.java:full_code"
    ```

    The return value from this tool will be wrapped into a Map<String, Object>.

    ```json
    For input `GOOG`: {"symbol": "GOOG", "price": "1.0"}
    ```

Best Practices

While you have considerable flexibility in defining your function, remember that simplicity enhances usability for the LLM. Consider these guidelines:

  • Fewer Parameters are Better: Minimize the number of parameters to reduce complexity.
  • Simple Data Types: Favor primitive data types like str and int over custom classes whenever possible.
  • Meaningful Names: The function's name and parameter names significantly influence how the LLM interprets and utilizes the tool. Choose names that clearly reflect the function's purpose and the meaning of its inputs. Avoid generic names like do_stuff() or beAgent().

2. Long Running Function Tool

Designed for tasks that require a significant amount of processing time without blocking the agent's execution. This tool is a subclass of FunctionTool.

When using a LongRunningFunctionTool, your function can initiate the long-running operation and optionally return an initial result** (e.g. the long-running operation id). Once a long running function tool is invoked the agent runner will pause the agent run and let the agent client to decide whether to continue or wait until the long-running operation finishes. The agent client can query the progress of the long-running operation and send back an intermediate or final response. The agent can then continue with other tasks. An example is the human-in-the-loop scenario where the agent needs human approval before proceeding with a task.

How it Works

In Python, you wrap a function with LongRunningFunctionTool. In Java, you pass a Method name to LongRunningFunctionTool.create().

  1. Initiation: When the LLM calls the tool, your function starts the long-running operation.

  2. Initial Updates: Your function should optionally return an initial result (e.g. the long-running operaiton id). The ADK framework takes the result and sends it back to the LLM packaged within a FunctionResponse. This allows the LLM to inform the user (e.g., status, percentage complete, messages). And then the agent run is ended / paused.

  3. Continue or Wait: After each agent run is completed. Agent client can query the progress of the long-running operation and decide whether to continue the agent run with an intermediate response (to update the progress) or wait until a final response is retrieved. Agent client should send the intermediate or final response back to the agent for the next run.

  4. Framework Handling: The ADK framework manages the execution. It sends the intermediate or final FunctionResponse sent by agent client to the LLM to generate a user friendly message.

Creating the Tool

Define your tool function and wrap it using the LongRunningFunctionTool class:

=== "Python"

```py
--8<-- "examples/python/snippets/tools/function-tools/human_in_the_loop.py:define_long_running_function"
```

=== "Java"

```java
import com.google.adk.agents.LlmAgent;
import com.google.adk.tools.LongRunningFunctionTool;
import java.util.HashMap;
import java.util.Map;

public class ExampleLongRunningFunction {

  // Define your Long Running function.
  // Ask for approval for the reimbursement.
  public static Map<String, Object> askForApproval(String purpose, double amount) {
    // Simulate creating a ticket and sending a notification
    System.out.println(
        "Simulating ticket creation for purpose: " + purpose + ", amount: " + amount);

    // Send a notification to the approver with the link of the ticket
    Map<String, Object> result = new HashMap<>();
    result.put("status", "pending");
    result.put("approver", "Sean Zhou");
    result.put("purpose", purpose);
    result.put("amount", amount);
    result.put("ticket-id", "approval-ticket-1");
    return result;
  }

  public static void main(String[] args) throws NoSuchMethodException {
    // Pass the method to LongRunningFunctionTool.create
    LongRunningFunctionTool approveTool =
        LongRunningFunctionTool.create(ExampleLongRunningFunction.class, "askForApproval");

    // Include the tool in the agent
    LlmAgent approverAgent =
        LlmAgent.builder()
            // ...
            .tools(approveTool)
            .build();
  }
}
```

Intermediate / Final result Updates

Agent client received an event with long running function calls and check the status of the ticket. Then Agent client can send the intermediate or final response back to update the progress. The framework packages this value (even if it's None) into the content of the FunctionResponse sent back to the LLM.

!!! Tip "Applies to only Java ADK"

When passing `ToolContext` with Function Tools, ensure that one of the following is true:

* The Schema is passed with the ToolContext parameter in the function signature, like:
  ```
  @com.google.adk.tools.Annotations.Schema(name = "toolContext") ToolContext toolContext
  ```
OR

* The following `-parameters` flag is set to the mvn compiler plugin

```
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.14.0</version> <!-- or newer -->
            <configuration>
                <compilerArgs>
                    <arg>-parameters</arg>
                </compilerArgs>
            </configuration>
        </plugin>
    </plugins>
</build>
```
This constraint is temporary and will be removed.

=== "Python"

```py
--8<-- "examples/python/snippets/tools/function-tools/human_in_the_loop.py:call_reimbursement_tool"
```

=== "Java"

```java
--8<-- "examples/java/snippets/src/main/java/tools/LongRunningFunctionExample.java:full_code"
```

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

```py
--8<-- "examples/python/snippets/tools/function-tools/human_in_the_loop.py"
```

Key aspects of this example

  • LongRunningFunctionTool: Wraps the supplied method/function; the framework handles sending yielded updates and the final return value as sequential FunctionResponses.

  • Agent instruction: Directs the LLM to use the tool and understand the 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.

3. Agent-as-a-Tool

This powerful feature allows you to leverage the capabilities of other agents within your system by calling them as tools. The Agent-as-a-Tool enables you to invoke another agent to perform a specific task, effectively delegating responsibility. This is conceptually similar to creating a Python function that calls another agent and uses the agent's response as the function's return value.

Key difference from sub-agents

It's important to distinguish an Agent-as-a-Tool from a Sub-Agent.

  • Agent-as-a-Tool: When Agent A calls Agent B as a tool (using Agent-as-a-Tool), Agent B's answer is passed back to Agent A, which then summarizes the answer and generates a response to the user. Agent A retains control and continues to handle future user input.

  • Sub-agent: When Agent A calls Agent B as a sub-agent, the responsibility of answering the user is completely transferred to Agent B. Agent A is effectively out of the loop. All subsequent user input will be answered by Agent B.

Usage

To use an agent as a tool, wrap the agent with the AgentTool class.

=== "Python"

```py
tools=[AgentTool(agent=agent_b)]
```

=== "Java"

```java
AgentTool.create(agent)
```

Customization

The AgentTool class provides the following attributes for customizing its behavior:

  • skip_summarization: bool: If set to True, the framework will bypass the LLM-based summarization of the tool agent's response. This can be useful when the tool's response is already well-formatted and requires no further processing.

??? "Example"

=== "Python"

    ```py
    --8<-- "examples/python/snippets/tools/function-tools/summarizer.py"
    ```

=== "Java"

    ```java
    --8<-- "examples/java/snippets/src/main/java/tools/AgentToolCustomization.java:full_code"
    ```

How it works

  1. When the main_agent receives the long text, its instruction tells it to use the 'summarize' tool for long texts.
  2. The framework recognizes 'summarize' as an AgentTool that wraps the summary_agent.
  3. Behind the scenes, the main_agent will call the summary_agent with the long text as input.
  4. The summary_agent will process the text according to its instruction and generate a summary.
  5. The response from the summary_agent is then passed back to the main_agent.
  6. The main_agent can then take the summary and formulate its final response to the user (e.g., "Here's a summary of the text: ...")