|
| 1 | +<!-- |
| 2 | +Copyright 2025 Google LLC |
| 3 | +
|
| 4 | +Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | +you may not use this file except in compliance with the License. |
| 6 | +You may obtain a copy of the License at |
| 7 | +
|
| 8 | + http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | +
|
| 10 | +Unless required by applicable law or agreed to in writing, software |
| 11 | +distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | +See the License for the specific language governing permissions and |
| 14 | +limitations under the License. |
| 15 | +--> |
| 16 | + |
| 17 | +# Human-in-the-Loop: Backend Blocking Poll Pattern |
| 18 | + |
| 19 | +## Overview |
| 20 | + |
| 21 | +This example demonstrates the **backend blocking poll pattern** for human-in-the-loop approval workflows. Unlike the webhook/callback pattern (`LongRunningFunctionTool`), this pattern polls an external approval API internally until a decision is made. |
| 22 | + |
| 23 | +### How It Works |
| 24 | + |
| 25 | +1. Agent calls approval tool **once** |
| 26 | +2. Tool creates approval ticket via external API |
| 27 | +3. **Tool polls API internally** every N seconds (invisible to agent) |
| 28 | +4. Tool returns final decision to agent when ready (or timeout) |
| 29 | + |
| 30 | +### Key Benefits |
| 31 | + |
| 32 | +- ✅ **Simpler integration**: No `FunctionResponse` injection needed |
| 33 | +- ✅ **Seamless UX**: Agent waits automatically, no manual "continue" clicks |
| 34 | +- ✅ **Fewer LLM API calls**: 1 inference vs. 15+ for agent-level polling |
| 35 | +- ✅ **Works with poll-only systems**: Jira, ServiceNow, email approvals, dashboards |
| 36 | + |
| 37 | +--- |
| 38 | + |
| 39 | +## When to Use This Pattern vs. LongRunningFunctionTool |
| 40 | + |
| 41 | +| Scenario | Backend Blocking Poll | LongRunningFunctionTool | |
| 42 | +|----------|----------------------|-------------------------| |
| 43 | +| **Poll-only system** (Jira, ServiceNow, custom dashboards) | ✅ Perfect fit | ❌ Complex application logic | |
| 44 | +| **Webhook-capable system** (GitHub, Slack, webhooks) | ❌ Overkill | ✅ Preferred | |
| 45 | +| **Email approval workflows** (user clicks link, poll for response) | ✅ Simple | ❌ Complex | |
| 46 | +| **User needs real-time updates** (e.g., "Still waiting...") | ❌ Blocks silently | ✅ Can show progress | |
| 47 | +| **Single decision** (<10 minutes) | ✅ Ideal | ⚠️ Overkill | |
| 48 | +| **Multi-step approval** (chain of approvers) | ⚠️ Works but may timeout | ✅ Can handle state transitions | |
| 49 | +| **High concurrency** (many simultaneous approvals) | ✅ Use async version | ✅ Both work well | |
| 50 | + |
| 51 | +### Quick Decision Guide |
| 52 | + |
| 53 | +**Use Backend Blocking Poll when:** |
| 54 | +- External system doesn't support webhooks |
| 55 | +- Simple approval workflow (single decision) |
| 56 | +- Prefer simple application code (no manual `FunctionResponse` management) |
| 57 | +- Approval typically completes in <10 minutes |
| 58 | + |
| 59 | +**Use LongRunningFunctionTool when:** |
| 60 | +- External system supports webhooks or callbacks |
| 61 | +- Need to show progress updates to user during waiting |
| 62 | +- Multi-step approval workflows with state transitions |
| 63 | +- Very long-duration approvals (>10 minutes) |
| 64 | + |
| 65 | +--- |
| 66 | + |
| 67 | +## Files in This Example |
| 68 | + |
| 69 | +### Core Patterns |
| 70 | + |
| 71 | +1. **`blocking_poll_approval_example.py`** - Synchronous version |
| 72 | + - Uses `requests` and `time.sleep()` |
| 73 | + - Simple, straightforward implementation |
| 74 | + - Good for standalone agents or low-concurrency scenarios |
| 75 | + |
| 76 | +2. **`blocking_poll_approval_example_async.py`** - Asynchronous version |
| 77 | + - Uses `aiohttp` and `asyncio.sleep()` |
| 78 | + - Non-blocking I/O for better concurrency |
| 79 | + - **Recommended for production** multi-agent systems |
| 80 | + |
| 81 | +### Testing Infrastructure |
| 82 | + |
| 83 | +3. **`mock_approval_api.py`** - Mock approval API server |
| 84 | + - FastAPI-based test server |
| 85 | + - HTML dashboard for manual testing |
| 86 | + - Simulates external approval system |
| 87 | + |
| 88 | +4. **`test_blocking_poll.py`** - Automated test script |
| 89 | + - Tests sync version with simulated approver |
| 90 | + - Validates pattern behavior |
| 91 | + |
| 92 | +--- |
| 93 | + |
| 94 | +## Setup |
| 95 | + |
| 96 | +### Prerequisites |
| 97 | + |
| 98 | +```bash |
| 99 | +# Python 3.11+ |
| 100 | +python --version |
| 101 | + |
| 102 | +# Install dependencies |
| 103 | +pip install google-adk aiohttp fastapi uvicorn requests |
| 104 | +``` |
| 105 | + |
| 106 | +### Running the Mock Approval API |
| 107 | + |
| 108 | +The mock API simulates an external approval system for testing. |
| 109 | + |
| 110 | +```bash |
| 111 | +# Start mock approval API server |
| 112 | +python mock_approval_api.py |
| 113 | + |
| 114 | +# Server starts at http://localhost:8003 |
| 115 | +# Dashboard: http://localhost:8003/ |
| 116 | +``` |
| 117 | + |
| 118 | +The dashboard provides a simple UI to manually approve/reject tickets during testing. |
| 119 | + |
| 120 | +--- |
| 121 | + |
| 122 | +## Usage |
| 123 | + |
| 124 | +### Synchronous Version |
| 125 | + |
| 126 | +```python |
| 127 | +from blocking_poll_approval_example import approval_agent, request_approval_blocking |
| 128 | + |
| 129 | +# Option 1: Use the tool function directly |
| 130 | +result = request_approval_blocking( |
| 131 | + proposal="Deploy version 2.0 to production", |
| 132 | + context={"priority": "high", "requester": "john.doe"} |
| 133 | +) |
| 134 | +print(result) |
| 135 | +# ✅ APPROVED by jane.smith |
| 136 | +# Reason: Tests passing, staging validated |
| 137 | +# Next Action: Proceed with deployment |
| 138 | + |
| 139 | +# Option 2: Use via ADK AgentRunner |
| 140 | +from google.adk import AgentRunner |
| 141 | + |
| 142 | +agent_runner = AgentRunner(approval_agent) |
| 143 | +result = agent_runner.run( |
| 144 | + user_id="user123", |
| 145 | + new_message="Please get approval for deploying to production" |
| 146 | +) |
| 147 | +``` |
| 148 | + |
| 149 | +### Asynchronous Version (Recommended for Production) |
| 150 | + |
| 151 | +```python |
| 152 | +from blocking_poll_approval_example_async import ( |
| 153 | + approval_agent_async, |
| 154 | + request_approval_blocking_async |
| 155 | +) |
| 156 | +import asyncio |
| 157 | + |
| 158 | +# Option 1: Use the tool function directly |
| 159 | +async def main(): |
| 160 | + result = await request_approval_blocking_async( |
| 161 | + proposal="Deploy version 2.0 to production", |
| 162 | + context={"priority": "high", "requester": "john.doe"} |
| 163 | + ) |
| 164 | + print(result) |
| 165 | + |
| 166 | +asyncio.run(main()) |
| 167 | + |
| 168 | +# Option 2: Use via ADK AgentRunner (async) |
| 169 | +from google.adk import AgentRunner |
| 170 | + |
| 171 | +agent_runner = AgentRunner(approval_agent_async) |
| 172 | +result = await agent_runner.run_async( |
| 173 | + user_id="user123", |
| 174 | + new_message="Please get approval for deploying to production" |
| 175 | +) |
| 176 | +``` |
| 177 | + |
| 178 | +--- |
| 179 | + |
| 180 | +## Testing |
| 181 | + |
| 182 | +### Automated Test |
| 183 | + |
| 184 | +```bash |
| 185 | +# 1. Start mock approval API |
| 186 | +python mock_approval_api.py |
| 187 | + |
| 188 | +# 2. In another terminal, run test |
| 189 | +python test_blocking_poll.py |
| 190 | +``` |
| 191 | + |
| 192 | +Expected output: |
| 193 | +``` |
| 194 | +✅ Approval API is running |
| 195 | +
|
| 196 | +Testing Backend Blocking Poll Pattern |
| 197 | +[Test] Creating approval ticket... |
| 198 | +✅ Ticket created: APR-XXXXXXXX |
| 199 | +[Test] Starting simulated approver (will approve in 5 seconds)... |
| 200 | +[Test] Calling request_approval_blocking (will block until approval)... |
| 201 | +[Test] Blocking poll completed in 5.1 seconds |
| 202 | +
|
| 203 | +[Result]: |
| 204 | +✅ APPROVED by automated_test |
| 205 | +Reason: Auto-approved for testing |
| 206 | +Next Action: Proceed with test |
| 207 | +
|
| 208 | +✅ TEST PASSED: Pattern works correctly! |
| 209 | +``` |
| 210 | + |
| 211 | +### Manual Test |
| 212 | + |
| 213 | +1. Start mock approval API: `python mock_approval_api.py` |
| 214 | +2. Run sync or async example: `python blocking_poll_approval_example.py` |
| 215 | +3. Open dashboard: http://localhost:8003/ |
| 216 | +4. Approve/reject pending ticket in the dashboard |
| 217 | +5. Observe tool returns decision when ticket is decided |
| 218 | + |
| 219 | +--- |
| 220 | + |
| 221 | +## Configuration |
| 222 | + |
| 223 | +All configuration via environment variables: |
| 224 | + |
| 225 | +```bash |
| 226 | +# Approval API settings |
| 227 | +export APPROVAL_API_URL="http://localhost:8003" # API endpoint |
| 228 | +export APPROVAL_POLL_INTERVAL="30" # Seconds between polls |
| 229 | +export APPROVAL_TIMEOUT="600" # Max wait time (10 minutes) |
| 230 | + |
| 231 | +# Optional authentication |
| 232 | +export APPROVAL_API_TOKEN="your-api-token-here" # Bearer token for API auth |
| 233 | + |
| 234 | +# Run example |
| 235 | +python blocking_poll_approval_example_async.py |
| 236 | +``` |
| 237 | + |
| 238 | +--- |
| 239 | + |
| 240 | +## Production Considerations |
| 241 | + |
| 242 | +### Concurrency |
| 243 | + |
| 244 | +Use the **async version** (`blocking_poll_approval_example_async.py`) for production deployments with multiple concurrent approvals. The sync version is suitable for standalone agents or low-volume scenarios (<10 concurrent approvals). |
| 245 | + |
| 246 | +### Security |
| 247 | + |
| 248 | +**Authentication**: Set `APPROVAL_API_TOKEN` environment variable for Bearer token authentication. |
| 249 | + |
| 250 | +**HTTPS**: Configure `APPROVAL_API_URL` to use HTTPS in production (`https://approvals.yourcompany.com`). |
| 251 | + |
| 252 | +**Input Validation**: Proposals are limited to 10,000 characters and cannot be empty. |
| 253 | + |
| 254 | +### Configuration |
| 255 | + |
| 256 | +**Timeouts**: Adjust `APPROVAL_TIMEOUT` based on your workflow: |
| 257 | +- Fast approvals (manager): 300s (5 minutes) |
| 258 | +- Standard approvals: 600s (10 minutes) - default |
| 259 | +- Complex approvals (committee): 1800s (30 minutes) |
| 260 | + |
| 261 | +**Poll Interval**: Balance responsiveness vs. API load with `APPROVAL_POLL_INTERVAL` (default: 30s). |
| 262 | + |
| 263 | +### Monitoring |
| 264 | + |
| 265 | +Monitor these key metrics: |
| 266 | +- Approval creation success rate |
| 267 | +- Average approval duration |
| 268 | +- Timeout rate |
| 269 | +- API error rate |
| 270 | + |
| 271 | +The pattern includes structured logging for all approval lifecycle events. |
| 272 | + |
| 273 | +--- |
| 274 | + |
| 275 | +## Performance Metrics (Production Validation) |
| 276 | + |
| 277 | +This pattern has been validated in production multi-agent workflows: |
| 278 | + |
| 279 | +| Metric | Agent-Level Polling (Anti-Pattern) | Backend Blocking Poll | |
| 280 | +|--------|-------------------------------------|----------------------| |
| 281 | +| **LLM API calls** | 15+ per approval | **1 per approval** | |
| 282 | +| **Manual user clicks** | 20+ "continue" clicks | **0 clicks** | |
| 283 | +| **Application complexity** | High (manual FunctionResponse injection) | **Low (tool handles everything)** | |
| 284 | +| **API call reduction** | Baseline | **93% reduction** | |
| 285 | +| **UX friction** | High (manual polling) | **Minimal (seamless)** | |
| 286 | + |
| 287 | +**Production Workflow Example**: |
| 288 | +- Multi-agent RFQ approval system |
| 289 | +- 10-minute average approval duration |
| 290 | +- Handled gracefully with no manual intervention |
| 291 | +- 93% reduction in LLM API calls vs. agent-level polling |
| 292 | + |
| 293 | +--- |
| 294 | + |
| 295 | +## Comparison with ADK's LongRunningFunctionTool Pattern |
| 296 | + |
| 297 | +### LongRunningFunctionTool Workflow |
| 298 | + |
| 299 | +```python |
| 300 | +# 1. Tool returns "pending" immediately |
| 301 | +def ask_for_approval(context): |
| 302 | + return {"status": "pending", "ticket_id": "xxx"} |
| 303 | + |
| 304 | +# 2. Agent acknowledges pending state |
| 305 | +# 3. External system completes task |
| 306 | +# 4. Application code MUST manually inject FunctionResponse: |
| 307 | +updated_response = types.Part( |
| 308 | + function_response=types.FunctionResponse( |
| 309 | + id=original_call.id, # Must track original call ID |
| 310 | + name=original_call.name, |
| 311 | + response={"status": "approved", ...} |
| 312 | + ) |
| 313 | +) |
| 314 | +await runner.run_async(new_message=types.Content(parts=[updated_response], role="user")) |
| 315 | +``` |
| 316 | + |
| 317 | +**Complexity**: Requires manual tracking of `FunctionCall.id` and constructing `FunctionResponse`. |
| 318 | + |
| 319 | +### Backend Blocking Poll Workflow |
| 320 | + |
| 321 | +```python |
| 322 | +# 1. Agent calls tool once |
| 323 | +result = await request_approval_blocking_async(proposal) |
| 324 | + |
| 325 | +# 2. Tool returns final decision (or timeout) |
| 326 | +# That's it! No manual FunctionResponse injection needed. |
| 327 | +``` |
| 328 | + |
| 329 | +**Simplicity**: Tool handles everything internally. |
| 330 | + |
| 331 | +--- |
| 332 | + |
| 333 | +## Troubleshooting |
| 334 | + |
| 335 | +**"Cannot connect to approval API"** |
| 336 | +- Ensure mock API is running: `python mock_approval_api.py` |
| 337 | +- Verify `APPROVAL_API_URL` is correct |
| 338 | +- Check firewall/network connectivity |
| 339 | + |
| 340 | +**"Approval timeout"** |
| 341 | +- Check approval dashboard at configured URL |
| 342 | +- Increase `APPROVAL_TIMEOUT` if needed |
| 343 | +- Verify approver has access to decision interface |
| 344 | + |
| 345 | +**"Configuration error: APPROVAL_TIMEOUT must be greater than APPROVAL_POLL_INTERVAL"** |
| 346 | +- Ensure `APPROVAL_TIMEOUT` > `APPROVAL_POLL_INTERVAL` (e.g., 600 > 30) |
| 347 | + |
| 348 | +**High API call volume** |
| 349 | +- Increase `APPROVAL_POLL_INTERVAL` to reduce polling frequency (trade-off: slower response time) |
| 350 | + |
| 351 | +--- |
| 352 | + |
| 353 | +## Additional Resources |
| 354 | + |
| 355 | +For questions or feedback: |
| 356 | +- [ADK Documentation](https://google.adk.dev) |
| 357 | +- [ADK GitHub Repository](https://github.com/google/adk-python) |
| 358 | +- Open issues on [ADK GitHub Issues](https://github.com/google/adk-python/issues) |
| 359 | +- Reference this pattern when discussing issues [#3184](https://github.com/google/adk-python/issues/3184) or [#1797](https://github.com/google/adk-python/issues/1797) |
0 commit comments