Skip to content

Commit afb0a64

Browse files
committed
feat(tools): Standardize request_input tool for proactive LLM clarification
Add RequestInputTool to allow single LLM agents to pause, request user input, and resume seamlessly mid-loop without workflow graphs. Also add comprehensive unit tests, integration tests, and a new official sample with automated session replay tests. Change-Id: Ifb50cc2e644aeb5ffb2be797255608d0d5a37234
1 parent af8bfe0 commit afb0a64

8 files changed

Lines changed: 567 additions & 7 deletions

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Request Input Tool Sample
2+
3+
## Overview
4+
5+
This sample demonstrates how an LLM agent can proactively request clarification or confirmation from the user using the built-in `request_input` tool without losing its context/flow.
6+
7+
It showcases a highly realistic support assistant that dynamically constructs a JSON schema to only ask for missing details when creating IT support tickets.
8+
9+
## Sample Inputs
10+
11+
- `I want to file a technical ticket for a database crash.`
12+
13+
The agent will analyze the prompt, identify that the `title` and `category` are already provided, and dynamically call `request_input` with a schema requesting only `description` and `priority`.
14+
15+
- `File a priority HIGH technical ticket titled database crash explained as the MySQL server throwing OOM errors.`
16+
17+
The agent has all required details and will call `create_support_ticket` immediately without needing clarification.
18+
19+
## Graph
20+
21+
```mermaid
22+
graph TD
23+
User[User Prompt] --> Agent[Support Assistant Agent]
24+
Agent -->|Needs Clarification| RequestInput[request_input tool]
25+
RequestInput -->|User Response| Agent
26+
Agent -->|All Details Gathered| CreateTicket[create_support_ticket tool]
27+
```
28+
29+
## How To
30+
31+
This sample uses **Pattern B: Standalone Agents** with the `request_input` tool:
32+
33+
1. **Import `request_input`**:
34+
35+
```python
36+
from google.adk.tools import request_input
37+
```
38+
39+
1. **Add it to the LLM Agent's tools list**:
40+
41+
```python
42+
root_agent = Agent(
43+
name="support_assistant_agent",
44+
tools=[create_support_ticket, request_input],
45+
...
46+
)
47+
```
48+
49+
When the LLM decides it needs clarification, it calls `request_input` with a question and a dynamic `response_schema`. The ADK framework automatically intercepts this, yields a long-running interrupt to the client, and injects the user's reply back as a `FunctionResponse` into the LLM's chat history.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Copyright 2026 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+
from google.adk import Agent
16+
from google.adk.tools import request_input
17+
from google.genai import types
18+
from pydantic import BaseModel
19+
from pydantic import Field
20+
21+
22+
class SupportTicket(BaseModel):
23+
"""Details of the IT support ticket to be created."""
24+
25+
title: str = Field(description="A brief summary of the issue.")
26+
description: str = Field(description="Detailed explanation of the problem.")
27+
priority: str = Field(
28+
default="MEDIUM",
29+
description="Ticket priority: LOW, MEDIUM, HIGH, or CRITICAL.",
30+
)
31+
category: str = Field(
32+
description=(
33+
"Issue category, e.g., billing, technical, account, or database."
34+
)
35+
)
36+
37+
38+
def create_support_ticket(ticket: SupportTicket) -> dict[str, str]:
39+
"""Create a support ticket in the IT ticketing system."""
40+
return {
41+
"status": "success",
42+
"message": (
43+
f"Successfully created ticket '{ticket.title}'"
44+
f" [Category: {ticket.category}, Priority: {ticket.priority}]."
45+
),
46+
"ticket_id": "INC-98471",
47+
}
48+
49+
50+
root_agent = Agent(
51+
name="support_assistant_agent",
52+
instruction="""
53+
You are a helpful IT support assistant responsible for creating support tickets.
54+
When the user requests to create or file a ticket:
55+
1. Identify which ticket details (title, description, priority, category) are already provided in the conversation.
56+
2. If any mandatory details are missing, call the `request_input` tool.
57+
3. When calling `request_input`, you must construct a dynamic JSON `response_schema` (type: "object") that ONLY requests the missing details, and specify a helpful message explaining what is needed.
58+
4. Once all details are gathered, call `create_support_ticket` with the complete SupportTicket details.
59+
""",
60+
tools=[create_support_ticket, request_input],
61+
generate_content_config=types.GenerateContentConfig(temperature=0.1),
62+
)
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
{
2+
"events": [
3+
{
4+
"author": "user",
5+
"content": {
6+
"parts": [
7+
{
8+
"text": "I want to file a technical ticket for a database crash."
9+
}
10+
],
11+
"role": "user"
12+
},
13+
"id": "e-1",
14+
"invocationId": "i-1",
15+
"nodeInfo": {
16+
"path": ""
17+
}
18+
},
19+
{
20+
"author": "support_assistant_agent",
21+
"content": {
22+
"parts": [
23+
{
24+
"functionCall": {
25+
"args": {
26+
"message": "I can help with that. Please provide the description and priority of the database crash:",
27+
"response_schema": {
28+
"properties": {
29+
"description": {
30+
"description": "Detailed explanation of the database crash problem.",
31+
"title": "Description",
32+
"type": "string"
33+
},
34+
"priority": {
35+
"description": "Ticket priority: LOW, MEDIUM, HIGH, or CRITICAL.",
36+
"title": "Priority",
37+
"type": "string"
38+
}
39+
},
40+
"required": [
41+
"description",
42+
"priority"
43+
],
44+
"title": "TicketClarification",
45+
"type": "object"
46+
}
47+
},
48+
"id": "fc-1",
49+
"name": "adk_request_input"
50+
}
51+
}
52+
],
53+
"role": "model"
54+
},
55+
"finishReason": "STOP",
56+
"id": "e-2",
57+
"invocationId": "i-1",
58+
"longRunningToolIds": [
59+
"fc-1"
60+
],
61+
"nodeInfo": {
62+
"path": "support_assistant_agent@1"
63+
}
64+
},
65+
{
66+
"author": "user",
67+
"content": {
68+
"parts": [
69+
{
70+
"functionResponse": {
71+
"id": "fc-1",
72+
"name": "adk_request_input",
73+
"response": {
74+
"description": "The MySQL server is throwing OOM errors and restarting repeatedly.",
75+
"priority": "HIGH"
76+
}
77+
}
78+
}
79+
],
80+
"role": "user"
81+
},
82+
"id": "e-3",
83+
"invocationId": "i-1",
84+
"nodeInfo": {
85+
"path": ""
86+
}
87+
},
88+
{
89+
"author": "support_assistant_agent",
90+
"content": {
91+
"parts": [
92+
{
93+
"functionCall": {
94+
"args": {
95+
"ticket": {
96+
"category": "database",
97+
"description": "The MySQL server is throwing OOM errors and restarting repeatedly.",
98+
"priority": "HIGH",
99+
"title": "Database crash"
100+
}
101+
},
102+
"id": "fc-2",
103+
"name": "create_support_ticket"
104+
}
105+
}
106+
],
107+
"role": "model"
108+
},
109+
"finishReason": "STOP",
110+
"id": "e-4",
111+
"invocationId": "i-1",
112+
"longRunningToolIds": [],
113+
"nodeInfo": {
114+
"path": "support_assistant_agent@1"
115+
}
116+
},
117+
{
118+
"author": "support_assistant_agent",
119+
"content": {
120+
"parts": [
121+
{
122+
"functionResponse": {
123+
"id": "fc-2",
124+
"name": "create_support_ticket",
125+
"response": {
126+
"message": "Successfully created ticket 'Database crash' [Category: database, Priority: HIGH].",
127+
"status": "success",
128+
"ticket_id": "INC-98471"
129+
}
130+
}
131+
}
132+
],
133+
"role": "user"
134+
},
135+
"id": "e-5",
136+
"invocationId": "i-1",
137+
"nodeInfo": {
138+
"path": "support_assistant_agent@1"
139+
}
140+
},
141+
{
142+
"author": "support_assistant_agent",
143+
"content": {
144+
"parts": [
145+
{
146+
"text": "Successfully created ticket 'Database crash' [Category: database, Priority: HIGH] with ticket ID INC-98471."
147+
}
148+
],
149+
"role": "model"
150+
},
151+
"finishReason": "STOP",
152+
"id": "e-6",
153+
"invocationId": "i-1",
154+
"nodeInfo": {
155+
"path": "support_assistant_agent@1"
156+
}
157+
}
158+
]
159+
}

