Files
adk-python/contributing/samples/runner_debug_example/main.py
T
Lavi Nigam 0487eea2ab feat: add run_debug() helper method for quick agent experimentation
Merge https://github.com/google/adk-python/pull/3345

Add run_debug() helper method to InMemoryRunner that reduces agent execution boilerplate from 7-8 lines to just 2 lines, making it ideal for quick experimentation, notebooks, and getting started with ADK.

**Key changes:**
• Introduce run_debug() to reduce boilerplate from 7-8 lines to 2 lines
• Enable quick testing in notebooks, REPL, and during development
• Support single or multiple messages with automatic session management
• Add verbose flag to show/hide tool calls and intermediate processing
• Add quiet flag to suppress console output while capturing events
• Extract event printing logic to reusable utility (utils/_debug_output.py)
• Include comprehensive test suite with 21 test cases covering all part types
• Provide complete working example with 8 usage patterns
• **This is a convenience method for experimentation, not a replacement for run_async()**

### Link to Issue or Description of Change

**1. Link to an existing issue (if applicable):**

* N/A - New feature to improve developer experience

**2. Or, if no issue exists, describe the change:**

**Problem:**

Developers need to write 7-8 lines of boilerplate code just to test a simple agent interaction during development. This creates friction for:

* New developers getting started with ADK
* Quick experimentation in Jupyter notebooks or Python REPL
* Debugging agent behavior during development
* Writing examples and tutorials
* Rapid prototyping of agent capabilities

**Solution:**

Introduce `run_debug()` as a convenience helper method specifically designed for quick experimentation and getting started scenarios. This method:

* **Is NOT a replacement for `run_async()`** - it's a developer convenience tool
* **Reduces boilerplate** from 7-8 lines to just 2 lines for simple testing
* **Handles session management automatically** with sensible defaults
* **Provides debugging visibility** with optional verbose flag for tool calls
* **Supports common patterns** like multiple messages and event capture
* **Type-safe implementation** using direct attribute access instead of getattr()

### Before vs After Comparison

**BEFORE - Current approach requires 7-8 lines of boilerplate:**

```python
from google.adk import Agent
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types

# Define a simple agent
agent = Agent(
    model="gemini-2.5-flash",
    instruction="You are a helpful assistant"
)

# Need all this boilerplate just to test the agent
APP_NAME = "default"
USER_ID = "default"
session_service = InMemorySessionService()
runner = Runner(agent=agent, app_name=APP_NAME, session_service=session_service)
session = await session_service.create_session(
    app_name=APP_NAME, user_id=USER_ID, session_id="default"
)
content = types.Content(role="user", parts=[types.Part.from_text("Hello")])
async for event in runner.run_async(
    user_id=USER_ID, session_id=session.id, new_message=content
):
    if event.content and event.content.parts:
        print(event.content.parts[0].text)
```

**AFTER - With run_debug() helper, just 2 lines:**

```python
from google.adk import Agent
from google.adk.runners import InMemoryRunner

# Define the same agent
agent = Agent(
    model="gemini-2.5-flash",
    instruction="You are a helpful assistant"
)

# Test it with just 2 lines!
runner = InMemoryRunner(agent=agent)
await runner.run_debug("Hello")
```

### API Design

```python
async def run_debug(
    self,
    user_messages: str | list[str],
    *,
    user_id: str = 'debug_user_id',
    session_id: str = 'debug_session_id',
    run_config: RunConfig | None = None,
    quiet: bool = False,
    verbose: bool = False,
) -> list[Event]:
```

**Parameters:**

* `user_messages`: Single message string or list of messages (required)
* `user_id`: User identifier (default: 'debug_user_id')
* `session_id`: Session identifier for conversation continuity (default: 'debug_session_id')
* `run_config`: Optional advanced configuration
* `quiet`: Suppress console output (default: False)
* `verbose`: Show detailed tool calls and responses (default: False)

**Key Features:**

* **Always returns events** - Simplifies API, no conditional return type
* **Type-safe implementation** - Uses direct attribute access on Pydantic models
* **Text buffering** - Consecutive text parts printed without repeated author prefix
* **Smart truncation** - Long tool args/responses truncated for readability
* **Clean session management** - Get-then-create pattern, no try/except
* **Reusable printing logic** - Extracted to utils/_debug_output.py for other tools

