diff --git a/src/services/llm/agent_tools/contextual_thinking_tool.ts b/src/services/llm/agent_tools/contextual_thinking_tool.ts index 70f2fa2a1..37db17293 100644 --- a/src/services/llm/agent_tools/contextual_thinking_tool.ts +++ b/src/services/llm/agent_tools/contextual_thinking_tool.ts @@ -20,388 +20,392 @@ import aiServiceManager from "../ai_service_manager.js"; * Represents a single reasoning step taken by the agent */ export interface ThinkingStep { - id: string; - content: string; - type: 'observation' | 'hypothesis' | 'question' | 'evidence' | 'conclusion'; - confidence?: number; - sources?: string[]; - parentId?: string; - children?: string[]; - metadata?: Record; + id: string; + content: string; + type: 'observation' | 'hypothesis' | 'question' | 'evidence' | 'conclusion'; + confidence?: number; + sources?: string[]; + parentId?: string; + children?: string[]; + metadata?: Record; } /** * Contains the full reasoning process */ export interface ThinkingProcess { - id: string; - query: string; - steps: ThinkingStep[]; - status: 'in_progress' | 'completed'; - startTime: number; - endTime?: number; + id: string; + query: string; + steps: ThinkingStep[]; + status: 'in_progress' | 'completed'; + startTime: number; + endTime?: number; } export class ContextualThinkingTool { - private static thinkingCounter = 0; - private static stepCounter = 0; - private activeProcId?: string; - private processes: Record = {}; + private static thinkingCounter = 0; + private static stepCounter = 0; + private activeProcId?: string; + private processes: Record = {}; - /** - * Start a new thinking process for a query - * - * @param query The user's query - * @returns The created thinking process ID - */ - startThinking(query: string): string { - const thinkingId = `thinking_${Date.now()}_${ContextualThinkingTool.thinkingCounter++}`; + /** + * Start a new thinking process for a query + * + * @param query The user's query + * @returns The created thinking process ID + */ + startThinking(query: string): string { + const thinkingId = `thinking_${Date.now()}_${ContextualThinkingTool.thinkingCounter++}`; - log.info(`Starting thinking process: ${thinkingId} for query "${query.substring(0, 50)}..."`); + log.info(`Starting thinking process: ${thinkingId} for query "${query.substring(0, 50)}..."`); - this.processes[thinkingId] = { - id: thinkingId, - query, - steps: [], - status: 'in_progress', - startTime: Date.now() - }; + this.processes[thinkingId] = { + id: thinkingId, + query, + steps: [], + status: 'in_progress', + startTime: Date.now() + }; - // Set as active process - this.activeProcId = thinkingId; + // Set as active process + this.activeProcId = thinkingId; - // Initialize with some starter thinking steps - this.addThinkingStep(thinkingId, { - type: 'observation', - content: `Starting analysis of the query: "${query}"` - }); + // Initialize with some starter thinking steps + this.addThinkingStep(thinkingId, { + type: 'observation', + content: `Starting analysis of the query: "${query}"` + }); - this.addThinkingStep(thinkingId, { - type: 'question', - content: `What are the key components of this query that need to be addressed?` - }); + this.addThinkingStep(thinkingId, { + type: 'question', + content: `What are the key components of this query that need to be addressed?` + }); - this.addThinkingStep(thinkingId, { - type: 'observation', - content: `Breaking down the query to understand its requirements and context.` - }); + this.addThinkingStep(thinkingId, { + type: 'observation', + content: `Breaking down the query to understand its requirements and context.` + }); - return thinkingId; - } - - /** - * Add a thinking step to a process - * - * @param processId The ID of the process to add to - * @param step The thinking step to add - * @returns The ID of the added step - */ - addThinkingStep( - processId: string, - step: Omit, - parentId?: string - ): string { - const process = this.processes[processId]; - - if (!process) { - throw new Error(`Thinking process ${processId} not found`); + return thinkingId; } - // Create full step with ID - const fullStep: ThinkingStep = { - id: `step_${Date.now()}_${Math.floor(Math.random() * 10000)}`, - ...step, - parentId - }; + /** + * Add a thinking step to a process + * + * @param processId The ID of the process to add to + * @param step The thinking step to add + * @returns The ID of the added step + */ + addThinkingStep( + processId: string, + step: Omit, + parentId?: string + ): string { + const process = this.processes[processId]; - // Add to process steps - process.steps.push(fullStep); - - // If this step has a parent, update the parent's children list - if (parentId) { - const parentStep = process.steps.find(s => s.id === parentId); - if (parentStep) { - if (!parentStep.children) { - parentStep.children = []; + if (!process) { + throw new Error(`Thinking process ${processId} not found`); } - parentStep.children.push(fullStep.id); - } - } - // Log the step addition with more detail - log.info(`Added thinking step to process ${processId}: [${step.type}] ${step.content.substring(0, 100)}...`); + // Create full step with ID + const fullStep: ThinkingStep = { + id: `step_${Date.now()}_${Math.floor(Math.random() * 10000)}`, + ...step, + parentId + }; - return fullStep.id; - } + // Add to process steps + process.steps.push(fullStep); - /** - * Complete the current thinking process - * - * @param processId The ID of the process to complete (defaults to active process) - * @returns The completed thinking process - */ - completeThinking(processId?: string): ThinkingProcess | null { - const id = processId || this.activeProcId; - - if (!id || !this.processes[id]) { - log.error(`Thinking process ${id} not found`); - return null; - } - - this.processes[id].status = 'completed'; - this.processes[id].endTime = Date.now(); - - if (id === this.activeProcId) { - this.activeProcId = undefined; - } - - return this.processes[id]; - } - - /** - * Get a thinking process by ID - */ - getThinkingProcess(processId: string): ThinkingProcess | null { - return this.processes[processId] || null; - } - - /** - * Get the active thinking process - */ - getActiveThinkingProcess(): ThinkingProcess | null { - if (!this.activeProcId) return null; - return this.processes[this.activeProcId] || null; - } - - /** - * Visualize the thinking process as HTML for display in the UI - * - * @param thinkingId The ID of the thinking process to visualize - * @returns HTML representation of the thinking process - */ - visualizeThinking(thinkingId: string): string { - log.info(`Visualizing thinking process: thinkingId=${thinkingId}`); - - const process = this.getThinkingProcess(thinkingId); - if (!process) { - log.info(`No thinking process found for id: ${thinkingId}`); - return "
No thinking process found
"; - } - - log.info(`Found thinking process with ${process.steps.length} steps for query: "${process.query.substring(0, 50)}..."`); - - let html = "
"; - html += `

Reasoning Process

`; - html += `
${process.query}
`; - - // Show overall time taken for the thinking process - const duration = process.endTime ? - Math.round((process.endTime - process.startTime) / 1000) : - Math.round((Date.now() - process.startTime) / 1000); - - html += `
Analysis took ${duration} seconds
`; - - // Create a more structured visualization with indentation for parent-child relationships - const renderStep = (step: ThinkingStep, level: number = 0) => { - const indent = level * 20; // 20px indentation per level - - let stepHtml = `
`; - - // Add an icon based on step type - const icon = this.getStepIcon(step.type); - stepHtml += ` `; - - // Add the step content - stepHtml += step.content; - - // Show confidence if available - if (step.metadata?.confidence) { - const confidence = Math.round((step.metadata.confidence as number) * 100); - stepHtml += ` (Confidence: ${confidence}%)`; - } - - // Show sources if available - if (step.sources && step.sources.length > 0) { - stepHtml += `
Sources: ${step.sources.join(', ')}
`; - } - - stepHtml += `
`; - - return stepHtml; - }; - - // Helper function to render a step and all its children recursively - const renderStepWithChildren = (stepId: string, level: number = 0) => { - const step = process.steps.find(s => s.id === stepId); - if (!step) return ''; - - let html = renderStep(step, level); - - if (step.children && step.children.length > 0) { - for (const childId of step.children) { - html += renderStepWithChildren(childId, level + 1); + // If this step has a parent, update the parent's children list + if (parentId) { + const parentStep = process.steps.find(s => s.id === parentId); + if (parentStep) { + if (!parentStep.children) { + parentStep.children = []; + } + parentStep.children.push(fullStep.id); + } } - } - return html; - }; + // Log the step addition with more detail + log.info(`Added thinking step to process ${processId}: [${step.type}] ${step.content.substring(0, 100)}...`); - // Render top-level steps and their children - const topLevelSteps = process.steps.filter(s => !s.parentId); - for (const step of topLevelSteps) { - html += renderStep(step); + return fullStep.id; + } - if (step.children && step.children.length > 0) { - for (const childId of step.children) { - html += renderStepWithChildren(childId, 1); + /** + * Complete the current thinking process + * + * @param processId The ID of the process to complete (defaults to active process) + * @returns The completed thinking process + */ + completeThinking(processId?: string): ThinkingProcess | null { + const id = processId || this.activeProcId; + + if (!id || !this.processes[id]) { + log.error(`Thinking process ${id} not found`); + return null; } - } + + this.processes[id].status = 'completed'; + this.processes[id].endTime = Date.now(); + + if (id === this.activeProcId) { + this.activeProcId = undefined; + } + + return this.processes[id]; } - html += "
"; - return html; - } - - /** - * Get an appropriate icon for a thinking step type - */ - private getStepIcon(type: string): string { - switch (type) { - case 'observation': - return 'bx-search'; - case 'hypothesis': - return 'bx-bulb'; - case 'evidence': - return 'bx-list-check'; - case 'conclusion': - return 'bx-check-circle'; - default: - return 'bx-message-square-dots'; - } - } - - /** - * Get a plain text summary of the thinking process - * - * @param thinkingId The ID of the thinking process to summarize - * @returns Text summary of the thinking process - */ - getThinkingSummary(thinkingId: string): string { - const process = this.getThinkingProcess(thinkingId); - if (!process) { - return "No thinking process available."; + /** + * Get a thinking process by ID + */ + getThinkingProcess(processId: string): ThinkingProcess | null { + return this.processes[processId] || null; } - let summary = `## Reasoning Process for Query: "${process.query}"\n\n`; - - // Group steps by type for better organization - const observations = process.steps.filter(s => s.type === 'observation'); - const questions = process.steps.filter(s => s.type === 'question'); - const hypotheses = process.steps.filter(s => s.type === 'hypothesis'); - const evidence = process.steps.filter(s => s.type === 'evidence'); - const conclusions = process.steps.filter(s => s.type === 'conclusion'); - - // Add observations - if (observations.length > 0) { - summary += "### Observations:\n"; - observations.forEach(step => { - summary += `- ${step.content}\n`; - }); - summary += "\n"; + /** + * Get the active thinking process + */ + getActiveThinkingProcess(): ThinkingProcess | null { + if (!this.activeProcId) return null; + return this.processes[this.activeProcId] || null; } - // Add questions - if (questions.length > 0) { - summary += "### Questions Considered:\n"; - questions.forEach(step => { - summary += `- ${step.content}\n`; - }); - summary += "\n"; + /** + * Visualize the thinking process as HTML for display in the UI + * + * @param thinkingId The ID of the thinking process to visualize + * @returns HTML representation of the thinking process + */ + visualizeThinking(thinkingId: string): string { + log.info(`Visualizing thinking process: thinkingId=${thinkingId}`); + + const process = this.getThinkingProcess(thinkingId); + if (!process) { + log.info(`No thinking process found for id: ${thinkingId}`); + return "
No thinking process found
"; + } + + log.info(`Found thinking process with ${process.steps.length} steps for query: "${process.query.substring(0, 50)}..."`); + + let html = "
"; + html += `

Reasoning Process

`; + html += `
${process.query}
`; + + // Show overall time taken for the thinking process + const duration = process.endTime ? + Math.round((process.endTime - process.startTime) / 1000) : + Math.round((Date.now() - process.startTime) / 1000); + + html += `
Analysis took ${duration} seconds
`; + + // Create a more structured visualization with indentation for parent-child relationships + const renderStep = (step: ThinkingStep, level: number = 0) => { + const indent = level * 20; // 20px indentation per level + + let stepHtml = `
`; + + // Add an icon based on step type + const icon = this.getStepIcon(step.type); + stepHtml += ` `; + + // Add the step content + stepHtml += step.content; + + // Show confidence if available + if (step.metadata?.confidence) { + const confidence = Math.round((step.metadata.confidence as number) * 100); + stepHtml += ` (Confidence: ${confidence}%)`; + } + + // Show sources if available + if (step.sources && step.sources.length > 0) { + stepHtml += `
Sources: ${step.sources.join(', ')}
`; + } + + stepHtml += `
`; + + return stepHtml; + }; + + // Helper function to render a step and all its children recursively + const renderStepWithChildren = (stepId: string, level: number = 0) => { + const step = process.steps.find(s => s.id === stepId); + if (!step) return ''; + + let html = renderStep(step, level); + + if (step.children && step.children.length > 0) { + for (const childId of step.children) { + html += renderStepWithChildren(childId, level + 1); + } + } + + return html; + }; + + // Render top-level steps and their children + const topLevelSteps = process.steps.filter(s => !s.parentId); + for (const step of topLevelSteps) { + html += renderStep(step); + + if (step.children && step.children.length > 0) { + for (const childId of step.children) { + html += renderStepWithChildren(childId, 1); + } + } + } + + html += "
"; + return html; } - // Add hypotheses - if (hypotheses.length > 0) { - summary += "### Hypotheses:\n"; - hypotheses.forEach(step => { - summary += `- ${step.content}\n`; - }); - summary += "\n"; + /** + * Get an appropriate icon for a thinking step type + */ + private getStepIcon(type: string): string { + switch (type) { + case 'observation': + return 'bx-search'; + case 'hypothesis': + return 'bx-bulb'; + case 'evidence': + return 'bx-list-check'; + case 'conclusion': + return 'bx-check-circle'; + default: + return 'bx-message-square-dots'; + } } - // Add evidence - if (evidence.length > 0) { - summary += "### Evidence Gathered:\n"; - evidence.forEach(step => { - summary += `- ${step.content}\n`; - }); - summary += "\n"; + /** + * Get a plain text summary of the thinking process + * + * @param thinkingId The ID of the thinking process to summarize + * @returns Text summary of the thinking process + */ + getThinkingSummary(thinkingId: string): string { + const process = this.getThinkingProcess(thinkingId); + if (!process) { + log.error(`No thinking process found for id: ${thinkingId}`); + return "No thinking process available."; + } + + let summary = `## Reasoning Process for Query: "${process.query}"\n\n`; + + // Group steps by type for better organization + const observations = process.steps.filter(s => s.type === 'observation'); + const questions = process.steps.filter(s => s.type === 'question'); + const hypotheses = process.steps.filter(s => s.type === 'hypothesis'); + const evidence = process.steps.filter(s => s.type === 'evidence'); + const conclusions = process.steps.filter(s => s.type === 'conclusion'); + + log.info(`Generating thinking summary with: ${observations.length} observations, ${questions.length} questions, ${hypotheses.length} hypotheses, ${evidence.length} evidence, ${conclusions.length} conclusions`); + + // Add observations + if (observations.length > 0) { + summary += "### Observations:\n"; + observations.forEach(step => { + summary += `- ${step.content}\n`; + }); + summary += "\n"; + } + + // Add questions + if (questions.length > 0) { + summary += "### Questions Considered:\n"; + questions.forEach(step => { + summary += `- ${step.content}\n`; + }); + summary += "\n"; + } + + // Add hypotheses + if (hypotheses.length > 0) { + summary += "### Hypotheses:\n"; + hypotheses.forEach(step => { + summary += `- ${step.content}\n`; + }); + summary += "\n"; + } + + // Add evidence + if (evidence.length > 0) { + summary += "### Evidence Gathered:\n"; + evidence.forEach(step => { + summary += `- ${step.content}\n`; + }); + summary += "\n"; + } + + // Add conclusions + if (conclusions.length > 0) { + summary += "### Conclusions:\n"; + conclusions.forEach(step => { + summary += `- ${step.content}\n`; + }); + summary += "\n"; + } + + log.info(`Generated thinking summary with ${summary.length} characters`); + return summary; } - // Add conclusions - if (conclusions.length > 0) { - summary += "### Conclusions:\n"; - conclusions.forEach(step => { - summary += `- ${step.content}\n`; - }); - summary += "\n"; + /** + * Reset the active thinking process + */ + resetActiveThinking(): void { + this.activeProcId = undefined; } - return summary; - } - - /** - * Reset the active thinking process - */ - resetActiveThinking(): void { - this.activeProcId = undefined; - } - - /** - * Generate a unique ID for a thinking process - */ - private generateProcessId(): string { - return `thinking_${Date.now()}_${ContextualThinkingTool.thinkingCounter++}`; - } - - /** - * Generate a unique ID for a thinking step - */ - private generateStepId(): string { - return `step_${Date.now()}_${ContextualThinkingTool.stepCounter++}`; - } - - /** - * Format duration between two timestamps - */ - private formatDuration(start: number, end: number): string { - const durationMs = end - start; - if (durationMs < 1000) { - return `${durationMs}ms`; - } else if (durationMs < 60000) { - return `${Math.round(durationMs / 1000)}s`; - } else { - return `${Math.round(durationMs / 60000)}m ${Math.round((durationMs % 60000) / 1000)}s`; + /** + * Generate a unique ID for a thinking process + */ + private generateProcessId(): string { + return `thinking_${Date.now()}_${ContextualThinkingTool.thinkingCounter++}`; } - } - /** - * Recursively render a step and its children - */ - private renderStepTree(step: ThinkingStep, allSteps: ThinkingStep[]): string { - const typeIcons: Record = { - 'observation': '🔍', - 'hypothesis': '🤔', - 'question': '❓', - 'evidence': '📋', - 'conclusion': '✅' - }; + /** + * Generate a unique ID for a thinking step + */ + private generateStepId(): string { + return `step_${Date.now()}_${ContextualThinkingTool.stepCounter++}`; + } - const icon = typeIcons[step.type] || '•'; - const confidenceDisplay = step.confidence !== undefined - ? `${Math.round(step.confidence * 100)}%` - : ''; + /** + * Format duration between two timestamps + */ + private formatDuration(start: number, end: number): string { + const durationMs = end - start; + if (durationMs < 1000) { + return `${durationMs}ms`; + } else if (durationMs < 60000) { + return `${Math.round(durationMs / 1000)}s`; + } else { + return `${Math.round(durationMs / 60000)}m ${Math.round((durationMs % 60000) / 1000)}s`; + } + } - let html = ` + /** + * Recursively render a step and its children + */ + private renderStepTree(step: ThinkingStep, allSteps: ThinkingStep[]): string { + const typeIcons: Record = { + 'observation': '🔍', + 'hypothesis': '🤔', + 'question': '❓', + 'evidence': '📋', + 'conclusion': '✅' + }; + + const icon = typeIcons[step.type] || '•'; + const confidenceDisplay = step.confidence !== undefined + ? `${Math.round(step.confidence * 100)}%` + : ''; + + let html = `
${icon} @@ -411,28 +415,28 @@ export class ContextualThinkingTool {
${step.content}
`; - // Add sources if available - if (step.sources && step.sources.length > 0) { - html += `
Sources: ${step.sources.join(', ')}
`; - } - - // Recursively render children - if (step.children && step.children.length > 0) { - html += `
`; - - for (const childId of step.children) { - const childStep = allSteps.find(s => s.id === childId); - if (childStep) { - html += this.renderStepTree(childStep, allSteps); + // Add sources if available + if (step.sources && step.sources.length > 0) { + html += `
Sources: ${step.sources.join(', ')}
`; } - } - html += `
`; + // Recursively render children + if (step.children && step.children.length > 0) { + html += `
`; + + for (const childId of step.children) { + const childStep = allSteps.find(s => s.id === childId); + if (childStep) { + html += this.renderStepTree(childStep, allSteps); + } + } + + html += `
`; + } + + html += `
`; + return html; } - - html += `
`; - return html; - } } export default ContextualThinkingTool; diff --git a/src/services/llm/agent_tools/note_navigator_tool.ts b/src/services/llm/agent_tools/note_navigator_tool.ts index 0d00c9716..115447431 100644 --- a/src/services/llm/agent_tools/note_navigator_tool.ts +++ b/src/services/llm/agent_tools/note_navigator_tool.ts @@ -443,11 +443,32 @@ export class NoteNavigatorTool { try { log.info(`Getting note structure for note ${noteId}`); + // Special handling for 'root' or other special notes + if (noteId === 'root' || !noteId) { + log.info('Using root as the special note for structure'); + return { + noteId: 'root', + title: 'Root', + type: 'root', + childCount: 0, // We don't know how many direct children root has + attributes: [], + parentPath: [] + }; + } + // Get the note from becca const note = becca.notes[noteId]; if (!note) { - throw new Error(`Note ${noteId} not found`); + log.error(`Note ${noteId} not found in becca.notes`); + return { + noteId, + title: 'Unknown', + type: 'unknown', + childCount: 0, + attributes: [], + parentPath: [] + }; } // Get child notes count diff --git a/src/services/llm/agent_tools/query_decomposition_tool.ts b/src/services/llm/agent_tools/query_decomposition_tool.ts index 36a594b56..9a2a545f6 100644 --- a/src/services/llm/agent_tools/query_decomposition_tool.ts +++ b/src/services/llm/agent_tools/query_decomposition_tool.ts @@ -15,459 +15,480 @@ import log from '../../log.js'; export interface SubQuery { - id: string; - text: string; - reason: string; - isAnswered: boolean; - answer?: string; + id: string; + text: string; + reason: string; + isAnswered: boolean; + answer?: string; } export interface DecomposedQuery { - originalQuery: string; - subQueries: SubQuery[]; - status: 'pending' | 'in_progress' | 'completed'; - complexity: number; + originalQuery: string; + subQueries: SubQuery[]; + status: 'pending' | 'in_progress' | 'completed'; + complexity: number; } export class QueryDecompositionTool { - private static queryCounter: number = 0; + private static queryCounter: number = 0; - /** - * Break down a complex query into smaller, more manageable sub-queries - * - * @param query The original user query - * @param context Optional context about the current note being viewed - * @returns A decomposed query object with sub-queries - */ - decomposeQuery(query: string, context?: string): DecomposedQuery { - try { - // Log the decomposition attempt for tracking - log.info(`Decomposing query: "${query.substring(0, 100)}..."`); + /** + * Break down a complex query into smaller, more manageable sub-queries + * + * @param query The original user query + * @param context Optional context about the current note being viewed + * @returns A decomposed query object with sub-queries + */ + decomposeQuery(query: string, context?: string): DecomposedQuery { + try { + // Log the decomposition attempt for tracking + log.info(`Decomposing query: "${query.substring(0, 100)}..."`); - // Assess query complexity to determine if decomposition is needed - const complexity = this.assessQueryComplexity(query); - log.info(`Query complexity assessment: ${complexity}/10`); + if (!query || query.trim().length === 0) { + log.info("Query decomposition called with empty query"); + return { + originalQuery: query, + subQueries: [], + status: 'pending', + complexity: 0 + }; + } - // For simple queries, just return the original as a single sub-query - // Use a lower threshold (2 instead of 3) to decompose more queries - if (complexity < 2) { - log.info(`Query is simple (complexity ${complexity}), returning as single sub-query`); - return { - originalQuery: query, - subQueries: [{ - id: this.generateSubQueryId(), - text: query, - reason: 'Direct question that can be answered without decomposition', - isAnswered: false - }], - status: 'pending', - complexity - }; - } + // Assess query complexity to determine if decomposition is needed + const complexity = this.assessQueryComplexity(query); + log.info(`Query complexity assessment: ${complexity}/10`); - // For complex queries, perform decomposition - const subQueries = this.createSubQueries(query, context); - log.info(`Decomposed query into ${subQueries.length} sub-queries`); + // For simple queries, just return the original as a single sub-query + // Use a lower threshold (2 instead of 3) to decompose more queries + if (complexity < 2) { + log.info(`Query is simple (complexity ${complexity}), returning as single sub-query`); - // Log the sub-queries for better visibility - subQueries.forEach((sq, index) => { - log.info(`Sub-query ${index + 1}: "${sq.text}" - Reason: ${sq.reason}`); - }); + const mainSubQuery = { + id: this.generateSubQueryId(), + text: query, + reason: 'Direct question that can be answered without decomposition', + isAnswered: false + }; - return { - originalQuery: query, - subQueries, - status: 'pending', - complexity - }; - } catch (error: any) { - log.error(`Error decomposing query: ${error.message}`); + // Still add a generic exploration query to get some related content + const genericQuery = { + id: this.generateSubQueryId(), + text: `Information related to ${query}`, + reason: "Generic exploration to find related content", + isAnswered: false + }; - // Fallback to treating it as a simple query - return { - originalQuery: query, - subQueries: [{ - id: this.generateSubQueryId(), - text: query, - reason: 'Error in decomposition, treating as simple query', - isAnswered: false - }], - status: 'pending', - complexity: 1 - }; - } - } + return { + originalQuery: query, + subQueries: [mainSubQuery, genericQuery], + status: 'pending', + complexity + }; + } - /** - * Update a sub-query with its answer - * - * @param decomposedQuery The decomposed query object - * @param subQueryId The ID of the sub-query to update - * @param answer The answer to the sub-query - * @returns The updated decomposed query - */ - updateSubQueryAnswer( - decomposedQuery: DecomposedQuery, - subQueryId: string, - answer: string - ): DecomposedQuery { - const updatedSubQueries = decomposedQuery.subQueries.map(sq => { - if (sq.id === subQueryId) { - return { - ...sq, - answer, - isAnswered: true - }; - } - return sq; - }); + // For complex queries, perform decomposition + const subQueries = this.createSubQueries(query, context); + log.info(`Decomposed query into ${subQueries.length} sub-queries`); - // Check if all sub-queries are answered - const allAnswered = updatedSubQueries.every(sq => sq.isAnswered); + // Log the sub-queries for better visibility + subQueries.forEach((sq, index) => { + log.info(`Sub-query ${index + 1}: "${sq.text}" - Reason: ${sq.reason}`); + }); - return { - ...decomposedQuery, - subQueries: updatedSubQueries, - status: allAnswered ? 'completed' : 'in_progress' - }; - } + return { + originalQuery: query, + subQueries, + status: 'pending', + complexity + }; + } catch (error: any) { + log.error(`Error decomposing query: ${error.message}`); - /** - * Synthesize all sub-query answers into a comprehensive response - * - * @param decomposedQuery The decomposed query with all sub-queries answered - * @returns A synthesized answer to the original query - */ - synthesizeAnswer(decomposedQuery: DecomposedQuery): string { - try { - // Ensure all sub-queries are answered - if (!decomposedQuery.subQueries.every(sq => sq.isAnswered)) { - return "Cannot synthesize answer - not all sub-queries have been answered."; - } - - // For simple queries with just one sub-query, return the answer directly - if (decomposedQuery.subQueries.length === 1) { - return decomposedQuery.subQueries[0].answer || ""; - } - - // For complex queries, build a structured response that references each sub-answer - let synthesized = `Answer to: "${decomposedQuery.originalQuery}"\n\n`; - - // Group by themes if there are many sub-queries - if (decomposedQuery.subQueries.length > 3) { - // Here we would ideally group related sub-queries, but for now we'll just present them in order - synthesized += "Based on the information gathered:\n\n"; - - for (const sq of decomposedQuery.subQueries) { - synthesized += `${sq.answer}\n\n`; + // Fallback to treating it as a simple query + return { + originalQuery: query, + subQueries: [{ + id: this.generateSubQueryId(), + text: query, + reason: 'Error in decomposition, treating as simple query', + isAnswered: false + }], + status: 'pending', + complexity: 1 + }; } - } else { - // For fewer sub-queries, present each one with its question - for (const sq of decomposedQuery.subQueries) { - synthesized += `${sq.answer}\n\n`; - } - } - - return synthesized.trim(); - } catch (error: any) { - log.error(`Error synthesizing answer: ${error.message}`); - return "Error synthesizing the final answer."; - } - } - - /** - * Generate a status report on the progress of answering a complex query - * - * @param decomposedQuery The decomposed query - * @returns A status report string - */ - getQueryStatus(decomposedQuery: DecomposedQuery): string { - const answeredCount = decomposedQuery.subQueries.filter(sq => sq.isAnswered).length; - const totalCount = decomposedQuery.subQueries.length; - - let status = `Progress: ${answeredCount}/${totalCount} sub-queries answered\n\n`; - - for (const sq of decomposedQuery.subQueries) { - status += `${sq.isAnswered ? '✓' : '○'} ${sq.text}\n`; - if (sq.isAnswered) { - status += ` Answer: ${this.truncateText(sq.answer || "", 100)}\n`; - } } - return status; - } - - /** - * Assess the complexity of a query on a scale of 1-10 - * This helps determine how many sub-queries are needed - * - * @param query The query to assess - * @returns A complexity score from 1-10 - */ - assessQueryComplexity(query: string): number { - // Count the number of question marks as a basic indicator - const questionMarkCount = (query.match(/\?/g) || []).length; - - // Count potential sub-questions based on question words - const questionWords = ['what', 'how', 'why', 'where', 'when', 'who', 'which']; - const questionWordMatches = questionWords.map(word => { - const regex = new RegExp(`\\b${word}\\b`, 'gi'); - return (query.match(regex) || []).length; - }); - - const questionWordCount = questionWordMatches.reduce((sum, count) => sum + count, 0); - - // Look for conjunctions which might join multiple questions - const conjunctionCount = (query.match(/\b(and|or|but|as well as)\b/gi) || []).length; - - // Look for complex requirements - const comparisonCount = (query.match(/\b(compare|versus|vs|difference|similarities?)\b/gi) || []).length; - const analysisCount = (query.match(/\b(analyze|examine|investigate|explore|explain|discuss)\b/gi) || []).length; - - // Calculate base complexity - let complexity = 1; - - // Add for multiple questions - complexity += Math.min(2, questionMarkCount); - - // Add for question words beyond the first one - complexity += Math.min(2, Math.max(0, questionWordCount - 1)); - - // Add for conjunctions that might join questions - complexity += Math.min(2, conjunctionCount); - - // Add for comparative/analytical requirements - complexity += Math.min(2, comparisonCount + analysisCount); - - // Add for overall length/complexity - if (query.length > 100) complexity += 1; - if (query.length > 200) complexity += 1; - - // Ensure we stay in the 1-10 range - return Math.max(1, Math.min(10, complexity)); - } - - /** - * Generate a unique ID for a sub-query - */ - private generateSubQueryId(): string { - return `sq_${Date.now()}_${QueryDecompositionTool.queryCounter++}`; - } - - /** - * Create sub-queries based on the original query and optional context - */ - private createSubQueries(query: string, context?: string): SubQuery[] { - const subQueries: SubQuery[] = []; - - // Use context to enhance sub-query generation if available - if (context) { - log.info(`Using context to enhance sub-query generation`); - - // Add context-specific questions - subQueries.push({ - id: this.generateSubQueryId(), - text: `What key information in the current note relates to: "${query}"?`, - reason: 'Identifying directly relevant information in the current context', - isAnswered: false - }); - } - - // 1. Look for multiple question marks - const questionSplit = query.split(/\?/).filter(q => q.trim().length > 0); - - if (questionSplit.length > 1) { - // Multiple distinct questions detected - for (let i = 0; i < questionSplit.length; i++) { - const text = questionSplit[i].trim() + '?'; - subQueries.push({ - id: this.generateSubQueryId(), - text, - reason: `Separate question ${i+1} detected in the original query`, - isAnswered: false + /** + * Update a sub-query with its answer + * + * @param decomposedQuery The decomposed query object + * @param subQueryId The ID of the sub-query to update + * @param answer The answer to the sub-query + * @returns The updated decomposed query + */ + updateSubQueryAnswer( + decomposedQuery: DecomposedQuery, + subQueryId: string, + answer: string + ): DecomposedQuery { + const updatedSubQueries = decomposedQuery.subQueries.map(sq => { + if (sq.id === subQueryId) { + return { + ...sq, + answer, + isAnswered: true + }; + } + return sq; }); - } - // Also add a synthesis question - subQueries.push({ - id: this.generateSubQueryId(), - text: `How do the answers to these questions relate to each other in the context of the original query?`, - reason: 'Synthesizing information from multiple questions', - isAnswered: false - }); + // Check if all sub-queries are answered + const allAnswered = updatedSubQueries.every(sq => sq.isAnswered); - return subQueries; + return { + ...decomposedQuery, + subQueries: updatedSubQueries, + status: allAnswered ? 'completed' : 'in_progress' + }; } - // 2. Look for "and", "or", etc. connecting potentially separate questions - const conjunctions = [ - { regex: /\b(compare|versus|vs\.?|difference between|similarities between)\b/i, label: 'comparison' }, - { regex: /\b(list|enumerate)\b/i, label: 'listing' }, - { regex: /\b(analyze|examine|investigate|explore)\b/i, label: 'analysis' }, - { regex: /\b(explain|why)\b/i, label: 'explanation' }, - { regex: /\b(how to|steps to|process of)\b/i, label: 'procedure' } - ]; + /** + * Synthesize all sub-query answers into a comprehensive response + * + * @param decomposedQuery The decomposed query with all sub-queries answered + * @returns A synthesized answer to the original query + */ + synthesizeAnswer(decomposedQuery: DecomposedQuery): string { + try { + // Ensure all sub-queries are answered + if (!decomposedQuery.subQueries.every(sq => sq.isAnswered)) { + return "Cannot synthesize answer - not all sub-queries have been answered."; + } - // Check for comparison queries - these often need multiple sub-queries - for (const conj of conjunctions) { - if (conj.regex.test(query)) { - if (conj.label === 'comparison') { - // For comparisons, we need to research each item, then compare them - const comparisonMatch = query.match(/\b(compare|versus|vs\.?|difference between|similarities between)\s+(.+?)\s+(and|with|to)\s+(.+?)(\?|$)/i); + // For simple queries with just one sub-query, return the answer directly + if (decomposedQuery.subQueries.length === 1) { + return decomposedQuery.subQueries[0].answer || ""; + } - if (comparisonMatch) { - const item1 = comparisonMatch[2].trim(); - const item2 = comparisonMatch[4].trim(); + // For complex queries, build a structured response that references each sub-answer + let synthesized = `Answer to: "${decomposedQuery.originalQuery}"\n\n`; + // Group by themes if there are many sub-queries + if (decomposedQuery.subQueries.length > 3) { + // Here we would ideally group related sub-queries, but for now we'll just present them in order + synthesized += "Based on the information gathered:\n\n"; + + for (const sq of decomposedQuery.subQueries) { + synthesized += `${sq.answer}\n\n`; + } + } else { + // For fewer sub-queries, present each one with its question + for (const sq of decomposedQuery.subQueries) { + synthesized += `${sq.answer}\n\n`; + } + } + + return synthesized.trim(); + } catch (error: any) { + log.error(`Error synthesizing answer: ${error.message}`); + return "Error synthesizing the final answer."; + } + } + + /** + * Generate a status report on the progress of answering a complex query + * + * @param decomposedQuery The decomposed query + * @returns A status report string + */ + getQueryStatus(decomposedQuery: DecomposedQuery): string { + const answeredCount = decomposedQuery.subQueries.filter(sq => sq.isAnswered).length; + const totalCount = decomposedQuery.subQueries.length; + + let status = `Progress: ${answeredCount}/${totalCount} sub-queries answered\n\n`; + + for (const sq of decomposedQuery.subQueries) { + status += `${sq.isAnswered ? '✓' : '○'} ${sq.text}\n`; + if (sq.isAnswered) { + status += ` Answer: ${this.truncateText(sq.answer || "", 100)}\n`; + } + } + + return status; + } + + /** + * Assess the complexity of a query on a scale of 1-10 + * This helps determine how many sub-queries are needed + * + * @param query The query to assess + * @returns A complexity score from 1-10 + */ + assessQueryComplexity(query: string): number { + // Count the number of question marks as a basic indicator + const questionMarkCount = (query.match(/\?/g) || []).length; + + // Count potential sub-questions based on question words + const questionWords = ['what', 'how', 'why', 'where', 'when', 'who', 'which']; + const questionWordMatches = questionWords.map(word => { + const regex = new RegExp(`\\b${word}\\b`, 'gi'); + return (query.match(regex) || []).length; + }); + + const questionWordCount = questionWordMatches.reduce((sum, count) => sum + count, 0); + + // Look for conjunctions which might join multiple questions + const conjunctionCount = (query.match(/\b(and|or|but|as well as)\b/gi) || []).length; + + // Look for complex requirements + const comparisonCount = (query.match(/\b(compare|versus|vs|difference|similarities?)\b/gi) || []).length; + const analysisCount = (query.match(/\b(analyze|examine|investigate|explore|explain|discuss)\b/gi) || []).length; + + // Calculate base complexity + let complexity = 1; + + // Add for multiple questions + complexity += Math.min(2, questionMarkCount); + + // Add for question words beyond the first one + complexity += Math.min(2, Math.max(0, questionWordCount - 1)); + + // Add for conjunctions that might join questions + complexity += Math.min(2, conjunctionCount); + + // Add for comparative/analytical requirements + complexity += Math.min(2, comparisonCount + analysisCount); + + // Add for overall length/complexity + if (query.length > 100) complexity += 1; + if (query.length > 200) complexity += 1; + + // Ensure we stay in the 1-10 range + return Math.max(1, Math.min(10, complexity)); + } + + /** + * Generate a unique ID for a sub-query + */ + private generateSubQueryId(): string { + return `sq_${Date.now()}_${QueryDecompositionTool.queryCounter++}`; + } + + /** + * Create sub-queries based on the original query and optional context + */ + private createSubQueries(query: string, context?: string): SubQuery[] { + const subQueries: SubQuery[] = []; + + // Use context to enhance sub-query generation if available + if (context) { + log.info(`Using context to enhance sub-query generation`); + + // Add context-specific questions subQueries.push({ - id: this.generateSubQueryId(), - text: `What are the key characteristics of ${item1}?`, - reason: `Need to understand ${item1} for the comparison`, - isAnswered: false + id: this.generateSubQueryId(), + text: `What key information in the current note relates to: "${query}"?`, + reason: 'Identifying directly relevant information in the current context', + isAnswered: false }); + } - subQueries.push({ - id: this.generateSubQueryId(), - text: `What are the key characteristics of ${item2}?`, - reason: `Need to understand ${item2} for the comparison`, - isAnswered: false - }); + // 1. Look for multiple question marks + const questionSplit = query.split(/\?/).filter(q => q.trim().length > 0); - subQueries.push({ - id: this.generateSubQueryId(), - text: `What are the main differences between ${item1} and ${item2}?`, - reason: 'Understanding key differences', - isAnswered: false - }); + if (questionSplit.length > 1) { + // Multiple distinct questions detected + for (let i = 0; i < questionSplit.length; i++) { + const text = questionSplit[i].trim() + '?'; + subQueries.push({ + id: this.generateSubQueryId(), + text, + reason: `Separate question ${i + 1} detected in the original query`, + isAnswered: false + }); + } + // Also add a synthesis question subQueries.push({ - id: this.generateSubQueryId(), - text: `What are the main similarities between ${item1} and ${item2}?`, - reason: 'Understanding key similarities', - isAnswered: false - }); - - subQueries.push({ - id: this.generateSubQueryId(), - text: `What practical implications do these differences and similarities have?`, - reason: 'Understanding practical significance of the comparison', - isAnswered: false + id: this.generateSubQueryId(), + text: `How do the answers to these questions relate to each other in the context of the original query?`, + reason: 'Synthesizing information from multiple questions', + isAnswered: false }); return subQueries; - } } - } - } - // 3. For complex questions without clear separation, create topic-based sub-queries - // Lowered the threshold to process more queries this way - if (query.length > 50) { - // Extract potential key topics from the query - const words = query.toLowerCase().split(/\W+/).filter(w => - w.length > 3 && - !['what', 'when', 'where', 'which', 'with', 'would', 'could', 'should', 'have', 'this', 'that', 'there', 'their'].includes(w) - ); + // 2. Look for "and", "or", etc. connecting potentially separate questions + const conjunctions = [ + { regex: /\b(compare|versus|vs\.?|difference between|similarities between)\b/i, label: 'comparison' }, + { regex: /\b(list|enumerate)\b/i, label: 'listing' }, + { regex: /\b(analyze|examine|investigate|explore)\b/i, label: 'analysis' }, + { regex: /\b(explain|why)\b/i, label: 'explanation' }, + { regex: /\b(how to|steps to|process of)\b/i, label: 'procedure' } + ]; - // Count word frequencies - const wordFrequency: Record = {}; - for (const word of words) { - wordFrequency[word] = (wordFrequency[word] || 0) + 1; - } + // Check for comparison queries - these often need multiple sub-queries + for (const conj of conjunctions) { + if (conj.regex.test(query)) { + if (conj.label === 'comparison') { + // For comparisons, we need to research each item, then compare them + const comparisonMatch = query.match(/\b(compare|versus|vs\.?|difference between|similarities between)\s+(.+?)\s+(and|with|to)\s+(.+?)(\?|$)/i); - // Get top frequent words - const topWords = Object.entries(wordFrequency) - .sort((a, b) => b[1] - a[1]) - .slice(0, 4) // Increased from 3 to 4 - .map(entry => entry[0]); + if (comparisonMatch) { + const item1 = comparisonMatch[2].trim(); + const item2 = comparisonMatch[4].trim(); - if (topWords.length > 0) { - // Create factual sub-query - subQueries.push({ - id: this.generateSubQueryId(), - text: `What are the key facts about ${topWords.join(' and ')} relevant to this question?`, - reason: 'Gathering basic information about main topics', - isAnswered: false - }); + subQueries.push({ + id: this.generateSubQueryId(), + text: `What are the key characteristics of ${item1}?`, + reason: `Need to understand ${item1} for the comparison`, + isAnswered: false + }); - // Add individual queries for each key topic - topWords.forEach(word => { - subQueries.push({ - id: this.generateSubQueryId(), - text: `What specific details about "${word}" are most relevant to the query?`, - reason: `Detailed exploration of the "${word}" concept`, - isAnswered: false - }); - }); + subQueries.push({ + id: this.generateSubQueryId(), + text: `What are the key characteristics of ${item2}?`, + reason: `Need to understand ${item2} for the comparison`, + isAnswered: false + }); - // Create relationship sub-query if multiple top words - if (topWords.length > 1) { - for (let i = 0; i < topWords.length; i++) { - for (let j = i + 1; j < topWords.length; j++) { - subQueries.push({ - id: this.generateSubQueryId(), - text: `How do ${topWords[i]} and ${topWords[j]} relate to each other?`, - reason: `Understanding relationship between ${topWords[i]} and ${topWords[j]}`, - isAnswered: false - }); + subQueries.push({ + id: this.generateSubQueryId(), + text: `What are the main differences between ${item1} and ${item2}?`, + reason: 'Understanding key differences', + isAnswered: false + }); + + subQueries.push({ + id: this.generateSubQueryId(), + text: `What are the main similarities between ${item1} and ${item2}?`, + reason: 'Understanding key similarities', + isAnswered: false + }); + + subQueries.push({ + id: this.generateSubQueryId(), + text: `What practical implications do these differences and similarities have?`, + reason: 'Understanding practical significance of the comparison', + isAnswered: false + }); + + return subQueries; + } + } } - } } - // Add a "what else" query to ensure comprehensive coverage + // 3. For complex questions without clear separation, create topic-based sub-queries + // Lowered the threshold to process more queries this way + if (query.length > 50) { + // Extract potential key topics from the query + const words = query.toLowerCase().split(/\W+/).filter(w => + w.length > 3 && + !['what', 'when', 'where', 'which', 'with', 'would', 'could', 'should', 'have', 'this', 'that', 'there', 'their'].includes(w) + ); + + // Count word frequencies + const wordFrequency: Record = {}; + for (const word of words) { + wordFrequency[word] = (wordFrequency[word] || 0) + 1; + } + + // Get top frequent words + const topWords = Object.entries(wordFrequency) + .sort((a, b) => b[1] - a[1]) + .slice(0, 4) // Increased from 3 to 4 + .map(entry => entry[0]); + + if (topWords.length > 0) { + // Create factual sub-query + subQueries.push({ + id: this.generateSubQueryId(), + text: `What are the key facts about ${topWords.join(' and ')} relevant to this question?`, + reason: 'Gathering basic information about main topics', + isAnswered: false + }); + + // Add individual queries for each key topic + topWords.forEach(word => { + subQueries.push({ + id: this.generateSubQueryId(), + text: `What specific details about "${word}" are most relevant to the query?`, + reason: `Detailed exploration of the "${word}" concept`, + isAnswered: false + }); + }); + + // Create relationship sub-query if multiple top words + if (topWords.length > 1) { + for (let i = 0; i < topWords.length; i++) { + for (let j = i + 1; j < topWords.length; j++) { + subQueries.push({ + id: this.generateSubQueryId(), + text: `How do ${topWords[i]} and ${topWords[j]} relate to each other?`, + reason: `Understanding relationship between ${topWords[i]} and ${topWords[j]}`, + isAnswered: false + }); + } + } + } + + // Add a "what else" query to ensure comprehensive coverage + subQueries.push({ + id: this.generateSubQueryId(), + text: `What other important aspects should be considered about this topic that might not be immediately obvious?`, + reason: 'Exploring non-obvious but relevant information', + isAnswered: false + }); + + // Add the original query as the final synthesizing question + subQueries.push({ + id: this.generateSubQueryId(), + text: query, + reason: 'Original question to be answered after gathering information', + isAnswered: false + }); + + return subQueries; + } + } + + // Fallback: If we can't meaningfully decompose, just use the original query + // But also add some generic exploration questions subQueries.push({ - id: this.generateSubQueryId(), - text: `What other important aspects should be considered about this topic that might not be immediately obvious?`, - reason: 'Exploring non-obvious but relevant information', - isAnswered: false + id: this.generateSubQueryId(), + text: query, + reason: 'Primary question', + isAnswered: false }); - // Add the original query as the final synthesizing question + // Add generic exploration questions even for "simple" queries subQueries.push({ - id: this.generateSubQueryId(), - text: query, - reason: 'Original question to be answered after gathering information', - isAnswered: false + id: this.generateSubQueryId(), + text: `What background information is helpful to understand this query better?`, + reason: 'Gathering background context', + isAnswered: false + }); + + subQueries.push({ + id: this.generateSubQueryId(), + text: `What related concepts might be important to consider?`, + reason: 'Exploring related concepts', + isAnswered: false }); return subQueries; - } } - // Fallback: If we can't meaningfully decompose, just use the original query - // But also add some generic exploration questions - subQueries.push({ - id: this.generateSubQueryId(), - text: query, - reason: 'Primary question', - isAnswered: false - }); - - // Add generic exploration questions even for "simple" queries - subQueries.push({ - id: this.generateSubQueryId(), - text: `What background information is helpful to understand this query better?`, - reason: 'Gathering background context', - isAnswered: false - }); - - subQueries.push({ - id: this.generateSubQueryId(), - text: `What related concepts might be important to consider?`, - reason: 'Exploring related concepts', - isAnswered: false - }); - - return subQueries; - } - - /** - * Truncate text to a maximum length with ellipsis - */ - private truncateText(text: string, maxLength: number): string { - if (text.length <= maxLength) return text; - return text.substring(0, maxLength - 3) + '...'; - } + /** + * Truncate text to a maximum length with ellipsis + */ + private truncateText(text: string, maxLength: number): string { + if (text.length <= maxLength) return text; + return text.substring(0, maxLength - 3) + '...'; + } } export default QueryDecompositionTool; diff --git a/src/services/llm/context/modules/context_service.ts b/src/services/llm/context/modules/context_service.ts index 00fbac10d..2cc7102f8 100644 --- a/src/services/llm/context/modules/context_service.ts +++ b/src/services/llm/context/modules/context_service.ts @@ -147,22 +147,24 @@ export class ContextService { // Step 4: Add agent tools context with thinking process if requested let enhancedContext = context; - if (contextNoteId) { - try { - const agentContext = await this.getAgentToolsContext( - contextNoteId, - userQuestion, - showThinking, - relevantNotes - ); + try { + // Pass 'root' as the default noteId when no specific note is selected + const noteIdToUse = contextNoteId || 'root'; + log.info(`Calling getAgentToolsContext with noteId=${noteIdToUse}, showThinking=${showThinking}`); - if (agentContext) { - enhancedContext = enhancedContext + "\n\n" + agentContext; - } - } catch (error) { - log.error(`Error getting agent tools context: ${error}`); - // Continue with the basic context + const agentContext = await this.getAgentToolsContext( + noteIdToUse, + userQuestion, + showThinking, + relevantNotes + ); + + if (agentContext) { + enhancedContext = enhancedContext + "\n\n" + agentContext; } + } catch (error) { + log.error(`Error getting agent tools context: ${error}`); + // Continue with the basic context } return { @@ -402,8 +404,13 @@ export class ContextService { // Add thinking process if requested if (showThinking) { + log.info(`Including thinking process in context (showThinking=true)`); agentContext += `\n## Reasoning Process\n`; - agentContext += contextualThinkingTool.getThinkingSummary(thinkingId); + const thinkingSummary = contextualThinkingTool.getThinkingSummary(thinkingId); + log.info(`Thinking summary length: ${thinkingSummary.length} characters`); + agentContext += thinkingSummary; + } else { + log.info(`Skipping thinking process in context (showThinking=false)`); } // Log stats about the context