mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 10:26:08 +01:00 
			
		
		
		
	Better use of interfaces, reducing useage of "any"
This commit is contained in:
		| @@ -931,7 +931,16 @@ async function sendMessage(req: Request, res: Response) { | |||||||
|  |  | ||||||
|                 // Get the generated context |                 // Get the generated context | ||||||
|                 const context = results.context; |                 const context = results.context; | ||||||
|                 sourceNotes = results.notes; |                 // Convert from NoteSearchResult to NoteSource | ||||||
|  |                 sourceNotes = results.sources.map(source => ({ | ||||||
|  |                     noteId: source.noteId, | ||||||
|  |                     title: source.title, | ||||||
|  |                     content: source.content || undefined, // Convert null to undefined | ||||||
|  |                     similarity: source.similarity | ||||||
|  |                 })); | ||||||
|  |  | ||||||
|  |                 // Build context from relevant notes | ||||||
|  |                 const contextFromNotes = buildContextFromNotes(sourceNotes, messageContent); | ||||||
|  |  | ||||||
|                 // Add system message with the context |                 // Add system message with the context | ||||||
|                 const contextMessage: Message = { |                 const contextMessage: Message = { | ||||||
| @@ -1063,8 +1072,7 @@ async function sendMessage(req: Request, res: Response) { | |||||||
|                         sources: sourceNotes.map(note => ({ |                         sources: sourceNotes.map(note => ({ | ||||||
|                             noteId: note.noteId, |                             noteId: note.noteId, | ||||||
|                             title: note.title, |                             title: note.title, | ||||||
|                             similarity: note.similarity, |                             similarity: note.similarity | ||||||
|                             branchId: note.branchId |  | ||||||
|                         })) |                         })) | ||||||
|                     }; |                     }; | ||||||
|                 } |                 } | ||||||
| @@ -1198,8 +1206,7 @@ async function sendMessage(req: Request, res: Response) { | |||||||
|                         sources: sourceNotes.map(note => ({ |                         sources: sourceNotes.map(note => ({ | ||||||
|                             noteId: note.noteId, |                             noteId: note.noteId, | ||||||
|                             title: note.title, |                             title: note.title, | ||||||
|                             similarity: note.similarity, |                             similarity: note.similarity | ||||||
|                             branchId: note.branchId |  | ||||||
|                         })) |                         })) | ||||||
|                     }; |                     }; | ||||||
|                 } |                 } | ||||||
|   | |||||||
| @@ -28,7 +28,7 @@ export interface ThinkingStep { | |||||||
|     sources?: string[]; |     sources?: string[]; | ||||||
|     parentId?: string; |     parentId?: string; | ||||||
|     children?: string[]; |     children?: string[]; | ||||||
|     metadata?: Record<string, any>; |     metadata?: Record<string, unknown>; | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ export interface ContentChunk { | |||||||
|     noteId?: string; |     noteId?: string; | ||||||
|     title?: string; |     title?: string; | ||||||
|     path?: string; |     path?: string; | ||||||
|     metadata?: Record<string, any>; |     metadata?: Record<string, unknown>; | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -43,7 +43,7 @@ export interface ChunkOptions { | |||||||
|     /** |     /** | ||||||
|      * Additional information to include in chunk metadata |      * Additional information to include in chunk metadata | ||||||
|      */ |      */ | ||||||
|     metadata?: Record<string, any>; |     metadata?: Record<string, unknown>; | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -293,3 +293,11 @@ export async function semanticChunking( | |||||||
|  |  | ||||||
|     return chunks; |     return chunks; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export interface NoteChunk { | ||||||
|  |     noteId: string; | ||||||
|  |     title: string; | ||||||
|  |     content: string; | ||||||
|  |     type?: string; | ||||||
|  |     metadata?: Record<string, unknown>; | ||||||
|  | } | ||||||
|   | |||||||
| @@ -8,6 +8,8 @@ import aiServiceManager from '../../ai_service_manager.js'; | |||||||
| import { ContextExtractor } from '../index.js'; | import { ContextExtractor } from '../index.js'; | ||||||
| import { CONTEXT_PROMPTS } from '../../constants/llm_prompt_constants.js'; | import { CONTEXT_PROMPTS } from '../../constants/llm_prompt_constants.js'; | ||||||
| import becca from '../../../../becca/becca.js'; | import becca from '../../../../becca/becca.js'; | ||||||
|  | import type { NoteSearchResult } from '../../interfaces/context_interfaces.js'; | ||||||
|  | import type { LLMServiceInterface } from '../../interfaces/agent_tool_interfaces.js'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Main context service that integrates all context-related functionality |  * Main context service that integrates all context-related functionality | ||||||
| @@ -73,10 +75,10 @@ export class ContextService { | |||||||
|      */ |      */ | ||||||
|     async processQuery( |     async processQuery( | ||||||
|         userQuestion: string, |         userQuestion: string, | ||||||
|         llmService: any, |         llmService: LLMServiceInterface, | ||||||
|         contextNoteId: string | null = null, |         contextNoteId: string | null = null, | ||||||
|         showThinking: boolean = false |         showThinking: boolean = false | ||||||
|     ) { |     ): Promise<{ context: string; sources: NoteSearchResult[]; thinking?: string }> { | ||||||
|         log.info(`Processing query with: question="${userQuestion.substring(0, 50)}...", noteId=${contextNoteId}, showThinking=${showThinking}`); |         log.info(`Processing query with: question="${userQuestion.substring(0, 50)}...", noteId=${contextNoteId}, showThinking=${showThinking}`); | ||||||
|  |  | ||||||
|         if (!this.initialized) { |         if (!this.initialized) { | ||||||
| @@ -87,8 +89,8 @@ export class ContextService { | |||||||
|                 // Return a fallback response if initialization fails |                 // Return a fallback response if initialization fails | ||||||
|                 return { |                 return { | ||||||
|                     context: CONTEXT_PROMPTS.NO_NOTES_CONTEXT, |                     context: CONTEXT_PROMPTS.NO_NOTES_CONTEXT, | ||||||
|                     notes: [], |                     sources: [], | ||||||
|                     queries: [userQuestion] |                     thinking: undefined | ||||||
|                 }; |                 }; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| @@ -105,10 +107,10 @@ export class ContextService { | |||||||
|             log.info(`Generated search queries: ${JSON.stringify(searchQueries)}`); |             log.info(`Generated search queries: ${JSON.stringify(searchQueries)}`); | ||||||
|  |  | ||||||
|             // Step 2: Find relevant notes using multi-query approach |             // Step 2: Find relevant notes using multi-query approach | ||||||
|             let relevantNotes: any[] = []; |             let relevantNotes: NoteSearchResult[] = []; | ||||||
|             try { |             try { | ||||||
|                 // Find notes for each query and combine results |                 // Find notes for each query and combine results | ||||||
|                 const allResults: Map<string, any> = new Map(); |                 const allResults: Map<string, NoteSearchResult> = new Map(); | ||||||
|  |  | ||||||
|                 for (const query of searchQueries) { |                 for (const query of searchQueries) { | ||||||
|                     const results = await semanticSearch.findRelevantNotes( |                     const results = await semanticSearch.findRelevantNotes( | ||||||
| @@ -124,7 +126,7 @@ export class ContextService { | |||||||
|                         } else { |                         } else { | ||||||
|                             // If note already exists, update similarity to max of both values |                             // If note already exists, update similarity to max of both values | ||||||
|                             const existing = allResults.get(result.noteId); |                             const existing = allResults.get(result.noteId); | ||||||
|                             if (result.similarity > existing.similarity) { |                             if (existing && result.similarity > existing.similarity) { | ||||||
|                                 existing.similarity = result.similarity; |                                 existing.similarity = result.similarity; | ||||||
|                                 allResults.set(result.noteId, existing); |                                 allResults.set(result.noteId, existing); | ||||||
|                             } |                             } | ||||||
| @@ -186,15 +188,15 @@ export class ContextService { | |||||||
|  |  | ||||||
|             return { |             return { | ||||||
|                 context: enhancedContext, |                 context: enhancedContext, | ||||||
|                 notes: relevantNotes, |                 sources: relevantNotes, | ||||||
|                 queries: searchQueries |                 thinking: showThinking ? this.summarizeContextStructure(enhancedContext) : undefined | ||||||
|             }; |             }; | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
|             log.error(`Error processing query: ${error}`); |             log.error(`Error processing query: ${error}`); | ||||||
|             return { |             return { | ||||||
|                 context: CONTEXT_PROMPTS.NO_NOTES_CONTEXT, |                 context: CONTEXT_PROMPTS.NO_NOTES_CONTEXT, | ||||||
|                 notes: [], |                 sources: [], | ||||||
|                 queries: [userQuestion] |                 thinking: undefined | ||||||
|             }; |             }; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @@ -212,7 +214,7 @@ export class ContextService { | |||||||
|         noteId: string, |         noteId: string, | ||||||
|         query: string, |         query: string, | ||||||
|         showThinking: boolean = false, |         showThinking: boolean = false, | ||||||
|         relevantNotes: Array<any> = [] |         relevantNotes: NoteSearchResult[] = [] | ||||||
|     ): Promise<string> { |     ): Promise<string> { | ||||||
|         try { |         try { | ||||||
|             log.info(`Building enhanced agent tools context for query: "${query.substring(0, 50)}...", noteId=${noteId}, showThinking=${showThinking}`); |             log.info(`Building enhanced agent tools context for query: "${query.substring(0, 50)}...", noteId=${noteId}, showThinking=${showThinking}`); | ||||||
| @@ -391,7 +393,7 @@ export class ContextService { | |||||||
|  |  | ||||||
|             // Combine the notes from both searches - the initial relevantNotes and from vector search |             // Combine the notes from both searches - the initial relevantNotes and from vector search | ||||||
|             // Start with a Map to deduplicate by noteId |             // Start with a Map to deduplicate by noteId | ||||||
|             const allNotes = new Map<string, any>(); |             const allNotes = new Map<string, NoteSearchResult>(); | ||||||
|  |  | ||||||
|             // Add notes from the initial search in processQuery (relevantNotes parameter) |             // Add notes from the initial search in processQuery (relevantNotes parameter) | ||||||
|             if (relevantNotes && relevantNotes.length > 0) { |             if (relevantNotes && relevantNotes.length > 0) { | ||||||
| @@ -409,7 +411,10 @@ export class ContextService { | |||||||
|                 log.info(`Adding ${vectorSearchNotes.length} notes from vector search to combined results`); |                 log.info(`Adding ${vectorSearchNotes.length} notes from vector search to combined results`); | ||||||
|                 for (const note of vectorSearchNotes) { |                 for (const note of vectorSearchNotes) { | ||||||
|                     // If note already exists, keep the one with higher similarity |                     // If note already exists, keep the one with higher similarity | ||||||
|                     if (!allNotes.has(note.noteId) || note.similarity > allNotes.get(note.noteId).similarity) { |                     const existing = allNotes.get(note.noteId); | ||||||
|  |                     if (existing && note.similarity > existing.similarity) { | ||||||
|  |                         existing.similarity = note.similarity; | ||||||
|  |                     } else { | ||||||
|                         allNotes.set(note.noteId, note); |                         allNotes.set(note.noteId, note); | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
| @@ -831,7 +836,7 @@ export class ContextService { | |||||||
|             } |             } | ||||||
|  |  | ||||||
|             // Get embeddings for the query and all chunks |             // Get embeddings for the query and all chunks | ||||||
|             const queryEmbedding = await provider.createEmbedding(query); |             const queryEmbedding = await provider.generateEmbeddings(query); | ||||||
|  |  | ||||||
|             // Process chunks in smaller batches to avoid overwhelming the provider |             // Process chunks in smaller batches to avoid overwhelming the provider | ||||||
|             const batchSize = 5; |             const batchSize = 5; | ||||||
| @@ -840,7 +845,7 @@ export class ContextService { | |||||||
|             for (let i = 0; i < chunks.length; i += batchSize) { |             for (let i = 0; i < chunks.length; i += batchSize) { | ||||||
|                 const batch = chunks.slice(i, i + batchSize); |                 const batch = chunks.slice(i, i + batchSize); | ||||||
|                 const batchEmbeddings = await Promise.all( |                 const batchEmbeddings = await Promise.all( | ||||||
|                     batch.map(chunk => provider.createEmbedding(chunk)) |                     batch.map(chunk => provider.generateEmbeddings(chunk)) | ||||||
|                 ); |                 ); | ||||||
|                 chunkEmbeddings.push(...batchEmbeddings); |                 chunkEmbeddings.push(...batchEmbeddings); | ||||||
|             } |             } | ||||||
| @@ -848,7 +853,8 @@ export class ContextService { | |||||||
|             // Calculate similarity between query and each chunk |             // Calculate similarity between query and each chunk | ||||||
|             const similarities: Array<{index: number, similarity: number, content: string}> = |             const similarities: Array<{index: number, similarity: number, content: string}> = | ||||||
|                 chunkEmbeddings.map((embedding, index) => { |                 chunkEmbeddings.map((embedding, index) => { | ||||||
|                     const similarity = provider.calculateSimilarity(queryEmbedding, embedding); |                     // Calculate cosine similarity manually since the method doesn't exist | ||||||
|  |                     const similarity = this.calculateCosineSimilarity(queryEmbedding, embedding); | ||||||
|                     return { index, similarity, content: chunks[index] }; |                     return { index, similarity, content: chunks[index] }; | ||||||
|                 }); |                 }); | ||||||
|  |  | ||||||
| @@ -891,6 +897,28 @@ export class ContextService { | |||||||
|             return content.substring(0, maxChars) + '...'; |             return content.substring(0, maxChars) + '...'; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Calculate cosine similarity between two vectors | ||||||
|  |      * @param vec1 - First vector | ||||||
|  |      * @param vec2 - Second vector | ||||||
|  |      * @returns Cosine similarity between the two vectors | ||||||
|  |      */ | ||||||
|  |     private calculateCosineSimilarity(vec1: number[], vec2: number[]): number { | ||||||
|  |         let dotProduct = 0; | ||||||
|  |         let norm1 = 0; | ||||||
|  |         let norm2 = 0; | ||||||
|  |  | ||||||
|  |         for (let i = 0; i < vec1.length; i++) { | ||||||
|  |             dotProduct += vec1[i] * vec2[i]; | ||||||
|  |             norm1 += vec1[i] * vec1[i]; | ||||||
|  |             norm2 += vec2[i] * vec2[i]; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const magnitude = Math.sqrt(norm1) * Math.sqrt(norm2); | ||||||
|  |         if (magnitude === 0) return 0; | ||||||
|  |         return dotProduct / magnitude; | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| // Export singleton instance | // Export singleton instance | ||||||
|   | |||||||
| @@ -2,11 +2,13 @@ import log from '../../../log.js'; | |||||||
| import cacheManager from './cache_manager.js'; | import cacheManager from './cache_manager.js'; | ||||||
| import type { Message } from '../../ai_interface.js'; | import type { Message } from '../../ai_interface.js'; | ||||||
| import { CONTEXT_PROMPTS } from '../../constants/llm_prompt_constants.js'; | import { CONTEXT_PROMPTS } from '../../constants/llm_prompt_constants.js'; | ||||||
|  | import type { LLMServiceInterface } from '../../interfaces/agent_tool_interfaces.js'; | ||||||
|  | import type { IQueryEnhancer } from '../../interfaces/context_interfaces.js'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Provides utilities for enhancing queries and generating search queries |  * Provides utilities for enhancing queries and generating search queries | ||||||
|  */ |  */ | ||||||
| export class QueryEnhancer { | export class QueryEnhancer implements IQueryEnhancer { | ||||||
|     // Use the centralized query enhancer prompt |     // Use the centralized query enhancer prompt | ||||||
|     private metaPrompt = CONTEXT_PROMPTS.QUERY_ENHANCER; |     private metaPrompt = CONTEXT_PROMPTS.QUERY_ENHANCER; | ||||||
|  |  | ||||||
| @@ -17,11 +19,15 @@ export class QueryEnhancer { | |||||||
|      * @param llmService - The LLM service to use for generating queries |      * @param llmService - The LLM service to use for generating queries | ||||||
|      * @returns Array of search queries |      * @returns Array of search queries | ||||||
|      */ |      */ | ||||||
|     async generateSearchQueries(userQuestion: string, llmService: any): Promise<string[]> { |     async generateSearchQueries(userQuestion: string, llmService: LLMServiceInterface): Promise<string[]> { | ||||||
|  |         if (!userQuestion || userQuestion.trim() === '') { | ||||||
|  |             return []; // Return empty array for empty input | ||||||
|  |         } | ||||||
|  |  | ||||||
|         try { |         try { | ||||||
|             // Check cache first |             // Check cache with proper type checking | ||||||
|             const cached = cacheManager.getQueryResults(`searchQueries:${userQuestion}`); |             const cached = cacheManager.getQueryResults<string[]>(`searchQueries:${userQuestion}`); | ||||||
|             if (cached) { |             if (cached && Array.isArray(cached)) { | ||||||
|                 return cached; |                 return cached; | ||||||
|             } |             } | ||||||
|  |  | ||||||
| @@ -120,7 +126,6 @@ export class QueryEnhancer { | |||||||
|         } catch (error: unknown) { |         } catch (error: unknown) { | ||||||
|             const errorMessage = error instanceof Error ? error.message : String(error); |             const errorMessage = error instanceof Error ? error.message : String(error); | ||||||
|             log.error(`Error generating search queries: ${errorMessage}`); |             log.error(`Error generating search queries: ${errorMessage}`); | ||||||
|             // Fallback to just using the original question |  | ||||||
|             return [userQuestion]; |             return [userQuestion]; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ | |||||||
| import log from '../log.js'; | import log from '../log.js'; | ||||||
| import contextService from './context/modules/context_service.js'; | import contextService from './context/modules/context_service.js'; | ||||||
| import { ContextExtractor } from './context/index.js'; | import { ContextExtractor } from './context/index.js'; | ||||||
|  | import type { NoteSearchResult } from './interfaces/context_interfaces.js'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Main Context Service for Trilium Notes |  * Main Context Service for Trilium Notes | ||||||
| @@ -84,7 +85,7 @@ class TriliumContextService { | |||||||
|      * @param query - The original user query |      * @param query - The original user query | ||||||
|      * @returns Formatted context string |      * @returns Formatted context string | ||||||
|      */ |      */ | ||||||
|     async buildContextFromNotes(sources: any[], query: string): Promise<string> { |     async buildContextFromNotes(sources: NoteSearchResult[], query: string): Promise<string> { | ||||||
|         const provider = await (await import('./context/modules/provider_manager.js')).default.getPreferredEmbeddingProvider(); |         const provider = await (await import('./context/modules/provider_manager.js')).default.getPreferredEmbeddingProvider(); | ||||||
|         const providerId = provider?.name || 'default'; |         const providerId = provider?.name || 'default'; | ||||||
|         return (await import('./context/modules/context_formatter.js')).default.buildContextFromNotes(sources, query, providerId); |         return (await import('./context/modules/context_formatter.js')).default.buildContextFromNotes(sources, query, providerId); | ||||||
|   | |||||||
| @@ -1,23 +1,48 @@ | |||||||
| import type { EmbeddingProvider, EmbeddingConfig, NoteEmbeddingContext } from './embeddings_interface.js'; |  | ||||||
| import { NormalizationStatus } from './embeddings_interface.js'; | import { NormalizationStatus } from './embeddings_interface.js'; | ||||||
|  | import type { NoteEmbeddingContext } from './embeddings_interface.js'; | ||||||
| import log from "../../log.js"; | import log from "../../log.js"; | ||||||
| import { LLM_CONSTANTS } from "../../../routes/api/llm.js"; | import { LLM_CONSTANTS } from "../../../routes/api/llm.js"; | ||||||
| import options from "../../options.js"; | import options from "../../options.js"; | ||||||
|  | import { isBatchSizeError as checkBatchSizeError } from '../interfaces/error_interfaces.js'; | ||||||
|  | import type { EmbeddingModelInfo } from '../interfaces/embedding_interfaces.js'; | ||||||
|  |  | ||||||
|  | export interface EmbeddingConfig { | ||||||
|  |     model: string; | ||||||
|  |     dimension: number; | ||||||
|  |     type: string; | ||||||
|  |     apiKey?: string; | ||||||
|  |     baseUrl?: string; | ||||||
|  |     batchSize?: number; | ||||||
|  |     contextWidth?: number; | ||||||
|  |     normalizationStatus?: NormalizationStatus; | ||||||
|  | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Base class that implements common functionality for embedding providers |  * Base class for embedding providers that implements common functionality | ||||||
|  */ |  */ | ||||||
| export abstract class BaseEmbeddingProvider implements EmbeddingProvider { | export abstract class BaseEmbeddingProvider { | ||||||
|     name: string = "base"; |     protected model: string; | ||||||
|     protected config: EmbeddingConfig; |     protected dimension: number; | ||||||
|  |     protected type: string; | ||||||
|  |     protected maxBatchSize: number = 100; | ||||||
|     protected apiKey?: string; |     protected apiKey?: string; | ||||||
|     protected baseUrl: string; |     protected baseUrl: string; | ||||||
|     protected modelInfoCache = new Map<string, any>(); |     protected name: string = 'base'; | ||||||
|  |     protected modelInfoCache = new Map<string, EmbeddingModelInfo>(); | ||||||
|  |     protected config: EmbeddingConfig; | ||||||
|  |  | ||||||
|     constructor(config: EmbeddingConfig) { |     constructor(config: EmbeddingConfig) { | ||||||
|         this.config = config; |         this.model = config.model; | ||||||
|  |         this.dimension = config.dimension; | ||||||
|  |         this.type = config.type; | ||||||
|         this.apiKey = config.apiKey; |         this.apiKey = config.apiKey; | ||||||
|         this.baseUrl = config.baseUrl || ""; |         this.baseUrl = config.baseUrl || ''; | ||||||
|  |         this.config = config; | ||||||
|  |  | ||||||
|  |         // If batch size is specified, use it as maxBatchSize | ||||||
|  |         if (config.batchSize) { | ||||||
|  |             this.maxBatchSize = config.batchSize; | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     getConfig(): EmbeddingConfig { |     getConfig(): EmbeddingConfig { | ||||||
| @@ -79,12 +104,12 @@ export abstract class BaseEmbeddingProvider implements EmbeddingProvider { | |||||||
|      * Process a batch of texts with adaptive handling |      * Process a batch of texts with adaptive handling | ||||||
|      * This method will try to process the batch and reduce batch size if encountering errors |      * This method will try to process the batch and reduce batch size if encountering errors | ||||||
|      */ |      */ | ||||||
|     protected async processWithAdaptiveBatch<T>( |     protected async processWithAdaptiveBatch<T, R>( | ||||||
|         items: T[], |         items: T[], | ||||||
|         processFn: (batch: T[]) => Promise<any[]>, |         processFn: (batch: T[]) => Promise<R[]>, | ||||||
|         isBatchSizeError: (error: any) => boolean |         isBatchSizeError: (error: unknown) => boolean | ||||||
|     ): Promise<any[]> { |     ): Promise<R[]> { | ||||||
|         const results: any[] = []; |         const results: R[] = []; | ||||||
|         const failures: { index: number, error: string }[] = []; |         const failures: { index: number, error: string }[] = []; | ||||||
|         let currentBatchSize = await this.getBatchSize(); |         let currentBatchSize = await this.getBatchSize(); | ||||||
|         let lastError: Error | null = null; |         let lastError: Error | null = null; | ||||||
| @@ -99,9 +124,9 @@ export abstract class BaseEmbeddingProvider implements EmbeddingProvider { | |||||||
|                 results.push(...batchResults); |                 results.push(...batchResults); | ||||||
|                 i += batch.length; |                 i += batch.length; | ||||||
|             } |             } | ||||||
|             catch (error: any) { |             catch (error) { | ||||||
|                 lastError = error; |                 lastError = error as Error; | ||||||
|                 const errorMessage = error.message || 'Unknown error'; |                 const errorMessage = (lastError as Error).message || 'Unknown error'; | ||||||
|  |  | ||||||
|                 // Check if this is a batch size related error |                 // Check if this is a batch size related error | ||||||
|                 if (isBatchSizeError(error) && currentBatchSize > 1) { |                 if (isBatchSizeError(error) && currentBatchSize > 1) { | ||||||
| @@ -142,17 +167,8 @@ export abstract class BaseEmbeddingProvider implements EmbeddingProvider { | |||||||
|      * Detect if an error is related to batch size limits |      * Detect if an error is related to batch size limits | ||||||
|      * Override in provider-specific implementations |      * Override in provider-specific implementations | ||||||
|      */ |      */ | ||||||
|     protected isBatchSizeError(error: any): boolean { |     protected isBatchSizeError(error: unknown): boolean { | ||||||
|         const errorMessage = error?.message || ''; |         return checkBatchSizeError(error); | ||||||
|         const batchSizeErrorPatterns = [ |  | ||||||
|             'batch size', 'too many items', 'too many inputs', |  | ||||||
|             'input too large', 'payload too large', 'context length', |  | ||||||
|             'token limit', 'rate limit', 'request too large' |  | ||||||
|         ]; |  | ||||||
|  |  | ||||||
|         return batchSizeErrorPatterns.some(pattern => |  | ||||||
|             errorMessage.toLowerCase().includes(pattern.toLowerCase()) |  | ||||||
|         ); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -173,11 +189,11 @@ export abstract class BaseEmbeddingProvider implements EmbeddingProvider { | |||||||
|                     ); |                     ); | ||||||
|                     return batchResults; |                     return batchResults; | ||||||
|                 }, |                 }, | ||||||
|                 this.isBatchSizeError |                 this.isBatchSizeError.bind(this) | ||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
|         catch (error: any) { |         catch (error) { | ||||||
|             const errorMessage = error.message || "Unknown error"; |             const errorMessage = (error as Error).message || "Unknown error"; | ||||||
|             log.error(`Batch embedding error for provider ${this.name}: ${errorMessage}`); |             log.error(`Batch embedding error for provider ${this.name}: ${errorMessage}`); | ||||||
|             throw new Error(`${this.name} batch embedding error: ${errorMessage}`); |             throw new Error(`${this.name} batch embedding error: ${errorMessage}`); | ||||||
|         } |         } | ||||||
| @@ -208,11 +224,11 @@ export abstract class BaseEmbeddingProvider implements EmbeddingProvider { | |||||||
|                     ); |                     ); | ||||||
|                     return batchResults; |                     return batchResults; | ||||||
|                 }, |                 }, | ||||||
|                 this.isBatchSizeError |                 this.isBatchSizeError.bind(this) | ||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
|         catch (error: any) { |         catch (error) { | ||||||
|             const errorMessage = error.message || "Unknown error"; |             const errorMessage = (error as Error).message || "Unknown error"; | ||||||
|             log.error(`Batch note embedding error for provider ${this.name}: ${errorMessage}`); |             log.error(`Batch note embedding error for provider ${this.name}: ${errorMessage}`); | ||||||
|             throw new Error(`${this.name} batch note embedding error: ${errorMessage}`); |             throw new Error(`${this.name} batch note embedding error: ${errorMessage}`); | ||||||
|         } |         } | ||||||
| @@ -357,4 +373,66 @@ export abstract class BaseEmbeddingProvider implements EmbeddingProvider { | |||||||
|  |  | ||||||
|         return result; |         return result; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Process a batch of items with automatic retries and batch size adjustment | ||||||
|  |      */ | ||||||
|  |     protected async processBatchWithRetries<T>( | ||||||
|  |         items: T[], | ||||||
|  |         processFn: (batch: T[]) => Promise<Float32Array[]>, | ||||||
|  |         isBatchSizeError: (error: unknown) => boolean = this.isBatchSizeError.bind(this) | ||||||
|  |     ): Promise<Float32Array[]> { | ||||||
|  |         const results: Float32Array[] = []; | ||||||
|  |         const failures: { index: number, error: string }[] = []; | ||||||
|  |         let currentBatchSize = await this.getBatchSize(); | ||||||
|  |         let lastError: Error | null = null; | ||||||
|  |  | ||||||
|  |         // Process items in batches | ||||||
|  |         for (let i = 0; i < items.length;) { | ||||||
|  |             const batch = items.slice(i, i + currentBatchSize); | ||||||
|  |  | ||||||
|  |             try { | ||||||
|  |                 // Process the current batch | ||||||
|  |                 const batchResults = await processFn(batch); | ||||||
|  |                 results.push(...batchResults); | ||||||
|  |                 i += batch.length; | ||||||
|  |             } | ||||||
|  |             catch (error) { | ||||||
|  |                 lastError = error as Error; | ||||||
|  |                 const errorMessage = lastError.message || 'Unknown error'; | ||||||
|  |  | ||||||
|  |                 // Check if this is a batch size related error | ||||||
|  |                 if (isBatchSizeError(error) && currentBatchSize > 1) { | ||||||
|  |                     // Reduce batch size and retry | ||||||
|  |                     const newBatchSize = Math.max(1, Math.floor(currentBatchSize / 2)); | ||||||
|  |                     console.warn(`Batch size error detected, reducing batch size from ${currentBatchSize} to ${newBatchSize}: ${errorMessage}`); | ||||||
|  |                     currentBatchSize = newBatchSize; | ||||||
|  |                 } | ||||||
|  |                 else if (currentBatchSize === 1) { | ||||||
|  |                     // If we're already at batch size 1, we can't reduce further, so log the error and skip this item | ||||||
|  |                     console.error(`Error processing item at index ${i} with batch size 1: ${errorMessage}`); | ||||||
|  |                     failures.push({ index: i, error: errorMessage }); | ||||||
|  |                     i++; // Move to the next item | ||||||
|  |                 } | ||||||
|  |                 else { | ||||||
|  |                     // For other errors, retry with a smaller batch size as a precaution | ||||||
|  |                     const newBatchSize = Math.max(1, Math.floor(currentBatchSize / 2)); | ||||||
|  |                     console.warn(`Error processing batch, reducing batch size from ${currentBatchSize} to ${newBatchSize} as a precaution: ${errorMessage}`); | ||||||
|  |                     currentBatchSize = newBatchSize; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // If all items failed and we have a last error, throw it | ||||||
|  |         if (results.length === 0 && failures.length > 0 && lastError) { | ||||||
|  |             throw lastError; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // If some items failed but others succeeded, log the summary | ||||||
|  |         if (failures.length > 0) { | ||||||
|  |             console.warn(`Processed ${results.length} items successfully, but ${failures.length} items failed`); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return results; | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -32,7 +32,7 @@ export interface ICacheManager { | |||||||
| export interface NoteSearchResult { | export interface NoteSearchResult { | ||||||
|   noteId: string; |   noteId: string; | ||||||
|   title: string; |   title: string; | ||||||
|   content?: string; |   content?: string | null; | ||||||
|   type?: string; |   type?: string; | ||||||
|   mime?: string; |   mime?: string; | ||||||
|   similarity: number; |   similarity: number; | ||||||
|   | |||||||
							
								
								
									
										108
									
								
								src/services/llm/interfaces/embedding_interfaces.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								src/services/llm/interfaces/embedding_interfaces.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | |||||||
|  | /** | ||||||
|  |  * Interface for embedding provider configuration | ||||||
|  |  */ | ||||||
|  | export interface EmbeddingProviderConfig { | ||||||
|  |   name: string; | ||||||
|  |   model: string; | ||||||
|  |   dimension: number; | ||||||
|  |   type: 'float32' | 'int8' | 'uint8' | 'float16'; | ||||||
|  |   enabled?: boolean; | ||||||
|  |   priority?: number; | ||||||
|  |   baseUrl?: string; | ||||||
|  |   apiKey?: string; | ||||||
|  |   contextWidth?: number; | ||||||
|  |   batchSize?: number; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Interface for embedding model information | ||||||
|  |  */ | ||||||
|  | export interface EmbeddingModelInfo { | ||||||
|  |   name: string; | ||||||
|  |   dimension: number; | ||||||
|  |   contextWidth?: number; | ||||||
|  |   maxBatchSize?: number; | ||||||
|  |   tokenizer?: string; | ||||||
|  |   type: 'float32' | 'int8' | 'uint8' | 'float16'; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Interface for embedding provider | ||||||
|  |  */ | ||||||
|  | export interface EmbeddingProvider { | ||||||
|  |   getName(): string; | ||||||
|  |   getModel(): string; | ||||||
|  |   getDimension(): number; | ||||||
|  |   getType(): 'float32' | 'int8' | 'uint8' | 'float16'; | ||||||
|  |   isEnabled(): boolean; | ||||||
|  |   getPriority(): number; | ||||||
|  |   getMaxBatchSize(): number; | ||||||
|  |   generateEmbedding(text: string): Promise<Float32Array>; | ||||||
|  |   generateBatchEmbeddings(texts: string[]): Promise<Float32Array[]>; | ||||||
|  |   initialize(): Promise<void>; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Interface for embedding process result | ||||||
|  |  */ | ||||||
|  | export interface EmbeddingProcessResult { | ||||||
|  |   noteId: string; | ||||||
|  |   title: string; | ||||||
|  |   success: boolean; | ||||||
|  |   message?: string; | ||||||
|  |   error?: Error; | ||||||
|  |   chunks?: number; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Interface for embedding queue item | ||||||
|  |  */ | ||||||
|  | export interface EmbeddingQueueItem { | ||||||
|  |   id: number; | ||||||
|  |   noteId: string; | ||||||
|  |   status: 'pending' | 'processing' | 'completed' | 'failed' | 'retrying'; | ||||||
|  |   provider: string; | ||||||
|  |   model: string; | ||||||
|  |   dimension: number; | ||||||
|  |   type: string; | ||||||
|  |   attempts: number; | ||||||
|  |   lastAttempt: string | null; | ||||||
|  |   dateCreated: string; | ||||||
|  |   dateCompleted: string | null; | ||||||
|  |   error: string | null; | ||||||
|  |   chunks: number; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Interface for embedding batch processing | ||||||
|  |  */ | ||||||
|  | export interface EmbeddingBatch { | ||||||
|  |   texts: string[]; | ||||||
|  |   noteIds: string[]; | ||||||
|  |   indexes: number[]; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Interface for embedding search result | ||||||
|  |  */ | ||||||
|  | export interface EmbeddingSearchResult { | ||||||
|  |   noteId: string; | ||||||
|  |   similarity: number; | ||||||
|  |   title?: string; | ||||||
|  |   content?: string; | ||||||
|  |   parentId?: string; | ||||||
|  |   parentTitle?: string; | ||||||
|  |   dateCreated?: string; | ||||||
|  |   dateModified?: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Interface for embedding chunk | ||||||
|  |  */ | ||||||
|  | export interface EmbeddingChunk { | ||||||
|  |   id: number; | ||||||
|  |   noteId: string; | ||||||
|  |   content: string; | ||||||
|  |   embedding: Float32Array | Int8Array | Uint8Array; | ||||||
|  |   metadata?: Record<string, unknown>; | ||||||
|  | } | ||||||
							
								
								
									
										78
									
								
								src/services/llm/interfaces/error_interfaces.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								src/services/llm/interfaces/error_interfaces.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | |||||||
|  | /** | ||||||
|  |  * Standard error interface for LLM services | ||||||
|  |  */ | ||||||
|  | export interface LLMServiceError extends Error { | ||||||
|  |   message: string; | ||||||
|  |   name: string; | ||||||
|  |   code?: string; | ||||||
|  |   status?: number; | ||||||
|  |   cause?: unknown; | ||||||
|  |   stack?: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Provider-specific error interface for OpenAI | ||||||
|  |  */ | ||||||
|  | export interface OpenAIError extends LLMServiceError { | ||||||
|  |   status: number; | ||||||
|  |   headers?: Record<string, string>; | ||||||
|  |   type?: string; | ||||||
|  |   code?: string; | ||||||
|  |   param?: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Provider-specific error interface for Anthropic | ||||||
|  |  */ | ||||||
|  | export interface AnthropicError extends LLMServiceError { | ||||||
|  |   status: number; | ||||||
|  |   type?: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Provider-specific error interface for Ollama | ||||||
|  |  */ | ||||||
|  | export interface OllamaError extends LLMServiceError { | ||||||
|  |   code?: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Embedding-specific error interface | ||||||
|  |  */ | ||||||
|  | export interface EmbeddingError extends LLMServiceError { | ||||||
|  |   provider: string; | ||||||
|  |   model?: string; | ||||||
|  |   batchSize?: number; | ||||||
|  |   isRetryable: boolean; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Guard function to check if an error is a specific type of error | ||||||
|  |  */ | ||||||
|  | export function isLLMServiceError(error: unknown): error is LLMServiceError { | ||||||
|  |   return ( | ||||||
|  |     typeof error === 'object' && | ||||||
|  |     error !== null && | ||||||
|  |     'message' in error && | ||||||
|  |     typeof (error as LLMServiceError).message === 'string' | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Guard function to check if an error is a batch size error | ||||||
|  |  */ | ||||||
|  | export function isBatchSizeError(error: unknown): boolean { | ||||||
|  |   if (!isLLMServiceError(error)) { | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const errorMessage = error.message.toLowerCase(); | ||||||
|  |   return ( | ||||||
|  |     errorMessage.includes('batch size') || | ||||||
|  |     errorMessage.includes('too many items') || | ||||||
|  |     errorMessage.includes('too many inputs') || | ||||||
|  |     errorMessage.includes('context length') || | ||||||
|  |     errorMessage.includes('token limit') || | ||||||
|  |     (error.code !== undefined && ['context_length_exceeded', 'token_limit_exceeded'].includes(error.code)) | ||||||
|  |   ); | ||||||
|  | } | ||||||
| @@ -3,6 +3,11 @@ import { BaseAIService } from '../base_ai_service.js'; | |||||||
| import type { ChatCompletionOptions, ChatResponse, Message } from '../ai_interface.js'; | import type { ChatCompletionOptions, ChatResponse, Message } from '../ai_interface.js'; | ||||||
| import { PROVIDER_CONSTANTS } from '../constants/provider_constants.js'; | import { PROVIDER_CONSTANTS } from '../constants/provider_constants.js'; | ||||||
|  |  | ||||||
|  | interface AnthropicMessage { | ||||||
|  |     role: string; | ||||||
|  |     content: string; | ||||||
|  | } | ||||||
|  |  | ||||||
| export class AnthropicService extends BaseAIService { | export class AnthropicService extends BaseAIService { | ||||||
|     // Map of simplified model names to full model names with versions |     // Map of simplified model names to full model names with versions | ||||||
|     private static MODEL_MAPPING: Record<string, string> = { |     private static MODEL_MAPPING: Record<string, string> = { | ||||||
| @@ -87,25 +92,31 @@ export class AnthropicService extends BaseAIService { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private formatMessages(messages: Message[], systemPrompt: string): { messages: any[], system: string } { |     /** | ||||||
|         // Extract system messages |      * Format messages for the Anthropic API | ||||||
|         const systemMessages = messages.filter(m => m.role === 'system'); |      */ | ||||||
|         const nonSystemMessages = messages.filter(m => m.role !== 'system'); |     private formatMessages(messages: Message[], systemPrompt: string): { messages: AnthropicMessage[], system: string } { | ||||||
|  |         const formattedMessages: AnthropicMessage[] = []; | ||||||
|  |  | ||||||
|         // Combine all system messages with our default |         // Extract the system message if present | ||||||
|         const combinedSystemPrompt = [systemPrompt] |         let sysPrompt = systemPrompt; | ||||||
|             .concat(systemMessages.map(m => m.content)) |  | ||||||
|             .join('\n\n'); |  | ||||||
|  |  | ||||||
|         // Format remaining messages for Anthropic's API |         // Process each message | ||||||
|         const formattedMessages = nonSystemMessages.map(m => ({ |         for (const msg of messages) { | ||||||
|             role: m.role === 'user' ? 'user' : 'assistant', |             if (msg.role === 'system') { | ||||||
|             content: m.content |                 // Anthropic handles system messages separately | ||||||
|         })); |                 sysPrompt = msg.content; | ||||||
|  |             } else { | ||||||
|  |                 formattedMessages.push({ | ||||||
|  |                     role: msg.role, | ||||||
|  |                     content: msg.content | ||||||
|  |                 }); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|         return { |         return { | ||||||
|             messages: formattedMessages, |             messages: formattedMessages, | ||||||
|             system: combinedSystemPrompt |             system: sysPrompt | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -3,6 +3,11 @@ import { BaseAIService } from '../base_ai_service.js'; | |||||||
| import type { ChatCompletionOptions, ChatResponse, Message } from '../ai_interface.js'; | import type { ChatCompletionOptions, ChatResponse, Message } from '../ai_interface.js'; | ||||||
| import { PROVIDER_CONSTANTS } from '../constants/provider_constants.js'; | import { PROVIDER_CONSTANTS } from '../constants/provider_constants.js'; | ||||||
|  |  | ||||||
|  | interface OllamaMessage { | ||||||
|  |     role: string; | ||||||
|  |     content: string; | ||||||
|  | } | ||||||
|  |  | ||||||
| export class OllamaService extends BaseAIService { | export class OllamaService extends BaseAIService { | ||||||
|     constructor() { |     constructor() { | ||||||
|         super('Ollama'); |         super('Ollama'); | ||||||
| @@ -282,42 +287,29 @@ export class OllamaService extends BaseAIService { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private formatMessages(messages: Message[], systemPrompt: string): any[] { |     /** | ||||||
|         console.log("Input messages for formatting:", JSON.stringify(messages, null, 2)); |      * Format messages for the Ollama API | ||||||
|  |      */ | ||||||
|  |     private formatMessages(messages: Message[], systemPrompt: string): OllamaMessage[] { | ||||||
|  |         const formattedMessages: OllamaMessage[] = []; | ||||||
|  |  | ||||||
|         // Check if there are any messages with empty content |         // Add system message if provided | ||||||
|         const emptyMessages = messages.filter(msg => !msg.content || msg.content === "Empty message"); |         if (systemPrompt) { | ||||||
|         if (emptyMessages.length > 0) { |             formattedMessages.push({ | ||||||
|             console.warn("Found messages with empty content:", emptyMessages); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // Add system message if it doesn't exist |  | ||||||
|         const hasSystemMessage = messages.some(m => m.role === 'system'); |  | ||||||
|         let resultMessages = [...messages]; |  | ||||||
|  |  | ||||||
|         if (!hasSystemMessage && systemPrompt) { |  | ||||||
|             resultMessages.unshift({ |  | ||||||
|                 role: 'system', |                 role: 'system', | ||||||
|                 content: systemPrompt |                 content: systemPrompt | ||||||
|             }); |             }); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // Validate each message has content |         // Add all messages | ||||||
|         resultMessages = resultMessages.map(msg => { |         for (const msg of messages) { | ||||||
|             // Ensure each message has a valid content |             // Ollama's API accepts 'user', 'assistant', and 'system' roles | ||||||
|             if (!msg.content || typeof msg.content !== 'string') { |             formattedMessages.push({ | ||||||
|                 console.warn(`Message with role ${msg.role} has invalid content:`, msg.content); |                 role: msg.role, | ||||||
|                 return { |                 content: msg.content | ||||||
|                     ...msg, |  | ||||||
|                     content: msg.content || "Empty message" |  | ||||||
|                 }; |  | ||||||
|             } |  | ||||||
|             return msg; |  | ||||||
|             }); |             }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         console.log("Formatted messages for Ollama:", JSON.stringify(resultMessages, null, 2)); |         return formattedMessages; | ||||||
|  |  | ||||||
|         // Ollama uses the same format as OpenAI for messages |  | ||||||
|         return resultMessages; |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user