Files
iiEsaywebUIapp/lib/shared/services/tasks/task_worker.dart
2025-09-02 11:12:48 +05:30

282 lines
10 KiB
Dart

import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/providers/app_providers.dart';
import '../../../core/services/attachment_upload_queue.dart';
import '../../../core/utils/debug_logger.dart';
import '../../../core/models/chat_message.dart';
import '../../../features/chat/providers/chat_providers.dart' as chat;
import 'package:uuid/uuid.dart';
import 'outbound_task.dart';
class TaskWorker {
final Ref _ref;
TaskWorker(this._ref);
Future<void> perform(OutboundTask task) async {
await task.map<Future<void>>(
sendTextMessage: _performSendText,
uploadMedia: _performUploadMedia,
executeToolCall: _performExecuteToolCall,
generateImage: _performGenerateImage,
);
}
Future<void> _performSendText(SendTextMessageTask task) async {
// Ensure uploads referenced in attachments are completed if they are local queued ids
// For now, assume attachments are already uploaded (fileIds or data URLs) as UI uploads eagerly.
// If needed, we could resolve queued uploads here by integrating with AttachmentUploadQueue.
final isReviewer = _ref.read(reviewerModeProvider);
if (!isReviewer) {
final api = _ref.read(apiServiceProvider);
if (api == null) {
throw Exception('API not available');
}
}
// Set active conversation if provided; otherwise keep current
try {
// If a specific conversation id is provided and differs from current, load it
final active = _ref.read(activeConversationProvider);
if (task.conversationId != null &&
task.conversationId!.isNotEmpty &&
(active == null || active.id != task.conversationId)) {
try {
final api = _ref.read(apiServiceProvider);
if (api != null) {
final conv = await api.getConversation(task.conversationId!);
_ref.read(activeConversationProvider.notifier).state = conv;
}
} catch (_) {
// If loading fails, proceed; send flow can create a new conversation
}
}
} catch (_) {}
// Delegate to existing unified send implementation
await chat.sendMessageFromService(
_ref,
task.text,
task.attachments.isEmpty ? null : task.attachments,
task.toolIds.isEmpty ? null : task.toolIds,
);
}
Future<void> _performUploadMedia(UploadMediaTask task) async {
final uploader = AttachmentUploadQueue();
// Ensure queue initialized with API upload callback
try {
final api = _ref.read(apiServiceProvider);
if (api != null) {
await uploader.initialize(
onUpload: (p, n) => api.uploadFile(p, n),
);
}
} catch (_) {}
// Enqueue and then wait until the item reaches a terminal state for basic parity
final id = await uploader.enqueue(
filePath: task.filePath,
fileName: task.fileName,
fileSize: task.fileSize ?? 0,
mimeType: task.mimeType,
checksum: task.checksum,
);
final completer = Completer<void>();
late final StreamSubscription<List<QueuedAttachment>> sub;
sub = uploader.queueStream.listen((items) {
QueuedAttachment? entry;
try {
entry = items.firstWhere((e) => e.id == id);
} catch (_) {
entry = null;
}
if (entry == null) return;
switch (entry.status) {
case QueuedAttachmentStatus.completed:
case QueuedAttachmentStatus.failed:
case QueuedAttachmentStatus.cancelled:
sub.cancel();
completer.complete();
break;
default:
break;
}
});
// Fire a process tick
unawaited(uploader.processQueue());
await completer.future.timeout(const Duration(minutes: 2), onTimeout: () {
try { sub.cancel(); } catch (_) {}
DebugLogger.warning('UploadMediaTask timed out: ${task.fileName}');
return;
});
}
Future<void> _performExecuteToolCall(ExecuteToolCallTask task) async {
// Placeholder: In this client, native tool execution is orchestrated server-side.
// We keep this task type for future local tools or MCP bridges.
debugPrint('ExecuteToolCallTask stub: ${task.toolName}');
}
Future<void> _performGenerateImage(GenerateImageTask task) async {
final api = _ref.read(apiServiceProvider);
final selectedModel = _ref.read(selectedModelProvider);
if (api == null) {
throw Exception('API not available');
}
// Add assistant placeholder to show progress
try {
final placeholder = ChatMessage(
id: const Uuid().v4(),
role: 'assistant',
content: '',
timestamp: DateTime.now(),
model: selectedModel?.id,
isStreaming: true,
);
_ref.read(chat.chatMessagesProvider.notifier).addMessage(placeholder);
} catch (_) {}
// Generate images
List<Map<String, dynamic>> _extractGeneratedFiles(dynamic resp) {
final results = <Map<String, dynamic>>[];
if (resp is List) {
for (final item in resp) {
if (item is String && item.isNotEmpty) {
results.add({'type': 'image', 'url': item});
} else if (item is Map) {
final url = item['url'];
final b64 = item['b64_json'] ?? item['b64'];
if (url is String && url.isNotEmpty) {
results.add({'type': 'image', 'url': url});
} else if (b64 is String && b64.isNotEmpty) {
results.add({'type': 'image', 'url': 'data:image/png;base64,$b64'});
}
}
}
return results;
}
if (resp is! Map) return results;
final data = resp['data'];
if (data is List) {
for (final item in data) {
if (item is Map) {
final url = item['url'];
final b64 = item['b64_json'] ?? item['b64'];
if (url is String && url.isNotEmpty) {
results.add({'type': 'image', 'url': url});
} else if (b64 is String && b64.isNotEmpty) {
results.add({'type': 'image', 'url': 'data:image/png;base64,$b64'});
}
} else if (item is String && item.isNotEmpty) {
results.add({'type': 'image', 'url': item});
}
}
}
final images = resp['images'];
if (images is List) {
for (final item in images) {
if (item is String && item.isNotEmpty) {
results.add({'type': 'image', 'url': item});
} else if (item is Map) {
final url = item['url'];
final b64 = item['b64_json'] ?? item['b64'];
if (url is String && url.isNotEmpty) {
results.add({'type': 'image', 'url': url});
} else if (b64 is String && b64.isNotEmpty) {
results.add({'type': 'image', 'url': 'data:image/png;base64,$b64'});
}
}
}
}
final singleUrl = resp['url'];
if (singleUrl is String && singleUrl.isNotEmpty) {
results.add({'type': 'image', 'url': singleUrl});
}
final singleB64 = resp['b64_json'] ?? resp['b64'];
if (singleB64 is String && singleB64.isNotEmpty) {
results.add({'type': 'image', 'url': 'data:image/png;base64,$singleB64'});
}
return results;
}
try {
final imageResponse = await api.generateImage(prompt: task.prompt);
final generatedFiles = _extractGeneratedFiles(imageResponse);
if (generatedFiles.isNotEmpty) {
_ref.read(chat.chatMessagesProvider.notifier).updateLastMessageWithFunction(
(m) => m.copyWith(files: generatedFiles, isStreaming: false),
);
// Sync conversation to server
try {
final messages = _ref.read(chat.chatMessagesProvider);
final activeConv = _ref.read(activeConversationProvider);
if (activeConv != null && messages.isNotEmpty) {
await api.updateConversationWithMessages(
activeConv.id,
messages,
model: selectedModel?.id,
);
// Update local active conversation messages
final updated = activeConv.copyWith(
messages: messages,
updatedAt: DateTime.now(),
);
_ref.read(activeConversationProvider.notifier).state = updated;
_ref.invalidate(conversationsProvider);
}
} catch (_) {}
// Trigger title generation (best-effort)
try {
final activeConv = _ref.read(activeConversationProvider);
final messages = _ref.read(chat.chatMessagesProvider);
final modelId = selectedModel?.id;
if (activeConv != null && modelId != null) {
final formatted = <Map<String, dynamic>>[];
for (final msg in messages) {
formatted.add({
'id': msg.id,
'role': msg.role,
'content': msg.content,
'timestamp': msg.timestamp.millisecondsSinceEpoch ~/ 1000,
});
}
final title = await api.generateTitle(
conversationId: activeConv.id,
messages: formatted,
model: modelId,
);
if (title != null && title.isNotEmpty && title != 'New Chat') {
final updated = activeConv.copyWith(
title: title.length > 100 ? '${title.substring(0, 100)}...' : title,
updatedAt: DateTime.now(),
);
_ref.read(activeConversationProvider.notifier).state = updated;
try {
final cur = _ref.read(chat.chatMessagesProvider);
await api.updateConversationWithMessages(
updated.id,
cur,
title: updated.title,
model: modelId,
);
} catch (_) {}
_ref.invalidate(conversationsProvider);
}
}
} catch (_) {}
} else {
_ref.read(chat.chatMessagesProvider.notifier).finishStreaming();
}
} catch (e) {
_ref.read(chat.chatMessagesProvider.notifier).finishStreaming();
}
}
}