mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-03 21:19:01 +01:00 
			
		
		
		
	improve LLM response parsing
This commit is contained in:
		
							parent
							
								
									c40c702761
								
							
						
					
					
						commit
						14acd1cd89
					
				@ -29,7 +29,7 @@ class TriliumContextService {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // Configuration
 | 
					    // Configuration
 | 
				
			||||||
    private cacheExpiryMs = 5 * 60 * 1000; // 5 minutes
 | 
					    private cacheExpiryMs = 5 * 60 * 1000; // 5 minutes
 | 
				
			||||||
    private metaPrompt = `You are an AI assistant that decides what information needs to be retrieved from a knowledge base to answer the user's question.
 | 
					    private metaPrompt = `You are an AI assistant that decides what information needs to be retrieved from a user's knowledge base called TriliumNext Notes to answer the user's question.
 | 
				
			||||||
Given the user's question, generate 3-5 specific search queries that would help find relevant information.
 | 
					Given the user's question, generate 3-5 specific search queries that would help find relevant information.
 | 
				
			||||||
Each query should be focused on a different aspect of the question.
 | 
					Each query should be focused on a different aspect of the question.
 | 
				
			||||||
Format your answer as a JSON array of strings, with each string being a search query.
 | 
					Format your answer as a JSON array of strings, with each string being a search query.
 | 
				
			||||||
@ -127,28 +127,74 @@ Example: ["exact topic mentioned", "related concept 1", "related concept 2"]`;
 | 
				
			|||||||
            const responseText = response.text; // Extract the text from the response object
 | 
					            const responseText = response.text; // Extract the text from the response object
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            try {
 | 
					            try {
 | 
				
			||||||
                // Parse the JSON response
 | 
					                // Remove code blocks, quotes, and clean up the response text
 | 
				
			||||||
                const jsonStr = responseText.trim().replace(/```json|```/g, '').trim();
 | 
					                let jsonStr = responseText
 | 
				
			||||||
                const queries = JSON.parse(jsonStr);
 | 
					                    .replace(/```(?:json)?|```/g, '') // Remove code block markers
 | 
				
			||||||
 | 
					                    .replace(/[\u201C\u201D]/g, '"')  // Replace smart quotes with straight quotes
 | 
				
			||||||
 | 
					                    .trim();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (Array.isArray(queries) && queries.length > 0) {
 | 
					                // Check if the text might contain a JSON array (has square brackets)
 | 
				
			||||||
                    return queries;
 | 
					                if (jsonStr.includes('[') && jsonStr.includes(']')) {
 | 
				
			||||||
                } else {
 | 
					                    // Extract just the array part if there's explanatory text
 | 
				
			||||||
                    throw new Error("Invalid response format");
 | 
					                    const arrayMatch = jsonStr.match(/\[[\s\S]*\]/);
 | 
				
			||||||
 | 
					                    if (arrayMatch) {
 | 
				
			||||||
 | 
					                        jsonStr = arrayMatch[0];
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
            } catch (parseError) {
 | 
					
 | 
				
			||||||
                // Fallback: if JSON parsing fails, try to extract queries line by line
 | 
					                    // Try to parse the JSON
 | 
				
			||||||
 | 
					                    try {
 | 
				
			||||||
 | 
					                        const queries = JSON.parse(jsonStr);
 | 
				
			||||||
 | 
					                        if (Array.isArray(queries) && queries.length > 0) {
 | 
				
			||||||
 | 
					                            return queries.map(q => typeof q === 'string' ? q : String(q)).filter(Boolean);
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    } catch (innerError) {
 | 
				
			||||||
 | 
					                        // If parsing fails, log it and continue to the fallback
 | 
				
			||||||
 | 
					                        log.info(`JSON parse error: ${innerError}. Will use fallback parsing for: ${jsonStr}`);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Fallback 1: Try to extract an array manually by splitting on commas between quotes
 | 
				
			||||||
 | 
					                if (jsonStr.includes('[') && jsonStr.includes(']')) {
 | 
				
			||||||
 | 
					                    const arrayContent = jsonStr.substring(
 | 
				
			||||||
 | 
					                        jsonStr.indexOf('[') + 1,
 | 
				
			||||||
 | 
					                        jsonStr.lastIndexOf(']')
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    // Use regex to match quoted strings, handling escaped quotes
 | 
				
			||||||
 | 
					                    const stringMatches = arrayContent.match(/"((?:\\.|[^"\\])*)"/g);
 | 
				
			||||||
 | 
					                    if (stringMatches && stringMatches.length > 0) {
 | 
				
			||||||
 | 
					                        return stringMatches
 | 
				
			||||||
 | 
					                            .map((m: string) => m.substring(1, m.length - 1)) // Remove surrounding quotes
 | 
				
			||||||
 | 
					                            .filter((s: string) => s.length > 0);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Fallback 2: Extract queries line by line
 | 
				
			||||||
                const lines = responseText.split('\n')
 | 
					                const lines = responseText.split('\n')
 | 
				
			||||||
                    .map((line: string) => line.trim())
 | 
					                    .map((line: string) => line.trim())
 | 
				
			||||||
                    .filter((line: string) => line.length > 0 && !line.startsWith('```'));
 | 
					                    .filter((line: string) =>
 | 
				
			||||||
 | 
					                        line.length > 0 &&
 | 
				
			||||||
 | 
					                        !line.startsWith('```') &&
 | 
				
			||||||
 | 
					                        !line.match(/^\d+\.?\s*$/) && // Skip numbered list markers alone
 | 
				
			||||||
 | 
					                        !line.match(/^\[|\]$/) // Skip lines that are just brackets
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (lines.length > 0) {
 | 
					                if (lines.length > 0) {
 | 
				
			||||||
                    return lines.map((line: string) => line.replace(/^["'\d\.\-\s]+/, '').trim());
 | 
					                    // Remove numbering, quotes and other list markers from each line
 | 
				
			||||||
 | 
					                    return lines.map((line: string) => {
 | 
				
			||||||
 | 
					                        return line
 | 
				
			||||||
 | 
					                            .replace(/^\d+\.?\s*/, '') // Remove numbered list markers (1., 2., etc)
 | 
				
			||||||
 | 
					                            .replace(/^[-*•]\s*/, '')  // Remove bullet list markers
 | 
				
			||||||
 | 
					                            .replace(/^["']|["']$/g, '') // Remove surrounding quotes
 | 
				
			||||||
 | 
					                            .trim();
 | 
				
			||||||
 | 
					                    }).filter((s: string) => s.length > 0);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } catch (parseError) {
 | 
				
			||||||
 | 
					                log.error(`Error parsing search queries: ${parseError}`);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // If all else fails, just use the original question
 | 
					            // If all else fails, just use the original question
 | 
				
			||||||
            return [userQuestion];
 | 
					            return [userQuestion];
 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        } 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}`);
 | 
				
			||||||
@ -195,10 +241,14 @@ Example: ["exact topic mentioned", "related concept 1", "related concept 2"]`;
 | 
				
			|||||||
            // Set to keep track of note IDs we've seen to avoid duplicates
 | 
					            // Set to keep track of note IDs we've seen to avoid duplicates
 | 
				
			||||||
            const seenNoteIds = new Set<string>();
 | 
					            const seenNoteIds = new Set<string>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Log the provider and model being used
 | 
				
			||||||
 | 
					            log.info(`Searching with embedding provider: ${this.provider.name}, model: ${this.provider.getConfig().model}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Process each query
 | 
					            // Process each query
 | 
				
			||||||
            for (const query of queries) {
 | 
					            for (const query of queries) {
 | 
				
			||||||
                // Get embeddings for this query using the correct method name
 | 
					                // Get embeddings for this query using the correct method name
 | 
				
			||||||
                const queryEmbedding = await this.provider.generateEmbeddings(query);
 | 
					                const queryEmbedding = await this.provider.generateEmbeddings(query);
 | 
				
			||||||
 | 
					                log.info(`Generated embedding for query: "${query}" (${queryEmbedding.length} dimensions)`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                // Find notes similar to this query
 | 
					                // Find notes similar to this query
 | 
				
			||||||
                let results;
 | 
					                let results;
 | 
				
			||||||
@ -209,6 +259,7 @@ Example: ["exact topic mentioned", "related concept 1", "related concept 2"]`;
 | 
				
			|||||||
                        contextNoteId,
 | 
					                        contextNoteId,
 | 
				
			||||||
                        Math.min(limit, 5) // Limit per query
 | 
					                        Math.min(limit, 5) // Limit per query
 | 
				
			||||||
                    );
 | 
					                    );
 | 
				
			||||||
 | 
					                    log.info(`Found ${results.length} notes within branch context for query: "${query}"`);
 | 
				
			||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
                    // Search all notes
 | 
					                    // Search all notes
 | 
				
			||||||
                    results = await vectorStore.findSimilarNotes(
 | 
					                    results = await vectorStore.findSimilarNotes(
 | 
				
			||||||
@ -218,6 +269,7 @@ Example: ["exact topic mentioned", "related concept 1", "related concept 2"]`;
 | 
				
			|||||||
                        Math.min(limit, 5), // Limit per query
 | 
					                        Math.min(limit, 5), // Limit per query
 | 
				
			||||||
                        0.5 // Lower threshold to get more diverse results
 | 
					                        0.5 // Lower threshold to get more diverse results
 | 
				
			||||||
                    );
 | 
					                    );
 | 
				
			||||||
 | 
					                    log.info(`Found ${results.length} notes in vector store for query: "${query}"`);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                // Process results
 | 
					                // Process results
 | 
				
			||||||
@ -246,6 +298,8 @@ Example: ["exact topic mentioned", "related concept 1", "related concept 2"]`;
 | 
				
			|||||||
                .sort((a, b) => b.similarity - a.similarity)
 | 
					                .sort((a, b) => b.similarity - a.similarity)
 | 
				
			||||||
                .slice(0, limit);
 | 
					                .slice(0, limit);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            log.info(`Total unique relevant notes found across all queries: ${sortedResults.length}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Cache the results
 | 
					            // Cache the results
 | 
				
			||||||
            this.recentQueriesCache.set(cacheKey, {
 | 
					            this.recentQueriesCache.set(cacheKey, {
 | 
				
			||||||
                timestamp: Date.now(),
 | 
					                timestamp: Date.now(),
 | 
				
			||||||
@ -362,22 +416,33 @@ Example: ["exact topic mentioned", "related concept 1", "related concept 2"]`;
 | 
				
			|||||||
                   "with general knowledge about Trilium or other topics you're interested in.";
 | 
					                   "with general knowledge about Trilium or other topics you're interested in.";
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let context = `I've found some relevant information in your notes that may help answer: "${query}"\n\n`;
 | 
					        // Get provider name to adjust context for different models
 | 
				
			||||||
 | 
					        const providerId = this.provider?.name || 'default';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Import the constants dynamically to avoid circular dependencies
 | 
				
			||||||
 | 
					        const { LLM_CONSTANTS } = await import('../../routes/api/llm.js');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Get appropriate context size and format based on provider
 | 
				
			||||||
 | 
					        const maxTotalLength =
 | 
				
			||||||
 | 
					            providerId === 'openai' ? LLM_CONSTANTS.CONTEXT_WINDOW.OPENAI :
 | 
				
			||||||
 | 
					            providerId === 'anthropic' ? LLM_CONSTANTS.CONTEXT_WINDOW.ANTHROPIC :
 | 
				
			||||||
 | 
					            providerId === 'ollama' ? LLM_CONSTANTS.CONTEXT_WINDOW.OLLAMA :
 | 
				
			||||||
 | 
					            LLM_CONSTANTS.CONTEXT_WINDOW.DEFAULT;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Use a format appropriate for the model family
 | 
				
			||||||
 | 
					        // Anthropic has a specific system message format that works better with certain structures
 | 
				
			||||||
 | 
					        const isAnthropicFormat = providerId === 'anthropic';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Start with different headers based on provider
 | 
				
			||||||
 | 
					        let context = isAnthropicFormat
 | 
				
			||||||
 | 
					            ? `I'm your AI assistant helping with your Trilium notes database. For your query: "${query}", I found these relevant notes:\n\n`
 | 
				
			||||||
 | 
					            : `I've found some relevant information in your notes that may help answer: "${query}"\n\n`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Sort sources by similarity if available to prioritize most relevant
 | 
					        // Sort sources by similarity if available to prioritize most relevant
 | 
				
			||||||
        if (sources[0] && sources[0].similarity !== undefined) {
 | 
					        if (sources[0] && sources[0].similarity !== undefined) {
 | 
				
			||||||
            sources = [...sources].sort((a, b) => (b.similarity || 0) - (a.similarity || 0));
 | 
					            sources = [...sources].sort((a, b) => (b.similarity || 0) - (a.similarity || 0));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Get provider name to adjust context for different models
 | 
					 | 
				
			||||||
        const providerId = this.provider?.name || 'default';
 | 
					 | 
				
			||||||
        // Get approximate max length based on provider using constants
 | 
					 | 
				
			||||||
        // Import the constants dynamically to avoid circular dependencies
 | 
					 | 
				
			||||||
        const { LLM_CONSTANTS } = await import('../../routes/api/llm.js');
 | 
					 | 
				
			||||||
        const maxTotalLength = providerId === 'ollama' ? LLM_CONSTANTS.CONTEXT_WINDOW.OLLAMA :
 | 
					 | 
				
			||||||
                              providerId === 'openai' ? LLM_CONSTANTS.CONTEXT_WINDOW.OPENAI :
 | 
					 | 
				
			||||||
                              LLM_CONSTANTS.CONTEXT_WINDOW.ANTHROPIC;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Track total context length to avoid oversized context
 | 
					        // Track total context length to avoid oversized context
 | 
				
			||||||
        let currentLength = context.length;
 | 
					        let currentLength = context.length;
 | 
				
			||||||
        const maxNoteContentLength = Math.min(LLM_CONSTANTS.CONTENT.MAX_NOTE_CONTENT_LENGTH,
 | 
					        const maxNoteContentLength = Math.min(LLM_CONSTANTS.CONTENT.MAX_NOTE_CONTENT_LENGTH,
 | 
				
			||||||
@ -387,7 +452,7 @@ Example: ["exact topic mentioned", "related concept 1", "related concept 2"]`;
 | 
				
			|||||||
            // Check if adding this source would exceed our total limit
 | 
					            // Check if adding this source would exceed our total limit
 | 
				
			||||||
            if (currentLength >= maxTotalLength) return;
 | 
					            if (currentLength >= maxTotalLength) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Build source section
 | 
					            // Build source section with formatting appropriate for the provider
 | 
				
			||||||
            let sourceSection = `### ${source.title}\n`;
 | 
					            let sourceSection = `### ${source.title}\n`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Add relationship context if available
 | 
					            // Add relationship context if available
 | 
				
			||||||
@ -429,11 +494,16 @@ Example: ["exact topic mentioned", "related concept 1", "related concept 2"]`;
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Add clear instructions about how to reference the notes
 | 
					        // Add provider-specific instructions
 | 
				
			||||||
 | 
					        if (isAnthropicFormat) {
 | 
				
			||||||
 | 
					            context += "When you refer to any information from these notes, cite the note title explicitly (e.g., \"According to the note [Title]...\"). " +
 | 
				
			||||||
 | 
					                      "If the provided notes don't answer the query fully, acknowledge that and then use your general knowledge to help.\n\n" +
 | 
				
			||||||
 | 
					                      "Be concise but thorough in your responses.";
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
            context += "When referring to information from these notes in your response, please cite them by their titles " +
 | 
					            context += "When referring to information from these notes in your response, please cite them by their titles " +
 | 
				
			||||||
                  "(e.g., \"According to your note on [Title]...\") rather than using labels like \"Note 1\" or \"Note 2\".\n\n";
 | 
					                      "(e.g., \"According to your note on [Title]...\") rather than using labels like \"Note 1\" or \"Note 2\".\n\n" +
 | 
				
			||||||
 | 
					                      "If the information doesn't contain what you need, just say so and use your general knowledge instead.";
 | 
				
			||||||
        context += "If the information doesn't contain what you need, just say so and use your general knowledge instead.";
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return context;
 | 
					        return context;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user