2025-03-02 19:39:10 -08:00
import options from '../options.js' ;
2025-06-04 20:13:13 +00:00
import eventService from '../events.js' ;
2025-04-08 21:24:56 +00:00
import type { AIService , ChatCompletionOptions , ChatResponse , Message } from './ai_interface.js' ;
2025-03-11 17:30:50 +00:00
import { AnthropicService } from './providers/anthropic_service.js' ;
2025-03-11 18:39:59 +00:00
import { ContextExtractor } from './context/index.js' ;
2025-04-07 22:34:24 +00:00
import agentTools from './context_extractors/index.js' ;
2025-04-08 21:24:56 +00:00
import contextService from './context/services/context_service.js' ;
import log from '../log.js' ;
import { OllamaService } from './providers/ollama_service.js' ;
import { OpenAIService } from './providers/openai_service.js' ;
2025-03-02 19:39:10 -08:00
2025-03-28 21:04:12 +00:00
// Import interfaces
import type {
ServiceProviders ,
IAIServiceManager ,
ProviderMetadata
} from './interfaces/ai_service_interfaces.js' ;
import type { NoteSearchResult } from './interfaces/context_interfaces.js' ;
2025-06-02 21:36:19 +00:00
// Import new configuration system
import {
2025-06-04 20:13:13 +00:00
getSelectedProvider ,
2025-06-02 21:36:19 +00:00
parseModelIdentifier ,
isAIEnabled ,
getDefaultModelForProvider ,
2025-06-02 21:49:35 +00:00
clearConfigurationCache ,
validateConfiguration
2025-06-02 21:36:19 +00:00
} from './config/configuration_helpers.js' ;
import type { ProviderType } from './interfaces/configuration_interfaces.js' ;
2025-04-16 17:29:35 +00:00
/ * *
* Interface representing relevant note context
* /
interface NoteContext {
title : string ;
content? : string ;
noteId? : string ;
summary? : string ;
score? : number ;
}
2025-03-28 21:04:12 +00:00
export class AIServiceManager implements IAIServiceManager {
2025-06-05 19:27:45 +00:00
private services : Partial < Record < ServiceProviders , AIService > > = { } ;
2025-03-02 19:39:10 -08:00
2025-03-09 02:19:26 +00:00
private initialized = false ;
2025-03-02 19:39:10 -08:00
constructor ( ) {
2025-04-11 22:52:09 +00:00
// Initialize tools immediately
this . initializeTools ( ) . catch ( error = > {
log . error ( ` Error initializing LLM tools during AIServiceManager construction: ${ error . message || String ( error ) } ` ) ;
} ) ;
2025-06-04 20:13:13 +00:00
// Set up event listener for provider changes
this . setupProviderChangeListener ( ) ;
2025-06-07 00:02:26 +00:00
this . initialized = true ;
2025-04-11 22:52:09 +00:00
}
2025-04-16 17:29:35 +00:00
2025-04-11 22:52:09 +00:00
/ * *
* Initialize all LLM tools in one place
* /
private async initializeTools ( ) : Promise < void > {
try {
log . info ( 'Initializing LLM tools during AIServiceManager construction...' ) ;
2025-04-16 17:29:35 +00:00
2025-04-11 22:52:09 +00:00
// Initialize agent tools
2025-04-16 17:29:35 +00:00
await this . initializeAgentTools ( ) ;
2025-04-11 22:52:09 +00:00
log . info ( "Agent tools initialized successfully" ) ;
2025-04-16 17:29:35 +00:00
2025-04-11 22:52:09 +00:00
// Initialize LLM tools
const toolInitializer = await import ( './tools/tool_initializer.js' ) ;
await toolInitializer . default . initializeTools ( ) ;
log . info ( "LLM tools initialized successfully" ) ;
2025-04-16 17:29:35 +00:00
} catch ( error : unknown ) {
log . error ( ` Error initializing tools: ${ this . handleError ( error ) } ` ) ;
2025-04-11 22:52:09 +00:00
// Don't throw, just log the error to prevent breaking construction
}
2025-03-02 19:39:10 -08:00
}
/ * *
2025-06-07 00:02:26 +00:00
* Get the currently selected provider using the new configuration system
2025-06-02 21:49:35 +00:00
* /
2025-06-07 00:02:26 +00:00
async getSelectedProviderAsync ( ) : Promise < ServiceProviders | null > {
2025-06-02 21:49:35 +00:00
try {
2025-06-04 20:13:13 +00:00
const selectedProvider = await getSelectedProvider ( ) ;
2025-06-07 00:02:26 +00:00
return selectedProvider as ServiceProviders || null ;
2025-06-02 21:49:35 +00:00
} catch ( error ) {
2025-06-04 20:13:13 +00:00
log . error ( ` Failed to get selected provider: ${ error } ` ) ;
2025-06-07 00:02:26 +00:00
return null ;
2025-03-09 02:19:26 +00:00
}
}
2025-03-17 16:23:58 +00:00
/ * *
2025-06-02 21:49:35 +00:00
* Validate AI configuration using the new configuration system
2025-03-17 16:23:58 +00:00
* /
2025-06-02 21:49:35 +00:00
async validateConfiguration ( ) : Promise < string | null > {
2025-03-17 16:23:58 +00:00
try {
2025-06-02 21:49:35 +00:00
const result = await validateConfiguration ( ) ;
2025-06-02 15:12:08 +00:00
2025-06-02 21:49:35 +00:00
if ( ! result . isValid ) {
let message = 'There are issues with your AI configuration:' ;
for ( const error of result . errors ) {
message += ` \ n• ${ error } ` ;
}
if ( result . warnings . length > 0 ) {
message += '\n\nWarnings:' ;
for ( const warning of result . warnings ) {
message += ` \ n• ${ warning } ` ;
2025-05-29 20:45:27 +00:00
}
}
2025-06-02 21:49:35 +00:00
message += '\n\nPlease check your AI settings.' ;
return message ;
2025-05-29 20:45:27 +00:00
}
2025-06-02 15:12:08 +00:00
2025-06-02 21:49:35 +00:00
if ( result . warnings . length > 0 ) {
let message = 'AI configuration warnings:' ;
for ( const warning of result . warnings ) {
message += ` \ n• ${ warning } ` ;
2025-03-17 16:23:58 +00:00
}
2025-06-02 21:49:35 +00:00
log . info ( message ) ;
2025-03-17 16:23:58 +00:00
}
return null ;
} catch ( error ) {
2025-06-02 21:49:35 +00:00
log . error ( ` Error validating AI configuration: ${ error } ` ) ;
return ` Configuration validation failed: ${ error } ` ;
2025-03-17 16:23:58 +00:00
}
}
2025-03-09 02:19:26 +00:00
/ * *
* Ensure manager is initialized before using
* /
private ensureInitialized() {
2025-06-07 00:02:26 +00:00
// No longer needed with simplified approach
}
/ * *
* Get or create any available AI service following the simplified pattern
* Returns a service or throws a meaningful error
* /
async getOrCreateAnyService ( ) : Promise < AIService > {
this . ensureInitialized ( ) ;
// Get the selected provider using the new configuration system
const selectedProvider = await this . getSelectedProviderAsync ( ) ;
if ( ! selectedProvider ) {
throw new Error ( 'No AI provider is selected. Please select a provider (OpenAI, Anthropic, or Ollama) in your AI settings.' ) ;
}
try {
const service = await this . getOrCreateChatProvider ( selectedProvider ) ;
if ( service ) {
return service ;
}
throw new Error ( ` Failed to create ${ selectedProvider } service ` ) ;
} catch ( error ) {
log . error ( ` Provider ${ selectedProvider } not available: ${ error } ` ) ;
throw new Error ( ` Selected AI provider ( ${ selectedProvider } ) is not available. Please check your configuration: ${ error } ` ) ;
2025-03-02 19:39:10 -08:00
}
}
/ * *
2025-06-07 00:02:26 +00:00
* Check if any AI service is available ( legacy method for backward compatibility )
2025-03-02 19:39:10 -08:00
* /
isAnyServiceAvailable ( ) : boolean {
2025-06-07 00:02:26 +00:00
this . ensureInitialized ( ) ;
// Check if we have the selected provider available
return this . getAvailableProviders ( ) . length > 0 ;
2025-03-02 19:39:10 -08:00
}
/ * *
* Get list of available providers
* /
getAvailableProviders ( ) : ServiceProviders [ ] {
2025-03-09 02:19:26 +00:00
this . ensureInitialized ( ) ;
2025-06-05 19:27:45 +00:00
const allProviders : ServiceProviders [ ] = [ 'openai' , 'anthropic' , 'ollama' ] ;
const availableProviders : ServiceProviders [ ] = [ ] ;
for ( const providerName of allProviders ) {
// Use a sync approach - check if we can create the provider
const service = this . services [ providerName ] ;
if ( service && service . isAvailable ( ) ) {
availableProviders . push ( providerName ) ;
} else {
// For providers not yet created, check configuration to see if they would be available
try {
switch ( providerName ) {
case 'openai' :
if ( options . getOption ( 'openaiApiKey' ) ) {
availableProviders . push ( providerName ) ;
}
break ;
case 'anthropic' :
if ( options . getOption ( 'anthropicApiKey' ) ) {
availableProviders . push ( providerName ) ;
}
break ;
case 'ollama' :
if ( options . getOption ( 'ollamaBaseUrl' ) ) {
availableProviders . push ( providerName ) ;
}
break ;
}
} catch ( error ) {
// Ignore configuration errors, provider just won't be available
}
}
}
return availableProviders ;
2025-03-02 19:39:10 -08:00
}
/ * *
* Generate a chat completion response using the first available AI service
* based on the configured precedence order
* /
async generateChatCompletion ( messages : Message [ ] , options : ChatCompletionOptions = { } ) : Promise < ChatResponse > {
2025-03-09 02:19:26 +00:00
this . ensureInitialized ( ) ;
2025-04-09 19:53:45 +00:00
log . info ( ` [AIServiceManager] generateChatCompletion called with options: ${ JSON . stringify ( {
model : options.model ,
stream : options.stream ,
enableTools : options.enableTools
} ) } ` );
log . info ( ` [AIServiceManager] Stream option type: ${ typeof options . stream } ` ) ;
2025-03-02 19:39:10 -08:00
if ( ! messages || messages . length === 0 ) {
throw new Error ( 'No messages provided for chat completion' ) ;
}
2025-06-07 00:02:26 +00:00
// Get the selected provider
const selectedProvider = await this . getSelectedProviderAsync ( ) ;
if ( ! selectedProvider ) {
throw new Error ( 'No AI provider is selected. Please select a provider in your AI settings.' ) ;
}
// Check if the selected provider is available
2025-03-02 19:39:10 -08:00
const availableProviders = this . getAvailableProviders ( ) ;
2025-06-07 00:02:26 +00:00
if ( ! availableProviders . includes ( selectedProvider ) ) {
throw new Error ( ` Selected AI provider ( ${ selectedProvider } ) is not available. Please check your configuration. ` ) ;
2025-03-02 19:39:10 -08:00
}
// If a specific provider is requested and available, use it
if ( options . model && options . model . includes ( ':' ) ) {
2025-06-02 21:36:19 +00:00
// Use the new configuration system to parse model identifier
const modelIdentifier = parseModelIdentifier ( options . model ) ;
2025-03-02 19:39:10 -08:00
2025-06-07 00:02:26 +00:00
if ( modelIdentifier . provider && modelIdentifier . provider === selectedProvider ) {
2025-03-02 19:39:10 -08:00
try {
2025-06-07 00:02:26 +00:00
const service = await this . getOrCreateChatProvider ( modelIdentifier . provider as ServiceProviders ) ;
2025-06-05 19:27:45 +00:00
if ( service ) {
const modifiedOptions = { . . . options , model : modelIdentifier.modelId } ;
log . info ( ` [AIServiceManager] Using provider ${ modelIdentifier . provider } from model prefix with modifiedOptions.stream: ${ modifiedOptions . stream } ` ) ;
return await service . generateChatCompletion ( messages , modifiedOptions ) ;
}
2025-03-02 19:39:10 -08:00
} catch ( error ) {
2025-06-02 21:36:19 +00:00
log . error ( ` Error with specified provider ${ modelIdentifier . provider } : ${ error } ` ) ;
2025-06-07 00:02:26 +00:00
throw new Error ( ` Failed to use specified provider ${ modelIdentifier . provider } : ${ error } ` ) ;
2025-03-02 19:39:10 -08:00
}
2025-06-07 00:02:26 +00:00
} else if ( modelIdentifier . provider && modelIdentifier . provider !== selectedProvider ) {
throw new Error ( ` Model specifies provider ' ${ modelIdentifier . provider } ' but selected provider is ' ${ selectedProvider } '. Please select the correct provider or use a model without provider prefix. ` ) ;
2025-03-02 19:39:10 -08:00
}
2025-06-02 15:12:08 +00:00
// If not a provider prefix, treat the entire string as a model name and continue with normal provider selection
2025-03-02 19:39:10 -08:00
}
2025-06-07 00:02:26 +00:00
// Use the selected provider
try {
2025-06-06 20:30:24 +00:00
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 ) ;
2025-06-07 00:02:26 +00:00
} catch ( error ) {
log . error ( ` Error with selected provider ${ selectedProvider } : ${ error } ` ) ;
throw new Error ( ` Selected AI provider ( ${ selectedProvider } ) failed: ${ error } ` ) ;
2025-06-06 20:30:24 +00:00
}
2025-03-02 19:39:10 -08:00
}
2025-03-11 18:07:28 +00:00
setupEventListeners() {
// Setup event listeners for AI services
}
/ * *
* Get the context extractor service
* @returns The context extractor instance
* /
getContextExtractor() {
return contextExtractor ;
}
2025-03-19 19:28:02 +00:00
/ * *
* Get the context service for advanced context management
* @returns The context service instance
* /
getContextService() {
return contextService ;
2025-03-11 18:07:28 +00:00
}
2025-03-17 16:23:58 +00:00
2025-03-11 23:26:47 +00:00
/ * *
* Get the index service for managing knowledge base indexing
2025-06-07 18:11:12 +00:00
* @returns null since index service has been removed
2025-03-11 23:26:47 +00:00
* /
getIndexService() {
2025-06-07 18:11:12 +00:00
log . info ( 'Index service has been removed - returning null' ) ;
return null ;
2025-03-11 23:26:47 +00:00
}
2025-03-19 16:19:48 +00:00
/ * *
2025-04-11 22:52:09 +00:00
* Ensure agent tools are initialized ( no - op as they ' re initialized in constructor )
* Kept for backward compatibility with existing API
2025-03-19 16:19:48 +00:00
* /
async initializeAgentTools ( ) : Promise < void > {
2025-04-11 22:52:09 +00:00
// Agent tools are already initialized in the constructor
// This method is kept for backward compatibility
2025-04-13 21:16:18 +00:00
log . info ( "initializeAgentTools called, but tools are already initialized in constructor" ) ;
2025-03-19 16:19:48 +00:00
}
/ * *
* Get the agent tools manager
* This provides access to all agent tools
* /
getAgentTools() {
return agentTools ;
}
/ * *
* Get the vector search tool for semantic similarity search
2025-06-07 18:11:12 +00:00
* Returns null since vector search has been removed
2025-03-19 16:19:48 +00:00
* /
getVectorSearchTool() {
2025-06-07 18:11:12 +00:00
log . info ( 'Vector search has been removed - getVectorSearchTool returning null' ) ;
return null ;
2025-03-19 16:19:48 +00:00
}
/ * *
* Get the note navigator tool for hierarchical exploration
* /
getNoteNavigatorTool() {
2025-04-08 21:24:56 +00:00
const tools = agentTools . getTools ( ) ;
return tools . noteNavigator ;
2025-03-19 16:19:48 +00:00
}
/ * *
* Get the query decomposition tool for complex queries
* /
getQueryDecompositionTool() {
2025-04-08 21:24:56 +00:00
const tools = agentTools . getTools ( ) ;
return tools . queryDecomposition ;
2025-03-19 16:19:48 +00:00
}
/ * *
* Get the contextual thinking tool for transparent reasoning
* /
getContextualThinkingTool() {
2025-04-08 21:24:56 +00:00
const tools = agentTools . getTools ( ) ;
return tools . contextualThinking ;
2025-03-19 16:19:48 +00:00
}
/ * *
2025-06-02 21:36:19 +00:00
* Get whether AI features are enabled using the new configuration system
2025-03-19 16:19:48 +00:00
* /
2025-06-02 21:49:35 +00:00
async getAIEnabledAsync ( ) : Promise < boolean > {
return isAIEnabled ( ) ;
}
/ * *
* Get whether AI features are enabled ( sync version for compatibility )
* /
2025-03-19 16:19:48 +00:00
getAIEnabled ( ) : boolean {
2025-06-02 21:36:19 +00:00
// For synchronous compatibility, use the old method
// In a full refactor, this should be async
2025-03-19 16:19:48 +00:00
return options . getOptionBool ( 'aiEnabled' ) ;
}
/ * *
2025-06-06 20:30:24 +00:00
* Get or create a chat provider on - demand with inline validation
2025-03-19 16:19:48 +00:00
* /
2025-06-05 19:27:45 +00:00
private async getOrCreateChatProvider ( providerName : ServiceProviders ) : Promise < AIService | null > {
// Return existing provider if already created
if ( this . services [ providerName ] ) {
return this . services [ providerName ] ;
}
2025-03-19 16:19:48 +00:00
2025-06-06 20:30:24 +00:00
// Create and validate provider on-demand
2025-06-05 19:27:45 +00:00
try {
2025-06-06 20:30:24 +00:00
let service : AIService | null = null ;
2025-06-05 19:27:45 +00:00
switch ( providerName ) {
2025-06-06 20:30:24 +00:00
case 'openai' : {
2025-06-07 00:02:26 +00:00
const apiKey = options . getOption ( 'openaiApiKey' ) ;
const baseUrl = options . getOption ( 'openaiBaseUrl' ) ;
2025-06-06 20:30:24 +00:00
if ( ! apiKey && ! baseUrl ) return null ;
service = new OpenAIService ( ) ;
// Validate by checking if it's available
if ( ! service . isAvailable ( ) ) {
throw new Error ( 'OpenAI service not available' ) ;
2025-06-05 19:27:45 +00:00
}
break ;
2025-06-06 20:30:24 +00:00
}
2025-06-05 19:27:45 +00:00
2025-06-06 20:30:24 +00:00
case 'anthropic' : {
2025-06-07 00:02:26 +00:00
const apiKey = options . getOption ( 'anthropicApiKey' ) ;
2025-06-06 20:30:24 +00:00
if ( ! apiKey ) return null ;
service = new AnthropicService ( ) ;
if ( ! service . isAvailable ( ) ) {
throw new Error ( 'Anthropic service not available' ) ;
2025-06-05 19:27:45 +00:00
}
break ;
2025-06-06 20:30:24 +00:00
}
2025-06-05 19:27:45 +00:00
2025-06-06 20:30:24 +00:00
case 'ollama' : {
2025-06-07 00:02:26 +00:00
const baseUrl = options . getOption ( 'ollamaBaseUrl' ) ;
2025-06-06 20:30:24 +00:00
if ( ! baseUrl ) return null ;
service = new OllamaService ( ) ;
if ( ! service . isAvailable ( ) ) {
throw new Error ( 'Ollama service not available' ) ;
2025-06-05 19:27:45 +00:00
}
break ;
2025-06-06 20:30:24 +00:00
}
}
if ( service ) {
this . services [ providerName ] = service ;
return service ;
2025-03-19 16:19:48 +00:00
}
} catch ( error : any ) {
2025-06-06 20:30:24 +00:00
log . error ( ` Failed to create ${ providerName } chat provider: ${ error . message || 'Unknown error' } ` ) ;
2025-03-19 16:19:48 +00:00
}
2025-06-05 19:27:45 +00:00
return null ;
2025-03-19 16:19:48 +00:00
}
/ * *
2025-06-02 21:49:35 +00:00
* Initialize the AI Service using the new configuration system
2025-03-19 16:19:48 +00:00
* /
async initialize ( ) : Promise < void > {
try {
log . info ( "Initializing AI service..." ) ;
2025-06-02 21:36:19 +00:00
// Check if AI is enabled using the new helper
2025-06-02 21:49:35 +00:00
const aiEnabled = await isAIEnabled ( ) ;
2025-03-19 16:19:48 +00:00
2025-06-02 21:49:35 +00:00
if ( ! aiEnabled ) {
2025-03-19 16:19:48 +00:00
log . info ( "AI features are disabled in options" ) ;
return ;
}
2025-06-07 18:11:12 +00:00
// Index service has been removed - no initialization needed
2025-03-19 16:19:48 +00:00
2025-04-11 22:52:09 +00:00
// Tools are already initialized in the constructor
// No need to initialize them again
2025-03-19 16:19:48 +00:00
this . initialized = true ;
log . info ( "AI service initialized successfully" ) ;
} catch ( error : any ) {
log . error ( ` Error initializing AI service: ${ error . message } ` ) ;
throw error ;
}
}
2025-03-19 19:28:02 +00:00
/ * *
2025-04-08 21:24:56 +00:00
* Get description of available agent tools
* /
async getAgentToolsDescription ( ) : Promise < string > {
try {
// Get all available tools
const tools = agentTools . getAllTools ( ) ;
if ( ! tools || tools . length === 0 ) {
return "" ;
}
// Format tool descriptions
const toolDescriptions = tools . map ( tool = >
` - ${ tool . name } : ${ tool . description } `
) . join ( '\n' ) ;
return ` Available tools: \ n ${ toolDescriptions } ` ;
} catch ( error ) {
log . error ( ` Error getting agent tools description: ${ error } ` ) ;
return "" ;
}
}
/ * *
2025-04-16 17:29:35 +00:00
* Get enhanced context with available agent tools
* @param noteId - The ID of the note
2025-04-08 21:24:56 +00:00
* @param query - The user ' s query
* @param showThinking - Whether to show LLM ' s thinking process
* @param relevantNotes - Optional notes already found to be relevant
* @returns Enhanced context with agent tools information
2025-03-19 19:28:02 +00:00
* /
async getAgentToolsContext (
noteId : string ,
query : string ,
showThinking : boolean = false ,
2025-04-16 17:29:35 +00:00
relevantNotes : NoteSearchResult [ ] = [ ]
2025-03-19 19:28:02 +00:00
) : Promise < string > {
2025-03-19 20:09:18 +00:00
try {
2025-04-08 21:24:56 +00:00
// Create agent tools message
const toolsMessage = await this . getAgentToolsDescription ( ) ;
2025-04-16 17:29:35 +00:00
2025-04-11 22:52:09 +00:00
// Agent tools are already initialized in the constructor
// No need to initialize them again
2025-04-08 21:24:56 +00:00
// If we have notes that were already found to be relevant, use them directly
let contextNotes = relevantNotes ;
// If no notes provided, find relevant ones
if ( ! contextNotes || contextNotes . length === 0 ) {
try {
// Get the default LLM service for context enhancement
2025-06-05 22:34:20 +00:00
const provider = this . getSelectedProvider ( ) ;
2025-06-05 19:27:45 +00:00
const llmService = await this . getService ( provider ) ;
2025-04-08 21:24:56 +00:00
// Find relevant notes
contextNotes = await contextService . findRelevantNotes (
query ,
noteId ,
{
maxResults : 5 ,
summarize : true ,
llmService
}
) ;
log . info ( ` Found ${ contextNotes . length } relevant notes for context ` ) ;
} catch ( error ) {
2025-04-16 17:29:35 +00:00
log . error ( ` Failed to find relevant notes: ${ this . handleError ( error ) } ` ) ;
2025-04-08 21:24:56 +00:00
// Continue without context notes
contextNotes = [ ] ;
}
}
// Format notes into context string if we have any
let contextStr = "" ;
if ( contextNotes && contextNotes . length > 0 ) {
contextStr = "\n\nRelevant context:\n" ;
contextNotes . forEach ( ( note , index ) = > {
contextStr += ` [ ${ index + 1 } ] " ${ note . title } " \ n ${ note . content || 'No content available' } \ n \ n ` ;
} ) ;
}
// Combine tool message with context
return toolsMessage + contextStr ;
2025-03-19 20:09:18 +00:00
} catch ( error ) {
2025-04-16 17:29:35 +00:00
log . error ( ` Error getting agent tools context: ${ this . handleError ( error ) } ` ) ;
2025-04-08 21:24:56 +00:00
return "" ;
2025-03-28 21:04:12 +00:00
}
}
/ * *
* Get AI service for the given provider
* /
2025-06-05 19:27:45 +00:00
async getService ( provider? : string ) : Promise < AIService > {
2025-03-28 21:04:12 +00:00
this . ensureInitialized ( ) ;
2025-06-05 19:27:45 +00:00
// If provider is specified, try to get or create it
if ( provider ) {
const service = await this . getOrCreateChatProvider ( provider as ServiceProviders ) ;
if ( service && service . isAvailable ( ) ) {
return service ;
}
2025-06-07 00:02:26 +00:00
throw new Error ( ` Specified provider ${ provider } is not available ` ) ;
2025-03-19 20:09:18 +00:00
}
2025-03-28 21:04:12 +00:00
2025-06-07 00:02:26 +00:00
// Otherwise, use the selected provider
const selectedProvider = await this . getSelectedProviderAsync ( ) ;
if ( ! selectedProvider ) {
throw new Error ( 'No AI provider is selected. Please select a provider in your AI settings.' ) ;
}
const service = await this . getOrCreateChatProvider ( selectedProvider ) ;
if ( service && service . isAvailable ( ) ) {
return service ;
2025-03-28 21:04:12 +00:00
}
2025-06-05 19:27:45 +00:00
// If no provider is available, throw a clear error
2025-06-07 00:02:26 +00:00
throw new Error ( ` Selected AI provider ( ${ selectedProvider } ) is not available. Please check your AI settings. ` ) ;
2025-03-28 21:04:12 +00:00
}
/ * *
2025-06-02 21:36:19 +00:00
* Get the preferred provider based on configuration using the new system
* /
async getPreferredProviderAsync ( ) : Promise < string > {
try {
2025-06-04 20:13:13 +00:00
const selectedProvider = await getSelectedProvider ( ) ;
if ( selectedProvider === null ) {
2025-06-07 00:02:26 +00:00
// No provider selected, fallback to default
log . info ( 'No provider selected, using default provider' ) ;
return 'openai' ;
2025-06-02 21:43:36 +00:00
}
2025-06-04 20:13:13 +00:00
return selectedProvider ;
2025-06-02 21:36:19 +00:00
} catch ( error ) {
log . error ( ` Error getting preferred provider: ${ error } ` ) ;
2025-06-07 00:02:26 +00:00
return 'openai' ;
2025-06-02 21:36:19 +00:00
}
}
/ * *
2025-06-05 22:34:20 +00:00
* Get the selected provider based on configuration ( sync version for compatibility )
2025-03-28 21:04:12 +00:00
* /
2025-06-05 22:34:20 +00:00
getSelectedProvider ( ) : string {
2025-03-28 21:04:12 +00:00
this . ensureInitialized ( ) ;
2025-06-07 00:02:26 +00:00
// Try to get the selected provider synchronously
try {
const selectedProvider = options . getOption ( 'aiSelectedProvider' ) ;
if ( selectedProvider ) {
return selectedProvider ;
2025-03-28 21:04:12 +00:00
}
2025-06-07 00:02:26 +00:00
} catch ( error ) {
log . error ( ` Error getting selected provider: ${ error } ` ) ;
2025-03-28 21:04:12 +00:00
}
2025-06-07 00:02:26 +00:00
// Return a default if nothing is selected (for backward compatibility)
return 'openai' ;
2025-03-28 21:04:12 +00:00
}
/ * *
* Check if a specific provider is available
* /
isProviderAvailable ( provider : string ) : boolean {
return this . services [ provider as ServiceProviders ] ? . isAvailable ( ) ? ? false ;
}
/ * *
* Get metadata about a provider
* /
getProviderMetadata ( provider : string ) : ProviderMetadata | null {
const service = this . services [ provider as ServiceProviders ] ;
if ( ! service ) {
return null ;
}
return {
name : provider ,
capabilities : {
chat : true ,
streaming : true ,
functionCalling : provider === 'openai' // Only OpenAI has function calling
} ,
models : [ 'default' ] , // Placeholder, could be populated from the service
defaultModel : 'default'
} ;
2025-03-19 19:28:02 +00:00
}
2025-04-16 17:29:35 +00:00
2025-06-04 20:13:13 +00:00
2025-04-16 17:29:35 +00:00
/ * *
* Error handler that properly types the error object
* /
private handleError ( error : unknown ) : string {
if ( error instanceof Error ) {
return error . message || String ( error ) ;
}
return String ( error ) ;
}
2025-06-04 20:13:13 +00:00
/ * *
* Set up event listener for provider changes
* /
private setupProviderChangeListener ( ) : void {
// List of AI-related options that should trigger service recreation
const aiRelatedOptions = [
2025-06-05 18:47:25 +00:00
'aiEnabled' ,
2025-06-04 20:13:13 +00:00
'aiSelectedProvider' ,
'openaiApiKey' ,
'openaiBaseUrl' ,
'openaiDefaultModel' ,
'anthropicApiKey' ,
'anthropicBaseUrl' ,
'anthropicDefaultModel' ,
'ollamaBaseUrl' ,
2025-06-07 18:11:12 +00:00
'ollamaDefaultModel'
2025-06-04 20:13:13 +00:00
] ;
2025-06-05 18:47:25 +00:00
eventService . subscribe ( [ 'entityChanged' ] , async ( { entityName , entity } ) = > {
2025-06-04 20:13:13 +00:00
if ( entityName === 'options' && entity && aiRelatedOptions . includes ( entity . name ) ) {
log . info ( ` AI-related option ' ${ entity . name } ' changed, recreating LLM services ` ) ;
2025-06-05 18:47:25 +00:00
// Special handling for aiEnabled toggle
if ( entity . name === 'aiEnabled' ) {
const isEnabled = entity . value === 'true' ;
if ( isEnabled ) {
2025-06-07 18:11:12 +00:00
log . info ( 'AI features enabled, initializing AI service' ) ;
2025-06-05 18:47:25 +00:00
// Initialize the AI service
await this . initialize ( ) ;
} else {
2025-06-07 18:11:12 +00:00
log . info ( 'AI features disabled, clearing providers' ) ;
2025-06-05 19:27:45 +00:00
// Clear chat providers
this . services = { } ;
2025-06-05 18:47:25 +00:00
}
} else {
2025-06-05 19:27:45 +00:00
// For other AI-related options, recreate services on-demand
await this . recreateServices ( ) ;
2025-06-05 18:47:25 +00:00
}
2025-06-04 20:13:13 +00:00
}
} ) ;
}
/ * *
* Recreate LLM services when provider settings change
* /
private async recreateServices ( ) : Promise < void > {
try {
log . info ( 'Recreating LLM services due to configuration change' ) ;
// Clear configuration cache first
clearConfigurationCache ( ) ;
2025-06-05 19:27:45 +00:00
// Clear existing chat providers (they will be recreated on-demand)
this . services = { } ;
2025-06-04 20:13:13 +00:00
log . info ( 'LLM services recreated successfully' ) ;
} catch ( error ) {
log . error ( ` Error recreating LLM services: ${ this . handleError ( error ) } ` ) ;
}
}
2025-03-02 19:39:10 -08:00
}
2025-03-09 02:19:26 +00:00
// Don't create singleton immediately, use a lazy-loading pattern
let instance : AIServiceManager | null = null ;
/ * *
* Get the AIServiceManager instance ( creates it if not already created )
* /
function getInstance ( ) : AIServiceManager {
if ( ! instance ) {
instance = new AIServiceManager ( ) ;
}
return instance ;
}
export default {
getInstance ,
// Also export methods directly for convenience
isAnyServiceAvailable ( ) : boolean {
return getInstance ( ) . isAnyServiceAvailable ( ) ;
} ,
2025-06-07 00:02:26 +00:00
async getOrCreateAnyService ( ) : Promise < AIService > {
return getInstance ( ) . getOrCreateAnyService ( ) ;
} ,
2025-03-09 02:19:26 +00:00
getAvailableProviders() {
return getInstance ( ) . getAvailableProviders ( ) ;
} ,
async generateChatCompletion ( messages : Message [ ] , options : ChatCompletionOptions = { } ) : Promise < ChatResponse > {
return getInstance ( ) . generateChatCompletion ( messages , options ) ;
2025-03-11 18:07:28 +00:00
} ,
2025-03-11 23:26:47 +00:00
// Context and index related methods
2025-03-11 18:07:28 +00:00
getContextExtractor() {
return getInstance ( ) . getContextExtractor ( ) ;
} ,
2025-03-19 19:28:02 +00:00
getContextService() {
return getInstance ( ) . getContextService ( ) ;
} ,
2025-03-11 23:26:47 +00:00
getIndexService() {
return getInstance ( ) . getIndexService ( ) ;
2025-03-19 16:19:48 +00:00
} ,
// Agent tools related methods
2025-04-11 22:52:09 +00:00
// Tools are now initialized in the constructor
2025-03-19 16:19:48 +00:00
getAgentTools() {
return getInstance ( ) . getAgentTools ( ) ;
} ,
getVectorSearchTool() {
return getInstance ( ) . getVectorSearchTool ( ) ;
} ,
getNoteNavigatorTool() {
return getInstance ( ) . getNoteNavigatorTool ( ) ;
} ,
getQueryDecompositionTool() {
return getInstance ( ) . getQueryDecompositionTool ( ) ;
} ,
getContextualThinkingTool() {
return getInstance ( ) . getContextualThinkingTool ( ) ;
2025-03-19 19:28:02 +00:00
} ,
async getAgentToolsContext (
noteId : string ,
query : string ,
showThinking : boolean = false ,
2025-04-16 17:29:35 +00:00
relevantNotes : NoteSearchResult [ ] = [ ]
2025-03-19 19:28:02 +00:00
) : Promise < string > {
return getInstance ( ) . getAgentToolsContext (
noteId ,
query ,
showThinking ,
relevantNotes
) ;
2025-03-28 21:04:12 +00:00
} ,
// New methods
2025-06-05 19:27:45 +00:00
async getService ( provider? : string ) : Promise < AIService > {
2025-03-28 21:04:12 +00:00
return getInstance ( ) . getService ( provider ) ;
} ,
2025-06-05 22:34:20 +00:00
getSelectedProvider ( ) : string {
return getInstance ( ) . getSelectedProvider ( ) ;
2025-03-28 21:04:12 +00:00
} ,
isProviderAvailable ( provider : string ) : boolean {
return getInstance ( ) . isProviderAvailable ( provider ) ;
} ,
getProviderMetadata ( provider : string ) : ProviderMetadata | null {
return getInstance ( ) . getProviderMetadata ( provider ) ;
2025-03-09 02:19:26 +00:00
}
} ;
2025-03-11 18:39:59 +00:00
// Create an instance of ContextExtractor for backward compatibility
const contextExtractor = new ContextExtractor ( ) ;