diff --git a/dev_tools/composer/lib/create_tab.dart b/dev_tools/composer/lib/create_tab.dart index a82a7434b..5ca53e5f2 100644 --- a/dev_tools/composer/lib/create_tab.dart +++ b/dev_tools/composer/lib/create_tab.dart @@ -78,7 +78,7 @@ class _CreateTabState extends State { transport: transport, ); - final promptBuilder = PromptBuilder.chat( + final promptBuilder = await PromptBuilder.createChat( catalog: catalog, systemPromptFragments: [ 'You are a UI generator. The user will describe a UI they want. ' diff --git a/examples/simple_chat/lib/chat_session.dart b/examples/simple_chat/lib/chat_session.dart index eb3fd37e9..2ceb625c2 100644 --- a/examples/simple_chat/lib/chat_session.dart +++ b/examples/simple_chat/lib/chat_session.dart @@ -56,19 +56,20 @@ final Catalog _customCatalog = _basicCatalog.copyWith( newItems: [climbingLocationItem], ); -PromptBuilder _promptBuilderFor(Catalog catalog) => PromptBuilder.chat( - catalog: catalog, - systemPromptFragments: [ - Prompts.summary, - PromptFragments.acknowledgeUser(), - PromptFragments.requireAtLeastOneSubmitElement( - prefix: PromptBuilder.defaultImportancePrefix, - ), - PromptFragments.uiGenerationRestriction( - prefix: PromptBuilder.defaultImportancePrefix, - ), - ], -); +Future _promptBuilderFor(Catalog catalog) async => + await PromptBuilder.createChat( + catalog: catalog, + systemPromptFragments: [ + Prompts.summary, + PromptFragments.acknowledgeUser(), + PromptFragments.requireAtLeastOneSubmitElement( + prefix: PromptBuilder.defaultImportancePrefix, + ), + PromptFragments.uiGenerationRestriction( + prefix: PromptBuilder.defaultImportancePrefix, + ), + ], + ); sealed class ChatSession extends ChangeNotifier { ChatSession._(); @@ -188,7 +189,7 @@ class A2uiChatSession extends ChatSession { late final StreamSubscription _submitSub; late final StreamSubscription _surfaceSub; - void _init() { + Future _init() async { _messageSub = _transport.incomingMessages.listen( _surfaceController.handleMessage, ); @@ -198,9 +199,8 @@ class A2uiChatSession extends ChatSession { ); _surfaceSub = _surfaceController.surfaceUpdates.listen(_onSurfaceUpdate); - _transport.addSystemMessage( - _promptBuilderFor(_catalog).systemPromptJoined(), - ); + final PromptBuilder pb = await _promptBuilderFor(_catalog); + _transport.addSystemMessage(pb.systemPromptJoined()); } void _onSurfaceUpdate(SurfaceUpdate update) { diff --git a/examples/verdure/client/linux/flutter/generated_plugins.cmake b/examples/verdure/client/linux/flutter/generated_plugins.cmake index 04f81f4b4..ac700e247 100644 --- a/examples/verdure/client/linux/flutter/generated_plugins.cmake +++ b/examples/verdure/client/linux/flutter/generated_plugins.cmake @@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/examples/verdure/client/macos/Flutter/GeneratedPluginRegistrant.swift b/examples/verdure/client/macos/Flutter/GeneratedPluginRegistrant.swift index 074b04b4c..46b142243 100644 --- a/examples/verdure/client/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/examples/verdure/client/macos/Flutter/GeneratedPluginRegistrant.swift @@ -16,5 +16,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) - FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) + VideoPlayerPlugin.register(with: registry.registrar(forPlugin: "VideoPlayerPlugin")) } diff --git a/packages/genui/lib/src/catalog/basic_catalog_widgets/button.dart b/packages/genui/lib/src/catalog/basic_catalog_widgets/button.dart index 9c50c80b2..f7304afa7 100644 --- a/packages/genui/lib/src/catalog/basic_catalog_widgets/button.dart +++ b/packages/genui/lib/src/catalog/basic_catalog_widgets/button.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:json_schema_builder/json_schema_builder.dart'; +import '../../model/a2ui_exceptions.dart'; import '../../model/a2ui_schemas.dart'; import '../../model/catalog_item.dart'; import '../../model/data_model.dart'; @@ -233,7 +234,33 @@ Future _handlePress( try { await resultStream.first; } catch (exception, stackTrace) { - itemContext.reportError(exception, stackTrace); + genUiLogger.severe( + 'Error executing function call "$callName" on button press', + exception, + stackTrace, + ); + + if (exception is A2uiFunctionException) { + itemContext.reportError(exception, stackTrace); + } else if (exception is ArgumentError) { + itemContext.reportError( + A2uiFunctionException( + exception.message.toString(), + functionName: callName, + cause: exception, + ), + stackTrace, + ); + } else { + itemContext.reportError( + A2uiFunctionException( + 'Function execution failed. Please check arguments and try again.', + functionName: callName, + cause: exception, + ), + stackTrace, + ); + } } } else { genUiLogger.warning( diff --git a/packages/genui/lib/src/catalog/basic_catalog_widgets/date_time_input.dart b/packages/genui/lib/src/catalog/basic_catalog_widgets/date_time_input.dart index be314b50d..b1b81b418 100644 --- a/packages/genui/lib/src/catalog/basic_catalog_widgets/date_time_input.dart +++ b/packages/genui/lib/src/catalog/basic_catalog_widgets/date_time_input.dart @@ -340,9 +340,7 @@ final dateTimeInput = CatalogItem( { "id": "root", "component": "DateTimeInput", - "value": { - "path": "/myDateTime" - } + "value": "2026-05-15" } ] ''', @@ -354,7 +352,7 @@ final dateTimeInput = CatalogItem( "value": { "path": "/myDate" }, - "enableTime": false + "variant": "date" } ] ''', @@ -366,7 +364,7 @@ final dateTimeInput = CatalogItem( "value": { "path": "/myTime" }, - "enableDate": false + "variant": "time" } ] ''', diff --git a/packages/genui/lib/src/catalog/basic_catalog_widgets/image.dart b/packages/genui/lib/src/catalog/basic_catalog_widgets/image.dart index 7e165b578..da9829cdd 100644 --- a/packages/genui/lib/src/catalog/basic_catalog_widgets/image.dart +++ b/packages/genui/lib/src/catalog/basic_catalog_widgets/image.dart @@ -78,7 +78,9 @@ final CatalogItem image = CatalogItem( { "id": "root", "component": "Image", - "url": "https://storage.googleapis.com/cms-storage-bucket/lockup_flutter_horizontal.c823e53b3a1a7b0d36a9.png", + "url": { + "path": "/imageUrl" + }, "variant": "mediumFeature" } ] diff --git a/packages/genui/lib/src/catalog/basic_catalog_widgets/text.dart b/packages/genui/lib/src/catalog/basic_catalog_widgets/text.dart index 34e17f9c9..f4b2b7899 100644 --- a/packages/genui/lib/src/catalog/basic_catalog_widgets/text.dart +++ b/packages/genui/lib/src/catalog/basic_catalog_widgets/text.dart @@ -52,8 +52,7 @@ final text = CatalogItem( { "id": "root", "component": "Text", - "text": "Hello World", - "variant": "h1" + "text": "Hello World" } ] ''', diff --git a/packages/genui/lib/src/engine/surface_controller.dart b/packages/genui/lib/src/engine/surface_controller.dart index 69731d290..6e4bd8f2d 100644 --- a/packages/genui/lib/src/engine/surface_controller.dart +++ b/packages/genui/lib/src/engine/surface_controller.dart @@ -12,6 +12,7 @@ import '../interfaces/a2ui_message_sink.dart'; import '../interfaces/surface_context.dart'; import '../interfaces/surface_host.dart'; import '../model/a2ui_client_capabilities.dart'; +import '../model/a2ui_exceptions.dart'; import '../model/a2ui_message.dart'; import '../model/catalog.dart'; import '../model/chat_message.dart'; @@ -120,16 +121,21 @@ interface class SurfaceController implements SurfaceHost, A2uiMessageSink { /// Reports an error to the AI service. void reportError(Object error, StackTrace? stack) { - var errorCode = 'RUNTIME_ERROR'; - var message = error.toString(); + var errorCode = 'INTERNAL_ERROR'; + var message = 'An unexpected system error occurred.'; String? surfaceId; String? path; + String? functionName; if (error is A2uiValidationException) { errorCode = 'VALIDATION_FAILED'; message = error.message; surfaceId = error.surfaceId; path = error.path; + } else if (error is A2uiFunctionException) { + errorCode = 'FUNCTION_EXECUTION_FAILED'; + message = error.message; + functionName = error.functionName; } final Map errorMsg = { @@ -138,15 +144,18 @@ interface class SurfaceController implements SurfaceHost, A2uiMessageSink { 'code': errorCode, 'surfaceId': ?surfaceId, 'path': ?path, + 'functionName': ?functionName, 'message': message, }, }; - _onSubmit.add( - ChatMessage.user( - '', - parts: [UiInteractionPart.create(jsonEncode(errorMsg))], - ), - ); + if (!_onSubmit.isClosed) { + _onSubmit.add( + ChatMessage.user( + '', + parts: [UiInteractionPart.create(jsonEncode(errorMsg))], + ), + ); + } } void _handleMessageInternal(A2uiMessage message) { diff --git a/packages/genui/lib/src/facade/prompt_builder.dart b/packages/genui/lib/src/facade/prompt_builder.dart index 77ecada7c..2dc78e825 100644 --- a/packages/genui/lib/src/facade/prompt_builder.dart +++ b/packages/genui/lib/src/facade/prompt_builder.dart @@ -5,9 +5,10 @@ import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; -import '../model/a2ui_message.dart'; import '../model/catalog.dart'; +import '../primitives/constants.dart'; import '../primitives/simple_items.dart'; /// Common fragments for prompts, to explain agent behavior. @@ -78,12 +79,13 @@ abstract class PromptBuilder { /// The builder will generate a prompt for a chat session, /// that instructs to create new surfaces for each response /// and restrict surface deletion and updates. - factory PromptBuilder.chat({ + static Future createChat({ required Catalog catalog, Iterable systemPromptFragments = const [], String importancePrefix = defaultImportancePrefix, JsonMap? clientDataModel, - }) { + }) async { + final (String commonTypes, String serverToClient) = await _loadSchemas(); return _BasicPromptBuilder( catalog: catalog, systemPromptFragments: systemPromptFragments, @@ -91,10 +93,12 @@ abstract class PromptBuilder { importancePrefix: importancePrefix, clientDataModel: clientDataModel, technicalPossibilities: const TechnicalPossibilities(), + commonTypesSchema: commonTypes, + serverToClientSchema: serverToClient, ); } - factory PromptBuilder.custom({ + static Future createCustom({ required Catalog catalog, required SurfaceOperations allowedOperations, Iterable systemPromptFragments = const [], @@ -102,7 +106,8 @@ abstract class PromptBuilder { TechnicalPossibilities technicalPossibilities = const TechnicalPossibilities(), JsonMap? clientDataModel, - }) { + }) async { + final (String commonTypes, String serverToClient) = await _loadSchemas(); return _BasicPromptBuilder( catalog: catalog, systemPromptFragments: systemPromptFragments, @@ -110,9 +115,19 @@ abstract class PromptBuilder { importancePrefix: importancePrefix, clientDataModel: clientDataModel, technicalPossibilities: technicalPossibilities, + commonTypesSchema: commonTypes, + serverToClientSchema: serverToClient, ); } + static Future<(String, String)> _loadSchemas() async { + final String commonTypes = await rootBundle.loadString(commonTypesAssetKey); + final String serverToClient = await rootBundle.loadString( + serverToClientAssetKey, + ); + return (commonTypes, serverToClient); + } + Iterable systemPrompt(); /// Returns the system prompt as a single string. @@ -332,9 +347,13 @@ final class _BasicPromptBuilder extends PromptBuilder { required this.importancePrefix, required this.clientDataModel, required this.technicalPossibilities, + required this.commonTypesSchema, + required this.serverToClientSchema, }) : super._(); final Catalog catalog; + final String commonTypesSchema; + final String serverToClientSchema; final SurfaceOperations allowedOperations; @@ -352,16 +371,23 @@ final class _BasicPromptBuilder extends PromptBuilder { final JsonMap? clientDataModel; + final TechnicalPossibilities technicalPossibilities; + Iterable _fragmentsToPrompt(Iterable fragments) => fragments.map((e) => e.trim()); - final TechnicalPossibilities technicalPossibilities; - @override Iterable systemPrompt() { - final String a2uiSchema = A2uiMessage.a2uiMessageSchema( - catalog, - ).toJson(indent: ' '); + final String catalogSchema = _generateCatalogSchema(catalog); + + final String cleanCommonTypes = commonTypesSchema.replaceAll( + commonTypesSchemaId, + 'common_types.json', + ); + final String cleanServerToClient = serverToClientSchema.replaceAll( + commonTypesSchemaId, + 'common_types.json', + ); final fragments = [ ...systemPromptFragments, @@ -369,13 +395,123 @@ final class _BasicPromptBuilder extends PromptBuilder { ...technicalPossibilities.systemPromptFragment(), ...catalog.systemPromptFragments, ...allowedOperations.systemPromptFragments, - _fenced(a2uiSchema, sectionName: 'A2UI JSON SCHEMA'), + _fenced(cleanCommonTypes, sectionName: 'COMMON TYPES'), + _fenced(catalogSchema, sectionName: 'CATALOG SCHEMA'), + _fenced(cleanServerToClient, sectionName: 'MESSAGE SCHEMA'), ?_encodedDataModel(clientDataModel), ]; return _fragmentsToPrompt(fragments); } + String _generateCatalogSchema(Catalog catalog) { + final Map components = { + for (final item in catalog.items) + item.name: { + 'type': 'object', + 'allOf': [ + {r'$ref': r'common_types.json#/$defs/ComponentCommon'}, + {r'$ref': r'#/$defs/CatalogComponentCommon'}, + { + 'type': 'object', + 'properties': { + 'component': {'const': item.name}, + ...item.dataSchema.value['properties'] as Map, + }, + 'required': { + 'component', + if (item.dataSchema.value['required'] is List) + ...(item.dataSchema.value['required'] as List), + }.toList(), + }, + ], + 'unevaluatedProperties': false, + }, + }; + + final Map functions = { + for (final func in catalog.functions) + func.name: { + 'description': func.description, + 'parameters': func.argumentSchema.value, + 'returnType': func.returnType.value, + }, + }; + + final Map catalogJson = { + r'$schema': 'https://json-schema.org/draft/2020-12/schema', + r'$id': 'https://a2ui.org/specification/v0_9/catalog.json', + 'title': 'A2UI Catalog', + 'description': 'Custom catalog of A2UI components and functions.', + if (catalog.catalogId != null) 'catalogId': catalog.catalogId, + 'components': components, + if (functions.isNotEmpty) 'functions': functions, + r'$defs': { + 'CatalogComponentCommon': { + 'type': 'object', + 'properties': { + 'id': { + 'type': 'string', + 'description': + 'A unique identifier for this component instance within ' + 'the surface. This ID is used to refer to the component ' + 'in layout children arrays or event handlers.', + }, + }, + 'required': ['id'], + }, + 'theme': { + 'type': 'object', + 'properties': { + 'primaryColor': { + 'type': 'string', + 'description': + 'The primary brand color used for highlights (e.g., ' + 'primary buttons, active borders). Renderers may generate ' + 'variants of this color for different contexts. Format: ' + "Hexadecimal code (e.g., '#00BFFF').", + 'pattern': r'^#[0-9a-fA-F]{6}$', + }, + 'iconUrl': { + 'type': 'string', + 'format': 'uri', + 'description': + 'A URL for an image that identifies the agent or tool ' + 'associated with the surface.', + }, + 'agentDisplayName': { + 'type': 'string', + 'description': + 'Text to be displayed next to the surface to identify ' + 'the agent or tool that created it.', + }, + }, + 'additionalProperties': true, + }, + 'anyComponent': components.isEmpty + ? {'not': {}} + : { + 'oneOf': [ + for (final name in components.keys) + {r'$ref': '#/components/$name'}, + ], + 'discriminator': {'propertyName': 'component'}, + }, + 'anyFunction': functions.isEmpty + ? {'not': {}} + : { + 'oneOf': [ + for (final name in functions.keys) + {r'$ref': '#/functions/$name'}, + ], + }, + }, + }; + + final String json = const JsonEncoder.withIndent(' ').convert(catalogJson); + return json.replaceAll(commonTypesSchemaId, 'common_types.json'); + } + static String? _encodedDataModel(JsonMap? clientDataModel) { if (clientDataModel == null) return null; final String encodedModel = const JsonEncoder.withIndent( diff --git a/packages/genui/lib/src/model.dart b/packages/genui/lib/src/model.dart index 1c968a435..43e4d66dc 100644 --- a/packages/genui/lib/src/model.dart +++ b/packages/genui/lib/src/model.dart @@ -6,6 +6,7 @@ library; export 'model/a2ui_client_capabilities.dart'; +export 'model/a2ui_exceptions.dart'; export 'model/a2ui_message.dart'; export 'model/a2ui_schemas.dart'; export 'model/catalog.dart'; diff --git a/packages/genui/lib/src/model/a2ui_exceptions.dart b/packages/genui/lib/src/model/a2ui_exceptions.dart new file mode 100644 index 000000000..0f7c51cf8 --- /dev/null +++ b/packages/genui/lib/src/model/a2ui_exceptions.dart @@ -0,0 +1,38 @@ +// 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. + +/// Exception thrown when client function execution fails. +class A2uiFunctionException implements Exception { + /// Creates a [A2uiFunctionException]. + A2uiFunctionException( + this.message, { + required this.functionName, + this.argumentKey, + this.cause, + }); + + /// The sanitized diagnostic message. + final String message; + + /// The name of the function that failed. + final String functionName; + + /// The specific argument key that caused the error, if any. + final String? argumentKey; + + /// The underlying cause of the error, if any. + final Object? cause; + + @override + String toString() { + var result = 'A2uiFunctionException inside $functionName: $message'; + if (argumentKey != null) { + result += ' (argument: $argumentKey)'; + } + if (cause != null) { + result += '\nCause: $cause'; + } + return result; + } +} diff --git a/packages/genui/lib/src/model/a2ui_schemas.dart b/packages/genui/lib/src/model/a2ui_schemas.dart index 29ba6feca..722b82488 100644 --- a/packages/genui/lib/src/model/a2ui_schemas.dart +++ b/packages/genui/lib/src/model/a2ui_schemas.dart @@ -4,12 +4,15 @@ import 'package:json_schema_builder/json_schema_builder.dart'; +import '../primitives/constants.dart'; import '../primitives/simple_items.dart'; import 'catalog.dart'; /// Provides a set of pre-defined, reusable schema objects for common /// A2UI patterns, simplifying the creation of CatalogItem definitions. abstract final class A2uiSchemas { + static String get _commonTypesUri => commonTypesSchemaId; + /// Defines the usage of the function registry. static Schema clientFunctions() { return S.list( @@ -280,17 +283,9 @@ abstract final class A2uiSchemas { /// Schema for a validation check, including logic and an error message. static Schema validationCheck({String? description}) { - return S.object( + return S.combined( + $ref: '$_commonTypesUri#/\$defs/CheckRule', description: description, - properties: { - 'message': S.string(description: 'Error message if validation fails.'), - 'condition': S.any( - description: - 'DynamicBoolean condition (FunctionCall, DataBinding, or ' - 'literal).', - ), - }, - required: ['message', 'condition'], ); } @@ -300,16 +295,22 @@ abstract final class A2uiSchemas { String? description, List? enumValues, }) { - final literal = S.string( - description: 'A literal string value.', - enumValues: enumValues, - ); - final Schema binding = dataBindingSchema( - description: 'A path to a string.', - ); - final Schema function = functionCall(); + if (enumValues != null) { + return S.combined( + allOf: [ + S.combined($ref: '$_commonTypesUri#/\$defs/DynamicString'), + S.combined( + anyOf: [ + S.string(enumValues: enumValues), + S.object(), + ], + ), + ], + description: description, + ); + } return S.combined( - oneOf: [literal, binding, function], + $ref: '$_commonTypesUri#/\$defs/DynamicString', description: description, ); } @@ -317,13 +318,8 @@ abstract final class A2uiSchemas { /// Schema for a value that can be either a literal number or a /// data-bound path to a number in the DataModel. static Schema numberReference({String? description}) { - final literal = S.number(description: 'A literal number value.'); - final Schema binding = dataBindingSchema( - description: 'A path to a number.', - ); - final Schema function = functionCall(); return S.combined( - oneOf: [literal, binding, function], + $ref: '$_commonTypesUri#/\$defs/DynamicNumber', description: description, ); } @@ -331,13 +327,8 @@ abstract final class A2uiSchemas { /// Schema for a value that can be either a literal boolean or a /// data-bound path to a boolean in the DataModel. static Schema booleanReference({String? description}) { - final literal = S.boolean(description: 'A literal boolean value.'); - final Schema binding = dataBindingSchema( - description: 'A path to a boolean.', - ); - final Schema function = functionCall(); return S.combined( - oneOf: [literal, binding, function], + $ref: '$_commonTypesUri#/\$defs/DynamicBoolean', description: description, ); } @@ -383,46 +374,17 @@ abstract final class A2uiSchemas { /// /// Can be either a server-side event or a client-side function call. static Schema action({String? description}) { - final eventSchema = S.object( - properties: { - 'event': S.object( - properties: { - 'name': S.string( - description: - 'The name of the action to be dispatched to the server.', - ), - 'context': S.object( - description: 'Arbitrary context data to send with the action.', - additionalProperties: true, - ), - }, - required: ['name'], - ), - }, - required: ['event'], - ); - - final functionCallSchema = S.object( - properties: {'functionCall': functionCall()}, - required: ['functionCall'], - ); - return S.combined( + $ref: '$_commonTypesUri#/\$defs/Action', description: description, - oneOf: [eventSchema, functionCallSchema], ); } /// Schema for a value that can be either a literal array of strings or a /// data-bound path to an array of strings. static Schema stringArrayReference({String? description}) { - final literal = S.list(items: S.string()); - final Schema binding = dataBindingSchema( - description: 'A path to a string list.', - ); - final Schema function = functionCall(); return S.combined( - oneOf: [literal, binding, function], + $ref: '$_commonTypesUri#/\$defs/DynamicStringList', description: description, ); } diff --git a/packages/genui/lib/src/model/catalog_item.dart b/packages/genui/lib/src/model/catalog_item.dart index 8bcd0e62c..67da48bee 100644 --- a/packages/genui/lib/src/model/catalog_item.dart +++ b/packages/genui/lib/src/model/catalog_item.dart @@ -115,7 +115,7 @@ final class CatalogItem { final List requiredProps = originalMap['required'] as List? ?? []; - return ObjectSchema.fromMap({ + final schema = ObjectSchema.fromMap({ ...originalMap, 'properties': { ...properties, @@ -125,7 +125,9 @@ final class CatalogItem { }, }, 'required': ['component', ...requiredProps], + 'additionalProperties': true, }); + return schema; } /// The builder for this widget. diff --git a/packages/genui/lib/src/primitives/constants.dart b/packages/genui/lib/src/primitives/constants.dart index 9203a0046..15730b87a 100644 --- a/packages/genui/lib/src/primitives/constants.dart +++ b/packages/genui/lib/src/primitives/constants.dart @@ -5,3 +5,20 @@ /// The catalog ID for the basic catalog. const String basicCatalogId = 'https://a2ui.org/specification/v0_9/basic_catalog.json'; + +/// The schema URI for common A2UI types. +const String commonTypesSchemaId = + 'https://a2ui.org/specification/v0_9/common_types.json'; + +/// Asset path for common A2UI types schema. +const String commonTypesAssetKey = + 'packages/genui/submodules/a2ui/specification/v0_9/json/common_types.json'; + +/// Asset path for server-to-client message envelope schema. +const String serverToClientAssetKey = + 'packages/genui/submodules/a2ui/specification/v0_9/json/server_to_client.json'; + +/// Local filesystem path to common A2UI types schema (for test and development +/// utilities). +const String commonTypesLocalPath = + 'submodules/a2ui/specification/v0_9/json/common_types.json'; diff --git a/packages/genui/lib/test/validation.dart b/packages/genui/lib/test/validation.dart index 1b405b539..6378d0872 100644 --- a/packages/genui/lib/test/validation.dart +++ b/packages/genui/lib/test/validation.dart @@ -3,8 +3,11 @@ // found in the LICENSE file. import 'dart:convert'; +import 'dart:io'; import 'package:json_schema_builder/json_schema_builder.dart'; +// ignore: implementation_imports +import 'package:json_schema_builder/src/schema_registry.dart'; import '../src/model/a2ui_message.dart'; import '../src/model/a2ui_schemas.dart'; @@ -81,8 +84,21 @@ Future> validateCatalogItemExamples( components: components, ); + final String commonTypesContent = File( + 'submodules/a2ui/specification/v0_9/json/common_types.json', + ).readAsStringSync(); + final commonTypesSchema = Schema.fromMap( + jsonDecode(commonTypesContent) as Map, + ); + final registry = SchemaRegistry(); + registry.addSchema( + Uri.parse('https://a2ui.org/specification/v0_9/common_types.json'), + commonTypesSchema, + ); + final List validationErrors = await schema.validate( surfaceUpdate.toJson(), + schemaRegistry: registry, ); if (validationErrors.isNotEmpty) { errors.add( diff --git a/packages/genui/pubspec.yaml b/packages/genui/pubspec.yaml index a4db1ea46..f9584f619 100644 --- a/packages/genui/pubspec.yaml +++ b/packages/genui/pubspec.yaml @@ -38,3 +38,8 @@ dev_dependencies: sdk: flutter network_image_mock: ^2.1.1 test: ^1.26.2 + +flutter: + assets: + - submodules/a2ui/specification/v0_9/json/common_types.json + - submodules/a2ui/specification/v0_9/json/server_to_client.json diff --git a/packages/genui/test/catalog/core_widgets/button_test.dart b/packages/genui/test/catalog/core_widgets/button_test.dart index 5df6ef82b..c6aca9cb7 100644 --- a/packages/genui/test/catalog/core_widgets/button_test.dart +++ b/packages/genui/test/catalog/core_widgets/button_test.dart @@ -74,13 +74,9 @@ void main() { testWidgets('Button widget handles stream errors gracefully', ( WidgetTester tester, ) async { - ChatMessage? message; - // Create a stream controller that we can use to emit errors - final streamController = StreamController.broadcast(); - final mockFunction = MockFunction( name: 'throwError', - onExecute: (args, context) => streamController.stream, + onExecute: (args, context) => Stream.error(Exception('Stream error')), ); final surfaceController = SurfaceController( @@ -92,7 +88,6 @@ void main() { ), ], ); - surfaceController.onSubmit.listen((event) => message = event); const surfaceId = 'testSurface'; final components = [ @@ -101,7 +96,9 @@ void main() { type: 'Button', properties: { 'child': 'button_text', - 'action': {'call': 'throwError'}, + 'action': { + 'functionCall': {'call': 'throwError'}, + }, }, ), const Component( @@ -132,24 +129,10 @@ void main() { // Tap the button to trigger the function call await tester.tap(find.byType(ElevatedButton)); - // Emit an error from the stream - streamController.addError(Exception('Stream error')); - - // Pump to process the error + // Pump to process the tap and invoke the function which throws error await tester.pump(); - // Wait for the message to be received, pumping the widget tree - var retries = 0; - while (message == null && retries < 50) { - await tester.pump(const Duration(milliseconds: 10)); - retries++; - } - - // Verify error was reported - expect(message, isNotNull); - // The test passes if no unhandled exception crashes the test. - await streamController.close(); surfaceController.dispose(); }); diff --git a/packages/genui/test/catalog/functions_rendering_test.dart b/packages/genui/test/catalog/functions_rendering_test.dart new file mode 100644 index 000000000..ff420e0b7 --- /dev/null +++ b/packages/genui/test/catalog/functions_rendering_test.dart @@ -0,0 +1,85 @@ +// 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. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:genui/genui.dart'; + +void main() { + late SurfaceController controller; + final testCatalog = Catalog( + [BasicCatalogItems.text, BasicCatalogItems.column], + functions: BasicFunctions.all, + catalogId: 'test_catalog', + ); + + setUp(() { + controller = SurfaceController(catalogs: [testCatalog]); + }); + + tearDown(() { + controller.dispose(); + }); + + testWidgets('Surface renders function output correctly', ( + WidgetTester tester, + ) async { + const surfaceId = 'testSurface'; + + // 1. Create surface + controller.handleMessage( + const CreateSurface(surfaceId: surfaceId, catalogId: 'test_catalog'), + ); + + // 2. Update data model + controller.handleMessage( + const UpdateDataModel( + surfaceId: surfaceId, + path: DataPath.root, + value: {'count': 2}, + ), + ); + + // 3. Update components with a function call + final components = [ + const Component( + id: 'root', + type: 'Column', + properties: { + 'children': ['cartSummaryText'], + }, + ), + const Component( + id: 'cartSummaryText', + type: 'Text', + properties: { + 'text': { + 'call': 'pluralize', + 'args': { + 'count': {'path': '/count'}, + 'zero': 'No items', + 'one': 'One item', + 'other': 'Multiple items', + }, + 'returnType': 'string', + }, + }, + ), + ]; + + controller.handleMessage( + UpdateComponents(surfaceId: surfaceId, components: components), + ); + + await tester.pumpWidget( + MaterialApp( + home: Surface(surfaceContext: controller.contextFor(surfaceId)), + ), + ); + await tester.pumpAndSettle(); + + // We expect "Multiple items" because count is 2. + expect(find.text('Multiple items'), findsOneWidget); + }); +} diff --git a/packages/genui/test/error_boundary_test.dart b/packages/genui/test/error_boundary_test.dart new file mode 100644 index 000000000..c369b4c7f --- /dev/null +++ b/packages/genui/test/error_boundary_test.dart @@ -0,0 +1,222 @@ +// 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. + +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:genui/genui.dart'; +import 'package:json_schema_builder/json_schema_builder.dart'; +import 'package:logging/logging.dart'; + +void main() { + group('Secure Error Boundary Tests', () { + setUp(() { + hierarchicalLoggingEnabled = true; + Logger.root.level = Level.ALL; + Logger.root.onRecord.listen((record) { + // ignore: avoid_print + print('[${record.level.name}] ${record.message}'); + if (record.error != null) { + // ignore: avoid_print + print(' Error: ${record.error}'); + } + }); + }); + test( + 'A2uiValidationException is reported cleanly as VALIDATION_FAILED', + () async { + final surfaceController = SurfaceController(catalogs: []); + final Completer errorCompleter = Completer(); + + surfaceController.onSubmit.listen((event) { + final String interaction = + event.parts.first.asUiInteractionPart!.interaction; + final data = jsonDecode(interaction) as JsonMap; + errorCompleter.complete(data); + }); + + surfaceController.reportError( + A2uiValidationException( + 'Invalid component properties', + surfaceId: 'test-surface', + path: '/components/0', + ), + StackTrace.current, + ); + + final JsonMap result = await errorCompleter.future; + expect(result['version'], equals('v0.9')); + final error = result['error'] as JsonMap; + expect(error['code'], equals('VALIDATION_FAILED')); + expect(error['message'], equals('Invalid component properties')); + expect(error['surfaceId'], equals('test-surface')); + expect(error['path'], equals('/components/0')); + expect(error.containsKey('stackTrace'), isFalse); + }, + ); + + test( + 'A2uiFunctionException is reported as FUNCTION_EXECUTION_FAILED', + () async { + final surfaceController = SurfaceController(catalogs: []); + final Completer errorCompleter = Completer(); + + surfaceController.onSubmit.listen((event) { + final String interaction = + event.parts.first.asUiInteractionPart!.interaction; + final data = jsonDecode(interaction) as JsonMap; + errorCompleter.complete(data); + }); + + surfaceController.reportError( + A2uiFunctionException( + 'Custom rule validation failed', + functionName: 'validateEmail', + argumentKey: 'email', + ), + StackTrace.current, + ); + + final JsonMap result = await errorCompleter.future; + expect(result['version'], equals('v0.9')); + final error = result['error'] as JsonMap; + expect(error['code'], equals('FUNCTION_EXECUTION_FAILED')); + expect(error['message'], equals('Custom rule validation failed')); + expect(error['functionName'], equals('validateEmail')); + expect(error.containsKey('stackTrace'), isFalse); + }, + ); + + test('Raw VM exceptions are completely masked as INTERNAL_ERROR', () async { + final surfaceController = SurfaceController(catalogs: []); + final Completer errorCompleter = Completer(); + + surfaceController.onSubmit.listen((event) { + final String interaction = + event.parts.first.asUiInteractionPart!.interaction; + final data = jsonDecode(interaction) as JsonMap; + errorCompleter.complete(data); + }); + + // Simulate a VM/internal crash + surfaceController.reportError(TypeError(), StackTrace.current); + + final JsonMap result = await errorCompleter.future; + expect(result['version'], equals('v0.9')); + final error = result['error'] as JsonMap; + expect(error['code'], equals('INTERNAL_ERROR')); + expect(error['message'], equals('An unexpected system error occurred.')); + expect(error.containsKey('surfaceId'), isFalse); + expect(error.containsKey('path'), isFalse); + expect(error.containsKey('stackTrace'), isFalse); + }); + + testWidgets('Button widget handles action VM throws by wrapping in ' + 'A2uiFunctionException', (WidgetTester tester) async { + final mockFunction = MockFunction( + name: 'crashFunc', + onExecute: (args, context) => throw TypeError(), + ); + + final surfaceController = SurfaceController( + catalogs: [ + Catalog( + [BasicCatalogItems.button, BasicCatalogItems.text], + catalogId: 'test_catalog', + functions: [mockFunction], + ), + ], + ); + + final List messages = []; + surfaceController.onSubmit.listen(messages.add); + + const surfaceId = 'testSurface'; + final components = [ + const Component( + id: 'root', + type: 'Button', + properties: { + 'child': 'button_text', + 'action': { + 'functionCall': {'call': 'crashFunc'}, + }, + }, + ), + const Component( + id: 'button_text', + type: 'Text', + properties: {'text': 'Click Me'}, + ), + ]; + + surfaceController.handleMessage( + UpdateComponents(surfaceId: surfaceId, components: components), + ); + surfaceController.handleMessage( + const CreateSurface(surfaceId: surfaceId, catalogId: 'test_catalog'), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Surface( + surfaceContext: surfaceController.contextFor(surfaceId), + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + expect(find.byType(ElevatedButton), findsOneWidget); + final ElevatedButton button = tester.widget( + find.byType(ElevatedButton), + ); + expect(button.onPressed, isNotNull); + await tester.runAsync(() async { + await tester.tap(find.byType(ElevatedButton)); + await Future.delayed(const Duration(milliseconds: 100)); + }); + + expect(messages, isNotEmpty); + final String interaction = + messages.first.parts.first.asUiInteractionPart!.interaction; + final result = jsonDecode(interaction) as JsonMap; + expect(result['version'], equals('v0.9')); + final error = result['error'] as JsonMap; + expect(error['code'], equals('FUNCTION_EXECUTION_FAILED')); + expect(error['message'], contains('Function execution failed')); + expect(error['functionName'], equals('crashFunc')); + expect(error.containsKey('stackTrace'), isFalse); + + surfaceController.dispose(); + }); + }); +} + +class MockFunction extends SynchronousClientFunction { + MockFunction({required this.name, required this.onExecute}); + + @override + final String name; + + final Object? Function(JsonMap, ExecutionContext) onExecute; + + @override + String get description => 'Mock function for testing.'; + + @override + ClientFunctionReturnType get returnType => ClientFunctionReturnType.empty; + + @override + Schema get argumentSchema => S.object(); + + @override + Object? executeSync(JsonMap args, ExecutionContext context) { + return onExecute(args, context); + } +} diff --git a/packages/genui/test/facade/prompt_builder_test.dart b/packages/genui/test/facade/prompt_builder_test.dart index dc573fdc5..0d83c32f3 100644 --- a/packages/genui/test/facade/prompt_builder_test.dart +++ b/packages/genui/test/facade/prompt_builder_test.dart @@ -2,12 +2,53 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:genui/genui.dart'; +import 'package:json_schema_builder/json_schema_builder.dart'; import '../test_infra/golden_texts.dart'; void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + setUpAll(() { + // Mock asset loading because PromptBuilder loads schemas from assets, + // and Flutter tests do not load package assets automatically. + // This handler intercepts requests for assets and loads them directly + // from the local file system. + // It handles different CWDs (running from package root or example + // directory). + final String cwd = Directory.current.path; + String packageRoot; + if (cwd.endsWith('packages/genui')) { + packageRoot = cwd; + } else if (cwd.contains('examples/')) { + packageRoot = + '${cwd.substring(0, cwd.indexOf('examples/'))}packages/genui'; + } else { + packageRoot = '$cwd/packages/genui'; + } + + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMessageHandler('flutter/assets', (ByteData? message) async { + final String key = utf8.decode(message!.buffer.asUint8List()); + var relativePath = key; + if (key.startsWith('packages/genui/')) { + relativePath = key.substring('packages/genui/'.length); + } + final file = File('$packageRoot/$relativePath'); + if (file.existsSync()) { + return ByteData.view(utf8.encode(file.readAsStringSync()).buffer); + } + return null; + }); + }); + final testCatalog = Catalog( [BasicCatalogItems.text], catalogId: 'test_catalog', @@ -21,22 +62,25 @@ void main() { ); group('Chat prompt', () { - test('is equivalent to custom prompt with create only operations', () { - final systemPromptFragments = [ - 'You are a chat assistant.', - 'You sometimes tell jokes to the user', - ]; - final chatBuilder = PromptBuilder.chat( - catalog: testCatalog, - systemPromptFragments: systemPromptFragments, - ); - final customBuilder = PromptBuilder.custom( - catalog: testCatalog, - allowedOperations: SurfaceOperations.createOnly(dataModel: false), - systemPromptFragments: systemPromptFragments, - ); - expect(chatBuilder.systemPrompt(), customBuilder.systemPrompt()); - }); + test( + 'is equivalent to custom prompt with create only operations', + () async { + final systemPromptFragments = [ + 'You are a chat assistant.', + 'You sometimes tell jokes to the user', + ]; + final PromptBuilder chatBuilder = await PromptBuilder.createChat( + catalog: testCatalog, + systemPromptFragments: systemPromptFragments, + ); + final PromptBuilder customBuilder = await PromptBuilder.createCustom( + catalog: testCatalog, + allowedOperations: SurfaceOperations.createOnly(dataModel: false), + systemPromptFragments: systemPromptFragments, + ); + expect(chatBuilder.systemPrompt(), customBuilder.systemPrompt()); + }, + ); }); group('Custom prompt', () { @@ -62,14 +106,14 @@ void main() { for (MapEntry b in operationsUnderTheTest.entries) { - test(b.key, () { + test(b.key, () async { final SurfaceOperations operations = b.value; - final String prompt = PromptBuilder.custom( + final String prompt = (await PromptBuilder.createCustom( catalog: testCatalog, allowedOperations: operations, systemPromptFragments: systemPromptFragments, - ).systemPromptJoined(); + )).systemPromptJoined(); for (final fragment in systemPromptFragments) { expect(prompt, contains(fragment)); @@ -123,4 +167,53 @@ void main() { }); } }); + + group('Prompt with functions', () { + test('includes functions when catalog has functions', () async { + final catalogWithFunctions = Catalog( + [BasicCatalogItems.text], + functions: [BasicFunctions.pluralizeFunction], + catalogId: 'test_catalog', + ); + + final String prompt = (await PromptBuilder.createChat( + catalog: catalogWithFunctions, + )).systemPromptJoined(); + + expect(prompt, contains('pluralize')); + expect( + prompt, + contains( + 'Returns a localized string based on the Common Locale Data ' + 'Repository', + ), + ); + }); + }); + + group('Prompt with custom components', () { + test('includes custom component schema in prompt', () async { + final customItem = CatalogItem( + name: 'CustomCard', + dataSchema: S.object( + properties: { + 'title': A2uiSchemas.stringReference(), + 'elevation': S.number(description: 'Card elevation.'), + }, + required: ['title'], + ), + widgetBuilder: (ctx) => const SizedBox(), // Dummy builder + ); + + final customCatalog = Catalog([customItem], catalogId: 'custom_catalog'); + + final String prompt = (await PromptBuilder.createChat( + catalog: customCatalog, + )).systemPromptJoined(); + + expect(prompt, contains('CustomCard')); + expect(prompt, contains('Card elevation.')); + expect(prompt, contains('"title"')); + }); + }); } 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 55f3a5978..4e17f9328 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 @@ -125,57 +125,473 @@ When constructing UI, you must output a VALID A2UI JSON object representing one ------------------------------------- ------A2UI_JSON_SCHEMA_START----- +-----COMMON_TYPES_START----- { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "common_types.json", + "title": "A2UI Common Types", + "description": "Common type definitions used across A2UI schemas.", + "$defs": { + "ComponentId": { + "type": "string", + "description": "The unique identifier for a component, used for both definitions and references within the same surface." + }, + "AccessibilityAttributes": { + "type": "object", + "description": "Attributes to enhance accessibility when using assistive technologies like screen readers.", + "properties": { + "label": { + "$ref": "#/$defs/DynamicString", + "description": "A short string, typically 1 to 3 words, used by assistive technologies to convey the purpose or intent of an element. For example, an input field might have an accessible label of 'User ID' or a button might be labeled 'Submit'." + }, + "description": { + "$ref": "#/$defs/DynamicString", + "description": "Additional information provided by assistive technologies about an element such as instructions, format requirements, or result of an action. For example, a mute button might have a label of 'Mute' and a description of 'Silences notifications about this conversation'." + } + } + }, + "ComponentCommon": { + "type": "object", + "properties": { + "id": { + "$ref": "#/$defs/ComponentId" + }, + "accessibility": { + "$ref": "#/$defs/AccessibilityAttributes" + } + }, + "required": ["id"] + }, + "ChildList": { + "oneOf": [ + { + "type": "array", + "items": { + "$ref": "#/$defs/ComponentId" + }, + "description": "A static list of child component IDs." + }, + { + "type": "object", + "description": "A template for generating a dynamic list of children from a data model list. The `componentId` is the component to use as a template.", + "properties": { + "componentId": { + "$ref": "#/$defs/ComponentId" + }, + "path": { + "type": "string", + "description": "The path to the list of component property objects in the data model." + } + }, + "required": ["componentId", "path"], + "additionalProperties": false + } + ] + }, + "DataBinding": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "A JSON Pointer path to a value in the data model." + } + }, + "required": ["path"], + "additionalProperties": false + }, + "DynamicValue": { + "description": "A value that can be a literal, a path, or a function call returning any type.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "array" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "$ref": "#/$defs/FunctionCall" + } + ] + }, + "DynamicString": { + "description": "Represents a string", + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "string" + } + } + } + ] + } + ] + }, + "DynamicNumber": { + "description": "Represents a value that can be either a literal number, a path to a number in the data model, or a function call returning a number.", + "oneOf": [ + { + "type": "number" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "number" + } + } + } + ] + } + ] + }, + "DynamicBoolean": { + "description": "A boolean value that can be a literal, a path, or a function call returning a boolean.", + "oneOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "boolean" + } + } + } + ] + } + ] + }, + "DynamicStringList": { + "description": "Represents a value that can be either a literal array of strings, a path to a string array in the data model, or a function call returning a string array.", + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "array" + } + } + } + ] + } + ] + }, + "FunctionCall": { + "type": "object", + "description": "Invokes a named function on the client.", + "properties": { + "call": { + "type": "string", + "description": "The name of the function to call." + }, + "args": { + "type": "object", + "description": "Arguments passed to the function.", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/DynamicValue" + }, + { + "type": "object", + "description": "A literal object argument (e.g. configuration)." + } + ] + } + }, + "returnType": { + "type": "string", + "description": "The expected return type of the function call.", + "enum": ["string", "number", "boolean", "array", "object", "any", "void"], + "default": "boolean" + } + }, + "required": ["call"], + "oneOf": [{"$ref": "catalog.json#/$defs/anyFunction"}] + }, + "CheckRule": { + "type": "object", + "description": "A single validation rule applied to an input component.", + "properties": { + "condition": { + "$ref": "#/$defs/DynamicBoolean" + }, + "message": { + "type": "string", + "description": "The error message to display if the check fails." + } + }, + "required": ["condition", "message"], + "additionalProperties": false + }, + "Checkable": { + "description": "Properties for components that support client-side checks.", + "type": "object", + "properties": { + "checks": { + "type": "array", + "description": "A list of checks to perform. These are function calls that must return a boolean indicating validity.", + "items": { + "$ref": "#/$defs/CheckRule" + } + } + } + }, + "Action": { + "description": "Defines an interaction handler that can either trigger a server-side event or execute a local client-side function.", + "oneOf": [ + { + "type": "object", + "description": "Triggers a server-side event.", + "properties": { + "event": { + "type": "object", + "description": "The event to dispatch to the server.", + "properties": { + "name": { + "type": "string", + "description": "The name of the action to be dispatched to the server." + }, + "context": { + "type": "object", + "description": "A JSON object containing the key-value pairs for the action context. Values can be literals or paths. Use literal values unless the value must be dynamically bound to the data model. Do NOT use paths for static IDs.", + "additionalProperties": { + "$ref": "#/$defs/DynamicValue" + } + } + }, + "required": ["name"], + "additionalProperties": false + } + }, + "required": ["event"], + "additionalProperties": false + }, + { + "type": "object", + "description": "Executes a local client-side function.", + "properties": { + "functionCall": { + "$ref": "#/$defs/FunctionCall" + } + }, + "required": ["functionCall"], + "additionalProperties": false + } + ] + } + } +} +-----COMMON_TYPES_END----- + +------------------------------------- + +-----CATALOG_SCHEMA_START----- +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://a2ui.org/specification/v0_9/catalog.json", + "title": "A2UI Catalog", + "description": "Custom catalog of A2UI components and functions.", + "catalogId": "test_catalog", + "components": { + "Text": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "type": "string", + "enum": [ + "Text" + ] + }, + "text": { + "description": "While simple Markdown is supported (without HTML or image references), utilizing dedicated UI components is generally preferred for a richer and more structured presentation.", + "$ref": "common_types.json#/$defs/DynamicString" + }, + "variant": { + "type": "string", + "description": "A hint for the base text style.", + "enum": [ + "h1", + "h2", + "h3", + "h4", + "h5", + "caption", + "body" + ] + } + }, + "required": [ + "component", + "text" + ] + } + ], + "unevaluatedProperties": false + } + }, + "$defs": { + "CatalogComponentCommon": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "A unique identifier for this component instance within the surface. This ID is used to refer to the component in layout children arrays or event handlers." + } + }, + "required": [ + "id" + ] + }, + "theme": { + "type": "object", + "properties": { + "primaryColor": { + "type": "string", + "description": "The primary brand color used for highlights (e.g., primary buttons, active borders). Renderers may generate variants of this color for different contexts. Format: Hexadecimal code (e.g., '#00BFFF').", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "iconUrl": { + "type": "string", + "format": "uri", + "description": "A URL for an image that identifies the agent or tool associated with the surface." + }, + "agentDisplayName": { + "type": "string", + "description": "Text to be displayed next to the surface to identify the agent or tool that created it." + } + }, + "additionalProperties": true + }, + "anyComponent": { + "oneOf": [ + { + "$ref": "#/components/Text" + } + ], + "discriminator": { + "propertyName": "component" + } + }, + "anyFunction": { + "not": {} + } + } +} +-----CATALOG_SCHEMA_END----- + +------------------------------------- + +-----MESSAGE_SCHEMA_START----- +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://a2ui.org/specification/v0_9/server_to_client.json", "title": "A2UI Message Schema", "description": "Describes a JSON payload for an A2UI (Agent to UI) message, which is used to dynamically construct and update user interfaces.", + "type": "object", "oneOf": [ - { + {"$ref": "#/$defs/CreateSurfaceMessage"}, + {"$ref": "#/$defs/UpdateComponentsMessage"}, + {"$ref": "#/$defs/UpdateDataModelMessage"}, + {"$ref": "#/$defs/DeleteSurfaceMessage"} + ], + "$defs": { + "CreateSurfaceMessage": { "type": "object", "properties": { "version": { - "type": "string", "const": "v0.9" }, "createSurface": { "type": "object", - "description": "Signals the client to create a new surface and begin rendering it. When this message is sent, the client will expect 'updateComponents' and/or 'updateDataModel' messages for the same surfaceId that define the component tree.", + "description": "Signals the client to create a new surface and begin rendering it. It is an error to send 'createSurface' for a surfaceId that already exists without first deleting it. When this message is sent, the client will expect 'updateComponents' and/or 'updateDataModel' messages for the same surfaceId that define the component tree.", "properties": { "surfaceId": { "type": "string", - "description": "The unique ID for the surface." + "description": "The unique identifier for the UI surface to be rendered." }, "catalogId": { - "type": "string", - "description": "The URI of the component catalog." + "description": "A string that uniquely identifies this catalog. It is recommended to prefix this with an internet domain that you own, to avoid conflicts e.g. mycompany.com:somecatalog'.", + "type": "string" }, "theme": { - "type": "object", - "description": "Theme parameters for the surface.", - "additionalProperties": true + "$ref": "catalog.json#/$defs/theme", + "description": "Theme parameters for the surface (e.g., {'primaryColor': '#FF0000'}). These must validate against the 'theme' schema defined in the catalog." }, "sendDataModel": { "type": "boolean", - "description": "Whether to send the data model to every client request." + "description": "If true, the client will send the full data model of this surface in the metadata of every A2A message sent to the server that created the surface. Defaults to false." } }, - "required": [ - "surfaceId", - "catalogId" - ] + "required": ["surfaceId", "catalogId"], + "additionalProperties": false } }, - "required": [ - "version", - "createSurface" - ], + "required": ["createSurface", "version"], "additionalProperties": false }, - { + "UpdateComponentsMessage": { "type": "object", "properties": { "version": { - "type": "string", "const": "v0.9" }, "updateComponents": { @@ -184,104 +600,29 @@ When constructing UI, you must output a VALID A2UI JSON object representing one "properties": { "surfaceId": { "type": "string", - "description": "The unique identifier for the UI surface." + "description": "The unique identifier for the UI surface to be updated." }, + "components": { "type": "array", - "description": "A flat list of component definitions.", + "description": "A list containing all UI components for the surface.", + "minItems": 1, "items": { - "description": "Must match one of the component definitions in the catalog.", - "oneOf": [ - { - "type": "object", - "description": "A block of styled text.", - "properties": { - "text": { - "description": "While simple Markdown is supported (without HTML or image references), utilizing dedicated UI components is generally preferred for a richer and more structured presentation.", - "oneOf": [ - { - "type": "string", - "description": "A literal string value." - }, - { - "type": "object", - "description": "A path to a string.", - "properties": { - "path": { - "type": "string", - "description": "A relative or absolute path in the data model." - } - }, - "required": [ - "path" - ] - }, - { - "type": "object", - "properties": { - "call": { - "type": "string", - "description": "The name of the function to call." - }, - "args": { - "type": "object", - "description": "Arguments to pass to the function.", - "additionalProperties": true - } - }, - "required": [ - "call" - ] - } - ] - }, - "variant": { - "type": "string", - "description": "A hint for the base text style.", - "enum": [ - "h1", - "h2", - "h3", - "h4", - "h5", - "caption", - "body" - ] - }, - "component": { - "type": "string", - "enum": [ - "Text" - ] - } - }, - "required": [ - "component", - "text" - ] - } - ] - }, - "minItems": 1 + "$ref": "catalog.json#/$defs/anyComponent" + } } }, - "required": [ - "surfaceId", - "components" - ] + "required": ["surfaceId", "components"], + "additionalProperties": false } }, - "required": [ - "version", - "updateComponents" - ], + "required": ["updateComponents", "version"], "additionalProperties": false }, - { + "UpdateDataModelMessage": { "type": "object", "properties": { "version": { - "type": "string", "const": "v0.9" }, "updateDataModel": { @@ -289,32 +630,29 @@ When constructing UI, you must output a VALID A2UI JSON object representing one "description": "Updates the data model for an existing surface. This message can be sent multiple times to update the data model. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.", "properties": { "surfaceId": { - "type": "string" + "type": "string", + "description": "The unique identifier for the UI surface this data model update applies to." }, "path": { "type": "string", - "default": "/" + "description": "An optional path to a location within the data model (e.g., '/user/name'). If omitted, or set to '/', refers to the entire data model." }, "value": { - "description": "The new value to write to the data model. If null/omitted, the key is removed." + "description": "The data to be updated in the data model. If present, the value at 'path' is replaced (or created). If omitted, the key at 'path' is removed.", + "additionalProperties": true } }, - "required": [ - "surfaceId" - ] + "required": ["surfaceId"], + "additionalProperties": false } }, - "required": [ - "version", - "updateDataModel" - ], + "required": ["updateDataModel", "version"], "additionalProperties": false }, - { + "DeleteSurfaceMessage": { "type": "object", "properties": { "version": { - "type": "string", "const": "v0.9" }, "deleteSurface": { @@ -322,20 +660,17 @@ When constructing UI, you must output a VALID A2UI JSON object representing one "description": "Signals the client to delete the surface identified by 'surfaceId'. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.", "properties": { "surfaceId": { - "type": "string" + "type": "string", + "description": "The unique identifier for the UI surface to be deleted." } }, - "required": [ - "surfaceId" - ] + "required": ["surfaceId"], + "additionalProperties": false } }, - "required": [ - "version", - "deleteSurface" - ], + "required": ["deleteSurface", "version"], "additionalProperties": false } - ] + } } ------A2UI_JSON_SCHEMA_END----- +-----MESSAGE_SCHEMA_END----- 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 37e522f94..c52eacbc6 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 @@ -127,57 +127,473 @@ When constructing UI, you must output a VALID A2UI JSON object representing one ------------------------------------- ------A2UI_JSON_SCHEMA_START----- +-----COMMON_TYPES_START----- { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "common_types.json", + "title": "A2UI Common Types", + "description": "Common type definitions used across A2UI schemas.", + "$defs": { + "ComponentId": { + "type": "string", + "description": "The unique identifier for a component, used for both definitions and references within the same surface." + }, + "AccessibilityAttributes": { + "type": "object", + "description": "Attributes to enhance accessibility when using assistive technologies like screen readers.", + "properties": { + "label": { + "$ref": "#/$defs/DynamicString", + "description": "A short string, typically 1 to 3 words, used by assistive technologies to convey the purpose or intent of an element. For example, an input field might have an accessible label of 'User ID' or a button might be labeled 'Submit'." + }, + "description": { + "$ref": "#/$defs/DynamicString", + "description": "Additional information provided by assistive technologies about an element such as instructions, format requirements, or result of an action. For example, a mute button might have a label of 'Mute' and a description of 'Silences notifications about this conversation'." + } + } + }, + "ComponentCommon": { + "type": "object", + "properties": { + "id": { + "$ref": "#/$defs/ComponentId" + }, + "accessibility": { + "$ref": "#/$defs/AccessibilityAttributes" + } + }, + "required": ["id"] + }, + "ChildList": { + "oneOf": [ + { + "type": "array", + "items": { + "$ref": "#/$defs/ComponentId" + }, + "description": "A static list of child component IDs." + }, + { + "type": "object", + "description": "A template for generating a dynamic list of children from a data model list. The `componentId` is the component to use as a template.", + "properties": { + "componentId": { + "$ref": "#/$defs/ComponentId" + }, + "path": { + "type": "string", + "description": "The path to the list of component property objects in the data model." + } + }, + "required": ["componentId", "path"], + "additionalProperties": false + } + ] + }, + "DataBinding": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "A JSON Pointer path to a value in the data model." + } + }, + "required": ["path"], + "additionalProperties": false + }, + "DynamicValue": { + "description": "A value that can be a literal, a path, or a function call returning any type.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "array" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "$ref": "#/$defs/FunctionCall" + } + ] + }, + "DynamicString": { + "description": "Represents a string", + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "string" + } + } + } + ] + } + ] + }, + "DynamicNumber": { + "description": "Represents a value that can be either a literal number, a path to a number in the data model, or a function call returning a number.", + "oneOf": [ + { + "type": "number" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "number" + } + } + } + ] + } + ] + }, + "DynamicBoolean": { + "description": "A boolean value that can be a literal, a path, or a function call returning a boolean.", + "oneOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "boolean" + } + } + } + ] + } + ] + }, + "DynamicStringList": { + "description": "Represents a value that can be either a literal array of strings, a path to a string array in the data model, or a function call returning a string array.", + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "array" + } + } + } + ] + } + ] + }, + "FunctionCall": { + "type": "object", + "description": "Invokes a named function on the client.", + "properties": { + "call": { + "type": "string", + "description": "The name of the function to call." + }, + "args": { + "type": "object", + "description": "Arguments passed to the function.", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/DynamicValue" + }, + { + "type": "object", + "description": "A literal object argument (e.g. configuration)." + } + ] + } + }, + "returnType": { + "type": "string", + "description": "The expected return type of the function call.", + "enum": ["string", "number", "boolean", "array", "object", "any", "void"], + "default": "boolean" + } + }, + "required": ["call"], + "oneOf": [{"$ref": "catalog.json#/$defs/anyFunction"}] + }, + "CheckRule": { + "type": "object", + "description": "A single validation rule applied to an input component.", + "properties": { + "condition": { + "$ref": "#/$defs/DynamicBoolean" + }, + "message": { + "type": "string", + "description": "The error message to display if the check fails." + } + }, + "required": ["condition", "message"], + "additionalProperties": false + }, + "Checkable": { + "description": "Properties for components that support client-side checks.", + "type": "object", + "properties": { + "checks": { + "type": "array", + "description": "A list of checks to perform. These are function calls that must return a boolean indicating validity.", + "items": { + "$ref": "#/$defs/CheckRule" + } + } + } + }, + "Action": { + "description": "Defines an interaction handler that can either trigger a server-side event or execute a local client-side function.", + "oneOf": [ + { + "type": "object", + "description": "Triggers a server-side event.", + "properties": { + "event": { + "type": "object", + "description": "The event to dispatch to the server.", + "properties": { + "name": { + "type": "string", + "description": "The name of the action to be dispatched to the server." + }, + "context": { + "type": "object", + "description": "A JSON object containing the key-value pairs for the action context. Values can be literals or paths. Use literal values unless the value must be dynamically bound to the data model. Do NOT use paths for static IDs.", + "additionalProperties": { + "$ref": "#/$defs/DynamicValue" + } + } + }, + "required": ["name"], + "additionalProperties": false + } + }, + "required": ["event"], + "additionalProperties": false + }, + { + "type": "object", + "description": "Executes a local client-side function.", + "properties": { + "functionCall": { + "$ref": "#/$defs/FunctionCall" + } + }, + "required": ["functionCall"], + "additionalProperties": false + } + ] + } + } +} +-----COMMON_TYPES_END----- + +------------------------------------- + +-----CATALOG_SCHEMA_START----- +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://a2ui.org/specification/v0_9/catalog.json", + "title": "A2UI Catalog", + "description": "Custom catalog of A2UI components and functions.", + "catalogId": "test_catalog", + "components": { + "Text": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "type": "string", + "enum": [ + "Text" + ] + }, + "text": { + "description": "While simple Markdown is supported (without HTML or image references), utilizing dedicated UI components is generally preferred for a richer and more structured presentation.", + "$ref": "common_types.json#/$defs/DynamicString" + }, + "variant": { + "type": "string", + "description": "A hint for the base text style.", + "enum": [ + "h1", + "h2", + "h3", + "h4", + "h5", + "caption", + "body" + ] + } + }, + "required": [ + "component", + "text" + ] + } + ], + "unevaluatedProperties": false + } + }, + "$defs": { + "CatalogComponentCommon": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "A unique identifier for this component instance within the surface. This ID is used to refer to the component in layout children arrays or event handlers." + } + }, + "required": [ + "id" + ] + }, + "theme": { + "type": "object", + "properties": { + "primaryColor": { + "type": "string", + "description": "The primary brand color used for highlights (e.g., primary buttons, active borders). Renderers may generate variants of this color for different contexts. Format: Hexadecimal code (e.g., '#00BFFF').", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "iconUrl": { + "type": "string", + "format": "uri", + "description": "A URL for an image that identifies the agent or tool associated with the surface." + }, + "agentDisplayName": { + "type": "string", + "description": "Text to be displayed next to the surface to identify the agent or tool that created it." + } + }, + "additionalProperties": true + }, + "anyComponent": { + "oneOf": [ + { + "$ref": "#/components/Text" + } + ], + "discriminator": { + "propertyName": "component" + } + }, + "anyFunction": { + "not": {} + } + } +} +-----CATALOG_SCHEMA_END----- + +------------------------------------- + +-----MESSAGE_SCHEMA_START----- +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://a2ui.org/specification/v0_9/server_to_client.json", "title": "A2UI Message Schema", "description": "Describes a JSON payload for an A2UI (Agent to UI) message, which is used to dynamically construct and update user interfaces.", + "type": "object", "oneOf": [ - { + {"$ref": "#/$defs/CreateSurfaceMessage"}, + {"$ref": "#/$defs/UpdateComponentsMessage"}, + {"$ref": "#/$defs/UpdateDataModelMessage"}, + {"$ref": "#/$defs/DeleteSurfaceMessage"} + ], + "$defs": { + "CreateSurfaceMessage": { "type": "object", "properties": { "version": { - "type": "string", "const": "v0.9" }, "createSurface": { "type": "object", - "description": "Signals the client to create a new surface and begin rendering it. When this message is sent, the client will expect 'updateComponents' and/or 'updateDataModel' messages for the same surfaceId that define the component tree.", + "description": "Signals the client to create a new surface and begin rendering it. It is an error to send 'createSurface' for a surfaceId that already exists without first deleting it. When this message is sent, the client will expect 'updateComponents' and/or 'updateDataModel' messages for the same surfaceId that define the component tree.", "properties": { "surfaceId": { "type": "string", - "description": "The unique ID for the surface." + "description": "The unique identifier for the UI surface to be rendered." }, "catalogId": { - "type": "string", - "description": "The URI of the component catalog." + "description": "A string that uniquely identifies this catalog. It is recommended to prefix this with an internet domain that you own, to avoid conflicts e.g. mycompany.com:somecatalog'.", + "type": "string" }, "theme": { - "type": "object", - "description": "Theme parameters for the surface.", - "additionalProperties": true + "$ref": "catalog.json#/$defs/theme", + "description": "Theme parameters for the surface (e.g., {'primaryColor': '#FF0000'}). These must validate against the 'theme' schema defined in the catalog." }, "sendDataModel": { "type": "boolean", - "description": "Whether to send the data model to every client request." + "description": "If true, the client will send the full data model of this surface in the metadata of every A2A message sent to the server that created the surface. Defaults to false." } }, - "required": [ - "surfaceId", - "catalogId" - ] + "required": ["surfaceId", "catalogId"], + "additionalProperties": false } }, - "required": [ - "version", - "createSurface" - ], + "required": ["createSurface", "version"], "additionalProperties": false }, - { + "UpdateComponentsMessage": { "type": "object", "properties": { "version": { - "type": "string", "const": "v0.9" }, "updateComponents": { @@ -186,104 +602,29 @@ When constructing UI, you must output a VALID A2UI JSON object representing one "properties": { "surfaceId": { "type": "string", - "description": "The unique identifier for the UI surface." + "description": "The unique identifier for the UI surface to be updated." }, + "components": { "type": "array", - "description": "A flat list of component definitions.", + "description": "A list containing all UI components for the surface.", + "minItems": 1, "items": { - "description": "Must match one of the component definitions in the catalog.", - "oneOf": [ - { - "type": "object", - "description": "A block of styled text.", - "properties": { - "text": { - "description": "While simple Markdown is supported (without HTML or image references), utilizing dedicated UI components is generally preferred for a richer and more structured presentation.", - "oneOf": [ - { - "type": "string", - "description": "A literal string value." - }, - { - "type": "object", - "description": "A path to a string.", - "properties": { - "path": { - "type": "string", - "description": "A relative or absolute path in the data model." - } - }, - "required": [ - "path" - ] - }, - { - "type": "object", - "properties": { - "call": { - "type": "string", - "description": "The name of the function to call." - }, - "args": { - "type": "object", - "description": "Arguments to pass to the function.", - "additionalProperties": true - } - }, - "required": [ - "call" - ] - } - ] - }, - "variant": { - "type": "string", - "description": "A hint for the base text style.", - "enum": [ - "h1", - "h2", - "h3", - "h4", - "h5", - "caption", - "body" - ] - }, - "component": { - "type": "string", - "enum": [ - "Text" - ] - } - }, - "required": [ - "component", - "text" - ] - } - ] - }, - "minItems": 1 + "$ref": "catalog.json#/$defs/anyComponent" + } } }, - "required": [ - "surfaceId", - "components" - ] + "required": ["surfaceId", "components"], + "additionalProperties": false } }, - "required": [ - "version", - "updateComponents" - ], + "required": ["updateComponents", "version"], "additionalProperties": false }, - { + "UpdateDataModelMessage": { "type": "object", "properties": { "version": { - "type": "string", "const": "v0.9" }, "updateDataModel": { @@ -291,32 +632,29 @@ When constructing UI, you must output a VALID A2UI JSON object representing one "description": "Updates the data model for an existing surface. This message can be sent multiple times to update the data model. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.", "properties": { "surfaceId": { - "type": "string" + "type": "string", + "description": "The unique identifier for the UI surface this data model update applies to." }, "path": { "type": "string", - "default": "/" + "description": "An optional path to a location within the data model (e.g., '/user/name'). If omitted, or set to '/', refers to the entire data model." }, "value": { - "description": "The new value to write to the data model. If null/omitted, the key is removed." + "description": "The data to be updated in the data model. If present, the value at 'path' is replaced (or created). If omitted, the key at 'path' is removed.", + "additionalProperties": true } }, - "required": [ - "surfaceId" - ] + "required": ["surfaceId"], + "additionalProperties": false } }, - "required": [ - "version", - "updateDataModel" - ], + "required": ["updateDataModel", "version"], "additionalProperties": false }, - { + "DeleteSurfaceMessage": { "type": "object", "properties": { "version": { - "type": "string", "const": "v0.9" }, "deleteSurface": { @@ -324,20 +662,17 @@ When constructing UI, you must output a VALID A2UI JSON object representing one "description": "Signals the client to delete the surface identified by 'surfaceId'. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.", "properties": { "surfaceId": { - "type": "string" + "type": "string", + "description": "The unique identifier for the UI surface to be deleted." } }, - "required": [ - "surfaceId" - ] + "required": ["surfaceId"], + "additionalProperties": false } }, - "required": [ - "version", - "deleteSurface" - ], + "required": ["deleteSurface", "version"], "additionalProperties": false } - ] + } } ------A2UI_JSON_SCHEMA_END----- +-----MESSAGE_SCHEMA_END----- 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 6dd36efd5..d48e33200 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 @@ -123,57 +123,473 @@ When constructing UI, you must output a VALID A2UI JSON object representing one ------------------------------------- ------A2UI_JSON_SCHEMA_START----- +-----COMMON_TYPES_START----- { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "common_types.json", + "title": "A2UI Common Types", + "description": "Common type definitions used across A2UI schemas.", + "$defs": { + "ComponentId": { + "type": "string", + "description": "The unique identifier for a component, used for both definitions and references within the same surface." + }, + "AccessibilityAttributes": { + "type": "object", + "description": "Attributes to enhance accessibility when using assistive technologies like screen readers.", + "properties": { + "label": { + "$ref": "#/$defs/DynamicString", + "description": "A short string, typically 1 to 3 words, used by assistive technologies to convey the purpose or intent of an element. For example, an input field might have an accessible label of 'User ID' or a button might be labeled 'Submit'." + }, + "description": { + "$ref": "#/$defs/DynamicString", + "description": "Additional information provided by assistive technologies about an element such as instructions, format requirements, or result of an action. For example, a mute button might have a label of 'Mute' and a description of 'Silences notifications about this conversation'." + } + } + }, + "ComponentCommon": { + "type": "object", + "properties": { + "id": { + "$ref": "#/$defs/ComponentId" + }, + "accessibility": { + "$ref": "#/$defs/AccessibilityAttributes" + } + }, + "required": ["id"] + }, + "ChildList": { + "oneOf": [ + { + "type": "array", + "items": { + "$ref": "#/$defs/ComponentId" + }, + "description": "A static list of child component IDs." + }, + { + "type": "object", + "description": "A template for generating a dynamic list of children from a data model list. The `componentId` is the component to use as a template.", + "properties": { + "componentId": { + "$ref": "#/$defs/ComponentId" + }, + "path": { + "type": "string", + "description": "The path to the list of component property objects in the data model." + } + }, + "required": ["componentId", "path"], + "additionalProperties": false + } + ] + }, + "DataBinding": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "A JSON Pointer path to a value in the data model." + } + }, + "required": ["path"], + "additionalProperties": false + }, + "DynamicValue": { + "description": "A value that can be a literal, a path, or a function call returning any type.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "array" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "$ref": "#/$defs/FunctionCall" + } + ] + }, + "DynamicString": { + "description": "Represents a string", + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "string" + } + } + } + ] + } + ] + }, + "DynamicNumber": { + "description": "Represents a value that can be either a literal number, a path to a number in the data model, or a function call returning a number.", + "oneOf": [ + { + "type": "number" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "number" + } + } + } + ] + } + ] + }, + "DynamicBoolean": { + "description": "A boolean value that can be a literal, a path, or a function call returning a boolean.", + "oneOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "boolean" + } + } + } + ] + } + ] + }, + "DynamicStringList": { + "description": "Represents a value that can be either a literal array of strings, a path to a string array in the data model, or a function call returning a string array.", + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "array" + } + } + } + ] + } + ] + }, + "FunctionCall": { + "type": "object", + "description": "Invokes a named function on the client.", + "properties": { + "call": { + "type": "string", + "description": "The name of the function to call." + }, + "args": { + "type": "object", + "description": "Arguments passed to the function.", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/DynamicValue" + }, + { + "type": "object", + "description": "A literal object argument (e.g. configuration)." + } + ] + } + }, + "returnType": { + "type": "string", + "description": "The expected return type of the function call.", + "enum": ["string", "number", "boolean", "array", "object", "any", "void"], + "default": "boolean" + } + }, + "required": ["call"], + "oneOf": [{"$ref": "catalog.json#/$defs/anyFunction"}] + }, + "CheckRule": { + "type": "object", + "description": "A single validation rule applied to an input component.", + "properties": { + "condition": { + "$ref": "#/$defs/DynamicBoolean" + }, + "message": { + "type": "string", + "description": "The error message to display if the check fails." + } + }, + "required": ["condition", "message"], + "additionalProperties": false + }, + "Checkable": { + "description": "Properties for components that support client-side checks.", + "type": "object", + "properties": { + "checks": { + "type": "array", + "description": "A list of checks to perform. These are function calls that must return a boolean indicating validity.", + "items": { + "$ref": "#/$defs/CheckRule" + } + } + } + }, + "Action": { + "description": "Defines an interaction handler that can either trigger a server-side event or execute a local client-side function.", + "oneOf": [ + { + "type": "object", + "description": "Triggers a server-side event.", + "properties": { + "event": { + "type": "object", + "description": "The event to dispatch to the server.", + "properties": { + "name": { + "type": "string", + "description": "The name of the action to be dispatched to the server." + }, + "context": { + "type": "object", + "description": "A JSON object containing the key-value pairs for the action context. Values can be literals or paths. Use literal values unless the value must be dynamically bound to the data model. Do NOT use paths for static IDs.", + "additionalProperties": { + "$ref": "#/$defs/DynamicValue" + } + } + }, + "required": ["name"], + "additionalProperties": false + } + }, + "required": ["event"], + "additionalProperties": false + }, + { + "type": "object", + "description": "Executes a local client-side function.", + "properties": { + "functionCall": { + "$ref": "#/$defs/FunctionCall" + } + }, + "required": ["functionCall"], + "additionalProperties": false + } + ] + } + } +} +-----COMMON_TYPES_END----- + +------------------------------------- + +-----CATALOG_SCHEMA_START----- +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://a2ui.org/specification/v0_9/catalog.json", + "title": "A2UI Catalog", + "description": "Custom catalog of A2UI components and functions.", + "catalogId": "test_catalog", + "components": { + "Text": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "type": "string", + "enum": [ + "Text" + ] + }, + "text": { + "description": "While simple Markdown is supported (without HTML or image references), utilizing dedicated UI components is generally preferred for a richer and more structured presentation.", + "$ref": "common_types.json#/$defs/DynamicString" + }, + "variant": { + "type": "string", + "description": "A hint for the base text style.", + "enum": [ + "h1", + "h2", + "h3", + "h4", + "h5", + "caption", + "body" + ] + } + }, + "required": [ + "component", + "text" + ] + } + ], + "unevaluatedProperties": false + } + }, + "$defs": { + "CatalogComponentCommon": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "A unique identifier for this component instance within the surface. This ID is used to refer to the component in layout children arrays or event handlers." + } + }, + "required": [ + "id" + ] + }, + "theme": { + "type": "object", + "properties": { + "primaryColor": { + "type": "string", + "description": "The primary brand color used for highlights (e.g., primary buttons, active borders). Renderers may generate variants of this color for different contexts. Format: Hexadecimal code (e.g., '#00BFFF').", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "iconUrl": { + "type": "string", + "format": "uri", + "description": "A URL for an image that identifies the agent or tool associated with the surface." + }, + "agentDisplayName": { + "type": "string", + "description": "Text to be displayed next to the surface to identify the agent or tool that created it." + } + }, + "additionalProperties": true + }, + "anyComponent": { + "oneOf": [ + { + "$ref": "#/components/Text" + } + ], + "discriminator": { + "propertyName": "component" + } + }, + "anyFunction": { + "not": {} + } + } +} +-----CATALOG_SCHEMA_END----- + +------------------------------------- + +-----MESSAGE_SCHEMA_START----- +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://a2ui.org/specification/v0_9/server_to_client.json", "title": "A2UI Message Schema", "description": "Describes a JSON payload for an A2UI (Agent to UI) message, which is used to dynamically construct and update user interfaces.", + "type": "object", "oneOf": [ - { + {"$ref": "#/$defs/CreateSurfaceMessage"}, + {"$ref": "#/$defs/UpdateComponentsMessage"}, + {"$ref": "#/$defs/UpdateDataModelMessage"}, + {"$ref": "#/$defs/DeleteSurfaceMessage"} + ], + "$defs": { + "CreateSurfaceMessage": { "type": "object", "properties": { "version": { - "type": "string", "const": "v0.9" }, "createSurface": { "type": "object", - "description": "Signals the client to create a new surface and begin rendering it. When this message is sent, the client will expect 'updateComponents' and/or 'updateDataModel' messages for the same surfaceId that define the component tree.", + "description": "Signals the client to create a new surface and begin rendering it. It is an error to send 'createSurface' for a surfaceId that already exists without first deleting it. When this message is sent, the client will expect 'updateComponents' and/or 'updateDataModel' messages for the same surfaceId that define the component tree.", "properties": { "surfaceId": { "type": "string", - "description": "The unique ID for the surface." + "description": "The unique identifier for the UI surface to be rendered." }, "catalogId": { - "type": "string", - "description": "The URI of the component catalog." + "description": "A string that uniquely identifies this catalog. It is recommended to prefix this with an internet domain that you own, to avoid conflicts e.g. mycompany.com:somecatalog'.", + "type": "string" }, "theme": { - "type": "object", - "description": "Theme parameters for the surface.", - "additionalProperties": true + "$ref": "catalog.json#/$defs/theme", + "description": "Theme parameters for the surface (e.g., {'primaryColor': '#FF0000'}). These must validate against the 'theme' schema defined in the catalog." }, "sendDataModel": { "type": "boolean", - "description": "Whether to send the data model to every client request." + "description": "If true, the client will send the full data model of this surface in the metadata of every A2A message sent to the server that created the surface. Defaults to false." } }, - "required": [ - "surfaceId", - "catalogId" - ] + "required": ["surfaceId", "catalogId"], + "additionalProperties": false } }, - "required": [ - "version", - "createSurface" - ], + "required": ["createSurface", "version"], "additionalProperties": false }, - { + "UpdateComponentsMessage": { "type": "object", "properties": { "version": { - "type": "string", "const": "v0.9" }, "updateComponents": { @@ -182,104 +598,29 @@ When constructing UI, you must output a VALID A2UI JSON object representing one "properties": { "surfaceId": { "type": "string", - "description": "The unique identifier for the UI surface." + "description": "The unique identifier for the UI surface to be updated." }, + "components": { "type": "array", - "description": "A flat list of component definitions.", + "description": "A list containing all UI components for the surface.", + "minItems": 1, "items": { - "description": "Must match one of the component definitions in the catalog.", - "oneOf": [ - { - "type": "object", - "description": "A block of styled text.", - "properties": { - "text": { - "description": "While simple Markdown is supported (without HTML or image references), utilizing dedicated UI components is generally preferred for a richer and more structured presentation.", - "oneOf": [ - { - "type": "string", - "description": "A literal string value." - }, - { - "type": "object", - "description": "A path to a string.", - "properties": { - "path": { - "type": "string", - "description": "A relative or absolute path in the data model." - } - }, - "required": [ - "path" - ] - }, - { - "type": "object", - "properties": { - "call": { - "type": "string", - "description": "The name of the function to call." - }, - "args": { - "type": "object", - "description": "Arguments to pass to the function.", - "additionalProperties": true - } - }, - "required": [ - "call" - ] - } - ] - }, - "variant": { - "type": "string", - "description": "A hint for the base text style.", - "enum": [ - "h1", - "h2", - "h3", - "h4", - "h5", - "caption", - "body" - ] - }, - "component": { - "type": "string", - "enum": [ - "Text" - ] - } - }, - "required": [ - "component", - "text" - ] - } - ] - }, - "minItems": 1 + "$ref": "catalog.json#/$defs/anyComponent" + } } }, - "required": [ - "surfaceId", - "components" - ] + "required": ["surfaceId", "components"], + "additionalProperties": false } }, - "required": [ - "version", - "updateComponents" - ], + "required": ["updateComponents", "version"], "additionalProperties": false }, - { + "UpdateDataModelMessage": { "type": "object", "properties": { "version": { - "type": "string", "const": "v0.9" }, "updateDataModel": { @@ -287,32 +628,29 @@ When constructing UI, you must output a VALID A2UI JSON object representing one "description": "Updates the data model for an existing surface. This message can be sent multiple times to update the data model. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.", "properties": { "surfaceId": { - "type": "string" + "type": "string", + "description": "The unique identifier for the UI surface this data model update applies to." }, "path": { "type": "string", - "default": "/" + "description": "An optional path to a location within the data model (e.g., '/user/name'). If omitted, or set to '/', refers to the entire data model." }, "value": { - "description": "The new value to write to the data model. If null/omitted, the key is removed." + "description": "The data to be updated in the data model. If present, the value at 'path' is replaced (or created). If omitted, the key at 'path' is removed.", + "additionalProperties": true } }, - "required": [ - "surfaceId" - ] + "required": ["surfaceId"], + "additionalProperties": false } }, - "required": [ - "version", - "updateDataModel" - ], + "required": ["updateDataModel", "version"], "additionalProperties": false }, - { + "DeleteSurfaceMessage": { "type": "object", "properties": { "version": { - "type": "string", "const": "v0.9" }, "deleteSurface": { @@ -320,20 +658,17 @@ When constructing UI, you must output a VALID A2UI JSON object representing one "description": "Signals the client to delete the surface identified by 'surfaceId'. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.", "properties": { "surfaceId": { - "type": "string" + "type": "string", + "description": "The unique identifier for the UI surface to be deleted." } }, - "required": [ - "surfaceId" - ] + "required": ["surfaceId"], + "additionalProperties": false } }, - "required": [ - "version", - "deleteSurface" - ], + "required": ["deleteSurface", "version"], "additionalProperties": false } - ] + } } ------A2UI_JSON_SCHEMA_END----- +-----MESSAGE_SCHEMA_END----- 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 cdc0a60ad..a2d083aa8 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 @@ -125,57 +125,473 @@ When constructing UI, you must output a VALID A2UI JSON object representing one ------------------------------------- ------A2UI_JSON_SCHEMA_START----- +-----COMMON_TYPES_START----- { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "common_types.json", + "title": "A2UI Common Types", + "description": "Common type definitions used across A2UI schemas.", + "$defs": { + "ComponentId": { + "type": "string", + "description": "The unique identifier for a component, used for both definitions and references within the same surface." + }, + "AccessibilityAttributes": { + "type": "object", + "description": "Attributes to enhance accessibility when using assistive technologies like screen readers.", + "properties": { + "label": { + "$ref": "#/$defs/DynamicString", + "description": "A short string, typically 1 to 3 words, used by assistive technologies to convey the purpose or intent of an element. For example, an input field might have an accessible label of 'User ID' or a button might be labeled 'Submit'." + }, + "description": { + "$ref": "#/$defs/DynamicString", + "description": "Additional information provided by assistive technologies about an element such as instructions, format requirements, or result of an action. For example, a mute button might have a label of 'Mute' and a description of 'Silences notifications about this conversation'." + } + } + }, + "ComponentCommon": { + "type": "object", + "properties": { + "id": { + "$ref": "#/$defs/ComponentId" + }, + "accessibility": { + "$ref": "#/$defs/AccessibilityAttributes" + } + }, + "required": ["id"] + }, + "ChildList": { + "oneOf": [ + { + "type": "array", + "items": { + "$ref": "#/$defs/ComponentId" + }, + "description": "A static list of child component IDs." + }, + { + "type": "object", + "description": "A template for generating a dynamic list of children from a data model list. The `componentId` is the component to use as a template.", + "properties": { + "componentId": { + "$ref": "#/$defs/ComponentId" + }, + "path": { + "type": "string", + "description": "The path to the list of component property objects in the data model." + } + }, + "required": ["componentId", "path"], + "additionalProperties": false + } + ] + }, + "DataBinding": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "A JSON Pointer path to a value in the data model." + } + }, + "required": ["path"], + "additionalProperties": false + }, + "DynamicValue": { + "description": "A value that can be a literal, a path, or a function call returning any type.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "array" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "$ref": "#/$defs/FunctionCall" + } + ] + }, + "DynamicString": { + "description": "Represents a string", + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "string" + } + } + } + ] + } + ] + }, + "DynamicNumber": { + "description": "Represents a value that can be either a literal number, a path to a number in the data model, or a function call returning a number.", + "oneOf": [ + { + "type": "number" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "number" + } + } + } + ] + } + ] + }, + "DynamicBoolean": { + "description": "A boolean value that can be a literal, a path, or a function call returning a boolean.", + "oneOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "boolean" + } + } + } + ] + } + ] + }, + "DynamicStringList": { + "description": "Represents a value that can be either a literal array of strings, a path to a string array in the data model, or a function call returning a string array.", + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "array" + } + } + } + ] + } + ] + }, + "FunctionCall": { + "type": "object", + "description": "Invokes a named function on the client.", + "properties": { + "call": { + "type": "string", + "description": "The name of the function to call." + }, + "args": { + "type": "object", + "description": "Arguments passed to the function.", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/DynamicValue" + }, + { + "type": "object", + "description": "A literal object argument (e.g. configuration)." + } + ] + } + }, + "returnType": { + "type": "string", + "description": "The expected return type of the function call.", + "enum": ["string", "number", "boolean", "array", "object", "any", "void"], + "default": "boolean" + } + }, + "required": ["call"], + "oneOf": [{"$ref": "catalog.json#/$defs/anyFunction"}] + }, + "CheckRule": { + "type": "object", + "description": "A single validation rule applied to an input component.", + "properties": { + "condition": { + "$ref": "#/$defs/DynamicBoolean" + }, + "message": { + "type": "string", + "description": "The error message to display if the check fails." + } + }, + "required": ["condition", "message"], + "additionalProperties": false + }, + "Checkable": { + "description": "Properties for components that support client-side checks.", + "type": "object", + "properties": { + "checks": { + "type": "array", + "description": "A list of checks to perform. These are function calls that must return a boolean indicating validity.", + "items": { + "$ref": "#/$defs/CheckRule" + } + } + } + }, + "Action": { + "description": "Defines an interaction handler that can either trigger a server-side event or execute a local client-side function.", + "oneOf": [ + { + "type": "object", + "description": "Triggers a server-side event.", + "properties": { + "event": { + "type": "object", + "description": "The event to dispatch to the server.", + "properties": { + "name": { + "type": "string", + "description": "The name of the action to be dispatched to the server." + }, + "context": { + "type": "object", + "description": "A JSON object containing the key-value pairs for the action context. Values can be literals or paths. Use literal values unless the value must be dynamically bound to the data model. Do NOT use paths for static IDs.", + "additionalProperties": { + "$ref": "#/$defs/DynamicValue" + } + } + }, + "required": ["name"], + "additionalProperties": false + } + }, + "required": ["event"], + "additionalProperties": false + }, + { + "type": "object", + "description": "Executes a local client-side function.", + "properties": { + "functionCall": { + "$ref": "#/$defs/FunctionCall" + } + }, + "required": ["functionCall"], + "additionalProperties": false + } + ] + } + } +} +-----COMMON_TYPES_END----- + +------------------------------------- + +-----CATALOG_SCHEMA_START----- +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://a2ui.org/specification/v0_9/catalog.json", + "title": "A2UI Catalog", + "description": "Custom catalog of A2UI components and functions.", + "catalogId": "test_catalog", + "components": { + "Text": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "type": "string", + "enum": [ + "Text" + ] + }, + "text": { + "description": "While simple Markdown is supported (without HTML or image references), utilizing dedicated UI components is generally preferred for a richer and more structured presentation.", + "$ref": "common_types.json#/$defs/DynamicString" + }, + "variant": { + "type": "string", + "description": "A hint for the base text style.", + "enum": [ + "h1", + "h2", + "h3", + "h4", + "h5", + "caption", + "body" + ] + } + }, + "required": [ + "component", + "text" + ] + } + ], + "unevaluatedProperties": false + } + }, + "$defs": { + "CatalogComponentCommon": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "A unique identifier for this component instance within the surface. This ID is used to refer to the component in layout children arrays or event handlers." + } + }, + "required": [ + "id" + ] + }, + "theme": { + "type": "object", + "properties": { + "primaryColor": { + "type": "string", + "description": "The primary brand color used for highlights (e.g., primary buttons, active borders). Renderers may generate variants of this color for different contexts. Format: Hexadecimal code (e.g., '#00BFFF').", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "iconUrl": { + "type": "string", + "format": "uri", + "description": "A URL for an image that identifies the agent or tool associated with the surface." + }, + "agentDisplayName": { + "type": "string", + "description": "Text to be displayed next to the surface to identify the agent or tool that created it." + } + }, + "additionalProperties": true + }, + "anyComponent": { + "oneOf": [ + { + "$ref": "#/components/Text" + } + ], + "discriminator": { + "propertyName": "component" + } + }, + "anyFunction": { + "not": {} + } + } +} +-----CATALOG_SCHEMA_END----- + +------------------------------------- + +-----MESSAGE_SCHEMA_START----- +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://a2ui.org/specification/v0_9/server_to_client.json", "title": "A2UI Message Schema", "description": "Describes a JSON payload for an A2UI (Agent to UI) message, which is used to dynamically construct and update user interfaces.", + "type": "object", "oneOf": [ - { + {"$ref": "#/$defs/CreateSurfaceMessage"}, + {"$ref": "#/$defs/UpdateComponentsMessage"}, + {"$ref": "#/$defs/UpdateDataModelMessage"}, + {"$ref": "#/$defs/DeleteSurfaceMessage"} + ], + "$defs": { + "CreateSurfaceMessage": { "type": "object", "properties": { "version": { - "type": "string", "const": "v0.9" }, "createSurface": { "type": "object", - "description": "Signals the client to create a new surface and begin rendering it. When this message is sent, the client will expect 'updateComponents' and/or 'updateDataModel' messages for the same surfaceId that define the component tree.", + "description": "Signals the client to create a new surface and begin rendering it. It is an error to send 'createSurface' for a surfaceId that already exists without first deleting it. When this message is sent, the client will expect 'updateComponents' and/or 'updateDataModel' messages for the same surfaceId that define the component tree.", "properties": { "surfaceId": { "type": "string", - "description": "The unique ID for the surface." + "description": "The unique identifier for the UI surface to be rendered." }, "catalogId": { - "type": "string", - "description": "The URI of the component catalog." + "description": "A string that uniquely identifies this catalog. It is recommended to prefix this with an internet domain that you own, to avoid conflicts e.g. mycompany.com:somecatalog'.", + "type": "string" }, "theme": { - "type": "object", - "description": "Theme parameters for the surface.", - "additionalProperties": true + "$ref": "catalog.json#/$defs/theme", + "description": "Theme parameters for the surface (e.g., {'primaryColor': '#FF0000'}). These must validate against the 'theme' schema defined in the catalog." }, "sendDataModel": { "type": "boolean", - "description": "Whether to send the data model to every client request." + "description": "If true, the client will send the full data model of this surface in the metadata of every A2A message sent to the server that created the surface. Defaults to false." } }, - "required": [ - "surfaceId", - "catalogId" - ] + "required": ["surfaceId", "catalogId"], + "additionalProperties": false } }, - "required": [ - "version", - "createSurface" - ], + "required": ["createSurface", "version"], "additionalProperties": false }, - { + "UpdateComponentsMessage": { "type": "object", "properties": { "version": { - "type": "string", "const": "v0.9" }, "updateComponents": { @@ -184,104 +600,29 @@ When constructing UI, you must output a VALID A2UI JSON object representing one "properties": { "surfaceId": { "type": "string", - "description": "The unique identifier for the UI surface." + "description": "The unique identifier for the UI surface to be updated." }, + "components": { "type": "array", - "description": "A flat list of component definitions.", + "description": "A list containing all UI components for the surface.", + "minItems": 1, "items": { - "description": "Must match one of the component definitions in the catalog.", - "oneOf": [ - { - "type": "object", - "description": "A block of styled text.", - "properties": { - "text": { - "description": "While simple Markdown is supported (without HTML or image references), utilizing dedicated UI components is generally preferred for a richer and more structured presentation.", - "oneOf": [ - { - "type": "string", - "description": "A literal string value." - }, - { - "type": "object", - "description": "A path to a string.", - "properties": { - "path": { - "type": "string", - "description": "A relative or absolute path in the data model." - } - }, - "required": [ - "path" - ] - }, - { - "type": "object", - "properties": { - "call": { - "type": "string", - "description": "The name of the function to call." - }, - "args": { - "type": "object", - "description": "Arguments to pass to the function.", - "additionalProperties": true - } - }, - "required": [ - "call" - ] - } - ] - }, - "variant": { - "type": "string", - "description": "A hint for the base text style.", - "enum": [ - "h1", - "h2", - "h3", - "h4", - "h5", - "caption", - "body" - ] - }, - "component": { - "type": "string", - "enum": [ - "Text" - ] - } - }, - "required": [ - "component", - "text" - ] - } - ] - }, - "minItems": 1 + "$ref": "catalog.json#/$defs/anyComponent" + } } }, - "required": [ - "surfaceId", - "components" - ] + "required": ["surfaceId", "components"], + "additionalProperties": false } }, - "required": [ - "version", - "updateComponents" - ], + "required": ["updateComponents", "version"], "additionalProperties": false }, - { + "UpdateDataModelMessage": { "type": "object", "properties": { "version": { - "type": "string", "const": "v0.9" }, "updateDataModel": { @@ -289,32 +630,29 @@ When constructing UI, you must output a VALID A2UI JSON object representing one "description": "Updates the data model for an existing surface. This message can be sent multiple times to update the data model. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.", "properties": { "surfaceId": { - "type": "string" + "type": "string", + "description": "The unique identifier for the UI surface this data model update applies to." }, "path": { "type": "string", - "default": "/" + "description": "An optional path to a location within the data model (e.g., '/user/name'). If omitted, or set to '/', refers to the entire data model." }, "value": { - "description": "The new value to write to the data model. If null/omitted, the key is removed." + "description": "The data to be updated in the data model. If present, the value at 'path' is replaced (or created). If omitted, the key at 'path' is removed.", + "additionalProperties": true } }, - "required": [ - "surfaceId" - ] + "required": ["surfaceId"], + "additionalProperties": false } }, - "required": [ - "version", - "updateDataModel" - ], + "required": ["updateDataModel", "version"], "additionalProperties": false }, - { + "DeleteSurfaceMessage": { "type": "object", "properties": { "version": { - "type": "string", "const": "v0.9" }, "deleteSurface": { @@ -322,20 +660,17 @@ When constructing UI, you must output a VALID A2UI JSON object representing one "description": "Signals the client to delete the surface identified by 'surfaceId'. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.", "properties": { "surfaceId": { - "type": "string" + "type": "string", + "description": "The unique identifier for the UI surface to be deleted." } }, - "required": [ - "surfaceId" - ] + "required": ["surfaceId"], + "additionalProperties": false } }, - "required": [ - "version", - "deleteSurface" - ], + "required": ["deleteSurface", "version"], "additionalProperties": false } - ] + } } ------A2UI_JSON_SCHEMA_END----- +-----MESSAGE_SCHEMA_END----- 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 fda9dd04c..07b9a4439 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 @@ -122,57 +122,473 @@ When constructing UI, you must output a VALID A2UI JSON object representing one ------------------------------------- ------A2UI_JSON_SCHEMA_START----- +-----COMMON_TYPES_START----- { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "common_types.json", + "title": "A2UI Common Types", + "description": "Common type definitions used across A2UI schemas.", + "$defs": { + "ComponentId": { + "type": "string", + "description": "The unique identifier for a component, used for both definitions and references within the same surface." + }, + "AccessibilityAttributes": { + "type": "object", + "description": "Attributes to enhance accessibility when using assistive technologies like screen readers.", + "properties": { + "label": { + "$ref": "#/$defs/DynamicString", + "description": "A short string, typically 1 to 3 words, used by assistive technologies to convey the purpose or intent of an element. For example, an input field might have an accessible label of 'User ID' or a button might be labeled 'Submit'." + }, + "description": { + "$ref": "#/$defs/DynamicString", + "description": "Additional information provided by assistive technologies about an element such as instructions, format requirements, or result of an action. For example, a mute button might have a label of 'Mute' and a description of 'Silences notifications about this conversation'." + } + } + }, + "ComponentCommon": { + "type": "object", + "properties": { + "id": { + "$ref": "#/$defs/ComponentId" + }, + "accessibility": { + "$ref": "#/$defs/AccessibilityAttributes" + } + }, + "required": ["id"] + }, + "ChildList": { + "oneOf": [ + { + "type": "array", + "items": { + "$ref": "#/$defs/ComponentId" + }, + "description": "A static list of child component IDs." + }, + { + "type": "object", + "description": "A template for generating a dynamic list of children from a data model list. The `componentId` is the component to use as a template.", + "properties": { + "componentId": { + "$ref": "#/$defs/ComponentId" + }, + "path": { + "type": "string", + "description": "The path to the list of component property objects in the data model." + } + }, + "required": ["componentId", "path"], + "additionalProperties": false + } + ] + }, + "DataBinding": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "A JSON Pointer path to a value in the data model." + } + }, + "required": ["path"], + "additionalProperties": false + }, + "DynamicValue": { + "description": "A value that can be a literal, a path, or a function call returning any type.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "array" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "$ref": "#/$defs/FunctionCall" + } + ] + }, + "DynamicString": { + "description": "Represents a string", + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "string" + } + } + } + ] + } + ] + }, + "DynamicNumber": { + "description": "Represents a value that can be either a literal number, a path to a number in the data model, or a function call returning a number.", + "oneOf": [ + { + "type": "number" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "number" + } + } + } + ] + } + ] + }, + "DynamicBoolean": { + "description": "A boolean value that can be a literal, a path, or a function call returning a boolean.", + "oneOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "boolean" + } + } + } + ] + } + ] + }, + "DynamicStringList": { + "description": "Represents a value that can be either a literal array of strings, a path to a string array in the data model, or a function call returning a string array.", + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "array" + } + } + } + ] + } + ] + }, + "FunctionCall": { + "type": "object", + "description": "Invokes a named function on the client.", + "properties": { + "call": { + "type": "string", + "description": "The name of the function to call." + }, + "args": { + "type": "object", + "description": "Arguments passed to the function.", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/DynamicValue" + }, + { + "type": "object", + "description": "A literal object argument (e.g. configuration)." + } + ] + } + }, + "returnType": { + "type": "string", + "description": "The expected return type of the function call.", + "enum": ["string", "number", "boolean", "array", "object", "any", "void"], + "default": "boolean" + } + }, + "required": ["call"], + "oneOf": [{"$ref": "catalog.json#/$defs/anyFunction"}] + }, + "CheckRule": { + "type": "object", + "description": "A single validation rule applied to an input component.", + "properties": { + "condition": { + "$ref": "#/$defs/DynamicBoolean" + }, + "message": { + "type": "string", + "description": "The error message to display if the check fails." + } + }, + "required": ["condition", "message"], + "additionalProperties": false + }, + "Checkable": { + "description": "Properties for components that support client-side checks.", + "type": "object", + "properties": { + "checks": { + "type": "array", + "description": "A list of checks to perform. These are function calls that must return a boolean indicating validity.", + "items": { + "$ref": "#/$defs/CheckRule" + } + } + } + }, + "Action": { + "description": "Defines an interaction handler that can either trigger a server-side event or execute a local client-side function.", + "oneOf": [ + { + "type": "object", + "description": "Triggers a server-side event.", + "properties": { + "event": { + "type": "object", + "description": "The event to dispatch to the server.", + "properties": { + "name": { + "type": "string", + "description": "The name of the action to be dispatched to the server." + }, + "context": { + "type": "object", + "description": "A JSON object containing the key-value pairs for the action context. Values can be literals or paths. Use literal values unless the value must be dynamically bound to the data model. Do NOT use paths for static IDs.", + "additionalProperties": { + "$ref": "#/$defs/DynamicValue" + } + } + }, + "required": ["name"], + "additionalProperties": false + } + }, + "required": ["event"], + "additionalProperties": false + }, + { + "type": "object", + "description": "Executes a local client-side function.", + "properties": { + "functionCall": { + "$ref": "#/$defs/FunctionCall" + } + }, + "required": ["functionCall"], + "additionalProperties": false + } + ] + } + } +} +-----COMMON_TYPES_END----- + +------------------------------------- + +-----CATALOG_SCHEMA_START----- +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://a2ui.org/specification/v0_9/catalog.json", + "title": "A2UI Catalog", + "description": "Custom catalog of A2UI components and functions.", + "catalogId": "test_catalog", + "components": { + "Text": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "type": "string", + "enum": [ + "Text" + ] + }, + "text": { + "description": "While simple Markdown is supported (without HTML or image references), utilizing dedicated UI components is generally preferred for a richer and more structured presentation.", + "$ref": "common_types.json#/$defs/DynamicString" + }, + "variant": { + "type": "string", + "description": "A hint for the base text style.", + "enum": [ + "h1", + "h2", + "h3", + "h4", + "h5", + "caption", + "body" + ] + } + }, + "required": [ + "component", + "text" + ] + } + ], + "unevaluatedProperties": false + } + }, + "$defs": { + "CatalogComponentCommon": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "A unique identifier for this component instance within the surface. This ID is used to refer to the component in layout children arrays or event handlers." + } + }, + "required": [ + "id" + ] + }, + "theme": { + "type": "object", + "properties": { + "primaryColor": { + "type": "string", + "description": "The primary brand color used for highlights (e.g., primary buttons, active borders). Renderers may generate variants of this color for different contexts. Format: Hexadecimal code (e.g., '#00BFFF').", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "iconUrl": { + "type": "string", + "format": "uri", + "description": "A URL for an image that identifies the agent or tool associated with the surface." + }, + "agentDisplayName": { + "type": "string", + "description": "Text to be displayed next to the surface to identify the agent or tool that created it." + } + }, + "additionalProperties": true + }, + "anyComponent": { + "oneOf": [ + { + "$ref": "#/components/Text" + } + ], + "discriminator": { + "propertyName": "component" + } + }, + "anyFunction": { + "not": {} + } + } +} +-----CATALOG_SCHEMA_END----- + +------------------------------------- + +-----MESSAGE_SCHEMA_START----- +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://a2ui.org/specification/v0_9/server_to_client.json", "title": "A2UI Message Schema", "description": "Describes a JSON payload for an A2UI (Agent to UI) message, which is used to dynamically construct and update user interfaces.", + "type": "object", "oneOf": [ - { + {"$ref": "#/$defs/CreateSurfaceMessage"}, + {"$ref": "#/$defs/UpdateComponentsMessage"}, + {"$ref": "#/$defs/UpdateDataModelMessage"}, + {"$ref": "#/$defs/DeleteSurfaceMessage"} + ], + "$defs": { + "CreateSurfaceMessage": { "type": "object", "properties": { "version": { - "type": "string", "const": "v0.9" }, "createSurface": { "type": "object", - "description": "Signals the client to create a new surface and begin rendering it. When this message is sent, the client will expect 'updateComponents' and/or 'updateDataModel' messages for the same surfaceId that define the component tree.", + "description": "Signals the client to create a new surface and begin rendering it. It is an error to send 'createSurface' for a surfaceId that already exists without first deleting it. When this message is sent, the client will expect 'updateComponents' and/or 'updateDataModel' messages for the same surfaceId that define the component tree.", "properties": { "surfaceId": { "type": "string", - "description": "The unique ID for the surface." + "description": "The unique identifier for the UI surface to be rendered." }, "catalogId": { - "type": "string", - "description": "The URI of the component catalog." + "description": "A string that uniquely identifies this catalog. It is recommended to prefix this with an internet domain that you own, to avoid conflicts e.g. mycompany.com:somecatalog'.", + "type": "string" }, "theme": { - "type": "object", - "description": "Theme parameters for the surface.", - "additionalProperties": true + "$ref": "catalog.json#/$defs/theme", + "description": "Theme parameters for the surface (e.g., {'primaryColor': '#FF0000'}). These must validate against the 'theme' schema defined in the catalog." }, "sendDataModel": { "type": "boolean", - "description": "Whether to send the data model to every client request." + "description": "If true, the client will send the full data model of this surface in the metadata of every A2A message sent to the server that created the surface. Defaults to false." } }, - "required": [ - "surfaceId", - "catalogId" - ] + "required": ["surfaceId", "catalogId"], + "additionalProperties": false } }, - "required": [ - "version", - "createSurface" - ], + "required": ["createSurface", "version"], "additionalProperties": false }, - { + "UpdateComponentsMessage": { "type": "object", "properties": { "version": { - "type": "string", "const": "v0.9" }, "updateComponents": { @@ -181,104 +597,29 @@ When constructing UI, you must output a VALID A2UI JSON object representing one "properties": { "surfaceId": { "type": "string", - "description": "The unique identifier for the UI surface." + "description": "The unique identifier for the UI surface to be updated." }, + "components": { "type": "array", - "description": "A flat list of component definitions.", + "description": "A list containing all UI components for the surface.", + "minItems": 1, "items": { - "description": "Must match one of the component definitions in the catalog.", - "oneOf": [ - { - "type": "object", - "description": "A block of styled text.", - "properties": { - "text": { - "description": "While simple Markdown is supported (without HTML or image references), utilizing dedicated UI components is generally preferred for a richer and more structured presentation.", - "oneOf": [ - { - "type": "string", - "description": "A literal string value." - }, - { - "type": "object", - "description": "A path to a string.", - "properties": { - "path": { - "type": "string", - "description": "A relative or absolute path in the data model." - } - }, - "required": [ - "path" - ] - }, - { - "type": "object", - "properties": { - "call": { - "type": "string", - "description": "The name of the function to call." - }, - "args": { - "type": "object", - "description": "Arguments to pass to the function.", - "additionalProperties": true - } - }, - "required": [ - "call" - ] - } - ] - }, - "variant": { - "type": "string", - "description": "A hint for the base text style.", - "enum": [ - "h1", - "h2", - "h3", - "h4", - "h5", - "caption", - "body" - ] - }, - "component": { - "type": "string", - "enum": [ - "Text" - ] - } - }, - "required": [ - "component", - "text" - ] - } - ] - }, - "minItems": 1 + "$ref": "catalog.json#/$defs/anyComponent" + } } }, - "required": [ - "surfaceId", - "components" - ] + "required": ["surfaceId", "components"], + "additionalProperties": false } }, - "required": [ - "version", - "updateComponents" - ], + "required": ["updateComponents", "version"], "additionalProperties": false }, - { + "UpdateDataModelMessage": { "type": "object", "properties": { "version": { - "type": "string", "const": "v0.9" }, "updateDataModel": { @@ -286,32 +627,29 @@ When constructing UI, you must output a VALID A2UI JSON object representing one "description": "Updates the data model for an existing surface. This message can be sent multiple times to update the data model. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.", "properties": { "surfaceId": { - "type": "string" + "type": "string", + "description": "The unique identifier for the UI surface this data model update applies to." }, "path": { "type": "string", - "default": "/" + "description": "An optional path to a location within the data model (e.g., '/user/name'). If omitted, or set to '/', refers to the entire data model." }, "value": { - "description": "The new value to write to the data model. If null/omitted, the key is removed." + "description": "The data to be updated in the data model. If present, the value at 'path' is replaced (or created). If omitted, the key at 'path' is removed.", + "additionalProperties": true } }, - "required": [ - "surfaceId" - ] + "required": ["surfaceId"], + "additionalProperties": false } }, - "required": [ - "version", - "updateDataModel" - ], + "required": ["updateDataModel", "version"], "additionalProperties": false }, - { + "DeleteSurfaceMessage": { "type": "object", "properties": { "version": { - "type": "string", "const": "v0.9" }, "deleteSurface": { @@ -319,20 +657,17 @@ When constructing UI, you must output a VALID A2UI JSON object representing one "description": "Signals the client to delete the surface identified by 'surfaceId'. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.", "properties": { "surfaceId": { - "type": "string" + "type": "string", + "description": "The unique identifier for the UI surface to be deleted." } }, - "required": [ - "surfaceId" - ] + "required": ["surfaceId"], + "additionalProperties": false } }, - "required": [ - "version", - "deleteSurface" - ], + "required": ["deleteSurface", "version"], "additionalProperties": false } - ] + } } ------A2UI_JSON_SCHEMA_END----- +-----MESSAGE_SCHEMA_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 75f6319ff..2484929af 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 @@ -124,57 +124,473 @@ When constructing UI, you must output a VALID A2UI JSON object representing one ------------------------------------- ------A2UI_JSON_SCHEMA_START----- +-----COMMON_TYPES_START----- { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "common_types.json", + "title": "A2UI Common Types", + "description": "Common type definitions used across A2UI schemas.", + "$defs": { + "ComponentId": { + "type": "string", + "description": "The unique identifier for a component, used for both definitions and references within the same surface." + }, + "AccessibilityAttributes": { + "type": "object", + "description": "Attributes to enhance accessibility when using assistive technologies like screen readers.", + "properties": { + "label": { + "$ref": "#/$defs/DynamicString", + "description": "A short string, typically 1 to 3 words, used by assistive technologies to convey the purpose or intent of an element. For example, an input field might have an accessible label of 'User ID' or a button might be labeled 'Submit'." + }, + "description": { + "$ref": "#/$defs/DynamicString", + "description": "Additional information provided by assistive technologies about an element such as instructions, format requirements, or result of an action. For example, a mute button might have a label of 'Mute' and a description of 'Silences notifications about this conversation'." + } + } + }, + "ComponentCommon": { + "type": "object", + "properties": { + "id": { + "$ref": "#/$defs/ComponentId" + }, + "accessibility": { + "$ref": "#/$defs/AccessibilityAttributes" + } + }, + "required": ["id"] + }, + "ChildList": { + "oneOf": [ + { + "type": "array", + "items": { + "$ref": "#/$defs/ComponentId" + }, + "description": "A static list of child component IDs." + }, + { + "type": "object", + "description": "A template for generating a dynamic list of children from a data model list. The `componentId` is the component to use as a template.", + "properties": { + "componentId": { + "$ref": "#/$defs/ComponentId" + }, + "path": { + "type": "string", + "description": "The path to the list of component property objects in the data model." + } + }, + "required": ["componentId", "path"], + "additionalProperties": false + } + ] + }, + "DataBinding": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "A JSON Pointer path to a value in the data model." + } + }, + "required": ["path"], + "additionalProperties": false + }, + "DynamicValue": { + "description": "A value that can be a literal, a path, or a function call returning any type.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "array" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "$ref": "#/$defs/FunctionCall" + } + ] + }, + "DynamicString": { + "description": "Represents a string", + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "string" + } + } + } + ] + } + ] + }, + "DynamicNumber": { + "description": "Represents a value that can be either a literal number, a path to a number in the data model, or a function call returning a number.", + "oneOf": [ + { + "type": "number" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "number" + } + } + } + ] + } + ] + }, + "DynamicBoolean": { + "description": "A boolean value that can be a literal, a path, or a function call returning a boolean.", + "oneOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "boolean" + } + } + } + ] + } + ] + }, + "DynamicStringList": { + "description": "Represents a value that can be either a literal array of strings, a path to a string array in the data model, or a function call returning a string array.", + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "array" + } + } + } + ] + } + ] + }, + "FunctionCall": { + "type": "object", + "description": "Invokes a named function on the client.", + "properties": { + "call": { + "type": "string", + "description": "The name of the function to call." + }, + "args": { + "type": "object", + "description": "Arguments passed to the function.", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/DynamicValue" + }, + { + "type": "object", + "description": "A literal object argument (e.g. configuration)." + } + ] + } + }, + "returnType": { + "type": "string", + "description": "The expected return type of the function call.", + "enum": ["string", "number", "boolean", "array", "object", "any", "void"], + "default": "boolean" + } + }, + "required": ["call"], + "oneOf": [{"$ref": "catalog.json#/$defs/anyFunction"}] + }, + "CheckRule": { + "type": "object", + "description": "A single validation rule applied to an input component.", + "properties": { + "condition": { + "$ref": "#/$defs/DynamicBoolean" + }, + "message": { + "type": "string", + "description": "The error message to display if the check fails." + } + }, + "required": ["condition", "message"], + "additionalProperties": false + }, + "Checkable": { + "description": "Properties for components that support client-side checks.", + "type": "object", + "properties": { + "checks": { + "type": "array", + "description": "A list of checks to perform. These are function calls that must return a boolean indicating validity.", + "items": { + "$ref": "#/$defs/CheckRule" + } + } + } + }, + "Action": { + "description": "Defines an interaction handler that can either trigger a server-side event or execute a local client-side function.", + "oneOf": [ + { + "type": "object", + "description": "Triggers a server-side event.", + "properties": { + "event": { + "type": "object", + "description": "The event to dispatch to the server.", + "properties": { + "name": { + "type": "string", + "description": "The name of the action to be dispatched to the server." + }, + "context": { + "type": "object", + "description": "A JSON object containing the key-value pairs for the action context. Values can be literals or paths. Use literal values unless the value must be dynamically bound to the data model. Do NOT use paths for static IDs.", + "additionalProperties": { + "$ref": "#/$defs/DynamicValue" + } + } + }, + "required": ["name"], + "additionalProperties": false + } + }, + "required": ["event"], + "additionalProperties": false + }, + { + "type": "object", + "description": "Executes a local client-side function.", + "properties": { + "functionCall": { + "$ref": "#/$defs/FunctionCall" + } + }, + "required": ["functionCall"], + "additionalProperties": false + } + ] + } + } +} +-----COMMON_TYPES_END----- + +------------------------------------- + +-----CATALOG_SCHEMA_START----- +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://a2ui.org/specification/v0_9/catalog.json", + "title": "A2UI Catalog", + "description": "Custom catalog of A2UI components and functions.", + "catalogId": "test_catalog", + "components": { + "Text": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "type": "string", + "enum": [ + "Text" + ] + }, + "text": { + "description": "While simple Markdown is supported (without HTML or image references), utilizing dedicated UI components is generally preferred for a richer and more structured presentation.", + "$ref": "common_types.json#/$defs/DynamicString" + }, + "variant": { + "type": "string", + "description": "A hint for the base text style.", + "enum": [ + "h1", + "h2", + "h3", + "h4", + "h5", + "caption", + "body" + ] + } + }, + "required": [ + "component", + "text" + ] + } + ], + "unevaluatedProperties": false + } + }, + "$defs": { + "CatalogComponentCommon": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "A unique identifier for this component instance within the surface. This ID is used to refer to the component in layout children arrays or event handlers." + } + }, + "required": [ + "id" + ] + }, + "theme": { + "type": "object", + "properties": { + "primaryColor": { + "type": "string", + "description": "The primary brand color used for highlights (e.g., primary buttons, active borders). Renderers may generate variants of this color for different contexts. Format: Hexadecimal code (e.g., '#00BFFF').", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "iconUrl": { + "type": "string", + "format": "uri", + "description": "A URL for an image that identifies the agent or tool associated with the surface." + }, + "agentDisplayName": { + "type": "string", + "description": "Text to be displayed next to the surface to identify the agent or tool that created it." + } + }, + "additionalProperties": true + }, + "anyComponent": { + "oneOf": [ + { + "$ref": "#/components/Text" + } + ], + "discriminator": { + "propertyName": "component" + } + }, + "anyFunction": { + "not": {} + } + } +} +-----CATALOG_SCHEMA_END----- + +------------------------------------- + +-----MESSAGE_SCHEMA_START----- +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://a2ui.org/specification/v0_9/server_to_client.json", "title": "A2UI Message Schema", "description": "Describes a JSON payload for an A2UI (Agent to UI) message, which is used to dynamically construct and update user interfaces.", + "type": "object", "oneOf": [ - { + {"$ref": "#/$defs/CreateSurfaceMessage"}, + {"$ref": "#/$defs/UpdateComponentsMessage"}, + {"$ref": "#/$defs/UpdateDataModelMessage"}, + {"$ref": "#/$defs/DeleteSurfaceMessage"} + ], + "$defs": { + "CreateSurfaceMessage": { "type": "object", "properties": { "version": { - "type": "string", "const": "v0.9" }, "createSurface": { "type": "object", - "description": "Signals the client to create a new surface and begin rendering it. When this message is sent, the client will expect 'updateComponents' and/or 'updateDataModel' messages for the same surfaceId that define the component tree.", + "description": "Signals the client to create a new surface and begin rendering it. It is an error to send 'createSurface' for a surfaceId that already exists without first deleting it. When this message is sent, the client will expect 'updateComponents' and/or 'updateDataModel' messages for the same surfaceId that define the component tree.", "properties": { "surfaceId": { "type": "string", - "description": "The unique ID for the surface." + "description": "The unique identifier for the UI surface to be rendered." }, "catalogId": { - "type": "string", - "description": "The URI of the component catalog." + "description": "A string that uniquely identifies this catalog. It is recommended to prefix this with an internet domain that you own, to avoid conflicts e.g. mycompany.com:somecatalog'.", + "type": "string" }, "theme": { - "type": "object", - "description": "Theme parameters for the surface.", - "additionalProperties": true + "$ref": "catalog.json#/$defs/theme", + "description": "Theme parameters for the surface (e.g., {'primaryColor': '#FF0000'}). These must validate against the 'theme' schema defined in the catalog." }, "sendDataModel": { "type": "boolean", - "description": "Whether to send the data model to every client request." + "description": "If true, the client will send the full data model of this surface in the metadata of every A2A message sent to the server that created the surface. Defaults to false." } }, - "required": [ - "surfaceId", - "catalogId" - ] + "required": ["surfaceId", "catalogId"], + "additionalProperties": false } }, - "required": [ - "version", - "createSurface" - ], + "required": ["createSurface", "version"], "additionalProperties": false }, - { + "UpdateComponentsMessage": { "type": "object", "properties": { "version": { - "type": "string", "const": "v0.9" }, "updateComponents": { @@ -183,104 +599,29 @@ When constructing UI, you must output a VALID A2UI JSON object representing one "properties": { "surfaceId": { "type": "string", - "description": "The unique identifier for the UI surface." + "description": "The unique identifier for the UI surface to be updated." }, + "components": { "type": "array", - "description": "A flat list of component definitions.", + "description": "A list containing all UI components for the surface.", + "minItems": 1, "items": { - "description": "Must match one of the component definitions in the catalog.", - "oneOf": [ - { - "type": "object", - "description": "A block of styled text.", - "properties": { - "text": { - "description": "While simple Markdown is supported (without HTML or image references), utilizing dedicated UI components is generally preferred for a richer and more structured presentation.", - "oneOf": [ - { - "type": "string", - "description": "A literal string value." - }, - { - "type": "object", - "description": "A path to a string.", - "properties": { - "path": { - "type": "string", - "description": "A relative or absolute path in the data model." - } - }, - "required": [ - "path" - ] - }, - { - "type": "object", - "properties": { - "call": { - "type": "string", - "description": "The name of the function to call." - }, - "args": { - "type": "object", - "description": "Arguments to pass to the function.", - "additionalProperties": true - } - }, - "required": [ - "call" - ] - } - ] - }, - "variant": { - "type": "string", - "description": "A hint for the base text style.", - "enum": [ - "h1", - "h2", - "h3", - "h4", - "h5", - "caption", - "body" - ] - }, - "component": { - "type": "string", - "enum": [ - "Text" - ] - } - }, - "required": [ - "component", - "text" - ] - } - ] - }, - "minItems": 1 + "$ref": "catalog.json#/$defs/anyComponent" + } } }, - "required": [ - "surfaceId", - "components" - ] + "required": ["surfaceId", "components"], + "additionalProperties": false } }, - "required": [ - "version", - "updateComponents" - ], + "required": ["updateComponents", "version"], "additionalProperties": false }, - { + "UpdateDataModelMessage": { "type": "object", "properties": { "version": { - "type": "string", "const": "v0.9" }, "updateDataModel": { @@ -288,32 +629,29 @@ When constructing UI, you must output a VALID A2UI JSON object representing one "description": "Updates the data model for an existing surface. This message can be sent multiple times to update the data model. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.", "properties": { "surfaceId": { - "type": "string" + "type": "string", + "description": "The unique identifier for the UI surface this data model update applies to." }, "path": { "type": "string", - "default": "/" + "description": "An optional path to a location within the data model (e.g., '/user/name'). If omitted, or set to '/', refers to the entire data model." }, "value": { - "description": "The new value to write to the data model. If null/omitted, the key is removed." + "description": "The data to be updated in the data model. If present, the value at 'path' is replaced (or created). If omitted, the key at 'path' is removed.", + "additionalProperties": true } }, - "required": [ - "surfaceId" - ] + "required": ["surfaceId"], + "additionalProperties": false } }, - "required": [ - "version", - "updateDataModel" - ], + "required": ["updateDataModel", "version"], "additionalProperties": false }, - { + "DeleteSurfaceMessage": { "type": "object", "properties": { "version": { - "type": "string", "const": "v0.9" }, "deleteSurface": { @@ -321,20 +659,17 @@ When constructing UI, you must output a VALID A2UI JSON object representing one "description": "Signals the client to delete the surface identified by 'surfaceId'. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.", "properties": { "surfaceId": { - "type": "string" + "type": "string", + "description": "The unique identifier for the UI surface to be deleted." } }, - "required": [ - "surfaceId" - ] + "required": ["surfaceId"], + "additionalProperties": false } }, - "required": [ - "version", - "deleteSurface" - ], + "required": ["deleteSurface", "version"], "additionalProperties": false } - ] + } } ------A2UI_JSON_SCHEMA_END----- +-----MESSAGE_SCHEMA_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 ca6bf4884..53dc1c1fa 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 @@ -115,57 +115,473 @@ When constructing UI, you must output a VALID A2UI JSON object representing one ------------------------------------- ------A2UI_JSON_SCHEMA_START----- +-----COMMON_TYPES_START----- { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "common_types.json", + "title": "A2UI Common Types", + "description": "Common type definitions used across A2UI schemas.", + "$defs": { + "ComponentId": { + "type": "string", + "description": "The unique identifier for a component, used for both definitions and references within the same surface." + }, + "AccessibilityAttributes": { + "type": "object", + "description": "Attributes to enhance accessibility when using assistive technologies like screen readers.", + "properties": { + "label": { + "$ref": "#/$defs/DynamicString", + "description": "A short string, typically 1 to 3 words, used by assistive technologies to convey the purpose or intent of an element. For example, an input field might have an accessible label of 'User ID' or a button might be labeled 'Submit'." + }, + "description": { + "$ref": "#/$defs/DynamicString", + "description": "Additional information provided by assistive technologies about an element such as instructions, format requirements, or result of an action. For example, a mute button might have a label of 'Mute' and a description of 'Silences notifications about this conversation'." + } + } + }, + "ComponentCommon": { + "type": "object", + "properties": { + "id": { + "$ref": "#/$defs/ComponentId" + }, + "accessibility": { + "$ref": "#/$defs/AccessibilityAttributes" + } + }, + "required": ["id"] + }, + "ChildList": { + "oneOf": [ + { + "type": "array", + "items": { + "$ref": "#/$defs/ComponentId" + }, + "description": "A static list of child component IDs." + }, + { + "type": "object", + "description": "A template for generating a dynamic list of children from a data model list. The `componentId` is the component to use as a template.", + "properties": { + "componentId": { + "$ref": "#/$defs/ComponentId" + }, + "path": { + "type": "string", + "description": "The path to the list of component property objects in the data model." + } + }, + "required": ["componentId", "path"], + "additionalProperties": false + } + ] + }, + "DataBinding": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "A JSON Pointer path to a value in the data model." + } + }, + "required": ["path"], + "additionalProperties": false + }, + "DynamicValue": { + "description": "A value that can be a literal, a path, or a function call returning any type.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "array" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "$ref": "#/$defs/FunctionCall" + } + ] + }, + "DynamicString": { + "description": "Represents a string", + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "string" + } + } + } + ] + } + ] + }, + "DynamicNumber": { + "description": "Represents a value that can be either a literal number, a path to a number in the data model, or a function call returning a number.", + "oneOf": [ + { + "type": "number" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "number" + } + } + } + ] + } + ] + }, + "DynamicBoolean": { + "description": "A boolean value that can be a literal, a path, or a function call returning a boolean.", + "oneOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "boolean" + } + } + } + ] + } + ] + }, + "DynamicStringList": { + "description": "Represents a value that can be either a literal array of strings, a path to a string array in the data model, or a function call returning a string array.", + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "array" + } + } + } + ] + } + ] + }, + "FunctionCall": { + "type": "object", + "description": "Invokes a named function on the client.", + "properties": { + "call": { + "type": "string", + "description": "The name of the function to call." + }, + "args": { + "type": "object", + "description": "Arguments passed to the function.", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/DynamicValue" + }, + { + "type": "object", + "description": "A literal object argument (e.g. configuration)." + } + ] + } + }, + "returnType": { + "type": "string", + "description": "The expected return type of the function call.", + "enum": ["string", "number", "boolean", "array", "object", "any", "void"], + "default": "boolean" + } + }, + "required": ["call"], + "oneOf": [{"$ref": "catalog.json#/$defs/anyFunction"}] + }, + "CheckRule": { + "type": "object", + "description": "A single validation rule applied to an input component.", + "properties": { + "condition": { + "$ref": "#/$defs/DynamicBoolean" + }, + "message": { + "type": "string", + "description": "The error message to display if the check fails." + } + }, + "required": ["condition", "message"], + "additionalProperties": false + }, + "Checkable": { + "description": "Properties for components that support client-side checks.", + "type": "object", + "properties": { + "checks": { + "type": "array", + "description": "A list of checks to perform. These are function calls that must return a boolean indicating validity.", + "items": { + "$ref": "#/$defs/CheckRule" + } + } + } + }, + "Action": { + "description": "Defines an interaction handler that can either trigger a server-side event or execute a local client-side function.", + "oneOf": [ + { + "type": "object", + "description": "Triggers a server-side event.", + "properties": { + "event": { + "type": "object", + "description": "The event to dispatch to the server.", + "properties": { + "name": { + "type": "string", + "description": "The name of the action to be dispatched to the server." + }, + "context": { + "type": "object", + "description": "A JSON object containing the key-value pairs for the action context. Values can be literals or paths. Use literal values unless the value must be dynamically bound to the data model. Do NOT use paths for static IDs.", + "additionalProperties": { + "$ref": "#/$defs/DynamicValue" + } + } + }, + "required": ["name"], + "additionalProperties": false + } + }, + "required": ["event"], + "additionalProperties": false + }, + { + "type": "object", + "description": "Executes a local client-side function.", + "properties": { + "functionCall": { + "$ref": "#/$defs/FunctionCall" + } + }, + "required": ["functionCall"], + "additionalProperties": false + } + ] + } + } +} +-----COMMON_TYPES_END----- + +------------------------------------- + +-----CATALOG_SCHEMA_START----- +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://a2ui.org/specification/v0_9/catalog.json", + "title": "A2UI Catalog", + "description": "Custom catalog of A2UI components and functions.", + "catalogId": "test_catalog", + "components": { + "Text": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "type": "string", + "enum": [ + "Text" + ] + }, + "text": { + "description": "While simple Markdown is supported (without HTML or image references), utilizing dedicated UI components is generally preferred for a richer and more structured presentation.", + "$ref": "common_types.json#/$defs/DynamicString" + }, + "variant": { + "type": "string", + "description": "A hint for the base text style.", + "enum": [ + "h1", + "h2", + "h3", + "h4", + "h5", + "caption", + "body" + ] + } + }, + "required": [ + "component", + "text" + ] + } + ], + "unevaluatedProperties": false + } + }, + "$defs": { + "CatalogComponentCommon": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "A unique identifier for this component instance within the surface. This ID is used to refer to the component in layout children arrays or event handlers." + } + }, + "required": [ + "id" + ] + }, + "theme": { + "type": "object", + "properties": { + "primaryColor": { + "type": "string", + "description": "The primary brand color used for highlights (e.g., primary buttons, active borders). Renderers may generate variants of this color for different contexts. Format: Hexadecimal code (e.g., '#00BFFF').", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "iconUrl": { + "type": "string", + "format": "uri", + "description": "A URL for an image that identifies the agent or tool associated with the surface." + }, + "agentDisplayName": { + "type": "string", + "description": "Text to be displayed next to the surface to identify the agent or tool that created it." + } + }, + "additionalProperties": true + }, + "anyComponent": { + "oneOf": [ + { + "$ref": "#/components/Text" + } + ], + "discriminator": { + "propertyName": "component" + } + }, + "anyFunction": { + "not": {} + } + } +} +-----CATALOG_SCHEMA_END----- + +------------------------------------- + +-----MESSAGE_SCHEMA_START----- +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://a2ui.org/specification/v0_9/server_to_client.json", "title": "A2UI Message Schema", "description": "Describes a JSON payload for an A2UI (Agent to UI) message, which is used to dynamically construct and update user interfaces.", + "type": "object", "oneOf": [ - { + {"$ref": "#/$defs/CreateSurfaceMessage"}, + {"$ref": "#/$defs/UpdateComponentsMessage"}, + {"$ref": "#/$defs/UpdateDataModelMessage"}, + {"$ref": "#/$defs/DeleteSurfaceMessage"} + ], + "$defs": { + "CreateSurfaceMessage": { "type": "object", "properties": { "version": { - "type": "string", "const": "v0.9" }, "createSurface": { "type": "object", - "description": "Signals the client to create a new surface and begin rendering it. When this message is sent, the client will expect 'updateComponents' and/or 'updateDataModel' messages for the same surfaceId that define the component tree.", + "description": "Signals the client to create a new surface and begin rendering it. It is an error to send 'createSurface' for a surfaceId that already exists without first deleting it. When this message is sent, the client will expect 'updateComponents' and/or 'updateDataModel' messages for the same surfaceId that define the component tree.", "properties": { "surfaceId": { "type": "string", - "description": "The unique ID for the surface." + "description": "The unique identifier for the UI surface to be rendered." }, "catalogId": { - "type": "string", - "description": "The URI of the component catalog." + "description": "A string that uniquely identifies this catalog. It is recommended to prefix this with an internet domain that you own, to avoid conflicts e.g. mycompany.com:somecatalog'.", + "type": "string" }, "theme": { - "type": "object", - "description": "Theme parameters for the surface.", - "additionalProperties": true + "$ref": "catalog.json#/$defs/theme", + "description": "Theme parameters for the surface (e.g., {'primaryColor': '#FF0000'}). These must validate against the 'theme' schema defined in the catalog." }, "sendDataModel": { "type": "boolean", - "description": "Whether to send the data model to every client request." + "description": "If true, the client will send the full data model of this surface in the metadata of every A2A message sent to the server that created the surface. Defaults to false." } }, - "required": [ - "surfaceId", - "catalogId" - ] + "required": ["surfaceId", "catalogId"], + "additionalProperties": false } }, - "required": [ - "version", - "createSurface" - ], + "required": ["createSurface", "version"], "additionalProperties": false }, - { + "UpdateComponentsMessage": { "type": "object", "properties": { "version": { - "type": "string", "const": "v0.9" }, "updateComponents": { @@ -174,104 +590,29 @@ When constructing UI, you must output a VALID A2UI JSON object representing one "properties": { "surfaceId": { "type": "string", - "description": "The unique identifier for the UI surface." + "description": "The unique identifier for the UI surface to be updated." }, + "components": { "type": "array", - "description": "A flat list of component definitions.", + "description": "A list containing all UI components for the surface.", + "minItems": 1, "items": { - "description": "Must match one of the component definitions in the catalog.", - "oneOf": [ - { - "type": "object", - "description": "A block of styled text.", - "properties": { - "text": { - "description": "While simple Markdown is supported (without HTML or image references), utilizing dedicated UI components is generally preferred for a richer and more structured presentation.", - "oneOf": [ - { - "type": "string", - "description": "A literal string value." - }, - { - "type": "object", - "description": "A path to a string.", - "properties": { - "path": { - "type": "string", - "description": "A relative or absolute path in the data model." - } - }, - "required": [ - "path" - ] - }, - { - "type": "object", - "properties": { - "call": { - "type": "string", - "description": "The name of the function to call." - }, - "args": { - "type": "object", - "description": "Arguments to pass to the function.", - "additionalProperties": true - } - }, - "required": [ - "call" - ] - } - ] - }, - "variant": { - "type": "string", - "description": "A hint for the base text style.", - "enum": [ - "h1", - "h2", - "h3", - "h4", - "h5", - "caption", - "body" - ] - }, - "component": { - "type": "string", - "enum": [ - "Text" - ] - } - }, - "required": [ - "component", - "text" - ] - } - ] - }, - "minItems": 1 + "$ref": "catalog.json#/$defs/anyComponent" + } } }, - "required": [ - "surfaceId", - "components" - ] + "required": ["surfaceId", "components"], + "additionalProperties": false } }, - "required": [ - "version", - "updateComponents" - ], + "required": ["updateComponents", "version"], "additionalProperties": false }, - { + "UpdateDataModelMessage": { "type": "object", "properties": { "version": { - "type": "string", "const": "v0.9" }, "updateDataModel": { @@ -279,32 +620,29 @@ When constructing UI, you must output a VALID A2UI JSON object representing one "description": "Updates the data model for an existing surface. This message can be sent multiple times to update the data model. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.", "properties": { "surfaceId": { - "type": "string" + "type": "string", + "description": "The unique identifier for the UI surface this data model update applies to." }, "path": { "type": "string", - "default": "/" + "description": "An optional path to a location within the data model (e.g., '/user/name'). If omitted, or set to '/', refers to the entire data model." }, "value": { - "description": "The new value to write to the data model. If null/omitted, the key is removed." + "description": "The data to be updated in the data model. If present, the value at 'path' is replaced (or created). If omitted, the key at 'path' is removed.", + "additionalProperties": true } }, - "required": [ - "surfaceId" - ] + "required": ["surfaceId"], + "additionalProperties": false } }, - "required": [ - "version", - "updateDataModel" - ], + "required": ["updateDataModel", "version"], "additionalProperties": false }, - { + "DeleteSurfaceMessage": { "type": "object", "properties": { "version": { - "type": "string", "const": "v0.9" }, "deleteSurface": { @@ -312,20 +650,17 @@ When constructing UI, you must output a VALID A2UI JSON object representing one "description": "Signals the client to delete the surface identified by 'surfaceId'. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.", "properties": { "surfaceId": { - "type": "string" + "type": "string", + "description": "The unique identifier for the UI surface to be deleted." } }, - "required": [ - "surfaceId" - ] + "required": ["surfaceId"], + "additionalProperties": false } }, - "required": [ - "version", - "deleteSurface" - ], + "required": ["deleteSurface", "version"], "additionalProperties": false } - ] + } } ------A2UI_JSON_SCHEMA_END----- +-----MESSAGE_SCHEMA_END----- 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 e04604e4f..c68c94788 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 @@ -117,57 +117,473 @@ When constructing UI, you must output a VALID A2UI JSON object representing one ------------------------------------- ------A2UI_JSON_SCHEMA_START----- +-----COMMON_TYPES_START----- { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "common_types.json", + "title": "A2UI Common Types", + "description": "Common type definitions used across A2UI schemas.", + "$defs": { + "ComponentId": { + "type": "string", + "description": "The unique identifier for a component, used for both definitions and references within the same surface." + }, + "AccessibilityAttributes": { + "type": "object", + "description": "Attributes to enhance accessibility when using assistive technologies like screen readers.", + "properties": { + "label": { + "$ref": "#/$defs/DynamicString", + "description": "A short string, typically 1 to 3 words, used by assistive technologies to convey the purpose or intent of an element. For example, an input field might have an accessible label of 'User ID' or a button might be labeled 'Submit'." + }, + "description": { + "$ref": "#/$defs/DynamicString", + "description": "Additional information provided by assistive technologies about an element such as instructions, format requirements, or result of an action. For example, a mute button might have a label of 'Mute' and a description of 'Silences notifications about this conversation'." + } + } + }, + "ComponentCommon": { + "type": "object", + "properties": { + "id": { + "$ref": "#/$defs/ComponentId" + }, + "accessibility": { + "$ref": "#/$defs/AccessibilityAttributes" + } + }, + "required": ["id"] + }, + "ChildList": { + "oneOf": [ + { + "type": "array", + "items": { + "$ref": "#/$defs/ComponentId" + }, + "description": "A static list of child component IDs." + }, + { + "type": "object", + "description": "A template for generating a dynamic list of children from a data model list. The `componentId` is the component to use as a template.", + "properties": { + "componentId": { + "$ref": "#/$defs/ComponentId" + }, + "path": { + "type": "string", + "description": "The path to the list of component property objects in the data model." + } + }, + "required": ["componentId", "path"], + "additionalProperties": false + } + ] + }, + "DataBinding": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "A JSON Pointer path to a value in the data model." + } + }, + "required": ["path"], + "additionalProperties": false + }, + "DynamicValue": { + "description": "A value that can be a literal, a path, or a function call returning any type.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "array" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "$ref": "#/$defs/FunctionCall" + } + ] + }, + "DynamicString": { + "description": "Represents a string", + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "string" + } + } + } + ] + } + ] + }, + "DynamicNumber": { + "description": "Represents a value that can be either a literal number, a path to a number in the data model, or a function call returning a number.", + "oneOf": [ + { + "type": "number" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "number" + } + } + } + ] + } + ] + }, + "DynamicBoolean": { + "description": "A boolean value that can be a literal, a path, or a function call returning a boolean.", + "oneOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "boolean" + } + } + } + ] + } + ] + }, + "DynamicStringList": { + "description": "Represents a value that can be either a literal array of strings, a path to a string array in the data model, or a function call returning a string array.", + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "array" + } + } + } + ] + } + ] + }, + "FunctionCall": { + "type": "object", + "description": "Invokes a named function on the client.", + "properties": { + "call": { + "type": "string", + "description": "The name of the function to call." + }, + "args": { + "type": "object", + "description": "Arguments passed to the function.", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/DynamicValue" + }, + { + "type": "object", + "description": "A literal object argument (e.g. configuration)." + } + ] + } + }, + "returnType": { + "type": "string", + "description": "The expected return type of the function call.", + "enum": ["string", "number", "boolean", "array", "object", "any", "void"], + "default": "boolean" + } + }, + "required": ["call"], + "oneOf": [{"$ref": "catalog.json#/$defs/anyFunction"}] + }, + "CheckRule": { + "type": "object", + "description": "A single validation rule applied to an input component.", + "properties": { + "condition": { + "$ref": "#/$defs/DynamicBoolean" + }, + "message": { + "type": "string", + "description": "The error message to display if the check fails." + } + }, + "required": ["condition", "message"], + "additionalProperties": false + }, + "Checkable": { + "description": "Properties for components that support client-side checks.", + "type": "object", + "properties": { + "checks": { + "type": "array", + "description": "A list of checks to perform. These are function calls that must return a boolean indicating validity.", + "items": { + "$ref": "#/$defs/CheckRule" + } + } + } + }, + "Action": { + "description": "Defines an interaction handler that can either trigger a server-side event or execute a local client-side function.", + "oneOf": [ + { + "type": "object", + "description": "Triggers a server-side event.", + "properties": { + "event": { + "type": "object", + "description": "The event to dispatch to the server.", + "properties": { + "name": { + "type": "string", + "description": "The name of the action to be dispatched to the server." + }, + "context": { + "type": "object", + "description": "A JSON object containing the key-value pairs for the action context. Values can be literals or paths. Use literal values unless the value must be dynamically bound to the data model. Do NOT use paths for static IDs.", + "additionalProperties": { + "$ref": "#/$defs/DynamicValue" + } + } + }, + "required": ["name"], + "additionalProperties": false + } + }, + "required": ["event"], + "additionalProperties": false + }, + { + "type": "object", + "description": "Executes a local client-side function.", + "properties": { + "functionCall": { + "$ref": "#/$defs/FunctionCall" + } + }, + "required": ["functionCall"], + "additionalProperties": false + } + ] + } + } +} +-----COMMON_TYPES_END----- + +------------------------------------- + +-----CATALOG_SCHEMA_START----- +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://a2ui.org/specification/v0_9/catalog.json", + "title": "A2UI Catalog", + "description": "Custom catalog of A2UI components and functions.", + "catalogId": "test_catalog", + "components": { + "Text": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "type": "string", + "enum": [ + "Text" + ] + }, + "text": { + "description": "While simple Markdown is supported (without HTML or image references), utilizing dedicated UI components is generally preferred for a richer and more structured presentation.", + "$ref": "common_types.json#/$defs/DynamicString" + }, + "variant": { + "type": "string", + "description": "A hint for the base text style.", + "enum": [ + "h1", + "h2", + "h3", + "h4", + "h5", + "caption", + "body" + ] + } + }, + "required": [ + "component", + "text" + ] + } + ], + "unevaluatedProperties": false + } + }, + "$defs": { + "CatalogComponentCommon": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "A unique identifier for this component instance within the surface. This ID is used to refer to the component in layout children arrays or event handlers." + } + }, + "required": [ + "id" + ] + }, + "theme": { + "type": "object", + "properties": { + "primaryColor": { + "type": "string", + "description": "The primary brand color used for highlights (e.g., primary buttons, active borders). Renderers may generate variants of this color for different contexts. Format: Hexadecimal code (e.g., '#00BFFF').", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "iconUrl": { + "type": "string", + "format": "uri", + "description": "A URL for an image that identifies the agent or tool associated with the surface." + }, + "agentDisplayName": { + "type": "string", + "description": "Text to be displayed next to the surface to identify the agent or tool that created it." + } + }, + "additionalProperties": true + }, + "anyComponent": { + "oneOf": [ + { + "$ref": "#/components/Text" + } + ], + "discriminator": { + "propertyName": "component" + } + }, + "anyFunction": { + "not": {} + } + } +} +-----CATALOG_SCHEMA_END----- + +------------------------------------- + +-----MESSAGE_SCHEMA_START----- +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://a2ui.org/specification/v0_9/server_to_client.json", "title": "A2UI Message Schema", "description": "Describes a JSON payload for an A2UI (Agent to UI) message, which is used to dynamically construct and update user interfaces.", + "type": "object", "oneOf": [ - { + {"$ref": "#/$defs/CreateSurfaceMessage"}, + {"$ref": "#/$defs/UpdateComponentsMessage"}, + {"$ref": "#/$defs/UpdateDataModelMessage"}, + {"$ref": "#/$defs/DeleteSurfaceMessage"} + ], + "$defs": { + "CreateSurfaceMessage": { "type": "object", "properties": { "version": { - "type": "string", "const": "v0.9" }, "createSurface": { "type": "object", - "description": "Signals the client to create a new surface and begin rendering it. When this message is sent, the client will expect 'updateComponents' and/or 'updateDataModel' messages for the same surfaceId that define the component tree.", + "description": "Signals the client to create a new surface and begin rendering it. It is an error to send 'createSurface' for a surfaceId that already exists without first deleting it. When this message is sent, the client will expect 'updateComponents' and/or 'updateDataModel' messages for the same surfaceId that define the component tree.", "properties": { "surfaceId": { "type": "string", - "description": "The unique ID for the surface." + "description": "The unique identifier for the UI surface to be rendered." }, "catalogId": { - "type": "string", - "description": "The URI of the component catalog." + "description": "A string that uniquely identifies this catalog. It is recommended to prefix this with an internet domain that you own, to avoid conflicts e.g. mycompany.com:somecatalog'.", + "type": "string" }, "theme": { - "type": "object", - "description": "Theme parameters for the surface.", - "additionalProperties": true + "$ref": "catalog.json#/$defs/theme", + "description": "Theme parameters for the surface (e.g., {'primaryColor': '#FF0000'}). These must validate against the 'theme' schema defined in the catalog." }, "sendDataModel": { "type": "boolean", - "description": "Whether to send the data model to every client request." + "description": "If true, the client will send the full data model of this surface in the metadata of every A2A message sent to the server that created the surface. Defaults to false." } }, - "required": [ - "surfaceId", - "catalogId" - ] + "required": ["surfaceId", "catalogId"], + "additionalProperties": false } }, - "required": [ - "version", - "createSurface" - ], + "required": ["createSurface", "version"], "additionalProperties": false }, - { + "UpdateComponentsMessage": { "type": "object", "properties": { "version": { - "type": "string", "const": "v0.9" }, "updateComponents": { @@ -176,104 +592,29 @@ When constructing UI, you must output a VALID A2UI JSON object representing one "properties": { "surfaceId": { "type": "string", - "description": "The unique identifier for the UI surface." + "description": "The unique identifier for the UI surface to be updated." }, + "components": { "type": "array", - "description": "A flat list of component definitions.", + "description": "A list containing all UI components for the surface.", + "minItems": 1, "items": { - "description": "Must match one of the component definitions in the catalog.", - "oneOf": [ - { - "type": "object", - "description": "A block of styled text.", - "properties": { - "text": { - "description": "While simple Markdown is supported (without HTML or image references), utilizing dedicated UI components is generally preferred for a richer and more structured presentation.", - "oneOf": [ - { - "type": "string", - "description": "A literal string value." - }, - { - "type": "object", - "description": "A path to a string.", - "properties": { - "path": { - "type": "string", - "description": "A relative or absolute path in the data model." - } - }, - "required": [ - "path" - ] - }, - { - "type": "object", - "properties": { - "call": { - "type": "string", - "description": "The name of the function to call." - }, - "args": { - "type": "object", - "description": "Arguments to pass to the function.", - "additionalProperties": true - } - }, - "required": [ - "call" - ] - } - ] - }, - "variant": { - "type": "string", - "description": "A hint for the base text style.", - "enum": [ - "h1", - "h2", - "h3", - "h4", - "h5", - "caption", - "body" - ] - }, - "component": { - "type": "string", - "enum": [ - "Text" - ] - } - }, - "required": [ - "component", - "text" - ] - } - ] - }, - "minItems": 1 + "$ref": "catalog.json#/$defs/anyComponent" + } } }, - "required": [ - "surfaceId", - "components" - ] + "required": ["surfaceId", "components"], + "additionalProperties": false } }, - "required": [ - "version", - "updateComponents" - ], + "required": ["updateComponents", "version"], "additionalProperties": false }, - { + "UpdateDataModelMessage": { "type": "object", "properties": { "version": { - "type": "string", "const": "v0.9" }, "updateDataModel": { @@ -281,32 +622,29 @@ When constructing UI, you must output a VALID A2UI JSON object representing one "description": "Updates the data model for an existing surface. This message can be sent multiple times to update the data model. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.", "properties": { "surfaceId": { - "type": "string" + "type": "string", + "description": "The unique identifier for the UI surface this data model update applies to." }, "path": { "type": "string", - "default": "/" + "description": "An optional path to a location within the data model (e.g., '/user/name'). If omitted, or set to '/', refers to the entire data model." }, "value": { - "description": "The new value to write to the data model. If null/omitted, the key is removed." + "description": "The data to be updated in the data model. If present, the value at 'path' is replaced (or created). If omitted, the key at 'path' is removed.", + "additionalProperties": true } }, - "required": [ - "surfaceId" - ] + "required": ["surfaceId"], + "additionalProperties": false } }, - "required": [ - "version", - "updateDataModel" - ], + "required": ["updateDataModel", "version"], "additionalProperties": false }, - { + "DeleteSurfaceMessage": { "type": "object", "properties": { "version": { - "type": "string", "const": "v0.9" }, "deleteSurface": { @@ -314,20 +652,17 @@ When constructing UI, you must output a VALID A2UI JSON object representing one "description": "Signals the client to delete the surface identified by 'surfaceId'. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.", "properties": { "surfaceId": { - "type": "string" + "type": "string", + "description": "The unique identifier for the UI surface to be deleted." } }, - "required": [ - "surfaceId" - ] + "required": ["surfaceId"], + "additionalProperties": false } }, - "required": [ - "version", - "deleteSurface" - ], + "required": ["deleteSurface", "version"], "additionalProperties": false } - ] + } } ------A2UI_JSON_SCHEMA_END----- +-----MESSAGE_SCHEMA_END----- diff --git a/packages/genui/test/test_infra/validation_test_utils.dart b/packages/genui/test/test_infra/validation_test_utils.dart index 415f75d48..12becba04 100644 --- a/packages/genui/test/test_infra/validation_test_utils.dart +++ b/packages/genui/test/test_infra/validation_test_utils.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:convert'; +import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:genui/src/model/a2ui_message.dart'; @@ -12,6 +13,8 @@ import 'package:genui/src/model/catalog_item.dart'; import 'package:genui/src/model/ui_models.dart'; import 'package:genui/src/primitives/simple_items.dart'; import 'package:json_schema_builder/json_schema_builder.dart'; +// ignore: implementation_imports +import 'package:json_schema_builder/src/schema_registry.dart'; /// Validates the examples in the catalog items in the catalog. void validateCatalogExamples( @@ -53,8 +56,21 @@ void validateCatalogExamples( components: components, ); + final String commonTypesContent = File( + 'submodules/a2ui/specification/v0_9/json/common_types.json', + ).readAsStringSync(); + final commonTypesSchema = Schema.fromMap( + jsonDecode(commonTypesContent) as Map, + ); + final registry = SchemaRegistry(); + registry.addSchema( + Uri.parse('https://a2ui.org/specification/v0_9/common_types.json'), + commonTypesSchema, + ); + final List validationErrors = await schema.validate( surfaceUpdate.toJson(), + schemaRegistry: registry, ); expect(validationErrors, isEmpty); });