Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ Thumbs.db
*.tmp
*.temp

# ADK local dev artifacts
.adk/
*.db
*.sqlite
hitl.db

# gemini-cli settings
.gemini/

Expand Down
103 changes: 103 additions & 0 deletions .pr_body.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
## Summary
Closes #[ISSUE NUMBER]
Adds a production-ready Human-in-the-Loop approval gateway for Google ADK agents. This addresses a documented gap where ADK's built-in Tool Confirmation feature explicitly does not support `DatabaseSessionService` or `VertexAiSessionService` — the two session backends required for production deployments — making structured human oversight unavailable in any persistent production environment.

## Problem
ADK's Tool Confirmation (v1.14.0+) is experimental and has three blockers for production use:
1. Does not support `DatabaseSessionService` or `VertexAiSessionService`
2. Does not trigger inside `AgentTool` or across A2A boundaries
3. No structured approval UI, audit trail, or persistence layer

Validated by community issues: #1797, #1851, #2645, #3276, #3567 on `google/adk-python`.

## Solution
A session-agnostic HITL approval gateway that manages approval state in its own persistence layer (SQLite, with a documented path to Postgres), independent of ADK's session service. The agent resumes via ADK's standard REST API after a human decision is submitted.

### What's included

**Core module** (`src/google/adk_community/tools/hitl/`)
- `gateway.py` — `hitl_tool` decorator that wraps any async function before it is passed to `FunctionTool`. Adding HITL to an existing tool takes ~5 lines.
- `models.py` — `ApprovalRequest` Pydantic model, normalised data contract capturing agent context, payload, risk level, and audit metadata
- `adapters/adk1.py` — ADK 1.x adapter translating `request_confirmation()` events into `ApprovalRequest` objects

**Service** (`src/google/adk_community/services/hitl_approval/`)
- `api.py` — FastAPI application
- `routes.py` — REST endpoints for approval queue management
- `store.py` — SQLite persistence with full audit log

**Sample** (`contributing/samples/hitl_approval/`)
- `credit_agent/agent.py` — Credit approval agent demonstrating end-to-end integration
- `dashboard/app.py` — Reference Streamlit approval inbox UI
- `start_servers.sh` — One-command startup for all three services
- `requirements.txt` — Sample-only dependencies

### Architecture
```
ADK Agent Pipeline
@hitl_tool decorator (wraps async function → FunctionTool)
↓ POST /approvals/ — creates ApprovalRequest
FastAPI + SQLite (approval state)
↓ serves pending approvals
Streamlit Dashboard (reviewer decides)
↓ POST /approvals/{id}/decide
FastAPI updates status in SQLite
↓ decorator polls GET /approvals/{id} every 2 s
Agent resumes execution (wrapper unblocks; runs tool if approved)
```

### Forward compatibility
Built with an adapter pattern so the same approval backend and dashboard work with ADK 1.x today and ADK 2.0's `RequestInput` pattern when it reaches stable — without teams needing to rebuild their approval layer on upgrade.

## Testing
### Unit tests
All 11 tests passing:
```text
============================= test session starts =============================
platform darwin -- Python 3.11.15, pytest-9.0.2, pluggy-1.6.0
rootdir: /Users/garythomasgeorge/Desktop/Work/AI Dev/adk-python-community
configfile: pyproject.toml
plugins: anyio-4.12.1, asyncio-1.3.0
asyncio: mode=Mode.AUTO, debug=False, asyncio_default_fixture_loop_scope=function, asyncio_default_test_loop_scope=function
collected 11 items

tests/unittests/tools/test_hitl_gateway.py ...... [ 54%]
tests/unittests/services/test_hitl_approval_api.py ..... [100%]

================================ 11 passed in 1.76s =================================
Exit code: 0
```

### Manual E2E
Full end-to-end flow verified:
- Agent triggers approval request → appears in Streamlit dashboard ✓
- Reviewer approves in dashboard → agent resumes correctly ✓
- Uvicorn restart → SQLite persists previous approvals ✓

> 🎥 *Please drag-and-drop your `hitl_demo_video_1774318429041.webp` file here before publishing*

## Testing plan
For reviewers wanting to reproduce locally:
```bash
cd contributing/samples/hitl_approval
uv pip install -r requirements.txt
./start_servers.sh
```

Then open:
- ADK Dev UI: `http://localhost:8080`
- Streamlit dashboard: `http://localhost:8501`
- FastAPI docs: `http://localhost:8000/docs`

Trigger an approval by asking the credit agent to process an amount over $500.

