An integration package for genui and the A2UI Streaming UI Protocol. This package allows Flutter applications to connect to an A2A (Agent-to-Agent) server and render dynamic user interfaces generated by an AI agent using the genui framework.
- A2A Server Connection: Establishes and manages a WebSocket connection to any server implementing the A2A protocol.
- A2UI Message Processing: Receives and parses A2UI messages (like
UpdateComponents,UpdateDataModel,CreateSurface) from the A2A stream. - Dynamic UI Rendering: Integrates seamlessly with
genui'sSurfaceto render UIs based on the received A2UI messages. - Content Generator Integration: Works with
genui'sSurfaceControllerby piping messages from the connector to the controller. - Event Handling: Captures UI events from
genuiand sends them back to the A2A server as A2A messages. - Stateful Conversation: Maintains the conversation context (
taskId,contextId) with the A2A server.
- A Flutter project.
- An A2A server endpoint that implements the A2UI Streaming UI Protocol.
Use flutter pub add to add the latest versions of genui and genui_a2a as
dependencies in your pubspec.yaml file:
flutter pub add genui genui_a2a- Initialize
SurfaceController: Set upSurfaceControllerwith your widgetCatalog. - Create
A2uiTransportAdapter: dedicated adapter to handle message transport. - Create
A2uiAgentConnector: InstantiateA2uiAgentConnector, providing the A2A serverUri. - Create
Conversation: Pass theSurfaceControllerandA2uiTransportAdapterto theConversation. - Connect Streams: Pipe the output of
A2uiAgentConnectorintoA2uiTransportAdapter. - Render with
Surface: UseSurfacewidgets in your UI to display the agent-generated content. - Send Messages: Use
Conversation.sendRequestto send user input to the agent.
import 'package:flutter/material.dart';
import 'package:genui/genui.dart';
import 'package:genui_a2a/genui_a2a.dart';
import 'package:logging/logging.dart';
void main() {
// Setup logging
Logger.root.level = Level.ALL;
Logger.root.onRecord.listen((record) {
print('${record.level.name}: ${record.time}: ${record.message}');
if (record.error != null) {
print(record.error);
}
if (record.stackTrace != null) {
print(record.stackTrace);
}
});
runApp(const GenUIExampleApp());
}
class GenUIExampleApp extends StatelessWidget {
const GenUIExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'A2UI Example',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const ChatScreen(),
);
}
}
class ChatScreen extends StatefulWidget {
const ChatScreen({super.key});
@override
State<ChatScreen> createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
final TextEditingController _textController = TextEditingController();
late final A2uiAgentConnector _connector;
late final SurfaceController _controller;
late final A2uiTransportAdapter _transport;
late final Conversation _conversation;
late final StreamSubscription _subscription;
late final StreamSubscription _textSubscription;
final List<ChatMessage> _messages = [];
@override
void initState() {
super.initState();
// Initialize the controller with the catalog
_controller = SurfaceController(catalogs: [CoreCatalogItems.asCatalog()]);
// Create the transport adapter
_transport = A2uiTransportAdapter(
onSend: _sendMessageToAgent,
);
// Create the connector
_connector = A2uiAgentConnector(
url: Uri.parse('http://localhost:8080'), // Replace with your A2A server URL
);
// Create the conversation facade
_conversation = Conversation(
controller: _controller,
transport: _transport,
);
// Listen for text responses from the conversation
_conversation.events.listen((event) {
if (event is ConversationContentReceived) {
setState(() {
if (_messages.isEmpty || _messages.first.role != Role.model) {
_messages.insert(0, ChatMessage.model(event.text));
} else {
// Append to existing message (simplification)
final lastMsg = _messages.first;
// Recreate message with appended text...
}
});
}
});
// Pipe connector output to transport
_subscription = _connector.stream.listen(_transport.addMessage);
_textSubscription = _connector.textStream.listen(_transport.addChunk);
}
Future<void> _sendMessageToAgent(ChatMessage message) async {
await _connector.connectAndSend(message);
}
@override
void dispose() {
_textController.dispose();
_conversation.dispose();
_transport.dispose();
_controller.dispose();
_connector.dispose();
_subscription.cancel();
_textSubscription.cancel();
super.dispose();
}
void _handleSubmitted(String text) {
if (text.isEmpty) return;
_textController.clear();
final message = ChatMessage.user(text);
setState(() {
_messages.insert(0, message);
});
_conversation.sendRequest(message);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('A2UI Example'),
),
body: Column(
children: <Widget>[
Expanded(
child: ListView.builder(
padding: const EdgeInsets.all(8.0),
reverse: true,
itemBuilder: (_, int index) =>
_buildMessage(_messages[index]),
itemCount: _messages.length,
),
),
const Divider(height: 1.0),
Container(
decoration: BoxDecoration(color: Theme.of(context).cardColor),
child: _buildTextComposer(),
),
// Surface for the main AI-generated UI
SizedBox(
height: 300,
child: Surface(
surfaceId: 'main_surface',
// Use controller as host
host: _controller,
)),
],
),
);
}
Widget _buildMessage(ChatMessage message) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 10.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
margin: const EdgeInsets.only(right: 16.0),
child: CircleAvatar(child: Text(message is UserMessage ? 'U' : 'A')),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(message is UserMessage ? 'User' : 'Agent',
style: const TextStyle(fontWeight: FontWeight.bold)),
Container(
margin: const EdgeInsets.only(top: 5.0),
child: Text(message.parts.whereType<TextPart>().map((e) => e.text).join('\n')),
),
],
),
),
],
),
);
}
Widget _buildTextComposer() {
return IconTheme(
data: IconThemeData(color: Theme.of(context).colorScheme.secondary),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
children: <Widget>[
Flexible(
child: TextField(
controller: _textController,
onSubmitted: _handleSubmitted,
decoration:
const InputDecoration.collapsed(hintText: 'Send a message'),
),
),
Container(
margin: const EdgeInsets.symmetric(horizontal: 4.0),
child: IconButton(
icon: const Icon(Icons.send),
onPressed: () => _handleSubmitted(_textController.text)),
),
],
),
),
);
}
}A2uiAgentConnector: Handles the low-level WebSocket communication with the A2A server, including sending messages and parsing stream events.AgentCard: A data class holding metadata about the connected AI agent.
See the example/ directory for a more complete application demonstrating the usage of this package.