Class FormAIController

java.lang.Object
com.vaadin.flow.component.ai.form.FormAIController
All Implemented Interfaces:
com.vaadin.flow.component.ai.orchestrator.AIController

public class FormAIController extends Object implements com.vaadin.flow.component.ai.orchestrator.AIController
Populates a layout's fields with values an LLM extracts from a user prompt or attached files. Attach it to an 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 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, not null
    • 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 default description when 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, not null
      binder - the binder whose property names default the field descriptions, not null; use the single-argument constructor for the no-binder case
      Throws:
      NullPointerException - if fieldContainer or binder is null
  • 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, not null
      description - the description text, not null
      Returns:
      this controller, for chaining
    • fieldValueOptions

      public <V> FormAIController fieldValueOptions(ValueOptions<V> config)
      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 on ValueOptions.

      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 MultiSelect fields the resolved items are wrapped into a LinkedHashSet before HasValue.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, not null; must have its item source set via either ValueOptions.options(Collection) or ValueOptions.options(BiFunction)
      Returns:
      this controller, for chaining
      Throws:
      NullPointerException - if config is null
      IllegalArgumentException - if the registration has no item source set; if the developer routed a MultiSelect field through the single-value forField overload (upcast reference); or if the field's value type is a Collection but the field does not implement MultiSelect
    • ignoreField

      public FormAIController ignoreField(com.vaadin.flow.component.HasValue<?,?> field)
      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_form response entirely, so the LLM does not even learn it exists. It can still be exposed through a bean-level cross-field validator: a binder.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, not null
      Returns:
      this controller, for chaining
    • setFieldValuesHidden

      public FormAIController setFieldValuesHidden(boolean valuesHidden)
      Controls whether the current value of every field is sent to the LLM as part of the form state. When true, 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 to false, meaning values are sent.

      Only the value is hidden: a field's description, type, and any option or enum labels 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, use ignoreField(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 - true to hide every field's value, false to 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:
      true when every field's value is hidden, false when 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 by Objects.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 its empty 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, not null
      Returns:
      a Registration that removes the listener when called
      Throws:
      NullPointerException - if listener is null
    • getFieldMarkerI18n

      public FieldMarkerI18n getFieldMarkerI18n()
      Returns the texts shown by the AI field marker.
      Returns:
      the configured texts, or null when the built-in defaults are used
      See Also:
    • setFieldMarkerI18n

      public FormAIController setFieldMarkerI18n(FieldMarkerI18n i18n)
      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 left null fall back to the built-in defaults.
      Parameters:
      i18n - the texts to use, or null to 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 to true.
      Returns:
      true when changed fields are marked automatically, false when they are left unmarked
      See Also:
    • setFieldMarkerEnabled

      public FormAIController 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. Defaults to true. Set to false for 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 events still 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 - true to mark changed fields, false to leave them unmarked
      Returns:
      this controller, for chaining
    • getTools

      public List<com.vaadin.flow.component.ai.provider.LLMProvider.ToolSpec> getTools()
      Specified by:
      getTools in interface com.vaadin.flow.component.ai.orchestrator.AIController
    • onRequest

      public void onRequest()
      Specified by:
      onRequest in interface com.vaadin.flow.component.ai.orchestrator.AIController
    • onResponse

      public void onResponse(Throwable error)
      Specified by:
      onResponse in interface com.vaadin.flow.component.ai.orchestrator.AIController