diff --git a/contributing/samples/plugin_basic/README.md b/contributing/samples/plugin_basic/README.md new file mode 100644 index 00000000..c90fa47a --- /dev/null +++ b/contributing/samples/plugin_basic/README.md @@ -0,0 +1,58 @@ +# ADK Agent with Plugin + +### What is ADK Plugin? + +At its core, ADK extensibility is built on +[**callbacks**](https://google.github.io/adk-docs/callbacks/): functions you +write that ADK automatically executes at key stages of an agent's lifecycle. +**A Plugin is simply a class that packages these individual callback functions +together for a broader purpose.** + +While a standard Agent Callback is configured on a *single agent, a single tool* +for a *specific task*, a Plugin is registered *once* on the `Runner` and its +callbacks apply *globally* to every agent, tool, and LLM call managed by that +runner. This makes Plugins the ideal solution for implementing horizontal +features that cut across your entire application. + +### What can plugins do? + +Plugins are incredibly versatile. By implementing different callback methods, you +can achieve a wide range of functionalities. + +* **Logging & Tracing**: Create detailed logs of agent, tool, and LLM activity + for debugging and performance analysis. +* **Policy Enforcement**: Implement security guardrails. For example, a + before\_tool\_callback can check if a user is authorized to use a specific + tool and prevent its execution by returning a value. +* **Monitoring & Metrics**: Collect and export metrics on token usage, + execution times, and invocation counts to monitoring systems like Prometheus + or Stackdriver. +* **Caching**: In before\_model\_callback or before\_tool\_callback, you can + check if a request has been made before. If so, you can return a cached + response, skipping the expensive LLM or tool call entirely. +* **Request/Response Modification**: Dynamically add information to LLM prompts + (e.g., in before\_model\_callback) or standardize tool outputs (e.g., in + after\_tool\_callback). + +### Run the agent + +**Note: Plugin is NOT supported in `adk web`yet.** + +Use following command to run the main.py + +```bash +python3 -m contributing.samples.plugin_basic.main +``` + +It should output the following content. Note that the outputs from plugin are +printed. + +```bash +[Plugin] Agent run count: 1 +[Plugin] LLM request count: 1 +** Got event from hello_world +Hello world: query is [hello world] +** Got event from hello_world +[Plugin] LLM request count: 2 +** Got event from hello_world +``` \ No newline at end of file diff --git a/contributing/samples/plugin_basic/__init__.py b/contributing/samples/plugin_basic/__init__.py new file mode 100644 index 00000000..dbd86450 --- /dev/null +++ b/contributing/samples/plugin_basic/__init__.py @@ -0,0 +1,15 @@ +# 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. + +from .main import root_agent diff --git a/contributing/samples/plugin_basic/count_plugin.py b/contributing/samples/plugin_basic/count_plugin.py new file mode 100644 index 00000000..67ef3ea6 --- /dev/null +++ b/contributing/samples/plugin_basic/count_plugin.py @@ -0,0 +1,43 @@ +# 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. + +from google.adk.agents.base_agent import BaseAgent +from google.adk.agents.callback_context import CallbackContext +from google.adk.models.llm_request import LlmRequest +from google.adk.plugins.base_plugin import BasePlugin + + +class CountInvocationPlugin(BasePlugin): + """A custom plugin that counts agent and tool invocations.""" + + def __init__(self) -> None: + """Initialize the plugin with counters.""" + super().__init__(name="count_invocation") + self.agent_count: int = 0 + self.tool_count: int = 0 + self.llm_request_count: int = 0 + + async def before_agent_callback( + self, *, agent: BaseAgent, callback_context: CallbackContext + ) -> None: + """Count agent runs.""" + self.agent_count += 1 + print(f"[Plugin] Agent run count: {self.agent_count}") + + async def before_model_callback( + self, *, callback_context: CallbackContext, llm_request: LlmRequest + ) -> None: + """Count LLM requests.""" + self.llm_request_count += 1 + print(f"[Plugin] LLM request count: {self.llm_request_count}") diff --git a/contributing/samples/plugin_basic/main.py b/contributing/samples/plugin_basic/main.py new file mode 100644 index 00000000..75c04d91 --- /dev/null +++ b/contributing/samples/plugin_basic/main.py @@ -0,0 +1,65 @@ +# 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. + +import asyncio + +from google.adk import Agent +from google.adk.runners import InMemoryRunner +from google.adk.tools.tool_context import ToolContext +from google.genai import types + +# [Step 2] Import the plugin. +from .count_plugin import CountInvocationPlugin + + +async def hello_world(tool_context: ToolContext, query: str): + print(f'Hello world: query is [{query}]') + + +root_agent = Agent( + model='gemini-2.0-flash', + name='hello_world', + description='Prints hello world with user query.', + instruction="""Use hello_world tool to print hello world and user query. + """, + tools=[hello_world], +) + + +async def main(): + """Main entry point for the agent.""" + prompt = 'hello world' + runner = InMemoryRunner( + agent=root_agent, + app_name='test_app_with_plugin', + # [Step 2] Add your plugin here. You can add multiple plugins. + plugins=[CountInvocationPlugin()], + ) + session = await runner.session_service.create_session( + user_id='user', + app_name='test_app_with_plugin', + ) + + async for event in runner.run_async( + user_id='user', + session_id=session.id, + new_message=types.Content( + role='user', parts=[types.Part.from_text(text=prompt)] + ), + ): + print(f'** Got event from {event.author}') + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/src/google/adk/plugins/base_plugin.py b/src/google/adk/plugins/base_plugin.py index cc7d85f3..729e3519 100644 --- a/src/google/adk/plugins/base_plugin.py +++ b/src/google/adk/plugins/base_plugin.py @@ -39,9 +39,6 @@ if TYPE_CHECKING: T = TypeVar("T") -@working_in_progress( - "Plugin is under development now. Check again around Jul. 2025" -) class BasePlugin(ABC): """Base class for creating plugins.