Skip to content

Commit e62f756

Browse files
fix: fix long running tool docs (#229)
Co-authored-by: Sean Zhou <[email protected]>
1 parent 35e0021 commit e62f756

File tree

3 files changed

+217
-101
lines changed

3 files changed

+217
-101
lines changed

docs/tools/function-tools.md

Lines changed: 81 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -58,62 +58,111 @@ While you have considerable flexibility in defining your function, remember that
5858

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

61-
When using a `LongRunningFunctionTool`, your Python function can initiate the long-running operation and optionally return an **intermediate result** to keep the model and user informed about the progress. 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.
61+
When using a `LongRunningFunctionTool`, your Python 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.
6262

6363
### How it Works
6464

65-
You wrap a Python *generator* function (a function using `yield`) with `LongRunningFunctionTool`.
65+
You wrap a Python function with LongRunningFunctionTool.
6666

67-
1. **Initiation:** When the LLM calls the tool, your generator function starts executing.
67+
1. **Initiation:** When the LLM calls the tool, your python function starts the long-running operation.
6868

69-
2. **Intermediate Updates (`yield`):** Your function should yield intermediate Python objects (typically dictionaries) periodically to report progress. The ADK framework takes each yielded value 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).
69+
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.
7070

71-
3. **Completion (`return`):** When the task is finished, the generator function uses `return` to provide the final Python object result.
71+
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.
7272

73-
4. **Framework Handling:** The ADK framework manages the execution. It sends each yielded value back as an intermediate `FunctionResponse`. When the generator completes, the framework sends the returned value as the content of the final `FunctionResponse`, signaling the end of the long-running operation to the LLM.
73+
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.
7474

7575
### Creating the Tool
7676

77-
Define your generator function and wrap it using the `LongRunningFunctionTool` class:
77+
Define your tool function and wrap it using the `LongRunningFunctionTool` class:
7878

7979
```py
8080
from google.adk.tools import LongRunningFunctionTool
8181

82-
# Define your generator function (see example below)
83-
def my_long_task_generator(*args, **kwargs):
84-
# ... setup ...
85-
yield {"status": "pending", "message": "Starting task..."} # Framework sends this as FunctionResponse
86-
# ... perform work incrementally ...
87-
yield {"status": "pending", "progress": 50} # Framework sends this as FunctionResponse
88-
# ... finish work ...
89-
return {"status": "completed", "result": "Final outcome"} # Framework sends this as final FunctionResponse
82+
# Define your long running function (see example below)
83+
def ask_for_approval(
84+
purpose: str, amount: float, tool_context: ToolContext
85+
) -> dict[str, Any]:
86+
"""Ask for approval for the reimbursement."""
87+
# create a ticket for the approval
88+
# Send a notification to the approver with the link of the ticket
89+
return {'status': 'pending', 'approver': 'Sean Zhou', 'purpose' : purpose, 'amount': amount, 'ticket-id': 'approval-ticket-1'}
9090

9191
# Wrap the function
92-
my_tool = LongRunningFunctionTool(func=my_long_task_generator)
92+
approve_tool = LongRunningFunctionTool(func=ask_for_approval)
9393
```
9494

95-
### Intermediate Updates
95+
### Intermediate / Final result Updates
9696

97-
Yielding structured Python objects (like dictionaries) is crucial for providing meaningful updates. Include keys like:
97+
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.
9898