src/google/adk/flows/llm_flows/contents.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
from .functions import remove_client_function_call_id
3030
from .functions import REQUEST_CONFIRMATION_FUNCTION_CALL_NAME
3131
from .functions import REQUEST_EUC_FUNCTION_CALL_NAME
32-
from .functions import REQUEST_INPUT_FUNCTION_CALL_NAME
3332

3433
logger = logging.getLogger('google_adk.' + __name__)
3534

@@ -397,7 +396,6 @@ def _should_include_event_in_context(
397396
or _is_adk_framework_event(event)
398397
or _is_auth_event(event)
399398
or _is_request_confirmation_event(event)
400-
or _is_request_input_event(event)
401399
)
402400

403401

@@ -927,11 +925,6 @@ def _is_adk_framework_event(event: Event) -> bool:
927925
return _is_function_call_event(event, 'adk_framework')
928926

929927

930-
def _is_request_input_event(event: Event) -> bool:
931-
"""Checks if the event is a request input event."""
932-
return _is_function_call_event(event, REQUEST_INPUT_FUNCTION_CALL_NAME)
933-
934-
935928
def _is_live_model_media_event_with_inline_data(event: Event) -> bool:
936929
"""Check if the event is a live/bidi media event (audio, video, image) with inline data.
937930

src/google/adk/tools/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
# The TYPE_CHECKING block is needed for autocomplete to work.
2121
if TYPE_CHECKING:
2222
from ..auth.auth_tool import AuthToolArguments
23+
from ._request_input_tool import request_input
2324
from .agent_tool import AgentTool
2425
from .api_registry import ApiRegistry
2526
from .apihub_tool.apihub_toolset import APIHubToolset
@@ -80,6 +81,7 @@
8081
'LongRunningFunctionTool',
8182
),
8283
'preload_memory': ('.preload_memory_tool', 'preload_memory_tool'),
84+
'request_input': ('._request_input_tool', 'request_input'),
8385
'ToolContext': ('.tool_context', 'ToolContext'),
8486
'transfer_to_agent': ('.transfer_to_agent_tool', 'transfer_to_agent'),
8587
'TransferToAgentTool': (
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Copyright 2026 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+
from __future__ import annotations
16+
17+
import logging
18+
from typing import Any
19+
from typing import Optional
20+
21+
from google.adk.flows.llm_flows.functions import REQUEST_INPUT_FUNCTION_CALL_NAME
22+
23+
from .long_running_tool import LongRunningFunctionTool
24+
25+
logger = logging.getLogger('google_adk.' + __name__)
26+
27+
28+
def _request_input_func(
29+
message: str,
30+
response_schema: Optional[dict[str, Any]] = None,
31+
) -> None:
32+
"""Ask the user a question and wait for their response.
33+
34+
Use this when you need clarification or additional information before
35+
proceeding.
36+
37+
Args:
38+
message: The question or prompt to display to the user.
39+
response_schema: JSON Schema describing the expected response format. Use
40+
{"type": "string"} for free-text, {"type": "boolean"} for
41+
yes/no, or a structured object schema for complex input.
42+
43+
Returns:
44+
None. Long-running tools return None to signal that the execution should
45+
pause and wait for user input.
46+
"""
47+
logger.info('request_input called with message: %s', message)
48+
# Returning None triggers the long-running tool interruption mechanism.
49+
return None
50+
51+
52+
# Dynamically rename the function to match the workflow interrupt naming space.
53+
# This allows direct instantiation of LongRunningFunctionTool without subclassing,
54+
# keeping RequestInputTool out of the public API.
55+
_request_input_func.__name__ = REQUEST_INPUT_FUNCTION_CALL_NAME
56+
57+
request_input = LongRunningFunctionTool(_request_input_func)

0 commit comments

Comments
 (0)