Skip to content

Python a2ui_agent: Refactor a2ui_agent to depend on a2ui_core#1582

Open
nan-yu wants to merge 15 commits into
a2ui-project:mainfrom
nan-yu:re-arch-9-agent
Open

Python a2ui_agent: Refactor a2ui_agent to depend on a2ui_core#1582
nan-yu wants to merge 15 commits into
a2ui-project:mainfrom
nan-yu:re-arch-9-agent

Conversation

@nan-yu

@nan-yu nan-yu commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Description

Replace this paragraph with a description of what this PR is changing or adding, and why. Consider including before/after screenshots.

List which issues are fixed by this PR. For larger changes, raising an issue first helps reduce redundant work.

Pre-launch Checklist

If you need help, consider asking for advice on the discussion board.

nan-yu added 15 commits June 9, 2026 00:00
- Implement ExpressionParser mirroring the TypeScript version to support dynamic formatString placeholders.
- Add basic catalog function implementations (arithmetic, comparison, logical, string, formatting, and validation).

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the a2ui_core Python library, which implements core state management, rendering, message processing, and schema validation for the A2UI v0.9+ specification. Key feedback highlights several critical issues and improvement opportunities: a NameError in model_catalog.py due to an undefined variable, incomplete TR35 date token formatting in function_impls.py, redundant subscriptions for TemplateChildList in generic_binder.py, an inefficient list expansion loop in data_model.py that could trigger OOM errors, silent acceptance of unclosed string literals in expression_parser.py, and a potential AttributeError in data_context.py when handling malformed action events.

Comment on lines +114 to +121
target_val = (
payload
if (
hasattr(fn.schema, "model_fields")
and "call" in fn.schema.model_fields
)
else args
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

A NameError will occur here because the variable payload is referenced but has not been defined in this scope. It should be replaced with the dictionary representing the full function call structure.

Suggested change
target_val = (
payload
if (
hasattr(fn.schema, "model_fields")
and "call" in fn.schema.model_fields
)
else args
)
target_val = (
{"call": name, "args": args}
if (
hasattr(fn.schema, "model_fields")
and "call" in fn.schema.model_fields
)
else args
)

Comment on lines +206 to +214
py_fmt = (
str(fmt)
.replace("yyyy", "%Y")
.replace("MM", "%m")
.replace("dd", "%d")
.replace("HH", "%H")
.replace("mm", "%M")
.replace("ss", "%S")
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current _format_date implementation only replaces a small subset of the documented TR35 date pattern tokens. Common tokens like yy, MMMM, MMM, EEEE, E, hh, h, and a are completely ignored, which will cause formatting to fail or output raw pattern characters.

Using a single-pass regular expression replacement is the most robust way to map these tokens safely without causing recursive replacement collisions (e.g., replacing d after dd has already introduced %d).

Suggested change
py_fmt = (
str(fmt)
.replace("yyyy", "%Y")
.replace("MM", "%m")
.replace("dd", "%d")
.replace("HH", "%H")
.replace("mm", "%M")
.replace("ss", "%S")
)
token_map = {
"EEEE": "%A", "E": "%a",
"yyyy": "%Y", "yy": "%y",
"MMMM": "%B", "MMM": "%b", "MM": "%m", "M": "%m",
"dd": "%d", "d": "%d",
"HH": "%H", "H": "%H", "hh": "%I", "h": "%I",
"mm": "%M", "ss": "%S", "a": "%p"
}
pattern = re.compile("|".join(sorted(token_map.keys(), key=len, reverse=True)))
py_fmt = pattern.sub(lambda m: token_map[m.group(0)], str(fmt))

Comment on lines +71 to +75
is_dynamic = (
isinstance(value, dict)
and "path" in value
and isinstance(value["path"], str)
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The is_dynamic check should exclude TemplateChildList objects (which contain a componentId key) to align with DataContext.resolve_dynamic_value and avoid setting up redundant dummy subscriptions.

Suggested change
is_dynamic = (
isinstance(value, dict)
and "path" in value
and isinstance(value["path"], str)
)
is_dynamic = (
isinstance(value, dict)
and "path" in value
and isinstance(value["path"], str)
and "componentId" not in value
)

Comment on lines +80 to +81
current = current[token]
elif isinstance(current, list) and NUMERIC_PATTERN.match(token):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Expanding the list sequentially using a while loop with append is highly inefficient and can lead to Out Of Memory (OOM) errors or performance bottlenecks if an unreasonably large index is passed.

Using extend is much faster, and adding a safety threshold check protects against potential denial of service vectors.

Suggested change
current = current[token]
elif isinstance(current, list) and NUMERIC_PATTERN.match(token):
if idx > 10000:
raise ValueError(f"Index {idx} exceeds maximum allowed array size.")
current.extend([None] * (idx - len(current) + 1))

Comment on lines +229 to +244
while not scanner.is_at_end():
c = scanner.advance()
if c == "\\":
next_c = scanner.advance()
if next_c == "n":
result += "\n"
elif next_c == "t":
result += "\t"
elif next_c == "r":
result += "\r"
else:
result += next_c
elif c == quote:
break
else:
result += c

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

An unclosed string literal at the end of an expression is currently accepted silently because the loop terminates at the end of the scanner input without raising an error. It should raise a ValueError to ensure syntax correctness.

Suggested change
while not scanner.is_at_end():
c = scanner.advance()
if c == "\\":
next_c = scanner.advance()
if next_c == "n":
result += "\n"
elif next_c == "t":
result += "\t"
elif next_c == "r":
result += "\r"
else:
result += next_c
elif c == quote:
break
else:
result += c
closed = False
while not scanner.is_at_end():
c = scanner.advance()
if c == "\\":
next_c = scanner.advance()
if next_c == "n":
result += "\n"
elif next_c == "t":
result += "\t"
elif next_c == "r":
result += "\r"
else:
result += next_c
elif c == quote:
closed = True
break
else:
result += c
if not closed:
raise ValueError("Unclosed string literal")

Comment on lines +132 to +139
if isinstance(action, dict) and "event" in action:
evt = copy.deepcopy(action["event"])
resolved_context = {}
if isinstance(evt.get("context"), dict):
for k, v in evt["context"].items():
resolved_context[k] = self.resolve_dynamic_value(v)
evt["context"] = resolved_context
return {"event": evt}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If action["event"] is not a dictionary (e.g., if it is malformed or of an unexpected type), calling evt.get("context") will raise an AttributeError. Adding an explicit type check ensures robust defensive programming.

Suggested change
if isinstance(action, dict) and "event" in action:
evt = copy.deepcopy(action["event"])
resolved_context = {}
if isinstance(evt.get("context"), dict):
for k, v in evt["context"].items():
resolved_context[k] = self.resolve_dynamic_value(v)
evt["context"] = resolved_context
return {"event": evt}
if isinstance(action, dict) and "event" in action:
evt = copy.deepcopy(action["event"])
if isinstance(evt, dict):
resolved_context = {}
if isinstance(evt.get("context"), dict):
for k, v in evt["context"].items():
resolved_context[k] = self.resolve_dynamic_value(v)
evt["context"] = resolved_context
return {"event": evt}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant