refactor(llm): improve search tools error handling and parameter validation with clearer guidance

This commit is contained in:
perf3ct 2025-05-30 00:23:18 +00:00
parent 910c5039f4
commit a7906d6b99
No known key found for this signature in database
GPG Key ID: 569C4EEC436F5232
3 changed files with 68 additions and 51 deletions

View File

@ -699,8 +699,9 @@ export class ToolCallingStage extends BasePipelineStage<ToolExecutionInput, { re
// Add suggestions based on the specific tool and error // Add suggestions based on the specific tool and error
if (toolName === 'attribute_search' && errorMessage.includes('Invalid attribute type')) { if (toolName === 'attribute_search' && errorMessage.includes('Invalid attribute type')) {
guidance += "CORRECT USAGE: The 'attribute_search' tool requires a valid 'attributeType' parameter that must be either 'label' or 'relation'.\n"; guidance += "CRITICAL REQUIREMENT: The 'attribute_search' tool requires 'attributeType' parameter that must be EXACTLY 'label' or 'relation' (lowercase, no other values).\n";
guidance += "EXAMPLE: { \"attributeType\": \"label\", \"attributeValue\": \"important\" }\n"; guidance += "CORRECT EXAMPLE: { \"attributeType\": \"label\", \"attributeName\": \"important\", \"attributeValue\": \"yes\" }\n";
guidance += "INCORRECT EXAMPLE: { \"attributeType\": \"Label\", ... } - Case matters! Must be lowercase.\n";
} }
else if (errorMessage.includes('Tool not found')) { else if (errorMessage.includes('Tool not found')) {
// Provide guidance on available search tools if a tool wasn't found // Provide guidance on available search tools if a tool wasn't found
@ -748,7 +749,8 @@ export class ToolCallingStage extends BasePipelineStage<ToolExecutionInput, { re
trimmed.includes('No results found') || trimmed.includes('No results found') ||
trimmed.includes('No matches found') || trimmed.includes('No matches found') ||
trimmed.includes('No notes found'))) { trimmed.includes('No notes found'))) {
return true; // This is a valid result (empty, but valid), don't mark as empty so LLM can see feedback
return false;
} }
if (toolName === 'vector_search' && if (toolName === 'vector_search' &&

View File

@ -19,18 +19,18 @@ export const attributeSearchToolDefinition: Tool = {
type: 'function', type: 'function',
function: { function: {
name: 'attribute_search', name: 'attribute_search',
description: 'Search for notes with specific attributes (labels or relations). Use this when you need to find notes based on their metadata rather than content.', description: 'Search for notes with specific attributes (labels or relations). Use this when you need to find notes based on their metadata rather than content. IMPORTANT: attributeType must be exactly "label" or "relation" (lowercase).',
parameters: { parameters: {
type: 'object', type: 'object',
properties: { properties: {
attributeType: { attributeType: {
type: 'string', type: 'string',
description: 'Type of attribute to search for: "label" or "relation"', description: 'MUST be exactly "label" or "relation" (lowercase, no other values are valid)',
enum: ['label', 'relation'] enum: ['label', 'relation']
}, },
attributeName: { attributeName: {
type: 'string', type: 'string',
description: 'Name of the attribute to search for' description: 'Name of the attribute to search for (e.g., "important", "todo", "related-to")'
}, },
attributeValue: { attributeValue: {
type: 'string', type: 'string',
@ -63,7 +63,7 @@ export class AttributeSearchTool implements ToolHandler {
// Validate attribute type // Validate attribute type
if (attributeType !== 'label' && attributeType !== 'relation') { if (attributeType !== 'label' && attributeType !== 'relation') {
return `Error: Invalid attribute type. Must be either "label" or "relation".`; return `Error: Invalid attribute type. Must be exactly "label" or "relation" (lowercase). You provided: "${attributeType}".`;
} }
// Execute the search // Execute the search
@ -133,7 +133,7 @@ export class AttributeSearchTool implements ToolHandler {
} else { } else {
contentPreview = String(content).substring(0, 150) + (String(content).length > 150 ? '...' : ''); contentPreview = String(content).substring(0, 150) + (String(content).length > 150 ? '...' : '');
} }
} catch (e) { } catch (_) {
contentPreview = '[Content not available]'; contentPreview = '[Content not available]';
} }
@ -148,9 +148,10 @@ export class AttributeSearchTool implements ToolHandler {
}; };
}) })
}; };
} catch (error: any) { } catch (error: unknown) {
log.error(`Error executing attribute_search tool: ${error.message || String(error)}`); const errorMessage = error instanceof Error ? error.message : String(error);
return `Error: ${error.message || String(error)}`; log.error(`Error executing attribute_search tool: ${errorMessage}`);
return `Error: ${errorMessage}`;
} }
} }
} }

View File

