From 0d4b6a71fcee4905d9b5104d10c789ff2581fca1 Mon Sep 17 00:00:00 2001 From: perf3ct Date: Wed, 19 Mar 2025 20:09:18 +0000 Subject: [PATCH] update agent tools --- src/public/translations/en/translation.json | 1 - .../agent_tools/contextual_thinking_tool.ts | 218 ++++++++++++---- .../llm/agent_tools/note_navigator_tool.ts | 243 ++++++++++++++---- .../agent_tools/query_decomposition_tool.ts | 109 +++++++- .../llm/agent_tools/vector_search_tool.ts | 78 ++++++ src/services/llm/ai_service_manager.ts | 19 +- .../llm/context/modules/context_service.ts | 234 ++++++++++++++++- 7 files changed, 777 insertions(+), 125 deletions(-) diff --git a/src/public/translations/en/translation.json b/src/public/translations/en/translation.json index 4a3881ed4..f7aeba144 100644 --- a/src/public/translations/en/translation.json +++ b/src/public/translations/en/translation.json @@ -1836,7 +1836,6 @@ "note_chat": "Note Chat", "notes_indexed": "{{ count }} note indexed", "notes_indexed_plural": "{{ count }} notes indexed", - "processing": "Processing", "reset_embeddings": "Reset Embeddings", "show_thinking": "Show Thinking", "show_thinking_description": "Reveals the reasoning process used to generate responses", diff --git a/src/services/llm/agent_tools/contextual_thinking_tool.ts b/src/services/llm/agent_tools/contextual_thinking_tool.ts index 9bae099d0..70f2fa2a1 100644 --- a/src/services/llm/agent_tools/contextual_thinking_tool.ts +++ b/src/services/llm/agent_tools/contextual_thinking_tool.ts @@ -49,75 +49,89 @@ export class ContextualThinkingTool { private processes: Record = {}; /** - * Start a new thinking process for a given query + * Start a new thinking process for a query * - * @param query The user query that initiated the thinking process - * @returns The ID of the new thinking process + * @param query The user's query + * @returns The created thinking process ID */ startThinking(query: string): string { - const id = this.generateProcessId(); + const thinkingId = `thinking_${Date.now()}_${ContextualThinkingTool.thinkingCounter++}`; - this.processes[id] = { - id, + 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.activeProcId = id; - return id; + // 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}"` + }); + + 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.` + }); + + return thinkingId; } /** - * Add a thinking step to the current active process + * Add a thinking step to a process * - * @param content The content of the thinking step - * @param type The type of thinking step - * @param options Additional options for the step - * @returns The ID of the new step + * @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( - content: string, - type: ThinkingStep['type'], - options: { - confidence?: number; - sources?: string[]; - parentId?: string; - metadata?: Record; - } = {} - ): string | null { - if (!this.activeProcId || !this.processes[this.activeProcId]) { - log.error("No active thinking process to add step to"); - return null; + processId: string, + step: Omit, + parentId?: string + ): string { + const process = this.processes[processId]; + + if (!process) { + throw new Error(`Thinking process ${processId} not found`); } - const stepId = this.generateStepId(); - const step: ThinkingStep = { - id: stepId, - content, - type, - ...options + // Create full step with ID + const fullStep: ThinkingStep = { + id: `step_${Date.now()}_${Math.floor(Math.random() * 10000)}`, + ...step, + parentId }; - // Add to parent's children if a parent is specified - if (options.parentId) { - const parentIdx = this.processes[this.activeProcId].steps.findIndex( - s => s.id === options.parentId - ); + // Add to process steps + process.steps.push(fullStep); - if (parentIdx >= 0) { - const parent = this.processes[this.activeProcId].steps[parentIdx]; - if (!parent.children) { - parent.children = []; + // 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 = []; } - parent.children.push(stepId); - this.processes[this.activeProcId].steps[parentIdx] = parent; + parentStep.children.push(fullStep.id); } } - this.processes[this.activeProcId].steps.push(step); - return stepId; + // Log the step addition with more detail + log.info(`Added thinking step to process ${processId}: [${step.type}] ${step.content.substring(0, 100)}...`); + + return fullStep.id; } /** @@ -177,25 +191,71 @@ export class ContextualThinkingTool { log.info(`Found thinking process with ${process.steps.length} steps for query: "${process.query.substring(0, 50)}..."`); let html = "
"; - html += `

Thinking Process

`; + html += `

Reasoning Process

`; + html += `
${process.query}
`; - for (const step of process.steps) { - html += `
`; + // 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); - html += ` `; + stepHtml += ` `; // Add the step content - html += step.content; + stepHtml += step.content; // Show confidence if available if (step.metadata?.confidence) { const confidence = Math.round((step.metadata.confidence as number) * 100); - html += ` (Confidence: ${confidence}%)`; + stepHtml += ` (Confidence: ${confidence}%)`; } - html += `
`; + // 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 += "
"; @@ -232,7 +292,61 @@ export class ContextualThinkingTool { return "No thinking process available."; } - return this.visualizeThinking(thinkingId); + 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"; + } + + // 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"; + } + + return summary; } /** diff --git a/src/services/llm/agent_tools/note_navigator_tool.ts b/src/services/llm/agent_tools/note_navigator_tool.ts index 17107a403..0d00c9716 100644 --- a/src/services/llm/agent_tools/note_navigator_tool.ts +++ b/src/services/llm/agent_tools/note_navigator_tool.ts @@ -41,6 +41,15 @@ export interface NoteHierarchyLevel { children?: NoteHierarchyLevel[]; } +interface NoteStructure { + noteId: string; + title: string; + type: string; + childCount: number; + attributes: Array<{name: string, value: string}>; + parentPath: Array<{title: string, noteId: string}>; +} + export class NoteNavigatorTool { private maxPathLength: number = 20; private maxBreadth: number = 100; @@ -113,45 +122,6 @@ export class NoteNavigatorTool { } } - /** - * Get the parent notes of a given note - */ - getParentNotes(noteId: string): NoteInfo[] { - try { - const note = becca.notes[noteId]; - if (!note || !note.parents) { - return []; - } - - return note.parents - .map(parent => this.getNoteInfo(parent.noteId)) - .filter((info): info is NoteInfo => info !== null); - } catch (error: any) { - log.error(`Error getting parent notes: ${error.message}`); - return []; - } - } - - /** - * Get the children notes of a given note - */ - getChildNotes(noteId: string, maxChildren: number = this.maxBreadth): NoteInfo[] { - try { - const note = becca.notes[noteId]; - if (!note || !note.children) { - return []; - } - - return note.children - .slice(0, maxChildren) - .map(child => this.getNoteInfo(child.noteId)) - .filter((info): info is NoteInfo => info !== null); - } catch (error: any) { - log.error(`Error getting child notes: ${error.message}`); - return []; - } - } - /** * Get a note's hierarchy (children up to specified depth) * This is useful for the LLM to understand the structure within a note's subtree @@ -349,7 +319,7 @@ export class NoteNavigatorTool { /** * Get clones of a note (if any) */ - getNoteClones(noteId: string): NoteInfo[] { + async getNoteClones(noteId: string): Promise { try { const note = becca.notes[noteId]; if (!note) { @@ -362,7 +332,10 @@ export class NoteNavigatorTool { } // Return parent notes, which represent different contexts for this note - return this.getParentNotes(noteId); + const parents = await this.getParentNotes(noteId); + return parents + .map(parent => this.getNoteInfo(parent.noteId)) + .filter((info): info is NoteInfo => info !== null); } catch (error: any) { log.error(`Error getting note clones: ${error.message}`); return []; @@ -373,7 +346,7 @@ export class NoteNavigatorTool { * Generate a readable overview of a note's position in the hierarchy * This is useful for the LLM to understand the context of a note */ - getNoteContextDescription(noteId: string): string { + async getNoteContextDescription(noteId: string): Promise { try { const note = becca.notes[noteId]; if (!note) { @@ -409,8 +382,9 @@ export class NoteNavigatorTool { result += `Path: ${path.notePathTitles.join(' > ')}\n`; } - // Children info - const children = this.getChildNotes(noteId, 5); + // Children info using the async function + const children = await this.getChildNotes(noteId, 5); + if (children.length > 0) { result += `\nContains ${note.children.length} child notes`; if (children.length < note.children.length) { @@ -419,7 +393,7 @@ export class NoteNavigatorTool { result += `:\n`; for (const child of children) { - result += `- ${child.title} (${child.type})\n`; + result += `- ${child.title}\n`; } if (children.length < note.children.length) { @@ -458,6 +432,185 @@ export class NoteNavigatorTool { return "Error generating note context description."; } } + + /** + * Get the structure of a note including its hierarchy and attributes + * + * @param noteId The ID of the note to get structure for + * @returns Structure information about the note + */ + async getNoteStructure(noteId: string): Promise { + try { + log.info(`Getting note structure for note ${noteId}`); + + // Get the note from becca + const note = becca.notes[noteId]; + + if (!note) { + throw new Error(`Note ${noteId} not found`); + } + + // Get child notes count + const childCount = note.children.length; + + // Get attributes + const attributes = note.getAttributes().map(attr => ({ + name: attr.name, + value: attr.value + })); + + // Build parent path + const parentPath: Array<{title: string, noteId: string}> = []; + let current = note.parents[0]; // Get first parent + + while (current && current.noteId !== 'root') { + parentPath.unshift({ + title: current.title, + noteId: current.noteId + }); + + current = current.parents[0]; + } + + return { + noteId: note.noteId, + title: note.title, + type: note.type, + childCount, + attributes, + parentPath + }; + } catch (error) { + log.error(`Error getting note structure: ${error}`); + // Return a minimal structure with empty arrays to avoid null errors + return { + noteId, + title: 'Unknown', + type: 'unknown', + childCount: 0, + attributes: [], + parentPath: [] + }; + } + } + + /** + * Get child notes of a specified note + */ + async getChildNotes(noteId: string, limit: number = 10): Promise> { + try { + const note = becca.notes[noteId]; + + if (!note) { + throw new Error(`Note ${noteId} not found`); + } + + return note.children + .slice(0, limit) + .map(child => ({ + noteId: child.noteId, + title: child.title + })); + } catch (error) { + log.error(`Error getting child notes: ${error}`); + return []; + } + } + + /** + * Get parent notes of a specified note + */ + async getParentNotes(noteId: string): Promise> { + try { + const note = becca.notes[noteId]; + + if (!note) { + throw new Error(`Note ${noteId} not found`); + } + + return note.parents.map(parent => ({ + noteId: parent.noteId, + title: parent.title + })); + } catch (error) { + log.error(`Error getting parent notes: ${error}`); + return []; + } + } + + /** + * Find notes linked to/from the specified note + */ + async getLinkedNotes(noteId: string, limit: number = 10): Promise> { + try { + const note = becca.notes[noteId]; + + if (!note) { + throw new Error(`Note ${noteId} not found`); + } + + // Links from this note to others + const outboundLinks = note.getRelations() + .slice(0, Math.floor(limit / 2)) + .map(relation => ({ + noteId: relation.targetNoteId || '', // Ensure noteId is never undefined + title: relation.name, + direction: 'to' as const + })) + .filter(link => link.noteId !== ''); // Filter out any with empty noteId + + // Links from other notes to this one + const inboundLinks: Array<{noteId: string, title: string, direction: 'from'}> = []; + + // Find all notes that have relations pointing to this note + for (const relatedNoteId in becca.notes) { + const relatedNote = becca.notes[relatedNoteId]; + if (relatedNote && !relatedNote.isDeleted) { + const relations = relatedNote.getRelations(); + for (const relation of relations) { + if (relation.targetNoteId === noteId) { + inboundLinks.push({ + noteId: relatedNote.noteId, + title: relation.name, + direction: 'from' + }); + + // Break if we've found enough inbound links + if (inboundLinks.length >= Math.floor(limit / 2)) { + break; + } + } + } + + // Break if we've found enough inbound links + if (inboundLinks.length >= Math.floor(limit / 2)) { + break; + } + } + } + + return [...outboundLinks, ...inboundLinks.slice(0, Math.floor(limit / 2))]; + } catch (error) { + log.error(`Error getting linked notes: ${error}`); + return []; + } + } + + /** + * Get the full path of a note from root + */ + async getNotePath(noteId: string): Promise { + try { + const structure = await this.getNoteStructure(noteId); + const path = structure.parentPath.map(p => p.title); + path.push(structure.title); + + return path.join(' > '); + } catch (error) { + log.error(`Error getting note path: ${error}`); + return 'Unknown path'; + } + } } export default NoteNavigatorTool; diff --git a/src/services/llm/agent_tools/query_decomposition_tool.ts b/src/services/llm/agent_tools/query_decomposition_tool.ts index 81053cfef..36a594b56 100644 --- a/src/services/llm/agent_tools/query_decomposition_tool.ts +++ b/src/services/llm/agent_tools/query_decomposition_tool.ts @@ -41,11 +41,17 @@ export class QueryDecompositionTool { */ 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`); // For simple queries, just return the original as a single sub-query - if (complexity < 3) { + // 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: [{ @@ -61,6 +67,12 @@ export class QueryDecompositionTool { // For complex queries, perform decomposition const subQueries = this.createSubQueries(query, context); + log.info(`Decomposed query into ${subQueries.length} sub-queries`); + + // Log the sub-queries for better visibility + subQueries.forEach((sq, index) => { + log.info(`Sub-query ${index + 1}: "${sq.text}" - Reason: ${sq.reason}`); + }); return { originalQuery: query, @@ -248,9 +260,18 @@ export class QueryDecompositionTool { private createSubQueries(query: string, context?: string): SubQuery[] { const subQueries: SubQuery[] = []; - // Simple heuristics for breaking down the query - // In a real implementation, this would be much more sophisticated, - // using natural language understanding to identify different intents + // 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); @@ -266,6 +287,15 @@ export class QueryDecompositionTool { isAnswered: false }); } + + // 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 + }); + return subQueries; } @@ -305,8 +335,22 @@ export class QueryDecompositionTool { subQueries.push({ id: this.generateSubQueryId(), - text: `What are the main differences and similarities between ${item1} and ${item2}?`, - reason: 'Direct comparison after understanding each item', + 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 }); @@ -317,7 +361,8 @@ export class QueryDecompositionTool { } // 3. For complex questions without clear separation, create topic-based sub-queries - if (query.length > 100) { + // 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 && @@ -333,7 +378,7 @@ export class QueryDecompositionTool { // Get top frequent words const topWords = Object.entries(wordFrequency) .sort((a, b) => b[1] - a[1]) - .slice(0, 3) + .slice(0, 4) // Increased from 3 to 4 .map(entry => entry[0]); if (topWords.length > 0) { @@ -345,16 +390,38 @@ export class QueryDecompositionTool { isAnswered: false }); - // Create relationship sub-query if multiple top words - if (topWords.length > 1) { + // Add individual queries for each key topic + topWords.forEach(word => { subQueries.push({ id: this.generateSubQueryId(), - text: `How do ${topWords.join(' and ')} relate to each other?`, - reason: 'Understanding relationships between key topics', + 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(), @@ -368,10 +435,26 @@ export class QueryDecompositionTool { } // 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: 'Question treated as a single unit', + 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 }); diff --git a/src/services/llm/agent_tools/vector_search_tool.ts b/src/services/llm/agent_tools/vector_search_tool.ts index 63900fd10..573a3f719 100644 --- a/src/services/llm/agent_tools/vector_search_tool.ts +++ b/src/services/llm/agent_tools/vector_search_tool.ts @@ -13,6 +13,7 @@ */ import log from '../../log.js'; +import type { ContextService } from '../context/modules/context_service.js'; // Define interface for context service to avoid circular imports interface IContextService { @@ -48,6 +49,12 @@ export interface ChunkSearchResultItem { parentId?: string; } +export interface VectorSearchOptions { + limit?: number; + threshold?: number; + includeContent?: boolean; +} + export class VectorSearchTool { private contextService: IContextService | null = null; private maxResults: number = 5; @@ -64,6 +71,77 @@ export class VectorSearchTool { log.info('Context service set in VectorSearchTool'); } + /** + * Perform a vector search for related notes + */ + async search( + query: string, + contextNoteId?: string, + searchOptions: VectorSearchOptions = {} + ): Promise { + if (!this.contextService) { + throw new Error("Context service not set, call setContextService() first"); + } + + try { + // Set more aggressive defaults to return more content + const options = { + limit: searchOptions.limit || 15, // Increased from default (likely 5 or 10) + threshold: searchOptions.threshold || 0.5, // Lower threshold to include more results (likely 0.65 or 0.7 before) + includeContent: searchOptions.includeContent !== undefined ? searchOptions.includeContent : true, + ...searchOptions + }; + + log.info(`Vector search: "${query.substring(0, 50)}..." with limit=${options.limit}, threshold=${options.threshold}`); + + // Check if contextService is set again to satisfy TypeScript + if (!this.contextService) { + throw new Error("Context service not set, call setContextService() first"); + } + + // Use contextService methods instead of direct imports + const results = await this.contextService.findRelevantNotesMultiQuery( + [query], + contextNoteId || null, + options.limit + ); + + // Log the number of results + log.info(`Vector search found ${results.length} relevant notes`); + + // Include more content from each note to provide richer context + if (options.includeContent) { + // Get full context for each note rather than summaries + for (let i = 0; i < results.length; i++) { + const result = results[i]; + try { + // Use contextService instead of direct import + if (this.contextService && 'processQuery' in this.contextService) { + const contextResult = await this.contextService.processQuery( + `Provide details about the note: ${result.title}`, + null, + result.noteId, + false + ); + + if (contextResult && contextResult.context) { + // Use more of the content, up to 2000 chars + result.content = contextResult.context.substring(0, 2000); + } + } + } catch (error) { + log.error(`Error getting content for note ${result.noteId}: ${error}`); + } + } + } + + return results; + } catch (error) { + log.error(`Vector search error: ${error}`); + return []; + } + } + /** * Search for notes that are semantically related to the query */ diff --git a/src/services/llm/ai_service_manager.ts b/src/services/llm/ai_service_manager.ts index 1314a4d59..b4d761f5c 100644 --- a/src/services/llm/ai_service_manager.ts +++ b/src/services/llm/ai_service_manager.ts @@ -414,12 +414,19 @@ export class AIServiceManager { showThinking: boolean = false, relevantNotes: Array = [] ): Promise { - return contextService.getAgentToolsContext( - noteId, - query, - showThinking, - relevantNotes - ); + // Just use the context service directly + try { + const cs = (await import('./context/modules/context_service.js')).default; + return cs.getAgentToolsContext( + noteId, + query, + showThinking, + relevantNotes + ); + } catch (error) { + log.error(`Error in AIServiceManager.getAgentToolsContext: ${error}`); + return `Error generating enhanced context: ${error}`; + } } } diff --git a/src/services/llm/context/modules/context_service.ts b/src/services/llm/context/modules/context_service.ts index 2edc95a47..00fbac10d 100644 --- a/src/services/llm/context/modules/context_service.ts +++ b/src/services/llm/context/modules/context_service.ts @@ -197,15 +197,222 @@ export class ContextService { relevantNotes: Array = [] ): Promise { try { - return await aiServiceManager.getInstance().getAgentToolsContext( - noteId, - query, - showThinking, - relevantNotes - ); + log.info(`Building enhanced agent tools context for query: "${query.substring(0, 50)}...", noteId=${noteId}, showThinking=${showThinking}`); + + // Make sure agent tools are initialized + const agentManager = aiServiceManager.getInstance(); + + // Initialize all tools if not already done + if (!agentManager.getAgentTools().isInitialized()) { + await agentManager.initializeAgentTools(); + log.info("Agent tools initialized on-demand in getAgentToolsContext"); + } + + // Get all agent tools + const vectorSearchTool = agentManager.getVectorSearchTool(); + const noteNavigatorTool = agentManager.getNoteNavigatorTool(); + const queryDecompositionTool = agentManager.getQueryDecompositionTool(); + const contextualThinkingTool = agentManager.getContextualThinkingTool(); + + // Step 1: Start a thinking process + const thinkingId = contextualThinkingTool.startThinking(query); + contextualThinkingTool.addThinkingStep(thinkingId, { + type: 'observation', + content: `Analyzing query: "${query}" for note ID: ${noteId}` + }); + + // Step 2: Decompose the query into sub-questions + const decomposedQuery = queryDecompositionTool.decomposeQuery(query); + contextualThinkingTool.addThinkingStep(thinkingId, { + type: 'observation', + content: `Query complexity: ${decomposedQuery.complexity}/10. Decomposed into ${decomposedQuery.subQueries.length} sub-queries.` + }); + + // Log each sub-query as a thinking step + for (const subQuery of decomposedQuery.subQueries) { + contextualThinkingTool.addThinkingStep(thinkingId, { + type: 'question', + content: subQuery.text, + metadata: { + reason: subQuery.reason + } + }); + } + + // Step 3: Use vector search to find related content + // Use an aggressive search with lower threshold to get more results + const searchOptions = { + threshold: 0.5, // Lower threshold to include more matches + limit: 15 // Get more results + }; + + const vectorSearchPromises = []; + + // Search for each sub-query that isn't just the original query + for (const subQuery of decomposedQuery.subQueries.filter(sq => sq.text !== query)) { + vectorSearchPromises.push( + vectorSearchTool.search(subQuery.text, noteId, searchOptions) + .then(results => { + return { + query: subQuery.text, + results + }; + }) + ); + } + + // Wait for all searches to complete + const searchResults = await Promise.all(vectorSearchPromises); + + // Record the search results in thinking steps + let totalResults = 0; + for (const result of searchResults) { + totalResults += result.results.length; + + if (result.results.length > 0) { + const stepId = contextualThinkingTool.addThinkingStep(thinkingId, { + type: 'evidence', + content: `Found ${result.results.length} relevant notes for sub-query: "${result.query}"`, + metadata: { + searchQuery: result.query + } + }); + + // Add top results as children + for (const note of result.results.slice(0, 3)) { + contextualThinkingTool.addThinkingStep(thinkingId, { + type: 'evidence', + content: `Note "${note.title}" (similarity: ${Math.round(note.similarity * 100)}%) contains relevant information`, + metadata: { + noteId: note.noteId, + similarity: note.similarity + } + }, stepId); + } + } else { + contextualThinkingTool.addThinkingStep(thinkingId, { + type: 'observation', + content: `No notes found for sub-query: "${result.query}"`, + metadata: { + searchQuery: result.query + } + }); + } + } + + // Step 4: Get note structure information + try { + const noteStructure = await noteNavigatorTool.getNoteStructure(noteId); + + contextualThinkingTool.addThinkingStep(thinkingId, { + type: 'observation', + content: `Note structure: ${noteStructure.childCount} child notes, ${noteStructure.attributes.length} attributes, ${noteStructure.parentPath.length} levels in hierarchy`, + metadata: { + structure: noteStructure + } + }); + + // Add information about parent path + if (noteStructure.parentPath.length > 0) { + const parentPathStr = noteStructure.parentPath.map((p: {title: string, noteId: string}) => p.title).join(' > '); + contextualThinkingTool.addThinkingStep(thinkingId, { + type: 'observation', + content: `Note hierarchy: ${parentPathStr}`, + metadata: { + parentPath: noteStructure.parentPath + } + }); + } + } catch (error) { + log.error(`Error getting note structure: ${error}`); + contextualThinkingTool.addThinkingStep(thinkingId, { + type: 'observation', + content: `Unable to retrieve note structure information: ${error}` + }); + } + + // Step 5: Conclude thinking process + contextualThinkingTool.addThinkingStep(thinkingId, { + type: 'conclusion', + content: `Analysis complete. Found ${totalResults} relevant notes across ${searchResults.length} search queries.`, + metadata: { + totalResults, + queryCount: searchResults.length + } + }); + + // Complete the thinking process + contextualThinkingTool.completeThinking(thinkingId); + + // Step 6: Build the context string combining all the information + let agentContext = ''; + + // Add note structure information + try { + const noteStructure = await noteNavigatorTool.getNoteStructure(noteId); + agentContext += `## Current Note Context\n`; + agentContext += `- Note Title: ${noteStructure.title}\n`; + + if (noteStructure.parentPath.length > 0) { + const parentPathStr = noteStructure.parentPath.map((p: {title: string, noteId: string}) => p.title).join(' > '); + agentContext += `- Location: ${parentPathStr}\n`; + } + + if (noteStructure.attributes.length > 0) { + agentContext += `- Attributes: ${noteStructure.attributes.map((a: {name: string, value: string}) => `${a.name}=${a.value}`).join(', ')}\n`; + } + + if (noteStructure.childCount > 0) { + agentContext += `- Contains ${noteStructure.childCount} child notes\n`; + } + + agentContext += `\n`; + } catch (error) { + log.error(`Error adding note structure to context: ${error}`); + } + + // Add most relevant notes from search results + const allSearchResults = searchResults.flatMap(r => r.results); + + // Deduplicate results by noteId + const uniqueResults = new Map(); + for (const result of allSearchResults) { + if (!uniqueResults.has(result.noteId) || uniqueResults.get(result.noteId).similarity < result.similarity) { + uniqueResults.set(result.noteId, result); + } + } + + // Sort by similarity + const sortedResults = Array.from(uniqueResults.values()) + .sort((a, b) => b.similarity - a.similarity) + .slice(0, 10); // Get top 10 unique results + + if (sortedResults.length > 0) { + agentContext += `## Relevant Information\n`; + + for (const result of sortedResults) { + agentContext += `### ${result.title}\n`; + + if (result.content) { + // Limit content to 500 chars per note to avoid token explosion + agentContext += `${result.content.substring(0, 500)}${result.content.length > 500 ? '...' : ''}\n\n`; + } + } + } + + // Add thinking process if requested + if (showThinking) { + agentContext += `\n## Reasoning Process\n`; + agentContext += contextualThinkingTool.getThinkingSummary(thinkingId); + } + + // Log stats about the context + log.info(`Agent tools context built: ${agentContext.length} chars, ${agentContext.split('\n').length} lines`); + + return agentContext; } catch (error) { log.error(`Error getting agent tools context: ${error}`); - return ''; + return `Error generating enhanced context: ${error}`; } } @@ -271,8 +478,19 @@ export class ContextService { return ''; } + // Convert parent notes from {id, title} to {noteId, title} for consistency + const normalizedRelatedNotes = allRelatedNotes.map(note => { + return { + noteId: 'id' in note ? note.id : note.noteId, + title: note.title + }; + }); + // Rank notes by relevance to query - const rankedNotes = await semanticSearch.rankNotesByRelevance(allRelatedNotes, userQuery); + const rankedNotes = await semanticSearch.rankNotesByRelevance( + normalizedRelatedNotes as Array<{noteId: string, title: string}>, + userQuery + ); // Get content for the top N most relevant notes const mostRelevantNotes = rankedNotes.slice(0, maxResults);