## Notes for reviewers
- Opening as **Draft** — happy to address structural feedback before requesting full review
- ADK 2.0 adapter (`adapters/adk2.py`) is planned as a follow-up PR once 2.0 moves toward stable
- Confirmed structure placement from proposal issue: `tools/hitl` for the gateway and models, `services/hitl_approval` for the FastAPI backend — let me know if you'd prefer a different organisation

## Related
- Proposal issue: #[ISSUE NUMBER]
- ADK Tool Confirmation docs (known limitations): https://google.github.io/adk-docs/tools-custom/confirmation/
- ADK multi-agent HITL pattern reference: https://developers.googleblog.com/developers-guide-to-multi-agent-patterns-in-adk/
- Existing community example this extends: https://github.com/jackwotherspoon/adk-human-in-the-loop
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## [Unreleased]

### Features

* **hitl:** add production-ready Human-in-the-Loop approval gateway for ADK agents — includes `@hitl_tool` decorator, FastAPI approval service with SQLite persistence, ADK 1.x adapter, and reference Streamlit dashboard (`contributing/samples/hitl_approval`)

## [0.5.0](https://github.com/google/adk-python-community/compare/v0.4.1...v0.5.0) (2026-05-20)


Expand Down
111 changes: 111 additions & 0 deletions contributing/samples/hitl_approval/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# ADK HITL Approval Dashboard

A drop-in **production-ready Human-in-the-Loop (HITL) approval middleware** for Google Agent Development Kit (ADK) agents — complete with an API backend and a demo Streamlit dashboard UI.

## The Problem Solved

ADK 1.x ships with an experimental `require_confirmation=True` feature that handles pausing the LLM loop for human verification. However, it is fundamentally built for local debugging and introduces major blockers to an enterprise environment:

1. **Incompatible with Persistent Sessions:** Native confirmations intentionally do not serialize well and will completely fail to resume your agent if you use `DatabaseSessionService`, `SpannerSessionService`, or `VertexAiSessionService` (the mandatory session backends for production deployments).
2. **Single-Agent Limitations:** They silently break across `AgentTool` nested bounds and true multi-agent (A2A) topologies, causing missing events or infinitely looping models.
3. **No Resilient Audit Log:** The native confirmation tool leaves no easily queryable paper trail linking the human supervisor to a precise LLM request.

