@vezlo/assistant-server 2.2.1 → 2.3.0

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.
Files changed (80) hide show
  1. package/README.md +27 -18
  2. package/database-schema.sql +181 -7
  3. package/dist/knexfile.js +5 -6
  4. package/dist/knexfile.js.map +1 -1
  5. package/dist/src/bootstrap/initializeServices.d.ts.map +1 -1
  6. package/dist/src/bootstrap/initializeServices.js +14 -1
  7. package/dist/src/bootstrap/initializeServices.js.map +1 -1
  8. package/dist/src/config/global.d.ts.map +1 -1
  9. package/dist/src/config/global.js +11 -2
  10. package/dist/src/config/global.js.map +1 -1
  11. package/dist/src/controllers/AuthController.d.ts +4 -1
  12. package/dist/src/controllers/AuthController.d.ts.map +1 -1
  13. package/dist/src/controllers/AuthController.js +8 -3
  14. package/dist/src/controllers/AuthController.js.map +1 -1
  15. package/dist/src/controllers/ChatController.d.ts +9 -1
  16. package/dist/src/controllers/ChatController.d.ts.map +1 -1
  17. package/dist/src/controllers/ChatController.js +502 -85
  18. package/dist/src/controllers/ChatController.js.map +1 -1
  19. package/dist/src/controllers/KnowledgeController.d.ts.map +1 -1
  20. package/dist/src/controllers/KnowledgeController.js +0 -4
  21. package/dist/src/controllers/KnowledgeController.js.map +1 -1
  22. package/dist/src/migrations/005_add_conversation_handoff_columns.d.ts +4 -0
  23. package/dist/src/migrations/005_add_conversation_handoff_columns.d.ts.map +1 -0
  24. package/dist/src/migrations/005_add_conversation_handoff_columns.js +42 -0
  25. package/dist/src/migrations/005_add_conversation_handoff_columns.js.map +1 -0
  26. package/dist/src/migrations/006_add_knowledge_chunks.d.ts +4 -0
  27. package/dist/src/migrations/006_add_knowledge_chunks.d.ts.map +1 -0
  28. package/dist/src/migrations/006_add_knowledge_chunks.js +245 -0
  29. package/dist/src/migrations/006_add_knowledge_chunks.js.map +1 -0
  30. package/dist/src/schemas/ConversationSchemas.d.ts +125 -33
  31. package/dist/src/schemas/ConversationSchemas.d.ts.map +1 -1
  32. package/dist/src/schemas/ConversationSchemas.js +47 -18
  33. package/dist/src/schemas/ConversationSchemas.js.map +1 -1
  34. package/dist/src/schemas/MessageSchemas.d.ts +9 -0
  35. package/dist/src/schemas/MessageSchemas.d.ts.map +1 -1
  36. package/dist/src/schemas/MessageSchemas.js +3 -1
  37. package/dist/src/schemas/MessageSchemas.js.map +1 -1
  38. package/dist/src/schemas/index.d.ts +134 -33
  39. package/dist/src/schemas/index.d.ts.map +1 -1
  40. package/dist/src/server.js +225 -27
  41. package/dist/src/server.js.map +1 -1
  42. package/dist/src/services/AIService.d.ts.map +1 -1
  43. package/dist/src/services/AIService.js +1 -3
  44. package/dist/src/services/AIService.js.map +1 -1
  45. package/dist/src/services/ChatManager.d.ts.map +1 -1
  46. package/dist/src/services/ChatManager.js +26 -29
  47. package/dist/src/services/ChatManager.js.map +1 -1
  48. package/dist/src/services/IntentService.d.ts +2 -1
  49. package/dist/src/services/IntentService.d.ts.map +1 -1
  50. package/dist/src/services/IntentService.js +29 -7
  51. package/dist/src/services/IntentService.js.map +1 -1
  52. package/dist/src/services/KnowledgeBaseService.d.ts +20 -5
  53. package/dist/src/services/KnowledgeBaseService.d.ts.map +1 -1
  54. package/dist/src/services/KnowledgeBaseService.js +203 -137
  55. package/dist/src/services/KnowledgeBaseService.js.map +1 -1
  56. package/dist/src/services/RealtimePublisher.d.ts +6 -0
  57. package/dist/src/services/RealtimePublisher.d.ts.map +1 -0
  58. package/dist/src/services/RealtimePublisher.js +43 -0
  59. package/dist/src/services/RealtimePublisher.js.map +1 -0
  60. package/dist/src/storage/ConversationRepository.d.ts +2 -2
  61. package/dist/src/storage/ConversationRepository.d.ts.map +1 -1
  62. package/dist/src/storage/ConversationRepository.js +36 -19
  63. package/dist/src/storage/ConversationRepository.js.map +1 -1
  64. package/dist/src/storage/MessageRepository.d.ts +4 -1
  65. package/dist/src/storage/MessageRepository.d.ts.map +1 -1
  66. package/dist/src/storage/MessageRepository.js +10 -4
  67. package/dist/src/storage/MessageRepository.js.map +1 -1
  68. package/dist/src/storage/SupabaseStorage.d.ts +5 -3
  69. package/dist/src/storage/SupabaseStorage.d.ts.map +1 -1
  70. package/dist/src/storage/SupabaseStorage.js +42 -11
  71. package/dist/src/storage/SupabaseStorage.js.map +1 -1
  72. package/dist/src/storage/UnifiedStorage.d.ts +5 -3
  73. package/dist/src/storage/UnifiedStorage.d.ts.map +1 -1
  74. package/dist/src/storage/UnifiedStorage.js +4 -4
  75. package/dist/src/storage/UnifiedStorage.js.map +1 -1
  76. package/dist/src/types/index.d.ts +24 -4
  77. package/dist/src/types/index.d.ts.map +1 -1
  78. package/knexfile.ts +4 -4
  79. package/package.json +2 -2
  80. package/scripts/test-chunks-embeddings.js +190 -0
