Skip to content

Commit 5ac5129

Browse files
google-genai-botcopybara-github
authored andcommitted
feat: Enhance BQ Plugin Schema, Error Handling, and Logging
This update enhances the BigQuery agent analytics plugin: * **Enhanced Error Logging:** Improved error messages for schema mismatches. * **Reordered Logging Content:** Prioritized metadata in `before_model_callback`. PiperOrigin-RevId: 833508755
1 parent c642f13 commit 5ac5129

File tree

2 files changed

+221
-41
lines changed

2 files changed

+221
-41
lines changed

src/google/adk/plugins/bigquery_agent_analytics_plugin.py

Lines changed: 151 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,7 @@ def _format_content_safely(
437437
return self._config.content_formatter(content), False
438438
return _format_content(content, max_len=self._config.max_content_length)
439439
except Exception as e:
440-
logging.warning(f"Content formatter failed: {e}")
440+
logging.warning("Content formatter failed: %s", e)
441441
return "[FORMATTING FAILED]", False
442442

443443
async def _ensure_init(self):
@@ -523,7 +523,21 @@ async def _perform_write(self, row: dict):
523523
self._write_client.append_rows(iter([req]))
524524
):
525525
if resp.error.code != 0:
526-
logging.error(f"BQ Plugin: Write Error: {resp.error.message}")
526+
msg = resp.error.message
527+
# Check for common schema mismatch indicators
528+
if (
529+
"schema mismatch" in msg.lower()
530+
or "field" in msg.lower()
531+
or "type" in msg.lower()
532+
):
533+
logging.error(
534+
"BQ Plugin: Schema Mismatch Error. The BigQuery table schema"
535+
" may be incorrect or out of sync with the plugin. Please"
536+
" verify the table definition. Details: %s",
537+
msg,
538+
)
539+
else:
540+
logging.error("BQ Plugin: Write Error: %s", msg)
527541

528542
except RuntimeError as e:
529543
if "Event loop is closed" not in str(e) and not self._is_shutting_down:
@@ -578,7 +592,7 @@ async def close(self):
578592

579593
if self._background_tasks:
580594
logging.info(
581-
f"BQ Plugin: Flushing {len(self._background_tasks)} pending logs..."
595+
"BQ Plugin: Flushing %s pending logs...", len(self._background_tasks)
582596
)
583597
try:
584598
await asyncio.wait(
@@ -598,12 +612,12 @@ async def close(self):
598612
timeout=self._config.client_close_timeout,
599613
)
600614
except Exception as e:
601-
logging.warning(f"BQ Plugin: Error closing write client: {e}")
615+
logging.warning("BQ Plugin: Error closing write client: %s", e)
602616
if self._bq_client:
603617
try:
604618
self._bq_client.close()
605619
except Exception as e:
606-
logging.warning(f"BQ Plugin: Error closing BQ client: {e}")
620+
logging.warning("BQ Plugin: Error closing BQ client: %s", e)
607621

