Skip to content

Commit 0487eea

Browse files
lavinigam-gcpJacksunwei
authored andcommitted
feat: add run_debug() helper method for quick agent experimentation
Merge #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=#3345 from lavinigam-gcp:adk-runner-helper e0050b9 PiperOrigin-RevId: 826607817
1 parent 0b56f22 commit 0487eea

7 files changed

Lines changed: 1743 additions & 0 deletions

File tree

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
# Runner Debug Helper Example
2+
3+
This example demonstrates the `run_debug()` helper method that simplifies agent interaction for debugging and experimentation in ADK.
4+
5+
## Overview
6+
7+
The `run_debug()` method reduces agent interaction boilerplate from 7-8 lines to just 2 lines, making it ideal for:
8+
9+
- Quick debugging sessions
10+
- Jupyter notebooks
11+
- REPL experimentation
12+
- Writing examples
13+
- Initial agent development
14+
15+
## Files Included
16+
17+
- `agent.py` - Agent with 2 tools: weather and calculate
18+
- `main.py` - 8 examples demonstrating all features
19+
- `README.md` - This documentation
20+
21+
## Setup
22+
23+
### Prerequisites
24+
25+
Set your Google API key:
26+
27+
```bash
28+
export GOOGLE_API_KEY="your-api-key"
29+
```
30+
31+
### Running the Example
32+
33+
```bash
34+
python -m contributing.samples.runner_debug_example.main
35+
```
36+
37+
## Features Demonstrated
38+
39+
1. **Minimal Usage**: Simple 2-line agent interaction
40+
2. **Multiple Messages**: Processing multiple messages in sequence
41+
3. **Session Persistence**: Maintaining conversation context
42+
4. **Separate Sessions**: Managing multiple user sessions
43+
5. **Tool Calls**: Displaying tool invocations and results
44+
6. **Event Capture**: Collecting events for programmatic inspection
45+
7. **Advanced Configuration**: Using RunConfig for custom settings
46+
8. **Comparison**: Before/after boilerplate reduction
47+
48+
## Part Types Supported
49+
50+
The `run_debug()` method properly displays all ADK part types:
51+
52+
| Part Type | Display Format | Use Case |
53+
|-----------|---------------|----------|
54+
| `text` | `agent > {text}` | Regular text responses |
55+
| `function_call` | `agent > [Calling tool: {name}({args})]` | Tool invocations |
56+
| `function_response` | `agent > [Tool result: {response}]` | Tool results |
57+
| `executable_code` | `agent > [Executing {language} code...]` | Code blocks |
58+
| `code_execution_result` | `agent > [Code output: {output}]` | Code execution results |
59+
| `inline_data` | `agent > [Inline data: {mime_type}]` | Images, files, etc. |
60+
| `file_data` | `agent > [File: {uri}]` | File references |
61+
62+
## Tools Available in Example
63+
64+
The example agent includes 2 tools to demonstrate tool handling:
65+
66+
1. **`get_weather(city)`** - Returns mock weather data for major cities
67+
2. **`calculate(expression)`** - Evaluates mathematical expressions safely
68+
69+
## Key Benefits
70+
71+
### Before (7-8 lines)
72+
73+
```python
74+
from google.adk.sessions import InMemorySessionService
75+
from google.genai import types
76+
77+
APP_NAME = "default"
78+
USER_ID = "default"
79+
session_service = InMemorySessionService()
80+
runner = Runner(agent=agent, app_name=APP_NAME, session_service=session_service)
81+
session = await session_service.create_session(
82+
app_name=APP_NAME, user_id=USER_ID, session_id="default"
83+
)
84+
content = types.Content(role="user", parts=[types.Part.from_text("Hi")])
85+
async for event in runner.run_async(
86+
user_id=USER_ID, session_id=session.id, new_message=content
87+
):
88+
if event.content and event.content.parts:
89+
print(event.content.parts[0].text)
90+
```
91+
92+
### After (2 lines)
93+
94+
```python
95+
runner = InMemoryRunner(agent=agent)
96+
await runner.run_debug("Hi")
97+
```
98+
99+
## API Reference
100+
101+
```python
102+
async def run_debug(
103+
self,
104+
user_messages: str | list[str],
105+
*,
106+
user_id: str = 'debug_user_id',
107+
session_id: str = 'debug_session_id',
108+
run_config: Optional[RunConfig] = None,
109+
quiet: bool = False,
110+
verbose: bool = False,
111+
) -> List[Event]:
112+
```
113+
114+
### Parameters
115+
116+
- `user_messages`: Single message string or list of messages (required)
117+
- `user_id`: User identifier for session tracking (default: 'debug_user_id')
118+
- `session_id`: Session identifier for conversation continuity (default: 'debug_session_id')
119+
- `run_config`: Optional advanced configuration
120+
- `quiet`: Whether to suppress output to console (default: False)
121+
- `verbose`: Whether to show detailed tool calls and responses (default: False)
122+
123+
### Usage Examples
124+
125+
```python
126+
# Minimal usage
127+
runner = InMemoryRunner(agent=agent)
128+
await runner.run_debug("What's the weather?")
129+
130+
# Multiple queries
131+
await runner.run_debug(["Query 1", "Query 2", "Query 3"])
132+
133+
# Custom session
134+
await runner.run_debug(
135+
"Hello",
136+
user_id="alice",
137+
session_id="debug_session"
138+
)
139+
140+
# Capture events without printing
141+
events = await runner.run_debug(
142+
"Process this",
143+
quiet=True
144+
)
145+
146+
# Show tool calls with verbose mode
147+
await runner.run_debug(
148+
"What's the weather?",
149+
verbose=True # Shows [Calling tool: ...] and [Tool result: ...]
150+
)
151+
152+
# With custom configuration
153+
from google.adk.agents.run_config import RunConfig
154+
config = RunConfig(support_cfc=False)
155+
await runner.run_debug("Query", run_config=config)
156+
```
157+
158+
## Troubleshooting
159+
160+
### Common Issues and Solutions
161+
162+
1. **Tool calls not showing in output**
163+
- **Issue**: Tool invocations and responses are not displayed
164+
- **Solution**: Set `verbose=True` to see detailed tool interactions:
165+
166+
```python
167+
await runner.run_debug("Query", verbose=True)
168+
```
169+
170+
2. **Import errors when running tests**
171+
- **Issue**: `ModuleNotFoundError: No module named 'google.adk'`
172+
- **Solution**: Ensure you're using the virtual environment:
173+
174+
```bash
175+
source .venv/bin/activate
176+
python -m pytest tests/
177+
```
178+
179+
3. **Session state not persisting between calls**
180+
- **Issue**: Agent doesn't remember previous interactions
181+
- **Solution**: Use the same `user_id` and `session_id` across calls:
182+
183+
```python
184+
await runner.run_debug("First query", user_id="alice", session_id="debug")
185+
await runner.run_debug("Follow-up", user_id="alice", session_id="debug")
186+
```
187+
188+
4. **Output truncation issues**
189+
- **Issue**: Long tool responses are truncated with "..."
190+
- **Solution**: This is by design to keep debug output readable. For full responses, use:
191+
192+
```python
193+
events = await runner.run_debug("Query", quiet=True)
194+
# Process events programmatically for full content
195+
```
196+
197+
5. **API key errors**
198+
- **Issue**: Authentication failures or missing API key
199+
- **Solution**: Ensure your Google API key is set:
200+
201+
```bash
202+
export GOOGLE_API_KEY="your-api-key"
203+
```
204+
205+
## Important Notes
206+
207+
`run_debug()` is designed for debugging and experimentation only. For production use requiring:
208+
209+
- Custom session/memory services (Spanner, Cloud SQL)
210+
- Fine-grained event processing
211+
- Error recovery and resumability
212+
- Performance optimization
213+
214+
Use the standard `run_async()` method instead.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Runner debug example demonstrating simplified agent interaction."""
16+
17+
from . import agent
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Example agent for demonstrating run_debug helper method."""
16+
17+
from google.adk import Agent
18+
from google.adk.tools.tool_context import ToolContext
19+
20+
21+
def get_weather(city: str, tool_context: ToolContext) -> str:
22+
"""Get weather information for a city.
23+
24+
Args:
25+
city: Name of the city to get weather for.
26+
tool_context: Tool context for session state.
27+
28+
Returns:
29+
Weather information as a string.
30+
"""
31+
# Store query history in session state
32+
if "weather_queries" not in tool_context.state:
33+
tool_context.state["weather_queries"] = [city]
34+
else:
35+
tool_context.state["weather_queries"] = tool_context.state[
36+
"weather_queries"
37+
] + [city]
38+
39+
# Mock weather data for demonstration
40+
weather_data = {
41+
"San Francisco": "Foggy, 15°C (59°F)",
42+
"New York": "Sunny, 22°C (72°F)",
43+
"London": "Rainy, 12°C (54°F)",
44+
"Tokyo": "Clear, 25°C (77°F)",
45+
"Paris": "Cloudy, 18°C (64°F)",
46+
}
47+
48+
return weather_data.get(
49+
city, f"Weather data not available for {city}. Try a major city."
50+
)
51+
52+
53+
def calculate(expression: str) -> str:
54+
"""Safely evaluate a mathematical expression.
55+
56+
This tool demonstrates how function calls are displayed in run_debug().
57+
58+
Args:
59+
expression: Mathematical expression to evaluate.
60+
61+
Returns:
62+
Result of the calculation as a string.
63+
"""
64+
import ast
65+
import operator
66+
67+
# Supported operators for safe evaluation
68+
operators = {
69+
ast.Add: operator.add,
70+
ast.Sub: operator.sub,
71+
ast.Mult: operator.mul,
72+
ast.Div: operator.truediv,
73+
ast.Pow: operator.pow,
74+
ast.USub: operator.neg,
75+
}
76+
77+
def _eval(node):
78+
"""Recursively evaluate an AST node."""
79+
if isinstance(node, ast.Expression):
80+
return _eval(node.body)
81+
elif isinstance(node, ast.Constant): # Python 3.8+
82+
return node.value
83+
elif isinstance(node, ast.Num): # For older Python versions
84+
return node.n
85+
elif isinstance(node, ast.BinOp):
86+
op = operators.get(type(node.op))
87+
if op:
88+
return op(_eval(node.left), _eval(node.right))
89+
else:
90+
raise ValueError(f"Unsupported operation: {type(node.op).__name__}")
91+
elif isinstance(node, ast.UnaryOp):
92+
op = operators.get(type(node.op))
93+
if op:
94+
return op(_eval(node.operand))
95+
else:
96+
raise ValueError(f"Unsupported operation: {type(node.op).__name__}")
97+
else:
98+
raise ValueError(f"Unsupported expression type: {type(node).__name__}")
99+
100+
try:
101+
# Parse the expression into an AST
102+
tree = ast.parse(expression, mode="eval")
103+
# Safely evaluate the AST
104+
result = _eval(tree)
105+
return f"Result: {result}"
106+
except (SyntaxError, ValueError) as e:
107+
return f"Error: {str(e)}"
108+
except ZeroDivisionError:
109+
return "Error: Division by zero"
110+
except Exception as e:
111+
return f"Error: {str(e)}"
112+
113+
114+
root_agent = Agent(
115+
model="gemini-2.5-flash-lite",
116+
name="agent",
117+
description="A helpful assistant demonstrating run_debug() helper method",
118+
instruction="""You are a helpful assistant that can:
119+
1. Provide weather information for major cities
120+
2. Perform mathematical calculations
121+
3. Remember previous queries in the conversation
122+
123+
When users ask about weather, use the get_weather tool.
124+
When users ask for calculations, use the calculate tool.
125+
Be friendly and conversational.""",
126+
tools=[get_weather, calculate],
127+
)

0 commit comments

Comments
 (0)