-
Notifications
You must be signed in to change notification settings - Fork 2k
Refactor callback system to eliminate code duplication #3113
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- Add CallbackPipeline generic class for type-safe callback execution - Add normalize_callbacks helper to replace 6 duplicate canonical methods - Add CallbackExecutor for plugin + agent callback integration - Add comprehensive test suite (24 test cases, all passing) This is Phase 1-3 and 6 of the refactoring plan. Seeking feedback before proceeding with full implementation. #non-breaking
Response from ADK Triaging Agent Hello @jaywang172, thank you for your contribution! This is a great refactor that will improve the maintainability of the codebase. According to our contribution guidelines, all PRs, other than small documentation or typo fixes, should have an issue associated with them. Could you please create an issue for this refactoring work and link it to this PR? This will help us track the work and ensure that it's aligned with our project goals. Thanks! |
Thank you for the feedback! I've created issue #3126 to track this refactoring work. This PR addresses the code duplication described in the issue by introducing a unified callback pipeline system. |
I apologize for the confusion - I accidentally marked this as "Ready for review" while it's still [WIP]. Current Status:
Request: Converting back to draft until the design is reviewed. Thank you! |
/gemini review |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This is an excellent pull request that successfully refactors the callback system to eliminate significant code duplication. The introduction of a unified CallbackPipeline
is a great approach, leading to more maintainable and type-safe code. The new components are well-designed, and the test suite is comprehensive, covering various scenarios and ensuring backward compatibility. I have a couple of minor suggestions to further improve the new module by removing unused type variables and reducing a small amount of internal code duplication. Overall, this is a high-quality contribution that significantly improves the codebase.
TInput = TypeVar('TInput') | ||
TOutput = TypeVar('TOutput') | ||
TCallback = TypeVar('TCallback', bound=Callable) | ||
|
||
|
||
class CallbackPipeline(Generic[TInput, TOutput]): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The TypeVar
s TInput
and TCallback
are defined but are not used within the CallbackPipeline
class or anywhere else in the module. This adds unnecessary code and can be confusing for developers reading it. To improve clarity and maintainability, I recommend removing these unused type variables and updating the CallbackPipeline
's generic signature to only include TOutput
.
TInput = TypeVar('TInput') | |
TOutput = TypeVar('TOutput') | |
TCallback = TypeVar('TCallback', bound=Callable) | |
class CallbackPipeline(Generic[TInput, TOutput]): | |
TOutput = TypeVar('TOutput') | |
class CallbackPipeline(Generic[TOutput]): |
# Step 1: Execute plugin callback (priority) | ||
result = plugin_callback(*args, **kwargs) | ||
if inspect.isawaitable(result): | ||
result = await result | ||
|
||
if result is not None: | ||
return result | ||
|
||
# Step 2: Execute agent callbacks if plugin returned None | ||
if agent_callbacks: | ||
pipeline = CallbackPipeline(callbacks=agent_callbacks) | ||
result = await pipeline.execute(*args, **kwargs) | ||
|
||
return result |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic to execute the plugin_callback
and handle its potentially awaitable
result is a reimplementation of what CallbackPipeline.execute
already does. This introduces some code duplication within the new module.
You can refactor this method to reuse CallbackPipeline
for executing the plugin_callback
as well. This will make the code more concise and maintainable by centralizing the callback execution logic.
# Step 1: Execute plugin callback (priority) | |
result = plugin_callback(*args, **kwargs) | |
if inspect.isawaitable(result): | |
result = await result | |
if result is not None: | |
return result | |
# Step 2: Execute agent callbacks if plugin returned None | |
if agent_callbacks: | |
pipeline = CallbackPipeline(callbacks=agent_callbacks) | |
result = await pipeline.execute(*args, **kwargs) | |
return result | |
# Step 1: Execute plugin callback (priority) | |
result = await CallbackPipeline([plugin_callback]).execute(*args, **kwargs) | |
if result is not None: | |
return result | |
# Step 2: Execute agent callbacks if plugin returned None | |
return await CallbackPipeline(agent_callbacks).execute(*args, **kwargs) |
- Remove unused TypeVars (TInput, TCallback) - Simplify CallbackExecutor by reusing CallbackPipeline - Reduces code duplication and improves maintainability Addresses feedback from gemini-code-assist bot review
/gemini review |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This is an excellent refactoring that successfully eliminates significant code duplication in the callback system. The introduction of CallbackPipeline
and normalize_callbacks
is a clean and effective solution. The new module is well-structured, and the accompanying unit tests are thorough, covering a wide range of scenarios, including edge cases and backward compatibility, which is crucial for a change of this nature. I have one suggestion to further refine the CallbackExecutor
for better performance and clarity.
@staticmethod | ||
async def execute_with_plugins( | ||
plugin_callback: Callable, | ||
agent_callbacks: list[Callable], | ||
*args: Any, | ||
**kwargs: Any, | ||
) -> Optional[Any]: | ||
"""Executes plugin and agent callbacks in order. | ||
Execution order: | ||
1. Plugin callback (priority) | ||
2. Agent callbacks (if plugin returns None) | ||
Args: | ||
plugin_callback: The plugin callback function to execute first. | ||
agent_callbacks: List of agent callbacks to execute if plugin returns | ||
None. | ||
*args: Positional arguments passed to callbacks | ||
**kwargs: Keyword arguments passed to callbacks | ||
Returns: | ||
First non-None result from plugin or agent callbacks, or None. | ||
Example: | ||
>>> result = await CallbackExecutor.execute_with_plugins( | ||
... plugin_callback=lambda: plugin_manager.run_before_model_callback( | ||
... callback_context=ctx, | ||
... llm_request=request, | ||
... ), | ||
... agent_callbacks=normalize_callbacks(agent.before_model_callback), | ||
... callback_context=ctx, | ||
... llm_request=request, | ||
... ) | ||
""" | ||
# Step 1: Execute plugin callback (priority) | ||
result = await CallbackPipeline([plugin_callback]).execute(*args, **kwargs) | ||
if result is not None: | ||
return result | ||
|
||
# Step 2: Execute agent callbacks if plugin returned None | ||
return await CallbackPipeline(agent_callbacks).execute(*args, **kwargs) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The implementation of execute_with_plugins
can be made more efficient and direct. It currently creates a CallbackPipeline
instance to run a single plugin_callback
, which adds unnecessary overhead. You can optimize this by calling the plugin_callback
directly and handling its potential awaitable result, which also makes the priority logic more explicit.
Additionally, the example in the docstring is incorrect as the lambda function for plugin_callback
would cause a TypeError
at runtime. The example should be updated to pass the callback function directly for correctness and clarity.
@staticmethod
async def execute_with_plugins(
plugin_callback: Callable,
agent_callbacks: list[Callable],
*args: Any,
**kwargs: Any,
) -> Optional[Any]:
"""Executes plugin and agent callbacks in order.
Execution order:
1. Plugin callback (priority)
2. Agent callbacks (if plugin returns None)
Args:
plugin_callback: The plugin callback function to execute first.
agent_callbacks: List of agent callbacks to execute if plugin returns
None.
*args: Positional arguments passed to callbacks
**kwargs: Keyword arguments passed to callbacks
Returns:
First non-None result from plugin or agent callbacks, or None.
Example:
>>> # Assuming `plugin_manager` is an instance available on the context `ctx`
>>> result = await CallbackExecutor.execute_with_plugins(
... plugin_callback=ctx.plugin_manager.run_before_model_callback,
... agent_callbacks=normalize_callbacks(agent.before_model_callback),
... callback_context=ctx,
... llm_request=request,
... )
"""
# Step 1: Execute plugin callback (priority)
result = plugin_callback(*args, **kwargs)
if inspect.isawaitable(result):
result = await result
if result is not None:
return result
# Step 2: Execute agent callbacks if plugin returned None
return await CallbackPipeline(agent_callbacks).execute(*args, **kwargs)
- Execute plugin_callback directly instead of wrapping in CallbackPipeline - Makes plugin callback priority more explicit - Fixes incorrect lambda in docstring example - Reduces unnecessary overhead for single callback execution Addresses feedback from gemini-code-assist bot review (round 2)
/gemini review |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This is an excellent refactoring that greatly simplifies the callback system and reduces code duplication. The new CallbackPipeline
and helper functions are well-designed and the accompanying tests are thorough, providing high confidence in the changes. I have a couple of suggestions to further improve the design and maintainability of the new callback_pipeline
module, mainly around centralizing logic and improving type hints.
class CallbackExecutor: | ||
"""Unified executor for plugin and agent callbacks. | ||
This class coordinates the execution order of plugin callbacks and agent | ||
callbacks: | ||
1. Execute plugin callback first (higher priority) | ||
2. If plugin returns None, execute agent callbacks | ||
3. Return first non-None result | ||
This pattern is used in: | ||
- Before/after agent callbacks | ||
- Before/after model callbacks | ||
- Before/after tool callbacks | ||
""" | ||
|
||
@staticmethod | ||
async def execute_with_plugins( | ||
plugin_callback: Callable, | ||
agent_callbacks: list[Callable], | ||
*args: Any, | ||
**kwargs: Any, | ||
) -> Optional[Any]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a great refactoring that significantly reduces duplication! I have a couple of suggestions for the CallbackExecutor
that could further improve the design and type safety.
-
Refactor to a module-level function: The
CallbackExecutor
class currently acts as a namespace for a single static method. In Python, it's often more idiomatic to use a module-level function in such cases. This would simplify the code by removing a layer of abstraction. -
Improve type safety with
TypeVar
: The return typeOptional[Any]
onexecute_with_plugins
is quite broad. Since a key goal of this PR is to improve type safety, we can introduce aTypeVar
to make the return type more specific and align it with theCallbackPipeline
's generic nature.
Combining these, you could have something like this:
TOutput = TypeVar('TOutput')
async def execute_callbacks_with_plugin_priority(
plugin_callback: Callable[..., Optional[TOutput]],
agent_callbacks: list[Callable[..., Optional[TOutput]]],
*args: Any,
**kwargs: Any,
) -> Optional[TOutput]:
"""Executes plugin and agent callbacks in order, with plugins having priority."""
# Step 1: Execute plugin callback (priority)
result = plugin_callback(*args, **kwargs)
if inspect.isawaitable(result):
result = await result
if result is not None:
return result
# Step 2: Execute agent callbacks if plugin returned None
pipeline: CallbackPipeline[TOutput] = CallbackPipeline(agent_callbacks)
return await pipeline.execute(*args, **kwargs)
This would make the API simpler to use (execute_callbacks_with_plugin_priority(...)
instead of CallbackExecutor.execute_with_plugins(...)
) and provide stronger type guarantees to callers.
result = plugin_callback(*args, **kwargs) | ||
if inspect.isawaitable(result): | ||
result = await result | ||
|
||
if result is not None: | ||
return result |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's an opportunity to reduce code duplication. The logic for handling both synchronous and asynchronous callbacks by checking inspect.isawaitable
is repeated here and in CallbackPipeline.execute
(lines 114-116).
To keep the code DRY (Don't Repeat Yourself), you could extract this into a private helper function. This would centralize the sync/async handling and improve maintainability.
For example:
async def _run_one_callback(callback: Callable, *args: Any, **kwargs: Any) -> Any:
"""Executes a single callback, handling both sync and async functions."""
result = callback(*args, **kwargs)
if inspect.isawaitable(result):
result = await result
return result
You could then use this helper in both CallbackPipeline.execute
and here.
This commit completes the callback system refactoring by replacing all 6 duplicate canonical methods with the unified normalize_callbacks function. Phase 4 (LlmAgent): - Remove 4 canonical methods: before_model, after_model, before_tool, after_tool - Update base_llm_flow.py to use normalize_callbacks (2 locations) - Update functions.py to use normalize_callbacks (4 locations) - Deleted: 53 lines of duplicate code Phase 5 (BaseAgent): - Remove 2 canonical methods: before_agent, after_agent - Update callback execution logic - Deleted: 22 lines of duplicate code Overall impact: - Total deleted: 110 lines (mostly duplicated code) - Total added: 26 lines (imports + normalize_callbacks calls) - Net reduction: 84 lines (-77%) - All unit tests passing: 24/24 - Lint score: 9.49/10 - Zero breaking changes #non-breaking
Update: Phase 4+5 CompleteI've extended the PR to include the actual refactoring work using the Phase 4 (LlmAgent):
Phase 5 (BaseAgent):
Impact:
The refactoring is now complete and ready for full review! |
/gemini review |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This is an excellent refactoring that significantly reduces code duplication by introducing a unified callback pipeline. The new callback_pipeline.py
module is well-designed, type-safe, and thoroughly tested. My feedback focuses on fully leveraging the new CallbackPipeline
component in the refactored areas. As you've noted in the PR description, this is pending feedback, and applying these suggestions will help complete the refactoring and further improve code maintainability and consistency.
src/google/adk/agents/base_agent.py
Outdated
if not before_agent_callback_content and callbacks: | ||
for callback in callbacks: | ||
before_agent_callback_content = callback( | ||
callback_context=callback_context | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This block manually iterates through callbacks, which duplicates the logic already implemented in the new CallbackPipeline
. To better align with the refactoring's goal of eliminating code duplication, you can use CallbackPipeline
to execute the callbacks. This will also correctly handle both sync and async callbacks and early exit on the first non-None result.
if not before_agent_callback_content and callbacks:
pipeline = CallbackPipeline(callbacks)
before_agent_callback_content = await pipeline.execute(
callback_context=callback_context
)
src/google/adk/agents/base_agent.py
Outdated
if not after_agent_callback_content and callbacks: | ||
for callback in callbacks: | ||
after_agent_callback_content = callback( | ||
callback_context=callback_context | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to __handle_before_agent_callback
, this block duplicates the logic of CallbackPipeline
. Using CallbackPipeline
here will make the code more consistent and maintainable.
if not after_agent_callback_content and callbacks:
pipeline = CallbackPipeline(callbacks)
after_agent_callback_content = await pipeline.execute(
callback_context=callback_context
)
... ) | ||
""" | ||
# Step 1: Execute plugin callback (priority) | ||
result = await CallbackPipeline([plugin_callback]).execute(*args, **kwargs) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Creating a CallbackPipeline
instance for a single plugin callback introduces a small but unnecessary overhead. Since you're only executing one callback, you can handle it directly. This would involve calling it and awaiting the result if it's a coroutine, which mirrors the logic inside CallbackPipeline.execute
but avoids the object creation.
result = await CallbackPipeline([plugin_callback]).execute(*args, **kwargs) | |
result = plugin_callback(*args, **kwargs) | |
if inspect.isawaitable(result): | |
result = await result |
if not callbacks: | ||
return | ||
for callback in agent.canonical_before_model_callbacks: | ||
for callback in callbacks: | ||
callback_response = callback( | ||
callback_context=callback_context, llm_request=llm_request | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This block manually handles callback execution, which is now encapsulated within CallbackPipeline
. Using the pipeline here would simplify the code and make it more consistent with the new callback system. It will correctly handle sync/async callbacks and early exit.
if not callbacks: | |
return | |
for callback in agent.canonical_before_model_callbacks: | |
for callback in callbacks: | |
callback_response = callback( | |
callback_context=callback_context, llm_request=llm_request | |
) | |
if not callbacks: | |
return | |
pipeline = CallbackPipeline(callbacks) | |
callback_response = await pipeline.execute( | |
callback_context=callback_context, llm_request=llm_request | |
) | |
if callback_response: | |
return callback_response |
if not callbacks: | ||
return await _maybe_add_grounding_metadata() | ||
for callback in agent.canonical_after_model_callbacks: | ||
for callback in callbacks: | ||
callback_response = callback( | ||
callback_context=callback_context, llm_response=llm_response | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to _handle_before_model_callback
, this can be simplified by using CallbackPipeline
to execute the callbacks. This will improve consistency and reduce duplicated logic.
if not callbacks:
return await _maybe_add_grounding_metadata()
pipeline = CallbackPipeline(callbacks)
callback_response = await pipeline.execute(
callback_context=callback_context, llm_response=llm_response
)
if callback_response:
return await _maybe_add_grounding_metadata(callback_response)
return await _maybe_add_grounding_metadata()
for callback in normalize_callbacks(agent.before_tool_callback): | ||
function_response = callback( | ||
tool=tool, args=function_args, tool_context=tool_context | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This loop can be simplified by using the new CallbackPipeline
to execute the before_tool_callback
callbacks. This will align with the refactoring's goal of centralizing callback execution logic.
pipeline = CallbackPipeline(normalize_callbacks(agent.before_tool_callback))
function_response = await pipeline.execute(
tool=tool, args=function_args, tool_context=tool_context
)
for callback in normalize_callbacks(agent.after_tool_callback): | ||
altered_function_response = callback( | ||
tool=tool, | ||
args=function_args, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly, CallbackPipeline
can be used here to execute the after_tool_callback
callbacks, making the code more concise and consistent.
pipeline = CallbackPipeline(normalize_callbacks(agent.after_tool_callback))
altered_function_response = await pipeline.execute(
tool=tool,
args=function_args,
tool_context=tool_context,
tool_response=function_response,
)
for callback in normalize_callbacks(agent.before_tool_callback): | ||
function_response = callback( | ||
tool=tool, args=function_args, tool_context=tool_context | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is another place where CallbackPipeline
can be used to simplify the callback execution logic for before_tool_callback
in live mode.
pipeline = CallbackPipeline(normalize_callbacks(agent.before_tool_callback))
function_response = await pipeline.execute(
tool=tool, args=function_args, tool_context=tool_context
)
for callback in normalize_callbacks(agent.after_tool_callback): | ||
altered_function_response = callback( | ||
tool=tool, | ||
args=function_args, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using CallbackPipeline
here for after_tool_callback
in live mode will maintain consistency with the other refactored callback sites.
pipeline = CallbackPipeline(normalize_callbacks(agent.after_tool_callback))
altered_function_response = await pipeline.execute(
tool=tool,
args=function_args,
tool_context=tool_context,
tool_response=function_response,
)
… sites Address bot feedback (round 4) by replacing all manual callback iterations with CallbackPipeline.execute() for consistency and maintainability. Changes (9 locations): 1. base_agent.py: Use CallbackPipeline for before/after agent callbacks 2. callback_pipeline.py: Optimize single plugin callback execution 3. base_llm_flow.py: Use CallbackPipeline for before/after model callbacks 4. functions.py: Use CallbackPipeline for all tool callbacks (async + live) Impact: - Eliminates remaining manual callback iteration logic (~40 lines) - Achieves 100% consistency in callback execution - All sync/async handling and early exit logic centralized - Tests: 24/24 passing - Lint: 9.57/10 (improved from 9.49/10) #non-breaking
Update: Bot Round 4 Feedback AddressedI've implemented all 9 suggestions from the latest bot review to achieve 100% consistency in callback execution: Changes:
Impact:
The refactoring is now complete with full consistency across the codebase! |
/gemini review |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request is a great step towards reducing code duplication in the callback system. The introduction of CallbackPipeline
and normalize_callbacks
significantly cleans up the codebase by removing the repetitive canonical_*_callbacks
methods. The changes are well-tested with 100% coverage for the new module.
My main feedback is focused on the new CallbackExecutor
abstraction. While it's a good idea, it has a limitation regarding differing callback signatures that prevents its universal adoption. I've also suggested a small refinement to its implementation to further reduce code duplication by reusing CallbackPipeline
.
Overall, this is a solid refactoring. Addressing the feedback on CallbackExecutor
will make the new callback system even more robust and reusable for the next phases of this work.
async def execute_with_plugins( | ||
plugin_callback: Callable, | ||
agent_callbacks: list[Callable], | ||
*args: Any, | ||
**kwargs: Any, | ||
) -> Optional[Any]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a great abstraction to unify plugin and agent callback execution. I have a couple of suggestions to make it even more robust and reusable:
-
Signature Mismatch Issue: There's a potential issue with passing the same
*args
and**kwargs
to bothplugin_callback
andagent_callbacks
. This assumes they have compatible signatures, which isn't always the case. For example, forbefore_agent_callback
, the plugin callback accepts anagent
argument while the agent-defined callbacks do not. This would cause aTypeError
. To make this more robust, you could consider inspecting signatures to filter kwargs, or documenting that callers must adapt signatures (e.g., withlambda
). -
Reuse
CallbackPipeline
: The logic for executing theplugin_callback
(handlingawaitable
and checking forNone
) is a reimplementation of whatCallbackPipeline
already does. You can reuseCallbackPipeline
for the plugin callback to avoid this duplication. For example, lines 241-248 could be simplified to:# Step 1: Execute plugin callback (priority) result = await CallbackPipeline([plugin_callback]).execute(*args, **kwargs) if result is not None: return result # Step 2: Execute agent callbacks if plugin returned None return await CallbackPipeline(agent_callbacks).execute(*args, **kwargs)
I noticed this utility isn't used in base_agent.py
(likely due to the signature mismatch), but it could be used in places like base_llm_flow.py
's _handle_before_model_callback
to simplify the logic, since the signatures match there. Addressing the flexibility issue would make CallbackExecutor
more universally applicable for the next phases of your refactoring.
Thank you for the continued thorough review and the positive feedback on the overall refactoring! Regarding the CallbackExecutor optimization:I notice this suggestion creates an interesting trade-off that highlights why human review is valuable: Round 3 feedback: "Creating a CallbackPipeline instance for a single plugin callback introduces unnecessary overhead. You can optimize this by calling it directly." Round 5 feedback: "You can reuse CallbackPipeline for the plugin callback to avoid code duplication." Both perspectives are valid - it's a classic performance vs consistency trade-off. The current implementation follows Round 3's guidance to avoid the overhead of pipeline instantiation for a single callback. Regarding the signature mismatch issue:You're absolutely correct about the signature mismatch limitation. This is a fundamental API design constraint: # Plugin callback signature
plugin_manager.run_before_agent_callback(agent=self, callback_context=ctx)
# Agent callback signature
agent.before_agent_callback(callback_context=ctx) # No 'agent' param This is precisely why This is by design, not a bug - different callback types legitimately require different parameters based on their context. Current Status:All original goals achieved:
Bot feedback addressed:
Next Steps:I believe the refactoring is complete and ready for human maintainer review. The remaining design decisions (micro-optimization vs consistency, CallbackExecutor applicability, etc.) would benefit from maintainer input on the team's priorities and coding standards. Thank you again for the incredibly detailed automated reviews - they've significantly improved the code quality! Note: This is my first contribution to ADK. I'm learning your team's review process and greatly appreciate the feedback loop, even when suggestions evolve as the code does! |
@Jacksunwei Hi! This PR is now ready for maintainer review. SummaryThis refactoring eliminates 117 lines of duplicated code (-80%) in the callback system by introducing a unified What was changed:
Impact:
Bot review addressed:
Testing# All tests passing
pytest tests/unittests/agents/test_callback_pipeline.py -v
# 24/24 passed
# Lint check
pylint src/google/adk/agents/callback_pipeline.py
# Score: 9.57/10 Files Changed
Related IssueCloses #3126 This is my first contribution to ADK - I'm happy to make any adjustments based on your feedback! cc @seanzhou1023 (in case you'd like to review the core changes as well) |
[WIP] Refactor callback system to eliminate code duplication
Problem
The current callback system has significant code duplication:
canonical_*_callbacks
methods (42 lines of repeated code)Solution
Introduce a unified callback pipeline system:
Core Components
CallbackPipeline[TInput, TOutput]
- Type-safe generic pipeline executornormalize_callbacks()
- Replaces all 6 duplicate canonical methodsCallbackExecutor
- Integrates plugin + agent callbacksCode Example
Testing
Changes
New Files:
src/google/adk/agents/callback_pipeline.py
(+242 lines)tests/unittests/agents/test_callback_pipeline.py
(+391 lines)Stats:
Impact
Implementation Phases
This PR represents Phase 1-3 and 6 of a 10-phase refactoring plan:
Seeking feedback before proceeding with Phase 4-10.
Why This Approach?
Questions?
This is my first contribution to ADK. I'm happy to:
Looking forward to your review!