Skip to content

Commit a87a01b

Browse files
committed
fix(agent): Autofix trailing commas in LLM-generated JSON
revert sample-agentand add fix to adk-agent for malformed JSON Add robust JSON autofix and tests Add robust JSON autofix logic
1 parent db3b335 commit a87a01b

3 files changed

Lines changed: 52 additions & 10 deletions

File tree

a2a_agents/python/a2ui_agent/src/a2ui/extension/send_a2ui_to_client_toolset.py

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ async def get_schema(ctx: ReadonlyContext) -> dict[str, Any]:
8080
import inspect
8181
import json
8282
import logging
83+
import re
8384
from typing import Any, Awaitable, Callable, Optional, TypeAlias, Union
8485

8586
import jsonschema
@@ -252,17 +253,27 @@ async def run_async(
252253
f" arg {self.A2UI_JSON_ARG_NAME} "
253254
)
254255

255-
a2ui_json_payload = json.loads(a2ui_json)
256+
a2ui_schema = await self.get_a2ui_schema(tool_context)
257+
try:
258+
# Attempt to parse and validate
259+
a2ui_json_payload = json.loads(a2ui_json)
260+
jsonschema.validate(instance=a2ui_json_payload, schema=a2ui_schema)
256261

257-
# Auto-wrap single object in list
258-
if not isinstance(a2ui_json_payload, list):
259-
logger.info(
260-
"Received a single JSON object, wrapping in a list for validation."
261-
)
262-
a2ui_json_payload = [a2ui_json_payload]
262+
except (jsonschema.exceptions.ValidationError, json.JSONDecodeError) as e:
263+
logger.warning(f"Initial A2UI JSON validation failed: {e}")
263264

264-
a2ui_schema = await self.get_a2ui_schema(tool_context)
265-
jsonschema.validate(instance=a2ui_json_payload, schema=a2ui_schema)
265+
# Run Fixer
266+
fixed_a2ui_json = re.sub(r",(?=\s*[\]}])", "", a2ui_json)
267+
268+
if fixed_a2ui_json != a2ui_json:
269+
# Emit Warning
270+
logger.warning("Detected trailing commas in LLM output; applied autofix.")
271+
272+
# Re-parse and Re-validate
273+
a2ui_json_payload = json.loads(fixed_a2ui_json)
274+
jsonschema.validate(instance=a2ui_json_payload, schema=a2ui_schema)
275+
else:
276+
raise e
266277

267278
logger.info(
268279
f"Validated call to tool {self.TOOL_NAME} with {self.A2UI_JSON_ARG_NAME}"
@@ -328,4 +339,4 @@ def convert_send_a2ui_to_client_genai_part_to_a2a_part(
328339
converted_part = part_converter.convert_genai_part_to_a2a_part(part)
329340

330341
logger.info(f"Returning converted part: {converted_part}")
331-
return [converted_part] if converted_part else []
342+
return [converted_part] if converted_part else []

a2a_agents/python/a2ui_agent/tests/extension/test_send_a2ui_to_client_toolset.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,33 @@ async def test_send_tool_run_async_schema_validation_fail():
243243
assert "'text' is a required property" in result["error"]
244244

245245

246+
@pytest.mark.asyncio
247+
async def test_send_tool_run_async_handles_trailing_comma(caplog):
248+
"""Tests that the tool's run_async can handle and fix a trailing comma in the JSON."""
249+
tool = SendA2uiToClientToolset._SendA2uiJsonToClientTool(TEST_A2UI_SCHEMA)
250+
tool_context_mock = MagicMock(spec=ToolContext)
251+
tool_context_mock.state = {}
252+
tool_context_mock.actions = MagicMock(skip_summarization=False)
253+
254+
# Malformed JSON with a trailing comma in the list
255+
malformed_a2ui_str = '[{"type": "Text", "text": "Hello"},]'
256+
257+
args = {
258+
SendA2uiToClientToolset._SendA2uiJsonToClientTool.A2UI_JSON_ARG_NAME: malformed_a2ui_str
259+
}
260+
261+
result = await tool.run_async(args=args, tool_context=tool_context_mock)
262+
263+
# Assert that the fix was successful and the result is correct
264+
expected_a2ui = [{"type": "Text", "text": "Hello"}]
265+
assert result == {
266+
SendA2uiToClientToolset._SendA2uiJsonToClientTool.VALIDATED_A2UI_JSON_KEY: expected_a2ui
267+
}
268+
269+
# Assert that the warning was logged
270+
assert "Detected trailing commas in LLM output; applied autofix." in caplog.text
271+
272+
246273
# endregion
247274

248275
# region send_a2ui_to_client_part_converter Tests

samples/agent/adk/restaurant_finder/agent.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import json
1616
import logging
1717
import os
18+
import re
1819
from collections.abc import AsyncIterable
1920
from typing import Any
2021

@@ -226,6 +227,9 @@ async def stream(self, query, session_id) -> AsyncIterable[dict[str, Any]]:
226227
if not json_string_cleaned:
227228
raise ValueError("Cleaned JSON string is empty.")
228229

230+
# Autofix: Remove trailing commas from arrays and objects
231+
json_string_cleaned = re.sub(r",\s*([\]}])", r"\1", json_string_cleaned)
232+
229233
# --- New Validation Steps ---
230234
# 1. Check if it's parsable JSON
231235
parsed_json_data = json.loads(json_string_cleaned)

0 commit comments

Comments
 (0)