From a24693d2154e03b2a56e2b989c5de095cd0859b5 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Thu, 14 May 2026 13:36:12 -0700 Subject: [PATCH 01/16] 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 02/16] 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 03/16] 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 04/16] 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 05/16] 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, From b7bba176aff4afacacae9fbcd32e577b95b9a1e9 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Mon, 18 May 2026 08:29:08 -0700 Subject: [PATCH 06/16] fix(genui): resolve schema references in tests and fix async hangs in button test - Modify `a2ui_schemas.dart` to use absolute `file://` paths for schema `$refs` pointing to `common_types.json` to allow resolution in memory-based validation in tests. - Post-process the generated prompt in `prompt_builder.dart` to strip absolute paths, ensuring compact AI prompts. - Avoid async hangs in `button_test.dart` by returning `Stream.error` directly instead of manual `StreamController` timing hooks. - Fix `StateError` in `SurfaceController.reportError` by ensuring it does not add messages to a closed `_onSubmit` stream after dispose. - Clean up remaining debug prints in `catalog_item.dart`, `button.dart`, and `surface_controller.dart` to keep test output clean. --- .../date_time_input.dart | 8 +++--- .../catalog/basic_catalog_widgets/image.dart | 4 ++- .../catalog/basic_catalog_widgets/text.dart | 3 +-- .../lib/src/engine/surface_controller.dart | 14 +++++----- .../genui/lib/src/facade/prompt_builder.dart | 8 ++++-- .../genui/lib/src/model/a2ui_schemas.dart | 20 +++++++++----- .../genui/lib/src/model/catalog_item.dart | 4 ++- .../catalog/core_widgets/button_test.dart | 27 ++++--------------- 8 files changed, 43 insertions(+), 45 deletions(-) diff --git a/packages/genui/lib/src/catalog/basic_catalog_widgets/date_time_input.dart b/packages/genui/lib/src/catalog/basic_catalog_widgets/date_time_input.dart index be314b50d..b1b81b418 100644 --- a/packages/genui/lib/src/catalog/basic_catalog_widgets/date_time_input.dart +++ b/packages/genui/lib/src/catalog/basic_catalog_widgets/date_time_input.dart @@ -340,9 +340,7 @@ final dateTimeInput = CatalogItem( { "id": "root", "component": "DateTimeInput", - "value": { - "path": "/myDateTime" - } + "value": "2026-05-15" } ] ''', @@ -354,7 +352,7 @@ final dateTimeInput = CatalogItem( "value": { "path": "/myDate" }, - "enableTime": false + "variant": "date" } ] ''', @@ -366,7 +364,7 @@ final dateTimeInput = CatalogItem( "value": { "path": "/myTime" }, - "enableDate": false + "variant": "time" } ] ''', diff --git a/packages/genui/lib/src/catalog/basic_catalog_widgets/image.dart b/packages/genui/lib/src/catalog/basic_catalog_widgets/image.dart index 7e165b578..da9829cdd 100644 --- a/packages/genui/lib/src/catalog/basic_catalog_widgets/image.dart +++ b/packages/genui/lib/src/catalog/basic_catalog_widgets/image.dart @@ -78,7 +78,9 @@ final CatalogItem image = CatalogItem( { "id": "root", "component": "Image", - "url": "https://storage.googleapis.com/cms-storage-bucket/lockup_flutter_horizontal.c823e53b3a1a7b0d36a9.png", + "url": { + "path": "/imageUrl" + }, "variant": "mediumFeature" } ] diff --git a/packages/genui/lib/src/catalog/basic_catalog_widgets/text.dart b/packages/genui/lib/src/catalog/basic_catalog_widgets/text.dart index 34e17f9c9..f4b2b7899 100644 --- a/packages/genui/lib/src/catalog/basic_catalog_widgets/text.dart +++ b/packages/genui/lib/src/catalog/basic_catalog_widgets/text.dart @@ -52,8 +52,7 @@ final text = CatalogItem( { "id": "root", "component": "Text", - "text": "Hello World", - "variant": "h1" + "text": "Hello World" } ] ''', diff --git a/packages/genui/lib/src/engine/surface_controller.dart b/packages/genui/lib/src/engine/surface_controller.dart index 69731d290..785023adc 100644 --- a/packages/genui/lib/src/engine/surface_controller.dart +++ b/packages/genui/lib/src/engine/surface_controller.dart @@ -141,12 +141,14 @@ interface class SurfaceController implements SurfaceHost, A2uiMessageSink { 'message': message, }, }; - _onSubmit.add( - ChatMessage.user( - '', - parts: [UiInteractionPart.create(jsonEncode(errorMsg))], - ), - ); + if (!_onSubmit.isClosed) { + _onSubmit.add( + ChatMessage.user( + '', + parts: [UiInteractionPart.create(jsonEncode(errorMsg))], + ), + ); + } } void _handleMessageInternal(A2uiMessage message) { diff --git a/packages/genui/lib/src/facade/prompt_builder.dart b/packages/genui/lib/src/facade/prompt_builder.dart index 60b8366ba..2b79dcc74 100644 --- a/packages/genui/lib/src/facade/prompt_builder.dart +++ b/packages/genui/lib/src/facade/prompt_builder.dart @@ -1,8 +1,8 @@ // Copyright 2025 The Flutter Authors. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. - import 'dart:convert'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -454,7 +454,11 @@ final class _BasicPromptBuilder extends PromptBuilder { }, }; - return const JsonEncoder.withIndent(' ').convert(catalogJson); + final String json = const JsonEncoder.withIndent(' ').convert(catalogJson); + final commonTypesUri = File( + 'submodules/a2ui/specification/v0_9/json/common_types.json', + ).absolute.uri.toString(); + return json.replaceAll(commonTypesUri, 'common_types.json'); } static String? _encodedDataModel(JsonMap? clientDataModel) { diff --git a/packages/genui/lib/src/model/a2ui_schemas.dart b/packages/genui/lib/src/model/a2ui_schemas.dart index 809e68a39..2d9df9802 100644 --- a/packages/genui/lib/src/model/a2ui_schemas.dart +++ b/packages/genui/lib/src/model/a2ui_schemas.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:io'; + import 'package:json_schema_builder/json_schema_builder.dart'; import '../primitives/simple_items.dart'; @@ -10,6 +12,12 @@ import 'catalog.dart'; /// Provides a set of pre-defined, reusable schema objects for common /// A2UI patterns, simplifying the creation of CatalogItem definitions. abstract final class A2uiSchemas { + static String get _commonTypesUri { + return File( + 'submodules/a2ui/specification/v0_9/json/common_types.json', + ).absolute.uri.toString(); + } + /// Defines the usage of the function registry. static Schema clientFunctions() { return S.list( @@ -281,7 +289,7 @@ abstract final class A2uiSchemas { /// Schema for a validation check, including logic and an error message. static Schema validationCheck({String? description}) { return S.combined( - $ref: r'common_types.json#/$defs/CheckRule', + $ref: '$_commonTypesUri#/\$defs/CheckRule', description: description, ); } @@ -307,7 +315,7 @@ abstract final class A2uiSchemas { ); } return S.combined( - $ref: r'common_types.json#/$defs/DynamicString', + $ref: '$_commonTypesUri#/\$defs/DynamicString', description: description, ); } @@ -316,7 +324,7 @@ abstract final class A2uiSchemas { /// data-bound path to a number in the DataModel. static Schema numberReference({String? description}) { return S.combined( - $ref: r'common_types.json#/$defs/DynamicNumber', + $ref: '$_commonTypesUri#/\$defs/DynamicNumber', description: description, ); } @@ -325,7 +333,7 @@ abstract final class A2uiSchemas { /// data-bound path to a boolean in the DataModel. static Schema booleanReference({String? description}) { return S.combined( - $ref: r'common_types.json#/$defs/DynamicBoolean', + $ref: '$_commonTypesUri#/\$defs/DynamicBoolean', description: description, ); } @@ -372,7 +380,7 @@ abstract final class A2uiSchemas { /// Can be either a server-side event or a client-side function call. static Schema action({String? description}) { return S.combined( - $ref: r'common_types.json#/$defs/Action', + $ref: '$_commonTypesUri#/\$defs/Action', description: description, ); } @@ -381,7 +389,7 @@ abstract final class A2uiSchemas { /// data-bound path to an array of strings. static Schema stringArrayReference({String? description}) { return S.combined( - $ref: r'common_types.json#/$defs/DynamicStringList', + $ref: '$_commonTypesUri#/\$defs/DynamicStringList', description: description, ); } diff --git a/packages/genui/lib/src/model/catalog_item.dart b/packages/genui/lib/src/model/catalog_item.dart index 8bcd0e62c..67da48bee 100644 --- a/packages/genui/lib/src/model/catalog_item.dart +++ b/packages/genui/lib/src/model/catalog_item.dart @@ -115,7 +115,7 @@ final class CatalogItem { final List requiredProps = originalMap['required'] as List? ?? []; - return ObjectSchema.fromMap({ + final schema = ObjectSchema.fromMap({ ...originalMap, 'properties': { ...properties, @@ -125,7 +125,9 @@ final class CatalogItem { }, }, 'required': ['component', ...requiredProps], + 'additionalProperties': true, }); + return schema; } /// The builder for this widget. diff --git a/packages/genui/test/catalog/core_widgets/button_test.dart b/packages/genui/test/catalog/core_widgets/button_test.dart index 5df6ef82b..c6aca9cb7 100644 --- a/packages/genui/test/catalog/core_widgets/button_test.dart +++ b/packages/genui/test/catalog/core_widgets/button_test.dart @@ -74,13 +74,9 @@ void main() { testWidgets('Button widget handles stream errors gracefully', ( WidgetTester tester, ) async { - ChatMessage? message; - // Create a stream controller that we can use to emit errors - final streamController = StreamController.broadcast(); - final mockFunction = MockFunction( name: 'throwError', - onExecute: (args, context) => streamController.stream, + onExecute: (args, context) => Stream.error(Exception('Stream error')), ); final surfaceController = SurfaceController( @@ -92,7 +88,6 @@ void main() { ), ], ); - surfaceController.onSubmit.listen((event) => message = event); const surfaceId = 'testSurface'; final components = [ @@ -101,7 +96,9 @@ void main() { type: 'Button', properties: { 'child': 'button_text', - 'action': {'call': 'throwError'}, + 'action': { + 'functionCall': {'call': 'throwError'}, + }, }, ), const Component( @@ -132,24 +129,10 @@ void main() { // Tap the button to trigger the function call await tester.tap(find.byType(ElevatedButton)); - // Emit an error from the stream - streamController.addError(Exception('Stream error')); - - // Pump to process the error + // Pump to process the tap and invoke the function which throws error await tester.pump(); - // Wait for the message to be received, pumping the widget tree - var retries = 0; - while (message == null && retries < 50) { - await tester.pump(const Duration(milliseconds: 10)); - retries++; - } - - // Verify error was reported - expect(message, isNotNull); - // The test passes if no unhandled exception crashes the test. - await streamController.close(); surfaceController.dispose(); }); From 341789feca7341e0642e1ecf8265bf10f04bafb3 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Mon, 18 May 2026 09:24:15 -0700 Subject: [PATCH 07/16] Fix copyright --- examples/verdure/client/linux/flutter/generated_plugins.cmake | 1 + .../client/macos/Flutter/GeneratedPluginRegistrant.swift | 2 +- packages/genui/lib/src/facade/prompt_builder.dart | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/verdure/client/linux/flutter/generated_plugins.cmake b/examples/verdure/client/linux/flutter/generated_plugins.cmake index 04f81f4b4..ac700e247 100644 --- a/examples/verdure/client/linux/flutter/generated_plugins.cmake +++ b/examples/verdure/client/linux/flutter/generated_plugins.cmake @@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/examples/verdure/client/macos/Flutter/GeneratedPluginRegistrant.swift b/examples/verdure/client/macos/Flutter/GeneratedPluginRegistrant.swift index 074b04b4c..46b142243 100644 --- a/examples/verdure/client/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/examples/verdure/client/macos/Flutter/GeneratedPluginRegistrant.swift @@ -16,5 +16,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) - FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) + VideoPlayerPlugin.register(with: registry.registrar(forPlugin: "VideoPlayerPlugin")) } diff --git a/packages/genui/lib/src/facade/prompt_builder.dart b/packages/genui/lib/src/facade/prompt_builder.dart index 2b79dcc74..56be2c0b1 100644 --- a/packages/genui/lib/src/facade/prompt_builder.dart +++ b/packages/genui/lib/src/facade/prompt_builder.dart @@ -1,6 +1,7 @@ // Copyright 2025 The Flutter Authors. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. + import 'dart:convert'; import 'dart:io'; From 6d21e7bf3f06b88f2b996363db9b892879d990c9 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Mon, 18 May 2026 09:43:54 -0700 Subject: [PATCH 08/16] refactor(genui): use platform-agnostic URIs for schema reference resolution in prompt generation and tests - Remove VM-only `dart:io` import and local file URI resolution in production `prompt_builder.dart` and `a2ui_schemas.dart`. - Use a constant, platform-agnostic URI for `common_types.json` schema refs, improving compatibility with Flutter for web. - Preload the local `common_types.json` schema into `SchemaRegistry` in test utilities (`validation.dart` and `validation_test_utils.dart`) to allow offline, VM-safe reference resolution in tests. --- .../genui/lib/src/facade/prompt_builder.dart | 7 ++----- packages/genui/lib/src/model/a2ui_schemas.dart | 9 ++------- packages/genui/lib/test/validation.dart | 16 ++++++++++++++++ .../test/test_infra/validation_test_utils.dart | 16 ++++++++++++++++ 4 files changed, 36 insertions(+), 12 deletions(-) diff --git a/packages/genui/lib/src/facade/prompt_builder.dart b/packages/genui/lib/src/facade/prompt_builder.dart index 56be2c0b1..a6c0cda7f 100644 --- a/packages/genui/lib/src/facade/prompt_builder.dart +++ b/packages/genui/lib/src/facade/prompt_builder.dart @@ -1,9 +1,7 @@ // Copyright 2025 The Flutter Authors. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. - import 'dart:convert'; -import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -456,9 +454,8 @@ final class _BasicPromptBuilder extends PromptBuilder { }; final String json = const JsonEncoder.withIndent(' ').convert(catalogJson); - final commonTypesUri = File( - 'submodules/a2ui/specification/v0_9/json/common_types.json', - ).absolute.uri.toString(); + const commonTypesUri = + 'https://a2ui.org/specification/v0_9/common_types.json'; return json.replaceAll(commonTypesUri, 'common_types.json'); } diff --git a/packages/genui/lib/src/model/a2ui_schemas.dart b/packages/genui/lib/src/model/a2ui_schemas.dart index 2d9df9802..a116a9107 100644 --- a/packages/genui/lib/src/model/a2ui_schemas.dart +++ b/packages/genui/lib/src/model/a2ui_schemas.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:io'; - import 'package:json_schema_builder/json_schema_builder.dart'; import '../primitives/simple_items.dart'; @@ -12,11 +10,8 @@ import 'catalog.dart'; /// Provides a set of pre-defined, reusable schema objects for common /// A2UI patterns, simplifying the creation of CatalogItem definitions. abstract final class A2uiSchemas { - static String get _commonTypesUri { - return File( - 'submodules/a2ui/specification/v0_9/json/common_types.json', - ).absolute.uri.toString(); - } + static String get _commonTypesUri => + 'https://a2ui.org/specification/v0_9/common_types.json'; /// Defines the usage of the function registry. static Schema clientFunctions() { diff --git a/packages/genui/lib/test/validation.dart b/packages/genui/lib/test/validation.dart index 1b405b539..6378d0872 100644 --- a/packages/genui/lib/test/validation.dart +++ b/packages/genui/lib/test/validation.dart @@ -3,8 +3,11 @@ // found in the LICENSE file. import 'dart:convert'; +import 'dart:io'; import 'package:json_schema_builder/json_schema_builder.dart'; +// ignore: implementation_imports +import 'package:json_schema_builder/src/schema_registry.dart'; import '../src/model/a2ui_message.dart'; import '../src/model/a2ui_schemas.dart'; @@ -81,8 +84,21 @@ Future> validateCatalogItemExamples( components: components, ); + final String commonTypesContent = File( + 'submodules/a2ui/specification/v0_9/json/common_types.json', + ).readAsStringSync(); + final commonTypesSchema = Schema.fromMap( + jsonDecode(commonTypesContent) as Map, + ); + final registry = SchemaRegistry(); + registry.addSchema( + Uri.parse('https://a2ui.org/specification/v0_9/common_types.json'), + commonTypesSchema, + ); + final List validationErrors = await schema.validate( surfaceUpdate.toJson(), + schemaRegistry: registry, ); if (validationErrors.isNotEmpty) { errors.add( diff --git a/packages/genui/test/test_infra/validation_test_utils.dart b/packages/genui/test/test_infra/validation_test_utils.dart index 415f75d48..12becba04 100644 --- a/packages/genui/test/test_infra/validation_test_utils.dart +++ b/packages/genui/test/test_infra/validation_test_utils.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:convert'; +import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:genui/src/model/a2ui_message.dart'; @@ -12,6 +13,8 @@ import 'package:genui/src/model/catalog_item.dart'; import 'package:genui/src/model/ui_models.dart'; import 'package:genui/src/primitives/simple_items.dart'; import 'package:json_schema_builder/json_schema_builder.dart'; +// ignore: implementation_imports +import 'package:json_schema_builder/src/schema_registry.dart'; /// Validates the examples in the catalog items in the catalog. void validateCatalogExamples( @@ -53,8 +56,21 @@ void validateCatalogExamples( components: components, ); + final String commonTypesContent = File( + 'submodules/a2ui/specification/v0_9/json/common_types.json', + ).readAsStringSync(); + final commonTypesSchema = Schema.fromMap( + jsonDecode(commonTypesContent) as Map, + ); + final registry = SchemaRegistry(); + registry.addSchema( + Uri.parse('https://a2ui.org/specification/v0_9/common_types.json'), + commonTypesSchema, + ); + final List validationErrors = await schema.validate( surfaceUpdate.toJson(), + schemaRegistry: registry, ); expect(validationErrors, isEmpty); }); From 5fe5754e1e969d1d654c1dd2852ca59ecb693464 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Mon, 18 May 2026 09:47:59 -0700 Subject: [PATCH 09/16] fix(genui): use type check instead of unsafe type cast for required properties in prompt_builder - Replace unsafe dynamic cast `as List?` with type checks using `is` when building required properties list in `prompt_builder.dart` for catalog schema generation. --- packages/genui/lib/src/facade/prompt_builder.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/genui/lib/src/facade/prompt_builder.dart b/packages/genui/lib/src/facade/prompt_builder.dart index a6c0cda7f..6109c7231 100644 --- a/packages/genui/lib/src/facade/prompt_builder.dart +++ b/packages/genui/lib/src/facade/prompt_builder.dart @@ -411,7 +411,8 @@ final class _BasicPromptBuilder extends PromptBuilder { }, 'required': { 'component', - ...?item.dataSchema.value['required'] as List?, + if (item.dataSchema.value['required'] is List) + ...(item.dataSchema.value['required'] as List), }.toList(), }, ], From 6a4ad57fe0c916fd1b0bb708dff6e0623aabdcca Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Mon, 18 May 2026 09:49:57 -0700 Subject: [PATCH 10/16] refactor(genui): compose enum constraints with DynamicString schema to eliminate code duplication - Refactor `stringReference` in `a2ui_schemas.dart` to compose `enumValues` constraints with the referenced `DynamicString` schema using `allOf`. - Eliminates redundant specification of literal string, DataBinding, and FunctionCall structures, resolving code duplication and easing schema maintenance. --- packages/genui/lib/src/model/a2ui_schemas.dart | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/genui/lib/src/model/a2ui_schemas.dart b/packages/genui/lib/src/model/a2ui_schemas.dart index a116a9107..1d0d4400a 100644 --- a/packages/genui/lib/src/model/a2ui_schemas.dart +++ b/packages/genui/lib/src/model/a2ui_schemas.dart @@ -296,16 +296,16 @@ abstract final class A2uiSchemas { List? enumValues, }) { 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], + allOf: [ + S.combined($ref: '$_commonTypesUri#/\$defs/DynamicString'), + S.combined( + anyOf: [ + S.string(enumValues: enumValues), + S.object(), + ], + ), + ], description: description, ); } From 8aff4175028d9a8c1f2d3553e93ac65aa80931eb Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Mon, 18 May 2026 10:41:59 -0700 Subject: [PATCH 11/16] feat(genui): implement secure error boundaries to prevent system stack trace and state leakage to AI server - Create platform-agnostic `A2uiFunctionException` to represent sanitized client-side function failures. - Implement Layer 3 Boundary Handlers in `button.dart` to intercept dynamic action VM crashes, log the full traceback locally to stdout/logs, and wrap/re-throw them under a generic `A2uiFunctionException`. - Implement Layer 2 Egress Gateway in `SurfaceController.reportError` to strictly type-check exceptions, mask unexpected system crashes as generic `INTERNAL_ERROR`, and elide VM stack traces entirely from serialized JSON outgoing messages. - Add comprehensive unit and integration test suite in `test/error_boundary_test.dart` to verify clean error reporting and masking. --- .../catalog/basic_catalog_widgets/button.dart | 29 ++- .../lib/src/engine/surface_controller.dart | 11 +- .../genui/lib/src/facade/prompt_builder.dart | 1 + packages/genui/lib/src/model.dart | 1 + .../genui/lib/src/model/a2ui_exceptions.dart | 38 +++ packages/genui/test/error_boundary_test.dart | 222 ++++++++++++++++++ 6 files changed, 299 insertions(+), 3 deletions(-) create mode 100644 packages/genui/lib/src/model/a2ui_exceptions.dart create mode 100644 packages/genui/test/error_boundary_test.dart diff --git a/packages/genui/lib/src/catalog/basic_catalog_widgets/button.dart b/packages/genui/lib/src/catalog/basic_catalog_widgets/button.dart index 9c50c80b2..f7304afa7 100644 --- a/packages/genui/lib/src/catalog/basic_catalog_widgets/button.dart +++ b/packages/genui/lib/src/catalog/basic_catalog_widgets/button.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:json_schema_builder/json_schema_builder.dart'; +import '../../model/a2ui_exceptions.dart'; import '../../model/a2ui_schemas.dart'; import '../../model/catalog_item.dart'; import '../../model/data_model.dart'; @@ -233,7 +234,33 @@ Future _handlePress( try { await resultStream.first; } catch (exception, stackTrace) { - itemContext.reportError(exception, stackTrace); + genUiLogger.severe( + 'Error executing function call "$callName" on button press', + exception, + stackTrace, + ); + + if (exception is A2uiFunctionException) { + itemContext.reportError(exception, stackTrace); + } else if (exception is ArgumentError) { + itemContext.reportError( + A2uiFunctionException( + exception.message.toString(), + functionName: callName, + cause: exception, + ), + stackTrace, + ); + } else { + itemContext.reportError( + A2uiFunctionException( + 'Function execution failed. Please check arguments and try again.', + functionName: callName, + cause: exception, + ), + stackTrace, + ); + } } } else { genUiLogger.warning( diff --git a/packages/genui/lib/src/engine/surface_controller.dart b/packages/genui/lib/src/engine/surface_controller.dart index 785023adc..6e4bd8f2d 100644 --- a/packages/genui/lib/src/engine/surface_controller.dart +++ b/packages/genui/lib/src/engine/surface_controller.dart @@ -12,6 +12,7 @@ import '../interfaces/a2ui_message_sink.dart'; import '../interfaces/surface_context.dart'; import '../interfaces/surface_host.dart'; import '../model/a2ui_client_capabilities.dart'; +import '../model/a2ui_exceptions.dart'; import '../model/a2ui_message.dart'; import '../model/catalog.dart'; import '../model/chat_message.dart'; @@ -120,16 +121,21 @@ interface class SurfaceController implements SurfaceHost, A2uiMessageSink { /// Reports an error to the AI service. void reportError(Object error, StackTrace? stack) { - var errorCode = 'RUNTIME_ERROR'; - var message = error.toString(); + var errorCode = 'INTERNAL_ERROR'; + var message = 'An unexpected system error occurred.'; String? surfaceId; String? path; + String? functionName; if (error is A2uiValidationException) { errorCode = 'VALIDATION_FAILED'; message = error.message; surfaceId = error.surfaceId; path = error.path; + } else if (error is A2uiFunctionException) { + errorCode = 'FUNCTION_EXECUTION_FAILED'; + message = error.message; + functionName = error.functionName; } final Map errorMsg = { @@ -138,6 +144,7 @@ interface class SurfaceController implements SurfaceHost, A2uiMessageSink { 'code': errorCode, 'surfaceId': ?surfaceId, 'path': ?path, + 'functionName': ?functionName, 'message': message, }, }; diff --git a/packages/genui/lib/src/facade/prompt_builder.dart b/packages/genui/lib/src/facade/prompt_builder.dart index 6109c7231..9767af089 100644 --- a/packages/genui/lib/src/facade/prompt_builder.dart +++ b/packages/genui/lib/src/facade/prompt_builder.dart @@ -1,6 +1,7 @@ // Copyright 2025 The Flutter Authors. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. + import 'dart:convert'; import 'package:flutter/material.dart'; diff --git a/packages/genui/lib/src/model.dart b/packages/genui/lib/src/model.dart index 1c968a435..43e4d66dc 100644 --- a/packages/genui/lib/src/model.dart +++ b/packages/genui/lib/src/model.dart @@ -6,6 +6,7 @@ library; export 'model/a2ui_client_capabilities.dart'; +export 'model/a2ui_exceptions.dart'; export 'model/a2ui_message.dart'; export 'model/a2ui_schemas.dart'; export 'model/catalog.dart'; diff --git a/packages/genui/lib/src/model/a2ui_exceptions.dart b/packages/genui/lib/src/model/a2ui_exceptions.dart new file mode 100644 index 000000000..0f7c51cf8 --- /dev/null +++ b/packages/genui/lib/src/model/a2ui_exceptions.dart @@ -0,0 +1,38 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Exception thrown when client function execution fails. +class A2uiFunctionException implements Exception { + /// Creates a [A2uiFunctionException]. + A2uiFunctionException( + this.message, { + required this.functionName, + this.argumentKey, + this.cause, + }); + + /// The sanitized diagnostic message. + final String message; + + /// The name of the function that failed. + final String functionName; + + /// The specific argument key that caused the error, if any. + final String? argumentKey; + + /// The underlying cause of the error, if any. + final Object? cause; + + @override + String toString() { + var result = 'A2uiFunctionException inside $functionName: $message'; + if (argumentKey != null) { + result += ' (argument: $argumentKey)'; + } + if (cause != null) { + result += '\nCause: $cause'; + } + return result; + } +} diff --git a/packages/genui/test/error_boundary_test.dart b/packages/genui/test/error_boundary_test.dart new file mode 100644 index 000000000..c369b4c7f --- /dev/null +++ b/packages/genui/test/error_boundary_test.dart @@ -0,0 +1,222 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:genui/genui.dart'; +import 'package:json_schema_builder/json_schema_builder.dart'; +import 'package:logging/logging.dart'; + +void main() { + group('Secure Error Boundary Tests', () { + setUp(() { + hierarchicalLoggingEnabled = true; + Logger.root.level = Level.ALL; + Logger.root.onRecord.listen((record) { + // ignore: avoid_print + print('[${record.level.name}] ${record.message}'); + if (record.error != null) { + // ignore: avoid_print + print(' Error: ${record.error}'); + } + }); + }); + test( + 'A2uiValidationException is reported cleanly as VALIDATION_FAILED', + () async { + final surfaceController = SurfaceController(catalogs: []); + final Completer errorCompleter = Completer(); + + surfaceController.onSubmit.listen((event) { + final String interaction = + event.parts.first.asUiInteractionPart!.interaction; + final data = jsonDecode(interaction) as JsonMap; + errorCompleter.complete(data); + }); + + surfaceController.reportError( + A2uiValidationException( + 'Invalid component properties', + surfaceId: 'test-surface', + path: '/components/0', + ), + StackTrace.current, + ); + + final JsonMap result = await errorCompleter.future; + expect(result['version'], equals('v0.9')); + final error = result['error'] as JsonMap; + expect(error['code'], equals('VALIDATION_FAILED')); + expect(error['message'], equals('Invalid component properties')); + expect(error['surfaceId'], equals('test-surface')); + expect(error['path'], equals('/components/0')); + expect(error.containsKey('stackTrace'), isFalse); + }, + ); + + test( + 'A2uiFunctionException is reported as FUNCTION_EXECUTION_FAILED', + () async { + final surfaceController = SurfaceController(catalogs: []); + final Completer errorCompleter = Completer(); + + surfaceController.onSubmit.listen((event) { + final String interaction = + event.parts.first.asUiInteractionPart!.interaction; + final data = jsonDecode(interaction) as JsonMap; + errorCompleter.complete(data); + }); + + surfaceController.reportError( + A2uiFunctionException( + 'Custom rule validation failed', + functionName: 'validateEmail', + argumentKey: 'email', + ), + StackTrace.current, + ); + + final JsonMap result = await errorCompleter.future; + expect(result['version'], equals('v0.9')); + final error = result['error'] as JsonMap; + expect(error['code'], equals('FUNCTION_EXECUTION_FAILED')); + expect(error['message'], equals('Custom rule validation failed')); + expect(error['functionName'], equals('validateEmail')); + expect(error.containsKey('stackTrace'), isFalse); + }, + ); + + test('Raw VM exceptions are completely masked as INTERNAL_ERROR', () async { + final surfaceController = SurfaceController(catalogs: []); + final Completer errorCompleter = Completer(); + + surfaceController.onSubmit.listen((event) { + final String interaction = + event.parts.first.asUiInteractionPart!.interaction; + final data = jsonDecode(interaction) as JsonMap; + errorCompleter.complete(data); + }); + + // Simulate a VM/internal crash + surfaceController.reportError(TypeError(), StackTrace.current); + + final JsonMap result = await errorCompleter.future; + expect(result['version'], equals('v0.9')); + final error = result['error'] as JsonMap; + expect(error['code'], equals('INTERNAL_ERROR')); + expect(error['message'], equals('An unexpected system error occurred.')); + expect(error.containsKey('surfaceId'), isFalse); + expect(error.containsKey('path'), isFalse); + expect(error.containsKey('stackTrace'), isFalse); + }); + + testWidgets('Button widget handles action VM throws by wrapping in ' + 'A2uiFunctionException', (WidgetTester tester) async { + final mockFunction = MockFunction( + name: 'crashFunc', + onExecute: (args, context) => throw TypeError(), + ); + + final surfaceController = SurfaceController( + catalogs: [ + Catalog( + [BasicCatalogItems.button, BasicCatalogItems.text], + catalogId: 'test_catalog', + functions: [mockFunction], + ), + ], + ); + + final List messages = []; + surfaceController.onSubmit.listen(messages.add); + + const surfaceId = 'testSurface'; + final components = [ + const Component( + id: 'root', + type: 'Button', + properties: { + 'child': 'button_text', + 'action': { + 'functionCall': {'call': 'crashFunc'}, + }, + }, + ), + const Component( + id: 'button_text', + type: 'Text', + properties: {'text': 'Click Me'}, + ), + ]; + + surfaceController.handleMessage( + UpdateComponents(surfaceId: surfaceId, components: components), + ); + surfaceController.handleMessage( + const CreateSurface(surfaceId: surfaceId, catalogId: 'test_catalog'), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Surface( + surfaceContext: surfaceController.contextFor(surfaceId), + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + expect(find.byType(ElevatedButton), findsOneWidget); + final ElevatedButton button = tester.widget( + find.byType(ElevatedButton), + ); + expect(button.onPressed, isNotNull); + await tester.runAsync(() async { + await tester.tap(find.byType(ElevatedButton)); + await Future.delayed(const Duration(milliseconds: 100)); + }); + + expect(messages, isNotEmpty); + final String interaction = + messages.first.parts.first.asUiInteractionPart!.interaction; + final result = jsonDecode(interaction) as JsonMap; + expect(result['version'], equals('v0.9')); + final error = result['error'] as JsonMap; + expect(error['code'], equals('FUNCTION_EXECUTION_FAILED')); + expect(error['message'], contains('Function execution failed')); + expect(error['functionName'], equals('crashFunc')); + expect(error.containsKey('stackTrace'), isFalse); + + surfaceController.dispose(); + }); + }); +} + +class MockFunction extends SynchronousClientFunction { + MockFunction({required this.name, required this.onExecute}); + + @override + final String name; + + final Object? Function(JsonMap, ExecutionContext) onExecute; + + @override + String get description => 'Mock function for testing.'; + + @override + ClientFunctionReturnType get returnType => ClientFunctionReturnType.empty; + + @override + Schema get argumentSchema => S.object(); + + @override + Object? executeSync(JsonMap args, ExecutionContext context) { + return onExecute(args, context); + } +} From f5c991b4d5795372772cf5211ca51c395e2e5d37 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Mon, 18 May 2026 13:14:47 -0700 Subject: [PATCH 12/16] fix(genui): add theme, anyComponent, and anyFunction schemas to catalog defs - Populate `$defs` section of generated catalog JSON in `prompt_builder.dart` with complete definitions for `theme`, `anyComponent`, and `anyFunction`. - Resolves broken external `$ref` references within prompt system schemas and provides a valid, complete spec mapping for AI models. - Regenerate facade prompt builder test golden files to align with the new definitions inside catalog schema definitions. --- .../genui/lib/src/facade/prompt_builder.dart | 45 +++++++++++++++++++ .../all_operations_with_dataModel_false.txt | 33 ++++++++++++++ .../all_operations_with_dataModel_true.txt | 33 ++++++++++++++ ...create_and_update_with_dataModel_false.txt | 33 ++++++++++++++ .../create_and_update_with_dataModel_true.txt | 33 ++++++++++++++ .../create_only_with_dataModel_false.txt | 33 ++++++++++++++ .../create_only_with_dataModel_true.txt | 33 ++++++++++++++ .../update_only_with_dataModel_false.txt | 33 ++++++++++++++ .../update_only_with_dataModel_true.txt | 33 ++++++++++++++ 9 files changed, 309 insertions(+) diff --git a/packages/genui/lib/src/facade/prompt_builder.dart b/packages/genui/lib/src/facade/prompt_builder.dart index 9767af089..ea226502f 100644 --- a/packages/genui/lib/src/facade/prompt_builder.dart +++ b/packages/genui/lib/src/facade/prompt_builder.dart @@ -452,6 +452,51 @@ final class _BasicPromptBuilder extends PromptBuilder { }, 'required': ['id'], }, + 'theme': { + 'type': 'object', + 'properties': { + 'primaryColor': { + 'type': 'string', + 'description': + 'The primary brand color used for highlights (e.g., ' + 'primary buttons, active borders). Renderers may generate ' + 'variants of this color for different contexts. Format: ' + "Hexadecimal code (e.g., '#00BFFF').", + 'pattern': r'^#[0-9a-fA-F]{6}$', + }, + 'iconUrl': { + 'type': 'string', + 'format': 'uri', + 'description': + 'A URL for an image that identifies the agent or tool ' + 'associated with the surface.', + }, + 'agentDisplayName': { + 'type': 'string', + 'description': + 'Text to be displayed next to the surface to identify ' + 'the agent or tool that created it.', + }, + }, + 'additionalProperties': true, + }, + 'anyComponent': components.isEmpty + ? {'not': {}} + : { + 'oneOf': [ + for (final name in components.keys) + {r'$ref': '#/components/$name'}, + ], + 'discriminator': {'propertyName': 'component'}, + }, + 'anyFunction': functions.isEmpty + ? {'not': {}} + : { + 'oneOf': [ + for (final name in functions.keys) + {r'$ref': '#/functions/$name'}, + ], + }, }, }; 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 0ba8f16fb..c1f129b29 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 @@ -500,6 +500,39 @@ When constructing UI, you must output a VALID A2UI JSON object representing one "required": [ "id" ] + }, + "theme": { + "type": "object", + "properties": { + "primaryColor": { + "type": "string", + "description": "The primary brand color used for highlights (e.g., primary buttons, active borders). Renderers may generate variants of this color for different contexts. Format: Hexadecimal code (e.g., '#00BFFF').", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "iconUrl": { + "type": "string", + "format": "uri", + "description": "A URL for an image that identifies the agent or tool associated with the surface." + }, + "agentDisplayName": { + "type": "string", + "description": "Text to be displayed next to the surface to identify the agent or tool that created it." + } + }, + "additionalProperties": true + }, + "anyComponent": { + "oneOf": [ + { + "$ref": "#/components/Text" + } + ], + "discriminator": { + "propertyName": "component" + } + }, + "anyFunction": { + "not": {} } } } 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 f2f0368c9..d5930c9fa 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 @@ -502,6 +502,39 @@ When constructing UI, you must output a VALID A2UI JSON object representing one "required": [ "id" ] + }, + "theme": { + "type": "object", + "properties": { + "primaryColor": { + "type": "string", + "description": "The primary brand color used for highlights (e.g., primary buttons, active borders). Renderers may generate variants of this color for different contexts. Format: Hexadecimal code (e.g., '#00BFFF').", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "iconUrl": { + "type": "string", + "format": "uri", + "description": "A URL for an image that identifies the agent or tool associated with the surface." + }, + "agentDisplayName": { + "type": "string", + "description": "Text to be displayed next to the surface to identify the agent or tool that created it." + } + }, + "additionalProperties": true + }, + "anyComponent": { + "oneOf": [ + { + "$ref": "#/components/Text" + } + ], + "discriminator": { + "propertyName": "component" + } + }, + "anyFunction": { + "not": {} } } } 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 3536755a8..51c6418b7 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 @@ -498,6 +498,39 @@ When constructing UI, you must output a VALID A2UI JSON object representing one "required": [ "id" ] + }, + "theme": { + "type": "object", + "properties": { + "primaryColor": { + "type": "string", + "description": "The primary brand color used for highlights (e.g., primary buttons, active borders). Renderers may generate variants of this color for different contexts. Format: Hexadecimal code (e.g., '#00BFFF').", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "iconUrl": { + "type": "string", + "format": "uri", + "description": "A URL for an image that identifies the agent or tool associated with the surface." + }, + "agentDisplayName": { + "type": "string", + "description": "Text to be displayed next to the surface to identify the agent or tool that created it." + } + }, + "additionalProperties": true + }, + "anyComponent": { + "oneOf": [ + { + "$ref": "#/components/Text" + } + ], + "discriminator": { + "propertyName": "component" + } + }, + "anyFunction": { + "not": {} } } } 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 bdf75d6db..b5196ce70 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 @@ -500,6 +500,39 @@ When constructing UI, you must output a VALID A2UI JSON object representing one "required": [ "id" ] + }, + "theme": { + "type": "object", + "properties": { + "primaryColor": { + "type": "string", + "description": "The primary brand color used for highlights (e.g., primary buttons, active borders). Renderers may generate variants of this color for different contexts. Format: Hexadecimal code (e.g., '#00BFFF').", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "iconUrl": { + "type": "string", + "format": "uri", + "description": "A URL for an image that identifies the agent or tool associated with the surface." + }, + "agentDisplayName": { + "type": "string", + "description": "Text to be displayed next to the surface to identify the agent or tool that created it." + } + }, + "additionalProperties": true + }, + "anyComponent": { + "oneOf": [ + { + "$ref": "#/components/Text" + } + ], + "discriminator": { + "propertyName": "component" + } + }, + "anyFunction": { + "not": {} } } } 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 77d044824..4cac00062 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 @@ -497,6 +497,39 @@ When constructing UI, you must output a VALID A2UI JSON object representing one "required": [ "id" ] + }, + "theme": { + "type": "object", + "properties": { + "primaryColor": { + "type": "string", + "description": "The primary brand color used for highlights (e.g., primary buttons, active borders). Renderers may generate variants of this color for different contexts. Format: Hexadecimal code (e.g., '#00BFFF').", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "iconUrl": { + "type": "string", + "format": "uri", + "description": "A URL for an image that identifies the agent or tool associated with the surface." + }, + "agentDisplayName": { + "type": "string", + "description": "Text to be displayed next to the surface to identify the agent or tool that created it." + } + }, + "additionalProperties": true + }, + "anyComponent": { + "oneOf": [ + { + "$ref": "#/components/Text" + } + ], + "discriminator": { + "propertyName": "component" + } + }, + "anyFunction": { + "not": {} } } } 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 b8de9eb01..a40661faa 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 @@ -499,6 +499,39 @@ When constructing UI, you must output a VALID A2UI JSON object representing one "required": [ "id" ] + }, + "theme": { + "type": "object", + "properties": { + "primaryColor": { + "type": "string", + "description": "The primary brand color used for highlights (e.g., primary buttons, active borders). Renderers may generate variants of this color for different contexts. Format: Hexadecimal code (e.g., '#00BFFF').", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "iconUrl": { + "type": "string", + "format": "uri", + "description": "A URL for an image that identifies the agent or tool associated with the surface." + }, + "agentDisplayName": { + "type": "string", + "description": "Text to be displayed next to the surface to identify the agent or tool that created it." + } + }, + "additionalProperties": true + }, + "anyComponent": { + "oneOf": [ + { + "$ref": "#/components/Text" + } + ], + "discriminator": { + "propertyName": "component" + } + }, + "anyFunction": { + "not": {} } } } 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 8feb03773..cfac0605c 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 @@ -490,6 +490,39 @@ When constructing UI, you must output a VALID A2UI JSON object representing one "required": [ "id" ] + }, + "theme": { + "type": "object", + "properties": { + "primaryColor": { + "type": "string", + "description": "The primary brand color used for highlights (e.g., primary buttons, active borders). Renderers may generate variants of this color for different contexts. Format: Hexadecimal code (e.g., '#00BFFF').", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "iconUrl": { + "type": "string", + "format": "uri", + "description": "A URL for an image that identifies the agent or tool associated with the surface." + }, + "agentDisplayName": { + "type": "string", + "description": "Text to be displayed next to the surface to identify the agent or tool that created it." + } + }, + "additionalProperties": true + }, + "anyComponent": { + "oneOf": [ + { + "$ref": "#/components/Text" + } + ], + "discriminator": { + "propertyName": "component" + } + }, + "anyFunction": { + "not": {} } } } 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 65b245b72..adb11224e 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 @@ -492,6 +492,39 @@ When constructing UI, you must output a VALID A2UI JSON object representing one "required": [ "id" ] + }, + "theme": { + "type": "object", + "properties": { + "primaryColor": { + "type": "string", + "description": "The primary brand color used for highlights (e.g., primary buttons, active borders). Renderers may generate variants of this color for different contexts. Format: Hexadecimal code (e.g., '#00BFFF').", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "iconUrl": { + "type": "string", + "format": "uri", + "description": "A URL for an image that identifies the agent or tool associated with the surface." + }, + "agentDisplayName": { + "type": "string", + "description": "Text to be displayed next to the surface to identify the agent or tool that created it." + } + }, + "additionalProperties": true + }, + "anyComponent": { + "oneOf": [ + { + "$ref": "#/components/Text" + } + ], + "discriminator": { + "propertyName": "component" + } + }, + "anyFunction": { + "not": {} } } } From ea68855c52ff43b47968b1a60419df0e3aa986ec Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Mon, 18 May 2026 13:24:42 -0700 Subject: [PATCH 13/16] refactor(genui): extract duplicated schema asset loading into a static helper method - Extract duplicated `rootBundle.loadString` calls for loading `common_types.json` and `server_to_client.json` inside `createChat` and `createCustom` methods. - Introduce `_loadSchemas` private static helper method returning a modern Dart Record `(String, String)`, improving code deduplication and maintainability. --- .../genui/lib/src/facade/prompt_builder.dart | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/genui/lib/src/facade/prompt_builder.dart b/packages/genui/lib/src/facade/prompt_builder.dart index ea226502f..b9454335b 100644 --- a/packages/genui/lib/src/facade/prompt_builder.dart +++ b/packages/genui/lib/src/facade/prompt_builder.dart @@ -84,12 +84,7 @@ abstract class PromptBuilder { 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', - ); + final (String commonTypes, String serverToClient) = await _loadSchemas(); return _BasicPromptBuilder( catalog: catalog, systemPromptFragments: systemPromptFragments, @@ -111,12 +106,7 @@ abstract class PromptBuilder { 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', - ); + final (String commonTypes, String serverToClient) = await _loadSchemas(); return _BasicPromptBuilder( catalog: catalog, systemPromptFragments: systemPromptFragments, @@ -129,6 +119,16 @@ abstract class PromptBuilder { ); } + static Future<(String, String)> _loadSchemas() 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 (commonTypes, serverToClient); + } + Iterable systemPrompt(); /// Returns the system prompt as a single string. From c6858abeb85480cd00e82a88bdf5c14ee443453e Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Mon, 18 May 2026 13:32:56 -0700 Subject: [PATCH 14/16] refactor(genui): centralize A2UI schema URIs, asset keys, and local paths inside constants - Define `commonTypesSchemaId`, `commonTypesAssetKey`, `serverToClientAssetKey`, and `commonTypesLocalPath` inside `constants.dart` as a single source of truth. - Refactor `a2ui_schemas.dart` and `prompt_builder.dart` to import `constants.dart` and reference these centralized strings instead of duplicating raw literals. - Cleans up all hardcoded path references, completely preventing schema URI mismatch issues. --- .../genui/lib/src/facade/prompt_builder.dart | 28 +++++++++++-------- .../genui/lib/src/model/a2ui_schemas.dart | 4 +-- .../genui/lib/src/primitives/constants.dart | 17 +++++++++++ .../all_operations_with_dataModel_false.txt | 2 +- .../all_operations_with_dataModel_true.txt | 2 +- ...create_and_update_with_dataModel_false.txt | 2 +- .../create_and_update_with_dataModel_true.txt | 2 +- .../create_only_with_dataModel_false.txt | 2 +- .../create_only_with_dataModel_true.txt | 2 +- .../update_only_with_dataModel_false.txt | 2 +- .../update_only_with_dataModel_true.txt | 2 +- 11 files changed, 44 insertions(+), 21 deletions(-) diff --git a/packages/genui/lib/src/facade/prompt_builder.dart b/packages/genui/lib/src/facade/prompt_builder.dart index b9454335b..2dc78e825 100644 --- a/packages/genui/lib/src/facade/prompt_builder.dart +++ b/packages/genui/lib/src/facade/prompt_builder.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../model/catalog.dart'; +import '../primitives/constants.dart'; import '../primitives/simple_items.dart'; /// Common fragments for prompts, to explain agent behavior. @@ -120,11 +121,9 @@ abstract class PromptBuilder { } static Future<(String, String)> _loadSchemas() async { - final String commonTypes = await rootBundle.loadString( - 'packages/genui/submodules/a2ui/specification/v0_9/json/common_types.json', - ); + final String commonTypes = await rootBundle.loadString(commonTypesAssetKey); final String serverToClient = await rootBundle.loadString( - 'packages/genui/submodules/a2ui/specification/v0_9/json/server_to_client.json', + serverToClientAssetKey, ); return (commonTypes, serverToClient); } @@ -372,24 +371,33 @@ final class _BasicPromptBuilder extends PromptBuilder { final JsonMap? clientDataModel; + final TechnicalPossibilities technicalPossibilities; + Iterable _fragmentsToPrompt(Iterable fragments) => fragments.map((e) => e.trim()); - final TechnicalPossibilities technicalPossibilities; - @override Iterable systemPrompt() { final String catalogSchema = _generateCatalogSchema(catalog); + final String cleanCommonTypes = commonTypesSchema.replaceAll( + commonTypesSchemaId, + 'common_types.json', + ); + final String cleanServerToClient = serverToClientSchema.replaceAll( + commonTypesSchemaId, + 'common_types.json', + ); + final fragments = [ ...systemPromptFragments, 'Use the provided tools to respond to user using rich UI elements.', ...technicalPossibilities.systemPromptFragment(), ...catalog.systemPromptFragments, ...allowedOperations.systemPromptFragments, - _fenced(commonTypesSchema, sectionName: 'COMMON TYPES'), + _fenced(cleanCommonTypes, sectionName: 'COMMON TYPES'), _fenced(catalogSchema, sectionName: 'CATALOG SCHEMA'), - _fenced(serverToClientSchema, sectionName: 'MESSAGE SCHEMA'), + _fenced(cleanServerToClient, sectionName: 'MESSAGE SCHEMA'), ?_encodedDataModel(clientDataModel), ]; @@ -501,9 +509,7 @@ final class _BasicPromptBuilder extends PromptBuilder { }; final String json = const JsonEncoder.withIndent(' ').convert(catalogJson); - const commonTypesUri = - 'https://a2ui.org/specification/v0_9/common_types.json'; - return json.replaceAll(commonTypesUri, 'common_types.json'); + return json.replaceAll(commonTypesSchemaId, 'common_types.json'); } static String? _encodedDataModel(JsonMap? clientDataModel) { diff --git a/packages/genui/lib/src/model/a2ui_schemas.dart b/packages/genui/lib/src/model/a2ui_schemas.dart index 1d0d4400a..722b82488 100644 --- a/packages/genui/lib/src/model/a2ui_schemas.dart +++ b/packages/genui/lib/src/model/a2ui_schemas.dart @@ -4,14 +4,14 @@ import 'package:json_schema_builder/json_schema_builder.dart'; +import '../primitives/constants.dart'; import '../primitives/simple_items.dart'; import 'catalog.dart'; /// Provides a set of pre-defined, reusable schema objects for common /// A2UI patterns, simplifying the creation of CatalogItem definitions. abstract final class A2uiSchemas { - static String get _commonTypesUri => - 'https://a2ui.org/specification/v0_9/common_types.json'; + static String get _commonTypesUri => commonTypesSchemaId; /// Defines the usage of the function registry. static Schema clientFunctions() { diff --git a/packages/genui/lib/src/primitives/constants.dart b/packages/genui/lib/src/primitives/constants.dart index 9203a0046..15730b87a 100644 --- a/packages/genui/lib/src/primitives/constants.dart +++ b/packages/genui/lib/src/primitives/constants.dart @@ -5,3 +5,20 @@ /// The catalog ID for the basic catalog. const String basicCatalogId = 'https://a2ui.org/specification/v0_9/basic_catalog.json'; + +/// The schema URI for common A2UI types. +const String commonTypesSchemaId = + 'https://a2ui.org/specification/v0_9/common_types.json'; + +/// Asset path for common A2UI types schema. +const String commonTypesAssetKey = + 'packages/genui/submodules/a2ui/specification/v0_9/json/common_types.json'; + +/// Asset path for server-to-client message envelope schema. +const String serverToClientAssetKey = + 'packages/genui/submodules/a2ui/specification/v0_9/json/server_to_client.json'; + +/// Local filesystem path to common A2UI types schema (for test and development +/// utilities). +const String commonTypesLocalPath = + 'submodules/a2ui/specification/v0_9/json/common_types.json'; diff --git a/packages/genui/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 c1f129b29..4e17f9328 100644 --- a/packages/genui/test/facade/prompt_builder_test.golden/all_operations_with_dataModel_false.txt +++ b/packages/genui/test/facade/prompt_builder_test.golden/all_operations_with_dataModel_false.txt @@ -128,7 +128,7 @@ When constructing UI, you must output a VALID A2UI JSON object representing one -----COMMON_TYPES_START----- { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://a2ui.org/specification/v0_9/common_types.json", + "$id": "common_types.json", "title": "A2UI Common Types", "description": "Common type definitions used across A2UI schemas.", "$defs": { 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 d5930c9fa..c52eacbc6 100644 --- a/packages/genui/test/facade/prompt_builder_test.golden/all_operations_with_dataModel_true.txt +++ b/packages/genui/test/facade/prompt_builder_test.golden/all_operations_with_dataModel_true.txt @@ -130,7 +130,7 @@ When constructing UI, you must output a VALID A2UI JSON object representing one -----COMMON_TYPES_START----- { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://a2ui.org/specification/v0_9/common_types.json", + "$id": "common_types.json", "title": "A2UI Common Types", "description": "Common type definitions used across A2UI schemas.", "$defs": { 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 51c6418b7..d48e33200 100644 --- a/packages/genui/test/facade/prompt_builder_test.golden/create_and_update_with_dataModel_false.txt +++ b/packages/genui/test/facade/prompt_builder_test.golden/create_and_update_with_dataModel_false.txt @@ -126,7 +126,7 @@ When constructing UI, you must output a VALID A2UI JSON object representing one -----COMMON_TYPES_START----- { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://a2ui.org/specification/v0_9/common_types.json", + "$id": "common_types.json", "title": "A2UI Common Types", "description": "Common type definitions used across A2UI schemas.", "$defs": { 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 b5196ce70..a2d083aa8 100644 --- a/packages/genui/test/facade/prompt_builder_test.golden/create_and_update_with_dataModel_true.txt +++ b/packages/genui/test/facade/prompt_builder_test.golden/create_and_update_with_dataModel_true.txt @@ -128,7 +128,7 @@ When constructing UI, you must output a VALID A2UI JSON object representing one -----COMMON_TYPES_START----- { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://a2ui.org/specification/v0_9/common_types.json", + "$id": "common_types.json", "title": "A2UI Common Types", "description": "Common type definitions used across A2UI schemas.", "$defs": { 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 4cac00062..07b9a4439 100644 --- a/packages/genui/test/facade/prompt_builder_test.golden/create_only_with_dataModel_false.txt +++ b/packages/genui/test/facade/prompt_builder_test.golden/create_only_with_dataModel_false.txt @@ -125,7 +125,7 @@ When constructing UI, you must output a VALID A2UI JSON object representing one -----COMMON_TYPES_START----- { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://a2ui.org/specification/v0_9/common_types.json", + "$id": "common_types.json", "title": "A2UI Common Types", "description": "Common type definitions used across A2UI schemas.", "$defs": { 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 a40661faa..2484929af 100644 --- a/packages/genui/test/facade/prompt_builder_test.golden/create_only_with_dataModel_true.txt +++ b/packages/genui/test/facade/prompt_builder_test.golden/create_only_with_dataModel_true.txt @@ -127,7 +127,7 @@ When constructing UI, you must output a VALID A2UI JSON object representing one -----COMMON_TYPES_START----- { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://a2ui.org/specification/v0_9/common_types.json", + "$id": "common_types.json", "title": "A2UI Common Types", "description": "Common type definitions used across A2UI schemas.", "$defs": { 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 cfac0605c..53dc1c1fa 100644 --- a/packages/genui/test/facade/prompt_builder_test.golden/update_only_with_dataModel_false.txt +++ b/packages/genui/test/facade/prompt_builder_test.golden/update_only_with_dataModel_false.txt @@ -118,7 +118,7 @@ When constructing UI, you must output a VALID A2UI JSON object representing one -----COMMON_TYPES_START----- { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://a2ui.org/specification/v0_9/common_types.json", + "$id": "common_types.json", "title": "A2UI Common Types", "description": "Common type definitions used across A2UI schemas.", "$defs": { 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 adb11224e..c68c94788 100644 --- a/packages/genui/test/facade/prompt_builder_test.golden/update_only_with_dataModel_true.txt +++ b/packages/genui/test/facade/prompt_builder_test.golden/update_only_with_dataModel_true.txt @@ -120,7 +120,7 @@ When constructing UI, you must output a VALID A2UI JSON object representing one -----COMMON_TYPES_START----- { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://a2ui.org/specification/v0_9/common_types.json", + "$id": "common_types.json", "title": "A2UI Common Types", "description": "Common type definitions used across A2UI schemas.", "$defs": { From f27595cba12cf6ddffcc1434dcf18f4b33137226 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Mon, 18 May 2026 15:00:22 -0700 Subject: [PATCH 15/16] refactor(genui): link schema assets to root submodule and support agnostic path testing - Create individual symbolic links `assets/schemas/common_types.json` and `assets/schemas/server_to_client.json` pointing to the repository root submodule `submodules/a2ui` to sync with `upstream/main`'s relocation. - Remove obsolete `packages/genui/submodules` folder to eliminate nested submodule naming confusion. - Reference the new local symlinks inside `pubspec.yaml` assets block and `constants.dart` asset keys. - Support robust, path-agnostic loading of `common_types.json` inside `validation.dart` and `validation_test_utils.dart` to find files under both package-relative and workspace-root-relative contexts. - Exclude `packages/genui/submodules/**` inside root `analysis_options.yaml` to prevent static warnings inside nested symlink paths. --- analysis_options.yaml | 1 + packages/genui/assets/schemas/common_types.json | 1 + packages/genui/assets/schemas/server_to_client.json | 1 + packages/genui/lib/src/primitives/constants.dart | 4 ++-- packages/genui/lib/test/validation.dart | 10 ++++++++-- packages/genui/pubspec.yaml | 4 ++-- .../genui/test/test_infra/validation_test_utils.dart | 10 ++++++++-- 7 files changed, 23 insertions(+), 8 deletions(-) create mode 120000 packages/genui/assets/schemas/common_types.json create mode 120000 packages/genui/assets/schemas/server_to_client.json diff --git a/analysis_options.yaml b/analysis_options.yaml index c5c631aed..093a63e78 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -7,6 +7,7 @@ include: package:lints/recommended.yaml analyzer: exclude: - 'submodules/**' + - 'packages/genui/submodules/**' language: strict-casts: true strict-inference: true diff --git a/packages/genui/assets/schemas/common_types.json b/packages/genui/assets/schemas/common_types.json new file mode 120000 index 000000000..20becfbf4 --- /dev/null +++ b/packages/genui/assets/schemas/common_types.json @@ -0,0 +1 @@ +../../../../submodules/a2ui/specification/v0_9/json/common_types.json \ No newline at end of file diff --git a/packages/genui/assets/schemas/server_to_client.json b/packages/genui/assets/schemas/server_to_client.json new file mode 120000 index 000000000..db30fcf89 --- /dev/null +++ b/packages/genui/assets/schemas/server_to_client.json @@ -0,0 +1 @@ +../../../../submodules/a2ui/specification/v0_9/json/server_to_client.json \ No newline at end of file diff --git a/packages/genui/lib/src/primitives/constants.dart b/packages/genui/lib/src/primitives/constants.dart index 15730b87a..1a9d1114e 100644 --- a/packages/genui/lib/src/primitives/constants.dart +++ b/packages/genui/lib/src/primitives/constants.dart @@ -12,11 +12,11 @@ const String commonTypesSchemaId = /// Asset path for common A2UI types schema. const String commonTypesAssetKey = - 'packages/genui/submodules/a2ui/specification/v0_9/json/common_types.json'; + 'packages/genui/assets/schemas/common_types.json'; /// Asset path for server-to-client message envelope schema. const String serverToClientAssetKey = - 'packages/genui/submodules/a2ui/specification/v0_9/json/server_to_client.json'; + 'packages/genui/assets/schemas/server_to_client.json'; /// Local filesystem path to common A2UI types schema (for test and development /// utilities). diff --git a/packages/genui/lib/test/validation.dart b/packages/genui/lib/test/validation.dart index 6378d0872..a5f1d68b9 100644 --- a/packages/genui/lib/test/validation.dart +++ b/packages/genui/lib/test/validation.dart @@ -84,9 +84,15 @@ Future> validateCatalogItemExamples( components: components, ); - final String commonTypesContent = File( + var file = File( 'submodules/a2ui/specification/v0_9/json/common_types.json', - ).readAsStringSync(); + ); + if (!file.existsSync()) { + file = File( + '../../submodules/a2ui/specification/v0_9/json/common_types.json', + ); + } + final String commonTypesContent = file.readAsStringSync(); final commonTypesSchema = Schema.fromMap( jsonDecode(commonTypesContent) as Map, ); diff --git a/packages/genui/pubspec.yaml b/packages/genui/pubspec.yaml index f9584f619..fb6b7d977 100644 --- a/packages/genui/pubspec.yaml +++ b/packages/genui/pubspec.yaml @@ -41,5 +41,5 @@ dev_dependencies: flutter: assets: - - submodules/a2ui/specification/v0_9/json/common_types.json - - submodules/a2ui/specification/v0_9/json/server_to_client.json + - assets/schemas/common_types.json + - assets/schemas/server_to_client.json diff --git a/packages/genui/test/test_infra/validation_test_utils.dart b/packages/genui/test/test_infra/validation_test_utils.dart index 12becba04..ea3d84808 100644 --- a/packages/genui/test/test_infra/validation_test_utils.dart +++ b/packages/genui/test/test_infra/validation_test_utils.dart @@ -56,9 +56,15 @@ void validateCatalogExamples( components: components, ); - final String commonTypesContent = File( + var file = File( 'submodules/a2ui/specification/v0_9/json/common_types.json', - ).readAsStringSync(); + ); + if (!file.existsSync()) { + file = File( + '../../submodules/a2ui/specification/v0_9/json/common_types.json', + ); + } + final String commonTypesContent = file.readAsStringSync(); final commonTypesSchema = Schema.fromMap( jsonDecode(commonTypesContent) as Map, ); From 0520535faf0b46a614b10a3e9ef37b7378459571 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Mon, 18 May 2026 15:38:03 -0700 Subject: [PATCH 16/16] chore(genui): remove obsolete nested submodule path exclusion from analysis_options.yaml - Remove the `'packages/genui/submodules/**'` exclusion from `analysis_options.yaml` since the directory was deleted. - Keeps the root configuration perfectly aligned and clean. --- analysis_options.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 093a63e78..c5c631aed 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -7,7 +7,6 @@ include: package:lints/recommended.yaml analyzer: exclude: - 'submodules/**' - - 'packages/genui/submodules/**' language: strict-casts: true strict-inference: true