diff --git a/src/services/llm/trilium_context_service.ts b/src/services/llm/trilium_context_service.ts index e159a1ed0..0f1a58e69 100644 --- a/src/services/llm/trilium_context_service.ts +++ b/src/services/llm/trilium_context_service.ts @@ -29,7 +29,7 @@ class TriliumContextService { // Configuration 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. 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. @@ -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 try { - // Parse the JSON response - const jsonStr = responseText.trim().replace(/```json|```/g, '').trim(); - const queries = JSON.parse(jsonStr); + // Remove code blocks, quotes, and clean up the response text + let jsonStr = responseText + .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) { - return queries; - } else { - throw new Error("Invalid response format"); + // Check if the text might contain a JSON array (has square brackets) + if (jsonStr.includes('[') && jsonStr.includes(']')) { + // Extract just the array part if there's explanatory text + const arrayMatch = jsonStr.match(/\[[\s\S]*\]/); + if (arrayMatch) { + jsonStr = arrayMatch[0]; + } + + // 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}`); + } } - } catch (parseError) { - // Fallback: if JSON parsing fails, try to extract queries line by line + + // 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') .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) { - 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); } - - // If all else fails, just use the original question - return [userQuestion]; + } catch (parseError) { + log.error(`Error parsing search queries: ${parseError}`); } + + // If all else fails, just use the original question + return [userQuestion]; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); 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 const seenNoteIds = new Set(); + // Log the provider and model being used + log.info(`Searching with embedding provider: ${this.provider.name}, model: ${this.provider.getConfig().model}`); + // Process each query for (const query of queries) { // Get embeddings for this query using the correct method name const queryEmbedding = await this.provider.generateEmbeddings(query); + log.info(`Generated embedding for query: "${query}" (${queryEmbedding.length} dimensions)`); // Find notes similar to this query let results; @@ -209,6 +259,7 @@ Example: ["exact topic mentioned", "related concept 1", "related concept 2"]`; contextNoteId, Math.min(limit, 5) // Limit per query ); + log.info(`Found ${results.length} notes within branch context for query: "${query}"`); } else { // Search all notes 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 0.5 // Lower threshold to get more diverse results ); + log.info(`Found ${results.length} notes in vector store for query: "${query}"`); } // Process results @@ -246,6 +298,8 @@ Example: ["exact topic mentioned", "related concept 1", "related concept 2"]`; .sort((a, b) => b.similarity - a.similarity) .slice(0, limit); + log.info(`Total unique relevant notes found across all queries: ${sortedResults.length}`); + // Cache the results this.recentQueriesCache.set(cacheKey, { 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."; } - 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 if (sources[0] && sources[0].similarity !== undefined) { 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 let currentLength = context.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 if (currentLength >= maxTotalLength) return; - // Build source section + // Build source section with formatting appropriate for the provider let sourceSection = `### ${source.title}\n`; // 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 - 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"; - - context += "If the information doesn't contain what you need, just say so and use your general knowledge instead."; + // 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 " + + "(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."; + } return context; }