kaily_flutter_sdk 1.0.1+2
kaily_flutter_sdk: ^1.0.1+2 copied to clipboard
A pure Flutter SDK for Kaily AI chatbot widget with dynamic tool registration and execution.
Kaily Flutter SDK #
A Flutter SDK for integrating Kaily AI chatbot with dynamic tool registration and execution capabilities. This SDK provides a WebView-based implementation that works across all Flutter platforms.
β¨ Features #
- π WebView-Based Integration: Pure Flutter implementation using WebView
- π οΈ Dynamic Tool Registration: Register Dart functions that the AI can call in real-time
- π€ User Management: Complete user context and authentication support
- π¨ Customizable Appearance: Full theming and styling options
- π‘ Real-time Events: Comprehensive event system for monitoring AI interactions
- π± Cross-Platform: Works on iOS, Android, Web, and Desktop
π Quick Start #
1. Installation #
Add this to your pubspec.yaml
:
dependencies:
kaily_flutter_sdk: ^1.0.0
Then run:
flutter pub get
2. Basic Setup #
import 'package:kaily_flutter_sdk/kaily_flutter_sdk.dart';
void main() {
runApp(const KailyExampleApp());
}
class KailyExampleApp extends StatelessWidget {
const KailyExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Kaily Flutter SDK Example',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
bool _isInitialized = false;
bool _isLoading = false;
String? _error;
final List<String> _logs = [];
final List<Map<String, dynamic>> _cartItems = [];
// Example user data
final _user = const KailyUser(
id: 'user_123',
name: 'John Doe',
email: 'john.doe@example.com',
attributes: {'tier': 'premium', 'location': 'San Francisco'},
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Kaily Flutter SDK Example'),
actions: [
IconButton(icon: const Icon(Icons.info), onPressed: _showInfo),
],
),
body: Column(
children: [
// Status Section
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
color: _getStatusColor(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Status: ${_getStatusText()}',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
if (_error != null) ...[
const SizedBox(height: 8),
Text(
'Error: $_error',
style: const TextStyle(color: Colors.white, fontSize: 12),
),
],
],
),
),
// Control Section
Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: _isLoading ? null : _initializeKaily,
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Initialize Kaily'),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: _isInitialized ? _showKailyWidget : null,
child: const Text('Open Chat'),
),
],
),
const SizedBox(height: 16),
// Cart Items Display
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Shopping Cart (${_cartItems.length} items)',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
if (_cartItems.isEmpty)
const Text('Cart is empty')
else
..._cartItems.map(
(item) => Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Text(
'β’ ${item['name']} (Qty: ${item['quantity']}) - \$${item['price']}',
style: const TextStyle(fontSize: 12),
),
),
),
],
),
),
],
),
),
// Logs Section
Expanded(
child: Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'Logs',
style: Theme.of(context).textTheme.titleMedium,
),
const Spacer(),
TextButton(
onPressed: () => setState(() => _logs.clear()),
child: const Text('Clear'),
),
],
),
const Divider(),
Expanded(
child: ListView.builder(
itemCount: _logs.length,
itemBuilder: (context, index) {
final log = _logs[index];
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Text(
log,
style: const TextStyle(
fontSize: 12,
fontFamily: 'monospace',
),
),
);
},
),
),
],
),
),
),
],
),
);
}
Color _getStatusColor() {
if (_error != null) return Colors.red;
if (_isInitialized) return Colors.green;
return Colors.orange;
}
String _getStatusText() {
if (_error != null) return 'Error';
if (_isInitialized) return 'Initialized';
if (_isLoading) return 'Initializing...';
return 'Not Initialized';
}
Future<void> _initializeKaily() async {
setState(() {
_isLoading = true;
_error = null;
});
_addLog('Initializing Kaily SDK...');
try {
// Initialize the SDK
await KailySDK.instance.initialize(
KailyConfig.withDefaults(
token: 'your-kaily-token',
user: _user,
).copyWith(debugMode: true),
);
_addLog('SDK initialized successfully');
// Register example tools
await _registerTools();
// Set initial context
await KailySDK.instance.setContext({
'current_page': 'home',
'cart_count': _cartItems.length,
'user_tier': 'premium',
});
_addLog('Context set successfully');
setState(() {
_isInitialized = true;
_isLoading = false;
});
_addLog('Kaily is ready to use!');
_setupEventListening();
} catch (e) {
setState(() {
_error = e.toString();
_isLoading = false;
});
_addLog('Initialization failed: $e');
}
}
Future<void> _registerTools() async {
// Register all tools at once using the new registerTools method
await KailySDK.instance.registerTools([
KailyTool(
name: 'add_to_cart',
description: 'Add a product to the shopping cart',
parameters: [
const KailyToolParameter(
name: 'product_id',
type: 'string',
description: 'The ID of the product to add',
required: true,
),
const KailyToolParameter(
name: 'product_name',
type: 'string',
description: 'The name of the product',
required: true,
),
const KailyToolParameter(
name: 'price',
type: 'number',
description: 'The price of the product',
required: true,
),
const KailyToolParameter(
name: 'quantity',
type: 'number',
description: 'The quantity to add (default: 1)',
defaultValue: 1,
),
],
handler: _handleAddToCart,
),
KailyTool(
name: 'get_cart_items',
description: 'Get all items currently in the shopping cart',
handler: _handleGetCartItems,
),
KailyTool(
name: 'remove_from_cart',
description: 'Remove a product from the shopping cart',
parameters: [
const KailyToolParameter(
name: 'product_id',
type: 'string',
description: 'The ID of the product to remove',
required: true,
),
],
handler: _handleRemoveFromCart,
),
KailyTool(
name: 'get_user_info',
description: 'Get information about the current user',
handler: _handleGetUserInfo,
),
]);
_addLog('Registered ${KailySDK.instance.getRegisteredTools().length} tools at once');
}
Future<KailyToolResult> _handleAddToCart(Map<String, dynamic> parameters) async {
try {
final productId = parameters['product_id'] as String;
final productName = parameters['product_name'] as String;
final price = (parameters['price'] as num).toDouble();
final quantity = (parameters['quantity'] as num?)?.toInt() ?? 1;
// Simulate adding to cart
final cartItem = {
'id': productId,
'name': productName,
'price': price,
'quantity': quantity,
'added_at': DateTime.now().toIso8601String(),
};
setState(() {
_cartItems.add(cartItem);
});
// Update context
await KailySDK.instance.setContext({
'current_page': 'home',
'cart_count': _cartItems.length,
'user_tier': 'premium',
});
_addLog('Added to cart: $productName (x$quantity)');
return KailyToolResult.success(
data: {
'success': true,
'product_id': productId,
'product_name': productName,
'quantity': quantity,
'cart_count': _cartItems.length,
'message': 'Successfully added $productName to your cart!',
},
);
} catch (e) {
_addLog('Add to cart failed: $e');
return KailyToolResult.failure(
error: 'Failed to add product to cart: $e',
);
}
}
Future<KailyToolResult> _handleGetCartItems(Map<String, dynamic> parameters) async {
try {
_addLog('Retrieved cart items (${_cartItems.length} items)');
return KailyToolResult.success(
data: {
'items': _cartItems,
'count': _cartItems.length,
'total_value': _cartItems.fold<double>(
0,
(sum, item) => sum + (item['price'] as double) * (item['quantity'] as int),
),
},
);
} catch (e) {
_addLog('Get cart items failed: $e');
return KailyToolResult.failure(error: 'Failed to get cart items: $e');
}
}
Future<KailyToolResult> _handleRemoveFromCart(Map<String, dynamic> parameters) async {
try {
final productId = parameters['product_id'] as String;
final index = _cartItems.indexWhere((item) => item['id'] == productId);
if (index == -1) {
return KailyToolResult.failure(error: 'Product not found in cart');
}
final removedItem = _cartItems.removeAt(index);
// Update context
await KailySDK.instance.setContext({
'current_page': 'home',
'cart_count': _cartItems.length,
'user_tier': 'premium',
});
setState(() {});
_addLog('Removed from cart: ${removedItem['name']}');
return KailyToolResult.success(
data: {
'success': true,
'removed_product': removedItem,
'cart_count': _cartItems.length,
'message': 'Successfully removed ${removedItem['name']} from your cart!',
},
);
} catch (e) {
_addLog('Remove from cart failed: $e');
return KailyToolResult.failure(
error: 'Failed to remove product from cart: $e',
);
}
}
Future<KailyToolResult> _handleGetUserInfo(Map<String, dynamic> parameters) async {
try {
_addLog('Retrieved user info for: ${_user.name}');
return KailyToolResult.success(
data: {
'user': _user.toJson(),
'preferences': {
'theme': 'light',
'notifications': true,
'language': 'en',
},
'stats': {
'total_orders': 42,
'favorite_category': 'electronics',
'member_since': '2023-01-15',
},
},
);
} catch (e) {
_addLog('Get user info failed: $e');
return KailyToolResult.failure(error: 'Failed to get user info: $e');
}
}
void _setupEventListening() {
_addLog('Setting up event listening...');
KailySDK.instance.eventStream.listen(
(event) {
_addLog('Event received: ${event.type.name}');
_addLog('Event data: ${event.data}');
_addLog('Event source: ${event.source}');
switch (event.type) {
case KailyEventType.conversationLoaded:
_addLog('Conversation loaded successfully');
break;
case KailyEventType.conversationFailedToLoad:
_addLog('Conversation failed to load: ${event.error}');
break;
case KailyEventType.toolCall:
final toolName = event.data?['tool_name'];
_addLog('Tool called: $toolName');
break;
case KailyEventType.toolResult:
final toolName = event.data?['tool_name'];
final success = event.data?['success'];
_addLog('Tool result: $toolName (${success ? 'success' : 'failed'})');
break;
case KailyEventType.error:
_addLog('Error: ${event.error}');
break;
default:
_addLog('Other event: ${event.type.name}');
}
},
onError: (error) {
_addLog('Event stream error: $error');
},
);
_addLog('Event listening setup complete');
}
void _showKailyWidget() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => Scaffold(
appBar: AppBar(title: const Text('Kaily Chat')),
body: KailyWidget(
config: KailyConfig.withDefaults(
token: 'your-kaily-token',
user: _user,
).copyWith(debugMode: true),
onConversationLoaded: () {
_addLog('Widget: Conversation loaded');
},
onConversationFailedToLoad: (error) {
_addLog('Widget: Conversation failed to load - $error');
},
onTelemetryEvent: (eventType, payload) {
_addLog('Widget: Telemetry - $eventType');
},
onEvent: (event) {
_addLog('Widget: Event - ${event.type.name}');
},
),
),
),
);
}
void _showInfo() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Kaily Flutter SDK Example'),
content: const Text(
'This example demonstrates:\n\n'
'β’ SDK initialization\n'
'β’ Tool registration (single or multiple tools at once)\n'
'β’ Event handling\n'
'β’ WebView-based chat widget\n'
'β’ Real-time JavaScript-Dart communication\n\n'
'New Feature: registerTools() method:\n'
'β’ Single tool: registerTools(KailyTool(...))\n'
'β’ Multiple tools: registerTools([KailyTool(...), ...])\n\n'
'Try asking the AI to:\n'
'β’ "Add iPhone to my cart for \$999"\n'
'β’ "Show me what\'s in my cart"\n'
'β’ "Remove the iPhone from my cart"\n'
'β’ "What\'s my user information?"',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('OK'),
),
],
),
);
}
void _addLog(String message) {
final timestamp = DateTime.now().toString().substring(11, 19);
setState(() {
_logs.add('[$timestamp] $message');
});
// Keep only last 100 logs
if (_logs.length > 100) {
_logs.removeAt(0);
}
}
}
π οΈ Advanced Usage #
Comprehensive Tool Registration #
The example above demonstrates a complete e-commerce shopping cart implementation with multiple tools. Here's how to create your own tools:
await KailySDK.instance.registerTools([
KailyTool(
name: 'add_to_cart',
description: 'Add a product to the shopping cart',
parameters: [
const KailyToolParameter(
name: 'product_id',
type: 'string',
description: 'The ID of the product to add',
required: true,
),
const KailyToolParameter(
name: 'product_name',
type: 'string',
description: 'The name of the product',
required: true,
),
const KailyToolParameter(
name: 'price',
type: 'number',
description: 'The price of the product',
required: true,
),
const KailyToolParameter(
name: 'quantity',
type: 'number',
description: 'The quantity to add (default: 1)',
defaultValue: 1,
),
],
handler: _handleAddToCart,
),
KailyTool(
name: 'get_cart_items',
description: 'Get all items currently in the shopping cart',
handler: _handleGetCartItems,
),
KailyTool(
name: 'remove_from_cart',
description: 'Remove a product from the shopping cart',
parameters: [
const KailyToolParameter(
name: 'product_id',
type: 'string',
description: 'The ID of the product to remove',
required: true,
),
],
handler: _handleRemoveFromCart,
),
KailyTool(
name: 'get_user_info',
description: 'Get information about the current user',
handler: _handleGetUserInfo,
),
]);
Tool Handler Implementation #
Each tool handler should return a KailyToolResult
:
Future<KailyToolResult> _handleAddToCart(Map<String, dynamic> parameters) async {
try {
final productId = parameters['product_id'] as String;
final productName = parameters['product_name'] as String;
final price = (parameters['price'] as num).toDouble();
final quantity = (parameters['quantity'] as num?)?.toInt() ?? 1;
// Your business logic here
// Update UI state, database, etc.
return KailyToolResult.success(
data: {
'success': true,
'product_id': productId,
'product_name': productName,
'quantity': quantity,
'message': 'Successfully added $productName to your cart!',
},
);
} catch (e) {
return KailyToolResult.failure(
error: 'Failed to add product to cart: $e',
);
}
}
Event Handling #
The example demonstrates comprehensive event handling with logging and UI updates:
void _setupEventListening() {
KailySDK.instance.eventStream.listen(
(event) {
_addLog('Event received: ${event.type.name}');
_addLog('Event data: ${event.data}');
_addLog('Event source: ${event.source}');
switch (event.type) {
case KailyEventType.conversationLoaded:
_addLog('Conversation loaded successfully');
break;
case KailyEventType.conversationFailedToLoad:
_addLog('Conversation failed to load: ${event.error}');
break;
case KailyEventType.toolCall:
final toolName = event.data?['tool_name'];
_addLog('Tool called: $toolName');
break;
case KailyEventType.toolResult:
final toolName = event.data?['tool_name'];
final success = event.data?['success'];
_addLog('Tool result: $toolName (${success ? 'success' : 'failed'})');
break;
case KailyEventType.error:
_addLog('Error: ${event.error}');
break;
default:
_addLog('Other event: ${event.type.name}');
}
},
onError: (error) {
_addLog('Event stream error: $error');
},
);
}
Widget Event Callbacks #
The KailyWidget
also supports direct event callbacks:
KailyWidget(
config: config,
onConversationLoaded: () {
print('Widget: Conversation loaded');
},
onConversationFailedToLoad: (error) {
print('Widget: Conversation failed to load - $error');
},
onTelemetryEvent: (eventType, payload) {
print('Widget: Telemetry - $eventType');
},
onEvent: (event) {
print('Widget: Event - ${event.type.name}');
},
)
Context Management #
The example shows dynamic context updates that keep the AI informed about your app's current state:
// Initial context setup
await KailySDK.instance.setContext({
'current_page': 'home',
'cart_count': _cartItems.length,
'user_tier': 'premium',
});
// Update context when cart changes
await KailySDK.instance.setContext({
'current_page': 'home',
'cart_count': _cartItems.length,
'user_tier': 'premium',
});
Context is automatically updated in the example when:
- Items are added to the cart
- Items are removed from the cart
- The user navigates to different pages
This ensures the AI always has current information about your app's state.
UI Features and Debugging #
The example includes several UI features that make development and debugging easier:
Status Indicators
- Color-coded status bar: Green (initialized), Orange (loading), Red (error)
- Real-time status updates: Shows current SDK state
- Error display: Clear error messages when initialization fails
Comprehensive Logging
- Timestamped logs: Each log entry includes a timestamp
- Real-time updates: Logs appear immediately as events occur
- Log management: Automatic cleanup (keeps last 100 logs)
- Clear functionality: Users can clear logs manually
Interactive Elements
- Loading states: Buttons show loading indicators during initialization
- Disabled states: Buttons are disabled when appropriate
- Info dialog: Helpful information about the example features
Live Data Display
- Shopping cart: Real-time display of cart items
- Dynamic updates: UI updates immediately when tools are called
- State management: Proper Flutter state management throughout
Custom Appearance #
Customize the chat widget appearance:
KailyWidget(
config: KailyConfig.withDefaults(
token: 'your-token',
user: user,
).copyWith(
appearance: KailyAppearance(
primaryColor: Colors.blue,
backgroundColor: Colors.white,
title: 'My Assistant',
showHeader: true,
),
),
)
π― Example App Features #
The comprehensive example app demonstrates all major SDK features:
What You Can Try #
Once the SDK is initialized, you can ask the AI to:
- "Add iPhone to my cart for $999" - Demonstrates tool calling with parameters
- "Show me what's in my cart" - Shows data retrieval tools
- "Remove the iPhone from my cart" - Demonstrates item removal
- "What's my user information?" - Shows user data access
- "Add 2 laptops to my cart for $1500 each" - Demonstrates quantity handling
Key Features Demonstrated #
- β SDK Initialization with error handling and loading states
- β Multiple Tool Registration with comprehensive parameters
- β Real-time Event Handling with detailed logging
- β Dynamic Context Updates that keep AI informed
- β UI State Management with live updates
- β Error Handling with user-friendly messages
- β Debug Mode with comprehensive logging
- β Shopping Cart Logic as a practical business use case
Example App Structure #
HomePage
βββ Status Bar (color-coded status)
βββ Control Buttons (Initialize/Open Chat)
βββ Shopping Cart Display (live data)
βββ Logs Section (real-time debugging)
π± Platform Setup #
Required Permissions #
The Kaily Flutter SDK requires specific permissions for WebView functionality and voice features. No manual plugin registration is needed - Flutter automatically handles this when you add the SDK dependency.
π§ Plugin Registration
Automatic Setup: The SDK automatically includes and registers the following plugins:
flutter_inappwebview
- For WebView functionalitypermission_handler
- For microphone permissions
Flutter will automatically generate the required plugin registration files when you run flutter pub get
.
π± Android Setup
Add the following permissions to android/app/src/main/AndroidManifest.xml
:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Required permissions for WebView functionality -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!-- Required for voice features (microphone access) -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<!-- WebView hardware acceleration -->
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<application
android:label="your_app_name"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:networkSecurityConfig="@xml/network_security_config"
android:enableOnBackInvokedCallback="true">
<!-- Your app configuration -->
</application>
</manifest>
Important Notes for Android:
- The
INTERNET
permission is required for WebView to load web content RECORD_AUDIO
andMODIFY_AUDIO_SETTINGS
are required for voice features- The SDK will automatically request microphone permission when needed
π iOS Setup
Add the following permissions to ios/Runner/Info.plist
:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Your existing app configuration -->
<!-- Required for WebView network access -->
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSAllowsArbitraryLoadsInWebContent</key>
<true/>
</dict>
<!-- Required for microphone access (voice features) -->
<key>NSMicrophoneUsageDescription</key>
<string>This app uses the microphone for voice interactions with Kaily AI assistant.</string>
<!-- Optional: For speech recognition -->
<key>NSSpeechRecognitionUsageDescription</key>
<string>This app uses speech recognition for voice commands with Kaily AI assistant.</string>
</dict>
</plist>
Important Notes for iOS:
NSAppTransportSecurity
allows WebView to load HTTP content (required for some web services)NSMicrophoneUsageDescription
is required for microphone access - customize the description for your appNSSpeechRecognitionUsageDescription
is optional but recommended for speech features- The SDK will automatically request microphone permission when needed
π Web Setup
For Flutter Web, no additional permissions are required. The browser will handle microphone permissions automatically when the user interacts with voice features.
π₯οΈ Desktop Setup
For Flutter Desktop (Windows, macOS, Linux), no additional permissions are required. The system will handle microphone permissions automatically.
π€ Voice Features Integration #
The SDK automatically handles microphone permission requests when users interact with voice features in the WebView. Here's how it works:
- Automatic Permission Request: When a user clicks the microphone button in the WebView, the SDK automatically requests microphone permission
- User-Friendly Dialogs: If permission is denied, the SDK shows helpful dialogs to guide users to app settings
- Web Widget Notification: The web widget is automatically notified of permission status changes
No additional code is required - the SDK handles all permission logic internally.
π Troubleshooting Permissions #
If voice features aren't working:
- Check Permissions: Ensure the required permissions are added to your platform files
- Test on Device: Test on a physical device (permissions don't work in simulators)
- Check App Settings: Verify permissions are granted in device settings
- Debug Mode: Enable debug mode in your KailyConfig to see permission request logs
KailyConfig.withDefaults(
token: 'your-token',
user: user,
debugMode: true, // Enable debug logging
)
π§ͺ Testing #
import 'package:flutter_test/flutter_test.dart';
import 'package:kaily_flutter_sdk/kaily_flutter_sdk.dart';
void main() {
test('should initialize with valid config', () async {
final config = KailyConfig.withDefaults(
token: 'test-token',
user: KailyUser(id: 'test', name: 'Test User'),
);
await KailySDK.instance.initialize(config);
expect(KailySDK.instance.isInitialized, isTrue);
});
}
π License #
This project is licensed under the MIT License - see the LICENSE file for details.
Made with β€οΈ by the Kaily team