mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-07-29 19:12:27 +08:00
refactor(llm): improve search tools error handling and parameter validation with clearer guidance
This commit is contained in:
parent
910c5039f4
commit
a7906d6b99
@ -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' &&
|
||||||
|
@ -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}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user