Skip to content

Generating Prompts

For a model to emit blocks your registry can parse, it needs to know the block format. Rather than hand-writing that prompt, Registry.to_prompt() builds it from your block definitions — the docstring, metadata fields, content format, and examples. The same registry then parses the model's output, so the instructions and the parser never drift apart. See Blocks & Registry for the round-trip idea.

Make your blocks self-describing

Prompt quality comes straight from your block definitions. Three things feed the prompt:

  • the block docstring: the first paragraph is the description; a paragraph starting with Usage: becomes the usage guidance (this is the only convention — implicit phrasing is not detected).
  • the content format: @parse_as_json / @parse_as_yaml mark the body format, rendered as a JSON/YAML structure hint built from the content model's fields and their Field(description=...).
  • examples: __examples__ entries are serialized in the registry's syntax and shown verbatim.
from typing import ClassVar, Literal

from hother.streamblocks import (
    BaseContent,
    BaseMetadata,
    Block,
    DelimiterFrontmatterSyntax,
    Registry,
    generate_block_prompt,
    parse_as_yaml,
)
class SearchMetadata(BaseMetadata):
    """Metadata for a catalog search block."""

    block_type: Literal["search"] = "search"


@parse_as_yaml()
class SearchContent(BaseContent):
    """Search parameters."""

    query: str = ""
    limit: int = 10


class Search(Block[SearchMetadata, SearchContent]):
    """Search the product catalog.

    Usage: emit a search block to look up products before answering.
    """

    # Declared examples are serialized into the prompt so the model sees a
    # concrete, correctly-formatted block.
    __examples__: ClassVar[list[dict[str, object]]] = [
        {
            "metadata": {"id": "s1", "block_type": "search"},
            "content": {"query": "wireless headphones", "limit": 5},
        },
    ]

Generate a registry prompt

Registry.to_prompt() documents every registered block in the registry's syntax:

registry = Registry(syntax=DelimiterFrontmatterSyntax())
registry.register("search", Search)

# A full system prompt documenting every registered block: the syntax
# format, the block description + "Usage:" line, its metadata fields, the
# YAML content format (from @parse_as_yaml), and serialized examples.
prompt = registry.to_prompt()
print(prompt)

Pass include_examples=False to omit the examples section. Because the prompt is built from the registry's own syntax, switching syntaxes (for example MarkdownFrontmatterSyntax) changes the format shown to the model automatically.

Document a single block

For one block type, call generate_block_prompt(block_class, syntax, ...):

# A prompt for a single block type -- here without the examples section.
single = generate_block_prompt(Search, registry.syntax, include_examples=False)
print(single)

Customize the template

Prompts render through Jinja2 templates. Register a custom template and select it with template_version; the template context exposes syntax_name, syntax_format, and blocks:

# Register a custom template and select it with template_version. Templates
# receive `syntax_name`, `syntax_format`, and `blocks` in their context.
registry.register_template("compact", "Available blocks: {{ blocks | length }}", mode="registry")
print(registry.to_prompt(template_version="compact"))

This is handy for A/B testing prompt phrasings without touching block definitions.

Serialize blocks back to text

Registry.serialize_block() (and each syntax's serialize_block()) renders a Block instance into its wire format — the same mechanism used to render __examples__. It is the inverse of parsing, useful for few-shot examples or round-trip tests. BaseSyntax.describe_format() returns the human-readable format description embedded in the prompt.

Next steps