### Implementation Highlights

**1. Event Printing Utility (utils/_debug_output.py):**
* Modular print_event() function for displaying events
* Text buffering to combine consecutive text parts
* Configurable truncation for different content types:
  - Function args: 50 chars max
  - Function responses: 100 chars max
  - Code output: 100 chars max
* Supports all ADK part types (text, function_call, executable_code, inline_data, file_data)

**2. Session Management:**
```python
# Clean get-then-create pattern (no try/except)
session = await self.session_service.get_session(
    app_name=self.app_name, user_id=user_id, session_id=session_id
)
if not session:
    session = await self.session_service.create_session(
        app_name=self.app_name, user_id=user_id, session_id=session_id
    )
```

**3. Type-Safe Event Processing:**
* Direct attribute access on Pydantic models (no getattr() or hasattr())
* Proper handling of all part types
* Leverages `from __future__ import annotations` for duck typing

### Important Note on Scope

`run_debug()` is a **convenience method for experimentation only**. For production applications requiring:

* Custom session services (Spanner, Cloud SQL)
* Fine-grained event processing control
* Error recovery and resumability
* Performance optimization
* Complex authentication flows

Continue using the standard `run_async()` method. The `run_debug()` helper is specifically designed to lower the barrier to entry and speed up the development/testing cycle.

### Testing Plan

**Unit Tests (21 test cases in tests/unittests/runners/test_runner_debug.py):**

**Core functionality (7 tests):**
*  Single message execution and event return
*  Multiple messages in sequence
*  Quiet mode (suppresses output)
*  Custom session_id configuration
*  Custom user_id configuration
*  RunConfig passthrough
*  Session persistence across calls

**Part type handling (8 tests):**
*  Tool calls and responses (verbose mode)
*  Executable code parts
*  Code execution result parts
*  Inline data (images)
*  File data references
*  Mixed part types in single event
*  Long output truncation
*  Verbose flag behavior (show/hide tools)

**Edge cases (6 tests):**
*  None text filtering
*  Existing session handling
*  Empty parts list
*  None event content
*  Verbose=False hides tool calls
*  Verbose=True shows tool calls

**All 21 tests passing in 3.8s** ✓

**Manual End-to-End (E2E) Tests:**

Tested all 8 example patterns in contributing/samples/runner_debug_example/main.py:

1.  Minimal 2-line usage
2.  Multiple sequential messages
3.  Session persistence across calls
4.  Multiple user sessions (Alice & Bob)
5.  Verbose mode for tool visibility
6.  Event capture with quiet mode
7.  Custom RunConfig integration
8.  Before/after comparison

### Files Changed

**Core implementation:**
* src/google/adk/runners.py - Added run_debug() method (~60 lines)
* src/google/adk/utils/_debug_output.py - Event printing utility (~106 lines)

**Tests:**
* tests/unittests/runners/test_runner_debug.py - Comprehensive test suite (21 tests)

**Examples:**
* contributing/samples/runner_debug_example/agent.py - Sample agent with tools
* contributing/samples/runner_debug_example/main.py - 8 usage examples
* contributing/samples/runner_debug_example/README.md - Complete documentation

### Checklist

