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 499369ea8..136139679 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 @@ -33,14 +33,17 @@ When responding to queries: 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 -When using tools, follow these best practices: -1. If a tool returns an error or no results, DO NOT give up immediately -2. Instead, try different parameters that might yield better results: - - For search tools: Try broader search terms, fewer filters, or synonyms - - For note navigation: Try parent or sibling notes if a specific note isn't found - - For content analysis: Try rephrasing or generalizing the query -3. When searching for information, start with specific search terms but be prepared to broaden your search if no results are found -4. If multiple attempts with different parameters still yield no results, clearly explain to the user that the information they're looking for might not be in their notes -5. When suggesting alternatives, be explicit about what parameters you've tried and what you're changing -6. Remember that empty results from tools don't mean the user's request can't be fulfilled - it often means the parameters need adjustment +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 ``` \ 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 7be1ca299..c41b75f58 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 @@ -6,6 +6,25 @@ import toolRegistry from '../../tools/tool_registry.js'; import chatStorageService from '../../chat_storage_service.js'; import aiServiceManager from '../../ai_service_manager.js'; +// Type definitions for tools and validation results +interface ToolInterface { + execute: (args: Record) => Promise; + [key: string]: unknown; +} + +interface ToolValidationResult { + toolCall: { + id?: string; + function: { + name: string; + arguments: string | Record; + }; + }; + valid: boolean; + tool: ToolInterface | null; + error: string | null; +} + /** * Pipeline stage for handling LLM tool calling * This stage is responsible for: @@ -50,12 +69,23 @@ export class ToolCallingStage extends BasePipelineStage 0) { - const availableToolNames = availableTools.map(t => t.definition.function.name).join(', '); + const availableToolNames = availableTools.map(t => { + // Safely access the name property using type narrowing + if (t && typeof t === 'object' && 'definition' in t && + t.definition && typeof t.definition === 'object' && + 'function' in t.definition && t.definition.function && + typeof t.definition.function === 'object' && + 'name' in t.definition.function && + typeof t.definition.function.name === 'string') { + return t.definition.function.name; + } + return 'unknown'; + }).join(', '); log.info(`Available tools: ${availableToolNames}`); } @@ -66,7 +96,8 @@ export class ToolCallingStage extends BasePipelineStage { + const validationResults: ToolValidationResult[] = await Promise.all((response.tool_calls || []).map(async (toolCall) => { try { // Get the tool from registry const tool = toolRegistry.getTool(toolCall.function.name); @@ -107,7 +138,8 @@ export class ToolCallingStage extends BasePipelineStage; // At this stage, arguments should already be processed by the provider-specific service // But we still need to handle different formats just in case if (typeof toolCall.function.arguments === 'string') { @@ -158,7 +191,7 @@ export class ToolCallingStage extends BasePipelineStage; log.info(`Parsed JSON arguments: ${Object.keys(args).join(', ')}`); } catch (e: unknown) { // If it's not valid JSON, try to check if it's a stringified object with quotes @@ -169,25 +202,26 @@ export class ToolCallingStage extends BasePipelineStage; log.info(`Successfully parsed cleaned arguments: ${Object.keys(args).join(', ')}`); } catch (cleanError: unknown) { // If all parsing fails, treat it as a text argument const cleanErrorMessage = cleanError instanceof Error ? cleanError.message : String(cleanError); log.info(`Failed to parse cleaned arguments: ${cleanErrorMessage}`); args = { text: toolCall.function.arguments }; - log.info(`Using text argument: ${args.text.substring(0, 50)}...`); + log.info(`Using text argument: ${(args.text as string).substring(0, 50)}...`); } } } else { // Arguments are already an object - args = toolCall.function.arguments; + args = toolCall.function.arguments as Record; log.info(`Using object arguments with keys: ${Object.keys(args).join(', ')}`); } @@ -423,10 +457,29 @@ 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('vector_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 VECTOR_SEARCH INSTEAD as it might find semantic 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: `The previous tool execution(s) completed but returned no useful results. Consider trying different parameters for better results. For search tools, try broader search terms, fewer filters, or synonyms. For navigation tools, try exploring parent or sibling notes.` + content: directiveMessage }); } @@ -517,7 +570,7 @@ export class ToolCallingStage extends BasePipelineStage) => Promise }, toolName: string): Promise { + private async validateToolBeforeExecution(tool: ToolInterface, toolName: string): Promise { try { if (!tool) { log.error(`Tool '${toolName}' not found or failed validation`);