You've already forked adk-python
mirror of
https://github.com/encounter/adk-python.git
synced 2026-03-30 10:57:20 -07:00
0487eea2ab
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
128 lines
3.9 KiB
Python
128 lines
3.9 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.
|
|
|
|
"""Example agent for demonstrating run_debug helper method."""
|
|
|
|
from google.adk import Agent
|
|
from google.adk.tools.tool_context import ToolContext
|
|
|
|
|
|
def get_weather(city: str, tool_context: ToolContext) -> str:
|
|
"""Get weather information for a city.
|
|
|
|
Args:
|
|
city: Name of the city to get weather for.
|
|
tool_context: Tool context for session state.
|
|
|
|
Returns:
|
|
Weather information as a string.
|
|
"""
|
|
# Store query history in session state
|
|
if "weather_queries" not in tool_context.state:
|
|
tool_context.state["weather_queries"] = [city]
|
|
else:
|
|
tool_context.state["weather_queries"] = tool_context.state[
|
|
"weather_queries"
|
|
] + [city]
|
|
|
|
# Mock weather data for demonstration
|
|
weather_data = {
|
|
"San Francisco": "Foggy, 15°C (59°F)",
|
|
"New York": "Sunny, 22°C (72°F)",
|
|
"London": "Rainy, 12°C (54°F)",
|
|
"Tokyo": "Clear, 25°C (77°F)",
|
|
"Paris": "Cloudy, 18°C (64°F)",
|
|
}
|
|
|
|
return weather_data.get(
|
|
city, f"Weather data not available for {city}. Try a major city."
|
|
)
|
|
|
|
|
|
def calculate(expression: str) -> str:
|
|
"""Safely evaluate a mathematical expression.
|
|
|
|
This tool demonstrates how function calls are displayed in run_debug().
|
|
|
|
Args:
|
|
expression: Mathematical expression to evaluate.
|
|
|
|
Returns:
|
|
Result of the calculation as a string.
|
|
"""
|
|
import ast
|
|
import operator
|
|
|
|
# Supported operators for safe evaluation
|
|
operators = {
|
|
ast.Add: operator.add,
|
|
ast.Sub: operator.sub,
|
|
ast.Mult: operator.mul,
|
|
ast.Div: operator.truediv,
|
|
ast.Pow: operator.pow,
|
|
ast.USub: operator.neg,
|
|
}
|
|
|
|
def _eval(node):
|
|
"""Recursively evaluate an AST node."""
|
|
if isinstance(node, ast.Expression):
|
|
return _eval(node.body)
|
|
elif isinstance(node, ast.Constant): # Python 3.8+
|
|
return node.value
|
|
elif isinstance(node, ast.Num): # For older Python versions
|
|
return node.n
|
|
elif isinstance(node, ast.BinOp):
|
|
op = operators.get(type(node.op))
|
|
if op:
|
|
return op(_eval(node.left), _eval(node.right))
|
|
else:
|
|
raise ValueError(f"Unsupported operation: {type(node.op).__name__}")
|
|
elif isinstance(node, ast.UnaryOp):
|
|
op = operators.get(type(node.op))
|
|
if op:
|
|
return op(_eval(node.operand))
|
|
else:
|
|
raise ValueError(f"Unsupported operation: {type(node.op).__name__}")
|
|
else:
|
|
raise ValueError(f"Unsupported expression type: {type(node).__name__}")
|
|
|
|
try:
|
|
# Parse the expression into an AST
|
|
tree = ast.parse(expression, mode="eval")
|
|
# Safely evaluate the AST
|
|
result = _eval(tree)
|
|
return f"Result: {result}"
|
|
except (SyntaxError, ValueError) as e:
|
|
return f"Error: {str(e)}"
|
|
except ZeroDivisionError:
|
|
return "Error: Division by zero"
|
|
except Exception as e:
|
|
return f"Error: {str(e)}"
|
|
|
|
|
|
root_agent = Agent(
|
|
model="gemini-2.5-flash-lite",
|
|
name="agent",
|
|
description="A helpful assistant demonstrating run_debug() helper method",
|
|
instruction="""You are a helpful assistant that can:
|
|
1. Provide weather information for major cities
|
|
2. Perform mathematical calculations
|
|
3. Remember previous queries in the conversation
|
|
|
|
When users ask about weather, use the get_weather tool.
|
|
When users ask for calculations, use the calculate tool.
|
|
Be friendly and conversational.""",
|
|
tools=[get_weather, calculate],
|
|
)
|