Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

README.md

genui_a2a

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.

Features

  • 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's Surface to render UIs based on the received A2UI messages.
  • Content Generator Integration: Works with genui's SurfaceController by piping messages from the connector to the controller.
  • Event Handling: Captures UI events from genui and sends them back to the A2A server as A2A messages.
  • Stateful Conversation: Maintains the conversation context (taskId, contextId) with the A2A server.

Getting Started

Prerequisites

  • A Flutter project.
  • An A2A server endpoint that implements the A2UI Streaming UI Protocol.

Installation

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

Basic Usage

  1. Initialize SurfaceController: Set up SurfaceController with your widget Catalog.
  2. Create A2uiTransportAdapter: dedicated adapter to handle message transport.
  3. Create A2uiAgentConnector: Instantiate A2uiAgentConnector, providing the A2A server Uri.
  4. Create Conversation: Pass the SurfaceController and A2uiTransportAdapter to the Conversation.
  5. Connect Streams: Pipe the output of A2uiAgentConnector into A2uiTransportAdapter.
  6. Render with Surface: Use Surface widgets in your UI to display the agent-generated content.
  7. Send Messages: Use Conversation.sendRequest to 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)),
            ),
          ],
        ),
      ),
    );
  }
}

Key Components

  • 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.

Example App

See the example/ directory for a more complete application demonstrating the usage of this package.

Further Information