@vezlo/assistant-server 2.3.0 โ 2.5.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 +10 -4
- package/database-schema.sql +6 -1
- package/dist/src/controllers/ChatController.d.ts +11 -1
- package/dist/src/controllers/ChatController.d.ts.map +1 -1
- package/dist/src/controllers/ChatController.js +258 -88
- package/dist/src/controllers/ChatController.js.map +1 -1
- package/dist/src/controllers/KnowledgeController.d.ts.map +1 -1
- package/dist/src/controllers/KnowledgeController.js +9 -4
- package/dist/src/controllers/KnowledgeController.js.map +1 -1
- package/dist/src/migrations/007_add_updated_at_to_feedback.d.ts +4 -0
- package/dist/src/migrations/007_add_updated_at_to_feedback.d.ts.map +1 -0
- package/dist/src/migrations/007_add_updated_at_to_feedback.js +22 -0
- package/dist/src/migrations/007_add_updated_at_to_feedback.js.map +1 -0
- package/dist/src/schemas/KnowledgeSchemas.d.ts +44 -0
- package/dist/src/schemas/KnowledgeSchemas.d.ts.map +1 -1
- package/dist/src/schemas/KnowledgeSchemas.js +18 -1
- package/dist/src/schemas/KnowledgeSchemas.js.map +1 -1
- package/dist/src/schemas/index.d.ts +44 -0
- package/dist/src/schemas/index.d.ts.map +1 -1
- package/dist/src/server.js +39 -8
- package/dist/src/server.js.map +1 -1
- package/dist/src/services/AIService.d.ts +9 -0
- package/dist/src/services/AIService.d.ts.map +1 -1
- package/dist/src/services/AIService.js +110 -0
- package/dist/src/services/AIService.js.map +1 -1
- package/dist/src/services/KnowledgeBaseService.d.ts +15 -0
- package/dist/src/services/KnowledgeBaseService.d.ts.map +1 -1
- package/dist/src/services/KnowledgeBaseService.js +87 -3
- package/dist/src/services/KnowledgeBaseService.js.map +1 -1
- package/dist/src/storage/FeedbackRepository.d.ts +1 -0
- package/dist/src/storage/FeedbackRepository.d.ts.map +1 -1
- package/dist/src/storage/FeedbackRepository.js +26 -0
- package/dist/src/storage/FeedbackRepository.js.map +1 -1
- package/dist/src/storage/UnifiedStorage.d.ts +1 -0
- package/dist/src/storage/UnifiedStorage.d.ts.map +1 -1
- package/dist/src/storage/UnifiedStorage.js +3 -0
- package/dist/src/storage/UnifiedStorage.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## ๐จ Breaking Change Notice
|
|
6
6
|
|
|
7
|
-
### v2.3.0 - Enhanced RAG System
|
|
7
|
+
### v2.3.0 - Enhanced RAG System
|
|
8
8
|
**New chunk-based architecture with adjacent retrieval for better code understanding.**
|
|
9
9
|
|
|
10
10
|
- **Database Schema**: New `vezlo_knowledge_chunks` table and RPC functions
|
|
@@ -384,12 +384,17 @@ http://localhost:3000/api
|
|
|
384
384
|
- `POST /api/messages/:uuid/generate` - Generate AI response
|
|
385
385
|
|
|
386
386
|
#### Knowledge Base
|
|
387
|
-
- `POST /api/knowledge/items` - Create knowledge item
|
|
387
|
+
- `POST /api/knowledge/items` - Create knowledge item (supports raw content, pre-chunked data, or chunks with embeddings)
|
|
388
388
|
- `GET /api/knowledge/items` - List knowledge items
|
|
389
389
|
- `GET /api/knowledge/items/:uuid` - Get knowledge item
|
|
390
390
|
- `PUT /api/knowledge/items/:uuid` - Update knowledge item
|
|
391
391
|
- `DELETE /api/knowledge/items/:uuid` - Delete knowledge item
|
|
392
392
|
|
|
393
|
+
**Knowledge Ingestion Options:**
|
|
394
|
+
- **Raw Content**: Send `content` field, server creates chunks and embeddings
|
|
395
|
+
- **Pre-chunked**: Send `chunks` array with `hasEmbeddings: false`, server generates embeddings
|
|
396
|
+
- **Chunks + Embeddings**: Send `chunks` array with embeddings and `hasEmbeddings: true`, server stores directly
|
|
397
|
+
|
|
393
398
|
#### Database Migrations
|
|
394
399
|
- `GET /api/migrate?key=<secret>` - Run pending database migrations
|
|
395
400
|
- `GET /api/migrate/status?key=<secret>` - Check migration status
|
|
@@ -425,7 +430,8 @@ npm run migrate:make add_users_table
|
|
|
425
430
|
- `POST /api/knowledge/search` - Search knowledge base
|
|
426
431
|
|
|
427
432
|
#### Feedback
|
|
428
|
-
- `POST /api/feedback` - Submit message feedback
|
|
433
|
+
- `POST /api/feedback` - Submit message feedback (Public API)
|
|
434
|
+
- `DELETE /api/feedback/:uuid` - Delete/undo message feedback (Public API)
|
|
429
435
|
|
|
430
436
|
### WebSocket Events
|
|
431
437
|
- `join-conversation` - Join conversation room
|
|
@@ -717,4 +723,4 @@ This project is dual-licensed:
|
|
|
717
723
|
|
|
718
724
|
---
|
|
719
725
|
|
|
720
|
-
**Status**: โ
Production Ready | **Version**: 2.
|
|
726
|
+
**Status**: โ
Production Ready | **Version**: 2.5.0 | **Node.js**: 20+ | **TypeScript**: 5+
|
package/database-schema.sql
CHANGED
|
@@ -50,6 +50,10 @@ INSERT INTO knex_migrations (name, batch, migration_time)
|
|
|
50
50
|
SELECT '006_add_knowledge_chunks.ts', 1, NOW()
|
|
51
51
|
WHERE NOT EXISTS (SELECT 1 FROM knex_migrations WHERE name = '006_add_knowledge_chunks.ts');
|
|
52
52
|
|
|
53
|
+
INSERT INTO knex_migrations (name, batch, migration_time)
|
|
54
|
+
SELECT '007_add_updated_at_to_feedback.ts', 1, NOW()
|
|
55
|
+
WHERE NOT EXISTS (SELECT 1 FROM knex_migrations WHERE name = '007_add_updated_at_to_feedback.ts');
|
|
56
|
+
|
|
53
57
|
-- Set migration lock to unlocked (0 = unlocked, 1 = locked)
|
|
54
58
|
INSERT INTO knex_migrations_lock (index, is_locked)
|
|
55
59
|
VALUES (1, 0)
|
|
@@ -147,7 +151,8 @@ CREATE TABLE IF NOT EXISTS vezlo_message_feedback (
|
|
|
147
151
|
category TEXT,
|
|
148
152
|
comment TEXT,
|
|
149
153
|
suggested_improvement TEXT,
|
|
150
|
-
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
154
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
155
|
+
updated_at TIMESTAMPTZ DEFAULT NOW() -- Added in migration 007
|
|
151
156
|
);
|
|
152
157
|
|
|
153
158
|
-- ============================================================================
|
|
@@ -27,9 +27,19 @@ export declare class ChatController {
|
|
|
27
27
|
sendAgentMessage(req: AuthenticatedRequest, res: Response): Promise<void>;
|
|
28
28
|
getUserConversations(req: AuthenticatedRequest, res: Response): Promise<void>;
|
|
29
29
|
deleteConversation(req: Request, res: Response): Promise<void>;
|
|
30
|
-
submitFeedback(req: AuthenticatedRequest, res: Response): Promise<void>;
|
|
30
|
+
submitFeedback(req: Request | AuthenticatedRequest, res: Response): Promise<void>;
|
|
31
|
+
deleteFeedback(req: Request | AuthenticatedRequest, res: Response): Promise<void>;
|
|
31
32
|
private classifyIntent;
|
|
33
|
+
/**
|
|
34
|
+
* Handle intent classification result
|
|
35
|
+
* Returns response content if non-knowledge intent, null if knowledge intent
|
|
36
|
+
*/
|
|
32
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;
|
|
33
43
|
private getFallbackResponse;
|
|
34
44
|
private respondWithAssistantMessage;
|
|
35
45
|
private saveAssistantMessage;
|
|
@@ -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,EAA8B,MAAM,2BAA2B,CAAC;AAEtF,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAElE,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;gBAG5C,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;IAY1G,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;
|
|
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,EAA8B,MAAM,2BAA2B,CAAC;AAEtF,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAElE,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;gBAG5C,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;IAY1G,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;IAsPzE,eAAe,CAAC,GAAG,EAAE,oBAAoB,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAoDxE,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,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;IA8D7E,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;IAyDjF,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,oBAAoB,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;YAkCzE,cAAc;IAmB5B;;;OAGG;YACW,kBAAkB;IAwBhC;;;OAGG;YACW,iBAAiB;IA2B/B,OAAO,CAAC,mBAAmB;YAeb,2BAA2B;YA4B3B,oBAAoB;CA4EnC"}
|
|
@@ -246,82 +246,202 @@ class ChatController {
|
|
|
246
246
|
});
|
|
247
247
|
return;
|
|
248
248
|
}
|
|
249
|
+
// Set up Server-Sent Events (SSE) headers for streaming (always stream, consistent format)
|
|
250
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
251
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
252
|
+
res.setHeader('Connection', 'keep-alive');
|
|
253
|
+
res.setHeader('X-Accel-Buffering', 'no'); // Disable nginx buffering
|
|
249
254
|
// Run intent classification to decide handling strategy
|
|
250
255
|
const intentResult = await this.classifyIntent(userMessageContent, messages);
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
256
|
+
const intentResponse = await this.handleIntentResult(intentResult, userMessage, conversationId, conversation);
|
|
257
|
+
let accumulatedContent = '';
|
|
258
|
+
let assistantMessageId;
|
|
259
|
+
try {
|
|
260
|
+
// If intent returned a response (non-knowledge intent), stream it
|
|
261
|
+
if (intentResponse) {
|
|
262
|
+
logger_1.default.info(`๐ค Streaming intent response for: ${intentResult.intent}`);
|
|
263
|
+
await this.streamTextContent(intentResponse, res);
|
|
264
|
+
accumulatedContent = intentResponse;
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
// Knowledge intent - proceed with RAG flow and stream AI response
|
|
268
|
+
logger_1.default.info('๐ Streaming knowledge-based response');
|
|
269
|
+
// Get knowledge base search results if available
|
|
270
|
+
const aiService = this.chatManager.aiService;
|
|
271
|
+
let knowledgeResults = null;
|
|
272
|
+
// Get conversation to extract company_id for knowledge base search
|
|
273
|
+
const companyIdRaw = req.profile?.companyId || conversation?.organizationId;
|
|
274
|
+
const companyId = companyIdRaw ? (typeof companyIdRaw === 'string' ? parseInt(companyIdRaw, 10) : companyIdRaw) : undefined;
|
|
275
|
+
if (aiService && aiService.knowledgeBaseService) {
|
|
276
|
+
try {
|
|
277
|
+
logger_1.default.info(`๐ Searching KB: query="${userMessageContent.substring(0, 50)}...", companyId=${companyId}`);
|
|
278
|
+
const searchResults = await aiService.knowledgeBaseService.search(userMessageContent, {
|
|
279
|
+
limit: 5,
|
|
280
|
+
company_id: companyId
|
|
281
|
+
});
|
|
282
|
+
logger_1.default.info(`๐ Found knowledge base results: ${searchResults.length}`);
|
|
283
|
+
if (searchResults.length > 0) {
|
|
284
|
+
knowledgeResults = '\n\nRelevant information from knowledge base:\n';
|
|
285
|
+
searchResults.forEach((result) => {
|
|
286
|
+
const title = result.title || 'Untitled';
|
|
287
|
+
const content = result.content || '';
|
|
288
|
+
if (content.trim()) {
|
|
289
|
+
knowledgeResults += `- ${title}: ${content}\n`;
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
// Verify we actually have meaningful content (not just the header)
|
|
293
|
+
const headerLength = '\n\nRelevant information from knowledge base:\n'.length;
|
|
294
|
+
if (knowledgeResults.length > headerLength + 10) {
|
|
295
|
+
logger_1.default.info(`โ
Knowledge context prepared (${knowledgeResults.length} chars, ${searchResults.length} results)`);
|
|
296
|
+
// Log first 200 chars for debugging
|
|
297
|
+
logger_1.default.info(`๐ Knowledge preview: ${knowledgeResults.substring(0, 200)}...`);
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
logger_1.default.warn(`โ ๏ธ Knowledge results found but content is empty or too short (${knowledgeResults.length} chars), treating as no results`);
|
|
301
|
+
knowledgeResults = '';
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
knowledgeResults = '';
|
|
306
|
+
logger_1.default.info('โ ๏ธ No knowledge base results found; will return appropriate fallback response');
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
console.error('โ Failed to search knowledge base:', error);
|
|
311
|
+
logger_1.default.error('Failed to search knowledge base:', error);
|
|
312
|
+
knowledgeResults = null;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
logger_1.default.warn('โ ๏ธ AI service or knowledge base service not available');
|
|
317
|
+
}
|
|
318
|
+
// Build context for AI
|
|
319
|
+
const chatContext = {
|
|
320
|
+
conversationHistory: messages.map(msg => ({
|
|
321
|
+
role: msg.role,
|
|
322
|
+
content: msg.content
|
|
323
|
+
})),
|
|
324
|
+
knowledgeResults: knowledgeResults ?? undefined
|
|
325
|
+
};
|
|
326
|
+
// Stream response from OpenAI
|
|
327
|
+
logger_1.default.info('๐ Starting OpenAI stream...');
|
|
328
|
+
const stream = aiService.generateResponseStream(userMessageContent, chatContext);
|
|
329
|
+
let chunkCount = 0;
|
|
330
|
+
for await (const { chunk, done, fullContent } of stream) {
|
|
331
|
+
chunkCount++;
|
|
332
|
+
// Always send the chunk (even if empty with done flag)
|
|
333
|
+
const chunkData = JSON.stringify({
|
|
334
|
+
type: 'chunk',
|
|
335
|
+
content: chunk,
|
|
336
|
+
done: done || false // Include done flag
|
|
337
|
+
});
|
|
338
|
+
res.write(`data: ${chunkData}\n\n`);
|
|
339
|
+
if (res.flush)
|
|
340
|
+
res.flush();
|
|
341
|
+
// Update accumulated content
|
|
342
|
+
if (chunk) {
|
|
343
|
+
accumulatedContent += chunk;
|
|
344
|
+
}
|
|
345
|
+
// Log first and last chunks
|
|
346
|
+
if (chunkCount === 1) {
|
|
347
|
+
logger_1.default.info(`๐ค First chunk sent: "${chunk.substring(0, 30)}..."`);
|
|
348
|
+
}
|
|
349
|
+
if (done && fullContent) {
|
|
350
|
+
accumulatedContent = fullContent;
|
|
351
|
+
logger_1.default.info(`๐ Stream complete: ${chunkCount} chunks sent, ${fullContent.length} total chars`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
// Save the message after streaming completes
|
|
356
|
+
try {
|
|
357
|
+
const assistantMessage = await this.saveAssistantMessage({
|
|
358
|
+
conversation,
|
|
359
|
+
conversationId,
|
|
360
|
+
parentMessageId: uuid,
|
|
361
|
+
content: accumulatedContent,
|
|
362
|
+
toolResults: []
|
|
363
|
+
});
|
|
364
|
+
assistantMessageId = assistantMessage.id;
|
|
365
|
+
// Send completion event with final message metadata (no content - already streamed)
|
|
366
|
+
const completionData = JSON.stringify({
|
|
367
|
+
type: 'completion',
|
|
368
|
+
uuid: assistantMessage.id,
|
|
369
|
+
parent_message_uuid: uuid,
|
|
370
|
+
status: 'completed',
|
|
371
|
+
created_at: assistantMessage.createdAt.toISOString()
|
|
372
|
+
});
|
|
373
|
+
res.write(`data: ${completionData}\n\n`);
|
|
374
|
+
}
|
|
375
|
+
catch (saveError) {
|
|
376
|
+
logger_1.default.error('Failed to save assistant message:', saveError);
|
|
377
|
+
const errorData = JSON.stringify({
|
|
378
|
+
type: 'error',
|
|
379
|
+
error: 'Failed to save message',
|
|
380
|
+
message: saveError instanceof Error ? saveError.message : 'Unknown error'
|
|
381
|
+
});
|
|
382
|
+
res.write(`data: ${errorData}\n\n`);
|
|
383
|
+
}
|
|
384
|
+
// Close the stream
|
|
385
|
+
res.end();
|
|
254
386
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
// Get conversation to extract company_id for knowledge base search
|
|
259
|
-
// Note: profile.companyId and conversation.organizationId should contain integer IDs (as strings)
|
|
260
|
-
const companyIdRaw = req.profile?.companyId || conversation?.organizationId;
|
|
261
|
-
const companyId = companyIdRaw ? (typeof companyIdRaw === 'string' ? parseInt(companyIdRaw, 10) : companyIdRaw) : undefined;
|
|
262
|
-
if (aiService && aiService.knowledgeBaseService) {
|
|
387
|
+
catch (streamError) {
|
|
388
|
+
logger_1.default.error('Streaming error:', streamError);
|
|
389
|
+
// Try to send error to client if connection is still open
|
|
263
390
|
try {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
391
|
+
const errorData = JSON.stringify({
|
|
392
|
+
type: 'error',
|
|
393
|
+
error: 'Failed to generate response',
|
|
394
|
+
message: streamError instanceof Error ? streamError.message : 'Unknown error'
|
|
268
395
|
});
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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: []
|
|
274
412
|
});
|
|
275
|
-
logger_1.default.info('โ
Knowledge context prepared');
|
|
276
413
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
knowledgeResults = '';
|
|
280
|
-
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);
|
|
281
416
|
}
|
|
282
417
|
}
|
|
283
|
-
catch (error) {
|
|
284
|
-
console.error('โ Failed to search knowledge base:', error);
|
|
285
|
-
logger_1.default.error('Failed to search knowledge base:', error);
|
|
286
|
-
// On error, set to null so AIService knows search was attempted
|
|
287
|
-
knowledgeResults = null;
|
|
288
|
-
}
|
|
289
418
|
}
|
|
290
|
-
else {
|
|
291
|
-
logger_1.default.warn('โ ๏ธ AI service or knowledge base service not available');
|
|
292
|
-
}
|
|
293
|
-
// Build context for AI
|
|
294
|
-
const chatContext = {
|
|
295
|
-
conversationHistory: messages.map(msg => ({
|
|
296
|
-
role: msg.role,
|
|
297
|
-
content: msg.content
|
|
298
|
-
})),
|
|
299
|
-
knowledgeResults: knowledgeResults ?? undefined // Convert null to undefined for cleaner check
|
|
300
|
-
};
|
|
301
|
-
// Generate AI response using the actual user message content
|
|
302
|
-
const response = await aiService.generateResponse(userMessageContent, chatContext);
|
|
303
|
-
const assistantMessage = await this.saveAssistantMessage({
|
|
304
|
-
conversation,
|
|
305
|
-
conversationId,
|
|
306
|
-
parentMessageId: uuid,
|
|
307
|
-
content: response.content,
|
|
308
|
-
toolResults: response.toolResults
|
|
309
|
-
});
|
|
310
|
-
res.json({
|
|
311
|
-
uuid: assistantMessage.id,
|
|
312
|
-
parent_message_uuid: uuid,
|
|
313
|
-
type: 'assistant',
|
|
314
|
-
content: response.content,
|
|
315
|
-
status: 'completed',
|
|
316
|
-
created_at: assistantMessage.createdAt.toISOString()
|
|
317
|
-
});
|
|
318
419
|
}
|
|
319
420
|
catch (error) {
|
|
320
421
|
logger_1.default.error('Generate response error:', error);
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
+
}
|
|
325
445
|
}
|
|
326
446
|
}
|
|
327
447
|
// Get conversation details
|
|
@@ -754,7 +874,7 @@ class ChatController {
|
|
|
754
874
|
});
|
|
755
875
|
}
|
|
756
876
|
}
|
|
757
|
-
// Submit message feedback
|
|
877
|
+
// Submit message feedback (create or update) - Public API
|
|
758
878
|
async submitFeedback(req, res) {
|
|
759
879
|
try {
|
|
760
880
|
const { message_uuid, rating, category, comment, suggested_improvement } = req.body;
|
|
@@ -762,25 +882,26 @@ class ChatController {
|
|
|
762
882
|
res.status(400).json({ error: 'message_uuid and rating are required' });
|
|
763
883
|
return;
|
|
764
884
|
}
|
|
765
|
-
if (!req.profile) {
|
|
766
|
-
res.status(401).json({ error: 'Authentication required' });
|
|
767
|
-
return;
|
|
768
|
-
}
|
|
769
885
|
// Get the message to find its conversationId
|
|
770
886
|
const message = await this.storage.getMessageById(message_uuid);
|
|
771
887
|
if (!message) {
|
|
772
888
|
res.status(404).json({ error: 'Message not found' });
|
|
773
889
|
return;
|
|
774
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);
|
|
775
895
|
const feedback = await this.storage.saveFeedback({
|
|
896
|
+
id: existingFeedback?.id, // Include ID if exists (will update instead of create)
|
|
776
897
|
messageId: message_uuid,
|
|
777
898
|
conversationId: message.conversationId,
|
|
778
|
-
userId
|
|
899
|
+
userId,
|
|
779
900
|
rating,
|
|
780
901
|
category,
|
|
781
902
|
comment,
|
|
782
903
|
suggestedImprovement: suggested_improvement,
|
|
783
|
-
createdAt: new Date()
|
|
904
|
+
createdAt: existingFeedback?.createdAt || new Date()
|
|
784
905
|
});
|
|
785
906
|
res.json({
|
|
786
907
|
success: true,
|
|
@@ -803,6 +924,36 @@ class ChatController {
|
|
|
803
924
|
});
|
|
804
925
|
}
|
|
805
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
|
+
}
|
|
806
957
|
async classifyIntent(message, history) {
|
|
807
958
|
if (!this.intentService) {
|
|
808
959
|
return {
|
|
@@ -818,31 +969,50 @@ class ChatController {
|
|
|
818
969
|
conversationHistory: resolvedHistory
|
|
819
970
|
});
|
|
820
971
|
}
|
|
821
|
-
|
|
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) {
|
|
822
977
|
if (result.needsGuardrail && result.intent !== 'guardrail') {
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
conversationId,
|
|
826
|
-
parentMessageId: userMessage.id,
|
|
827
|
-
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.`
|
|
828
|
-
}, res);
|
|
829
|
-
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.`;
|
|
830
980
|
}
|
|
831
981
|
logger_1.default.info(`๐งพ Intent result: ${result.intent}${result.needsGuardrail ? ' (guardrail triggered)' : ''}`);
|
|
832
|
-
// For non-knowledge intents,
|
|
982
|
+
// For non-knowledge intents, return the response content to be streamed
|
|
833
983
|
if (result.intent !== 'knowledge') {
|
|
834
984
|
const responseContent = result.response || this.getFallbackResponse(result.intent);
|
|
835
|
-
|
|
836
|
-
conversation,
|
|
837
|
-
conversationId,
|
|
838
|
-
parentMessageId: userMessage.id,
|
|
839
|
-
content: responseContent
|
|
840
|
-
}, res);
|
|
841
|
-
return true;
|
|
985
|
+
return responseContent;
|
|
842
986
|
}
|
|
843
|
-
// Knowledge intent - proceed to RAG flow
|
|
987
|
+
// Knowledge intent - proceed to RAG flow (return null to indicate streaming will happen later)
|
|
844
988
|
logger_1.default.info('๐ Intent requires knowledge lookup; proceeding with RAG flow.');
|
|
845
|
-
return
|
|
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
|
+
}
|
|
846
1016
|
}
|
|
847
1017
|
getFallbackResponse(intent) {
|
|
848
1018
|
// Fallback responses in case LLM doesn't generate one (shouldn't happen, but safety net)
|