@@ -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: new Date(),
106
- updatedAt: new Date()
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: conversation.messageCount + 1
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
- const messages = await this.chatManager.getRecentMessages(conversationId, this.chatHistoryLength);
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);
@@ -199,8 +264,6 @@ class ChatController {
199
264
  logger_1.default.info(`🔍 Searching KB: query="${userMessageContent.substring(0, 50)}...", companyId=${companyId}`);
200
265
  const searchResults = await aiService.knowledgeBaseService.search(userMessageContent, {
201
266
  limit: 5,
202
- threshold: 0.5, // Balanced precision/recall (0.5 is industry standard)
203
- type: 'semantic', // Modern RAG best practice: semantic-only for better context
204
267
  company_id: companyId
205
268
  });
206
269
  logger_1.default.info(`📊 Found knowledge base results: ${searchResults.length}`);
@@ -261,37 +324,360 @@ class ChatController {
261
324
  });
262
325
  }
263
326
  }
264
- // Get conversation details with messages
327
+ // Get conversation details
265
328
  async getConversation(req, res) {
266
329
  try {
330
+ if (!req.profile) {
331
+ res.status(401).json({ error: 'Authentication required' });
332
+ return;
333
+ }
267
334
  const { uuid } = req.params;
268
335
  const conversation = await this.storage.getConversation(uuid);
269
336
  if (!conversation) {
270
337
  res.status(404).json({ error: 'Conversation not found' });
271
338
  return;
272
339
  }
273
- const messages = await this.storage.getMessages(uuid, 50);
340
+ if (conversation.organizationId && conversation.organizationId !== req.profile.companyId) {
341
+ res.status(404).json({ error: 'Conversation not found' });
342
+ return;
343
+ }
344
+ const toIso = (date) => (date ? date.toISOString() : null);
345
+ const status = conversation.closedAt
346
+ ? 'closed'
347
+ : conversation.joinedAt
348
+ ? 'in_progress'
349
+ : 'open';
274
350
  res.json({
275
351
  uuid: conversation.id,
276
352
  title: conversation.title,
277
353
  user_uuid: conversation.userId,
278
354
  company_uuid: conversation.organizationId,
279
355
  message_count: conversation.messageCount,
280
- created_at: conversation.createdAt,
356
+ created_at: toIso(conversation.createdAt),
357
+ updated_at: toIso(conversation.updatedAt),
358
+ joined_at: toIso(conversation.joinedAt),
359
+ responded_at: toIso(conversation.respondedAt),
360
+ closed_at: toIso(conversation.closedAt),
361
+ last_message_at: toIso(conversation.lastMessageAt),
362
+ status
363
+ });
364
+ }
365
+ catch (error) {
366
+ logger_1.default.error('Get conversation error:', error);
367
+ res.status(500).json({
368
+ error: 'Failed to get conversation',
369
+ message: error instanceof Error ? error.message : 'Unknown error'
370
+ });
371
+ }
372
+ }
373
+ // Get conversation messages
374
+ async getConversationMessages(req, res) {
375
+ try {
376
+ if (!req.profile) {
377
+ res.status(401).json({ error: 'Authentication required' });
378
+ return;
379
+ }
380
+ const { uuid } = req.params;
381
+ const page = Math.max(1, parseInt(req.query.page || '1', 10) || 1);
382
+ const pageSizeRaw = parseInt(req.query.page_size || '50', 10);
383
+ const pageSize = Math.min(200, Math.max(1, isNaN(pageSizeRaw) ? 50 : pageSizeRaw));
384
+ const offset = (page - 1) * pageSize;
385
+ const orderParam = (req.query.order || 'desc').toLowerCase();
386
+ const order = orderParam === 'asc' ? 'asc' : 'desc';
387
+ const conversation = await this.storage.getConversation(uuid);
388
+ if (!conversation) {
389
+ res.status(404).json({ error: 'Conversation not found' });
390
+ return;
391
+ }
392
+ if (conversation.organizationId && conversation.organizationId !== req.profile.companyId) {
393
+ res.status(404).json({ error: 'Conversation not found' });
394
+ return;
395
+ }
396
+ const messages = await this.storage.getMessages(uuid, pageSize, offset, { order });
397
+ const hasMore = messages.length === pageSize;
398
+ const toIso = (date) => (date ? date.toISOString() : null);
399
+ res.json({
400
+ conversation_uuid: conversation.id,
401
+ order,
281
402
  messages: messages.map(msg => ({
282
403
  uuid: msg.id,
283
404
  parent_message_uuid: msg.parentMessageId,
284
405
  type: msg.role,
285
406
  content: msg.content,
286
407
  status: 'completed',
287
- created_at: msg.createdAt
288
- }))
408
+ created_at: toIso(msg.createdAt),
409
+ author_id: msg.authorId ?? null
410
+ })),
411
+ pagination: {
412
+ page,
413
+ page_size: pageSize,
414
+ has_more: hasMore,
415
+ next_offset: hasMore ? offset + pageSize : null
416
+ }
289
417
  });