@ -17,17 +17,17 @@ export const searchNotesToolDefinition: Tool = {
type: 'function', type: 'function',
function: { function: {
name: 'search_notes', name: 'search_notes',
description: 'Search for notes in the database using semantic search. Returns notes most semantically related to the query.', 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.',
parameters: { parameters: {
type: 'object', type: 'object',
properties: { properties: {
query: { query: {
type: 'string', type: 'string',
description: 'The search query to find semantically related notes' description: 'The search query to find semantically related notes. Be specific and descriptive for best results.'
}, },
parentNoteId: { parentNoteId: {
type: 'string', 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".' 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.'
}, },
maxResults: { maxResults: {
type: 'number', type: 'number',
@ -142,11 +142,11 @@ export class SearchNotesTool implements ToolHandler {
const result = await llmService.generateChatCompletion(messages, { const result = await llmService.generateChatCompletion(messages, {
temperature: 0.3, temperature: 0.3,
maxTokens: 200, maxTokens: 200,
// Use any to bypass the type checking for special parameters // Type assertion to bypass type checking for special internal parameters
...(({ ...(({
bypassFormatter: true, bypassFormatter: true,
bypassContextProcessing: true bypassContextProcessing: true
} as any)) } as Record<string, boolean>))
}); });
if (result && result.text) { if (result && result.text) {
@ -159,30 +159,33 @@ export class SearchNotesTool implements ToolHandler {
} }
} }
// Fall back to smart truncation if summarization fails or isn't requested try {
const previewLength = Math.min(formattedContent.length, 600); // Fall back to smart truncation if summarization fails or isn't requested
let preview = formattedContent.substring(0, previewLength); const previewLength = Math.min(formattedContent.length, 600);
let preview = formattedContent.substring(0, previewLength);
// Only add ellipsis if we've truncated the content // Only add ellipsis if we've truncated the content
if (previewLength < formattedContent.length) { if (previewLength < formattedContent.length) {
// Try to find a natural break point // Try to find a natural break point
const breakPoints = ['. ', '.\n', '\n\n', '\n', '. ']; const breakPoints = ['. ', '.\n', '\n\n', '\n', '. '];
let breakFound = false;
for (const breakPoint of breakPoints) { for (const breakPoint of breakPoints) {
const lastBreak = preview.lastIndexOf(breakPoint); const lastBreak = preview.lastIndexOf(breakPoint);
if (lastBreak > previewLength * 0.6) { // At least 60% of the way through if (lastBreak > previewLength * 0.6) { // At least 60% of the way through
preview = preview.substring(0, lastBreak + breakPoint.length); preview = preview.substring(0, lastBreak + breakPoint.length);
breakFound = true; break;
break; }
} }
// Add ellipsis if truncated
preview += '...';
} }
// Add ellipsis if truncated return preview;
preview += '...'; } catch (error) {
log.error(`Error getting rich content preview: ${error}`);
return 'Error retrieving content preview';
} }
return preview;
} catch (error) { } catch (error) {
log.error(`Error getting rich content preview: ${error}`); log.error(`Error getting rich content preview: ${error}`);
return 'Error retrieving content preview'; return 'Error retrieving content preview';
@ -226,11 +229,8 @@ export class SearchNotesTool implements ToolHandler {
// Execute the search // Execute the search
log.info(`Performing semantic search for: "${query}"`); log.info(`Performing semantic search for: "${query}"`);
const searchStartTime = Date.now(); const searchStartTime = Date.now();
const results = await vectorSearchTool.searchNotes(query, { const response = await vectorSearchTool.searchNotes(query, parentNoteId, maxResults);
parentNoteId, const results: Array<Record<string, unknown>> = response?.matches ?? [];
maxResults
// Don't pass summarize - we'll handle it ourselves
});
const searchDuration = Date.now() - searchStartTime; const searchDuration = Date.now() - searchStartTime;
log.info(`Search completed in ${searchDuration}ms, found ${results.length} matching notes`); log.info(`Search completed in ${searchDuration}ms, found ${results.length} matching notes`);
@ -247,12 +247,16 @@ export class SearchNotesTool implements ToolHandler {
// Get enhanced previews for each result // Get enhanced previews for each result
const enhancedResults = await Promise.all( const enhancedResults = await Promise.all(
results.map(async (result: any) => { results.map(async (result: any) => {
const preview = await this.getRichContentPreview(result.noteId, summarize); const noteId = result.noteId;
const preview = await this.getRichContentPreview(noteId, summarize);
return { return {
noteId: result.noteId, noteId: noteId,
title: result.title, title: result?.title as string || '[Unknown title]',
preview: preview, preview: preview,
score: result?.score as number,
dateCreated: result?.dateCreated as string,
dateModified: result?.dateModified as string,
similarity: Math.round(result.similarity * 100) / 100, similarity: Math.round(result.similarity * 100) / 100,
parentId: result.parentId parentId: result.parentId
}; };
@ -260,14 +264,24 @@ export class SearchNotesTool implements ToolHandler {
); );
// Format the results // Format the results
return { if (results.length === 0) {
count: enhancedResults.length, return {
results: enhancedResults, count: 0,
message: "Note: Use the noteId (not the title) when performing operations on specific notes with other tools." results: [],
}; query: query,
} catch (error: any) { 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.'
log.error(`Error executing search_notes tool: ${error.message || String(error)}`); };
return `Error: ${error.message || String(error)}`; } else {
return {
count: enhancedResults.length,
results: enhancedResults,
message: "Note: Use the noteId (not the title) when performing operations on specific notes with other tools."
};
}
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`Error executing search_notes tool: ${errorMessage}`);
return `Error: ${errorMessage}`;
} }
} }
} }