608622
self._write_client = None
609623
self._bq_client = None
@@ -617,7 +631,14 @@ async def on_user_message_callback(
617631
invocation_context: InvocationContext,
618632
user_message: types.Content,
619633
) -> None:
620-
"""Callback for user messages."""
634+
"""Callback for user messages.
635+
636+
Logs the user message details including:
637+
1. User content (text)
638+
639+
The content is formatted as 'User Content: {content}'.
640+
If the content length exceeds `max_content_length`, it is truncated.
641+
"""
621642
content, truncated = self._format_content_safely(user_message)
622643
await self._log({
623644
"event_type": "USER_MESSAGE_RECEIVED",
@@ -632,7 +653,12 @@ async def on_user_message_callback(
632653
async def before_run_callback(
633654
self, *, invocation_context: InvocationContext
634655
) -> None:
635-
"""Callback before agent invocation."""
656+
"""Callback before agent invocation.
657+
658+
Logs the start of an agent invocation.
659+
No specific content payload is logged for this event, but standard metadata
660+
(agent name, session ID, invocation ID, user ID) is captured.
661+
"""
636662
await self._log({
637663
"event_type": "INVOCATION_STARTING",
638664
"agent": invocation_context.agent.name,
@@ -644,7 +670,16 @@ async def before_run_callback(
644670
async def on_event_callback(
645671
self, *, invocation_context: InvocationContext, event: Event
646672
) -> None:
647-
"""Callback for agent events."""
673+
"""Callback for agent events.
674+
675+
Logs generic agent events including:
676+
1. Event type (determined from event properties)
677+
2. Event content (text, function calls, or responses)
678+
3. Error messages (if any)
679+
680+
The content is formatted based on the event type.
681+
If the content length exceeds `max_content_length`, it is truncated.
682+
"""
648683
content, truncated = self._format_content_safely(event.content)
649684
await self._log({
650685
"event_type": _get_event_type(event),
@@ -661,7 +696,12 @@ async def on_event_callback(
661696
async def after_run_callback(
662697
self, *, invocation_context: InvocationContext
663698
) -> None:
664-
"""Callback after agent invocation."""
699+
"""Callback after agent invocation.
700+
701+
Logs the completion of an agent invocation.
702+
No specific content payload is logged for this event, but standard metadata
703+
(agent name, session ID, invocation ID, user ID) is captured.
704+
"""
665705
await self._log({
666706
"event_type": "INVOCATION_COMPLETED",
667707
"agent": invocation_context.agent.name,
@@ -673,7 +713,12 @@ async def after_run_callback(
673713
async def before_agent_callback(
674714
self, *, agent: BaseAgent, callback_context: CallbackContext
675715
) -> None:
676-
"""Callback before an agent starts."""
716+
"""Callback before an agent starts.
717+
718+
Logs the start of a specific agent execution.
719+
Content includes:
720+
1. Agent Name (from callback context)
721+
"""
677722
await self._log({
678723
"event_type": "AGENT_STARTING",
679724
"agent": agent.name,
@@ -686,7 +731,12 @@ async def before_agent_callback(
686731
async def after_agent_callback(
687732
self, *, agent: BaseAgent, callback_context: CallbackContext
688733
) -> None:
689-
"""Callback after an agent completes."""
734+
"""Callback after an agent completes.
735+
736+
Logs the completion of a specific agent execution.
737+
Content includes:
738+
1. Agent Name (from callback context)
739+
"""
690740
await self._log({
691741
"event_type": "AGENT_COMPLETED",
692742
"agent": agent.name,
@@ -699,11 +749,52 @@ async def after_agent_callback(
699749
async def before_model_callback(
700750
self, *, callback_context: CallbackContext, llm_request: LlmRequest
701751
) -> None:
702-
"""Callback before LLM call."""
752+
"""Callback before LLM call.
753+
754+
Logs the LLM request details including:
755+
1. Model name
756+
2. Configuration parameters (temperature, top_p, top_k, max_output_tokens)
757+
3. Available tool names
758+
4. Prompt content (user/model messages)
759+
5. System instructions
760+
761+
The content is formatted as a single string with fields separated by ' | '.
762+
If the total length exceeds `max_content_length`, the string is truncated,
763+
prioritizing the metadata (Model, Params, Tools) over the Prompt and System
764+
Prompt.
765+
"""
703766
content_parts = [
704767
f"Model: {llm_request.model or 'default'}",
705768
]
706769
is_truncated = False
770+
771+
# 1. Params
772+
if llm_request.config:
773+
config = llm_request.config
774+
params_to_log = {}
775+
if hasattr(config, "temperature") and config.temperature is not None:
776+
params_to_log["temperature"] = config.temperature
777+
if hasattr(config, "top_p") and config.top_p is not None:
778+
params_to_log["top_p"] = config.top_p
779+
if hasattr(config, "top_k") and config.top_k is not None:
780+
params_to_log["top_k"] = config.top_k
781+
if (
782+
hasattr(config, "max_output_tokens")
783+
and config.max_output_tokens is not None
784+
):
785+
params_to_log["max_output_tokens"] = config.max_output_tokens
786+
787+
if params_to_log:
788+
params_str = ", ".join([f"{k}={v}" for k, v in params_to_log.items()])
789+
content_parts.append(f"Params: {{{params_str}}}")
790+
791+
# 2. Tools
792+
if llm_request.tools_dict:
793+
content_parts.append(
794+
f"Available Tools: {list(llm_request.tools_dict.keys())}"
795+
)
796+
797+
# 3. Prompt
707798
if contents := getattr(llm_request, "contents", None):
708799
prompt_parts = []
709800
for c in contents:
@@ -713,6 +804,8 @@ async def before_model_callback(
713804
is_truncated = True
714805
prompt_str = " | ".join(prompt_parts)
715806
content_parts.append(f"Prompt: {prompt_str}")
807+
808+
# 4. System Prompt
716809
system_instruction_text = "None"
717810
if llm_request.config and llm_request.config.system_instruction:
718811
si = llm_request.config.system_instruction
@@ -736,29 +829,6 @@ async def before_model_callback(
736829
system_instruction_text = "Empty"
737830

738831
content_parts.append(f"System Prompt: {system_instruction_text}")
739-
if llm_request.config:
740-
config = llm_request.config
741-
params_to_log = {}
742-
if hasattr(config, "temperature") and config.temperature is not None:
743-
params_to_log["temperature"] = config.temperature
744-
if hasattr(config, "top_p") and config.top_p is not None:
745-
params_to_log["top_p"] = config.top_p
746-
if hasattr(config, "top_k") and config.top_k is not None:
747-
params_to_log["top_k"] = config.top_k
748-
if (
749-
hasattr(config, "max_output_tokens")
750-
and config.max_output_tokens is not None
751-
):
752-
params_to_log["max_output_tokens"] = config.max_output_tokens
753-
754-
if params_to_log:
755-
params_str = ", ".join([f"{k}={v}" for k, v in params_to_log.items()])
756-
content_parts.append(f"Params: {{{params_str}}}")
757-
758-
if llm_request.tools_dict:
759-
content_parts.append(
760-
f"Available Tools: {list(llm_request.tools_dict.keys())}"
761-
)
762832

763833
final_content = " | ".join(content_parts)
764834
max_len = self._config.max_content_length
@@ -778,7 +848,16 @@ async def before_model_callback(
778848
async def after_model_callback(
779849
self, *, callback_context: CallbackContext, llm_response: LlmResponse
780850
) -> None:
781-
"""Callback after LLM call."""
851+
"""Callback after LLM call.
852+
853+
Logs the LLM response details including:
854+
1. Tool calls (if any)
855+
2. Text response (if no tool calls)
856+
3. Token usage statistics (prompt, candidates, total)
857+
858+
The content is formatted as a single string with fields separated by ' | '.
859+
If the content length exceeds `max_content_length`, it is truncated.
860+
"""
782861
content_parts = []
783862
content = llm_response.content
784863
is_tool_call = False
@@ -838,7 +917,17 @@ async def before_tool_callback(
838917
tool_args: dict[str, Any],
839918
tool_context: ToolContext,
840919
) -> None:
841-
"""Callback before tool call."""
920+
"""Callback before tool call.
921+
922+
Logs the tool execution start details including:
923+
1. Tool name
924+
2. Tool description
925+
3. Tool arguments
926+
927+
The content is formatted as 'Tool Name: ..., Description: ..., Arguments:
928+
...'.
929+
If the content length exceeds `max_content_length`, it is truncated.
930+
"""
842931
args_str, truncated = _format_args(
843932
tool_args, max_len=self._config.max_content_length
844933
)
@@ -867,7 +956,15 @@ async def after_tool_callback(
867956
tool_context: ToolContext,
868957
result: dict[str, Any],
869958
) -> None:
870-
"""Callback after tool call."""
959+
"""Callback after tool call.
960+
961+
Logs the tool execution result details including:
962+
1. Tool name
963+
2. Tool result
964+
965+
The content is formatted as 'Tool Name: ..., Result: ...'.
966+
If the content length exceeds `max_content_length`, it is truncated.
967+
"""
871968
result_str, truncated = _format_args(
872969
result, max_len=self._config.max_content_length
873970
)
@@ -892,7 +989,12 @@ async def on_model_error_callback(
892989
llm_request: LlmRequest,
893990
error: Exception,
894991
) -> None:
895-
"""Callback for LLM errors."""
992+
"""Callback for model errors.
993+
994+
Logs errors that occur during LLM calls.
995+
No specific content payload is logged, but the error message is captured
996+
in the `error_message` field.
997+
"""
896998
await self._log({
897999
"event_type": "LLM_ERROR",
8981000
"agent": callback_context.agent_name,
@@ -910,7 +1012,16 @@ async def on_tool_error_callback(
9101012
tool_context: ToolContext,
9111013
error: Exception,
9121014
) -> None:
913-
"""Callback for tool errors."""
1015+
"""Callback for tool errors.
1016+
1017+
Logs errors that occur during tool execution.
1018+
Content includes:
1019+
1. Tool name
1020+
2. Tool arguments
1021+
1022+
The error message is captured in the `error_message` field.
1023+
If the content length exceeds `max_content_length`, it is truncated.
1024+
"""
9141025
args_str, truncated = _format_args(
9151026
tool_args, max_len=self._config.max_content_length
9161027
)

0 commit comments

Comments
 (0)