mirror of
https://github.com/zadam/trilium.git
synced 2025-12-23 16:49:58 +01:00
Merge pull request #2205 from TriliumNext/feat/llm-remove-embeddings
Remove Embeddings from LLM feature
This commit is contained in:
@@ -48,17 +48,6 @@ interface AnthropicModel {
|
||||
* type: string
|
||||
* type:
|
||||
* type: string
|
||||
* embeddingModels:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: string
|
||||
* name:
|
||||
* type: string
|
||||
* type:
|
||||
* type: string
|
||||
* '500':
|
||||
* description: Error listing models
|
||||
* security:
|
||||
@@ -90,14 +79,10 @@ async function listModels(req: Request, res: Response) {
|
||||
type: 'chat'
|
||||
}));
|
||||
|
||||
// Anthropic doesn't currently have embedding models
|
||||
const embeddingModels: AnthropicModel[] = [];
|
||||
|
||||
// Return the models list
|
||||
return {
|
||||
success: true,
|
||||
chatModels,
|
||||
embeddingModels
|
||||
chatModels
|
||||
};
|
||||
} catch (error: any) {
|
||||
log.error(`Error listing Anthropic models: ${error.message || 'Unknown error'}`);
|
||||
|
||||
@@ -1,843 +0,0 @@
|
||||
import options from "../../services/options.js";
|
||||
import vectorStore from "../../services/llm/embeddings/index.js";
|
||||
import providerManager from "../../services/llm/providers/providers.js";
|
||||
import indexService from "../../services/llm/index_service.js";
|
||||
import becca from "../../becca/becca.js";
|
||||
import type { Request, Response } from "express";
|
||||
import log from "../../services/log.js";
|
||||
import sql from "../../services/sql.js";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/llm/embeddings/similar/{noteId}:
|
||||
* get:
|
||||
* summary: Find similar notes based on a given note ID
|
||||
* operationId: embeddings-similar-by-note
|
||||
* parameters:
|
||||
* - name: noteId
|
||||
* in: path
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* - name: providerId
|
||||
* in: query
|
||||
* required: false
|
||||
* schema:
|
||||
* type: string
|
||||
* default: openai
|
||||
* description: Embedding provider ID
|
||||
* - name: modelId
|
||||
* in: query
|
||||
* required: false
|
||||
* schema:
|
||||
* type: string
|
||||
* default: text-embedding-3-small
|
||||
* description: Embedding model ID
|
||||
* - name: limit
|
||||
* in: query
|
||||
* required: false
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 10
|
||||
* description: Maximum number of similar notes to return
|
||||
* - name: threshold
|
||||
* in: query
|
||||
* required: false
|
||||
* schema:
|
||||
* type: number
|
||||
* format: float
|
||||
* default: 0.7
|
||||
* description: Similarity threshold (0.0-1.0)
|
||||
* responses:
|
||||
* '200':
|
||||
* description: List of similar notes
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* similarNotes:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* noteId:
|
||||
* type: string
|
||||
* title:
|
||||
* type: string
|
||||
* similarity:
|
||||
* type: number
|
||||
* format: float
|
||||
* '400':
|
||||
* description: Invalid request parameters
|
||||
* '404':
|
||||
* description: Note not found
|
||||
* security:
|
||||
* - session: []
|
||||
* tags: ["llm"]
|
||||
*/
|
||||
async function findSimilarNotes(req: Request, res: Response) {
|
||||
const noteId = req.params.noteId;
|
||||
const providerId = req.query.providerId as string || 'openai';
|
||||
const modelId = req.query.modelId as string || 'text-embedding-3-small';
|
||||
const limit = parseInt(req.query.limit as string || '10', 10);
|
||||
const threshold = parseFloat(req.query.threshold as string || '0.7');
|
||||
|
||||
if (!noteId) {
|
||||
return [400, {
|
||||
success: false,
|
||||
message: "Note ID is required"
|
||||
}];
|
||||
}
|
||||
|
||||
const embedding = await vectorStore.getEmbeddingForNote(noteId, providerId, modelId);
|
||||
|
||||
if (!embedding) {
|
||||
// If no embedding exists for this note yet, generate one
|
||||
const note = becca.getNote(noteId);
|
||||
if (!note) {
|
||||
return [404, {
|
||||
success: false,
|
||||
message: "Note not found"
|
||||
}];
|
||||
}
|
||||
|
||||
const context = await vectorStore.getNoteEmbeddingContext(noteId);
|
||||
const provider = providerManager.getEmbeddingProvider(providerId);
|
||||
|
||||
if (!provider) {
|
||||
return [400, {
|
||||
success: false,
|
||||
message: `Embedding provider '${providerId}' not found`
|
||||
}];
|
||||
}
|
||||
|
||||
const newEmbedding = await provider.generateNoteEmbeddings(context);
|
||||
await vectorStore.storeNoteEmbedding(noteId, providerId, modelId, newEmbedding);
|
||||
|
||||
const similarNotes = await vectorStore.findSimilarNotes(
|
||||
newEmbedding, providerId, modelId, limit, threshold
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
similarNotes
|
||||
};
|
||||
}
|
||||
|
||||
const similarNotes = await vectorStore.findSimilarNotes(
|
||||
embedding.embedding, providerId, modelId, limit, threshold
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
similarNotes
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/llm/embeddings/search:
|
||||
* post:
|
||||
* summary: Search for notes similar to provided text
|
||||
* operationId: embeddings-search-by-text
|
||||
* parameters:
|
||||
* - name: providerId
|
||||
* in: query
|
||||
* required: false
|
||||
* schema:
|
||||
* type: string
|
||||
* default: openai
|
||||
* description: Embedding provider ID
|
||||
* - name: modelId
|
||||
* in: query
|
||||
* required: false
|
||||
* schema:
|
||||
* type: string
|
||||
* default: text-embedding-3-small
|
||||
* description: Embedding model ID
|
||||
* - name: limit
|
||||
* in: query
|
||||
* required: false
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 10
|
||||
* description: Maximum number of similar notes to return
|
||||
* - name: threshold
|
||||
* in: query
|
||||
* required: false
|
||||
* schema:
|
||||
* type: number
|
||||
* format: float
|
||||
* default: 0.7
|
||||
* description: Similarity threshold (0.0-1.0)
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* text:
|
||||
* type: string
|
||||
* description: Text to search with
|
||||
* responses:
|
||||
* '200':
|
||||
* description: List of similar notes
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* similarNotes:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* noteId:
|
||||
* type: string
|
||||
* title:
|
||||
* type: string
|
||||
* similarity:
|
||||
* type: number
|
||||
* format: float
|
||||
* '400':
|
||||
* description: Invalid request parameters
|
||||
* security:
|
||||
* - session: []
|
||||
* tags: ["llm"]
|
||||
*/
|
||||
async function searchByText(req: Request, res: Response) {
|
||||
const { text } = req.body;
|
||||
const providerId = req.query.providerId as string || 'openai';
|
||||
const modelId = req.query.modelId as string || 'text-embedding-3-small';
|
||||
const limit = parseInt(req.query.limit as string || '10', 10);
|
||||
const threshold = parseFloat(req.query.threshold as string || '0.7');
|
||||
|
||||
if (!text) {
|
||||
return [400, {
|
||||
success: false,
|
||||
message: "Search text is required"
|
||||
}];
|
||||
}
|
||||
|
||||
const provider = providerManager.getEmbeddingProvider(providerId);
|
||||
|
||||
if (!provider) {
|
||||
return [400, {
|
||||
success: false,
|
||||
message: `Embedding provider '${providerId}' not found`
|
||||
}];
|
||||
}
|
||||
|
||||
// Generate embedding for the search text
|
||||
const embedding = await provider.generateEmbeddings(text);
|
||||
|
||||
// Find similar notes
|
||||
const similarNotes = await vectorStore.findSimilarNotes(
|
||||
embedding, providerId, modelId, limit, threshold
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
similarNotes
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/llm/embeddings/providers:
|
||||
* get:
|
||||
* summary: Get available embedding providers
|
||||
* operationId: embeddings-get-providers
|
||||
* responses:
|
||||
* '200':
|
||||
* description: List of available embedding providers
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* providers:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: string
|
||||
* name:
|
||||
* type: string
|
||||
* isEnabled:
|
||||
* type: boolean
|
||||
* priority:
|
||||
* type: integer
|
||||
* config:
|
||||
* type: object
|
||||
* security:
|
||||
* - session: []
|
||||
* tags: ["llm"]
|
||||
*/
|
||||
async function getProviders(req: Request, res: Response) {
|
||||
const providerConfigs = await providerManager.getEmbeddingProviderConfigs();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
providers: providerConfigs
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/llm/embeddings/providers/{providerId}:
|
||||
* patch:
|
||||
* summary: Update embedding provider configuration
|
||||
* operationId: embeddings-update-provider
|
||||
* parameters:
|
||||
* - name: providerId
|
||||
* in: path
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Provider ID to update
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* enabled:
|
||||
* type: boolean
|
||||
* description: Whether provider is enabled
|
||||
* priority:
|
||||
* type: integer
|
||||
* description: Priority order (lower is higher priority)
|
||||
* config:
|
||||
* type: object
|
||||
* description: Provider-specific configuration
|
||||
* responses:
|
||||
* '200':
|
||||
* description: Provider updated successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* '400':
|
||||
* description: Invalid provider ID or configuration
|
||||
* security:
|
||||
* - session: []
|
||||
* tags: ["llm"]
|
||||
*/
|
||||
async function updateProvider(req: Request, res: Response) {
|
||||
const { providerId } = req.params;
|
||||
const { isEnabled, priority, config } = req.body;
|
||||
|
||||
const success = await providerManager.updateEmbeddingProviderConfig(
|
||||
providerId, isEnabled, priority
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
return [404, {
|
||||
success: false,
|
||||
message: "Provider not found"
|
||||
}];
|
||||
}
|
||||
|
||||
return {
|
||||
success: true
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/llm/embeddings/reprocess:
|
||||
* post:
|
||||
* summary: Reprocess embeddings for all notes
|
||||
* operationId: embeddings-reprocess-all
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* providerId:
|
||||
* type: string
|
||||
* description: Provider ID to use for reprocessing
|
||||
* modelId:
|
||||
* type: string
|
||||
* description: Model ID to use for reprocessing
|
||||
* forceReprocess:
|
||||
* type: boolean
|
||||
* description: Whether to reprocess notes that already have embeddings
|
||||
* responses:
|
||||
* '200':
|
||||
* description: Reprocessing started
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* jobId:
|
||||
* type: string
|
||||
* message:
|
||||
* type: string
|
||||
* '400':
|
||||
* description: Invalid provider ID or configuration
|
||||
* security:
|
||||
* - session: []
|
||||
* tags: ["llm"]
|
||||
*/
|
||||
async function reprocessAllNotes(req: Request, res: Response) {
|
||||
// Import cls
|
||||
const cls = (await import("../../services/cls.js")).default;
|
||||
|
||||
// Start the reprocessing operation in the background
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
// Wrap the operation in cls.init to ensure proper context
|
||||
cls.init(async () => {
|
||||
await indexService.reprocessAllNotes();
|
||||
log.info("Embedding reprocessing completed successfully");
|
||||
});
|
||||
} catch (error: any) {
|
||||
log.error(`Error during background embedding reprocessing: ${error.message || "Unknown error"}`);
|
||||
}
|
||||
}, 0);
|
||||
|
||||
// Return the response data
|
||||
return {
|
||||
success: true,
|
||||
message: "Embedding reprocessing started in the background"
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/llm/embeddings/queue-status:
|
||||
* get:
|
||||
* summary: Get status of the embedding processing queue
|
||||
* operationId: embeddings-queue-status
|
||||
* parameters:
|
||||
* - name: jobId
|
||||
* in: query
|
||||
* required: false
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Optional job ID to get status for a specific processing job
|
||||
* responses:
|
||||
* '200':
|
||||
* description: Queue status information
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [idle, processing, paused]
|
||||
* progress:
|
||||
* type: number
|
||||
* format: float
|
||||
* description: Progress percentage (0-100)
|
||||
* details:
|
||||
* type: object
|
||||
* security:
|
||||
* - session: []
|
||||
* tags: ["llm"]
|
||||
*/
|
||||
async function getQueueStatus(req: Request, res: Response) {
|
||||
// Use the imported sql instead of requiring it
|
||||
const queueCount = await sql.getValue(
|
||||
"SELECT COUNT(*) FROM embedding_queue"
|
||||
);
|
||||
|
||||
const failedCount = await sql.getValue(
|
||||
"SELECT COUNT(*) FROM embedding_queue WHERE attempts > 0"
|
||||
);
|
||||
|
||||
const totalEmbeddingsCount = await sql.getValue(
|
||||
"SELECT COUNT(*) FROM note_embeddings"
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
status: {
|
||||
queueCount,
|
||||
failedCount,
|
||||
totalEmbeddingsCount
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/llm/embeddings/stats:
|
||||
* get:
|
||||
* summary: Get embedding statistics
|
||||
* operationId: embeddings-stats
|
||||
* responses:
|
||||
* '200':
|
||||
* description: Embedding statistics
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* stats:
|
||||
* type: object
|
||||
* properties:
|
||||
* totalEmbeddings:
|
||||
* type: integer
|
||||
* providers:
|
||||
* type: object
|
||||
* modelCounts:
|
||||
* type: object
|
||||
* lastUpdated:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* security:
|
||||
* - session: []
|
||||
* tags: ["llm"]
|
||||
*/
|
||||
async function getEmbeddingStats(req: Request, res: Response) {
|
||||
const stats = await vectorStore.getEmbeddingStats();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
stats
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/llm/embeddings/failed:
|
||||
* get:
|
||||
* summary: Get list of notes that failed embedding generation
|
||||
* operationId: embeddings-failed-notes
|
||||
* responses:
|
||||
* '200':
|
||||
* description: List of failed notes
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* failedNotes:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* noteId:
|
||||
* type: string
|
||||
* title:
|
||||
* type: string
|
||||
* error:
|
||||
* type: string
|
||||
* failedAt:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* security:
|
||||
* - session: []
|
||||
* tags: ["llm"]
|
||||
*/
|
||||
async function getFailedNotes(req: Request, res: Response) {
|
||||
const limit = parseInt(req.query.limit as string || '100', 10);
|
||||
const failedNotes = await vectorStore.getFailedEmbeddingNotes(limit);
|
||||
|
||||
// No need to fetch note titles here anymore as they're already included in the response
|
||||
return {
|
||||
success: true,
|
||||
failedNotes: failedNotes
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/llm/embeddings/retry/{noteId}:
|
||||
* post:
|
||||
* summary: Retry generating embeddings for a failed note
|
||||
* operationId: embeddings-retry-note
|
||||
* parameters:
|
||||
* - name: noteId
|
||||
* in: path
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Note ID to retry
|
||||
* - name: providerId
|
||||
* in: query
|
||||
* required: false
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Provider ID to use (defaults to configured default)
|
||||
* - name: modelId
|
||||
* in: query
|
||||
* required: false
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Model ID to use (defaults to provider default)
|
||||
* responses:
|
||||
* '200':
|
||||
* description: Retry result
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* '400':
|
||||
* description: Invalid request
|
||||
* '404':
|
||||
* description: Note not found
|
||||
* security:
|
||||
* - session: []
|
||||
* tags: ["llm"]
|
||||
*/
|
||||
async function retryFailedNote(req: Request, res: Response) {
|
||||
const { noteId } = req.params;
|
||||
|
||||
if (!noteId) {
|
||||
return [400, {
|
||||
success: false,
|
||||
message: "Note ID is required"
|
||||
}];
|
||||
}
|
||||
|
||||
const success = await vectorStore.retryFailedEmbedding(noteId);
|
||||
|
||||
if (!success) {
|
||||
return [404, {
|
||||
success: false,
|
||||
message: "Failed note not found or note is not marked as failed"
|
||||
}];
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Note queued for retry"
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/llm/embeddings/retry-all-failed:
|
||||
* post:
|
||||
* summary: Retry generating embeddings for all failed notes
|
||||
* operationId: embeddings-retry-all-failed
|
||||
* requestBody:
|
||||
* required: false
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* providerId:
|
||||
* type: string
|
||||
* description: Provider ID to use (defaults to configured default)
|
||||
* modelId:
|
||||
* type: string
|
||||
* description: Model ID to use (defaults to provider default)
|
||||
* responses:
|
||||
* '200':
|
||||
* description: Retry started
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* jobId:
|
||||
* type: string
|
||||
* security:
|
||||
* - session: []
|
||||
* tags: ["llm"]
|
||||
*/
|
||||
async function retryAllFailedNotes(req: Request, res: Response) {
|
||||
const count = await vectorStore.retryAllFailedEmbeddings();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `${count} failed notes queued for retry`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/llm/embeddings/rebuild-index:
|
||||
* post:
|
||||
* summary: Rebuild the vector store index
|
||||
* operationId: embeddings-rebuild-index
|
||||
* responses:
|
||||
* '200':
|
||||
* description: Rebuild started
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* jobId:
|
||||
* type: string
|
||||
* security:
|
||||
* - session: []
|
||||
* tags: ["llm"]
|
||||
*/
|
||||
async function rebuildIndex(req: Request, res: Response) {
|
||||
// Start the index rebuilding operation in the background
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await indexService.startFullIndexing(true);
|
||||
log.info("Index rebuilding completed successfully");
|
||||
} catch (error: any) {
|
||||
log.error(`Error during background index rebuilding: ${error.message || "Unknown error"}`);
|
||||
}
|
||||
}, 0);
|
||||
|
||||
// Return the response data
|
||||
return {
|
||||
success: true,
|
||||
message: "Index rebuilding started in the background"
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/llm/embeddings/index-rebuild-status:
|
||||
* get:
|
||||
* summary: Get status of the vector index rebuild operation
|
||||
* operationId: embeddings-rebuild-status
|
||||
* parameters:
|
||||
* - name: jobId
|
||||
* in: query
|
||||
* required: false
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Optional job ID to get status for a specific rebuild job
|
||||
* responses:
|
||||
* '200':
|
||||
* description: Rebuild status information
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [idle, in_progress, completed, failed]
|
||||
* progress:
|
||||
* type: number
|
||||
* format: float
|
||||
* description: Progress percentage (0-100)
|
||||
* message:
|
||||
* type: string
|
||||
* details:
|
||||
* type: object
|
||||
* properties:
|
||||
* startTime:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* processed:
|
||||
* type: integer
|
||||
* total:
|
||||
* type: integer
|
||||
* security:
|
||||
* - session: []
|
||||
* tags: ["llm"]
|
||||
*/
|
||||
async function getIndexRebuildStatus(req: Request, res: Response) {
|
||||
const status = indexService.getIndexRebuildStatus();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
status
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
findSimilarNotes,
|
||||
searchByText,
|
||||
getProviders,
|
||||
updateProvider,
|
||||
reprocessAllNotes,
|
||||
getQueueStatus,
|
||||
getEmbeddingStats,
|
||||
getFailedNotes,
|
||||
retryFailedNote,
|
||||
retryAllFailedNotes,
|
||||
rebuildIndex,
|
||||
getIndexRebuildStatus,
|
||||
startEmbeddings,
|
||||
stopEmbeddings
|
||||
};
|
||||
@@ -2,8 +2,6 @@ import type { Request, Response } from "express";
|
||||
import log from "../../services/log.js";
|
||||
import options from "../../services/options.js";
|
||||
|
||||
// Import the index service for knowledge base management
|
||||
import indexService from "../../services/llm/index_service.js";
|
||||
import restChatService from "../../services/llm/rest_chat_service.js";
|
||||
import chatStorageService from '../../services/llm/chat_storage_service.js';
|
||||
|
||||
@@ -371,400 +369,13 @@ async function sendMessage(req: Request, res: Response) {
|
||||
return restChatService.handleSendMessage(req, res);
|
||||
}
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/llm/indexes/stats:
|
||||
* get:
|
||||
* summary: Get stats about the LLM knowledge base indexing status
|
||||
* operationId: llm-index-stats
|
||||
* responses:
|
||||
* '200':
|
||||
* description: Index stats successfully retrieved
|
||||
* security:
|
||||
* - session: []
|
||||
* tags: ["llm"]
|
||||
*/
|
||||
async function getIndexStats(req: Request, res: Response) {
|
||||
try {
|
||||
// Check if AI is enabled
|
||||
const aiEnabled = await options.getOptionBool('aiEnabled');
|
||||
if (!aiEnabled) {
|
||||
return {
|
||||
success: false,
|
||||
message: "AI features are disabled"
|
||||
};
|
||||
}
|
||||
|
||||
// Return indexing stats
|
||||
const stats = await indexService.getIndexingStats();
|
||||
return {
|
||||
success: true,
|
||||
...stats
|
||||
};
|
||||
} catch (error: any) {
|
||||
log.error(`Error getting index stats: ${error.message || 'Unknown error'}`);
|
||||
throw new Error(`Failed to get index stats: ${error.message || 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/llm/indexes:
|
||||
* post:
|
||||
* summary: Start or continue indexing the knowledge base
|
||||
* operationId: llm-start-indexing
|
||||
* requestBody:
|
||||
* required: false
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* force:
|
||||
* type: boolean
|
||||
* description: Whether to force reindexing of all notes
|
||||
* responses:
|
||||
* '200':
|
||||
* description: Indexing started successfully
|
||||
* security:
|
||||
* - session: []
|
||||
* tags: ["llm"]
|
||||
*/
|
||||
async function startIndexing(req: Request, res: Response) {
|
||||
try {
|
||||
// Check if AI is enabled
|
||||
const aiEnabled = await options.getOptionBool('aiEnabled');
|
||||
if (!aiEnabled) {
|
||||
return {
|
||||
success: false,
|
||||
message: "AI features are disabled"
|
||||
};
|
||||
}
|
||||
|
||||
const { force = false } = req.body;
|
||||
|
||||
// Start indexing
|
||||
await indexService.startFullIndexing(force);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Indexing started"
|
||||
};
|
||||
} catch (error: any) {
|
||||
log.error(`Error starting indexing: ${error.message || 'Unknown error'}`);
|
||||
throw new Error(`Failed to start indexing: ${error.message || 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/llm/indexes/failed:
|
||||
* get:
|
||||
* summary: Get list of notes that failed to index
|
||||
* operationId: llm-failed-indexes
|
||||
* parameters:
|
||||
* - name: limit
|
||||
* in: query
|
||||
* required: false
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 100
|
||||
* responses:
|
||||
* '200':
|
||||
* description: Failed indexes successfully retrieved
|
||||
* security:
|
||||
* - session: []
|
||||
* tags: ["llm"]
|
||||
*/
|
||||
async function getFailedIndexes(req: Request, res: Response) {
|
||||
try {
|
||||
// Check if AI is enabled
|
||||
const aiEnabled = await options.getOptionBool('aiEnabled');
|
||||
if (!aiEnabled) {
|
||||
return {
|
||||
success: false,
|
||||
message: "AI features are disabled"
|
||||
};
|
||||
}
|
||||
|
||||
const limit = parseInt(req.query.limit as string || "100", 10);
|
||||
|
||||
// Get failed indexes
|
||||
const failed = await indexService.getFailedIndexes(limit);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
failed
|
||||
};
|
||||
} catch (error: any) {
|
||||
log.error(`Error getting failed indexes: ${error.message || 'Unknown error'}`);
|
||||
throw new Error(`Failed to get failed indexes: ${error.message || 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/llm/indexes/notes/{noteId}:
|
||||
* put:
|
||||
* summary: Retry indexing a specific note that previously failed
|
||||
* operationId: llm-retry-index
|
||||
* parameters:
|
||||
* - name: noteId
|
||||
* in: path
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* '200':
|
||||
* description: Index retry successfully initiated
|
||||
* security:
|
||||
* - session: []
|
||||
* tags: ["llm"]
|
||||
*/
|
||||
async function retryFailedIndex(req: Request, res: Response) {
|
||||
try {
|
||||
// Check if AI is enabled
|
||||
const aiEnabled = await options.getOptionBool('aiEnabled');
|
||||
if (!aiEnabled) {
|
||||
return {
|
||||
success: false,
|
||||
message: "AI features are disabled"
|
||||
};
|
||||
}
|
||||
|
||||
const { noteId } = req.params;
|
||||
|
||||
// Retry indexing the note
|
||||
const result = await indexService.retryFailedNote(noteId);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: result ? "Note queued for indexing" : "Failed to queue note for indexing"
|
||||
};
|
||||
} catch (error: any) {
|
||||
log.error(`Error retrying failed index: ${error.message || 'Unknown error'}`);
|
||||
throw new Error(`Failed to retry index: ${error.message || 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/llm/indexes/failed:
|
||||
* put:
|
||||
* summary: Retry indexing all failed notes
|
||||
* operationId: llm-retry-all-indexes
|
||||
* responses:
|
||||
* '200':
|
||||
* description: Retry of all failed indexes successfully initiated
|
||||
* security:
|
||||
* - session: []
|
||||
* tags: ["llm"]
|
||||
*/
|
||||
async function retryAllFailedIndexes(req: Request, res: Response) {
|
||||
try {
|
||||
// Check if AI is enabled
|
||||
const aiEnabled = await options.getOptionBool('aiEnabled');
|
||||
if (!aiEnabled) {
|
||||
return {
|
||||
success: false,
|
||||
message: "AI features are disabled"
|
||||
};
|
||||
}
|
||||
|
||||
// Retry all failed notes
|
||||
const count = await indexService.retryAllFailedNotes();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `${count} notes queued for reprocessing`
|
||||
};
|
||||
} catch (error: any) {
|
||||
log.error(`Error retrying all failed indexes: ${error.message || 'Unknown error'}`);
|
||||
throw new Error(`Failed to retry all indexes: ${error.message || 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/llm/indexes/notes/similar:
|
||||
* get:
|
||||
* summary: Find notes similar to a query string
|
||||
* operationId: llm-find-similar-notes
|
||||
* parameters:
|
||||
* - name: query
|
||||
* in: query
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* - name: contextNoteId
|
||||
* in: query
|
||||
* required: false
|
||||
* schema:
|
||||
* type: string
|
||||
* - name: limit
|
||||
* in: query
|
||||
* required: false
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 5
|
||||
* responses:
|
||||
* '200':
|
||||
* description: Similar notes found successfully
|
||||
* security:
|
||||
* - session: []
|
||||
* tags: ["llm"]
|
||||
*/
|
||||
async function findSimilarNotes(req: Request, res: Response) {
|
||||
try {
|
||||
// Check if AI is enabled
|
||||
const aiEnabled = await options.getOptionBool('aiEnabled');
|
||||
if (!aiEnabled) {
|
||||
return {
|
||||
success: false,
|
||||
message: "AI features are disabled"
|
||||
};
|
||||
}
|
||||
|
||||
const query = req.query.query as string;
|
||||
const contextNoteId = req.query.contextNoteId as string | undefined;
|
||||
const limit = parseInt(req.query.limit as string || "5", 10);
|
||||
|
||||
if (!query) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Query is required"
|
||||
};
|
||||
}
|
||||
|
||||
// Find similar notes
|
||||
const similar = await indexService.findSimilarNotes(query, contextNoteId, limit);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
similar
|
||||
};
|
||||
} catch (error: any) {
|
||||
log.error(`Error finding similar notes: ${error.message || 'Unknown error'}`);
|
||||
throw new Error(`Failed to find similar notes: ${error.message || 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/llm/indexes/context:
|
||||
* get:
|
||||
* summary: Generate context for an LLM query based on the knowledge base
|
||||
* operationId: llm-generate-context
|
||||
* parameters:
|
||||
* - name: query
|
||||
* in: query
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* - name: contextNoteId
|
||||
* in: query
|
||||
* required: false
|
||||
* schema:
|
||||
* type: string
|
||||
* - name: depth
|
||||
* in: query
|
||||
* required: false
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 2
|
||||
* responses:
|
||||
* '200':
|
||||
* description: Context generated successfully
|
||||
* security:
|
||||
* - session: []
|
||||
* tags: ["llm"]
|
||||
*/
|
||||
async function generateQueryContext(req: Request, res: Response) {
|
||||
try {
|
||||
// Check if AI is enabled
|
||||
const aiEnabled = await options.getOptionBool('aiEnabled');
|
||||
if (!aiEnabled) {
|
||||
return {
|
||||
success: false,
|
||||
message: "AI features are disabled"
|
||||
};
|
||||
}
|
||||
|
||||
const query = req.query.query as string;
|
||||
const contextNoteId = req.query.contextNoteId as string | undefined;
|
||||
const depth = parseInt(req.query.depth as string || "2", 10);
|
||||
|
||||
if (!query) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Query is required"
|
||||
};
|
||||
}
|
||||
|
||||
// Generate context
|
||||
const context = await indexService.generateQueryContext(query, contextNoteId, depth);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
context
|
||||
};
|
||||
} catch (error: any) {
|
||||
log.error(`Error generating query context: ${error.message || 'Unknown error'}`);
|
||||
throw new Error(`Failed to generate query context: ${error.message || 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/llm/indexes/notes/{noteId}:
|
||||
* post:
|
||||
* summary: Index a specific note for LLM knowledge base
|
||||
* operationId: llm-index-note
|
||||
* parameters:
|
||||
* - name: noteId
|
||||
* in: path
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* '200':
|
||||
* description: Note indexed successfully
|
||||
* security:
|
||||
* - session: []
|
||||
* tags: ["llm"]
|
||||
*/
|
||||
async function indexNote(req: Request, res: Response) {
|
||||
try {
|
||||
// Check if AI is enabled
|
||||
const aiEnabled = await options.getOptionBool('aiEnabled');
|
||||
if (!aiEnabled) {
|
||||
return {
|
||||
success: false,
|
||||
message: "AI features are disabled"
|
||||
};
|
||||
}
|
||||
|
||||
const { noteId } = req.params;
|
||||
|
||||
if (!noteId) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Note ID is required"
|
||||
};
|
||||
}
|
||||
|
||||
// Index the note
|
||||
const result = await indexService.generateNoteIndex(noteId);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: result ? "Note indexed successfully" : "Failed to index note"
|
||||
};
|
||||
} catch (error: any) {
|
||||
log.error(`Error indexing note: ${error.message || 'Unknown error'}`);
|
||||
throw new Error(`Failed to index note: ${error.message || 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
@@ -936,15 +547,5 @@ export default {
|
||||
listSessions,
|
||||
deleteSession,
|
||||
sendMessage,
|
||||
streamMessage,
|
||||
|
||||
// Knowledge base index management
|
||||
getIndexStats,
|
||||
startIndexing,
|
||||
getFailedIndexes,
|
||||
retryFailedIndex,
|
||||
retryAllFailedIndexes,
|
||||
findSimilarNotes,
|
||||
generateQueryContext,
|
||||
indexNote
|
||||
streamMessage
|
||||
};
|
||||
|
||||
@@ -99,12 +99,6 @@ type MetricsData = ReturnType<typeof etapiMetrics.collectMetrics>;
|
||||
* totalRecentNotes:
|
||||
* type: integer
|
||||
* example: 50
|
||||
* totalEmbeddings:
|
||||
* type: integer
|
||||
* example: 123
|
||||
* totalEmbeddingProviders:
|
||||
* type: integer
|
||||
* example: 2
|
||||
* noteTypes:
|
||||
* type: object
|
||||
* additionalProperties:
|
||||
|
||||
@@ -40,17 +40,6 @@ import OpenAI from "openai";
|
||||
* type: string
|
||||
* type:
|
||||
* type: string
|
||||
* embeddingModels:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: string
|
||||
* name:
|
||||
* type: string
|
||||
* type:
|
||||
* type: string
|
||||
* '500':
|
||||
* description: Error listing models
|
||||
* security:
|
||||
@@ -82,8 +71,7 @@ async function listModels(req: Request, res: Response) {
|
||||
// Filter and categorize models
|
||||
const allModels = response.data || [];
|
||||
|
||||
// Include all models as chat models, without filtering by specific model names
|
||||
// This allows models from providers like OpenRouter to be displayed
|
||||
// Include all models as chat models, excluding embedding models
|
||||
const chatModels = allModels
|
||||
.filter((model) =>
|
||||
// Exclude models that are explicitly for embeddings
|
||||
@@ -96,23 +84,10 @@ async function listModels(req: Request, res: Response) {
|
||||
type: 'chat'
|
||||
}));
|
||||
|
||||
const embeddingModels = allModels
|
||||
.filter((model) =>
|
||||
// Only include embedding-specific models
|
||||
model.id.includes('embedding') ||
|
||||
model.id.includes('embed')
|
||||
)
|
||||
.map((model) => ({
|
||||
id: model.id,
|
||||
name: model.id,
|
||||
type: 'embedding'
|
||||
}));
|
||||
|
||||
// Return the models list
|
||||
return {
|
||||
success: true,
|
||||
chatModels,
|
||||
embeddingModels
|
||||
chatModels
|
||||
};
|
||||
} catch (error: any) {
|
||||
log.error(`Error listing OpenAI models: ${error.message || 'Unknown error'}`);
|
||||
|
||||
@@ -100,30 +100,11 @@ const ALLOWED_OPTIONS = new Set<OptionNames>([
|
||||
"openaiApiKey",
|
||||
"openaiBaseUrl",
|
||||
"openaiDefaultModel",
|
||||
"openaiEmbeddingModel",
|
||||
"openaiEmbeddingApiKey",
|
||||
"openaiEmbeddingBaseUrl",
|
||||
"anthropicApiKey",
|
||||
"anthropicBaseUrl",
|
||||
"anthropicDefaultModel",
|
||||
"voyageApiKey",
|
||||
"voyageEmbeddingModel",
|
||||
"voyageEmbeddingBaseUrl",
|
||||
"ollamaBaseUrl",
|
||||
"ollamaDefaultModel",
|
||||
"ollamaEmbeddingModel",
|
||||
"ollamaEmbeddingBaseUrl",
|
||||
"embeddingAutoUpdateEnabled",
|
||||
"embeddingDimensionStrategy",
|
||||
"embeddingSelectedProvider",
|
||||
"embeddingSimilarityThreshold",
|
||||
"embeddingBatchSize",
|
||||
"embeddingUpdateInterval",
|
||||
"enableAutomaticIndexing",
|
||||
"maxNotesPerLlmQuery",
|
||||
|
||||
// Embedding options
|
||||
"embeddingDefaultDimension",
|
||||
"mfaEnabled",
|
||||
"mfaMethod"
|
||||
]);
|
||||
|
||||
@@ -54,7 +54,6 @@ import relationMapApiRoute from "./api/relation-map.js";
|
||||
import otherRoute from "./api/other.js";
|
||||
import metricsRoute from "./api/metrics.js";
|
||||
import shareRoutes from "../share/routes.js";
|
||||
import embeddingsRoute from "./api/embeddings.js";
|
||||
import ollamaRoute from "./api/ollama.js";
|
||||
import openaiRoute from "./api/openai.js";
|
||||
import anthropicRoute from "./api/anthropic.js";
|
||||
@@ -377,31 +376,7 @@ function register(app: express.Application) {
|
||||
asyncApiRoute(PST, "/api/llm/chat/:chatNoteId/messages", llmRoute.sendMessage);
|
||||
asyncApiRoute(PST, "/api/llm/chat/:chatNoteId/messages/stream", llmRoute.streamMessage);
|
||||
|
||||
// LLM index management endpoints - reorganized for REST principles
|
||||
asyncApiRoute(GET, "/api/llm/indexes/stats", llmRoute.getIndexStats);
|
||||
asyncApiRoute(PST, "/api/llm/indexes", llmRoute.startIndexing); // Create index process
|
||||
asyncApiRoute(GET, "/api/llm/indexes/failed", llmRoute.getFailedIndexes);
|
||||
asyncApiRoute(PUT, "/api/llm/indexes/notes/:noteId", llmRoute.retryFailedIndex); // Update index for note
|
||||
asyncApiRoute(PUT, "/api/llm/indexes/failed", llmRoute.retryAllFailedIndexes); // Update all failed indexes
|
||||
asyncApiRoute(GET, "/api/llm/indexes/notes/similar", llmRoute.findSimilarNotes); // Get similar notes
|
||||
asyncApiRoute(GET, "/api/llm/indexes/context", llmRoute.generateQueryContext); // Get context
|
||||
asyncApiRoute(PST, "/api/llm/indexes/notes/:noteId", llmRoute.indexNote); // Create index for specific note
|
||||
|
||||
// LLM embeddings endpoints
|
||||
asyncApiRoute(GET, "/api/llm/embeddings/similar/:noteId", embeddingsRoute.findSimilarNotes);
|
||||
asyncApiRoute(PST, "/api/llm/embeddings/search", embeddingsRoute.searchByText);
|
||||
asyncApiRoute(GET, "/api/llm/embeddings/providers", embeddingsRoute.getProviders);
|
||||
asyncApiRoute(PATCH, "/api/llm/embeddings/providers/:providerId", embeddingsRoute.updateProvider);
|
||||
asyncApiRoute(PST, "/api/llm/embeddings/reprocess", embeddingsRoute.reprocessAllNotes);
|
||||
asyncApiRoute(GET, "/api/llm/embeddings/queue-status", embeddingsRoute.getQueueStatus);
|
||||
asyncApiRoute(GET, "/api/llm/embeddings/stats", embeddingsRoute.getEmbeddingStats);
|
||||
asyncApiRoute(GET, "/api/llm/embeddings/failed", embeddingsRoute.getFailedNotes);
|
||||
asyncApiRoute(PST, "/api/llm/embeddings/retry/:noteId", embeddingsRoute.retryFailedNote);
|
||||
asyncApiRoute(PST, "/api/llm/embeddings/retry-all-failed", embeddingsRoute.retryAllFailedNotes);
|
||||
asyncApiRoute(PST, "/api/llm/embeddings/rebuild-index", embeddingsRoute.rebuildIndex);
|
||||
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
|
||||
asyncApiRoute(GET, "/api/llm/providers/ollama/models", ollamaRoute.listModels);
|
||||
|
||||
Reference in New Issue
Block a user