Skip to content

Commit cd72b0a

Browse files
pwwpchecopybara-github
authored andcommitted
feat: Implement PluginService for registering and executing plugins
PluginService takes the registration of plugins, and provide the wrapper utilities to execute all plugins. PiperOrigin-RevId: 769834609
1 parent 5a721d9 commit cd72b0a

File tree

5 files changed

+1091
-0
lines changed

5 files changed

+1091
-0
lines changed

src/google/adk/plugins/__init__.py

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 in 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+
from .base_plugin import BasePlugin
16+
17+
__all__ = ['BasePlugin']

src/google/adk/plugins/base_plugin.py

Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
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 in 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+
from __future__ import annotations
16+
17+
from abc import ABC
18+
from typing import Any
19+
from typing import Optional
20+
from typing import TYPE_CHECKING
21+
from typing import TypeVar
22+
23+
from google.genai import types
24+
25+
from ..agents.base_agent import BaseAgent
26+
from ..agents.callback_context import CallbackContext
27+
from ..events.event import Event
28+
from ..models.llm_request import LlmRequest
29+
from ..models.llm_response import LlmResponse
30+
from ..tools.base_tool import BaseTool
31+
from ..utils.feature_decorator import working_in_progress
32+
33+
if TYPE_CHECKING:
34+
from ..agents.invocation_context import InvocationContext
35+
from ..tools.tool_context import ToolContext
36+
37+
38+
# Type alias: The value may or may not be awaitable, and value is optional.
39+
T = TypeVar("T")
40+
41+
42+
@working_in_progress(
43+
"Plugin is under development now. Check again around Jul. 2025"
44+
)
45+
class BasePlugin(ABC):
46+
"""Base class for creating plugins.
47+
48+
Plugins provide a structured way to intercept and modify agent, tool, and
49+
LLM behaviors at critical execution points in a callback manner. While agent
50+
callbacks apply to a particular agent, plugins applies globally to all
51+
agents added in the runner. Plugins are best used for adding custom behaviors
52+
like logging, monitoring, caching, or modifying requests and responses at key
53+
stages.
54+
55+
A plugin can implement one or more methods of callbacks, but should not
56+
implement the same method of callback for multiple times.
57+
58+
Relation with [Agent callbacks](https://google.github.io/adk-docs/callbacks/):
59+
60+
**Execution Order**
61+
Similar to Agent callbacks, Plugins are executed in the order they are
62+
registered. However, Plugin and Agent Callbacks are executed sequentially,
63+
with Plugins takes precedence over agent callbacks. When the callback in a
64+
plugin returns a value, it will short circuit all remaining plugins and
65+
agent callbacks, causing all remaining plugins and agent callbacks
66+
to be skipped.
67+
68+
**Change Propagation**
69+
Plugins and agent callbacks can both modify the value of the input parameters,
70+
including agent input, tool input, and LLM request/response, etc. They work in
71+
the exactly same way. The modifications will be visible and passed to the next
72+
callback in the chain. For example, if a plugin modifies the tool input with
73+
before_tool_callback, the modified tool input will be passed to the
74+
before_tool_callback of the next plugin, and further passed to the agent
75+
callbacks if not short circuited.
76+
77+
To use a plugin, implement the desired callback methods and pass an instance
78+
of your custom plugin class to the ADK Runner.
79+
80+
Examples:
81+
A simple plugin that logs every tool call.
82+
83+
>>> class ToolLoggerPlugin(BasePlugin):
84+
.. def __init__(self):
85+
.. super().__init__(name="tool_logger")
86+
..
87+
.. async def before_tool_callback(
88+
.. self, *, tool: BaseTool, tool_args: dict[str, Any],
89+
tool_context:
90+
ToolContext
91+
.. ):
92+
.. print(f"[{self.name}] Calling tool '{tool.name}' with args:
93+
{tool_args}")
94+
..
95+
.. async def after_tool_callback(
96+
.. self, *, tool: BaseTool, tool_args: dict, tool_context:
97+
ToolContext, result: dict
98+
.. ):
99+
.. print(f"[{self.name}] Tool '{tool.name}' finished with result:
100+
{result}")
101+
..
102+
>>> # Add the plugin to ADK Runner
103+
>>> # runner = Runner(
104+
>>> # ...
105+
>>> # plugins=[ToolLoggerPlugin(), AgentPolicyPlugin()],
106+
>>> # )
107+
"""
108+
109+
def __init__(self, name: str):
110+
"""Initializes the plugin.
111+
112+
Args:
113+
name: A unique identifier for this plugin instance.
114+
"""
115+
super().__init__()
116+
self.name = name
117+
118+
async def on_user_message_callback(
119+
self,
120+
*,
121+
invocation_context: InvocationContext,
122+
user_message: types.Content,
123+
) -> Optional[types.Content]:
124+
"""Callback executed when a user message is received before an invocation starts.
125+
126+
This callback helps logging and modifying the user message before the
127+
runner starts the invocation.
128+
129+
Args:
130+
invocation_context: The context for the entire invocation.
131+
user_message: The message content input by user.
132+
133+
Returns:
134+
An optional `types.Content` to be returned to the ADK. Returning a
135+
value to replace the user message. Returning `None` to proceed
136+
normally.
137+
"""
138+
pass
139+
140+
async def before_run_callback(
141+
self, *, invocation_context: InvocationContext
142+
) -> Optional[types.Content]:
143+
"""Callback executed before the ADK runner runs.
144+
145+
This is the first callback to be called in the lifecycle, ideal for global
146+
setup or initialization tasks.
147+
148+
Args:
149+
invocation_context: The context for the entire invocation, containing
150+
session information, the root agent, etc.
151+
152+
Returns:
153+
An optional `Event` to be returned to the ADK. Returning a value to
154+
halt execution of the runner and ends the runner with that event. Return
155+
`None` to proceed normally.
156+
"""
157+
pass
158+
159+
async def on_event_callback(
160+
self, *, invocation_context: InvocationContext, event: Event
161+
) -> Optional[Event]:
162+
"""Callback executed after an event is yielded from runner.
163+
164+
This is the ideal place to make modification to the event before the event
165+
is handled by the underlying agent app.
166+
167+
Args:
168+
invocation_context: The context for the entire invocation.
169+
event: The event raised by the runner.
170+
171+
Returns:
172+
An optional value. A non-`None` return may be used by the framework to
173+
modify or replace the response. Returning `None` allows the original
174+
response to be used.
175+
"""
176+
pass
177+
178+
async def after_run_callback(
179+
self, *, invocation_context: InvocationContext
180+
) -> Optional[None]:
181+
"""Callback executed after an ADK runner run has completed.
182+
183+
This is the final callback in the ADK lifecycle, suitable for cleanup, final
184+
logging, or reporting tasks.
185+
186+
Args:
187+
invocation_context: The context for the entire invocation.
188+
189+
Returns:
190+
None
191+
"""
192+
pass
193+
194+
async def before_agent_callback(
195+
self, *, agent: BaseAgent, callback_context: CallbackContext
196+
) -> Optional[types.Content]:
197+
"""Callback executed before an agent's primary logic is invoked.
198+
199+
This callback can be used for logging, setup, or to short-circuit the
200+
agent's execution by returning a value.
201+
202+
Args:
203+
agent: The agent that is about to run.
204+
callback_context: The context for the agent invocation.
205+
206+
Returns:
207+
An optional `types.Content` object. If a value is returned, it will bypass
208+
the agent's callbacks and its execution, and return this value directly.
209+
Returning `None` allows the agent to proceed normally.
210+
"""
211+
pass
212+
213+
async def after_agent_callback(
214+
self, *, agent: BaseAgent, callback_context: CallbackContext
215+
) -> Optional[types.Content]:
216+
"""Callback executed after an agent's primary logic has completed.
217+
218+
This callback can be used to inspect, log, or modify the agent's final
219+
result before it is returned.
220+
221+
Args:
222+
agent: The agent that has just run.
223+
callback_context: The context for the agent invocation.
224+
225+
Returns:
226+
An optional `types.Content` object. If a value is returned, it will
227+
replace the agent's original result. Returning `None` uses the original,
228+
unmodified result.
229+
"""
230+
pass
231+
232+
async def before_model_callback(
233+
self, *, callback_context: CallbackContext, llm_request: LlmRequest
234+
) -> Optional[LlmResponse]:
235+
"""Callback executed before a request is sent to the model.
236+
237+
This provides an opportunity to inspect, log, or modify the `LlmRequest`
238+
object. It can also be used to implement caching by returning a cached
239+
`LlmResponse`, which would skip the actual model call.
240+
241+
Args:
242+
callback_context: The context for the current agent call.
243+
llm_request: The prepared request object to be sent to the model.
244+
245+
Returns:
246+
An optional value. The interpretation of a non-`None` trigger an early
247+
exit and returns the response immediately. Returning `None` allows the LLM
248+
request to proceed normally.
249+
"""
250+
pass
251+
252+
async def after_model_callback(
253+
self, *, callback_context: CallbackContext, llm_response: LlmResponse
254+
) -> Optional[LlmResponse]:
255+
"""Callback executed after a response is received from the model.
256+
257+
This is the ideal place to log model responses, collect metrics on token
258+
usage, or perform post-processing on the raw `LlmResponse`.
259+
260+
Args:
261+
callback_context: The context for the current agent call.
262+
llm_response: The response object received from the model.
263+
264+
Returns:
265+
An optional value. A non-`None` return may be used by the framework to
266+
modify or replace the response. Returning `None` allows the original
267+
response to be used.
268+
"""
269+
pass
270+
271+
async def before_tool_callback(
272+
self,
273+
*,
274+
tool: BaseTool,
275+
tool_args: dict[str, Any],
276+
tool_context: ToolContext,
277+
) -> Optional[dict]:
278+
"""Callback executed before a tool is called.
279+
280+
This callback is useful for logging tool usage, input validation, or
281+
modifying the arguments before they are passed to the tool.
282+
283+
Args:
284+
tool: The tool instance that is about to be executed.
285+
tool_args: The dictionary of arguments to be used for invoking the tool.
286+
tool_context: The context specific to the tool execution.
287+
288+
Returns:
289+
An optional dictionary. If a dictionary is returned, it will stop the tool
290+
execution and return this response immediately. Returning `None` uses the
291+
original, unmodified arguments.
292+
"""
293+
pass
294+
295+
async def after_tool_callback(
296+
self,
297+
*,
298+
tool: BaseTool,
299+
tool_args: dict[str, Any],
300+
tool_context: ToolContext,
301+
result: dict,
302+
) -> Optional[dict]:
303+
"""Callback executed after a tool has been called.
304+
305+
This callback allows for inspecting, logging, or modifying the result
306+
returned by a tool.
307+
308+
Args:
309+
tool: The tool instance that has just been executed.
310+
tool_args: The original arguments that were passed to the tool.
311+
tool_context: The context specific to the tool execution.
312+
result: The dictionary returned by the tool invocation.
313+
314+
Returns:
315+
An optional dictionary. If a dictionary is returned, it will **replace**
316+
the original result from the tool. This allows for post-processing or
317+
altering tool outputs. Returning `None` uses the original, unmodified
318+
result.
319+
"""
320+
pass

0 commit comments

Comments
 (0)