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
51 changes: 29 additions & 22 deletions blueprints/features/skill_generator.blueprint.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ This document specifies the language-agnostic architecture, API contracts, metad

To support granular skill modularity and avoid duplicating prompt-building logic, every language SDK's `PromptGenerator` base contract MUST decompose prompt generation into three independent sub-methods:

- `generate_base_rules() -> String`: Emits base syntax contracts, grammar specifications, and sentinel tags (e.g. `<a2ui>`). Must be completely catalog-agnostic.
- `generate_base_rules() -> String`: Emits what every catalog shares, so text that combines several catalogs' instructions holds it once: base syntax contracts, grammar specifications, sentinel tags (e.g. `<a2ui>`), and any protocol schemas the format shows the model (such as Direct JSON's agent-to-renderer and common types schemas). It names no single catalog's components or functions.
- `generate_catalog_instructions(include_schema, catalog) -> String`: Emits component and function signatures or JSON schemas for the target catalog (or all bound catalogs if omitted).
- `generate_examples(catalog, validate) -> String`: Loads and formats few-shot examples for the target catalog (or all bound catalogs).

`PromptGenerator.generate()` serves as the single **Template Method** assembling these sub-methods into a complete agent system prompt.
The prompt snippet and the monolithic skill are assembled from these sub-methods by one shared function, `assemble_instructions(generator, catalogs)`. `PromptGenerator.generate()` is final and returns `assemble_instructions(self)`; formats implement the sub-methods and the `catalogs` property and never override `generate()`.

### **2. Additive Composition Layer**

