chat_pilot_kit_dart 1.0.0-beta.6
chat_pilot_kit_dart: ^1.0.0-beta.6 copied to clipboard
Headless chat SDK for AI-powered conversation apps. Pure Dart, no Flutter dependency. Supports streaming, plugins, and multiple transport layers.
chat_pilot_kit_dart #
English | 简体中文
Headless chat SDK for AI-powered conversation apps. Pure Dart, no Flutter dependency.
Supports streaming, plugin extensions, conversation persistence, and multiple transport layers.
Flutter UI? See chat_pilot_kit_flutter for ready-made widgets.
Install #
dependencies:
chat_pilot_kit_dart: ^1.0.0-beta.1
Quick Start #
import 'package:chat_pilot_kit_dart/chat_pilot_kit_dart.dart';
// 1. Implement your agent service
class MyAgentService extends BaseAgentService {
@override
Future<void> query(
String text, {
List<AttachmentInput>? attachments,
QueryOptions? options,
}) async {
onData(AgentMessageData(
answer: 'Hello from AI!',
nodeType: 'markdown',
queryId: queryId,
sessionId: sessionId,
));
onCompleted(queryId: queryId, sessionId: sessionId);
}
@override
void abort([String? reason]) {}
}
// 2. Create controller
final controller = createChatPilotKit(
agentService: MyAgentService(),
);
// 3. Listen to events
controller.events.listen((event) {
switch (event) {
case ConversationAddEvent(:final role):
print('New conversation: $role');
case NodeAddEvent(:final nodeType):
print('Node added: $nodeType');
case ConversationChangeEvent(:final completed):
if (completed) print('Response complete');
case ErrorEvent(:final error):
print('Error: $error');
default:
break;
}
});
// 4. Send a query
await controller.query('What is Dart?');
// 5. Read conversations
for (final conv in controller.conversations) {
for (final node in conv.nodes) {
print('[${node.type}] ${node.content}');
}
}
// 6. Clean up
controller.dispose();
Architecture #
┌─────────────────────────────────────────────┐
│ ChatPilotKitController │
│ ┌──────────┐ ┌────────────────────────┐ │
│ │ Agent │ │ ConversationService │ │
│ │ Service │ │ ┌──────────────────┐ │ │
│ │ (yours) │──│ │ Extensions │ │ │
│ └──────────┘ │ │ text│md│image│… │ │ │
│ │ └──────────────────┘ │ │
│ Events ◄──── Stream<ChatEvent> │ │
└─────────────────────────────────────────────┘
- Controller — orchestrates queries, manages sessions, emits events
- AgentService — your backend adapter (SSE, WebSocket, REST, etc.)
- ConversationService — stores conversations and routes data through extensions
- Extensions — transform
AgentMessageDatainto typedConversationNodes
Agent Service #
Subclass BaseAgentService and implement query() and abort():
class SSEAgentService extends BaseAgentService {
@override
Future<void> query(
String text, {
List<AttachmentInput>? attachments,
QueryOptions? options,
}) async {
final stream = connectToSSE(text);
await for (final chunk in stream) {
onData(AgentMessageData(
answer: chunk.text,
nodeType: chunk.type, // 'markdown', 'thinking', 'tool_call', ...
queryId: queryId,
sessionId: sessionId,
// Streaming: omit nodeCompleted (defaults to false)
// Final chunk: set nodeCompleted: true
));
}
onCompleted(queryId: queryId, sessionId: sessionId);
}
@override
void abort([String? reason]) {
cancelSSEConnection();
}
}
Callbacks #
| Method | When to call |
|---|---|
onData(AgentMessageData) |
Each data chunk from the AI |
onCompleted(queryId, sessionId) |
Query finished successfully |
onError(error, queryId, sessionId) |
Query failed |
onTtft(timestamp, queryId) |
Time-to-first-token metric |
Built-in Extensions #
| Extension | Node Type | Content Type | Streamable |
|---|---|---|---|
TextExtension |
text |
String |
No |
MarkdownExtension |
markdown |
String |
Yes |
ImageExtension |
image |
ImageContent |
No |
AudioExtension |
audio |
AudioContent |
No |
VideoExtension |
video |
VideoContent |
No |
FileExtension |
file |
FileContent |
No |
ThinkingBlockExtension |
thinking |
ThinkingContent |
Yes |
ToolCallExtension |
tool_call |
ToolCallContent |
No |
Custom Extension #
class ChartExtension extends MessageExtensionConfig<GenericNode> {
@override
String get name => 'chart';
@override
bool canProcess(AgentMessageData data) => data.nodeType == 'chart';
@override
GenericNode process(AgentMessageData data, [Map<String, dynamic> options = const {}]) {
return GenericNode(
customType: 'chart',
content: Map<String, dynamic>.from(data.nodeData ?? {}),
);
}
@override
GenericNode? hydrate(ConversationNodeSnapshot snapshot) {
if (snapshot.type != 'chart') return null;
return GenericNode(
customType: 'chart',
content: Map<String, dynamic>.from(snapshot.content as Map? ?? {}),
id: snapshot.id,
createdAt: snapshot.createdAt,
completed: snapshot.completed,
);
}
}
// Register
final controller = createChatPilotKit(
agentService: myService,
extensions: [MessageExtensionInstance(ChartExtension())],
);
Node Behaviors #
| Behavior | Effect |
|---|---|
NodeBehavior.create |
Force create a new node |
NodeBehavior.append |
Append content to the last node of the same type |
NodeBehavior.replace |
Replace the last node's content entirely |
NodeBehavior.remove |
Remove the last node of the same type |
Target Node #
Direct updates to a specific node:
// By ID
targetNode: AgentTargetNode.byId('node-123')
// By matcher
targetNode: AgentTargetNode.byMatcher(
(node) => node.metadata['key'] == 'value',
)
Events #
controller.events.listen((event) {
switch (event) {
case ConversationAddEvent(:final conversationId, :final role):
print('New conversation: $role');
case NodeAddEvent(:final nodeId, :final nodeType):
print('Node added: $nodeType');
case NodeUpdateEvent(:final nodeId):
print('Node updated: $nodeId');
case NodeRemoveEvent(:final nodeId):
print('Node removed: $nodeId');
case NodeInteractionEvent(:final type, :final data, :final timestamp):
print('Interaction: $type at $timestamp');
case ConversationChangeEvent(:final completed):
if (completed) print('Response complete');
case ErrorEvent(:final error):
print('Error: $error');
case ClearEvent():
print('Cleared');
case HistoryImportEvent(:final count, :final position):
print('Imported $count conversations ($position)');
default:
break;
}
});
Attachments #
The url field accepts any URI scheme: file://, https://, or blob://.
// Local file
await controller.queryWithAttachments(
'Analyze this image',
[
AttachmentInput(
url: 'file:///path/to/photo.jpg',
fileName: 'photo.jpg',
fileSize: 1024000,
fileType: 'image/jpeg',
),
],
);
// Remote URL
await controller.queryWithAttachments(
'Analyze this image',
[
AttachmentInput(
url: 'https://example.com/photo.jpg',
fileName: 'photo.jpg',
fileSize: 1024000,
fileType: 'image/jpeg',
),
],
);
// Blob (e.g. picked from device)
await controller.queryWithAttachments(
'Analyze this image',
[
AttachmentInput(
url: 'blob:https://example.com/abc-123',
fileName: 'photo.jpg',
fileSize: 1024000,
fileType: 'image/jpeg',
),
],
);
Conversation Persistence #
// Export
final snapshots = controller.exportConversations();
final json = snapshots.map((s) => s.toJson()).toList();
// Import (prepend, default)
controller.importConversations(inputs);
// Import (replace all)
controller.importConversations(
inputs,
options: const ImportOptions(position: ImportPosition.replace),
);
Configuration #
final controller = createChatPilotKit(
agentService: myService,
options: const ChatPilotKitOptions(
sessionTimeout: Duration(minutes: 30),
enableDebugMode: false,
),
overrideExtensions: [MessageExtensionInstance(MyTextExtension())],
extensions: [MessageExtensionInstance(ChartExtension())],
);
Error Handling #
controller.events
.where((e) => e is ErrorEvent)
.cast<ErrorEvent>()
.listen((e) {
final err = e.error;
if (err is ChatPilotKitError) {
print('${err.category} [${err.severity}]: ${err.message}');
}
});
License #
MIT