From a24693d2154e03b2a56e2b989c5de095cd0859b5 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Thu, 14 May 2026 13:36:12 -0700 Subject: [PATCH 1/5] feat(genui): support functions in prompts and verify rendering - Update PromptBuilder to include available functions in the prompt when present in the catalog. - Add a widget test to verify that function output (specifically pluralize) renders correctly. - Add a test to verify that PromptBuilder includes the functions section. Follow-up for #874 and #873. --- .../genui/lib/src/facade/prompt_builder.dart | 13 +++ .../catalog/functions_rendering_test.dart | 85 +++++++++++++++++++ .../test/facade/prompt_builder_test.dart | 24 ++++++ 3 files changed, 122 insertions(+) create mode 100644 packages/genui/test/catalog/functions_rendering_test.dart diff --git a/packages/genui/lib/src/facade/prompt_builder.dart b/packages/genui/lib/src/facade/prompt_builder.dart index 77ecada7c..76081c4c1 100644 --- a/packages/genui/lib/src/facade/prompt_builder.dart +++ b/packages/genui/lib/src/facade/prompt_builder.dart @@ -370,6 +370,19 @@ final class _BasicPromptBuilder extends PromptBuilder { ...catalog.systemPromptFragments, ...allowedOperations.systemPromptFragments, _fenced(a2uiSchema, sectionName: 'A2UI JSON SCHEMA'), + if (catalog.functions.isNotEmpty) + _fenced( + const JsonEncoder.withIndent(' ').convert([ + for (final func in catalog.functions) + { + 'name': func.name, + 'description': func.description, + 'parameters': func.argumentSchema.value, + 'returnType': func.returnType.value, + }, + ]), + sectionName: 'AVAILABLE FUNCTIONS', + ), ?_encodedDataModel(clientDataModel), ]; 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..1bc59c613 100644 --- a/packages/genui/test/facade/prompt_builder_test.dart +++ b/packages/genui/test/facade/prompt_builder_test.dart @@ -123,4 +123,28 @@ void main() { }); } }); + + group('Prompt with functions', () { + test('includes functions section when catalog has functions', () { + final catalogWithFunctions = Catalog( + [BasicCatalogItems.text], + functions: [BasicFunctions.pluralizeFunction], + catalogId: 'test_catalog', + ); + + final String prompt = PromptBuilder.chat( + catalog: catalogWithFunctions, + ).systemPromptJoined(); + + expect(prompt, contains('AVAILABLE_FUNCTIONS')); + expect(prompt, contains('pluralize')); + expect( + prompt, + contains( + 'Returns a localized string based on the Common Locale Data ' + 'Repository', + ), + ); + }); + }); } From 4134cb3a111b2d2f3e711421ba2e42769cad25fc Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Thu, 14 May 2026 16:59:42 -0700 Subject: [PATCH 2/5] feat(genui): refactor PromptBuilder to use spec schemas and async API - Refactor `PromptBuilder` to load `server_to_client.json` and `common_types.json` as assets and use `$refs` in the generated prompt. - Make `PromptBuilder` creation asynchronous to support asset loading. - Fix examples (`composer`, `simple_chat`, `travel_app`) to use the new async API. - Add mock asset handlers in tests to support loading schemas. - Update golden files for prompt builder tests. Resolves #873 and #874. --- examples/composer/lib/create_tab.dart | 2 +- examples/simple_chat/lib/chat_session.dart | 34 +- .../google_generative_ai_client.dart | 2 +- .../google_generative_ai_client_test.dart | 39 ++ .../genui/lib/src/facade/prompt_builder.dart | 112 +++- packages/genui/pubspec.yaml | 5 + .../test/facade/prompt_builder_test.dart | 87 ++- .../all_operations_with_dataModel_false.txt | 594 ++++++++++++++---- .../all_operations_with_dataModel_true.txt | 594 ++++++++++++++---- ...create_and_update_with_dataModel_false.txt | 594 ++++++++++++++---- .../create_and_update_with_dataModel_true.txt | 594 ++++++++++++++---- .../create_only_with_dataModel_false.txt | 594 ++++++++++++++---- .../create_only_with_dataModel_true.txt | 594 ++++++++++++++---- .../update_only_with_dataModel_false.txt | 594 ++++++++++++++---- .../update_only_with_dataModel_true.txt | 594 ++++++++++++++---- .../test/functions/format_string_test.dart | 1 + .../test/model/catalog_exception_test.dart | 1 + 17 files changed, 3947 insertions(+), 1088 deletions(-) diff --git a/examples/composer/lib/create_tab.dart b/examples/composer/lib/create_tab.dart index a82a7434b..5ca53e5f2 100644 --- a/examples/composer/lib/create_tab.dart +++ b/examples/composer/lib/create_tab.dart @@ -78,7 +78,7 @@ class _CreateTabState extends State { transport: transport, ); - final promptBuilder = PromptBuilder.chat( + final promptBuilder = await PromptBuilder.createChat( catalog: catalog, systemPromptFragments: [ 'You are a UI generator. The user will describe a UI they want. ' diff --git a/examples/simple_chat/lib/chat_session.dart b/examples/simple_chat/lib/chat_session.dart index eb3fd37e9..2ceb625c2 100644 --- a/examples/simple_chat/lib/chat_session.dart +++ b/examples/simple_chat/lib/chat_session.dart @@ -56,19 +56,20 @@ final Catalog _customCatalog = _basicCatalog.copyWith( newItems: [climbingLocationItem], ); -PromptBuilder _promptBuilderFor(Catalog catalog) => PromptBuilder.chat( - catalog: catalog, - systemPromptFragments: [ - Prompts.summary, - PromptFragments.acknowledgeUser(), - PromptFragments.requireAtLeastOneSubmitElement( - prefix: PromptBuilder.defaultImportancePrefix, - ), - PromptFragments.uiGenerationRestriction( - prefix: PromptBuilder.defaultImportancePrefix, - ), - ], -); +Future _promptBuilderFor(Catalog catalog) async => + await PromptBuilder.createChat( + catalog: catalog, + systemPromptFragments: [ + Prompts.summary, + PromptFragments.acknowledgeUser(), + PromptFragments.requireAtLeastOneSubmitElement( + prefix: PromptBuilder.defaultImportancePrefix, + ), + PromptFragments.uiGenerationRestriction( + prefix: PromptBuilder.defaultImportancePrefix, + ), + ], + ); sealed class ChatSession extends ChangeNotifier { ChatSession._(); @@ -188,7 +189,7 @@ class A2uiChatSession extends ChatSession { late final StreamSubscription _submitSub; late final StreamSubscription _surfaceSub; - void _init() { + Future _init() async { _messageSub = _transport.incomingMessages.listen( _surfaceController.handleMessage, ); @@ -198,9 +199,8 @@ class A2uiChatSession extends ChatSession { ); _surfaceSub = _surfaceController.surfaceUpdates.listen(_onSurfaceUpdate); - _transport.addSystemMessage( - _promptBuilderFor(_catalog).systemPromptJoined(), - ); + final PromptBuilder pb = await _promptBuilderFor(_catalog); + _transport.addSystemMessage(pb.systemPromptJoined()); } void _onSurfaceUpdate(SurfaceUpdate update) { diff --git a/examples/travel_app/lib/src/ai_client/google_generative_ai_client.dart b/examples/travel_app/lib/src/ai_client/google_generative_ai_client.dart index 4d96139c5..5780da915 100644 --- a/examples/travel_app/lib/src/ai_client/google_generative_ai_client.dart +++ b/examples/travel_app/lib/src/ai_client/google_generative_ai_client.dart @@ -471,7 +471,7 @@ class GoogleGenerativeAiClient implements AiClient { var toolUsageCycle = 0; const maxToolUsageCycles = 40; // Safety break for tool loops - final promptBuilder = PromptBuilder.custom( + final PromptBuilder promptBuilder = await PromptBuilder.createCustom( catalog: catalog, systemPromptFragments: systemInstruction, allowedOperations: SurfaceOperations.createAndUpdate(dataModel: true), diff --git a/examples/travel_app/test/ai_client/google_generative_ai_client_test.dart b/examples/travel_app/test/ai_client/google_generative_ai_client_test.dart index 214c7bfb3..e9bd66898 100644 --- a/examples/travel_app/test/ai_client/google_generative_ai_client_test.dart +++ b/examples/travel_app/test/ai_client/google_generative_ai_client_test.dart @@ -2,6 +2,10 @@ // 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_test/flutter_test.dart'; import 'package:genui/genui.dart'; import 'package:google_cloud_ai_generativelanguage_v1beta/generativelanguage.dart' @@ -10,6 +14,41 @@ import 'package:travel_app/src/ai_client/google_generative_ai_client.dart'; import 'package:travel_app/src/ai_client/google_generative_service_interface.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; + }); + }); + group('GoogleGenerativeAiClient', () { late FakeGoogleGenerativeService fakeService; late GoogleGenerativeAiClient client; diff --git a/packages/genui/lib/src/facade/prompt_builder.dart b/packages/genui/lib/src/facade/prompt_builder.dart index 76081c4c1..bd31948bd 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,26 +387,76 @@ final class _BasicPromptBuilder extends PromptBuilder { ...technicalPossibilities.systemPromptFragment(), ...catalog.systemPromptFragments, ...allowedOperations.systemPromptFragments, - _fenced(a2uiSchema, sectionName: 'A2UI JSON SCHEMA'), - if (catalog.functions.isNotEmpty) - _fenced( - const JsonEncoder.withIndent(' ').convert([ - for (final func in catalog.functions) - { - 'name': func.name, - 'description': func.description, - 'parameters': func.argumentSchema.value, - 'returnType': func.returnType.value, - }, - ]), - sectionName: 'AVAILABLE FUNCTIONS', - ), + _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?, + ], + }, + ], + '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/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/facade/prompt_builder_test.dart b/packages/genui/test/facade/prompt_builder_test.dart index 1bc59c613..5cd269685 100644 --- a/packages/genui/test/facade/prompt_builder_test.dart +++ b/packages/genui/test/facade/prompt_builder_test.dart @@ -2,12 +2,51 @@ // 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_test/flutter_test.dart'; import 'package:genui/genui.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 +60,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 +104,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)); @@ -125,18 +167,17 @@ void main() { }); group('Prompt with functions', () { - test('includes functions section when catalog has functions', () { + test('includes functions when catalog has functions', () async { final catalogWithFunctions = Catalog( [BasicCatalogItems.text], functions: [BasicFunctions.pluralizeFunction], catalogId: 'test_catalog', ); - final String prompt = PromptBuilder.chat( + final String prompt = (await PromptBuilder.createChat( catalog: catalogWithFunctions, - ).systemPromptJoined(); + )).systemPromptJoined(); - expect(prompt, contains('AVAILABLE_FUNCTIONS')); expect(prompt, contains('pluralize')); expect( prompt, 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..9558cfe2e 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,476 @@ 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.", + "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" + ] + } + }, + "required": [ + "component", + "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 +603,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 +633,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 +663,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..0b5627350 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,476 @@ 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.", + "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" + ] + } + }, + "required": [ + "component", + "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 +605,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 +635,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 +665,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..04fef3b0f 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,476 @@ 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.", + "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" + ] + } + }, + "required": [ + "component", + "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 +601,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 +631,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 +661,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..cf9095d47 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,476 @@ 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.", + "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" + ] + } + }, + "required": [ + "component", + "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 +603,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 +633,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 +663,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..1f148e85a 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,476 @@ 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.", + "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" + ] + } + }, + "required": [ + "component", + "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 +600,29 @@ When constructing UI, you must output a VALID A2UI JSON object representing one "properties": { "surfaceId": { "type": "string", - "description": "The unique identifier for the UI surface." + "description": "The unique identifier for the UI surface to be updated." }, + "components": { "type": "array", - "description": "A flat list of component definitions.", + "description": "A list containing all UI components for the surface.", + "minItems": 1, "items": { - "description": "Must match one of the component definitions in the catalog.", - "oneOf": [ - { - "type": "object", - "description": "A block of styled text.", - "properties": { - "text": { - "description": "While simple Markdown is supported (without HTML or image references), utilizing dedicated UI components is generally preferred for a richer and more structured presentation.", - "oneOf": [ - { - "type": "string", - "description": "A literal string value." - }, - { - "type": "object", - "description": "A path to a string.", - "properties": { - "path": { - "type": "string", - "description": "A relative or absolute path in the data model." - } - }, - "required": [ - "path" - ] - }, - { - "type": "object", - "properties": { - "call": { - "type": "string", - "description": "The name of the function to call." - }, - "args": { - "type": "object", - "description": "Arguments to pass to the function.", - "additionalProperties": true - } - }, - "required": [ - "call" - ] - } - ] - }, - "variant": { - "type": "string", - "description": "A hint for the base text style.", - "enum": [ - "h1", - "h2", - "h3", - "h4", - "h5", - "caption", - "body" - ] - }, - "component": { - "type": "string", - "enum": [ - "Text" - ] - } - }, - "required": [ - "component", - "text" - ] - } - ] - }, - "minItems": 1 + "$ref": "catalog.json#/$defs/anyComponent" + } } }, - "required": [ - "surfaceId", - "components" - ] + "required": ["surfaceId", "components"], + "additionalProperties": false } }, - "required": [ - "version", - "updateComponents" - ], + "required": ["updateComponents", "version"], "additionalProperties": false }, - { + "UpdateDataModelMessage": { "type": "object", "properties": { "version": { - "type": "string", "const": "v0.9" }, "updateDataModel": { @@ -286,32 +630,29 @@ When constructing UI, you must output a VALID A2UI JSON object representing one "description": "Updates the data model for an existing surface. This message can be sent multiple times to update the data model. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.", "properties": { "surfaceId": { - "type": "string" + "type": "string", + "description": "The unique identifier for the UI surface this data model update applies to." }, "path": { "type": "string", - "default": "/" + "description": "An optional path to a location within the data model (e.g., '/user/name'). If omitted, or set to '/', refers to the entire data model." }, "value": { - "description": "The new value to write to the data model. If null/omitted, the key is removed." + "description": "The data to be updated in the data model. If present, the value at 'path' is replaced (or created). If omitted, the key at 'path' is removed.", + "additionalProperties": true } }, - "required": [ - "surfaceId" - ] + "required": ["surfaceId"], + "additionalProperties": false } }, - "required": [ - "version", - "updateDataModel" - ], + "required": ["updateDataModel", "version"], "additionalProperties": false }, - { + "DeleteSurfaceMessage": { "type": "object", "properties": { "version": { - "type": "string", "const": "v0.9" }, "deleteSurface": { @@ -319,20 +660,17 @@ When constructing UI, you must output a VALID A2UI JSON object representing one "description": "Signals the client to delete the surface identified by 'surfaceId'. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.", "properties": { "surfaceId": { - "type": "string" + "type": "string", + "description": "The unique identifier for the UI surface to be deleted." } }, - "required": [ - "surfaceId" - ] + "required": ["surfaceId"], + "additionalProperties": false } }, - "required": [ - "version", - "deleteSurface" - ], + "required": ["deleteSurface", "version"], "additionalProperties": false } - ] + } } ------A2UI_JSON_SCHEMA_END----- +-----MESSAGE_SCHEMA_END----- diff --git a/packages/genui/test/facade/prompt_builder_test.golden/create_only_with_dataModel_true.txt b/packages/genui/test/facade/prompt_builder_test.golden/create_only_with_dataModel_true.txt index 75f6319ff..548821a01 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,476 @@ 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.", + "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" + ] + } + }, + "required": [ + "component", + "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 +602,29 @@ When constructing UI, you must output a VALID A2UI JSON object representing one "properties": { "surfaceId": { "type": "string", - "description": "The unique identifier for the UI surface." + "description": "The unique identifier for the UI surface to be updated." }, + "components": { "type": "array", - "description": "A flat list of component definitions.", + "description": "A list containing all UI components for the surface.", + "minItems": 1, "items": { - "description": "Must match one of the component definitions in the catalog.", - "oneOf": [ - { - "type": "object", - "description": "A block of styled text.", - "properties": { - "text": { - "description": "While simple Markdown is supported (without HTML or image references), utilizing dedicated UI components is generally preferred for a richer and more structured presentation.", - "oneOf": [ - { - "type": "string", - "description": "A literal string value." - }, - { - "type": "object", - "description": "A path to a string.", - "properties": { - "path": { - "type": "string", - "description": "A relative or absolute path in the data model." - } - }, - "required": [ - "path" - ] - }, - { - "type": "object", - "properties": { - "call": { - "type": "string", - "description": "The name of the function to call." - }, - "args": { - "type": "object", - "description": "Arguments to pass to the function.", - "additionalProperties": true - } - }, - "required": [ - "call" - ] - } - ] - }, - "variant": { - "type": "string", - "description": "A hint for the base text style.", - "enum": [ - "h1", - "h2", - "h3", - "h4", - "h5", - "caption", - "body" - ] - }, - "component": { - "type": "string", - "enum": [ - "Text" - ] - } - }, - "required": [ - "component", - "text" - ] - } - ] - }, - "minItems": 1 + "$ref": "catalog.json#/$defs/anyComponent" + } } }, - "required": [ - "surfaceId", - "components" - ] + "required": ["surfaceId", "components"], + "additionalProperties": false } }, - "required": [ - "version", - "updateComponents" - ], + "required": ["updateComponents", "version"], "additionalProperties": false }, - { + "UpdateDataModelMessage": { "type": "object", "properties": { "version": { - "type": "string", "const": "v0.9" }, "updateDataModel": { @@ -288,32 +632,29 @@ When constructing UI, you must output a VALID A2UI JSON object representing one "description": "Updates the data model for an existing surface. This message can be sent multiple times to update the data model. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.", "properties": { "surfaceId": { - "type": "string" + "type": "string", + "description": "The unique identifier for the UI surface this data model update applies to." }, "path": { "type": "string", - "default": "/" + "description": "An optional path to a location within the data model (e.g., '/user/name'). If omitted, or set to '/', refers to the entire data model." }, "value": { - "description": "The new value to write to the data model. If null/omitted, the key is removed." + "description": "The data to be updated in the data model. If present, the value at 'path' is replaced (or created). If omitted, the key at 'path' is removed.", + "additionalProperties": true } }, - "required": [ - "surfaceId" - ] + "required": ["surfaceId"], + "additionalProperties": false } }, - "required": [ - "version", - "updateDataModel" - ], + "required": ["updateDataModel", "version"], "additionalProperties": false }, - { + "DeleteSurfaceMessage": { "type": "object", "properties": { "version": { - "type": "string", "const": "v0.9" }, "deleteSurface": { @@ -321,20 +662,17 @@ When constructing UI, you must output a VALID A2UI JSON object representing one "description": "Signals the client to delete the surface identified by 'surfaceId'. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.", "properties": { "surfaceId": { - "type": "string" + "type": "string", + "description": "The unique identifier for the UI surface to be deleted." } }, - "required": [ - "surfaceId" - ] + "required": ["surfaceId"], + "additionalProperties": false } }, - "required": [ - "version", - "deleteSurface" - ], + "required": ["deleteSurface", "version"], "additionalProperties": false } - ] + } } ------A2UI_JSON_SCHEMA_END----- +-----MESSAGE_SCHEMA_END----- diff --git a/packages/genui/test/facade/prompt_builder_test.golden/update_only_with_dataModel_false.txt b/packages/genui/test/facade/prompt_builder_test.golden/update_only_with_dataModel_false.txt index ca6bf4884..1af4ef454 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,476 @@ 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.", + "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" + ] + } + }, + "required": [ + "component", + "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 +593,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 +623,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 +653,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..fc266f85d 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,476 @@ 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.", + "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" + ] + } + }, + "required": [ + "component", + "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 +595,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 +625,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 +655,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/functions/format_string_test.dart b/packages/genui/test/functions/format_string_test.dart index 979af193d..144ffbf6c 100644 --- a/packages/genui/test/functions/format_string_test.dart +++ b/packages/genui/test/functions/format_string_test.dart @@ -8,6 +8,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:genui/src/catalog/basic_catalog.dart'; import 'package:genui/src/functions/format_string.dart'; import 'package:genui/src/model/data_model.dart'; + // import 'package:genui/src/primitives/simple_items.dart'; // Unused void main() { diff --git a/packages/genui/test/model/catalog_exception_test.dart b/packages/genui/test/model/catalog_exception_test.dart index 61a17fda1..98b982639 100644 --- a/packages/genui/test/model/catalog_exception_test.dart +++ b/packages/genui/test/model/catalog_exception_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:genui/genui.dart'; + // import 'package:genui/src/model/catalog.dart'; // Exceptions should be exported by genui.dart, but if not we might need this. // Assuming CatalogItemNotFoundException is exported or available. From 1affaab034599f9fe6b4f77356dce34e5993997b Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Thu, 14 May 2026 17:28:55 -0700 Subject: [PATCH 3/5] fix(genui): remove duplicate 'component' in required list in prompt - Use a `Set` to avoid duplicate entries when generating the `required` list for components in the catalog schema. - Update golden files to reflect the fix. - Add mock asset handler to `travel_app` tests to fix failing test. Follow-up for #873. --- packages/genui/lib/src/facade/prompt_builder.dart | 4 ++-- .../all_operations_with_dataModel_false.txt | 1 - .../all_operations_with_dataModel_true.txt | 1 - .../create_and_update_with_dataModel_false.txt | 1 - .../create_and_update_with_dataModel_true.txt | 1 - .../create_only_with_dataModel_false.txt | 1 - .../create_only_with_dataModel_true.txt | 1 - .../update_only_with_dataModel_false.txt | 1 - .../update_only_with_dataModel_true.txt | 1 - 9 files changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/genui/lib/src/facade/prompt_builder.dart b/packages/genui/lib/src/facade/prompt_builder.dart index bd31948bd..60b8366ba 100644 --- a/packages/genui/lib/src/facade/prompt_builder.dart +++ b/packages/genui/lib/src/facade/prompt_builder.dart @@ -410,10 +410,10 @@ final class _BasicPromptBuilder extends PromptBuilder { 'component': {'const': item.name}, ...item.dataSchema.value['properties'] as Map, }, - 'required': [ + 'required': { 'component', ...?item.dataSchema.value['required'] as List?, - ], + }.toList(), }, ], 'unevaluatedProperties': false, 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 9558cfe2e..c36874770 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 @@ -515,7 +515,6 @@ When constructing UI, you must output a VALID A2UI JSON object representing one } }, "required": [ - "component", "component", "text" ] 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 0b5627350..d04a6264a 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 @@ -517,7 +517,6 @@ When constructing UI, you must output a VALID A2UI JSON object representing one } }, "required": [ - "component", "component", "text" ] 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 04fef3b0f..66d8ed6eb 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 @@ -513,7 +513,6 @@ When constructing UI, you must output a VALID A2UI JSON object representing one } }, "required": [ - "component", "component", "text" ] 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 cf9095d47..1bb7eb225 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 @@ -515,7 +515,6 @@ When constructing UI, you must output a VALID A2UI JSON object representing one } }, "required": [ - "component", "component", "text" ] 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 1f148e85a..d3ec1ffcc 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 @@ -512,7 +512,6 @@ When constructing UI, you must output a VALID A2UI JSON object representing one } }, "required": [ - "component", "component", "text" ] 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 548821a01..9a9701f47 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 @@ -514,7 +514,6 @@ When constructing UI, you must output a VALID A2UI JSON object representing one } }, "required": [ - "component", "component", "text" ] 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 1af4ef454..5e63e3dd8 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 @@ -505,7 +505,6 @@ When constructing UI, you must output a VALID A2UI JSON object representing one } }, "required": [ - "component", "component", "text" ] 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 fc266f85d..b52b72e60 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 @@ -507,7 +507,6 @@ When constructing UI, you must output a VALID A2UI JSON object representing one } }, "required": [ - "component", "component", "text" ] From cb629b6cae8ef93ff5cf18566a93da18ce6de4c2 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Thu, 14 May 2026 17:59:44 -0700 Subject: [PATCH 4/5] feat(genui): use $refs in A2uiSchemas and add custom component test - Update `A2uiSchemas` to use `Schema.combined($ref: '...')` for `DynamicString`, `DynamicNumber`, `DynamicBoolean`, `Action`, and `DynamicStringList`. - Update golden files to reflect the use of refs in the generated prompt. - Add a new test group for custom components in `prompt_builder_test.dart`. Follow-up for #873. --- examples/composer/pubspec.yaml | 1 - .../genui/lib/src/model/a2ui_schemas.dart | 83 +++++-------------- .../test/facade/prompt_builder_test.dart | 31 +++++++ .../all_operations_with_dataModel_false.txt | 37 +-------- .../all_operations_with_dataModel_true.txt | 37 +-------- ...create_and_update_with_dataModel_false.txt | 37 +-------- .../create_and_update_with_dataModel_true.txt | 37 +-------- .../create_only_with_dataModel_false.txt | 37 +-------- .../create_only_with_dataModel_true.txt | 37 +-------- .../update_only_with_dataModel_false.txt | 37 +-------- .../update_only_with_dataModel_true.txt | 37 +-------- 11 files changed, 60 insertions(+), 351 deletions(-) diff --git a/examples/composer/pubspec.yaml b/examples/composer/pubspec.yaml index 5a7959fc7..41d353e97 100644 --- a/examples/composer/pubspec.yaml +++ b/examples/composer/pubspec.yaml @@ -33,4 +33,3 @@ flutter: uses-material-design: true assets: - samples/ - - ../travel_app/assets/travel_images/ 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/test/facade/prompt_builder_test.dart b/packages/genui/test/facade/prompt_builder_test.dart index 5cd269685..84f9b3f3d 100644 --- a/packages/genui/test/facade/prompt_builder_test.dart +++ b/packages/genui/test/facade/prompt_builder_test.dart @@ -6,8 +6,10 @@ 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'; @@ -188,4 +190,33 @@ void main() { ); }); }); + + 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 c36874770..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 @@ -463,42 +463,7 @@ When constructing UI, you must output a VALID A2UI JSON object representing one }, "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" - ] - } - ] + "$ref": "common_types.json#/$defs/DynamicString" }, "variant": { "type": "string", 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 d04a6264a..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 @@ -465,42 +465,7 @@ When constructing UI, you must output a VALID A2UI JSON object representing one }, "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" - ] - } - ] + "$ref": "common_types.json#/$defs/DynamicString" }, "variant": { "type": "string", 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 66d8ed6eb..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 @@ -461,42 +461,7 @@ When constructing UI, you must output a VALID A2UI JSON object representing one }, "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" - ] - } - ] + "$ref": "common_types.json#/$defs/DynamicString" }, "variant": { "type": "string", 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 1bb7eb225..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 @@ -463,42 +463,7 @@ When constructing UI, you must output a VALID A2UI JSON object representing one }, "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" - ] - } - ] + "$ref": "common_types.json#/$defs/DynamicString" }, "variant": { "type": "string", 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 d3ec1ffcc..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 @@ -460,42 +460,7 @@ When constructing UI, you must output a VALID A2UI JSON object representing one }, "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" - ] - } - ] + "$ref": "common_types.json#/$defs/DynamicString" }, "variant": { "type": "string", 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 9a9701f47..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 @@ -462,42 +462,7 @@ When constructing UI, you must output a VALID A2UI JSON object representing one }, "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" - ] - } - ] + "$ref": "common_types.json#/$defs/DynamicString" }, "variant": { "type": "string", 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 5e63e3dd8..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 @@ -453,42 +453,7 @@ When constructing UI, you must output a VALID A2UI JSON object representing one }, "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" - ] - } - ] + "$ref": "common_types.json#/$defs/DynamicString" }, "variant": { "type": "string", 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 b52b72e60..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 @@ -455,42 +455,7 @@ When constructing UI, you must output a VALID A2UI JSON object representing one }, "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" - ] - } - ] + "$ref": "common_types.json#/$defs/DynamicString" }, "variant": { "type": "string", From 9c4711020f5712829901d2f898fe8c62860f99d5 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Fri, 15 May 2026 14:38:55 -0700 Subject: [PATCH 5/5] Fix formatting --- dev_tools/catalog_gallery/test/src/sample_locator.dart | 3 ++- packages/genui/test/facade/prompt_builder_test.dart | 5 +---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/dev_tools/catalog_gallery/test/src/sample_locator.dart b/dev_tools/catalog_gallery/test/src/sample_locator.dart index cda2845ba..a1536c5d7 100644 --- a/dev_tools/catalog_gallery/test/src/sample_locator.dart +++ b/dev_tools/catalog_gallery/test/src/sample_locator.dart @@ -15,7 +15,8 @@ Directory? findSamplesDir() { for (final candidate in [ current.childDirectory('samples'), current.childDirectory('../samples'), - if (current.path.endsWith('/test')) current.parent.childDirectory('samples'), + if (current.path.endsWith('/test')) + current.parent.childDirectory('samples'), current.childDirectory('dev_tools/catalog_gallery/samples'), ]) { if (candidate.existsSync()) return candidate; diff --git a/packages/genui/test/facade/prompt_builder_test.dart b/packages/genui/test/facade/prompt_builder_test.dart index 84f9b3f3d..0d83c32f3 100644 --- a/packages/genui/test/facade/prompt_builder_test.dart +++ b/packages/genui/test/facade/prompt_builder_test.dart @@ -205,10 +205,7 @@ void main() { widgetBuilder: (ctx) => const SizedBox(), // Dummy builder ); - final customCatalog = Catalog( - [customItem], - catalogId: 'custom_catalog', - ); + final customCatalog = Catalog([customItem], catalogId: 'custom_catalog'); final String prompt = (await PromptBuilder.createChat( catalog: customCatalog,