*This project is the production implementation of the HITL pattern covered in the [ADK Multi-Agent Patterns Guide (Advent of Agents Day 13)](https://medium.com/@garythomasgeorge/why-google-adks-human-in-the-loop-story-has-a-production-gap-and-one-way-it-could-be-fixed-66aabef33a32).*

## What This Library Provides

This project solves the production gaps by explicitly decoupling the human approval payload from ADK's internal session memory. It introduces a session-agnostic REST API layer using an Adapter pattern.

### The 3-Layer Architecture

```
┌─────────────────────────────────────────┐
│ Dashboard UI (Streamlit) │ Layer 3: Demo/reference UI
│ Approval inbox, audit log viewer │ (Easily replaced by Zendesk/etc.)
└──────────────────┬──────────────────────┘
┌──────────────────▼──────────────────────┐
│ ApprovalRequest Model (Pydantic) │ Layer 2: Normalised Contract API
│ FastAPI backend + SQLite store │ Session-agnostic persistence
└────────────────┬────────────────────────┘
┌──────────┴───────────┐
┌─────▼──────┐ ┌──────────▼──────┐
│ ADK 1.x │ │ ADK 2.0 │ Layer 1: Adapters
│ Adapter │ │ Adapter │ Only this changes between versions
└────────────┘ └─────────────────┘
```

By retaining HITL state inside an independent FastAPI engine and SQLite database, an active agent can pause safely. When a human supervisor hits "Approve" inside a centralized web portal hours later, the middleware simply posts the decision back into the agent's `/run_sse` stream seamlessly.

## Configuration

| Environment Variable | Default | Description |
|---|---|---|
| `ADK_HITL_API_URL` | `http://localhost:8000` | URL of the HITL approval FastAPI backend. Override for Cloud Run or any remote deployment. |
| `ADK_HITL_POLL_INTERVAL_S` | `2.0` | Base polling interval in seconds. Up to 1s of random jitter is added automatically to reduce backend traffic under concurrent load. |

Set these before starting the gateway:

```bash
export ADK_HITL_API_URL="https://your-hitl-service.run.app"
export ADK_HITL_POLL_INTERVAL_S="3.0"
```

## Quick Start (Local Sandbox)

We have provided a demo customer service agent (`credit_agent`) alongside a launch script to test the interaction end-to-end.

1. Create your Python virtual environment and sync dependencies using `uv` (requires Python 3.11+):

```bash
uv venv --python "python3.11" ".venv"
source .venv/bin/activate
uv sync --all-extras
```

2. Start the FastAPI backend, Streamlit dashboard, and ADK Live Chat agent all at once:

```bash
./start_servers.sh
```

3. Open `http://localhost:8080` to chat with the agent and ask for a $75 account credit.
4. When the agent pauses and asks for a supervisor, open `http://localhost:8501` to approve or reject the request.

## How to Use in Your Own ADK Application

Wrapping an ADK agent with a formal enterprise HITL checkpoint takes under 5 lines of code:

1. Import the `hitl_tool` gateway wrapper.
2. Decorate your function tool.
3. Attach it to your ADK Agent initialization using a standard `FunctionTool`.

```python
from google.adk.tools import FunctionTool
from google.adk_community.tools.hitl.gateway import hitl_tool

# 1. Wrap your function with the decorator
@hitl_tool(agent_name="my_billing_agent")
async def issue_refund(user_id: str, amount: float):
# This block won't execute until explicitly approved in the dashboard
return {"status": "success", "amount_refunded": amount}

# 2. Attach to ADK Agent
root_agent = Agent(
name="my_billing_agent",
tools=[FunctionTool(issue_refund)]
)
```

## Production Integration Strategies

This repository acts as the production baseline for a contact center or enterprise orchestration grid. Once deployed to staging, consider swapping out:

- **Storage Layer:** Replace the local `SQLite` engine in `app/api/store.py` with `PostgreSQL` or `Cloud Spanner`.
- **Proactive Notification:** Hook the FastAPI `POST /approvals/` route into Slack, PagerDuty, or Microsoft Teams to actively ping channels when a high-risk request pops up.
- **Remove Streamlit:** Bypass the Streamlit frontend completely and point your existing support portal interface (like Salesforce Service Cloud) directly to `GET /approvals/pending` and `POST /approvals/{id}/decide`.

## ADK 2.0 Compatibility

This project currently uses ADK 1.x conventions and event triggers. Because it strictly implements an `adapters` layer, all the Pydantic API schemas and Streamlit logic are completely forward-compatible with ADK 2.0 `RequestInput` workflow yielding. You'll simply need to switch the adapter layer translation once ADK 2.0 exits Alpha. The `ADK_HITL_API_URL` and `ADK_HITL_POLL_INTERVAL_S` environment variables remain valid across both adapter versions.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import agent
80 changes: 80 additions & 0 deletions contributing/samples/hitl_approval/credit_agent/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# 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.

"""Credit agent — external supervisor HITL demo.

This agent demonstrates the cross-user approval pattern:
- Customer chats in ADK web (:8080)
- Agent wants to apply a credit → submits request to HITL API (:8000)
- Agent blocks (non-blocking async poll) waiting for a decision
- Supervisor opens Streamlit dashboard (:8501), reviews and approves/rejects
- Agent resumes and informs the customer of the outcome

Make sure all three services are running before chatting (see start_servers.sh):
HITL API: uvicorn google.adk_community.services.hitl_approval.api:app --port 8000
Dashboard: streamlit run dashboard/app.py --server.headless true
ADK web: adk web credit_agent/ --port 8080
"""

from __future__ import annotations

import os
import sys

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../.."))

from google.adk.agents import Agent
from google.adk.tools import FunctionTool

from google.adk_community.tools.hitl.gateway import hitl_tool


@hitl_tool(agent_name="credit_agent")
async def apply_account_credit(account_id: str, amount: float, reason: str) -> dict:
"""Apply a credit to a customer account. Requires supervisor approval.

Args:
account_id: The customer account ID to credit.
amount: Credit amount in USD.
reason: Business justification for the credit.

Returns:
Confirmation with the updated account balance.
"""
# Real implementation would call your billing/CRM API here
return {
"status": "credited",
"account_id": account_id,
"amount_credited": amount,
"new_balance": f"${amount:.2f} credit applied successfully.",
}


root_agent = Agent(
name="credit_agent",
model="gemini-2.5-flash",
description=(
"Customer support agent that can apply account credits. "
"Every credit requires supervisor approval via the HITL dashboard."
),
instruction=(
"You are a customer support agent. When a customer requests an account credit, "
"call apply_account_credit with their account ID, the amount, and the reason. "
"Let them know their request is being reviewed by a supervisor and that you will "
"update them once a decision is made. "
"If the credit is approved, confirm it to the customer. "
"If rejected, apologise and explain that the supervisor did not approve it."
),
tools=[FunctionTool(apply_account_credit)],
)
Loading
Loading