99-
* status: e.g., "pending", "running", "waiting_for_input"
100-
101-
* progress: e.g., percentage, steps completed
102-
103-
* message: Descriptive text for the user/LLM
104-
105-
* estimated_completion_time: If calculable
106-
107-
Each value you yield is packaged into a FunctionResponse by the framework and sent to the LLM.
108-
109-
### Final Result
99+
```py
100+
# runner = Runner(...)
101+
# session = session_service.create_session(...)
102+
# content = types.Content(...) # User's initial query
103+
104+
def get_long_running_function_call(event: Event) -> types.FunctionCall:
105+
# Get the long running function call from the event
106+
if not event.long_running_tool_ids or not event.content or not event.content.parts:
107+
return
108+
for part in event.content.parts:
109+
if (
110+
part
111+
and part.function_call
112+
and event.long_running_tool_ids
113+
and part.function_call.id in event.long_running_tool_ids
114+
):
115+
return part.function_call
116+
117+
def get_function_response(event: Event, function_call_id: str) -> types.FunctionResponse:
118+
# Get the function response for the fuction call with specified id.
119+
if not event.content or not event.content.parts:
120+
return
121+
for part in event.content.parts:
122+
if (
123+
part
124+
and part.function_response
125+
and part.function_response.id == function_call_id
126+
):
127+
return part.function_response
128+
129+
print("\nRunning agent...")
130+
events_async = runner.run_async(
131+
session_id=session.id, user_id='user', new_message=content
132+
)
133+
134+
135+
long_running_function_call, long_running_function_response, ticket_id = None, None, None
136+
async for event in events_async:
137+
# Use helper to check for the specific auth request event
138+
if not long_running_function_call:
139+
long_running_function_call = get_long_running_function_call(event)
140+
else:
141+
long_running_function_response = get_function_response(event, long_running_function_call.id)
142+
if long_running_function_response:
143+
ticket_id = long_running_function_response.response['ticket_id']
144+
if event.content and event.content.parts:
145+
if text := ''.join(part.text or '' for part in event.content.parts):
146+
print(f'[{event.author}]: {text}')
147+
148+
if long_running_function_response:
149+
# query the status of the correpsonding ticket via tciket_id
150+
# send back an intermediate / final response
151+
updated_response = long_running_function_response.model_copy(deep=True)
152+
updated_response.response = {'status': 'approved'}
153+
async for event in runner.run_async(
154+
session_id=session.id, user_id='user', new_message=types.Content(parts=[types.Part(function_response = updated_response)], role='user')
155+
):
156+
if event.content and event.content.parts:
157+
if text := ''.join(part.text or '' for part in event.content.parts):
158+
print(f'[{event.author}]: {text}')
159+
```
110160

