Skip to content

feat!: switch form filler value options to items-based resolution#9564

Open
ugur-vaadin wants to merge 4 commits into
mainfrom
feat-add-item-label-generator-to-value-options
Open

feat!: switch form filler value options to items-based resolution#9564
ugur-vaadin wants to merge 4 commits into
mainfrom
feat-add-item-label-generator-to-value-options

Conversation

@ugur-vaadin

@ugur-vaadin ugur-vaadin commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Description

In form filler DX test sessions, the return type of value options APIs was a pain point. Even after updating examples and clarifying it in the documentation, it still was not intuitive: the developer had to think in labels (strings) when the rest of their codebase thought in domain objects, and had to supply a toValue converter that re-encoded the label-to-value mapping the field's ItemLabelGenerator already expressed.

This PR replaces that shape with an items-based resolution path:

  • Registrations now carry the domain objects, not pre-rendered labels. Aligned with how the rest of the codebase models selection options.
  • Labels for the LLM are derived through one ItemLabelGenerator. By default, the controller reflects the field's own setItemLabelGenerator(...) so common cases need no extra wiring; an explicit one can be supplied on ValueOptions for custom components or cases where the LLM should see a different label than the UI.
  • When the LLM picks a label, the controller walks the registration's items, applies the same generator per item, and writes the first matching item to the field. The bijection lives in one place — the label generator — and the toValue converter is no longer needed and has been removed.

Changed in ValueOptions:

  • options(Collection<String>)options(Collection<V>)
  • options(BiFunction<String, Integer, List<String>>)options(BiFunction<String, Integer, List<V>>)

New in ValueOptions:

  • itemLabelGenerator(ItemLabelGenerator<V>) — optional; falls back to the field's own labeler, then to String.valueOf(item).

Changed in FormAIController:

  • fieldValueOptions(ValueOptions<String>)fieldValueOptions(ValueOptions<V>)
  • fieldValueOptions(ValueOptions<V>, Function<String, V> toValue) removed.

Other behavior:

  • Items with duplicate labels resolve to the first in registration order; a fixed-options registration logs a warning at registration so the developer notices.
  • A query-mode fill_form that arrives before any query_field_options call is rejected with a reason naming query_field_options so the LLM can recover on the next turn.

Warning

The two-argument fieldValueOptions(ValueOptions, Function<String, V>) overload and the String-only generics on the single-argument variant are gone.

Based on DX test findings.

No related issue.

Type of change

  • Bugfix
  • Feature

Checklist

  • I have read the contribution guide: https://vaadin.com/docs/latest/contributing/overview
  • I have added a description following the guideline.
  • The issue is created in the corresponding repository and I have referenced it.
  • I have added tests to ensure my change is effective and works as intended.
  • New and existing tests are passing locally with my change.
  • I have performed self-review and corrected misspellings.
  • Enhancement / new feature was discussed in a corresponding GitHub issue and Acceptance Criteria were created.

@vaadin vaadin deleted a comment from github-actions Bot Jun 18, 2026
@ugur-vaadin ugur-vaadin requested review from sissbruecker and web-padawan and removed request for tomivirkki and yuriy-fix June 23, 2026 16:23

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JavaDoc says:

Each item is also the label the LLM sees — the chosen label is itself the value written to the field, with no converter needed

But it runs the same applyValueOptions method that applies a potential item labeler. So when you provide a label function through the value options (and presumably also when through the field), the labels and values differ.

From my understanding this method should not use an item labeler, and potentially log a warning if one is configured in the options.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped the Function.identity() along with the two-arg overload entirely. For duplicate labels: first registered item wins, and the registration logs a warning including the duplicates and pointing at ItemLabelGenerator.

* converter does not recognize — the write is rejected back to the LLM with
* a reason and the model can correct on the next turn.
* <p>
* A typical converter delegates to a service or repository that looks the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be worth pointing out explicitly that toValue must produce the reverse of whatever an item label generator produced. Currently this is connection is only implicit.

I feel this surfaces a gap in the design where you configure model -> label and label -> model converters in separate places, which has a higher chance for a misconfiguration. It's also problematic that this uses the label generator from the field as a fallback: That one can be easily changed in the UI code for whatever reason, but then you also need to remember to update the converter in the code that configures the form controller to resolve the model from the changed label.

I wonder if toValue is even necessary? We already keep references to the items in the value options now, so could we also add a lookup for the reverse mapping? Or was there some concern about stale data?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped toValue. We iterated on this API quite a bit and I agree that it doesn't provide much value anymore, and it creates more confusion.

@ugur-vaadin ugur-vaadin changed the title feat: add item label generator to form filler value options feat!: switch form filler value options to items-based resolution Jun 25, 2026
hints.itemLabelGenerator = labeler;
// Items the converter resolves chosen labels against. Pre-populated
// with the fixed list, or appended on each query-callback invocation.
var observedItems = new ArrayList<>();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to use a map to automatically dedupe, instead of keeping lots of object references that will never be used because we only pick the first one? Also the lookups would become easier / cheaper.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. We not use a LinkedHashMap that uses insertion order.

Comment on lines +430 to +434
hints.valueOptionsQuery = (filter, limit) -> {
var batch = rawQuery.apply(filter, limit);
observedItems.addAll(batch);
return batch.stream().map(labeler).toList();
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't look correct, as it keeps adding potentially the same instances over and over if multiple queries match them. A map could fix this part as well.

Other than that it's questionable if this should keep previous entries, in case the application has removed data or updated it. Maybe it should clear on every query, or there might be some other point in the workflow where that is sensible.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the code to use the map. It should fix the potential inflation issue. For the second part, the controller now refreshes every value options binding at the start of each turn. It re-reads the field's labeler, repopulates the fixed options map from its item list, and clears the query options map.

@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants