From 33eae50bd31ef42301f639e9d5490c0813329744 Mon Sep 17 00:00:00 2001 From: samanhappy Date: Sat, 20 Dec 2025 12:16:09 +0800 Subject: [PATCH] Refactor smart routing configuration and async database handling (#519) --- frontend/src/pages/SettingsPage.tsx | 37 +++++------ src/controllers/serverController.ts | 17 +++++- src/db/connection.ts | 65 +++++++++++--------- src/services/vectorSearchService.ts | 95 +++++++++++++---------------- src/utils/smartRouting.ts | 12 ++-- 5 files changed, 122 insertions(+), 104 deletions(-) diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index 91435e4..f431039 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -1233,24 +1233,27 @@ const SettingsPage: React.FC = () => { /> -
-
-

- * - {t('settings.dbUrl')} -

+ {/* hide when DB_URL env is set */} + {smartRoutingConfig.dbUrl !== '${DB_URL}' && ( +
+
+

+ * + {t('settings.dbUrl')} +

+
+
+ handleSmartRoutingConfigChange('dbUrl', e.target.value)} + placeholder={t('settings.dbUrlPlaceholder')} + className="flex-1 mt-1 block w-full py-2 px-3 border rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm border-gray-300 form-input" + disabled={loading} + /> +
-
- handleSmartRoutingConfigChange('dbUrl', e.target.value)} - placeholder={t('settings.dbUrlPlaceholder')} - className="flex-1 mt-1 block w-full py-2 px-3 border rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm border-gray-300 form-input" - disabled={loading} - /> -
-
+ )}
diff --git a/src/controllers/serverController.ts b/src/controllers/serverController.ts index 9f6dff3..5433dd4 100644 --- a/src/controllers/serverController.ts +++ b/src/controllers/serverController.ts @@ -66,6 +66,20 @@ export const getAllSettings = async (_: Request, res: Response): Promise = const systemConfigDao = getSystemConfigDao(); const systemConfig = await systemConfigDao.get(); + // Ensure smart routing config has DB URL set if environment variable is present + const dbUrlEnv = process.env.DB_URL || ''; + if (!systemConfig.smartRouting) { + systemConfig.smartRouting = { + enabled: false, + dbUrl: dbUrlEnv ? '${DB_URL}' : '', + openaiApiBaseUrl: '', + openaiApiKey: '', + openaiApiEmbeddingModel: '', + }; + } else if (!systemConfig.smartRouting.dbUrl) { + systemConfig.smartRouting.dbUrl = dbUrlEnv ? '${DB_URL}' : ''; + } + // Get bearer auth keys from DAO const bearerKeyDao = getBearerKeyDao(); const bearerKeys = await bearerKeyDao.findAll(); @@ -978,7 +992,8 @@ export const updateSystemConfig = async (req: Request, res: Response): Promise = }; // Get database URL from smart routing config or fallback to environment variable -const getDatabaseUrl = (): string => { - return getSmartRoutingConfig().dbUrl; +const getDatabaseUrl = async (): Promise => { + return (await getSmartRoutingConfig()).dbUrl; }; -// Default database configuration -const defaultConfig: DataSourceOptions = { - type: 'postgres', - url: getDatabaseUrl(), - synchronize: true, - entities: entities, - subscribers: [VectorEmbeddingSubscriber], +// Default database configuration (without URL - will be set during initialization) +const getDefaultConfig = async (): Promise => { + return { + type: 'postgres', + url: await getDatabaseUrl(), + synchronize: true, + entities: entities, + subscribers: [VectorEmbeddingSubscriber], + }; }; -// AppDataSource is the TypeORM data source -let appDataSource = new DataSource(defaultConfig); +// AppDataSource is the TypeORM data source (initialized with empty config, will be updated) +let appDataSource: DataSource | null = null; // Global promise to track initialization status let initializationPromise: Promise | null = null; // Function to create a new DataSource with updated configuration -export const updateDataSourceConfig = (): DataSource => { - const newConfig: DataSourceOptions = { - ...defaultConfig, - url: getDatabaseUrl(), - }; +export const updateDataSourceConfig = async (): Promise => { + const newConfig = await getDefaultConfig(); // If the configuration has changed, we need to create a new DataSource - const currentUrl = (appDataSource.options as any).url; - if (currentUrl !== newConfig.url) { - console.log('Database URL configuration changed, updating DataSource...'); + if (appDataSource) { + const currentUrl = (appDataSource.options as any).url; + const newUrl = (newConfig as any).url; + if (currentUrl !== newUrl) { + console.log('Database URL configuration changed, updating DataSource...'); + appDataSource = new DataSource(newConfig); + // Reset initialization promise when configuration changes + initializationPromise = null; + } + } else { + // First time initialization appDataSource = new DataSource(newConfig); - // Reset initialization promise when configuration changes - initializationPromise = null; } return appDataSource; @@ -65,6 +70,9 @@ export const updateDataSourceConfig = (): DataSource => { // Get the current AppDataSource instance export const getAppDataSource = (): DataSource => { + if (!appDataSource) { + throw new Error('Database not initialized. Call initializeDatabase() first.'); + } return appDataSource; }; @@ -72,7 +80,7 @@ export const getAppDataSource = (): DataSource => { export const reconnectDatabase = async (): Promise => { try { // Close existing connection if it exists - if (appDataSource.isInitialized) { + if (appDataSource && appDataSource.isInitialized) { console.log('Closing existing database connection...'); await appDataSource.destroy(); } @@ -81,7 +89,7 @@ export const reconnectDatabase = async (): Promise => { initializationPromise = null; // Update configuration and reconnect - appDataSource = updateDataSourceConfig(); + appDataSource = await updateDataSourceConfig(); return await initializeDatabase(); } catch (error) { console.error('Error during database reconnection:', error); @@ -98,7 +106,7 @@ export const initializeDatabase = async (): Promise => { } // If already initialized, return the existing instance - if (appDataSource.isInitialized) { + if (appDataSource && appDataSource.isInitialized) { console.log('Database already initialized, returning existing instance'); return Promise.resolve(appDataSource); } @@ -122,7 +130,7 @@ export const initializeDatabase = async (): Promise => { const performDatabaseInitialization = async (): Promise => { try { // Update configuration before initializing - appDataSource = updateDataSourceConfig(); + appDataSource = await updateDataSourceConfig(); if (!appDataSource.isInitialized) { console.log('Initializing database connection...'); @@ -250,7 +258,8 @@ const performDatabaseInitialization = async (): Promise => { console.log('Database connection established successfully.'); // Run one final setup check after schema synchronization is done - if (defaultConfig.synchronize) { + const config = await getDefaultConfig(); + if (config.synchronize) { try { console.log('Running final vector configuration check...'); @@ -325,12 +334,12 @@ const performDatabaseInitialization = async (): Promise => { // Get database connection status export const isDatabaseConnected = (): boolean => { - return appDataSource.isInitialized; + return appDataSource ? appDataSource.isInitialized : false; }; // Close database connection export const closeDatabase = async (): Promise => { - if (appDataSource.isInitialized) { + if (appDataSource && appDataSource.isInitialized) { await appDataSource.destroy(); console.log('Database connection closed.'); } diff --git a/src/services/vectorSearchService.ts b/src/services/vectorSearchService.ts index 537180e..1706f7b 100644 --- a/src/services/vectorSearchService.ts +++ b/src/services/vectorSearchService.ts @@ -6,8 +6,8 @@ import { getSmartRoutingConfig } from '../utils/smartRouting.js'; import OpenAI from 'openai'; // Get OpenAI configuration from smartRouting settings or fallback to environment variables -const getOpenAIConfig = () => { - const smartRoutingConfig = getSmartRoutingConfig(); +const getOpenAIConfig = async () => { + const smartRoutingConfig = await getSmartRoutingConfig(); return { apiKey: smartRoutingConfig.openaiApiKey, baseURL: smartRoutingConfig.openaiApiBaseUrl, @@ -34,8 +34,8 @@ const getDimensionsForModel = (model: string): number => { }; // Initialize the OpenAI client with smartRouting configuration -const getOpenAIClient = () => { - const config = getOpenAIConfig(); +const getOpenAIClient = async () => { + const config = await getOpenAIConfig(); return new OpenAI({ apiKey: config.apiKey, // Get API key from smartRouting settings or environment variables baseURL: config.baseURL, // Get base URL from smartRouting settings or fallback to default @@ -53,32 +53,26 @@ const getOpenAIClient = () => { * @returns Promise with vector embedding as number array */ async function generateEmbedding(text: string): Promise { - try { - const config = getOpenAIConfig(); - const openai = getOpenAIClient(); + const config = await getOpenAIConfig(); + const openai = await getOpenAIClient(); - // Check if API key is configured - if (!openai.apiKey) { - console.warn('OpenAI API key is not configured. Using fallback embedding method.'); - return generateFallbackEmbedding(text); - } - - // Truncate text if it's too long (OpenAI has token limits) - const truncatedText = text.length > 8000 ? text.substring(0, 8000) : text; - - // Call OpenAI's embeddings API - const response = await openai.embeddings.create({ - model: config.embeddingModel, // Modern model with better performance - input: truncatedText, - }); - - // Return the embedding - return response.data[0].embedding; - } catch (error) { - console.error('Error generating embedding:', error); - console.warn('Falling back to simple embedding method'); + // Check if API key is configured + if (!openai.apiKey) { + console.warn('OpenAI API key is not configured. Using fallback embedding method.'); return generateFallbackEmbedding(text); } + + // Truncate text if it's too long (OpenAI has token limits) + const truncatedText = text.length > 8000 ? text.substring(0, 8000) : text; + + // Call OpenAI's embeddings API + const response = await openai.embeddings.create({ + model: config.embeddingModel, // Modern model with better performance + input: truncatedText, + }); + + // Return the embedding + return response.data[0].embedding; } /** @@ -198,12 +192,12 @@ export const saveToolsAsVectorEmbeddings = async ( return; } - const smartRoutingConfig = getSmartRoutingConfig(); + const smartRoutingConfig = await getSmartRoutingConfig(); if (!smartRoutingConfig.enabled) { return; } - const config = getOpenAIConfig(); + const config = await getOpenAIConfig(); const vectorRepository = getRepositoryFactory( 'vectorEmbeddings', )() as VectorEmbeddingRepository; @@ -227,31 +221,26 @@ export const saveToolsAsVectorEmbeddings = async ( .filter(Boolean) .join(' '); - try { - // Generate embedding - const embedding = await generateEmbedding(searchableText); + // Generate embedding + const embedding = await generateEmbedding(searchableText); - // Check database compatibility before saving - await checkDatabaseVectorDimensions(embedding.length); + // Check database compatibility before saving + await checkDatabaseVectorDimensions(embedding.length); - // Save embedding - await vectorRepository.saveEmbedding( - 'tool', - `${serverName}:${tool.name}`, - searchableText, - embedding, - { - serverName, - toolName: tool.name, - description: tool.description, - inputSchema: tool.inputSchema, - }, - config.embeddingModel, // Store the model used for this embedding - ); - } catch (toolError) { - console.error(`Error processing tool ${tool.name} for server ${serverName}:`, toolError); - // Continue with the next tool rather than failing the whole batch - } + // Save embedding + await vectorRepository.saveEmbedding( + 'tool', + `${serverName}:${tool.name}`, + searchableText, + embedding, + { + serverName, + toolName: tool.name, + description: tool.description, + inputSchema: tool.inputSchema, + }, + config.embeddingModel, // Store the model used for this embedding + ); } console.log(`Saved ${tools.length} tool embeddings for server: ${serverName}`); @@ -381,7 +370,7 @@ export const getAllVectorizedTools = async ( }> > => { try { - const config = getOpenAIConfig(); + const config = await getOpenAIConfig(); const vectorRepository = getRepositoryFactory( 'vectorEmbeddings', )() as VectorEmbeddingRepository; diff --git a/src/utils/smartRouting.ts b/src/utils/smartRouting.ts index d6bc32f..5f61c7c 100644 --- a/src/utils/smartRouting.ts +++ b/src/utils/smartRouting.ts @@ -1,4 +1,5 @@ -import { loadSettings, expandEnvVars } from '../config/index.js'; +import { expandEnvVars } from '../config/index.js'; +import { getSystemConfigDao } from '../dao/DaoFactory.js'; /** * Smart routing configuration interface @@ -22,10 +23,11 @@ export interface SmartRoutingConfig { * * @returns {SmartRoutingConfig} Complete smart routing configuration */ -export function getSmartRoutingConfig(): SmartRoutingConfig { - const settings = loadSettings(); - const smartRoutingSettings: Partial = - settings.systemConfig?.smartRouting || {}; +export async function getSmartRoutingConfig(): Promise { + // Get system config from DAO + const systemConfigDao = getSystemConfigDao(); + const systemConfig = await systemConfigDao.get(); + const smartRoutingSettings: Partial = systemConfig.smartRouting || {}; return { // Enabled status - check multiple environment variables