111-
The Python object your generator function returns is considered the final result of the tool execution. The framework packages this value (even if it's None) into the content of the final `FunctionResponse` sent back to the LLM, indicating the tool execution is complete.
112161

113162
??? "Example: File Processing Simulation"
114163

115164
```py
116-
--8<-- "examples/python/snippets/tools/function-tools/file_processor.py"
165+
--8<-- "examples/python/snippets/tools/function-tools/human_in_the_loop.py"
117166
```
118167

119168
#### Key aspects of this example
@@ -166,3 +215,4 @@ The `AgentTool` class provides the following attributes for customizing its beha
166215
4. The `summary_agent` will process the text according to its instruction and generate a summary.
167216
5. **The response from the `summary_agent` is then passed back to the `main_agent`.**
168217
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: ...")
218+

examples/python/snippets/tools/function-tools/file_processor.py

Lines changed: 0 additions & 70 deletions
This file was deleted.
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import asyncio
16+
from typing import Any
17+
from google.adk.agents import Agent
18+
from google.adk.events import Event
19+
from google.adk.runners import Runner
20+
from google.adk.tools import LongRunningFunctionTool
21+
from google.adk.sessions import InMemorySessionService
22+
from google.genai import types
23+
24+
# 1. Define the long running function
25+
def ask_for_approval(
26+
purpose: str, amount: float
27+
) -> dict[str, Any]:
28+
"""Ask for approval for the reimbursement."""
29+
# create a ticket for the approval
30+
# Send a notification to the approver with the link of the ticket
31+
return {'status': 'pending', 'approver': 'Sean Zhou', 'purpose' : purpose, 'amount': amount, 'ticket-id': 'approval-ticket-1'}
32+
33+
def reimburse(purpose: str, amount: float) -> str:
34+
"""Reimburse the amount of money to the employee."""
35+
# send the reimbrusement request to payment vendor
36+
return {'status': 'ok'}
37+
38+
# 2. Wrap the function with LongRunningFunctionTool
39+
long_running_tool = LongRunningFunctionTool(func=ask_for_approval)
40+
41+
# 3. Use the tool in an Agent
42+
file_processor_agent = Agent(
43+
# Use a model compatible with function calling
44+
model="gemini-2.0-flash",
45+
name='reimbursement_agent',
46+
instruction="""
47+
You are an agent whose job is to handle the reimbursement process for
48+
the employees. If the amount is less than $100, you will automatically
49+
approve the reimbursement.
50+
51+
If the amount is greater than $100, you will
52+
ask for approval from the manager. If the manager approves, you will
53+
call reimburse() to reimburse the amount to the employee. If the manager
54+
rejects, you will inform the employee of the rejection.
55+
""",
56+
tools=[reimburse, long_running_tool]
57+
)
58+
59+
60+
APP_NAME = "human_in_the_loop"
61+
USER_ID = "1234"
62+
SESSION_ID = "session1234"
63+
64+
# Session and Runner
65+
session_service = InMemorySessionService()
66+
session = session_service.create_session(app_name=APP_NAME, user_id=USER_ID, session_id=SESSION_ID)
67+
runner = Runner(agent=file_processor_agent, app_name=APP_NAME, session_service=session_service)
68+
69+
70+
# Agent Interaction
71+
async def call_agent(query):
72+
73+
def get_long_running_function_call(event: Event) -> types.FunctionCall:
74+
# Get the long running function call from the event
75+
if not event.long_running_tool_ids or not event.content or not event.content.parts:
76+
return
77+
for part in event.content.parts:
78+
if (
79+
part
80+
and part.function_call
81+
and event.long_running_tool_ids
82+
and part.function_call.id in event.long_running_tool_ids
83+
):
84+
return part.function_call
85+
86+
def get_function_response(event: Event, function_call_id: str) -> types.FunctionResponse:
87+
# Get the function response for the fuction call with specified id.
88+
if not event.content or not event.content.parts:
89+
return
90+
for part in event.content.parts:
91+
if (
92+
part
93+
and part.function_response
94+
and part.function_response.id == function_call_id
95+
):
96+
return part.function_response
97+
98+
content = types.Content(role='user', parts=[types.Part(text=query)])
99+
events = runner.run_async(user_id=USER_ID, session_id=SESSION_ID, new_message=content)
100+
101+
print("\nRunning agent...")
102+
events_async = runner.run_async(
103+
session_id=session.id, user_id='user', new_message=content
104+
)
105+
106+
107+
long_running_function_call, long_running_function_response, ticket_id = None, None, None
108+
async for event in events_async:
109+
# Use helper to check for the specific auth request event
110+
if not long_running_function_call:
111+
long_running_function_call = get_long_running_function_call(event)
112+
else:
113+
long_running_function_response = get_function_response(event, long_running_function_call.id)
114+
if long_running_function_response:
115+
ticket_id = long_running_function_response.response['ticket_id']
116+
if event.content and event.content.parts:
117+
if text := ''.join(part.text or '' for part in event.content.parts):
118+
print(f'[{event.author}]: {text}')
119+
120+
121+
if long_running_function_response:
122+
# query the status of the correpsonding ticket via tciket_id
123+
# send back an intermediate / final response
124+
updated_response = long_running_function_response.model_copy(deep=True)
125+
updated_response.response = {'status': 'approved'}
126+
async for event in runner.run_async(
127+
session_id=session.id, user_id='user', new_message=types.Content(parts=[types.Part(function_response = updated_response)], role='user')
128+
):
129+
if event.content and event.content.parts:
130+
if text := ''.join(part.text or '' for part in event.content.parts):
131+
print(f'[{event.author}]: {text}')
132+
133+
# reimbursement that doesn't require approval
134+
asyncio.run(call_agent("Please reimburse 50$ for meals"))
135+
# reimbursement that requires approval
136+
asyncio.run(call_agent("Please reimburse 200$ for meals"))

0 commit comments

Comments
 (0)