diff --git a/CHANGELOG.md b/CHANGELOG.md index a9529a7..de8734b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## [2.9.0] - 2025-12-17 + +### 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 010: Added `response_mode` column to `vezlo_companies` table. + ## [2.8.0] - 2025-12-17 ### Added diff --git a/api/index.ts b/api/index.ts index 8b2b2ad..78edcdf 100644 --- a/api/index.ts +++ b/api/index.ts @@ -314,6 +314,9 @@ app.get('/api/api-keys/status', requireServices, requireAuth, (req, res) => apiK */ 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)); app.get('/api/conversations/:uuid', requireServices, requireAuth, (req, res) => diff --git a/database-schema.sql b/database-schema.sql index a4e2ad1..56c3e43 100644 --- a/database-schema.sql +++ b/database-schema.sql @@ -54,14 +54,18 @@ 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) +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) +INSERT INTO knex_migrations (name, batch, migration_time) SELECT '009_add_archived_at_column.ts', 1, NOW() WHERE NOT EXISTS (SELECT 1 FROM knex_migrations WHERE name = '009_add_archived_at_column.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) @@ -88,6 +92,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 ); @@ -584,7 +589,7 @@ BEGIN FROM vezlo_conversations WHERE company_id = p_company_id AND deleted_at IS NULL; - + RETURN result; END; $$; @@ -606,7 +611,7 @@ BEGIN INTO result FROM vezlo_message_feedback WHERE company_id = p_company_id; - + RETURN result; END; $$; 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/package.json b/package.json index 1797dbd..60cc8e9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vezlo/assistant-server", - "version": "2.8.0", + "version": "2.9.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/bootstrap/initializeServices.ts b/src/bootstrap/initializeServices.ts index 5eb9df0..190b2ff 100644 --- a/src/bootstrap/initializeServices.ts +++ b/src/bootstrap/initializeServices.ts @@ -123,6 +123,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, @@ -142,7 +143,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 @@ -152,7 +153,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 59d8e3d..f018ef5 100644 --- a/src/controllers/ChatController.ts +++ b/src/controllers/ChatController.ts @@ -9,12 +9,15 @@ import { ChatConversation, ChatMessage, StoredChatMessage } from '../types'; import { RealtimePublisher } from '../services/RealtimePublisher'; import { ResponseGenerationService } from '../services/ResponseGenerationService'; import { ResponseStreamingService } from '../services/ResponseStreamingService'; +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; private responseGenerationService: ResponseGenerationService; @@ -24,6 +27,7 @@ export class ChatController { chatManager: ChatManager, storage: UnifiedStorage, supabase: SupabaseClient, + companyService: CompanyService, options: { historyLength?: number; intentService?: IntentService; realtimePublisher?: RealtimePublisher } = {} ) { this.chatManager = chatManager; @@ -33,7 +37,8 @@ export class ChatController { this.chatHistoryLength = typeof historyLength === 'number' && historyLength > 0 ? historyLength : 2; this.intentService = options.intentService; this.realtimePublisher = options.realtimePublisher; - + this.companyService = companyService; + // Initialize services const aiService = (chatManager as any).aiService; this.responseGenerationService = new ResponseGenerationService( @@ -317,8 +322,18 @@ export class ChatController { // Set up Server-Sent Events (SSE) headers for streaming this.responseStreamingService.setupSSEHeaders(res); + // Get company settings for response mode + let responseMode: ResponseMode = RESPONSE_MODES.USER; + const companyId = conversation?.organizationId; + 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.responseGenerationService.classifyIntent(userMessageContent, messages); + const intentResult = await this.responseGenerationService.classifyIntent(userMessageContent, messages, responseMode); const intentResponse = this.responseGenerationService.handleIntentResult(intentResult, userMessageContent); let accumulatedContent = ''; @@ -334,7 +349,7 @@ export class ChatController { } else { // Knowledge intent - proceed with RAG flow and stream AI response logger.info('📚 Streaming knowledge-based response'); - + // Get conversation to extract company_id for knowledge base search const companyIdRaw = req.profile?.companyId || conversation?.organizationId; const companyId = companyIdRaw ? (typeof companyIdRaw === 'string' ? parseInt(companyIdRaw, 10) : companyIdRaw) : undefined; @@ -342,12 +357,13 @@ export class ChatController { // Search knowledge base and extract sources const { knowledgeResults, sources: extractedSources } = await this.responseGenerationService.searchKnowledgeBase( userMessageContent, + responseMode, companyId ); sources = extractedSources; // Build context for AI - const chatContext = this.responseGenerationService.buildChatContext(messages, knowledgeResults); + const chatContext = this.responseGenerationService.buildChatContext(messages, responseMode, knowledgeResults); const aiService = this.responseGenerationService.getAIService(); if (!aiService) { diff --git a/src/controllers/CompanyController.ts b/src/controllers/CompanyController.ts index e3008cc..a24bdef 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/010_add_response_mode.ts b/src/migrations/010_add_response_mode.ts new file mode 100644 index 0000000..d1a063b --- /dev/null +++ b/src/migrations/010_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('user').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 563e624..fbf16c0 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 a21b273..3d6d2f6 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; @@ -37,7 +38,6 @@ export class AIService { this.systemPrompt = this.buildSystemPrompt(); } - private buildSystemPrompt(): string { const orgName = this.config.organizationName || 'Your Organization'; const assistantName = this.config.assistantName || `${orgName} AI Assistant`; @@ -93,6 +93,7 @@ The knowledge base contains curated content ingested through the src-to-kb pipel 5. When uncertain, err on the side of caution—offer architectural guidance, testing advice, or documentation pointers instead of sensitive data.`; } + async generateResponse(message: string, context?: ChatContext | any): Promise { try { let knowledgeResults: string = ''; @@ -128,6 +129,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 +226,18 @@ 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..f589734 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.USER; + 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/services/ResponseGenerationService.ts b/src/services/ResponseGenerationService.ts index 40e190d..ae48376 100644 --- a/src/services/ResponseGenerationService.ts +++ b/src/services/ResponseGenerationService.ts @@ -2,6 +2,7 @@ import { IntentService, IntentClassificationResult } from './IntentService'; import { AIService } from './AIService'; import { ChatMessage } from '../types'; import logger from '../config/logger'; +import {ResponseMode, RESPONSE_MODES} from "../config/responseModes"; export interface KnowledgeSearchResult { knowledgeResults: string | null; @@ -44,7 +45,7 @@ export class ResponseGenerationService { /** * Classify user intent */ - async classifyIntent(message: string, history: ChatMessage[]): Promise { + async classifyIntent(message: string, history: ChatMessage[], responseMode: ResponseMode): Promise { if (!this.intentService) { return { intent: 'knowledge', @@ -58,7 +59,8 @@ export class ResponseGenerationService { return this.intentService.classify({ message, - conversationHistory: resolvedHistory + conversationHistory: resolvedHistory, + responseMode }); } @@ -93,6 +95,7 @@ export class ResponseGenerationService { */ async searchKnowledgeBase( query: string, + responseMode: ResponseMode, companyId?: number ): Promise { const sources: Array<{ @@ -136,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); + } + }); + } } } } @@ -185,17 +191,20 @@ export class ResponseGenerationService { */ buildChatContext( messages: ChatMessage[], - knowledgeResults?: string | null + responseMode: ResponseMode, + knowledgeResults?: string | null, ): { conversationHistory: Array<{ role: 'user' | 'assistant' | 'system'; content: string }>; knowledgeResults?: string; + responseMode: ResponseMode } { return { conversationHistory: messages.map(msg => ({ role: msg.role as 'user' | 'assistant' | 'system', content: msg.content })), - knowledgeResults: knowledgeResults ?? undefined + knowledgeResults: knowledgeResults ?? undefined, + responseMode }; } diff --git a/src/storage/CompanyRepository.ts b/src/storage/CompanyRepository.ts index 72b7ea1..a75896b 100644 --- a/src/storage/CompanyRepository.ts +++ b/src/storage/CompanyRepository.ts @@ -105,7 +105,7 @@ export class CompanyRepository { .eq(`${conversationsTable}.company_id`, companyId); if (error) throw new Error(`Failed to fetch ${messageType} message count: ${error.message}`); - + return count || 0; } @@ -122,7 +122,7 @@ export class CompanyRepository { .neq('type', 'system'); if (error) throw new Error(`Failed to fetch total message count: ${error.message}`); - + return count || 0; } @@ -141,5 +141,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 16e482c..3b15b52 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 {