@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.
Files changed (38) hide show
  1. package/README.md +10 -4
  2. package/database-schema.sql +6 -1
  3. package/dist/src/controllers/ChatController.d.ts +11 -1
  4. package/dist/src/controllers/ChatController.d.ts.map +1 -1
  5. package/dist/src/controllers/ChatController.js +258 -88
  6. package/dist/src/controllers/ChatController.js.map +1 -1
  7. package/dist/src/controllers/KnowledgeController.d.ts.map +1 -1
  8. package/dist/src/controllers/KnowledgeController.js +9 -4
  9. package/dist/src/controllers/KnowledgeController.js.map +1 -1
  10. package/dist/src/migrations/007_add_updated_at_to_feedback.d.ts +4 -0
  11. package/dist/src/migrations/007_add_updated_at_to_feedback.d.ts.map +1 -0
  12. package/dist/src/migrations/007_add_updated_at_to_feedback.js +22 -0
  13. package/dist/src/migrations/007_add_updated_at_to_feedback.js.map +1 -0
  14. package/dist/src/schemas/KnowledgeSchemas.d.ts +44 -0
  15. package/dist/src/schemas/KnowledgeSchemas.d.ts.map +1 -1
  16. package/dist/src/schemas/KnowledgeSchemas.js +18 -1
  17. package/dist/src/schemas/KnowledgeSchemas.js.map +1 -1
  18. package/dist/src/schemas/index.d.ts +44 -0
  19. package/dist/src/schemas/index.d.ts.map +1 -1
  20. package/dist/src/server.js +39 -8
  21. package/dist/src/server.js.map +1 -1
  22. package/dist/src/services/AIService.d.ts +9 -0
  23. package/dist/src/services/AIService.d.ts.map +1 -1
  24. package/dist/src/services/AIService.js +110 -0
  25. package/dist/src/services/AIService.js.map +1 -1
  26. package/dist/src/services/KnowledgeBaseService.d.ts +15 -0
  27. package/dist/src/services/KnowledgeBaseService.d.ts.map +1 -1
  28. package/dist/src/services/KnowledgeBaseService.js +87 -3
  29. package/dist/src/services/KnowledgeBaseService.js.map +1 -1
  30. package/dist/src/storage/FeedbackRepository.d.ts +1 -0
  31. package/dist/src/storage/FeedbackRepository.d.ts.map +1 -1
  32. package/dist/src/storage/FeedbackRepository.js +26 -0
  33. package/dist/src/storage/FeedbackRepository.js.map +1 -1
  34. package/dist/src/storage/UnifiedStorage.d.ts +1 -0
  35. package/dist/src/storage/UnifiedStorage.d.ts.map +1 -1
  36. package/dist/src/storage/UnifiedStorage.js +3 -0
  37. package/dist/src/storage/UnifiedStorage.js.map +1 -1
  38. 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 (Latest)
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.2.1 | **Node.js**: 20+ | **TypeScript**: 5+
726
+ **Status**: โœ… Production Ready | **Version**: 2.5.0 | **Node.js**: 20+ | **TypeScript**: 5+
@@ -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;IAuHzE,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,oBAAoB,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;YAsD/D,cAAc;YAmBd,kBAAkB;IAqChC,OAAO,CAAC,mBAAmB;YAeb,2BAA2B;YA4B3B,oBAAoB;CA4EnC"}
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 handled = await this.handleIntentResult(intentResult, userMessage, conversationId, conversation, res);
252
- if (handled) {
253
- return;
256
+ const intentResponse = await this.handleIntentResult(intentResult, userMessage, conversationId, conversation);
257
+ let accumulatedContent = '';
258
+ let assistantMessageId;
259
+ try {
260
+ // If intent returned a response (non-knowledge intent), stream it
261
+ if (intentResponse) {
262
+ logger_1.default.info(`๐Ÿ“ค Streaming intent response for: ${intentResult.intent}`);
263
+ await this.streamTextContent(intentResponse, res);
264
+ accumulatedContent = intentResponse;
265
+ }
266
+ else {
267
+ // Knowledge intent - proceed with RAG flow and stream AI response
268
+ logger_1.default.info('๐Ÿ“š Streaming knowledge-based response');
269
+ // Get knowledge base search results if available
270
+ const aiService = this.chatManager.aiService;
271
+ let knowledgeResults = null;
272
+ // Get conversation to extract company_id for knowledge base search
273
+ const companyIdRaw = req.profile?.companyId || conversation?.organizationId;
274
+ const companyId = companyIdRaw ? (typeof companyIdRaw === 'string' ? parseInt(companyIdRaw, 10) : companyIdRaw) : undefined;
275
+ if (aiService && aiService.knowledgeBaseService) {
276
+ try {
277
+ logger_1.default.info(`๐Ÿ” Searching KB: query="${userMessageContent.substring(0, 50)}...", companyId=${companyId}`);
278
+ const searchResults = await aiService.knowledgeBaseService.search(userMessageContent, {
279
+ limit: 5,
280
+ company_id: companyId
281
+ });
282
+ logger_1.default.info(`๐Ÿ“Š Found knowledge base results: ${searchResults.length}`);
283
+ if (searchResults.length > 0) {
284
+ knowledgeResults = '\n\nRelevant information from knowledge base:\n';
285
+ searchResults.forEach((result) => {
286
+ const title = result.title || 'Untitled';
287
+ const content = result.content || '';
288
+ if (content.trim()) {
289
+ knowledgeResults += `- ${title}: ${content}\n`;
290
+ }
291
+ });
292
+ // Verify we actually have meaningful content (not just the header)
293
+ const headerLength = '\n\nRelevant information from knowledge base:\n'.length;
294
+ if (knowledgeResults.length > headerLength + 10) {
295
+ logger_1.default.info(`โœ… Knowledge context prepared (${knowledgeResults.length} chars, ${searchResults.length} results)`);
296
+ // Log first 200 chars for debugging
297
+ logger_1.default.info(`๐Ÿ“ Knowledge preview: ${knowledgeResults.substring(0, 200)}...`);
298
+ }
299
+ else {
300
+ logger_1.default.warn(`โš ๏ธ Knowledge results found but content is empty or too short (${knowledgeResults.length} chars), treating as no results`);
301
+ knowledgeResults = '';
302
+ }
303
+ }
304
+ else {
305
+ knowledgeResults = '';
306
+ logger_1.default.info('โš ๏ธ No knowledge base results found; will return appropriate fallback response');
307
+ }
308
+ }
309
+ catch (error) {
310
+ console.error('โŒ Failed to search knowledge base:', error);
311
+ logger_1.default.error('Failed to search knowledge base:', error);
312
+ knowledgeResults = null;
313
+ }
314
+ }
315
+ else {
316
+ logger_1.default.warn('โš ๏ธ AI service or knowledge base service not available');
317
+ }
318
+ // Build context for AI
319
+ const chatContext = {
320
+ conversationHistory: messages.map(msg => ({
321
+ role: msg.role,
322
+ content: msg.content
323
+ })),
324
+ knowledgeResults: knowledgeResults ?? undefined
325
+ };
326
+ // Stream response from OpenAI
327
+ logger_1.default.info('๐Ÿ”„ Starting OpenAI stream...');
328
+ const stream = aiService.generateResponseStream(userMessageContent, chatContext);
329
+ let chunkCount = 0;
330
+ for await (const { chunk, done, fullContent } of stream) {
331
+ chunkCount++;
332
+ // Always send the chunk (even if empty with done flag)
333
+ const chunkData = JSON.stringify({
334
+ type: 'chunk',
335
+ content: chunk,
336
+ done: done || false // Include done flag
337
+ });
338
+ res.write(`data: ${chunkData}\n\n`);
339
+ if (res.flush)
340
+ res.flush();
341
+ // Update accumulated content
342
+ if (chunk) {
343
+ accumulatedContent += chunk;
344
+ }
345
+ // Log first and last chunks
346
+ if (chunkCount === 1) {
347
+ logger_1.default.info(`๐Ÿ“ค First chunk sent: "${chunk.substring(0, 30)}..."`);
348
+ }
349
+ if (done && fullContent) {
350
+ accumulatedContent = fullContent;
351
+ logger_1.default.info(`๐Ÿ Stream complete: ${chunkCount} chunks sent, ${fullContent.length} total chars`);
352
+ }
353
+ }
354
+ }
355
+ // Save the message after streaming completes
356
+ try {
357
+ const assistantMessage = await this.saveAssistantMessage({
358
+ conversation,
359
+ conversationId,
360
+ parentMessageId: uuid,
361
+ content: accumulatedContent,
362
+ toolResults: []
363
+ });
364
+ assistantMessageId = assistantMessage.id;
365
+ // Send completion event with final message metadata (no content - already streamed)
366
+ const completionData = JSON.stringify({
367
+ type: 'completion',
368
+ uuid: assistantMessage.id,
369
+ parent_message_uuid: uuid,
370
+ status: 'completed',
371
+ created_at: assistantMessage.createdAt.toISOString()
372
+ });
373
+ res.write(`data: ${completionData}\n\n`);
374
+ }
375
+ catch (saveError) {
376
+ logger_1.default.error('Failed to save assistant message:', saveError);
377
+ const errorData = JSON.stringify({
378
+ type: 'error',
379
+ error: 'Failed to save message',
380
+ message: saveError instanceof Error ? saveError.message : 'Unknown error'
381
+ });
382
+ res.write(`data: ${errorData}\n\n`);
383
+ }
384
+ // Close the stream
385
+ res.end();
254
386
  }
255
- // Get knowledge base search results if available
256
- const aiService = this.chatManager.aiService;
257
- let knowledgeResults = null; // null = search done, no results; undefined = not searched yet
258
- // Get conversation to extract company_id for knowledge base search
259
- // Note: profile.companyId and conversation.organizationId should contain integer IDs (as strings)
260
- const companyIdRaw = req.profile?.companyId || conversation?.organizationId;
261
- const companyId = companyIdRaw ? (typeof companyIdRaw === 'string' ? parseInt(companyIdRaw, 10) : companyIdRaw) : undefined;
262
- if (aiService && aiService.knowledgeBaseService) {
387
+ catch (streamError) {
388
+ logger_1.default.error('Streaming error:', streamError);
389
+ // Try to send error to client if connection is still open
263
390
  try {
264
- logger_1.default.info(`๐Ÿ” Searching KB: query="${userMessageContent.substring(0, 50)}...", companyId=${companyId}`);
265
- const searchResults = await aiService.knowledgeBaseService.search(userMessageContent, {
266
- limit: 5,
267
- company_id: companyId
391
+ const errorData = JSON.stringify({
392
+ type: 'error',
393
+ error: 'Failed to generate response',
394
+ message: streamError instanceof Error ? streamError.message : 'Unknown error'
268
395
  });
269
- logger_1.default.info(`๐Ÿ“Š Found knowledge base results: ${searchResults.length}`);
270
- if (searchResults.length > 0) {
271
- knowledgeResults = '\n\nRelevant information from knowledge base:\n';
272
- searchResults.forEach((result) => {
273
- knowledgeResults += `- ${result.title}: ${result.content}\n`;
396
+ res.write(`data: ${errorData}\n\n`);
397
+ res.end();
398
+ }
399
+ catch (writeError) {
400
+ logger_1.default.error('Failed to send error to client:', writeError);
401
+ res.end();
402
+ }
403
+ // If we have accumulated content but failed to save, try to save it
404
+ if (accumulatedContent && !assistantMessageId) {
405
+ try {
406
+ await this.saveAssistantMessage({
407
+ conversation,
408
+ conversationId,
409
+ parentMessageId: uuid,
410
+ content: accumulatedContent,
411
+ toolResults: []
274
412
  });
275
- logger_1.default.info('โœ… Knowledge context prepared');
276
413
  }
277
- else {
278
- // Set to empty string to indicate search was done but no results found
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
- res.status(500).json({
322
- error: 'Failed to generate response',
323
- message: error instanceof Error ? error.message : 'Unknown error'
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: req.user.id,
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
- async handleIntentResult(result, userMessage, conversationId, conversation, res) {
972
+ /**
973
+ * Handle intent classification result
974
+ * Returns response content if non-knowledge intent, null if knowledge intent
975
+ */
976
+ async handleIntentResult(result, userMessage, conversationId, conversation) {
822
977
  if (result.needsGuardrail && result.intent !== 'guardrail') {
823
- await this.respondWithAssistantMessage({
824
- conversation,
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, use LLM-generated response from intent classification
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
- await this.respondWithAssistantMessage({
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 false;
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)