Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 3 additions & 0 deletions api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down
13 changes: 9 additions & 4 deletions database-schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
);
Expand Down Expand Up @@ -584,7 +589,7 @@ BEGIN
FROM vezlo_conversations
WHERE company_id = p_company_id
AND deleted_at IS NULL;

RETURN result;
END;
$$;
Expand All @@ -606,7 +611,7 @@ BEGIN
INTO result
FROM vezlo_message_feedback
WHERE company_id = p_company_id;

RETURN result;
END;
$$;
56 changes: 28 additions & 28 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
4 changes: 2 additions & 2 deletions src/bootstrap/initializeServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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 {
Expand Down
11 changes: 11 additions & 0 deletions src/config/responseModes.ts
Original file line number Diff line number Diff line change
@@ -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."
};
24 changes: 20 additions & 4 deletions src/controllers/ChatController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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(
Expand Down Expand Up @@ -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 = '';
Expand All @@ -334,20 +349,21 @@ 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;

// 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) {
Expand Down
48 changes: 48 additions & 0 deletions src/controllers/CompanyController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -31,4 +32,51 @@ export class CompanyController {
});
}
}

async getCompany(req: AuthenticatedRequest, res: Response): Promise<void> {
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<void> {
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<string, any> = {};
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' });
}
}
}
15 changes: 15 additions & 0 deletions src/migrations/010_add_response_mode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Knex } from 'knex';

export async function up(knex: Knex): Promise<void> {
// 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<void> {
await knex.schema.alterTable('vezlo_companies', (table) => {
table.dropColumn('response_mode');
});
}

Loading