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,195 @@
|
||||
# A2A Human-in-the-Loop Sample Agent
|
||||
|
||||
This sample demonstrates the **Agent-to-Agent (A2A)** architecture with **Human-in-the-Loop** workflows in the Agent Development Kit (ADK). The sample implements a reimbursement processing agent that automatically handles small expenses while requiring remote agent to process for larger amounts. The remote agent will require a human approval for large amounts, thus surface this request to local agent and human interacting with local agent can approve the request.
|
||||
|
||||
## Overview
|
||||
|
||||
The A2A Human-in-the-Loop sample consists of:
|
||||
|
||||
- **Root Agent** (`root_agent`): The main reimbursement agent that handles expense requests and delegates approval to remote Approval Agent for large amounts
|
||||
- **Approval Agent** (`approval_agent`): A remote A2A agent that handles the human approval process via long-running tools (which implements asynchronous approval workflows that can pause execution and wait for human input), this agent is running on a separate A2A server
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌────────────────────┐ ┌──────────────────┐
|
||||
│ Human Manager │───▶│ Root Agent │───▶│ Approval Agent │
|
||||
│ (External) │ │ (Local) │ │ (Remote A2A) │
|
||||
│ │ │ │ │ (localhost:8001) │
|
||||
│ Approval UI │◀───│ │◀───│ │
|
||||
└─────────────────┘ └────────────────────┘ └──────────────────┘
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
### 1. **Automated Decision Making**
|
||||
|
||||
- Automatically approves reimbursements under $100
|
||||
- Uses business logic to determine when human intervention is required
|
||||
- Provides immediate responses for simple cases
|
||||
|
||||
### 2. **Human-in-the-Loop Workflow**
|
||||
|
||||
- Seamlessly escalates high-value requests (>$100) to remote approval agent
|
||||
- Remote approval agent uses long-running tools to surface approval requests back to the root agent
|
||||
- Human managers interact directly with the root agent to approve/reject requests
|
||||
|
||||
### 3. **Long-Running Tool Integration**
|
||||
|
||||
- Demonstrates `LongRunningFunctionTool` for asynchronous operations
|
||||
- Shows how to handle pending states and external updates
|
||||
- Implements proper tool response handling for delayed approvals
|
||||
|
||||
### 4. **Remote A2A Agent Communication**
|
||||
|
||||
- The approval agent runs as a separate service that processes approval workflows
|
||||
- Communicates via HTTP at `http://localhost:8001/a2a/human_in_loop`
|
||||
- Surfaces approval requests back to the root agent for human interaction
|
||||
|
||||
## Setup and Usage
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. **Start the Remote Approval Agent server**:
|
||||
|
||||
```bash
|
||||
# Start the remote a2a server that serves the human-in-the-loop approval agent on port 8001
|
||||
adk api_server --a2a --port 8001 contributing/samples/a2a/a2a_human_in_loop/remote_a2a
|
||||
```
|
||||
|
||||
1. **Run the Main Agent**:
|
||||
|
||||
```bash
|
||||
# In a separate terminal, run the adk web server
|
||||
adk web contributing/samples/a2a
|
||||
```
|
||||
|
||||
### Example Interactions
|
||||
|
||||
Once both services are running, you can interact with the root agent through the approval workflow:
|
||||
|
||||
**Automatic Approval (Under $100):**
|
||||
|
||||
```
|
||||
User: Please reimburse $50 for meals
|
||||
Agent: I'll process your reimbursement request for $50 for meals. Since this amount is under $100, I can approve it automatically.
|
||||
Agent: ✅ Reimbursement approved and processed: $50 for meals
|
||||
```
|
||||
|
||||
**Human Approval Required (Over $100):**
|
||||
|
||||
```
|
||||
User: Please reimburse $200 for conference travel
|
||||
Agent: I'll process your reimbursement request for $200 for conference travel. Since this amount exceeds $100, I need to get manager approval.
|
||||
Agent: 🔄 Request submitted for approval (Ticket: reimbursement-ticket-001). Please wait for manager review.
|
||||
[Human manager approves the pending request from the ADK Web UI]
|
||||
Agent: ✅ Great news! Your reimbursement has been approved by the manager. Processing $200 for conference travel.
|
||||
```
|
||||
|
||||
> **Approving from the ADK Web UI:** The approval is a *long-running tool* call
|
||||
> that runs on the remote approval agent. The pending call is surfaced in the
|
||||
> Web UI as a function call awaiting a response. To approve (or reject), hover
|
||||
> over the pending `ask_for_approval` function response in the UI and use
|
||||
> **"Send another response"** to send back an updated response such as
|
||||
> `{"status": "approved", "ticketId": "reimbursement-ticket-001"}`. Simply
|
||||
> typing "I approve" as a chat message will **not** resume the pending request,
|
||||
> because the framework needs a `FunctionResponse` that carries the same call
|
||||
> `id` to resume the long-running tool.
|
||||
>
|
||||
> For this resume to be routed back to the remote approval agent (rather than
|
||||
> restarting at the root agent), the sample is exposed as an `App` with
|
||||
> `ResumabilityConfig(is_resumable=True)` in `agent.py`.
|
||||
|
||||
## Code Structure
|
||||
|
||||
### Main Agent (`agent.py`)
|
||||
|
||||
- **`reimburse(purpose: str, amount: float)`**: Function tool for processing reimbursements
|
||||
- **`approval_agent`**: Remote A2A agent configuration for human approval workflows
|
||||
- **`root_agent`**: Main reimbursement agent with automatic/manual approval logic
|
||||
|
||||
### Remote Approval Agent (`remote_a2a/human_in_loop/`)
|
||||
|
||||
- **`agent.py`**: Implementation of the approval agent with long-running tools
|
||||
|
||||
- **`agent.json`**: Agent card of the A2A agent
|
||||
|
||||
- **`ask_for_approval()`**: Long-running tool that handles approval requests
|
||||
|
||||
## Long-Running Tool Workflow
|
||||
|
||||
The human-in-the-loop process follows this pattern:
|
||||
|
||||
1. **Initial Call**: Root agent delegates approval request to remote approval agent for amounts >$100
|
||||
1. **Pending Response**: Remote approval agent returns immediate response with `status: "pending"` and ticket ID and surface the approval request to root agent
|
||||
1. **Agent Acknowledgment**: Root agent informs user about pending approval status
|
||||
1. **Human Interaction**: Human manager interacts with root agent to review and approve/reject the request
|
||||
1. **Updated Response**: Root agent receives updated tool response with approval decision and send it to remote agent
|
||||
1. **Final Action**: Remote agent processes the approval and completes the reimbursement and send the result to root_agent
|
||||
|
||||
## Extending the Sample
|
||||
|
||||
You can extend this sample by:
|
||||
|
||||
- Adding more complex approval hierarchies (multiple approval levels)
|
||||
- Implementing different approval rules based on expense categories
|
||||
- Creating additional remote agent for budget checking or policy validation
|
||||
- Adding notification systems for approval status updates
|
||||
- Integrating with external approval systems or databases
|
||||
- Implementing approval timeouts and escalation procedures
|
||||
|
||||
## Deployment to Other Environments
|
||||
|
||||
When deploying the remote approval A2A agent to different environments (e.g., Cloud Run, different hosts/ports), you **must** update the `url` field in the agent card JSON file:
|
||||
|
||||
### Local Development
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "http://localhost:8001/a2a/human_in_loop",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Cloud Run Example
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "https://your-approval-service-abc123-uc.a.run.app/a2a/human_in_loop",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Host/Port Example
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "https://your-domain.com:9000/a2a/human_in_loop",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**Important:** The `url` field in `remote_a2a/human_in_loop/agent.json` must point to the actual RPC endpoint where your remote approval A2A agent is deployed and accessible.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Connection Issues:**
|
||||
|
||||
- Ensure the local ADK web server is running on port 8000
|
||||
- Ensure the remote A2A server is running on port 8001
|
||||
- Check that no firewall is blocking localhost connections
|
||||
- **Verify the `url` field in `remote_a2a/human_in_loop/agent.json` matches the actual deployed location of your remote A2A server**
|
||||
- Verify the agent card URL passed to RemoteA2AAgent constructor matches the running A2A server
|
||||
|
||||
**Agent Not Responding:**
|
||||
|
||||
- Check the logs for both the local ADK web server on port 8000 and remote A2A server on port 8001
|
||||
- Verify the agent instructions are clear and unambiguous
|
||||
- Ensure long-running tool responses are properly formatted with matching IDs
|
||||
- **Double-check that the RPC URL in the agent.json file is correct and accessible**
|
||||
|
||||
**Approval Workflow Issues:**
|
||||
|
||||
- Verify that updated tool responses use the same `id` and `name` as the original function call
|
||||
- Check that the approval status is correctly updated in the tool response
|
||||
- Ensure the human approval process is properly simulated or integrated
|
||||
@@ -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,68 @@
|
||||
# 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.agents.llm_agent import Agent
|
||||
from google.adk.agents.remote_a2a_agent import AGENT_CARD_WELL_KNOWN_PATH
|
||||
from google.adk.agents.remote_a2a_agent import RemoteA2aAgent
|
||||
from google.adk.apps import App
|
||||
from google.adk.apps import ResumabilityConfig
|
||||
from google.genai import types
|
||||
|
||||
|
||||
def reimburse(purpose: str, amount: float) -> str:
|
||||
"""Reimburse the amount of money to the employee."""
|
||||
return {
|
||||
'status': 'ok',
|
||||
}
|
||||
|
||||
|
||||
approval_agent = RemoteA2aAgent(
|
||||
name='approval_agent',
|
||||
description='Help approve the reimburse if the amount is greater than 100.',
|
||||
agent_card=(
|
||||
f'http://localhost:8001/a2a/human_in_loop{AGENT_CARD_WELL_KNOWN_PATH}'
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
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. And call reimburse() to reimburse the amount to the employee.
|
||||
|
||||
If the amount is greater than $100. You will hand over the request to
|
||||
approval_agent to handle the reimburse.
|
||||
""",
|
||||
tools=[reimburse],
|
||||
sub_agents=[approval_agent],
|
||||
generate_content_config=types.GenerateContentConfig(temperature=0.1),
|
||||
)
|
||||
|
||||
# The human-in-the-loop approval runs as a long-running tool on the remote
|
||||
# approval_agent. When the manager approves (or rejects) the request, the ADK
|
||||
# Web UI sends back a FunctionResponse for that pending long-running call. For
|
||||
# the next turn to be routed back to the (remote) approval_agent so it can
|
||||
# resume the paused tool instead of restarting at the root reimbursement_agent,
|
||||
# the app must be resumable. Without this, the confirmation is delivered to the
|
||||
# root agent, which has no pending call, and nothing happens (see issue #5871).
|
||||
app = App(
|
||||
name='a2a_human_in_loop',
|
||||
root_agent=root_agent,
|
||||
resumability_config=ResumabilityConfig(
|
||||
is_resumable=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,45 @@
|
||||
{
|
||||
"capabilities": {},
|
||||
"defaultInputModes": [
|
||||
"text/plain"
|
||||
],
|
||||
"defaultOutputModes": [
|
||||
"application/json"
|
||||
],
|
||||
"description": "A reimbursement agent that handles employee expense reimbursement requests. Automatically approves amounts under $100 and requires manager approval for larger amounts using long-running tools for human-in-the-loop workflows.",
|
||||
"name": "reimbursement_agent",
|
||||
"skills": [
|
||||
{
|
||||
"id": "automatic_reimbursement",
|
||||
"name": "Automatic Reimbursement",
|
||||
"description": "Automatically process and approve reimbursements under $100",
|
||||
"tags": [
|
||||
"reimbursement",
|
||||
"automation",
|
||||
"finance"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "approval_workflow",
|
||||
"name": "Approval Workflow",
|
||||
"description": "Request manager approval for reimbursements over $100 using long-running tools",
|
||||
"tags": [
|
||||
"approval",
|
||||
"workflow",
|
||||
"human-in-loop"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "expense_processing",
|
||||
"name": "Expense Processing",
|
||||
"description": "Process employee expense claims and handle reimbursement logic",
|
||||
"tags": [
|
||||
"expenses",
|
||||
"processing",
|
||||
"employee-services"
|
||||
]
|
||||
}
|
||||
],
|
||||
"url": "http://localhost:8001/a2a/human_in_loop",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
Reference in New Issue
Block a user