@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.
Files changed (46) hide show
  1. package/README.md +21 -15
  2. package/database-schema.sql +178 -8
  3. package/dist/src/bootstrap/initializeServices.d.ts.map +1 -1
  4. package/dist/src/bootstrap/initializeServices.js +2 -0
  5. package/dist/src/bootstrap/initializeServices.js.map +1 -1
  6. package/dist/src/config/global.js +1 -1
  7. package/dist/src/config/global.js.map +1 -1
  8. package/dist/src/controllers/ChatController.d.ts +12 -1
  9. package/dist/src/controllers/ChatController.d.ts.map +1 -1
  10. package/dist/src/controllers/ChatController.js +274 -148
  11. package/dist/src/controllers/ChatController.js.map +1 -1
  12. package/dist/src/controllers/KnowledgeController.d.ts.map +1 -1
  13. package/dist/src/controllers/KnowledgeController.js +0 -4
  14. package/dist/src/controllers/KnowledgeController.js.map +1 -1
  15. package/dist/src/migrations/006_add_knowledge_chunks.d.ts +4 -0
  16. package/dist/src/migrations/006_add_knowledge_chunks.d.ts.map +1 -0
  17. package/dist/src/migrations/006_add_knowledge_chunks.js +245 -0
  18. package/dist/src/migrations/006_add_knowledge_chunks.js.map +1 -0
  19. package/dist/src/migrations/007_add_updated_at_to_feedback.d.ts +4 -0
  20. package/dist/src/migrations/007_add_updated_at_to_feedback.d.ts.map +1 -0
  21. package/dist/src/migrations/007_add_updated_at_to_feedback.js +22 -0
  22. package/dist/src/migrations/007_add_updated_at_to_feedback.js.map +1 -0
  23. package/dist/src/server.js +42 -8
  24. package/dist/src/server.js.map +1 -1
  25. package/dist/src/services/AIService.d.ts +9 -0
  26. package/dist/src/services/AIService.d.ts.map +1 -1
  27. package/dist/src/services/AIService.js +111 -3
  28. package/dist/src/services/AIService.js.map +1 -1
  29. package/dist/src/services/IntentService.d.ts +2 -1
  30. package/dist/src/services/IntentService.d.ts.map +1 -1
  31. package/dist/src/services/IntentService.js +23 -4
  32. package/dist/src/services/IntentService.js.map +1 -1
  33. package/dist/src/services/KnowledgeBaseService.d.ts +20 -5
  34. package/dist/src/services/KnowledgeBaseService.d.ts.map +1 -1
  35. package/dist/src/services/KnowledgeBaseService.js +203 -137
  36. package/dist/src/services/KnowledgeBaseService.js.map +1 -1
  37. package/dist/src/storage/FeedbackRepository.d.ts +1 -0
  38. package/dist/src/storage/FeedbackRepository.d.ts.map +1 -1
  39. package/dist/src/storage/FeedbackRepository.js +26 -0
  40. package/dist/src/storage/FeedbackRepository.js.map +1 -1
  41. package/dist/src/storage/UnifiedStorage.d.ts +1 -0
  42. package/dist/src/storage/UnifiedStorage.d.ts.map +1 -1
  43. package/dist/src/storage/UnifiedStorage.js +3 -0
  44. package/dist/src/storage/UnifiedStorage.js.map +1 -1
  45. package/package.json +2 -2
  46. 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 handled = await this.handleIntentResult(intentResult, userMessage, conversationId, conversation, res);
252
- if (handled) {
253
- return;
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
- // Get knowledge base search results if available
256
- const aiService = this.chatManager.aiService;
257
- let knowledgeResults = null; // null = search done, no results; undefined = not searched yet
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
- logger_1.default.info(`๐Ÿ” Searching KB: query="${userMessageContent.substring(0, 50)}...", companyId=${companyId}`);
265
- const searchResults = await aiService.knowledgeBaseService.search(userMessageContent, {
266
- limit: 5,
267
- threshold: 0.5, // Balanced precision/recall (0.5 is industry standard)
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
- logger_1.default.info(`๐Ÿ“Š Found knowledge base results: ${searchResults.length}`);
272
- if (searchResults.length > 0) {
273
- knowledgeResults = '\n\nRelevant information from knowledge base:\n';
274
- searchResults.forEach((result) => {
275
- knowledgeResults += `- ${result.title}: ${result.content}\n`;
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
- else {
280
- // Set to empty string to indicate search was done but no results found
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
- res.status(500).json({
324
- error: 'Failed to generate response',
325
- message: error instanceof Error ? error.message : 'Unknown error'
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: req.user.id,
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
- async handleIntentResult(result, userMessage, conversationId, conversation, res) {
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
- await this.respondWithAssistantMessage({
826
- conversation,
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
- switch (result.intent) {
835
- case 'greeting':
836
- await this.respondWithAssistantMessage({
837
- conversation,
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({