mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-07-29 19:12:27 +08:00
feat(llm): create endpoints for starting/stopping embeddings
This commit is contained in:
parent
a084805762
commit
49e123f399
@ -51,6 +51,35 @@ export default class AiSettingsWidget extends OptionsWidget {
|
|||||||
|
|
||||||
await this.updateOption(optionName, value);
|
await this.updateOption(optionName, value);
|
||||||
|
|
||||||
|
// Special handling for aiEnabled option
|
||||||
|
if (optionName === 'aiEnabled') {
|
||||||
|
try {
|
||||||
|
const isEnabled = value === 'true';
|
||||||
|
|
||||||
|
if (isEnabled) {
|
||||||
|
// Start embedding generation
|
||||||
|
await server.post('llm/embeddings/start');
|
||||||
|
toastService.showMessage(t("ai_llm.embeddings_started") || "Embedding generation started");
|
||||||
|
|
||||||
|
// Start polling for stats updates
|
||||||
|
this.refreshEmbeddingStats();
|
||||||
|
} else {
|
||||||
|
// Stop embedding generation
|
||||||
|
await server.post('llm/embeddings/stop');
|
||||||
|
toastService.showMessage(t("ai_llm.embeddings_stopped") || "Embedding generation stopped");
|
||||||
|
|
||||||
|
// Clear any active polling intervals
|
||||||
|
if (this.indexRebuildRefreshInterval) {
|
||||||
|
clearInterval(this.indexRebuildRefreshInterval);
|
||||||
|
this.indexRebuildRefreshInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error toggling embeddings:', error);
|
||||||
|
toastService.showError(t("ai_llm.embeddings_toggle_error") || "Error toggling embeddings");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (validateAfter) {
|
if (validateAfter) {
|
||||||
await this.displayValidationWarnings();
|
await this.displayValidationWarnings();
|
||||||
}
|
}
|
||||||
|
@ -782,6 +782,49 @@ async function getIndexRebuildStatus(req: Request, res: Response) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start embedding generation when AI is enabled
|
||||||
|
*/
|
||||||
|
async function startEmbeddings(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
log.info("Starting embedding generation system");
|
||||||
|
|
||||||
|
// Initialize the index service if not already initialized
|
||||||
|
await indexService.initialize();
|
||||||
|
|
||||||
|
// Start automatic indexing
|
||||||
|
await indexService.startEmbeddingGeneration();
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Embedding generation started"
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error(`Error starting embeddings: ${error.message || 'Unknown error'}`);
|
||||||
|
throw new Error(`Failed to start embeddings: ${error.message || 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop embedding generation when AI is disabled
|
||||||
|
*/
|
||||||
|
async function stopEmbeddings(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
log.info("Stopping embedding generation system");
|
||||||
|
|
||||||
|
// Stop automatic indexing
|
||||||
|
await indexService.stopEmbeddingGeneration();
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Embedding generation stopped"
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error(`Error stopping embeddings: ${error.message || 'Unknown error'}`);
|
||||||
|
throw new Error(`Failed to stop embeddings: ${error.message || 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
findSimilarNotes,
|
findSimilarNotes,
|
||||||
searchByText,
|
searchByText,
|
||||||
@ -794,5 +837,7 @@ export default {
|
|||||||
retryFailedNote,
|
retryFailedNote,
|
||||||
retryAllFailedNotes,
|
retryAllFailedNotes,
|
||||||
rebuildIndex,
|
rebuildIndex,
|
||||||
getIndexRebuildStatus
|
getIndexRebuildStatus,
|
||||||
|
startEmbeddings,
|
||||||
|
stopEmbeddings
|
||||||
};
|
};
|
||||||
|
@ -400,6 +400,8 @@ function register(app: express.Application) {
|
|||||||
asyncApiRoute(PST, "/api/llm/embeddings/retry-all-failed", embeddingsRoute.retryAllFailedNotes);
|
asyncApiRoute(PST, "/api/llm/embeddings/retry-all-failed", embeddingsRoute.retryAllFailedNotes);
|
||||||
asyncApiRoute(PST, "/api/llm/embeddings/rebuild-index", embeddingsRoute.rebuildIndex);
|
asyncApiRoute(PST, "/api/llm/embeddings/rebuild-index", embeddingsRoute.rebuildIndex);
|
||||||
asyncApiRoute(GET, "/api/llm/embeddings/index-rebuild-status", embeddingsRoute.getIndexRebuildStatus);
|
asyncApiRoute(GET, "/api/llm/embeddings/index-rebuild-status", embeddingsRoute.getIndexRebuildStatus);
|
||||||
|
asyncApiRoute(PST, "/api/llm/embeddings/start", embeddingsRoute.startEmbeddings);
|
||||||
|
asyncApiRoute(PST, "/api/llm/embeddings/stop", embeddingsRoute.stopEmbeddings);
|
||||||
|
|
||||||
// LLM provider endpoints - moved under /api/llm/providers hierarchy
|
// LLM provider endpoints - moved under /api/llm/providers hierarchy
|
||||||
asyncApiRoute(GET, "/api/llm/providers/ollama/models", ollamaRoute.listModels);
|
asyncApiRoute(GET, "/api/llm/providers/ollama/models", ollamaRoute.listModels);
|
||||||
|
@ -605,6 +605,7 @@ export class AIServiceManager implements IAIServiceManager {
|
|||||||
private setupProviderChangeListener(): void {
|
private setupProviderChangeListener(): void {
|
||||||
// List of AI-related options that should trigger service recreation
|
// List of AI-related options that should trigger service recreation
|
||||||
const aiRelatedOptions = [
|
const aiRelatedOptions = [
|
||||||
|
'aiEnabled',
|
||||||
'aiSelectedProvider',
|
'aiSelectedProvider',
|
||||||
'embeddingSelectedProvider',
|
'embeddingSelectedProvider',
|
||||||
'openaiApiKey',
|
'openaiApiKey',
|
||||||
@ -618,10 +619,29 @@ export class AIServiceManager implements IAIServiceManager {
|
|||||||
'voyageApiKey'
|
'voyageApiKey'
|
||||||
];
|
];
|
||||||
|
|
||||||
eventService.subscribe(['entityChanged'], ({ entityName, entity }) => {
|
eventService.subscribe(['entityChanged'], async ({ entityName, entity }) => {
|
||||||
if (entityName === 'options' && entity && aiRelatedOptions.includes(entity.name)) {
|
if (entityName === 'options' && entity && aiRelatedOptions.includes(entity.name)) {
|
||||||
log.info(`AI-related option '${entity.name}' changed, recreating LLM services`);
|
log.info(`AI-related option '${entity.name}' changed, recreating LLM services`);
|
||||||
this.recreateServices();
|
|
||||||
|
// Special handling for aiEnabled toggle
|
||||||
|
if (entity.name === 'aiEnabled') {
|
||||||
|
const isEnabled = entity.value === 'true';
|
||||||
|
|
||||||
|
if (isEnabled) {
|
||||||
|
log.info('AI features enabled, initializing AI service and embeddings');
|
||||||
|
// Initialize the AI service
|
||||||
|
await this.initialize();
|
||||||
|
// Initialize embeddings through index service
|
||||||
|
await indexService.startEmbeddingGeneration();
|
||||||
|
} else {
|
||||||
|
log.info('AI features disabled, stopping embeddings');
|
||||||
|
// Stop embeddings through index service
|
||||||
|
await indexService.stopEmbeddingGeneration();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For other AI-related options, just recreate services
|
||||||
|
this.recreateServices();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,9 @@ import becca from "../../../becca/becca.js";
|
|||||||
// Add mutex to prevent concurrent processing
|
// Add mutex to prevent concurrent processing
|
||||||
let isProcessingEmbeddings = false;
|
let isProcessingEmbeddings = false;
|
||||||
|
|
||||||
|
// Store interval reference for cleanup
|
||||||
|
let backgroundProcessingInterval: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup event listeners for embedding-related events
|
* Setup event listeners for embedding-related events
|
||||||
*/
|
*/
|
||||||
@ -53,9 +56,15 @@ export function setupEmbeddingEventListeners() {
|
|||||||
* Setup background processing of the embedding queue
|
* Setup background processing of the embedding queue
|
||||||
*/
|
*/
|
||||||
export async function setupEmbeddingBackgroundProcessing() {
|
export async function setupEmbeddingBackgroundProcessing() {
|
||||||
|
// Clear any existing interval
|
||||||
|
if (backgroundProcessingInterval) {
|
||||||
|
clearInterval(backgroundProcessingInterval);
|
||||||
|
backgroundProcessingInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
const interval = parseInt(await options.getOption('embeddingUpdateInterval') || '200', 10);
|
const interval = parseInt(await options.getOption('embeddingUpdateInterval') || '200', 10);
|
||||||
|
|
||||||
setInterval(async () => {
|
backgroundProcessingInterval = setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
// Skip if already processing
|
// Skip if already processing
|
||||||
if (isProcessingEmbeddings) {
|
if (isProcessingEmbeddings) {
|
||||||
@ -78,6 +87,17 @@ export async function setupEmbeddingBackgroundProcessing() {
|
|||||||
}, interval);
|
}, interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop background processing of the embedding queue
|
||||||
|
*/
|
||||||
|
export function stopEmbeddingBackgroundProcessing() {
|
||||||
|
if (backgroundProcessingInterval) {
|
||||||
|
clearInterval(backgroundProcessingInterval);
|
||||||
|
backgroundProcessingInterval = null;
|
||||||
|
log.info("Embedding background processing stopped");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize embeddings system
|
* Initialize embeddings system
|
||||||
*/
|
*/
|
||||||
|
@ -58,6 +58,7 @@ export const processNoteWithChunking = async (
|
|||||||
export const {
|
export const {
|
||||||
setupEmbeddingEventListeners,
|
setupEmbeddingEventListeners,
|
||||||
setupEmbeddingBackgroundProcessing,
|
setupEmbeddingBackgroundProcessing,
|
||||||
|
stopEmbeddingBackgroundProcessing,
|
||||||
initEmbeddings
|
initEmbeddings
|
||||||
} = events;
|
} = events;
|
||||||
|
|
||||||
@ -100,6 +101,7 @@ export default {
|
|||||||
// Event handling
|
// Event handling
|
||||||
setupEmbeddingEventListeners: events.setupEmbeddingEventListeners,
|
setupEmbeddingEventListeners: events.setupEmbeddingEventListeners,
|
||||||
setupEmbeddingBackgroundProcessing: events.setupEmbeddingBackgroundProcessing,
|
setupEmbeddingBackgroundProcessing: events.setupEmbeddingBackgroundProcessing,
|
||||||
|
stopEmbeddingBackgroundProcessing: events.stopEmbeddingBackgroundProcessing,
|
||||||
initEmbeddings: events.initEmbeddings,
|
initEmbeddings: events.initEmbeddings,
|
||||||
|
|
||||||
// Stats and maintenance
|
// Stats and maintenance
|
||||||
|
@ -837,6 +837,81 @@ export class IndexService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start embedding generation (called when AI is enabled)
|
||||||
|
*/
|
||||||
|
async startEmbeddingGeneration() {
|
||||||
|
try {
|
||||||
|
log.info("Starting embedding generation system");
|
||||||
|
|
||||||
|
// Re-initialize if needed
|
||||||
|
if (!this.initialized) {
|
||||||
|
await this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
const aiEnabled = options.getOptionOrNull('aiEnabled') === "true";
|
||||||
|
if (!aiEnabled) {
|
||||||
|
log.error("Cannot start embedding generation - AI features are disabled");
|
||||||
|
throw new Error("AI features must be enabled first");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this instance should process embeddings
|
||||||
|
const embeddingLocation = await options.getOption('embeddingGenerationLocation') || 'client';
|
||||||
|
const isSyncServer = await this.isSyncServerForEmbeddings();
|
||||||
|
const shouldProcessEmbeddings = embeddingLocation === 'client' || isSyncServer;
|
||||||
|
|
||||||
|
if (!shouldProcessEmbeddings) {
|
||||||
|
log.info("This instance is not configured to process embeddings");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup automatic indexing if enabled
|
||||||
|
if (await options.getOptionBool('embeddingAutoUpdateEnabled')) {
|
||||||
|
this.setupAutomaticIndexing();
|
||||||
|
log.info(`Automatic embedding indexing started ${isSyncServer ? 'as sync server' : 'as client'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-initialize event listeners
|
||||||
|
this.setupEventListeners();
|
||||||
|
|
||||||
|
// Start processing the queue immediately
|
||||||
|
await this.runBatchIndexing(20);
|
||||||
|
|
||||||
|
log.info("Embedding generation started successfully");
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error(`Error starting embedding generation: ${error.message || "Unknown error"}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop embedding generation (called when AI is disabled)
|
||||||
|
*/
|
||||||
|
async stopEmbeddingGeneration() {
|
||||||
|
try {
|
||||||
|
log.info("Stopping embedding generation system");
|
||||||
|
|
||||||
|
// Clear automatic indexing interval
|
||||||
|
if (this.automaticIndexingInterval) {
|
||||||
|
clearInterval(this.automaticIndexingInterval);
|
||||||
|
this.automaticIndexingInterval = undefined;
|
||||||
|
log.info("Automatic indexing stopped");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop the background processing from embeddings/events.ts
|
||||||
|
vectorStore.stopEmbeddingBackgroundProcessing();
|
||||||
|
|
||||||
|
// Mark as not indexing
|
||||||
|
this.indexingInProgress = false;
|
||||||
|
this.indexRebuildInProgress = false;
|
||||||
|
|
||||||
|
log.info("Embedding generation stopped successfully");
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error(`Error stopping embedding generation: ${error.message || "Unknown error"}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create singleton instance
|
// Create singleton instance
|
||||||
|
Loading…
x
Reference in New Issue
Block a user