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/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/ai-powered-chart.adoc b/articles/flow/ai-support/ai-powered-chart.adoc new file mode 100644 index 0000000000..72c12b39c9 --- /dev/null +++ b/articles/flow/ai-support/ai-powered-chart.adoc @@ -0,0 +1,114 @@ +--- +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# + + +[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. + +== 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, 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." +* "Turn this into an area chart and add a 'Revenue (USD)' y-axis title." + +.Built-In Workflow Instructions +[TIP] +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] +[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 + +[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); +} +---- + +`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. + + +== Reconnecting After Deserialization + +[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] +---- +ChartAIController controller = new ChartAIController(chart, databaseProvider); +orchestrator.reconnect(provider) + .withController(controller) + .apply(); +---- + + +== Custom Data Conversion + +[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] +---- +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. + + +== 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 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 new file mode 100644 index 0000000000..302e2e14a5 --- /dev/null +++ b/articles/flow/ai-support/ai-powered-grid.adoc @@ -0,0 +1,84 @@ +--- +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# + + +[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. + + +== 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. [classname]`AIDataRow` is a framework-owned row type -- you never construct instances yourself. + +[source,java] +---- +Grid grid = new Grid<>(); +MessageInput messageInput = new MessageInput(); + +DatabaseProvider databaseProvider = new JdbcDatabaseProvider(dataSource); +GridAIController controller = new GridAIController(grid, databaseProvider); + +AIOrchestrator.builder(provider, systemPrompt) + .withInput(messageInput) + .withController(controller) + .build(); + +add(messageInput, grid); +---- + +A message list is not required -- the grid itself is the output surface. Example prompts: + +* "Show product name and category grouped under Product, and monthly revenue." +* "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] +The controller already informs the LLM of the workflow it needs. You can focus your own system prompt on application-specific behavior. + + +== Query Validation + +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 + +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); +} +---- + +`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. + + +== Reconnecting After Deserialization + +[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] +---- +GridAIController controller = new GridAIController(grid, databaseProvider); +orchestrator.reconnect(provider) + .withController(controller) + .apply(); +---- diff --git a/articles/flow/ai-support/controllers.adoc b/articles/flow/ai-support/controllers.adoc new file mode 100644 index 0000000000..adc0370780 --- /dev/null +++ b/articles/flow/ai-support/controllers.adoc @@ -0,0 +1,181 @@ +--- +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# + + +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: + +* <> -- 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, systemPrompt) + .withMessageList(messageList) + .withInput(messageInput) + .withController(controller) + .build(); +---- + +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 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. 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: + +[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); + } + } +} +---- + +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. + +.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. + + +== Building a Custom Controller + +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: + +* [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]`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: + +* [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 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: + +[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(JsonNode arguments) { + String city = arguments.get("city").asString(); + return weatherService.lookup(city); + } + }); + } + + @Override + public void onResponseComplete() { + // 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 prefixed 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/conversation-history.adoc b/articles/flow/ai-support/conversation-history.adoc index e3d9343569..68d11bf9fc 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. 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`. .LLMProvider Implementations 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] ---- diff --git a/articles/flow/ai-support/index.adoc b/articles/flow/ai-support/index.adoc index 71650a0c82..887934d351 100644 --- a/articles/flow/ai-support/index.adoc +++ b/articles/flow/ai-support/index.adoc @@ -20,16 +20,15 @@ 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 -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. +* **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`. 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. @@ -62,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." + +.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 @@ -84,8 +91,6 @@ Synchronous mode does not require push. If push is not available in your environ section_outline::[] -endif::flow[] - == Related Components [cols="1,2"] @@ -101,4 +106,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/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] diff --git a/articles/flow/ai-support/tool-calling.adoc b/articles/flow/ai-support/tool-calling.adoc index 8445b6e166..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 -ifdef::flow[] +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 @@ -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 @@ -61,5 +65,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[] 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 }