Skip to content

feat(core,prompt): Simplified property value transformers for AI Prompt generation (v17) - #251

Merged
mattbrailsford merged 3 commits into
v17/devfrom
v17/feature/prompt-block-schema-guard
Jul 20, 2026
Merged

feat(core,prompt): Simplified property value transformers for AI Prompt generation (v17)#251
mattbrailsford merged 3 commits into
v17/devfrom
v17/feature/prompt-block-schema-guard

Conversation

@mattbrailsford

@mattbrailsford mattbrailsford commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Summary

v17 backport of #250. Fixes #249 — executing an AI Prompt against a rich-text (Umbraco.RichText / TipTap) property failed with an opaque HTTP 400 (invalid_json_schema) from strict providers like OpenAI. (Reported as "Block List/Grid", but the reporter's real case was RichText — whose write schema embeds the same block structure.)

The schema-driven wand constrains strict structured output to the target property's CMS write schema. RichText's blocks embeds an intentionally unconstrained values[].value node ({}, from CMS BlockJsonSchemaHelper), which has no faithful equivalent in OpenAI's strict subset → rejected before generation.

Note: the original commit on this branch took a fail-fast approach; after design review it evolved into the general transformer below. The AIPromptSchemaCompatibility guard from that commit is retained as a fallback.

Approach — simplified value transformers

New general server-side abstraction IAISimplifiedPropertyValueTransformer (Umbraco.AI.Core, discovered like IAIPropertyValueHandler): a per-editor simplified, strict-representable LLM schema plus a total/idempotent transform back to the editor's write value.

RichTextSimplifiedPropertyValueTransformer offers the LLM a plain markup string and wraps the result into { markup, blocks }, preserving existing inline blocks from the current value.

Consumer chooses the representation by capability (no regression):

Consumer Schema presented
Prompt (one-shot, strict) the simplified schema when a transformer is registered; else the write schema; else the string model
Agent (loose, iterative, add_item handlers) unchanged — keeps the full write schema so block authoring is preserved

What AIPromptService now does

  • Uses the simplified schema when a transformer exists, and runs the transform server-side so ValueChange.Value is the write shape, while DisplayValue stays the simplified (markup) value for the preview.
  • Reads the current write-shape value from the serialized entity/element already in the runtime context to preserve existing blocks — no new request field / client plumbing.
  • Runs the IsStrictRepresentable guard on whichever schema is used (incl. a simplified one); block editors (no transformer) still fail fast rather than 400.
  • Runs plain-text output mode for DisplayMode == TipTapTool prompts (decided server-side), so the in-editor toolbar keeps receiving a string.
  • Surfaces transform failures as a clean 400 and isolates per-option failures.
  • Resolver: additive ResolvePropertyValueSchemaAsync; old ResolveValueSchemaAsync kept and marked [Obsolete("… Will be removed in v19")] (v17 line: current major + 2).

Frontend

applyValueChange (document/block/media adapters) resolves the editor alias from the data-type item repository when a field is empty, so preparers still run into empty fields.

Validation

Not in scope (follow-ups)

  • Agent adoption of the transform at the dispatcher SetValue leaf (additive).
  • Moving DateTime normalisation server-side + preparer cleanup.
  • "Proper" recursive block-content generation for the Prompt path (intractable strict schema).

🤖 Generated with Claude Code

mattbrailsford and others added 3 commits July 16, 2026 13:43
…structured output

Executing a prompt against a Block List or Block Grid property failed with an
opaque HTTP 400 (invalid_json_schema) from strict providers like OpenAI. The
resolved CMS property value schema embeds an intentionally unconstrained `{}`
node (BlockJsonSchemaHelper's "any type - depends on property editor"), which
has no faithful equivalent in the strict structured-output subset.

Add AIPromptSchemaCompatibility to detect schemas that can't be represented in
the strict subset (nodes with no resolvable type, arrays without items) and
fail fast in AIPromptService with a clear, user-facing message instead of
letting the provider reject the request. Editors that expose no schema still
fall back to the string-based response models; well-typed editor schemas (e.g.
ColorPicker) remain schema-constrained.

Fixes #249

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…pt generation

Executing an AI Prompt against a rich-text (Umbraco.RichText / TipTap) property
returned an opaque HTTP 400 (invalid_json_schema) from strict providers: the
schema-driven wand constrained structured output to the editor's write schema,
whose nested block value is an unconstrained `{}` node OpenAI strict mode rejects.

Introduce IAISimplifiedPropertyValueTransformer (Umbraco.AI.Core): a per-editor
simplified, strict-representable LLM schema plus a total/idempotent transform back
to the editor's write value. A RichText transformer offers the LLM a plain markup
string and wraps it into { markup, blocks }, preserving existing inline blocks.

AIPromptService now:
- uses the simplified schema when a transformer is registered for the target editor
  (else the write schema, else the string model);
- runs the transform server-side so ValueChange.Value is the editor's write shape,
  while DisplayValue stays the simplified (markup) value for the preview;
- reads the current write-shape value from the serialized entity/element already in
  the runtime context (block preservation on regeneration) — no new request field;
- runs the strict-representability guard on whichever schema is used (incl. a
  simplified one) so a mis-behaving transformer fails fast cleanly, and blocks
  (no transformer) still fail fast rather than 400;
- runs in plain-text output mode for TipTapTool prompts (decided server-side from
  prompt.DisplayMode), so the in-editor toolbar keeps receiving a string;
- surfaces transform failures as a clean 400 and isolates per-option failures.

The resolver gains ResolvePropertyValueSchemaAsync (the old ResolveValueSchemaAsync
is kept and marked [Obsolete], delegating to it). Client applyValueChange now
resolves the editor alias from the data type when a field is empty, so value
preparers still run into empty fields.

Fixes #249

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…quest/response

Pins the exact live execute payload from the manual #249 validation: given the LLM's
markup string plus the property's current value (with an empty blocks envelope,
layout: {}), the RichText transformer reproduces the server response value
byte-for-byte and preserves the existing empty blocks rather than substituting the
Empty("Umbraco.RichText") default.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mattbrailsford mattbrailsford changed the title fix(prompt): Fail fast when property schema can't be constrained for structured output (v17) feat(core,prompt): Simplified property value transformers for AI Prompt generation (v17) Jul 17, 2026
@mattbrailsford
mattbrailsford merged commit cb9982e into v17/dev Jul 20, 2026
24 checks passed
@mattbrailsford
mattbrailsford deleted the v17/feature/prompt-block-schema-guard branch July 20, 2026 12:07
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