From 92914217a3ee8e52b78383430c286d3a5db37199 Mon Sep 17 00:00:00 2001 From: ugur-vaadin Date: Mon, 13 Apr 2026 18:51:17 +0300 Subject: [PATCH 01/24] docs: add ai controller docoumentation --- articles/building-apps/ai/index.adoc | 5 +- .../flow/ai-support/ai-powered-chart.adoc | 140 ++++++++++++++++ articles/flow/ai-support/ai-powered-grid.adoc | 105 ++++++++++++ articles/flow/ai-support/controllers.adoc | 155 ++++++++++++++++++ .../flow/ai-support/conversation-history.adoc | 5 + articles/flow/ai-support/index.adoc | 9 +- articles/flow/ai-support/tool-calling.adoc | 4 + 7 files changed, 420 insertions(+), 3 deletions(-) create mode 100644 articles/flow/ai-support/ai-powered-chart.adoc create mode 100644 articles/flow/ai-support/ai-powered-grid.adoc create mode 100644 articles/flow/ai-support/controllers.adoc diff --git a/articles/building-apps/ai/index.adoc b/articles/building-apps/ai/index.adoc index e2a9b2e55d..08c8c9a394 100644 --- a/articles/building-apps/ai/index.adoc +++ b/articles/building-apps/ai/index.adoc @@ -15,11 +15,12 @@ You'll learn how to: * connect your application to an AI client with popular Java libraries such as Spring AI and LangChain4j, * use the xref:{articles}/flow/ai-support#[AI support features] to connect LLM providers to Vaadin UI components with minimal boilerplate, -* choose Vaadin components that create intuitive, AI-powered workflows -- such as `MessageInput`, `MessageList`, and `UploadManager`, and +* choose Vaadin components that create intuitive, AI-powered workflows -- such as `MessageInput`, `MessageList`, and `UploadManager`, +* let users explore application data through natural language with <<{articles}/flow/ai-support/ai-powered-grid#,AI-powered Grid>> and <<{articles}/flow/ai-support/ai-powered-chart#,AI-powered Chart>>, and * deliver real-time updates to users through server push. [TIP] -The <<{articles}/flow/ai-support#,AI support features>> eliminate the boilerplate of wiring UI components to LLM frameworks. The [classname]`AIOrchestrator` handles streaming, conversation history, file attachments, and tool calling behind a simple builder API. See the <<{articles}/flow/ai-support#,documentation>> for the full API reference. +The <<{articles}/flow/ai-support#,AI support features>> eliminate the boilerplate of wiring UI components to LLM frameworks. The [classname]`AIOrchestrator` handles streaming, conversation history, file attachments, and tool calling behind a simple builder API. Ready-made controllers -- [classname]`GridAIController` and [classname]`ChartAIController` -- bring natural-language data exploration to [classname]`Grid` and [classname]`Chart`. See the <<{articles}/flow/ai-support#,documentation>> for the full API reference. section_outline::[] diff --git a/articles/flow/ai-support/ai-powered-chart.adoc b/articles/flow/ai-support/ai-powered-chart.adoc new file mode 100644 index 0000000000..3eb44863d8 --- /dev/null +++ b/articles/flow/ai-support/ai-powered-chart.adoc @@ -0,0 +1,140 @@ +--- +title: AI-Powered Chart +description: Use ChartAIController to let users build and update Highcharts visualizations from the application database using natural language. +meta-description: Learn how to configure the Vaadin ChartAIController to create and update Charts from a database via natural language prompts and persist the resulting state. +order: 70 +--- + + += [since:com.vaadin:vaadin@V25.2]#AI-Powered Chart# + +ifdef::flow[] + +[classname]`ChartAIController` creates and updates a [classname]`Chart` visualization based on natural-language requests. Given a <>, the controller exposes tools that let the LLM inspect the database schema, write SQL queries for one or more series, and update the Highcharts configuration independently of the data. + +The controller exposes five tools: + +* `get_database_schema` -- describes the tables, columns, and SQL dialect. +* `get_chart_state` -- returns the current Highcharts configuration and the queries powering the chart. +* `update_chart_data_source` -- validates and queues SQL queries, one per series. +* `update_chart_configuration` -- queues a Highcharts configuration update, such as chart type, axes, tooltip, legend, or per-series styling. +* `get_plot_options_schema` -- returns the JSON schema for a specific chart type's plot options, allowing the LLM to discover available styling properties. + +Data and configuration are kept separate: series data comes from SQL queries, while visual appearance comes from the configuration. Both are queued during the LLM turn and applied atomically in [methodname]`onRequestCompleted()`. + + +== Basic Usage + +Create a [classname]`Chart`, construct a controller, and wire it to the orchestrator: + +[source,java] +---- +Chart chart = new Chart(); +MessageInput messageInput = new MessageInput(); + +DatabaseProvider databaseProvider = new JdbcDatabaseProvider(dataSource); +ChartAIController controller = new ChartAIController(chart, databaseProvider); + +AIOrchestrator.builder(provider, ChartAIController.getSystemPrompt()) + .withInput(messageInput) + .withController(controller) + .build(); + +add(messageInput, chart); +---- + +Example prompts: + +* "Plot monthly revenue for the last year as a column chart." +* "Show revenue by region as a pie chart." +* "Compare 2025 and 2026 quarterly sales side-by-side with a legend." +* "Turn this into an area chart and add a 'Revenue (USD)' y-axis title." + +.Recommended System Prompt +[TIP] +[methodname]`ChartAIController.getSystemPrompt()` returns a recommended system prompt that instructs the LLM to inspect state before making changes and to keep data and configuration separate. + + +== Data Conventions + +The default converter recognizes specific column aliases when mapping SQL results to Highcharts series. All reserved names are prefixed with `_` to avoid colliding with real data columns. Common patterns include: + +* Basic charts -- line, column, bar, area, pie, and spline charts take two columns: category and value. No special aliases are required. +* Multi-series -- add a `_series` column to group rows into separate named series. +* Scatter and bubble -- `_x`, `_y`, and optionally `_z` for bubble size. +* Range (`arearange`, `columnrange`) -- `_low`, `_high`, and optionally `_x`. +* OHLC and candlestick -- `_x`, `_open`, `_high`, `_low`, `_close`. +* Heatmap -- `_x`, `_y`, `_value`. +* Gantt, Timeline, Treemap, Organization, Sankey, and Waterfall -- use the dedicated name-based aliases documented in the tool description. + +Any pattern that produces a [classname]`DataSeriesItem` also accepts an optional `_color` column for per-point coloring. The full list of aliases is defined in the [classname]`ColumnNames` class. + + +== Custom Data Conversion + +The built-in [classname]`DefaultDataConverter` handles the common patterns. To take full control -- for example, to post-process rows, merge data from multiple queries into a single series, or apply custom formatting -- implement [classname]`DataConverter` and register it with [methodname]`setDataConverter()`: + +[source,java] +---- +public class CurrencyDataConverter implements DataConverter { + + @Override + public List convertToSeries(List> data) { + DataSeries series = new DataSeries(); + for (Map row : data) { + String name = (String) row.get("category"); + Number value = (Number) row.get("value"); + series.add(new DataSeriesItem(name, roundToCents(value))); + } + return List.of(series); + } +} + +controller.setDataConverter(new CurrencyDataConverter()); +---- + +The converter receives the raw rows from [methodname]`DatabaseProvider.executeQuery()` and returns one or more [classname]`Series` instances. + + +== Persisting Chart State + +[classname]`ChartState` captures both the SQL queries and the Highcharts configuration. Register a state change listener to persist the state automatically after each successful AI request: + +[source,java] +---- +controller.addStateChangeListener(state -> + sessionStore.save(sessionId, state)); + +// Restore on a new session +ChartState saved = sessionStore.load(sessionId); +if (saved != null) { + controller.restoreState(saved); +} +---- + +Listeners do not fire when [methodname]`restoreState()` is called. The queries and configuration are also stored on the [classname]`Chart` component, so they survive session serialization automatically. + + +== Reconnecting After Deserialization + +[classname]`ChartAIController` is not serializable. After session restore, create a new controller and pass it to the orchestrator's reconnector together with the new provider: + +[source,java] +---- +ChartAIController controller = new ChartAIController(chart, databaseProvider); +ChartState saved = sessionStore.load(sessionId); +if (saved != null) { + controller.restoreState(saved); +} + +orchestrator.reconnect(provider) + .withController(controller) + .apply(); +---- + + +== Multiple Charts + +[classname]`ChartAIController` manages a single chart. To expose several charts to the same orchestrator -- for example in a dashboard -- implement a custom [classname]`AIController` that builds its tool list from [classname]`ChartAITools.createAll()` with a [classname]`ChartAITools.Callbacks` implementation that returns a stable ID per chart. + +endif::flow[] diff --git a/articles/flow/ai-support/ai-powered-grid.adoc b/articles/flow/ai-support/ai-powered-grid.adoc new file mode 100644 index 0000000000..5856e9571d --- /dev/null +++ b/articles/flow/ai-support/ai-powered-grid.adoc @@ -0,0 +1,105 @@ +--- +title: AI-Powered Grid +description: Use GridAIController to let users populate a Grid from the application database using natural language. +meta-description: Learn how to configure the Vaadin GridAIController to populate a Grid from a database via natural language prompts and persist the resulting state. +order: 60 +--- + + += [since:com.vaadin:vaadin@V25.2]#AI-Powered Grid# + +ifdef::flow[] + +[classname]`GridAIController` populates a [classname]`Grid` with data from the application database based on natural-language requests. Given a <>, the controller exposes three tools to the LLM: + +* `get_database_schema` -- describes the tables, columns, and SQL dialect. +* `get_grid_state` -- returns the SQL query currently driving the grid. +* `update_grid_data` -- queues a new SQL query for the grid. + +When an LLM request completes, the queued query is executed and the grid re-renders with dynamically generated columns, type-appropriate renderers, right-aligned numeric columns, lazy loading via SQL `LIMIT`/`OFFSET`, and optional column grouping. + + +== Basic Usage + +Create a grid typed as `Map`, construct a controller with the grid and a [classname]`DatabaseProvider`, and attach it to an orchestrator: + +[source,java] +---- +Grid> grid = new Grid<>(); +MessageInput messageInput = new MessageInput(); + +DatabaseProvider databaseProvider = new JdbcDatabaseProvider(dataSource); +GridAIController controller = new GridAIController(grid, databaseProvider); + +AIOrchestrator.builder(provider, GridAIController.getSystemPrompt()) + .withInput(messageInput) + .withController(controller) + .build(); + +add(messageInput, grid); +---- + +A message list is not required -- the grid itself is the output surface. Example prompts: + +* "Show me all employees in the Sales department." +* "List the top 10 highest-paid employees with name, salary, and hire date." +* "Show product name and category grouped under Product, and monthly revenue." + +.Recommended System Prompt +[TIP] +[methodname]`GridAIController.getSystemPrompt()` returns a recommended system prompt that teaches the LLM how to use the controller's tools. Concatenate it with your own instructions if you need additional behavior. + + +== SQL Conventions + +The tool description instructs the LLM to produce queries the grid can render cleanly: + +* Every column must have a human-readable `AS` alias. `SELECT *` is not allowed. +* The grid handles pagination, so queries must not include `LIMIT` or `OFFSET`. +* Dot-separated aliases -- for example `"Product.Name"` and `"Product.Category"` -- produce grouped column headers. A header row labelled "Product" spans both columns. + +Before queueing an update, the controller executes a lightweight probe (`SELECT * FROM () AS _v LIMIT 1`) to validate the query. If the probe fails, the error is returned to the LLM so that it can correct the query on the next turn. + + +== Persisting Grid State + +Grid state is a single SQL query represented by the [classname]`GridState` record. Capture it with [methodname]`getState()` and restore it later with [methodname]`restoreState()`. Register a state change listener to persist the state automatically after each successful AI request: + +[source,java] +---- +controller.addStateChangeListener(state -> + sessionStore.save(sessionId, state)); + +// Restore on a new session +GridState saved = sessionStore.load(sessionId); +if (saved != null) { + controller.restoreState(saved); +} +---- + +[methodname]`addStateChangeListener()` fires only when the grid is updated by the LLM, not when [methodname]`restoreState()` is called. The current query is also stored directly on the [classname]`Grid` component, so it survives session serialization automatically. + + +== Reconnecting After Deserialization + +[classname]`GridAIController` is not serializable. After session restore, create a new controller and pass it to the orchestrator's reconnector together with the new provider: + +[source,java] +---- +GridAIController controller = new GridAIController(grid, databaseProvider); +GridState saved = sessionStore.load(sessionId); +if (saved != null) { + controller.restoreState(saved); +} + +orchestrator.reconnect(provider) + .withController(controller) + .apply(); +---- + + +== Multiple Grids + +[classname]`GridAIController` manages a single grid. To coordinate several grids from the same orchestrator, implement a custom [classname]`AIController` that builds its tool list from [classname]`GridAITools.createAll()` with a custom [classname]`GridAITools.Callbacks` implementation. The callbacks -- [methodname]`getGridIds()`, [methodname]`getState(gridId)`, and [methodname]`updateData(gridId, query)` -- let the LLM address each grid by its ID. + +endif::flow[] diff --git a/articles/flow/ai-support/controllers.adoc b/articles/flow/ai-support/controllers.adoc new file mode 100644 index 0000000000..1971c03216 --- /dev/null +++ b/articles/flow/ai-support/controllers.adoc @@ -0,0 +1,155 @@ +--- +title: Controllers +description: Extend the AIOrchestrator with reusable, framework-agnostic tools and lifecycle hooks using the AIController interface. +meta-description: Learn how to use AIController to add reusable tools and lifecycle hooks to the Vaadin AIOrchestrator, and how to expose a database to the LLM via DatabaseProvider. +order: 45 +--- + + += [since:com.vaadin:vaadin@V25.2]#Controllers# + +ifdef::flow[] + +Controllers extend the orchestrator with domain-specific tools and lifecycle hooks. Objects registered via [methodname]`withTools()` rely on vendor-specific annotations such as LangChain4j's or Spring AI's [annotationname]`@Tool`. A controller, by contrast, contributes tools through the framework-agnostic [classname]`AIController` interface. The orchestrator collects the controller's tools before every LLM request and notifies the controller once the request cycle completes. + +Vaadin provides two built-in controllers: + +* <> -- populates a [classname]`Grid` from a database using natural-language requests. +* <> -- creates and updates [classname]`Chart` visualizations from a database using natural-language requests. + +Both rely on a <<#database-provider,[classname]`DatabaseProvider`>> to expose schema information and execute queries on behalf of the LLM. + + +== Attaching a Controller + +Pass a controller to the orchestrator's builder with [methodname]`withController()`: + +[source,java] +---- +var orchestrator = AIOrchestrator + .builder(provider, GridAIController.getSystemPrompt()) + .withMessageList(messageList) + .withInput(messageInput) + .withController(controller) + .build(); +---- + +Only one controller can be attached per orchestrator. Controllers and tool objects registered via [methodname]`withTools()` can be combined -- the orchestrator merges their tool lists before each request. + + +== The [classname]`AIController` Interface + +[classname]`AIController` defines two methods, both with default implementations: + +* [methodname]`getTools()` -- returns the list of [classname]`LLMProvider.ToolSpec` instances the controller contributes to each LLM request. Defaults to an empty list. Tools are collected before every request, so a controller can vary its tool set based on current state. +* [methodname]`onRequestCompleted()` -- runs after all tool calls for a user request have finished and the LLM has produced its final response. Controllers use this hook to apply deferred state changes, avoiding partial state and multiple redraws during a multi-tool turn. + +A minimal custom controller looks like this: + +[source,java] +---- +public class WeatherController implements AIController { + + @Override + public List getTools() { + return List.of(new LLMProvider.ToolSpec() { + @Override + public String getName() { + return "get_weather"; + } + + @Override + public String getDescription() { + return "Returns the current weather for a city."; + } + + @Override + public String getParametersSchema() { + return """ + { + "type": "object", + "properties": { + "city": { "type": "string" } + }, + "required": ["city"] + }"""; + } + + @Override + public String execute(String arguments) { + // Parse arguments as JSON and call your service + return weatherService.lookup(arguments); + } + }); + } +} +---- + +.Controller Serialization +[NOTE] +Controllers are not serialized with the orchestrator. After session restore, pass the controller to [methodname]`reconnect(provider).withController(controller).apply()` -- see <>. + + +[[database-provider]] +== Database Provider + +[classname]`DatabaseProvider` is the bridge between the LLM and an application database. The built-in grid and chart controllers use it to let the LLM discover the schema and execute read-only SQL queries on demand. Any controller that needs database access can reuse [classname]`DatabaseProviderAITools` to register the shared `get_database_schema` tool. + +The interface defines two methods: + +* [methodname]`getSchema()` -- returns a plain-text description of the tables, columns, and SQL dialect. The LLM uses this to write valid queries. +* [methodname]`executeQuery(String sql)` -- executes a SQL query and returns the rows as a list of column-name-to-value maps. + +The following example is a straightforward JDBC implementation: + +[source,java] +---- +public class JdbcDatabaseProvider implements DatabaseProvider { + + private final DataSource readOnlyDataSource; + + public JdbcDatabaseProvider(DataSource readOnlyDataSource) { + this.readOnlyDataSource = readOnlyDataSource; + } + + @Override + public String getSchema() { + return """ + Tables: + employees(id INT, name VARCHAR, department VARCHAR, salary NUMERIC, hired_on DATE) + departments(id INT, name VARCHAR) + Dialect: PostgreSQL. + """; + } + + @Override + public List> executeQuery(String sql) { + try (var conn = readOnlyDataSource.getConnection(); + var stmt = conn.prepareStatement(sql); + var rs = stmt.executeQuery()) { + var meta = rs.getMetaData(); + var rows = new ArrayList>(); + while (rs.next()) { + var row = new LinkedHashMap(); + for (int i = 1; i <= meta.getColumnCount(); i++) { + row.put(meta.getColumnLabel(i), rs.getObject(i)); + } + rows.add(row); + } + return rows; + } catch (SQLException e) { + throw new IllegalArgumentException("Query failed: " + e.getMessage(), e); + } + } +} +---- + +.Read-Only Database Access +[IMPORTANT] +The LLM writes the SQL that gets executed. Always back a [classname]`DatabaseProvider` implementation with a database account that has read-only access to the tables and views you intend to expose. This prevents the LLM from modifying or deleting data and limits the impact of a prompt-injection attempt that tries to trick the LLM into running destructive statements. + +.Schema Scope +[TIP] +Return only the tables, columns, and relationships the LLM needs. A smaller, well-described schema produces better queries, uses fewer tokens, and reduces the chance of leaking sensitive columns. + +endif::flow[] diff --git a/articles/flow/ai-support/conversation-history.adoc b/articles/flow/ai-support/conversation-history.adoc index e3d9343569..2b70033c13 100644 --- a/articles/flow/ai-support/conversation-history.adoc +++ b/articles/flow/ai-support/conversation-history.adoc @@ -91,10 +91,15 @@ Map> savedAttachments = orchestrator.reconnect(provider) .withTools(new WeatherTools()) // optional + .withController(controller) // optional .withAttachments(savedAttachments) // optional .apply(); ---- +.Controller Reattachment +[NOTE] +[classname]`AIController` instances -- including [classname]`GridAIController` and [classname]`ChartAIController` -- are not serialized with the orchestrator. Create a new controller after session restore and pass it to [methodname]`withController()` on the reconnector. See <> for state capture and restoration guidance. + If [methodname]`prompt()` is called before reconnecting, it throws an [classname]`IllegalStateException`. .LLMProvider Implementations diff --git a/articles/flow/ai-support/index.adoc b/articles/flow/ai-support/index.adoc index 71650a0c82..32f1fe0e05 100644 --- a/articles/flow/ai-support/index.adoc +++ b/articles/flow/ai-support/index.adoc @@ -25,11 +25,12 @@ ifdef::flow[] == Overview -The AI support module consists of three parts: +The AI support module consists of four parts: * **Orchestrator** -- [classname]`AIOrchestrator` is a non-visual coordination engine that connects UI components to an LLM provider. It has no DOM element and should not be added to a layout. * **Provider** -- [classname]`LLMProvider` is the interface for communicating with LLM frameworks. Create a provider by instantiating [classname]`SpringAILLMProvider` or [classname]`LangChain4JLLMProvider` directly. You can also implement this interface to connect to any other LLM framework. * **Component interfaces** -- [classname]`AIInput`, [classname]`AIMessageList`, [classname]`AIMessage`, and [classname]`AIFileReceiver` define contracts for UI components that the orchestrator can work with. The builder also accepts standard Vaadin components ([classname]`MessageInput`, [classname]`MessageList`, [classname]`UploadManager`, [classname]`Upload`) directly. +* **Controllers** -- [classname]`AIController` is the framework-agnostic interface for contributing tools and lifecycle hooks to the orchestrator. Built-in controllers such as [classname]`GridAIController` and [classname]`ChartAIController` bring AI-powered data exploration to [classname]`Grid` and [classname]`Chart`, backed by a [classname]`DatabaseProvider`. Add the UI components to your layout and pass them to the orchestrator through its builder. The orchestrator wires them together and manages the LLM interaction. @@ -101,4 +102,10 @@ endif::flow[] |<<{articles}/components/upload#,Upload>> |File uploads via [classname]`UploadManager`. Pass to the orchestrator with [methodname]`withFileReceiver()`. +|<<{articles}/components/grid#,Grid>> +|Populated from the application database via natural-language requests. See <>. + +|<<{articles}/components/charts#,Charts>> +|Built and updated from the application database via natural-language requests. See <>. + |=== diff --git a/articles/flow/ai-support/tool-calling.adoc b/articles/flow/ai-support/tool-calling.adoc index 8445b6e166..727bdbdcee 100644 --- a/articles/flow/ai-support/tool-calling.adoc +++ b/articles/flow/ai-support/tool-calling.adoc @@ -34,6 +34,10 @@ var orchestrator = AIOrchestrator For Spring AI, use [annotationname]`@org.springframework.ai.tool.annotation.Tool`. For LangChain4j, use [annotationname]`@dev.langchain4j.agent.tool.Tool`. +.Framework-Agnostic Tools via Controllers +[TIP] +For a reusable set of tools that does not depend on a specific LLM framework's annotations, or when a lifecycle hook is needed after each LLM request cycle, implement <> instead. [classname]`GridAIController` and [classname]`ChartAIController` are built-in examples. Controllers and tool objects can be combined on the same orchestrator. + == Programmatic Prompts From d4bd01f88b98ce5bbc463e0f5ea31df92c770cae Mon Sep 17 00:00:00 2001 From: Ugur Saglam <106508695+ugur-vaadin@users.noreply.github.com> Date: Wed, 22 Apr 2026 14:43:53 +0300 Subject: [PATCH 02/24] Update articles/flow/ai-support/ai-powered-chart.adoc Co-authored-by: Jouni Koivuviita --- articles/flow/ai-support/ai-powered-chart.adoc | 1 - 1 file changed, 1 deletion(-) diff --git a/articles/flow/ai-support/ai-powered-chart.adoc b/articles/flow/ai-support/ai-powered-chart.adoc index 3eb44863d8..eec9012a81 100644 --- a/articles/flow/ai-support/ai-powered-chart.adoc +++ b/articles/flow/ai-support/ai-powered-chart.adoc @@ -8,7 +8,6 @@ order: 70 = [since:com.vaadin:vaadin@V25.2]#AI-Powered Chart# -ifdef::flow[] [classname]`ChartAIController` creates and updates a [classname]`Chart` visualization based on natural-language requests. Given a <>, the controller exposes tools that let the LLM inspect the database schema, write SQL queries for one or more series, and update the Highcharts configuration independently of the data. From 4059f84f7cd5a82ea61cfa99277359f90687f9ad Mon Sep 17 00:00:00 2001 From: Ugur Saglam <106508695+ugur-vaadin@users.noreply.github.com> Date: Wed, 22 Apr 2026 14:44:00 +0300 Subject: [PATCH 03/24] Update articles/flow/ai-support/ai-powered-chart.adoc Co-authored-by: Jouni Koivuviita --- articles/flow/ai-support/ai-powered-chart.adoc | 1 - 1 file changed, 1 deletion(-) diff --git a/articles/flow/ai-support/ai-powered-chart.adoc b/articles/flow/ai-support/ai-powered-chart.adoc index eec9012a81..abc7c537a5 100644 --- a/articles/flow/ai-support/ai-powered-chart.adoc +++ b/articles/flow/ai-support/ai-powered-chart.adoc @@ -136,4 +136,3 @@ orchestrator.reconnect(provider) [classname]`ChartAIController` manages a single chart. To expose several charts to the same orchestrator -- for example in a dashboard -- implement a custom [classname]`AIController` that builds its tool list from [classname]`ChartAITools.createAll()` with a [classname]`ChartAITools.Callbacks` implementation that returns a stable ID per chart. -endif::flow[] From ee50e9a5d9293fb5dfc7ad0d78fe02606101c9ac Mon Sep 17 00:00:00 2001 From: Ugur Saglam <106508695+ugur-vaadin@users.noreply.github.com> Date: Wed, 22 Apr 2026 14:44:08 +0300 Subject: [PATCH 04/24] Update articles/flow/ai-support/ai-powered-grid.adoc Co-authored-by: Jouni Koivuviita --- articles/flow/ai-support/ai-powered-grid.adoc | 1 - 1 file changed, 1 deletion(-) diff --git a/articles/flow/ai-support/ai-powered-grid.adoc b/articles/flow/ai-support/ai-powered-grid.adoc index 5856e9571d..7c75669a86 100644 --- a/articles/flow/ai-support/ai-powered-grid.adoc +++ b/articles/flow/ai-support/ai-powered-grid.adoc @@ -8,7 +8,6 @@ order: 60 = [since:com.vaadin:vaadin@V25.2]#AI-Powered Grid# -ifdef::flow[] [classname]`GridAIController` populates a [classname]`Grid` with data from the application database based on natural-language requests. Given a <>, the controller exposes three tools to the LLM: From 4b8df58ae7f962430a9611008a571ed9dbe4d67b Mon Sep 17 00:00:00 2001 From: Ugur Saglam <106508695+ugur-vaadin@users.noreply.github.com> Date: Wed, 22 Apr 2026 14:44:14 +0300 Subject: [PATCH 05/24] Update articles/flow/ai-support/controllers.adoc Co-authored-by: Jouni Koivuviita --- articles/flow/ai-support/controllers.adoc | 1 - 1 file changed, 1 deletion(-) diff --git a/articles/flow/ai-support/controllers.adoc b/articles/flow/ai-support/controllers.adoc index 1971c03216..0f80a74e7b 100644 --- a/articles/flow/ai-support/controllers.adoc +++ b/articles/flow/ai-support/controllers.adoc @@ -8,7 +8,6 @@ order: 45 = [since:com.vaadin:vaadin@V25.2]#Controllers# -ifdef::flow[] Controllers extend the orchestrator with domain-specific tools and lifecycle hooks. Objects registered via [methodname]`withTools()` rely on vendor-specific annotations such as LangChain4j's or Spring AI's [annotationname]`@Tool`. A controller, by contrast, contributes tools through the framework-agnostic [classname]`AIController` interface. The orchestrator collects the controller's tools before every LLM request and notifies the controller once the request cycle completes. From 9fe413c2cc209fd5e84f7deac1eaa14c90bc7003 Mon Sep 17 00:00:00 2001 From: Ugur Saglam <106508695+ugur-vaadin@users.noreply.github.com> Date: Wed, 22 Apr 2026 14:44:21 +0300 Subject: [PATCH 06/24] Update articles/flow/ai-support/ai-powered-grid.adoc Co-authored-by: Jouni Koivuviita --- articles/flow/ai-support/ai-powered-grid.adoc | 1 - 1 file changed, 1 deletion(-) diff --git a/articles/flow/ai-support/ai-powered-grid.adoc b/articles/flow/ai-support/ai-powered-grid.adoc index 7c75669a86..4d698f6be4 100644 --- a/articles/flow/ai-support/ai-powered-grid.adoc +++ b/articles/flow/ai-support/ai-powered-grid.adoc @@ -101,4 +101,3 @@ orchestrator.reconnect(provider) [classname]`GridAIController` manages a single grid. To coordinate several grids from the same orchestrator, implement a custom [classname]`AIController` that builds its tool list from [classname]`GridAITools.createAll()` with a custom [classname]`GridAITools.Callbacks` implementation. The callbacks -- [methodname]`getGridIds()`, [methodname]`getState(gridId)`, and [methodname]`updateData(gridId, query)` -- let the LLM address each grid by its ID. -endif::flow[] From bf0ba0d93e496b51379ef4f4d388be5f03ac7ee6 Mon Sep 17 00:00:00 2001 From: Ugur Saglam <106508695+ugur-vaadin@users.noreply.github.com> Date: Wed, 22 Apr 2026 14:44:29 +0300 Subject: [PATCH 07/24] Update articles/flow/ai-support/controllers.adoc Co-authored-by: Jouni Koivuviita --- articles/flow/ai-support/controllers.adoc | 1 - 1 file changed, 1 deletion(-) diff --git a/articles/flow/ai-support/controllers.adoc b/articles/flow/ai-support/controllers.adoc index 0f80a74e7b..1f2c5dd8f4 100644 --- a/articles/flow/ai-support/controllers.adoc +++ b/articles/flow/ai-support/controllers.adoc @@ -151,4 +151,3 @@ The LLM writes the SQL that gets executed. Always back a [classname]`DatabasePro [TIP] Return only the tables, columns, and relationships the LLM needs. A smaller, well-described schema produces better queries, uses fewer tokens, and reduces the chance of leaking sensitive columns. -endif::flow[] From dc40dc2ec6b58b9c2f2e9d3f69845d6b89efc858 Mon Sep 17 00:00:00 2001 From: ugur-vaadin Date: Wed, 22 Apr 2026 14:58:08 +0300 Subject: [PATCH 08/24] docs: remove unnecessary lines --- articles/flow/ai-support/index.adoc | 4 ---- articles/flow/ai-support/tool-calling.adoc | 4 ---- 2 files changed, 8 deletions(-) diff --git a/articles/flow/ai-support/index.adoc b/articles/flow/ai-support/index.adoc index 32f1fe0e05..f9fab26541 100644 --- a/articles/flow/ai-support/index.adoc +++ b/articles/flow/ai-support/index.adoc @@ -20,8 +20,6 @@ Vaadin provides a set of UI interfaces and a coordination engine for building AI :feature-flag: com.vaadin.experimental.aiComponents include::{articles}/_preview-banner.adoc[opts=optional] -ifdef::flow[] - == Overview @@ -85,8 +83,6 @@ Synchronous mode does not require push. If push is not available in your environ section_outline::[] -endif::flow[] - == Related Components [cols="1,2"] diff --git a/articles/flow/ai-support/tool-calling.adoc b/articles/flow/ai-support/tool-calling.adoc index 727bdbdcee..61c59d2453 100644 --- a/articles/flow/ai-support/tool-calling.adoc +++ b/articles/flow/ai-support/tool-calling.adoc @@ -8,8 +8,6 @@ order: 40 = Tool Calling & Programmatic Prompts -ifdef::flow[] - == Tool Calling @@ -65,5 +63,3 @@ The orchestrator processes one prompt at a time. If [methodname]`prompt()` is ca .UI Context Required [IMPORTANT] [methodname]`prompt()` requires an active UI context. If called from a background thread or outside a Vaadin request, it throws an [classname]`IllegalStateException`. Always call [methodname]`prompt()` from within a UI event handler or wrap the call in `ui.access()`. - -endif::flow[] From 0b65547729a0f98b1699d68b7d61a34ff1895c31 Mon Sep 17 00:00:00 2001 From: ugur-vaadin Date: Wed, 22 Apr 2026 14:59:42 +0300 Subject: [PATCH 09/24] docs: move tools to end of controller pages --- .../flow/ai-support/ai-powered-chart.adoc | 21 +++++++++++-------- articles/flow/ai-support/ai-powered-grid.adoc | 15 ++++++++----- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/articles/flow/ai-support/ai-powered-chart.adoc b/articles/flow/ai-support/ai-powered-chart.adoc index abc7c537a5..ad18bb5154 100644 --- a/articles/flow/ai-support/ai-powered-chart.adoc +++ b/articles/flow/ai-support/ai-powered-chart.adoc @@ -11,15 +11,7 @@ order: 70 [classname]`ChartAIController` creates and updates a [classname]`Chart` visualization based on natural-language requests. Given a <>, the controller exposes tools that let the LLM inspect the database schema, write SQL queries for one or more series, and update the Highcharts configuration independently of the data. -The controller exposes five tools: - -* `get_database_schema` -- describes the tables, columns, and SQL dialect. -* `get_chart_state` -- returns the current Highcharts configuration and the queries powering the chart. -* `update_chart_data_source` -- validates and queues SQL queries, one per series. -* `update_chart_configuration` -- queues a Highcharts configuration update, such as chart type, axes, tooltip, legend, or per-series styling. -* `get_plot_options_schema` -- returns the JSON schema for a specific chart type's plot options, allowing the LLM to discover available styling properties. - -Data and configuration are kept separate: series data comes from SQL queries, while visual appearance comes from the configuration. Both are queued during the LLM turn and applied atomically in [methodname]`onRequestCompleted()`. +Data and configuration are kept separate: series data comes from SQL queries, while visual appearance comes from the configuration. Both are queued during the LLM turn and applied atomically in [methodname]`onRequestCompleted()`. See <<#available-tools,Available Tools>> for the full list. == Basic Usage @@ -136,3 +128,14 @@ orchestrator.reconnect(provider) [classname]`ChartAIController` manages a single chart. To expose several charts to the same orchestrator -- for example in a dashboard -- implement a custom [classname]`AIController` that builds its tool list from [classname]`ChartAITools.createAll()` with a [classname]`ChartAITools.Callbacks` implementation that returns a stable ID per chart. + +[[available-tools]] +== Available Tools + +[classname]`ChartAIController` exposes five tools to the LLM: + +* `get_database_schema` -- describes the tables, columns, and SQL dialect. +* `get_chart_state` -- returns the current Highcharts configuration and the queries powering the chart. +* `update_chart_data_source` -- validates and queues SQL queries, one per series. +* `update_chart_configuration` -- queues a Highcharts configuration update, such as chart type, axes, tooltip, legend, or per-series styling. +* `get_plot_options_schema` -- returns the JSON schema for a specific chart type's plot options, allowing the LLM to discover available styling properties. diff --git a/articles/flow/ai-support/ai-powered-grid.adoc b/articles/flow/ai-support/ai-powered-grid.adoc index 4d698f6be4..f9eb55f4ed 100644 --- a/articles/flow/ai-support/ai-powered-grid.adoc +++ b/articles/flow/ai-support/ai-powered-grid.adoc @@ -9,11 +9,7 @@ order: 60 = [since:com.vaadin:vaadin@V25.2]#AI-Powered Grid# -[classname]`GridAIController` populates a [classname]`Grid` with data from the application database based on natural-language requests. Given a <>, the controller exposes three tools to the LLM: - -* `get_database_schema` -- describes the tables, columns, and SQL dialect. -* `get_grid_state` -- returns the SQL query currently driving the grid. -* `update_grid_data` -- queues a new SQL query for the grid. +[classname]`GridAIController` populates a [classname]`Grid` with data from the application database based on natural-language requests. Given a <>, the controller exposes tools that let the LLM inspect the database schema and write SQL queries to drive the grid. See <<#available-tools,Available Tools>> for the full list. When an LLM request completes, the queued query is executed and the grid re-renders with dynamically generated columns, type-appropriate renderers, right-aligned numeric columns, lazy loading via SQL `LIMIT`/`OFFSET`, and optional column grouping. @@ -101,3 +97,12 @@ orchestrator.reconnect(provider) [classname]`GridAIController` manages a single grid. To coordinate several grids from the same orchestrator, implement a custom [classname]`AIController` that builds its tool list from [classname]`GridAITools.createAll()` with a custom [classname]`GridAITools.Callbacks` implementation. The callbacks -- [methodname]`getGridIds()`, [methodname]`getState(gridId)`, and [methodname]`updateData(gridId, query)` -- let the LLM address each grid by its ID. + +[[available-tools]] +== Available Tools + +[classname]`GridAIController` exposes three tools to the LLM: + +* `get_database_schema` -- describes the tables, columns, and SQL dialect. +* `get_grid_state` -- returns the SQL query currently driving the grid. +* `update_grid_data` -- queues a new SQL query for the grid. From 229d07ecfdf61879ede9b1961ac0346c2455e536 Mon Sep 17 00:00:00 2001 From: ugur-vaadin Date: Wed, 22 Apr 2026 15:03:35 +0300 Subject: [PATCH 10/24] docs: remove unnecessary restore state from examples --- articles/flow/ai-support/ai-powered-chart.adoc | 5 ----- articles/flow/ai-support/ai-powered-grid.adoc | 5 ----- 2 files changed, 10 deletions(-) diff --git a/articles/flow/ai-support/ai-powered-chart.adoc b/articles/flow/ai-support/ai-powered-chart.adoc index ad18bb5154..d82f028ab7 100644 --- a/articles/flow/ai-support/ai-powered-chart.adoc +++ b/articles/flow/ai-support/ai-powered-chart.adoc @@ -113,11 +113,6 @@ Listeners do not fire when [methodname]`restoreState()` is called. The queries a [source,java] ---- ChartAIController controller = new ChartAIController(chart, databaseProvider); -ChartState saved = sessionStore.load(sessionId); -if (saved != null) { - controller.restoreState(saved); -} - orchestrator.reconnect(provider) .withController(controller) .apply(); diff --git a/articles/flow/ai-support/ai-powered-grid.adoc b/articles/flow/ai-support/ai-powered-grid.adoc index f9eb55f4ed..9eca8eda2c 100644 --- a/articles/flow/ai-support/ai-powered-grid.adoc +++ b/articles/flow/ai-support/ai-powered-grid.adoc @@ -82,11 +82,6 @@ if (saved != null) { [source,java] ---- GridAIController controller = new GridAIController(grid, databaseProvider); -GridState saved = sessionStore.load(sessionId); -if (saved != null) { - controller.restoreState(saved); -} - orchestrator.reconnect(provider) .withController(controller) .apply(); From bea3331555871b909a1505a0da38f8d6ddd7adc2 Mon Sep 17 00:00:00 2001 From: ugur-vaadin Date: Wed, 22 Apr 2026 15:22:35 +0300 Subject: [PATCH 11/24] docs: cleanup chart tools description --- articles/flow/ai-support/ai-powered-chart.adoc | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/articles/flow/ai-support/ai-powered-chart.adoc b/articles/flow/ai-support/ai-powered-chart.adoc index d82f028ab7..68373ef7f9 100644 --- a/articles/flow/ai-support/ai-powered-chart.adoc +++ b/articles/flow/ai-support/ai-powered-chart.adoc @@ -9,10 +9,9 @@ order: 70 = [since:com.vaadin:vaadin@V25.2]#AI-Powered Chart# -[classname]`ChartAIController` creates and updates a [classname]`Chart` visualization based on natural-language requests. Given a <>, the controller exposes tools that let the LLM inspect the database schema, write SQL queries for one or more series, and update the Highcharts configuration independently of the data. - -Data and configuration are kept separate: series data comes from SQL queries, while visual appearance comes from the configuration. Both are queued during the LLM turn and applied atomically in [methodname]`onRequestCompleted()`. See <<#available-tools,Available Tools>> for the full list. +[classname]`ChartAIController` creates and updates a [classname]`Chart` visualization based on natural-language requests. Given a <>, the controller exposes tools that let the LLM inspect the database schema, write SQL queries for one or more series, and update the Highcharts configuration independently of the data. See <<#available-tools,Available Tools>> for the full list. +Data and configuration are kept separate: series data comes from SQL queries, while visual appearance comes from the configuration. Both are queued during the LLM turn and applied atomically in [methodname]`onRequestCompleted()`. == Basic Usage From 99648fc558a65ec67fdf1d1d37df44e1c8f1b3e4 Mon Sep 17 00:00:00 2001 From: ugur-vaadin Date: Wed, 22 Apr 2026 16:36:07 +0300 Subject: [PATCH 12/24] docs: update articles based on changes --- .../flow/ai-support/ai-powered-chart.adoc | 21 ++++++++----- articles/flow/ai-support/ai-powered-grid.adoc | 21 ++++++++----- articles/flow/ai-support/controllers.adoc | 30 +++++++++++++++---- articles/flow/ai-support/index.adoc | 8 +++++ 4 files changed, 58 insertions(+), 22 deletions(-) diff --git a/articles/flow/ai-support/ai-powered-chart.adoc b/articles/flow/ai-support/ai-powered-chart.adoc index 68373ef7f9..21b817d92c 100644 --- a/articles/flow/ai-support/ai-powered-chart.adoc +++ b/articles/flow/ai-support/ai-powered-chart.adoc @@ -25,7 +25,7 @@ MessageInput messageInput = new MessageInput(); DatabaseProvider databaseProvider = new JdbcDatabaseProvider(dataSource); ChartAIController controller = new ChartAIController(chart, databaseProvider); -AIOrchestrator.builder(provider, ChartAIController.getSystemPrompt()) +AIOrchestrator.builder(provider, systemPrompt) .withInput(messageInput) .withController(controller) .build(); @@ -40,9 +40,13 @@ Example prompts: * "Compare 2025 and 2026 quarterly sales side-by-side with a legend." * "Turn this into an area chart and add a 'Revenue (USD)' y-axis title." -.Recommended System Prompt +.Built-In Workflow Instructions [TIP] -[methodname]`ChartAIController.getSystemPrompt()` returns a recommended system prompt that instructs the LLM to inspect state before making changes and to keep data and configuration separate. +The controller registers a `get_chart_instructions` tool whose description carries the recommended workflow -- inspect state first, keep data and configuration separate, match query aliases to the chart type. Because LLMs read tool descriptions as part of the tool manifest, the instructions are effectively always in front of the model without needing to be concatenated into the system prompt. You can focus your own system prompt on application-specific behavior such as tone, persona, or business rules. + +.Provider Compatibility +[NOTE] +The chart tools use optional properties in their JSON Schema definitions, which is incompatible with OpenAI's strict tool-calling mode. Strict mode is off by default in both LangChain4j and Spring AI; only users who explicitly opt in (for example by calling `strictTools(true)` on LangChain4j's `OpenAiStreamingChatModel` builder) are affected. == Data Conventions @@ -107,7 +111,7 @@ Listeners do not fire when [methodname]`restoreState()` is called. The queries a == Reconnecting After Deserialization -[classname]`ChartAIController` is not serializable. After session restore, create a new controller and pass it to the orchestrator's reconnector together with the new provider: +[classname]`ChartAIController` is not serializable. After session restore, create a new controller, wire it into the orchestrator's reconnector together with the new provider, and optionally re-apply the saved state: [source,java] ---- @@ -126,10 +130,11 @@ orchestrator.reconnect(provider) [[available-tools]] == Available Tools -[classname]`ChartAIController` exposes five tools to the LLM: +[classname]`ChartAIController` registers six tools on the orchestrator: +* `get_chart_instructions` -- returns the recommended workflow for chart requests. The tool's description carries the same text, so the LLM already sees it as part of the tool manifest and rarely needs to call it explicitly. * `get_database_schema` -- describes the tables, columns, and SQL dialect. * `get_chart_state` -- returns the current Highcharts configuration and the queries powering the chart. -* `update_chart_data_source` -- validates and queues SQL queries, one per series. -* `update_chart_configuration` -- queues a Highcharts configuration update, such as chart type, axes, tooltip, legend, or per-series styling. -* `get_plot_options_schema` -- returns the JSON schema for a specific chart type's plot options, allowing the LLM to discover available styling properties. +* `update_chart_data_source` -- validates and queues SQL queries, one per series. Queries are executed eagerly to catch SQL errors before the turn ends. +* `update_chart_configuration` -- queues a Highcharts configuration update, such as chart type, axes, tooltip, legend, or per-series styling. The JSON is parsed eagerly to catch configuration errors. +* `get_plot_options_schema` -- returns the JSON schema for a specific chart type's plot options, allowing the LLM to discover available styling properties when mixing chart types in a single configuration. diff --git a/articles/flow/ai-support/ai-powered-grid.adoc b/articles/flow/ai-support/ai-powered-grid.adoc index 9eca8eda2c..a39e33dbcd 100644 --- a/articles/flow/ai-support/ai-powered-grid.adoc +++ b/articles/flow/ai-support/ai-powered-grid.adoc @@ -16,17 +16,17 @@ When an LLM request completes, the queued query is executed and the grid re-rend == Basic Usage -Create a grid typed as `Map`, construct a controller with the grid and a [classname]`DatabaseProvider`, and attach it to an orchestrator: +Create a grid typed as [classname]`Grid`, construct a controller with the grid and a [classname]`DatabaseProvider`, and attach it to an orchestrator: [source,java] ---- -Grid> grid = new Grid<>(); +Grid grid = new Grid<>(); MessageInput messageInput = new MessageInput(); DatabaseProvider databaseProvider = new JdbcDatabaseProvider(dataSource); GridAIController controller = new GridAIController(grid, databaseProvider); -AIOrchestrator.builder(provider, GridAIController.getSystemPrompt()) +AIOrchestrator.builder(provider, systemPrompt) .withInput(messageInput) .withController(controller) .build(); @@ -40,9 +40,13 @@ A message list is not required -- the grid itself is the output surface. Example * "List the top 10 highest-paid employees with name, salary, and hire date." * "Show product name and category grouped under Product, and monthly revenue." -.Recommended System Prompt +.Grid Item Type +[NOTE] +[classname]`AIDataRow` is a framework-owned row type. Instances are created internally when query results are rendered. Application code should not construct or inspect them directly; the type parameter makes the grid's role explicit and signals that row data is managed by the controller. + +.Built-In Workflow Instructions [TIP] -[methodname]`GridAIController.getSystemPrompt()` returns a recommended system prompt that teaches the LLM how to use the controller's tools. Concatenate it with your own instructions if you need additional behavior. +The controller registers a `get_grid_instructions` tool whose description contains the recommended workflow for the LLM. Because LLMs read tool descriptions before deciding which tool to call, these instructions are effectively always available without needing to be concatenated into the system prompt. You can focus your own system prompt on application-specific behavior such as tone, persona, or business rules. == SQL Conventions @@ -77,7 +81,7 @@ if (saved != null) { == Reconnecting After Deserialization -[classname]`GridAIController` is not serializable. After session restore, create a new controller and pass it to the orchestrator's reconnector together with the new provider: +[classname]`GridAIController` is not serializable. After session restore, create a new controller, wire it into the orchestrator's reconnector together with the new provider, and optionally re-apply the saved state: [source,java] ---- @@ -96,8 +100,9 @@ orchestrator.reconnect(provider) [[available-tools]] == Available Tools -[classname]`GridAIController` exposes three tools to the LLM: +[classname]`GridAIController` registers four tools on the orchestrator: +* `get_grid_instructions` -- returns the recommended workflow for grid-related requests. The tool's description carries the same text, so the LLM already sees it as part of the tool manifest and rarely needs to call it explicitly. * `get_database_schema` -- describes the tables, columns, and SQL dialect. * `get_grid_state` -- returns the SQL query currently driving the grid. -* `update_grid_data` -- queues a new SQL query for the grid. +* `update_grid_data` -- queues a new SQL query for the grid. The query is validated eagerly with a lightweight probe before being accepted; invalid queries are returned to the LLM as an error. diff --git a/articles/flow/ai-support/controllers.adoc b/articles/flow/ai-support/controllers.adoc index 1f2c5dd8f4..017e7e4dab 100644 --- a/articles/flow/ai-support/controllers.adoc +++ b/articles/flow/ai-support/controllers.adoc @@ -26,7 +26,7 @@ Pass a controller to the orchestrator's builder with [methodname]`withController [source,java] ---- var orchestrator = AIOrchestrator - .builder(provider, GridAIController.getSystemPrompt()) + .builder(provider, systemPrompt) .withMessageList(messageList) .withInput(messageInput) .withController(controller) @@ -38,11 +38,18 @@ Only one controller can be attached per orchestrator. Controllers and tool objec == The [classname]`AIController` Interface -[classname]`AIController` defines two methods, both with default implementations: +[classname]`AIController` defines two methods: -* [methodname]`getTools()` -- returns the list of [classname]`LLMProvider.ToolSpec` instances the controller contributes to each LLM request. Defaults to an empty list. Tools are collected before every request, so a controller can vary its tool set based on current state. +* [methodname]`getTools()` -- returns the list of [classname]`LLMProvider.ToolSpec` instances the controller contributes to each LLM request. Tools are collected before every request, so a controller can vary its tool set based on current state. * [methodname]`onRequestCompleted()` -- runs after all tool calls for a user request have finished and the LLM has produced its final response. Controllers use this hook to apply deferred state changes, avoiding partial state and multiple redraws during a multi-tool turn. +Each tool is an implementation of [classname]`LLMProvider.ToolSpec`, which has four methods: + +* [methodname]`getName()` -- the unique name the LLM uses to invoke the tool. +* [methodname]`getDescription()` -- a human-readable description shown to the LLM. +* [methodname]`getParametersSchema()` -- a JSON Schema string describing the tool's parameters, or `null` for parameterless tools. +* [methodname]`execute(JsonNode arguments)` -- receives the LLM's arguments as a [classname]`JsonNode` (from `tools.jackson.databind`) and returns the tool's result as a string. + A minimal custom controller looks like this: [source,java] @@ -75,15 +82,26 @@ public class WeatherController implements AIController { } @Override - public String execute(String arguments) { - // Parse arguments as JSON and call your service - return weatherService.lookup(arguments); + public String execute(JsonNode arguments) { + String city = arguments.get("city").asString(); + return weatherService.lookup(city); } }); } + + @Override + public void onRequestCompleted() { + // Apply any deferred state changes here. + } } ---- +Tool names must match the pattern `^[a-zA-Z0-9_-]{1,64}$`, as required by popular LLM APIs. Names are validated at build time; invalid names cause an [classname]`IllegalArgumentException`. Use a namespaced name such as `"MyController_getWeather"` to avoid collisions with tools from other controllers. + +.Generating JSON Schemas +[TIP] +Hand-writing JSON schemas is fine for small tools but becomes error-prone as they grow. For larger tools, use a schema generator to catch typos and structural mistakes at compile time. When using [classname]`SpringAILLMProvider`, Spring AI's `JsonSchemaGenerator` is already on the classpath -- pair it with a record annotated with `@JsonPropertyDescription` and parse the arguments via Jackson. Whichever approach you pick, verify the output stays inside the portable JSON Schema subset (string, integer, number, boolean, array, object, plus `anyOf` and `enum`); some generators emit keywords such as `format`, `pattern`, or `$ref` that not every LLM provider accepts. + .Controller Serialization [NOTE] Controllers are not serialized with the orchestrator. After session restore, pass the controller to [methodname]`reconnect(provider).withController(controller).apply()` -- see <>. diff --git a/articles/flow/ai-support/index.adoc b/articles/flow/ai-support/index.adoc index f9fab26541..e535e6a2e1 100644 --- a/articles/flow/ai-support/index.adoc +++ b/articles/flow/ai-support/index.adoc @@ -61,6 +61,14 @@ When the user submits a message through the Message Input, the orchestrator auto By default, user messages are labeled "You" and assistant messages are labeled "Assistant". Use [methodname]`withUserName()` and [methodname]`withAssistantName()` on the builder to customize these display names. +.Always Provide a System Prompt +[TIP] +A system prompt is strongly recommended. Without one, the LLM has no guidance beyond your tool descriptions and may behave inconsistently from turn to turn. Use it to set the assistant's role, tone, constraints, and any domain-specific rules -- for example, "You are a helpful customer-support assistant for an online bookstore. Keep answers short and cite order numbers verbatim." If the system prompt is `null`, a warning is logged at build time. + +.One Orchestrator per Instance +[NOTE] +Each [classname]`LLMProvider`, [classname]`MessageList` (or [classname]`AIMessageList`), [classname]`MessageInput` (or [classname]`AIInput`), file receiver, and [classname]`AIController` may be passed to only one [classname]`AIOrchestrator`. Attempting to share an instance across two orchestrators throws [classname]`IllegalStateException` at build time. When building multi-view or dashboard applications, create a separate set of components -- including a dedicated provider -- for each orchestrator. + == Server Push From a8064c4d4a4416dd5020c4391437123347683a18 Mon Sep 17 00:00:00 2001 From: ugur-vaadin Date: Wed, 22 Apr 2026 17:01:47 +0300 Subject: [PATCH 13/24] docs: polish based on updates --- .../flow/ai-support/ai-powered-chart.adoc | 60 ++++---- articles/flow/ai-support/ai-powered-grid.adoc | 12 +- articles/flow/ai-support/controllers.adoc | 136 +++++++++--------- .../flow/ai-support/conversation-history.adoc | 2 +- articles/flow/ai-support/index.adoc | 4 +- 5 files changed, 108 insertions(+), 106 deletions(-) diff --git a/articles/flow/ai-support/ai-powered-chart.adoc b/articles/flow/ai-support/ai-powered-chart.adoc index 21b817d92c..4964f5d62b 100644 --- a/articles/flow/ai-support/ai-powered-chart.adoc +++ b/articles/flow/ai-support/ai-powered-chart.adoc @@ -49,11 +49,13 @@ The controller registers a `get_chart_instructions` tool whose description carri The chart tools use optional properties in their JSON Schema definitions, which is incompatible with OpenAI's strict tool-calling mode. Strict mode is off by default in both LangChain4j and Spring AI; only users who explicitly opt in (for example by calling `strictTools(true)` on LangChain4j's `OpenAiStreamingChatModel` builder) are affected. +[[data-conventions]] == Data Conventions -The default converter recognizes specific column aliases when mapping SQL results to Highcharts series. All reserved names are prefixed with `_` to avoid colliding with real data columns. Common patterns include: +For the common chart types -- line, column, bar, area, pie, and spline -- no column conventions are needed. Pick a category column and a value column, and the converter does the rest. + +The reserved column aliases below unlock specialized chart types. All reserved names are prefixed with `_` to avoid colliding with real data columns. The LLM discovers them from the `update_chart_data_source` tool description and applies them as each chart type requires: -* Basic charts -- line, column, bar, area, pie, and spline charts take two columns: category and value. No special aliases are required. * Multi-series -- add a `_series` column to group rows into separate named series. * Scatter and bubble -- `_x`, `_y`, and optionally `_z` for bubble size. * Range (`arearange`, `columnrange`) -- `_low`, `_high`, and optionally `_x`. @@ -64,32 +66,6 @@ The default converter recognizes specific column aliases when mapping SQL result Any pattern that produces a [classname]`DataSeriesItem` also accepts an optional `_color` column for per-point coloring. The full list of aliases is defined in the [classname]`ColumnNames` class. -== Custom Data Conversion - -The built-in [classname]`DefaultDataConverter` handles the common patterns. To take full control -- for example, to post-process rows, merge data from multiple queries into a single series, or apply custom formatting -- implement [classname]`DataConverter` and register it with [methodname]`setDataConverter()`: - -[source,java] ----- -public class CurrencyDataConverter implements DataConverter { - - @Override - public List convertToSeries(List> data) { - DataSeries series = new DataSeries(); - for (Map row : data) { - String name = (String) row.get("category"); - Number value = (Number) row.get("value"); - series.add(new DataSeriesItem(name, roundToCents(value))); - } - return List.of(series); - } -} - -controller.setDataConverter(new CurrencyDataConverter()); ----- - -The converter receives the raw rows from [methodname]`DatabaseProvider.executeQuery()` and returns one or more [classname]`Series` instances. - - == Persisting Chart State [classname]`ChartState` captures both the SQL queries and the Highcharts configuration. Register a state change listener to persist the state automatically after each successful AI request: @@ -124,7 +100,33 @@ orchestrator.reconnect(provider) == Multiple Charts -[classname]`ChartAIController` manages a single chart. To expose several charts to the same orchestrator -- for example in a dashboard -- implement a custom [classname]`AIController` that builds its tool list from [classname]`ChartAITools.createAll()` with a [classname]`ChartAITools.Callbacks` implementation that returns a stable ID per chart. +[classname]`ChartAIController` manages a single chart. To expose several charts to the same orchestrator -- for example in a dashboard -- build a custom [classname]`AIController` on top of [classname]`ChartAITools.createAll()` with your own [classname]`ChartAITools.Callbacks` implementation that returns a stable ID per chart. See the [classname]`ChartAITools` Javadoc for the callback contract. + + +== Custom Data Conversion + +The built-in [classname]`DefaultDataConverter` handles the common patterns described in <<#data-conventions,Data Conventions>>. To take full control -- for example, to post-process rows, merge data from multiple queries into a single series, or apply custom formatting -- implement [classname]`DataConverter` and register it with [methodname]`setDataConverter()`: + +[source,java] +---- +public class CurrencyDataConverter implements DataConverter { + + @Override + public List convertToSeries(List> data) { + DataSeries series = new DataSeries(); + for (Map row : data) { + String name = (String) row.get("category"); + Number value = (Number) row.get("value"); + series.add(new DataSeriesItem(name, roundToCents(value))); + } + return List.of(series); + } +} + +controller.setDataConverter(new CurrencyDataConverter()); +---- + +The converter receives the raw rows from [methodname]`DatabaseProvider.executeQuery()` and returns one or more [classname]`Series` instances. [[available-tools]] diff --git a/articles/flow/ai-support/ai-powered-grid.adoc b/articles/flow/ai-support/ai-powered-grid.adoc index a39e33dbcd..63c6145786 100644 --- a/articles/flow/ai-support/ai-powered-grid.adoc +++ b/articles/flow/ai-support/ai-powered-grid.adoc @@ -16,7 +16,7 @@ When an LLM request completes, the queued query is executed and the grid re-rend == Basic Usage -Create a grid typed as [classname]`Grid`, construct a controller with the grid and a [classname]`DatabaseProvider`, and attach it to an orchestrator: +Create a grid typed as [classname]`Grid`, construct a controller with the grid and a [classname]`DatabaseProvider`, and attach it to an orchestrator. [classname]`AIDataRow` is a framework-owned row type -- you never construct instances yourself. [source,java] ---- @@ -36,13 +36,9 @@ add(messageInput, grid); A message list is not required -- the grid itself is the output surface. Example prompts: -* "Show me all employees in the Sales department." -* "List the top 10 highest-paid employees with name, salary, and hire date." * "Show product name and category grouped under Product, and monthly revenue." - -.Grid Item Type -[NOTE] -[classname]`AIDataRow` is a framework-owned row type. Instances are created internally when query results are rendered. Application code should not construct or inspect them directly; the type parameter makes the grid's role explicit and signals that row data is managed by the controller. +* "List the top 10 highest-paid employees with name, salary, and hire date." +* "Show me all employees in the Sales department." .Built-In Workflow Instructions [TIP] @@ -94,7 +90,7 @@ orchestrator.reconnect(provider) == Multiple Grids -[classname]`GridAIController` manages a single grid. To coordinate several grids from the same orchestrator, implement a custom [classname]`AIController` that builds its tool list from [classname]`GridAITools.createAll()` with a custom [classname]`GridAITools.Callbacks` implementation. The callbacks -- [methodname]`getGridIds()`, [methodname]`getState(gridId)`, and [methodname]`updateData(gridId, query)` -- let the LLM address each grid by its ID. +[classname]`GridAIController` manages a single grid. To coordinate several grids from the same orchestrator -- for example a dashboard where the LLM routes each request to the appropriate grid -- build a custom [classname]`AIController` on top of [classname]`GridAITools.createAll()` with your own [classname]`GridAITools.Callbacks` implementation. See the [classname]`GridAITools` Javadoc for the callback contract. [[available-tools]] diff --git a/articles/flow/ai-support/controllers.adoc b/articles/flow/ai-support/controllers.adoc index 017e7e4dab..51cd0643a9 100644 --- a/articles/flow/ai-support/controllers.adoc +++ b/articles/flow/ai-support/controllers.adoc @@ -9,7 +9,9 @@ order: 45 = [since:com.vaadin:vaadin@V25.2]#Controllers# -Controllers extend the orchestrator with domain-specific tools and lifecycle hooks. Objects registered via [methodname]`withTools()` rely on vendor-specific annotations such as LangChain4j's or Spring AI's [annotationname]`@Tool`. A controller, by contrast, contributes tools through the framework-agnostic [classname]`AIController` interface. The orchestrator collects the controller's tools before every LLM request and notifies the controller once the request cycle completes. +Controllers expose your application's capabilities to the LLM as callable tools -- a customer database, an inventory lookup, a weather API, a form-filling routine -- and give you a lifecycle hook that fires after the LLM finishes each turn. Use a controller when you want reusable tools that don't depend on a specific AI framework's annotations, when you need to defer UI updates until after a multi-step AI response, or when you want to package an AI capability once and share it across applications. + +If you've registered tool objects via [methodname]`withTools()` elsewhere, controllers are the framework-agnostic equivalent: the same tool calling, but defined through the [classname]`AIController` interface instead of LangChain4j's or Spring AI's [annotationname]`@Tool` annotations. Vaadin provides two built-in controllers: @@ -33,10 +35,75 @@ var orchestrator = AIOrchestrator .build(); ---- -Only one controller can be attached per orchestrator. Controllers and tool objects registered via [methodname]`withTools()` can be combined -- the orchestrator merges their tool lists before each request. +Only one controller can be attached per orchestrator, because the orchestrator needs a single owner for the [methodname]`onRequestCompleted()` lifecycle hook. If you need tools from several sources, compose them into one controller that delegates, or combine a controller with tool objects registered via [methodname]`withTools()` -- the orchestrator merges their tool lists before each request. + + +[[database-provider]] +== Database Provider + +[classname]`DatabaseProvider` is the bridge between the LLM and an application database. The built-in grid and chart controllers use it to let the LLM discover the schema and execute read-only SQL queries on demand. Any controller that needs database access can reuse [classname]`DatabaseProviderAITools` to register the shared `get_database_schema` tool. + +The interface defines two methods: + +* [methodname]`getSchema()` -- returns a plain-text description of the tables, columns, and SQL dialect. The LLM uses this to write valid queries. +* [methodname]`executeQuery(String sql)` -- executes a SQL query and returns the rows as a list of column-name-to-value maps. + +The following example is a straightforward JDBC implementation: + +[source,java] +---- +public class JdbcDatabaseProvider implements DatabaseProvider { + + private final DataSource readOnlyDataSource; + + public JdbcDatabaseProvider(DataSource readOnlyDataSource) { + this.readOnlyDataSource = readOnlyDataSource; + } + + @Override + public String getSchema() { + return """ + Tables: + employees(id INT, name VARCHAR, department VARCHAR, salary NUMERIC, hired_on DATE) + departments(id INT, name VARCHAR) + Dialect: PostgreSQL. + """; + } + + @Override + public List> executeQuery(String sql) { + try (var conn = readOnlyDataSource.getConnection(); + var stmt = conn.prepareStatement(sql); + var rs = stmt.executeQuery()) { + var meta = rs.getMetaData(); + var rows = new ArrayList>(); + while (rs.next()) { + var row = new LinkedHashMap(); + for (int i = 1; i <= meta.getColumnCount(); i++) { + row.put(meta.getColumnLabel(i), rs.getObject(i)); + } + rows.add(row); + } + return rows; + } catch (SQLException e) { + throw new IllegalArgumentException("Query failed: " + e.getMessage(), e); + } + } +} +---- + +.Read-Only Database Access +[IMPORTANT] +The LLM writes the SQL that gets executed. Always back a [classname]`DatabaseProvider` implementation with a database account that has read-only access to the tables and views you intend to expose. This prevents the LLM from modifying or deleting data and limits the impact of a prompt-injection attempt that tries to trick the LLM into running destructive statements. + +.Schema Scope +[TIP] +Return only the tables, columns, and relationships the LLM needs. A smaller, well-described schema produces better queries, uses fewer tokens, and reduces the chance of leaking sensitive columns. + +== Building a Custom Controller -== The [classname]`AIController` Interface +The built-in controllers cover grid and chart data exploration. To expose your own capabilities to the LLM, implement [classname]`AIController` directly. [classname]`AIController` defines two methods: @@ -106,66 +173,3 @@ Hand-writing JSON schemas is fine for small tools but becomes error-prone as the [NOTE] Controllers are not serialized with the orchestrator. After session restore, pass the controller to [methodname]`reconnect(provider).withController(controller).apply()` -- see <>. - -[[database-provider]] -== Database Provider - -[classname]`DatabaseProvider` is the bridge between the LLM and an application database. The built-in grid and chart controllers use it to let the LLM discover the schema and execute read-only SQL queries on demand. Any controller that needs database access can reuse [classname]`DatabaseProviderAITools` to register the shared `get_database_schema` tool. - -The interface defines two methods: - -* [methodname]`getSchema()` -- returns a plain-text description of the tables, columns, and SQL dialect. The LLM uses this to write valid queries. -* [methodname]`executeQuery(String sql)` -- executes a SQL query and returns the rows as a list of column-name-to-value maps. - -The following example is a straightforward JDBC implementation: - -[source,java] ----- -public class JdbcDatabaseProvider implements DatabaseProvider { - - private final DataSource readOnlyDataSource; - - public JdbcDatabaseProvider(DataSource readOnlyDataSource) { - this.readOnlyDataSource = readOnlyDataSource; - } - - @Override - public String getSchema() { - return """ - Tables: - employees(id INT, name VARCHAR, department VARCHAR, salary NUMERIC, hired_on DATE) - departments(id INT, name VARCHAR) - Dialect: PostgreSQL. - """; - } - - @Override - public List> executeQuery(String sql) { - try (var conn = readOnlyDataSource.getConnection(); - var stmt = conn.prepareStatement(sql); - var rs = stmt.executeQuery()) { - var meta = rs.getMetaData(); - var rows = new ArrayList>(); - while (rs.next()) { - var row = new LinkedHashMap(); - for (int i = 1; i <= meta.getColumnCount(); i++) { - row.put(meta.getColumnLabel(i), rs.getObject(i)); - } - rows.add(row); - } - return rows; - } catch (SQLException e) { - throw new IllegalArgumentException("Query failed: " + e.getMessage(), e); - } - } -} ----- - -.Read-Only Database Access -[IMPORTANT] -The LLM writes the SQL that gets executed. Always back a [classname]`DatabaseProvider` implementation with a database account that has read-only access to the tables and views you intend to expose. This prevents the LLM from modifying or deleting data and limits the impact of a prompt-injection attempt that tries to trick the LLM into running destructive statements. - -.Schema Scope -[TIP] -Return only the tables, columns, and relationships the LLM needs. A smaller, well-described schema produces better queries, uses fewer tokens, and reduces the chance of leaking sensitive columns. - diff --git a/articles/flow/ai-support/conversation-history.adoc b/articles/flow/ai-support/conversation-history.adoc index 2b70033c13..68d11bf9fc 100644 --- a/articles/flow/ai-support/conversation-history.adoc +++ b/articles/flow/ai-support/conversation-history.adoc @@ -98,7 +98,7 @@ orchestrator.reconnect(provider) .Controller Reattachment [NOTE] -[classname]`AIController` instances -- including [classname]`GridAIController` and [classname]`ChartAIController` -- are not serialized with the orchestrator. Create a new controller after session restore and pass it to [methodname]`withController()` on the reconnector. See <> for state capture and restoration guidance. +[classname]`AIController` instances -- including [classname]`GridAIController` and [classname]`ChartAIController` -- are not serialized with the orchestrator. Create a new controller after session restore and pass it to [methodname]`withController()` on the reconnector. The built-in controllers each have their own state capture and restoration API; see <> and <> for the specifics. If [methodname]`prompt()` is called before reconnecting, it throws an [classname]`IllegalStateException`. diff --git a/articles/flow/ai-support/index.adoc b/articles/flow/ai-support/index.adoc index e535e6a2e1..d9dbd1eb32 100644 --- a/articles/flow/ai-support/index.adoc +++ b/articles/flow/ai-support/index.adoc @@ -26,7 +26,7 @@ include::{articles}/_preview-banner.adoc[opts=optional] The AI support module consists of four parts: * **Orchestrator** -- [classname]`AIOrchestrator` is a non-visual coordination engine that connects UI components to an LLM provider. It has no DOM element and should not be added to a layout. -* **Provider** -- [classname]`LLMProvider` is the interface for communicating with LLM frameworks. Create a provider by instantiating [classname]`SpringAILLMProvider` or [classname]`LangChain4JLLMProvider` directly. You can also implement this interface to connect to any other LLM framework. +* **Provider** -- plugs the orchestrator into any LLM framework. Spring AI and LangChain4j are built in via [classname]`SpringAILLMProvider` and [classname]`LangChain4JLLMProvider`; add others by implementing the [classname]`LLMProvider` interface. * **Component interfaces** -- [classname]`AIInput`, [classname]`AIMessageList`, [classname]`AIMessage`, and [classname]`AIFileReceiver` define contracts for UI components that the orchestrator can work with. The builder also accepts standard Vaadin components ([classname]`MessageInput`, [classname]`MessageList`, [classname]`UploadManager`, [classname]`Upload`) directly. * **Controllers** -- [classname]`AIController` is the framework-agnostic interface for contributing tools and lifecycle hooks to the orchestrator. Built-in controllers such as [classname]`GridAIController` and [classname]`ChartAIController` bring AI-powered data exploration to [classname]`Grid` and [classname]`Chart`, backed by a [classname]`DatabaseProvider`. @@ -63,7 +63,7 @@ By default, user messages are labeled "You" and assistant messages are labeled " .Always Provide a System Prompt [TIP] -A system prompt is strongly recommended. Without one, the LLM has no guidance beyond your tool descriptions and may behave inconsistently from turn to turn. Use it to set the assistant's role, tone, constraints, and any domain-specific rules -- for example, "You are a helpful customer-support assistant for an online bookstore. Keep answers short and cite order numbers verbatim." If the system prompt is `null`, a warning is logged at build time. +A system prompt is strongly recommended. Without one, the LLM has no guidance beyond your tool descriptions and may behave inconsistently from turn to turn. Use it to set the assistant's role, tone, constraints, and any domain-specific rules -- for example, "You are a helpful customer-support assistant for an online bookstore. Keep answers short and cite order numbers verbatim." .One Orchestrator per Instance [NOTE] From 36a7c801882a84f5c6a8836fd290ab652ce4c94f Mon Sep 17 00:00:00 2001 From: ugur-vaadin Date: Thu, 23 Apr 2026 17:49:28 +0300 Subject: [PATCH 14/24] docs: fix issues with documentation --- .../flow/ai-support/ai-powered-chart.adoc | 47 +++---------------- articles/flow/ai-support/ai-powered-grid.adoc | 30 ++---------- articles/flow/ai-support/controllers.adoc | 10 ++-- 3 files changed, 18 insertions(+), 69 deletions(-) diff --git a/articles/flow/ai-support/ai-powered-chart.adoc b/articles/flow/ai-support/ai-powered-chart.adoc index 4964f5d62b..ace038e148 100644 --- a/articles/flow/ai-support/ai-powered-chart.adoc +++ b/articles/flow/ai-support/ai-powered-chart.adoc @@ -9,9 +9,9 @@ order: 70 = [since:com.vaadin:vaadin@V25.2]#AI-Powered Chart# -[classname]`ChartAIController` creates and updates a [classname]`Chart` visualization based on natural-language requests. Given a <>, the controller exposes tools that let the LLM inspect the database schema, write SQL queries for one or more series, and update the Highcharts configuration independently of the data. See <<#available-tools,Available Tools>> for the full list. +[classname]`ChartAIController` creates and updates a [classname]`Chart` visualization based on natural-language requests. Backed by a <>, the controller lets the LLM inspect the database schema, write SQL queries for one or more series, and update the Highcharts configuration independently of the data. -Data and configuration are kept separate: series data comes from SQL queries, while visual appearance comes from the configuration. Both are queued during the LLM turn and applied atomically in [methodname]`onRequestCompleted()`. +Data and configuration are kept separate: series data comes from SQL queries, while visual appearance comes from the configuration. Both updates are applied together at the end of the LLM turn, so the user never sees a half-updated chart. == Basic Usage @@ -42,28 +42,11 @@ Example prompts: .Built-In Workflow Instructions [TIP] -The controller registers a `get_chart_instructions` tool whose description carries the recommended workflow -- inspect state first, keep data and configuration separate, match query aliases to the chart type. Because LLMs read tool descriptions as part of the tool manifest, the instructions are effectively always in front of the model without needing to be concatenated into the system prompt. You can focus your own system prompt on application-specific behavior such as tone, persona, or business rules. +The controller already informs the LLM of the workflow it needs. You can focus your own system prompt on application-specific behavior. .Provider Compatibility [NOTE] -The chart tools use optional properties in their JSON Schema definitions, which is incompatible with OpenAI's strict tool-calling mode. Strict mode is off by default in both LangChain4j and Spring AI; only users who explicitly opt in (for example by calling `strictTools(true)` on LangChain4j's `OpenAiStreamingChatModel` builder) are affected. - - -[[data-conventions]] -== Data Conventions - -For the common chart types -- line, column, bar, area, pie, and spline -- no column conventions are needed. Pick a category column and a value column, and the converter does the rest. - -The reserved column aliases below unlock specialized chart types. All reserved names are prefixed with `_` to avoid colliding with real data columns. The LLM discovers them from the `update_chart_data_source` tool description and applies them as each chart type requires: - -* Multi-series -- add a `_series` column to group rows into separate named series. -* Scatter and bubble -- `_x`, `_y`, and optionally `_z` for bubble size. -* Range (`arearange`, `columnrange`) -- `_low`, `_high`, and optionally `_x`. -* OHLC and candlestick -- `_x`, `_open`, `_high`, `_low`, `_close`. -* Heatmap -- `_x`, `_y`, `_value`. -* Gantt, Timeline, Treemap, Organization, Sankey, and Waterfall -- use the dedicated name-based aliases documented in the tool description. - -Any pattern that produces a [classname]`DataSeriesItem` also accepts an optional `_color` column for per-point coloring. The full list of aliases is defined in the [classname]`ColumnNames` class. +[classname]`ChartAIController` does not support OpenAI's strict tool-calling mode. Strict mode is off by default in both LangChain4j and Spring AI; only users who explicitly opt in are affected. == Persisting Chart State @@ -82,7 +65,7 @@ if (saved != null) { } ---- -Listeners do not fire when [methodname]`restoreState()` is called. The queries and configuration are also stored on the [classname]`Chart` component, so they survive session serialization automatically. +Listeners do not fire when [methodname]`restoreState()` is called. The current state is also automatically included in session serialization, so no extra save/restore code is needed for in-session persistence. == Reconnecting After Deserialization @@ -98,14 +81,9 @@ orchestrator.reconnect(provider) ---- -== Multiple Charts - -[classname]`ChartAIController` manages a single chart. To expose several charts to the same orchestrator -- for example in a dashboard -- build a custom [classname]`AIController` on top of [classname]`ChartAITools.createAll()` with your own [classname]`ChartAITools.Callbacks` implementation that returns a stable ID per chart. See the [classname]`ChartAITools` Javadoc for the callback contract. - - == Custom Data Conversion -The built-in [classname]`DefaultDataConverter` handles the common patterns described in <<#data-conventions,Data Conventions>>. To take full control -- for example, to post-process rows, merge data from multiple queries into a single series, or apply custom formatting -- implement [classname]`DataConverter` and register it with [methodname]`setDataConverter()`: +[classname]`DefaultDataConverter` maps SQL result rows to Highcharts series automatically. To take full control -- for example, to post-process rows, merge data from multiple queries into a single series, or apply custom formatting -- implement [classname]`DataConverter` and register it with [methodname]`setDataConverter()`: [source,java] ---- @@ -127,16 +105,3 @@ controller.setDataConverter(new CurrencyDataConverter()); ---- The converter receives the raw rows from [methodname]`DatabaseProvider.executeQuery()` and returns one or more [classname]`Series` instances. - - -[[available-tools]] -== Available Tools - -[classname]`ChartAIController` registers six tools on the orchestrator: - -* `get_chart_instructions` -- returns the recommended workflow for chart requests. The tool's description carries the same text, so the LLM already sees it as part of the tool manifest and rarely needs to call it explicitly. -* `get_database_schema` -- describes the tables, columns, and SQL dialect. -* `get_chart_state` -- returns the current Highcharts configuration and the queries powering the chart. -* `update_chart_data_source` -- validates and queues SQL queries, one per series. Queries are executed eagerly to catch SQL errors before the turn ends. -* `update_chart_configuration` -- queues a Highcharts configuration update, such as chart type, axes, tooltip, legend, or per-series styling. The JSON is parsed eagerly to catch configuration errors. -* `get_plot_options_schema` -- returns the JSON schema for a specific chart type's plot options, allowing the LLM to discover available styling properties when mixing chart types in a single configuration. diff --git a/articles/flow/ai-support/ai-powered-grid.adoc b/articles/flow/ai-support/ai-powered-grid.adoc index 63c6145786..a235812780 100644 --- a/articles/flow/ai-support/ai-powered-grid.adoc +++ b/articles/flow/ai-support/ai-powered-grid.adoc @@ -9,7 +9,7 @@ order: 60 = [since:com.vaadin:vaadin@V25.2]#AI-Powered Grid# -[classname]`GridAIController` populates a [classname]`Grid` with data from the application database based on natural-language requests. Given a <>, the controller exposes tools that let the LLM inspect the database schema and write SQL queries to drive the grid. See <<#available-tools,Available Tools>> for the full list. +[classname]`GridAIController` populates a [classname]`Grid` with data from the application database based on natural-language requests. Backed by a <>, the controller lets the LLM inspect the database schema and run SQL queries that drive the grid. When an LLM request completes, the queued query is executed and the grid re-renders with dynamically generated columns, type-appropriate renderers, right-aligned numeric columns, lazy loading via SQL `LIMIT`/`OFFSET`, and optional column grouping. @@ -42,18 +42,12 @@ A message list is not required -- the grid itself is the output surface. Example .Built-In Workflow Instructions [TIP] -The controller registers a `get_grid_instructions` tool whose description contains the recommended workflow for the LLM. Because LLMs read tool descriptions before deciding which tool to call, these instructions are effectively always available without needing to be concatenated into the system prompt. You can focus your own system prompt on application-specific behavior such as tone, persona, or business rules. +The controller already informs the LLM of the workflow it needs. You can focus your own system prompt on application-specific behavior. -== SQL Conventions +== Query Validation -The tool description instructs the LLM to produce queries the grid can render cleanly: - -* Every column must have a human-readable `AS` alias. `SELECT *` is not allowed. -* The grid handles pagination, so queries must not include `LIMIT` or `OFFSET`. -* Dot-separated aliases -- for example `"Product.Name"` and `"Product.Category"` -- produce grouped column headers. A header row labelled "Product" spans both columns. - -Before queueing an update, the controller executes a lightweight probe (`SELECT * FROM () AS _v LIMIT 1`) to validate the query. If the probe fails, the error is returned to the LLM so that it can correct the query on the next turn. +Before queueing an update, the controller runs a lightweight probe against the database to validate the LLM's query. If the probe fails, the error is returned to the LLM so that it can correct the query on the next turn. Invalid queries never reach the grid. == Persisting Grid State @@ -72,7 +66,7 @@ if (saved != null) { } ---- -[methodname]`addStateChangeListener()` fires only when the grid is updated by the LLM, not when [methodname]`restoreState()` is called. The current query is also stored directly on the [classname]`Grid` component, so it survives session serialization automatically. +[methodname]`addStateChangeListener()` fires only when the grid is updated by the LLM, not when [methodname]`restoreState()` is called. The current state is also automatically included in session serialization, so no extra save/restore code is needed for in-session persistence. == Reconnecting After Deserialization @@ -88,17 +82,3 @@ orchestrator.reconnect(provider) ---- -== Multiple Grids - -[classname]`GridAIController` manages a single grid. To coordinate several grids from the same orchestrator -- for example a dashboard where the LLM routes each request to the appropriate grid -- build a custom [classname]`AIController` on top of [classname]`GridAITools.createAll()` with your own [classname]`GridAITools.Callbacks` implementation. See the [classname]`GridAITools` Javadoc for the callback contract. - - -[[available-tools]] -== Available Tools - -[classname]`GridAIController` registers four tools on the orchestrator: - -* `get_grid_instructions` -- returns the recommended workflow for grid-related requests. The tool's description carries the same text, so the LLM already sees it as part of the tool manifest and rarely needs to call it explicitly. -* `get_database_schema` -- describes the tables, columns, and SQL dialect. -* `get_grid_state` -- returns the SQL query currently driving the grid. -* `update_grid_data` -- queues a new SQL query for the grid. The query is validated eagerly with a lightweight probe before being accepted; invalid queries are returned to the LLM as an error. diff --git a/articles/flow/ai-support/controllers.adoc b/articles/flow/ai-support/controllers.adoc index 51cd0643a9..8824eb28e9 100644 --- a/articles/flow/ai-support/controllers.adoc +++ b/articles/flow/ai-support/controllers.adoc @@ -35,18 +35,18 @@ var orchestrator = AIOrchestrator .build(); ---- -Only one controller can be attached per orchestrator, because the orchestrator needs a single owner for the [methodname]`onRequestCompleted()` lifecycle hook. If you need tools from several sources, compose them into one controller that delegates, or combine a controller with tool objects registered via [methodname]`withTools()` -- the orchestrator merges their tool lists before each request. +Only one controller can be attached per orchestrator. If you need tools from several sources, compose them into one controller that delegates, or combine a controller with tool objects registered via [methodname]`withTools()` -- the orchestrator merges their tool lists before each request. [[database-provider]] == Database Provider -[classname]`DatabaseProvider` is the bridge between the LLM and an application database. The built-in grid and chart controllers use it to let the LLM discover the schema and execute read-only SQL queries on demand. Any controller that needs database access can reuse [classname]`DatabaseProviderAITools` to register the shared `get_database_schema` tool. +[classname]`DatabaseProvider` is the bridge between the LLM and an application database. The built-in grid and chart controllers use it to let the LLM discover the schema and run SQL queries on demand. Critically, the data flow is asymmetric: the LLM sees the schema so it can write valid queries, but the query results are rendered in the [classname]`Grid` or [classname]`Chart` component only -- they are never sent back to the LLM. The interface defines two methods: * [methodname]`getSchema()` -- returns a plain-text description of the tables, columns, and SQL dialect. The LLM uses this to write valid queries. -* [methodname]`executeQuery(String sql)` -- executes a SQL query and returns the rows as a list of column-name-to-value maps. +* [methodname]`executeQuery(String sql)` -- executes a SQL query and returns the rows as a list of column-name-to-value maps. These rows are handed to the grid or chart for rendering; they do not appear in any prompt. The following example is a straightforward JDBC implementation: @@ -96,6 +96,10 @@ public class JdbcDatabaseProvider implements DatabaseProvider { [IMPORTANT] The LLM writes the SQL that gets executed. Always back a [classname]`DatabaseProvider` implementation with a database account that has read-only access to the tables and views you intend to expose. This prevents the LLM from modifying or deleting data and limits the impact of a prompt-injection attempt that tries to trick the LLM into running destructive statements. +.Query Results Stay in the Application +[IMPORTANT] +The LLM receives only the schema, never the query results. Every row returned by [methodname]`executeQuery()` is rendered in the grid or chart component and discarded from the request cycle. This boundary is unconditional: sensitive row values cannot leak into a follow-up prompt, into the conversation history, or to the LLM provider. + .Schema Scope [TIP] Return only the tables, columns, and relationships the LLM needs. A smaller, well-described schema produces better queries, uses fewer tokens, and reduces the chance of leaking sensitive columns. From aa5f71bb4f3be88dc88e75dd454ec729714d5877 Mon Sep 17 00:00:00 2001 From: ugur-vaadin Date: Mon, 27 Apr 2026 17:40:50 +0300 Subject: [PATCH 15/24] docs: update method name --- articles/flow/ai-support/controllers.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/articles/flow/ai-support/controllers.adoc b/articles/flow/ai-support/controllers.adoc index 8824eb28e9..a752ac8c0c 100644 --- a/articles/flow/ai-support/controllers.adoc +++ b/articles/flow/ai-support/controllers.adoc @@ -112,7 +112,7 @@ The built-in controllers cover grid and chart data exploration. To expose your o [classname]`AIController` defines two methods: * [methodname]`getTools()` -- returns the list of [classname]`LLMProvider.ToolSpec` instances the controller contributes to each LLM request. Tools are collected before every request, so a controller can vary its tool set based on current state. -* [methodname]`onRequestCompleted()` -- runs after all tool calls for a user request have finished and the LLM has produced its final response. Controllers use this hook to apply deferred state changes, avoiding partial state and multiple redraws during a multi-tool turn. +* [methodname]`onResponseComplete()` -- runs after all tool calls for a user request have finished and the LLM has produced its final response. Controllers use this hook to apply deferred state changes, avoiding partial state and multiple redraws during a multi-tool turn. Each tool is an implementation of [classname]`LLMProvider.ToolSpec`, which has four methods: @@ -161,7 +161,7 @@ public class WeatherController implements AIController { } @Override - public void onRequestCompleted() { + public void onResponseComplete() { // Apply any deferred state changes here. } } From 426ae89cd184ae27f9046ee037b055323cac4055 Mon Sep 17 00:00:00 2001 From: ugur-vaadin Date: Fri, 15 May 2026 13:00:09 +0300 Subject: [PATCH 16/24] docs: link ai features from related components and quickstart --- .../building-apps/ai/quickstart-guide.adoc | 2 + articles/components/charts/ai-powered.adoc | 37 +++++++++++++++++++ articles/components/grid/index.adoc | 1 + .../flow/ai-support/file-attachments.adoc | 2 +- 4 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 articles/components/charts/ai-powered.adoc diff --git a/articles/building-apps/ai/quickstart-guide.adoc b/articles/building-apps/ai/quickstart-guide.adoc index d415aee849..ad88009eb8 100644 --- a/articles/building-apps/ai/quickstart-guide.adoc +++ b/articles/building-apps/ai/quickstart-guide.adoc @@ -189,6 +189,8 @@ Start the application, open the browser, and try your first prompts. * Add **file attachments** with `UploadManager` via <<{articles}/flow/ai-support/file-attachments#,`withFileReceiver()`>>. * Support **tool calls** via <<{articles}/flow/ai-support/tool-calling#,`withTools()`>>. * **Persist conversation history** via <<{articles}/flow/ai-support/conversation-history#,`ResponseCompleteListener`>>. +* Let users **populate a Grid** from your database in natural language with <<{articles}/flow/ai-support/ai-powered-grid#, AI-Powered Grid>>. +* Let users **build and update Charts** from your database in natural language with <<{articles}/flow/ai-support/ai-powered-chart#, AI-Powered Chart>>. * Log prompts/responses for observability. diff --git a/articles/components/charts/ai-powered.adoc b/articles/components/charts/ai-powered.adoc new file mode 100644 index 0000000000..26d652f421 --- /dev/null +++ b/articles/components/charts/ai-powered.adoc @@ -0,0 +1,37 @@ +--- +title: AI-Powered Chart +page-title: AI-Powered Chart | Vaadin components +description: Let users build and update charts from your application database using natural language. +meta-description: Use ChartAIController to let an LLM create and update Vaadin Charts visualizations from a database via natural-language prompts. +order: 7 +section-nav: badge-flow +--- + + += [since:com.vaadin:vaadin@V25.2]#AI-Powered Chart# [badge-flow]#Flow# + +AI-Powered Chart lets your users build and update Vaadin Charts visualizations by typing in natural language. The [classname]`ChartAIController` from the <<{articles}/flow/ai-support#, AI Support>> module wires an [classname]`AIOrchestrator` to a [classname]`Chart` and a [classname]`DatabaseProvider`, so an LLM can inspect the database schema, write SQL queries, and update the Highcharts configuration on the fly. + +[source,java] +---- +Chart chart = new Chart(); +MessageInput messageInput = new MessageInput(); + +DatabaseProvider databaseProvider = new JdbcDatabaseProvider(dataSource); +ChartAIController controller = new ChartAIController(chart, databaseProvider); + +AIOrchestrator.builder(provider, systemPrompt) + .withInput(messageInput) + .withController(controller) + .build(); + +add(messageInput, chart); +---- + +Example prompts: + +* "Plot monthly revenue for the last year as a column chart." +* "Show revenue by region as a pie chart." +* "Compare 2025 and 2026 quarterly sales side-by-side with a legend." + +For the full guide -- including state persistence, custom data conversion, and combining several charts with the <<{articles}/components/dashboard#, Dashboard>> component -- see <<{articles}/flow/ai-support/ai-powered-chart#, AI-Powered Chart>> in the AI Support section. The same approach is available for tabular data via <<{articles}/flow/ai-support/ai-powered-grid#, AI-Powered Grid>>. diff --git a/articles/components/grid/index.adoc b/articles/components/grid/index.adoc index 6601d8f663..5c04801c34 100644 --- a/articles/components/grid/index.adoc +++ b/articles/components/grid/index.adoc @@ -24,6 +24,7 @@ Some of the more complex topics are described on separate pages: * <> * <> * <> +* <<{articles}/flow/ai-support/ai-powered-grid#, AI-Powered Grid [badge-flow]#Flow#>> -- let users populate the grid from your database via natural language. // Allow "will" in example pass:[] diff --git a/articles/flow/ai-support/file-attachments.adoc b/articles/flow/ai-support/file-attachments.adoc index c422c28c91..a816d6b17c 100644 --- a/articles/flow/ai-support/file-attachments.adoc +++ b/articles/flow/ai-support/file-attachments.adoc @@ -10,7 +10,7 @@ order: 30 ifdef::flow[] -Enable file uploads by passing an [classname]`UploadManager`, [classname]`Upload`, or any custom [classname]`AIFileReceiver` implementation to the builder via [methodname]`withFileReceiver()`. Uploaded files are sent to the LLM as attachments with the next prompt. +Enable file uploads by passing an <<{articles}/components/upload#,[classname]`UploadManager`>>, <<{articles}/components/upload#,[classname]`Upload`>>, or any custom [classname]`AIFileReceiver` implementation to the builder via [methodname]`withFileReceiver()`. Uploaded files are sent to the LLM as attachments with the next prompt. [source,java] ---- From 83ee9260298a79e206b94787555dd3b77836f34a Mon Sep 17 00:00:00 2001 From: ugur-vaadin Date: Fri, 15 May 2026 13:01:16 +0300 Subject: [PATCH 17/24] docs: explain components and ai frameworks --- articles/flow/ai-support/ai-powered-chart.adoc | 2 +- articles/flow/ai-support/ai-powered-grid.adoc | 2 +- articles/flow/ai-support/llm-providers.adoc | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/articles/flow/ai-support/ai-powered-chart.adoc b/articles/flow/ai-support/ai-powered-chart.adoc index ace038e148..e387777dc4 100644 --- a/articles/flow/ai-support/ai-powered-chart.adoc +++ b/articles/flow/ai-support/ai-powered-chart.adoc @@ -9,7 +9,7 @@ order: 70 = [since:com.vaadin:vaadin@V25.2]#AI-Powered Chart# -[classname]`ChartAIController` creates and updates a [classname]`Chart` visualization based on natural-language requests. Backed by a <>, the controller lets the LLM inspect the database schema, write SQL queries for one or more series, and update the Highcharts configuration independently of the data. +[classname]`ChartAIController` creates and updates a <<{articles}/components/charts#,[classname]`Chart`>> (Vaadin's interactive charting component) visualization based on natural-language requests. Backed by a <>, the controller lets the LLM inspect the database schema, write SQL queries for one or more series, and update the Highcharts configuration independently of the data. Data and configuration are kept separate: series data comes from SQL queries, while visual appearance comes from the configuration. Both updates are applied together at the end of the LLM turn, so the user never sees a half-updated chart. diff --git a/articles/flow/ai-support/ai-powered-grid.adoc b/articles/flow/ai-support/ai-powered-grid.adoc index a235812780..91bac562c1 100644 --- a/articles/flow/ai-support/ai-powered-grid.adoc +++ b/articles/flow/ai-support/ai-powered-grid.adoc @@ -9,7 +9,7 @@ order: 60 = [since:com.vaadin:vaadin@V25.2]#AI-Powered Grid# -[classname]`GridAIController` populates a [classname]`Grid` with data from the application database based on natural-language requests. Backed by a <>, the controller lets the LLM inspect the database schema and run SQL queries that drive the grid. +[classname]`GridAIController` populates a <<{articles}/components/grid#,[classname]`Grid`>> (Vaadin's component for displaying tabular data) with data from the application database based on natural-language requests. Backed by a <>, the controller lets the LLM inspect the database schema and run SQL queries that drive the grid. When an LLM request completes, the queued query is executed and the grid re-renders with dynamically generated columns, type-appropriate renderers, right-aligned numeric columns, lazy loading via SQL `LIMIT`/`OFFSET`, and optional column grouping. diff --git a/articles/flow/ai-support/llm-providers.adoc b/articles/flow/ai-support/llm-providers.adoc index a4bbfe1345..3972cabc5d 100644 --- a/articles/flow/ai-support/llm-providers.adoc +++ b/articles/flow/ai-support/llm-providers.adoc @@ -10,7 +10,7 @@ order: 20 ifdef::flow[] -The orchestrator uses the [classname]`LLMProvider` interface to communicate with LLM frameworks. Create a provider by instantiating the appropriate implementation directly. Two implementations are provided: one for Spring AI and one for LangChain4j. +An AI framework -- such as Spring AI or LangChain4j -- is a Java library that handles the protocol details of talking to LLM services like OpenAI or Anthropic. The orchestrator plugs into your chosen framework through the [classname]`LLMProvider` interface. Create a provider by instantiating the appropriate implementation directly. Two implementations are provided: one for Spring AI and one for LangChain4j. .Memory Window Limit [IMPORTANT] From e8836c41eabd6332c6f9382069315992bc1e0dfd Mon Sep 17 00:00:00 2001 From: ugur-vaadin Date: Fri, 15 May 2026 13:03:00 +0300 Subject: [PATCH 18/24] docs: rename provider to llm provider --- articles/flow/ai-support/index.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/articles/flow/ai-support/index.adoc b/articles/flow/ai-support/index.adoc index d9dbd1eb32..887934d351 100644 --- a/articles/flow/ai-support/index.adoc +++ b/articles/flow/ai-support/index.adoc @@ -26,7 +26,7 @@ include::{articles}/_preview-banner.adoc[opts=optional] The AI support module consists of four parts: * **Orchestrator** -- [classname]`AIOrchestrator` is a non-visual coordination engine that connects UI components to an LLM provider. It has no DOM element and should not be added to a layout. -* **Provider** -- plugs the orchestrator into any LLM framework. Spring AI and LangChain4j are built in via [classname]`SpringAILLMProvider` and [classname]`LangChain4JLLMProvider`; add others by implementing the [classname]`LLMProvider` interface. +* **LLM Provider** -- plugs the orchestrator into any LLM framework. Spring AI and LangChain4j are built in via [classname]`SpringAILLMProvider` and [classname]`LangChain4JLLMProvider`; add others by implementing the [classname]`LLMProvider` interface. * **Component interfaces** -- [classname]`AIInput`, [classname]`AIMessageList`, [classname]`AIMessage`, and [classname]`AIFileReceiver` define contracts for UI components that the orchestrator can work with. The builder also accepts standard Vaadin components ([classname]`MessageInput`, [classname]`MessageList`, [classname]`UploadManager`, [classname]`Upload`) directly. * **Controllers** -- [classname]`AIController` is the framework-agnostic interface for contributing tools and lifecycle hooks to the orchestrator. Built-in controllers such as [classname]`GridAIController` and [classname]`ChartAIController` bring AI-powered data exploration to [classname]`Grid` and [classname]`Chart`, backed by a [classname]`DatabaseProvider`. From 329e21457e5711d75afb141741e0a8ff73e71e27 Mon Sep 17 00:00:00 2001 From: ugur-vaadin Date: Fri, 15 May 2026 13:03:27 +0300 Subject: [PATCH 19/24] docs: introduce tool calling for newcomers --- articles/flow/ai-support/tool-calling.adoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/articles/flow/ai-support/tool-calling.adoc b/articles/flow/ai-support/tool-calling.adoc index 61c59d2453..f8e7847c45 100644 --- a/articles/flow/ai-support/tool-calling.adoc +++ b/articles/flow/ai-support/tool-calling.adoc @@ -8,6 +8,8 @@ order: 40 = Tool Calling & Programmatic Prompts +Tools let the LLM call methods in your application during a conversation -- query a database, look up an order, send an email, or any other action you want the assistant to be able to perform. There are two ways to expose tools: registered tool objects, covered on this page, and framework-agnostic <>. + == Tool Calling From 969aa49e69767cae34c1ffa0b554a977a2df71f1 Mon Sep 17 00:00:00 2001 From: ugur-vaadin Date: Fri, 15 May 2026 13:04:45 +0300 Subject: [PATCH 20/24] docs: note schema flexibility on db example --- articles/flow/ai-support/controllers.adoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/articles/flow/ai-support/controllers.adoc b/articles/flow/ai-support/controllers.adoc index a752ac8c0c..6cc9ad22fd 100644 --- a/articles/flow/ai-support/controllers.adoc +++ b/articles/flow/ai-support/controllers.adoc @@ -92,6 +92,8 @@ public class JdbcDatabaseProvider implements DatabaseProvider { } ---- +The example returns a hardcoded string for clarity, but [methodname]`getSchema()` can return whatever helps the LLM. Build the description at runtime from [classname]`java.sql.DatabaseMetaData` if your schema changes often, and add free-form context -- column meanings, business rules, common joins, units, sample values -- in plain English. The LLM treats the entire string as guidance, so anything that improves its queries is fair game. + .Read-Only Database Access [IMPORTANT] The LLM writes the SQL that gets executed. Always back a [classname]`DatabaseProvider` implementation with a database account that has read-only access to the tables and views you intend to expose. This prevents the LLM from modifying or deleting data and limits the impact of a prompt-injection attempt that tries to trick the LLM into running destructive statements. From 382e64f273e1bd5f4850597de5491bc3eeca741f Mon Sep 17 00:00:00 2001 From: ugur-vaadin Date: Fri, 15 May 2026 13:05:35 +0300 Subject: [PATCH 21/24] docs: explain sessionstore placeholder --- articles/flow/ai-support/ai-powered-chart.adoc | 2 ++ articles/flow/ai-support/ai-powered-grid.adoc | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/articles/flow/ai-support/ai-powered-chart.adoc b/articles/flow/ai-support/ai-powered-chart.adoc index e387777dc4..bb1da82524 100644 --- a/articles/flow/ai-support/ai-powered-chart.adoc +++ b/articles/flow/ai-support/ai-powered-chart.adoc @@ -65,6 +65,8 @@ if (saved != null) { } ---- +`sessionStore` here is a placeholder for your own storage -- a database table, a file, a [classname]`VaadinSession` attribute, or whatever fits your application. + Listeners do not fire when [methodname]`restoreState()` is called. The current state is also automatically included in session serialization, so no extra save/restore code is needed for in-session persistence. diff --git a/articles/flow/ai-support/ai-powered-grid.adoc b/articles/flow/ai-support/ai-powered-grid.adoc index 91bac562c1..5e58281783 100644 --- a/articles/flow/ai-support/ai-powered-grid.adoc +++ b/articles/flow/ai-support/ai-powered-grid.adoc @@ -66,6 +66,8 @@ if (saved != null) { } ---- +`sessionStore` here is a placeholder for your own storage -- a database table, a file, a [classname]`VaadinSession` attribute, or whatever fits your application. + [methodname]`addStateChangeListener()` fires only when the grid is updated by the LLM, not when [methodname]`restoreState()` is called. The current state is also automatically included in session serialization, so no extra save/restore code is needed for in-session persistence. @@ -80,5 +82,3 @@ orchestrator.reconnect(provider) .withController(controller) .apply(); ---- - - From e3b8f2896fd791eea74fa3733a7990657491dbc0 Mon Sep 17 00:00:00 2001 From: ugur-vaadin Date: Fri, 15 May 2026 13:06:27 +0300 Subject: [PATCH 22/24] docs: describe combining ai chart with dashboard --- articles/flow/ai-support/ai-powered-chart.adoc | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/articles/flow/ai-support/ai-powered-chart.adoc b/articles/flow/ai-support/ai-powered-chart.adoc index bb1da82524..167e555cb7 100644 --- a/articles/flow/ai-support/ai-powered-chart.adoc +++ b/articles/flow/ai-support/ai-powered-chart.adoc @@ -107,3 +107,8 @@ controller.setDataConverter(new CurrencyDataConverter()); ---- The converter receives the raw rows from [methodname]`DatabaseProvider.executeQuery()` and returns one or more [classname]`Series` instances. + + +== Combining with Dashboard + +[classname]`ChartAIController` manages a single chart, but you can combine several of them with the <<{articles}/components/dashboard#,[classname]`Dashboard`>> component to build a user-arrangeable, persistable, AI-generated dashboard. Each dashboard widget hosts its own [classname]`Chart` and [classname]`ChartAIController` instance; persist each controller's state alongside the dashboard's own layout state to let users return to the same chart set and layout in a later session. From 17c7c378fe9709c8c1abb0726fee1ef4a55f5c31 Mon Sep 17 00:00:00 2001 From: ugur-vaadin Date: Fri, 15 May 2026 13:06:49 +0300 Subject: [PATCH 23/24] docs: simplify basic orchestrator example --- .../vaadin/demo/flow/aicomponents/AIOrchestratorBasic.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/main/java/com/vaadin/demo/flow/aicomponents/AIOrchestratorBasic.java b/src/main/java/com/vaadin/demo/flow/aicomponents/AIOrchestratorBasic.java index 8c84e5dbbe..0e25e62045 100644 --- a/src/main/java/com/vaadin/demo/flow/aicomponents/AIOrchestratorBasic.java +++ b/src/main/java/com/vaadin/demo/flow/aicomponents/AIOrchestratorBasic.java @@ -16,7 +16,7 @@ public AIOrchestratorBasic() { MessageList messageList = new MessageList(); MessageInput messageInput = new MessageInput(); - LLMProvider provider = getLLMProvider(); + LLMProvider provider = new MockLLMProvider(); AIOrchestrator.builder(provider, "You are a helpful assistant.") .withMessageList(messageList).withInput(messageInput).build(); @@ -27,10 +27,6 @@ public AIOrchestratorBasic() { com.vaadin.demo.component.messages.LLMClient.initPolling(messageList); // hidden-source-line } - private LLMProvider getLLMProvider() { - return new MockLLMProvider(); - } - public static class Exporter extends DemoExporter { // hidden-source-line } // hidden-source-line } From 280fbb4c20bad7f69eaa2dd6a0e827def816fb20 Mon Sep 17 00:00:00 2001 From: ugur-vaadin Date: Fri, 15 May 2026 14:29:02 +0300 Subject: [PATCH 24/24] docs: reword to address vale lint warnings --- articles/flow/ai-support/ai-powered-chart.adoc | 4 ++-- articles/flow/ai-support/ai-powered-grid.adoc | 2 +- articles/flow/ai-support/controllers.adoc | 6 +++--- articles/flow/ai-support/tool-calling.adoc | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/articles/flow/ai-support/ai-powered-chart.adoc b/articles/flow/ai-support/ai-powered-chart.adoc index 167e555cb7..72c12b39c9 100644 --- a/articles/flow/ai-support/ai-powered-chart.adoc +++ b/articles/flow/ai-support/ai-powered-chart.adoc @@ -72,7 +72,7 @@ Listeners do not fire when [methodname]`restoreState()` is called. The current s == Reconnecting After Deserialization -[classname]`ChartAIController` is not serializable. After session restore, create a new controller, wire it into the orchestrator's reconnector together with the new provider, and optionally re-apply the saved state: +[classname]`ChartAIController` is not serializable. After session restore, create a new controller, pass it to [methodname]`reconnect()` together with the new provider, and optionally re-apply the saved state: [source,java] ---- @@ -111,4 +111,4 @@ The converter receives the raw rows from [methodname]`DatabaseProvider.executeQu == Combining with Dashboard -[classname]`ChartAIController` manages a single chart, but you can combine several of them with the <<{articles}/components/dashboard#,[classname]`Dashboard`>> component to build a user-arrangeable, persistable, AI-generated dashboard. Each dashboard widget hosts its own [classname]`Chart` and [classname]`ChartAIController` instance; persist each controller's state alongside the dashboard's own layout state to let users return to the same chart set and layout in a later session. +[classname]`ChartAIController` manages a single chart, but you can combine several of them with the <<{articles}/components/dashboard#,[classname]`Dashboard`>> component to build an AI-generated dashboard that users arrange themselves and whose layout and chart state can be saved across sessions. Each dashboard widget hosts its own [classname]`Chart` and [classname]`ChartAIController` instance; persist each controller's state alongside the dashboard's own layout state to let users return to the same chart set and layout in a later session. diff --git a/articles/flow/ai-support/ai-powered-grid.adoc b/articles/flow/ai-support/ai-powered-grid.adoc index 5e58281783..302e2e14a5 100644 --- a/articles/flow/ai-support/ai-powered-grid.adoc +++ b/articles/flow/ai-support/ai-powered-grid.adoc @@ -73,7 +73,7 @@ if (saved != null) { == Reconnecting After Deserialization -[classname]`GridAIController` is not serializable. After session restore, create a new controller, wire it into the orchestrator's reconnector together with the new provider, and optionally re-apply the saved state: +[classname]`GridAIController` is not serializable. After session restore, create a new controller, pass it to [methodname]`reconnect()` together with the new provider, and optionally re-apply the saved state: [source,java] ---- diff --git a/articles/flow/ai-support/controllers.adoc b/articles/flow/ai-support/controllers.adoc index 6cc9ad22fd..adc0370780 100644 --- a/articles/flow/ai-support/controllers.adoc +++ b/articles/flow/ai-support/controllers.adoc @@ -120,8 +120,8 @@ Each tool is an implementation of [classname]`LLMProvider.ToolSpec`, which has f * [methodname]`getName()` -- the unique name the LLM uses to invoke the tool. * [methodname]`getDescription()` -- a human-readable description shown to the LLM. -* [methodname]`getParametersSchema()` -- a JSON Schema string describing the tool's parameters, or `null` for parameterless tools. -* [methodname]`execute(JsonNode arguments)` -- receives the LLM's arguments as a [classname]`JsonNode` (from `tools.jackson.databind`) and returns the tool's result as a string. +* [methodname]`getParametersSchema()` -- a JSON Schema string describing the tool's parameters, or `null` for tools that take no parameters. +* [methodname]`execute(JsonNode arguments)` -- receives the arguments passed by the LLM as a [classname]`JsonNode` (from `tools.jackson.databind`) and returns the tool's result as a string. A minimal custom controller looks like this: @@ -169,7 +169,7 @@ public class WeatherController implements AIController { } ---- -Tool names must match the pattern `^[a-zA-Z0-9_-]{1,64}$`, as required by popular LLM APIs. Names are validated at build time; invalid names cause an [classname]`IllegalArgumentException`. Use a namespaced name such as `"MyController_getWeather"` to avoid collisions with tools from other controllers. +Tool names must match the pattern `^[a-zA-Z0-9_-]{1,64}$`, as required by popular LLM APIs. Names are validated at build time; invalid names cause an [classname]`IllegalArgumentException`. Use a prefixed name such as `"MyController_getWeather"` to avoid collisions with tools from other controllers. .Generating JSON Schemas [TIP] diff --git a/articles/flow/ai-support/tool-calling.adoc b/articles/flow/ai-support/tool-calling.adoc index f8e7847c45..d56a39b6e3 100644 --- a/articles/flow/ai-support/tool-calling.adoc +++ b/articles/flow/ai-support/tool-calling.adoc @@ -8,7 +8,7 @@ order: 40 = Tool Calling & Programmatic Prompts -Tools let the LLM call methods in your application during a conversation -- query a database, look up an order, send an email, or any other action you want the assistant to be able to perform. There are two ways to expose tools: registered tool objects, covered on this page, and framework-agnostic <>. +Tools let the LLM call methods in your application during a conversation -- query a database, look up an order, send an email, or any other action you want the assistant to be able to perform. Vaadin supports two ways to expose tools: registered tool objects (covered on this page) and framework-agnostic <>. == Tool Calling