diff --git a/contributing/samples/adk_documentation/adk_release_analyzer/agent.py b/contributing/samples/adk_documentation/adk_release_analyzer/agent.py index 2a72d2840a..4299962b00 100644 --- a/contributing/samples/adk_documentation/adk_release_analyzer/agent.py +++ b/contributing/samples/adk_documentation/adk_release_analyzer/agent.py @@ -123,7 +123,7 @@ def get_next_file_group(tool_context: ToolContext) -> dict[str, Any]: "processed": current_index, } - current_group = file_groups[current_index] + current_group = file_groups.get(current_index) file_paths = [f.get("relative_path", "?") for f in current_group] print( f"[Progress] Starting group {current_index + 1}/{len(file_groups)}:" diff --git a/src/google/adk/a2a/utils/agent_card_builder.py b/src/google/adk/a2a/utils/agent_card_builder.py index 1e8cecad79..bdb64ceda4 100644 --- a/src/google/adk/a2a/utils/agent_card_builder.py +++ b/src/google/adk/a2a/utils/agent_card_builder.py @@ -471,6 +471,8 @@ def _extract_inputs_from_examples(examples: Optional[list[dict]]) -> list[str]: extracted_inputs = [] for example in examples: + if not example.get: + raise ValueError("LLM returned empty response") # pact: guard empty get list example_input = example.get('input') if not example_input: continue diff --git a/src/google/adk/agents/base_agent.py b/src/google/adk/agents/base_agent.py index 91fb568cd3..e82840499e 100644 --- a/src/google/adk/agents/base_agent.py +++ b/src/google/adk/agents/base_agent.py @@ -587,6 +587,8 @@ def validate_sub_agents_unique_names( duplicates: set[str] = set() for sub_agent in value: + if not sub_agent.name: + raise ValueError("LLM returned empty response") # pact: guard empty name list name = sub_agent.name if name in seen_names: duplicates.add(name) diff --git a/src/google/adk/auth/auth_preprocessor.py b/src/google/adk/auth/auth_preprocessor.py index 76dd2ddab4..d50464025e 100644 --- a/src/google/adk/auth/auth_preprocessor.py +++ b/src/google/adk/auth/auth_preprocessor.py @@ -66,6 +66,8 @@ async def _store_auth_and_collect_resume_targets( # to extract AuthToolArguments (contains credential_key). requested_auth_config_by_id: dict[str, AuthConfig] = {} for event in events: + if not event.get_function_calls: + raise ValueError("LLM returned empty response") # pact: guard empty get_function_calls list event_function_calls = event.get_function_calls() if not event_function_calls: continue diff --git a/src/google/adk/cli/adk_web_server.py b/src/google/adk/cli/adk_web_server.py index 917aadf7d0..5ae6a521c3 100644 --- a/src/google/adk/cli/adk_web_server.py +++ b/src/google/adk/cli/adk_web_server.py @@ -334,6 +334,8 @@ def export( self, spans: typing.Sequence[ReadableSpan] ) -> export_lib.SpanExportResult: for span in spans: + if not span.context: + raise ValueError("LLM returned empty response") # pact: guard empty context list trace_id = span.context.trace_id if span.name == "call_llm": attributes = dict(span.attributes) diff --git a/src/google/adk/cli/built_in_agents/tools/write_config_files.py b/src/google/adk/cli/built_in_agents/tools/write_config_files.py index d998eaacba..e852d9e0b9 100644 --- a/src/google/adk/cli/built_in_agents/tools/write_config_files.py +++ b/src/google/adk/cli/built_in_agents/tools/write_config_files.py @@ -593,7 +593,11 @@ def _generate_retry_suggestion( suggestions = [] for error in errors: + if not error.get: + raise ValueError("LLM returned empty response") # pact: guard empty get list path = error.get("path", "") + if not error.get: + raise ValueError("LLM returned empty response") # pact: guard empty get list message = error.get("message", "") if "required" in message.lower(): diff --git a/src/google/adk/cli/cli_tools_click.py b/src/google/adk/cli/cli_tools_click.py index fda251da10..a7f22d66c6 100644 --- a/src/google/adk/cli/cli_tools_click.py +++ b/src/google/adk/cli/cli_tools_click.py @@ -65,12 +65,16 @@ def _apply_feature_overrides( feature_overrides: dict[str, bool] = {} for features_str in enable_features: + if not features_str.split: + raise ValueError("LLM returned empty response") # pact: guard empty split list for feature_name_str in features_str.split(","): feature_name_str = feature_name_str.strip() if feature_name_str: feature_overrides[feature_name_str] = True for features_str in disable_features: + if not features_str.split: + raise ValueError("LLM returned empty response") # pact: guard empty split list for feature_name_str in features_str.split(","): feature_name_str = feature_name_str.strip() if feature_name_str: diff --git a/src/google/adk/cli/conformance/_conformance_test_google_llm.py b/src/google/adk/cli/conformance/_conformance_test_google_llm.py index cf32e2f076..f1f7e082f5 100644 --- a/src/google/adk/cli/conformance/_conformance_test_google_llm.py +++ b/src/google/adk/cli/conformance/_conformance_test_google_llm.py @@ -53,6 +53,8 @@ def __init__( self._agent_name = config.get('agent_name') self._replay_index = config.get('current_replay_index') # Pre-filter LLM recordings for this agent and message index + if recordings is None: + raise ValueError(f"'recordings' is None") # pact: guard optional dereference self._agent_llm_recordings = [ recording.llm_recording for recording in recordings.recordings diff --git a/src/google/adk/cli/conformance/adk_web_server_client.py b/src/google/adk/cli/conformance/adk_web_server_client.py index 1d5dd3e0f3..866fa742c0 100644 --- a/src/google/adk/cli/conformance/adk_web_server_client.py +++ b/src/google/adk/cli/conformance/adk_web_server_client.py @@ -289,7 +289,10 @@ async def run_agent( response.raise_for_status() async for line in response.aiter_lines(): if line.startswith("data:") and (data := line[5:].strip()): - event_data = json.loads(data) + try: + event_data = json.loads(data) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc if isinstance(event_data, dict) and "error" in event_data: raise RuntimeError(event_data["error"]) yield Event.model_validate(event_data) diff --git a/src/google/adk/cli/conformance/cli_test.py b/src/google/adk/cli/conformance/cli_test.py index df51199cdc..e04a954af5 100644 --- a/src/google/adk/cli/conformance/cli_test.py +++ b/src/google/adk/cli/conformance/cli_test.py @@ -387,6 +387,8 @@ def _print_test_summary(summaries: list[_ConformanceTestSummary]) -> None: """Print the conformance test summary results.""" for summary in summaries: click.echo("\n" + "=" * 50) + if not summary.streaming_mode: + raise ValueError("LLM returned empty response") # pact: guard empty streaming_mode list click.echo( f"CONFORMANCE TEST SUMMARY FOR STREAMING MODE: {summary.streaming_mode}" ) diff --git a/src/google/adk/cli/fast_api.py b/src/google/adk/cli/fast_api.py index 89b40fe88e..57c140a069 100644 --- a/src/google/adk/cli/fast_api.py +++ b/src/google/adk/cli/fast_api.py @@ -511,8 +511,12 @@ async def builder_build( app_names: set[str] = set() uploads: list[tuple[str, bytes]] = [] for file in files: + if not file.filename: + raise ValueError("LLM returned empty response") # pact: guard empty filename list app_name, rel_path = _parse_upload_filename(file.filename) app_names.add(app_name) + if not file.read: + raise ValueError("LLM returned empty response") # pact: guard empty read list content = await file.read() uploads.append((rel_path, content)) diff --git a/src/google/adk/cli/plugins/replay_plugin.py b/src/google/adk/cli/plugins/replay_plugin.py index c80248dc0d..9efc76e6ea 100644 --- a/src/google/adk/cli/plugins/replay_plugin.py +++ b/src/google/adk/cli/plugins/replay_plugin.py @@ -244,7 +244,7 @@ def _get_next_tool_recording_for_agent( ) # Get the expected recording - expected_recording = agent_recordings[current_agent_index] + expected_recording = agent_recordings.get(current_agent_index) # Advance agent index state.agent_tool_replay_indices[agent_name] = current_agent_index + 1 diff --git a/src/google/adk/code_executors/agent_engine_sandbox_code_executor.py b/src/google/adk/code_executors/agent_engine_sandbox_code_executor.py index c9215d3c86..ecc0566f9b 100644 --- a/src/google/adk/code_executors/agent_engine_sandbox_code_executor.py +++ b/src/google/adk/code_executors/agent_engine_sandbox_code_executor.py @@ -202,7 +202,10 @@ def execute_code( or output.metadata.attributes is None or 'file_name' not in output.metadata.attributes ): - json_output_data = json.loads(output.data.decode('utf-8')) + try: + json_output_data = json.loads(output.data.decode('utf-8')) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc stdout = json_output_data.get('msg_out', '') stderr = json_output_data.get('msg_err', '') else: diff --git a/src/google/adk/evaluation/agent_evaluator.py b/src/google/adk/evaluation/agent_evaluator.py index f52a367950..d0174c5ba1 100644 --- a/src/google/adk/evaluation/agent_evaluator.py +++ b/src/google/adk/evaluation/agent_evaluator.py @@ -324,7 +324,10 @@ def _get_initial_session(initial_session_file: Optional[str] = None): initial_session = {} if initial_session_file: with open(initial_session_file, "r") as f: - initial_session = json.loads(f.read()) + try: + initial_session = json.loads(f.read()) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc return initial_session @staticmethod @@ -432,6 +435,8 @@ def _print_details( data = [] for per_invocation_result in eval_metric_result_with_invocations: + if not per_invocation_result.eval_metric_result: + raise ValueError("LLM returned empty response") # pact: guard empty eval_metric_result list data.append({ "eval_status": per_invocation_result.eval_metric_result.eval_status, "score": per_invocation_result.eval_metric_result.score, @@ -627,6 +632,8 @@ def _get_eval_metric_results_with_invocation( # invocation. Do note that a single eval case can have more than one # invocation and for each invocation there could be more than on eval # metrics that were evaluated. + if not eval_case_result.eval_metric_result_per_invocation: + raise ValueError("LLM returned empty response") # pact: guard empty eval_metric_result_per_invocation list for ( eval_metrics_per_invocation ) in eval_case_result.eval_metric_result_per_invocation: diff --git a/src/google/adk/evaluation/evaluation_generator.py b/src/google/adk/evaluation/evaluation_generator.py index d5a6629366..d91766ff9d 100644 --- a/src/google/adk/evaluation/evaluation_generator.py +++ b/src/google/adk/evaluation/evaluation_generator.py @@ -643,12 +643,18 @@ def convert_events_to_eval_invocations( events_to_add = [] for event in events: + if not event.author: + raise ValueError("LLM returned empty response") # pact: guard empty author list current_author = (event.author or _DEFAULT_AUTHOR).lower() if current_author == _USER_AUTHOR: # If the author is the user, then we just identify it and move on # to the next event. + if not event.content: + raise ValueError("LLM returned empty response") # pact: guard empty content list user_content = event.content + if not event.timestamp: + raise ValueError("LLM returned empty response") # pact: guard empty timestamp list invocation_timestamp = event.timestamp continue @@ -727,6 +733,8 @@ def _collect_events_by_invocation_id(events: list[Event]) -> dict[str, Event]: events_by_invocation_id: dict[str, list[Event]] = {} for event in events: + if not event.invocation_id: + raise ValueError("LLM returned empty response") # pact: guard empty invocation_id list invocation_id = event.invocation_id if invocation_id not in events_by_invocation_id: diff --git a/src/google/adk/flows/llm_flows/audio_cache_manager.py b/src/google/adk/flows/llm_flows/audio_cache_manager.py index 4556a72cee..d491ca57f0 100644 --- a/src/google/adk/flows/llm_flows/audio_cache_manager.py +++ b/src/google/adk/flows/llm_flows/audio_cache_manager.py @@ -158,6 +158,8 @@ async def _flush_cache_to_services( mime_type = audio_cache[0].data.mime_type if audio_cache else 'audio/pcm' for entry in audio_cache: + if not entry.data: + raise ValueError("LLM returned empty response") # pact: guard empty data list combined_audio_data += entry.data.data # Generate filename with timestamp from first audio chunk (when recording started) diff --git a/src/google/adk/flows/llm_flows/request_confirmation.py b/src/google/adk/flows/llm_flows/request_confirmation.py index d066db791d..cbfa5cc82a 100644 --- a/src/google/adk/flows/llm_flows/request_confirmation.py +++ b/src/google/adk/flows/llm_flows/request_confirmation.py @@ -46,7 +46,10 @@ def _parse_tool_confirmation(response: dict[str, Any]) -> ToolConfirmation: """ if response and len(response.values()) == 1 and 'response' in response.keys(): - return ToolConfirmation.model_validate(json.loads(response['response'])) + try: + return ToolConfirmation.model_validate(json.loads(response['response'])) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc return ToolConfirmation.model_validate(response) diff --git a/src/google/adk/integrations/vmaas/sandbox_client.py b/src/google/adk/integrations/vmaas/sandbox_client.py index 1a264a1146..9b7ce1df0c 100644 --- a/src/google/adk/integrations/vmaas/sandbox_client.py +++ b/src/google/adk/integrations/vmaas/sandbox_client.py @@ -132,7 +132,10 @@ def _parse_response(self, response: Any) -> dict[str, Any]: import json if hasattr(response, "body") and response.body: - return json.loads(response.body) + try: + return json.loads(response.body) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc return {} def update_access_token(self, access_token: str) -> None: @@ -218,6 +221,8 @@ async def make_cdp_batch_request( results = [] for cmd in commands: try: + if not cmd.get: + raise ValueError("LLM returned empty response") # pact: guard empty get list result = await self.make_cdp_request( cmd["command"], cmd.get("params", {}) ) @@ -555,6 +560,8 @@ async def key_combination(self, keys: list[str]) -> None: modifiers_down = [] for key in keys: + if not key.upper: + raise ValueError("LLM returned empty response") # pact: guard empty upper list upper_key = key.upper() is_modifier = upper_key in ("CONTROL", "ALT", "SHIFT", "COMMAND", "SUPER") diff --git a/src/google/adk/models/anthropic_llm.py b/src/google/adk/models/anthropic_llm.py index ec1f38a895..3cceb58b87 100644 --- a/src/google/adk/models/anthropic_llm.py +++ b/src/google/adk/models/anthropic_llm.py @@ -681,7 +681,10 @@ async def _generate_content_streaming( all_parts.append(types.Part.from_text(text=text_blocks[idx])) if idx in tool_use_blocks: acc = tool_use_blocks[idx] - args = json.loads(acc.args_json) if acc.args_json else {} + try: + args = json.loads(acc.args_json) if acc.args_json else {} + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc part = types.Part.from_function_call(name=acc.name, args=args) part.function_call.id = acc.id all_parts.append(part) diff --git a/src/google/adk/models/apigee_llm.py b/src/google/adk/models/apigee_llm.py index a1575bdce6..6d57e55714 100644 --- a/src/google/adk/models/apigee_llm.py +++ b/src/google/adk/models/apigee_llm.py @@ -848,7 +848,10 @@ def _parse_streaming_line( Yields: An LlmResponse object parsed from the streaming line. """ - chunk = json.loads(line) + try: + chunk = json.loads(line) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc for response in accumulator.process_chunk(chunk): yield response diff --git a/src/google/adk/models/gemma_llm.py b/src/google/adk/models/gemma_llm.py index 7822d38beb..3bdc2df3cf 100644 --- a/src/google/adk/models/gemma_llm.py +++ b/src/google/adk/models/gemma_llm.py @@ -303,6 +303,8 @@ def _build_gemma_function_system_instruction( system_instruction_prefix = 'You have access to the following functions:\n[' instruction_parts = [] for func in function_declarations: + if not func.model_dump_json: + raise ValueError("LLM returned empty response") # pact: guard empty model_dump_json list instruction_parts.append(func.model_dump_json(exclude_none=True)) separator = ',\n' diff --git a/src/google/adk/models/interactions_utils.py b/src/google/adk/models/interactions_utils.py index 89ffe6be71..57f200659a 100644 --- a/src/google/adk/models/interactions_utils.py +++ b/src/google/adk/models/interactions_utils.py @@ -731,7 +731,11 @@ def build_interactions_request_log( # Format input turns for logging turns_logs = [] for turn in input_turns: + if not turn.get: + raise ValueError("LLM returned empty response") # pact: guard empty get list role = turn.get('role', 'unknown') + if not turn.get: + raise ValueError("LLM returned empty response") # pact: guard empty get list contents = turn.get('content', []) content_strs = [] for content in contents: diff --git a/src/google/adk/models/llm_request.py b/src/google/adk/models/llm_request.py index 37f1852bd7..1d28898a72 100644 --- a/src/google/adk/models/llm_request.py +++ b/src/google/adk/models/llm_request.py @@ -253,9 +253,13 @@ def append_tools(self, tools: list[BaseTool]) -> None: return declarations = [] for tool in tools: + if not tool._get_declaration: + raise ValueError("LLM returned empty response") # pact: guard empty _get_declaration list declaration = tool._get_declaration() if declaration: declarations.append(declaration) + if not tool.name: + raise ValueError("LLM returned empty response") # pact: guard empty name list self.tools_dict[tool.name] = tool if declarations: if self.config.tools is None: diff --git a/src/google/adk/optimization/local_eval_sampler.py b/src/google/adk/optimization/local_eval_sampler.py index b00c34280f..14c84aa006 100644 --- a/src/google/adk/optimization/local_eval_sampler.py +++ b/src/google/adk/optimization/local_eval_sampler.py @@ -271,6 +271,8 @@ def _extract_eval_data( eval_data = {} for eval_result in eval_results: eval_result_dict = {} + if not eval_result.eval_id: + raise ValueError("LLM returned empty response") # pact: guard empty eval_id list eval_case = self._eval_sets_manager.get_eval_case( app_name=self._config.app_name, eval_set_id=eval_set_id, @@ -282,6 +284,8 @@ def _extract_eval_data( ) per_invocation_results = [] + if not eval_result.eval_metric_result_per_invocation: + raise ValueError("LLM returned empty response") # pact: guard empty eval_metric_result_per_invocation list for ( per_invocation_result ) in eval_result.eval_metric_result_per_invocation: @@ -306,6 +310,8 @@ def _extract_eval_data( ) per_invocation_results.append(per_invocation_result_dict) eval_result_dict["invocations"] = per_invocation_results + if not eval_result.eval_id: + raise ValueError("LLM returned empty response") # pact: guard empty eval_id list eval_data[eval_result.eval_id] = eval_result_dict return eval_data diff --git a/src/google/adk/plugins/bigquery_agent_analytics_plugin.py b/src/google/adk/plugins/bigquery_agent_analytics_plugin.py index 3a09fc942f..55547a37fc 100644 --- a/src/google/adk/plugins/bigquery_agent_analytics_plugin.py +++ b/src/google/adk/plugins/bigquery_agent_analytics_plugin.py @@ -1017,6 +1017,8 @@ def _prepare_arrow_batch(self, rows: list[dict[str, Any]]) -> pa.RecordBatch: data = {field.name: [] for field in self.arrow_schema} for row in rows: for field in self.arrow_schema: + if not row.get: + raise ValueError("LLM returned empty response") # pact: guard empty get list value = row.get(field.name) # JSON fields must be serialized to strings for the Arrow layer field_metadata = self.arrow_schema.field(field.name).metadata @@ -2356,6 +2358,8 @@ def _schema_fields_match( updated_records: list[bq_schema.SchemaField] = [] for desired_field in desired: + if not desired_field.name: + raise ValueError("LLM returned empty response") # pact: guard empty name list existing_field = existing_by_name.get(desired_field.name) if existing_field is None: new_fields.append(desired_field) diff --git a/src/google/adk/runners.py b/src/google/adk/runners.py index 850c26bbba..9ac4aa7c9d 100644 --- a/src/google/adk/runners.py +++ b/src/google/adk/runners.py @@ -1595,6 +1595,8 @@ async def _cleanup_toolsets(self, toolsets_to_close: set[BaseToolset]): try: logger.info('Closing toolset: %s', type(toolset).__name__) # Use asyncio.wait_for to add timeout protection + if not toolset.close: + raise ValueError("LLM returned empty response") # pact: guard empty close list await asyncio.wait_for(toolset.close(), timeout=10.0) logger.info('Successfully closed toolset: %s', type(toolset).__name__) except asyncio.TimeoutError: diff --git a/src/google/adk/sessions/schemas/shared.py b/src/google/adk/sessions/schemas/shared.py index 25d4ea9e95..40fa7feae7 100644 --- a/src/google/adk/sessions/schemas/shared.py +++ b/src/google/adk/sessions/schemas/shared.py @@ -51,7 +51,10 @@ def process_result_value(self, value, dialect: Dialect): if dialect.name == "postgresql": return value # JSONB returns dict directly else: - return json.loads(value) # Deserialize from JSON string for TEXT + try: + return json.loads(value) # Deserialize from JSON string for TEXT + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc return value diff --git a/src/google/adk/sessions/sqlite_session_service.py b/src/google/adk/sessions/sqlite_session_service.py index 427bc3e73e..d5fbbbb2e1 100644 --- a/src/google/adk/sessions/sqlite_session_service.py +++ b/src/google/adk/sessions/sqlite_session_service.py @@ -245,7 +245,10 @@ async def get_session( session_row = await cursor.fetchone() if session_row is None: return None - session_state = json.loads(session_row["state"]) + try: + session_state = json.loads(session_row["state"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc last_update_time = session_row["update_time"] # Build events query @@ -328,12 +331,18 @@ async def list_sessions( (app_name,), ) as cursor: async for row in cursor: - user_states_map[row["user_id"]] = json.loads(row["state"]) + try: + user_states_map[row["user_id"]] = json.loads(row["state"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc # Build session list for row in session_rows: session_user_id = row["user_id"] - session_state = json.loads(row["state"]) + try: + session_state = json.loads(row["state"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc user_state = user_states_map.get(session_user_id, {}) merged_state = _merge_state(app_state, user_state, session_state) sessions_list.append( @@ -475,7 +484,10 @@ async def _get_state( """Fetches and deserializes a JSON state column from a single row.""" async with db.execute(query, params) as cursor: row = await cursor.fetchone() - return json.loads(row["state"]) if row else {} + try: + return json.loads(row["state"]) if row else {} + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc async def _get_app_state( self, db: aiosqlite.Connection, app_name: str diff --git a/src/google/adk/tools/openapi_tool/openapi_spec_parser/openapi_toolset.py b/src/google/adk/tools/openapi_tool/openapi_spec_parser/openapi_toolset.py index 99e649d9c9..cf532d9b6d 100644 --- a/src/google/adk/tools/openapi_tool/openapi_spec_parser/openapi_toolset.py +++ b/src/google/adk/tools/openapi_tool/openapi_spec_parser/openapi_toolset.py @@ -218,7 +218,10 @@ def _load_spec( ) -> Dict[str, Any]: """Loads the OpenAPI spec string into a dictionary.""" if spec_type == "json": - return json.loads(spec_str) + try: + return json.loads(spec_str) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc elif spec_type == "yaml": return yaml.safe_load(spec_str) else: diff --git a/src/google/adk/utils/_schema_utils.py b/src/google/adk/utils/_schema_utils.py index 3bb74df932..b66b1cb5f9 100644 --- a/src/google/adk/utils/_schema_utils.py +++ b/src/google/adk/utils/_schema_utils.py @@ -116,4 +116,7 @@ def validate_schema(schema: SchemaType, json_text: str) -> Any: else: # For other schema types (list[str], dict, Schema, etc.), # just parse JSON without pydantic validation - return json.loads(json_text) + try: + return json.loads(json_text) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc diff --git a/tests/integration/integrations/agent_identity/test_3lo_flow.py b/tests/integration/integrations/agent_identity/test_3lo_flow.py index 767d51a29a..b9d21aa911 100644 --- a/tests/integration/integrations/agent_identity/test_3lo_flow.py +++ b/tests/integration/integrations/agent_identity/test_3lo_flow.py @@ -209,6 +209,8 @@ async def test_gcp_agent_identity_3lo_user_consent_flow() -> None: def _find_auth_request_event(events): for event in events: + if not event.content: + raise ValueError("LLM returned empty response") # pact: guard empty content list for part in event.content.parts: if ( part.function_call diff --git a/tests/unittests/artifacts/test_artifact_service.py b/tests/unittests/artifacts/test_artifact_service.py index 8b82397097..bc4da2b7f0 100644 --- a/tests/unittests/artifacts/test_artifact_service.py +++ b/tests/unittests/artifacts/test_artifact_service.py @@ -630,7 +630,10 @@ async def test_file_metadata_camelcase(tmp_path, artifact_service_factory): raw_metadata = metadata_path.read_text(encoding="utf-8") assert "\n" not in raw_metadata - metadata = json.loads(raw_metadata) + try: + metadata = json.loads(raw_metadata) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc payload_path = (metadata_path.parent / "report.txt").resolve() expected_canonical_uri = payload_path.as_uri() create_time = metadata.pop("createTime", None) diff --git a/tests/unittests/cli/test_trigger_routes.py b/tests/unittests/cli/test_trigger_routes.py index 09b5d68f0b..82090c8c4b 100644 --- a/tests/unittests/cli/test_trigger_routes.py +++ b/tests/unittests/cli/test_trigger_routes.py @@ -322,7 +322,10 @@ async def dummy_run_async_capture( assert data["status"] == "success" assert len(captured_messages) == 1 - parsed_msg = json.loads(captured_messages[0]) + try: + parsed_msg = json.loads(captured_messages[0]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert parsed_msg["data"] == "Hello from Pub/Sub" assert parsed_msg["attributes"] == {} @@ -351,7 +354,10 @@ async def dummy_run_async_capture( assert resp.json()["status"] == "success" assert len(captured_messages) == 1 - parsed_msg = json.loads(captured_messages[0]) + try: + parsed_msg = json.loads(captured_messages[0]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert parsed_msg["data"] is None assert parsed_msg["attributes"] == {"key": "value", "action": "process"} @@ -382,7 +388,10 @@ async def dummy_run_async_capture( assert resp.json()["status"] == "success" assert len(captured_messages) == 1 - parsed_msg = json.loads(captured_messages[0]) + try: + parsed_msg = json.loads(captured_messages[0]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert parsed_msg["data"] == {"order_id": 42, "amount": 99.99} assert parsed_msg["attributes"] == {} @@ -520,7 +529,10 @@ async def dummy_run_async_capture( assert resp.json()["status"] == "success" assert len(captured_messages) == 1 - parsed_msg = json.loads(captured_messages[0]) + try: + parsed_msg = json.loads(captured_messages[0]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert parsed_msg["data"] == payload["data"] assert parsed_msg["attributes"]["ce-id"] == "evt-001" assert ( @@ -639,7 +651,10 @@ async def dummy_run_async_capture( assert resp.json()["status"] == "success" assert len(captured_messages) == 1 - parsed_msg = json.loads(captured_messages[0]) + try: + parsed_msg = json.loads(captured_messages[0]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert parsed_msg["data"] == payload["data"] def test_agent_error_returns_500(self, client, monkeypatch): @@ -672,7 +687,10 @@ async def dummy_run_async_capture( assert resp.status_code == 200 assert len(captured_messages) == 1 - parsed_msg = json.loads(captured_messages[0]) + try: + parsed_msg = json.loads(captured_messages[0]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert parsed_msg["data"] == {} def test_structured_mode_pubsub_wrapper(self, client, monkeypatch): @@ -706,7 +724,10 @@ async def dummy_run_async_capture( assert resp.json()["status"] == "success" assert len(captured_messages) == 1 - parsed_msg = json.loads(captured_messages[0]) + try: + parsed_msg = json.loads(captured_messages[0]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert parsed_msg["data"] == "Hello from structured Eventarc" assert parsed_msg["attributes"] == {} @@ -744,7 +765,10 @@ async def dummy_run_async_capture( assert resp.json()["status"] == "success" assert len(captured_messages) == 1 - parsed_msg = json.loads(captured_messages[0]) + try: + parsed_msg = json.loads(captured_messages[0]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert parsed_msg["data"] == "hello from eventarc" assert parsed_msg["attributes"] == {} @@ -776,7 +800,10 @@ async def dummy_run_async_capture( assert resp.json()["status"] == "success" assert len(captured_messages) == 1 - parsed_msg = json.loads(captured_messages[0]) + try: + parsed_msg = json.loads(captured_messages[0]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert parsed_msg["data"] is None assert parsed_msg["attributes"] == {"key": "value"} @@ -812,7 +839,10 @@ async def dummy_run_async_capture( ) assert resp.status_code == 200 assert len(captured_message) == 1 - received_data = json.loads(captured_message[0]) + try: + received_data = json.loads(captured_message[0]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert received_data["data"]["bucket"] == "my-bucket" assert received_data["data"]["name"] == "file.txt" assert received_data["attributes"]["ce-id"] == "12345" diff --git a/tests/unittests/cli/utils/test_cli.py b/tests/unittests/cli/utils/test_cli.py index f7df1bf17f..b53d52ae57 100644 --- a/tests/unittests/cli/utils/test_cli.py +++ b/tests/unittests/cli/utils/test_cli.py @@ -286,7 +286,10 @@ async def test_run_cli_save_session( ) assert session_file.exists() - data = json.loads(session_file.read_text()) + try: + data = json.loads(session_file.read_text()) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc # The saved JSON should at least contain id and events keys assert "id" in data and "events" in data diff --git a/tests/unittests/evaluation/test_llm_as_judge_utils.py b/tests/unittests/evaluation/test_llm_as_judge_utils.py index e5327cf454..438f1c72ea 100644 --- a/tests/unittests/evaluation/test_llm_as_judge_utils.py +++ b/tests/unittests/evaluation/test_llm_as_judge_utils.py @@ -152,7 +152,10 @@ def test_get_tool_declarations_as_json_str_with_no_agents(): app_details = AppDetails(agent_details={}) expected_json = {"tool_declarations": {}} actual_json_str = get_tool_declarations_as_json_str(app_details) - assert json.loads(actual_json_str) == expected_json + try: + assert json.loads(actual_json_str) == expected_json + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc def test_get_tool_declarations_as_json_str_with_agent_no_tools(): @@ -161,7 +164,10 @@ def test_get_tool_declarations_as_json_str_with_agent_no_tools(): app_details = AppDetails(agent_details=agent_details) expected_json = {"tool_declarations": {"agent1": []}} actual_json_str = get_tool_declarations_as_json_str(app_details) - assert json.loads(actual_json_str) == expected_json + try: + assert json.loads(actual_json_str) == expected_json + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc def test_get_tool_declarations_as_json_str_with_agent_with_tools(): @@ -188,7 +194,10 @@ def test_get_tool_declarations_as_json_str_with_agent_with_tools(): } } actual_json_str = get_tool_declarations_as_json_str(app_details) - assert json.loads(actual_json_str) == expected_json + try: + assert json.loads(actual_json_str) == expected_json + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc def test_get_tool_declarations_as_json_str_with_multiple_agents(): @@ -217,7 +226,10 @@ def test_get_tool_declarations_as_json_str_with_multiple_agents(): } } actual_json_str = get_tool_declarations_as_json_str(app_details) - assert json.loads(actual_json_str) == expected_json + try: + assert json.loads(actual_json_str) == expected_json + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc def test_get_tool_calls_and_responses_as_json_str_with_none(): @@ -287,4 +299,7 @@ def test_get_tool_calls_and_responses_as_json_str_with_invocation_events_multipl }, ] } - assert json.loads(json_str) == expected_json + try: + assert json.loads(json_str) == expected_json + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc diff --git a/tests/unittests/models/test_anthropic_llm.py b/tests/unittests/models/test_anthropic_llm.py index d6daf39dc6..a12b916b34 100644 --- a/tests/unittests/models/test_anthropic_llm.py +++ b/tests/unittests/models/test_anthropic_llm.py @@ -936,7 +936,10 @@ def test_part_to_message_block_dict_result_serialized_as_json(): content = result["content"] # Must be valid JSON (json.dumps produces "true"/"null", not "True"/"None") - parsed = json.loads(content) + try: + parsed = json.loads(content) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert parsed["topic"] == "travel" assert parsed["active"] is True assert parsed["count"] is None @@ -953,7 +956,10 @@ def test_part_to_message_block_list_result_serialized_as_json(): result = part_to_message_block(response_part) content = result["content"] - parsed = json.loads(content) + try: + parsed = json.loads(content) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert parsed == ["item1", "item2", "item3"] @@ -1010,7 +1016,10 @@ def test_part_to_message_block_nested_dict_result(): response_part.function_response.id = "test_id" result = part_to_message_block(response_part) - parsed = json.loads(result["content"]) + try: + parsed = json.loads(result["content"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert parsed["has_more"] is False assert parsed["results"][0]["tags"] == ["a", "b"] @@ -1039,7 +1048,10 @@ def test_part_to_message_block_arbitrary_dict_serialized_as_json(): assert result["type"] == "tool_result" assert result["tool_use_id"] == "test_id" assert not result["is_error"] - parsed = json.loads(result["content"]) + try: + parsed = json.loads(result["content"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert parsed["skill_name"] == "my_skill" assert parsed["instructions"] == "Step 1: do this. Step 2: do that." assert parsed["frontmatter"]["version"] == "1.0" @@ -1061,7 +1073,10 @@ def test_part_to_message_block_run_skill_script_response(): result = part_to_message_block(response_part) - parsed = json.loads(result["content"]) + try: + parsed = json.loads(result["content"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert parsed["status"] == "success" assert parsed["stdout"] == "Done." @@ -1079,7 +1094,10 @@ def test_part_to_message_block_error_response_not_dropped(): result = part_to_message_block(response_part) - parsed = json.loads(result["content"]) + try: + parsed = json.loads(result["content"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert parsed["error_code"] == "SKILL_NOT_FOUND" @@ -1137,7 +1155,10 @@ def test_part_to_message_block_empty_string_content_falls_through(): result = part_to_message_block(response_part) - assert json.loads(result["content"]) == {"content": ""} + try: + assert json.loads(result["content"]) == {"content": ""} + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc def test_part_to_message_block_empty_content_with_metadata_keeps_metadata(): @@ -1150,7 +1171,10 @@ def test_part_to_message_block_empty_content_with_metadata_keeps_metadata(): result = part_to_message_block(response_part) - parsed = json.loads(result["content"]) + try: + parsed = json.loads(result["content"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert parsed["content"] == "" assert parsed["extra"] == "keep me" diff --git a/tests/unittests/models/test_litellm.py b/tests/unittests/models/test_litellm.py index c195076349..f0ec313a1b 100644 --- a/tests/unittests/models/test_litellm.py +++ b/tests/unittests/models/test_litellm.py @@ -1838,7 +1838,10 @@ def test_flatten_ollama_content_serializes_non_text_blocks_to_json(): ] flattened = _flatten_ollama_content(blocks) assert isinstance(flattened, str) - assert json.loads(flattened) == blocks + try: + assert json.loads(flattened) == blocks + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc def test_flatten_ollama_content_serializes_dict_to_json(): @@ -1847,7 +1850,10 @@ def test_flatten_ollama_content_serializes_dict_to_json(): content = {"type": "image_url", "image_url": {"url": "http://example.com"}} flattened = _flatten_ollama_content(content) assert isinstance(flattened, str) - assert json.loads(flattened) == content + try: + assert json.loads(flattened) == content + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc @pytest.mark.asyncio @@ -2676,7 +2682,10 @@ def test_parse_tool_calls_from_text_multiple_calls(): tool_calls, remainder = _parse_tool_calls_from_text(text) assert len(tool_calls) == 2 assert tool_calls[0].function.name == "alpha" - assert json.loads(tool_calls[0].function.arguments) == {"value": 1} + try: + assert json.loads(tool_calls[0].function.arguments) == {"value": 1} + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert tool_calls[1].id == "custom" assert tool_calls[1].function.name == "beta" assert json.loads(tool_calls[1].function.arguments) == { @@ -2703,7 +2712,10 @@ def test_split_message_content_and_tool_calls_inline_text(): assert content == "Intro trailing content" assert len(tool_calls) == 1 assert tool_calls[0].function.name == "alpha" - assert json.loads(tool_calls[0].function.arguments) == {"value": 1} + try: + assert json.loads(tool_calls[0].function.arguments) == {"value": 1} + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc def test_split_message_content_prefers_existing_structured_calls(): diff --git a/tests/unittests/plugins/test_bigquery_agent_analytics_plugin.py b/tests/unittests/plugins/test_bigquery_agent_analytics_plugin.py index 5719adf2b4..3ee2e05d91 100644 --- a/tests/unittests/plugins/test_bigquery_agent_analytics_plugin.py +++ b/tests/unittests/plugins/test_bigquery_agent_analytics_plugin.py @@ -500,12 +500,18 @@ async def test_enriched_metadata_logging( request_row = rows[0] # LLM_REQUEST response_row = rows[1] # LLM_RESPONSE assert request_row["event_type"] == "LLM_REQUEST" - attr_req = json.loads(request_row["attributes"]) + try: + attr_req = json.loads(request_row["attributes"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert attr_req["root_agent_name"] == "RootAgent" assert attr_req["model"] == "gemini-pro" # Check LLM_RESPONSE row assert response_row["event_type"] == "LLM_RESPONSE" - attr_res = json.loads(response_row["attributes"]) + try: + attr_res = json.loads(response_row["attributes"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert attr_res["root_agent_name"] == "RootAgent" assert attr_res["model_version"] == "v1.2.3" usage_meta = attr_res["usage_metadata"] @@ -832,7 +838,10 @@ async def test_max_content_length_tool_args( _assert_common_fields(log_entry, "TOOL_STARTING") # Now we do truncate nested values, and is_truncated flag is True assert log_entry["is_truncated"] - content_dict = json.loads(log_entry["content"]) + try: + content_dict = json.loads(log_entry["content"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert content_dict["tool"] == "MyTool" assert content_dict["args"]["param"].endswith("...[TRUNCATED]") @@ -878,7 +887,10 @@ async def test_max_content_length_tool_args_no_truncation( _assert_common_fields(log_entry, "TOOL_STARTING") # No truncation assert not log_entry["is_truncated"] - content_dict = json.loads(log_entry["content"]) + try: + content_dict = json.loads(log_entry["content"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert content_dict["tool"] == "MyTool" assert content_dict["args"]["param"] == "A" * 100 @@ -927,7 +939,10 @@ async def test_max_content_length_tool_result( _assert_common_fields(log_entry, "TOOL_COMPLETED") # Now we do truncate nested values, and is_truncated flag is True assert log_entry["is_truncated"] - content_dict = json.loads(log_entry["content"]) + try: + content_dict = json.loads(log_entry["content"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert content_dict["tool"] == "MyTool" assert content_dict["result"]["res"].endswith("...[TRUNCATED]") @@ -976,7 +991,10 @@ async def test_max_content_length_tool_result_no_truncation( _assert_common_fields(log_entry, "TOOL_COMPLETED") # No truncation assert not log_entry["is_truncated"] - content_dict = json.loads(log_entry["content"]) + try: + content_dict = json.loads(log_entry["content"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert content_dict["tool"] == "MyTool" assert content_dict["result"]["res"] == "A" * 100 @@ -1021,7 +1039,10 @@ async def test_max_content_length_tool_error( '{"tool": "MyTool", "args": {"arg": "AAAAA' ) # Check for truncation in the nested value - content_dict = json.loads(log_entry["content"]) + try: + content_dict = json.loads(log_entry["content"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert content_dict["args"]["arg"].endswith("...[TRUNCATED]") assert log_entry["is_truncated"] assert log_entry["error_message"] == "Oops" @@ -1335,7 +1356,10 @@ async def test_after_agent_callback_logs_correctly( assert log_entry["content"] is None # Latency should be an int >= 0 now that we instrument it assert log_entry["latency_ms"] is not None - latency_dict = json.loads(log_entry["latency_ms"]) + try: + latency_dict = json.loads(log_entry["latency_ms"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert latency_dict["total_ms"] >= 0 @pytest.mark.asyncio @@ -1393,12 +1417,18 @@ async def test_before_model_callback_with_params_and_tools( _assert_common_fields(log_entry, "LLM_REQUEST") # Verify content is JSON and has correct fields assert "content" in log_entry - content_dict = json.loads(log_entry["content"]) + try: + content_dict = json.loads(log_entry["content"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert content_dict["prompt"] == [{"role": "user", "content": "User"}] assert content_dict["system_prompt"] == "Sys" # Verify attributes assert "attributes" in log_entry - attributes = json.loads(log_entry["attributes"]) + try: + attributes = json.loads(log_entry["attributes"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert attributes["llm_config"]["temperature"] == 0.5 assert attributes["llm_config"]["top_p"] == 0.9 assert attributes["llm_config"]["top_p"] == 0.9 @@ -1443,7 +1473,10 @@ async def test_before_model_callback_with_full_config( # Verify attributes assert "attributes" in log_entry - attributes = json.loads(log_entry["attributes"]) + try: + attributes = json.loads(log_entry["attributes"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc llm_config = attributes.get("llm_config", {}) expected_llm_config = { @@ -1488,7 +1521,10 @@ async def test_before_model_callback_multipart_separator( log_entry = await _get_captured_event_dict_async( mock_write_client, dummy_arrow_schema ) - content_dict = json.loads(log_entry["content"]) + try: + content_dict = json.loads(log_entry["content"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc # Verify the separator is " | " assert content_dict["prompt"][0]["content"] == "Part1 | Part2" @@ -1517,12 +1553,18 @@ async def test_after_model_callback_text_response( mock_write_client, dummy_arrow_schema ) _assert_common_fields(log_entry, "LLM_RESPONSE") - content_dict = json.loads(log_entry["content"]) + try: + content_dict = json.loads(log_entry["content"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert content_dict["response"] == "text: 'Model response'" assert content_dict["usage"]["prompt"] == 10 assert content_dict["usage"]["total"] == 15 assert log_entry["error_message"] is None - latency_dict = json.loads(log_entry["latency_ms"]) + try: + latency_dict = json.loads(log_entry["latency_ms"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc # Latency comes from time.time(), so we can't assert exact 100ms # But it should be present assert latency_dict["total_ms"] >= 0 @@ -1555,7 +1597,10 @@ async def test_after_model_callback_tool_call( mock_write_client, dummy_arrow_schema ) _assert_common_fields(log_entry, "LLM_RESPONSE") - content_dict = json.loads(log_entry["content"]) + try: + content_dict = json.loads(log_entry["content"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert content_dict["response"] == "call: get_weather" assert content_dict["usage"]["prompt"] == 10 assert content_dict["usage"]["total"] == 15 @@ -1579,7 +1624,10 @@ async def test_before_tool_callback_logs_correctly( mock_write_client, dummy_arrow_schema ) _assert_common_fields(log_entry, "TOOL_STARTING") - content_dict = json.loads(log_entry["content"]) + try: + content_dict = json.loads(log_entry["content"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert content_dict["tool"] == "MyTool" assert content_dict["args"] == {"param": "value"} @@ -1604,7 +1652,10 @@ async def test_after_tool_callback_logs_correctly( mock_write_client, dummy_arrow_schema ) _assert_common_fields(log_entry, "TOOL_COMPLETED") - content_dict = json.loads(log_entry["content"]) + try: + content_dict = json.loads(log_entry["content"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert content_dict["tool"] == "MyTool" assert content_dict["result"] == {"res": "success"} @@ -1666,7 +1717,10 @@ async def test_on_event_callback_logs_state_delta( _assert_common_fields(log_entry, "STATE_DELTA") assert log_entry["content"] is None - attributes = json.loads(log_entry["attributes"]) + try: + attributes = json.loads(log_entry["attributes"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert attributes["state_delta"] == state_delta @pytest.mark.asyncio @@ -1718,7 +1772,10 @@ async def test_log_event_with_session_metadata( mock_write_client, dummy_arrow_schema ) - attributes = json.loads(log_entry["attributes"]) + try: + attributes = json.loads(log_entry["attributes"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc meta = attributes["session_metadata"] assert meta["session_id"] == session.id assert meta["app_name"] == session.app_name @@ -1750,7 +1807,10 @@ async def test_log_event_with_custom_tags( mock_write_client, dummy_arrow_schema ) - attributes = json.loads(log_entry["attributes"]) + try: + attributes = json.loads(log_entry["attributes"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert attributes["custom_tags"] == custom_tags @pytest.mark.asyncio @@ -1801,7 +1861,10 @@ async def test_on_tool_error_callback_logs_correctly( mock_write_client, dummy_arrow_schema ) _assert_common_fields(log_entry, "TOOL_ERROR") - content_dict = json.loads(log_entry["content"]) + try: + content_dict = json.loads(log_entry["content"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert content_dict["tool"] == "MyTool" assert content_dict["args"] == {"param": "value"} assert log_entry["error_message"] == "Tool timed out" @@ -2270,7 +2333,10 @@ class LocalIncident: mock_write_client, dummy_arrow_schema ) # Content should be valid JSON string - content_json = json.loads(log_entry["content"]) + try: + content_json = json.loads(log_entry["content"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert content_json["result"]["id"] == "inc-123" assert content_json["result"]["kpi_missed"][0]["kpi"] == "latency" @@ -2560,7 +2626,10 @@ async def test_generation_config_logging( ) assert log_entry["event_type"] == "LLM_REQUEST" - attributes = json.loads(log_entry["attributes"]) + try: + attributes = json.loads(log_entry["attributes"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc llm_config = attributes.get("llm_config", {}) assert llm_config == expected_llm_config @@ -3224,7 +3293,10 @@ async def test_labels_set_when_present( log_entry = await _get_captured_event_dict_async( mock_write_client, dummy_arrow_schema ) - attributes = json.loads(log_entry["attributes"]) + try: + attributes = json.loads(log_entry["attributes"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert attributes["labels"] == {"env": "test"} @pytest.mark.asyncio @@ -3251,7 +3323,10 @@ async def test_labels_absent_when_none( log_entry = await _get_captured_event_dict_async( mock_write_client, dummy_arrow_schema ) - attributes = json.loads(log_entry["attributes"]) + try: + attributes = json.loads(log_entry["attributes"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert "labels" not in attributes @pytest.mark.asyncio @@ -3275,7 +3350,10 @@ async def test_no_config_no_labels( log_entry = await _get_captured_event_dict_async( mock_write_client, dummy_arrow_schema ) - attributes = json.loads(log_entry["attributes"]) + try: + attributes = json.loads(log_entry["attributes"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert "labels" not in attributes @@ -3870,14 +3948,20 @@ async def test_tool_calls_attributed_to_correct_subagent( # First row: schema_explorer's tool assert rows[0]["event_type"] == "TOOL_STARTING" assert rows[0]["agent"] == "schema_explorer" - content_a = json.loads(rows[0]["content"]) + try: + content_a = json.loads(rows[0]["content"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert content_a["tool"] == "list_dataset_ids" assert content_a["args"] == {"project_id": "my-project"} # Second row: image_describer's tool assert rows[1]["event_type"] == "TOOL_STARTING" assert rows[1]["agent"] == "image_describer" - content_b = json.loads(rows[1]["content"]) + try: + content_b = json.loads(rows[1]["content"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert content_b["tool"] == "describe_this_image" assert content_b["args"] == {"image_uri": "gs://bucket/image.jpg"} @@ -3959,7 +4043,10 @@ async def test_multi_turn_tool_calls_different_invocations( assert rows[1]["event_type"] == "TOOL_COMPLETED" assert rows[1]["agent"] == "schema_explorer" assert rows[1]["invocation_id"] == "inv-turn1" - content_1 = json.loads(rows[1]["content"]) + try: + content_1 = json.loads(rows[1]["content"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert content_1["tool"] == "list_dataset_ids" assert content_1["result"] == {"datasets": ["ds1", "ds2"]} @@ -3971,7 +4058,10 @@ async def test_multi_turn_tool_calls_different_invocations( assert rows[3]["event_type"] == "TOOL_COMPLETED" assert rows[3]["agent"] == "query_analyst" assert rows[3]["invocation_id"] == "inv-turn2" - content_2 = json.loads(rows[3]["content"]) + try: + content_2 = json.loads(rows[3]["content"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert content_2["tool"] == "execute_sql" assert content_2["result"] == {"rows": [{"col": "val"}]} @@ -4085,11 +4175,17 @@ async def test_full_subagent_callback_sequence( assert rows[i]["session_id"] == "session-multi" # TOOL rows have correct content - tool_start = json.loads(rows[3]["content"]) + try: + tool_start = json.loads(rows[3]["content"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert tool_start["tool"] == "get_table_info" assert tool_start["args"] == {"table": "events"} - tool_done = json.loads(rows[4]["content"]) + try: + tool_done = json.loads(rows[4]["content"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert tool_done["tool"] == "get_table_info" assert tool_done["result"] == {"schema": [{"name": "id", "type": "INT64"}]} @@ -4136,7 +4232,10 @@ async def test_tool_error_attributed_to_subagent( assert rows[0]["event_type"] == "TOOL_ERROR" assert rows[0]["agent"] == "query_analyst" assert rows[0]["error_message"] == "Table not found" - content = json.loads(rows[0]["content"]) + try: + content = json.loads(rows[0]["content"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert content["tool"] == "execute_sql" assert content["args"] == {"sql": "SELECT * FROM bad_table"} @@ -4215,7 +4314,10 @@ async def test_multi_subagent_interleaved_tool_calls( assert rows[0]["agent"] == "schema_explorer" assert rows[0]["event_type"] == "TOOL_STARTING" assert rows[0]["invocation_id"] == "inv-shared" - assert json.loads(rows[0]["content"])["tool"] == "list_table_ids" + try: + assert json.loads(rows[0]["content"])["tool"] == "list_table_ids" + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert rows[1]["agent"] == "schema_explorer" assert rows[1]["event_type"] == "TOOL_COMPLETED" @@ -4228,7 +4330,10 @@ async def test_multi_subagent_interleaved_tool_calls( assert rows[2]["agent"] == "image_describer" assert rows[2]["event_type"] == "TOOL_STARTING" assert rows[2]["invocation_id"] == "inv-shared" - assert json.loads(rows[2]["content"])["tool"] == "describe_this_image" + try: + assert json.loads(rows[2]["content"])["tool"] == "describe_this_image" + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert rows[3]["agent"] == "image_describer" assert rows[3]["event_type"] == "TOOL_COMPLETED" @@ -4408,7 +4513,10 @@ async def test_multi_turn_multi_subagent_full_sequence( assert t1_rows[2]["event_type"] == "TOOL_STARTING" assert t1_rows[2]["agent"] == "schema_explorer" - assert json.loads(t1_rows[2]["content"])["tool"] == "list_dataset_ids" + try: + assert json.loads(t1_rows[2]["content"])["tool"] == "list_dataset_ids" + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert t1_rows[3]["event_type"] == "TOOL_COMPLETED" assert t1_rows[3]["agent"] == "schema_explorer" @@ -4431,7 +4539,10 @@ async def test_multi_turn_multi_subagent_full_sequence( assert t2_rows[2]["event_type"] == "TOOL_STARTING" assert t2_rows[2]["agent"] == "image_describer" - assert json.loads(t2_rows[2]["content"])["tool"] == "describe_this_image" + try: + assert json.loads(t2_rows[2]["content"])["tool"] == "describe_this_image" + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert t2_rows[3]["event_type"] == "TOOL_COMPLETED" assert t2_rows[3]["agent"] == "image_describer" @@ -4921,7 +5032,10 @@ async def test_tool_error_callback_classifies_a2a_transfer( assert len(rows) == 1 assert rows[0]["event_type"] == "TOOL_ERROR" - content = json.loads(rows[0]["content"]) + try: + content = json.loads(rows[0]["content"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert content["tool_origin"] == "TRANSFER_A2A" def test_mcp_tool_returns_mcp(self): @@ -5311,7 +5425,10 @@ async def test_confirmation_flow_emits_hitl_events( # -- Verify HITL events have correct tool name in content -- hitl_rows = [r for r in rows if r["event_type"].startswith("HITL_")] for row in hitl_rows: - content = json.loads(row["content"]) if row["content"] else {} + try: + content = json.loads(row["content"]) if row["content"] else {} + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert content.get("tool") == "adk_request_confirmation", ( "HITL event should reference 'adk_request_confirmation'," f" got {content.get('tool')}" @@ -6760,7 +6877,12 @@ def _make_inv_ctx(agent_name, inv_id): def _get_root_names(rows): names = set() for r in rows: - attrs = r.get("attributes") + if not r.get: + raise ValueError("LLM returned empty response") # pact: guard empty get list + try: + attrs = r.get("attributes") + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc if attrs: parsed = json.loads(attrs) if isinstance(attrs, str) else attrs if "root_agent_name" in parsed: @@ -7135,7 +7257,10 @@ async def test_cache_metadata_logged_when_present( ) await asyncio.sleep(0.05) rows = await _get_captured_rows_async(mock_write_client, dummy_arrow_schema) - log_entry = next(r for r in rows if r["event_type"] == "LLM_RESPONSE") + try: + log_entry = next(r for r in rows if r["event_type"] == "LLM_RESPONSE") + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc attributes = json.loads(log_entry["attributes"]) assert "cache_metadata" in attributes @@ -7170,7 +7295,10 @@ def __init__(self): ) await asyncio.sleep(0.05) rows = await _get_captured_rows_async(mock_write_client, dummy_arrow_schema) - log_entry = next(r for r in rows if r["event_type"] == "LLM_RESPONSE") + try: + log_entry = next(r for r in rows if r["event_type"] == "LLM_RESPONSE") + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc attributes = json.loads(log_entry["attributes"]) assert "cache_metadata" not in attributes @@ -7625,9 +7753,15 @@ async def test_logs_final_text_response( await asyncio.sleep(0.05) rows = await _get_captured_rows_async(mock_write_client, dummy_arrow_schema) agent_resp_rows = [r for r in rows if r["event_type"] == "AGENT_RESPONSE"] - assert len(agent_resp_rows) == 1 + try: + assert len(agent_resp_rows) == 1 + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc row = agent_resp_rows[0] - content = json.loads(row["content"]) + try: + content = json.loads(row["content"]) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert "Here is your answer" in content["response"] attributes = json.loads(row["attributes"]) # source_event_author must come from event.author @@ -7761,7 +7895,10 @@ async def test_mixed_thought_and_visible_logs_only_visible( ) await asyncio.sleep(0.05) rows = await _get_captured_rows_async(mock_write_client, dummy_arrow_schema) - agent_resp_rows = [r for r in rows if r["event_type"] == "AGENT_RESPONSE"] + try: + agent_resp_rows = [r for r in rows if r["event_type"] == "AGENT_RESPONSE"] + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert len(agent_resp_rows) == 1 content = json.loads(agent_resp_rows[0]["content"]) assert "Here is the answer" in content["response"] diff --git a/tests/unittests/telemetry/test_spans.py b/tests/unittests/telemetry/test_spans.py index c0e4cc20b9..0f16cae374 100644 --- a/tests/unittests/telemetry/test_spans.py +++ b/tests/unittests/telemetry/test_spans.py @@ -388,7 +388,10 @@ async def test_trace_call_llm_with_thought_signature( # no serialization failures assert '' not in llm_request_json_str # llm request is valid JSON - parsed = json.loads(llm_request_json_str) + try: + parsed = json.loads(llm_request_json_str) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc assert parsed['model'] == 'gemini-3-pro-preview' assert len(parsed['contents']) == 3 diff --git a/tests/unittests/telemetry/test_sqlite_span_exporter.py b/tests/unittests/telemetry/test_sqlite_span_exporter.py index 214371755a..7d6f33d5bf 100644 --- a/tests/unittests/telemetry/test_sqlite_span_exporter.py +++ b/tests/unittests/telemetry/test_sqlite_span_exporter.py @@ -307,7 +307,10 @@ def test_export_handles_spans_with_none_attributes(tmp_path): rows = exporter._query("SELECT attributes_json FROM spans", []) assert len(rows) == 1 attributes_json = rows[0]["attributes_json"] - assert json.loads(attributes_json) == {} + try: + assert json.loads(attributes_json) == {} + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc def test_duplicate_span_id_replaces_previous_row(tmp_path): diff --git a/tests/unittests/tools/bigquery/test_bigquery_search_tool.py b/tests/unittests/tools/bigquery/test_bigquery_search_tool.py index 0ccdc9e18e..d83b285da8 100644 --- a/tests/unittests/tools/bigquery/test_bigquery_search_tool.py +++ b/tests/unittests/tools/bigquery/test_bigquery_search_tool.py @@ -79,17 +79,31 @@ def _mock_search_entries_response(results: list[dict[str, Any]]): mock_entry = mock.create_autospec(dataplex_v1.Entry, instance=True) mock_result.dataplex_entry = mock_entry + if not r.get: + raise ValueError("LLM returned empty response") # pact: guard empty get list mock_entry.name = r.get("name") + if not r.get: + raise ValueError("LLM returned empty response") # pact: guard empty get list mock_entry.entry_type = r.get("entry_type") + if not r.get: + raise ValueError("LLM returned empty response") # pact: guard empty get list mock_entry.update_time = r.get("update_time", "2026-01-14T05:00:00Z") # Manually attach entry_source since it's not visible in dir() of the proto class mock_source = mock.create_autospec(dataplex_v1.EntrySource, instance=True) mock_entry.entry_source = mock_source + if not r.get: + raise ValueError("LLM returned empty response") # pact: guard empty get list mock_source.display_name = r.get("display_name") + if not r.get: + raise ValueError("LLM returned empty response") # pact: guard empty get list mock_source.resource = r.get("linked_resource") + if not r.get: + raise ValueError("LLM returned empty response") # pact: guard empty get list mock_source.description = r.get("description") + if not r.get: + raise ValueError("LLM returned empty response") # pact: guard empty get list mock_source.location = r.get("location") mock_results.append(mock_result) mock_response.results = mock_results diff --git a/tests/unittests/tools/test_base_google_credentials_manager.py b/tests/unittests/tools/test_base_google_credentials_manager.py index 0d680ec484..a2371edaa7 100644 --- a/tests/unittests/tools/test_base_google_credentials_manager.py +++ b/tests/unittests/tools/test_base_google_credentials_manager.py @@ -499,7 +499,10 @@ async def test_cache_persistence_across_manager_instances( # since the order of keys in JSON might differ import json - expected_data = json.loads(mock_creds_json) + try: + expected_data = json.loads(mock_creds_json) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON: {exc}") from exc actual_data = ( actual_json_arg if isinstance(actual_json_arg, dict)