mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 02:16:05 +01:00 
			
		
		
		
	feat(llm): for sure overcomplicate what should be a very simple thing
This commit is contained in:
		| @@ -4,6 +4,7 @@ import { initEmbeddings } from "./index.js"; | |||||||
| import providerManager from "../providers/providers.js"; | import providerManager from "../providers/providers.js"; | ||||||
| import sqlInit from "../../sql_init.js"; | import sqlInit from "../../sql_init.js"; | ||||||
| import sql from "../../sql.js"; | import sql from "../../sql.js"; | ||||||
|  | import { validateProviders, logValidationResults, hasWorkingEmbeddingProviders } from "../provider_validation.js"; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Reset any stuck embedding queue items that were left in processing state |  * Reset any stuck embedding queue items that were left in processing state | ||||||
| @@ -45,9 +46,18 @@ export async function initializeEmbeddings() { | |||||||
|  |  | ||||||
|         // Start the embedding system if AI is enabled |         // Start the embedding system if AI is enabled | ||||||
|         if (await options.getOptionBool('aiEnabled')) { |         if (await options.getOptionBool('aiEnabled')) { | ||||||
|             // Embedding providers will be created on-demand when needed |             // Validate providers before starting the embedding system | ||||||
|             await initEmbeddings(); |             log.info("Validating AI providers before starting embedding system..."); | ||||||
|             log.info("Embedding system initialized successfully."); |             const validation = await validateProviders(); | ||||||
|  |             logValidationResults(validation); | ||||||
|  |              | ||||||
|  |             if (await hasWorkingEmbeddingProviders()) { | ||||||
|  |                 // Embedding providers will be created on-demand when needed | ||||||
|  |                 await initEmbeddings(); | ||||||
|  |                 log.info("Embedding system initialized successfully."); | ||||||
|  |             } else { | ||||||
|  |                 log.info("Embedding system not started: No working embedding providers found. Please configure at least one AI provider (OpenAI, Ollama, or Voyage) to use embedding features."); | ||||||
|  |             } | ||||||
|         } else { |         } else { | ||||||
|             log.info("Embedding system disabled (AI features are turned off)."); |             log.info("Embedding system disabled (AI features are turned off)."); | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -22,6 +22,7 @@ import sqlInit from "../sql_init.js"; | |||||||
| import { CONTEXT_PROMPTS } from './constants/llm_prompt_constants.js'; | import { CONTEXT_PROMPTS } from './constants/llm_prompt_constants.js'; | ||||||
| import { SEARCH_CONSTANTS } from './constants/search_constants.js'; | import { SEARCH_CONSTANTS } from './constants/search_constants.js'; | ||||||
| import { isNoteExcludedFromAI } from "./utils/ai_exclusion_utils.js"; | import { isNoteExcludedFromAI } from "./utils/ai_exclusion_utils.js"; | ||||||
|  | import { hasWorkingEmbeddingProviders } from "./provider_validation.js"; | ||||||
|  |  | ||||||
| export class IndexService { | export class IndexService { | ||||||
|     private initialized = false; |     private initialized = false; | ||||||
| @@ -61,9 +62,15 @@ export class IndexService { | |||||||
|             } |             } | ||||||
|  |  | ||||||
|             // Check if embedding system is ready |             // Check if embedding system is ready | ||||||
|  |             if (!(await hasWorkingEmbeddingProviders())) { | ||||||
|  |                 log.info("Index service: No working embedding providers available, skipping initialization"); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |              | ||||||
|             const providers = await providerManager.getEnabledEmbeddingProviders(); |             const providers = await providerManager.getEnabledEmbeddingProviders(); | ||||||
|             if (!providers || providers.length === 0) { |             if (!providers || providers.length === 0) { | ||||||
|                 throw new Error("No embedding providers available"); |                 log.info("Index service: No enabled embedding providers, skipping initialization"); | ||||||
|  |                 return; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             // Check if this instance should process embeddings |             // Check if this instance should process embeddings | ||||||
| @@ -866,7 +873,11 @@ export class IndexService { | |||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             // Verify providers are available (this will create them on-demand if needed) |             // Verify providers are available | ||||||
|  |             if (!(await hasWorkingEmbeddingProviders())) { | ||||||
|  |                 throw new Error("No working embedding providers available"); | ||||||
|  |             } | ||||||
|  |              | ||||||
|             const providers = await providerManager.getEnabledEmbeddingProviders(); |             const providers = await providerManager.getEnabledEmbeddingProviders(); | ||||||
|             if (providers.length === 0) { |             if (providers.length === 0) { | ||||||
|                 throw new Error("No embedding providers available"); |                 throw new Error("No embedding providers available"); | ||||||
|   | |||||||
							
								
								
									
										330
									
								
								apps/server/src/services/llm/provider_validation.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										330
									
								
								apps/server/src/services/llm/provider_validation.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,330 @@ | |||||||
|  | /** | ||||||
|  |  * Provider Validation Service | ||||||
|  |  *  | ||||||
|  |  * Validates AI provider configurations before initializing the embedding system. | ||||||
|  |  * This prevents startup errors when AI is enabled but providers are misconfigured. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import log from "../log.js"; | ||||||
|  | import options from "../options.js"; | ||||||
|  | import type { EmbeddingProvider } from "./embeddings/embeddings_interface.js"; | ||||||
|  |  | ||||||
|  | export interface ProviderValidationResult { | ||||||
|  |     hasValidProviders: boolean; | ||||||
|  |     validEmbeddingProviders: EmbeddingProvider[]; | ||||||
|  |     validChatProviders: string[]; | ||||||
|  |     errors: string[]; | ||||||
|  |     warnings: string[]; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Validate all available providers without throwing errors | ||||||
|  |  */ | ||||||
|  | export async function validateProviders(): Promise<ProviderValidationResult> { | ||||||
|  |     const result: ProviderValidationResult = { | ||||||
|  |         hasValidProviders: false, | ||||||
|  |         validEmbeddingProviders: [], | ||||||
|  |         validChatProviders: [], | ||||||
|  |         errors: [], | ||||||
|  |         warnings: [] | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |         // Check if AI is enabled | ||||||
|  |         const aiEnabled = await options.getOptionBool('aiEnabled'); | ||||||
|  |         if (!aiEnabled) { | ||||||
|  |             result.warnings.push("AI features are disabled"); | ||||||
|  |             return result; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Validate embedding providers | ||||||
|  |         await validateEmbeddingProviders(result); | ||||||
|  |          | ||||||
|  |         // Validate chat providers | ||||||
|  |         await validateChatProviders(result); | ||||||
|  |  | ||||||
|  |         // Determine if we have any valid providers | ||||||
|  |         result.hasValidProviders = result.validEmbeddingProviders.length > 0 || result.validChatProviders.length > 0; | ||||||
|  |  | ||||||
|  |         if (!result.hasValidProviders) { | ||||||
|  |             result.errors.push("No valid AI providers are configured"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |     } catch (error: any) { | ||||||
|  |         result.errors.push(`Error during provider validation: ${error.message || 'Unknown error'}`); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return result; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Validate embedding providers | ||||||
|  |  */ | ||||||
|  | async function validateEmbeddingProviders(result: ProviderValidationResult): Promise<void> { | ||||||
|  |     try { | ||||||
|  |         // Import provider classes and check configurations | ||||||
|  |         const { OpenAIEmbeddingProvider } = await import("./embeddings/providers/openai.js"); | ||||||
|  |         const { OllamaEmbeddingProvider } = await import("./embeddings/providers/ollama.js"); | ||||||
|  |         const { VoyageEmbeddingProvider } = await import("./embeddings/providers/voyage.js"); | ||||||
|  |  | ||||||
|  |         // Check OpenAI embedding provider | ||||||
|  |         await validateOpenAIEmbeddingProvider(result, OpenAIEmbeddingProvider); | ||||||
|  |          | ||||||
|  |         // Check Ollama embedding provider | ||||||
|  |         await validateOllamaEmbeddingProvider(result, OllamaEmbeddingProvider); | ||||||
|  |          | ||||||
|  |         // Check Voyage embedding provider | ||||||
|  |         await validateVoyageEmbeddingProvider(result, VoyageEmbeddingProvider); | ||||||
|  |  | ||||||
|  |         // Local provider is always available as fallback | ||||||
|  |         await validateLocalEmbeddingProvider(result); | ||||||
|  |  | ||||||
|  |     } catch (error: any) { | ||||||
|  |         result.errors.push(`Error validating embedding providers: ${error.message || 'Unknown error'}`); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Validate chat providers | ||||||
|  |  */ | ||||||
|  | async function validateChatProviders(result: ProviderValidationResult): Promise<void> { | ||||||
|  |     try { | ||||||
|  |         // Check OpenAI chat provider | ||||||
|  |         const openaiApiKey = await options.getOption('openaiApiKey'); | ||||||
|  |         const openaiBaseUrl = await options.getOption('openaiBaseUrl'); | ||||||
|  |          | ||||||
|  |         if (openaiApiKey || openaiBaseUrl) { | ||||||
|  |             if (!openaiApiKey && !openaiBaseUrl) { | ||||||
|  |                 result.warnings.push("OpenAI chat provider: No API key or base URL configured"); | ||||||
|  |             } else if (!openaiApiKey) { | ||||||
|  |                 result.warnings.push("OpenAI chat provider: No API key configured (may work with compatible endpoints)"); | ||||||
|  |                 result.validChatProviders.push('openai'); | ||||||
|  |             } else { | ||||||
|  |                 result.validChatProviders.push('openai'); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Check Anthropic chat provider | ||||||
|  |         const anthropicApiKey = await options.getOption('anthropicApiKey'); | ||||||
|  |         if (anthropicApiKey) { | ||||||
|  |             result.validChatProviders.push('anthropic'); | ||||||
|  |         } else { | ||||||
|  |             result.warnings.push("Anthropic chat provider: No API key configured"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Check Ollama chat provider | ||||||
|  |         const ollamaBaseUrl = await options.getOption('ollamaBaseUrl'); | ||||||
|  |         if (ollamaBaseUrl) { | ||||||
|  |             result.validChatProviders.push('ollama'); | ||||||
|  |         } else { | ||||||
|  |             result.warnings.push("Ollama chat provider: No base URL configured"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |     } catch (error: any) { | ||||||
|  |         result.errors.push(`Error validating chat providers: ${error.message || 'Unknown error'}`); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Validate OpenAI embedding provider | ||||||
|  |  */ | ||||||
|  | async function validateOpenAIEmbeddingProvider( | ||||||
|  |     result: ProviderValidationResult,  | ||||||
|  |     OpenAIEmbeddingProvider: any | ||||||
|  | ): Promise<void> { | ||||||
|  |     try { | ||||||
|  |         const openaiApiKey = await options.getOption('openaiApiKey'); | ||||||
|  |         const openaiBaseUrl = await options.getOption('openaiBaseUrl'); | ||||||
|  |          | ||||||
|  |         if (openaiApiKey || openaiBaseUrl) { | ||||||
|  |             const openaiModel = await options.getOption('openaiEmbeddingModel'); | ||||||
|  |             const finalBaseUrl = openaiBaseUrl || 'https://api.openai.com/v1'; | ||||||
|  |  | ||||||
|  |             if (!openaiApiKey) { | ||||||
|  |                 result.warnings.push("OpenAI embedding provider: No API key configured (may work with compatible endpoints)"); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             const provider = new OpenAIEmbeddingProvider({ | ||||||
|  |                 model: openaiModel, | ||||||
|  |                 dimension: 1536, | ||||||
|  |                 type: 'float32', | ||||||
|  |                 apiKey: openaiApiKey || '', | ||||||
|  |                 baseUrl: finalBaseUrl | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |             result.validEmbeddingProviders.push(provider); | ||||||
|  |             log.info(`Validated OpenAI embedding provider: ${openaiModel} at ${finalBaseUrl}`); | ||||||
|  |         } else { | ||||||
|  |             result.warnings.push("OpenAI embedding provider: No API key or base URL configured"); | ||||||
|  |         } | ||||||
|  |     } catch (error: any) { | ||||||
|  |         result.errors.push(`OpenAI embedding provider validation failed: ${error.message || 'Unknown error'}`); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Validate Ollama embedding provider | ||||||
|  |  */ | ||||||
|  | async function validateOllamaEmbeddingProvider( | ||||||
|  |     result: ProviderValidationResult,  | ||||||
|  |     OllamaEmbeddingProvider: any | ||||||
|  | ): Promise<void> { | ||||||
|  |     try { | ||||||
|  |         const ollamaEmbeddingBaseUrl = await options.getOption('ollamaEmbeddingBaseUrl'); | ||||||
|  |          | ||||||
|  |         if (ollamaEmbeddingBaseUrl) { | ||||||
|  |             const embeddingModel = await options.getOption('ollamaEmbeddingModel'); | ||||||
|  |  | ||||||
|  |             try { | ||||||
|  |                 const provider = new OllamaEmbeddingProvider({ | ||||||
|  |                     model: embeddingModel, | ||||||
|  |                     dimension: 768, | ||||||
|  |                     type: 'float32', | ||||||
|  |                     baseUrl: ollamaEmbeddingBaseUrl | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |                 // Try to initialize to validate connection | ||||||
|  |                 await provider.initialize(); | ||||||
|  |                 result.validEmbeddingProviders.push(provider); | ||||||
|  |                 log.info(`Validated Ollama embedding provider: ${embeddingModel} at ${ollamaEmbeddingBaseUrl}`); | ||||||
|  |             } catch (error: any) { | ||||||
|  |                 result.warnings.push(`Ollama embedding provider initialization failed: ${error.message || 'Unknown error'}`); | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             result.warnings.push("Ollama embedding provider: No base URL configured"); | ||||||
|  |         } | ||||||
|  |     } catch (error: any) { | ||||||
|  |         result.errors.push(`Ollama embedding provider validation failed: ${error.message || 'Unknown error'}`); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Validate Voyage embedding provider | ||||||
|  |  */ | ||||||
|  | async function validateVoyageEmbeddingProvider( | ||||||
|  |     result: ProviderValidationResult,  | ||||||
|  |     VoyageEmbeddingProvider: any | ||||||
|  | ): Promise<void> { | ||||||
|  |     try { | ||||||
|  |         const voyageApiKey = await options.getOption('voyageApiKey' as any); | ||||||
|  |          | ||||||
|  |         if (voyageApiKey) { | ||||||
|  |             const voyageModel = await options.getOption('voyageEmbeddingModel') || 'voyage-2'; | ||||||
|  |              | ||||||
|  |             const provider = new VoyageEmbeddingProvider({ | ||||||
|  |                 model: voyageModel, | ||||||
|  |                 dimension: 1024, | ||||||
|  |                 type: 'float32', | ||||||
|  |                 apiKey: voyageApiKey, | ||||||
|  |                 baseUrl: 'https://api.voyageai.com/v1' | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |             result.validEmbeddingProviders.push(provider); | ||||||
|  |             log.info(`Validated Voyage embedding provider: ${voyageModel}`); | ||||||
|  |         } else { | ||||||
|  |             result.warnings.push("Voyage embedding provider: No API key configured"); | ||||||
|  |         } | ||||||
|  |     } catch (error: any) { | ||||||
|  |         result.errors.push(`Voyage embedding provider validation failed: ${error.message || 'Unknown error'}`); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Validate local embedding provider (always available as fallback) | ||||||
|  |  */ | ||||||
|  | async function validateLocalEmbeddingProvider(result: ProviderValidationResult): Promise<void> { | ||||||
|  |     try { | ||||||
|  |         // Simple local embedding provider implementation | ||||||
|  |         class SimpleLocalEmbeddingProvider { | ||||||
|  |             name = "local"; | ||||||
|  |             config = { | ||||||
|  |                 model: 'local', | ||||||
|  |                 dimension: 384, | ||||||
|  |                 type: 'float32' as const | ||||||
|  |             }; | ||||||
|  |  | ||||||
|  |             getConfig() { | ||||||
|  |                 return this.config; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             getNormalizationStatus() { | ||||||
|  |                 return 0; // NormalizationStatus.NEVER | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             async generateEmbeddings(text: string): Promise<Float32Array> { | ||||||
|  |                 const result = new Float32Array(this.config.dimension); | ||||||
|  |                 for (let i = 0; i < result.length; i++) { | ||||||
|  |                     const charSum = Array.from(text).reduce((sum, char, idx) => | ||||||
|  |                         sum + char.charCodeAt(0) * Math.sin(idx * 0.1), 0); | ||||||
|  |                     result[i] = Math.sin(i * 0.1 + charSum * 0.01); | ||||||
|  |                 } | ||||||
|  |                 return result; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             async generateBatchEmbeddings(texts: string[]): Promise<Float32Array[]> { | ||||||
|  |                 return Promise.all(texts.map(text => this.generateEmbeddings(text))); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             async generateNoteEmbeddings(context: any): Promise<Float32Array> { | ||||||
|  |                 const text = (context.title || "") + " " + (context.content || ""); | ||||||
|  |                 return this.generateEmbeddings(text); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             async generateBatchNoteEmbeddings(contexts: any[]): Promise<Float32Array[]> { | ||||||
|  |                 return Promise.all(contexts.map(context => this.generateNoteEmbeddings(context))); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const localProvider = new SimpleLocalEmbeddingProvider(); | ||||||
|  |         result.validEmbeddingProviders.push(localProvider as any); | ||||||
|  |         log.info("Validated local embedding provider as fallback"); | ||||||
|  |     } catch (error: any) { | ||||||
|  |         result.errors.push(`Local embedding provider validation failed: ${error.message || 'Unknown error'}`); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Check if any working providers are available for embeddings | ||||||
|  |  */ | ||||||
|  | export async function hasWorkingEmbeddingProviders(): Promise<boolean> { | ||||||
|  |     const validation = await validateProviders(); | ||||||
|  |     return validation.validEmbeddingProviders.length > 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Check if any working providers are available for chat | ||||||
|  |  */ | ||||||
|  | export async function hasWorkingChatProviders(): Promise<boolean> { | ||||||
|  |     const validation = await validateProviders(); | ||||||
|  |     return validation.validChatProviders.length > 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Get only the working embedding providers | ||||||
|  |  */ | ||||||
|  | export async function getWorkingEmbeddingProviders(): Promise<EmbeddingProvider[]> { | ||||||
|  |     const validation = await validateProviders(); | ||||||
|  |     return validation.validEmbeddingProviders; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Log validation results in a user-friendly way | ||||||
|  |  */ | ||||||
|  | export function logValidationResults(validation: ProviderValidationResult): void { | ||||||
|  |     if (validation.hasValidProviders) { | ||||||
|  |         log.info(`AI provider validation passed: ${validation.validEmbeddingProviders.length} embedding providers, ${validation.validChatProviders.length} chat providers`); | ||||||
|  |          | ||||||
|  |         if (validation.validEmbeddingProviders.length > 0) { | ||||||
|  |             log.info(`Working embedding providers: ${validation.validEmbeddingProviders.map(p => p.name).join(', ')}`); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         if (validation.validChatProviders.length > 0) { | ||||||
|  |             log.info(`Working chat providers: ${validation.validChatProviders.join(', ')}`); | ||||||
|  |         } | ||||||
|  |     } else { | ||||||
|  |         log.info("AI provider validation failed: No working providers found"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     validation.warnings.forEach(warning => log.info(`Provider validation: ${warning}`)); | ||||||
|  |     validation.errors.forEach(error => log.error(`Provider validation: ${error}`)); | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user