mirror of
https://github.com/zadam/trilium.git
synced 2025-12-05 23:14:24 +01:00
feat(llm): try to squeeze even more out of the tools
This commit is contained in:
parent
e98954c555
commit
e0e1f0796b
@ -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
|
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
|
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
|
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
|
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:
|
CRITICAL INSTRUCTIONS FOR TOOL USAGE:
|
||||||
1. YOU MUST TRY MULTIPLE TOOLS AND SEARCH VARIATIONS before concluding information isn't available
|
YOU ARE EXPECTED TO USE 10-30 TOOLS PER REQUEST. This is NORMAL and EXPECTED behavior.
|
||||||
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:
|
TOOL EXECUTION STRATEGY:
|
||||||
- Use broader terms: If "Kubernetes deployment" fails, try just "Kubernetes" or "container orchestration"
|
USE BATCH EXECUTION FOR SPEED:
|
||||||
- Try synonyms: If "meeting notes" fails, try "conference", "discussion", or "conversation"
|
1. execute_batch([{tool:"search",params:{query:"main topic"}},{tool:"search",params:{query:"related topic"}}])
|
||||||
- Remove specific qualifiers: If "quarterly financial report 2024" fails, try just "financial report"
|
2. execute_batch([{tool:"read",params:{noteId:"id1"}},{tool:"read",params:{noteId:"id2"}},{tool:"read",params:{noteId:"id3"}}])
|
||||||
- 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
|
SMART RETRY ON FAILURES:
|
||||||
5. NEVER respond with "there are no notes about X" until you've tried at least 3 different search variations
|
- Empty results? → retry_search("original query") automatically tries variations
|
||||||
6. DO NOT ask the user what to do next when searches fail - AUTOMATICALLY try different approaches
|
- Don't manually retry - use retry_search tool
|
||||||
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
|
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
|
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
|
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
|
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 = '';
|
let directiveMessage = '';
|
||||||
|
|
||||||
if (hasEmptyResults) {
|
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 {
|
} 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({
|
updatedMessages.push({
|
||||||
|
|||||||
@ -19,13 +19,13 @@ export const attributeSearchToolDefinition: Tool = {
|
|||||||
type: 'function',
|
type: 'function',
|
||||||
function: {
|
function: {
|
||||||
name: 'attribute_search',
|
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: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
attributeType: {
|
attributeType: {
|
||||||
type: 'string',
|
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']
|
enum: ['label', 'relation']
|
||||||
},
|
},
|
||||||
attributeName: {
|
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> {
|
public async execute(args: { attributeType: string, attributeName: string, attributeValue?: string, maxResults?: number }): Promise<string | object> {
|
||||||
try {
|
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}`);
|
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') {
|
if (attributeType !== 'label' && attributeType !== 'relation') {
|
||||||
const suggestions: string[] = [];
|
const suggestions: string[] = [];
|
||||||
|
|
||||||
if (attributeType.toLowerCase() === 'label' || attributeType.toLowerCase() === 'relation') {
|
// Check for common variations and provide helpful guidance
|
||||||
suggestions.push(`CASE SENSITIVE: Use "${attributeType.toLowerCase()}" (lowercase)`);
|
if (attributeType?.includes('tag') || attributeType?.includes('category')) {
|
||||||
|
suggestions.push('Use "label" for tags and categories');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attributeType.includes('label') || attributeType.includes('Label')) {
|
if (attributeType?.includes('link') || attributeType?.includes('connection')) {
|
||||||
suggestions.push('CORRECT: Use "label" for tags and categories');
|
suggestions.push('Use "relation" for links and connections');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attributeType.includes('relation') || attributeType.includes('Relation')) {
|
const errorMessage = `Invalid attributeType: "${attributeType}". Use "label" for tags/categories or "relation" for connections. Examples:
|
||||||
suggestions.push('CORRECT: Use "relation" for connections and relationships');
|
- Find tagged notes: {"attributeType": "label", "attributeName": "important"}
|
||||||
}
|
- Find related notes: {"attributeType": "relation", "attributeName": "relatedTo"}`;
|
||||||
|
|
||||||
const errorMessage = `Invalid attributeType: "${attributeType}". Must be exactly "label" or "relation" (lowercase). Example: {"attributeType": "label", "attributeName": "important"}`;
|
|
||||||
|
|
||||||
return errorMessage;
|
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 = {
|
export const readNoteToolDefinition: Tool = {
|
||||||
type: 'function',
|
type: 'function',
|
||||||
function: {
|
function: {
|
||||||
name: 'read_note',
|
name: 'read',
|
||||||
description: 'Read the full content of a note by its ID. Use noteId from search results, not note titles.',
|
description: 'Read note content. Example: read("noteId123") → returns full content. Use noteIds from search results.',
|
||||||
parameters: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
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[] }> {
|
private getToolInfo(): Record<string, { description: string; bestFor: string; parameters: string[] }> {
|
||||||
return {
|
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': {
|
'search_notes': {
|
||||||
description: '🧠 Semantic/conceptual search for notes',
|
description: '🧠 Semantic/conceptual search for notes',
|
||||||
bestFor: 'Finding notes about ideas, concepts, or topics described in various ways',
|
bestFor: 'Finding notes about ideas, concepts, or topics described in various ways',
|
||||||
parameters: ['query (required)', 'parentNoteId', 'maxResults', 'summarize']
|
parameters: ['query (required)', 'parentNoteId', 'maxResults', 'summarize']
|
||||||
},
|
},
|
||||||
'keyword_search_notes': {
|
'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',
|
bestFor: 'Finding notes with specific words, phrases, or using search operators',
|
||||||
parameters: ['query (required)', 'maxResults', 'includeArchived']
|
parameters: ['query (required)', 'maxResults', 'includeArchived']
|
||||||
},
|
},
|
||||||
|
|||||||
@ -8,6 +8,9 @@ import toolRegistry from './tool_registry.js';
|
|||||||
import { SearchNotesTool } from './search_notes_tool.js';
|
import { SearchNotesTool } from './search_notes_tool.js';
|
||||||
import { KeywordSearchTool } from './keyword_search_tool.js';
|
import { KeywordSearchTool } from './keyword_search_tool.js';
|
||||||
import { AttributeSearchTool } from './attribute_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 { SearchSuggestionTool } from './search_suggestion_tool.js';
|
||||||
import { ReadNoteTool } from './read_note_tool.js';
|
import { ReadNoteTool } from './read_note_tool.js';
|
||||||
import { NoteCreationTool } from './note_creation_tool.js';
|
import { NoteCreationTool } from './note_creation_tool.js';
|
||||||
@ -33,12 +36,19 @@ export async function initializeTools(): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
log.info('Initializing LLM tools...');
|
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 SearchNotesTool()); // Semantic search
|
||||||
toolRegistry.registerTool(new KeywordSearchTool()); // Keyword-based search
|
toolRegistry.registerTool(new KeywordSearchTool()); // Keyword-based search
|
||||||
toolRegistry.registerTool(new AttributeSearchTool()); // Attribute-specific search
|
toolRegistry.registerTool(new AttributeSearchTool()); // Attribute-specific search
|
||||||
|
|
||||||
|
// Register other discovery tools
|
||||||
toolRegistry.registerTool(new SearchSuggestionTool()); // Search syntax helper
|
toolRegistry.registerTool(new SearchSuggestionTool()); // Search syntax helper
|
||||||
toolRegistry.registerTool(new ReadNoteTool()); // Read note content
|
|
||||||
|
|
||||||
// Register note creation and manipulation tools
|
// Register note creation and manipulation tools
|
||||||
toolRegistry.registerTool(new NoteCreationTool()); // Create new notes
|
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}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user