Skip to content

refactor(python/agent)!: assemble prompts and skills with one function - #3568

Open
nan-yu wants to merge 14 commits into
mainfrom
stack/prompt-assembler
Open

nan-yu wants to merge 14 commits into
mainfrom
stack/prompt-assembler

Conversation

@nan-yu

@nan-yu nan-yu commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Important

Stacked PR, 2 of 3. Stack: #3567 → this PR → #3569.
The base branch is #3567's branch (stack/prompt-layout), so the diff shows only this PR's changes. When #3567 merges, GitHub retargets this PR to main.

Summary

This PR gives Python one shared function that builds both prompt snippets and monolithic skills. PromptGenerator.generate() becomes a method that subclasses can't override. It's the "shared assembler + fixed generate()" item in #3416, Python only.

Changes

  • a2ui.prompt.assemble_instructions(generator, catalogs=None) joins the base rules, the catalog instructions and the examples (under EXAMPLES_HEADING), and caches the snippet for each generator's own catalogs (weakly keyed on the generator, so the cache keeps nothing alive beyond it).
    • For the generator's own catalogs, it calls generate_catalog_instructions() and generate_examples() once each, so a format can still render shared content once. Express's multi-catalog # Catalog headings and Elemental's shared type definitions rely on this.
    • For any other set of catalogs, it renders each catalog separately and drops repeated examples, as SkillGenerator did.
  • PromptGenerator:
    • generate() is marked @typing.final and returns assemble_instructions(self).
    • catalogs is a new abstract property.
    • The three hooks stay public for third-party formats.
  • Removed generate() overrides from the Express, Direct JSON, Elemental and Atom generators. Their snippets don't change: the prompt conformance fixtures and every existing test pass unchanged.
  • SkillGenerator.generate_skill() builds its body with assemble_instructions, so a monolithic skill for the format's own catalogs holds the prompt snippet.
  • No longer exported: ExpressPromptGenerator, DirectJsonPromptGenerator, ElementalPromptGenerator and AtomPromptGenerator are removed from a2ui.inference_formats and their format packages. Use InferenceFormat.prompt_generator to reach a format's generator.
  • Tests: added tests/test_prompt_assembler.py and updated the two test subclasses of PromptGenerator to the new contract.
  • Docs: CHANGELOG (two BREAKING entries) and README.
  • Blueprints: the a2ui_agent module blueprint and the skill_generator feature blueprint describe the new PromptGenerator contract and assemble_instructions.

Impact & Risks

  • Breaking for third-party PromptGenerator subclasses: they must implement catalogs and stop overriding generate(). mypy reports an override as an error; at runtime the override still runs.
  • Breaking for code that imported the per-format generator classes. Nothing in the repo does.
  • The cache assumes a generator doesn't change after construction. That holds for every generator in the repo. Text for other catalog sets isn't cached.
  • Prompt output is unchanged.

Testing

  • uv run pytest python/a2ui_agent/tests python/a2ui_adk/tests python/a2ui_a2a/tests: all passed, plus 9 new tests in test_prompt_assembler.py.
    • The new tests include a check that the monolithic skill body equals the prompt snippet for Express and Direct JSON with one and two catalogs.
  • uv run mypy python/a2ui_agent and uv run pyink are clean.

Refs #3416.

@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 refactors the prompt generation system by making PromptGenerator.generate() a final method that delegates to a new shared assemble_instructions function. Subclasses of PromptGenerator are now required to implement an abstract catalogs property and three hook methods (generate_base_rules, generate_catalog_instructions, and generate_examples) instead of overriding generate(). Specific format prompt generators are no longer exported directly from a2ui.inference_formats. Additionally, SkillGenerator has been updated to use assemble_instructions to prevent logic duplication, caching has been introduced for generator instructions, and comprehensive unit tests have been added. As there are no review comments, I have no feedback to provide.

@nan-yu
nan-yu force-pushed the stack/prompt-assembler branch 2 times, most recently from 0891ca7 to 5f6f7b8 Compare October 23, 2026 23:54
@nan-yu
nan-yu added this pull request to stack #3589 October 24, 2026 00:03
@nan-yu
nan-yu marked this pull request as ready for review October 24, 2026 00:23
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@nan-yu
nan-yu requested a review from ditman October 24, 2026 00:23
@ditman

ditman commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator

Should this also update the blueprints?

@nan-yu

nan-yu commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator Author

@ditman Good call, done in 3a387f3:

  • blueprints/modules/a2ui_agent.blueprint.md, PromptGenerator section: describes the abstract catalogs property, the three hooks, the final generate() returning assemble_instructions(self), and assemble_instructions itself (layout, the own-catalogs vs. other-catalogs paths, caching). It also notes that the per-format generator classes are internal. The DirectJsonPromptGenerator sketch now shows the hooks instead of a generate() override, with the protocol schemas in the base rules.
  • blueprints/features/skill_generator.blueprint.md: the base contract and the assembly steps replace the old generate(role_description, workflow_description, ...) template method, which no SDK had anymore. generate_skill documents that its body comes from assemble_instructions, base rules may hold shared protocol schemas instead of being strictly "catalog-agnostic", and the implementation steps say to make generate() final.

validate_blueprints.py passes. I left the codebase blueprints' module_blueprint_commit alone for now, since the squash merge will change the hash.

