Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 24 additions & 10 deletions src/google/adk/plugins/logging_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,25 @@ class LoggingPlugin(BasePlugin):
... )
"""

def __init__(self, name: str = "logging_plugin"):
def __init__(
self,
name: str = "logging_plugin",
*,
max_content_length: Optional[int] = 200,
max_args_length: Optional[int] = 300,
):
"""Initialize the logging plugin.

Args:
name: The name of the plugin instance.
max_content_length: Maximum characters per text part and rendered system
instruction. Set to None to disable truncation.
max_args_length: Maximum characters for tool arguments and results.
Set to None to disable truncation.
"""
super().__init__(name)
self._max_content_length = max_content_length
self._max_args_length = max_args_length

@override
async def on_user_message_callback(
Expand Down Expand Up @@ -177,9 +189,11 @@ async def before_model_callback(
system_instruction = llm_request.config.system_instruction
if system_instruction:
rendered_instruction = self._render_system_instruction(system_instruction)
sys_instruction = rendered_instruction[:200]
if len(rendered_instruction) > 200:
sys_instruction += "..."
max_length = self._max_content_length
if max_length is not None and len(rendered_instruction) > max_length:
sys_instruction = rendered_instruction[:max_length] + "..."
else:
sys_instruction = rendered_instruction
self._log(f" System Instruction: '{sys_instruction}'")

# Note: Content logging removed due to type compatibility issues
Expand Down Expand Up @@ -311,18 +325,17 @@ def _render_system_instruction(self, system_instruction: Any) -> str:
)
return str(system_instruction)

def _format_content(
self, content: Optional[types.Content], max_length: int = 200
) -> str:
def _format_content(self, content: Optional[types.Content]) -> str:
"""Format content for logging, truncating if too long."""
if not content or not content.parts:
return "None"

parts = []
max_length = self._max_content_length
for part in content.parts:
if part.text:
text = part.text.strip()
if len(text) > max_length:
if max_length is not None and len(text) > max_length:
text = text[:max_length] + "..."
parts.append(f"text: '{text}'")
elif part.function_call:
Expand All @@ -336,12 +349,13 @@ def _format_content(

return " | ".join(parts)

def _format_args(self, args: dict[str, Any], max_length: int = 300) -> str:
def _format_args(self, args: dict[str, Any]) -> str:
"""Format arguments dictionary for logging."""
if not args:
return "{}"

formatted = str(args)
if len(formatted) > max_length:
max_length = self._max_args_length
if max_length is not None and len(formatted) > max_length:
formatted = formatted[:max_length] + "...}"
return formatted
79 changes: 79 additions & 0 deletions tests/unittests/plugins/test_logging_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,3 +255,82 @@ async def test_before_model_callback_formats_list_system_instruction(
out = capsys.readouterr().out
assert 'Stay concise.' in out
assert 'Cite sources.' in out


@pytest.mark.parametrize(
('limit', 'as_content', 'expected'),
[
(0, False, '...'),
(0, True, '...'),
(250, False, 'a' * 250),
(250, True, "text: '" + 'a' * 243 + '...'),
(500, False, 'a' * 250),
(500, True, "text: '" + 'a' * 250 + "'"),
(None, False, 'a' * 250),
(None, True, "text: '" + 'a' * 250 + "'"),
],
)
async def test_system_instruction_respects_content_limit(
limit, as_content, expected, callback_context, capsys
):
"""The configured limit applies to string and Content instructions."""
text = 'a' * 250
instruction = (
types.Content(parts=[types.Part(text=text)]) if as_content else text
)
plugin = LoggingPlugin(max_content_length=limit)
request = LlmRequest(
config=types.GenerateContentConfig(system_instruction=instruction)
)

await plugin.before_model_callback(
callback_context=callback_context, llm_request=request
)

assert f"System Instruction: '{expected}'" in capsys.readouterr().out


@pytest.mark.parametrize(
('limit', 'expected'),
[(0, '...'), (250, 'a' * 250), (500, 'a' * 250), (None, 'a' * 250)],
)
async def test_event_respects_content_limit(limit, expected, capsys):
"""Events use the configured text limit, including unlimited output."""
plugin = LoggingPlugin(max_content_length=limit)
event = Event(
author='test-agent',
content=types.Content(parts=[types.Part(text='a' * 250)]),
)

await plugin.on_event_callback(invocation_context=None, event=event)

assert f"Content: text: '{expected}'" in capsys.readouterr().out


@pytest.mark.parametrize(
('limit', 'expected'),
[
(0, '...}'),
(400, "{'payload': '" + 'a' * 387 + '...}'),
(500, str({'payload': 'a' * 400})),
(None, str({'payload': 'a' * 400})),
],
)
@pytest.mark.parametrize('after', [False, True])
async def test_tool_logging_respects_args_limit(
limit, expected, after, tool_context, capsys
):
"""Tool arguments and results both use the configured dictionary limit."""
plugin = LoggingPlugin(max_args_length=limit)
payload = {'payload': 'a' * 400}
kwargs = dict(
tool=_tool('my_tool'), tool_args=payload, tool_context=tool_context
)

if after:
await plugin.after_tool_callback(**kwargs, result=payload)
else:
await plugin.before_tool_callback(**kwargs)

label = 'Result' if after else 'Arguments'
assert f'{label}: {expected}' in capsys.readouterr().out