main
dart 131 lines 3.84 KB
Raw
1 import 'dart:async';
2
3 import '../../../core/logger/app_logger.dart';
4 import '../../../core/models/enums.dart';
5 import '../../servers/data/models/server.dart';
6 import 'chat_service.dart';
7 import 'models/chat_parameters.dart';
8 import 'models/message.dart';
9
10 class TitleGenerationService {
11 static const _defaultTitle = 'New Chat';
12
13 Future<String?> generateTitleWithLLM({
14 required ChatService chatService,
15 required Server server,
16 required String modelId,
17 required List<Message> messages,
18 required ChatParameters params,
19 }) async {
20 if (messages.isEmpty) return null;
21
22 final promptMessage = Message(
23 id: 'title-generation-prompt',
24 conversationId: messages.first.conversationId,
25 role: MessageRole.user,
26 content:
27 'Based on the conversation above, generate a short, descriptive title for this chat. '
28 'Return ONLY the title text — no quotes, markdown, labels, or explanation. '
29 'Keep it under 8 words.',
30 createdAt: DateTime.now(),
31 );
32
33 final titleParams = params.copyWith(
34 temperature: 0.3,
35 maxTokens: 32,
36 systemPrompt:
37 'You generate concise chat titles. Output only the title string, nothing else.',
38 );
39
40 try {
41 final completer = Completer<String>();
42 var accumulatedContent = '';
43
44 final stream = chatService.sendMessage(
45 server: server,
46 modelId: modelId,
47 messages: [...messages, promptMessage],
48 params: titleParams,
49 );
50
51 StreamSubscription? subscription;
52 subscription = stream.listen(
53 (response) {
54 if (response.type == ChatResponseType.message &&
55 response.content != null) {
56 accumulatedContent += response.content!;
57 } else if (response.type == ChatResponseType.done) {
58 subscription?.cancel();
59 if (!completer.isCompleted) {
60 completer.complete(accumulatedContent);
61 }
62 } else if (response.type == ChatResponseType.error) {
63 subscription?.cancel();
64 if (!completer.isCompleted) {
65 completer.completeError(
66 response.content ?? 'Unknown streaming error',
67 );
68 }
69 }
70 },
71 onError: (err) {
72 subscription?.cancel();
73 if (!completer.isCompleted) {
74 completer.completeError(err);
75 }
76 },
77 onDone: () {
78 subscription?.cancel();
79 if (!completer.isCompleted) {
80 completer.complete(accumulatedContent);
81 }
82 },
83 cancelOnError: true,
84 );
85
86 final rawResponse = await completer.future.timeout(
87 const Duration(seconds: 15),
88 onTimeout: () {
89 subscription?.cancel();
90 return accumulatedContent;
91 },
92 );
93
94 return _parseTitle(rawResponse);
95 } catch (e) {
96 Log.warning('LLM title generation failed: $e');
97 return null;
98 }
99 }
100
101 String truncateFirstMessageTitle(String content, {int maxLength = 50}) {
102 final trimmed = content.trim();
103 if (trimmed.isEmpty) return _defaultTitle;
104 if (trimmed.length <= maxLength) return trimmed;
105 return '${trimmed.substring(0, maxLength)}...';
106 }
107
108 String _parseTitle(String text) {
109 var title = text.trim();
110 if (title.isEmpty) return '';
111
112 title = title.replaceFirst(
113 RegExp(r'^(title|chat title|subject)\s*:\s*', caseSensitive: false),
114 '',
115 );
116 title = title.replaceAll(RegExp(r'```.*?```', dotAll: true), '');
117 title = title.replaceAll('`', '');
118 title = title.split('\n').first.trim();
119
120 if ((title.startsWith('"') && title.endsWith('"')) ||
121 (title.startsWith("'") && title.endsWith("'"))) {
122 title = title.substring(1, title.length - 1).trim();
123 }
124
125 if (title.length > 80) {
126 title = '${title.substring(0, 80).trim()}...';
127 }
128
129 return title;
130 }
131 }