Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dev_tools/composer/lib/create_tab.dart
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class _CreateTabState extends State<CreateTab> {
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. '
Expand Down
34 changes: 17 additions & 17 deletions examples/simple_chat/lib/chat_session.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<PromptBuilder> _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._();
Expand Down Expand Up @@ -188,7 +189,7 @@ class A2uiChatSession extends ChatSession {
late final StreamSubscription<ChatMessage> _submitSub;
late final StreamSubscription<SurfaceUpdate> _surfaceSub;

void _init() {
Future<void> _init() async {
_messageSub = _transport.incomingMessages.listen(
_surfaceController.handleMessage,
);
Expand All @@ -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) {
Expand Down
99 changes: 90 additions & 9 deletions packages/genui/lib/src/facade/prompt_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -78,38 +78,54 @@ 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<PromptBuilder> createChat({
required Catalog catalog,
Iterable<String> 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,
allowedOperations: SurfaceOperations.createOnly(dataModel: false),
importancePrefix: importancePrefix,
clientDataModel: clientDataModel,
technicalPossibilities: const TechnicalPossibilities(),
commonTypesSchema: commonTypes,
serverToClientSchema: serverToClient,
);
}

factory PromptBuilder.custom({
static Future<PromptBuilder> createCustom({
required Catalog catalog,
required SurfaceOperations allowedOperations,
Iterable<String> systemPromptFragments = const [],
String importancePrefix = defaultImportancePrefix,
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,
allowedOperations: allowedOperations,
importancePrefix: importancePrefix,
clientDataModel: clientDataModel,
technicalPossibilities: technicalPossibilities,
commonTypesSchema: commonTypes,
serverToClientSchema: serverToClient,
);
}

Expand Down Expand Up @@ -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;

Expand All @@ -359,23 +379,84 @@ final class _BasicPromptBuilder extends PromptBuilder {

@override
Iterable<String> systemPrompt() {
final String a2uiSchema = A2uiMessage.a2uiMessageSchema(
catalog,
).toJson(indent: ' ');
final String catalogSchema = _generateCatalogSchema(catalog);

final fragments = <String>[
...systemPromptFragments,
'Use the provided tools to respond to user using rich UI elements.',
...technicalPossibilities.systemPromptFragment(),
...catalog.systemPromptFragments,
...allowedOperations.systemPromptFragments,
_fenced(a2uiSchema, sectionName: 'A2UI JSON SCHEMA'),
_fenced(commonTypesSchema, sectionName: 'COMMON TYPES'),
_fenced(catalogSchema, sectionName: 'CATALOG SCHEMA'),
_fenced(serverToClientSchema, sectionName: 'MESSAGE SCHEMA'),
?_encodedDataModel(clientDataModel),
];

return _fragmentsToPrompt(fragments);
}

String _generateCatalogSchema(Catalog catalog) {
final Map<String, dynamic> 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<String, dynamic>,
},
'required': {
'component',
...?item.dataSchema.value['required'] as List?,
}.toList(),
},
],
'unevaluatedProperties': false,
},
};

final Map<String, dynamic> functions = {
for (final func in catalog.functions)
func.name: {
'description': func.description,
'parameters': func.argumentSchema.value,
'returnType': func.returnType.value,
},
};

final Map<String, dynamic> 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(
Expand Down
83 changes: 21 additions & 62 deletions packages/genui/lib/src/model/a2ui_schemas.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
);
}

Expand All @@ -300,44 +292,40 @@ abstract final class A2uiSchemas {
String? description,
List<String>? 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,
);
}

/// 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,
);
}

/// 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,
);
}
Expand Down Expand Up @@ -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,
);
}
Expand Down
5 changes: 5 additions & 0 deletions packages/genui/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading
Loading