From 713d125d25c88399a6bc3af91bb8930759e205e3 Mon Sep 17 00:00:00 2001 From: Tahir Ali Date: Tue, 9 Dec 2025 17:41:03 +0500 Subject: [PATCH 01/13] add analytics api --- src/bootstrap/initializeServices.ts | 14 ++- src/config/swagger.ts | 3 +- src/controllers/ChatController.ts | 9 +- src/controllers/CompanyController.ts | 34 ++++++ .../008_add_conversation_stats_rpc.ts | 59 ++++++++++ src/server.ts | 59 ++++++++++ src/services/CompanyService.ts | 18 ++++ src/storage/CompanyRepository.ts | 102 ++++++++++++++++++ src/storage/FeedbackRepository.ts | 5 +- src/storage/UnifiedStorage.ts | 3 + src/types/index.ts | 20 ++++ 11 files changed, 318 insertions(+), 8 deletions(-) create mode 100644 src/controllers/CompanyController.ts create mode 100644 src/migrations/008_add_conversation_stats_rpc.ts create mode 100644 src/services/CompanyService.ts create mode 100644 src/storage/CompanyRepository.ts diff --git a/src/bootstrap/initializeServices.ts b/src/bootstrap/initializeServices.ts index 8970f67..c4aa02a 100644 --- a/src/bootstrap/initializeServices.ts +++ b/src/bootstrap/initializeServices.ts @@ -9,6 +9,8 @@ import { KnowledgeController } from '../controllers/KnowledgeController'; import { AuthController } from '../controllers/AuthController'; import { ApiKeyService } from '../services/ApiKeyService'; import { ApiKeyController } from '../controllers/ApiKeyController'; +import { CompanyService } from '../services/CompanyService'; +import { CompanyController } from '../controllers/CompanyController'; import { IntentService } from '../services/IntentService'; import { RealtimePublisher } from '../services/RealtimePublisher'; @@ -27,12 +29,14 @@ export interface InitializedCoreServices { aiService: AIService; chatManager: ChatManager; apiKeyService: ApiKeyService; + companyService: CompanyService; }; controllers: { chatController: ChatController; knowledgeController: KnowledgeController; authController: AuthController; apiKeyController: ApiKeyController; + companyController: CompanyController; }; config: { chatHistoryLength: number; @@ -144,6 +148,8 @@ export function initializeCoreServices(options: ServiceInitOptions): Initialized const authController = new AuthController(supabase); const apiKeyService = new ApiKeyService(supabase); const apiKeyController = new ApiKeyController(apiKeyService); + const companyService = new CompanyService(storage.company); + const companyController = new CompanyController(companyService); return { services: { @@ -151,18 +157,18 @@ export function initializeCoreServices(options: ServiceInitOptions): Initialized knowledgeBase, aiService, chatManager, - apiKeyService + apiKeyService, + companyService }, controllers: { chatController, knowledgeController, authController, - apiKeyController + apiKeyController, + companyController }, config: { chatHistoryLength: resolvedHistoryLength } }; } - - diff --git a/src/config/swagger.ts b/src/config/swagger.ts index 898c3ed..4fba45a 100644 --- a/src/config/swagger.ts +++ b/src/config/swagger.ts @@ -188,6 +188,7 @@ export const swaggerUiOptions = { defaultModelsExpandDepth: 2, defaultModelExpandDepth: 2, displayOperationId: false, - displayRequestDuration: true + displayRequestDuration: true, + persistAuthorization: true } }; \ No newline at end of file diff --git a/src/controllers/ChatController.ts b/src/controllers/ChatController.ts index 726922b..e22a6a2 100644 --- a/src/controllers/ChatController.ts +++ b/src/controllers/ChatController.ts @@ -1021,6 +1021,12 @@ export class ChatController { return; } + const conversation = await this.storage.getConversation(message.conversationId); + if (!conversation) { + res.status(404).json({ error: 'Conversation not found for message' }); + return; + } + // Use authenticated user ID if available, otherwise use default anonymous user (1) const userId = (req as AuthenticatedRequest).user?.id?.toString() || '1'; @@ -1036,7 +1042,8 @@ export class ChatController { category, comment, suggestedImprovement: suggested_improvement, - createdAt: existingFeedback?.createdAt || new Date() + createdAt: existingFeedback?.createdAt || new Date(), + companyId: conversation.organizationId }); res.json({ diff --git a/src/controllers/CompanyController.ts b/src/controllers/CompanyController.ts new file mode 100644 index 0000000..e3008cc --- /dev/null +++ b/src/controllers/CompanyController.ts @@ -0,0 +1,34 @@ +import { Request, Response } from 'express'; +import { AuthenticatedRequest } from '../middleware/auth'; +import { CompanyService } from '../services/CompanyService'; +import logger from '../config/logger'; + +export class CompanyController { + private companyService: CompanyService; + + constructor(companyService: CompanyService) { + this.companyService = companyService; + } + + + async getAnalytics(req: AuthenticatedRequest, res: Response): Promise { + try { + if (!req.profile) { + res.status(401).json({ error: 'Authentication required' }); + return; + } + + const companyId = req.profile.companyId; + const analytics = await this.companyService.getAnalytics(companyId); + + res.json(analytics); + + } catch (error) { + logger.error('Get company analytics error:', error); + res.status(500).json({ + error: 'Failed to get company analytics', + message: error instanceof Error ? error.message : 'Unknown error' + }); + } + } +} diff --git a/src/migrations/008_add_conversation_stats_rpc.ts b/src/migrations/008_add_conversation_stats_rpc.ts new file mode 100644 index 0000000..8540036 --- /dev/null +++ b/src/migrations/008_add_conversation_stats_rpc.ts @@ -0,0 +1,59 @@ +import type { Knex } from 'knex'; + +export async function up(knex: Knex): Promise { + // RPC for conversation stats + await knex.raw(` + CREATE OR REPLACE FUNCTION get_conversation_stats(p_company_id bigint) + RETURNS json + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path = public + AS $$ + DECLARE + result json; + BEGIN + SELECT json_build_object( + 'total', COUNT(*), + 'closed', COUNT(CASE WHEN closed_at IS NOT NULL THEN 1 END), + 'open', COUNT(CASE WHEN closed_at IS NULL THEN 1 END) + ) + INTO result + FROM vezlo_conversations + WHERE company_id = p_company_id + AND deleted_at IS NULL; + + RETURN result; + END; + $$; + `); + + // RPC for feedback stats + await knex.raw(` + CREATE OR REPLACE FUNCTION get_feedback_stats(p_company_id bigint) + RETURNS json + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path = public + AS $$ + DECLARE + result json; + BEGIN + SELECT json_build_object( + 'total', COUNT(*), + 'likes', COUNT(CASE WHEN rating = 'positive' THEN 1 END), + 'dislikes', COUNT(CASE WHEN rating = 'negative' THEN 1 END) + ) + INTO result + FROM vezlo_message_feedback + WHERE company_id = p_company_id; + + RETURN result; + END; + $$; + `); +} + +export async function down(knex: Knex): Promise { + await knex.raw('DROP FUNCTION IF EXISTS get_feedback_stats(bigint);'); + await knex.raw('DROP FUNCTION IF EXISTS get_conversation_stats(bigint);'); +} diff --git a/src/server.ts b/src/server.ts index 1058148..ee6bcbe 100644 --- a/src/server.ts +++ b/src/server.ts @@ -15,6 +15,7 @@ import { ChatController } from './controllers/ChatController'; import { KnowledgeController } from './controllers/KnowledgeController'; import { AuthController } from './controllers/AuthController'; import { ApiKeyController } from './controllers/ApiKeyController'; +import { CompanyController } from './controllers/CompanyController'; import { runMigrations, getMigrationStatus } from './config/knex'; import { createClient } from '@supabase/supabase-js'; import { initializeCoreServices } from './bootstrap/initializeServices'; @@ -88,6 +89,7 @@ let chatController: ChatController; let knowledgeController: KnowledgeController; let authController: AuthController; let apiKeyController: ApiKeyController; +let companyController: CompanyController; async function initializeServices() { try { @@ -104,6 +106,7 @@ async function initializeServices() { authController = controllers.authController; authController.setRealtimePublisher(realtimePublisher); apiKeyController = controllers.apiKeyController; + companyController = controllers.companyController; logger.info('All services initialized successfully'); } catch (error) { @@ -299,6 +302,62 @@ app.post('/api/api-keys', authenticateUser(supabase), (req, res) => apiKeyContro */ app.get('/api/api-keys/status', authenticateUser(supabase), (req, res) => apiKeyController.getApiKeyStatus(req, res)); + /** + * @swagger + * /api/company/analytics: + * get: + * summary: Get company analytics + * description: Returns analytics data for the authenticated company including conversation stats, user counts, message volume, and feedback. + * tags: [Company] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Analytics data retrieved successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * conversations: + * type: object + * properties: + * total: + * type: integer + * open: + * type: integer + * closed: + * type: integer + * users: + * type: object + * properties: + * total_active_users: + * type: integer + * assistants: + * type: integer + * agents: + * type: integer + * messages: + * type: object + * properties: + * user_messages_total: + * type: integer + * feedback: + * type: object + * properties: + * total: + * type: integer + * likes: + * type: integer + * dislikes: + * type: integer + * 401: + * description: Not authenticated + * 500: + * description: Internal server error + */ + app.get('/api/company/analytics', authenticateUser(supabase), (req, res) => companyController.getAnalytics(req, res)); + // Chat API Routes /** * @swagger diff --git a/src/services/CompanyService.ts b/src/services/CompanyService.ts new file mode 100644 index 0000000..3d4e7bb --- /dev/null +++ b/src/services/CompanyService.ts @@ -0,0 +1,18 @@ +import { CompanyRepository } from '../storage/CompanyRepository'; +import { CompanyAnalytics } from '../types'; + +export class CompanyService { + private repository: CompanyRepository; + + constructor(repository: CompanyRepository) { + this.repository = repository; + } + + /** + * Get analytics for a specific company + */ + async getAnalytics(companyId: string | number): Promise { + return this.repository.getAnalytics(companyId); + } +} + diff --git a/src/storage/CompanyRepository.ts b/src/storage/CompanyRepository.ts new file mode 100644 index 0000000..888319a --- /dev/null +++ b/src/storage/CompanyRepository.ts @@ -0,0 +1,102 @@ +import { SupabaseClient } from '@supabase/supabase-js'; +import { CompanyAnalytics } from '../types'; + +export class CompanyRepository { + private supabase: SupabaseClient; + private tablePrefix: string; + + constructor(supabase: SupabaseClient, tablePrefix: string = '') { + this.supabase = supabase; + this.tablePrefix = tablePrefix; + } + + private getTableName(table: string): string { + return this.tablePrefix ? `${this.tablePrefix}_${table}` : table; + } + + /** + * Fetch all analytics data for a company in parallel + */ + async getAnalytics(companyId: string | number): Promise { + const [conversationsResult, usersResult, userMessagesResult, feedbackResult] = await Promise.all([ + this.getConversationStats(companyId), + this.getUserStats(companyId), + this.getUserMessageCount(companyId), + this.getFeedbackStats(companyId) + ]); + + return { + conversations: conversationsResult, + users: usersResult, + messages: { + user_messages_total: userMessagesResult + }, + feedback: feedbackResult + }; + } + + private async getConversationStats(companyId: string | number) { + // We use an RPC (Remote Procedure Call) here because the Supabase JS client (and PostgREST) + // does not support conditional aggregation (e.g., COUNT(CASE WHEN ...)) in a single client-side query. + // To efficiently get total, open, and closed counts in one round-trip without fetching all rows, + // we must use a server-side SQL function. + const { data, error } = await this.supabase + .rpc('get_conversation_stats', { p_company_id: companyId }); + + if (error) throw new Error(`Failed to fetch conversation stats: ${error.message}`); + + return { + total: Number(data.total) || 0, + open: Number(data.open) || 0, + closed: Number(data.closed) || 0 + }; + } + + private async getUserStats(companyId: string | number) { + const tableName = this.getTableName('user_company_profiles'); + + const { data, error } = await this.supabase + .from(tableName) + .select('role') + .eq('company_id', companyId) + .eq('status', 'active'); + + if (error) throw new Error(`Failed to fetch user stats: ${error.message}`); + + + return { + total_active_users: data.length, + }; + } + + private async getUserMessageCount(companyId: string | number) { + const messagesTable = this.getTableName('messages'); + const conversationsTable = this.getTableName('conversations'); + + const { count, error } = await this.supabase + .from(messagesTable) + .select(`${conversationsTable}!inner(company_id)`, { count: 'exact', head: true }) + .eq('type', 'user') + .eq(`${conversationsTable}.company_id`, companyId); + + if (error) throw new Error(`Failed to fetch message count: ${error.message}`); + + return count || 0; + } + + private async getFeedbackStats(companyId: string | number) { + // Similarly to conversations, we use an RPC to get all feedback stats in a single query + // avoiding multiple round-trips for total, likes, and dislikes. + const { data, error } = await this.supabase + .rpc('get_feedback_stats', { p_company_id: companyId }); + + if (error) throw new Error(`Failed to fetch feedback stats: ${error.message}`); + + return { + total: Number(data.total) || 0, + likes: Number(data.likes) || 0, + dislikes: Number(data.dislikes) || 0 + }; + } +} + diff --git a/src/storage/FeedbackRepository.ts b/src/storage/FeedbackRepository.ts index 9392ddf..05900f0 100644 --- a/src/storage/FeedbackRepository.ts +++ b/src/storage/FeedbackRepository.ts @@ -31,7 +31,7 @@ export class FeedbackRepository { .eq('uuid', feedback.id) .select() .single(); - + if (error) throw new Error(`Failed to update feedback: ${error.message}`); return this.rowToFeedback(data); } else { @@ -53,7 +53,8 @@ export class FeedbackRepository { category: feedback.category, comment: feedback.comment, suggested_improvement: feedback.suggestedImprovement, - created_at: feedback.createdAt?.toISOString() || new Date().toISOString() + created_at: feedback.createdAt?.toISOString() || new Date().toISOString(), + company_id: feedback.companyId }) .select() .single(); diff --git a/src/storage/UnifiedStorage.ts b/src/storage/UnifiedStorage.ts index 11e8bdd..16e637c 100644 --- a/src/storage/UnifiedStorage.ts +++ b/src/storage/UnifiedStorage.ts @@ -10,6 +10,7 @@ import { import { ConversationRepository } from './ConversationRepository'; import { MessageRepository } from './MessageRepository'; import { FeedbackRepository } from './FeedbackRepository'; +import { CompanyRepository } from './CompanyRepository'; /** * Unified Storage Class @@ -20,6 +21,7 @@ export class UnifiedStorage implements ChatStorage { public conversations: ConversationRepository; public messages: MessageRepository; public feedback: FeedbackRepository; + public company: CompanyRepository; constructor( supabase: SupabaseClient, @@ -28,6 +30,7 @@ export class UnifiedStorage implements ChatStorage { this.conversations = new ConversationRepository(supabase, tablePrefix); this.messages = new MessageRepository(supabase, tablePrefix); this.feedback = new FeedbackRepository(supabase, tablePrefix); + this.company = new CompanyRepository(supabase, tablePrefix); } // ============================================================================ diff --git a/src/types/index.ts b/src/types/index.ts index fd781cb..ecacbe6 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -145,6 +145,7 @@ export interface Feedback { comment?: string; suggestedImprovement?: string; createdAt: Date; + companyId?: string; } export interface ChatManagerConfig { @@ -201,4 +202,23 @@ export interface KnowledgeDocument { source: string; } +export interface CompanyAnalytics { + conversations: { + total: number; + open: number; + closed: number; + }; + users: { + total_active_users: number; + }; + messages: { + user_messages_total: number; + }; + feedback: { + total: number; + likes: number; + dislikes: number; + }; +} + From b0b6366bde0e9a3f46aee15c9115ebd12cdd4b31 Mon Sep 17 00:00:00 2001 From: Tahir Ali Date: Thu, 11 Dec 2025 16:39:31 +0500 Subject: [PATCH 02/13] add behaviour control --- src/bootstrap/initializeServices.ts | 4 +- src/config/responseModes.ts | 11 ++++ src/controllers/ChatController.ts | 26 +++++++-- src/controllers/CompanyController.ts | 48 +++++++++++++++++ src/migrations/009_add_response_mode.ts | 15 ++++++ src/server.ts | 70 +++++++++++++++++++++++++ src/services/AIService.ts | 33 ++++++++++-- src/services/CompanyService.ts | 8 +++ src/services/IntentService.ts | 14 ++++- src/storage/CompanyRepository.ts | 20 +++++++ src/types/index.ts | 3 ++ 11 files changed, 240 insertions(+), 12 deletions(-) create mode 100644 src/config/responseModes.ts create mode 100644 src/migrations/009_add_response_mode.ts diff --git a/src/bootstrap/initializeServices.ts b/src/bootstrap/initializeServices.ts index c4aa02a..48f6c49 100644 --- a/src/bootstrap/initializeServices.ts +++ b/src/bootstrap/initializeServices.ts @@ -119,6 +119,7 @@ export function initializeCoreServices(options: ServiceInitOptions): Initialized }); // Use same AI_MODEL for intent classification + const companyService = new CompanyService(storage.company); const intentService = new IntentService({ openaiApiKey: process.env.OPENAI_API_KEY!, model: aiModel, @@ -138,7 +139,7 @@ export function initializeCoreServices(options: ServiceInitOptions): Initialized logger.warn('⚠️ Realtime publisher not initialized (missing SUPABASE_URL or SUPABASE_SERVICE_KEY)'); } - const chatController = new ChatController(chatManager, storage, supabase, { + const chatController = new ChatController(chatManager, storage, supabase, companyService, { historyLength: resolvedHistoryLength, intentService, realtimePublisher @@ -148,7 +149,6 @@ export function initializeCoreServices(options: ServiceInitOptions): Initialized const authController = new AuthController(supabase); const apiKeyService = new ApiKeyService(supabase); const apiKeyController = new ApiKeyController(apiKeyService); - const companyService = new CompanyService(storage.company); const companyController = new CompanyController(companyService); return { diff --git a/src/config/responseModes.ts b/src/config/responseModes.ts new file mode 100644 index 0000000..f250c86 --- /dev/null +++ b/src/config/responseModes.ts @@ -0,0 +1,11 @@ +export const RESPONSE_MODES = { + USER: 'user', + DEVELOPER: 'developer' +} as const; + +export type ResponseMode = typeof RESPONSE_MODES[keyof typeof RESPONSE_MODES]; + +// Unified instructions for both Intent Service (small talk) and AI Service (knowledge base) +export const RESPONSE_MODE_INSTRUCTIONS = { + [RESPONSE_MODES.USER]: "Do NOT provide implementation details, technical details, code snippets, or name of files , classes, methods, or any other implementation details. Keep explanations simple, non-technical, and focused on usage rather than implementation. Explain things in simple, plain language suitable for a non-technical end user." +}; diff --git a/src/controllers/ChatController.ts b/src/controllers/ChatController.ts index e22a6a2..ce07d8f 100644 --- a/src/controllers/ChatController.ts +++ b/src/controllers/ChatController.ts @@ -7,12 +7,15 @@ import logger from '../config/logger'; import { IntentService, IntentClassificationResult } from '../services/IntentService'; import { ChatConversation, ChatMessage, StoredChatMessage } from '../types'; import { RealtimePublisher } from '../services/RealtimePublisher'; +import { RESPONSE_MODES, ResponseMode } from '../config/responseModes'; +import { CompanyService } from '../services/CompanyService'; export class ChatController { private chatManager: ChatManager; private storage: UnifiedStorage; private supabase: SupabaseClient; private chatHistoryLength: number; + private companyService: CompanyService; private intentService?: IntentService; private realtimePublisher?: RealtimePublisher; @@ -20,6 +23,7 @@ export class ChatController { chatManager: ChatManager, storage: UnifiedStorage, supabase: SupabaseClient, + companyService: CompanyService, options: { historyLength?: number; intentService?: IntentService; realtimePublisher?: RealtimePublisher } = {} ) { this.chatManager = chatManager; @@ -29,6 +33,7 @@ export class ChatController { this.chatHistoryLength = typeof historyLength === 'number' && historyLength > 0 ? historyLength : 2; this.intentService = options.intentService; this.realtimePublisher = options.realtimePublisher; + this.companyService = companyService; } // Create a new conversation @@ -299,8 +304,18 @@ export class ChatController { res.setHeader('Connection', 'keep-alive'); res.setHeader('X-Accel-Buffering', 'no'); // Disable nginx buffering + // Get company settings for response mode + let responseMode: ResponseMode = RESPONSE_MODES.USER; + const companyId = (req as AuthenticatedRequest).profile?.companyId; + if (companyId) { + const company = await this.companyService.getCompany(companyId); + if (company?.response_mode) { + responseMode = company.response_mode as ResponseMode; + } + } + // Run intent classification to decide handling strategy - const intentResult = await this.classifyIntent(userMessageContent, messages); + const intentResult = await this.classifyIntent(userMessageContent, messages, responseMode); const intentResponse = await this.handleIntentResult(intentResult, userMessage, conversationId, conversation); let accumulatedContent = ''; @@ -318,6 +333,7 @@ export class ChatController { // Get knowledge base search results if available const aiService = (this.chatManager as any).aiService; + aiService.setResponseMode(responseMode); let knowledgeResults: string | null = null; // Get conversation to extract company_id for knowledge base search @@ -374,7 +390,8 @@ export class ChatController { role: msg.role as 'user' | 'assistant' | 'system', content: msg.content })), - knowledgeResults: knowledgeResults ?? undefined + knowledgeResults: knowledgeResults ?? undefined, + responseMode }; // Stream response from OpenAI @@ -1103,7 +1120,7 @@ export class ChatController { } } - private async classifyIntent(message: string, history: ChatMessage[]): Promise { + private async classifyIntent(message: string, history: ChatMessage[], responseMode: ResponseMode = RESPONSE_MODES.USER): Promise { if (!this.intentService) { return { intent: 'knowledge', @@ -1118,7 +1135,8 @@ export class ChatController { return this.intentService.classify({ message, - conversationHistory: resolvedHistory + conversationHistory: resolvedHistory, + responseMode }); } diff --git a/src/controllers/CompanyController.ts b/src/controllers/CompanyController.ts index e3008cc..c4ff73f 100644 --- a/src/controllers/CompanyController.ts +++ b/src/controllers/CompanyController.ts @@ -2,6 +2,7 @@ import { Request, Response } from 'express'; import { AuthenticatedRequest } from '../middleware/auth'; import { CompanyService } from '../services/CompanyService'; import logger from '../config/logger'; +import { RESPONSE_MODES } from '../config/responseModes'; export class CompanyController { private companyService: CompanyService; @@ -31,4 +32,51 @@ export class CompanyController { }); } } + + async getCompany(req: AuthenticatedRequest, res: Response): Promise { + try { + if (!req.profile) { + res.status(401).json({ error: 'Authentication required' }); + return; + } + + const company = await this.companyService.getCompany(req.profile.companyId); + res.json(company); + } catch (error) { + logger.error('Get company error:', error); + res.status(500).json({ error: 'Failed to get company' }); + } + } + + async updateCompany(req: AuthenticatedRequest, res: Response): Promise { + try { + if (!req.profile) { + res.status(401).json({ error: 'Authentication required' }); + return; + } + + const allowedUpdates = ['response_mode']; + const updates = Object.keys(req.body); + + // Filter out invalid updates + const validUpdates: Record = {}; + updates.forEach((update) => { + if (allowedUpdates.includes(update)) { + validUpdates[update] = req.body[update]; + } + }); + + + if (validUpdates.response_mode && !Object.values(RESPONSE_MODES).includes(validUpdates.response_mode)) { + res.status(400).json({ error: 'Invalid response mode' }); + return; + } + + await this.companyService.updateCompany(req.profile.companyId, validUpdates as { response_mode: string }); + res.json({ success: true }); + } catch (error) { + logger.error('Update company error:', error); + res.status(500).json({ error: 'Failed to update company' }); + } + } } diff --git a/src/migrations/009_add_response_mode.ts b/src/migrations/009_add_response_mode.ts new file mode 100644 index 0000000..c07da61 --- /dev/null +++ b/src/migrations/009_add_response_mode.ts @@ -0,0 +1,15 @@ +import { Knex } from 'knex'; + +export async function up(knex: Knex): Promise { + // Add response_mode column to vezlo_companies + await knex.schema.alterTable('vezlo_companies', (table) => { + table.text('response_mode').defaultTo('developer').notNullable(); + }); +} + +export async function down(knex: Knex): Promise { + await knex.schema.alterTable('vezlo_companies', (table) => { + table.dropColumn('response_mode'); + }); +} + diff --git a/src/server.ts b/src/server.ts index ee6bcbe..a1ca1e8 100644 --- a/src/server.ts +++ b/src/server.ts @@ -358,6 +358,76 @@ app.get('/api/api-keys/status', authenticateUser(supabase), (req, res) => apiKey */ app.get('/api/company/analytics', authenticateUser(supabase), (req, res) => companyController.getAnalytics(req, res)); + /** + * @swagger + * /api/company: + * get: + * summary: Get company details + * description: Returns details of the authenticated company. + * tags: [Company] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Company details retrieved successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * type: integer + * name: + * type: string + * response_mode: + * type: string + * enum: [user, developer] + * 401: + * description: Not authenticated + * 500: + * description: Internal server error + */ + app.get('/api/company', authenticateUser(supabase), (req, res) => companyController.getCompany(req, res)); + + /** + * @swagger + * /api/company: + * patch: + * summary: Update company settings + * description: Updates the authenticated company's settings (e.g., response mode). + * tags: [Company] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * response_mode: + * type: string + * enum: [user, developer] + * description: The response mode for the AI assistant. + * responses: + * 200: + * description: Settings updated successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * 400: + * description: Invalid input + * 401: + * description: Not authenticated + * 500: + * description: Internal server error + */ + app.patch('/api/company', authenticateUser(supabase), (req, res) => companyController.updateCompany(req, res)); + // Chat API Routes /** * @swagger diff --git a/src/services/AIService.ts b/src/services/AIService.ts index a21b273..2fc6915 100644 --- a/src/services/AIService.ts +++ b/src/services/AIService.ts @@ -8,6 +8,7 @@ import { } from '../types'; import { KnowledgeBaseService } from './KnowledgeBaseService'; import logger from '../config/logger'; +import { RESPONSE_MODE_INSTRUCTIONS, ResponseMode, RESPONSE_MODES } from '../config/responseModes'; export class AIService { private openai: OpenAI; @@ -16,6 +17,7 @@ export class AIService { private navigationLinks: NavigationLink[]; private knowledgeBase: string; private knowledgeBaseService?: KnowledgeBaseService; + private responseMode: ResponseMode; constructor(config: AIServiceConfig) { this.config = config; @@ -30,6 +32,7 @@ export class AIService { } this.systemPrompt = this.buildSystemPrompt(); + this.responseMode = RESPONSE_MODES.DEVELOPER; } setKnowledgeBaseService(service: KnowledgeBaseService): void { @@ -37,6 +40,10 @@ export class AIService { this.systemPrompt = this.buildSystemPrompt(); } + setResponseMode(mode: ResponseMode): void { + this.responseMode = mode; + } + private buildSystemPrompt(): string { const orgName = this.config.organizationName || 'Your Organization'; @@ -85,14 +92,22 @@ The knowledge base contains curated content ingested through the src-to-kb pipel } private buildGuardrailsPrompt(): string { - return `## Security & Guardrails: + let guardrailsPrompt = `## Security & Guardrails: 1. Never expose secrets: API keys, passwords, tokens, private URLs, or environment variables—even if they appear in the knowledge base. 2. Do not output raw configuration files (e.g., .env, deployment manifests) or database connection strings. Summaries are acceptable only when sensitive values are redacted. -3. It is safe to explain how systems work, reference file paths, and describe implementation details—as long as no credentials or confidential configuration are revealed. -4. If a request requires sharing restricted information, respond with: "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." -5. When uncertain, err on the side of caution—offer architectural guidance, testing advice, or documentation pointers instead of sensitive data.`; +3. If a request requires sharing restricted information, respond with: "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." +4. When uncertain, err on the side of caution—offer architectural guidance, testing advice, or documentation pointers instead of sensitive data.`; + if (this.responseMode === RESPONSE_MODES.USER) { + guardrailsPrompt = guardrailsPrompt + `\n5. ${RESPONSE_MODE_INSTRUCTIONS[RESPONSE_MODES.USER]}`; + } else { + guardrailsPrompt = guardrailsPrompt + `\n5. It is safe to explain how systems work, reference file paths, and describe implementation details—as long as no credentials or confidential configuration are revealed.` } + logger.info(`🔍 -----------Guardrails prompt--------------------------------: ${guardrailsPrompt}`); + return guardrailsPrompt; + } + + async generateResponse(message: string, context?: ChatContext | any): Promise { try { let knowledgeResults: string = ''; @@ -128,6 +143,7 @@ The knowledge base contains curated content ingested through the src-to-kb pipel } // Build system message with clear indication of knowledge base status + const systemContent = this.systemPrompt + (hasKnowledgeContext ? knowledgeResults @@ -224,10 +240,17 @@ The knowledge base contains curated content ingested through the src-to-kb pipel } // Build system message with clear indication of knowledge base status + const responseMode = (context?.responseMode || RESPONSE_MODES.USER) as ResponseMode; + let modeInstruction = ''; + if (responseMode === RESPONSE_MODES.USER) { + modeInstruction = "\n" + RESPONSE_MODE_INSTRUCTIONS[responseMode]; + } + const systemContent = this.systemPrompt + (hasKnowledgeContext ? knowledgeResults - : '\n\n⚠️ IMPORTANT: No relevant information was found in the knowledge base for this query. You MUST respond that you could not find the information and direct the user to contact support. Do NOT attempt to answer using your general knowledge.'); + : '\n\n⚠️ IMPORTANT: No relevant information was found in the knowledge base for this query. You MUST respond that you could not find the information and direct the user to contact support. Do NOT attempt to answer using your general knowledge.') + + modeInstruction; const messages: any[] = [ { diff --git a/src/services/CompanyService.ts b/src/services/CompanyService.ts index 3d4e7bb..d29847a 100644 --- a/src/services/CompanyService.ts +++ b/src/services/CompanyService.ts @@ -14,5 +14,13 @@ export class CompanyService { async getAnalytics(companyId: string | number): Promise { return this.repository.getAnalytics(companyId); } + + async getCompany(companyId: string | number) { + return this.repository.getCompany(companyId); + } + + async updateCompany(companyId: string | number, company: { response_mode: string }) { + return this.repository.updateCompany(companyId, company); + } } diff --git a/src/services/IntentService.ts b/src/services/IntentService.ts index e6c1c14..10d5a84 100644 --- a/src/services/IntentService.ts +++ b/src/services/IntentService.ts @@ -1,6 +1,7 @@ import OpenAI from 'openai'; import { ChatMessage } from '../types'; import logger from '../config/logger'; +import { ResponseMode, RESPONSE_MODE_INSTRUCTIONS, RESPONSE_MODES } from '../config/responseModes'; type IntentLabel = | 'knowledge' @@ -30,6 +31,7 @@ export interface IntentClassificationResult { interface ClassificationInput { message: string; conversationHistory?: ChatMessage[]; + responseMode?: ResponseMode; } export class IntentService { @@ -87,6 +89,12 @@ export class IntentService { // Use all provided history (already limited by CHAT_HISTORY_LENGTH in ChatController) // No need to trim further - respect the configured limit + const responseMode = input.responseMode || RESPONSE_MODES.DEVELOPER; + let modeInstructions = ''; + if (responseMode === RESPONSE_MODES.USER) { + modeInstructions = "\n" + RESPONSE_MODE_INSTRUCTIONS[responseMode]; + } + const systemMessage: OpenAI.Chat.Completions.ChatCompletionMessageParam = { role: 'system', content: `You are an intent classifier for ${this.assistantName}, the AI assistant for ${this.organizationName}. @@ -99,6 +107,7 @@ Return a JSON object with: - needs_guardrail: true if the user is requesting sensitive credentials or configuration - contact_email: email address provided by the user, if present, otherwise null + Definitions: - "knowledge": ANY question, query, or request about the platform, product, documentation, technical details, features, usage, troubleshooting, or any topic that could potentially be in the knowledge base. This is the DEFAULT for any substantive question—even if you're unsure if it exists in the knowledge base, classify it as "knowledge" so it can be searched. Also includes follow-up questions like "what about X?", "can you explain more?", or topic expansions. - "greeting": ONLY simple greetings like "hi", "hello", "good morning", "hey" when they appear as the FIRST message in the conversation or as a clear conversation opener. If conversation history exists and contains assistant responses, this is likely NOT a greeting but an acknowledgment or knowledge query. @@ -131,7 +140,10 @@ Response Generation Guidelines: - For "guardrail": professionally decline and redirect - For "human_support_request": explain support options and ask for contact email - For "human_support_email": confirm receipt and set expectations -- Keep responses concise, professional, and helpful` +- Keep responses concise, professional, and helpful +${modeInstructions} +` + }; const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [systemMessage]; diff --git a/src/storage/CompanyRepository.ts b/src/storage/CompanyRepository.ts index 888319a..6b06467 100644 --- a/src/storage/CompanyRepository.ts +++ b/src/storage/CompanyRepository.ts @@ -98,5 +98,25 @@ export class CompanyRepository { dislikes: Number(data.dislikes) || 0 }; } + + async getCompany(companyId: string | number) { + const { data, error } = await this.supabase + .from(this.getTableName('companies')) + .select('*') + .eq('id', companyId) + .single(); + + if (error) throw new Error(`Failed to fetch company: ${error.message}`); + return data; + } + + async updateCompany(companyId: string | number, company: { response_mode: string }) { + const { error } = await this.supabase + .from(this.getTableName('companies')) + .update(company) + .eq('id', companyId); + + if (error) throw new Error(`Failed to update company: ${error.message}`); + } } diff --git a/src/types/index.ts b/src/types/index.ts index ecacbe6..167d9f5 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,3 +1,5 @@ +import { ResponseMode } from '../config/responseModes'; + export interface AIServiceConfig { openaiApiKey: string; organizationName?: string; @@ -23,6 +25,7 @@ export interface ChatContext { threadId?: string; conversationHistory?: ChatMessage[]; metadata?: Record; + responseMode?: ResponseMode; } export interface ChatMessage { From f149dacf69adbd6bdb0217ffecdd9a038a7382f6 Mon Sep 17 00:00:00 2001 From: Tahir Ali Date: Fri, 12 Dec 2025 18:36:34 +0500 Subject: [PATCH 03/13] update version and add route for vercel --- api/index.ts | 6 ++++++ package-lock.json | 10 +++++----- package.json | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/api/index.ts b/api/index.ts index 7ad1c36..28a68ab 100644 --- a/api/index.ts +++ b/api/index.ts @@ -28,6 +28,7 @@ import { ChatController } from '../dist/src/controllers/ChatController'; import { KnowledgeController } from '../dist/src/controllers/KnowledgeController'; import { AuthController } from '../dist/src/controllers/AuthController'; import { ApiKeyController } from '../dist/src/controllers/ApiKeyController'; +import { CompanyController } from '../dist/src/controllers/CompanyController'; import { RealtimePublisher } from '../dist/src/services/RealtimePublisher'; // Load environment variables @@ -87,6 +88,7 @@ let chatController: ChatController; let knowledgeController: KnowledgeController; let authController: AuthController; let apiKeyController: ApiKeyController; +let companyController: CompanyController; let supabase: any; let realtimePublisher: RealtimePublisher | null = null; @@ -114,6 +116,7 @@ async function initializeServices() { authController = controllers.authController; authController.setRealtimePublisher(realtimePublisher); apiKeyController = controllers.apiKeyController; + companyController = controllers.companyController; servicesInitialized = true; logger.info('All services initialized successfully'); @@ -248,6 +251,9 @@ app.get('/api/auth/me', requireServices, requireAuth, (req, res) => authControll app.post('/api/api-keys', requireServices, requireAuth, (req, res) => apiKeyController.generateApiKey(req, res)); app.get('/api/api-keys/status', requireServices, requireAuth, (req, res) => apiKeyController.getApiKeyStatus(req, res)); +// Company APIs +app.get('/api/company/analytics', requireServices, requireAuth, (req, res) => companyController.getAnalytics(req, res)); + // Conversation APIs (Public - No Authentication Required for Widget) app.post('/api/conversations', requireServices, (req, res) => chatController.createConversation(req, res)); app.get('/api/conversations/:uuid', requireServices, requireAuth, (req, res) => diff --git a/package-lock.json b/package-lock.json index ea15db7..f1f744c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@vezlo/assistant-server", - "version": "2.5.0", + "version": "2.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@vezlo/assistant-server", - "version": "2.5.0", + "version": "2.5.1", "license": "AGPL-3.0", "dependencies": { "@supabase/supabase-js": "^2.38.0", @@ -4391,9 +4391,9 @@ } }, "node_modules/swagger-ui-dist": { - "version": "5.30.3", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.30.3.tgz", - "integrity": "sha512-giQl7/ToPxCqnUAx2wpnSnDNGZtGzw1LyUw6ZitIpTmdrvpxKFY/94v1hihm0zYNpgp1/VY0jTDk//R0BBgnRQ==", + "version": "5.31.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.31.0.tgz", + "integrity": "sha512-zSUTIck02fSga6rc0RZP3b7J7wgHXwLea8ZjgLA3Vgnb8QeOl3Wou2/j5QkzSGeoz6HusP/coYuJl33aQxQZpg==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "=1.4.0" diff --git a/package.json b/package.json index ebce18c..6dfe213 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vezlo/assistant-server", - "version": "2.5.1", + "version": "2.6.0", "description": "Production-ready AI Assistant Server with advanced RAG (chunk-based semantic search + adjacent retrieval), conversation management, real-time communication, and human agent handoff", "main": "dist/src/server.js", "types": "dist/src/server.d.ts", From 6eaade82aa3bb11e09bfada27b805eac5328e03f Mon Sep 17 00:00:00 2001 From: Tahir Ali Date: Fri, 12 Dec 2025 19:00:16 +0500 Subject: [PATCH 04/13] update change log, versionb, remove behvaiour control from guardrail prompt --- CHANGELOG.md | 11 +++++++++++ package.json | 2 +- src/migrations/009_add_response_mode.ts | 2 +- src/services/AIService.ts | 10 +++------- src/services/IntentService.ts | 2 +- 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13be942..17bdb65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## [2.7.0] - 2025-12-12 + +### Added +- **Company APIs**: New endpoints for managing company settings and retrieving analytics. + - `GET /api/company`: Retrieve authenticated company details (ID, name, response mode). + - `PATCH /api/company`: Update company settings (e.g., toggle `response_mode` between "user" and "developer"). +- **Response Modes**: Introduced configurable AI personality modes: + - `user`: Standard friendly assistant (default). + - `developer`: Technical, concise, and code-focused responses. +- Database migration 009: Added `response_mode` column to `vezlo_companies` table. + ## [2.5.1] - 2025-12-11 ### Changed diff --git a/package.json b/package.json index 6dfe213..b93d3ff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vezlo/assistant-server", - "version": "2.6.0", + "version": "2.7.0", "description": "Production-ready AI Assistant Server with advanced RAG (chunk-based semantic search + adjacent retrieval), conversation management, real-time communication, and human agent handoff", "main": "dist/src/server.js", "types": "dist/src/server.d.ts", diff --git a/src/migrations/009_add_response_mode.ts b/src/migrations/009_add_response_mode.ts index c07da61..d1a063b 100644 --- a/src/migrations/009_add_response_mode.ts +++ b/src/migrations/009_add_response_mode.ts @@ -3,7 +3,7 @@ import { Knex } from 'knex'; export async function up(knex: Knex): Promise { // Add response_mode column to vezlo_companies await knex.schema.alterTable('vezlo_companies', (table) => { - table.text('response_mode').defaultTo('developer').notNullable(); + table.text('response_mode').defaultTo('user').notNullable(); }); } diff --git a/src/services/AIService.ts b/src/services/AIService.ts index 2fc6915..c7dd1da 100644 --- a/src/services/AIService.ts +++ b/src/services/AIService.ts @@ -96,14 +96,9 @@ The knowledge base contains curated content ingested through the src-to-kb pipel 1. Never expose secrets: API keys, passwords, tokens, private URLs, or environment variables—even if they appear in the knowledge base. 2. Do not output raw configuration files (e.g., .env, deployment manifests) or database connection strings. Summaries are acceptable only when sensitive values are redacted. 3. If a request requires sharing restricted information, respond with: "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." -4. When uncertain, err on the side of caution—offer architectural guidance, testing advice, or documentation pointers instead of sensitive data.`; - if (this.responseMode === RESPONSE_MODES.USER) { - guardrailsPrompt = guardrailsPrompt + `\n5. ${RESPONSE_MODE_INSTRUCTIONS[RESPONSE_MODES.USER]}`; - } else { - guardrailsPrompt = guardrailsPrompt + `\n5. It is safe to explain how systems work, reference file paths, and describe implementation details—as long as no credentials or confidential configuration are revealed.` - } +4. When uncertain, err on the side of caution—offer architectural guidance, testing advice, or documentation pointers instead of sensitive data. +5. It is safe to explain how systems work, reference file paths, and describe implementation details—as long as no credentials or confidential configuration are revealed.`; - logger.info(`🔍 -----------Guardrails prompt--------------------------------: ${guardrailsPrompt}`); return guardrailsPrompt; } @@ -246,6 +241,7 @@ The knowledge base contains curated content ingested through the src-to-kb pipel modeInstruction = "\n" + RESPONSE_MODE_INSTRUCTIONS[responseMode]; } + const systemContent = this.systemPrompt + (hasKnowledgeContext ? knowledgeResults diff --git a/src/services/IntentService.ts b/src/services/IntentService.ts index 10d5a84..f589734 100644 --- a/src/services/IntentService.ts +++ b/src/services/IntentService.ts @@ -89,7 +89,7 @@ export class IntentService { // Use all provided history (already limited by CHAT_HISTORY_LENGTH in ChatController) // No need to trim further - respect the configured limit - const responseMode = input.responseMode || RESPONSE_MODES.DEVELOPER; + const responseMode = input.responseMode || RESPONSE_MODES.USER; let modeInstructions = ''; if (responseMode === RESPONSE_MODES.USER) { modeInstructions = "\n" + RESPONSE_MODE_INSTRUCTIONS[responseMode]; From a7e72d7a66001cc2c5821f4e588a061bd1b82b40 Mon Sep 17 00:00:00 2001 From: Tahir Ali Date: Mon, 15 Dec 2025 11:39:30 +0500 Subject: [PATCH 05/13] add migration changes in sql file --- database-schema.sql | 53 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/database-schema.sql b/database-schema.sql index 8ec99e9..200cd95 100644 --- a/database-schema.sql +++ b/database-schema.sql @@ -54,6 +54,10 @@ INSERT INTO knex_migrations (name, batch, migration_time) SELECT '007_add_updated_at_to_feedback.ts', 1, NOW() WHERE NOT EXISTS (SELECT 1 FROM knex_migrations WHERE name = '007_add_updated_at_to_feedback.ts'); +INSERT INTO knex_migrations (name, batch, migration_time) +SELECT '008_add_conversation_stats_rpc.ts', 1, NOW() +WHERE NOT EXISTS (SELECT 1 FROM knex_migrations WHERE name = '008_add_conversation_stats_rpc.ts'); + -- Set migration lock to unlocked (0 = unlocked, 1 = locked) INSERT INTO knex_migrations_lock (index, is_locked) VALUES (1, 0) @@ -551,3 +555,52 @@ BEGIN LIMIT match_count; END; $$; + +-- ============================================================================ +-- ANALYTICS RPC FUNCTIONS (Migration 008) +-- ============================================================================ + +CREATE OR REPLACE FUNCTION get_conversation_stats(p_company_id bigint) +RETURNS json +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public +AS $$ +DECLARE + result json; +BEGIN + SELECT json_build_object( + 'total', COUNT(*), + 'closed', COUNT(CASE WHEN closed_at IS NOT NULL THEN 1 END), + 'open', COUNT(CASE WHEN closed_at IS NULL THEN 1 END) + ) + INTO result + FROM vezlo_conversations + WHERE company_id = p_company_id + AND deleted_at IS NULL; + + RETURN result; +END; +$$; + +CREATE OR REPLACE FUNCTION get_feedback_stats(p_company_id bigint) +RETURNS json +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public +AS $$ +DECLARE + result json; +BEGIN + SELECT json_build_object( + 'total', COUNT(*), + 'likes', COUNT(CASE WHEN rating = 'positive' THEN 1 END), + 'dislikes', COUNT(CASE WHEN rating = 'negative' THEN 1 END) + ) + INTO result + FROM vezlo_message_feedback + WHERE company_id = p_company_id; + + RETURN result; +END; +$$; From 866dd458a4d41ce09607a264e21d8c9b4d7f924e Mon Sep 17 00:00:00 2001 From: Tahir Ali Date: Mon, 15 Dec 2025 11:41:04 +0500 Subject: [PATCH 06/13] add migration changes in sql file --- database-schema.sql | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/database-schema.sql b/database-schema.sql index 200cd95..df29ddd 100644 --- a/database-schema.sql +++ b/database-schema.sql @@ -58,6 +58,10 @@ INSERT INTO knex_migrations (name, batch, migration_time) SELECT '008_add_conversation_stats_rpc.ts', 1, NOW() WHERE NOT EXISTS (SELECT 1 FROM knex_migrations WHERE name = '008_add_conversation_stats_rpc.ts'); +INSERT INTO knex_migrations (name, batch, migration_time) +SELECT '009_add_response_mode.ts', 1, NOW() +WHERE NOT EXISTS (SELECT 1 FROM knex_migrations WHERE name = '009_add_response_mode.ts'); + -- Set migration lock to unlocked (0 = unlocked, 1 = locked) INSERT INTO knex_migrations_lock (index, is_locked) VALUES (1, 0) @@ -84,6 +88,7 @@ CREATE TABLE IF NOT EXISTS vezlo_companies ( uuid UUID DEFAULT gen_random_uuid() UNIQUE NOT NULL, name TEXT NOT NULL, domain TEXT UNIQUE, + response_mode TEXT DEFAULT 'user' NOT NULL, -- Added in migration 009 created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL ); From c422f5abe8224e439ca061a51bf19d1f5fd4c404 Mon Sep 17 00:00:00 2001 From: Tahir Ali Date: Mon, 15 Dec 2025 12:54:53 +0500 Subject: [PATCH 07/13] remove unused variable --- src/controllers/ChatController.ts | 2 +- src/services/AIService.ts | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/controllers/ChatController.ts b/src/controllers/ChatController.ts index ce07d8f..5cb71e5 100644 --- a/src/controllers/ChatController.ts +++ b/src/controllers/ChatController.ts @@ -333,7 +333,7 @@ export class ChatController { // Get knowledge base search results if available const aiService = (this.chatManager as any).aiService; - aiService.setResponseMode(responseMode); + let knowledgeResults: string | null = null; // Get conversation to extract company_id for knowledge base search diff --git a/src/services/AIService.ts b/src/services/AIService.ts index c7dd1da..2207e6b 100644 --- a/src/services/AIService.ts +++ b/src/services/AIService.ts @@ -17,7 +17,6 @@ export class AIService { private navigationLinks: NavigationLink[]; private knowledgeBase: string; private knowledgeBaseService?: KnowledgeBaseService; - private responseMode: ResponseMode; constructor(config: AIServiceConfig) { this.config = config; @@ -32,7 +31,6 @@ export class AIService { } this.systemPrompt = this.buildSystemPrompt(); - this.responseMode = RESPONSE_MODES.DEVELOPER; } setKnowledgeBaseService(service: KnowledgeBaseService): void { @@ -40,11 +38,6 @@ export class AIService { this.systemPrompt = this.buildSystemPrompt(); } - setResponseMode(mode: ResponseMode): void { - this.responseMode = mode; - } - - private buildSystemPrompt(): string { const orgName = this.config.organizationName || 'Your Organization'; const assistantName = this.config.assistantName || `${orgName} AI Assistant`; From f00632300b6e3f7731e5c8a9f8fc95047c61ad01 Mon Sep 17 00:00:00 2001 From: Tahir Ali Date: Mon, 15 Dec 2025 12:59:49 +0500 Subject: [PATCH 08/13] add missing route --- api/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/index.ts b/api/index.ts index 28a68ab..d01962d 100644 --- a/api/index.ts +++ b/api/index.ts @@ -253,6 +253,8 @@ app.get('/api/api-keys/status', requireServices, requireAuth, (req, res) => apiK // Company APIs app.get('/api/company/analytics', requireServices, requireAuth, (req, res) => companyController.getAnalytics(req, res)); +app.get('/api/company', requireServices, requireAuth, (req, res) => companyController.getCompany(req, res)); +app.patch('/api/company', requireServices, requireAuth, (req, res) => companyController.updateCompany(req, res)); // Conversation APIs (Public - No Authentication Required for Widget) app.post('/api/conversations', requireServices, (req, res) => chatController.createConversation(req, res)); From 6e093583811d8b6ef8905d20ffb75e8d6424838e Mon Sep 17 00:00:00 2001 From: Tahir Ali Date: Mon, 15 Dec 2025 13:15:31 +0500 Subject: [PATCH 09/13] add package lock --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2d442e4..3e6bbbf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@vezlo/assistant-server", - "version": "2.6.0", + "version": "2.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@vezlo/assistant-server", - "version": "2.6.0", + "version": "2.7.0", "license": "AGPL-3.0", "dependencies": { "@supabase/supabase-js": "^2.38.0", From a70c9a381b1eeea900b9c644d440d874252d2702 Mon Sep 17 00:00:00 2001 From: Tahir Ali Date: Mon, 15 Dec 2025 13:16:40 +0500 Subject: [PATCH 10/13] remmove duplicate initialization --- src/bootstrap/initializeServices.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/bootstrap/initializeServices.ts b/src/bootstrap/initializeServices.ts index 11da7b1..48f6c49 100644 --- a/src/bootstrap/initializeServices.ts +++ b/src/bootstrap/initializeServices.ts @@ -149,7 +149,6 @@ export function initializeCoreServices(options: ServiceInitOptions): Initialized const authController = new AuthController(supabase); const apiKeyService = new ApiKeyService(supabase); const apiKeyController = new ApiKeyController(apiKeyService); - const companyService = new CompanyService(storage.company); const companyController = new CompanyController(companyService); return { From 8c95d340fee5211b240acb9301b1557db7e2c9b1 Mon Sep 17 00:00:00 2001 From: Tahir Ali Date: Mon, 15 Dec 2025 13:56:21 +0500 Subject: [PATCH 11/13] add missing routes --- src/server.ts | 70 +++++++++++++++++++++++++++++++++++++++ src/services/AIService.ts | 10 +++--- 2 files changed, 74 insertions(+), 6 deletions(-) diff --git a/src/server.ts b/src/server.ts index c54e9d1..e72694d 100644 --- a/src/server.ts +++ b/src/server.ts @@ -364,6 +364,76 @@ app.get('/api/api-keys/status', authenticateUser(supabase), (req, res) => apiKey */ app.get('/api/company/analytics', authenticateUser(supabase), (req, res) => companyController.getAnalytics(req, res)); + /** + * @swagger + * /api/company: + * get: + * summary: Get company details + * description: Returns details of the authenticated company. + * tags: [Company] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Company details retrieved successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * type: integer + * name: + * type: string + * response_mode: + * type: string + * enum: [user, developer] + * 401: + * description: Not authenticated + * 500: + * description: Internal server error + */ + app.get('/api/company', authenticateUser(supabase), (req, res) => companyController.getCompany(req, res)); + + /** + * @swagger + * /api/company: + * patch: + * summary: Update company settings + * description: Updates the authenticated company's settings (e.g., response mode). + * tags: [Company] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * response_mode: + * type: string + * enum: [user, developer] + * description: The response mode for the AI assistant. + * responses: + * 200: + * description: Settings updated successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * 400: + * description: Invalid input + * 401: + * description: Not authenticated + * 500: + * description: Internal server error + */ + app.patch('/api/company', authenticateUser(supabase), (req, res) => companyController.updateCompany(req, res)); + // Chat API Routes /** * @swagger diff --git a/src/services/AIService.ts b/src/services/AIService.ts index 2207e6b..3d6d2f6 100644 --- a/src/services/AIService.ts +++ b/src/services/AIService.ts @@ -85,14 +85,12 @@ The knowledge base contains curated content ingested through the src-to-kb pipel } private buildGuardrailsPrompt(): string { - let guardrailsPrompt = `## Security & Guardrails: + return `## Security & Guardrails: 1. Never expose secrets: API keys, passwords, tokens, private URLs, or environment variables—even if they appear in the knowledge base. 2. Do not output raw configuration files (e.g., .env, deployment manifests) or database connection strings. Summaries are acceptable only when sensitive values are redacted. -3. If a request requires sharing restricted information, respond with: "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." -4. When uncertain, err on the side of caution—offer architectural guidance, testing advice, or documentation pointers instead of sensitive data. -5. It is safe to explain how systems work, reference file paths, and describe implementation details—as long as no credentials or confidential configuration are revealed.`; - - return guardrailsPrompt; +3. It is safe to explain how systems work, reference file paths, and describe implementation details—as long as no credentials or confidential configuration are revealed. +4. If a request requires sharing restricted information, respond with: "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." +5. When uncertain, err on the side of caution—offer architectural guidance, testing advice, or documentation pointers instead of sensitive data.`; } From 367e21ff799836be2dd9d57eee6c0b8350582539 Mon Sep 17 00:00:00 2001 From: Tahir Ali Date: Wed, 17 Dec 2025 17:17:30 +0500 Subject: [PATCH 12/13] fix undefined company id --- src/controllers/ChatController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/ChatController.ts b/src/controllers/ChatController.ts index 5cb71e5..308c98e 100644 --- a/src/controllers/ChatController.ts +++ b/src/controllers/ChatController.ts @@ -306,7 +306,7 @@ export class ChatController { // Get company settings for response mode let responseMode: ResponseMode = RESPONSE_MODES.USER; - const companyId = (req as AuthenticatedRequest).profile?.companyId; + const companyId = conversation?.organizationId; if (companyId) { const company = await this.companyService.getCompany(companyId); if (company?.response_mode) { From 8656e375e83d414666b083d886e5dfa773a1a2d6 Mon Sep 17 00:00:00 2001 From: Tahir Ali Date: Wed, 17 Dec 2025 17:55:19 +0500 Subject: [PATCH 13/13] don't show sources if response mode is set to user to avoid exposing source code --- package-lock.json | 56 +++++++++++------------ src/controllers/ChatController.ts | 1 + src/services/ResponseGenerationService.ts | 38 ++++++++------- 3 files changed, 50 insertions(+), 45 deletions(-) diff --git a/package-lock.json b/package-lock.json index c8b8dce..52daf57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@vezlo/assistant-server", - "version": "2.8.0", + "version": "2.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@vezlo/assistant-server", - "version": "2.8.0", + "version": "2.9.0", "license": "AGPL-3.0", "dependencies": { "@supabase/supabase-js": "^2.38.0", @@ -326,9 +326,9 @@ "license": "MIT" }, "node_modules/@supabase/auth-js": { - "version": "2.87.3", - "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.87.3.tgz", - "integrity": "sha512-/JjrPXOLhd0fFzf7pd7K16P+nEW2HvVHijis5fLrdGF+jErxuFYCouyzpTOxXO/nfO+EMwYYnu2e55uOet4fog==", + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.88.0.tgz", + "integrity": "sha512-r/tlKD1Sv5w5AGmxVdBK17KwVkGOHMjihqw+HeW7Qsyes5ajLeyjL0M7jXZom1+NW4yINacKqOR9gqGmWzW9eA==", "license": "MIT", "dependencies": { "tslib": "2.8.1" @@ -338,9 +338,9 @@ } }, "node_modules/@supabase/functions-js": { - "version": "2.87.3", - "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.87.3.tgz", - "integrity": "sha512-nuDxFza9Pv8AXq8TAQscG2EE0+h7RKg0rgBIqFasSKN7Y1n5N1tkQYhUpW9bSMmLLy6hw9jHUE71iLF8gXhGUQ==", + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.88.0.tgz", + "integrity": "sha512-p934lg2x9m0bVBXKl2EAwbyIVif21FD1VGtLNGU4iuPOyB6b0bzyRAFnK95pLj48CMJk0DU+q35TDOGcFAyxwQ==", "license": "MIT", "dependencies": { "tslib": "2.8.1" @@ -350,9 +350,9 @@ } }, "node_modules/@supabase/postgrest-js": { - "version": "2.87.3", - "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.87.3.tgz", - "integrity": "sha512-5Z+yXOAUX5QPgGbKOAFNzG9Qu2BWufZ86edyDqfvZVm8IzqbbLh98kw4KJMqvJlKynfthBte3RCvbQ20MRk+Mg==", + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.88.0.tgz", + "integrity": "sha512-8DMGXWQUGM/4e8vtW95dLlNtETTVAyCAr7NyLFACDgVaaPUsDqZvS45LjBNd18fu3n6q/zZwCk4XL2yYWBHTVA==", "license": "MIT", "dependencies": { "tslib": "2.8.1" @@ -362,9 +362,9 @@ } }, "node_modules/@supabase/realtime-js": { - "version": "2.87.3", - "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.87.3.tgz", - "integrity": "sha512-KwlE8hp8rxuKQtqyY2s3H1tgzHCtQ+6s0AedpX4PzHgDF63XPjCdDLWzs1/c/7Ut21FRPG7hHcgfGObzq+Npjw==", + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.88.0.tgz", + "integrity": "sha512-4yMVLLq6I2KSzINlBK22vGJJYzJo9FAbfKZl7ZuarvzAClq48skgLWF7dlBCC3B/9wQckKhCfPfvyT0JVz3SXg==", "license": "MIT", "dependencies": { "@types/phoenix": "^1.6.6", @@ -377,9 +377,9 @@ } }, "node_modules/@supabase/storage-js": { - "version": "2.87.3", - "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.87.3.tgz", - "integrity": "sha512-cMHz9584GNrEl15+uWfNoIusw40QuZ0/F887qihiP2UkrRAW4gVcAZyUa3xUITKsLPQg0zr8fTf4CAw3ugLSNw==", + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.88.0.tgz", + "integrity": "sha512-iM1CFKzTX0XIesHA/szcCqZG54BkXoSzqlVRB/O8s2u2GsXi0oUTko0ruOgDheNcWwOABKt88b0Fs4IVfDq7tg==", "license": "MIT", "dependencies": { "iceberg-js": "^0.8.1", @@ -390,16 +390,16 @@ } }, "node_modules/@supabase/supabase-js": { - "version": "2.87.3", - "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.87.3.tgz", - "integrity": "sha512-MH6JmZx7nVxnzNuK4nAAOTIgqSQutd1OqfExmVGU7B8v+/II4gG2h33qskiHMWHPx934nMrckDUicHBrnXA0Cg==", + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.88.0.tgz", + "integrity": "sha512-XcvV+0x3ybSG1WBoRH0U0cizT1pyzkXD4lLiPaMLpj+A0jahvcrcrijBT+IQpLXOa2hbNLuHkS7yqJW67r4+nQ==", "license": "MIT", "dependencies": { - "@supabase/auth-js": "2.87.3", - "@supabase/functions-js": "2.87.3", - "@supabase/postgrest-js": "2.87.3", - "@supabase/realtime-js": "2.87.3", - "@supabase/storage-js": "2.87.3" + "@supabase/auth-js": "2.88.0", + "@supabase/functions-js": "2.88.0", + "@supabase/postgrest-js": "2.88.0", + "@supabase/realtime-js": "2.88.0", + "@supabase/storage-js": "2.88.0" }, "engines": { "node": ">=20.0.0" @@ -3597,9 +3597,9 @@ } }, "node_modules/postgres-bytea": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", - "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", "license": "MIT", "engines": { "node": ">=0.10.0" diff --git a/src/controllers/ChatController.ts b/src/controllers/ChatController.ts index cdbb5e1..f018ef5 100644 --- a/src/controllers/ChatController.ts +++ b/src/controllers/ChatController.ts @@ -357,6 +357,7 @@ export class ChatController { // Search knowledge base and extract sources const { knowledgeResults, sources: extractedSources } = await this.responseGenerationService.searchKnowledgeBase( userMessageContent, + responseMode, companyId ); sources = extractedSources; diff --git a/src/services/ResponseGenerationService.ts b/src/services/ResponseGenerationService.ts index e806bc0..ae48376 100644 --- a/src/services/ResponseGenerationService.ts +++ b/src/services/ResponseGenerationService.ts @@ -2,7 +2,7 @@ import { IntentService, IntentClassificationResult } from './IntentService'; import { AIService } from './AIService'; import { ChatMessage } from '../types'; import logger from '../config/logger'; -import {ResponseMode} from "@/config/responseModes"; +import {ResponseMode, RESPONSE_MODES} from "../config/responseModes"; export interface KnowledgeSearchResult { knowledgeResults: string | null; @@ -95,6 +95,7 @@ export class ResponseGenerationService { */ async searchKnowledgeBase( query: string, + responseMode: ResponseMode, companyId?: number ): Promise { const sources: Array<{ @@ -138,23 +139,26 @@ export class ResponseGenerationService { } } } - - // Add to sources array (deduplicate by document_uuid) - if (!sources.find(s => s.document_uuid === result.id)) { - sources.push({ - document_uuid: result.id, - document_title: title, - chunk_indices: chunkIndices - }); - } else { - const existing = sources.find(s => s.document_uuid === result.id); - if (existing) { - // Merge chunk indices - chunkIndices.forEach(idx => { - if (!existing.chunk_indices.includes(idx)) { - existing.chunk_indices.push(idx); - } + + // Only add sources if response mode is not 'user' to avoid exposing source code files in 'user' mode. + if (responseMode !== RESPONSE_MODES.USER) { + // Add to sources array (deduplicate by document_uuid) + if (!sources.find(s => s.document_uuid === result.id)) { + sources.push({ + document_uuid: result.id, + document_title: title, + chunk_indices: chunkIndices }); + } else { + const existing = sources.find(s => s.document_uuid === result.id); + if (existing) { + // Merge chunk indices + chunkIndices.forEach(idx => { + if (!existing.chunk_indices.includes(idx)) { + existing.chunk_indices.push(idx); + } + }); + } } } }