mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-08-10 18:39:22 +08:00
Better use of interfaces, reducing useage of "any"
This commit is contained in:
parent
d1cd0a8817
commit
2899707e64
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -52,7 +52,7 @@ export interface ChunkOptions {
|
|||||||
async function getDefaultChunkOptions(): Promise<Required<ChunkOptions>> {
|
async function getDefaultChunkOptions(): Promise<Required<ChunkOptions>> {
|
||||||
// Import constants dynamically to avoid circular dependencies
|
// Import constants dynamically to avoid circular dependencies
|
||||||
const { LLM_CONSTANTS } = await import('../../../routes/api/llm.js');
|
const { LLM_CONSTANTS } = await import('../../../routes/api/llm.js');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
maxChunkSize: LLM_CONSTANTS.CHUNKING.DEFAULT_SIZE,
|
maxChunkSize: LLM_CONSTANTS.CHUNKING.DEFAULT_SIZE,
|
||||||
overlapSize: LLM_CONSTANTS.CHUNKING.DEFAULT_OVERLAP,
|
overlapSize: LLM_CONSTANTS.CHUNKING.DEFAULT_OVERLAP,
|
||||||
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user