mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-09-26 23:11:34 +08:00
feat(llm): still work on decomplicating provider creation
This commit is contained in:
parent
8f33f37de3
commit
20ec294774
@ -267,12 +267,23 @@ export class AIServiceManager implements IAIServiceManager {
|
|||||||
// If not a provider prefix, treat the entire string as a model name and continue with normal provider selection
|
// If not a provider prefix, treat the entire string as a model name and continue with normal provider selection
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try each provider in order until one succeeds
|
// If user has a specific provider selected, try only that one and fail fast
|
||||||
|
if (this.providerOrder.length === 1 && sortedProviders.length === 1) {
|
||||||
|
const selectedProvider = sortedProviders[0];
|
||||||
|
const service = await this.getOrCreateChatProvider(selectedProvider);
|
||||||
|
if (!service) {
|
||||||
|
throw new Error(`Failed to create selected chat provider: ${selectedProvider}. Please check your configuration.`);
|
||||||
|
}
|
||||||
|
log.info(`[AIServiceManager] Using selected provider ${selectedProvider} with options.stream: ${options.stream}`);
|
||||||
|
return await service.generateChatCompletion(messages, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no specific provider selected, try each provider in order until one succeeds
|
||||||
let lastError: Error | null = null;
|
let lastError: Error | null = null;
|
||||||
|
|
||||||
for (const provider of sortedProviders) {
|
for (const provider of sortedProviders) {
|
||||||
try {
|
try {
|
||||||
const service = this.services[provider];
|
const service = await this.getOrCreateChatProvider(provider);
|
||||||
if (service) {
|
if (service) {
|
||||||
log.info(`[AIServiceManager] Trying provider ${provider} with options.stream: ${options.stream}`);
|
log.info(`[AIServiceManager] Trying provider ${provider} with options.stream: ${options.stream}`);
|
||||||
return await service.generateChatCompletion(messages, options);
|
return await service.generateChatCompletion(messages, options);
|
||||||
@ -383,7 +394,7 @@ export class AIServiceManager implements IAIServiceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get or create a chat provider on-demand
|
* Get or create a chat provider on-demand with inline validation
|
||||||
*/
|
*/
|
||||||
private async getOrCreateChatProvider(providerName: ServiceProviders): Promise<AIService | null> {
|
private async getOrCreateChatProvider(providerName: ServiceProviders): Promise<AIService | null> {
|
||||||
// Return existing provider if already created
|
// Return existing provider if already created
|
||||||
@ -391,38 +402,54 @@ export class AIServiceManager implements IAIServiceManager {
|
|||||||
return this.services[providerName];
|
return this.services[providerName];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create provider on-demand based on configuration
|
// Create and validate provider on-demand
|
||||||
try {
|
try {
|
||||||
|
let service: AIService | null = null;
|
||||||
|
|
||||||
switch (providerName) {
|
switch (providerName) {
|
||||||
case 'openai':
|
case 'openai': {
|
||||||
const openaiApiKey = await options.getOption('openaiApiKey');
|
const apiKey = await options.getOption('openaiApiKey');
|
||||||
if (openaiApiKey) {
|
const baseUrl = await options.getOption('openaiBaseUrl');
|
||||||
this.services.openai = new OpenAIService();
|
if (!apiKey && !baseUrl) return null;
|
||||||
log.info('Created OpenAI chat provider on-demand');
|
|
||||||
return this.services.openai;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'anthropic':
|
service = new OpenAIService();
|
||||||
const anthropicApiKey = await options.getOption('anthropicApiKey');
|
// Validate by checking if it's available
|
||||||
if (anthropicApiKey) {
|
if (!service.isAvailable()) {
|
||||||
this.services.anthropic = new AnthropicService();
|
throw new Error('OpenAI service not available');
|
||||||
log.info('Created Anthropic chat provider on-demand');
|
|
||||||
return this.services.anthropic;
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'ollama':
|
case 'anthropic': {
|
||||||
const ollamaBaseUrl = await options.getOption('ollamaBaseUrl');
|
const apiKey = await options.getOption('anthropicApiKey');
|
||||||
if (ollamaBaseUrl) {
|
if (!apiKey) return null;
|
||||||
this.services.ollama = new OllamaService();
|
|
||||||
log.info('Created Ollama chat provider on-demand');
|
service = new AnthropicService();
|
||||||
return this.services.ollama;
|
if (!service.isAvailable()) {
|
||||||
|
throw new Error('Anthropic service not available');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'ollama': {
|
||||||
|
const baseUrl = await options.getOption('ollamaBaseUrl');
|
||||||
|
if (!baseUrl) return null;
|
||||||
|
|
||||||
|
service = new OllamaService();
|
||||||
|
if (!service.isAvailable()) {
|
||||||
|
throw new Error('Ollama service not available');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (service) {
|
||||||
|
this.services[providerName] = service;
|
||||||
|
log.info(`Created and validated ${providerName} chat provider`);
|
||||||
|
return service;
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
log.error(`Error creating ${providerName} provider on-demand: ${error.message || 'Unknown error'}`);
|
log.error(`Failed to create ${providerName} chat provider: ${error.message || 'Unknown error'}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -48,53 +48,16 @@ export class IndexService {
|
|||||||
async initialize() {
|
async initialize() {
|
||||||
if (this.initialized) return;
|
if (this.initialized) return;
|
||||||
|
|
||||||
try {
|
// Setup event listeners for note changes
|
||||||
// Check if database is initialized before proceeding
|
this.setupEventListeners();
|
||||||
if (!sqlInit.isDbInitialized()) {
|
|
||||||
log.info("Index service: Database not initialized yet, skipping initialization");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const aiEnabled = options.getOptionOrNull('aiEnabled') === "true";
|
// Setup automatic indexing if enabled
|
||||||
if (!aiEnabled) {
|
if (await options.getOptionBool('embeddingAutoUpdateEnabled')) {
|
||||||
log.info("Index service: AI features disabled, skipping initialization");
|
this.setupAutomaticIndexing();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
if (!providers || providers.length === 0) {
|
|
||||||
log.info("Index service: No enabled embedding providers, skipping initialization");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this instance should process embeddings
|
|
||||||
const embeddingLocation = await options.getOption('embeddingGenerationLocation') || 'client';
|
|
||||||
const isSyncServer = await this.isSyncServerForEmbeddings();
|
|
||||||
const shouldProcessEmbeddings = embeddingLocation === 'client' || isSyncServer;
|
|
||||||
|
|
||||||
// Setup automatic indexing if enabled and this instance should process embeddings
|
|
||||||
if (await options.getOptionBool('embeddingAutoUpdateEnabled') && shouldProcessEmbeddings) {
|
|
||||||
this.setupAutomaticIndexing();
|
|
||||||
log.info(`Index service: Automatic indexing enabled, processing embeddings ${isSyncServer ? 'as sync server' : 'as client'}`);
|
|
||||||
} else if (await options.getOptionBool('embeddingAutoUpdateEnabled')) {
|
|
||||||
log.info("Index service: Automatic indexing enabled, but this instance is not configured to process embeddings");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listen for note changes to update index
|
|
||||||
this.setupEventListeners();
|
|
||||||
|
|
||||||
this.initialized = true;
|
|
||||||
log.info("Index service initialized successfully");
|
|
||||||
} catch (error: any) {
|
|
||||||
log.error(`Error initializing index service: ${error.message || "Unknown error"}`);
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.initialized = true;
|
||||||
|
log.info("Index service initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -147,23 +110,7 @@ export class IndexService {
|
|||||||
this.automaticIndexingInterval = setInterval(async () => {
|
this.automaticIndexingInterval = setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
if (!this.indexingInProgress) {
|
if (!this.indexingInProgress) {
|
||||||
// Check if this instance should process embeddings
|
await this.runBatchIndexing(50); // Processing logic handles sync server checks
|
||||||
const embeddingLocation = await options.getOption('embeddingGenerationLocation') || 'client';
|
|
||||||
const isSyncServer = await this.isSyncServerForEmbeddings();
|
|
||||||
const shouldProcessEmbeddings = embeddingLocation === 'client' || isSyncServer;
|
|
||||||
|
|
||||||
if (!shouldProcessEmbeddings) {
|
|
||||||
// This instance is not configured to process embeddings
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stats = await vectorStore.getEmbeddingStats();
|
|
||||||
|
|
||||||
// Only run automatic indexing if we're below 95% completion
|
|
||||||
if (stats.percentComplete < 95) {
|
|
||||||
log.info(`Starting automatic indexing (current completion: ${stats.percentComplete}%)`);
|
|
||||||
await this.runBatchIndexing(50); // Process 50 notes at a time
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
log.error(`Error in automatic indexing: ${error.message || "Unknown error"}`);
|
log.error(`Error in automatic indexing: ${error.message || "Unknown error"}`);
|
||||||
@ -498,35 +445,14 @@ export class IndexService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get all enabled embedding providers
|
// Get the selected embedding provider on-demand
|
||||||
const providers = await providerManager.getEnabledEmbeddingProviders();
|
|
||||||
if (!providers || providers.length === 0) {
|
|
||||||
throw new Error("No embedding providers available");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the selected embedding provider
|
|
||||||
const options = (await import('../options.js')).default;
|
|
||||||
const selectedEmbeddingProvider = await options.getOption('embeddingSelectedProvider');
|
const selectedEmbeddingProvider = await options.getOption('embeddingSelectedProvider');
|
||||||
let provider;
|
const provider = selectedEmbeddingProvider
|
||||||
|
? await providerManager.getOrCreateEmbeddingProvider(selectedEmbeddingProvider)
|
||||||
if (selectedEmbeddingProvider) {
|
: (await providerManager.getEnabledEmbeddingProviders())[0];
|
||||||
// Try to use the selected provider
|
|
||||||
const enabledProviders = await providerManager.getEnabledEmbeddingProviders();
|
|
||||||
provider = enabledProviders.find(p => p.name === selectedEmbeddingProvider);
|
|
||||||
|
|
||||||
if (!provider) {
|
|
||||||
log.info(`Selected embedding provider ${selectedEmbeddingProvider} is not available, using first enabled provider`);
|
|
||||||
// Fall back to first enabled provider
|
|
||||||
provider = providers[0];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No provider selected, use first available provider
|
|
||||||
log.info('No embedding provider selected, using first available provider');
|
|
||||||
provider = providers[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
throw new Error("No suitable embedding provider found");
|
throw new Error("No embedding provider available");
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(`Searching with embedding provider: ${provider.name}, model: ${provider.getConfig().model}`);
|
log.info(`Searching with embedding provider: ${provider.name}, model: ${provider.getConfig().model}`);
|
||||||
@ -684,6 +610,12 @@ export class IndexService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Get embedding providers on-demand
|
||||||
|
const providers = await providerManager.getEnabledEmbeddingProviders();
|
||||||
|
if (providers.length === 0) {
|
||||||
|
return "I don't have access to your note embeddings. Please configure an embedding provider in your AI settings.";
|
||||||
|
}
|
||||||
|
|
||||||
// Find similar notes to the query
|
// Find similar notes to the query
|
||||||
const similarNotes = await this.findSimilarNotes(
|
const similarNotes = await this.findSimilarNotes(
|
||||||
query,
|
query,
|
||||||
@ -819,9 +751,13 @@ export class IndexService {
|
|||||||
// Get complete note context for indexing
|
// Get complete note context for indexing
|
||||||
const context = await vectorStore.getNoteEmbeddingContext(noteId);
|
const context = await vectorStore.getNoteEmbeddingContext(noteId);
|
||||||
|
|
||||||
// Queue note for embedding with all available providers
|
// Generate embedding with the selected provider
|
||||||
const providers = await providerManager.getEnabledEmbeddingProviders();
|
const selectedEmbeddingProvider = await options.getOption('embeddingSelectedProvider');
|
||||||
for (const provider of providers) {
|
const provider = selectedEmbeddingProvider
|
||||||
|
? await providerManager.getOrCreateEmbeddingProvider(selectedEmbeddingProvider)
|
||||||
|
: (await providerManager.getEnabledEmbeddingProviders())[0];
|
||||||
|
|
||||||
|
if (provider) {
|
||||||
try {
|
try {
|
||||||
const embedding = await provider.generateNoteEmbeddings(context);
|
const embedding = await provider.generateNoteEmbeddings(context);
|
||||||
if (embedding) {
|
if (embedding) {
|
||||||
@ -873,16 +809,13 @@ export class IndexService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify providers are available
|
// Get embedding providers (will be created on-demand when needed)
|
||||||
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");
|
log.info("No embedding providers configured, but continuing initialization");
|
||||||
|
} else {
|
||||||
|
log.info(`Found ${providers.length} embedding providers: ${providers.map(p => p.name).join(', ')}`);
|
||||||
}
|
}
|
||||||
log.info(`Found ${providers.length} embedding providers: ${providers.map(p => p.name).join(', ')}`);
|
|
||||||
|
|
||||||
// Setup automatic indexing if enabled
|
// Setup automatic indexing if enabled
|
||||||
if (await options.getOptionBool('embeddingAutoUpdateEnabled')) {
|
if (await options.getOptionBool('embeddingAutoUpdateEnabled')) {
|
||||||
|
@ -18,7 +18,7 @@ export interface ProviderValidationResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate all available providers without throwing errors
|
* Simplified provider validation - just checks configuration without creating providers
|
||||||
*/
|
*/
|
||||||
export async function validateProviders(): Promise<ProviderValidationResult> {
|
export async function validateProviders(): Promise<ProviderValidationResult> {
|
||||||
const result: ProviderValidationResult = {
|
const result: ProviderValidationResult = {
|
||||||
@ -37,14 +37,12 @@ export async function validateProviders(): Promise<ProviderValidationResult> {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate embedding providers
|
// Check configuration only - don't create providers
|
||||||
await validateEmbeddingProviders(result);
|
await checkEmbeddingProviderConfigs(result);
|
||||||
|
await checkChatProviderConfigs(result);
|
||||||
|
|
||||||
// Validate chat providers
|
// Determine if we have any valid providers based on configuration
|
||||||
await validateChatProviders(result);
|
result.hasValidProviders = result.validChatProviders.length > 0;
|
||||||
|
|
||||||
// Determine if we have any valid providers
|
|
||||||
result.hasValidProviders = result.validEmbeddingProviders.length > 0 || result.validChatProviders.length > 0;
|
|
||||||
|
|
||||||
if (!result.hasValidProviders) {
|
if (!result.hasValidProviders) {
|
||||||
result.errors.push("No valid AI providers are configured");
|
result.errors.push("No valid AI providers are configured");
|
||||||
@ -58,241 +56,80 @@ export async function validateProviders(): Promise<ProviderValidationResult> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate embedding providers
|
* Check embedding provider configurations without creating providers
|
||||||
*/
|
*/
|
||||||
async function validateEmbeddingProviders(result: ProviderValidationResult): Promise<void> {
|
async function checkEmbeddingProviderConfigs(result: ProviderValidationResult): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Import provider classes and check configurations
|
// Check OpenAI embedding configuration
|
||||||
const { OpenAIEmbeddingProvider } = await import("./embeddings/providers/openai.js");
|
const openaiApiKey = await options.getOption('openaiApiKey');
|
||||||
const { OllamaEmbeddingProvider } = await import("./embeddings/providers/ollama.js");
|
const openaiBaseUrl = await options.getOption('openaiBaseUrl');
|
||||||
const { VoyageEmbeddingProvider } = await import("./embeddings/providers/voyage.js");
|
if (openaiApiKey || openaiBaseUrl) {
|
||||||
|
if (!openaiApiKey) {
|
||||||
|
result.warnings.push("OpenAI embedding: No API key (may work with compatible endpoints)");
|
||||||
|
}
|
||||||
|
log.info("OpenAI embedding provider configuration available");
|
||||||
|
}
|
||||||
|
|
||||||
// Check OpenAI embedding provider
|
// Check Ollama embedding configuration
|
||||||
await validateOpenAIEmbeddingProvider(result, OpenAIEmbeddingProvider);
|
const ollamaEmbeddingBaseUrl = await options.getOption('ollamaEmbeddingBaseUrl');
|
||||||
|
if (ollamaEmbeddingBaseUrl) {
|
||||||
|
log.info("Ollama embedding provider configuration available");
|
||||||
|
}
|
||||||
|
|
||||||
// Check Ollama embedding provider
|
// Check Voyage embedding configuration
|
||||||
await validateOllamaEmbeddingProvider(result, OllamaEmbeddingProvider);
|
const voyageApiKey = await options.getOption('voyageApiKey' as any);
|
||||||
|
if (voyageApiKey) {
|
||||||
|
log.info("Voyage embedding provider configuration available");
|
||||||
|
}
|
||||||
|
|
||||||
// Check Voyage embedding provider
|
// Local provider is always available
|
||||||
await validateVoyageEmbeddingProvider(result, VoyageEmbeddingProvider);
|
log.info("Local embedding provider available as fallback");
|
||||||
|
|
||||||
// Local provider is always available as fallback
|
|
||||||
await validateLocalEmbeddingProvider(result);
|
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
result.errors.push(`Error validating embedding providers: ${error.message || 'Unknown error'}`);
|
result.errors.push(`Error checking embedding provider configs: ${error.message || 'Unknown error'}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate chat providers
|
* Check chat provider configurations without creating providers
|
||||||
*/
|
*/
|
||||||
async function validateChatProviders(result: ProviderValidationResult): Promise<void> {
|
async function checkChatProviderConfigs(result: ProviderValidationResult): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Check OpenAI chat provider
|
// Check OpenAI chat provider
|
||||||
const openaiApiKey = await options.getOption('openaiApiKey');
|
const openaiApiKey = await options.getOption('openaiApiKey');
|
||||||
const openaiBaseUrl = await options.getOption('openaiBaseUrl');
|
const openaiBaseUrl = await options.getOption('openaiBaseUrl');
|
||||||
|
|
||||||
if (openaiApiKey || openaiBaseUrl) {
|
if (openaiApiKey || openaiBaseUrl) {
|
||||||
if (!openaiApiKey && !openaiBaseUrl) {
|
if (!openaiApiKey) {
|
||||||
result.warnings.push("OpenAI chat provider: No API key or base URL configured");
|
result.warnings.push("OpenAI chat: No API key (may work with compatible endpoints)");
|
||||||
} 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');
|
|
||||||
}
|
}
|
||||||
|
result.validChatProviders.push('openai');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check Anthropic chat provider
|
// Check Anthropic chat provider
|
||||||
const anthropicApiKey = await options.getOption('anthropicApiKey');
|
const anthropicApiKey = await options.getOption('anthropicApiKey');
|
||||||
if (anthropicApiKey) {
|
if (anthropicApiKey) {
|
||||||
result.validChatProviders.push('anthropic');
|
result.validChatProviders.push('anthropic');
|
||||||
} else {
|
|
||||||
result.warnings.push("Anthropic chat provider: No API key configured");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check Ollama chat provider
|
// Check Ollama chat provider
|
||||||
const ollamaBaseUrl = await options.getOption('ollamaBaseUrl');
|
const ollamaBaseUrl = await options.getOption('ollamaBaseUrl');
|
||||||
if (ollamaBaseUrl) {
|
if (ollamaBaseUrl) {
|
||||||
result.validChatProviders.push('ollama');
|
result.validChatProviders.push('ollama');
|
||||||
} else {
|
}
|
||||||
result.warnings.push("Ollama chat provider: No base URL configured");
|
|
||||||
|
if (result.validChatProviders.length === 0) {
|
||||||
|
result.warnings.push("No chat providers configured. Please configure at least one provider.");
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
result.errors.push(`Error validating chat providers: ${error.message || 'Unknown error'}`);
|
result.errors.push(`Error checking chat provider configs: ${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
|
* Check if any chat providers are configured
|
||||||
*/
|
|
||||||
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> {
|
export async function hasWorkingChatProviders(): Promise<boolean> {
|
||||||
const validation = await validateProviders();
|
const validation = await validateProviders();
|
||||||
@ -300,11 +137,21 @@ export async function hasWorkingChatProviders(): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get only the working embedding providers
|
* Check if any embedding providers are configured (simplified)
|
||||||
*/
|
*/
|
||||||
export async function getWorkingEmbeddingProviders(): Promise<EmbeddingProvider[]> {
|
export async function hasWorkingEmbeddingProviders(): Promise<boolean> {
|
||||||
const validation = await validateProviders();
|
if (!(await options.getOptionBool('aiEnabled'))) {
|
||||||
return validation.validEmbeddingProviders;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any embedding provider is configured
|
||||||
|
const openaiKey = await options.getOption('openaiApiKey');
|
||||||
|
const openaiBaseUrl = await options.getOption('openaiBaseUrl');
|
||||||
|
const ollamaUrl = await options.getOption('ollamaEmbeddingBaseUrl');
|
||||||
|
const voyageKey = await options.getOption('voyageApiKey' as any);
|
||||||
|
|
||||||
|
// Local provider is always available as fallback
|
||||||
|
return !!(openaiKey || openaiBaseUrl || ollamaUrl || voyageKey) || true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -124,118 +124,129 @@ export function getEmbeddingProvider(name: string): EmbeddingProvider | undefine
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create providers on-demand based on current options
|
* Get or create a specific embedding provider with inline validation
|
||||||
*/
|
*/
|
||||||
export async function createProvidersFromCurrentOptions(): Promise<EmbeddingProvider[]> {
|
export async function getOrCreateEmbeddingProvider(providerName: string): Promise<EmbeddingProvider | null> {
|
||||||
const result: EmbeddingProvider[] = [];
|
// Return existing provider if already created and valid
|
||||||
|
const existing = providers.get(providerName);
|
||||||
try {
|
if (existing) {
|
||||||
// Create Ollama provider if embedding base URL is configured
|
return existing;
|
||||||
const ollamaEmbeddingBaseUrl = await options.getOption('ollamaEmbeddingBaseUrl');
|
|
||||||
if (ollamaEmbeddingBaseUrl) {
|
|
||||||
const embeddingModel = await options.getOption('ollamaEmbeddingModel');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const ollamaProvider = new OllamaEmbeddingProvider({
|
|
||||||
model: embeddingModel,
|
|
||||||
dimension: 768, // Initial value, will be updated during initialization
|
|
||||||
type: 'float32',
|
|
||||||
baseUrl: ollamaEmbeddingBaseUrl
|
|
||||||
});
|
|
||||||
|
|
||||||
await ollamaProvider.initialize();
|
|
||||||
registerEmbeddingProvider(ollamaProvider);
|
|
||||||
result.push(ollamaProvider);
|
|
||||||
log.info(`Created Ollama provider on-demand: ${embeddingModel} at ${ollamaEmbeddingBaseUrl}`);
|
|
||||||
} catch (error: any) {
|
|
||||||
log.error(`Error creating Ollama embedding provider on-demand: ${error.message || 'Unknown error'}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create OpenAI provider even without API key (for OpenAI-compatible endpoints)
|
|
||||||
const openaiApiKey = await options.getOption('openaiApiKey');
|
|
||||||
const openaiBaseUrl = await options.getOption('openaiBaseUrl');
|
|
||||||
|
|
||||||
// Only create OpenAI provider if base URL is set or API key is provided
|
|
||||||
if (openaiApiKey || openaiBaseUrl) {
|
|
||||||
const openaiModel = await options.getOption('openaiEmbeddingModel')
|
|
||||||
const finalBaseUrl = openaiBaseUrl || 'https://api.openai.com/v1';
|
|
||||||
|
|
||||||
if (!openaiApiKey) {
|
|
||||||
log.info('Creating OpenAI embedding provider without API key. This may cause issues with official OpenAI endpoints.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const openaiProvider = new OpenAIEmbeddingProvider({
|
|
||||||
model: openaiModel,
|
|
||||||
dimension: 1536,
|
|
||||||
type: 'float32',
|
|
||||||
apiKey: openaiApiKey || '', // Default to empty string
|
|
||||||
baseUrl: finalBaseUrl
|
|
||||||
});
|
|
||||||
|
|
||||||
registerEmbeddingProvider(openaiProvider);
|
|
||||||
result.push(openaiProvider);
|
|
||||||
log.info(`Created OpenAI provider on-demand: ${openaiModel} at ${finalBaseUrl}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create Voyage provider if API key is configured
|
|
||||||
const voyageApiKey = await options.getOption('voyageApiKey' as any);
|
|
||||||
if (voyageApiKey) {
|
|
||||||
const voyageModel = await options.getOption('voyageEmbeddingModel') || 'voyage-2';
|
|
||||||
const voyageBaseUrl = 'https://api.voyageai.com/v1';
|
|
||||||
|
|
||||||
const voyageProvider = new VoyageEmbeddingProvider({
|
|
||||||
model: voyageModel,
|
|
||||||
dimension: 1024,
|
|
||||||
type: 'float32',
|
|
||||||
apiKey: voyageApiKey,
|
|
||||||
baseUrl: voyageBaseUrl
|
|
||||||
});
|
|
||||||
|
|
||||||
registerEmbeddingProvider(voyageProvider);
|
|
||||||
result.push(voyageProvider);
|
|
||||||
log.info(`Created Voyage provider on-demand: ${voyageModel}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always include local provider as fallback
|
|
||||||
if (!providers.has('local')) {
|
|
||||||
const localProvider = new SimpleLocalEmbeddingProvider({
|
|
||||||
model: 'local',
|
|
||||||
dimension: 384,
|
|
||||||
type: 'float32'
|
|
||||||
});
|
|
||||||
registerEmbeddingProvider(localProvider);
|
|
||||||
result.push(localProvider);
|
|
||||||
log.info(`Created local provider on-demand as fallback`);
|
|
||||||
} else {
|
|
||||||
result.push(providers.get('local')!);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
log.error(`Error creating providers from current options: ${error.message || 'Unknown error'}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
// Create and validate provider on-demand
|
||||||
|
try {
|
||||||
|
let provider: EmbeddingProvider | null = null;
|
||||||
|
|
||||||
|
switch (providerName) {
|
||||||
|
case 'ollama': {
|
||||||
|
const baseUrl = await options.getOption('ollamaEmbeddingBaseUrl');
|
||||||
|
if (!baseUrl) return null;
|
||||||
|
|
||||||
|
const model = await options.getOption('ollamaEmbeddingModel');
|
||||||
|
provider = new OllamaEmbeddingProvider({
|
||||||
|
model,
|
||||||
|
dimension: 768,
|
||||||
|
type: 'float32',
|
||||||
|
baseUrl
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate by initializing (if provider supports it)
|
||||||
|
if ('initialize' in provider && typeof provider.initialize === 'function') {
|
||||||
|
await provider.initialize();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'openai': {
|
||||||
|
const apiKey = await options.getOption('openaiApiKey');
|
||||||
|
const baseUrl = await options.getOption('openaiBaseUrl');
|
||||||
|
if (!apiKey && !baseUrl) return null;
|
||||||
|
|
||||||
|
const model = await options.getOption('openaiEmbeddingModel');
|
||||||
|
provider = new OpenAIEmbeddingProvider({
|
||||||
|
model,
|
||||||
|
dimension: 1536,
|
||||||
|
type: 'float32',
|
||||||
|
apiKey: apiKey || '',
|
||||||
|
baseUrl: baseUrl || 'https://api.openai.com/v1'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
log.info('OpenAI embedding provider created without API key for compatible endpoints');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'voyage': {
|
||||||
|
const apiKey = await options.getOption('voyageApiKey' as any);
|
||||||
|
if (!apiKey) return null;
|
||||||
|
|
||||||
|
const model = await options.getOption('voyageEmbeddingModel') || 'voyage-2';
|
||||||
|
provider = new VoyageEmbeddingProvider({
|
||||||
|
model,
|
||||||
|
dimension: 1024,
|
||||||
|
type: 'float32',
|
||||||
|
apiKey,
|
||||||
|
baseUrl: 'https://api.voyageai.com/v1'
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'local': {
|
||||||
|
provider = new SimpleLocalEmbeddingProvider({
|
||||||
|
model: 'local',
|
||||||
|
dimension: 384,
|
||||||
|
type: 'float32'
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (provider) {
|
||||||
|
registerEmbeddingProvider(provider);
|
||||||
|
log.info(`Created and validated ${providerName} embedding provider`);
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error(`Failed to create ${providerName} embedding provider: ${error.message || 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all enabled embedding providers
|
* Get all enabled embedding providers for the specified feature
|
||||||
*/
|
*/
|
||||||
export async function getEnabledEmbeddingProviders(): Promise<EmbeddingProvider[]> {
|
export async function getEnabledEmbeddingProviders(feature: 'embeddings' | 'chat' = 'embeddings'): Promise<EmbeddingProvider[]> {
|
||||||
if (!(await options.getOptionBool('aiEnabled'))) {
|
if (!(await options.getOptionBool('aiEnabled'))) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// First try to get existing registered providers
|
const result: EmbeddingProvider[] = [];
|
||||||
const existingProviders = Array.from(providers.values());
|
|
||||||
|
|
||||||
// If no providers are registered, create them on-demand from current options
|
// Get the selected provider for the feature
|
||||||
if (existingProviders.length === 0) {
|
const selectedProvider = feature === 'embeddings'
|
||||||
log.info('No providers registered, creating from current options');
|
? await options.getOption('embeddingSelectedProvider')
|
||||||
return await createProvidersFromCurrentOptions();
|
: await options.getOption('aiSelectedProvider');
|
||||||
|
|
||||||
|
// Try to get or create the specific selected provider
|
||||||
|
const provider = await getOrCreateEmbeddingProvider(selectedProvider);
|
||||||
|
if (!provider) {
|
||||||
|
throw new Error(`Failed to create selected embedding provider: ${selectedProvider}. Please check your configuration.`);
|
||||||
|
}
|
||||||
|
result.push(provider);
|
||||||
|
|
||||||
|
|
||||||
|
// Always ensure local provider as fallback
|
||||||
|
const localProvider = await getOrCreateEmbeddingProvider('local');
|
||||||
|
if (localProvider && !result.some(p => p.name === 'local')) {
|
||||||
|
result.push(localProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
return existingProviders;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -342,7 +353,7 @@ export default {
|
|||||||
getEmbeddingProviders,
|
getEmbeddingProviders,
|
||||||
getEmbeddingProvider,
|
getEmbeddingProvider,
|
||||||
getEnabledEmbeddingProviders,
|
getEnabledEmbeddingProviders,
|
||||||
createProvidersFromCurrentOptions,
|
getOrCreateEmbeddingProvider,
|
||||||
createEmbeddingProviderConfig,
|
createEmbeddingProviderConfig,
|
||||||
updateEmbeddingProviderConfig,
|
updateEmbeddingProviderConfig,
|
||||||
deleteEmbeddingProviderConfig,
|
deleteEmbeddingProviderConfig,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user