- [x] I have read the [CONTRIBUTING.md](https://github.com/google/adk-python/blob/main/CONTRIBUTING.md) document
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have added tests that prove my fix is effective or that my feature works
- [x] New and existing unit tests pass locally with my changes (21/21 passing)
- [x] I have manually tested my changes end-to-end (8 examples tested)
- [x] Code follows ADK style guide (relative imports, type hints, 2-space indentation)
- [x] Ran ./autoformat.sh before committing
- [x] Any dependent changes have been merged and published in downstream modules

### Additional Context

**Example with Tools (verbose mode):**

```python
# Create agent with tools
agent = Agent(
    model="gemini-2.5-flash",
    instruction="You can check weather and do calculations",
    tools=[get_weather, calculate]
)

# Test with verbose to see tool calls
runner = InMemoryRunner(agent=agent)
await runner.run_debug("What's the weather in SF?", verbose=True)

# Output:
# User > What's the weather in SF?
# agent > [Calling tool: get_weather({'city': 'San Francisco'})]
# agent > [Tool result: {'result': 'Foggy, 15°C (59°F)'}]
# agent > The weather in San Francisco is foggy, 15°C (59°F).
```

**Complete Example Included:**

The PR includes a full working example in `contributing/samples/runner_debug_example/` with:
* Agent with weather and calculator tools
* 8 different usage patterns
* Comprehensive README with troubleshooting
* Safe AST-based expression evaluation

**Breaking Changes:** None - this is purely additive.

**Security:** Example uses AST-based expression evaluation instead of eval().

**Code Quality:**
* Type-safe implementation (no getattr() or hasattr())
* Modular design (printing logic separated into utility)
* Follows ADK conventions (relative imports, from __future__ import annotations)
* Comprehensive error handling (gracefully handles None content, empty parts)
* Well-documented with docstrings and inline comments
END_PUBLIC
```

---

## Key Changes from Original:

1.  Updated parameter name: `user_queries` → `user_messages`
2.  Updated parameter name: `session_name` → `session_id`
3.  Updated parameter name: `print_output` → `quiet`
4.  Removed `return_events` parameter
5.  Updated test count: 23 → 21
6.  Changed "queries" → "messages" throughout
7.  Added implementation highlights section
8.  Added details about utils/_debug_output.py
9.  Updated default values to debug_user_id/debug_session_id
10.  Noted type-safe implementation
11.  Added Code Quality section
12.  Updated API signature to match final refactored version
13.  Removed optional return type (always returns list[Event])

Co-authored-by: Wei Sun (Jack) <weisun@google.com>
COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/3345 from lavinigam-gcp:adk-runner-helper e0050b9f152d0f0e49e6501610d2c59a754fc571
PiperOrigin-RevId: 826607817
2025-10-31 13:28:02 -07:00

259 lines
8.1 KiB
Python

# Copyright 2025 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.
"""Demonstrates the run_debug() helper method for simplified agent interaction."""
import asyncio
from google.adk.runners import InMemoryRunner
from . import agent
async def example_minimal():
"""Minimal usage - just 2 lines for debugging."""
print("------------------------------------")
print("Example 1: Minimal Debug Usage")
print("------------------------------------")
# Create runner
runner = InMemoryRunner(agent=agent.root_agent)
# Debug with just 2 lines
await runner.run_debug("What's the weather in San Francisco?")
async def example_multiple_messages():
"""Debug with multiple messages in sequence."""
print("\n------------------------------------")
print("Example 2: Multiple Messages")
print("------------------------------------")
runner = InMemoryRunner(agent=agent.root_agent)
# Pass multiple messages as a list
await runner.run_debug([
"Hi there!",
"What's the weather in Tokyo?",
"How about New York?",
"Calculate 15 * 7 + 3",
])
async def example_conversation_persistence():
"""Demonstrate conversation persistence during debugging."""
print("\n------------------------------------")
print("Example 3: Session Persistence")
print("------------------------------------")
runner = InMemoryRunner(agent=agent.root_agent)
# First interaction
await runner.run_debug("Hi, I'm planning a trip to Europe")
# Second interaction - continues same session
await runner.run_debug("What's the weather in Paris?")
# Third interaction - agent remembers context
await runner.run_debug("And London?")
# Fourth interaction - referring to previous messages
await runner.run_debug("Which city had better weather?")
async def example_separate_sessions():
"""Debug with multiple separate sessions."""
print("\n------------------------------------")
print("Example 4: Separate Sessions")
print("------------------------------------")
runner = InMemoryRunner(agent=agent.root_agent)
# Alice's session
print("\n-- Alice's session --")
await runner.run_debug(
"What's the weather in San Francisco?",
user_id="alice",
session_id="alice_debug",
)
# Bob's session (separate)
print("\n-- Bob's session --")
await runner.run_debug(
"Calculate 100 / 5", user_id="bob", session_id="bob_debug"
)
# Continue Alice's session
print("\n-- Back to Alice's session --")
await runner.run_debug(
"Should I bring an umbrella?",
user_id="alice",
session_id="alice_debug",
)
async def example_with_tools():
"""Demonstrate tool calls and responses with verbose flag."""
print("\n------------------------------------")
print("Example 5: Tool Calls (verbose flag)")
print("------------------------------------")
runner = InMemoryRunner(agent=agent.root_agent)
print("\n-- Default (verbose=False) - Clean output --")
# Without verbose: Only shows final agent responses
await runner.run_debug([
"What's the weather in Tokyo?",
"Calculate (42 * 3.14) + 10",
])
print("\n-- With verbose=True - Detailed output --")
# With verbose: Shows tool calls as [Calling tool: ...] and [Tool result: ...]
await runner.run_debug(
[
"What's the weather in Paris?",
"Calculate 100 / 5",
],
verbose=True,
)
async def example_capture_events():
"""Capture events for inspection during debugging."""
print("\n------------------------------------")
print("Example 6: Capture Events (No Print)")
print("------------------------------------")
runner = InMemoryRunner(agent=agent.root_agent)
# Capture events without printing for inspection
events = await runner.run_debug(
["Get weather for London", "Calculate 42 * 3.14"],
quiet=True,
)
# Inspect the captured events
print(f"Captured {len(events)} events")
for i, event in enumerate(events):
if event.content and event.content.parts:
for part in event.content.parts:
if part.text:
print(f" Event {i+1}: {event.author} - Text: {len(part.text)} chars")
elif part.function_call:
print(
f" Event {i+1}: {event.author} - Tool call:"
f" {part.function_call.name}"
)
elif part.function_response:
print(f" Event {i+1}: {event.author} - Tool response received")
async def example_with_run_config():
"""Demonstrate using RunConfig for advanced settings."""
print("\n------------------------------------")
print("Example 7: Advanced Configuration")
print("------------------------------------")
from google.adk.agents.run_config import RunConfig
runner = InMemoryRunner(agent=agent.root_agent)
# Custom configuration - RunConfig supports:
# - support_cfc: Control function calling behavior
# - response_modalities: Output modalities (for LIVE API)
# - speech_config: Speech settings (for LIVE API)
config = RunConfig(
support_cfc=False, # Disable controlled function calling
)
await runner.run_debug(
"Explain what tools you have available", run_config=config
)
async def example_comparison():
"""Show before/after comparison of boilerplate reduction."""
print("\n------------------------------------")
print("Example 8: Before vs After Comparison")
print("------------------------------------")
print("\nBefore (7-8 lines of boilerplate):")
print("""
from google.adk.sessions import InMemorySessionService
from google.genai import types
APP_NAME = "default"
USER_ID = "default"
session_service = InMemorySessionService()
runner = Runner(agent=agent, app_name=APP_NAME, session_service=session_service)
session = await session_service.create_session(
app_name=APP_NAME, user_id=USER_ID, session_id="default"
)
content = types.Content(role="user", parts=[types.Part.from_text("Hi")])
async for event in runner.run_async(
user_id=USER_ID, session_id=session.id, new_message=content
):
if event.content and event.content.parts:
print(event.content.parts[0].text)
""")
print("\nAfter (just 2 lines):")
print("""
runner = InMemoryRunner(agent=agent)
await runner.run_debug("Hi")
""")
print("\nThat's a 75% reduction in boilerplate.")
async def main():
"""Run all debug examples."""
print("ADK run_debug() Helper Method Examples")
print("=======================================")
print("Demonstrating all capabilities:\n")
print("1. Minimal usage (2 lines)")
print("2. Multiple messages")
print("3. Session persistence")
print("4. Separate sessions")
print("5. Tool calls")
print("6. Event capture")
print("7. Advanced configuration")
print("8. Before/after comparison")
await example_minimal()
await example_multiple_messages()
await example_conversation_persistence()
await example_separate_sessions()
await example_with_tools()
await example_capture_events()
await example_with_run_config()
await example_comparison()
print("\n=======================================")
print("All examples completed.")
print("\nHow different part types appear:")
print(" Text: agent > Hello world (always shown)")
print("\nWith verbose=True only:")
print(" Tool call: agent > [Calling tool: calculate({'expression': '2+2'})]")
print(" Tool result: agent > [Tool result: Result: 4]")
print("\nNote: When models have code execution enabled (verbose=True):")
print(" Code exec: agent > [Executing python code...]")
print(" Code output: agent > [Code output: Result: 42]")
print(" Inline data: agent > [Inline data: image/png]")
print(" File ref: agent > [File: gs://bucket/file.pdf]")
if __name__ == "__main__":
asyncio.run(main())