@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.
- package/README.md +27 -18
- package/database-schema.sql +181 -7
- 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 +14 -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 +11 -2
- 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 +9 -1
- package/dist/src/controllers/ChatController.d.ts.map +1 -1
- package/dist/src/controllers/ChatController.js +502 -85
- package/dist/src/controllers/ChatController.js.map +1 -1
- package/dist/src/controllers/KnowledgeController.d.ts.map +1 -1
- package/dist/src/controllers/KnowledgeController.js +0 -4
- package/dist/src/controllers/KnowledgeController.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/migrations/006_add_knowledge_chunks.d.ts +4 -0
- package/dist/src/migrations/006_add_knowledge_chunks.d.ts.map +1 -0
- package/dist/src/migrations/006_add_knowledge_chunks.js +245 -0
- package/dist/src/migrations/006_add_knowledge_chunks.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 +225 -27
- package/dist/src/server.js.map +1 -1
- package/dist/src/services/AIService.d.ts.map +1 -1
- package/dist/src/services/AIService.js +1 -3
- package/dist/src/services/AIService.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 +2 -1
- package/dist/src/services/IntentService.d.ts.map +1 -1
- package/dist/src/services/IntentService.js +29 -7
- package/dist/src/services/IntentService.js.map +1 -1
- package/dist/src/services/KnowledgeBaseService.d.ts +20 -5
- package/dist/src/services/KnowledgeBaseService.d.ts.map +1 -1
- package/dist/src/services/KnowledgeBaseService.js +203 -137
- package/dist/src/services/KnowledgeBaseService.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
- 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:
|
|
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);
|
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
}
|