mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-08-10 02:02:29 +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
|
||||
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
|
||||
const contextMessage: Message = {
|
||||
@ -1063,8 +1072,7 @@ async function sendMessage(req: Request, res: Response) {
|
||||
sources: sourceNotes.map(note => ({
|
||||
noteId: note.noteId,
|
||||
title: note.title,
|
||||
similarity: note.similarity,
|
||||
branchId: note.branchId
|
||||
similarity: note.similarity
|
||||
}))
|
||||
};
|
||||
}
|
||||
@ -1198,8 +1206,7 @@ async function sendMessage(req: Request, res: Response) {
|
||||
sources: sourceNotes.map(note => ({
|
||||
noteId: note.noteId,
|
||||
title: note.title,
|
||||
similarity: note.similarity,
|
||||
branchId: note.branchId
|
||||
similarity: note.similarity
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ export interface ThinkingStep {
|
||||
sources?: string[];
|
||||
parentId?: string;
|
||||
children?: string[];
|
||||
metadata?: Record<string, any>;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -12,7 +12,7 @@ export interface ContentChunk {
|
||||
noteId?: string;
|
||||
title?: 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
|
||||
*/
|
||||
metadata?: Record<string, any>;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -52,7 +52,7 @@ export interface ChunkOptions {
|
||||
async function getDefaultChunkOptions(): Promise<Required<ChunkOptions>> {
|
||||
// Import constants dynamically to avoid circular dependencies
|
||||
const { LLM_CONSTANTS } = await import('../../../routes/api/llm.js');
|
||||
|
||||
|
||||
return {
|
||||
maxChunkSize: LLM_CONSTANTS.CHUNKING.DEFAULT_SIZE,
|
||||
overlapSize: LLM_CONSTANTS.CHUNKING.DEFAULT_OVERLAP,
|
||||
@ -293,3 +293,11 @@ export async function semanticChunking(
|
||||
|
||||
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 { CONTEXT_PROMPTS } from '../../constants/llm_prompt_constants.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
|
||||
@ -73,10 +75,10 @@ export class ContextService {
|
||||
*/
|
||||
async processQuery(
|
||||
userQuestion: string,
|
||||
llmService: any,
|
||||
llmService: LLMServiceInterface,
|
||||
contextNoteId: string | null = null,
|
||||
showThinking: boolean = false
|
||||
) {
|
||||
): Promise<{ context: string; sources: NoteSearchResult[]; thinking?: string }> {
|
||||
log.info(`Processing query with: question="${userQuestion.substring(0, 50)}...", noteId=${contextNoteId}, showThinking=${showThinking}`);
|
||||
|
||||
if (!this.initialized) {
|
||||
@ -87,8 +89,8 @@ export class ContextService {
|
||||
// Return a fallback response if initialization fails
|
||||
return {
|
||||
context: CONTEXT_PROMPTS.NO_NOTES_CONTEXT,
|
||||
notes: [],
|
||||
queries: [userQuestion]
|
||||
sources: [],
|
||||
thinking: undefined
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -105,10 +107,10 @@ export class ContextService {
|
||||
log.info(`Generated search queries: ${JSON.stringify(searchQueries)}`);
|
||||
|
||||
// Step 2: Find relevant notes using multi-query approach
|
||||
let relevantNotes: any[] = [];
|
||||
let relevantNotes: NoteSearchResult[] = [];
|
||||
try {
|
||||
// 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) {
|
||||
const results = await semanticSearch.findRelevantNotes(
|
||||
@ -124,7 +126,7 @@ export class ContextService {
|
||||
} else {
|
||||
// If note already exists, update similarity to max of both values
|
||||
const existing = allResults.get(result.noteId);
|
||||
if (result.similarity > existing.similarity) {
|
||||
if (existing && result.similarity > existing.similarity) {
|
||||
existing.similarity = result.similarity;
|
||||
allResults.set(result.noteId, existing);
|
||||
}
|
||||
@ -186,15 +188,15 @@ export class ContextService {
|
||||
|
||||
return {
|
||||
context: enhancedContext,
|
||||
notes: relevantNotes,
|
||||
queries: searchQueries
|
||||
sources: relevantNotes,
|
||||
thinking: showThinking ? this.summarizeContextStructure(enhancedContext) : undefined
|
||||
};
|
||||
} catch (error) {
|
||||
log.error(`Error processing query: ${error}`);
|
||||
return {
|
||||
context: CONTEXT_PROMPTS.NO_NOTES_CONTEXT,
|
||||
notes: [],
|
||||
queries: [userQuestion]
|
||||
sources: [],
|
||||
thinking: undefined
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -212,7 +214,7 @@ export class ContextService {
|
||||
noteId: string,
|
||||
query: string,
|
||||
showThinking: boolean = false,
|
||||
relevantNotes: Array<any> = []
|
||||
relevantNotes: NoteSearchResult[] = []
|
||||
): Promise<string> {
|
||||
try {
|
||||
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
|
||||
// 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)
|
||||
if (relevantNotes && relevantNotes.length > 0) {
|
||||
@ -409,7 +411,10 @@ export class ContextService {
|
||||
log.info(`Adding ${vectorSearchNotes.length} notes from vector search to combined results`);
|
||||
for (const note of vectorSearchNotes) {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@ -831,7 +836,7 @@ export class ContextService {
|
||||
}
|
||||
|
||||
// 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
|
||||
const batchSize = 5;
|
||||
@ -840,7 +845,7 @@ export class ContextService {
|
||||
for (let i = 0; i < chunks.length; i += batchSize) {
|
||||
const batch = chunks.slice(i, i + batchSize);
|
||||
const batchEmbeddings = await Promise.all(
|
||||
batch.map(chunk => provider.createEmbedding(chunk))
|
||||
batch.map(chunk => provider.generateEmbeddings(chunk))
|
||||
);
|
||||
chunkEmbeddings.push(...batchEmbeddings);
|
||||
}
|
||||
@ -848,7 +853,8 @@ export class ContextService {
|
||||
// Calculate similarity between query and each chunk
|
||||
const similarities: Array<{index: number, similarity: number, content: string}> =
|
||||
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] };
|
||||
});
|
||||
|
||||
@ -891,6 +897,28 @@ export class ContextService {
|
||||
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
|
||||
|
@ -2,11 +2,13 @@ import log from '../../../log.js';
|
||||
import cacheManager from './cache_manager.js';
|
||||
import type { Message } from '../../ai_interface.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
|
||||
*/
|
||||
export class QueryEnhancer {
|
||||
export class QueryEnhancer implements IQueryEnhancer {
|
||||
// Use the centralized query enhancer prompt
|
||||
private metaPrompt = CONTEXT_PROMPTS.QUERY_ENHANCER;
|
||||
|
||||
@ -17,11 +19,15 @@ export class QueryEnhancer {
|
||||
* @param llmService - The LLM service to use for generating 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 {
|
||||
// Check cache first
|
||||
const cached = cacheManager.getQueryResults(`searchQueries:${userQuestion}`);
|
||||
if (cached) {
|
||||
// Check cache with proper type checking
|
||||
const cached = cacheManager.getQueryResults<string[]>(`searchQueries:${userQuestion}`);
|
||||
if (cached && Array.isArray(cached)) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
@ -120,7 +126,6 @@ export class QueryEnhancer {
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
log.error(`Error generating search queries: ${errorMessage}`);
|
||||
// Fallback to just using the original question
|
||||
return [userQuestion];
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@
|
||||
import log from '../log.js';
|
||||
import contextService from './context/modules/context_service.js';
|
||||
import { ContextExtractor } from './context/index.js';
|
||||
import type { NoteSearchResult } from './interfaces/context_interfaces.js';
|
||||
|
||||
/**
|
||||
* Main Context Service for Trilium Notes
|
||||
@ -84,7 +85,7 @@ class TriliumContextService {
|
||||
* @param query - The original user query
|
||||
* @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 providerId = provider?.name || 'default';
|
||||
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 type { NoteEmbeddingContext } from './embeddings_interface.js';
|
||||
import log from "../../log.js";
|
||||
import { LLM_CONSTANTS } from "../../../routes/api/llm.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 {
|
||||
name: string = "base";
|
||||
protected config: EmbeddingConfig;
|
||||
export abstract class BaseEmbeddingProvider {
|
||||
protected model: string;
|
||||
protected dimension: number;
|
||||
protected type: string;
|
||||
protected maxBatchSize: number = 100;
|
||||
protected apiKey?: 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) {
|
||||
this.config = config;
|
||||
this.model = config.model;
|
||||
this.dimension = config.dimension;
|
||||
this.type = config.type;
|
||||
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 {
|
||||
@ -79,12 +104,12 @@ export abstract class BaseEmbeddingProvider implements EmbeddingProvider {
|
||||
* Process a batch of texts with adaptive handling
|
||||
* 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[],
|
||||
processFn: (batch: T[]) => Promise<any[]>,
|
||||
isBatchSizeError: (error: any) => boolean
|
||||
): Promise<any[]> {
|
||||
const results: any[] = [];
|
||||
processFn: (batch: T[]) => Promise<R[]>,
|
||||
isBatchSizeError: (error: unknown) => boolean
|
||||
): Promise<R[]> {
|
||||
const results: R[] = [];
|
||||
const failures: { index: number, error: string }[] = [];
|
||||
let currentBatchSize = await this.getBatchSize();
|
||||
let lastError: Error | null = null;
|
||||
@ -99,9 +124,9 @@ export abstract class BaseEmbeddingProvider implements EmbeddingProvider {
|
||||
results.push(...batchResults);
|
||||
i += batch.length;
|
||||
}
|
||||
catch (error: any) {
|
||||
lastError = error;
|
||||
const errorMessage = error.message || 'Unknown error';
|
||||
catch (error) {
|
||||
lastError = error as Error;
|
||||
const errorMessage = (lastError as Error).message || 'Unknown error';
|
||||
|
||||
// Check if this is a batch size related error
|
||||
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
|
||||
* Override in provider-specific implementations
|
||||
*/
|
||||
protected isBatchSizeError(error: any): boolean {
|
||||
const errorMessage = error?.message || '';
|
||||
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())
|
||||
);
|
||||
protected isBatchSizeError(error: unknown): boolean {
|
||||
return checkBatchSizeError(error);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -173,11 +189,11 @@ export abstract class BaseEmbeddingProvider implements EmbeddingProvider {
|
||||
);
|
||||
return batchResults;
|
||||
},
|
||||
this.isBatchSizeError
|
||||
this.isBatchSizeError.bind(this)
|
||||
);
|
||||
}
|
||||
catch (error: any) {
|
||||
const errorMessage = error.message || "Unknown error";
|
||||
catch (error) {
|
||||
const errorMessage = (error as Error).message || "Unknown error";
|
||||
log.error(`Batch embedding error for provider ${this.name}: ${errorMessage}`);
|
||||
throw new Error(`${this.name} batch embedding error: ${errorMessage}`);
|
||||
}
|
||||
@ -208,11 +224,11 @@ export abstract class BaseEmbeddingProvider implements EmbeddingProvider {
|
||||
);
|
||||
return batchResults;
|
||||
},
|
||||
this.isBatchSizeError
|
||||
this.isBatchSizeError.bind(this)
|
||||
);
|
||||
}
|
||||
catch (error: any) {
|
||||
const errorMessage = error.message || "Unknown error";
|
||||
catch (error) {
|
||||
const errorMessage = (error as Error).message || "Unknown error";
|
||||
log.error(`Batch note embedding error for provider ${this.name}: ${errorMessage}`);
|
||||
throw new Error(`${this.name} batch note embedding error: ${errorMessage}`);
|
||||
}
|
||||
@ -357,4 +373,66 @@ export abstract class BaseEmbeddingProvider implements EmbeddingProvider {
|
||||
|
||||
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 {
|
||||
noteId: string;
|
||||
title: string;
|
||||
content?: string;
|
||||
content?: string | null;
|
||||
type?: string;
|
||||
mime?: string;
|
||||
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 { PROVIDER_CONSTANTS } from '../constants/provider_constants.js';
|
||||
|
||||
interface AnthropicMessage {
|
||||
role: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export class AnthropicService extends BaseAIService {
|
||||
// Map of simplified model names to full model names with versions
|
||||
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
|
||||
const systemMessages = messages.filter(m => m.role === 'system');
|
||||
const nonSystemMessages = messages.filter(m => m.role !== 'system');
|
||||
/**
|
||||
* Format messages for the Anthropic API
|
||||
*/
|
||||
private formatMessages(messages: Message[], systemPrompt: string): { messages: AnthropicMessage[], system: string } {
|
||||
const formattedMessages: AnthropicMessage[] = [];
|
||||
|
||||
// Combine all system messages with our default
|
||||
const combinedSystemPrompt = [systemPrompt]
|
||||
.concat(systemMessages.map(m => m.content))
|
||||
.join('\n\n');
|
||||
// Extract the system message if present
|
||||
let sysPrompt = systemPrompt;
|
||||
|
||||
// Format remaining messages for Anthropic's API
|
||||
const formattedMessages = nonSystemMessages.map(m => ({
|
||||
role: m.role === 'user' ? 'user' : 'assistant',
|
||||
content: m.content
|
||||
}));
|
||||
// Process each message
|
||||
for (const msg of messages) {
|
||||
if (msg.role === 'system') {
|
||||
// Anthropic handles system messages separately
|
||||
sysPrompt = msg.content;
|
||||
} else {
|
||||
formattedMessages.push({
|
||||
role: msg.role,
|
||||
content: msg.content
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
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 { PROVIDER_CONSTANTS } from '../constants/provider_constants.js';
|
||||
|
||||
interface OllamaMessage {
|
||||
role: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export class OllamaService extends BaseAIService {
|
||||
constructor() {
|
||||
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
|
||||
const emptyMessages = messages.filter(msg => !msg.content || msg.content === "Empty message");
|
||||
if (emptyMessages.length > 0) {
|
||||
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({
|
||||
// Add system message if provided
|
||||
if (systemPrompt) {
|
||||
formattedMessages.push({
|
||||
role: 'system',
|
||||
content: systemPrompt
|
||||
});
|
||||
}
|
||||
|
||||
// Validate each message has content
|
||||
resultMessages = resultMessages.map(msg => {
|
||||
// Ensure each message has a valid content
|
||||
if (!msg.content || typeof msg.content !== 'string') {
|
||||
console.warn(`Message with role ${msg.role} has invalid content:`, msg.content);
|
||||
return {
|
||||
...msg,
|
||||
content: msg.content || "Empty message"
|
||||
};
|
||||
}
|
||||
return msg;
|
||||
});
|
||||
// Add all messages
|
||||
for (const msg of messages) {
|
||||
// Ollama's API accepts 'user', 'assistant', and 'system' roles
|
||||
formattedMessages.push({
|
||||
role: msg.role,
|
||||
content: msg.content
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Formatted messages for Ollama:", JSON.stringify(resultMessages, null, 2));
|
||||
|
||||
// Ollama uses the same format as OpenAI for messages
|
||||
return resultMessages;
|
||||
return formattedMessages;
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user