@vezlo/assistant-server 2.2.1 → 2.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -5
- package/database-schema.sql +9 -0
- package/dist/knexfile.js +5 -6
- package/dist/knexfile.js.map +1 -1
- package/dist/src/bootstrap/initializeServices.d.ts.map +1 -1
- package/dist/src/bootstrap/initializeServices.js +12 -1
- package/dist/src/bootstrap/initializeServices.js.map +1 -1
- package/dist/src/config/global.d.ts.map +1 -1
- package/dist/src/config/global.js +10 -1
- package/dist/src/config/global.js.map +1 -1
- package/dist/src/controllers/AuthController.d.ts +4 -1
- package/dist/src/controllers/AuthController.d.ts.map +1 -1
- package/dist/src/controllers/AuthController.js +8 -3
- package/dist/src/controllers/AuthController.js.map +1 -1
- package/dist/src/controllers/ChatController.d.ts +8 -1
- package/dist/src/controllers/ChatController.d.ts.map +1 -1
- package/dist/src/controllers/ChatController.js +476 -15
- package/dist/src/controllers/ChatController.js.map +1 -1
- package/dist/src/migrations/005_add_conversation_handoff_columns.d.ts +4 -0
- package/dist/src/migrations/005_add_conversation_handoff_columns.d.ts.map +1 -0
- package/dist/src/migrations/005_add_conversation_handoff_columns.js +42 -0
- package/dist/src/migrations/005_add_conversation_handoff_columns.js.map +1 -0
- package/dist/src/schemas/ConversationSchemas.d.ts +125 -33
- package/dist/src/schemas/ConversationSchemas.d.ts.map +1 -1
- package/dist/src/schemas/ConversationSchemas.js +47 -18
- package/dist/src/schemas/ConversationSchemas.js.map +1 -1
- package/dist/src/schemas/MessageSchemas.d.ts +9 -0
- package/dist/src/schemas/MessageSchemas.d.ts.map +1 -1
- package/dist/src/schemas/MessageSchemas.js +3 -1
- package/dist/src/schemas/MessageSchemas.js.map +1 -1
- package/dist/src/schemas/index.d.ts +134 -33
- package/dist/src/schemas/index.d.ts.map +1 -1
- package/dist/src/server.js +222 -27
- package/dist/src/server.js.map +1 -1
- package/dist/src/services/ChatManager.d.ts.map +1 -1
- package/dist/src/services/ChatManager.js +26 -29
- package/dist/src/services/ChatManager.js.map +1 -1
- package/dist/src/services/IntentService.d.ts.map +1 -1
- package/dist/src/services/IntentService.js +6 -3
- package/dist/src/services/IntentService.js.map +1 -1
- package/dist/src/services/RealtimePublisher.d.ts +6 -0
- package/dist/src/services/RealtimePublisher.d.ts.map +1 -0
- package/dist/src/services/RealtimePublisher.js +43 -0
- package/dist/src/services/RealtimePublisher.js.map +1 -0
- package/dist/src/storage/ConversationRepository.d.ts +2 -2
- package/dist/src/storage/ConversationRepository.d.ts.map +1 -1
- package/dist/src/storage/ConversationRepository.js +36 -19
- package/dist/src/storage/ConversationRepository.js.map +1 -1
- package/dist/src/storage/MessageRepository.d.ts +4 -1
- package/dist/src/storage/MessageRepository.d.ts.map +1 -1
- package/dist/src/storage/MessageRepository.js +10 -4
- package/dist/src/storage/MessageRepository.js.map +1 -1
- package/dist/src/storage/SupabaseStorage.d.ts +5 -3
- package/dist/src/storage/SupabaseStorage.d.ts.map +1 -1
- package/dist/src/storage/SupabaseStorage.js +42 -11
- package/dist/src/storage/SupabaseStorage.js.map +1 -1
- package/dist/src/storage/UnifiedStorage.d.ts +5 -3
- package/dist/src/storage/UnifiedStorage.d.ts.map +1 -1
- package/dist/src/storage/UnifiedStorage.js +4 -4
- package/dist/src/storage/UnifiedStorage.js.map +1 -1
- package/dist/src/types/index.d.ts +24 -4
- package/dist/src/types/index.d.ts.map +1 -1
- package/knexfile.ts +4 -4
- package/package.json +2 -2
|
@@ -13,6 +13,7 @@ class ChatController {
|
|
|
13
13
|
const { historyLength } = options;
|
|
14
14
|
this.chatHistoryLength = typeof historyLength === 'number' && historyLength > 0 ? historyLength : 2;
|
|
15
15
|
this.intentService = options.intentService;
|
|
16
|
+
this.realtimePublisher = options.realtimePublisher;
|
|
16
17
|
}
|
|
17
18
|
// Create a new conversation
|
|
18
19
|
async createConversation(req, res) {
|
|
@@ -96,15 +97,34 @@ class ChatController {
|
|
|
96
97
|
}
|
|
97
98
|
// Generate a unique thread ID for the conversation
|
|
98
99
|
const threadId = `conv_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
100
|
+
const now = new Date();
|
|
99
101
|
const conversation = await this.storage.saveConversation({
|
|
100
102
|
threadId,
|
|
101
103
|
userId,
|
|
102
104
|
organizationId: companyId,
|
|
103
105
|
title: title || 'New Conversation',
|
|
104
106
|
messageCount: 0,
|
|
105
|
-
createdAt:
|
|
106
|
-
updatedAt:
|
|
107
|
+
createdAt: now,
|
|
108
|
+
updatedAt: now,
|
|
109
|
+
lastMessageAt: now
|
|
107
110
|
});
|
|
111
|
+
// Publish realtime update for new conversation
|
|
112
|
+
if (this.realtimePublisher && companyUuid) {
|
|
113
|
+
try {
|
|
114
|
+
await this.realtimePublisher.publish(`company:${companyUuid}:conversations`, 'conversation:created', {
|
|
115
|
+
conversation: {
|
|
116
|
+
uuid: conversation.id,
|
|
117
|
+
status: 'open',
|
|
118
|
+
message_count: conversation.messageCount,
|
|
119
|
+
last_message_at: conversation.lastMessageAt?.toISOString() || now.toISOString(),
|
|
120
|
+
created_at: conversation.createdAt.toISOString()
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
logger_1.default.error('[ChatController] Failed to publish conversation:created event:', error);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
108
128
|
res.json({
|
|
109
129
|
uuid: conversation.id,
|
|
110
130
|
title: conversation.title,
|
|
@@ -146,9 +166,44 @@ class ChatController {
|
|
|
146
166
|
createdAt: new Date()
|
|
147
167
|
});
|
|
148
168
|
// Update conversation message count
|
|
169
|
+
const timestamp = new Date();
|
|
170
|
+
const newMessageCount = conversation.messageCount + 1;
|
|
149
171
|
await this.storage.updateConversation(uuid, {
|
|
150
|
-
messageCount:
|
|
172
|
+
messageCount: newMessageCount,
|
|
173
|
+
lastMessageAt: timestamp
|
|
151
174
|
});
|
|
175
|
+
// Publish realtime update
|
|
176
|
+
if (this.realtimePublisher && conversation.organizationId) {
|
|
177
|
+
try {
|
|
178
|
+
logger_1.default.info(`[ChatController] Fetching company UUID for organizationId: ${conversation.organizationId}`);
|
|
179
|
+
const { data: company } = await this.supabase
|
|
180
|
+
.from('vezlo_companies')
|
|
181
|
+
.select('uuid')
|
|
182
|
+
.eq('id', conversation.organizationId)
|
|
183
|
+
.single();
|
|
184
|
+
if (company?.uuid) {
|
|
185
|
+
logger_1.default.info(`[ChatController] Publishing user message update for company: ${company.uuid}`);
|
|
186
|
+
await this.realtimePublisher.publish(`company:${company.uuid}:conversations`, 'message:created', {
|
|
187
|
+
conversation_uuid: uuid,
|
|
188
|
+
message: {
|
|
189
|
+
uuid: userMessage.id,
|
|
190
|
+
content: userMessage.content,
|
|
191
|
+
type: userMessage.role,
|
|
192
|
+
author_id: null,
|
|
193
|
+
created_at: userMessage.createdAt.toISOString()
|
|
194
|
+
},
|
|
195
|
+
conversation_update: {
|
|
196
|
+
message_count: newMessageCount,
|
|
197
|
+
last_message_at: timestamp.toISOString(),
|
|
198
|
+
status: conversation.joinedAt ? 'in_progress' : 'open'
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
logger_1.default.error('[ChatController] Failed to publish realtime update:', error);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
152
207
|
res.json({
|
|
153
208
|
uuid: userMessage.id,
|
|
154
209
|
conversation_uuid: uuid,
|
|
@@ -178,9 +233,19 @@ class ChatController {
|
|
|
178
233
|
const conversationId = userMessage.conversationId;
|
|
179
234
|
const userMessageContent = userMessage.content;
|
|
180
235
|
// Get conversation context (recent messages)
|
|
181
|
-
|
|
236
|
+
// Exclude the current user message to avoid duplication (it's added separately as the query)
|
|
237
|
+
const allMessages = await this.chatManager.getRecentMessages(conversationId, this.chatHistoryLength + 1);
|
|
238
|
+
const messages = allMessages.filter(msg => msg.id !== uuid).slice(-this.chatHistoryLength);
|
|
182
239
|
logger_1.default.info(`📜 Retrieved ${messages.length} message(s) from conversation history (limit: ${this.chatHistoryLength})`);
|
|
183
240
|
const conversation = await this.storage.getConversation(conversationId);
|
|
241
|
+
// Check if conversation has been joined by an agent
|
|
242
|
+
if (conversation?.joinedAt) {
|
|
243
|
+
res.status(400).json({
|
|
244
|
+
error: 'Conversation is being handled by an agent',
|
|
245
|
+
message: 'AI responses are disabled when an agent has joined the conversation'
|
|
246
|
+
});
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
184
249
|
// Run intent classification to decide handling strategy
|
|
185
250
|
const intentResult = await this.classifyIntent(userMessageContent, messages);
|
|
186
251
|
const handled = await this.handleIntentResult(intentResult, userMessage, conversationId, conversation, res);
|
|
@@ -261,37 +326,360 @@ class ChatController {
|
|
|
261
326
|
});
|
|
262
327
|
}
|
|
263
328
|
}
|
|
264
|
-
// Get conversation details
|
|
329
|
+
// Get conversation details
|
|
265
330
|
async getConversation(req, res) {
|
|
266
331
|
try {
|
|
332
|
+
if (!req.profile) {
|
|
333
|
+
res.status(401).json({ error: 'Authentication required' });
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
267
336
|
const { uuid } = req.params;
|
|
268
337
|
const conversation = await this.storage.getConversation(uuid);
|
|
269
338
|
if (!conversation) {
|
|
270
339
|
res.status(404).json({ error: 'Conversation not found' });
|
|
271
340
|
return;
|
|
272
341
|
}
|
|
273
|
-
|
|
342
|
+
if (conversation.organizationId && conversation.organizationId !== req.profile.companyId) {
|
|
343
|
+
res.status(404).json({ error: 'Conversation not found' });
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const toIso = (date) => (date ? date.toISOString() : null);
|
|
347
|
+
const status = conversation.closedAt
|
|
348
|
+
? 'closed'
|
|
349
|
+
: conversation.joinedAt
|
|
350
|
+
? 'in_progress'
|
|
351
|
+
: 'open';
|
|
274
352
|
res.json({
|
|
275
353
|
uuid: conversation.id,
|
|
276
354
|
title: conversation.title,
|
|
277
355
|
user_uuid: conversation.userId,
|
|
278
356
|
company_uuid: conversation.organizationId,
|
|
279
357
|
message_count: conversation.messageCount,
|
|
280
|
-
created_at: conversation.createdAt,
|
|
358
|
+
created_at: toIso(conversation.createdAt),
|
|
359
|
+
updated_at: toIso(conversation.updatedAt),
|
|
360
|
+
joined_at: toIso(conversation.joinedAt),
|
|
361
|
+
responded_at: toIso(conversation.respondedAt),
|
|
362
|
+
closed_at: toIso(conversation.closedAt),
|
|
363
|
+
last_message_at: toIso(conversation.lastMessageAt),
|
|
364
|
+
status
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
logger_1.default.error('Get conversation error:', error);
|
|
369
|
+
res.status(500).json({
|
|
370
|
+
error: 'Failed to get conversation',
|
|
371
|
+
message: error instanceof Error ? error.message : 'Unknown error'
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
// Get conversation messages
|
|
376
|
+
async getConversationMessages(req, res) {
|
|
377
|
+
try {
|
|
378
|
+
if (!req.profile) {
|
|
379
|
+
res.status(401).json({ error: 'Authentication required' });
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const { uuid } = req.params;
|
|
383
|
+
const page = Math.max(1, parseInt(req.query.page || '1', 10) || 1);
|
|
384
|
+
const pageSizeRaw = parseInt(req.query.page_size || '50', 10);
|
|
385
|
+
const pageSize = Math.min(200, Math.max(1, isNaN(pageSizeRaw) ? 50 : pageSizeRaw));
|
|
386
|
+
const offset = (page - 1) * pageSize;
|
|
387
|
+
const orderParam = (req.query.order || 'desc').toLowerCase();
|
|
388
|
+
const order = orderParam === 'asc' ? 'asc' : 'desc';
|
|
389
|
+
const conversation = await this.storage.getConversation(uuid);
|
|
390
|
+
if (!conversation) {
|
|
391
|
+
res.status(404).json({ error: 'Conversation not found' });
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (conversation.organizationId && conversation.organizationId !== req.profile.companyId) {
|
|
395
|
+
res.status(404).json({ error: 'Conversation not found' });
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
const messages = await this.storage.getMessages(uuid, pageSize, offset, { order });
|
|
399
|
+
const hasMore = messages.length === pageSize;
|
|
400
|
+
const toIso = (date) => (date ? date.toISOString() : null);
|
|
401
|
+
res.json({
|
|
402
|
+
conversation_uuid: conversation.id,
|
|
403
|
+
order,
|
|
281
404
|
messages: messages.map(msg => ({
|
|
282
405
|
uuid: msg.id,
|
|
283
406
|
parent_message_uuid: msg.parentMessageId,
|
|
284
407
|
type: msg.role,
|
|
285
408
|
content: msg.content,
|
|
286
409
|
status: 'completed',
|
|
287
|
-
created_at: msg.createdAt
|
|
288
|
-
|
|
410
|
+
created_at: toIso(msg.createdAt),
|
|
411
|
+
author_id: msg.authorId ?? null
|
|
412
|
+
})),
|
|
413
|
+
pagination: {
|
|
414
|
+
page,
|
|
415
|
+
page_size: pageSize,
|
|
416
|
+
has_more: hasMore,
|
|
417
|
+
next_offset: hasMore ? offset + pageSize : null
|
|
418
|
+
}
|
|
289
419
|
});
|
|
290
420
|
}
|
|
291
421
|
catch (error) {
|
|
292
|
-
logger_1.default.error('Get conversation error:', error);
|
|
422
|
+
logger_1.default.error('Get conversation messages error:', error);
|
|
293
423
|
res.status(500).json({
|
|
294
|
-
error: 'Failed to get conversation',
|
|
424
|
+
error: 'Failed to get conversation messages',
|
|
425
|
+
message: error instanceof Error ? error.message : 'Unknown error'
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
// Join conversation
|
|
430
|
+
async joinConversation(req, res) {
|
|
431
|
+
try {
|
|
432
|
+
if (!req.user || !req.profile) {
|
|
433
|
+
res.status(401).json({ error: 'Authentication required' });
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const { uuid } = req.params;
|
|
437
|
+
const conversation = await this.storage.getConversation(uuid);
|
|
438
|
+
if (!conversation) {
|
|
439
|
+
res.status(404).json({ error: 'Conversation not found' });
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (conversation.organizationId !== req.profile.companyId) {
|
|
443
|
+
res.status(404).json({ error: 'Conversation not found' });
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (conversation.closedAt) {
|
|
447
|
+
res.status(400).json({ error: 'Conversation is closed' });
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
const joinedAt = new Date();
|
|
451
|
+
await this.storage.updateConversation(uuid, {
|
|
452
|
+
joinedAt,
|
|
453
|
+
status: 'in_progress'
|
|
454
|
+
});
|
|
455
|
+
const systemMessage = await this.storage.saveMessage({
|
|
456
|
+
conversationId: uuid,
|
|
457
|
+
threadId: conversation.threadId,
|
|
458
|
+
role: 'system',
|
|
459
|
+
content: `${req.user.name} has joined the conversation.`,
|
|
460
|
+
createdAt: joinedAt,
|
|
461
|
+
authorId: parseInt(req.user.id)
|
|
462
|
+
});
|
|
463
|
+
const newMessageCount = conversation.messageCount + 1;
|
|
464
|
+
await this.storage.updateConversation(uuid, {
|
|
465
|
+
messageCount: newMessageCount,
|
|
466
|
+
lastMessageAt: joinedAt
|
|
467
|
+
});
|
|
468
|
+
if (this.realtimePublisher) {
|
|
469
|
+
try {
|
|
470
|
+
const { data: company } = await this.supabase
|
|
471
|
+
.from('vezlo_companies')
|
|
472
|
+
.select('uuid')
|
|
473
|
+
.eq('id', conversation.organizationId)
|
|
474
|
+
.single();
|
|
475
|
+
if (company?.uuid) {
|
|
476
|
+
await this.realtimePublisher.publish(`company:${company.uuid}:conversations`, 'message:created', {
|
|
477
|
+
conversation_uuid: uuid,
|
|
478
|
+
message: {
|
|
479
|
+
uuid: systemMessage.id,
|
|
480
|
+
content: systemMessage.content,
|
|
481
|
+
type: systemMessage.role,
|
|
482
|
+
author_id: systemMessage.authorId,
|
|
483
|
+
created_at: systemMessage.createdAt.toISOString()
|
|
484
|
+
},
|
|
485
|
+
conversation_update: {
|
|
486
|
+
message_count: newMessageCount,
|
|
487
|
+
last_message_at: joinedAt.toISOString(),
|
|
488
|
+
joined_at: joinedAt.toISOString(),
|
|
489
|
+
status: 'in_progress'
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
catch (error) {
|
|
495
|
+
logger_1.default.error('[ChatController] Failed to publish join conversation update:', error);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
res.json({
|
|
499
|
+
success: true,
|
|
500
|
+
message: {
|
|
501
|
+
uuid: systemMessage.id,
|
|
502
|
+
content: systemMessage.content,
|
|
503
|
+
type: systemMessage.role,
|
|
504
|
+
author_id: systemMessage.authorId,
|
|
505
|
+
created_at: systemMessage.createdAt.toISOString()
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
catch (error) {
|
|
510
|
+
logger_1.default.error('Join conversation error:', error);
|
|
511
|
+
res.status(500).json({
|
|
512
|
+
error: 'Failed to join conversation',
|
|
513
|
+
message: error instanceof Error ? error.message : 'Unknown error'
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
// Close conversation
|
|
518
|
+
async closeConversation(req, res) {
|
|
519
|
+
try {
|
|
520
|
+
if (!req.user || !req.profile) {
|
|
521
|
+
res.status(401).json({ error: 'Authentication required' });
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
const { uuid } = req.params;
|
|
525
|
+
const conversation = await this.storage.getConversation(uuid);
|
|
526
|
+
if (!conversation) {
|
|
527
|
+
res.status(404).json({ error: 'Conversation not found' });
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
if (conversation.organizationId !== req.profile.companyId) {
|
|
531
|
+
res.status(404).json({ error: 'Conversation not found' });
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
if (conversation.closedAt) {
|
|
535
|
+
res.status(400).json({ error: 'Conversation is already closed' });
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const closedAt = new Date();
|
|
539
|
+
const systemMessage = await this.storage.saveMessage({
|
|
540
|
+
conversationId: uuid,
|
|
541
|
+
threadId: conversation.threadId,
|
|
542
|
+
role: 'system',
|
|
543
|
+
content: `${req.user.name} has closed the conversation.`,
|
|
544
|
+
createdAt: closedAt,
|
|
545
|
+
authorId: parseInt(req.user.id, 10)
|
|
546
|
+
});
|
|
547
|
+
const newMessageCount = conversation.messageCount + 1;
|
|
548
|
+
await this.storage.updateConversation(uuid, {
|
|
549
|
+
messageCount: newMessageCount,
|
|
550
|
+
lastMessageAt: closedAt,
|
|
551
|
+
closedAt
|
|
552
|
+
});
|
|
553
|
+
if (this.realtimePublisher) {
|
|
554
|
+
try {
|
|
555
|
+
const { data: company } = await this.supabase
|
|
556
|
+
.from('vezlo_companies')
|
|
557
|
+
.select('uuid')
|
|
558
|
+
.eq('id', conversation.organizationId)
|
|
559
|
+
.single();
|
|
560
|
+
if (company?.uuid) {
|
|
561
|
+
await this.realtimePublisher.publish(`company:${company.uuid}:conversations`, 'message:created', {
|
|
562
|
+
conversation_uuid: uuid,
|
|
563
|
+
message: {
|
|
564
|
+
uuid: systemMessage.id,
|
|
565
|
+
content: systemMessage.content,
|
|
566
|
+
type: systemMessage.role,
|
|
567
|
+
author_id: systemMessage.authorId,
|
|
568
|
+
created_at: systemMessage.createdAt.toISOString()
|
|
569
|
+
},
|
|
570
|
+
conversation_update: {
|
|
571
|
+
message_count: newMessageCount,
|
|
572
|
+
last_message_at: closedAt.toISOString(),
|
|
573
|
+
joined_at: conversation.joinedAt?.toISOString() || null,
|
|
574
|
+
closed_at: closedAt.toISOString(),
|
|
575
|
+
status: 'closed'
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
catch (error) {
|
|
581
|
+
logger_1.default.error('[ChatController] Failed to publish close conversation update:', error);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
res.json({
|
|
585
|
+
success: true,
|
|
586
|
+
message: {
|
|
587
|
+
uuid: systemMessage.id,
|
|
588
|
+
content: systemMessage.content,
|
|
589
|
+
type: systemMessage.role,
|
|
590
|
+
author_id: systemMessage.authorId,
|
|
591
|
+
created_at: systemMessage.createdAt.toISOString()
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
catch (error) {
|
|
596
|
+
logger_1.default.error('Close conversation error:', error);
|
|
597
|
+
res.status(500).json({
|
|
598
|
+
error: 'Failed to close conversation',
|
|
599
|
+
message: error instanceof Error ? error.message : 'Unknown error'
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
// Send agent message
|
|
604
|
+
async sendAgentMessage(req, res) {
|
|
605
|
+
try {
|
|
606
|
+
if (!req.user || !req.profile) {
|
|
607
|
+
res.status(401).json({ error: 'Authentication required' });
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
const { uuid } = req.params;
|
|
611
|
+
const { content } = req.body;
|
|
612
|
+
if (!content) {
|
|
613
|
+
res.status(400).json({ error: 'content is required' });
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
const conversation = await this.storage.getConversation(uuid);
|
|
617
|
+
if (!conversation) {
|
|
618
|
+
res.status(404).json({ error: 'Conversation not found' });
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
if (conversation.organizationId !== req.profile.companyId) {
|
|
622
|
+
res.status(404).json({ error: 'Conversation not found' });
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
if (conversation.closedAt) {
|
|
626
|
+
res.status(400).json({ error: 'Conversation is closed' });
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
const agentMessage = await this.storage.saveMessage({
|
|
630
|
+
conversationId: uuid,
|
|
631
|
+
threadId: conversation.threadId,
|
|
632
|
+
role: 'agent',
|
|
633
|
+
content,
|
|
634
|
+
createdAt: new Date(),
|
|
635
|
+
authorId: parseInt(req.user.id)
|
|
636
|
+
});
|
|
637
|
+
const newMessageCount = conversation.messageCount + 1;
|
|
638
|
+
const timestamp = new Date();
|
|
639
|
+
await this.storage.updateConversation(uuid, {
|
|
640
|
+
messageCount: newMessageCount,
|
|
641
|
+
lastMessageAt: timestamp
|
|
642
|
+
});
|
|
643
|
+
if (this.realtimePublisher) {
|
|
644
|
+
try {
|
|
645
|
+
const { data: company } = await this.supabase
|
|
646
|
+
.from('vezlo_companies')
|
|
647
|
+
.select('uuid')
|
|
648
|
+
.eq('id', conversation.organizationId)
|
|
649
|
+
.single();
|
|
650
|
+
if (company?.uuid) {
|
|
651
|
+
await this.realtimePublisher.publish(`company:${company.uuid}:conversations`, 'message:created', {
|
|
652
|
+
conversation_uuid: uuid,
|
|
653
|
+
message: {
|
|
654
|
+
uuid: agentMessage.id,
|
|
655
|
+
content: agentMessage.content,
|
|
656
|
+
type: agentMessage.role,
|
|
657
|
+
author_id: agentMessage.authorId,
|
|
658
|
+
created_at: agentMessage.createdAt.toISOString()
|
|
659
|
+
},
|
|
660
|
+
conversation_update: {
|
|
661
|
+
message_count: newMessageCount,
|
|
662
|
+
last_message_at: timestamp.toISOString()
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
catch (error) {
|
|
668
|
+
logger_1.default.error('[ChatController] Failed to publish agent message update:', error);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
res.json({
|
|
672
|
+
uuid: agentMessage.id,
|
|
673
|
+
content: agentMessage.content,
|
|
674
|
+
type: agentMessage.role,
|
|
675
|
+
author_id: agentMessage.authorId,
|
|
676
|
+
created_at: agentMessage.createdAt.toISOString()
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
catch (error) {
|
|
680
|
+
logger_1.default.error('Send agent message error:', error);
|
|
681
|
+
res.status(500).json({
|
|
682
|
+
error: 'Failed to send agent message',
|
|
295
683
|
message: error instanceof Error ? error.message : 'Unknown error'
|
|
296
684
|
});
|
|
297
685
|
}
|
|
@@ -303,14 +691,42 @@ class ChatController {
|
|
|
303
691
|
res.status(401).json({ error: 'Authentication required' });
|
|
304
692
|
return;
|
|
305
693
|
}
|
|
306
|
-
const
|
|
694
|
+
const page = Math.max(1, parseInt(req.query.page || '1', 10) || 1);
|
|
695
|
+
const pageSizeRaw = parseInt(req.query.page_size || '20', 10);
|
|
696
|
+
const pageSize = Math.min(100, Math.max(1, isNaN(pageSizeRaw) ? 20 : pageSizeRaw));
|
|
697
|
+
const offset = (page - 1) * pageSize;
|
|
698
|
+
const orderParam = req.query.order_by || 'last_message_at';
|
|
699
|
+
const orderBy = orderParam === 'created_at' ? 'updated_at' : 'last_message_at';
|
|
700
|
+
const { conversations, total } = await this.storage.getUserConversations(req.user.id, req.profile.companyId, {
|
|
701
|
+
limit: pageSize,
|
|
702
|
+
offset,
|
|
703
|
+
orderBy: orderBy
|
|
704
|
+
});
|
|
705
|
+
const toIso = (date) => (date ? date.toISOString() : null);
|
|
307
706
|
res.json({
|
|
308
707
|
conversations: conversations.map(conversation => ({
|
|
309
708
|
uuid: conversation.id,
|
|
310
709
|
title: conversation.title,
|
|
311
710
|
message_count: conversation.messageCount,
|
|
312
|
-
created_at: conversation.createdAt
|
|
313
|
-
|
|
711
|
+
created_at: toIso(conversation.createdAt),
|
|
712
|
+
updated_at: toIso(conversation.updatedAt),
|
|
713
|
+
joined_at: toIso(conversation.joinedAt),
|
|
714
|
+
responded_at: toIso(conversation.respondedAt),
|
|
715
|
+
closed_at: toIso(conversation.closedAt),
|
|
716
|
+
last_message_at: toIso(conversation.lastMessageAt),
|
|
717
|
+
status: conversation.closedAt
|
|
718
|
+
? 'closed'
|
|
719
|
+
: conversation.joinedAt
|
|
720
|
+
? 'in_progress'
|
|
721
|
+
: 'open'
|
|
722
|
+
})),
|
|
723
|
+
pagination: {
|
|
724
|
+
page,
|
|
725
|
+
page_size: pageSize,
|
|
726
|
+
total,
|
|
727
|
+
total_pages: Math.max(1, Math.ceil(total / pageSize)),
|
|
728
|
+
has_more: offset + conversations.length < total
|
|
729
|
+
}
|
|
314
730
|
});
|
|
315
731
|
}
|
|
316
732
|
catch (error) {
|
|
@@ -514,10 +930,55 @@ class ChatController {
|
|
|
514
930
|
});
|
|
515
931
|
if (options.conversation) {
|
|
516
932
|
const nextCount = (options.conversation.messageCount || 0) + 1;
|
|
933
|
+
const timestamp = new Date();
|
|
517
934
|
await this.storage.updateConversation(options.conversationId, {
|
|
518
|
-
messageCount: nextCount
|
|
935
|
+
messageCount: nextCount,
|
|
936
|
+
lastMessageAt: timestamp
|
|
519
937
|
});
|
|
520
938
|
options.conversation.messageCount = nextCount;
|
|
939
|
+
options.conversation.lastMessageAt = timestamp;
|
|
940
|
+
// Publish realtime update
|
|
941
|
+
if (this.realtimePublisher && options.conversation.organizationId) {
|
|
942
|
+
try {
|
|
943
|
+
logger_1.default.info(`[ChatController] Fetching company UUID for assistant message, organizationId: ${options.conversation.organizationId}`);
|
|
944
|
+
const { data: company } = await this.supabase
|
|
945
|
+
.from('vezlo_companies')
|
|
946
|
+
.select('uuid')
|
|
947
|
+
.eq('id', options.conversation.organizationId)
|
|
948
|
+
.single();
|
|
949
|
+
if (company?.uuid) {
|
|
950
|
+
logger_1.default.info(`[ChatController] Publishing assistant message update for company: ${company.uuid}`);
|
|
951
|
+
await this.realtimePublisher.publish(`company:${company.uuid}:conversations`, 'message:created', {
|
|
952
|
+
conversation_uuid: options.conversationId,
|
|
953
|
+
message: {
|
|
954
|
+
uuid: assistantMessage.id,
|
|
955
|
+
content: assistantMessage.content,
|
|
956
|
+
type: assistantMessage.role,
|
|
957
|
+
author_id: null,
|
|
958
|
+
created_at: assistantMessage.createdAt.toISOString()
|
|
959
|
+
},
|
|
960
|
+
conversation_update: {
|
|
961
|
+
message_count: nextCount,
|
|
962
|
+
last_message_at: timestamp.toISOString()
|
|
963
|
+
}
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
else {
|
|
967
|
+
logger_1.default.warn(`[ChatController] No company UUID found for organizationId: ${options.conversation.organizationId}`);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
catch (error) {
|
|
971
|
+
logger_1.default.error('[ChatController] Failed to publish realtime update:', error);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
else {
|
|
975
|
+
if (!this.realtimePublisher) {
|
|
976
|
+
logger_1.default.warn('[ChatController] Realtime publisher not available for assistant message');
|
|
977
|
+
}
|
|
978
|
+
if (!options.conversation.organizationId) {
|
|
979
|
+
logger_1.default.warn('[ChatController] No organizationId in conversation for assistant message');
|
|
980
|
+
}
|
|
981
|
+
}
|
|
521
982
|
}
|
|
522
983
|
return assistantMessage;
|
|
523
984
|
}
|