@vezlo/assistant-server 2.2.2 โ 2.4.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 +21 -15
- package/database-schema.sql +178 -8
- package/dist/src/bootstrap/initializeServices.d.ts.map +1 -1
- package/dist/src/bootstrap/initializeServices.js +2 -0
- package/dist/src/bootstrap/initializeServices.js.map +1 -1
- package/dist/src/config/global.js +1 -1
- package/dist/src/config/global.js.map +1 -1
- package/dist/src/controllers/ChatController.d.ts +12 -1
- package/dist/src/controllers/ChatController.d.ts.map +1 -1
- package/dist/src/controllers/ChatController.js +274 -148
- 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/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/migrations/007_add_updated_at_to_feedback.d.ts +4 -0
- package/dist/src/migrations/007_add_updated_at_to_feedback.d.ts.map +1 -0
- package/dist/src/migrations/007_add_updated_at_to_feedback.js +22 -0
- package/dist/src/migrations/007_add_updated_at_to_feedback.js.map +1 -0
- package/dist/src/server.js +42 -8
- package/dist/src/server.js.map +1 -1
- package/dist/src/services/AIService.d.ts +9 -0
- package/dist/src/services/AIService.d.ts.map +1 -1
- package/dist/src/services/AIService.js +111 -3
- package/dist/src/services/AIService.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 +23 -4
- 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/storage/FeedbackRepository.d.ts +1 -0
- package/dist/src/storage/FeedbackRepository.d.ts.map +1 -1
- package/dist/src/storage/FeedbackRepository.js +26 -0
- package/dist/src/storage/FeedbackRepository.js.map +1 -1
- package/dist/src/storage/UnifiedStorage.d.ts +1 -0
- package/dist/src/storage/UnifiedStorage.d.ts.map +1 -1
- package/dist/src/storage/UnifiedStorage.js +3 -0
- package/dist/src/storage/UnifiedStorage.js.map +1 -1
- package/package.json +2 -2
- package/scripts/test-chunks-embeddings.js +190 -0
|
@@ -246,84 +246,202 @@ class ChatController {
|
|
|
246
246
|
});
|
|
247
247
|
return;
|
|
248
248
|
}
|
|
249
|
+
// Set up Server-Sent Events (SSE) headers for streaming (always stream, consistent format)
|
|
250
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
251
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
252
|
+
res.setHeader('Connection', 'keep-alive');
|
|
253
|
+
res.setHeader('X-Accel-Buffering', 'no'); // Disable nginx buffering
|
|
249
254
|
// Run intent classification to decide handling strategy
|
|
250
255
|
const intentResult = await this.classifyIntent(userMessageContent, messages);
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
256
|
+
const intentResponse = await this.handleIntentResult(intentResult, userMessage, conversationId, conversation);
|
|
257
|
+
let accumulatedContent = '';
|
|
258
|
+
let assistantMessageId;
|
|
259
|
+
try {
|
|
260
|
+
// If intent returned a response (non-knowledge intent), stream it
|
|
261
|
+
if (intentResponse) {
|
|
262
|
+
logger_1.default.info(`๐ค Streaming intent response for: ${intentResult.intent}`);
|
|
263
|
+
await this.streamTextContent(intentResponse, res);
|
|
264
|
+
accumulatedContent = intentResponse;
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
// Knowledge intent - proceed with RAG flow and stream AI response
|
|
268
|
+
logger_1.default.info('๐ Streaming knowledge-based response');
|
|
269
|
+
// Get knowledge base search results if available
|
|
270
|
+
const aiService = this.chatManager.aiService;
|
|
271
|
+
let knowledgeResults = null;
|
|
272
|
+
// Get conversation to extract company_id for knowledge base search
|
|
273
|
+
const companyIdRaw = req.profile?.companyId || conversation?.organizationId;
|
|
274
|
+
const companyId = companyIdRaw ? (typeof companyIdRaw === 'string' ? parseInt(companyIdRaw, 10) : companyIdRaw) : undefined;
|
|
275
|
+
if (aiService && aiService.knowledgeBaseService) {
|
|
276
|
+
try {
|
|
277
|
+
logger_1.default.info(`๐ Searching KB: query="${userMessageContent.substring(0, 50)}...", companyId=${companyId}`);
|
|
278
|
+
const searchResults = await aiService.knowledgeBaseService.search(userMessageContent, {
|
|
279
|
+
limit: 5,
|
|
280
|
+
company_id: companyId
|
|
281
|
+
});
|
|
282
|
+
logger_1.default.info(`๐ Found knowledge base results: ${searchResults.length}`);
|
|
283
|
+
if (searchResults.length > 0) {
|
|
284
|
+
knowledgeResults = '\n\nRelevant information from knowledge base:\n';
|
|
285
|
+
searchResults.forEach((result) => {
|
|
286
|
+
const title = result.title || 'Untitled';
|
|
287
|
+
const content = result.content || '';
|
|
288
|
+
if (content.trim()) {
|
|
289
|
+
knowledgeResults += `- ${title}: ${content}\n`;
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
// Verify we actually have meaningful content (not just the header)
|
|
293
|
+
const headerLength = '\n\nRelevant information from knowledge base:\n'.length;
|
|
294
|
+
if (knowledgeResults.length > headerLength + 10) {
|
|
295
|
+
logger_1.default.info(`โ
Knowledge context prepared (${knowledgeResults.length} chars, ${searchResults.length} results)`);
|
|
296
|
+
// Log first 200 chars for debugging
|
|
297
|
+
logger_1.default.info(`๐ Knowledge preview: ${knowledgeResults.substring(0, 200)}...`);
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
logger_1.default.warn(`โ ๏ธ Knowledge results found but content is empty or too short (${knowledgeResults.length} chars), treating as no results`);
|
|
301
|
+
knowledgeResults = '';
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
knowledgeResults = '';
|
|
306
|
+
logger_1.default.info('โ ๏ธ No knowledge base results found; will return appropriate fallback response');
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
console.error('โ Failed to search knowledge base:', error);
|
|
311
|
+
logger_1.default.error('Failed to search knowledge base:', error);
|
|
312
|
+
knowledgeResults = null;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
logger_1.default.warn('โ ๏ธ AI service or knowledge base service not available');
|
|
317
|
+
}
|
|
318
|
+
// Build context for AI
|
|
319
|
+
const chatContext = {
|
|
320
|
+
conversationHistory: messages.map(msg => ({
|
|
321
|
+
role: msg.role,
|
|
322
|
+
content: msg.content
|
|
323
|
+
})),
|
|
324
|
+
knowledgeResults: knowledgeResults ?? undefined
|
|
325
|
+
};
|
|
326
|
+
// Stream response from OpenAI
|
|
327
|
+
logger_1.default.info('๐ Starting OpenAI stream...');
|
|
328
|
+
const stream = aiService.generateResponseStream(userMessageContent, chatContext);
|
|
329
|
+
let chunkCount = 0;
|
|
330
|
+
for await (const { chunk, done, fullContent } of stream) {
|
|
331
|
+
chunkCount++;
|
|
332
|
+
// Always send the chunk (even if empty with done flag)
|
|
333
|
+
const chunkData = JSON.stringify({
|
|
334
|
+
type: 'chunk',
|
|
335
|
+
content: chunk,
|
|
336
|
+
done: done || false // Include done flag
|
|
337
|
+
});
|
|
338
|
+
res.write(`data: ${chunkData}\n\n`);
|
|
339
|
+
if (res.flush)
|
|
340
|
+
res.flush();
|
|
341
|
+
// Update accumulated content
|
|
342
|
+
if (chunk) {
|
|
343
|
+
accumulatedContent += chunk;
|
|
344
|
+
}
|
|
345
|
+
// Log first and last chunks
|
|
346
|
+
if (chunkCount === 1) {
|
|
347
|
+
logger_1.default.info(`๐ค First chunk sent: "${chunk.substring(0, 30)}..."`);
|
|
348
|
+
}
|
|
349
|
+
if (done && fullContent) {
|
|
350
|
+
accumulatedContent = fullContent;
|
|
351
|
+
logger_1.default.info(`๐ Stream complete: ${chunkCount} chunks sent, ${fullContent.length} total chars`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
// Save the message after streaming completes
|
|
356
|
+
try {
|
|
357
|
+
const assistantMessage = await this.saveAssistantMessage({
|
|
358
|
+
conversation,
|
|
359
|
+
conversationId,
|
|
360
|
+
parentMessageId: uuid,
|
|
361
|
+
content: accumulatedContent,
|
|
362
|
+
toolResults: []
|
|
363
|
+
});
|
|
364
|
+
assistantMessageId = assistantMessage.id;
|
|
365
|
+
// Send completion event with final message metadata (no content - already streamed)
|
|
366
|
+
const completionData = JSON.stringify({
|
|
367
|
+
type: 'completion',
|
|
368
|
+
uuid: assistantMessage.id,
|
|
369
|
+
parent_message_uuid: uuid,
|
|
370
|
+
status: 'completed',
|
|
371
|
+
created_at: assistantMessage.createdAt.toISOString()
|
|
372
|
+
});
|
|
373
|
+
res.write(`data: ${completionData}\n\n`);
|
|
374
|
+
}
|
|
375
|
+
catch (saveError) {
|
|
376
|
+
logger_1.default.error('Failed to save assistant message:', saveError);
|
|
377
|
+
const errorData = JSON.stringify({
|
|
378
|
+
type: 'error',
|
|
379
|
+
error: 'Failed to save message',
|
|
380
|
+
message: saveError instanceof Error ? saveError.message : 'Unknown error'
|
|
381
|
+
});
|
|
382
|
+
res.write(`data: ${errorData}\n\n`);
|
|
383
|
+
}
|
|
384
|
+
// Close the stream
|
|
385
|
+
res.end();
|
|
254
386
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
// Get conversation to extract company_id for knowledge base search
|
|
259
|
-
// Note: profile.companyId and conversation.organizationId should contain integer IDs (as strings)
|
|
260
|
-
const companyIdRaw = req.profile?.companyId || conversation?.organizationId;
|
|
261
|
-
const companyId = companyIdRaw ? (typeof companyIdRaw === 'string' ? parseInt(companyIdRaw, 10) : companyIdRaw) : undefined;
|
|
262
|
-
if (aiService && aiService.knowledgeBaseService) {
|
|
387
|
+
catch (streamError) {
|
|
388
|
+
logger_1.default.error('Streaming error:', streamError);
|
|
389
|
+
// Try to send error to client if connection is still open
|
|
263
390
|
try {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
type: 'semantic', // Modern RAG best practice: semantic-only for better context
|
|
269
|
-
company_id: companyId
|
|
391
|
+
const errorData = JSON.stringify({
|
|
392
|
+
type: 'error',
|
|
393
|
+
error: 'Failed to generate response',
|
|
394
|
+
message: streamError instanceof Error ? streamError.message : 'Unknown error'
|
|
270
395
|
});
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
396
|
+
res.write(`data: ${errorData}\n\n`);
|
|
397
|
+
res.end();
|
|
398
|
+
}
|
|
399
|
+
catch (writeError) {
|
|
400
|
+
logger_1.default.error('Failed to send error to client:', writeError);
|
|
401
|
+
res.end();
|
|
402
|
+
}
|
|
403
|
+
// If we have accumulated content but failed to save, try to save it
|
|
404
|
+
if (accumulatedContent && !assistantMessageId) {
|
|
405
|
+
try {
|
|
406
|
+
await this.saveAssistantMessage({
|
|
407
|
+
conversation,
|
|
408
|
+
conversationId,
|
|
409
|
+
parentMessageId: uuid,
|
|
410
|
+
content: accumulatedContent,
|
|
411
|
+
toolResults: []
|
|
276
412
|
});
|
|
277
|
-
logger_1.default.info('โ
Knowledge context prepared');
|
|
278
413
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
knowledgeResults = '';
|
|
282
|
-
logger_1.default.info('โ ๏ธ No knowledge base results found; will return appropriate fallback response');
|
|
414
|
+
catch (saveError) {
|
|
415
|
+
logger_1.default.error('Failed to save partial message after stream error:', saveError);
|
|
283
416
|
}
|
|
284
417
|
}
|
|
285
|
-
catch (error) {
|
|
286
|
-
console.error('โ Failed to search knowledge base:', error);
|
|
287
|
-
logger_1.default.error('Failed to search knowledge base:', error);
|
|
288
|
-
// On error, set to null so AIService knows search was attempted
|
|
289
|
-
knowledgeResults = null;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
else {
|
|
293
|
-
logger_1.default.warn('โ ๏ธ AI service or knowledge base service not available');
|
|
294
418
|
}
|
|
295
|
-
// Build context for AI
|
|
296
|
-
const chatContext = {
|
|
297
|
-
conversationHistory: messages.map(msg => ({
|
|
298
|
-
role: msg.role,
|
|
299
|
-
content: msg.content
|
|
300
|
-
})),
|
|
301
|
-
knowledgeResults: knowledgeResults ?? undefined // Convert null to undefined for cleaner check
|
|
302
|
-
};
|
|
303
|
-
// Generate AI response using the actual user message content
|
|
304
|
-
const response = await aiService.generateResponse(userMessageContent, chatContext);
|
|
305
|
-
const assistantMessage = await this.saveAssistantMessage({
|
|
306
|
-
conversation,
|
|
307
|
-
conversationId,
|
|
308
|
-
parentMessageId: uuid,
|
|
309
|
-
content: response.content,
|
|
310
|
-
toolResults: response.toolResults
|
|
311
|
-
});
|
|
312
|
-
res.json({
|
|
313
|
-
uuid: assistantMessage.id,
|
|
314
|
-
parent_message_uuid: uuid,
|
|
315
|
-
type: 'assistant',
|
|
316
|
-
content: response.content,
|
|
317
|
-
status: 'completed',
|
|
318
|
-
created_at: assistantMessage.createdAt.toISOString()
|
|
319
|
-
});
|
|
320
419
|
}
|
|
321
420
|
catch (error) {
|
|
322
421
|
logger_1.default.error('Generate response error:', error);
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
422
|
+
// If headers haven't been sent yet, send JSON error
|
|
423
|
+
if (!res.headersSent) {
|
|
424
|
+
res.status(500).json({
|
|
425
|
+
error: 'Failed to generate response',
|
|
426
|
+
message: error instanceof Error ? error.message : 'Unknown error'
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
// Headers already sent, try to send SSE error
|
|
431
|
+
try {
|
|
432
|
+
const errorData = JSON.stringify({
|
|
433
|
+
type: 'error',
|
|
434
|
+
error: 'Failed to generate response',
|
|
435
|
+
message: error instanceof Error ? error.message : 'Unknown error'
|
|
436
|
+
});
|
|
437
|
+
res.write(`data: ${errorData}\n\n`);
|
|
438
|
+
res.end();
|
|
439
|
+
}
|
|
440
|
+
catch (writeError) {
|
|
441
|
+
logger_1.default.error('Failed to send error to client:', writeError);
|
|
442
|
+
res.end();
|
|
443
|
+
}
|
|
444
|
+
}
|
|
327
445
|
}
|
|
328
446
|
}
|
|
329
447
|
// Get conversation details
|
|
@@ -756,7 +874,7 @@ class ChatController {
|
|
|
756
874
|
});
|
|
757
875
|
}
|
|
758
876
|
}
|
|
759
|
-
// Submit message feedback
|
|
877
|
+
// Submit message feedback (create or update) - Public API
|
|
760
878
|
async submitFeedback(req, res) {
|
|
761
879
|
try {
|
|
762
880
|
const { message_uuid, rating, category, comment, suggested_improvement } = req.body;
|
|
@@ -764,25 +882,26 @@ class ChatController {
|
|
|
764
882
|
res.status(400).json({ error: 'message_uuid and rating are required' });
|
|
765
883
|
return;
|
|
766
884
|
}
|
|
767
|
-
if (!req.profile) {
|
|
768
|
-
res.status(401).json({ error: 'Authentication required' });
|
|
769
|
-
return;
|
|
770
|
-
}
|
|
771
885
|
// Get the message to find its conversationId
|
|
772
886
|
const message = await this.storage.getMessageById(message_uuid);
|
|
773
887
|
if (!message) {
|
|
774
888
|
res.status(404).json({ error: 'Message not found' });
|
|
775
889
|
return;
|
|
776
890
|
}
|
|
891
|
+
// Use authenticated user ID if available, otherwise use default anonymous user (1)
|
|
892
|
+
const userId = req.user?.id?.toString() || '1';
|
|
893
|
+
// Check if feedback already exists for this message and user
|
|
894
|
+
const existingFeedback = await this.storage.getFeedbackByMessageAndUser(message_uuid, userId);
|
|
777
895
|
const feedback = await this.storage.saveFeedback({
|
|
896
|
+
id: existingFeedback?.id, // Include ID if exists (will update instead of create)
|
|
778
897
|
messageId: message_uuid,
|
|
779
898
|
conversationId: message.conversationId,
|
|
780
|
-
userId
|
|
899
|
+
userId,
|
|
781
900
|
rating,
|
|
782
901
|
category,
|
|
783
902
|
comment,
|
|
784
903
|
suggestedImprovement: suggested_improvement,
|
|
785
|
-
createdAt: new Date()
|
|
904
|
+
createdAt: existingFeedback?.createdAt || new Date()
|
|
786
905
|
});
|
|
787
906
|
res.json({
|
|
788
907
|
success: true,
|
|
@@ -805,6 +924,36 @@ class ChatController {
|
|
|
805
924
|
});
|
|
806
925
|
}
|
|
807
926
|
}
|
|
927
|
+
// Delete/undo message feedback - Public API
|
|
928
|
+
async deleteFeedback(req, res) {
|
|
929
|
+
try {
|
|
930
|
+
const { uuid } = req.params;
|
|
931
|
+
if (!uuid) {
|
|
932
|
+
res.status(400).json({ error: 'Feedback UUID is required' });
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
// Verify feedback exists
|
|
936
|
+
const feedback = await this.storage.getFeedbackById(uuid);
|
|
937
|
+
if (!feedback) {
|
|
938
|
+
res.status(404).json({ error: 'Feedback not found' });
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
// For public API, allow deletion by UUID only (no user verification)
|
|
942
|
+
// This is acceptable since feedback UUIDs are unique and not easily guessable
|
|
943
|
+
await this.storage.deleteFeedback(uuid);
|
|
944
|
+
res.json({
|
|
945
|
+
success: true,
|
|
946
|
+
message: 'Feedback deleted successfully'
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
catch (error) {
|
|
950
|
+
logger_1.default.error('Delete feedback error:', error);
|
|
951
|
+
res.status(500).json({
|
|
952
|
+
error: 'Failed to delete feedback',
|
|
953
|
+
message: error instanceof Error ? error.message : 'Unknown error'
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
}
|
|
808
957
|
async classifyIntent(message, history) {
|
|
809
958
|
if (!this.intentService) {
|
|
810
959
|
return {
|
|
@@ -820,86 +969,63 @@ class ChatController {
|
|
|
820
969
|
conversationHistory: resolvedHistory
|
|
821
970
|
});
|
|
822
971
|
}
|
|
823
|
-
|
|
972
|
+
/**
|
|
973
|
+
* Handle intent classification result
|
|
974
|
+
* Returns response content if non-knowledge intent, null if knowledge intent
|
|
975
|
+
*/
|
|
976
|
+
async handleIntentResult(result, userMessage, conversationId, conversation) {
|
|
824
977
|
if (result.needsGuardrail && result.intent !== 'guardrail') {
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
conversationId,
|
|
828
|
-
parentMessageId: userMessage.id,
|
|
829
|
-
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.`
|
|
830
|
-
}, res);
|
|
831
|
-
return true;
|
|
978
|
+
logger_1.default.info('๐ก๏ธ Guardrail triggered');
|
|
979
|
+
return `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.`;
|
|
832
980
|
}
|
|
833
981
|
logger_1.default.info(`๐งพ Intent result: ${result.intent}${result.needsGuardrail ? ' (guardrail triggered)' : ''}`);
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
conversationId,
|
|
839
|
-
parentMessageId: userMessage.id,
|
|
840
|
-
content: 'Hello! How can I help you today?'
|
|
841
|
-
}, res);
|
|
842
|
-
return true;
|
|
843
|
-
case 'personality':
|
|
844
|
-
// Get assistant name and organization from environment
|
|
845
|
-
const assistantName = process.env.ASSISTANT_NAME || 'AI Assistant';
|
|
846
|
-
const orgName = process.env.ORGANIZATION_NAME || 'Your Organization';
|
|
847
|
-
await this.respondWithAssistantMessage({
|
|
848
|
-
conversation,
|
|
849
|
-
conversationId,
|
|
850
|
-
parentMessageId: userMessage.id,
|
|
851
|
-
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?`
|
|
852
|
-
}, res);
|
|
853
|
-
return true;
|
|
854
|
-
case 'clarification':
|
|
855
|
-
await this.respondWithAssistantMessage({
|
|
856
|
-
conversation,
|
|
857
|
-
conversationId,
|
|
858
|
-
parentMessageId: userMessage.id,
|
|
859
|
-
content: "I'm not sure I understood. Could you clarify what you need help with?"
|
|
860
|
-
}, res);
|
|
861
|
-
return true;
|
|
862
|
-
case 'guardrail':
|
|
863
|
-
await this.respondWithAssistantMessage({
|
|
864
|
-
conversation,
|
|
865
|
-
conversationId,
|
|
866
|
-
parentMessageId: userMessage.id,
|
|
867
|
-
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.`
|
|
868
|
-
}, res);
|
|
869
|
-
return true;
|
|
870
|
-
case 'human_support_email':
|
|
871
|
-
await this.respondWithAssistantMessage({
|
|
872
|
-
conversation,
|
|
873
|
-
conversationId,
|
|
874
|
-
parentMessageId: userMessage.id,
|
|
875
|
-
content: `Thanks for sharing your email. Our team will reach out soonโresponse times may vary depending on support volume.`
|
|
876
|
-
}, res);
|
|
877
|
-
logger_1.default.info('๐จ Recorded human support email from user.');
|
|
878
|
-
return true;
|
|
879
|
-
case 'human_support_request':
|
|
880
|
-
if (result.contactEmail) {
|
|
881
|
-
await this.respondWithAssistantMessage({
|
|
882
|
-
conversation,
|
|
883
|
-
conversationId,
|
|
884
|
-
parentMessageId: userMessage.id,
|
|
885
|
-
content: `Thanks for sharing your email. Our team will reach out soonโresponse times may vary depending on support volume.`
|
|
886
|
-
}, res);
|
|
887
|
-
logger_1.default.info('๐จ Human support request with email handled in a single step.');
|
|
888
|
-
}
|
|
889
|
-
else {
|
|
890
|
-
await this.respondWithAssistantMessage({
|
|
891
|
-
conversation,
|
|
892
|
-
conversationId,
|
|
893
|
-
parentMessageId: userMessage.id,
|
|
894
|
-
content: 'I can connect you with a human teammate. Please share your email address so we can follow up.'
|
|
895
|
-
}, res);
|
|
896
|
-
logger_1.default.info('๐ Human support requested; awaiting email from user.');
|
|
897
|
-
}
|
|
898
|
-
return true;
|
|
899
|
-
default:
|
|
900
|
-
logger_1.default.info('๐ Intent requires knowledge lookup; proceeding with RAG flow.');
|
|
901
|
-
return false;
|
|
982
|
+
// For non-knowledge intents, return the response content to be streamed
|
|
983
|
+
if (result.intent !== 'knowledge') {
|
|
984
|
+
const responseContent = result.response || this.getFallbackResponse(result.intent);
|
|
985
|
+
return responseContent;
|
|
902
986
|
}
|
|
987
|
+
// Knowledge intent - proceed to RAG flow (return null to indicate streaming will happen later)
|
|
988
|
+
logger_1.default.info('๐ Intent requires knowledge lookup; proceeding with RAG flow.');
|
|
989
|
+
return null;
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Stream text content word by word to simulate streaming
|
|
993
|
+
* This ensures consistent SSE format for all responses
|
|
994
|
+
*/
|
|
995
|
+
async streamTextContent(content, res) {
|
|
996
|
+
const words = content.split(' ');
|
|
997
|
+
const chunkSize = 2; // Stream 2 words at a time for smoother experience
|
|
998
|
+
const totalChunks = Math.ceil(words.length / chunkSize);
|
|
999
|
+
for (let i = 0; i < words.length; i += chunkSize) {
|
|
1000
|
+
const chunk = words.slice(i, i + chunkSize).join(' ') + (i + chunkSize < words.length ? ' ' : '');
|
|
1001
|
+
const chunkIndex = Math.floor(i / chunkSize) + 1;
|
|
1002
|
+
const isLastChunk = chunkIndex === totalChunks;
|
|
1003
|
+
const chunkData = JSON.stringify({
|
|
1004
|
+
type: 'chunk',
|
|
1005
|
+
content: chunk,
|
|
1006
|
+
done: isLastChunk // Mark last chunk with done: true
|
|
1007
|
+
});
|
|
1008
|
+
res.write(`data: ${chunkData}\n\n`);
|
|
1009
|
+
// Flush the response to ensure chunks are sent immediately
|
|
1010
|
+
if (res.flush) {
|
|
1011
|
+
res.flush();
|
|
1012
|
+
}
|
|
1013
|
+
// Delay for smooth streaming effect (30ms for better visibility)
|
|
1014
|
+
await new Promise(resolve => setTimeout(resolve, 30));
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
getFallbackResponse(intent) {
|
|
1018
|
+
// Fallback responses in case LLM doesn't generate one (shouldn't happen, but safety net)
|
|
1019
|
+
const fallbacks = {
|
|
1020
|
+
greeting: 'Hello! How can I help you today?',
|
|
1021
|
+
acknowledgment: "You're welcome! Let me know if you need anything else.",
|
|
1022
|
+
personality: `I'm ${process.env.ASSISTANT_NAME || 'AI Assistant'}, your AI assistant for ${process.env.ORGANIZATION_NAME || 'Your Organization'}.`,
|
|
1023
|
+
clarification: "I'm not sure I understood. Could you clarify what you need help with?",
|
|
1024
|
+
guardrail: "I can help with documentation or implementation guidance, but I can't share credentials or confidential configuration.",
|
|
1025
|
+
human_support_request: "I'd be happy to connect you with our support team. Could you please provide your email address?",
|
|
1026
|
+
human_support_email: "Thank you! Our support team will reach out to you shortly."
|
|
1027
|
+
};
|
|
1028
|
+
return fallbacks[intent] || "I'm here to help. What would you like to know?";
|
|
903
1029
|
}
|
|
904
1030
|
async respondWithAssistantMessage(payload, res) {
|
|
905
1031
|
const assistantMessage = await this.saveAssistantMessage({
|