Class FormAIController
- All Implemented Interfaces:
AIController
AIOrchestrator via
withController(...).
var controller = new FormAIController(formLayout, binder);
controller
.describeField(discountField,
"Discount as a percentage, not an amount")
.ignoreField(internalReferenceField);
AIOrchestrator orchestrator = AIOrchestrator
.builder(llmProvider, systemPrompt).withController(controller)
.build();
The controller accepts any HasComponents container. It discovers
fields by walking the container's component tree and collecting every
component that implements HasValue. The walk recurses into nested
HasComponents children so layouts containing layouts are handled.
Per-field configuration: use the chained
describeField,
ignoreField, and
fieldValueOptions methods.
fieldValueOptions takes a ValueOptions built via
forField — the compiler picks the
MultiSelect overload automatically
for fields statically typed as MultiSelect. The controller resolves a
chosen label back to one of the registered items via the registration's
item-label generator; for multi-select fields the resolved elements are
aggregated into a LinkedHashSet before HasValue.setValue(V).
LLM-facing labels are derived from the field's
setItemLabelGenerator(...) by default; see ValueOptions for
the full resolution chain.
Hiding field values: setFieldValuesHidden(boolean) keeps the
current value of every field private while still letting the LLM see and fill
the fields — useful when the form may already hold data the AI should not
read (for example personal data the user typed in). To hide a single field
entirely, so the LLM does not even learn it exists, use
ignoreField(HasValue).
How the LLM understands fields: everything the LLM knows about a field
comes from the field's label, its helper text, and the
describeField(HasValue, String) hint. Make sure every field carries
a meaningful label, or add a describeField(...) hint for fields whose
purpose is not evident from the label alone.
Binder integration: the two-argument constructor accepts a
Binder, which affects the workflow in two ways. First, for every
named binding (bind("propertyName"),
bindInstanceFields(this), or @PropertyId) the property name
is used as a default field description, so the LLM can recognize what the
field means even when it has no label. The default only applies when no
explicit describeField(HasValue, String) has been registered;
calling describeField(...) always wins. Lambda-bound bindings carry
no property name and contribute no default. Second, the binder drives
validation of the values the LLM writes, including bean-level cross-field
rules — see Validation below.
Validation: each value the LLM writes is validated immediately after
it is applied. A bound field is validated through its binding, so the
converter and every registered validator run as one unit; an unbound field
that exposes a default validator is validated through that validator. A value
that fails validation stays in the field and the failure is reported back to
the LLM as a rejection, so it can supply a corrected value within the same
turn. When the controller was created with a Binder and a bean is set
(setBean), the binder's bean-level validators
(binder.withValidator(...)) also run after the writes; a cross-field
failure (for example "start date must precede end date") is likewise reported
back to the LLM so it can adjust the offending fields within the same turn.
Field locking: while a fill is in progress, every non-ignored field the user can currently edit (visible, enabled, and not already read-only) is made read-only on the client so the user cannot type into a field the AI is about to overwrite. This is a UX guard only: the field's server-side read-only state is never changed, so it does not affect what the LLM sees or writes, and a field's application-set read-only state is left untouched. The guard is applied and cleared together with the "AI is working" state (see below), so it is released when the turn ends, successfully or otherwise. A field switched to read-only on the server mid-turn — for example by a value-change listener reacting to one of the AI's writes — stays read-only on the client when the guard is released.
Change tracking and field marker: while a turn runs, every visible
field shows an "AI is working" shimmer; when the turn ends the shimmer clears
and every field whose value changed during the turn is marked automatically
with the AI marker, which offers a revert control that restores the field's
value from before the AI's first change to it. The marker clears itself once
the user edits the field. Marking is the controller's own doing end to end;
an application that does not want it turns it off with
setFieldMarkerEnabled(boolean). A
popover content provider can add application content to the popover each
marker opens — for example the source data behind the value. A listener registered through
addFieldValueChangeListener(FieldValueChangeListener) fires once per
field whose value changed during a successful turn, for applications that
need to react to the AI's edits beyond the marker.
Source tracking: with setSourceTrackingEnabled(boolean) on,
the LLM also reports, per filled value, the snippets it read, where each one
sits in the document, and a confidence level. The data is available from
FieldValueChangeEvent.getFieldSource() and
getFieldSource(HasValue), and can be stored and put back later with
restoreFieldSource(HasValue, ValueSource). Off by default.
Serialization: the controller is not serialized with the orchestrator.
After deserialization, create a new controller against the same form (and
binder, if any) and call
orchestrator.reconnect(provider).withController(controller).apply().
Re-register the same describeField / fieldValueOptions /
ignoreField hints on the new controller.
- Since:
- 25.2
- Author:
- Vaadin Ltd
-
Constructor Summary
ConstructorsConstructorDescriptionFormAIController(T fieldContainer) Creates a new form AI controller for the given container.FormAIController(T fieldContainer, Binder<?> binder) Creates a new form AI controller for the given container and binder. -
Method Summary
Modifier and TypeMethodDescriptionRegisters a listener that is invoked once per field whose value changed during a successful AI turn.describeConfidenceLevel(ConfidenceLevel level, String description) Replaces the meaning the LLM is given for one confidence level, overriding the built-in wording in thefill_formtool description.describeField(HasValue<?, ?> field, String description) Adds a free-form description that the LLM sees alongside the field when deciding what to fill in.<V> FormAIControllerfieldValueOptions(ValueOptions<V> config) Registers a known set of items for a field.Returns the texts shown by the AI field marker.Returns the provider that supplies the extra content shown in the AI field marker's popover.getFieldSource(HasValue<?, ?> field) Returns the source the LLM reported for the value the field currently holds.getTools()Returns the tools this controller exposes to the LLM.ignoreField(HasValue<?, ?> field) Hides the given field from the LLM.booleanReturns whether fields changed by the AI are marked automatically at the end of a turn.booleanReturns whether field values are hidden in the form state sent to the LLM.booleanReturns whether the LLM is asked to report a source for each value it fills.voidCalled synchronously on the UI thread just before the LLM stream opens.voidonResponse(Throwable error) Called when the turn ends: normally when the LLM stream has completed, successfully or with an error, but also when the turn fails before a stream ever opens.voidrestoreFieldSource(HasValue<?, ?> field, ValueSource source) Attaches a previously stored source to the field, so an application that persisted the data (seeFieldValueChangeEvent.getFieldSource()) can show it again after a reload without the original chat session.setFieldMarkerEnabled(boolean fieldMarkerEnabled) Controls whether every field whose value the AI changed during a turn is marked when the turn ends — an "AI" badge with a popover explaining the fill and offering a revert control.Sets the texts shown by the AI field marker — the "AI" badge, its tooltip, the popover with the revert control, and the confidence indicator — replacing the built-in English defaults.setFieldMarkerPopoverContentProvider(FieldMarkerPopoverContentProvider fieldMarkerPopoverContentProvider) Sets a provider that supplies extra content for the popover that opens when the AI field marker badge is clicked, shown between the explanation message and the revert control.setFieldValuesHidden(boolean valuesHidden) Controls whether the current value of every field is sent to the LLM as part of the form state.setSourceTrackingEnabled(boolean sourceTrackingEnabled) Controls whether the LLM is asked to report, along with each value it fills, the source it read: the snippets, where each snippet sits in the document, and aconfidence level.
-
Constructor Details
-
FormAIController
public FormAIController(T fieldContainer) Creates a new form AI controller for the given container. Fields are discovered by walking the container's component tree each time the controller is asked for tools, so fields added or removed between turns are picked up automatically.- Type Parameters:
T- the container type- Parameters:
fieldContainer- the container whose fields the LLM may populate, notnull
-
FormAIController
Creates a new form AI controller for the given container and binder. For every named binding on the binder, the bean property name is used as a defaultdescriptionwhen the developer has not registered one explicitly; lambda-bound bindings carry no property name and contribute no default. The binder also drives validation of the values the LLM writes: bound fields are validated through their bindings (converter and validators as one unit), and bean-level cross-field validators run as well when a bean is set. See the class-level documentation for details.- Type Parameters:
T- the container type- Parameters:
fieldContainer- the container whose fields the LLM may populate, notnullbinder- the binder whose property names default the field descriptions, notnull; use the single-argument constructor for the no-binder case- Throws:
NullPointerException- iffieldContainerorbinderisnull
-
-
Method Details
-
describeField
Adds a free-form description that the LLM sees alongside the field when deciding what to fill in. Use it to add business semantics that are not implied by the field's label, helper text, or component type (for example clarifying that a numeric field expects a percentage rather than an absolute amount). Later calls for the same field overwrite earlier ones.- Parameters:
field- the field to describe, notnulldescription- the description text, notnull- Returns:
- this controller, for chaining
-
fieldValueOptions
Registers a known set of items for a field. The LLM sees one label per item; when it picks a label, the controller walks the registration's items, applies the item-label generator per item, and returns the first whose label matches. The label-generator chain is documented onValueOptions.Items that share a label resolve to the first in registration order; a fixed-options registration logs a warning when this happens. Labels that match no item are rejected back to the LLM with a reason it can correct on the next turn. For
MultiSelectfields the resolved items are wrapped into aLinkedHashSetbeforeHasValue.setValue(V). Later calls for the same field overwrite earlier ones.- Type Parameters:
V- the item type — the field's value type for single-value fields, the per-element type for multi-select- Parameters:
config- the field's options registration, notnull; must have its item source set via eitherValueOptions.options(Collection)orValueOptions.options(BiFunction)- Returns:
- this controller, for chaining
- Throws:
NullPointerException- ifconfigisnullIllegalArgumentException- if the registration has no item source set; if the developer routed aMultiSelectfield through the single-valueforFieldoverload (upcast reference); or if the field's value type is a Collection but the field does not implementMultiSelect
-
ignoreField
Hides the given field from the LLM. The field's value is never exposed to the LLM, the LLM cannot write to it, and it is not locked during a fill. Use this for fields the AI must not read or write (internal IDs, PII). Password fields are excluded automatically and do not need to be ignored.The field is kept out of the form state and the
fill_formresponse entirely, so the LLM does not even learn it exists. It can still be exposed through a bean-level cross-field validator: abinder.withValidator((bean, ctx) -> ...)rule reads the whole bean, so a rejection message it builds is sent to the LLM as-is. Such a message must not reveal anything about an ignored field — neither its value nor its existence.- Parameters:
field- the field to hide, notnull- Returns:
- this controller, for chaining
-
setFieldValuesHidden
Controls whether the current value of every field is sent to the LLM as part of the form state. Whentrue, each field still appears with its description and type so the LLM can fill it, but its value is hidden. Use this when the form may already hold values the AI should not read (for example personal data the user typed in) but should still be able to populate. Defaults tofalse, meaning values are sent.Only the value is hidden: a field's description, type, and any option or
enumlabels are still sent, since the LLM needs them to fill the field. For choice fields whose option labels are themselves sensitive, or to hide a single field's value or content entirely, useignoreField(HasValue).Values can still reach the LLM through validation rejection messages, which are sent as-is. A field stays fillable while its value is hidden, so its own validators run on what the AI writes, and a bean-level cross-field validator (
binder.withValidator((bean, ctx) -> ...)) reads the whole bean and so can name any field's value. A validator message must not embed a field's value.- Parameters:
valuesHidden-trueto hide every field's value,falseto send values as usual- Returns:
- this controller, for chaining
-
isFieldValuesHidden
public boolean isFieldValuesHidden()Returns whether field values are hidden in the form state sent to the LLM.- Returns:
truewhen every field's value is hidden,falsewhen values are sent- See Also:
-
isSourceTrackingEnabled
public boolean isSourceTrackingEnabled()Returns whether the LLM is asked to report a source for each value it fills.- Returns:
truewhen source tracking is on,falseotherwise- Since:
- 25.3
- See Also:
-
setSourceTrackingEnabled
Controls whether the LLM is asked to report, along with each value it fills, the source it read: the snippets, where each snippet sits in the document, and aconfidence level. The reported source is available fromFieldValueChangeEvent.getFieldSource()and fromgetFieldSource(HasValue). While theautomatic field markeris on, the marker also shows the reported confidence level; a value reported without a level shows no indicator.Off by default: source tracking costs extra output tokens on every fill and brings document snippets into the server, so an application that does not use the data should not pay for either. It can be switched on or off at any time; turns that ran while it was off simply have no sources.
Source data is best effort and never blocks a fill: a malformed part (an unknown confidence level, a rectangle with no size, an unknown location type) is dropped and logged while the value is still written. The extracts are what the model says it read — they are not checked against the document.
A source says where a snippet is inside a document, but not which document. Send at most one attachment per prompt while source tracking is on: with several, there is no way to tell which one a reported location points into.
- Parameters:
sourceTrackingEnabled-trueto ask the LLM for sources,falseto not- Returns:
- this controller, for chaining
- Since:
- 25.3
-
describeConfidenceLevel
Replaces the meaning the LLM is given for one confidence level, overriding the built-in wording in thefill_formtool description. Levels not touched keep their defaults:highmeans the value is written in the document and copied as it is,mediumthat it follows from the document but needed some interpretation, andlowthat the document is unclear or the value is a guess.- Parameters:
level- the level to describe, notnulldescription- the meaning the LLM sees for the level, notnull- Returns:
- this controller, for chaining
- Since:
- 25.3
-
getFieldSource
Returns the source the LLM reported for the value the field currently holds. Use it to go through the whole form at once, or for the case where the model wrote a field with the value it already had, which fires no change event.A source lasts as long as the value it describes: as soon as the field's value changes — the user edits the field, the application overwrites it, or a revert restores the old value — the source is dropped for good, and putting the reported value back by hand does not bring it back. Sources survive across turns, so a field a later prompt did not touch keeps the source from the earlier one, and filling a field again replaces its source.
- Parameters:
field- the field to read the source of, notnull- Returns:
- the source describing the field's current value, or empty when none applies
- Throws:
NullPointerException- iffieldisnull- Since:
- 25.3
-
restoreFieldSource
Attaches a previously stored source to the field, so an application that persisted the data (seeFieldValueChangeEvent.getFieldSource()) can show it again after a reload without the original chat session. Restore the field's value first and the source after: the source is tied to the field's value at the moment of this call, so it is dropped on the next edit just like a fresh one.- Parameters:
field- the field the source describes, notnullsource- the source to restore, notnull- Throws:
NullPointerException- iffieldorsourceisnullIllegalArgumentException- iffieldis not aComponent- Since:
- 25.3
-
addFieldValueChangeListener
Registers a listener that is invoked once per field whose value changed during a successful AI turn. The listener fires once per changed field, in document order, after every field's post-turn value has been applied. Comparison is byObjects.equals(Object, Object)so multi-select sets, dates, and other value-objects work naturally.Multiple listeners are supported. For each changed field, every listener fires in registration order before the next field's event is dispatched. If one listener throws, the exception is logged and the remaining listeners still fire — both for that change and for subsequent changes in the same turn.
Only non-ignored fields are tracked, and only fields whose value differs at end-of-turn produce events. A field's pre-turn value is captured regardless of its current visibility, so a value cascaded into a freshly-revealed field is reported with the field's real pre-turn value rather than a spurious
null. A field added to the form during the turn is compared against itsempty value. No events fire when the turn ended in error.Listeners run on the UI thread with the session lock held, so they can update components directly without
ui.access(...). Marking the changed fields is not the listener's job — the controller has already done it by the time the listener runs — so this is for application-specific reactions to the AI's edits.- Parameters:
listener- the listener to register, notnull- Returns:
- a
Registrationthat removes the listener when called - Throws:
NullPointerException- iflistenerisnull
-
getFieldMarkerI18n
Returns the texts shown by the AI field marker.- Returns:
- the configured texts, or
nullwhen the built-in defaults are used - See Also:
-
setFieldMarkerI18n
Sets the texts shown by the AI field marker — the "AI" badge, its tooltip, the popover with the revert control, and the confidence indicator — replacing the built-in English defaults. The texts are applied to every marker the controller puts on a field, so set them before the first turn to localize them all. A marker already on a field keeps its texts until the controller marks that field again. Texts leftnullfall back to the built-in defaults.- Parameters:
i18n- the texts to use, ornullto restore the built-in defaults- Returns:
- this controller, for chaining
-
isFieldMarkerEnabled
public boolean isFieldMarkerEnabled()Returns whether fields changed by the AI are marked automatically at the end of a turn. Defaults totrue.- Returns:
truewhen changed fields are marked automatically,falsewhen they are left unmarked- See Also:
-
setFieldMarkerEnabled
Controls whether every field whose value the AI changed during a turn is marked when the turn ends — an "AI" badge with a popover explaining the fill and offering a revert control. Defaults totrue. Set tofalsefor a form that should carry no trace of the AI's edits.Only the mark is affected. The "AI is working" state shown while a turn runs — the shimmer and the client-side guard against editing a field the AI is about to overwrite — applies to every writable field regardless of this setting, and
change eventsstill report what the AI wrote.Turning it off does not clear marks already shown; fields marked by earlier turns stay marked until the user edits or reverts them.
- Parameters:
fieldMarkerEnabled-trueto mark changed fields,falseto leave them unmarked- Returns:
- this controller, for chaining
-
getFieldMarkerPopoverContentProvider
Returns the provider that supplies the extra content shown in the AI field marker's popover.- Returns:
- the content provider, or
nullwhen the popover shows only its built-in parts - Since:
- 25.3
- See Also:
-
setFieldMarkerPopoverContentProvider
public FormAIController setFieldMarkerPopoverContentProvider(FieldMarkerPopoverContentProvider fieldMarkerPopoverContentProvider) Sets a provider that supplies extra content for the popover that opens when the AI field marker badge is clicked, shown between the explanation message and the revert control. Use it to show what the AI based a value on — for example thesource datareported whensource trackingis on. Defaults tonull, meaning the popover shows only its built-in parts.The provider is called whenever the controller marks a field: once per field whose value changed during a successful turn, with the same event the
field-value-change listenersreceive, before those listeners run. A provider that returnsnullleaves that field's popover without extra content, and with themarker turned offthe provider is never called.The controller owns the returned component's lifecycle: it stays in the popover for as long as the mark it belongs to, is replaced when a later turn fills the field again, and goes away with the mark when the user edits or reverts the field. Return a fresh component for every call — a component that already has a parent is rejected: a warning is logged and the field is marked without extra content.
- Parameters:
fieldMarkerPopoverContentProvider- the provider to use, ornullto show no extra content- Returns:
- this controller, for chaining
- Since:
- 25.3
-
getTools
Description copied from interface:AIControllerReturns the tools this controller exposes to the LLM.- Specified by:
getToolsin interfaceAIController- Returns:
- list of tools, or empty list if controller provides no tools
-
onRequest
public void onRequest()Description copied from interface:AIControllerCalled synchronously on the UI thread just before the LLM stream opens. By the time this method fires, the user message and an empty assistant placeholder are already in the message list; the turn is committed to the conversation history and theRequestListeneronly after this method returns successfully. Implementations can prepare for the turn — locking UI surfaces, snapshotting state the tool definitions depend on, and so on. Since tools may execute on a background thread, this is the moment to capture any state that depends on Vaadin thread locals such asUI.getCurrent()orVaadinSession.getCurrent().The default does nothing. Throwing from this method aborts the turn before the commit step: the conversation history is unchanged, the request listener is not notified, the LLM stream is not opened, the assistant placeholder is updated to a generic error message,
AIController.onResponse(Throwable)fires with the thrown exception so per-turn state captured before the throw can still be released, and the exception propagates back to the caller of the prompt entry point.- Specified by:
onRequestin interfaceAIController
-
onResponse
Description copied from interface:AIControllerCalled when the turn ends: normally when the LLM stream has completed, successfully or with an error, but also when the turn fails before a stream ever opens. The call runs throughui.access(), so the session lock is held and Vaadin thread locals are bound — though not necessarily on a request thread.Fires at most once per prompt. A prompt rejected by the
RequestInterceptorends without firing it, as does a postponed prompt abandoned because its UI was detached. A turn whose UI is detached when it ends also skips the hook, which requiresui.access().On success
errorisnull; use the call to commit staged state or run deferred UI updates. On failureerrorcarries the cause (stream error, timeout, or any throw on the prompt path before the stream opens); release per-turn state captured inonRequest(locks, pending writes, snapshots) and discard the staged work. Note that a failure beforeAIController.onRequest()— for example a throwingRequestInterceptor— also fires this method, so it can run without a precedingonRequestcall.The default does nothing. Exceptions thrown from the hook are caught and logged; Errors propagate.
- Specified by:
onResponsein interfaceAIController- Parameters:
error- the cause of failure, ornullon success
-