Expand Down Expand Up @@ -71,7 +71,11 @@ Every language implementation (`a2ui-python`, `a2ui-swift`, `a2ui-kotlin`, `a2ui

```
interface PromptGenerator {
// Returns base syntax rules, grammar specs, and sentinel output tags. Must be catalog-agnostic.
// The catalogs the generator describes, in order. Implemented by each format.
catalogs: List<Catalog>

// Returns what every catalog shares: syntax rules, grammar specs, sentinel output tags,
// and any protocol schemas the format shows the model.
generate_base_rules() -> String

// Returns component and function signatures or JSON schemas for the target catalog (or all bound catalogs).
Expand All @@ -80,28 +84,29 @@ interface PromptGenerator {
// Returns formatted few-shot examples for the target catalog (or all bound catalogs).
generate_examples(catalog: Catalog? = null, validate: Boolean = false) -> String

// Template Method: Assembles format-specific system prompt instructions.
generate(
role_description: String = "",
workflow_description: String = "",
ui_description: String = "",
include_schema: Boolean = true,
include_examples: Boolean = false,
validate_examples: Boolean = false
) -> String
// Final: returns assemble_instructions(this). Formats never override it.
generate() -> String
}

// Joins a generator's sub-methods into the instructions for some catalogs.
// Defaults to the generator's own catalogs.
assemble_instructions(generator: PromptGenerator, catalogs: List<Catalog>? = null) -> String
```

#### **Template Method Assembly (`generate`)**
#### **Assembly (`assemble_instructions`)**

`PromptGenerator.generate()` and `SkillGenerator.generate_skill()` both build their text with `assemble_instructions`:

The base `generate()` method orchestrates prompt assembly across all inference formats:
1. Appends `generate_base_rules()` if non-empty.
2. Appends the catalog instructions if non-empty:
- For the generator's own catalogs, calls `generate_catalog_instructions()` once with no catalog, so a format can render what its catalogs share once (for example, headings that name each catalog).
- For any other catalogs, calls `generate_catalog_instructions(catalog=c)` for each and appends each non-empty result.
3. Appends the examples under `### Examples:` followed by a blank line if any are non-empty:
- For the generator's own catalogs, calls `generate_examples()` once with no catalog.
- For any other catalogs, calls `generate_examples(catalog=c)` for each and keeps each distinct result once, since a format whose examples aren't kept per catalog returns the same examples for each catalog.
4. Joins all non-empty sections with double newlines (`"\n\n"`).

1. Appends `role_description` if non-empty.
2. Combines `generate_base_rules()` and `workflow_description` under `## Workflow Description:` if present.
3. Appends `ui_description` under `## UI Description:` if present.
4. If `include_schema` is true, calls `generate_catalog_instructions(include_schema=true)` and appends the result if non-empty.
5. If `include_examples` is true, calls `generate_examples(validate=validate_examples)` and appends the result under `### Examples:` if non-empty.
6. Joins all non-empty sections with double newlines (`"\n\n"`).
The snippet has no role, workflow or UI preamble; the agent adds its own text around it. Implementations may cache the text for a generator's own catalogs, treating a generator as immutable once built.

### **2. `Skill` Domain Model**

Expand Down Expand Up @@ -212,6 +217,8 @@ class SkillGenerator {

// Compiles an InferenceFormat into a single unified (monolithic) Skill.
// Defaults to all catalogs bound to the format if catalogs is omitted.
// The body is assemble_instructions(format.prompt_generator, catalogs), so for the
// format's own catalogs it is the format's prompt snippet.
// Default name: "a2ui"
// Default description: "Generates interactive user interface components for user requests."
generate_skill(
Expand Down Expand Up @@ -293,13 +300,13 @@ To guarantee 100% behavioral parity across multi-language SDKs, all implementati

When implementing this feature in a client language SDK:

1. **Decompose `PromptGenerator`**: In the base `PromptGenerator` class, implement `generate_base_rules()`, `generate_catalog_instructions()`, and `generate_examples()`, and wire `generate()` as a Template Method.
1. **Decompose `PromptGenerator`**: In the base `PromptGenerator` class, declare the `catalogs` property and the `generate_base_rules()`, `generate_catalog_instructions()`, and `generate_examples()` sub-methods, implement `assemble_instructions()`, and make `generate()` a final method returning `assemble_instructions(self)` (for example `@typing.final` in Python or `@nonVirtual` in Dart).
2. **Implement `Skill` and `SkillSet` Domain Models**:
- Implement `Skill` with YAML frontmatter serialization in `to_markdown()`.
- Implement `SkillSet` with 3-tier lookup resolution in `get()` and scoped directory cleaning in `export_to_directory()`.
3. **Implement `SkillGenerator`**:
- Wrap `InferenceFormat`.
- Implement `generate_core_skill()`, `generate_catalog_skill()`, `generate_skill()`, and `generate_skillset()` adhering to catalog defaulting semantics.
- Implement `clean_catalog_name()` helper.
4. **Wire Format Implementations**: Ensure specific format prompt generators (e.g. `ExpressPromptGenerator`, `DirectJsonPromptGenerator`) implement the decomposed sub-methods.
4. **Wire Format Implementations**: Ensure specific format prompt generators (e.g. `ExpressPromptGenerator`, `DirectJsonPromptGenerator`) implement the `catalogs` property and the decomposed sub-methods without overriding `generate()`, and keep them internal to their format packages.
5. **Verify Conformance**: Run unit tests and execute the shared test suite in `conformance/agent/skill.yaml` against `conformance/test_data/skills/` golden files.
84 changes: 72 additions & 12 deletions blueprints/modules/a2ui_agent.blueprint.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,25 +161,71 @@ class FunctionPruningTransformer(CatalogTransformer):

#### `PromptGenerator`

Abstract base interface for constructing system prompt instruction snippets across inference formats.
Abstract base class for constructing system prompt instruction snippets across inference formats.

The interface is `generate()` alone. Application code gets a prompt generator from `InferenceFormat.prompt_generator` and only renders it, so what a generator is built from (catalogs, examples, format-specific options) belongs to each format's constructor rather than to the shared interface.
Application code gets a prompt generator from `InferenceFormat.prompt_generator` and renders it with `generate()`. What a generator is built from (catalogs, examples, format-specific options) belongs to each format's constructor rather than to the shared interface.

A format implements the abstract `catalogs` property and three hooks, and never overrides `generate()`:

- `generate_base_rules()`: what every catalog shares, rendered once: the syntax contract, grammar and sentinel tags, plus any protocol schemas the format shows the model (Direct JSON's agent-to-renderer and common types schemas).
- `generate_catalog_instructions(include_schema, catalog)`: the component and function signatures or schemas of one catalog, or of every catalog in `catalogs` when `catalog` is omitted.
- `generate_examples(catalog, validate)`: the formatted example turns for one catalog, or for every catalog when `catalog` is omitted.

`generate()` is final and returns `assemble_instructions(self)`, the same function `SkillGenerator` builds a monolithic skill's body with, so a prompt snippet and a monolithic skill for the same catalogs have the same body. The per-format generator classes are not part of the public API: callers reach them through `InferenceFormat.prompt_generator`, and a third-party format subclasses `PromptGenerator` directly.

Every format's generator renders the prompt example turns it is given in the order given. Each turn is the list of `AgentToRendererMessage` objects making it up. An example carries no label of its own: what the model learns from it is the payload, so a description would be the prompt author's prose rather than part of the contract.

A generator lists only agent-callable functions as `callRendererFunction` targets (see [Agent-callable functions](#agent-callable-functions)).

```python
EXAMPLES_HEADING = "### Examples:\n\n"

class PromptGenerator(ABC):
"""Abstract base class for format-specific prompt generators."""

@property
@abstractmethod
def catalogs(self) -> list[CatalogApi]:
"""The catalogs the generator describes, in order."""

def generate_base_rules(self) -> str:
"""Renders what every catalog shares: syntax rules, sentinel tags, shared schemas."""
return ""

def generate_catalog_instructions(
self, include_schema: bool = True, catalog: CatalogApi | None = None
) -> str:
"""Renders one catalog's signatures or schemas, or every catalog's when omitted."""
return ""

def generate_examples(
self, catalog: CatalogApi | None = None, validate: bool = False
) -> str:
"""Renders one catalog's example turns, or every catalog's when omitted."""
return ""

@final
def generate(self) -> str:
"""
Renders format-specific system prompt instructions and catalog schemas.
The caller (Agent / Framework) prepends role/workflow preambles and appends suffixes.
"""
pass
"""Renders the prompt snippet. The caller adds its own role and workflow text."""
return assemble_instructions(self)


def assemble_instructions(
generator: PromptGenerator, catalogs: Sequence[CatalogApi] | None = None
) -> str:
"""Joins the hooks' output for some catalogs (default: the generator's own).

The text is the base rules, then the catalog instructions, then the examples
under EXAMPLES_HEADING, separated by blank lines; sections that render nothing
are left out.

- For the generator's own catalogs, the catalog instructions and examples come
from one call each with no catalog, so a format can render what its catalogs
share once. The result is cached per generator, which is treated as
immutable once built.
- For any other catalogs, each catalog's instructions and examples are rendered
on their own and joined, and examples a catalog repeats are added once.
"""
```

---
Expand Down Expand Up @@ -906,14 +952,28 @@ class DirectJsonPromptGenerator(PromptGenerator):
self.examples = examples
self.allowed_messages = allowed_messages

def generate(self) -> str:
"""Renders system instructions containing pruned JSON schemas inside <a2ui_schema> tags and <a2ui-json> output instructions.
def generate_base_rules(self) -> str:
"""Renders the <a2ui-json> output rules, then a schema block with the protocol schemas.

Returns:
Formatted system prompt instruction snippet string.
The protocol schemas are the agent-to-renderer schema (pruned to
`allowed_messages`) and the common types schema (pruned to what the
catalogs and that schema use). They belong to the base rules, so text that
combines several catalogs' instructions holds them once.
"""
pass

def generate_catalog_instructions(
self, include_schema: bool = True, catalog: Optional[CatalogApi] = None
) -> str:
"""Renders one schema block per catalog, each holding only that catalog's schema."""
pass

def generate_examples(
self, catalog: Optional[CatalogApi] = None, validate: bool = False
) -> str:
"""Renders each example turn as JSON inside <a2ui-json> tags."""
pass

class DirectJsonParser(Parser):
"""Parser for standard A2UI JSON payload envelopes enclosed in <a2ui-json> sentinel tags."""

Expand Down Expand Up @@ -981,7 +1041,7 @@ Compact functional DSL format designed to reduce output token consumption, enclo
The Express format package under `a2ui/inference_formats/express/` contains:

- `format`: `ExpressFormatFactory` (subclassing `InferenceFormatFactory`) and `ExpressFormat` (subclassing `InferenceFormat`).
- `prompt_generator`: `ExpressPromptGenerator` (subclassing `PromptGenerator`), rendering compact positional signatures for catalog components and functions into prompt instructions.
- `prompt_generator`: `ExpressPromptGenerator` (subclassing `PromptGenerator`, internal to the package and reached through `ExpressFormat.prompt_generator`), rendering compact positional signatures for catalog components and functions into prompt instructions.
- `compiler`: `ExpressCompiler`, lexing and parsing the DSL expressions found between `<a2ui>` tags into standard `AgentToRendererMessage` list structures, both in single-shot mode and incrementally across streaming chunks.
- `decompiler`: `ExpressDecompiler`, converting `AgentToRendererMessage` payload lists back into Express DSL string format.
- `parser`: `ExpressParser` (subclassing `Parser`), delegating compilation and decompilation to `ExpressCompiler` and `ExpressDecompiler`, and opening `ExpressResponseStream` sessions via `open_stream()`.
Expand Down
14 changes: 14 additions & 0 deletions python/a2ui_agent/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
## Unreleased

- **BREAKING CHANGE**: `PromptGenerator.generate()` is now final and returns
`assemble_instructions(self)`, which `SkillGenerator.generate_skill()` also
uses, so a monolithic skill for a format's own catalogs holds its prompt
snippet. A `PromptGenerator` subclass now implements the abstract `catalogs`
property and the three hooks (`generate_base_rules`,
`generate_catalog_instructions`, `generate_examples`) instead of overriding
`generate()`; built-in snippets are unchanged.
[#3568](https://github.com/a2ui-project/a2ui/pull/3568)
- **BREAKING CHANGE**: `ExpressPromptGenerator`, `DirectJsonPromptGenerator`,
`ElementalPromptGenerator` and `AtomPromptGenerator` are no longer exported
from `a2ui.inference_formats` or their format packages. Reach a format's
generator through `InferenceFormat.prompt_generator`, and subclass
`PromptGenerator` to write a format of your own.
[#3568](https://github.com/a2ui-project/a2ui/pull/3568)
- **BREAKING CHANGE**: (v1_0) The `TextField` builder in
`a2ui.builder.v1_0.catalogs.basic` requires `value`, following the basic
catalog. Recommended migration: pass `value`, such as
Expand Down
2 changes: 1 addition & 1 deletion python/a2ui_agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ The following directories contain the base protocol logic, parsing, and schema o

### Prompt & Inference Formats (`src/a2ui/prompt`, `src/a2ui/inference_formats`)

- **`prompt/generator.py`**: Defines the `PromptGenerator` (`A2uiPromptGenerator`) abstract base class (`generate_base_rules`, `generate_catalog_instructions`, `generate_examples`, `generate`).
- **`prompt/generator.py`**: Defines the `PromptGenerator` (`A2uiPromptGenerator`) abstract base class, whose formats implement `catalogs` and the three hooks (`generate_base_rules`, `generate_catalog_instructions`, `generate_examples`), and `assemble_instructions`, which joins the hooks' output. `generate()` returns `assemble_instructions(self)` and is not overridden.
- **`inference_format.py`**: Defines `InferenceFormat` and `InferenceFormatFactory`.
- **`inference_formats/`**: Format implementations and factories for Direct JSON (`direct_json/`), Express (`express/`), and experimental Elemental and Atom (`experimental/`) formats. Direct JSON and Express both stream: `DirectJsonParser.open_stream` heals partial JSON per chunk, and `ExpressParser.open_stream` emits each statement once it closes.

Expand Down
8 changes: 0 additions & 8 deletions python/a2ui_agent/src/a2ui/inference_formats/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,39 +22,31 @@
from .direct_json import DirectJsonFormat
from .direct_json import DirectJsonFormatFactory
from .direct_json import DirectJsonParser
from .direct_json import DirectJsonPromptGenerator
from .experimental.atom import AtomFormat
from .experimental.atom import AtomFormatFactory
from .experimental.atom import AtomParser
from .experimental.atom import AtomPromptGenerator
from .experimental.elemental import ElementalFormat
from .experimental.elemental import ElementalFormatFactory
from .experimental.elemental import ElementalParser
from .experimental.elemental import ElementalPromptGenerator
from .express import ExpressFormat
from .express import ExpressFormatFactory
from .express import ExpressParser
from .express import ExpressPromptGenerator
from .express import ExpressResponseStream

__all__ = [
"AtomFormat",
"AtomFormatFactory",
"AtomParser",
"AtomPromptGenerator",
"DirectJsonDecompiler",
"DirectJsonFormat",
"DirectJsonFormatFactory",
"DirectJsonParser",
"DirectJsonPromptGenerator",
"ElementalFormat",
"ElementalFormatFactory",
"ElementalParser",
"ElementalPromptGenerator",
"ExpressFormat",
"ExpressFormatFactory",
"ExpressParser",
"ExpressPromptGenerator",
"ExpressResponseStream",
"direct_json",
"express",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
from .format import DirectJsonFormatFactory
from .parser import DirectJsonParser
from .prompt_generator import DEFAULT_WORKFLOW_RULES
from .prompt_generator import DirectJsonPromptGenerator
from .response_stream import DirectJsonResponseStream
from .schema_prompt import schema_to_prompt

Expand All @@ -32,7 +31,6 @@
"DirectJsonFormat",
"DirectJsonFormatFactory",
"DirectJsonParser",
"DirectJsonPromptGenerator",
"DirectJsonResponseStream",
"schema_to_prompt",
]
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

from collections.abc import Sequence

from a2ui.constants import DEFAULT_WORKFLOW_RULES, EXAMPLES_HEADING
from a2ui.constants import DEFAULT_WORKFLOW_RULES
from a2ui.core import CatalogApi
from a2ui.core.schema import AgentToRendererMessage
from a2ui.inference_formats._shared import check_catalogs
Expand Down Expand Up @@ -131,11 +131,3 @@ def generate_examples(
decompiled = self._decompiler.decompile(turn)
blocks.append(f"<a2ui-json>\n{decompiled}\n</a2ui-json>")
return "\n\n".join(blocks)

def generate(self) -> str:
"""Assembles the prompt snippet for the Direct JSON format."""
parts = [self.generate_base_rules(), self.generate_catalog_instructions()]
examples_str = self.generate_examples()
if examples_str:
parts.append(f"{EXAMPLES_HEADING}{examples_str}")
return "\n\n".join(parts)
Loading
Loading