UNPKG

n8n-nodes-query-retriever-rerank

Version:

Advanced n8n community node for intelligent document retrieval with multi-step reasoning, reranking, and comprehensive debugging

242 lines (224 loc) 11.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.MultiStepQueryStrategy = void 0; const BaseStrategy_1 = require("./BaseStrategy"); const reranking_1 = require("../shared/reranking"); class MultiStepQueryStrategy extends BaseStrategy_1.BaseStrategy { getName() { return 'multi_step_query'; } getDescription() { return 'Break complex queries into sequential reasoning steps, building context progressively'; } async execute(input, context) { try { // Initialize debug manager this.initializeDebugManager('multi_step_query', context.config); const maxSteps = context.config.maxSteps || 3; const enableEarlyStop = context.config.enableEarlyStop !== false; let prevReasoning = ""; let allDocuments = []; const stepResults = []; const stepStart = Date.now(); this.debugManager.setQueryDetails({ original: input, maxSteps, enableEarlyStop }); // Execute multi-step reasoning for (let step = 0; step < maxSteps; step++) { const stepStartTime = Date.now(); try { // 1. Generate sub-question for this step const subQuery = await this.generateSubQuery(input, prevReasoning, step, context); // 2. Retrieve and rerank documents for this sub-question const stepDocs = await this.retrieveForStep(subQuery, step, context); // 3. Generate intermediate answer for this step const stepAnswer = await this.generateStepAnswer(stepDocs, subQuery, prevReasoning, context); // 4. Check if we should stop early const shouldStop = enableEarlyStop && step > 0 && await this.shouldStopEarly(stepAnswer, input, prevReasoning, context); // 5. Update accumulated reasoning const stepReasoning = `Step ${step + 1}: ${subQuery}\nAnswer: ${stepAnswer}`; prevReasoning += (prevReasoning ? '\n\n' : '') + stepReasoning; // 6. Store step result const stepResult = { step: step + 1, subQuery, documents: stepDocs, stepAnswer, reasoning: stepReasoning, shouldStop }; stepResults.push(stepResult); // 7. Add documents to collection (with deduplication) this.addDocumentsWithDeduplication(allDocuments, stepDocs); this.debugManager.addTiming(`step_${step + 1}`, stepStartTime); // 8. Early stopping check if (shouldStop) { this.debugManager.setQueryDetails({ stoppedEarly: true, stoppedAtStep: step + 1, reason: 'Early stopping condition met' }); break; } } catch (stepError) { // Log step error but continue with other steps this.debugManager.setQueryDetails({ [`step_${step + 1}_error`]: stepError instanceof Error ? stepError.message : String(stepError) }); continue; } } this.debugManager.addTiming('allSteps', stepStart); this.debugManager.setDocumentFlow({ totalStepsExecuted: stepResults.length, totalDocumentsCollected: allDocuments.length, finalCount: allDocuments.length }); // Store detailed step debugging information if (context.debugging) { this.debugManager.setRerankingData({ multiStepDetails: { stepResults: stepResults.map(r => ({ step: r.step, subQuery: r.subQuery, documentsRetrieved: r.documents.length, stepAnswer: r.stepAnswer.substring(0, 200) + (r.stepAnswer.length > 200 ? '...' : ''), shouldStop: r.shouldStop })), stepsData: stepResults.map(r => ({ step: r.step, documentsFound: r.documents.length, subQuery: r.subQuery })), progressiveReasoning: prevReasoning, documentDeduplication: { totalCollected: allDocuments.length, uniqueAfterDedup: allDocuments.length } } }); } // 9. Final synthesis from all steps const finalAnswer = await this.synthesizeFinalAnswer(input, prevReasoning, allDocuments, stepResults, context); // Handle debugging await this.handleDebugging(input, context); // Format and return result return this.formatResult(finalAnswer, allDocuments, context); } catch (error) { return { error: error instanceof Error ? error.message : String(error) }; } } // Generate next sub-question based on original query and previous reasoning async generateSubQuery(originalQuery, prevReasoning, step, context) { const stepPrompt = `You are helping to answer a complex question through step-by-step reasoning. Original Question: "${originalQuery}" ${prevReasoning ? `Previous Steps:\n${prevReasoning}\n` : ''} Generate the next specific sub-question that should be answered in Step ${step + 1} to help build toward answering the original question. The sub-question should: - Build on previous steps if any exist - Focus on a specific aspect that hasn't been fully addressed - Be answerable with document retrieval - Move toward answering the original question Output only the sub-question, no explanations:`; const response = await context.model.invoke(stepPrompt); const subQuery = typeof response === 'string' ? response : response.content || response.text || String(response); return subQuery.trim().replace(/^["']|["']$/g, ''); // Remove quotes if present } // Retrieve documents for a specific step async retrieveForStep(subQuery, step, context) { try { // Retrieve documents for this sub-query const rawDocs = await context.vectorStore.similaritySearch(subQuery, context.config.documentsToRetrieve); // Apply reranking specific to this step const rerankResult = await reranking_1.RerankingManager.performReranking(rawDocs, subQuery, context.embeddings, Math.min(context.config.documentsToReturn, rawDocs.length), { strategyType: 'multi_step_query', queryIndex: step, label: `step ${step + 1}` }, context.debugging); return rerankResult.docs; } catch (error) { // Return empty array if retrieval fails return []; } } // Generate answer for a specific step async generateStepAnswer(stepDocs, subQuery, prevReasoning, context) { if (stepDocs.length === 0) { return "No relevant documents found for this step."; } // Prepare context from step documents const stepContext = stepDocs.map((doc, index) => `Document ${index + 1}:\n${doc.pageContent}\n`).join('\n'); const stepPrompt = `Answer the following sub-question using the provided documents. Be concise but thorough. ${prevReasoning ? `Previous reasoning:\n${prevReasoning}\n\n` : ''} Sub-question: ${subQuery} Available documents: ${stepContext} Answer:`; const response = await context.model.invoke(stepPrompt); return typeof response === 'string' ? response : response.content || response.text || String(response); } // Check if we should stop early based on the current step's answer async shouldStopEarly(stepAnswer, originalQuery, prevReasoning, context) { const stopPrompt = `Determine if we have sufficient information to answer the original question. Original Question: "${originalQuery}" Reasoning so far: ${prevReasoning} Current step answer: ${stepAnswer} Can the original question be sufficiently answered with the information gathered so far? Respond with only "YES" or "NO":`; try { const response = await context.model.invoke(stopPrompt); const decision = typeof response === 'string' ? response : response.content || response.text || String(response); return decision.trim().toUpperCase().includes('YES'); } catch (error) { // If check fails, continue with more steps return false; } } // Add documents with deduplication addDocumentsWithDeduplication(allDocuments, newDocuments) { const existingContent = new Set(allDocuments.map(doc => doc.pageContent.trim())); for (const doc of newDocuments) { if (!existingContent.has(doc.pageContent.trim())) { allDocuments.push(doc); existingContent.add(doc.pageContent.trim()); } } } // Synthesize final answer from all steps async synthesizeFinalAnswer(originalQuery, prevReasoning, allDocuments, stepResults, context) { // Prepare comprehensive context const documentContext = allDocuments.map((doc, index) => `Document ${index + 1}:\n${doc.pageContent}\n`).join('\n'); const synthesisPrompt = `Provide a comprehensive answer to the original question using the step-by-step reasoning and supporting documents. Original Question: "${originalQuery}" Step-by-step reasoning: ${prevReasoning} Supporting documents: ${documentContext} Synthesize a complete, well-structured answer that: 1. Directly addresses the original question 2. Incorporates insights from the step-by-step reasoning 3. References relevant information from the documents 4. Is coherent and comprehensive Final Answer:`; const answerStart = Date.now(); const response = await context.model.invoke(synthesisPrompt); this.debugManager.addTiming('finalSynthesis', answerStart); this.debugManager.setAnswerGenerationData({ stepsCount: stepResults.length, reasoningLength: prevReasoning.length, documentsUsed: allDocuments.length, synthesisPromptLength: synthesisPrompt.length }); return typeof response === 'string' ? response : response.content || response.text || String(response); } } exports.MultiStepQueryStrategy = MultiStepQueryStrategy;