Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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."
),
)
Expand Down
4 changes: 2 additions & 2 deletions agent_sdks/python/src/a2ui/core/inference_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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.
Expand Down
15 changes: 13 additions & 2 deletions agent_sdks/python/src/a2ui/core/parser/payload_fixer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.

Expand Down
15 changes: 10 additions & 5 deletions agent_sdks/python/src/a2ui/core/schema/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 "{}"
Expand All @@ -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)

Expand All @@ -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}---"
Expand All @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions agent_sdks/python/src/a2ui/core/schema/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@
A2UI_OPEN_TAG = "<a2ui-json>"
A2UI_CLOSE_TAG = "</a2ui-json>"

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.
Expand Down
8 changes: 8 additions & 0 deletions agent_sdks/python/tests/core/parser/test_payload_fixer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}]
11 changes: 8 additions & 3 deletions agent_sdks/python/tests/core/schema/test_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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
16 changes: 10 additions & 6 deletions agent_sdks/python/tests/core/schema/test_schema_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(" ", "")


Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down
Loading