diff --git a/agent_sdks/python/src/a2ui/adk/a2a_extension/send_a2ui_to_client_toolset.py b/agent_sdks/python/src/a2ui/adk/a2a_extension/send_a2ui_to_client_toolset.py index 8803b71c2..50555ef1b 100644 --- a/agent_sdks/python/src/a2ui/adk/a2a_extension/send_a2ui_to_client_toolset.py +++ b/agent_sdks/python/src/a2ui/adk/a2a_extension/send_a2ui_to_client_toolset.py @@ -110,6 +110,7 @@ async def get_examples(ctx: ReadonlyContext) -> str: ) from a2ui.core.parser.parser import has_a2ui_parts from a2ui.core.parser.payload_fixer import parse_and_fix +from a2ui.core.schema.constants import A2UI_SCHEMA_BLOCK_START, A2UI_SCHEMA_BLOCK_END from a2ui.core.schema.catalog import A2uiCatalog from google.adk.a2a.converters import part_converter from google.adk.agents.readonly_context import ReadonlyContext @@ -225,7 +226,7 @@ def __init__( " render multiple UI surfaces.Args: " f" {self.A2UI_JSON_ARG_NAME}: Valid A2UI JSON Schema to send to" " the client. The A2UI JSON Schema definition is between" - " ---BEGIN A2UI JSON SCHEMA--- and ---END A2UI JSON SCHEMA--- in" + f" {A2UI_SCHEMA_BLOCK_START} and {A2UI_SCHEMA_BLOCK_END} in" " the system instructions." ), ) diff --git a/agent_sdks/python/src/a2ui/core/inference_strategy.py b/agent_sdks/python/src/a2ui/core/inference_strategy.py index f74db8585..b25e1233d 100644 --- a/agent_sdks/python/src/a2ui/core/inference_strategy.py +++ b/agent_sdks/python/src/a2ui/core/inference_strategy.py @@ -24,7 +24,7 @@ def generate_system_prompt( role_description: str, workflow_description: str = "", ui_description: str = "", - supported_catalog_ids: List[str] = [], + client_ui_capabilities: Optional[dict[str, Any]] = None, allowed_components: List[str] = [], include_schema: bool = False, include_examples: bool = False, @@ -37,7 +37,7 @@ def generate_system_prompt( role_description: Description of the agent's role. workflow_description: Description of the workflow. ui_description: Description of the UI. - supported_catalog_ids: List of supported catalog IDs. + client_ui_capabilities: Capabilities reported by the client for targeted schema pruning. allowed_components: List of allowed components. include_schema: Whether to include the schema. include_examples: Whether to include examples. diff --git a/agent_sdks/python/src/a2ui/core/parser/payload_fixer.py b/agent_sdks/python/src/a2ui/core/parser/payload_fixer.py index 08a316c71..8fc5f8163 100644 --- a/agent_sdks/python/src/a2ui/core/parser/payload_fixer.py +++ b/agent_sdks/python/src/a2ui/core/parser/payload_fixer.py @@ -30,15 +30,16 @@ def parse_and_fix(payload: str) -> List[Dict[str, Any]]: Returns: A parsed and potentially fixed payload (list of dicts). """ + normalized_payload = _normalize_smart_quotes(payload) try: - a2ui_json = _parse(payload) + a2ui_json = _parse(normalized_payload) return a2ui_json except ( json.JSONDecodeError, ValueError, ) as e: logger.warning(f"Initial A2UI payload validation failed: {e}") - updated_payload = _remove_trailing_commas(payload) + updated_payload = _remove_trailing_commas(normalized_payload) a2ui_json = _parse(updated_payload) return a2ui_json @@ -56,6 +57,16 @@ def _parse(payload: str) -> List[Dict[str, Any]]: raise ValueError(f"Failed to parse JSON: {e}") +def _normalize_smart_quotes(json_str: str) -> str: + """Replaces smart (curly) quotes with standard straight quotes.""" + return ( + json_str.replace("\u201C", '"') + .replace("\u201D", '"') + .replace("\u2018", "'") + .replace("\u2019", "'") + ) + + def _remove_trailing_commas(json_str: str) -> str: """Attempts to remove trailing commas from a JSON string. diff --git a/agent_sdks/python/src/a2ui/core/schema/catalog.py b/agent_sdks/python/src/a2ui/core/schema/catalog.py index b02b5faf8..b845c170c 100644 --- a/agent_sdks/python/src/a2ui/core/schema/catalog.py +++ b/agent_sdks/python/src/a2ui/core/schema/catalog.py @@ -20,7 +20,12 @@ from typing import Any, Dict, List, Optional, TYPE_CHECKING from .catalog_provider import A2uiCatalogProvider, FileSystemCatalogProvider -from .constants import CATALOG_COMPONENTS_KEY, CATALOG_ID_KEY +from .constants import ( + A2UI_SCHEMA_BLOCK_START, + A2UI_SCHEMA_BLOCK_END, + CATALOG_COMPONENTS_KEY, + CATALOG_ID_KEY, +) @dataclass @@ -132,7 +137,7 @@ def with_pruned_components(self, allowed_components: List[str]) -> "A2uiCatalog" def render_as_llm_instructions(self) -> str: """Renders the catalog and schema as LLM instructions.""" all_schemas = [] - all_schemas.append("---BEGIN A2UI JSON SCHEMA---") + all_schemas.append(A2UI_SCHEMA_BLOCK_START) server_client_str = ( json.dumps(self.s2c_schema, indent=2) if self.s2c_schema else "{}" @@ -146,7 +151,7 @@ def render_as_llm_instructions(self) -> str: catalog_str = json.dumps(self.catalog_schema, indent=2) all_schemas.append(f"### Catalog Schema:\n{catalog_str}") - all_schemas.append("---END A2UI JSON SCHEMA---") + all_schemas.append(A2UI_SCHEMA_BLOCK_END) return "\n\n".join(all_schemas) @@ -166,7 +171,7 @@ def load_examples(self, path: Optional[str], validate: bool = False) -> str: content = f.read() if validate: - self._validate_example(full_path, basename, content) + self._validate_example(full_path, content) merged_examples.append( f"---BEGIN {basename}---\n{content}\n---END {basename}---" @@ -176,7 +181,7 @@ def load_examples(self, path: Optional[str], validate: bool = False) -> str: return "" return "\n\n".join(merged_examples) - def _validate_example(self, full_path: str, basename: str, content: str) -> None: + def _validate_example(self, full_path: str, content: str) -> None: try: json_data = json.loads(content) self.validator.validate(json_data) diff --git a/agent_sdks/python/src/a2ui/core/schema/constants.py b/agent_sdks/python/src/a2ui/core/schema/constants.py index e1df873e3..8cbbb6b11 100644 --- a/agent_sdks/python/src/a2ui/core/schema/constants.py +++ b/agent_sdks/python/src/a2ui/core/schema/constants.py @@ -50,6 +50,9 @@ A2UI_OPEN_TAG = "" A2UI_CLOSE_TAG = "" +A2UI_SCHEMA_BLOCK_START = "---BEGIN A2UI JSON SCHEMA---" +A2UI_SCHEMA_BLOCK_END = "---END A2UI JSON SCHEMA---" + DEFAULT_WORKFLOW_RULES = f""" The generated response MUST follow these rules: 1. The response can contain one or more A2UI JSON blocks. diff --git a/agent_sdks/python/tests/core/parser/test_payload_fixer.py b/agent_sdks/python/tests/core/parser/test_payload_fixer.py index 8bf46a524..2bd554d08 100644 --- a/agent_sdks/python/tests/core/parser/test_payload_fixer.py +++ b/agent_sdks/python/tests/core/parser/test_payload_fixer.py @@ -70,3 +70,11 @@ def test_fix_payload_success_after_fix(caplog): assert result == [{"type": "Text", "text": "Hello"}] assert "Initial A2UI payload validation failed" in caplog.text assert "Detected trailing commas in LLM output; applied autofix." in caplog.text + + +def test_normalizes_smart_quotes(): + """Replaces smart quotes with standard straight quotes.""" + smart_quotes_json = '{"type": “Text”, "other": "Value’s"}' + result = parse_and_fix(smart_quotes_json) + + assert result == [{"type": "Text", "other": "Value's"}] diff --git a/agent_sdks/python/tests/core/schema/test_catalog.py b/agent_sdks/python/tests/core/schema/test_catalog.py index b0996d914..f29d37a4d 100644 --- a/agent_sdks/python/tests/core/schema/test_catalog.py +++ b/agent_sdks/python/tests/core/schema/test_catalog.py @@ -17,7 +17,12 @@ import pytest from typing import Any, Dict, List from a2ui.core.schema.catalog import A2uiCatalog -from a2ui.core.schema.constants import VERSION_0_8, VERSION_0_9 +from a2ui.core.schema.constants import ( + A2UI_SCHEMA_BLOCK_START, + A2UI_SCHEMA_BLOCK_END, + VERSION_0_8, + VERSION_0_9, +) from a2ui.basic_catalog.constants import BASIC_CATALOG_NAME @@ -197,10 +202,10 @@ def test_render_as_llm_instructions(): ) schema_str = catalog.render_as_llm_instructions() - assert "---BEGIN A2UI JSON SCHEMA---" in schema_str + assert A2UI_SCHEMA_BLOCK_START in schema_str assert '### Server To Client Schema:\n{\n "s2c": "schema"\n}' in schema_str assert '### Common Types Schema:\n{\n "common": "types"\n}' in schema_str assert "### Catalog Schema:" in schema_str assert '"catalog": "schema"' in schema_str assert '"catalogId": "id_basic"' in schema_str - assert "---END A2UI JSON SCHEMA---" in schema_str + assert A2UI_SCHEMA_BLOCK_END in schema_str diff --git a/agent_sdks/python/tests/core/schema/test_schema_manager.py b/agent_sdks/python/tests/core/schema/test_schema_manager.py index e4435635d..9355fd776 100644 --- a/agent_sdks/python/tests/core/schema/test_schema_manager.py +++ b/agent_sdks/python/tests/core/schema/test_schema_manager.py @@ -18,13 +18,17 @@ from a2ui.basic_catalog import BasicCatalog from a2ui.basic_catalog.constants import BASIC_CATALOG_NAME from a2ui.core.schema.constants import ( - CATALOG_COMPONENTS_KEY, DEFAULT_WORKFLOW_RULES, INLINE_CATALOG_NAME, VERSION_0_8, VERSION_0_9, ) -from a2ui.core.schema.constants import INLINE_CATALOGS_KEY, SUPPORTED_CATALOG_IDS_KEY +from a2ui.core.schema.constants import ( + A2UI_SCHEMA_BLOCK_START, + A2UI_SCHEMA_BLOCK_END, + INLINE_CATALOGS_KEY, + SUPPORTED_CATALOG_IDS_KEY, +) @pytest.fixture @@ -214,10 +218,10 @@ def joinpath_side_effect(path): assert "Manage workflow." in prompt assert "## UI Description:" in prompt assert "RENDERUI." in prompt.replace(" ", "").upper() - assert "---BEGIN A2UI JSON SCHEMA---" in prompt + assert A2UI_SCHEMA_BLOCK_START in prompt assert "### Server To Client Schema:" in prompt assert "### Catalog Schema" in prompt - assert "---END A2UI JSON SCHEMA---" in prompt + assert A2UI_SCHEMA_BLOCK_END in prompt assert '"Text":{}' in prompt.replace(" ", "") @@ -336,7 +340,7 @@ def joinpath_side_effect(path): assert "## UI Description:" not in prompt assert "## Examples:" not in prompt assert "Just Role" in prompt - assert "---BEGIN A2UI JSON SCHEMA---" not in prompt + assert A2UI_SCHEMA_BLOCK_START not in prompt def test_generate_system_prompt_custom_workflow_appending(mock_importlib_resources): @@ -411,7 +415,7 @@ def joinpath_side_effect(path): ) assert "Role" in prompt - assert "---BEGIN A2UI JSON SCHEMA---" in prompt + assert A2UI_SCHEMA_BLOCK_START in prompt # Inline catalog is merged onto the base catalog (catalogId: "basic") assert "### Catalog Schema:" in prompt assert '"catalogId": "basic"' in prompt