feat(llm): try to squeeze even more out of the tools

This commit is contained in:
perf3ct
2025-06-19 15:31:07 +00:00
parent e98954c555
commit e0e1f0796b
11 changed files with 996 additions and 31 deletions

View File

@@ -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:
<function_calls>
<invoke name="execute_batch"><parameter name="tools">[{"tool": "search", "params": {"query": "main topic"}}, {"tool": "search", "params": {"query": "related topic"}}]</parameter></invoke>
</function_calls>
2. BATCH READ all discovered notes:
<function_calls>
<invoke name="execute_batch"><parameter name="tools">[{"tool": "read", "params": {"noteId": "id1"}}, {"tool": "read", "params": {"noteId": "id2"}}, {"tool": "read", "params": {"noteId": "id3"}}]</parameter></invoke>
</function_calls>
3. AUTO-RETRY failed searches:
<function_calls>
<invoke name="retry_search"><parameter name="originalQuery">failed search</parameter><parameter name="strategy">all</parameter></invoke>
</function_calls>
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!
```

View File

@@ -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!
```

View File

@@ -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:
<tool_calls>
<tool_call id="1"><n>execute_batch</n><parameters>{"tools": [{"tool": "search", "params": {"query": "main topic"}}, {"tool": "search", "params": {"query": "related topic"}}]}</parameters></tool_call>
</tool_calls>
2. Read ALL found notes in batches:
<tool_calls>
<tool_call id="2"><n>execute_batch</n><parameters>{"tools": [{"tool": "read", "params": {"noteId": "id1"}}, {"tool": "read", "params": {"noteId": "id2"}}, {"tool": "read", "params": {"noteId": "id3"}}]}</parameters></tool_call>
</tool_calls>
3. Use SMART RETRY for empty results:
<tool_calls>
<tool_call id="3"><n>retry_search</n><parameters>{"originalQuery": "failed query", "strategy": "all"}</parameters></tool_call>
</tool_calls>
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!
```

View File

@@ -490,9 +490,24 @@ export class ToolCallingStage extends BasePipelineStage<ToolExecutionInput, { re
let directiveMessage = '';
if (hasEmptyResults) {
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.`;
directiveMessage = `IMPORTANT: No results found with your search. You MUST continue searching with different approaches:
1. Use discover_tools to find alternative search methods
2. Try broader search terms or synonyms
3. Use different search tools (search_notes, keyword_search_notes, attribute_search)
4. Search for related concepts instead of specific terms
5. Use read_note on any noteIds you've found previously
CRITICAL: Continue executing tools to find information. Do NOT ask the user for guidance yet - exhaust all search options first.`;
} else {
directiveMessage = `You found results! Use read_note with the noteId values to get full content and continue your analysis.`;
directiveMessage = `EXCELLENT! You found ${toolResultMessages.length} results. Now you MUST continue with these actions:
1. Use read_note with ALL noteId values to get full content
2. After reading notes, use search_notes or keyword_search_notes to find related information
3. Use attribute_search to find notes with similar tags/labels
4. Use note_summarization on long notes
5. Use content_extraction to pull specific information
6. Use relationship tool to find connected notes
REMEMBER: Execute multiple tools in sequence to gather comprehensive information. The user expects thorough analysis using 10-20+ tool calls. Continue executing tools!`;
}
updatedMessages.push({

View File

@@ -19,13 +19,13 @@ export const attributeSearchToolDefinition: Tool = {
type: 'function',
function: {
name: 'attribute_search',
description: 'Search notes by attributes (labels/relations). attributeType must be exactly "label" or "relation" (lowercase).',
description: 'Search notes by attributes (labels/relations). Finds notes with specific tags, categories, or relationships.',
parameters: {
type: 'object',
properties: {
attributeType: {
type: 'string',
description: 'Must be exactly "label" or "relation" (lowercase only).',
description: 'Type of attribute: "label" for tags/categories or "relation" for connections. Case-insensitive.',
enum: ['label', 'relation']
},
attributeName: {
@@ -57,7 +57,10 @@ export class AttributeSearchTool implements ToolHandler {
*/
public async execute(args: { attributeType: string, attributeName: string, attributeValue?: string, maxResults?: number }): Promise<string | object> {
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)`);
// 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('label') || attributeType.includes('Label')) {
suggestions.push('CORRECT: Use "label" for tags and categories');
if (attributeType?.includes('link') || attributeType?.includes('connection')) {
suggestions.push('Use "relation" for links and connections');
}
if (attributeType.includes('relation') || attributeType.includes('Relation')) {
suggestions.push('CORRECT: Use "relation" for connections and relationships');
}
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;
}

View File

@@ -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<string | object> {
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'
};
}
}
}

View File

@@ -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: {

View File

@@ -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<string, string[]> = {
'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<string, string[]> = {
'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<string, string[]> = {
'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<string, string[]> = {
'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<string | object> {
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';
}
}
}

View File

@@ -116,13 +116,18 @@ export class ToolDiscoveryHelper implements ToolHandler {
*/
private getToolInfo(): Record<string, { description: string; bestFor: string; parameters: string[] }> {
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']
},

View File

@@ -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<void> {
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

View File

@@ -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<string | object> {
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}`;
}
}
}