chore: import upstream snapshot with attribution
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
# Agent with Long-Running Tools
|
||||
|
||||
This example demonstrates an agent using a long-running tool (`ask_for_approval`).
|
||||
|
||||
## Key Flow for Long-Running Tools
|
||||
|
||||
1. **Initial Call**: The agent calls the long-running tool (e.g., `ask_for_approval`).
|
||||
|
||||
1. **Initial Tool Response**: The tool immediately returns an initial response, typically indicating a "pending" status and a way to track the request (e.g., a `ticket-id`). This is sent back to the agent as a `types.FunctionResponse` (usually processed internally by the runner and then influencing the agent's next turn).
|
||||
|
||||
1. **Agent Acknowledges**: The agent processes this initial response and usually informs the user about the pending status.
|
||||
|
||||
1. **External Process/Update**: The long-running task progresses externally (e.g., a human approves the request).
|
||||
|
||||
1. **❗️Crucial Step: Provide Updated Tool Response❗️**:
|
||||
|
||||
- Once the external process completes or updates, your application **must** construct a new `types.FunctionResponse`.
|
||||
- This response should use the **same `id` and `name`** as the original `FunctionCall` to the long-running tool.
|
||||
- The `response` field within this `types.FunctionResponse` should contain the *updated data* (e.g., `{'status': 'approved', ...}`).
|
||||
- Send this `types.FunctionResponse` back to the agent as a part within a new message using `role="user"`.
|
||||
|
||||
```python
|
||||
# Example: After external approval
|
||||
updated_tool_output_data = {
|
||||
"status": "approved",
|
||||
"ticketId": ticket_id, # from original call
|
||||
# ... other relevant updated data
|
||||
}
|
||||
|
||||
updated_function_response_part = types.Part(
|
||||
function_response=types.FunctionResponse(
|
||||
id=long_running_function_call.id, # Original call ID
|
||||
name=long_running_function_call.name, # Original call name
|
||||
response=updated_tool_output_data,
|
||||
)
|
||||
)
|
||||
|
||||
# Send this back to the agent
|
||||
async for _ in runner.run_async(
|
||||
# ... session_id, user_id ...
|
||||
new_message=types.Content(
|
||||
parts=[updated_function_response_part], role="user"
|
||||
),
|
||||
):
|
||||
pass # exhaust generator (or handle events)
|
||||
```
|
||||
|
||||
1. **Agent Acts on Update**: The agent receives this message containing the `types.FunctionResponse` and, based on its instructions, proceeds with the next steps (e.g., calling another tool like `reimburse`).
|
||||
|
||||
**Why is this important?** The agent relies on receiving this subsequent `types.FunctionResponse` (provided in a message with `role="user"` containing the specific `Part`) to understand that the long-running task has concluded or its state has changed. Without it, the agent will remain unaware of the outcome of the pending task.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from . import agent
|
||||
@@ -0,0 +1,55 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from typing import Any
|
||||
|
||||
from google.adk import Agent
|
||||
from google.adk.tools.long_running_tool import LongRunningFunctionTool
|
||||
from google.adk.tools.tool_context import ToolContext
|
||||
from google.genai import types
|
||||
|
||||
|
||||
def reimburse(purpose: str, amount: float) -> str:
|
||||
"""Reimburse the amount of money to the employee."""
|
||||
return {
|
||||
'status': 'ok',
|
||||
}
|
||||
|
||||
|
||||
def ask_for_approval(
|
||||
purpose: str, amount: float, tool_context: ToolContext
|
||||
) -> dict[str, Any]:
|
||||
"""Ask for approval for the reimbursement."""
|
||||
return {
|
||||
'status': 'pending',
|
||||
'amount': amount,
|
||||
'ticketId': 'reimbursement-ticket-001',
|
||||
}
|
||||
|
||||
|
||||
root_agent = Agent(
|
||||
name='reimbursement_agent',
|
||||
instruction="""
|
||||
You are an agent whose job is to handle the reimbursement process for
|
||||
the employees. If the amount is less than $100, you will automatically
|
||||
approve the reimbursement.
|
||||
|
||||
If the amount is greater than $100, you will
|
||||
ask for approval from the manager. If the manager approves, you will
|
||||
call reimburse() to reimburse the amount to the employee. If the manager
|
||||
rejects, you will inform the employee of the rejection.
|
||||
""",
|
||||
tools=[reimburse, LongRunningFunctionTool(func=ask_for_approval)],
|
||||
generate_content_config=types.GenerateContentConfig(temperature=0.1),
|
||||
)
|
||||
@@ -0,0 +1,192 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from typing import Any
|
||||
from typing import Union
|
||||
|
||||
import agent
|
||||
from dotenv import load_dotenv
|
||||
from google.adk.agents.llm_agent import Agent
|
||||
from google.adk.events.event import Event
|
||||
from google.adk.runners import Runner
|
||||
from google.adk.sessions.in_memory_session_service import InMemorySessionService
|
||||
from google.adk.tools.long_running_tool import LongRunningFunctionTool
|
||||
from google.genai import types
|
||||
from opentelemetry import trace
|
||||
from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter
|
||||
from opentelemetry.sdk.trace import export
|
||||
from opentelemetry.sdk.trace import TracerProvider
|
||||
|
||||
load_dotenv(override=True)
|
||||
|
||||
APP_NAME = "human_in_the_loop"
|
||||
USER_ID = "1234"
|
||||
SESSION_ID = "session1234"
|
||||
|
||||
session_service = InMemorySessionService()
|
||||
|
||||
|
||||
async def main():
|
||||
session = await session_service.create_session(
|
||||
app_name=APP_NAME, user_id=USER_ID, session_id=SESSION_ID
|
||||
)
|
||||
runner = Runner(
|
||||
agent=agent.root_agent,
|
||||
app_name=APP_NAME,
|
||||
session_service=session_service,
|
||||
)
|
||||
|
||||
async def call_agent(query: str):
|
||||
content = types.Content(role="user", parts=[types.Part(text=query)])
|
||||
|
||||
print(f'>>> User Query: "{query}"')
|
||||
print("--- Running agent's initial turn ---")
|
||||
|
||||
events_async = runner.run_async(
|
||||
session_id=session.id, user_id=USER_ID, new_message=content
|
||||
)
|
||||
|
||||
long_running_function_call: Union[types.FunctionCall, None] = None
|
||||
initial_tool_response: Union[types.FunctionResponse, None] = None
|
||||
ticket_id: Union[str, None] = None
|
||||
|
||||
async for event in events_async:
|
||||
if event.content and event.content.parts:
|
||||
for i, part in enumerate(event.content.parts):
|
||||
if part.text:
|
||||
print(f" Part {i} [Text]: {part.text.strip()}")
|
||||
if part.function_call:
|
||||
print(
|
||||
f" Part {i} [FunctionCall]:"
|
||||
f" {part.function_call.name}({part.function_call.args}) ID:"
|
||||
f" {part.function_call.id}"
|
||||
)
|
||||
if not long_running_function_call and part.function_call.id in (
|
||||
event.long_running_tool_ids or []
|
||||
):
|
||||
long_running_function_call = part.function_call
|
||||
print(
|
||||
" (Captured as long_running_function_call for"
|
||||
f" '{part.function_call.name}')"
|
||||
)
|
||||
if part.function_response:
|
||||
print(
|
||||
f" Part {i} [FunctionResponse]: For"
|
||||
f" '{part.function_response.name}', ID:"
|
||||
f" {part.function_response.id}, Response:"
|
||||
f" {part.function_response.response}"
|
||||
)
|
||||
if (
|
||||
long_running_function_call
|
||||
and part.function_response.id == long_running_function_call.id
|
||||
):
|
||||
initial_tool_response = part.function_response
|
||||
if initial_tool_response.response:
|
||||
ticket_id = initial_tool_response.response.get("ticketId")
|
||||
print(
|
||||
" (Captured as initial_tool_response for"
|
||||
f" '{part.function_response.name}', Ticket ID: {ticket_id})"
|
||||
)
|
||||
|
||||
print("--- End of agent's initial turn ---\n")
|
||||
|
||||
if (
|
||||
long_running_function_call
|
||||
and initial_tool_response
|
||||
and initial_tool_response.response.get("status") == "pending"
|
||||
):
|
||||
print(f"--- Simulating external approval for ticket: {ticket_id} ---\n")
|
||||
|
||||
updated_tool_output_data = {
|
||||
"status": "approved",
|
||||
"ticketId": ticket_id,
|
||||
"approver_feedback": (
|
||||
"Approved by manager at " + str(asyncio.get_event_loop().time())
|
||||
),
|
||||
}
|
||||
|
||||
updated_function_response_part = types.Part(
|
||||
function_response=types.FunctionResponse(
|
||||
id=long_running_function_call.id,
|
||||
name=long_running_function_call.name,
|
||||
response=updated_tool_output_data,
|
||||
)
|
||||
)
|
||||
|
||||
print(
|
||||
"--- Sending updated tool result to agent for call ID"
|
||||
f" {long_running_function_call.id}: {updated_tool_output_data} ---"
|
||||
)
|
||||
print("--- Running agent's turn AFTER receiving updated tool result ---")
|
||||
|
||||
async for event in runner.run_async(
|
||||
session_id=session.id,
|
||||
user_id=USER_ID,
|
||||
new_message=types.Content(
|
||||
parts=[updated_function_response_part], role="user"
|
||||
),
|
||||
):
|
||||
if event.content and event.content.parts:
|
||||
for i, part in enumerate(event.content.parts):
|
||||
if part.text:
|
||||
print(f" Part {i} [Text]: {part.text.strip()}")
|
||||
if part.function_call:
|
||||
print(
|
||||
f" Part {i} [FunctionCall]:"
|
||||
f" {part.function_call.name}({part.function_call.args}) ID:"
|
||||
f" {part.function_call.id}"
|
||||
)
|
||||
if part.function_response:
|
||||
print(
|
||||
f" Part {i} [FunctionResponse]: For"
|
||||
f" '{part.function_response.name}', ID:"
|
||||
f" {part.function_response.id}, Response:"
|
||||
f" {part.function_response.response}"
|
||||
)
|
||||
print("--- End of agent's turn AFTER receiving updated tool result ---")
|
||||
|
||||
elif long_running_function_call and not initial_tool_response:
|
||||
print(
|
||||
f"--- Long running function '{long_running_function_call.name}' was"
|
||||
" called, but its initial response was not captured. ---"
|
||||
)
|
||||
elif not long_running_function_call:
|
||||
print(
|
||||
"--- No long running function call was detected in the initial"
|
||||
" turn. ---"
|
||||
)
|
||||
|
||||
await call_agent("Please reimburse $50 for meals")
|
||||
print("=" * 70)
|
||||
await call_agent("Please reimburse $200 for conference travel")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
provider = TracerProvider()
|
||||
project_id = os.environ.get("GOOGLE_CLOUD_PROJECT")
|
||||
if not project_id:
|
||||
raise ValueError("GOOGLE_CLOUD_PROJECT environment variable is not set.")
|
||||
print("Tracing to project", project_id)
|
||||
processor = export.BatchSpanProcessor(
|
||||
CloudTraceSpanExporter(project_id=project_id)
|
||||
)
|
||||
provider.add_span_processor(processor)
|
||||
trace.set_tracer_provider(provider)
|
||||
|
||||
asyncio.run(main())
|
||||
|
||||
provider.force_flush()
|
||||
print("Done tracing to project", project_id)
|
||||
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"events": [
|
||||
{
|
||||
"author": "user",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "Can I get a reimbursement of $15.50 for some coffee?"
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-1",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "reimbursement_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionCall": {
|
||||
"args": {
|
||||
"amount": 15.5,
|
||||
"purpose": "coffee"
|
||||
},
|
||||
"id": "fc-1",
|
||||
"name": "reimburse"
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-2",
|
||||
"invocationId": "i-1",
|
||||
"longRunningToolIds": [],
|
||||
"nodeInfo": {
|
||||
"path": "reimbursement_agent@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "reimbursement_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-1",
|
||||
"name": "reimburse",
|
||||
"response": {
|
||||
"status": "ok"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-3",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "reimbursement_agent@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "reimbursement_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "You got it! I've reimbursed you $15.50 for coffee."
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-4",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "reimbursement_agent@1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"events": [
|
||||
{
|
||||
"author": "user",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "I would like to request a reimbursement of $150 for a client dinner."
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-1",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "reimbursement_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionCall": {
|
||||
"args": {
|
||||
"amount": 150,
|
||||
"purpose": "client dinner"
|
||||
},
|
||||
"id": "fc-1",
|
||||
"name": "ask_for_approval"
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-2",
|
||||
"invocationId": "i-1",
|
||||
"longRunningToolIds": [
|
||||
"fc-1"
|
||||
],
|
||||
"nodeInfo": {
|
||||
"path": "reimbursement_agent@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "reimbursement_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-1",
|
||||
"name": "ask_for_approval",
|
||||
"response": {
|
||||
"amount": 150,
|
||||
"status": "pending",
|
||||
"ticketId": "reimbursement-ticket-001"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-3",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "reimbursement_agent@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "reimbursement_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "Your reimbursement request for $150 for a client dinner has been sent for approval. Your ticket ID is reimbursement-ticket-001. I will let you know once I have an update."
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-4",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "reimbursement_agent@1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from . import agent
|
||||
@@ -0,0 +1,100 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from google.adk import Agent
|
||||
from google.adk.apps import App
|
||||
from google.adk.apps import ResumabilityConfig
|
||||
from google.adk.tools.function_tool import FunctionTool
|
||||
from google.adk.tools.tool_confirmation import ToolConfirmation
|
||||
from google.adk.tools.tool_context import ToolContext
|
||||
from google.genai import types
|
||||
|
||||
|
||||
def reimburse(amount: int, tool_context: ToolContext) -> str:
|
||||
"""Reimburse the employee for the given amount."""
|
||||
return {'status': 'ok'}
|
||||
|
||||
|
||||
async def confirmation_threshold(
|
||||
amount: int, tool_context: ToolContext
|
||||
) -> bool:
|
||||
"""Returns true if the amount is greater than 1000."""
|
||||
return amount > 1000
|
||||
|
||||
|
||||
def request_time_off(days: int, tool_context: ToolContext):
|
||||
"""Request day off for the employee."""
|
||||
if days <= 0:
|
||||
return {'status': 'Invalid days to request.'}
|
||||
|
||||
if days <= 2:
|
||||
return {
|
||||
'status': 'ok',
|
||||
'approved_days': days,
|
||||
}
|
||||
|
||||
tool_confirmation = tool_context.tool_confirmation
|
||||
if not tool_confirmation:
|
||||
tool_context.request_confirmation(
|
||||
hint=(
|
||||
'Please approve or reject the tool call request_time_off() by'
|
||||
' responding with a FunctionResponse with an expected'
|
||||
' ToolConfirmation payload.'
|
||||
),
|
||||
payload={
|
||||
'approved_days': 0,
|
||||
},
|
||||
)
|
||||
return {'status': 'Manager approval is required.'}
|
||||
|
||||
approved_days = tool_confirmation.payload['approved_days']
|
||||
approved_days = min(approved_days, days)
|
||||
if approved_days == 0:
|
||||
return {'status': 'The time off request is rejected.', 'approved_days': 0}
|
||||
return {
|
||||
'status': 'ok',
|
||||
'approved_days': approved_days,
|
||||
}
|
||||
|
||||
|
||||
root_agent = Agent(
|
||||
name='time_off_agent',
|
||||
instruction="""
|
||||
You are a helpful assistant that can help employees with reimbursement and time off requests.
|
||||
- Use the `reimburse` tool for reimbursement requests.
|
||||
- Use the `request_time_off` tool for time off requests.
|
||||
- Prioritize using tools to fulfill the user's request.
|
||||
- Always respond to the user with the tool results.
|
||||
""",
|
||||
tools=[
|
||||
# Set require_confirmation to True or a callable to require user
|
||||
# confirmation for the tool call. This is an easier way to get user
|
||||
# confirmation if the tool just need a boolean confirmation.
|
||||
FunctionTool(
|
||||
reimburse,
|
||||
require_confirmation=confirmation_threshold,
|
||||
),
|
||||
request_time_off,
|
||||
],
|
||||
generate_content_config=types.GenerateContentConfig(temperature=0.1),
|
||||
)
|
||||
|
||||
app = App(
|
||||
name='human_tool_confirmation',
|
||||
root_agent=root_agent,
|
||||
# Set the resumability config to enable resumability.
|
||||
resumability_config=ResumabilityConfig(
|
||||
is_resumable=True,
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,49 @@
|
||||
# Request Input Tool Sample
|
||||
|
||||
## Overview
|
||||
|
||||
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.
|
||||
|
||||
It showcases a highly realistic support assistant that dynamically constructs a JSON schema to only ask for missing details when creating IT support tickets.
|
||||
|
||||
## Sample Inputs
|
||||
|
||||
- `I want to file a technical ticket for a database crash.`
|
||||
|
||||
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`.
|
||||
|
||||
- `File a priority HIGH technical ticket titled database crash explained as the MySQL server throwing OOM errors.`
|
||||
|
||||
The agent has all required details and will call `create_support_ticket` immediately without needing clarification.
|
||||
|
||||
## Graph
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
User[User Prompt] --> Agent[Support Assistant Agent]
|
||||
Agent -->|Needs Clarification| RequestInput[request_input tool]
|
||||
RequestInput -->|User Response| Agent
|
||||
Agent -->|All Details Gathered| CreateTicket[create_support_ticket tool]
|
||||
```
|
||||
|
||||
## How To
|
||||
|
||||
This sample uses **Pattern B: Standalone Agents** with the `request_input` tool:
|
||||
|
||||
1. **Import `request_input`**:
|
||||
|
||||
```python
|
||||
from google.adk.tools import request_input
|
||||
```
|
||||
|
||||
1. **Add it to the LLM Agent's tools list**:
|
||||
|
||||
```python
|
||||
root_agent = Agent(
|
||||
name="support_assistant_agent",
|
||||
tools=[create_support_ticket, request_input],
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,62 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from google.adk import Agent
|
||||
from google.adk.tools import request_input
|
||||
from google.genai import types
|
||||
from pydantic import BaseModel
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class SupportTicket(BaseModel):
|
||||
"""Details of the IT support ticket to be created."""
|
||||
|
||||
title: str = Field(description="A brief summary of the issue.")
|
||||
description: str = Field(description="Detailed explanation of the problem.")
|
||||
priority: str = Field(
|
||||
default="MEDIUM",
|
||||
description="Ticket priority: LOW, MEDIUM, HIGH, or CRITICAL.",
|
||||
)
|
||||
category: str = Field(
|
||||
description=(
|
||||
"Issue category, e.g., billing, technical, account, or database."
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_support_ticket(ticket: SupportTicket) -> dict[str, str]:
|
||||
"""Create a support ticket in the IT ticketing system."""
|
||||
return {
|
||||
"status": "success",
|
||||
"message": (
|
||||
f"Successfully created ticket '{ticket.title}'"
|
||||
f" [Category: {ticket.category}, Priority: {ticket.priority}]."
|
||||
),
|
||||
"ticket_id": "INC-98471",
|
||||
}
|
||||
|
||||
|
||||
root_agent = Agent(
|
||||
name="support_assistant_agent",
|
||||
instruction="""
|
||||
You are a helpful IT support assistant responsible for creating support tickets.
|
||||
When the user requests to create or file a ticket:
|
||||
1. Identify which ticket details (title, description, priority, category) are already provided in the conversation.
|
||||
2. If any mandatory details are missing, call the `request_input` tool.
|
||||
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.
|
||||
4. Once all details are gathered, call `create_support_ticket` with the complete SupportTicket details.
|
||||
""",
|
||||
tools=[create_support_ticket, request_input],
|
||||
generate_content_config=types.GenerateContentConfig(temperature=0.1),
|
||||
)
|
||||
@@ -0,0 +1,159 @@
|
||||
{
|
||||
"events": [
|
||||
{
|
||||
"author": "user",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "I want to file a technical ticket for a database crash."
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-1",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "support_assistant_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionCall": {
|
||||
"args": {
|
||||
"message": "I can help with that. Please provide the description and priority of the database crash:",
|
||||
"response_schema": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"description": "Detailed explanation of the database crash problem.",
|
||||
"title": "Description",
|
||||
"type": "string"
|
||||
},
|
||||
"priority": {
|
||||
"description": "Ticket priority: LOW, MEDIUM, HIGH, or CRITICAL.",
|
||||
"title": "Priority",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"description",
|
||||
"priority"
|
||||
],
|
||||
"title": "TicketClarification",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"id": "fc-1",
|
||||
"name": "adk_request_input"
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-2",
|
||||
"invocationId": "i-1",
|
||||
"longRunningToolIds": [
|
||||
"fc-1"
|
||||
],
|
||||
"nodeInfo": {
|
||||
"path": "support_assistant_agent@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "user",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-1",
|
||||
"name": "adk_request_input",
|
||||
"response": {
|
||||
"description": "The MySQL server is throwing OOM errors and restarting repeatedly.",
|
||||
"priority": "HIGH"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-3",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "support_assistant_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionCall": {
|
||||
"args": {
|
||||
"ticket": {
|
||||
"category": "database",
|
||||
"description": "The MySQL server is throwing OOM errors and restarting repeatedly.",
|
||||
"priority": "HIGH",
|
||||
"title": "Database crash"
|
||||
}
|
||||
},
|
||||
"id": "fc-2",
|
||||
"name": "create_support_ticket"
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-4",
|
||||
"invocationId": "i-1",
|
||||
"longRunningToolIds": [],
|
||||
"nodeInfo": {
|
||||
"path": "support_assistant_agent@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "support_assistant_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-2",
|
||||
"name": "create_support_ticket",
|
||||
"response": {
|
||||
"message": "Successfully created ticket 'Database crash' [Category: database, Priority: HIGH].",
|
||||
"status": "success",
|
||||
"ticket_id": "INC-98471"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-5",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "support_assistant_agent@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "support_assistant_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "Successfully created ticket 'Database crash' [Category: database, Priority: HIGH] with ticket ID INC-98471."
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-6",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "support_assistant_agent@1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
# Tool Confirmation Sample
|
||||
|
||||
## Overview
|
||||
|
||||
This sample demonstrates how to use the Tool Confirmation feature in ADK to implement Human-in-the-Loop (HITL) flows. It shows how a tool can dynamically request confirmation from the user before proceeding with a sensitive action (e.g., transferring funds).
|
||||
|
||||
## Sample Inputs
|
||||
|
||||
- `Transfer $50 to Alice`
|
||||
|
||||
- `Transfer $200 to Bob`
|
||||
|
||||
- `Close account ACC123`
|
||||
|
||||
- `Transfer $500 to Charlie and close account ACC123`
|
||||
|
||||
*This will cause parallel tools being called in a single step*
|
||||
|
||||
## How To
|
||||
|
||||
### 1. Requesting Confirmation
|
||||
|
||||
In your tool function, you can access the `ToolContext` and check if `tool_confirmation` is present. If not, you can call `tool_context.request_confirmation()` to request approval from the user.
|
||||
|
||||
```python
|
||||
def transfer_funds(amount: float, recipient: str, tool_context: ToolContext):
|
||||
# Only request confirmation for amounts >= 100
|
||||
if amount >= 100:
|
||||
if not tool_context.tool_confirmation:
|
||||
tool_context.request_confirmation(
|
||||
hint=f"Confirm transfer of ${amount} to {recipient}.",
|
||||
)
|
||||
return {"error": "This tool call requires confirmation, please approve or reject."}
|
||||
```
|
||||
|
||||
### 2. Handling the Response
|
||||
|
||||
When the user responds to the confirmation request, the tool will be called again. This time, `tool_context.tool_confirmation` will be populated with the user's decision (`confirmed` boolean).
|
||||
|
||||
```python
|
||||
elif not tool_context.tool_confirmation.confirmed:
|
||||
return {"error": "Transfer rejected by user."}
|
||||
|
||||
return {"result": f"Successfully transferred ${amount} to {recipient}."}
|
||||
```
|
||||
|
||||
### 3. Using `FunctionTool` for Automatic Confirmation
|
||||
|
||||
Alternatively, you can specify that a tool always requires confirmation by wrapping it in a `FunctionTool` and setting `require_confirmation=True` when defining the agent's tools. In this case, the runner will automatically handle the confirmation request before calling your function.
|
||||
|
||||
```python
|
||||
from google.adk.tools.function_tool import FunctionTool
|
||||
|
||||
def close_account(account_id: str, tool_context: ToolContext):
|
||||
# This code only runs if the user approves the confirmation
|
||||
return {"result": f"Account {account_id} closed."}
|
||||
|
||||
root_agent = Agent(
|
||||
...
|
||||
tools=[
|
||||
FunctionTool(func=close_account, require_confirmation=True),
|
||||
],
|
||||
)
|
||||
```
|
||||
@@ -0,0 +1,15 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from . import agent
|
||||
@@ -0,0 +1,57 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from google.adk.agents import Agent
|
||||
from google.adk.tools.function_tool import FunctionTool
|
||||
from google.adk.tools.tool_context import ToolContext
|
||||
|
||||
|
||||
def transfer_funds(
|
||||
amount: float, recipient: str, tool_context: ToolContext
|
||||
) -> dict[str, str]:
|
||||
"""Transfers funds to a recipient."""
|
||||
# Only request confirmation for amounts >= 100
|
||||
if amount >= 100:
|
||||
if not tool_context.tool_confirmation:
|
||||
tool_context.request_confirmation(
|
||||
hint=f"Confirm transfer of ${amount} to {recipient}.",
|
||||
)
|
||||
return {
|
||||
"error": (
|
||||
"This tool call requires confirmation, please approve or reject."
|
||||
)
|
||||
}
|
||||
elif not tool_context.tool_confirmation.confirmed:
|
||||
return {"error": "Transfer rejected by user."}
|
||||
|
||||
# Proceed with transfer for amounts < 100 or if confirmed
|
||||
return {"result": f"Successfully transferred ${amount} to {recipient}."}
|
||||
|
||||
|
||||
def close_account(account_id: str) -> dict[str, str]:
|
||||
"""Closes a user account. This is a destructive action."""
|
||||
# With require_confirmation=True, this function is only called if the user
|
||||
# approves.
|
||||
return {"result": f"Account {account_id} closed successfully."}
|
||||
|
||||
|
||||
root_agent = Agent(
|
||||
name="money_transfer_assistant",
|
||||
tools=[
|
||||
transfer_funds,
|
||||
FunctionTool(func=close_account, require_confirmation=True),
|
||||
],
|
||||
)
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
{
|
||||
"events": [
|
||||
{
|
||||
"author": "user",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "Transfer $500 to Charlie and close account ACC123"
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-1",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "money_transfer_assistant",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionCall": {
|
||||
"args": {
|
||||
"amount": 500,
|
||||
"recipient": "Charlie"
|
||||
},
|
||||
"id": "fc-1",
|
||||
"name": "transfer_funds"
|
||||
}
|
||||
},
|
||||
{
|
||||
"functionCall": {
|
||||
"args": {
|
||||
"account_id": "ACC123"
|
||||
},
|
||||
"id": "fc-2",
|
||||
"name": "close_account"
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-2",
|
||||
"invocationId": "i-1",
|
||||
"longRunningToolIds": [],
|
||||
"nodeInfo": {
|
||||
"path": "money_transfer_assistant@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "money_transfer_assistant",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionCall": {
|
||||
"args": {
|
||||
"originalFunctionCall": {
|
||||
"args": {
|
||||
"amount": 500,
|
||||
"recipient": "Charlie"
|
||||
},
|
||||
"id": "fc-1",
|
||||
"name": "transfer_funds"
|
||||
},
|
||||
"toolConfirmation": {
|
||||
"confirmed": false,
|
||||
"hint": "Confirm transfer of $500 to Charlie."
|
||||
}
|
||||
},
|
||||
"id": "fc-3",
|
||||
"name": "adk_request_confirmation"
|
||||
}
|
||||
},
|
||||
{
|
||||
"functionCall": {
|
||||
"args": {
|
||||
"originalFunctionCall": {
|
||||
"args": {
|
||||
"account_id": "ACC123"
|
||||
},
|
||||
"id": "fc-2",
|
||||
"name": "close_account"
|
||||
},
|
||||
"toolConfirmation": {
|
||||
"confirmed": false,
|
||||
"hint": "Please approve or reject the tool call close_account() by responding with a FunctionResponse with an expected ToolConfirmation payload."
|
||||
}
|
||||
},
|
||||
"id": "fc-4",
|
||||
"name": "adk_request_confirmation"
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"id": "e-3",
|
||||
"invocationId": "i-1",
|
||||
"longRunningToolIds": [
|
||||
"fc-4",
|
||||
"fc-3"
|
||||
],
|
||||
"nodeInfo": {
|
||||
"path": "money_transfer_assistant@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"actions": {
|
||||
"requestedToolConfirmations": {
|
||||
"fc-1": {
|
||||
"confirmed": false,
|
||||
"hint": "Confirm transfer of $500 to Charlie."
|
||||
},
|
||||
"fc-2": {
|
||||
"confirmed": false,
|
||||
"hint": "Please approve or reject the tool call close_account() by responding with a FunctionResponse with an expected ToolConfirmation payload."
|
||||
}
|
||||
},
|
||||
"skipSummarization": true
|
||||
},
|
||||
"author": "money_transfer_assistant",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-1",
|
||||
"name": "transfer_funds",
|
||||
"response": {
|
||||
"error": "This tool call requires confirmation, please approve or reject."
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-2",
|
||||
"name": "close_account",
|
||||
"response": {
|
||||
"error": "This tool call requires confirmation, please approve or reject."
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-4",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "money_transfer_assistant@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "user",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-3",
|
||||
"name": "adk_request_confirmation",
|
||||
"response": {
|
||||
"confirmed": true
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-5",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "money_transfer_assistant",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-1",
|
||||
"name": "transfer_funds",
|
||||
"response": {
|
||||
"result": "Successfully transferred $500 to Charlie."
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-6",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "money_transfer_assistant@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "money_transfer_assistant",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "I transferred $500 to Charlie. I can close account ACC123, but it is a destructive action that requires confirmation. Do you want me to go ahead and close it?"
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-7",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "money_transfer_assistant@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "user",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-4",
|
||||
"name": "adk_request_confirmation",
|
||||
"response": {
|
||||
"confirmed": true
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-8",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "money_transfer_assistant",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-2",
|
||||
"name": "close_account",
|
||||
"response": {
|
||||
"result": "Account ACC123 closed successfully."
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-9",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "money_transfer_assistant@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "money_transfer_assistant",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "I have transferred $500 to Charlie and closed account ACC123.\n"
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-10",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "money_transfer_assistant@1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
{
|
||||
"events": [
|
||||
{
|
||||
"author": "user",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "Close account ACC123\n"
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-1",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "money_transfer_assistant",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionCall": {
|
||||
"args": {
|
||||
"account_id": "ACC123"
|
||||
},
|
||||
"id": "fc-1",
|
||||
"name": "close_account"
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-2",
|
||||
"invocationId": "i-1",
|
||||
"longRunningToolIds": [],
|
||||
"nodeInfo": {
|
||||
"path": "money_transfer_assistant@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "money_transfer_assistant",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionCall": {
|
||||
"args": {
|
||||
"originalFunctionCall": {
|
||||
"args": {
|
||||
"account_id": "ACC123"
|
||||
},
|
||||
"id": "fc-1",
|
||||
"name": "close_account"
|
||||
},
|
||||
"toolConfirmation": {
|
||||
"confirmed": false,
|
||||
"hint": "Please approve or reject the tool call close_account() by responding with a FunctionResponse with an expected ToolConfirmation payload."
|
||||
}
|
||||
},
|
||||
"id": "fc-2",
|
||||
"name": "adk_request_confirmation"
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"id": "e-3",
|
||||
"invocationId": "i-1",
|
||||
"longRunningToolIds": [
|
||||
"fc-2"
|
||||
],
|
||||
"nodeInfo": {
|
||||
"path": "money_transfer_assistant@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"actions": {
|
||||
"requestedToolConfirmations": {
|
||||
"fc-1": {
|
||||
"confirmed": false,
|
||||
"hint": "Please approve or reject the tool call close_account() by responding with a FunctionResponse with an expected ToolConfirmation payload."
|
||||
}
|
||||
},
|
||||
"skipSummarization": true
|
||||
},
|
||||
"author": "money_transfer_assistant",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-1",
|
||||
"name": "close_account",
|
||||
"response": {
|
||||
"error": "This tool call requires confirmation, please approve or reject."
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-4",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "money_transfer_assistant@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "user",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-2",
|
||||
"name": "adk_request_confirmation",
|
||||
"response": {
|
||||
"confirmed": true
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-5",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "money_transfer_assistant",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-1",
|
||||
"name": "close_account",
|
||||
"response": {
|
||||
"result": "Account ACC123 closed successfully."
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-6",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "money_transfer_assistant@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "money_transfer_assistant",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "I have closed the account ACC123."
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-7",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "money_transfer_assistant@1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
{
|
||||
"events": [
|
||||
{
|
||||
"author": "user",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "Transfer $200 to Bob"
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-1",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "money_transfer_assistant",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionCall": {
|
||||
"args": {
|
||||
"amount": 200,
|
||||
"recipient": "Bob"
|
||||
},
|
||||
"id": "fc-1",
|
||||
"name": "transfer_funds"
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-2",
|
||||
"invocationId": "i-1",
|
||||
"longRunningToolIds": [],
|
||||
"nodeInfo": {
|
||||
"path": "money_transfer_assistant@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "money_transfer_assistant",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionCall": {
|
||||
"args": {
|
||||
"originalFunctionCall": {
|
||||
"args": {
|
||||
"amount": 200,
|
||||
"recipient": "Bob"
|
||||
},
|
||||
"id": "fc-1",
|
||||
"name": "transfer_funds"
|
||||
},
|
||||
"toolConfirmation": {
|
||||
"confirmed": false,
|
||||
"hint": "Confirm transfer of $200 to Bob."
|
||||
}
|
||||
},
|
||||
"id": "fc-2",
|
||||
"name": "adk_request_confirmation"
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"id": "e-3",
|
||||
"invocationId": "i-1",
|
||||
"longRunningToolIds": [
|
||||
"fc-2"
|
||||
],
|
||||
"nodeInfo": {
|
||||
"path": "money_transfer_assistant@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"actions": {
|
||||
"requestedToolConfirmations": {
|
||||
"fc-1": {
|
||||
"confirmed": false,
|
||||
"hint": "Confirm transfer of $200 to Bob."
|
||||
}
|
||||
}
|
||||
},
|
||||
"author": "money_transfer_assistant",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-1",
|
||||
"name": "transfer_funds",
|
||||
"response": {
|
||||
"error": "This tool call requires confirmation, please approve or reject."
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-4",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "money_transfer_assistant@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "money_transfer_assistant",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "I need to confirm this transfer with you. Please approve or reject."
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-5",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "money_transfer_assistant@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "user",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-2",
|
||||
"name": "adk_request_confirmation",
|
||||
"response": {
|
||||
"confirmed": true
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-6",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "money_transfer_assistant",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-1",
|
||||
"name": "transfer_funds",
|
||||
"response": {
|
||||
"result": "Successfully transferred $200 to Bob."
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-7",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "money_transfer_assistant@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "money_transfer_assistant",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "Successfully transferred $200 to Bob."
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-8",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "money_transfer_assistant@1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
{
|
||||
"events": [
|
||||
{
|
||||
"author": "user",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "Transfer $200 to Bob"
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-1",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "money_transfer_assistant",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionCall": {
|
||||
"args": {
|
||||
"amount": 200,
|
||||
"recipient": "Bob"
|
||||
},
|
||||
"id": "fc-1",
|
||||
"name": "transfer_funds"
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-2",
|
||||
"invocationId": "i-1",
|
||||
"longRunningToolIds": [],
|
||||
"nodeInfo": {
|
||||
"path": "money_transfer_assistant@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "money_transfer_assistant",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionCall": {
|
||||
"args": {
|
||||
"originalFunctionCall": {
|
||||
"args": {
|
||||
"amount": 200,
|
||||
"recipient": "Bob"
|
||||
},
|
||||
"id": "fc-1",
|
||||
"name": "transfer_funds"
|
||||
},
|
||||
"toolConfirmation": {
|
||||
"confirmed": false,
|
||||
"hint": "Confirm transfer of $200 to Bob."
|
||||
}
|
||||
},
|
||||
"id": "fc-2",
|
||||
"name": "adk_request_confirmation"
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"id": "e-3",
|
||||
"invocationId": "i-1",
|
||||
"longRunningToolIds": [
|
||||
"fc-2"
|
||||
],
|
||||
"nodeInfo": {
|
||||
"path": "money_transfer_assistant@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"actions": {
|
||||
"requestedToolConfirmations": {
|
||||
"fc-1": {
|
||||
"confirmed": false,
|
||||
"hint": "Confirm transfer of $200 to Bob."
|
||||
}
|
||||
}
|
||||
},
|
||||
"author": "money_transfer_assistant",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-1",
|
||||
"name": "transfer_funds",
|
||||
"response": {
|
||||
"error": "This tool call requires confirmation, please approve or reject."
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-4",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "money_transfer_assistant@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "money_transfer_assistant",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "Your transfer of $200 to Bob requires confirmation."
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-5",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "money_transfer_assistant@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "user",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-2",
|
||||
"name": "adk_request_confirmation",
|
||||
"response": {
|
||||
"confirmed": false
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-6",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "money_transfer_assistant",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-1",
|
||||
"name": "transfer_funds",
|
||||
"response": {
|
||||
"error": "Transfer rejected by user."
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-7",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "money_transfer_assistant@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "money_transfer_assistant",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "I'm sorry, your transfer could not be completed. It was rejected by the user."
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-8",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "money_transfer_assistant@1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"events": [
|
||||
{
|
||||
"author": "user",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "Transfer $50 to Alice\n"
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-1",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "money_transfer_assistant",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionCall": {
|
||||
"args": {
|
||||
"amount": 50,
|
||||
"recipient": "Alice"
|
||||
},
|
||||
"id": "fc-1",
|
||||
"name": "transfer_funds"
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-2",
|
||||
"invocationId": "i-1",
|
||||
"longRunningToolIds": [],
|
||||
"nodeInfo": {
|
||||
"path": "money_transfer_assistant@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "money_transfer_assistant",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-1",
|
||||
"name": "transfer_funds",
|
||||
"response": {
|
||||
"result": "Successfully transferred $50 to Alice."
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-3",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "money_transfer_assistant@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "money_transfer_assistant",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "Successfully transferred $50 to Alice."
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-4",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "money_transfer_assistant@1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# Config-based Agent Sample - Human-In-The-Loop
|
||||
|
||||
From contributing/samples/human_in_loop/
|
||||
@@ -0,0 +1,13 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/google/adk-python/refs/heads/main/src/google/adk/agents/config_schemas/AgentConfig.json
|
||||
name: reimbursement_agent
|
||||
model: gemini-2.5-flash
|
||||
instruction: |
|
||||
You are an agent whose job is to handle the reimbursement process for
|
||||
the employees. If the amount is less than $100, you will automatically
|
||||
approve the reimbursement.
|
||||
|
||||
If the amount is greater than $100, you will
|
||||
ask for approval from the manager. If the manager approves, you will
|
||||
call reimburse() to reimburse the amount to the employee. If the manager
|
||||
rejects, you will inform the employee of the rejection.
|
||||
tools:
|
||||
- name: tool_human_in_the_loop_config.tools.reimburse
|
||||
- name: LongRunningFunctionTool
|
||||
args:
|
||||
func: tool_human_in_the_loop_config.tools.ask_for_approval
|
||||
@@ -0,0 +1,35 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from typing import Any
|
||||
|
||||
from google.adk.tools.tool_context import ToolContext
|
||||
|
||||
|
||||
def reimburse(purpose: str, amount: float) -> str:
|
||||
"""Reimburse the amount of money to the employee."""
|
||||
return {
|
||||
'status': 'ok',
|
||||
}
|
||||
|
||||
|
||||
def ask_for_approval(
|
||||
purpose: str, amount: float, tool_context: ToolContext
|
||||
) -> dict[str, Any]:
|
||||
"""Ask for approval for the reimbursement."""
|
||||
return {
|
||||
'status': 'pending',
|
||||
'amount': amount,
|
||||
'ticketId': 'reimbursement-ticket-001',
|
||||
}
|
||||
Reference in New Issue
Block a user