Class FormAIController
- All Implemented Interfaces:
com.vaadin.flow.component.ai.orchestrator.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 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.
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, com.vaadin.flow.data.binder.Binder<?> binder) Creates a new form AI controller for the given container and binder. -
Method Summary
Modifier and TypeMethodDescriptioncom.vaadin.flow.shared.RegistrationRegisters a listener that is invoked once per field whose value changed during a successful AI turn.describeField(com.vaadin.flow.component.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.List<com.vaadin.flow.component.ai.provider.LLMProvider.ToolSpec> getTools()ignoreField(com.vaadin.flow.component.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.voidvoidonResponse(Throwable error) 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, and the popover with the revert control — replacing the built-in English defaults.setFieldValuesHidden(boolean valuesHidden) Controls whether the current value of every field is sent to the LLM as part of the form state.
-
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
public FormAIController(T fieldContainer, com.vaadin.flow.data.binder.Binder<?> binder) 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
public FormAIController describeField(com.vaadin.flow.component.HasValue<?, ?> field, String description) 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:
-
addFieldValueChangeListener
public com.vaadin.flow.shared.Registration addFieldValueChangeListener(FieldValueChangeListener listener) 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, and the popover with the revert control — 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
-
getTools
- Specified by:
getToolsin interfacecom.vaadin.flow.component.ai.orchestrator.AIController
-
onRequest
public void onRequest()- Specified by:
onRequestin interfacecom.vaadin.flow.component.ai.orchestrator.AIController
-
onResponse
- Specified by:
onResponsein interfacecom.vaadin.flow.component.ai.orchestrator.AIController
-