Skip to content
This repository was archived by the owner on Jun 24, 2025. It is now read-only.

Commit dccd647

Browse files
committed
feat(llm): try to improve tool and tool calling, part 1
1 parent 8ed6239 commit dccd647

File tree

8 files changed

+1143
-52
lines changed

8 files changed

+1143
-52
lines changed

apps/server/src/services/llm/pipeline/stages/tool_calling_stage.ts

Lines changed: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -483,28 +483,52 @@ export class ToolCallingStage extends BasePipelineStage<ToolExecutionInput, { re
483483
log.info(`Follow-up needed: ${needsFollowUp}`);
484484
log.info(`Reasoning: ${hasToolResults ? 'Has tool results to process' : 'No tool results'} ${hasErrors ? ', contains errors' : ''} ${hasEmptyResults ? ', contains empty results' : ''}`);
485485

486-
// Add a system message with hints for empty results
487-
if (hasEmptyResults && needsFollowUp) {
488-
log.info('Adding system message requiring the LLM to run additional tools with different parameters');
489-
490-
// Build a more directive message based on which tools were empty
491-
const emptyToolNames = toolResultMessages
492-
.filter(msg => this.isEmptyToolResult(msg.content, msg.name || ''))
493-
.map(msg => msg.name);
494-
495-
let directiveMessage = `YOU MUST NOT GIVE UP AFTER A SINGLE EMPTY SEARCH RESULT. `;
496-
497-
if (emptyToolNames.includes('search_notes') || emptyToolNames.includes('keyword_search')) {
498-
directiveMessage += `IMMEDIATELY RUN ANOTHER SEARCH TOOL with broader search terms, alternative keywords, or related concepts. `;
499-
directiveMessage += `Try synonyms, more general terms, or related topics. `;
500-
}
501-
502-
if (emptyToolNames.includes('keyword_search')) {
503-
directiveMessage += `IMMEDIATELY TRY SEARCH_NOTES INSTEAD as it might find matches where keyword search failed. `;
486+
// Add aggressive system message for continued tool usage
487+
if (needsFollowUp) {
488+
log.info('Adding enhanced system message to encourage continued tool usage');
489+
490+
let directiveMessage = '';
491+
492+
if (hasEmptyResults) {
493+
// Empty results - be very directive about trying alternatives
494+
const emptyToolNames = toolResultMessages
495+
.filter(msg => this.isEmptyToolResult(msg.content, msg.name || ''))
496+
.map(msg => msg.name);
497+
498+
directiveMessage = `CRITICAL INSTRUCTION: YOU MUST NOT STOP AFTER EMPTY RESULTS!\n\n`;
499+
directiveMessage += `REQUIRED ACTIONS:\n`;
500+
501+
if (emptyToolNames.includes('search_notes')) {
502+
directiveMessage += `1. IMMEDIATELY use keyword_search_notes with specific terms\n`;
503+
directiveMessage += `2. Try attribute_search if content might be tagged/categorized\n`;
504+
directiveMessage += `3. Use discover_tools to find alternative approaches\n`;
505+
}
506+
507+
if (emptyToolNames.includes('keyword_search_notes')) {
508+
directiveMessage += `1. IMMEDIATELY use search_notes for semantic matching\n`;
509+
directiveMessage += `2. Try broader or alternative keyword terms\n`;
510+
directiveMessage += `3. Use workflow_helper for guidance on next steps\n`;
511+
}
512+
513+
if (emptyToolNames.includes('attribute_search')) {
514+
directiveMessage += `1. Use search_notes to find content about the attribute topic\n`;
515+
directiveMessage += `2. Try different attribute names or types\n`;
516+
directiveMessage += `3. Use search_suggestion to see available attributes\n`;
517+
}
518+
519+
directiveMessage += `\nFORBIDDEN: Do NOT ask user for clarification or offer general information!\n`;
520+
directiveMessage += `REQUIRED: CONTINUE with alternative tools and approaches immediately!`;
521+
} else {
522+
// Has results - encourage follow-up actions
523+
directiveMessage = `EXCELLENT! You found results. Now CONTINUE the workflow:\n\n`;
524+
directiveMessage += `NEXT REQUIRED ACTIONS:\n`;
525+
directiveMessage += `1. Use read_note to examine the most relevant results\n`;
526+
directiveMessage += `2. Use workflow_helper to plan next steps based on your findings\n`;
527+
directiveMessage += `3. Consider using related tools for deeper analysis\n\n`;
528+
directiveMessage += `GOAL: Provide comprehensive information by using multiple tools in sequence.\n`;
529+
directiveMessage += `CONTINUE with tool usage - don't stop at just search results!`;
504530
}
505531

506-
directiveMessage += `DO NOT ask the user what to do next or if they want general information. CONTINUE SEARCHING with different parameters.`;
507-
508532
updatedMessages.push({
509533
role: 'system',
510534
content: directiveMessage
@@ -609,10 +633,19 @@ export class ToolCallingStage extends BasePipelineStage<ToolExecutionInput, { re
609633
}
610634
}
611635

612-
// Add a general suggestion to try search_notes as a fallback
636+
// Add general recommendations including new helper tools
613637
if (!toolName.includes('search_notes')) {
614638
guidance += "RECOMMENDATION: If specific searches fail, try the 'search_notes' tool which performs semantic searches.\n";
615639
}
640+
641+
// Always suggest helper tools for guidance
642+
guidance += "HELPER TOOLS AVAILABLE:\n";
643+
guidance += "• Use 'discover_tools' to find the right tool for your task\n";
644+
guidance += "• Use 'workflow_helper' to get guidance on next steps\n";
645+
guidance += "• Use 'search_suggestion' for search syntax help\n";
646+
647+
// Encourage continued tool usage
648+
guidance += "\nIMPORTANT: Don't stop after one failed tool - try alternatives immediately!";
616649

617650
return guidance;
618651
}

apps/server/src/services/llm/tools/attribute_search_tool.ts

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,26 +19,61 @@ export const attributeSearchToolDefinition: Tool = {
1919
type: 'function',
2020
function: {
2121
name: 'attribute_search',
22-
description: 'Search for notes with specific attributes (labels or relations). Use this when you need to find notes based on their metadata rather than content. IMPORTANT: attributeType must be exactly "label" or "relation" (lowercase).',
22+
description: `ATTRIBUTE-BASED search for notes. Find notes by their labels or relations (metadata/tags).
23+
24+
BEST FOR: Finding notes by categories, tags, status, relationships, or other metadata
25+
USE WHEN: You need notes with specific labels, relations, or organizational attributes
26+
DIFFERENT FROM: search_notes (content) and keyword_search_notes (text)
27+
28+
CRITICAL: attributeType MUST be exactly "label" or "relation" (lowercase only!)
29+
30+
COMMON ATTRIBUTES:
31+
• Labels: #important, #todo, #project, #status, #priority
32+
• Relations: ~relatedTo, ~childOf, ~contains, ~references
33+
34+
NEXT STEPS: Use read_note with returned noteId values for full content`,
2335
parameters: {
2436
type: 'object',
2537
properties: {
2638
attributeType: {
2739
type: 'string',
28-
description: 'MUST be exactly "label" or "relation" (lowercase, no other values are valid)',
40+
description: `MUST be exactly "label" or "relation" (lowercase only!)
41+
42+
CORRECT: "label", "relation"
43+
WRONG: "Label", "LABEL", "labels", "relations"
44+
45+
• "label" = tags/categories like #important, #todo
46+
• "relation" = connections like ~relatedTo, ~childOf`,
2947
enum: ['label', 'relation']
3048
},
3149
attributeName: {
3250
type: 'string',
33-
description: 'Name of the attribute to search for (e.g., "important", "todo", "related-to")'
51+
description: `Name of the attribute to search for.
52+
53+
LABEL EXAMPLES:
54+
- "important" (finds notes with #important)
55+
- "status" (finds notes with #status label)
56+
- "project" (finds notes tagged #project)
57+
58+
RELATION EXAMPLES:
59+
- "relatedTo" (finds notes with ~relatedTo relation)
60+
- "childOf" (finds notes with ~childOf relation)
61+
- "contains" (finds notes with ~contains relation)`
3462
},
3563
attributeValue: {
3664
type: 'string',
37-
description: 'Optional value of the attribute. If not provided, will find all notes with the given attribute name.'
65+
description: `OPTIONAL: Specific value of the attribute.
66+
67+
• Leave empty to find ALL notes with this attribute
68+
• Specify value to find notes where attribute = specific value
69+
70+
EXAMPLES:
71+
- attributeName: "status", attributeValue: "completed"
72+
- attributeName: "priority", attributeValue: "high"`
3873
},
3974
maxResults: {
4075
type: 'number',
41-
description: 'Maximum number of results to return (default: 20)'
76+
description: 'Number of results (1-50, default: 20). Use higher values for comprehensive searches.'
4277
}
4378
},
4479
required: ['attributeType', 'attributeName']
@@ -61,9 +96,33 @@ export class AttributeSearchTool implements ToolHandler {
6196

6297
log.info(`Executing attribute_search tool - Type: "${attributeType}", Name: "${attributeName}", Value: "${attributeValue || 'any'}", MaxResults: ${maxResults}`);
6398

64-
// Validate attribute type
99+
// Enhanced validation with helpful guidance
65100
if (attributeType !== 'label' && attributeType !== 'relation') {
66-
return `Error: Invalid attribute type. Must be exactly "label" or "relation" (lowercase). You provided: "${attributeType}".`;
101+
const suggestions: string[] = [];
102+
103+
if (attributeType.toLowerCase() === 'label' || attributeType.toLowerCase() === 'relation') {
104+
suggestions.push(`CASE SENSITIVE: Use "${attributeType.toLowerCase()}" (lowercase)`);
105+
}
106+
107+
if (attributeType.includes('label') || attributeType.includes('Label')) {
108+
suggestions.push('CORRECT: Use "label" for tags and categories');
109+
}
110+
111+
if (attributeType.includes('relation') || attributeType.includes('Relation')) {
112+
suggestions.push('CORRECT: Use "relation" for connections and relationships');
113+
}
114+
115+
const errorMessage = `Invalid attributeType: "${attributeType}"
116+
117+
REQUIRED: Must be exactly "label" or "relation" (lowercase only!)
118+
119+
${suggestions.length > 0 ? suggestions.join('\n') : ''}
120+
121+
EXAMPLES:
122+
• Find notes with #important tag: { "attributeType": "label", "attributeName": "important" }
123+
• Find notes with ~relatedTo relation: { "attributeType": "relation", "attributeName": "relatedTo" }`;
124+
125+
return errorMessage;
67126
}
68127

69128
// Execute the search

apps/server/src/services/llm/tools/keyword_search_tool.ts

Lines changed: 85 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,49 @@ export const keywordSearchToolDefinition: Tool = {
1717
type: 'function',
1818
function: {
1919
name: 'keyword_search_notes',
20-
description: 'Search for notes using exact keyword matching and attribute filters. Use this for precise searches when you need exact matches or want to filter by attributes.',
20+
description: `EXACT KEYWORD search for notes. Finds notes containing specific words, phrases, or attribute filters.
21+
22+
BEST FOR: Finding notes with specific words/phrases you know exist
23+
USE WHEN: You need exact text matches, specific terms, or attribute-based filtering
24+
DIFFERENT FROM: search_notes (which finds conceptual/semantic matches)
25+
26+
SEARCH TYPES:
27+
• Simple: "machine learning" (finds notes containing both words)
28+
• Phrase: "\"exact phrase\"" (finds this exact phrase)
29+
• Attributes: "#label" or "~relation" (notes with specific labels/relations)
30+
• Complex: "AI #project ~relatedTo" (combines keywords with attributes)
31+
32+
NEXT STEPS: Use read_note with returned noteId values for full content`,
2133
parameters: {
2234
type: 'object',
2335
properties: {
2436
query: {
2537
type: 'string',
26-
description: 'The search query using Trilium\'s search syntax. Examples: "rings tolkien" (find notes with both words), "#book #year >= 2000" (notes with label "book" and "year" attribute >= 2000), "note.content *=* important" (notes with "important" in content)'
38+
description: `Keyword search query using Trilium search syntax.
39+
40+
SIMPLE EXAMPLES:
41+
- "machine learning" (both words anywhere)
42+
- "\"project management\"" (exact phrase)
43+
- "python OR javascript" (either word)
44+
45+
ATTRIBUTE EXAMPLES:
46+
- "#important" (notes with 'important' label)
47+
- "~project" (notes with 'project' relation)
48+
- "#status = completed" (specific label value)
49+
50+
COMBINED EXAMPLES:
51+
- "AI #project #status = active" (AI content with project label and active status)
52+
- "note.title *= \"weekly\"" (titles containing 'weekly')
53+
54+
AVOID: Conceptual queries better suited for search_notes`
2755
},
2856
maxResults: {
2957
type: 'number',
30-
description: 'Maximum number of results to return (default: 10)'
58+
description: 'Number of results (1-50, default: 10). Use higher values for comprehensive searches.'
3159
},
3260
includeArchived: {
3361
type: 'boolean',
34-
description: 'Whether to include archived notes in search results (default: false)'
62+
description: 'INCLUDE ARCHIVED: Search archived notes too (default: false). Use true for complete historical search.'
3563
}
3664
},
3765
required: ['query']
@@ -45,6 +73,22 @@ export const keywordSearchToolDefinition: Tool = {
4573
export class KeywordSearchTool implements ToolHandler {
4674
public definition: Tool = keywordSearchToolDefinition;
4775

76+
/**
77+
* Convert a keyword query to a semantic query suggestion
78+
*/
79+
private convertToSemanticQuery(keywordQuery: string): string {
80+
// Remove search operators and attributes to create a semantic query
81+
return keywordQuery
82+
.replace(/#\w+/g, '') // Remove label filters
83+
.replace(/~\w+/g, '') // Remove relation filters
84+
.replace(/\"[^\"]*\"/g, (match) => match.slice(1, -1)) // Remove quotes but keep content
85+
.replace(/\s+OR\s+/gi, ' ') // Replace OR with space
86+
.replace(/\s+AND\s+/gi, ' ') // Replace AND with space
87+
.replace(/note\.(title|content)\s*\*=\*\s*/gi, '') // Remove note.content operators
88+
.replace(/\s+/g, ' ') // Normalize spaces
89+
.trim();
90+
}
91+
4892
/**
4993
* Execute the keyword search notes tool
5094
*/
@@ -80,21 +124,52 @@ export class KeywordSearchTool implements ToolHandler {
80124
log.info(`No matching notes found for query: "${query}"`);
81125
}
82126

83-
// Format the results
127+
// Format the results with enhanced guidance
128+
if (limitedResults.length === 0) {
129+
return {
130+
count: 0,
131+
results: [],
132+
query: query,
133+
searchType: 'keyword',
134+
message: 'No exact keyword matches found.',
135+
nextSteps: {
136+
immediate: [
137+
`Try search_notes for semantic/conceptual search: "${this.convertToSemanticQuery(query)}"`,
138+
`Use attribute_search if looking for specific labels or relations`,
139+
`Try simpler keywords or check spelling`
140+
],
141+
queryHelp: [
142+
'Remove quotes for broader matching',
143+
'Try individual words instead of phrases',
144+
'Use OR operator: "word1 OR word2"',
145+
'Check if content might be in archived notes (set includeArchived: true)'
146+
]
147+
}
148+
};
149+
}
150+
84151
return {
85152
count: limitedResults.length,
86153
totalFound: searchResults.length,
154+
query: query,
155+
searchType: 'keyword',
156+
message: 'Found exact keyword matches. Use noteId values with other tools.',
157+
nextSteps: {
158+
examine: `Use read_note with any noteId (e.g., "${limitedResults[0].noteId}") to get full content`,
159+
refine: limitedResults.length < searchResults.length ? `Found ${searchResults.length} total matches (showing ${limitedResults.length}). Increase maxResults for more.` : null,
160+
related: 'Use search_notes for conceptually related content beyond exact keywords'
161+
},
87162
results: limitedResults.map(note => {
88-
// Get a preview of the note content
163+
// Get a preview of the note content with highlighted search terms
89164
let contentPreview = '';
90165
try {
91166
const content = note.getContent();
92167
if (typeof content === 'string') {
93-
contentPreview = content.length > 150 ? content.substring(0, 150) + '...' : content;
168+
contentPreview = content.length > 200 ? content.substring(0, 200) + '...' : content;
94169
} else if (Buffer.isBuffer(content)) {
95170
contentPreview = '[Binary content]';
96171
} else {
97-
contentPreview = String(content).substring(0, 150) + (String(content).length > 150 ? '...' : '');
172+
contentPreview = String(content).substring(0, 200) + (String(content).length > 200 ? '...' : '');
98173
}
99174
} catch (e) {
100175
contentPreview = '[Content not available]';
@@ -114,7 +189,8 @@ export class KeywordSearchTool implements ToolHandler {
114189
attributes: attributes.length > 0 ? attributes : undefined,
115190
type: note.type,
116191
mime: note.mime,
117-
isArchived: note.isArchived
192+
isArchived: note.isArchived,
193+
dateModified: note.dateModified
118194
};
119195
})
120196
};

0 commit comments

Comments
 (0)