From dccd6477d2abc4ce0f401704c157f374d736919d Mon Sep 17 00:00:00 2001 From: perf3ct Date: Wed, 11 Jun 2025 19:34:30 +0000 Subject: [PATCH] feat(llm): try to improve tool and tool calling, part 1 --- .../llm/pipeline/stages/tool_calling_stage.ts | 69 ++- .../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, 1140 insertions(+), 49 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 8299f8fd6..a2eaa00fb 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 = ''; - let directiveMessage = `YOU MUST NOT GIVE UP AFTER A SINGLE EMPTY SEARCH RESULT. `; + 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); - 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. `; + 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!`; } - if (emptyToolNames.includes('keyword_search')) { - directiveMessage += `IMMEDIATELY TRY SEARCH_NOTES INSTEAD as it might find matches where keyword search failed. `; - } - - 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 8365d38f4..d32f87303 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 ddcad559f..ddb8ce589 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 152187dec..5da23b728 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 000000000..530bcb977 --- /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 e8ceca3ee..2245470d1 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 000000000..18536f26f --- /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