@vezlo/assistant-server 2.6.0 โ 2.8.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 +5 -1
- package/database-schema.sql +6 -0
- package/dist/src/bootstrap/initializeServices.d.ts +2 -0
- package/dist/src/bootstrap/initializeServices.d.ts.map +1 -1
- package/dist/src/bootstrap/initializeServices.js +4 -1
- package/dist/src/bootstrap/initializeServices.js.map +1 -1
- package/dist/src/controllers/ChatController.d.ts +3 -12
- package/dist/src/controllers/ChatController.d.ts.map +1 -1
- package/dist/src/controllers/ChatController.js +121 -203
- package/dist/src/controllers/ChatController.js.map +1 -1
- package/dist/src/controllers/KnowledgeController.d.ts +8 -1
- package/dist/src/controllers/KnowledgeController.d.ts.map +1 -1
- package/dist/src/controllers/KnowledgeController.js +38 -1
- package/dist/src/controllers/KnowledgeController.js.map +1 -1
- package/dist/src/migrations/009_add_archived_at_column.d.ts +4 -0
- package/dist/src/migrations/009_add_archived_at_column.d.ts.map +1 -0
- package/dist/src/migrations/009_add_archived_at_column.js +17 -0
- package/dist/src/migrations/009_add_archived_at_column.js.map +1 -0
- package/dist/src/schemas/ConversationSchemas.d.ts +10 -0
- package/dist/src/schemas/ConversationSchemas.d.ts.map +1 -1
- package/dist/src/schemas/ConversationSchemas.js +4 -2
- package/dist/src/schemas/ConversationSchemas.js.map +1 -1
- package/dist/src/schemas/index.d.ts +10 -0
- package/dist/src/schemas/index.d.ts.map +1 -1
- package/dist/src/server.js +79 -1
- package/dist/src/server.js.map +1 -1
- package/dist/src/services/CitationService.d.ts +14 -0
- package/dist/src/services/CitationService.d.ts.map +1 -0
- package/dist/src/services/CitationService.js +76 -0
- package/dist/src/services/CitationService.js.map +1 -0
- package/dist/src/services/ResponseGenerationService.d.ts +65 -0
- package/dist/src/services/ResponseGenerationService.d.ts.map +1 -0
- package/dist/src/services/ResponseGenerationService.js +165 -0
- package/dist/src/services/ResponseGenerationService.js.map +1 -0
- package/dist/src/services/ResponseStreamingService.d.ts +33 -0
- package/dist/src/services/ResponseStreamingService.d.ts.map +1 -0
- package/dist/src/services/ResponseStreamingService.js +114 -0
- package/dist/src/services/ResponseStreamingService.js.map +1 -0
- package/dist/src/storage/ConversationRepository.d.ts.map +1 -1
- package/dist/src/storage/ConversationRepository.js +10 -1
- package/dist/src/storage/ConversationRepository.js.map +1 -1
- package/dist/src/storage/SupabaseStorage.d.ts.map +1 -1
- package/dist/src/storage/SupabaseStorage.js +7 -1
- package/dist/src/storage/SupabaseStorage.js.map +1 -1
- package/dist/src/types/index.d.ts +2 -0
- package/dist/src/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
# Vezlo AI Assistant Server
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@vezlo/assistant-server) [](https://opensource.org/licenses/AGPL-3.0)
|
|
4
|
+
|
|
3
5
|
๐ **Production-ready Node.js/TypeScript API server** for the Vezlo AI Assistant platform - Complete backend APIs with advanced RAG (chunk-based semantic search + adjacent retrieval), Docker deployment, and database migrations.
|
|
4
6
|
|
|
7
|
+
**๐ [Changelog](./CHANGELOG.md)** | **๐ [Report Issue](https://github.com/vezlo/assistant-server/issues)** | **๐ฌ [Discussions](https://github.com/vezlo/assistant-server/discussions)**
|
|
8
|
+
|
|
5
9
|
## ๐จ Breaking Change Notice
|
|
6
10
|
|
|
7
11
|
### v2.3.0 - Enhanced RAG System
|
|
@@ -723,4 +727,4 @@ This project is dual-licensed:
|
|
|
723
727
|
|
|
724
728
|
---
|
|
725
729
|
|
|
726
|
-
**Status**: โ
Production Ready | **Version**: 2.
|
|
730
|
+
**Status**: โ
Production Ready | **Version**: 2.8.0 | **Node.js**: 20+ | **TypeScript**: 5+
|
package/database-schema.sql
CHANGED
|
@@ -58,6 +58,10 @@ INSERT INTO knex_migrations (name, batch, migration_time)
|
|
|
58
58
|
SELECT '008_add_conversation_stats_rpc.ts', 1, NOW()
|
|
59
59
|
WHERE NOT EXISTS (SELECT 1 FROM knex_migrations WHERE name = '008_add_conversation_stats_rpc.ts');
|
|
60
60
|
|
|
61
|
+
INSERT INTO knex_migrations (name, batch, migration_time)
|
|
62
|
+
SELECT '009_add_archived_at_column.ts', 1, NOW()
|
|
63
|
+
WHERE NOT EXISTS (SELECT 1 FROM knex_migrations WHERE name = '009_add_archived_at_column.ts');
|
|
64
|
+
|
|
61
65
|
-- Set migration lock to unlocked (0 = unlocked, 1 = locked)
|
|
62
66
|
INSERT INTO knex_migrations_lock (index, is_locked)
|
|
63
67
|
VALUES (1, 0)
|
|
@@ -125,6 +129,7 @@ CREATE TABLE IF NOT EXISTS vezlo_conversations (
|
|
|
125
129
|
joined_at TIMESTAMPTZ,
|
|
126
130
|
responded_at TIMESTAMPTZ,
|
|
127
131
|
closed_at TIMESTAMPTZ,
|
|
132
|
+
archived_at TIMESTAMPTZ,
|
|
128
133
|
last_message_at TIMESTAMPTZ,
|
|
129
134
|
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
|
|
130
135
|
updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
|
|
@@ -284,6 +289,7 @@ CREATE INDEX IF NOT EXISTS idx_vezlo_conversations_updated_at ON vezlo_conversat
|
|
|
284
289
|
CREATE INDEX IF NOT EXISTS idx_vezlo_conversations_last_message_at ON vezlo_conversations(last_message_at DESC);
|
|
285
290
|
CREATE INDEX IF NOT EXISTS idx_vezlo_conversations_joined_at ON vezlo_conversations(joined_at);
|
|
286
291
|
CREATE INDEX IF NOT EXISTS idx_vezlo_conversations_closed_at ON vezlo_conversations(closed_at);
|
|
292
|
+
CREATE INDEX IF NOT EXISTS idx_vezlo_conversations_archived_at ON vezlo_conversations(archived_at);
|
|
287
293
|
|
|
288
294
|
-- Messages indexes
|
|
289
295
|
CREATE INDEX IF NOT EXISTS idx_vezlo_messages_uuid ON vezlo_messages(uuid);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SupabaseClient } from '@supabase/supabase-js';
|
|
2
2
|
import { UnifiedStorage } from '../storage/UnifiedStorage';
|
|
3
3
|
import { KnowledgeBaseService } from '../services/KnowledgeBaseService';
|
|
4
|
+
import { CitationService } from '../services/CitationService';
|
|
4
5
|
import { AIService } from '../services/AIService';
|
|
5
6
|
import { ChatManager } from '../services/ChatManager';
|
|
6
7
|
import { ChatController } from '../controllers/ChatController';
|
|
@@ -25,6 +26,7 @@ export interface InitializedCoreServices {
|
|
|
25
26
|
chatManager: ChatManager;
|
|
26
27
|
apiKeyService: ApiKeyService;
|
|
27
28
|
companyService: CompanyService;
|
|
29
|
+
citationService: CitationService;
|
|
28
30
|
};
|
|
29
31
|
controllers: {
|
|
30
32
|
chatController: ChatController;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"initializeServices.d.ts","sourceRoot":"","sources":["../../../src/bootstrap/initializeServices.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEvD,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AACxE,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,oCAAoC,CAAC;AACzE,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACnE,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AAIrE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,cAAc,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE;QACR,OAAO,EAAE,cAAc,CAAC;QACxB,aAAa,EAAE,oBAAoB,CAAC;QACpC,SAAS,EAAE,SAAS,CAAC;QACrB,WAAW,EAAE,WAAW,CAAC;QACzB,aAAa,EAAE,aAAa,CAAC;QAC7B,cAAc,EAAE,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"initializeServices.d.ts","sourceRoot":"","sources":["../../../src/bootstrap/initializeServices.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEvD,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AACxE,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,oCAAoC,CAAC;AACzE,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACnE,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AAIrE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,cAAc,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE;QACR,OAAO,EAAE,cAAc,CAAC;QACxB,aAAa,EAAE,oBAAoB,CAAC;QACpC,SAAS,EAAE,SAAS,CAAC;QACrB,WAAW,EAAE,WAAW,CAAC;QACzB,aAAa,EAAE,aAAa,CAAC;QAC7B,cAAc,EAAE,cAAc,CAAC;QAC/B,eAAe,EAAE,eAAe,CAAC;KAClC,CAAC;IACF,WAAW,EAAE;QACX,cAAc,EAAE,cAAc,CAAC;QAC/B,mBAAmB,EAAE,mBAAmB,CAAC;QACzC,cAAc,EAAE,cAAc,CAAC;QAC/B,gBAAgB,EAAE,gBAAgB,CAAC;QACnC,iBAAiB,EAAE,iBAAiB,CAAC;KACtC,CAAC;IACF,MAAM,EAAE;QACN,iBAAiB,EAAE,MAAM,CAAC;KAC3B,CAAC;CACH;AAKD,wBAAgB,oBAAoB,IAAI,MAAM,CAa7C;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,kBAAkB,GAAG,uBAAuB,CAiH3F"}
|
|
@@ -8,6 +8,7 @@ exports.initializeCoreServices = initializeCoreServices;
|
|
|
8
8
|
const logger_1 = __importDefault(require("../config/logger"));
|
|
9
9
|
const UnifiedStorage_1 = require("../storage/UnifiedStorage");
|
|
10
10
|
const KnowledgeBaseService_1 = require("../services/KnowledgeBaseService");
|
|
11
|
+
const CitationService_1 = require("../services/CitationService");
|
|
11
12
|
const AIService_1 = require("../services/AIService");
|
|
12
13
|
const ChatManager_1 = require("../services/ChatManager");
|
|
13
14
|
const ChatController_1 = require("../controllers/ChatController");
|
|
@@ -44,6 +45,7 @@ function initializeCoreServices(options) {
|
|
|
44
45
|
supabase,
|
|
45
46
|
tableName: knowledgeTableName
|
|
46
47
|
});
|
|
48
|
+
const citationService = new CitationService_1.CitationService(supabase, tablePrefix);
|
|
47
49
|
// Initialize V2 service for adjacent chunk retrieval
|
|
48
50
|
// Read AI configuration from environment
|
|
49
51
|
// Use AI_MODEL for all OpenAI calls (intent classification + response generation)
|
|
@@ -94,7 +96,7 @@ function initializeCoreServices(options) {
|
|
|
94
96
|
intentService,
|
|
95
97
|
realtimePublisher
|
|
96
98
|
});
|
|
97
|
-
const knowledgeController = new KnowledgeController_1.KnowledgeController(knowledgeBase, aiService);
|
|
99
|
+
const knowledgeController = new KnowledgeController_1.KnowledgeController(knowledgeBase, aiService, citationService);
|
|
98
100
|
const authController = new AuthController_1.AuthController(supabase);
|
|
99
101
|
const apiKeyService = new ApiKeyService_1.ApiKeyService(supabase);
|
|
100
102
|
const apiKeyController = new ApiKeyController_1.ApiKeyController(apiKeyService);
|
|
@@ -104,6 +106,7 @@ function initializeCoreServices(options) {
|
|
|
104
106
|
services: {
|
|
105
107
|
storage,
|
|
106
108
|
knowledgeBase,
|
|
109
|
+
citationService,
|
|
107
110
|
aiService,
|
|
108
111
|
chatManager,
|
|
109
112
|
apiKeyService,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"initializeServices.js","sourceRoot":"","sources":["../../../src/bootstrap/initializeServices.ts"],"names":[],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"initializeServices.js","sourceRoot":"","sources":["../../../src/bootstrap/initializeServices.ts"],"names":[],"mappings":"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"}
|
|
@@ -12,6 +12,8 @@ export declare class ChatController {
|
|
|
12
12
|
private chatHistoryLength;
|
|
13
13
|
private intentService?;
|
|
14
14
|
private realtimePublisher?;
|
|
15
|
+
private responseGenerationService;
|
|
16
|
+
private responseStreamingService;
|
|
15
17
|
constructor(chatManager: ChatManager, storage: UnifiedStorage, supabase: SupabaseClient, options?: {
|
|
16
18
|
historyLength?: number;
|
|
17
19
|
intentService?: IntentService;
|
|
@@ -24,23 +26,12 @@ export declare class ChatController {
|
|
|
24
26
|
getConversationMessages(req: AuthenticatedRequest, res: Response): Promise<void>;
|
|
25
27
|
joinConversation(req: AuthenticatedRequest, res: Response): Promise<void>;
|
|
26
28
|
closeConversation(req: AuthenticatedRequest, res: Response): Promise<void>;
|
|
29
|
+
archiveConversation(req: AuthenticatedRequest, res: Response): Promise<void>;
|
|
27
30
|
sendAgentMessage(req: AuthenticatedRequest, res: Response): Promise<void>;
|
|
28
31
|
getUserConversations(req: AuthenticatedRequest, res: Response): Promise<void>;
|
|
29
32
|
deleteConversation(req: Request, res: Response): Promise<void>;
|
|
30
33
|
submitFeedback(req: Request | AuthenticatedRequest, res: Response): Promise<void>;
|
|
31
34
|
deleteFeedback(req: Request | AuthenticatedRequest, res: Response): Promise<void>;
|
|
32
|
-
private classifyIntent;
|
|
33
|
-
/**
|
|
34
|
-
* Handle intent classification result
|
|
35
|
-
* Returns response content if non-knowledge intent, null if knowledge intent
|
|
36
|
-
*/
|
|
37
|
-
private handleIntentResult;
|
|
38
|
-
/**
|
|
39
|
-
* Stream text content word by word to simulate streaming
|
|
40
|
-
* This ensures consistent SSE format for all responses
|
|
41
|
-
*/
|
|
42
|
-
private streamTextContent;
|
|
43
|
-
private getFallbackResponse;
|
|
44
35
|
private respondWithAssistantMessage;
|
|
45
36
|
private saveAssistantMessage;
|
|
46
37
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatController.d.ts","sourceRoot":"","sources":["../../../src/controllers/ChatController.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAE1D,OAAO,EAAE,aAAa,
|
|
1
|
+
{"version":3,"file":"ChatController.d.ts","sourceRoot":"","sources":["../../../src/controllers/ChatController.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAE1D,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAE1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAIlE,qBAAa,cAAc;IACzB,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,OAAO,CAAiB;IAChC,OAAO,CAAC,QAAQ,CAAiB;IACjC,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,aAAa,CAAC,CAAgB;IACtC,OAAO,CAAC,iBAAiB,CAAC,CAAoB;IAC9C,OAAO,CAAC,yBAAyB,CAA4B;IAC7D,OAAO,CAAC,wBAAwB,CAA2B;gBAGzD,WAAW,EAAE,WAAW,EACxB,OAAO,EAAE,cAAc,EACvB,QAAQ,EAAE,cAAc,EACxB,OAAO,GAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,aAAa,CAAC;QAAC,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;KAAO;IAqB1G,kBAAkB,CAAC,GAAG,EAAE,oBAAoB,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IA4I3E,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAyF7D,gBAAgB,CAAC,GAAG,EAAE,oBAAoB,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAgLzE,eAAe,CAAC,GAAG,EAAE,oBAAoB,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAuDxE,uBAAuB,CAAC,GAAG,EAAE,oBAAoB,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IA4DhF,gBAAgB,CAAC,GAAG,EAAE,oBAAoB,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAuGzE,iBAAiB,CAAC,GAAG,EAAE,oBAAoB,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAoG1E,mBAAmB,CAAC,GAAG,EAAE,oBAAoB,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IA6E5E,gBAAgB,CAAC,GAAG,EAAE,oBAAoB,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAmGzE,oBAAoB,CAAC,GAAG,EAAE,oBAAoB,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAoE7E,kBAAkB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAsB9D,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,oBAAoB,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAgEjF,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,oBAAoB,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;YAmCzE,2BAA2B;YA4B3B,oBAAoB;CA4EnC"}
|
|
@@ -5,6 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.ChatController = void 0;
|
|
7
7
|
const logger_1 = __importDefault(require("../config/logger"));
|
|
8
|
+
const ResponseGenerationService_1 = require("../services/ResponseGenerationService");
|
|
9
|
+
const ResponseStreamingService_1 = require("../services/ResponseStreamingService");
|
|
8
10
|
class ChatController {
|
|
9
11
|
constructor(chatManager, storage, supabase, options = {}) {
|
|
10
12
|
this.chatManager = chatManager;
|
|
@@ -14,6 +16,10 @@ class ChatController {
|
|
|
14
16
|
this.chatHistoryLength = typeof historyLength === 'number' && historyLength > 0 ? historyLength : 2;
|
|
15
17
|
this.intentService = options.intentService;
|
|
16
18
|
this.realtimePublisher = options.realtimePublisher;
|
|
19
|
+
// Initialize services
|
|
20
|
+
const aiService = chatManager.aiService;
|
|
21
|
+
this.responseGenerationService = new ResponseGenerationService_1.ResponseGenerationService(this.intentService, aiService, this.chatHistoryLength);
|
|
22
|
+
this.responseStreamingService = new ResponseStreamingService_1.ResponseStreamingService();
|
|
17
23
|
}
|
|
18
24
|
// Create a new conversation
|
|
19
25
|
async createConversation(req, res) {
|
|
@@ -222,10 +228,16 @@ class ChatController {
|
|
|
222
228
|
}
|
|
223
229
|
// Generate AI response for a user message
|
|
224
230
|
async generateResponse(req, res) {
|
|
231
|
+
const { uuid } = req.params;
|
|
232
|
+
if (!uuid) {
|
|
233
|
+
res.status(400).json({ error: 'Message UUID is required' });
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
// After validation, uuid is guaranteed to be a string - use type assertion
|
|
237
|
+
const messageUuid = uuid;
|
|
225
238
|
try {
|
|
226
|
-
const { uuid } = req.params;
|
|
227
239
|
// Get the user message by ID using the repository
|
|
228
|
-
const userMessage = await this.storage.getMessageById(
|
|
240
|
+
const userMessage = await this.storage.getMessageById(messageUuid);
|
|
229
241
|
if (!userMessage) {
|
|
230
242
|
res.status(404).json({ error: 'Message not found' });
|
|
231
243
|
return;
|
|
@@ -235,7 +247,7 @@ class ChatController {
|
|
|
235
247
|
// Get conversation context (recent messages)
|
|
236
248
|
// Exclude the current user message to avoid duplication (it's added separately as the query)
|
|
237
249
|
const allMessages = await this.chatManager.getRecentMessages(conversationId, this.chatHistoryLength + 1);
|
|
238
|
-
const messages = allMessages.filter(msg => msg.id !==
|
|
250
|
+
const messages = allMessages.filter(msg => msg.id !== messageUuid).slice(-this.chatHistoryLength);
|
|
239
251
|
logger_1.default.info(`๐ Retrieved ${messages.length} message(s) from conversation history (limit: ${this.chatHistoryLength})`);
|
|
240
252
|
const conversation = await this.storage.getConversation(conversationId);
|
|
241
253
|
// Check if conversation has been joined by an agent
|
|
@@ -246,140 +258,57 @@ class ChatController {
|
|
|
246
258
|
});
|
|
247
259
|
return;
|
|
248
260
|
}
|
|
249
|
-
// Set up Server-Sent Events (SSE) headers for streaming
|
|
250
|
-
|
|
251
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
252
|
-
res.setHeader('Connection', 'keep-alive');
|
|
253
|
-
res.setHeader('X-Accel-Buffering', 'no'); // Disable nginx buffering
|
|
261
|
+
// Set up Server-Sent Events (SSE) headers for streaming
|
|
262
|
+
this.responseStreamingService.setupSSEHeaders(res);
|
|
254
263
|
// Run intent classification to decide handling strategy
|
|
255
|
-
const intentResult = await this.classifyIntent(userMessageContent, messages);
|
|
256
|
-
const intentResponse =
|
|
264
|
+
const intentResult = await this.responseGenerationService.classifyIntent(userMessageContent, messages);
|
|
265
|
+
const intentResponse = this.responseGenerationService.handleIntentResult(intentResult, userMessageContent);
|
|
257
266
|
let accumulatedContent = '';
|
|
258
267
|
let assistantMessageId;
|
|
268
|
+
let sources = [];
|
|
259
269
|
try {
|
|
260
270
|
// If intent returned a response (non-knowledge intent), stream it
|
|
261
271
|
if (intentResponse) {
|
|
262
272
|
logger_1.default.info(`๐ค Streaming intent response for: ${intentResult.intent}`);
|
|
263
|
-
await this.streamTextContent(intentResponse, res);
|
|
273
|
+
await this.responseStreamingService.streamTextContent(intentResponse, res);
|
|
264
274
|
accumulatedContent = intentResponse;
|
|
265
275
|
}
|
|
266
276
|
else {
|
|
267
277
|
// Knowledge intent - proceed with RAG flow and stream AI response
|
|
268
278
|
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
279
|
// Get conversation to extract company_id for knowledge base search
|
|
273
280
|
const companyIdRaw = req.profile?.companyId || conversation?.organizationId;
|
|
274
281
|
const companyId = companyIdRaw ? (typeof companyIdRaw === 'string' ? parseInt(companyIdRaw, 10) : companyIdRaw) : undefined;
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
}
|
|
282
|
+
// Search knowledge base and extract sources
|
|
283
|
+
const { knowledgeResults, sources: extractedSources } = await this.responseGenerationService.searchKnowledgeBase(userMessageContent, companyId);
|
|
284
|
+
sources = extractedSources;
|
|
318
285
|
// Build context for AI
|
|
319
|
-
const chatContext =
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
knowledgeResults: knowledgeResults ?? undefined
|
|
325
|
-
};
|
|
286
|
+
const chatContext = this.responseGenerationService.buildChatContext(messages, knowledgeResults);
|
|
287
|
+
const aiService = this.responseGenerationService.getAIService();
|
|
288
|
+
if (!aiService) {
|
|
289
|
+
throw new Error('AI service not available');
|
|
290
|
+
}
|
|
326
291
|
// Stream response from OpenAI
|
|
327
|
-
logger_1.default.info('๐ Starting OpenAI stream...');
|
|
328
292
|
const stream = aiService.generateResponseStream(userMessageContent, chatContext);
|
|
329
|
-
|
|
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
|
-
}
|
|
293
|
+
accumulatedContent = await this.responseStreamingService.streamAIResponse(stream, res, sources, knowledgeResults);
|
|
354
294
|
}
|
|
355
295
|
// Save the message after streaming completes
|
|
356
296
|
try {
|
|
357
297
|
const assistantMessage = await this.saveAssistantMessage({
|
|
358
298
|
conversation,
|
|
359
299
|
conversationId,
|
|
360
|
-
parentMessageId:
|
|
300
|
+
parentMessageId: messageUuid,
|
|
361
301
|
content: accumulatedContent,
|
|
362
302
|
toolResults: []
|
|
363
303
|
});
|
|
364
304
|
assistantMessageId = assistantMessage.id;
|
|
365
|
-
// Send completion event with final message metadata
|
|
366
|
-
|
|
367
|
-
|
|
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`);
|
|
305
|
+
// Send completion event with final message metadata
|
|
306
|
+
// Use non-null assertion since both are guaranteed to exist at this point
|
|
307
|
+
this.responseStreamingService.sendCompletionEvent(res, assistantMessage.id, messageUuid, assistantMessage.createdAt);
|
|
374
308
|
}
|
|
375
309
|
catch (saveError) {
|
|
376
310
|
logger_1.default.error('Failed to save assistant message:', saveError);
|
|
377
|
-
|
|
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`);
|
|
311
|
+
this.responseStreamingService.sendErrorEvent(res, 'Failed to save message', saveError instanceof Error ? saveError.message : 'Unknown error');
|
|
383
312
|
}
|
|
384
313
|
// Close the stream
|
|
385
314
|
res.end();
|
|
@@ -388,12 +317,7 @@ class ChatController {
|
|
|
388
317
|
logger_1.default.error('Streaming error:', streamError);
|
|
389
318
|
// Try to send error to client if connection is still open
|
|
390
319
|
try {
|
|
391
|
-
|
|
392
|
-
type: 'error',
|
|
393
|
-
error: 'Failed to generate response',
|
|
394
|
-
message: streamError instanceof Error ? streamError.message : 'Unknown error'
|
|
395
|
-
});
|
|
396
|
-
res.write(`data: ${errorData}\n\n`);
|
|
320
|
+
this.responseStreamingService.sendErrorEvent(res, 'Failed to generate response', streamError instanceof Error ? streamError.message : 'Unknown error');
|
|
397
321
|
res.end();
|
|
398
322
|
}
|
|
399
323
|
catch (writeError) {
|
|
@@ -406,7 +330,7 @@ class ChatController {
|
|
|
406
330
|
await this.saveAssistantMessage({
|
|
407
331
|
conversation,
|
|
408
332
|
conversationId,
|
|
409
|
-
parentMessageId:
|
|
333
|
+
parentMessageId: messageUuid,
|
|
410
334
|
content: accumulatedContent,
|
|
411
335
|
toolResults: []
|
|
412
336
|
});
|
|
@@ -429,12 +353,7 @@ class ChatController {
|
|
|
429
353
|
else {
|
|
430
354
|
// Headers already sent, try to send SSE error
|
|
431
355
|
try {
|
|
432
|
-
|
|
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`);
|
|
356
|
+
this.responseStreamingService.sendErrorEvent(res, 'Failed to generate response', error instanceof Error ? error.message : 'Unknown error');
|
|
438
357
|
res.end();
|
|
439
358
|
}
|
|
440
359
|
catch (writeError) {
|
|
@@ -462,11 +381,13 @@ class ChatController {
|
|
|
462
381
|
return;
|
|
463
382
|
}
|
|
464
383
|
const toIso = (date) => (date ? date.toISOString() : null);
|
|
465
|
-
const status = conversation.
|
|
466
|
-
? '
|
|
467
|
-
: conversation.
|
|
468
|
-
? '
|
|
469
|
-
:
|
|
384
|
+
const status = conversation.archivedAt
|
|
385
|
+
? 'archived'
|
|
386
|
+
: conversation.closedAt
|
|
387
|
+
? 'closed'
|
|
388
|
+
: conversation.joinedAt
|
|
389
|
+
? 'in_progress'
|
|
390
|
+
: 'open';
|
|
470
391
|
res.json({
|
|
471
392
|
uuid: conversation.id,
|
|
472
393
|
title: conversation.title,
|
|
@@ -478,6 +399,7 @@ class ChatController {
|
|
|
478
399
|
joined_at: toIso(conversation.joinedAt),
|
|
479
400
|
responded_at: toIso(conversation.respondedAt),
|
|
480
401
|
closed_at: toIso(conversation.closedAt),
|
|
402
|
+
archived_at: toIso(conversation.archivedAt),
|
|
481
403
|
last_message_at: toIso(conversation.lastMessageAt),
|
|
482
404
|
status
|
|
483
405
|
});
|
|
@@ -718,6 +640,69 @@ class ChatController {
|
|
|
718
640
|
});
|
|
719
641
|
}
|
|
720
642
|
}
|
|
643
|
+
// Archive conversation
|
|
644
|
+
async archiveConversation(req, res) {
|
|
645
|
+
try {
|
|
646
|
+
if (!req.user || !req.profile) {
|
|
647
|
+
res.status(401).json({ error: 'Authentication required' });
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
const { uuid } = req.params;
|
|
651
|
+
const conversation = await this.storage.getConversation(uuid);
|
|
652
|
+
if (!conversation) {
|
|
653
|
+
res.status(404).json({ error: 'Conversation not found' });
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
if (conversation.organizationId !== req.profile.companyId) {
|
|
657
|
+
res.status(404).json({ error: 'Conversation not found' });
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
if (!conversation.closedAt) {
|
|
661
|
+
res.status(400).json({ error: 'Conversation must be closed before archiving' });
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
if (conversation.archivedAt) {
|
|
665
|
+
res.status(400).json({ error: 'Conversation is already archived' });
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
const archivedAt = new Date();
|
|
669
|
+
await this.storage.updateConversation(uuid, {
|
|
670
|
+
archivedAt
|
|
671
|
+
});
|
|
672
|
+
if (this.realtimePublisher) {
|
|
673
|
+
try {
|
|
674
|
+
const { data: company } = await this.supabase
|
|
675
|
+
.from('vezlo_companies')
|
|
676
|
+
.select('uuid')
|
|
677
|
+
.eq('id', conversation.organizationId)
|
|
678
|
+
.single();
|
|
679
|
+
if (company?.uuid) {
|
|
680
|
+
await this.realtimePublisher.publish(`company:${company.uuid}:conversations`, 'conversation:archived', {
|
|
681
|
+
conversation_uuid: uuid,
|
|
682
|
+
conversation_update: {
|
|
683
|
+
archived_at: archivedAt.toISOString(),
|
|
684
|
+
status: 'archived'
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
catch (error) {
|
|
690
|
+
logger_1.default.error('[ChatController] Failed to publish archive conversation update:', error);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
res.json({
|
|
694
|
+
success: true,
|
|
695
|
+
archived_at: archivedAt.toISOString()
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
catch (error) {
|
|
699
|
+
logger_1.default.error('Archive conversation error:', error);
|
|
700
|
+
res.status(500).json({
|
|
701
|
+
error: 'Failed to archive conversation',
|
|
702
|
+
message: error instanceof Error ? error.message : 'Unknown error'
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
}
|
|
721
706
|
// Send agent message
|
|
722
707
|
async sendAgentMessage(req, res) {
|
|
723
708
|
try {
|
|
@@ -815,10 +800,13 @@ class ChatController {
|
|
|
815
800
|
const offset = (page - 1) * pageSize;
|
|
816
801
|
const orderParam = req.query.order_by || 'last_message_at';
|
|
817
802
|
const orderBy = orderParam === 'created_at' ? 'updated_at' : 'last_message_at';
|
|
803
|
+
const statusParam = req.query.status;
|
|
804
|
+
const status = statusParam === 'archived' ? 'archived' : statusParam === 'active' ? 'active' : undefined;
|
|
818
805
|
const { conversations, total } = await this.storage.getUserConversations(req.user.id, req.profile.companyId, {
|
|
819
806
|
limit: pageSize,
|
|
820
807
|
offset,
|
|
821
|
-
orderBy: orderBy
|
|
808
|
+
orderBy: orderBy,
|
|
809
|
+
status
|
|
822
810
|
});
|
|
823
811
|
const toIso = (date) => (date ? date.toISOString() : null);
|
|
824
812
|
res.json({
|
|
@@ -831,12 +819,15 @@ class ChatController {
|
|
|
831
819
|
joined_at: toIso(conversation.joinedAt),
|
|
832
820
|
responded_at: toIso(conversation.respondedAt),
|
|
833
821
|
closed_at: toIso(conversation.closedAt),
|
|
822
|
+
archived_at: toIso(conversation.archivedAt),
|
|
834
823
|
last_message_at: toIso(conversation.lastMessageAt),
|
|
835
|
-
status: conversation.
|
|
836
|
-
? '
|
|
837
|
-
: conversation.
|
|
838
|
-
? '
|
|
839
|
-
:
|
|
824
|
+
status: conversation.archivedAt
|
|
825
|
+
? 'archived'
|
|
826
|
+
: conversation.closedAt
|
|
827
|
+
? 'closed'
|
|
828
|
+
: conversation.joinedAt
|
|
829
|
+
? 'in_progress'
|
|
830
|
+
: 'open'
|
|
840
831
|
})),
|
|
841
832
|
pagination: {
|
|
842
833
|
page,
|
|
@@ -960,79 +951,6 @@ class ChatController {
|
|
|
960
951
|
});
|
|
961
952
|
}
|
|
962
953
|
}
|
|
963
|
-
async classifyIntent(message, history) {
|
|
964
|
-
if (!this.intentService) {
|
|
965
|
-
return {
|
|
966
|
-
intent: 'knowledge',
|
|
967
|
-
needsGuardrail: false,
|
|
968
|
-
contactEmail: null
|
|
969
|
-
};
|
|
970
|
-
}
|
|
971
|
-
const resolvedHistory = Array.isArray(history) ? history : [];
|
|
972
|
-
logger_1.default.info('๐งญ Classifying user intent...');
|
|
973
|
-
return this.intentService.classify({
|
|
974
|
-
message,
|
|
975
|
-
conversationHistory: resolvedHistory
|
|
976
|
-
});
|
|
977
|
-
}
|
|
978
|
-
/**
|
|
979
|
-
* Handle intent classification result
|
|
980
|
-
* Returns response content if non-knowledge intent, null if knowledge intent
|
|
981
|
-
*/
|
|
982
|
-
async handleIntentResult(result, userMessage, conversationId, conversation) {
|
|
983
|
-
if (result.needsGuardrail && result.intent !== 'guardrail') {
|
|
984
|
-
logger_1.default.info('๐ก๏ธ Guardrail triggered');
|
|
985
|
-
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.`;
|
|
986
|
-
}
|
|
987
|
-
logger_1.default.info(`๐งพ Intent result: ${result.intent}${result.needsGuardrail ? ' (guardrail triggered)' : ''}`);
|
|
988
|
-
// For non-knowledge intents, return the response content to be streamed
|
|
989
|
-
if (result.intent !== 'knowledge') {
|
|
990
|
-
const responseContent = result.response || this.getFallbackResponse(result.intent);
|
|
991
|
-
return responseContent;
|
|
992
|
-
}
|
|
993
|
-
// Knowledge intent - proceed to RAG flow (return null to indicate streaming will happen later)
|
|
994
|
-
logger_1.default.info('๐ Intent requires knowledge lookup; proceeding with RAG flow.');
|
|
995
|
-
return null;
|
|
996
|
-
}
|
|
997
|
-
/**
|
|
998
|
-
* Stream text content word by word to simulate streaming
|
|
999
|
-
* This ensures consistent SSE format for all responses
|
|
1000
|
-
*/
|
|
1001
|
-
async streamTextContent(content, res) {
|
|
1002
|
-
const words = content.split(' ');
|
|
1003
|
-
const chunkSize = 2; // Stream 2 words at a time for smoother experience
|
|
1004
|
-
const totalChunks = Math.ceil(words.length / chunkSize);
|
|
1005
|
-
for (let i = 0; i < words.length; i += chunkSize) {
|
|
1006
|
-
const chunk = words.slice(i, i + chunkSize).join(' ') + (i + chunkSize < words.length ? ' ' : '');
|
|
1007
|
-
const chunkIndex = Math.floor(i / chunkSize) + 1;
|
|
1008
|
-
const isLastChunk = chunkIndex === totalChunks;
|
|
1009
|
-
const chunkData = JSON.stringify({
|
|
1010
|
-
type: 'chunk',
|
|
1011
|
-
content: chunk,
|
|
1012
|
-
done: isLastChunk // Mark last chunk with done: true
|
|
1013
|
-
});
|
|
1014
|
-
res.write(`data: ${chunkData}\n\n`);
|
|
1015
|
-
// Flush the response to ensure chunks are sent immediately
|
|
1016
|
-
if (res.flush) {
|
|
1017
|
-
res.flush();
|
|
1018
|
-
}
|
|
1019
|
-
// Delay for smooth streaming effect (30ms for better visibility)
|
|
1020
|
-
await new Promise(resolve => setTimeout(resolve, 30));
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
getFallbackResponse(intent) {
|
|
1024
|
-
// Fallback responses in case LLM doesn't generate one (shouldn't happen, but safety net)
|
|
1025
|
-
const fallbacks = {
|
|
1026
|
-
greeting: 'Hello! How can I help you today?',
|
|
1027
|
-
acknowledgment: "You're welcome! Let me know if you need anything else.",
|
|
1028
|
-
personality: `I'm ${process.env.ASSISTANT_NAME || 'AI Assistant'}, your AI assistant for ${process.env.ORGANIZATION_NAME || 'Your Organization'}.`,
|
|
1029
|
-
clarification: "I'm not sure I understood. Could you clarify what you need help with?",
|
|
1030
|
-
guardrail: "I can help with documentation or implementation guidance, but I can't share credentials or confidential configuration.",
|
|
1031
|
-
human_support_request: "I'd be happy to connect you with our support team. Could you please provide your email address?",
|
|
1032
|
-
human_support_email: "Thank you! Our support team will reach out to you shortly."
|
|
1033
|
-
};
|
|
1034
|
-
return fallbacks[intent] || "I'm here to help. What would you like to know?";
|
|
1035
|
-
}
|
|
1036
954
|
async respondWithAssistantMessage(payload, res) {
|
|
1037
955
|
const assistantMessage = await this.saveAssistantMessage({
|
|
1038
956
|
conversation: payload.conversation,
|