diff --git a/examples/simple_chat/lib/main.dart b/examples/simple_chat/lib/main.dart index 616ae1669..d14367c82 100644 --- a/examples/simple_chat/lib/main.dart +++ b/examples/simple_chat/lib/main.dart @@ -76,7 +76,12 @@ class _ChatScreenState extends State { void initState() { super.initState(); final Catalog catalog = CoreCatalogItems.asCatalog(); - _genUiManager = GenUiManager(catalog: catalog); + _genUiManager = GenUiManager( + catalog: catalog, + configuration: const GenUiConfiguration( + actions: ActionsConfig.createOnly(), + ), + ); final systemInstruction = '''You are a helpful assistant who chats with a user, @@ -84,11 +89,6 @@ giving exactly one response for each user message. Your responses should contain acknowledgment of the user message. - -IMPORTANT: When you generate UI in a response, you MUST always create -a new surface with a unique `surfaceId`. Do NOT reuse or update -existing `surfaceId`s. Each UI response must be in its own new surface. - ${GenUiPromptFragments.basicChat}'''; // Create the appropriate content generator based on configuration diff --git a/examples/simple_chat/linux/flutter/generated_plugin_registrant.cc b/examples/simple_chat/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 000000000..e71a16d23 --- /dev/null +++ b/examples/simple_chat/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void fl_register_plugins(FlPluginRegistry* registry) { +} diff --git a/examples/simple_chat/linux/flutter/generated_plugin_registrant.h b/examples/simple_chat/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 000000000..e0f0a47bc --- /dev/null +++ b/examples/simple_chat/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/examples/simple_chat/linux/flutter/generated_plugins.cmake b/examples/simple_chat/linux/flutter/generated_plugins.cmake new file mode 100644 index 000000000..2e1de87a7 --- /dev/null +++ b/examples/simple_chat/linux/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/examples/simple_chat/macos/Flutter/GeneratedPluginRegistrant.swift b/examples/simple_chat/macos/Flutter/GeneratedPluginRegistrant.swift index e5d2c93d9..c6c180db8 100644 --- a/examples/simple_chat/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/examples/simple_chat/macos/Flutter/GeneratedPluginRegistrant.swift @@ -1,7 +1,3 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - // // Generated file. Do not edit. // diff --git a/examples/simple_chat/windows/flutter/generated_plugin_registrant.cc b/examples/simple_chat/windows/flutter/generated_plugin_registrant.cc index dc93d0ce0..d141b74f5 100644 --- a/examples/simple_chat/windows/flutter/generated_plugin_registrant.cc +++ b/examples/simple_chat/windows/flutter/generated_plugin_registrant.cc @@ -1,7 +1,3 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - // // Generated file. Do not edit. // diff --git a/examples/simple_chat/windows/flutter/generated_plugin_registrant.h b/examples/simple_chat/windows/flutter/generated_plugin_registrant.h index 35e206378..dc139d85a 100644 --- a/examples/simple_chat/windows/flutter/generated_plugin_registrant.h +++ b/examples/simple_chat/windows/flutter/generated_plugin_registrant.h @@ -1,7 +1,3 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - // // Generated file. Do not edit. // diff --git a/examples/simple_chat/windows/flutter/generated_plugins.cmake b/examples/simple_chat/windows/flutter/generated_plugins.cmake index 7036e9bee..29944d5b1 100644 --- a/examples/simple_chat/windows/flutter/generated_plugins.cmake +++ b/examples/simple_chat/windows/flutter/generated_plugins.cmake @@ -1,7 +1,3 @@ -# Copyright 2025 The Flutter Authors. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - # # Generated file, do not edit. # diff --git a/examples/travel_app/linux/flutter/generated_plugin_registrant.cc b/examples/travel_app/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 000000000..e71a16d23 --- /dev/null +++ b/examples/travel_app/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void fl_register_plugins(FlPluginRegistry* registry) { +} diff --git a/examples/travel_app/linux/flutter/generated_plugin_registrant.h b/examples/travel_app/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 000000000..e0f0a47bc --- /dev/null +++ b/examples/travel_app/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/examples/travel_app/linux/flutter/generated_plugins.cmake b/examples/travel_app/linux/flutter/generated_plugins.cmake new file mode 100644 index 000000000..2e1de87a7 --- /dev/null +++ b/examples/travel_app/linux/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/genui/lib/src/core/ui_tools.dart b/packages/genui/lib/src/core/ui_tools.dart index 69740f365..286b3871f 100644 --- a/packages/genui/lib/src/core/ui_tools.dart +++ b/packages/genui/lib/src/core/ui_tools.dart @@ -22,10 +22,14 @@ class SurfaceUpdateTool extends AiTool { required this.handleMessage, required Catalog catalog, required this.configuration, + required SurfaceUpdateMode updateMode, + super.name = 'surfaceUpdate', + super.description = 'Updates a surface with a new set of components.', }) : super( - name: 'surfaceUpdate', - description: 'Updates a surface with a new set of components.', - parameters: A2uiSchemas.surfaceUpdateSchema(catalog), + parameters: A2uiSchemas.surfaceUpdateSchema( + catalog, + updateMode: updateMode, + ), ); /// The callback to invoke when adding or updating a surface. @@ -88,27 +92,16 @@ class DeleteSurfaceTool extends AiTool { /// This tool allows the AI to specify the root component of a UI surface. class BeginRenderingTool extends AiTool { /// Creates a [BeginRenderingTool]. - BeginRenderingTool({required this.handleMessage}) - : super( - name: 'beginRendering', - description: - 'Signals the client to begin rendering a surface with a ' - 'root component.', - parameters: S.object( - properties: { - surfaceIdKey: S.string( - description: - 'The unique identifier for the UI surface to render.', - ), - 'root': S.string( - description: - 'The ID of the root widget. This ID must correspond to ' - 'the ID of one of the widgets in the `components` list.', - ), - }, - required: [surfaceIdKey, 'root'], - ), - ); + BeginRenderingTool({ + required this.handleMessage, + SurfaceUpdateMode updateMode = SurfaceUpdateMode.both, + }) : super( + name: 'beginRendering', + description: + 'Signals the client to begin rendering a surface with a ' + 'root component.', + parameters: A2uiSchemas.beginRenderingSchema(updateMode: updateMode), + ); /// The callback to invoke when signaling to begin rendering. final void Function(A2uiMessage message) handleMessage; diff --git a/packages/genui/lib/src/model/a2ui_schemas.dart b/packages/genui/lib/src/model/a2ui_schemas.dart index f5571d954..40deb8165 100644 --- a/packages/genui/lib/src/model/a2ui_schemas.dart +++ b/packages/genui/lib/src/model/a2ui_schemas.dart @@ -7,6 +7,18 @@ import 'package:json_schema_builder/json_schema_builder.dart'; import 'catalog.dart'; import 'tools.dart'; +/// Defines the allowed operations for a surface update tool. +enum SurfaceUpdateMode { + /// The tool can only create new surfaces. + create, + + /// The tool can only update existing surfaces. + update, + + /// The tool can both create and update surfaces. + both, +} + /// Provides a set of pre-defined, reusable schema objects for common /// A2UI patterns, simplifying the creation of CatalogItem definitions. class A2uiSchemas { @@ -133,28 +145,54 @@ class A2uiSchemas { /// Schema for a beginRendering message, which provides the root widget ID for /// the given surface so that the surface can be rendered. - static Schema beginRenderingSchema() => S.object( - properties: { - surfaceIdKey: S.string( - description: 'The surface ID of the surface to render.', - ), - 'root': S.string( - description: - 'The root widget ID for the surface. ' - 'All components must be descendents of this root in order to be ' - 'displayed.', - ), - 'styles': S.object( - properties: { - 'font': S.string(description: 'The base font for this surface'), - 'primaryColor': S.string( - description: 'The seed color for the theme of this surface.', - ), - }, - ), - }, - required: [surfaceIdKey, 'root'], - ); + static Schema beginRenderingSchema({ + SurfaceUpdateMode updateMode = SurfaceUpdateMode.both, + }) { + final String surfaceIdDescription; + switch (updateMode) { + case SurfaceUpdateMode.create: + surfaceIdDescription = + 'The unique identifier for the new UI surface to render. This ' + '*must* be a new, unique identifier that matches the surfaceId ' + 'of a `surfaceCreate` call.'; + break; + case SurfaceUpdateMode.update: + surfaceIdDescription = + 'The unique identifier for the existing UI surface to render.'; + break; + case SurfaceUpdateMode.both: + surfaceIdDescription = + 'The unique identifier for the UI surface to render. This may be a ' + 'new or existing surface ID.'; + break; + } + return S.object( + description: + 'A message which can be sent before or after surfaceUpdate to ' + 'begin rendering a surface with a given ID. Any surfaceUpdate messages ' + 'sent before this message will be cached but not trigger any rendering ' + ' yet. The surfaceId *must* match associated surfaceUpdate or ' + 'dataModelUpdate messages.', + properties: { + surfaceIdKey: S.string(description: surfaceIdDescription), + 'root': S.string( + description: + 'The root widget ID for the surface. ' + 'All components must be descendents of this root in order to be ' + 'displayed.', + ), + 'styles': S.object( + properties: { + 'font': S.string(description: 'The base font for this surface'), + 'primaryColor': S.string( + description: 'The seed color for the theme of this surface.', + ), + }, + ), + }, + required: [surfaceIdKey, 'root'], + ); + } /// Schema for a `deleteSurface` message which will delete the given surface. static Schema surfaceDeletionSchema() => S.object( @@ -177,46 +215,65 @@ class A2uiSchemas { /// Schema for a `surfaceUpdate` message which defines the components to be /// rendered on a surface. - static Schema surfaceUpdateSchema(Catalog catalog) => S.object( - properties: { - surfaceIdKey: S.string( - description: + static Schema surfaceUpdateSchema( + Catalog catalog, { + SurfaceUpdateMode updateMode = SurfaceUpdateMode.both, + }) { + final String surfaceIdDescription; + switch (updateMode) { + case SurfaceUpdateMode.create: + surfaceIdDescription = + 'The unique identifier for the new UI surface to create. This ' + '*must* be a new identifier different to any existing surfaces ' + 'that have already been displayed.'; + break; + case SurfaceUpdateMode.update: + surfaceIdDescription = + 'The unique identifier for the existing UI surface to update.'; + break; + case SurfaceUpdateMode.both: + surfaceIdDescription = 'The unique identifier for the UI surface to create or ' 'update. If you are adding a new surface this *must* be a ' 'new, unique identified that has never been used for any ' - 'existing surfaces shown.', - ), - 'components': S.list( - description: 'A list of component definitions.', - minItems: 1, - items: S.object( - description: - 'Represents a *single* component in a UI widget tree. ' - 'This component could be one of many supported types.', - properties: { - 'id': S.string(), - 'weight': S.integer( - description: - 'Optional layout weight for use in Row/Column children.', - ), - 'component': S.object( - description: - '''A wrapper object that MUST contain exactly one key, which is the name of the component type (e.g., 'Text'). The value is an object containing the properties for that specific component.''', - properties: { - for (var entry - in ((catalog.definition as ObjectSchema) - .properties!['components']! - as ObjectSchema) - .properties! - .entries) - entry.key: entry.value, - }, - ), - }, - required: ['id', 'component'], + 'existing surfaces shown.'; + break; + } + return S.object( + properties: { + surfaceIdKey: S.string(description: surfaceIdDescription), + 'components': S.list( + description: 'A list of component definitions.', + minItems: 1, + items: S.object( + description: + 'Represents a *single* component in a UI widget tree. ' + 'This component could be one of many supported types.', + properties: { + 'id': S.string(), + 'weight': S.integer( + description: + 'Optional layout weight for use in Row/Column children.', + ), + 'component': S.object( + description: + '''A wrapper object that MUST contain exactly one key, which is the name of the component type (e.g., 'Text'). The value is an object containing the properties for that specific component.''', + properties: { + for (var entry + in ((catalog.definition as ObjectSchema) + .properties!['components']! + as ObjectSchema) + .properties! + .entries) + entry.key: entry.value, + }, + ), + }, + required: ['id', 'component'], + ), ), - ), - }, - required: [surfaceIdKey, 'components'], - ); + }, + required: [surfaceIdKey, 'components'], + ); + } } diff --git a/packages/genui/test/core/ui_tools_test.dart b/packages/genui/test/core/ui_tools_test.dart index 9a9ca8d9a..c7d34aa3a 100644 --- a/packages/genui/test/core/ui_tools_test.dart +++ b/packages/genui/test/core/ui_tools_test.dart @@ -7,6 +7,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:genui/src/core/genui_configuration.dart'; import 'package:genui/src/core/ui_tools.dart'; import 'package:genui/src/model/a2ui_message.dart'; +import 'package:genui/src/model/a2ui_schemas.dart'; import 'package:genui/src/model/catalog.dart'; import 'package:genui/src/model/catalog_item.dart'; import 'package:genui/src/model/tools.dart'; @@ -33,6 +34,7 @@ void main() { ), ]), configuration: const GenUiConfiguration(), + updateMode: SurfaceUpdateMode.both, ); final Map args = { diff --git a/packages/genui/test/ui_tools_test.dart b/packages/genui/test/ui_tools_test.dart index 754acd90e..70822070b 100644 --- a/packages/genui/test/ui_tools_test.dart +++ b/packages/genui/test/ui_tools_test.dart @@ -29,6 +29,7 @@ void main() { handleMessage: genUiManager.handleMessage, catalog: catalog, configuration: const GenUiConfiguration(), + updateMode: SurfaceUpdateMode.both, ); final Map args = { diff --git a/packages/genui_firebase_ai/lib/src/firebase_ai_content_generator.dart b/packages/genui_firebase_ai/lib/src/firebase_ai_content_generator.dart index 09463db31..022815136 100644 --- a/packages/genui_firebase_ai/lib/src/firebase_ai_content_generator.dart +++ b/packages/genui_firebase_ai/lib/src/firebase_ai_content_generator.dart @@ -343,15 +343,34 @@ class FirebaseAiContentGenerator implements ContentGenerator { final adapter = GeminiSchemaAdapter(); final List> availableTools = [ - if (configuration.actions.allowCreate || - configuration.actions.allowUpdate) ...[ + if (configuration.actions.allowCreate) SurfaceUpdateTool( handleMessage: _a2uiMessageController.add, catalog: catalog, configuration: configuration, + updateMode: SurfaceUpdateMode.create, + name: 'surfaceCreate', + description: 'Creates a new UI surface.', + ), + if (configuration.actions.allowUpdate) + SurfaceUpdateTool( + handleMessage: _a2uiMessageController.add, + catalog: catalog, + configuration: configuration, + updateMode: SurfaceUpdateMode.update, + name: 'surfaceUpdate', + description: 'Updates an existing UI surface.', + ), + if (configuration.actions.allowCreate) + BeginRenderingTool( + handleMessage: _a2uiMessageController.add, + updateMode: SurfaceUpdateMode.create, + ), + if (configuration.actions.allowUpdate) + BeginRenderingTool( + handleMessage: _a2uiMessageController.add, + updateMode: SurfaceUpdateMode.update, ), - BeginRenderingTool(handleMessage: _a2uiMessageController.add), - ], if (configuration.actions.allowDelete) DeleteSurfaceTool(handleMessage: _a2uiMessageController.add), ...additionalTools, diff --git a/packages/genui_google_generative_ai/lib/src/google_generative_ai_content_generator.dart b/packages/genui_google_generative_ai/lib/src/google_generative_ai_content_generator.dart index 5a5683746..5df137a54 100644 --- a/packages/genui_google_generative_ai/lib/src/google_generative_ai_content_generator.dart +++ b/packages/genui_google_generative_ai/lib/src/google_generative_ai_content_generator.dart @@ -344,15 +344,34 @@ class GoogleGenerativeAiContentGenerator implements ContentGenerator { try { final availableTools = [ - if (configuration.actions.allowCreate || - configuration.actions.allowUpdate) ...[ + if (configuration.actions.allowCreate) SurfaceUpdateTool( handleMessage: _a2uiMessageController.add, catalog: catalog, configuration: configuration, + updateMode: SurfaceUpdateMode.create, + name: 'surfaceCreate', + description: 'Creates a new UI surface.', + ), + if (configuration.actions.allowUpdate) + SurfaceUpdateTool( + handleMessage: _a2uiMessageController.add, + catalog: catalog, + configuration: configuration, + updateMode: SurfaceUpdateMode.update, + name: 'surfaceUpdate', + description: 'Updates an existing UI surface.', + ), + if (configuration.actions.allowCreate) + BeginRenderingTool( + handleMessage: _a2uiMessageController.add, + updateMode: SurfaceUpdateMode.create, + ), + if (configuration.actions.allowUpdate) + BeginRenderingTool( + handleMessage: _a2uiMessageController.add, + updateMode: SurfaceUpdateMode.update, ), - BeginRenderingTool(handleMessage: _a2uiMessageController.add), - ], if (configuration.actions.allowDelete) DeleteSurfaceTool(handleMessage: _a2uiMessageController.add), ...additionalTools,