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/packages/genui/lib/src/facade/prompt_builder.dart b/packages/genui/lib/src/facade/prompt_builder.dart index 77ecada7c..60b8366ba 100644 --- a/packages/genui/lib/src/facade/prompt_builder.dart +++ b/packages/genui/lib/src/facade/prompt_builder.dart @@ -5,8 +5,8 @@ import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; -import '../model/a2ui_message.dart'; import '../model/catalog.dart'; import '../primitives/simple_items.dart'; @@ -78,12 +78,18 @@ 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 = await rootBundle.loadString( + 'packages/genui/submodules/a2ui/specification/v0_9/json/common_types.json', + ); + final String serverToClient = await rootBundle.loadString( + 'packages/genui/submodules/a2ui/specification/v0_9/json/server_to_client.json', + ); return _BasicPromptBuilder( catalog: catalog, systemPromptFragments: systemPromptFragments, @@ -91,10 +97,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 +110,13 @@ abstract class PromptBuilder { TechnicalPossibilities technicalPossibilities = const TechnicalPossibilities(), JsonMap? clientDataModel, - }) { + }) async { + final String commonTypes = await rootBundle.loadString( + 'packages/genui/submodules/a2ui/specification/v0_9/json/common_types.json', + ); + final String serverToClient = await rootBundle.loadString( + 'packages/genui/submodules/a2ui/specification/v0_9/json/server_to_client.json', + ); return _BasicPromptBuilder( catalog: catalog, systemPromptFragments: systemPromptFragments, @@ -110,6 +124,8 @@ abstract class PromptBuilder { importancePrefix: importancePrefix, clientDataModel: clientDataModel, technicalPossibilities: technicalPossibilities, + commonTypesSchema: commonTypes, + serverToClientSchema: serverToClient, ); } @@ -332,9 +348,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; @@ -359,9 +379,7 @@ final class _BasicPromptBuilder extends PromptBuilder { @override Iterable systemPrompt() { - final String a2uiSchema = A2uiMessage.a2uiMessageSchema( - catalog, - ).toJson(indent: ' '); + final String catalogSchema = _generateCatalogSchema(catalog); final fragments = [ ...systemPromptFragments, @@ -369,13 +387,76 @@ final class _BasicPromptBuilder extends PromptBuilder { ...technicalPossibilities.systemPromptFragment(), ...catalog.systemPromptFragments, ...allowedOperations.systemPromptFragments, - _fenced(a2uiSchema, sectionName: 'A2UI JSON SCHEMA'), + _fenced(commonTypesSchema, sectionName: 'COMMON TYPES'), + _fenced(catalogSchema, sectionName: 'CATALOG SCHEMA'), + _fenced(serverToClientSchema, 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', + ...?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'], + }, + }, + }; + + return const JsonEncoder.withIndent(' ').convert(catalogJson); + } + static String? _encodedDataModel(JsonMap? clientDataModel) { if (clientDataModel == null) return null; final String encodedModel = const JsonEncoder.withIndent( diff --git a/packages/genui/lib/src/model/a2ui_schemas.dart b/packages/genui/lib/src/model/a2ui_schemas.dart index 29ba6feca..809e68a39 100644 --- a/packages/genui/lib/src/model/a2ui_schemas.dart +++ b/packages/genui/lib/src/model/a2ui_schemas.dart @@ -280,17 +280,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: r'common_types.json#/$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 +292,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) { + 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(); + return S.combined( + oneOf: [literal, binding, function], + description: description, + ); + } return S.combined( - oneOf: [literal, binding, function], + $ref: r'common_types.json#/$defs/DynamicString', description: description, ); } @@ -317,13 +315,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: r'common_types.json#/$defs/DynamicNumber', description: description, ); } @@ -331,13 +324,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: r'common_types.json#/$defs/DynamicBoolean', description: description, ); } @@ -383,46 +371,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: r'common_types.json#/$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: r'common_types.json#/$defs/DynamicStringList', description: description, ); } 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/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/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..0ba8f16fb 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,440 @@ 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": "https://a2ui.org/specification/v0_9/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" + ] + } + } +} +-----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 +567,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 +597,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 +627,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..f2f0368c9 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,440 @@ 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": "https://a2ui.org/specification/v0_9/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" + ] + } + } +} +-----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 +569,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 +599,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 +629,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..3536755a8 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,440 @@ 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": "https://a2ui.org/specification/v0_9/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" + ] + } + } +} +-----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 +565,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 +595,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 +625,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..bdf75d6db 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,440 @@ 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": "https://a2ui.org/specification/v0_9/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" + ] + } + } +} +-----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 +567,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 +597,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 +627,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..77d044824 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,440 @@ 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": "https://a2ui.org/specification/v0_9/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" + ] + } + } +} +-----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 +564,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 +594,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 +624,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..b8de9eb01 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,440 @@ 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": "https://a2ui.org/specification/v0_9/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" + ] + } + } +} +-----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 +566,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 +596,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 +626,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..8feb03773 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,440 @@ 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": "https://a2ui.org/specification/v0_9/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" + ] + } + } +} +-----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 +557,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 +587,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 +617,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..65b245b72 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,440 @@ 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": "https://a2ui.org/specification/v0_9/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" + ] + } + } +} +-----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 +559,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 +589,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 +619,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-----