@vezlo/assistant-server 2.7.0 → 2.9.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 +2 -1
- package/database-schema.sql +7 -0
- package/dist/src/bootstrap/initializeServices.d.ts +6 -0
- package/dist/src/bootstrap/initializeServices.d.ts.map +1 -1
- package/dist/src/bootstrap/initializeServices.js +13 -3
- package/dist/src/bootstrap/initializeServices.js.map +1 -1
- package/dist/src/controllers/ChatController.d.ts +2 -12
- package/dist/src/controllers/ChatController.d.ts.map +1 -1
- package/dist/src/controllers/ChatController.js +38 -192
- 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/controllers/SlackController.d.ts +36 -0
- package/dist/src/controllers/SlackController.d.ts.map +1 -0
- package/dist/src/controllers/SlackController.js +200 -0
- package/dist/src/controllers/SlackController.js.map +1 -0
- package/dist/src/migrations/010_add_slack_fields.d.ts +4 -0
- package/dist/src/migrations/010_add_slack_fields.d.ts.map +1 -0
- package/dist/src/migrations/010_add_slack_fields.js +27 -0
- package/dist/src/migrations/010_add_slack_fields.js.map +1 -0
- package/dist/src/server.js +85 -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/services/SlackService.d.ts +16 -0
- package/dist/src/services/SlackService.d.ts.map +1 -0
- package/dist/src/services/SlackService.js +133 -0
- package/dist/src/services/SlackService.js.map +1 -0
- package/dist/src/storage/ConversationRepository.d.ts +1 -0
- package/dist/src/storage/ConversationRepository.d.ts.map +1 -1
- package/dist/src/storage/ConversationRepository.js +39 -9
- package/dist/src/storage/ConversationRepository.js.map +1 -1
- package/dist/src/types/index.d.ts +2 -0
- package/dist/src/types/index.d.ts.map +1 -1
- package/env.example +5 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -36,6 +36,7 @@ See [CHANGELOG.md](./CHANGELOG.md) for complete migration guide.
|
|
|
36
36
|
- **Human Agent Handoff** - Agent join/leave workflows with realtime status updates and message synchronization
|
|
37
37
|
- **Advanced RAG System** - Chunk-based semantic search with adjacent retrieval using OpenAI text-embedding-3-large (3072 dims) and pgvector
|
|
38
38
|
- **Conversation Management** - Persistent conversation history with agent support
|
|
39
|
+
- **Slack Integration** - Direct query bot with full AI responses, conversation history, and reaction-based feedback ([setup guide](./docs/SLACK_INTEGRATION.md))
|
|
39
40
|
- **Feedback System** - Message rating and improvement tracking
|
|
40
41
|
- **Database Migrations** - Knex.js migration system for schema management
|
|
41
42
|
- **Production Ready** - Docker containerization with health checks
|
|
@@ -727,4 +728,4 @@ This project is dual-licensed:
|
|
|
727
728
|
|
|
728
729
|
---
|
|
729
730
|
|
|
730
|
-
**Status**: ✅ Production Ready | **Version**: 2.
|
|
731
|
+
**Status**: ✅ Production Ready | **Version**: 2.9.0 | **Node.js**: 20+ | **TypeScript**: 5+
|
package/database-schema.sql
CHANGED
|
@@ -62,6 +62,10 @@ INSERT INTO knex_migrations (name, batch, migration_time)
|
|
|
62
62
|
SELECT '009_add_archived_at_column.ts', 1, NOW()
|
|
63
63
|
WHERE NOT EXISTS (SELECT 1 FROM knex_migrations WHERE name = '009_add_archived_at_column.ts');
|
|
64
64
|
|
|
65
|
+
INSERT INTO knex_migrations (name, batch, migration_time)
|
|
66
|
+
SELECT '010_add_slack_fields.ts', 1, NOW()
|
|
67
|
+
WHERE NOT EXISTS (SELECT 1 FROM knex_migrations WHERE name = '010_add_slack_fields.ts');
|
|
68
|
+
|
|
65
69
|
-- Set migration lock to unlocked (0 = unlocked, 1 = locked)
|
|
66
70
|
INSERT INTO knex_migrations_lock (index, is_locked)
|
|
67
71
|
VALUES (1, 0)
|
|
@@ -131,6 +135,8 @@ CREATE TABLE IF NOT EXISTS vezlo_conversations (
|
|
|
131
135
|
closed_at TIMESTAMPTZ,
|
|
132
136
|
archived_at TIMESTAMPTZ,
|
|
133
137
|
last_message_at TIMESTAMPTZ,
|
|
138
|
+
slack_channel_id TEXT, -- Slack integration (migration 010)
|
|
139
|
+
slack_thread_ts TEXT, -- Slack integration (migration 010)
|
|
134
140
|
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
|
|
135
141
|
updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
|
|
136
142
|
deleted_at TIMESTAMPTZ -- Soft delete
|
|
@@ -290,6 +296,7 @@ CREATE INDEX IF NOT EXISTS idx_vezlo_conversations_last_message_at ON vezlo_conv
|
|
|
290
296
|
CREATE INDEX IF NOT EXISTS idx_vezlo_conversations_joined_at ON vezlo_conversations(joined_at);
|
|
291
297
|
CREATE INDEX IF NOT EXISTS idx_vezlo_conversations_closed_at ON vezlo_conversations(closed_at);
|
|
292
298
|
CREATE INDEX IF NOT EXISTS idx_vezlo_conversations_archived_at ON vezlo_conversations(archived_at);
|
|
299
|
+
CREATE INDEX IF NOT EXISTS idx_vezlo_conversations_slack_thread ON vezlo_conversations(slack_channel_id, slack_thread_ts) WHERE slack_channel_id IS NOT NULL AND slack_thread_ts IS NOT NULL;
|
|
293
300
|
|
|
294
301
|
-- Messages indexes
|
|
295
302
|
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';
|
|
@@ -10,6 +11,8 @@ import { ApiKeyService } from '../services/ApiKeyService';
|
|
|
10
11
|
import { ApiKeyController } from '../controllers/ApiKeyController';
|
|
11
12
|
import { CompanyService } from '../services/CompanyService';
|
|
12
13
|
import { CompanyController } from '../controllers/CompanyController';
|
|
14
|
+
import { SlackService } from '../services/SlackService';
|
|
15
|
+
import { SlackController } from '../controllers/SlackController';
|
|
13
16
|
export interface ServiceInitOptions {
|
|
14
17
|
supabase: SupabaseClient;
|
|
15
18
|
tablePrefix?: string;
|
|
@@ -25,6 +28,8 @@ export interface InitializedCoreServices {
|
|
|
25
28
|
chatManager: ChatManager;
|
|
26
29
|
apiKeyService: ApiKeyService;
|
|
27
30
|
companyService: CompanyService;
|
|
31
|
+
citationService: CitationService;
|
|
32
|
+
slackService: SlackService;
|
|
28
33
|
};
|
|
29
34
|
controllers: {
|
|
30
35
|
chatController: ChatController;
|
|
@@ -32,6 +37,7 @@ export interface InitializedCoreServices {
|
|
|
32
37
|
authController: AuthController;
|
|
33
38
|
apiKeyController: ApiKeyController;
|
|
34
39
|
companyController: CompanyController;
|
|
40
|
+
slackController: SlackController;
|
|
35
41
|
};
|
|
36
42
|
config: {
|
|
37
43
|
chatHistoryLength: number;
|
|
@@ -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;
|
|
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;AAGrE,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAEjE,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;QACjC,YAAY,EAAE,YAAY,CAAC;KAC5B,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;QACrC,eAAe,EAAE,eAAe,CAAC;KAClC,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,CAuH3F"}
|
|
@@ -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");
|
|
@@ -19,6 +20,8 @@ const CompanyService_1 = require("../services/CompanyService");
|
|
|
19
20
|
const CompanyController_1 = require("../controllers/CompanyController");
|
|
20
21
|
const IntentService_1 = require("../services/IntentService");
|
|
21
22
|
const RealtimePublisher_1 = require("../services/RealtimePublisher");
|
|
23
|
+
const SlackService_1 = require("../services/SlackService");
|
|
24
|
+
const SlackController_1 = require("../controllers/SlackController");
|
|
22
25
|
const DEFAULT_CHAT_HISTORY_LENGTH = 2;
|
|
23
26
|
const DEFAULT_CONVERSATION_TIMEOUT = 3600000; // 1 hour
|
|
24
27
|
function getChatHistoryLength() {
|
|
@@ -44,6 +47,7 @@ function initializeCoreServices(options) {
|
|
|
44
47
|
supabase,
|
|
45
48
|
tableName: knowledgeTableName
|
|
46
49
|
});
|
|
50
|
+
const citationService = new CitationService_1.CitationService(supabase, tablePrefix);
|
|
47
51
|
// Initialize V2 service for adjacent chunk retrieval
|
|
48
52
|
// Read AI configuration from environment
|
|
49
53
|
// Use AI_MODEL for all OpenAI calls (intent classification + response generation)
|
|
@@ -94,27 +98,33 @@ function initializeCoreServices(options) {
|
|
|
94
98
|
intentService,
|
|
95
99
|
realtimePublisher
|
|
96
100
|
});
|
|
97
|
-
const knowledgeController = new KnowledgeController_1.KnowledgeController(knowledgeBase, aiService);
|
|
101
|
+
const knowledgeController = new KnowledgeController_1.KnowledgeController(knowledgeBase, aiService, citationService);
|
|
98
102
|
const authController = new AuthController_1.AuthController(supabase);
|
|
99
103
|
const apiKeyService = new ApiKeyService_1.ApiKeyService(supabase);
|
|
100
104
|
const apiKeyController = new ApiKeyController_1.ApiKeyController(apiKeyService);
|
|
101
105
|
const companyService = new CompanyService_1.CompanyService(storage.company);
|
|
102
106
|
const companyController = new CompanyController_1.CompanyController(companyService);
|
|
107
|
+
// Initialize Slack integration
|
|
108
|
+
const slackService = new SlackService_1.SlackService();
|
|
109
|
+
const slackController = new SlackController_1.SlackController(slackService, chatManager, storage, resolvedHistoryLength);
|
|
103
110
|
return {
|
|
104
111
|
services: {
|
|
105
112
|
storage,
|
|
106
113
|
knowledgeBase,
|
|
114
|
+
citationService,
|
|
107
115
|
aiService,
|
|
108
116
|
chatManager,
|
|
109
117
|
apiKeyService,
|
|
110
|
-
companyService
|
|
118
|
+
companyService,
|
|
119
|
+
slackService
|
|
111
120
|
},
|
|
112
121
|
controllers: {
|
|
113
122
|
chatController,
|
|
114
123
|
knowledgeController,
|
|
115
124
|
authController,
|
|
116
125
|
apiKeyController,
|
|
117
|
-
companyController
|
|
126
|
+
companyController,
|
|
127
|
+
slackController
|
|
118
128
|
},
|
|
119
129
|
config: {
|
|
120
130
|
chatHistoryLength: resolvedHistoryLength
|
|
@@ -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;
|
|
@@ -30,18 +32,6 @@ export declare class ChatController {
|
|
|
30
32
|
deleteConversation(req: Request, res: Response): Promise<void>;
|
|
31
33
|
submitFeedback(req: Request | AuthenticatedRequest, res: Response): Promise<void>;
|
|
32
34
|
deleteFeedback(req: Request | AuthenticatedRequest, res: Response): Promise<void>;
|
|
33
|
-
private classifyIntent;
|
|
34
|
-
/**
|
|
35
|
-
* Handle intent classification result
|
|
36
|
-
* Returns response content if non-knowledge intent, null if knowledge intent
|
|
37
|
-
*/
|
|
38
|
-
private handleIntentResult;
|
|
39
|
-
/**
|
|
40
|
-
* Stream text content word by word to simulate streaming
|
|
41
|
-
* This ensures consistent SSE format for all responses
|
|
42
|
-
*/
|
|
43
|
-
private streamTextContent;
|
|
44
|
-
private getFallbackResponse;
|
|
45
35
|
private respondWithAssistantMessage;
|
|
46
36
|
private saveAssistantMessage;
|
|
47
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) {
|
|
@@ -1032,79 +951,6 @@ class ChatController {
|
|
|
1032
951
|
});
|
|
1033
952
|
}
|
|
1034
953
|
}
|
|
1035
|
-
async classifyIntent(message, history) {
|
|
1036
|
-
if (!this.intentService) {
|
|
1037
|
-
return {
|
|
1038
|
-
intent: 'knowledge',
|
|
1039
|
-
needsGuardrail: false,
|
|
1040
|
-
contactEmail: null
|
|
1041
|
-
};
|
|
1042
|
-
}
|
|
1043
|
-
const resolvedHistory = Array.isArray(history) ? history : [];
|
|
1044
|
-
logger_1.default.info('🧭 Classifying user intent...');
|
|
1045
|
-
return this.intentService.classify({
|
|
1046
|
-
message,
|
|
1047
|
-
conversationHistory: resolvedHistory
|
|
1048
|
-
});
|
|
1049
|
-
}
|
|
1050
|
-
/**
|
|
1051
|
-
* Handle intent classification result
|
|
1052
|
-
* Returns response content if non-knowledge intent, null if knowledge intent
|
|
1053
|
-
*/
|
|
1054
|
-
async handleIntentResult(result, userMessage, conversationId, conversation) {
|
|
1055
|
-
if (result.needsGuardrail && result.intent !== 'guardrail') {
|
|
1056
|
-
logger_1.default.info('🛡️ Guardrail triggered');
|
|
1057
|
-
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.`;
|
|
1058
|
-
}
|
|
1059
|
-
logger_1.default.info(`🧾 Intent result: ${result.intent}${result.needsGuardrail ? ' (guardrail triggered)' : ''}`);
|
|
1060
|
-
// For non-knowledge intents, return the response content to be streamed
|
|
1061
|
-
if (result.intent !== 'knowledge') {
|
|
1062
|
-
const responseContent = result.response || this.getFallbackResponse(result.intent);
|
|
1063
|
-
return responseContent;
|
|
1064
|
-
}
|
|
1065
|
-
// Knowledge intent - proceed to RAG flow (return null to indicate streaming will happen later)
|
|
1066
|
-
logger_1.default.info('📚 Intent requires knowledge lookup; proceeding with RAG flow.');
|
|
1067
|
-
return null;
|
|
1068
|
-
}
|
|
1069
|
-
/**
|
|
1070
|
-
* Stream text content word by word to simulate streaming
|
|
1071
|
-
* This ensures consistent SSE format for all responses
|
|
1072
|
-
*/
|
|
1073
|
-
async streamTextContent(content, res) {
|
|
1074
|
-
const words = content.split(' ');
|
|
1075
|
-
const chunkSize = 2; // Stream 2 words at a time for smoother experience
|
|
1076
|
-
const totalChunks = Math.ceil(words.length / chunkSize);
|
|
1077
|
-
for (let i = 0; i < words.length; i += chunkSize) {
|
|
1078
|
-
const chunk = words.slice(i, i + chunkSize).join(' ') + (i + chunkSize < words.length ? ' ' : '');
|
|
1079
|
-
const chunkIndex = Math.floor(i / chunkSize) + 1;
|
|
1080
|
-
const isLastChunk = chunkIndex === totalChunks;
|
|
1081
|
-
const chunkData = JSON.stringify({
|
|
1082
|
-
type: 'chunk',
|
|
1083
|
-
content: chunk,
|
|
1084
|
-
done: isLastChunk // Mark last chunk with done: true
|
|
1085
|
-
});
|
|
1086
|
-
res.write(`data: ${chunkData}\n\n`);
|
|
1087
|
-
// Flush the response to ensure chunks are sent immediately
|
|
1088
|
-
if (res.flush) {
|
|
1089
|
-
res.flush();
|
|
1090
|
-
}
|
|
1091
|
-
// Delay for smooth streaming effect (30ms for better visibility)
|
|
1092
|
-
await new Promise(resolve => setTimeout(resolve, 30));
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
getFallbackResponse(intent) {
|
|
1096
|
-
// Fallback responses in case LLM doesn't generate one (shouldn't happen, but safety net)
|
|
1097
|
-
const fallbacks = {
|
|
1098
|
-
greeting: 'Hello! How can I help you today?',
|
|
1099
|
-
acknowledgment: "You're welcome! Let me know if you need anything else.",
|
|
1100
|
-
personality: `I'm ${process.env.ASSISTANT_NAME || 'AI Assistant'}, your AI assistant for ${process.env.ORGANIZATION_NAME || 'Your Organization'}.`,
|
|
1101
|
-
clarification: "I'm not sure I understood. Could you clarify what you need help with?",
|
|
1102
|
-
guardrail: "I can help with documentation or implementation guidance, but I can't share credentials or confidential configuration.",
|
|
1103
|
-
human_support_request: "I'd be happy to connect you with our support team. Could you please provide your email address?",
|
|
1104
|
-
human_support_email: "Thank you! Our support team will reach out to you shortly."
|
|
1105
|
-
};
|
|
1106
|
-
return fallbacks[intent] || "I'm here to help. What would you like to know?";
|
|
1107
|
-
}
|
|
1108
954
|
async respondWithAssistantMessage(payload, res) {
|
|
1109
955
|
const assistantMessage = await this.saveAssistantMessage({
|
|
1110
956
|
conversation: payload.conversation,
|