Class FormAIController

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

public class FormAIController extends Object implements 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(...).

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 describe, ignore, and valueOptions methods. valueOptions takes a ValueOptions built via forField — the compiler picks the MultiSelect overload automatically for fields statically typed as MultiSelect. For fields whose value type is anything other than String, the two-argument overload also accepts a label-to-value converter, which the controller applies per label; for multi-select fields the resolved elements are then aggregated into a LinkedHashSet before HasValue.setValue(V).

Field identifiers: the controller assigns an opaque UUID to each field at discovery time and uses that UUID as the field's id in every tool call the LLM makes. Developers never see UUIDs; the LLM never sees field labels in the id slot. Semantic meaning travels through each field's description — the field's label, helper text, and the describe(HasValue, String) hint.

Binder integration: the two-argument constructor accepts a Binder. For every named binding (bind("propertyName"), bindInstanceFields(this), or @PropertyId) the property name is used as a default field description so the LLM can refer to the field by its bean-side name. The default only applies when no explicit describe(HasValue, String) has been registered; calling describe(...) always wins. Lambda-bound bindings carry no property name and contribute no default.

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.

Field locking: while a fill is in progress, every non-ignored field that wasn't already read-only is set to read-only so the user cannot type into a field the AI is about to overwrite. Locks are released when the turn ends, successfully or otherwise. Application code that changes a field's read-only state mid-turn (e.g. from a value-change listener reacting to the LLM's writes) will be overridden when the controller releases its own locks at turn end — applications should avoid toggling read-only state during a fill turn, or reapply it after the turn completes.

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 describe / valueOptions hints; field ids remain stable across the round-trip because they live on the field Components themselves.

Author:
Vaadin Ltd
  • Constructor Details

    • FormAIController

      public FormAIController(T form)
      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:
      form - the container whose fields the LLM may populate, not null
    • FormAIController

      public FormAIController(T form, 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; the controller itself still uses an opaque UUID as the field's tool-call id. Lambda-bound bindings carry no property name and contribute no default.
      Type Parameters:
      T - the container type
      Parameters:
      form - 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 form or binder is null
  • Method Details

    • describe

      public FormAIController describe(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
    • valueOptions

      public FormAIController valueOptions(ValueOptions<String> config)
      Registers options for a String-typed field. The config carries the field and either a fixed label list or a query callback; the label-to-value converter is implicitly Function.identity() because the chosen label is the value. For any other value type, use the two-argument overload — the type system enforces at compile time that a non-String field's registration is paired with an explicit converter. For MultiSelect fields the controller wraps resolved elements into a LinkedHashSet before HasValue.setValue(V). Later calls for the same field overwrite earlier ones.
      Parameters:
      config - the field's options registration, not null; must have options(...) (fixed or queryable) set
      Returns:
      this controller, for chaining
      Throws:
      NullPointerException - if config is null
      IllegalArgumentException - if the registration has no options(...) 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
    • valueOptions

      public <V> FormAIController valueOptions(ValueOptions<V> config, Function<String,V> toValue)
      Registers options for a field paired with an explicit label-to-value converter. The converter resolves one LLM-supplied label into one element of the field's value type (or per-element type for a MultiSelect). For MultiSelect fields the controller wraps resolved elements into a LinkedHashSet before HasValue.setValue(V). Later calls for the same field overwrite earlier ones.
      Type Parameters:
      V - the per-label 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 options(...) (fixed or queryable) set
      toValue - converts a chosen label to one element of the field's value type, not null
      Returns:
      this controller, for chaining
      Throws:
      NullPointerException - if config or toValue is null
      IllegalArgumentException - if the registration has no options(...) 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
    • ignore

      public FormAIController ignore(HasValue<?,?> field)
      Hides the given field from the LLM. The field is excluded from the tool surface and is not locked during a fill. Use this for fields the AI must not read or write (password fields, internal IDs, PII).
      Parameters:
      field - the field to hide, not null
      Returns:
      this controller, for chaining
    • getTools

      public List<LLMProvider.ToolSpec> getTools()
      Description copied from interface: AIController
      Returns the tools this controller exposes to the LLM.
      Specified by:
      getTools in interface AIController
      Returns:
      list of tools, or empty list if controller provides no tools
    • onRequest

      public void onRequest()
      Description copied from interface: AIController
      Called 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 the RequestListener only after this method returns successfully. Implementations can prepare for the turn — locking UI surfaces, snapshotting state the tool definitions depend on, and so on.

      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:
      onRequest in interface AIController
    • onResponse

      public void onResponse(Throwable error)
      Description copied from interface: AIController
      Called on the UI thread under the session lock when the LLM stream has completed — either successfully or with an error. Every turn fires this exactly once.

      On success error is null; use the call to commit staged state or run deferred UI updates. On failure error carries the cause (stream error, timeout, or any throw between AIController.onRequest() and the start of the stream); release per-turn state captured in onRequest (locks, pending writes, snapshots) and discard the staged work.

      The default does nothing. Exceptions thrown from the hook are caught and logged; Errors propagate.

      Specified by:
      onResponse in interface AIController
      Parameters:
      error - the cause of failure, or null on success