290
418
  }
291
419
  catch (error) {
292
- logger_1.default.error('Get conversation error:', error);
420
+ logger_1.default.error('Get conversation messages error:', error);
293
421
  res.status(500).json({
294
- error: 'Failed to get conversation',
422
+ error: 'Failed to get conversation messages',
423
+ message: error instanceof Error ? error.message : 'Unknown error'
424
+ });
425
+ }
426
+ }
427
+ // Join conversation
428
+ async joinConversation(req, res) {
429
+ try {
430
+ if (!req.user || !req.profile) {
431
+ res.status(401).json({ error: 'Authentication required' });
432
+ return;
433
+ }
434
+ const { uuid } = req.params;
435
+ const conversation = await this.storage.getConversation(uuid);
436
+ if (!conversation) {
437
+ res.status(404).json({ error: 'Conversation not found' });
438
+ return;
439
+ }
440
+ if (conversation.organizationId !== req.profile.companyId) {
441
+ res.status(404).json({ error: 'Conversation not found' });
442
+ return;
443
+ }
444
+ if (conversation.closedAt) {
445
+ res.status(400).json({ error: 'Conversation is closed' });
446
+ return;
447
+ }
448
+ const joinedAt = new Date();
449
+ await this.storage.updateConversation(uuid, {
450
+ joinedAt,
451
+ status: 'in_progress'
452
+ });
453
+ const systemMessage = await this.storage.saveMessage({
454
+ conversationId: uuid,
455
+ threadId: conversation.threadId,
456
+ role: 'system',
457
+ content: `${req.user.name} has joined the conversation.`,
458
+ createdAt: joinedAt,
459
+ authorId: parseInt(req.user.id)
460
+ });
461
+ const newMessageCount = conversation.messageCount + 1;
462
+ await this.storage.updateConversation(uuid, {
463
+ messageCount: newMessageCount,
464
+ lastMessageAt: joinedAt
465
+ });
466
+ if (this.realtimePublisher) {
467
+ try {
468
+ const { data: company } = await this.supabase
469
+ .from('vezlo_companies')
470
+ .select('uuid')
471
+ .eq('id', conversation.organizationId)
472
+ .single();
473
+ if (company?.uuid) {
474
+ await this.realtimePublisher.publish(`company:${company.uuid}:conversations`, 'message:created', {
475
+ conversation_uuid: uuid,
476
+ message: {
477
+ uuid: systemMessage.id,
478
+ content: systemMessage.content,
479
+ type: systemMessage.role,
480
+ author_id: systemMessage.authorId,
481
+ created_at: systemMessage.createdAt.toISOString()
482
+ },
483
+ conversation_update: {
484
+ message_count: newMessageCount,
485
+ last_message_at: joinedAt.toISOString(),
486
+ joined_at: joinedAt.toISOString(),
487
+ status: 'in_progress'
488
+ }
489
+ });
490
+ }
491
+ }
492
+ catch (error) {
493
+ logger_1.default.error('[ChatController] Failed to publish join conversation update:', error);
494
+ }
495
+ }
496
+ res.json({
497
+ success: true,
498
+ message: {
499
+ uuid: systemMessage.id,
500
+ content: systemMessage.content,
501
+ type: systemMessage.role,
502
+ author_id: systemMessage.authorId,
503
+ created_at: systemMessage.createdAt.toISOString()
504
+ }
505
+ });
506
+ }
507
+ catch (error) {
508
+ logger_1.default.error('Join conversation error:', error);
509
+ res.status(500).json({
510
+ error: 'Failed to join conversation',
511
+ message: error instanceof Error ? error.message : 'Unknown error'
512
+ });
513
+ }
514
+ }
515
+ // Close conversation
516
+ async closeConversation(req, res) {
517
+ try {
518
+ if (!req.user || !req.profile) {
519
+ res.status(401).json({ error: 'Authentication required' });
520
+ return;
521
+ }
522
+ const { uuid } = req.params;
523
+ const conversation = await this.storage.getConversation(uuid);
524
+ if (!conversation) {
525
+ res.status(404).json({ error: 'Conversation not found' });
526
+ return;
527
+ }
528
+ if (conversation.organizationId !== req.profile.companyId) {
529
+ res.status(404).json({ error: 'Conversation not found' });
530
+ return;
531
+ }
532
+ if (conversation.closedAt) {
533
+ res.status(400).json({ error: 'Conversation is already closed' });
534
+ return;
535
+ }
536
+ const closedAt = new Date();
537
+ const systemMessage = await this.storage.saveMessage({
538
+ conversationId: uuid,
539
+ threadId: conversation.threadId,
540
+ role: 'system',
541
+ content: `${req.user.name} has closed the conversation.`,
542
+ createdAt: closedAt,
543
+ authorId: parseInt(req.user.id, 10)
544
+ });
545
+ const newMessageCount = conversation.messageCount + 1;
546
+ await this.storage.updateConversation(uuid, {
547
+ messageCount: newMessageCount,
548
+ lastMessageAt: closedAt,
549
+ closedAt
550
+ });
551
+ if (this.realtimePublisher) {
552
+ try {
553
+ const { data: company } = await this.supabase
554
+ .from('vezlo_companies')
555
+ .select('uuid')
556
+ .eq('id', conversation.organizationId)
557
+ .single();
558
+ if (company?.uuid) {
559
+ await this.realtimePublisher.publish(`company:${company.uuid}:conversations`, 'message:created', {
560
+ conversation_uuid: uuid,
561
+ message: {
562
+ uuid: systemMessage.id,
563
+ content: systemMessage.content,
564
+ type: systemMessage.role,
565
+ author_id: systemMessage.authorId,
566
+ created_at: systemMessage.createdAt.toISOString()
567
+ },
568
+ conversation_update: {
569
+ message_count: newMessageCount,
570
+ last_message_at: closedAt.toISOString(),
571
+ joined_at: conversation.joinedAt?.toISOString() || null,
572
+ closed_at: closedAt.toISOString(),
573
+ status: 'closed'
574
+ }
575
+ });
576
+ }
577
+ }
578
+ catch (error) {
579
+ logger_1.default.error('[ChatController] Failed to publish close conversation update:', error);
580
+ }
581
+ }
582
+ res.json({
583
+ success: true,
584
+ message: {
585
+ uuid: systemMessage.id,
586
+ content: systemMessage.content,
587
+ type: systemMessage.role,
588
+ author_id: systemMessage.authorId,
589
+ created_at: systemMessage.createdAt.toISOString()
590
+ }
591
+ });
592
+ }
593
+ catch (error) {
594
+ logger_1.default.error('Close conversation error:', error);
595
+ res.status(500).json({
596
+ error: 'Failed to close conversation',
597
+ message: error instanceof Error ? error.message : 'Unknown error'
598
+ });
599
+ }
600
+ }
601
+ // Send agent message
602
+ async sendAgentMessage(req, res) {
603
+ try {
604
+ if (!req.user || !req.profile) {
605
+ res.status(401).json({ error: 'Authentication required' });
606
+ return;
607
+ }
608
+ const { uuid } = req.params;
609
+ const { content } = req.body;
610
+ if (!content) {
611
+ res.status(400).json({ error: 'content is required' });
612
+ return;
613
+ }
614
+ const conversation = await this.storage.getConversation(uuid);
615
+ if (!conversation) {
616
+ res.status(404).json({ error: 'Conversation not found' });
617
+ return;
618
+ }
619
+ if (conversation.organizationId !== req.profile.companyId) {
620
+ res.status(404).json({ error: 'Conversation not found' });
621
+ return;
622
+ }
623
+ if (conversation.closedAt) {
624
+ res.status(400).json({ error: 'Conversation is closed' });
625
+ return;
626
+ }
627
+ const agentMessage = await this.storage.saveMessage({
628
+ conversationId: uuid,
629
+ threadId: conversation.threadId,
630
+ role: 'agent',
631
+ content,
632
+ createdAt: new Date(),
633
+ authorId: parseInt(req.user.id)
634
+ });
635
+ const newMessageCount = conversation.messageCount + 1;
636
+ const timestamp = new Date();
637
+ await this.storage.updateConversation(uuid, {
638
+ messageCount: newMessageCount,
639
+ lastMessageAt: timestamp
640
+ });
641
+ if (this.realtimePublisher) {
642
+ try {
643
+ const { data: company } = await this.supabase
644
+ .from('vezlo_companies')
645
+ .select('uuid')
646
+ .eq('id', conversation.organizationId)
647
+ .single();
648
+ if (company?.uuid) {
649
+ await this.realtimePublisher.publish(`company:${company.uuid}:conversations`, 'message:created', {
650
+ conversation_uuid: uuid,
651
+ message: {
652
+ uuid: agentMessage.id,
653
+ content: agentMessage.content,
654
+ type: agentMessage.role,
655
+ author_id: agentMessage.authorId,
656
+ created_at: agentMessage.createdAt.toISOString()
657
+ },
658
+ conversation_update: {
659
+ message_count: newMessageCount,
660
+ last_message_at: timestamp.toISOString()
661
+ }
662
+ });
663
+ }
664
+ }
665
+ catch (error) {
666
+ logger_1.default.error('[ChatController] Failed to publish agent message update:', error);
667
+ }
668
+ }
669
+ res.json({
670
+ uuid: agentMessage.id,
671
+ content: agentMessage.content,
672
+ type: agentMessage.role,
673
+ author_id: agentMessage.authorId,
674
+ created_at: agentMessage.createdAt.toISOString()
675
+ });
676
+ }
677
+ catch (error) {
678
+ logger_1.default.error('Send agent message error:', error);
679
+ res.status(500).json({
680
+ error: 'Failed to send agent message',
295
681
  message: error instanceof Error ? error.message : 'Unknown error'
296
682
  });
297
683
  }
@@ -303,14 +689,42 @@ class ChatController {
303
689
  res.status(401).json({ error: 'Authentication required' });
304
690
  return;
305
691
  }
306
- const conversations = await this.storage.getUserConversations(req.user.id, req.profile?.companyId || undefined);
692
+ const page = Math.max(1, parseInt(req.query.page || '1', 10) || 1);
693
+ const pageSizeRaw = parseInt(req.query.page_size || '20', 10);
694
+ const pageSize = Math.min(100, Math.max(1, isNaN(pageSizeRaw) ? 20 : pageSizeRaw));
695
+ const offset = (page - 1) * pageSize;
696
+ const orderParam = req.query.order_by || 'last_message_at';
697
+ const orderBy = orderParam === 'created_at' ? 'updated_at' : 'last_message_at';
698
+ const { conversations, total } = await this.storage.getUserConversations(req.user.id, req.profile.companyId, {
699
+ limit: pageSize,
700
+ offset,
701
+ orderBy: orderBy
702
+ });
703
+ const toIso = (date) => (date ? date.toISOString() : null);
307
704
  res.json({
308
705
  conversations: conversations.map(conversation => ({
309
706
  uuid: conversation.id,
310
707
  title: conversation.title,
311
708
  message_count: conversation.messageCount,
312
- created_at: conversation.createdAt
313
- }))
709
+ created_at: toIso(conversation.createdAt),
710
+ updated_at: toIso(conversation.updatedAt),
711
+ joined_at: toIso(conversation.joinedAt),
712
+ responded_at: toIso(conversation.respondedAt),
713
+ closed_at: toIso(conversation.closedAt),
714
+ last_message_at: toIso(conversation.lastMessageAt),
715
+ status: conversation.closedAt
716
+ ? 'closed'
717
+ : conversation.joinedAt
718
+ ? 'in_progress'
719
+ : 'open'
720
+ })),
721
+ pagination: {
722
+ page,
723
+ page_size: pageSize,
724
+ total,
725
+ total_pages: Math.max(1, Math.ceil(total / pageSize)),
726
+ has_more: offset + conversations.length < total
727
+ }
314
728
  });