@nan-yu
nan-yu force-pushed the stack/prompt-assembler branch 2 times, most recently from da38fc0 to 2b913eb Compare October 27, 2026 16:53
@nan-yu
nan-yu force-pushed the stack/prompt-assembler branch from 2b913eb to ffc9627 Compare October 27, 2026 19:12
Prompt snippets now follow one layout in every SDK and format: the base
rules, then the catalog instructions, then the examples under
'### Examples:' and a blank line.

- Drop the '## Workflow Description:' heading (and Atom's
  '## Instructions:') before the base rules. Callers can add their own.
- Direct JSON puts the protocol schemas in one schema block after the
  rules and each catalog schema in a block of its own, in Python, TS and
  Dart. The TS DirectJsonPromptGenerator moves the protocol schemas into
  generateBaseRules() and no longer overrides generate() (#3562).
- Regenerate the shared Direct JSON prompt fixtures under
  conformance/test_data/prompts/direct_json/.

Refs #3416, #3464

Signed-off-by: Nan Yu <nanyu@google.com>
The snippet layout is an implementation detail rather than a public API
contract, so the entries no longer carry a breaking-change marker.

Signed-off-by: Nan Yu <nanyu@google.com>
Add assemble_instructions(generator, catalogs=None) to a2ui.prompt. It
joins a generator's base rules, catalog instructions and examples, and
caches the result per generator and catalogs.

- PromptGenerator.generate() is now @Final and returns
  assemble_instructions(self). Subclasses implement the abstract catalogs
  property and the three hooks. The Express, Direct JSON, Elemental and
  Atom generators drop their generate() overrides; snippets are unchanged.
- SkillGenerator.generate_skill() builds its body with the same function,
  so a monolithic skill for the format's catalogs holds the prompt snippet.
- Stop exporting the per-format PromptGenerator subclasses; reach them
  through InferenceFormat.prompt_generator.

Refs #3416

Signed-off-by: Nan Yu <nanyu@google.com>
Caching text for arbitrary catalogs kept those catalogs alive with the
generator. Cache just the generator's own snippet, keyed weakly on the
generator; other catalogs are assembled on each call.

Signed-off-by: Nan Yu <nanyu@google.com>
…rate()

Update the a2ui_agent module blueprint and the skill_generator feature
blueprint to match the Python SDK: PromptGenerator declares catalogs and
three hooks, generate() is final and returns assemble_instructions(self),
SkillGenerator.generate_skill() builds its body with the same function,
and the per-format generator classes are internal. Base rules hold what
every catalog shares, including protocol schemas such as Direct JSON's.

Signed-off-by: Nan Yu <nanyu@google.com>
…rompt assembler entries

Signed-off-by: Nan Yu <nanyu@google.com>
@nan-yu
nan-yu force-pushed the stack/prompt-assembler branch from ffc9627 to 61dffd0 Compare October 27, 2026 19:36
@nan-yu
nan-yu removed this pull request from stack #3589 October 27, 2026 19:36
@nan-yu
nan-yu added this pull request to stack #3663 October 27, 2026 19:37

@ditman ditman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for updating the blueprints as well! Centralizing prompt and monolithic skill assembly in assemble_instructions(), marking PromptGenerator.generate() @final, and hiding the format-specific PromptGenerator subclasses behind InferenceFormat.prompt_generator is a great simplification.

Left a few minor comments on a docstring contradiction in PromptGenerator.generate_catalog_instructions, a small caching docstring clarification, and an optional cleanup in SkillGenerator.generate_skill.

Signed-off-by: Nan Yu <nanyu@google.com>

# Conflicts:
#	conformance/test_data/prompts/direct_json/cross_ref_catalogs_pruned.txt
#	dart/a2ui_agent/CHANGELOG.md
#	python/a2ui_agent/CHANGELOG.md
#	typescript/agents/a2ui_agent/CHANGELOG.md
…hema_prompt

generate_catalog_instructions() now makes a single catalog_schema_prompt()
call, so the catalogs' shared protocol version is checked once. Mark the TS
changelog entry as a breaking change and link the Dart entry to the PR.

Signed-off-by: Nan Yu <nanyu@google.com>
Keep main's ExpressResponseStream exports while leaving
ExpressPromptGenerator private, and reach the generator through
ExpressFormat.prompt_generator in the test that came in from main.

Signed-off-by: Nan Yu <nanyu@google.com>
Type the hooks' catalog argument as CatalogApi, describe the shared
multi-catalog content that generate_catalog_instructions() returns with no
catalog, say that only the generator's own catalogs are cached, and pass
catalogs straight to assemble_instructions in SkillGenerator.

Signed-off-by: Nan Yu <nanyu@google.com>
Base automatically changed from stack/prompt-layout to main November 5, 2026 22:30
Signed-off-by: Nan Yu <nanyu@google.com>

# Conflicts:
#	python/a2ui_agent/src/a2ui/inference_formats/direct_json/prompt_generator.py
#	python/a2ui_agent/src/a2ui/inference_formats/experimental/atom/prompt_generator.py
#	python/a2ui_agent/src/a2ui/inference_formats/experimental/elemental/prompt_generator.py
#	python/a2ui_agent/src/a2ui/inference_formats/express/prompt_generator.py
#	python/a2ui_agent/src/a2ui/prompt/generator.py
#	python/a2ui_agent/src/a2ui/skill/generator.py

This branch has not been deployed

No deployments
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.

2 participants