From dccd6477d2abc4ce0f401704c157f374d736919d Mon Sep 17 00:00:00 2001 From: perf3ct Date: Wed, 11 Jun 2025 19:34:30 +0000 Subject: [PATCH 1/5] feat(llm): try to improve tool and tool calling, part 1 --- .../llm/pipeline/stages/tool_calling_stage.ts | 75 +++- .../llm/tools/attribute_search_tool.ts | 73 +++- .../services/llm/tools/keyword_search_tool.ts | 94 +++- .../src/services/llm/tools/read_note_tool.ts | 77 +++- .../services/llm/tools/search_notes_tool.ts | 94 +++- .../llm/tools/tool_discovery_helper.ts | 368 ++++++++++++++++ .../services/llm/tools/tool_initializer.ts | 6 + .../src/services/llm/tools/workflow_helper.ts | 408 ++++++++++++++++++ 8 files changed, 1143 insertions(+), 52 deletions(-) create mode 100644 apps/server/src/services/llm/tools/tool_discovery_helper.ts create mode 100644 apps/server/src/services/llm/tools/workflow_helper.ts diff --git a/apps/server/src/services/llm/pipeline/stages/tool_calling_stage.ts b/apps/server/src/services/llm/pipeline/stages/tool_calling_stage.ts index 8299f8fd64..a2eaa00fb5 100644 --- a/apps/server/src/services/llm/pipeline/stages/tool_calling_stage.ts +++ b/apps/server/src/services/llm/pipeline/stages/tool_calling_stage.ts @@ -483,28 +483,52 @@ export class ToolCallingStage extends BasePipelineStage this.isEmptyToolResult(msg.content, msg.name || '')) - .map(msg => msg.name); - - let directiveMessage = `YOU MUST NOT GIVE UP AFTER A SINGLE EMPTY SEARCH RESULT. `; - - if (emptyToolNames.includes('search_notes') || emptyToolNames.includes('keyword_search')) { - directiveMessage += `IMMEDIATELY RUN ANOTHER SEARCH TOOL with broader search terms, alternative keywords, or related concepts. `; - directiveMessage += `Try synonyms, more general terms, or related topics. `; - } - - if (emptyToolNames.includes('keyword_search')) { - directiveMessage += `IMMEDIATELY TRY SEARCH_NOTES INSTEAD as it might find matches where keyword search failed. `; + // Add aggressive system message for continued tool usage + if (needsFollowUp) { + log.info('Adding enhanced system message to encourage continued tool usage'); + + let directiveMessage = ''; + + if (hasEmptyResults) { + // Empty results - be very directive about trying alternatives + const emptyToolNames = toolResultMessages + .filter(msg => this.isEmptyToolResult(msg.content, msg.name || '')) + .map(msg => msg.name); + + directiveMessage = `CRITICAL INSTRUCTION: YOU MUST NOT STOP AFTER EMPTY RESULTS!\n\n`; + directiveMessage += `REQUIRED ACTIONS:\n`; + + if (emptyToolNames.includes('search_notes')) { + directiveMessage += `1. IMMEDIATELY use keyword_search_notes with specific terms\n`; + directiveMessage += `2. Try attribute_search if content might be tagged/categorized\n`; + directiveMessage += `3. Use discover_tools to find alternative approaches\n`; + } + + if (emptyToolNames.includes('keyword_search_notes')) { + directiveMessage += `1. IMMEDIATELY use search_notes for semantic matching\n`; + directiveMessage += `2. Try broader or alternative keyword terms\n`; + directiveMessage += `3. Use workflow_helper for guidance on next steps\n`; + } + + if (emptyToolNames.includes('attribute_search')) { + directiveMessage += `1. Use search_notes to find content about the attribute topic\n`; + directiveMessage += `2. Try different attribute names or types\n`; + directiveMessage += `3. Use search_suggestion to see available attributes\n`; + } + + directiveMessage += `\nFORBIDDEN: Do NOT ask user for clarification or offer general information!\n`; + directiveMessage += `REQUIRED: CONTINUE with alternative tools and approaches immediately!`; + } else { + // Has results - encourage follow-up actions + directiveMessage = `EXCELLENT! You found results. Now CONTINUE the workflow:\n\n`; + directiveMessage += `NEXT REQUIRED ACTIONS:\n`; + directiveMessage += `1. Use read_note to examine the most relevant results\n`; + directiveMessage += `2. Use workflow_helper to plan next steps based on your findings\n`; + directiveMessage += `3. Consider using related tools for deeper analysis\n\n`; + directiveMessage += `GOAL: Provide comprehensive information by using multiple tools in sequence.\n`; + directiveMessage += `CONTINUE with tool usage - don't stop at just search results!`; } - directiveMessage += `DO NOT ask the user what to do next or if they want general information. CONTINUE SEARCHING with different parameters.`; - updatedMessages.push({ role: 'system', content: directiveMessage @@ -609,10 +633,19 @@ export class ToolCallingStage extends BasePipelineStage 0 ? suggestions.join('\n') : ''} + +EXAMPLES: +• Find notes with #important tag: { "attributeType": "label", "attributeName": "important" } +• Find notes with ~relatedTo relation: { "attributeType": "relation", "attributeName": "relatedTo" }`; + + return errorMessage; } // Execute the search diff --git a/apps/server/src/services/llm/tools/keyword_search_tool.ts b/apps/server/src/services/llm/tools/keyword_search_tool.ts index 8365d38f4e..d32f873034 100644 --- a/apps/server/src/services/llm/tools/keyword_search_tool.ts +++ b/apps/server/src/services/llm/tools/keyword_search_tool.ts @@ -17,21 +17,49 @@ export const keywordSearchToolDefinition: Tool = { type: 'function', function: { name: 'keyword_search_notes', - description: 'Search for notes using exact keyword matching and attribute filters. Use this for precise searches when you need exact matches or want to filter by attributes.', + description: `EXACT KEYWORD search for notes. Finds notes containing specific words, phrases, or attribute filters. + + BEST FOR: Finding notes with specific words/phrases you know exist + USE WHEN: You need exact text matches, specific terms, or attribute-based filtering + DIFFERENT FROM: search_notes (which finds conceptual/semantic matches) + + SEARCH TYPES: + • Simple: "machine learning" (finds notes containing both words) + • Phrase: "\"exact phrase\"" (finds this exact phrase) + • Attributes: "#label" or "~relation" (notes with specific labels/relations) + • Complex: "AI #project ~relatedTo" (combines keywords with attributes) + + NEXT STEPS: Use read_note with returned noteId values for full content`, parameters: { type: 'object', properties: { query: { type: 'string', - description: 'The search query using Trilium\'s search syntax. Examples: "rings tolkien" (find notes with both words), "#book #year >= 2000" (notes with label "book" and "year" attribute >= 2000), "note.content *=* important" (notes with "important" in content)' + description: `Keyword search query using Trilium search syntax. + + SIMPLE EXAMPLES: + - "machine learning" (both words anywhere) + - "\"project management\"" (exact phrase) + - "python OR javascript" (either word) + + ATTRIBUTE EXAMPLES: + - "#important" (notes with 'important' label) + - "~project" (notes with 'project' relation) + - "#status = completed" (specific label value) + + COMBINED EXAMPLES: + - "AI #project #status = active" (AI content with project label and active status) + - "note.title *= \"weekly\"" (titles containing 'weekly') + + AVOID: Conceptual queries better suited for search_notes` }, maxResults: { type: 'number', - description: 'Maximum number of results to return (default: 10)' + description: 'Number of results (1-50, default: 10). Use higher values for comprehensive searches.' }, includeArchived: { type: 'boolean', - description: 'Whether to include archived notes in search results (default: false)' + description: 'INCLUDE ARCHIVED: Search archived notes too (default: false). Use true for complete historical search.' } }, required: ['query'] @@ -45,6 +73,22 @@ export const keywordSearchToolDefinition: Tool = { export class KeywordSearchTool implements ToolHandler { public definition: Tool = keywordSearchToolDefinition; + /** + * Convert a keyword query to a semantic query suggestion + */ + private convertToSemanticQuery(keywordQuery: string): string { + // Remove search operators and attributes to create a semantic query + return keywordQuery + .replace(/#\w+/g, '') // Remove label filters + .replace(/~\w+/g, '') // Remove relation filters + .replace(/\"[^\"]*\"/g, (match) => match.slice(1, -1)) // Remove quotes but keep content + .replace(/\s+OR\s+/gi, ' ') // Replace OR with space + .replace(/\s+AND\s+/gi, ' ') // Replace AND with space + .replace(/note\.(title|content)\s*\*=\*\s*/gi, '') // Remove note.content operators + .replace(/\s+/g, ' ') // Normalize spaces + .trim(); + } + /** * Execute the keyword search notes tool */ @@ -80,21 +124,52 @@ export class KeywordSearchTool implements ToolHandler { log.info(`No matching notes found for query: "${query}"`); } - // Format the results + // Format the results with enhanced guidance + if (limitedResults.length === 0) { + return { + count: 0, + results: [], + query: query, + searchType: 'keyword', + message: 'No exact keyword matches found.', + nextSteps: { + immediate: [ + `Try search_notes for semantic/conceptual search: "${this.convertToSemanticQuery(query)}"`, + `Use attribute_search if looking for specific labels or relations`, + `Try simpler keywords or check spelling` + ], + queryHelp: [ + 'Remove quotes for broader matching', + 'Try individual words instead of phrases', + 'Use OR operator: "word1 OR word2"', + 'Check if content might be in archived notes (set includeArchived: true)' + ] + } + }; + } + return { count: limitedResults.length, totalFound: searchResults.length, + query: query, + searchType: 'keyword', + message: 'Found exact keyword matches. Use noteId values with other tools.', + nextSteps: { + examine: `Use read_note with any noteId (e.g., "${limitedResults[0].noteId}") to get full content`, + refine: limitedResults.length < searchResults.length ? `Found ${searchResults.length} total matches (showing ${limitedResults.length}). Increase maxResults for more.` : null, + related: 'Use search_notes for conceptually related content beyond exact keywords' + }, results: limitedResults.map(note => { - // Get a preview of the note content + // Get a preview of the note content with highlighted search terms let contentPreview = ''; try { const content = note.getContent(); if (typeof content === 'string') { - contentPreview = content.length > 150 ? content.substring(0, 150) + '...' : content; + contentPreview = content.length > 200 ? content.substring(0, 200) + '...' : content; } else if (Buffer.isBuffer(content)) { contentPreview = '[Binary content]'; } else { - contentPreview = String(content).substring(0, 150) + (String(content).length > 150 ? '...' : ''); + contentPreview = String(content).substring(0, 200) + (String(content).length > 200 ? '...' : ''); } } catch (e) { contentPreview = '[Content not available]'; @@ -114,7 +189,8 @@ export class KeywordSearchTool implements ToolHandler { attributes: attributes.length > 0 ? attributes : undefined, type: note.type, mime: note.mime, - isArchived: note.isArchived + isArchived: note.isArchived, + dateModified: note.dateModified }; }) }; diff --git a/apps/server/src/services/llm/tools/read_note_tool.ts b/apps/server/src/services/llm/tools/read_note_tool.ts index ddcad559f1..ddb8ce5892 100644 --- a/apps/server/src/services/llm/tools/read_note_tool.ts +++ b/apps/server/src/services/llm/tools/read_note_tool.ts @@ -34,17 +34,37 @@ export const readNoteToolDefinition: Tool = { type: 'function', function: { name: 'read_note', - description: 'Read the content of a specific note by its ID', + description: `READ FULL CONTENT of a specific note by its ID. Get complete note content and metadata. + + BEST FOR: Getting complete content after finding notes through search tools + USE WHEN: You have a noteId from search results and need the full content + IMPORTANT: Must use noteId (like "abc123def456") from search results - NOT note titles + + TIP: This is typically used after search_notes, keyword_search_notes, or attribute_search + + NEXT STEPS: Use note_update or attribute_manager tools to modify the note if needed`, parameters: { type: 'object', properties: { noteId: { type: 'string', - description: 'The system ID of the note to read (not the title). This is a unique identifier like "abc123def456" that must be used to access a specific note.' + description: `SYSTEM ID of the note to read. + + CRITICAL: Must be a noteId (like "abc123def456") - NOT a note title! + + CORRECT: "abc123def456" (from search results) + WRONG: "My Note Title" (this will fail) + + WHERE TO GET: From noteId field in search tool results` }, includeAttributes: { type: 'boolean', - description: 'Whether to include note attributes in the response (default: false)' + description: `INCLUDE METADATA: Get note attributes (labels, relations) in response. + + • true = Get full note with all attributes/metadata + • false = Get just note content (default) + + Use true when you need to see tags, labels, relations, or other metadata` } }, required: ['noteId'] @@ -71,8 +91,23 @@ export class ReadNoteTool implements ToolHandler { const note = becca.notes[noteId]; if (!note) { - log.info(`Note with ID ${noteId} not found - returning error`); - return `Error: Note with ID ${noteId} not found`; + log.info(`Note with ID ${noteId} not found - returning helpful error`); + return { + error: `Note not found: "${noteId}"`, + troubleshooting: { + possibleCauses: [ + 'Invalid noteId format (should be like "abc123def456")', + 'Note may have been deleted or moved', + 'Using note title instead of noteId' + ], + solutions: [ + 'Use search_notes to find the note by content or title', + 'Use keyword_search_notes to find notes with specific text', + 'Use attribute_search if you know the note has specific attributes', + 'Ensure you\'re using noteId from search results, not the note title' + ] + } + }; } log.info(`Found note: "${note.title}" (Type: ${note.type})`); @@ -84,14 +119,33 @@ export class ReadNoteTool implements ToolHandler { log.info(`Retrieved note content in ${duration}ms, content length: ${content?.length || 0} chars`); - // Prepare the response - const response: NoteResponse = { + // Prepare enhanced response with next steps + const response: NoteResponse & { + nextSteps?: { + modify?: string; + related?: string; + organize?: string; + }; + metadata?: { + wordCount?: number; + hasAttributes?: boolean; + lastModified?: string; + }; + } = { noteId: note.noteId, title: note.title, type: note.type, content: content || '' }; + // Add helpful metadata + const contentStr = typeof content === 'string' ? content : String(content || ''); + response.metadata = { + wordCount: contentStr.split(/\s+/).filter(word => word.length > 0).length, + hasAttributes: note.getOwnedAttributes().length > 0, + lastModified: note.dateModified + }; + // Include attributes if requested if (includeAttributes) { const attributes = note.getOwnedAttributes(); @@ -111,6 +165,15 @@ export class ReadNoteTool implements ToolHandler { } } + // Add next steps guidance + response.nextSteps = { + modify: `Use note_update with noteId: "${noteId}" to edit this note's content`, + related: `Use search_notes with related concepts to find similar notes`, + organize: response.metadata.hasAttributes + ? `Use attribute_manager with noteId: "${noteId}" to modify attributes` + : `Use attribute_manager with noteId: "${noteId}" to add labels or relations` + }; + return response; } catch (error: unknown) { const errorMessage = isError(error) ? error.message : String(error); diff --git a/apps/server/src/services/llm/tools/search_notes_tool.ts b/apps/server/src/services/llm/tools/search_notes_tool.ts index 152187decb..5da23b7287 100644 --- a/apps/server/src/services/llm/tools/search_notes_tool.ts +++ b/apps/server/src/services/llm/tools/search_notes_tool.ts @@ -17,25 +17,50 @@ export const searchNotesToolDefinition: Tool = { type: 'function', function: { name: 'search_notes', - description: 'Search for notes in the database using semantic search. Returns notes most semantically related to the query. Use specific, descriptive queries for best results.', + description: `SEMANTIC/CONCEPTUAL search for notes. Finds notes related to concepts, topics, or themes even without exact keyword matches. + + BEST FOR: Finding notes about ideas, concepts, or topics described in various ways + USE WHEN: Looking for conceptual relationships, thematic content, or related ideas + DIFFERENT FROM: keyword_search (which finds exact text matches) + + TIPS: + - Use descriptive phrases like "project management methodologies" rather than single words + - Think conceptually: "machine learning classification" vs just "ML" + - Results include noteId values - ALWAYS use these IDs (not titles) with other tools + + NEXT STEPS: Use read_note with returned noteId values to get full content`, parameters: { type: 'object', properties: { query: { type: 'string', - description: 'The search query to find semantically related notes. Be specific and descriptive for best results.' + description: `Descriptive search query for semantic matching. + + GOOD EXAMPLES: + - "machine learning algorithms for classification" + - "personal productivity and time management techniques" + - "software development best practices" + + AVOID: + - Single words: "ML", "productivity" + - Overly broad: "work", "notes" + - Overly specific: exact phrases that might not exist` }, parentNoteId: { type: 'string', - description: 'Optional system ID of the parent note to restrict search to a specific branch (not the title). This is a unique identifier like "abc123def456". Do not use note titles here.' + description: `SCOPE LIMITER: Search only within children of this note. + + IMPORTANT: Must be a noteId (like "abc123def456") from previous search results - NOT a note title. + + USE FOR: Searching within specific projects, categories, or sections.` }, maxResults: { type: 'number', - description: 'Maximum number of results to return (default: 5)' + description: 'Number of results (1-20, default: 5). Use 10-15 for comprehensive exploration, 3-5 for quick lookup.' }, summarize: { type: 'boolean', - description: 'Whether to provide summarized content previews instead of truncated ones (default: false)' + description: 'AI SUMMARIES: Get intelligent summaries instead of truncated text (default: false). Use true for cleaner result overview.' } }, required: ['query'] @@ -189,6 +214,39 @@ export class SearchNotesTool implements ToolHandler { } } + /** + * Extract keywords from a semantic query for alternative search suggestions + */ + private extractKeywords(query: string): string { + return query.split(' ') + .filter(word => word.length > 3 && !['using', 'with', 'for', 'and', 'the', 'that', 'this'].includes(word.toLowerCase())) + .slice(0, 3) + .join(' '); + } + + /** + * Suggest broader search terms when specific searches fail + */ + private suggestBroaderTerms(query: string): string { + const broaderTermsMap: Record = { + 'machine learning': 'AI technology', + 'productivity': 'work methods', + 'development': 'programming', + 'management': 'organization', + 'planning': 'strategy' + }; + + for (const [specific, broader] of Object.entries(broaderTermsMap)) { + if (query.toLowerCase().includes(specific)) { + return broader; + } + } + + // Default: take first significant word and make it broader + const firstWord = query.split(' ').find(word => word.length > 3); + return firstWord ? `${firstWord} concepts` : 'general topics'; + } + /** * Execute the search notes tool */ @@ -260,19 +318,39 @@ export class SearchNotesTool implements ToolHandler { }) ); - // Format the results + // Format the results with enhanced guidance if (results.length === 0) { return { count: 0, results: [], query: query, - message: 'No notes found matching your query. Try using more general terms or try the keyword_search_notes tool with a different query. Note: Use the noteId (not the title) when performing operations on specific notes with other tools.' + searchType: 'semantic', + message: 'No semantic matches found for your query.', + nextSteps: { + immediate: [ + `Try keyword_search with specific terms: "${this.extractKeywords(query)}"`, + `Use attribute_search if looking for labeled/categorized notes`, + `Try broader search terms like "${this.suggestBroaderTerms(query)}"` + ], + tips: [ + 'Semantic search finds conceptual matches - try describing the topic differently', + 'If you know specific words that appear in the notes, use keyword_search instead', + 'Check if the content might be tagged with labels using attribute_search' + ] + } }; } else { return { count: enhancedResults.length, results: enhancedResults, - message: "Note: Use the noteId (not the title) when performing operations on specific notes with other tools." + query: query, + searchType: 'semantic', + message: 'Found semantic matches. Use noteId values with other tools.', + nextSteps: { + examine: `Use read_note with any noteId (e.g., "${enhancedResults[0].noteId}") to get full content`, + refine: parentNoteId ? 'Remove parentNoteId to search all notes' : `Add parentNoteId: "${enhancedResults[0].noteId}" to search within the first result's children`, + related: 'Search for related concepts or use different descriptive terms' + } }; } } catch (error: unknown) { diff --git a/apps/server/src/services/llm/tools/tool_discovery_helper.ts b/apps/server/src/services/llm/tools/tool_discovery_helper.ts new file mode 100644 index 0000000000..530bcb9770 --- /dev/null +++ b/apps/server/src/services/llm/tools/tool_discovery_helper.ts @@ -0,0 +1,368 @@ +/** + * Tool Discovery Helper + * + * This tool helps LLMs understand what tools are available and when to use them. + * It provides smart recommendations based on user queries and current context. + */ + +import type { Tool, ToolHandler } from './tool_interfaces.js'; +import log from '../../log.js'; +import toolRegistry from './tool_registry.js'; + +/** + * Definition of the tool discovery helper + */ +export const toolDiscoveryHelperDefinition: Tool = { + type: 'function', + function: { + name: 'discover_tools', + description: `DISCOVER AVAILABLE TOOLS and get guidance on which tools to use for your task. + + BEST FOR: Understanding what tools are available and getting usage recommendations + USE WHEN: You're unsure which tool to use, want to see all options, or need workflow guidance + HELPS WITH: Tool selection, parameter guidance, workflow planning + + TIP: Use this when you have a task but aren't sure which tools can help accomplish it + + NEXT STEPS: Use the recommended tools based on the guidance provided`, + parameters: { + type: 'object', + properties: { + taskDescription: { + type: 'string', + description: `📝 DESCRIBE YOUR TASK: What are you trying to accomplish? + + ✅ GOOD EXAMPLES: + - "Find notes about machine learning" + - "Create a new project planning note" + - "Find all notes tagged as important" + - "Read the content of a specific note" + + 💡 Be specific about your goal for better tool recommendations` + }, + includeExamples: { + type: 'boolean', + description: 'INCLUDE EXAMPLES: Get specific usage examples for recommended tools (default: true)' + }, + showAllTools: { + type: 'boolean', + description: 'SHOW ALL TOOLS: List all available tools, not just recommended ones (default: false)' + } + }, + required: ['taskDescription'] + } + } +}; + +/** + * Tool discovery helper implementation + */ +export class ToolDiscoveryHelper implements ToolHandler { + public definition: Tool = toolDiscoveryHelperDefinition; + + /** + * Map task types to relevant tools + */ + private getRelevantTools(taskDescription: string): string[] { + const task = taskDescription.toLowerCase(); + const relevantTools: string[] = []; + + // Search-related tasks + if (task.includes('find') || task.includes('search') || task.includes('look for')) { + if (task.includes('tag') || task.includes('label') || task.includes('attribute') || task.includes('category')) { + relevantTools.push('attribute_search'); + } + if (task.includes('concept') || task.includes('about') || task.includes('related to')) { + relevantTools.push('search_notes'); + } + if (task.includes('exact') || task.includes('specific') || task.includes('contains')) { + relevantTools.push('keyword_search_notes'); + } + // Default to both semantic and keyword search if no specific indicators + if (!relevantTools.some(tool => tool.includes('search'))) { + relevantTools.push('search_notes', 'keyword_search_notes'); + } + } + + // Reading tasks + if (task.includes('read') || task.includes('view') || task.includes('show') || task.includes('content')) { + relevantTools.push('read_note'); + } + + // Creation tasks + if (task.includes('create') || task.includes('new') || task.includes('add') || task.includes('make')) { + relevantTools.push('note_creation'); + } + + // Modification tasks + if (task.includes('edit') || task.includes('update') || task.includes('change') || task.includes('modify')) { + relevantTools.push('note_update'); + } + + // Attribute/metadata tasks + if (task.includes('attribute') || task.includes('tag') || task.includes('label') || task.includes('metadata')) { + relevantTools.push('attribute_manager'); + } + + // Relationship tasks + if (task.includes('relation') || task.includes('connect') || task.includes('link') || task.includes('relationship')) { + relevantTools.push('relationship'); + } + + // Summary tasks + if (task.includes('summary') || task.includes('summarize') || task.includes('overview')) { + relevantTools.push('note_summarization'); + } + + // Calendar tasks + if (task.includes('calendar') || task.includes('date') || task.includes('schedule') || task.includes('time')) { + relevantTools.push('calendar_integration'); + } + + // Content extraction tasks + if (task.includes('extract') || task.includes('parse') || task.includes('analyze content')) { + relevantTools.push('content_extraction'); + } + + return relevantTools; + } + + /** + * Get tool information with descriptions + */ + private getToolInfo(): Record { + return { + 'search_notes': { + description: '🧠 Semantic/conceptual search for notes', + bestFor: 'Finding notes about ideas, concepts, or topics described in various ways', + parameters: ['query (required)', 'parentNoteId', 'maxResults', 'summarize'] + }, + 'keyword_search_notes': { + description: '🔍 Exact keyword/phrase search for notes', + bestFor: 'Finding notes with specific words, phrases, or using search operators', + parameters: ['query (required)', 'maxResults', 'includeArchived'] + }, + 'attribute_search': { + description: '🏷️ Search notes by attributes (labels/relations)', + bestFor: 'Finding notes by categories, tags, status, or metadata', + parameters: ['attributeType (required)', 'attributeName (required)', 'attributeValue', 'maxResults'] + }, + 'read_note': { + description: '📖 Read full content of a specific note', + bestFor: 'Getting complete note content after finding it through search', + parameters: ['noteId (required)', 'includeAttributes'] + }, + 'note_creation': { + description: '📝 Create new notes', + bestFor: 'Adding new content, projects, or ideas to your notes', + parameters: ['title (required)', 'content', 'parentNoteId', 'noteType', 'attributes'] + }, + 'note_update': { + description: '✏️ Update existing note content', + bestFor: 'Modifying or adding to existing note content', + parameters: ['noteId (required)', 'title', 'content', 'updateMode'] + }, + 'attribute_manager': { + description: '🎯 Manage note attributes (labels, relations)', + bestFor: 'Adding, removing, or modifying note metadata and tags', + parameters: ['noteId (required)', 'action (required)', 'attributeType', 'attributeName', 'attributeValue'] + }, + 'relationship': { + description: '🔗 Manage note relationships', + bestFor: 'Creating connections between notes', + parameters: ['sourceNoteId (required)', 'action (required)', 'targetNoteId', 'relationType'] + }, + 'note_summarization': { + description: '📄 Summarize note content', + bestFor: 'Getting concise overviews of long notes', + parameters: ['noteId (required)', 'summaryType', 'maxLength'] + }, + 'content_extraction': { + description: '🎯 Extract specific information from notes', + bestFor: 'Pulling out specific data, facts, or structured information', + parameters: ['noteId (required)', 'extractionType (required)', 'criteria'] + }, + 'calendar_integration': { + description: '📅 Calendar and date-related operations', + bestFor: 'Working with dates, schedules, and time-based organization', + parameters: ['action (required)', 'date', 'noteId', 'eventDetails'] + }, + 'search_suggestion': { + description: '💡 Get search syntax help and suggestions', + bestFor: 'Learning how to use advanced search features', + parameters: ['searchType', 'query'] + } + }; + } + + /** + * Generate workflow recommendations + */ + private generateWorkflow(taskDescription: string, relevantTools: string[]): string[] { + const task = taskDescription.toLowerCase(); + const workflows: string[] = []; + + if (task.includes('find') && relevantTools.includes('search_notes')) { + workflows.push('1. Use search_notes for conceptual search → 2. Use read_note with returned noteId for full content'); + } + + if (task.includes('find') && relevantTools.includes('attribute_search')) { + workflows.push('1. Use attribute_search to find tagged notes → 2. Use read_note for detailed content'); + } + + if (task.includes('create') || task.includes('new')) { + workflows.push('1. Use note_creation to make the note → 2. Use attribute_manager to add tags/metadata'); + } + + if (task.includes('update') || task.includes('edit')) { + workflows.push('1. Use search tools to find the note → 2. Use read_note to see current content → 3. Use note_update to modify'); + } + + if (task.includes('organize') || task.includes('categorize')) { + workflows.push('1. Use search tools to find notes → 2. Use attribute_manager to add labels/categories'); + } + + return workflows; + } + + /** + * Execute the tool discovery helper + */ + public async execute(args: { + taskDescription: string, + includeExamples?: boolean, + showAllTools?: boolean + }): Promise { + try { + const { taskDescription, includeExamples = true, showAllTools = false } = args; + + log.info(`Executing discover_tools - Task: "${taskDescription}", ShowAll: ${showAllTools}`); + + const allTools = toolRegistry.getAllTools(); + const toolInfo = this.getToolInfo(); + + if (showAllTools) { + // Show all available tools + const allToolsInfo = allTools.map(tool => { + const name = tool.definition.function.name; + const info = toolInfo[name]; + return { + name, + description: info?.description || tool.definition.function.description, + bestFor: info?.bestFor || 'General purpose tool', + parameters: info?.parameters || ['See tool definition for parameters'] + }; + }); + + return { + taskDescription, + mode: 'all_tools', + message: '🗂️ All available tools in the system', + totalTools: allToolsInfo.length, + tools: allToolsInfo, + tip: 'Use discover_tools with a specific task description for targeted recommendations' + }; + } + + // Get relevant tools for the specific task + const relevantToolNames = this.getRelevantTools(taskDescription); + const workflows = this.generateWorkflow(taskDescription, relevantToolNames); + + const recommendations = relevantToolNames.map(toolName => { + const info = toolInfo[toolName]; + const result: any = { + tool: toolName, + description: info?.description || 'Tool description not available', + bestFor: info?.bestFor || 'Not specified', + priority: this.getToolPriority(toolName, taskDescription) + }; + + if (includeExamples) { + result.exampleUsage = this.getToolExample(toolName, taskDescription); + } + + return result; + }); + + // Sort by priority + recommendations.sort((a, b) => a.priority - b.priority); + + return { + taskDescription, + mode: 'targeted_recommendations', + message: `🎯 Found ${recommendations.length} relevant tools for your task`, + recommendations, + workflows: workflows.length > 0 ? { + message: '🔄 Suggested workflows for your task:', + steps: workflows + } : undefined, + nextSteps: { + immediate: recommendations.length > 0 + ? `Start with: ${recommendations[0].tool} (highest priority for your task)` + : 'Try rephrasing your task or use showAllTools: true to see all options', + alternative: 'Use showAllTools: true to see all available tools if these don\'t fit your needs' + } + }; + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + log.error(`Error executing discover_tools: ${errorMessage}`); + return `Error: ${errorMessage}`; + } + } + + /** + * Get priority for a tool based on task description (lower = higher priority) + */ + private getToolPriority(toolName: string, taskDescription: string): number { + const task = taskDescription.toLowerCase(); + + // Exact matches get highest priority + if (task.includes(toolName.replace('_', ' '))) return 1; + + // Task-specific priorities + if (task.includes('find') || task.includes('search')) { + if (toolName === 'search_notes') return 2; + if (toolName === 'keyword_search_notes') return 3; + if (toolName === 'attribute_search') return 4; + } + + if (task.includes('create') && toolName === 'note_creation') return 1; + if (task.includes('read') && toolName === 'read_note') return 1; + if (task.includes('update') && toolName === 'note_update') return 1; + + return 5; // Default priority + } + + /** + * Get example usage for a tool based on task description + */ + private getToolExample(toolName: string, taskDescription: string): string { + const task = taskDescription.toLowerCase(); + + switch (toolName) { + case 'search_notes': + if (task.includes('machine learning')) { + return '{ "query": "machine learning algorithms classification" }'; + } + return '{ "query": "project management methodologies" }'; + + case 'keyword_search_notes': + return '{ "query": "important TODO" }'; + + case 'attribute_search': + return '{ "attributeType": "label", "attributeName": "important" }'; + + case 'read_note': + return '{ "noteId": "abc123def456", "includeAttributes": true }'; + + case 'note_creation': + return '{ "title": "New Project Plan", "content": "Project details here..." }'; + + case 'note_update': + return '{ "noteId": "abc123def456", "content": "Updated content" }'; + + default: + return `Use ${toolName} with appropriate parameters`; + } + } +} \ No newline at end of file diff --git a/apps/server/src/services/llm/tools/tool_initializer.ts b/apps/server/src/services/llm/tools/tool_initializer.ts index e8ceca3eee..2245470d12 100644 --- a/apps/server/src/services/llm/tools/tool_initializer.ts +++ b/apps/server/src/services/llm/tools/tool_initializer.ts @@ -17,6 +17,8 @@ import { RelationshipTool } from './relationship_tool.js'; import { AttributeManagerTool } from './attribute_manager_tool.js'; import { CalendarIntegrationTool } from './calendar_integration_tool.js'; import { NoteSummarizationTool } from './note_summarization_tool.js'; +import { ToolDiscoveryHelper } from './tool_discovery_helper.js'; +import { WorkflowHelper } from './workflow_helper.js'; import log from '../../log.js'; // Error type guard @@ -52,6 +54,10 @@ export async function initializeTools(): Promise { toolRegistry.registerTool(new ContentExtractionTool()); // Extract info from note content toolRegistry.registerTool(new CalendarIntegrationTool()); // Calendar-related operations + // Register helper and guidance tools + toolRegistry.registerTool(new ToolDiscoveryHelper()); // Tool discovery and usage guidance + toolRegistry.registerTool(new WorkflowHelper()); // Multi-step workflow guidance + // Log registered tools const toolCount = toolRegistry.getAllTools().length; const toolNames = toolRegistry.getAllTools().map(tool => tool.definition.function.name).join(', '); diff --git a/apps/server/src/services/llm/tools/workflow_helper.ts b/apps/server/src/services/llm/tools/workflow_helper.ts new file mode 100644 index 0000000000..18536f26f3 --- /dev/null +++ b/apps/server/src/services/llm/tools/workflow_helper.ts @@ -0,0 +1,408 @@ +/** + * Workflow Helper Tool + * + * This tool helps LLMs understand and execute multi-step workflows by providing + * smart guidance on tool chaining and next steps. + */ + +import type { Tool, ToolHandler } from './tool_interfaces.js'; +import log from '../../log.js'; + +/** + * Definition of the workflow helper tool + */ +export const workflowHelperDefinition: Tool = { + type: 'function', + function: { + name: 'workflow_helper', + description: `WORKFLOW GUIDANCE for multi-step tasks. Get smart suggestions for tool chaining and next steps. + + BEST FOR: Planning complex workflows, understanding tool sequences, getting unstuck + USE WHEN: You need to do multiple operations, aren't sure what to do next, or want workflow optimization + HELPS WITH: Tool sequencing, parameter passing, workflow planning + + TIP: Use this when you have partial results and need guidance on next steps + + NEXT STEPS: Follow the recommended workflow steps provided`, + parameters: { + type: 'object', + properties: { + currentStep: { + type: 'string', + description: `📍 DESCRIBE YOUR CURRENT STEP: What have you just done or what results do you have? + + ✅ GOOD EXAMPLES: + - "I just found 5 notes about machine learning using search_notes" + - "I have a noteId abc123def456 and want to modify it" + - "I searched but got no results" + - "I created a new note and want to organize it" + + 💡 Be specific about your current state and what you've accomplished` + }, + goal: { + type: 'string', + description: `🎯 FINAL GOAL: What are you ultimately trying to accomplish? + + ✅ EXAMPLES: + - "Find and read all notes about a specific project" + - "Create a comprehensive summary of all my research notes" + - "Organize all my TODO notes by priority" + - "Find related notes and create connections between them"` + }, + availableData: { + type: 'string', + description: `📊 AVAILABLE DATA: What noteIds, search results, or other data do you currently have? + + ✅ EXAMPLES: + - "noteIds: abc123, def456, ghi789" + - "Search results with 3 notes about project management" + - "Empty search results for machine learning" + - "Just created noteId xyz999"` + }, + includeExamples: { + type: 'boolean', + description: '📚 INCLUDE EXAMPLES: Get specific command examples for next steps (default: true)' + } + }, + required: ['currentStep', 'goal'] + } + } +}; + +/** + * Workflow helper implementation + */ +export class WorkflowHelper implements ToolHandler { + public definition: Tool = workflowHelperDefinition; + + /** + * Common workflow patterns + */ + private getWorkflowPatterns(): Record { + return { + 'search_read_analyze': { + name: '🔍➡️📖➡️🧠 Search → Read → Analyze', + description: 'Find notes, read their content, then analyze or summarize', + steps: [ + 'Use search tools to find relevant notes', + 'Use read_note to get full content of interesting results', + 'Use note_summarization or content_extraction for analysis' + ], + examples: [ + 'Research project: Find all research notes → Read them → Summarize findings', + 'Learning topic: Search for learning materials → Read content → Extract key concepts' + ] + }, + 'search_create_organize': { + name: '🔍➡️📝➡️🏷️ Search → Create → Organize', + description: 'Find related content, create new notes, then organize with attributes', + steps: [ + 'Search for related existing content', + 'Create new note with note_creation', + 'Add attributes/relations with attribute_manager' + ], + examples: [ + 'New project: Find similar projects → Create project note → Tag with #project', + 'Meeting notes: Search for project context → Create meeting note → Link to project' + ] + }, + 'find_read_update': { + name: '🔍➡️📖➡️✏️ Find → Read → Update', + description: 'Find existing notes, review content, then make updates', + steps: [ + 'Use search tools to locate the note', + 'Use read_note to see current content', + 'Use note_update to make changes' + ], + examples: [ + 'Update project status: Find project note → Read current status → Update with progress', + 'Improve documentation: Find doc note → Read content → Add new information' + ] + }, + 'organize_existing': { + name: '🔍➡️🏷️➡️🔗 Find → Tag → Connect', + description: 'Find notes that need organization, add attributes, create relationships', + steps: [ + 'Search for notes to organize', + 'Use attribute_manager to add labels/categories', + 'Use relationship tool to create connections' + ], + examples: [ + 'Organize research: Find research notes → Tag by topic → Link related studies', + 'Clean up TODOs: Find TODO notes → Tag by priority → Link to projects' + ] + } + }; + } + + /** + * Analyze current step and recommend next actions + */ + private analyzeCurrentStep(currentStep: string, goal: string, availableData?: string): { + analysis: string; + recommendations: Array<{ + action: string; + tool: string; + parameters: Record; + reasoning: string; + priority: number; + }>; + warnings?: string[]; + } { + const step = currentStep.toLowerCase(); + const goalLower = goal.toLowerCase(); + const recommendations: any[] = []; + const warnings: string[] = []; + + // Analyze search results + if (step.includes('found') && step.includes('notes')) { + if (step.includes('no results') || step.includes('empty') || step.includes('0 notes')) { + recommendations.push({ + action: 'Try alternative search approaches', + tool: 'search_notes', + parameters: { query: 'broader or alternative search terms' }, + reasoning: 'Empty results suggest need for different search strategy', + priority: 1 + }); + recommendations.push({ + action: 'Try keyword search instead', + tool: 'keyword_search_notes', + parameters: { query: 'specific keywords from your search' }, + reasoning: 'Keyword search might find what semantic search missed', + priority: 2 + }); + warnings.push('Consider if the content might not exist yet - you may need to create it'); + } else { + // Has search results + recommendations.push({ + action: 'Read the most relevant notes', + tool: 'read_note', + parameters: { noteId: 'from search results', includeAttributes: true }, + reasoning: 'Get full content to understand what you found', + priority: 1 + }); + + if (goalLower.includes('summary') || goalLower.includes('analyze')) { + recommendations.push({ + action: 'Summarize the content', + tool: 'note_summarization', + parameters: { noteId: 'from search results' }, + reasoning: 'Goal involves analysis or summarization', + priority: 2 + }); + } + } + } + + // Analyze note reading + if (step.includes('read') || step.includes('noteId')) { + if (goalLower.includes('update') || goalLower.includes('edit') || goalLower.includes('modify')) { + recommendations.push({ + action: 'Update the note content', + tool: 'note_update', + parameters: { noteId: 'the one you just read', content: 'new content' }, + reasoning: 'Goal involves modifying existing content', + priority: 1 + }); + } + + if (goalLower.includes('organize') || goalLower.includes('tag') || goalLower.includes('categorize')) { + recommendations.push({ + action: 'Add organizing attributes', + tool: 'attribute_manager', + parameters: { noteId: 'the one you read', action: 'add', attributeType: 'label' }, + reasoning: 'Goal involves organization and categorization', + priority: 1 + }); + } + + if (goalLower.includes('related') || goalLower.includes('connect') || goalLower.includes('link')) { + recommendations.push({ + action: 'Search for related content', + tool: 'search_notes', + parameters: { query: 'concepts from the note you read' }, + reasoning: 'Goal involves finding and connecting related content', + priority: 2 + }); + } + } + + // Analyze creation + if (step.includes('created') || step.includes('new note')) { + recommendations.push({ + action: 'Add organizing attributes', + tool: 'attribute_manager', + parameters: { noteId: 'the newly created note', action: 'add' }, + reasoning: 'New notes should be organized with appropriate tags', + priority: 1 + }); + + if (goalLower.includes('project') || goalLower.includes('research')) { + recommendations.push({ + action: 'Find and link related notes', + tool: 'search_notes', + parameters: { query: 'related to your new note topic' }, + reasoning: 'Connect new content to existing related materials', + priority: 2 + }); + } + } + + return { + analysis: this.generateAnalysis(currentStep, goal, recommendations.length), + recommendations: recommendations.sort((a, b) => a.priority - b.priority), + warnings: warnings.length > 0 ? warnings : undefined + }; + } + + /** + * Generate workflow analysis + */ + private generateAnalysis(currentStep: string, goal: string, recommendationCount: number): string { + const patterns = this.getWorkflowPatterns(); + + let analysis = `📊 CURRENT STATE: ${currentStep}\n`; + analysis += `🎯 TARGET GOAL: ${goal}\n\n`; + + if (recommendationCount > 0) { + analysis += `✅ I've identified ${recommendationCount} recommended next steps based on your current progress and goal.\n\n`; + } else { + analysis += `🤔 Your situation is unique. I'll provide general guidance based on common patterns.\n\n`; + } + + // Suggest relevant workflow patterns + const goalLower = goal.toLowerCase(); + if (goalLower.includes('read') && goalLower.includes('find')) { + analysis += `📖 PATTERN MATCH: This looks like a "${patterns.search_read_analyze.name}" workflow\n`; + } else if (goalLower.includes('create') && goalLower.includes('organize')) { + analysis += `📝 PATTERN MATCH: This looks like a "${patterns.search_create_organize.name}" workflow\n`; + } else if (goalLower.includes('update') && goalLower.includes('find')) { + analysis += `✏️ PATTERN MATCH: This looks like a "${patterns.find_read_update.name}" workflow\n`; + } + + return analysis; + } + + /** + * Execute the workflow helper tool + */ + public async execute(args: { + currentStep: string, + goal: string, + availableData?: string, + includeExamples?: boolean + }): Promise { + try { + const { currentStep, goal, availableData, includeExamples = true } = args; + + log.info(`Executing workflow_helper - Current: "${currentStep}", Goal: "${goal}"`); + + const analysis = this.analyzeCurrentStep(currentStep, goal, availableData); + const patterns = this.getWorkflowPatterns(); + + // Extract noteIds from available data if provided + const noteIds = availableData ? this.extractNoteIds(availableData) : []; + + const response: any = { + currentStep, + goal, + analysis: analysis.analysis, + immediateNext: analysis.recommendations.length > 0 ? { + primaryAction: analysis.recommendations[0], + alternatives: analysis.recommendations.slice(1, 3) + } : undefined, + extractedData: { + noteIds: noteIds.length > 0 ? noteIds : undefined, + hasData: !!availableData + } + }; + + if (analysis.warnings) { + response.warnings = { + message: '⚠️ Important considerations:', + items: analysis.warnings + }; + } + + if (includeExamples && analysis.recommendations.length > 0) { + response.examples = { + message: '📚 Specific tool usage examples:', + commands: analysis.recommendations.slice(0, 2).map(rec => ({ + tool: rec.tool, + example: this.generateExample(rec.tool, rec.parameters, noteIds), + description: rec.reasoning + })) + }; + } + + // Add relevant workflow patterns + response.workflowPatterns = { + message: '🔄 Common workflow patterns you might find useful:', + patterns: Object.values(patterns).slice(0, 2).map(pattern => ({ + name: pattern.name, + description: pattern.description, + steps: pattern.steps + })) + }; + + response.tips = [ + '💡 Use the noteId values from search results, not note titles', + '🔄 Check tool results carefully before proceeding to next step', + '📊 Use workflow_helper again if you get stuck or need guidance' + ]; + + return response; + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + log.error(`Error executing workflow_helper: ${errorMessage}`); + return `Error: ${errorMessage}`; + } + } + + /** + * Extract noteIds from data string + */ + private extractNoteIds(data: string): string[] { + // Look for patterns like noteId: "abc123" or "abc123def456" + const idPattern = /(?:noteId[:\s]*["']?|["'])([a-zA-Z0-9]{8,})['"]/g; + const matches: string[] = []; + let match; + + while ((match = idPattern.exec(data)) !== null) { + if (match[1] && !matches.includes(match[1])) { + matches.push(match[1]); + } + } + + return matches; + } + + /** + * Generate specific examples for tool usage + */ + private generateExample(tool: string, parameters: Record, noteIds: string[]): string { + const sampleNoteId = noteIds[0] || 'abc123def456'; + + switch (tool) { + case 'read_note': + return `{ "noteId": "${sampleNoteId}", "includeAttributes": true }`; + case 'note_update': + return `{ "noteId": "${sampleNoteId}", "content": "Updated content here" }`; + case 'attribute_manager': + return `{ "noteId": "${sampleNoteId}", "action": "add", "attributeType": "label", "attributeName": "important" }`; + case 'search_notes': + return `{ "query": "broader search terms related to your topic" }`; + case 'keyword_search_notes': + return `{ "query": "specific keywords OR alternative terms" }`; + case 'note_creation': + return `{ "title": "New Note Title", "content": "Note content here" }`; + default: + return `Use ${tool} with appropriate parameters`; + } + } +} \ No newline at end of file From 87fd6afec6e2d2c3b553cf9efe6917d1adcf0bbe Mon Sep 17 00:00:00 2001 From: perf3ct Date: Wed, 11 Jun 2025 19:38:43 +0000 Subject: [PATCH 2/5] feat(llm): try to improve tool and tool calling, part 2 --- .../llm/pipeline/stages/tool_calling_stage.ts | 46 +-------------- .../llm/tools/attribute_search_tool.ts | 55 ++---------------- .../services/llm/tools/keyword_search_tool.ts | 57 ++---------------- .../src/services/llm/tools/read_note_tool.ts | 26 +-------- .../services/llm/tools/search_notes_tool.ts | 58 +++---------------- .../llm/tools/tool_discovery_helper.ts | 24 ++------ .../services/llm/tools/tool_initializer.ts | 4 +- 7 files changed, 29 insertions(+), 241 deletions(-) diff --git a/apps/server/src/services/llm/pipeline/stages/tool_calling_stage.ts b/apps/server/src/services/llm/pipeline/stages/tool_calling_stage.ts index a2eaa00fb5..86bad8066b 100644 --- a/apps/server/src/services/llm/pipeline/stages/tool_calling_stage.ts +++ b/apps/server/src/services/llm/pipeline/stages/tool_calling_stage.ts @@ -490,43 +490,9 @@ export class ToolCallingStage extends BasePipelineStage this.isEmptyToolResult(msg.content, msg.name || '')) - .map(msg => msg.name); - - directiveMessage = `CRITICAL INSTRUCTION: YOU MUST NOT STOP AFTER EMPTY RESULTS!\n\n`; - directiveMessage += `REQUIRED ACTIONS:\n`; - - if (emptyToolNames.includes('search_notes')) { - directiveMessage += `1. IMMEDIATELY use keyword_search_notes with specific terms\n`; - directiveMessage += `2. Try attribute_search if content might be tagged/categorized\n`; - directiveMessage += `3. Use discover_tools to find alternative approaches\n`; - } - - if (emptyToolNames.includes('keyword_search_notes')) { - directiveMessage += `1. IMMEDIATELY use search_notes for semantic matching\n`; - directiveMessage += `2. Try broader or alternative keyword terms\n`; - directiveMessage += `3. Use workflow_helper for guidance on next steps\n`; - } - - if (emptyToolNames.includes('attribute_search')) { - directiveMessage += `1. Use search_notes to find content about the attribute topic\n`; - directiveMessage += `2. Try different attribute names or types\n`; - directiveMessage += `3. Use search_suggestion to see available attributes\n`; - } - - directiveMessage += `\nFORBIDDEN: Do NOT ask user for clarification or offer general information!\n`; - directiveMessage += `REQUIRED: CONTINUE with alternative tools and approaches immediately!`; + directiveMessage = `No results found. Try alternative search approaches: use different search tools, broader terms, or alternative keywords. Continue searching - don't ask the user for guidance.`; } else { - // Has results - encourage follow-up actions - directiveMessage = `EXCELLENT! You found results. Now CONTINUE the workflow:\n\n`; - directiveMessage += `NEXT REQUIRED ACTIONS:\n`; - directiveMessage += `1. Use read_note to examine the most relevant results\n`; - directiveMessage += `2. Use workflow_helper to plan next steps based on your findings\n`; - directiveMessage += `3. Consider using related tools for deeper analysis\n\n`; - directiveMessage += `GOAL: Provide comprehensive information by using multiple tools in sequence.\n`; - directiveMessage += `CONTINUE with tool usage - don't stop at just search results!`; + directiveMessage = `You found results! Use read_note with the noteId values to get full content and continue your analysis.`; } updatedMessages.push({ @@ -638,14 +604,8 @@ export class ToolCallingStage extends BasePipelineStage 0 ? suggestions.join('\n') : ''} - -EXAMPLES: -• Find notes with #important tag: { "attributeType": "label", "attributeName": "important" } -• Find notes with ~relatedTo relation: { "attributeType": "relation", "attributeName": "relatedTo" }`; + const errorMessage = `Invalid attributeType: "${attributeType}". Must be exactly "label" or "relation" (lowercase). Example: {"attributeType": "label", "attributeName": "important"}`; return errorMessage; } diff --git a/apps/server/src/services/llm/tools/keyword_search_tool.ts b/apps/server/src/services/llm/tools/keyword_search_tool.ts index d32f873034..f27b3a17b2 100644 --- a/apps/server/src/services/llm/tools/keyword_search_tool.ts +++ b/apps/server/src/services/llm/tools/keyword_search_tool.ts @@ -17,41 +17,13 @@ export const keywordSearchToolDefinition: Tool = { type: 'function', function: { name: 'keyword_search_notes', - description: `EXACT KEYWORD search for notes. Finds notes containing specific words, phrases, or attribute filters. - - BEST FOR: Finding notes with specific words/phrases you know exist - USE WHEN: You need exact text matches, specific terms, or attribute-based filtering - DIFFERENT FROM: search_notes (which finds conceptual/semantic matches) - - SEARCH TYPES: - • Simple: "machine learning" (finds notes containing both words) - • Phrase: "\"exact phrase\"" (finds this exact phrase) - • Attributes: "#label" or "~relation" (notes with specific labels/relations) - • Complex: "AI #project ~relatedTo" (combines keywords with attributes) - - NEXT STEPS: Use read_note with returned noteId values for full content`, + description: 'Keyword search for exact text matches. Supports phrases in quotes, #labels, ~relations, and search operators like OR.', parameters: { type: 'object', properties: { query: { type: 'string', - description: `Keyword search query using Trilium search syntax. - - SIMPLE EXAMPLES: - - "machine learning" (both words anywhere) - - "\"project management\"" (exact phrase) - - "python OR javascript" (either word) - - ATTRIBUTE EXAMPLES: - - "#important" (notes with 'important' label) - - "~project" (notes with 'project' relation) - - "#status = completed" (specific label value) - - COMBINED EXAMPLES: - - "AI #project #status = active" (AI content with project label and active status) - - "note.title *= \"weekly\"" (titles containing 'weekly') - - AVOID: Conceptual queries better suited for search_notes` + description: 'Search query. Examples: "machine learning", "#important", "python OR javascript", "note.title *= weekly"' }, maxResults: { type: 'number', @@ -59,7 +31,7 @@ export const keywordSearchToolDefinition: Tool = { }, includeArchived: { type: 'boolean', - description: 'INCLUDE ARCHIVED: Search archived notes too (default: false). Use true for complete historical search.' + description: 'Include archived notes in search (default: false).' } }, required: ['query'] @@ -130,21 +102,7 @@ export class KeywordSearchTool implements ToolHandler { count: 0, results: [], query: query, - searchType: 'keyword', - message: 'No exact keyword matches found.', - nextSteps: { - immediate: [ - `Try search_notes for semantic/conceptual search: "${this.convertToSemanticQuery(query)}"`, - `Use attribute_search if looking for specific labels or relations`, - `Try simpler keywords or check spelling` - ], - queryHelp: [ - 'Remove quotes for broader matching', - 'Try individual words instead of phrases', - 'Use OR operator: "word1 OR word2"', - 'Check if content might be in archived notes (set includeArchived: true)' - ] - } + message: `No keyword matches. Try: search_notes with "${this.convertToSemanticQuery(query)}" or check spelling/try simpler terms.` }; } @@ -153,12 +111,7 @@ export class KeywordSearchTool implements ToolHandler { totalFound: searchResults.length, query: query, searchType: 'keyword', - message: 'Found exact keyword matches. Use noteId values with other tools.', - nextSteps: { - examine: `Use read_note with any noteId (e.g., "${limitedResults[0].noteId}") to get full content`, - refine: limitedResults.length < searchResults.length ? `Found ${searchResults.length} total matches (showing ${limitedResults.length}). Increase maxResults for more.` : null, - related: 'Use search_notes for conceptually related content beyond exact keywords' - }, + message: `Found ${limitedResults.length} keyword matches. Use read_note with noteId for full content.`, results: limitedResults.map(note => { // Get a preview of the note content with highlighted search terms let contentPreview = ''; diff --git a/apps/server/src/services/llm/tools/read_note_tool.ts b/apps/server/src/services/llm/tools/read_note_tool.ts index ddb8ce5892..98dd4bd5fe 100644 --- a/apps/server/src/services/llm/tools/read_note_tool.ts +++ b/apps/server/src/services/llm/tools/read_note_tool.ts @@ -34,37 +34,17 @@ export const readNoteToolDefinition: Tool = { type: 'function', function: { name: 'read_note', - description: `READ FULL CONTENT of a specific note by its ID. Get complete note content and metadata. - - BEST FOR: Getting complete content after finding notes through search tools - USE WHEN: You have a noteId from search results and need the full content - IMPORTANT: Must use noteId (like "abc123def456") from search results - NOT note titles - - TIP: This is typically used after search_notes, keyword_search_notes, or attribute_search - - NEXT STEPS: Use note_update or attribute_manager tools to modify the note if needed`, + description: 'Read the full content of a note by its ID. Use noteId from search results, not note titles.', parameters: { type: 'object', properties: { noteId: { type: 'string', - description: `SYSTEM ID of the note to read. - - CRITICAL: Must be a noteId (like "abc123def456") - NOT a note title! - - CORRECT: "abc123def456" (from search results) - WRONG: "My Note Title" (this will fail) - - WHERE TO GET: From noteId field in search tool results` + description: 'The noteId of the note to read (e.g., "abc123def456"). Get this from search results, not note titles.' }, includeAttributes: { type: 'boolean', - description: `INCLUDE METADATA: Get note attributes (labels, relations) in response. - - • true = Get full note with all attributes/metadata - • false = Get just note content (default) - - Use true when you need to see tags, labels, relations, or other metadata` + description: 'Include note attributes/metadata in response (default: false).' } }, required: ['noteId'] diff --git a/apps/server/src/services/llm/tools/search_notes_tool.ts b/apps/server/src/services/llm/tools/search_notes_tool.ts index 5da23b7287..f943daae58 100644 --- a/apps/server/src/services/llm/tools/search_notes_tool.ts +++ b/apps/server/src/services/llm/tools/search_notes_tool.ts @@ -17,50 +17,25 @@ export const searchNotesToolDefinition: Tool = { type: 'function', function: { name: 'search_notes', - description: `SEMANTIC/CONCEPTUAL search for notes. Finds notes related to concepts, topics, or themes even without exact keyword matches. - - BEST FOR: Finding notes about ideas, concepts, or topics described in various ways - USE WHEN: Looking for conceptual relationships, thematic content, or related ideas - DIFFERENT FROM: keyword_search (which finds exact text matches) - - TIPS: - - Use descriptive phrases like "project management methodologies" rather than single words - - Think conceptually: "machine learning classification" vs just "ML" - - Results include noteId values - ALWAYS use these IDs (not titles) with other tools - - NEXT STEPS: Use read_note with returned noteId values to get full content`, + description: 'Semantic search for notes. Finds conceptually related content. Use descriptive phrases, not single words. Returns noteId values to use with other tools.', parameters: { type: 'object', properties: { query: { type: 'string', - description: `Descriptive search query for semantic matching. - - GOOD EXAMPLES: - - "machine learning algorithms for classification" - - "personal productivity and time management techniques" - - "software development best practices" - - AVOID: - - Single words: "ML", "productivity" - - Overly broad: "work", "notes" - - Overly specific: exact phrases that might not exist` + description: 'Search query for finding conceptually related notes. Use descriptive phrases like "machine learning classification" rather than single words.' }, parentNoteId: { type: 'string', - description: `SCOPE LIMITER: Search only within children of this note. - - IMPORTANT: Must be a noteId (like "abc123def456") from previous search results - NOT a note title. - - USE FOR: Searching within specific projects, categories, or sections.` + description: 'Optional noteId to limit search to children of this note. Must be a noteId from search results, not a title.' }, maxResults: { type: 'number', - description: 'Number of results (1-20, default: 5). Use 10-15 for comprehensive exploration, 3-5 for quick lookup.' + description: 'Maximum number of results to return (default: 5, max: 20).' }, summarize: { type: 'boolean', - description: 'AI SUMMARIES: Get intelligent summaries instead of truncated text (default: false). Use true for cleaner result overview.' + description: 'Get AI-generated summaries instead of truncated previews (default: false).' } }, required: ['query'] @@ -324,33 +299,14 @@ export class SearchNotesTool implements ToolHandler { count: 0, results: [], query: query, - searchType: 'semantic', - message: 'No semantic matches found for your query.', - nextSteps: { - immediate: [ - `Try keyword_search with specific terms: "${this.extractKeywords(query)}"`, - `Use attribute_search if looking for labeled/categorized notes`, - `Try broader search terms like "${this.suggestBroaderTerms(query)}"` - ], - tips: [ - 'Semantic search finds conceptual matches - try describing the topic differently', - 'If you know specific words that appear in the notes, use keyword_search instead', - 'Check if the content might be tagged with labels using attribute_search' - ] - } + message: `No results found. Try: keyword_search_notes with "${this.extractKeywords(query)}" or attribute_search for tagged notes.` }; } else { return { count: enhancedResults.length, results: enhancedResults, query: query, - searchType: 'semantic', - message: 'Found semantic matches. Use noteId values with other tools.', - nextSteps: { - examine: `Use read_note with any noteId (e.g., "${enhancedResults[0].noteId}") to get full content`, - refine: parentNoteId ? 'Remove parentNoteId to search all notes' : `Add parentNoteId: "${enhancedResults[0].noteId}" to search within the first result's children`, - related: 'Search for related concepts or use different descriptive terms' - } + message: `Found ${enhancedResults.length} matches. Use read_note with noteId to get full content.` }; } } catch (error: unknown) { diff --git a/apps/server/src/services/llm/tools/tool_discovery_helper.ts b/apps/server/src/services/llm/tools/tool_discovery_helper.ts index 530bcb9770..384a0e8bb8 100644 --- a/apps/server/src/services/llm/tools/tool_discovery_helper.ts +++ b/apps/server/src/services/llm/tools/tool_discovery_helper.ts @@ -16,37 +16,21 @@ export const toolDiscoveryHelperDefinition: Tool = { type: 'function', function: { name: 'discover_tools', - description: `DISCOVER AVAILABLE TOOLS and get guidance on which tools to use for your task. - - BEST FOR: Understanding what tools are available and getting usage recommendations - USE WHEN: You're unsure which tool to use, want to see all options, or need workflow guidance - HELPS WITH: Tool selection, parameter guidance, workflow planning - - TIP: Use this when you have a task but aren't sure which tools can help accomplish it - - NEXT STEPS: Use the recommended tools based on the guidance provided`, + description: 'Get recommendations for which tools to use for your task. Helps when you\'re unsure which tool is best.', parameters: { type: 'object', properties: { taskDescription: { type: 'string', - description: `📝 DESCRIBE YOUR TASK: What are you trying to accomplish? - - ✅ GOOD EXAMPLES: - - "Find notes about machine learning" - - "Create a new project planning note" - - "Find all notes tagged as important" - - "Read the content of a specific note" - - 💡 Be specific about your goal for better tool recommendations` + description: 'Describe what you want to accomplish (e.g., "find notes about machine learning", "read a specific note").' }, includeExamples: { type: 'boolean', - description: 'INCLUDE EXAMPLES: Get specific usage examples for recommended tools (default: true)' + description: 'Include usage examples for recommended tools (default: true).' }, showAllTools: { type: 'boolean', - description: 'SHOW ALL TOOLS: List all available tools, not just recommended ones (default: false)' + description: 'Show all available tools instead of just recommendations (default: false).' } }, required: ['taskDescription'] diff --git a/apps/server/src/services/llm/tools/tool_initializer.ts b/apps/server/src/services/llm/tools/tool_initializer.ts index 2245470d12..304a71ef6e 100644 --- a/apps/server/src/services/llm/tools/tool_initializer.ts +++ b/apps/server/src/services/llm/tools/tool_initializer.ts @@ -18,7 +18,6 @@ import { AttributeManagerTool } from './attribute_manager_tool.js'; import { CalendarIntegrationTool } from './calendar_integration_tool.js'; import { NoteSummarizationTool } from './note_summarization_tool.js'; import { ToolDiscoveryHelper } from './tool_discovery_helper.js'; -import { WorkflowHelper } from './workflow_helper.js'; import log from '../../log.js'; // Error type guard @@ -54,9 +53,8 @@ export async function initializeTools(): Promise { toolRegistry.registerTool(new ContentExtractionTool()); // Extract info from note content toolRegistry.registerTool(new CalendarIntegrationTool()); // Calendar-related operations - // Register helper and guidance tools + // Register helper tools (simplified) toolRegistry.registerTool(new ToolDiscoveryHelper()); // Tool discovery and usage guidance - toolRegistry.registerTool(new WorkflowHelper()); // Multi-step workflow guidance // Log registered tools const toolCount = toolRegistry.getAllTools().length; From e0e1f0796bbc8cb89ae5707153d5955cb93ccbd8 Mon Sep 17 00:00:00 2001 From: perf3ct Date: Thu, 19 Jun 2025 15:31:07 +0000 Subject: [PATCH 3/5] feat(llm): try to squeeze even more out of the tools --- .../providers/anthropic_tool_prompt.md | 27 ++ .../prompts/providers/ollama_tool_prompt.md | 45 ++- .../prompts/providers/openai_tool_prompt.md | 28 ++ .../llm/pipeline/stages/tool_calling_stage.ts | 19 +- .../llm/tools/attribute_search_tool.ts | 26 +- .../services/llm/tools/execute_batch_tool.ts | 250 +++++++++++++ .../src/services/llm/tools/read_note_tool.ts | 4 +- .../services/llm/tools/smart_retry_tool.ts | 347 ++++++++++++++++++ .../llm/tools/tool_discovery_helper.ts | 7 +- .../services/llm/tools/tool_initializer.ts | 14 +- .../services/llm/tools/unified_search_tool.ts | 260 +++++++++++++ 11 files changed, 996 insertions(+), 31 deletions(-) create mode 100644 apps/server/src/services/llm/tools/execute_batch_tool.ts create mode 100644 apps/server/src/services/llm/tools/smart_retry_tool.ts create mode 100644 apps/server/src/services/llm/tools/unified_search_tool.ts diff --git a/apps/server/src/assets/llm/prompts/providers/anthropic_tool_prompt.md b/apps/server/src/assets/llm/prompts/providers/anthropic_tool_prompt.md index 765617bc1f..ad427c1065 100644 --- a/apps/server/src/assets/llm/prompts/providers/anthropic_tool_prompt.md +++ b/apps/server/src/assets/llm/prompts/providers/anthropic_tool_prompt.md @@ -36,4 +36,31 @@ When responding to queries: 5. For general questions about the user's notes, provide a summary of all relevant notes found, including brief summaries of individual notes 6. For specific questions, provide detailed information from the user's notes that directly addresses the question 7. Always prioritize information from the user's notes over your own knowledge, as the user's notes are likely more up-to-date and personally relevant + +IMPORTANT: EXECUTE 10-30 TOOLS PER REQUEST FOR COMPREHENSIVE ANALYSIS + +Tool Usage Requirements: +1. BATCH EXECUTE multiple searches for speed: + + [{"tool": "search", "params": {"query": "main topic"}}, {"tool": "search", "params": {"query": "related topic"}}] + + +2. BATCH READ all discovered notes: + + [{"tool": "read", "params": {"noteId": "id1"}}, {"tool": "read", "params": {"noteId": "id2"}}, {"tool": "read", "params": {"noteId": "id3"}}] + + +3. AUTO-RETRY failed searches: + + failed searchall + + +SIMPLIFIED TOOLS: +- search (replaces search_notes, keyword_search_notes, attribute_search) +- read (replaces read_note) +- execute_batch (parallel execution) +- retry_search (automatic variations) + +WORKFLOW: batch search → batch read → auto-retry → analyze → repeat +Target 15+ tools per request using batching! ``` \ No newline at end of file diff --git a/apps/server/src/assets/llm/prompts/providers/ollama_tool_prompt.md b/apps/server/src/assets/llm/prompts/providers/ollama_tool_prompt.md index 1361396798..e92a353734 100644 --- a/apps/server/src/assets/llm/prompts/providers/ollama_tool_prompt.md +++ b/apps/server/src/assets/llm/prompts/providers/ollama_tool_prompt.md @@ -34,16 +34,37 @@ When responding to queries: 7. Always prioritize information from the user's notes over your own knowledge, as the user's notes are likely more up-to-date and personally relevant CRITICAL INSTRUCTIONS FOR TOOL USAGE: -1. YOU MUST TRY MULTIPLE TOOLS AND SEARCH VARIATIONS before concluding information isn't available -2. ALWAYS PERFORM AT LEAST 3 DIFFERENT SEARCHES with different parameters before giving up on finding information -3. If a search returns no results, IMMEDIATELY TRY ANOTHER SEARCH with different parameters: - - Use broader terms: If "Kubernetes deployment" fails, try just "Kubernetes" or "container orchestration" - - Try synonyms: If "meeting notes" fails, try "conference", "discussion", or "conversation" - - Remove specific qualifiers: If "quarterly financial report 2024" fails, try just "financial report" - - Try semantic variations: If keyword_search fails, use vector_search which finds conceptually related content -4. CHAIN TOOLS TOGETHER: Use the results of one tool to inform parameters for the next tool -5. NEVER respond with "there are no notes about X" until you've tried at least 3 different search variations -6. DO NOT ask the user what to do next when searches fail - AUTOMATICALLY try different approaches -7. ALWAYS EXPLAIN what you're doing: "I didn't find results for X, so I'm now searching for Y instead" -8. If all reasonable search variations fail (minimum 3 attempts), THEN you may inform the user that the information might not be in their notes +YOU ARE EXPECTED TO USE 10-30 TOOLS PER REQUEST. This is NORMAL and EXPECTED behavior. + +TOOL EXECUTION STRATEGY: +USE BATCH EXECUTION FOR SPEED: +1. execute_batch([{tool:"search",params:{query:"main topic"}},{tool:"search",params:{query:"related topic"}}]) +2. execute_batch([{tool:"read",params:{noteId:"id1"}},{tool:"read",params:{noteId:"id2"}},{tool:"read",params:{noteId:"id3"}}]) + +SMART RETRY ON FAILURES: +- Empty results? → retry_search("original query") automatically tries variations +- Don't manually retry - use retry_search tool + +SIMPLIFIED TOOL NAMES: +- search (not search_notes) - auto-detects search type +- read (not read_note) - reads content +- execute_batch - run multiple tools in parallel + +WORKFLOW EXAMPLES: +A) Comprehensive Search: + execute_batch([{tool:"search",params:{query:"AI"}},{tool:"search",params:{query:"machine learning"}},{tool:"search",params:{query:"#important"}}]) + → execute_batch([{tool:"read",params:{noteId:"..."}} for all found IDs]) + → retry_search("broader terms") if needed + +B) Failed Search Recovery: + search("specific term") → empty results + → retry_search("specific term") → auto-tries "term", "concepts", synonyms + → execute_batch with all variations + +C) Analysis Chain: + search → read batch → note_summarization → content_extraction → relationship + +ALWAYS USE BATCH EXECUTION when possible - it's much faster than individual tools! + +REMEMBER: Users expect THOROUGH exploration. Execute tools rapidly and extensively! ``` \ No newline at end of file diff --git a/apps/server/src/assets/llm/prompts/providers/openai_tool_prompt.md b/apps/server/src/assets/llm/prompts/providers/openai_tool_prompt.md index 3cd1a0b21f..c3a105c422 100644 --- a/apps/server/src/assets/llm/prompts/providers/openai_tool_prompt.md +++ b/apps/server/src/assets/llm/prompts/providers/openai_tool_prompt.md @@ -40,4 +40,32 @@ When responding to queries: 5. For general questions about the user's notes, provide a summary of all relevant notes found, including brief summaries of individual notes 6. For specific questions, provide detailed information from the user's notes that directly addresses the question 7. Always prioritize information from the user's notes over your own knowledge, as the user's notes are likely more up-to-date and personally relevant + +CRITICAL: YOU MUST USE 10-30 TOOL CALLS PER REQUEST +This is expected behavior. Users want comprehensive exploration of their knowledge base. + +MANDATORY TOOL USAGE PATTERN: +1. Use BATCH EXECUTION for parallel processing: + + execute_batch{"tools": [{"tool": "search", "params": {"query": "main topic"}}, {"tool": "search", "params": {"query": "related topic"}}]} + + +2. Read ALL found notes in batches: + + execute_batch{"tools": [{"tool": "read", "params": {"noteId": "id1"}}, {"tool": "read", "params": {"noteId": "id2"}}, {"tool": "read", "params": {"noteId": "id3"}}]} + + +3. Use SMART RETRY for empty results: + + retry_search{"originalQuery": "failed query", "strategy": "all"} + + +SIMPLIFIED TOOL NAMES: +- search (auto-detects type) instead of search_notes/keyword_search_notes +- read instead of read_note +- execute_batch for parallel execution +- retry_search for automatic variations + +WORKFLOW: search batch → read batch → retry if needed → analyze → repeat +Minimum 10+ tools per request using batch execution for speed! ``` \ No newline at end of file diff --git a/apps/server/src/services/llm/pipeline/stages/tool_calling_stage.ts b/apps/server/src/services/llm/pipeline/stages/tool_calling_stage.ts index 86bad8066b..2db0316366 100644 --- a/apps/server/src/services/llm/pipeline/stages/tool_calling_stage.ts +++ b/apps/server/src/services/llm/pipeline/stages/tool_calling_stage.ts @@ -490,9 +490,24 @@ export class ToolCallingStage extends BasePipelineStage { try { - const { attributeType, attributeName, attributeValue, maxResults = 20 } = args; + let { attributeType, attributeName, attributeValue, maxResults = 20 } = args; + + // Normalize attributeType to lowercase for case-insensitive handling + attributeType = attributeType?.toLowerCase(); log.info(`Executing attribute_search tool - Type: "${attributeType}", Name: "${attributeName}", Value: "${attributeValue || 'any'}", MaxResults: ${maxResults}`); @@ -65,19 +68,18 @@ export class AttributeSearchTool implements ToolHandler { if (attributeType !== 'label' && attributeType !== 'relation') { const suggestions: string[] = []; - if (attributeType.toLowerCase() === 'label' || attributeType.toLowerCase() === 'relation') { - suggestions.push(`CASE SENSITIVE: Use "${attributeType.toLowerCase()}" (lowercase)`); - } - - if (attributeType.includes('label') || attributeType.includes('Label')) { - suggestions.push('CORRECT: Use "label" for tags and categories'); + // Check for common variations and provide helpful guidance + if (attributeType?.includes('tag') || attributeType?.includes('category')) { + suggestions.push('Use "label" for tags and categories'); } - if (attributeType.includes('relation') || attributeType.includes('Relation')) { - suggestions.push('CORRECT: Use "relation" for connections and relationships'); + if (attributeType?.includes('link') || attributeType?.includes('connection')) { + suggestions.push('Use "relation" for links and connections'); } - const errorMessage = `Invalid attributeType: "${attributeType}". Must be exactly "label" or "relation" (lowercase). Example: {"attributeType": "label", "attributeName": "important"}`; + const errorMessage = `Invalid attributeType: "${attributeType}". Use "label" for tags/categories or "relation" for connections. Examples: +- Find tagged notes: {"attributeType": "label", "attributeName": "important"} +- Find related notes: {"attributeType": "relation", "attributeName": "relatedTo"}`; return errorMessage; } diff --git a/apps/server/src/services/llm/tools/execute_batch_tool.ts b/apps/server/src/services/llm/tools/execute_batch_tool.ts new file mode 100644 index 0000000000..44e08f1d93 --- /dev/null +++ b/apps/server/src/services/llm/tools/execute_batch_tool.ts @@ -0,0 +1,250 @@ +/** + * Batch Execution Tool + * + * Allows LLMs to execute multiple tools in parallel for faster results, + * similar to how Claude Code works. + */ + +import type { Tool, ToolHandler } from './tool_interfaces.js'; +import log from '../../log.js'; +import toolRegistry from './tool_registry.js'; + +/** + * Definition of the batch execution tool + */ +export const executeBatchToolDefinition: Tool = { + type: 'function', + function: { + name: 'execute_batch', + description: 'Execute multiple tools in parallel. Example: execute_batch([{tool:"search",params:{query:"AI"}},{tool:"search",params:{query:"ML"}}]) → run both searches simultaneously', + parameters: { + type: 'object', + properties: { + tools: { + type: 'array', + description: 'Array of tools to execute in parallel', + items: { + type: 'object', + properties: { + tool: { + type: 'string', + description: 'Tool name (e.g., "search", "read", "attribute_search")' + }, + params: { + type: 'object', + description: 'Parameters for the tool' + }, + id: { + type: 'string', + description: 'Optional ID to identify this tool execution' + } + }, + required: ['tool', 'params'] + }, + minItems: 1, + maxItems: 10 + }, + returnFormat: { + type: 'string', + description: 'Result format: "concise" for noteIds only, "full" for complete results', + enum: ['concise', 'full'], + default: 'concise' + } + }, + required: ['tools'] + } + } +}; + +/** + * Batch execution tool implementation + */ +export class ExecuteBatchTool implements ToolHandler { + public definition: Tool = executeBatchToolDefinition; + + /** + * Format results in concise format for easier LLM parsing + */ + private formatConciseResult(toolName: string, result: any, id?: string): any { + const baseResult = { + tool: toolName, + id: id || undefined, + status: 'success' + }; + + // Handle different result types + if (typeof result === 'string') { + if (result.startsWith('Error:')) { + return { ...baseResult, status: 'error', error: result }; + } + return { ...baseResult, result: result.substring(0, 200) }; + } + + if (typeof result === 'object' && result !== null) { + // Extract key information for search results + if ('results' in result && Array.isArray(result.results)) { + const noteIds = result.results.map((r: any) => r.noteId).filter(Boolean); + return { + ...baseResult, + found: result.count || result.results.length, + noteIds: noteIds.slice(0, 20), // Limit to 20 IDs + total: result.totalFound || result.count, + next: noteIds.length > 0 ? 'Use read tool with these noteIds' : 'Try different search terms' + }; + } + + // Handle note content results + if ('content' in result) { + return { + ...baseResult, + title: result.title || 'Unknown', + preview: typeof result.content === 'string' + ? result.content.substring(0, 300) + '...' + : 'Binary content', + length: typeof result.content === 'string' ? result.content.length : 0 + }; + } + + // Default object handling + return { ...baseResult, summary: this.summarizeObject(result) }; + } + + return { ...baseResult, result }; + } + + /** + * Summarize complex objects for concise output + */ + private summarizeObject(obj: any): string { + const keys = Object.keys(obj); + if (keys.length === 0) return 'Empty result'; + + const summary = keys.slice(0, 3).map(key => { + const value = obj[key]; + if (Array.isArray(value)) { + return `${key}: ${value.length} items`; + } + if (typeof value === 'string') { + return `${key}: "${value.substring(0, 50)}${value.length > 50 ? '...' : ''}"`; + } + return `${key}: ${typeof value}`; + }).join(', '); + + return keys.length > 3 ? `${summary}, +${keys.length - 3} more` : summary; + } + + /** + * Execute multiple tools in parallel + */ + public async execute(args: { + tools: Array<{ tool: string, params: any, id?: string }>, + returnFormat?: 'concise' | 'full' + }): Promise { + try { + const { tools, returnFormat = 'concise' } = args; + + log.info(`Executing batch of ${tools.length} tools in parallel`); + + // Validate all tools exist before execution + const toolHandlers = tools.map(({ tool, id }) => { + const handler = toolRegistry.getTool(tool); + if (!handler) { + throw new Error(`Tool '${tool}' not found. ID: ${id || 'none'}`); + } + return { handler, id }; + }); + + // Execute all tools in parallel + const startTime = Date.now(); + const results = await Promise.allSettled( + tools.map(async ({ tool, params, id }, index) => { + try { + log.info(`Batch execution [${index + 1}/${tools.length}]: ${tool} ${id ? `(${id})` : ''}`); + const handler = toolHandlers[index].handler; + const result = await handler.execute(params); + return { tool, params, id, result, status: 'fulfilled' as const }; + } catch (error) { + log.error(`Batch tool ${tool} failed: ${error}`); + return { + tool, + params, + id, + error: error instanceof Error ? error.message : String(error), + status: 'rejected' as const + }; + } + }) + ); + + const executionTime = Date.now() - startTime; + log.info(`Batch execution completed in ${executionTime}ms`); + + // Process results + const processedResults = results.map((result, index) => { + const toolInfo = tools[index]; + + if (result.status === 'fulfilled') { + if (returnFormat === 'concise') { + return this.formatConciseResult(toolInfo.tool, result.value.result, toolInfo.id); + } else { + return { + tool: toolInfo.tool, + id: toolInfo.id, + status: 'success', + result: result.value.result + }; + } + } else { + return { + tool: toolInfo.tool, + id: toolInfo.id, + status: 'error', + error: result.reason?.message || String(result.reason) + }; + } + }); + + // Create summary + const successful = processedResults.filter(r => r.status === 'success').length; + const failed = processedResults.length - successful; + + const batchResult = { + executed: tools.length, + successful, + failed, + executionTime: `${executionTime}ms`, + results: processedResults + }; + + // Add suggestions for next actions + if (returnFormat === 'concise') { + const noteIds = processedResults + .flatMap(r => r.noteIds || []) + .filter(Boolean); + + const errors = processedResults + .filter(r => r.status === 'error') + .map(r => r.error); + + if (noteIds.length > 0) { + batchResult['next_suggestion'] = `Found ${noteIds.length} notes. Use read tool: execute_batch([${noteIds.slice(0, 5).map(id => `{tool:"read",params:{noteId:"${id}"}}`).join(',')}])`; + } + + if (errors.length > 0) { + batchResult['retry_suggestion'] = 'Some tools failed. Try with broader terms or different search types.'; + } + } + + return batchResult; + + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + log.error(`Error in batch execution: ${errorMessage}`); + return { + status: 'error', + error: errorMessage, + suggestion: 'Try executing tools individually to identify the issue' + }; + } + } +} \ No newline at end of file diff --git a/apps/server/src/services/llm/tools/read_note_tool.ts b/apps/server/src/services/llm/tools/read_note_tool.ts index 98dd4bd5fe..9a0ebfa9d4 100644 --- a/apps/server/src/services/llm/tools/read_note_tool.ts +++ b/apps/server/src/services/llm/tools/read_note_tool.ts @@ -33,8 +33,8 @@ function isError(error: unknown): error is Error { export const readNoteToolDefinition: Tool = { type: 'function', function: { - name: 'read_note', - description: 'Read the full content of a note by its ID. Use noteId from search results, not note titles.', + name: 'read', + description: 'Read note content. Example: read("noteId123") → returns full content. Use noteIds from search results.', parameters: { type: 'object', properties: { diff --git a/apps/server/src/services/llm/tools/smart_retry_tool.ts b/apps/server/src/services/llm/tools/smart_retry_tool.ts new file mode 100644 index 0000000000..e6c0342f09 --- /dev/null +++ b/apps/server/src/services/llm/tools/smart_retry_tool.ts @@ -0,0 +1,347 @@ +/** + * Smart Retry Tool + * + * Automatically retries failed searches with variations, similar to how Claude Code + * handles failures by trying different approaches. + */ + +import type { Tool, ToolHandler } from './tool_interfaces.js'; +import log from '../../log.js'; +import toolRegistry from './tool_registry.js'; + +/** + * Definition of the smart retry tool + */ +export const smartRetryToolDefinition: Tool = { + type: 'function', + function: { + name: 'retry_search', + description: 'Automatically retry failed searches with variations. Example: retry_search("machine learning algorithms") → tries "ML", "algorithms", "machine learning", etc.', + parameters: { + type: 'object', + properties: { + originalQuery: { + type: 'string', + description: 'The original search query that failed or returned no results' + }, + searchType: { + type: 'string', + description: 'Type of search to retry', + enum: ['auto', 'semantic', 'keyword', 'attribute'], + default: 'auto' + }, + maxAttempts: { + type: 'number', + description: 'Maximum number of retry attempts (default: 5)', + minimum: 1, + maximum: 10, + default: 5 + }, + strategy: { + type: 'string', + description: 'Retry strategy to use', + enum: ['broader', 'narrower', 'synonyms', 'related', 'all'], + default: 'all' + } + }, + required: ['originalQuery'] + } + } +}; + +/** + * Smart retry tool implementation + */ +export class SmartRetryTool implements ToolHandler { + public definition: Tool = smartRetryToolDefinition; + + /** + * Generate broader search terms + */ + private generateBroaderTerms(query: string): string[] { + const terms = query.toLowerCase().split(/\s+/); + const broader = []; + + // Single words from multi-word queries + if (terms.length > 1) { + broader.push(...terms.filter(term => term.length > 3)); + } + + // Category-based broader terms + const broaderMap: Record = { + 'machine learning': ['AI', 'artificial intelligence', 'ML', 'algorithms'], + 'deep learning': ['neural networks', 'machine learning', 'AI'], + 'project management': ['management', 'projects', 'planning'], + 'task management': ['tasks', 'todos', 'productivity'], + 'meeting notes': ['meetings', 'notes', 'discussions'], + 'financial report': ['finance', 'reports', 'financial'], + 'software development': ['development', 'programming', 'software'], + 'data analysis': ['data', 'analytics', 'analysis'] + }; + + for (const [specific, broaderTerms] of Object.entries(broaderMap)) { + if (query.toLowerCase().includes(specific)) { + broader.push(...broaderTerms); + } + } + + return [...new Set(broader)]; + } + + /** + * Generate synonyms and related terms + */ + private generateSynonyms(query: string): string[] { + const synonymMap: Record = { + 'meeting': ['conference', 'discussion', 'call', 'session'], + 'task': ['todo', 'action item', 'assignment', 'work'], + 'project': ['initiative', 'program', 'effort', 'work'], + 'note': ['document', 'memo', 'record', 'entry'], + 'important': ['critical', 'priority', 'urgent', 'key'], + 'development': ['coding', 'programming', 'building', 'creation'], + 'analysis': ['review', 'study', 'examination', 'research'], + 'report': ['summary', 'document', 'findings', 'results'] + }; + + const synonyms = []; + const queryLower = query.toLowerCase(); + + for (const [word, syns] of Object.entries(synonymMap)) { + if (queryLower.includes(word)) { + synonyms.push(...syns); + // Replace word with synonyms in original query + syns.forEach(syn => { + synonyms.push(query.replace(new RegExp(word, 'gi'), syn)); + }); + } + } + + return [...new Set(synonyms)]; + } + + /** + * Generate narrower, more specific terms + */ + private generateNarrowerTerms(query: string): string[] { + const narrowerMap: Record = { + 'AI': ['machine learning', 'deep learning', 'neural networks'], + 'programming': ['javascript', 'python', 'typescript', 'react'], + 'management': ['project management', 'task management', 'team management'], + 'analysis': ['data analysis', 'financial analysis', 'performance analysis'], + 'notes': ['meeting notes', 'research notes', 'project notes'] + }; + + const narrower = []; + const queryLower = query.toLowerCase(); + + for (const [broad, narrowTerms] of Object.entries(narrowerMap)) { + if (queryLower.includes(broad.toLowerCase())) { + narrower.push(...narrowTerms); + } + } + + return [...new Set(narrower)]; + } + + /** + * Generate related concept terms + */ + private generateRelatedTerms(query: string): string[] { + const relatedMap: Record = { + 'machine learning': ['data science', 'statistics', 'algorithms', 'models'], + 'project management': ['agile', 'scrum', 'planning', 'timeline'], + 'javascript': ['react', 'node.js', 'typescript', 'frontend'], + 'data analysis': ['visualization', 'statistics', 'metrics', 'reporting'], + 'meeting': ['agenda', 'minutes', 'action items', 'participants'] + }; + + const related = []; + const queryLower = query.toLowerCase(); + + for (const [concept, relatedTerms] of Object.entries(relatedMap)) { + if (queryLower.includes(concept)) { + related.push(...relatedTerms); + } + } + + return [...new Set(related)]; + } + + /** + * Execute smart retry with various strategies + */ + public async execute(args: { + originalQuery: string, + searchType?: string, + maxAttempts?: number, + strategy?: string + }): Promise { + try { + const { + originalQuery, + searchType = 'auto', + maxAttempts = 5, + strategy = 'all' + } = args; + + log.info(`Smart retry for query: "${originalQuery}" with strategy: ${strategy}`); + + // Generate alternative queries based on strategy + let alternatives: string[] = []; + + switch (strategy) { + case 'broader': + alternatives = this.generateBroaderTerms(originalQuery); + break; + case 'narrower': + alternatives = this.generateNarrowerTerms(originalQuery); + break; + case 'synonyms': + alternatives = this.generateSynonyms(originalQuery); + break; + case 'related': + alternatives = this.generateRelatedTerms(originalQuery); + break; + case 'all': + default: + alternatives = [ + ...this.generateBroaderTerms(originalQuery), + ...this.generateSynonyms(originalQuery), + ...this.generateRelatedTerms(originalQuery), + ...this.generateNarrowerTerms(originalQuery) + ]; + break; + } + + // Remove duplicates and limit attempts + alternatives = [...new Set(alternatives)].slice(0, maxAttempts); + + if (alternatives.length === 0) { + return { + success: false, + message: 'No alternative search terms could be generated', + suggestion: 'Try a completely different approach or search for broader concepts' + }; + } + + log.info(`Generated ${alternatives.length} alternative search terms: ${alternatives.join(', ')}`); + + // Get the search tool + const searchTool = toolRegistry.getTool('search') || toolRegistry.getTool('search_notes'); + if (!searchTool) { + return { + success: false, + error: 'Search tool not available', + alternatives: alternatives + }; + } + + // Try each alternative + const results = []; + let successfulSearches = 0; + let totalResults = 0; + + for (let i = 0; i < alternatives.length; i++) { + const alternative = alternatives[i]; + + try { + log.info(`Retry attempt ${i + 1}/${alternatives.length}: "${alternative}"`); + + const result = await searchTool.execute({ + query: alternative, + maxResults: 5 + }); + + // Check if this search was successful + let hasResults = false; + let resultCount = 0; + + if (typeof result === 'object' && result !== null) { + if ('results' in result && Array.isArray(result.results)) { + resultCount = result.results.length; + hasResults = resultCount > 0; + } else if ('count' in result && typeof result.count === 'number') { + resultCount = result.count; + hasResults = resultCount > 0; + } + } + + if (hasResults) { + successfulSearches++; + totalResults += resultCount; + + results.push({ + query: alternative, + success: true, + count: resultCount, + result: result + }); + + log.info(`Success with "${alternative}": found ${resultCount} results`); + } else { + results.push({ + query: alternative, + success: false, + count: 0, + message: 'No results found' + }); + } + + } catch (error) { + log.error(`Error with alternative "${alternative}": ${error}`); + results.push({ + query: alternative, + success: false, + error: error instanceof Error ? error.message : String(error) + }); + } + } + + // Summarize results + const summary = { + originalQuery, + strategy, + attemptsMade: alternatives.length, + successfulSearches, + totalResultsFound: totalResults, + alternatives: results.filter(r => r.success), + failures: results.filter(r => !r.success), + recommendation: this.generateRecommendation(successfulSearches, totalResults, strategy) + }; + + if (successfulSearches > 0) { + summary['next_action'] = `Found results! Use read tool on noteIds from successful searches.`; + } + + return summary; + + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + log.error(`Error in smart retry: ${errorMessage}`); + return { + success: false, + error: errorMessage, + suggestion: 'Try manual search with simpler terms' + }; + } + } + + /** + * Generate recommendations based on retry results + */ + private generateRecommendation(successful: number, totalResults: number, strategy: string): string { + if (successful === 0) { + if (strategy === 'broader') { + return 'Try with synonyms or related terms instead'; + } else if (strategy === 'narrower') { + return 'Try broader terms or check spelling'; + } else { + return 'Consider searching for completely different concepts or check if notes exist on this topic'; + } + } else if (totalResults < 3) { + return 'Found few results. Try additional related terms or create notes on this topic'; + } else { + return 'Good results found! Read the notes and search for more specific aspects'; + } + } +} \ No newline at end of file diff --git a/apps/server/src/services/llm/tools/tool_discovery_helper.ts b/apps/server/src/services/llm/tools/tool_discovery_helper.ts index 384a0e8bb8..1595155cab 100644 --- a/apps/server/src/services/llm/tools/tool_discovery_helper.ts +++ b/apps/server/src/services/llm/tools/tool_discovery_helper.ts @@ -116,13 +116,18 @@ export class ToolDiscoveryHelper implements ToolHandler { */ private getToolInfo(): Record { return { + 'search': { + description: '🔍 Universal search - automatically uses semantic, keyword, or attribute search', + bestFor: 'ANY search need - it intelligently routes to the best search method', + parameters: ['query (required)', 'searchType', 'maxResults', 'filters'] + }, 'search_notes': { description: '🧠 Semantic/conceptual search for notes', bestFor: 'Finding notes about ideas, concepts, or topics described in various ways', parameters: ['query (required)', 'parentNoteId', 'maxResults', 'summarize'] }, 'keyword_search_notes': { - description: '🔍 Exact keyword/phrase search for notes', + description: '🔎 Exact keyword/phrase search for notes', bestFor: 'Finding notes with specific words, phrases, or using search operators', parameters: ['query (required)', 'maxResults', 'includeArchived'] }, diff --git a/apps/server/src/services/llm/tools/tool_initializer.ts b/apps/server/src/services/llm/tools/tool_initializer.ts index 304a71ef6e..4a96d8cd46 100644 --- a/apps/server/src/services/llm/tools/tool_initializer.ts +++ b/apps/server/src/services/llm/tools/tool_initializer.ts @@ -8,6 +8,9 @@ import toolRegistry from './tool_registry.js'; import { SearchNotesTool } from './search_notes_tool.js'; import { KeywordSearchTool } from './keyword_search_tool.js'; import { AttributeSearchTool } from './attribute_search_tool.js'; +import { UnifiedSearchTool } from './unified_search_tool.js'; +import { ExecuteBatchTool } from './execute_batch_tool.js'; +import { SmartRetryTool } from './smart_retry_tool.js'; import { SearchSuggestionTool } from './search_suggestion_tool.js'; import { ReadNoteTool } from './read_note_tool.js'; import { NoteCreationTool } from './note_creation_tool.js'; @@ -33,12 +36,19 @@ export async function initializeTools(): Promise { try { log.info('Initializing LLM tools...'); - // Register search and discovery tools + // Register core utility tools FIRST (highest priority) + toolRegistry.registerTool(new ExecuteBatchTool()); // Batch execution for parallel tools + toolRegistry.registerTool(new UnifiedSearchTool()); // Universal search interface + toolRegistry.registerTool(new SmartRetryTool()); // Automatic retry with variations + toolRegistry.registerTool(new ReadNoteTool()); // Read note content + + // Register individual search tools (kept for backwards compatibility but lower priority) toolRegistry.registerTool(new SearchNotesTool()); // Semantic search toolRegistry.registerTool(new KeywordSearchTool()); // Keyword-based search toolRegistry.registerTool(new AttributeSearchTool()); // Attribute-specific search + + // Register other discovery tools toolRegistry.registerTool(new SearchSuggestionTool()); // Search syntax helper - toolRegistry.registerTool(new ReadNoteTool()); // Read note content // Register note creation and manipulation tools toolRegistry.registerTool(new NoteCreationTool()); // Create new notes diff --git a/apps/server/src/services/llm/tools/unified_search_tool.ts b/apps/server/src/services/llm/tools/unified_search_tool.ts new file mode 100644 index 0000000000..46f9214fa4 --- /dev/null +++ b/apps/server/src/services/llm/tools/unified_search_tool.ts @@ -0,0 +1,260 @@ +/** + * Unified Search Tool + * + * This tool combines semantic search, keyword search, and attribute search into a single + * intelligent search interface that automatically routes to the appropriate backend. + */ + +import type { Tool, ToolHandler } from './tool_interfaces.js'; +import log from '../../log.js'; +import { SearchNotesTool } from './search_notes_tool.js'; +import { KeywordSearchTool } from './keyword_search_tool.js'; +import { AttributeSearchTool } from './attribute_search_tool.js'; + +/** + * Definition of the unified search tool + */ +export const unifiedSearchToolDefinition: Tool = { + type: 'function', + function: { + name: 'search', + description: 'Find notes intelligently. Example: search("machine learning") → finds related notes. Auto-detects search type (semantic/keyword/attribute).', + parameters: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'Search query. Can be: conceptual phrases ("machine learning algorithms"), exact terms in quotes ("meeting notes"), labels (#important), relations (~relatedTo), or attribute queries (label:todo)' + }, + searchType: { + type: 'string', + description: 'Optional: Force specific search type. Auto-detected if not specified.', + enum: ['auto', 'semantic', 'keyword', 'attribute'] + }, + maxResults: { + type: 'number', + description: 'Maximum results to return (default: 10, max: 50)' + }, + filters: { + type: 'object', + description: 'Optional filters', + properties: { + parentNoteId: { + type: 'string', + description: 'Limit search to children of this note' + }, + includeArchived: { + type: 'boolean', + description: 'Include archived notes (default: false)' + }, + attributeType: { + type: 'string', + description: 'For attribute searches: "label" or "relation"' + }, + attributeValue: { + type: 'string', + description: 'Optional value for attribute searches' + } + } + } + }, + required: ['query'] + } + } +}; + +/** + * Unified search tool implementation + */ +export class UnifiedSearchTool implements ToolHandler { + public definition: Tool = unifiedSearchToolDefinition; + private semanticSearchTool: SearchNotesTool; + private keywordSearchTool: KeywordSearchTool; + private attributeSearchTool: AttributeSearchTool; + + constructor() { + this.semanticSearchTool = new SearchNotesTool(); + this.keywordSearchTool = new KeywordSearchTool(); + this.attributeSearchTool = new AttributeSearchTool(); + } + + /** + * Detect the search type from the query + */ + private detectSearchType(query: string): 'semantic' | 'keyword' | 'attribute' { + // Check for attribute patterns + if (query.startsWith('#') || query.startsWith('~')) { + return 'attribute'; + } + + // Check for label: or relation: patterns + if (query.match(/^(label|relation):/i)) { + return 'attribute'; + } + + // Check for exact phrase searches (quoted strings) + if (query.includes('"') && query.indexOf('"') !== query.lastIndexOf('"')) { + return 'keyword'; + } + + // Check for boolean operators + if (query.match(/\b(AND|OR|NOT)\b/)) { + return 'keyword'; + } + + // Check for special search operators + if (query.includes('note.') || query.includes('*=')) { + return 'keyword'; + } + + // Default to semantic search for natural language queries + return 'semantic'; + } + + /** + * Parse attribute search from query + */ + private parseAttributeSearch(query: string): { type: string, name: string, value?: string } | null { + // Handle #label or ~relation format + if (query.startsWith('#')) { + const parts = query.substring(1).split('='); + return { + type: 'label', + name: parts[0], + value: parts[1] + }; + } + + if (query.startsWith('~')) { + const parts = query.substring(1).split('='); + return { + type: 'relation', + name: parts[0], + value: parts[1] + }; + } + + // Handle label:name or relation:name format + const match = query.match(/^(label|relation):(\w+)(?:=(.+))?$/i); + if (match) { + return { + type: match[1].toLowerCase(), + name: match[2], + value: match[3] + }; + } + + return null; + } + + /** + * Execute the unified search tool + */ + public async execute(args: { + query: string, + searchType?: string, + maxResults?: number, + filters?: { + parentNoteId?: string, + includeArchived?: boolean, + attributeType?: string, + attributeValue?: string + } + }): Promise { + try { + const { query, searchType = 'auto', maxResults = 10, filters = {} } = args; + + log.info(`Executing unified search - Query: "${query}", Type: ${searchType}, MaxResults: ${maxResults}`); + + // Detect search type if auto + let actualSearchType = searchType; + if (searchType === 'auto') { + actualSearchType = this.detectSearchType(query); + log.info(`Auto-detected search type: ${actualSearchType}`); + } + + // Route to appropriate search tool + switch (actualSearchType) { + case 'semantic': { + log.info('Routing to semantic search'); + const result = await this.semanticSearchTool.execute({ + query, + parentNoteId: filters.parentNoteId, + maxResults, + summarize: false + }); + + // Add search type indicator + if (typeof result === 'object' && !Array.isArray(result)) { + return { + ...result, + searchMethod: 'semantic', + tip: 'For exact matches, try keyword search. For tagged notes, try attribute search.' + }; + } + return result; + } + + case 'keyword': { + log.info('Routing to keyword search'); + const result = await this.keywordSearchTool.execute({ + query, + maxResults, + includeArchived: filters.includeArchived || false + }); + + // Add search type indicator + if (typeof result === 'object' && !Array.isArray(result)) { + return { + ...result, + searchMethod: 'keyword', + tip: 'For conceptual matches, try semantic search. For tagged notes, try attribute search.' + }; + } + return result; + } + + case 'attribute': { + log.info('Routing to attribute search'); + + // Parse attribute from query if not provided in filters + const parsed = this.parseAttributeSearch(query); + if (!parsed) { + return { + error: 'Invalid attribute search format', + help: 'Use #labelname, ~relationname, label:name, or relation:name', + examples: ['#important', '~relatedTo', 'label:todo', 'relation:partOf=projectX'] + }; + } + + const result = await this.attributeSearchTool.execute({ + attributeType: filters.attributeType || parsed.type, + attributeName: parsed.name, + attributeValue: filters.attributeValue || parsed.value, + maxResults + }); + + // Add search type indicator + if (typeof result === 'object' && !Array.isArray(result)) { + return { + ...result, + searchMethod: 'attribute', + tip: 'For content matches, try semantic or keyword search.' + }; + } + return result; + } + + default: + return { + error: `Unknown search type: ${actualSearchType}`, + validTypes: ['auto', 'semantic', 'keyword', 'attribute'] + }; + } + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + log.error(`Error executing unified search: ${errorMessage}`); + return `Error: ${errorMessage}`; + } + } +} \ No newline at end of file From 6e0fee6cb3bede49b08899e03a123ecd00aa9b46 Mon Sep 17 00:00:00 2001 From: perf3ct Date: Thu, 19 Jun 2025 16:13:28 +0000 Subject: [PATCH 4/5] fix(llm): resolve tool lint errors --- .../src/services/llm/tools/smart_retry_tool.ts | 17 ++++++++++++----- .../src/services/llm/tools/tool_interfaces.ts | 6 ++++++ .../services/llm/tools/unified_search_tool.ts | 2 +- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/apps/server/src/services/llm/tools/smart_retry_tool.ts b/apps/server/src/services/llm/tools/smart_retry_tool.ts index e6c0342f09..fa40a9e0df 100644 --- a/apps/server/src/services/llm/tools/smart_retry_tool.ts +++ b/apps/server/src/services/llm/tools/smart_retry_tool.ts @@ -60,7 +60,7 @@ export class SmartRetryTool implements ToolHandler { */ private generateBroaderTerms(query: string): string[] { const terms = query.toLowerCase().split(/\s+/); - const broader = []; + const broader: string[] = []; // Single words from multi-word queries if (terms.length > 1) { @@ -103,7 +103,7 @@ export class SmartRetryTool implements ToolHandler { 'report': ['summary', 'document', 'findings', 'results'] }; - const synonyms = []; + const synonyms: string[] = []; const queryLower = query.toLowerCase(); for (const [word, syns] of Object.entries(synonymMap)) { @@ -131,7 +131,7 @@ export class SmartRetryTool implements ToolHandler { 'notes': ['meeting notes', 'research notes', 'project notes'] }; - const narrower = []; + const narrower: string[] = []; const queryLower = query.toLowerCase(); for (const [broad, narrowTerms] of Object.entries(narrowerMap)) { @@ -155,7 +155,7 @@ export class SmartRetryTool implements ToolHandler { 'meeting': ['agenda', 'minutes', 'action items', 'participants'] }; - const related = []; + const related: string[] = []; const queryLower = query.toLowerCase(); for (const [concept, relatedTerms] of Object.entries(relatedMap)) { @@ -237,7 +237,14 @@ export class SmartRetryTool implements ToolHandler { } // Try each alternative - const results = []; + const results: Array<{ + query: string; + success: boolean; + count?: number; + result?: any; + message?: string; + error?: string; + }> = []; let successfulSearches = 0; let totalResults = 0; diff --git a/apps/server/src/services/llm/tools/tool_interfaces.ts b/apps/server/src/services/llm/tools/tool_interfaces.ts index ec90df67fd..9bd249c3c9 100644 --- a/apps/server/src/services/llm/tools/tool_interfaces.ts +++ b/apps/server/src/services/llm/tools/tool_interfaces.ts @@ -34,6 +34,12 @@ export interface ToolParameter { type: string; description: string; enum?: string[]; + default?: any; + minimum?: number; + maximum?: number; + minItems?: number; + maxItems?: number; + properties?: Record; items?: ToolParameter | { type: string; properties?: Record; diff --git a/apps/server/src/services/llm/tools/unified_search_tool.ts b/apps/server/src/services/llm/tools/unified_search_tool.ts index 46f9214fa4..220c2b25eb 100644 --- a/apps/server/src/services/llm/tools/unified_search_tool.ts +++ b/apps/server/src/services/llm/tools/unified_search_tool.ts @@ -37,7 +37,7 @@ export const unifiedSearchToolDefinition: Tool = { }, filters: { type: 'object', - description: 'Optional filters', + description: 'Optional filters for search', properties: { parentNoteId: { type: 'string', From d798d29e9275726c40c1cdd41738459b88220af8 Mon Sep 17 00:00:00 2001 From: perf3ct Date: Thu, 19 Jun 2025 19:38:55 -0700 Subject: [PATCH 5/5] fix(llm): remove the vector search tool from the search_notes tool --- .../services/llm/tools/search_notes_tool.ts | 104 +++++++----------- 1 file changed, 42 insertions(+), 62 deletions(-) diff --git a/apps/server/src/services/llm/tools/search_notes_tool.ts b/apps/server/src/services/llm/tools/search_notes_tool.ts index f943daae58..27b4f1d3c0 100644 --- a/apps/server/src/services/llm/tools/search_notes_tool.ts +++ b/apps/server/src/services/llm/tools/search_notes_tool.ts @@ -1,14 +1,15 @@ /** * Search Notes Tool * - * This tool allows the LLM to search for notes using semantic search. + * This tool allows the LLM to search for notes using keyword search. */ import type { Tool, ToolHandler } from './tool_interfaces.js'; import log from '../../log.js'; -import aiServiceManager from '../ai_service_manager.js'; +import searchService from '../../search/services/search.js'; import becca from '../../../becca/becca.js'; import { ContextExtractor } from '../context/index.js'; +import aiServiceManager from '../ai_service_manager.js'; /** * Definition of the search notes tool @@ -17,13 +18,13 @@ export const searchNotesToolDefinition: Tool = { type: 'function', function: { name: 'search_notes', - description: 'Semantic search for notes. Finds conceptually related content. Use descriptive phrases, not single words. Returns noteId values to use with other tools.', + description: 'Search for notes using keywords and phrases. Use descriptive terms and phrases for best results. Returns noteId values to use with other tools.', parameters: { type: 'object', properties: { query: { type: 'string', - description: 'Search query for finding conceptually related notes. Use descriptive phrases like "machine learning classification" rather than single words.' + description: 'Search query for finding notes. Use descriptive phrases like "machine learning classification" for better results.' }, parentNoteId: { type: 'string', @@ -44,50 +45,46 @@ export const searchNotesToolDefinition: Tool = { }; /** - * Get or create the vector search tool dependency - * @returns The vector search tool or null if it couldn't be created + * Perform keyword search for notes */ -async function getOrCreateVectorSearchTool(): Promise { +async function searchNotesWithKeywords(query: string, parentNoteId?: string, maxResults: number = 5): Promise { try { - // Try to get the existing vector search tool - let vectorSearchTool = aiServiceManager.getVectorSearchTool(); - - if (vectorSearchTool) { - log.info(`Found existing vectorSearchTool`); - return vectorSearchTool; - } - - // No existing tool, try to initialize it - log.info(`VectorSearchTool not found, attempting initialization`); - - // Get agent tools manager and initialize it - const agentTools = aiServiceManager.getAgentTools(); - if (agentTools && typeof agentTools.initialize === 'function') { - try { - // Force initialization to ensure it runs even if previously marked as initialized - await agentTools.initialize(true); - } catch (initError: any) { - log.error(`Failed to initialize agent tools: ${initError.message}`); - return null; - } - } else { - log.error('Agent tools manager not available'); - return null; + log.info(`Performing keyword search for: "${query}"`); + + // Build search query with parent filter if specified + let searchQuery = query; + if (parentNoteId) { + // Add parent filter to the search query + searchQuery = `${query} note.parents.noteId = ${parentNoteId}`; } - // Try getting the vector search tool again after initialization - vectorSearchTool = aiServiceManager.getVectorSearchTool(); + const searchContext = { + includeArchivedNotes: false, + fuzzyAttributeSearch: false + }; - if (vectorSearchTool) { - log.info('Successfully created vectorSearchTool'); - return vectorSearchTool; - } else { - log.error('Failed to create vectorSearchTool after initialization'); - return null; - } + const searchResults = searchService.searchNotes(searchQuery, searchContext); + const limitedResults = searchResults.slice(0, maxResults); + + // Convert search results to the expected format + return limitedResults.map(note => { + // Get the first parent (notes can have multiple parents) + const parentNotes = note.getParentNotes(); + const firstParent = parentNotes.length > 0 ? parentNotes[0] : null; + + return { + noteId: note.noteId, + title: note.title, + dateCreated: note.dateCreated, + dateModified: note.dateModified, + parentId: firstParent?.noteId || null, + similarity: 1.0, // Keyword search doesn't provide similarity scores + score: 1.0 + }; + }); } catch (error: any) { - log.error(`Error getting or creating vectorSearchTool: ${error.message}`); - return null; + log.error(`Error in keyword search: ${error.message}`); + return []; } } @@ -241,26 +238,9 @@ export class SearchNotesTool implements ToolHandler { log.info(`Executing search_notes tool - Query: "${query}", ParentNoteId: ${parentNoteId || 'not specified'}, MaxResults: ${maxResults}, Summarize: ${summarize}`); - // Get the vector search tool from the AI service manager - const vectorSearchTool = await getOrCreateVectorSearchTool(); - - if (!vectorSearchTool) { - return `Error: Vector search tool is not available. The system may still be initializing or there could be a configuration issue.`; - } - - log.info(`Retrieved vector search tool from AI service manager`); - - // Check if searchNotes method exists - if (!vectorSearchTool.searchNotes || typeof vectorSearchTool.searchNotes !== 'function') { - log.error(`Vector search tool is missing searchNotes method`); - return `Error: Vector search tool is improperly configured (missing searchNotes method).`; - } - - // Execute the search - log.info(`Performing semantic search for: "${query}"`); + // Execute the search using keyword search const searchStartTime = Date.now(); - const response = await vectorSearchTool.searchNotes(query, parentNoteId, maxResults); - const results: Array> = response?.matches ?? []; + const results = await searchNotesWithKeywords(query, parentNoteId, maxResults); const searchDuration = Date.now() - searchStartTime; log.info(`Search completed in ${searchDuration}ms, found ${results.length} matching notes`); @@ -299,7 +279,7 @@ export class SearchNotesTool implements ToolHandler { count: 0, results: [], query: query, - message: `No results found. Try: keyword_search_notes with "${this.extractKeywords(query)}" or attribute_search for tagged notes.` + message: `No results found. Try rephrasing your query, using simpler terms, or check your spelling.` }; } else { return {