315
729
  }
316
730
  catch (error) {
@@ -415,75 +829,33 @@ class ChatController {
415
829
  return true;
416
830
  }
417
831
  logger_1.default.info(`🧾 Intent result: ${result.intent}${result.needsGuardrail ? ' (guardrail triggered)' : ''}`);
418
- switch (result.intent) {
419
- case 'greeting':
420
- await this.respondWithAssistantMessage({
421
- conversation,
422
- conversationId,
423
- parentMessageId: userMessage.id,
424
- content: 'Hello! How can I help you today?'
425
- }, res);
426
- return true;
427
- case 'personality':
428
- // Get assistant name and organization from environment
429
- const assistantName = process.env.ASSISTANT_NAME || 'AI Assistant';
430
- const orgName = process.env.ORGANIZATION_NAME || 'Your Organization';
431
- await this.respondWithAssistantMessage({
432
- conversation,
433
- conversationId,
434
- parentMessageId: userMessage.id,
435
- content: `I'm ${assistantName}, your AI assistant for ${orgName}. I help teams understand and work with the ${orgName} platform by answering questions about features, documentation, and technical details. How can I assist you today?`
436
- }, res);
437
- return true;
438
- case 'clarification':
439
- await this.respondWithAssistantMessage({
440
- conversation,
441
- conversationId,
442
- parentMessageId: userMessage.id,
443
- content: "I'm not sure I understood. Could you clarify what you need help with?"
444
- }, res);
445
- return true;
446
- case 'guardrail':
447
- await this.respondWithAssistantMessage({
448
- conversation,
449
- conversationId,
450
- parentMessageId: userMessage.id,
451
- content: `I can help with documentation or implementation guidance, but I can't share credentials or confidential configuration. Please contact your system administrator or support for access.`
452
- }, res);
453
- return true;
454
- case 'human_support_email':
455
- await this.respondWithAssistantMessage({
456
- conversation,
457
- conversationId,
458
- parentMessageId: userMessage.id,
459
- content: `Thanks for sharing your email. Our team will reach out soon—response times may vary depending on support volume.`
460
- }, res);
461
- logger_1.default.info('📨 Recorded human support email from user.');
462
- return true;
463
- case 'human_support_request':
464
- if (result.contactEmail) {
465
- await this.respondWithAssistantMessage({
466
- conversation,
467
- conversationId,
468
- parentMessageId: userMessage.id,
469
- content: `Thanks for sharing your email. Our team will reach out soon—response times may vary depending on support volume.`
470
- }, res);
471
- logger_1.default.info('📨 Human support request with email handled in a single step.');
472
- }
473
- else {
474
- await this.respondWithAssistantMessage({
475
- conversation,
476
- conversationId,
477
- parentMessageId: userMessage.id,
478
- content: 'I can connect you with a human teammate. Please share your email address so we can follow up.'
479
- }, res);
480
- logger_1.default.info('🙋 Human support requested; awaiting email from user.');
481
- }
482
- return true;
483
- default:
484
- logger_1.default.info('📚 Intent requires knowledge lookup; proceeding with RAG flow.');
485
- return false;
832
+ // For non-knowledge intents, use LLM-generated response from intent classification
833
+ if (result.intent !== 'knowledge') {
834
+ const responseContent = result.response || this.getFallbackResponse(result.intent);
835
+ await this.respondWithAssistantMessage({
836
+ conversation,
837
+ conversationId,
838
+ parentMessageId: userMessage.id,
839
+ content: responseContent
840
+ }, res);
841
+ return true;
486
842
  }
843
+ // Knowledge intent - proceed to RAG flow
844
+ logger_1.default.info('📚 Intent requires knowledge lookup; proceeding with RAG flow.');
845
+ return false;
846
+ }
847
+ getFallbackResponse(intent) {
848
+ // Fallback responses in case LLM doesn't generate one (shouldn't happen, but safety net)
849
+ const fallbacks = {
850
+ greeting: 'Hello! How can I help you today?',
851
+ acknowledgment: "You're welcome! Let me know if you need anything else.",
852
+ personality: `I'm ${process.env.ASSISTANT_NAME || 'AI Assistant'}, your AI assistant for ${process.env.ORGANIZATION_NAME || 'Your Organization'}.`,
853
+ clarification: "I'm not sure I understood. Could you clarify what you need help with?",
854
+ guardrail: "I can help with documentation or implementation guidance, but I can't share credentials or confidential configuration.",
855
+ human_support_request: "I'd be happy to connect you with our support team. Could you please provide your email address?",
856
+ human_support_email: "Thank you! Our support team will reach out to you shortly."
857
+ };
858
+ return fallbacks[intent] || "I'm here to help. What would you like to know?";
487
859
  }
488
860
  async respondWithAssistantMessage(payload, res) {
489
861
  const assistantMessage = await this.saveAssistantMessage({
@@ -514,10 +886,55 @@ class ChatController {
514
886
  });
515
887
  if (options.conversation) {
516
888
  const nextCount = (options.conversation.messageCount || 0) + 1;
889
+ const timestamp = new Date();
517
890
  await this.storage.updateConversation(options.conversationId, {
518
- messageCount: nextCount
891
+ messageCount: nextCount,
892
+ lastMessageAt: timestamp
519
893
  });
520
894
  options.conversation.messageCount = nextCount;
895
+ options.conversation.lastMessageAt = timestamp;
896
+ // Publish realtime update
897
+ if (this.realtimePublisher && options.conversation.organizationId) {
898
+ try {
899
+ logger_1.default.info(`[ChatController] Fetching company UUID for assistant message, organizationId: ${options.conversation.organizationId}`);
900
+ const { data: company } = await this.supabase
901
+ .from('vezlo_companies')
902
+ .select('uuid')
903
+ .eq('id', options.conversation.organizationId)
904
+ .single();
905
+ if (company?.uuid) {
906
+ logger_1.default.info(`[ChatController] Publishing assistant message update for company: ${company.uuid}`);
907
+ await this.realtimePublisher.publish(`company:${company.uuid}:conversations`, 'message:created', {
908
+ conversation_uuid: options.conversationId,
909
+ message: {
910
+ uuid: assistantMessage.id,
911
+ content: assistantMessage.content,
912
+ type: assistantMessage.role,
913
+ author_id: null,
914
+ created_at: assistantMessage.createdAt.toISOString()
915
+ },
916
+ conversation_update: {
917
+ message_count: nextCount,
918
+ last_message_at: timestamp.toISOString()
919
+ }
920
+ });
921
+ }
922
+ else {
923
+ logger_1.default.warn(`[ChatController] No company UUID found for organizationId: ${options.conversation.organizationId}`);
924
+ }
925
+ }
926
+ catch (error) {
927
+ logger_1.default.error('[ChatController] Failed to publish realtime update:', error);
928
+ }
929
+ }
930
+ else {
931
+ if (!this.realtimePublisher) {
932
+ logger_1.default.warn('[ChatController] Realtime publisher not available for assistant message');
933
+ }
934
+ if (!options.conversation.organizationId) {
935
+ logger_1.default.warn('[ChatController] No organizationId in conversation for assistant message');
936
+ }
937
+ }
521
938
  }
522
939
  return assistantMessage;
523
940
  }