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
1 change: 1 addition & 0 deletions python/a2ui_agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
standalone.
[#3232](https://github.com/a2ui-project/a2ui/issues/3232),
[#3575](https://github.com/a2ui-project/a2ui/pull/3575)
- The Express compiler accepts `userMessage=` and `context=` keyword arguments on `Event(...)`, compiling `userMessage` to the v1.0 `event.userMessage` field, and raises `ExpressValidationError` for `userMessage` before v1.0 and for any keyword `Event` does not define, where it used to drop them. A function call in an action slot nested inside a list item or object property (such as `items: [{label, action: openUrl(...)}]`) is now compiled as a `functionCall` action. `ExpressDecompiler` writes `userMessage=` back ([#3598](https://github.com/a2ui-project/a2ui/issues/3598)).
- The Express compiler keeps a map literal as written, quoted keys or not, when
one of its keys shares a name with an Express construct (`variable`, `path`,
`call`, `check`, `skipped`); before, such a map was read as a variable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
surface_catalog_id,
)
from .coalesce import CoalescedMessage, coalesce_surface_messages
from .schema_helper import CatalogSchemaHelper
from .schema_helper import CatalogSchemaHelper, is_action_slot

__all__ = [
"CatalogSchemaHelper",
Expand All @@ -44,6 +44,7 @@
"check_catalogs",
"check_mixed_catalogs",
"coalesce_surface_messages",
"is_action_slot",
"normalize_prompt_example_messages",
"surface_catalog_id",
"to_message_dicts",
Expand Down
117 changes: 117 additions & 0 deletions python/a2ui_agent/src/a2ui/inference_formats/_shared/schema_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,37 @@
_COMMON_DEF_RE = re.compile(r"(?:^|/)common_types\.json#/(?:\$defs|definitions)/(\w+)$")


def _common_def_name(ref: Any) -> str | None:
"""Returns the common type a `$ref` names, or None if it is not one."""
if not isinstance(ref, str):
return None
m = _COMMON_DEF_RE.search(ref)
return m.group(1) if m else None


def is_action_slot(schema: Any) -> bool:
"""Checks whether a property schema is an Action slot.

A property is an action slot when its schema references the common `Action`
def, directly or through `oneOf`/`anyOf`/`allOf`.

Args:
schema: The property JSON schema to inspect.

Returns:
True if the schema references the common `Action` type.
"""
if not isinstance(schema, dict):
return False
if _common_def_name(schema.get("$ref")) == "Action":
return True
for key in ("allOf", "oneOf", "anyOf"):
subs = schema.get(key)
if isinstance(subs, list) and any(is_action_slot(sub) for sub in subs):
return True
return False


def _find_enum(s: Any) -> list[Any] | None:
"""Returns the first `enum` list found in a schema or its combinators."""
if isinstance(s, dict):
Expand Down Expand Up @@ -462,3 +493,89 @@ def _check(s: Any, in_common_types: bool) -> bool:
return False

return _check(schema, False)

def resolve_item_schema(self, schema: Any) -> dict[str, Any] | None:
"""Resolves the schema of an array's items.

Follows `$ref` (into both the common types and the catalog document),
`allOf`, `oneOf` and `anyOf` to find the schema declared at that
position.

Args:
schema: The array's JSON schema.

Returns:
The resolved item schema, or None if none is declared.
"""
return self._resolve_at(schema, None)

def resolve_property_schema(self, schema: Any, name: str) -> dict[str, Any] | None:
"""Resolves the schema of an object's property.

Follows `$ref` (into both the common types and the catalog document),
`allOf`, `oneOf` and `anyOf` to find the schema declared at that
position, and falls back to `additionalProperties` when the property
is not declared. A property named `items` is looked up as a property,
not as the array items.

Args:
schema: The object's JSON schema.
name: The property name.

Returns:
The resolved property schema, or None if none is declared.
"""
return self._resolve_at(schema, name)

def _resolve_at(self, schema: Any, prop: str | None) -> dict[str, Any] | None:
"""Resolves the schema at a property, or at the array items when `prop` is None."""
visited: set[int] = set()

def _resolve(s: Any, in_common_types: bool) -> dict[str, Any] | None:
if not isinstance(s, dict) or id(s) in visited:
return None
visited.add(id(s))

if prop is None:
items = s.get("items")
if isinstance(items, dict):
return items
else:
props = s.get("properties")
declared = props.get(prop) if isinstance(props, dict) else None
if isinstance(declared, dict):
return declared
additional = s.get("additionalProperties")
if isinstance(additional, dict):
return additional

ref = s.get("$ref")
if isinstance(ref, str):
def_name = _common_def_name(ref)
if def_name:
found = _resolve(self._lookup_common_def(def_name), True)
elif in_common_types and (
ref.startswith("#/$defs/") or ref.startswith("#/definitions/")
):
name = ref[8:] if ref.startswith("#/$defs/") else ref[14:]
found = _resolve(self._lookup_common_def(name), True)
elif ref.startswith("#/"):
found = _resolve(
self._resolve_json_pointer(self.catalog, ref), False
)
else:
found = None
if found:
return found

for combinator in ("allOf", "oneOf", "anyOf"):
subs = s.get(combinator)
if isinstance(subs, list):
for sub in subs:
found = _resolve(sub, in_common_types)
if found:
return found

return None

return _resolve(schema, False)
Loading
Loading