Skip to content

Commit 7bedea8

Browse files
pwwpchecopybara-github
authored andcommitted
feat: Add sample plugin for logging
This plugin helps printing all critical events in the console. It is not a replacement of existing logging in ADK. It rather helps terminal based debugging by showing all logs in the console, and serves as a simple demo so everyone could develop their own plugins. PiperOrigin-RevId: 783052801
1 parent 00afaaf commit 7bedea8

File tree

1 file changed

+307
-0
lines changed

1 file changed

+307
-0
lines changed
Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
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+
from __future__ import annotations
16+
17+
from typing import Any
18+
from typing import Optional
19+
20+
from google.genai import types
21+
22+
from ..agents.base_agent import BaseAgent
23+
from ..agents.callback_context import CallbackContext
24+
from ..agents.invocation_context import InvocationContext
25+
from ..events.event import Event
26+
from ..models.llm_request import LlmRequest
27+
from ..models.llm_response import LlmResponse
28+
from ..tools.base_tool import BaseTool
29+
from ..tools.tool_context import ToolContext
30+
from .base_plugin import BasePlugin
31+
32+
33+
class LoggingPlugin(BasePlugin):
34+
"""A plugin that logs important information at each callback point.
35+
36+
This plugin helps printing all critical events in the console. It is not a
37+
replacement of existing logging in ADK. It rather helps terminal based
38+
debugging by showing all logs in the console, and serves as a simple demo for
39+
everyone to leverage when developing new plugins.
40+
41+
This plugin helps users track the invocation status by logging:
42+
- User messages and invocation context
43+
- Agent execution flow
44+
- LLM requests and responses
45+
- Tool calls with arguments and results
46+
- Events and final responses
47+
- Errors during model and tool execution
48+
49+
Example:
50+
>>> logging_plugin = LoggingPlugin()
51+
>>> runner = Runner(
52+
... agents=[my_agent],
53+
... # ...
54+
... plugins=[logging_plugin],
55+
... )
56+
"""
57+
58+
def __init__(self, name: str = "logging_plugin"):
59+
"""Initialize the logging plugin.
60+
61+
Args:
62+
name: The name of the plugin instance.
63+
"""
64+
super().__init__(name)
65+
66+
async def on_user_message_callback(
67+
self,
68+
*,
69+
invocation_context: InvocationContext,
70+
user_message: types.Content,
71+
) -> Optional[types.Content]:
72+
"""Log user message and invocation start."""
73+
self._log(f"🚀 USER MESSAGE RECEIVED")
74+
self._log(f" Invocation ID: {invocation_context.invocation_id}")
75+
self._log(f" Session ID: {invocation_context.session.id}")
76+
self._log(f" User ID: {invocation_context.user_id}")
77+
self._log(f" App Name: {invocation_context.app_name}")
78+
self._log(
79+
" Root Agent:"
80+
f" {invocation_context.agent.name if hasattr(invocation_context.agent, 'name') else 'Unknown'}"
81+
)
82+
self._log(f" User Content: {self._format_content(user_message)}")
83+
if invocation_context.branch:
84+
self._log(f" Branch: {invocation_context.branch}")
85+
return None
86+
87+
async def before_run_callback(
88+
self, *, invocation_context: InvocationContext
89+
) -> Optional[types.Content]:
90+
"""Log invocation start."""
91+
self._log(f"🏃 INVOCATION STARTING")
92+
self._log(f" Invocation ID: {invocation_context.invocation_id}")
93+
self._log(
94+
" Starting Agent:"
95+
f" {invocation_context.agent.name if hasattr(invocation_context.agent, 'name') else 'Unknown'}"
96+
)
97+
return None
98+
99+
async def on_event_callback(
100+
self, *, invocation_context: InvocationContext, event: Event
101+
) -> Optional[Event]:
102+
"""Log events yielded from the runner."""
103+
self._log(f"📢 EVENT YIELDED")
104+
self._log(f" Event ID: {event.id}")
105+
self._log(f" Author: {event.author}")
106+
self._log(f" Content: {self._format_content(event.content)}")
107+
self._log(f" Final Response: {event.is_final_response()}")
108+
109+
if event.get_function_calls():
110+
func_calls = [fc.name for fc in event.get_function_calls()]
111+
self._log(f" Function Calls: {func_calls}")
112+
113+
if event.get_function_responses():
114+
func_responses = [fr.name for fr in event.get_function_responses()]
115+
self._log(f" Function Responses: {func_responses}")
116+
117+
if event.long_running_tool_ids:
118+
self._log(f" Long Running Tools: {list(event.long_running_tool_ids)}")
119+
120+
return None
121+
122+
async def after_run_callback(
123+
self, *, invocation_context: InvocationContext
124+
) -> Optional[None]:
125+
"""Log invocation completion."""
126+
self._log(f"✅ INVOCATION COMPLETED")
127+
self._log(f" Invocation ID: {invocation_context.invocation_id}")
128+
self._log(
129+
" Final Agent:"
130+
f" {invocation_context.agent.name if hasattr(invocation_context.agent, 'name') else 'Unknown'}"
131+
)
132+
return None
133+
134+
async def before_agent_callback(
135+
self, *, agent: BaseAgent, callback_context: CallbackContext
136+
) -> Optional[types.Content]:
137+
"""Log agent execution start."""
138+
self._log(f"🤖 AGENT STARTING")
139+
self._log(f" Agent Name: {callback_context.agent_name}")
140+
self._log(f" Invocation ID: {callback_context.invocation_id}")
141+
if callback_context._invocation_context.branch:
142+
self._log(f" Branch: {callback_context._invocation_context.branch}")
143+
return None
144+
145+
async def after_agent_callback(
146+
self, *, agent: BaseAgent, callback_context: CallbackContext
147+
) -> Optional[types.Content]:
148+
"""Log agent execution completion."""
149+
self._log(f"🤖 AGENT COMPLETED")
150+
self._log(f" Agent Name: {callback_context.agent_name}")
151+
self._log(f" Invocation ID: {callback_context.invocation_id}")
152+
return None
153+
154+
async def before_model_callback(
155+
self, *, callback_context: CallbackContext, llm_request: LlmRequest
156+
) -> Optional[LlmResponse]:
157+
"""Log LLM request before sending to model."""
158+
self._log(f"🧠 LLM REQUEST")
159+
self._log(f" Model: {llm_request.model or 'default'}")
160+
self._log(f" Agent: {callback_context.agent_name}")
161+
162+
# Log system instruction if present
163+
if llm_request.config and llm_request.config.system_instruction:
164+
sys_instruction = llm_request.config.system_instruction[:200]
165+
if len(llm_request.config.system_instruction) > 200:
166+
sys_instruction += "..."
167+
self._log(f" System Instruction: '{sys_instruction}'")
168+
169+
# Note: Content logging removed due to type compatibility issues
170+
# Users can still see content in the LLM response
171+
172+
# Log available tools
173+
if llm_request.tools_dict:
174+
tool_names = list(llm_request.tools_dict.keys())
175+
self._log(f" Available Tools: {tool_names}")
176+
177+
return None
178+
179+
async def after_model_callback(
180+
self, *, callback_context: CallbackContext, llm_response: LlmResponse
181+
) -> Optional[LlmResponse]:
182+
"""Log LLM response after receiving from model."""
183+
self._log(f"🧠 LLM RESPONSE")
184+
self._log(f" Agent: {callback_context.agent_name}")
185+
186+
if llm_response.error_code:
187+
self._log(f" ❌ ERROR - Code: {llm_response.error_code}")
188+
self._log(f" Error Message: {llm_response.error_message}")
189+
else:
190+
self._log(f" Content: {self._format_content(llm_response.content)}")
191+
if llm_response.partial:
192+
self._log(f" Partial: {llm_response.partial}")
193+
if llm_response.turn_complete is not None:
194+
self._log(f" Turn Complete: {llm_response.turn_complete}")
195+
196+
# Log usage metadata if available
197+
if llm_response.usage_metadata:
198+
self._log(
199+
" Token Usage - Input:"
200+
f" {llm_response.usage_metadata.prompt_token_count}, Output:"
201+
f" {llm_response.usage_metadata.candidates_token_count}"
202+
)
203+
204+
return None
205+
206+
async def before_tool_callback(
207+
self,
208+
*,
209+
tool: BaseTool,
210+
tool_args: dict[str, Any],
211+
tool_context: ToolContext,
212+
) -> Optional[dict]:
213+
"""Log tool execution start."""
214+
self._log(f"🔧 TOOL STARTING")
215+
self._log(f" Tool Name: {tool.name}")
216+
self._log(f" Agent: {tool_context.agent_name}")
217+
self._log(f" Function Call ID: {tool_context.function_call_id}")
218+
self._log(f" Arguments: {self._format_args(tool_args)}")
219+
return None
220+
221+
async def after_tool_callback(
222+
self,
223+
*,
224+
tool: BaseTool,
225+
tool_args: dict[str, Any],
226+
tool_context: ToolContext,
227+
result: dict,
228+
) -> Optional[dict]:
229+
"""Log tool execution completion."""
230+
self._log(f"🔧 TOOL COMPLETED")
231+
self._log(f" Tool Name: {tool.name}")
232+
self._log(f" Agent: {tool_context.agent_name}")
233+
self._log(f" Function Call ID: {tool_context.function_call_id}")
234+
self._log(f" Result: {self._format_args(result)}")
235+
return None
236+
237+
async def on_model_error_callback(
238+
self,
239+
*,
240+
callback_context: CallbackContext,
241+
llm_request: LlmRequest,
242+
error: Exception,
243+
) -> Optional[LlmResponse]:
244+
"""Log LLM error."""
245+
self._log(f"🧠 LLM ERROR")
246+
self._log(f" Agent: {callback_context.agent_name}")
247+
self._log(f" Error: {error}")
248+
249+
return None
250+
251+
async def on_tool_error_callback(
252+
self,
253+
*,
254+
tool: BaseTool,
255+
tool_args: dict[str, Any],
256+
tool_context: ToolContext,
257+
error: Exception,
258+
) -> Optional[dict]:
259+
"""Log tool error."""
260+
self._log(f"🔧 TOOL ERROR")
261+
self._log(f" Tool Name: {tool.name}")
262+
self._log(f" Agent: {tool_context.agent_name}")
263+
self._log(f" Function Call ID: {tool_context.function_call_id}")
264+
self._log(f" Arguments: {self._format_args(tool_args)}")
265+
self._log(f" Error: {error}")
266+
return None
267+
268+
def _log(self, message: str) -> None:
269+
"""Internal method to format and print log messages."""
270+
# ANSI color codes: \033[90m for grey, \033[0m to reset
271+
formatted_message: str = f"\033[90m[{self.name}] {message}\033[0m"
272+
print(formatted_message)
273+
274+
def _format_content(
275+
self, content: Optional[types.Content], max_length: int = 200
276+
) -> str:
277+
"""Format content for logging, truncating if too long."""
278+
if not content or not content.parts:
279+
return "None"
280+
281+
parts = []
282+
for part in content.parts:
283+
if part.text:
284+
text = part.text.strip()
285+
if len(text) > max_length:
286+
text = text[:max_length] + "..."
287+
parts.append(f"text: '{text}'")
288+
elif part.function_call:
289+
parts.append(f"function_call: {part.function_call.name}")
290+
elif part.function_response:
291+
parts.append(f"function_response: {part.function_response.name}")
292+
elif part.code_execution_result:
293+
parts.append("code_execution_result")
294+
else:
295+
parts.append("other_part")
296+
297+
return " | ".join(parts)
298+
299+
def _format_args(self, args: dict[str, Any], max_length: int = 300) -> str:
300+
"""Format arguments dictionary for logging."""
301+
if not args:
302+
return "{}"
303+
304+
formatted = str(args)
305+
if len(formatted) > max_length:
306+
formatted = formatted[:max_length] + "...}"
307+
return formatted

0 commit comments

Comments
 (0)