mirror of
https://github.com/zadam/trilium.git
synced 2025-10-26 07:46:30 +01:00
feat(llm): try to squeeze even more out of the tools
This commit is contained in:
@@ -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!
|
||||
```
|
||||
@@ -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!
|
||||
```
|
||||
@@ -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!
|
||||
```
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
250
apps/server/src/services/llm/tools/execute_batch_tool.ts
Normal file
250
apps/server/src/services/llm/tools/execute_batch_tool.ts
Normal 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'
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
347
apps/server/src/services/llm/tools/smart_retry_tool.ts
Normal file
347
apps/server/src/services/llm/tools/smart_retry_tool.ts
Normal 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';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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']
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
260
apps/server/src/services/llm/tools/unified_search_tool.ts
Normal file
260
apps/server/src/services/llm/tools/unified_search_tool.ts
Normal 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}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user