diff --git a/examples/eval/test/simple_chat_test.dart b/examples/eval/test/simple_chat_test.dart index cd7c7c2a9..4781ee955 100644 --- a/examples/eval/test/simple_chat_test.dart +++ b/examples/eval/test/simple_chat_test.dart @@ -90,22 +90,48 @@ class _ChatSessionTester { var content = 0; var waiting = 0; final errors = []; + + var currentTurnCreates = 0; + var currentTurnUpdates = 0; + var turnCount = 0; + + void verifyTurn() { + if (turnCount > 0) { + reporter.expect( + currentTurnCreates <= 1, + 'Turn $turnCount should create at most 1 surface', + ); + reporter.expect( + currentTurnUpdates == currentTurnCreates, + 'Turn $turnCount should have matching creates ($currentTurnCreates) ' + 'and updates ($currentTurnUpdates)', + ); + } + } + for (final ConversationEvent event in events) { switch (event) { case ConversationSurfaceAdded(): created.add(event.surfaceId); + currentTurnCreates++; case ConversationComponentsUpdated(): updated.add(event.surfaceId); + currentTurnUpdates++; case ConversationSurfaceRemoved(): removed.add(event.surfaceId); case ConversationContentReceived(): content++; case ConversationWaiting(): + verifyTurn(); + turnCount++; waiting++; + currentTurnCreates = 0; + currentTurnUpdates = 0; case ConversationError(): errors.add(event.error.toString()); } } + verifyTurn(); print('Conversation summary:'); print(' Created surfaces: $created'); @@ -117,9 +143,16 @@ class _ChatSessionTester { reporter.expect(errors.isEmpty, 'No errors should occur'); reporter.expect( - updated.isEmpty, - 'In chat setup surfaces should not be updated', + updated.length == created.length, + 'In chat setup surfaces should not be updated after initial creation', ); + for (final id in created) { + final int updateCount = updated.where((u) => u == id).length; + reporter.expect( + updateCount == 1, + 'Surface $id should be updated exactly once', + ); + } } void failIfIssuesFound() => reporter.failIfIssuesFound(); diff --git a/packages/genui/lib/src/engine/surface_controller.dart b/packages/genui/lib/src/engine/surface_controller.dart index 51f011218..69731d290 100644 --- a/packages/genui/lib/src/engine/surface_controller.dart +++ b/packages/genui/lib/src/engine/surface_controller.dart @@ -234,9 +234,8 @@ interface class SurfaceController implements SurfaceHost, A2uiMessageSink { final DataModel model = _store.getDataModel(surfaceId); model.update(path, value); - // Trigger generic update on surface to refresh UI - final SurfaceDefinition current = _registry.getSurface(surfaceId)!; - _registry.updateSurface(surfaceId, current); + // Note: We don't trigger a surface update here to avoid full UI refreshes + // on data changes. Components should listen to the DataModel directly. case DeleteSurface(:final surfaceId): _pendingUpdates.remove(surfaceId); diff --git a/packages/genui/lib/src/facade/conversation.dart b/packages/genui/lib/src/facade/conversation.dart index 0b73fccd3..5b09ff07d 100644 --- a/packages/genui/lib/src/facade/conversation.dart +++ b/packages/genui/lib/src/facade/conversation.dart @@ -57,7 +57,8 @@ final class ConversationContentReceived extends ConversationEvent { final String text; } -/// Fired when the conversation is waiting for a response. +/// Fired when a request is sent to the LLM and the conversation is waiting +/// for an AI response. final class ConversationWaiting extends ConversationEvent {} /// Fired when an error occurs during the conversation. diff --git a/packages/genui/lib/src/facade/prompt_builder.dart b/packages/genui/lib/src/facade/prompt_builder.dart index fbef8f403..3a0e1c5ae 100644 --- a/packages/genui/lib/src/facade/prompt_builder.dart +++ b/packages/genui/lib/src/facade/prompt_builder.dart @@ -22,7 +22,7 @@ abstract class PromptFragments { /// [prefix] is a prefix to be added to the prompt. /// Is useful when you want to emphasize the importance of this fragment. static String acknowledgeUser({String prefix = ''}) => - ''' + ''' ${prefix}Your responses should contain acknowledgment of the user message. ''' .trim(); @@ -131,7 +131,7 @@ enum ProtocolMessages { explanation: 'Creates a new surface.', properties: ''' Requires `surfaceId` (you must always use a unique ID for each created surface), -`catalogId` (use the catalog ID provided in system instructions), +`catalogId` (use the catalog ID provided in system instructions), and `sendDataModel: true`. ''', // TODO: figure out why we instruct AI to always set sendDataModel: true, @@ -142,7 +142,7 @@ and `sendDataModel: true`. name: 'updateComponents', explanation: 'Updates components in a surface.', properties: ''' -Requires `surfaceId` and a list of `components`. +Requires `surfaceId` and a list of `components`. One component MUST have `id: "root"`. ''', ), @@ -150,7 +150,7 @@ One component MUST have `id: "root"`. name: 'updateDataModel', explanation: 'Updates the data model.', properties: ''' -Requires `surfaceId`, `path` and `value`. +Requires `surfaceId`, `path` and `value`. ''', ), deleteSurface( @@ -286,6 +286,10 @@ You can control the UI by outputting valid A2UI JSON messages wrapped in markdow To create a new UI: 1. Output a ${ProtocolMessages.createSurface.tickedName} message with a unique `surfaceId` and `catalogId` (use the catalog ID provided in system instructions). 2. Output an ${ProtocolMessages.updateComponents.tickedName} message with the `surfaceId` and the component definitions. +''', + if (!update) + ''' +IMPORTANT: DO NOT update or modify surfaces created in previous turns. If the UI needs to change, you MUST create a NEW surface with a new unique `surfaceId`. You may only use ${ProtocolMessages.updateComponents.tickedName} to populate the components of a freshly created surface. ''', if (update) ''' diff --git a/packages/genui/test/facade/prompt_builder_test.golden/all_operations_with_dataModel_false.txt b/packages/genui/test/facade/prompt_builder_test.golden/all_operations_with_dataModel_false.txt index e8677efdb..3888111e3 100644 --- a/packages/genui/test/facade/prompt_builder_test.golden/all_operations_with_dataModel_false.txt +++ b/packages/genui/test/facade/prompt_builder_test.golden/all_operations_with_dataModel_false.txt @@ -100,9 +100,9 @@ Supported messages are: `createSurface`, `updateComponents`, `deleteSurface`. Properties: - `createSurface`: Requires `surfaceId` (you must always use a unique ID for each created surface), -`catalogId` (use the catalog ID provided in system instructions), +`catalogId` (use the catalog ID provided in system instructions), and `sendDataModel: true`. -- `updateComponents`: Requires `surfaceId` and a list of `components`. +- `updateComponents`: Requires `surfaceId` and a list of `components`. One component MUST have `id: "root"`. - `deleteSurface`: Requires `surfaceId`. diff --git a/packages/genui/test/facade/prompt_builder_test.golden/all_operations_with_dataModel_true.txt b/packages/genui/test/facade/prompt_builder_test.golden/all_operations_with_dataModel_true.txt index 5e8179ebf..8778192dd 100644 --- a/packages/genui/test/facade/prompt_builder_test.golden/all_operations_with_dataModel_true.txt +++ b/packages/genui/test/facade/prompt_builder_test.golden/all_operations_with_dataModel_true.txt @@ -101,9 +101,9 @@ Supported messages are: `createSurface`, `updateComponents`, `deleteSurface`, `u Properties: - `createSurface`: Requires `surfaceId` (you must always use a unique ID for each created surface), -`catalogId` (use the catalog ID provided in system instructions), +`catalogId` (use the catalog ID provided in system instructions), and `sendDataModel: true`. -- `updateComponents`: Requires `surfaceId` and a list of `components`. +- `updateComponents`: Requires `surfaceId` and a list of `components`. One component MUST have `id: "root"`. - `deleteSurface`: Requires `surfaceId`. - `updateDataModel`: Requires `surfaceId`, `path` and `value`. diff --git a/packages/genui/test/facade/prompt_builder_test.golden/create_and_update_with_dataModel_false.txt b/packages/genui/test/facade/prompt_builder_test.golden/create_and_update_with_dataModel_false.txt index 9b0b36e04..4d6716c4b 100644 --- a/packages/genui/test/facade/prompt_builder_test.golden/create_and_update_with_dataModel_false.txt +++ b/packages/genui/test/facade/prompt_builder_test.golden/create_and_update_with_dataModel_false.txt @@ -99,9 +99,9 @@ Supported messages are: `createSurface`, `updateComponents`. Properties: - `createSurface`: Requires `surfaceId` (you must always use a unique ID for each created surface), -`catalogId` (use the catalog ID provided in system instructions), +`catalogId` (use the catalog ID provided in system instructions), and `sendDataModel: true`. -- `updateComponents`: Requires `surfaceId` and a list of `components`. +- `updateComponents`: Requires `surfaceId` and a list of `components`. One component MUST have `id: "root"`. To create a new UI: diff --git a/packages/genui/test/facade/prompt_builder_test.golden/create_and_update_with_dataModel_true.txt b/packages/genui/test/facade/prompt_builder_test.golden/create_and_update_with_dataModel_true.txt index 2415cb23f..6dcd4d3de 100644 --- a/packages/genui/test/facade/prompt_builder_test.golden/create_and_update_with_dataModel_true.txt +++ b/packages/genui/test/facade/prompt_builder_test.golden/create_and_update_with_dataModel_true.txt @@ -100,9 +100,9 @@ Supported messages are: `createSurface`, `updateComponents`, `updateDataModel`. Properties: - `createSurface`: Requires `surfaceId` (you must always use a unique ID for each created surface), -`catalogId` (use the catalog ID provided in system instructions), +`catalogId` (use the catalog ID provided in system instructions), and `sendDataModel: true`. -- `updateComponents`: Requires `surfaceId` and a list of `components`. +- `updateComponents`: Requires `surfaceId` and a list of `components`. One component MUST have `id: "root"`. - `updateDataModel`: Requires `surfaceId`, `path` and `value`. diff --git a/packages/genui/test/facade/prompt_builder_test.golden/create_only_with_dataModel_false.txt b/packages/genui/test/facade/prompt_builder_test.golden/create_only_with_dataModel_false.txt index e97f8940f..5a746e597 100644 --- a/packages/genui/test/facade/prompt_builder_test.golden/create_only_with_dataModel_false.txt +++ b/packages/genui/test/facade/prompt_builder_test.golden/create_only_with_dataModel_false.txt @@ -99,14 +99,16 @@ Supported messages are: `createSurface`, `updateComponents`. Properties: - `createSurface`: Requires `surfaceId` (you must always use a unique ID for each created surface), -`catalogId` (use the catalog ID provided in system instructions), +`catalogId` (use the catalog ID provided in system instructions), and `sendDataModel: true`. -- `updateComponents`: Requires `surfaceId` and a list of `components`. +- `updateComponents`: Requires `surfaceId` and a list of `components`. One component MUST have `id: "root"`. To create a new UI: 1. Output a `createSurface` message with a unique `surfaceId` and `catalogId` (use the catalog ID provided in system instructions). 2. Output an `updateComponents` message with the `surfaceId` and the component definitions. + +IMPORTANT: DO NOT update or modify surfaces created in previous turns. If the UI needs to change, you MUST create a NEW surface with a new unique `surfaceId`. You may only use `updateComponents` to populate the components of a freshly created surface. -----CONTROLLING_THE_UI_END----- ------------------------------------- diff --git a/packages/genui/test/facade/prompt_builder_test.golden/create_only_with_dataModel_true.txt b/packages/genui/test/facade/prompt_builder_test.golden/create_only_with_dataModel_true.txt index 31a468da0..8538fc1a3 100644 --- a/packages/genui/test/facade/prompt_builder_test.golden/create_only_with_dataModel_true.txt +++ b/packages/genui/test/facade/prompt_builder_test.golden/create_only_with_dataModel_true.txt @@ -100,15 +100,17 @@ Supported messages are: `createSurface`, `updateComponents`, `updateDataModel`. Properties: - `createSurface`: Requires `surfaceId` (you must always use a unique ID for each created surface), -`catalogId` (use the catalog ID provided in system instructions), +`catalogId` (use the catalog ID provided in system instructions), and `sendDataModel: true`. -- `updateComponents`: Requires `surfaceId` and a list of `components`. +- `updateComponents`: Requires `surfaceId` and a list of `components`. One component MUST have `id: "root"`. - `updateDataModel`: Requires `surfaceId`, `path` and `value`. To create a new UI: 1. Output a `createSurface` message with a unique `surfaceId` and `catalogId` (use the catalog ID provided in system instructions). 2. Output an `updateComponents` message with the `surfaceId` and the component definitions. + +IMPORTANT: DO NOT update or modify surfaces created in previous turns. If the UI needs to change, you MUST create a NEW surface with a new unique `surfaceId`. You may only use `updateComponents` to populate the components of a freshly created surface. -----CONTROLLING_THE_UI_END----- ------------------------------------- diff --git a/packages/genui/test/facade/prompt_builder_test.golden/update_only_with_dataModel_false.txt b/packages/genui/test/facade/prompt_builder_test.golden/update_only_with_dataModel_false.txt index 32e59a090..3b6ca3e37 100644 --- a/packages/genui/test/facade/prompt_builder_test.golden/update_only_with_dataModel_false.txt +++ b/packages/genui/test/facade/prompt_builder_test.golden/update_only_with_dataModel_false.txt @@ -97,7 +97,7 @@ Supported messages are: `updateComponents`. Properties: -- `updateComponents`: Requires `surfaceId` and a list of `components`. +- `updateComponents`: Requires `surfaceId` and a list of `components`. One component MUST have `id: "root"`. To update an existing UI: diff --git a/packages/genui/test/facade/prompt_builder_test.golden/update_only_with_dataModel_true.txt b/packages/genui/test/facade/prompt_builder_test.golden/update_only_with_dataModel_true.txt index ccc7d09cd..0ed23463a 100644 --- a/packages/genui/test/facade/prompt_builder_test.golden/update_only_with_dataModel_true.txt +++ b/packages/genui/test/facade/prompt_builder_test.golden/update_only_with_dataModel_true.txt @@ -98,7 +98,7 @@ Supported messages are: `updateComponents`, `updateDataModel`. Properties: -- `updateComponents`: Requires `surfaceId` and a list of `components`. +- `updateComponents`: Requires `surfaceId` and a list of `components`. One component MUST have `id: "root"`. - `updateDataModel`: Requires `surfaceId`, `path` and `value`.