diff --git a/src/services/llm/tools/attribute_search_tool.ts b/src/services/llm/tools/attribute_search_tool.ts new file mode 100644 index 000000000..6d6bdaff6 --- /dev/null +++ b/src/services/llm/tools/attribute_search_tool.ts @@ -0,0 +1,156 @@ +/** + * Attribute Search Tool + * + * This tool allows the LLM to search for notes based specifically on attributes. + * It's specialized for finding notes with specific labels or relations. + */ + +import type { Tool, ToolHandler } from './tool_interfaces.js'; +import log from '../../log.js'; +import attributes from '../../attributes.js'; +import searchService from '../../search/services/search.js'; +import attributeFormatter from '../../attribute_formatter.js'; +import type BNote from '../../../becca/entities/bnote.js'; + +/** + * Definition of the attribute search tool + */ +export const attributeSearchToolDefinition: Tool = { + type: 'function', + function: { + name: 'attribute_search', + description: 'Search for notes with specific attributes (labels or relations). Use this when you need to find notes based on their metadata rather than content.', + parameters: { + type: 'object', + properties: { + attributeType: { + type: 'string', + description: 'Type of attribute to search for: "label" or "relation"', + enum: ['label', 'relation'] + }, + attributeName: { + type: 'string', + description: 'Name of the attribute to search for' + }, + attributeValue: { + type: 'string', + description: 'Optional value of the attribute. If not provided, will find all notes with the given attribute name.' + }, + maxResults: { + type: 'number', + description: 'Maximum number of results to return (default: 20)' + } + }, + required: ['attributeType', 'attributeName'] + } + } +}; + +/** + * Attribute search tool implementation + */ +export class AttributeSearchTool implements ToolHandler { + public definition: Tool = attributeSearchToolDefinition; + + /** + * Execute the attribute search tool + */ + public async execute(args: { attributeType: string, attributeName: string, attributeValue?: string, maxResults?: number }): Promise { + try { + const { attributeType, attributeName, attributeValue, maxResults = 20 } = args; + + log.info(`Executing attribute_search tool - Type: "${attributeType}", Name: "${attributeName}", Value: "${attributeValue || 'any'}", MaxResults: ${maxResults}`); + + // Validate attribute type + if (attributeType !== 'label' && attributeType !== 'relation') { + return `Error: Invalid attribute type. Must be either "label" or "relation".`; + } + + // Execute the search + log.info(`Searching for notes with ${attributeType}: ${attributeName}${attributeValue ? ' = ' + attributeValue : ''}`); + const searchStartTime = Date.now(); + + let results: BNote[] = []; + + if (attributeType === 'label') { + // For labels, we can use the existing getNotesWithLabel function + results = attributes.getNotesWithLabel(attributeName, attributeValue); + } else { + // For relations, we need to build a search query + const query = attributeFormatter.formatAttrForSearch({ + type: "relation", + name: attributeName, + value: attributeValue + }, attributeValue !== undefined); + + results = searchService.searchNotes(query, { + includeArchivedNotes: true, + ignoreHoistedNote: true + }); + } + + // Limit results + const limitedResults = results.slice(0, maxResults); + + const searchDuration = Date.now() - searchStartTime; + + log.info(`Attribute search completed in ${searchDuration}ms, found ${results.length} matching notes, returning ${limitedResults.length}`); + + if (limitedResults.length > 0) { + // Log top results + limitedResults.slice(0, 3).forEach((note: BNote, index: number) => { + log.info(`Result ${index + 1}: "${note.title}"`); + }); + } else { + log.info(`No notes found with ${attributeType} "${attributeName}"${attributeValue ? ' = ' + attributeValue : ''}`); + } + + // Format the results + return { + count: limitedResults.length, + totalFound: results.length, + attributeType, + attributeName, + attributeValue, + results: limitedResults.map((note: BNote) => { + // Get relevant attributes of this type + const relevantAttributes = note.getOwnedAttributes() + .filter(attr => attr.type === attributeType && attr.name === attributeName) + .map(attr => ({ + type: attr.type, + name: attr.name, + value: attr.value + })); + + // Get a preview of the note content + let contentPreview = ''; + try { + const content = note.getContent(); + if (typeof content === 'string') { + contentPreview = content.length > 150 ? content.substring(0, 150) + '...' : content; + } else if (Buffer.isBuffer(content)) { + contentPreview = '[Binary content]'; + } else { + contentPreview = String(content).substring(0, 150) + (String(content).length > 150 ? '...' : ''); + } + } catch (e) { + contentPreview = '[Content not available]'; + } + + return { + noteId: note.noteId, + title: note.title, + preview: contentPreview, + relevantAttributes: relevantAttributes, + type: note.type, + dateCreated: note.dateCreated, + dateModified: note.dateModified + }; + }) + }; + } catch (error: any) { + log.error(`Error executing attribute_search tool: ${error.message || String(error)}`); + return `Error: ${error.message || String(error)}`; + } + } +} diff --git a/src/services/llm/tools/keyword_search_tool.ts b/src/services/llm/tools/keyword_search_tool.ts new file mode 100644 index 000000000..8365d38f4 --- /dev/null +++ b/src/services/llm/tools/keyword_search_tool.ts @@ -0,0 +1,126 @@ +/** + * Keyword Search Notes Tool + * + * This tool allows the LLM to search for notes using exact keyword matching and attribute-based filters. + * It complements the semantic search tool by providing more precise, rule-based search capabilities. + */ + +import type { Tool, ToolHandler } from './tool_interfaces.js'; +import log from '../../log.js'; +import searchService from '../../search/services/search.js'; +import becca from '../../../becca/becca.js'; + +/** + * Definition of the keyword search notes tool + */ +export const keywordSearchToolDefinition: Tool = { + type: 'function', + function: { + name: 'keyword_search_notes', + 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.', + parameters: { + type: 'object', + properties: { + query: { + type: 'string', + 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)' + }, + maxResults: { + type: 'number', + description: 'Maximum number of results to return (default: 10)' + }, + includeArchived: { + type: 'boolean', + description: 'Whether to include archived notes in search results (default: false)' + } + }, + required: ['query'] + } + } +}; + +/** + * Keyword search notes tool implementation + */ +export class KeywordSearchTool implements ToolHandler { + public definition: Tool = keywordSearchToolDefinition; + + /** + * Execute the keyword search notes tool + */ + public async execute(args: { query: string, maxResults?: number, includeArchived?: boolean }): Promise { + try { + const { query, maxResults = 10, includeArchived = false } = args; + + log.info(`Executing keyword_search_notes tool - Query: "${query}", MaxResults: ${maxResults}, IncludeArchived: ${includeArchived}`); + + // Execute the search + log.info(`Performing keyword search for: "${query}"`); + const searchStartTime = Date.now(); + + // Find results with the given query + const searchContext = { + includeArchivedNotes: includeArchived, + fuzzyAttributeSearch: false + }; + + const searchResults = searchService.searchNotes(query, searchContext); + const limitedResults = searchResults.slice(0, maxResults); + + const searchDuration = Date.now() - searchStartTime; + + log.info(`Keyword search completed in ${searchDuration}ms, found ${searchResults.length} matching notes, returning ${limitedResults.length}`); + + if (limitedResults.length > 0) { + // Log top results + limitedResults.slice(0, 3).forEach((result, index) => { + log.info(`Result ${index + 1}: "${result.title}"`); + }); + } else { + log.info(`No matching notes found for query: "${query}"`); + } + + // Format the results + return { + count: limitedResults.length, + totalFound: searchResults.length, + results: limitedResults.map(note => { + // Get a preview of the note content + let contentPreview = ''; + try { + const content = note.getContent(); + if (typeof content === 'string') { + contentPreview = content.length > 150 ? content.substring(0, 150) + '...' : content; + } else if (Buffer.isBuffer(content)) { + contentPreview = '[Binary content]'; + } else { + contentPreview = String(content).substring(0, 150) + (String(content).length > 150 ? '...' : ''); + } + } catch (e) { + contentPreview = '[Content not available]'; + } + + // Get note attributes + const attributes = note.getOwnedAttributes().map(attr => ({ + type: attr.type, + name: attr.name, + value: attr.value + })); + + return { + noteId: note.noteId, + title: note.title, + preview: contentPreview, + attributes: attributes.length > 0 ? attributes : undefined, + type: note.type, + mime: note.mime, + isArchived: note.isArchived + }; + }) + }; + } catch (error: any) { + log.error(`Error executing keyword_search_notes tool: ${error.message || String(error)}`); + return `Error: ${error.message || String(error)}`; + } + } +} diff --git a/src/services/llm/tools/search_suggestion_tool.ts b/src/services/llm/tools/search_suggestion_tool.ts new file mode 100644 index 000000000..6962e65b2 --- /dev/null +++ b/src/services/llm/tools/search_suggestion_tool.ts @@ -0,0 +1,179 @@ +/** + * Search Suggestion Tool + * + * This tool provides guidance on how to formulate different types of search queries in Trilium. + * It helps the LLM understand the correct syntax for various search scenarios. + */ + +import type { Tool, ToolHandler } from './tool_interfaces.js'; +import log from '../../log.js'; + +// Template types +type QueryTemplate = { + template: string; + description: string; +}; + +type SearchTypesMap = { + basic: QueryTemplate[]; + attribute: QueryTemplate[]; + content: QueryTemplate[]; + relation: QueryTemplate[]; + date: QueryTemplate[]; + advanced: QueryTemplate[]; +}; + +type SearchType = keyof SearchTypesMap; + +/** + * Definition of the search suggestion tool + */ +export const searchSuggestionToolDefinition: Tool = { + type: 'function', + function: { + name: 'search_suggestion', + description: 'Get suggestions on how to formulate different types of search queries in Trilium. Use this when you need help constructing the right search syntax.', + parameters: { + type: 'object', + properties: { + searchType: { + type: 'string', + description: 'Type of search you want suggestions for', + enum: [ + 'basic', + 'attribute', + 'content', + 'relation', + 'date', + 'advanced' + ] + }, + userQuery: { + type: 'string', + description: 'The user\'s original query or description of what they want to search for' + } + }, + required: ['searchType'] + } + } +}; + +/** + * Search suggestion tool implementation + */ +export class SearchSuggestionTool implements ToolHandler { + public definition: Tool = searchSuggestionToolDefinition; + + // Example query templates for each search type + private queryTemplates: SearchTypesMap = { + basic: [ + { template: '"{term1}"', description: 'Exact phrase search' }, + { template: '{term1} {term2}', description: 'Find notes containing both terms' }, + { template: '{term1} OR {term2}', description: 'Find notes containing either term' } + ], + attribute: [ + { template: '#{attributeName}', description: 'Find notes with a specific label' }, + { template: '#{attributeName} = {value}', description: 'Find notes with label equal to value' }, + { template: '#{attributeName} >= {value}', description: 'Find notes with numeric label greater or equal to value' }, + { template: '#{attributeName} *= {value}', description: 'Find notes with label containing value' }, + { template: '~{relationName}.title *= {value}', description: 'Find notes with relation to note whose title contains value' } + ], + content: [ + { template: 'note.content *= "{text}"', description: 'Find notes containing specific text in content' }, + { template: 'note.content =* "{text}"', description: 'Find notes whose content starts with text' }, + { template: 'note.content %= "{regex}"', description: 'Find notes whose content matches regex pattern' } + ], + relation: [ + { template: '~{relationName}', description: 'Find notes with a specific relation' }, + { template: '~{relationName}.title *= {text}', description: 'Find notes related to notes with title containing text' }, + { template: '~{relationName}.#tag', description: 'Find notes related to notes with specific label' } + ], + date: [ + { template: '#dateNote = MONTH', description: 'Find notes with dateNote attribute equal to current month' }, + { template: '#dateNote >= TODAY-7', description: 'Find notes with dateNote in the last week' }, + { template: '#dateCreated >= YEAR-1', description: 'Find notes created within the last year' } + ], + advanced: [ + { template: '#book AND #year >= 2020 AND note.content *= "important"', description: 'Combined attribute and content search' }, + { template: '#project AND (#status=active OR #status=pending)', description: 'Complex attribute condition' }, + { template: 'note.children.title *= {text}', description: 'Find notes whose children contain text in title' } + ] + }; + + /** + * Execute the search suggestion tool + */ + public async execute(args: { searchType: string, userQuery?: string }): Promise { + try { + const { searchType, userQuery = '' } = args; + + log.info(`Executing search_suggestion tool - Type: "${searchType}", UserQuery: "${userQuery}"`); + + // Validate search type + if (!this.isValidSearchType(searchType)) { + return { + error: `Invalid search type: ${searchType}`, + validTypes: Object.keys(this.queryTemplates) + }; + } + + // Generate suggestions based on search type and user query + const templates = this.queryTemplates[searchType as SearchType]; + + // Extract potential terms from the user query + const terms = userQuery + .split(/\s+/) + .filter(term => term.length > 2) + .map(term => term.replace(/[^\w\s]/g, '')); + + // Fill templates with user terms if available + const suggestions = templates.map((template: QueryTemplate) => { + let filledTemplate = template.template; + + // Try to fill in term1, term2, etc. + if (terms.length > 0) { + for (let i = 0; i < Math.min(terms.length, 3); i++) { + filledTemplate = filledTemplate.replace(`{term${i+1}}`, terms[i]); + } + } + + // For attribute/relation examples, try to use something meaningful + if (searchType === 'attribute' || searchType === 'relation') { + // These are common attribute/relation names in note-taking contexts + const commonAttributes = ['tag', 'category', 'status', 'priority', 'project', 'area', 'year']; + filledTemplate = filledTemplate.replace('{attributeName}', commonAttributes[Math.floor(Math.random() * commonAttributes.length)]); + filledTemplate = filledTemplate.replace('{relationName}', 'parent'); + } + + // Fill remaining placeholders with generic examples + filledTemplate = filledTemplate + .replace('{text}', terms[0] || 'example') + .replace('{value}', terms[1] || 'value') + .replace('{regex}', '[a-z]+'); + + return { + query: filledTemplate, + description: template.description + }; + }); + + return { + searchType, + userQuery, + suggestions, + note: "Use these suggestions with keyword_search_notes or attribute_search tools to find relevant notes." + }; + + } catch (error: any) { + log.error(`Error executing search_suggestion tool: ${error.message || String(error)}`); + return `Error: ${error.message || String(error)}`; + } + } + + /** + * Check if a search type is valid + */ + private isValidSearchType(searchType: string): searchType is SearchType { + return Object.keys(this.queryTemplates).includes(searchType); + } +} diff --git a/src/services/llm/tools/tool_initializer.ts b/src/services/llm/tools/tool_initializer.ts index 9f6ef9750..1d2b7e56a 100644 --- a/src/services/llm/tools/tool_initializer.ts +++ b/src/services/llm/tools/tool_initializer.ts @@ -6,11 +6,17 @@ 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 { SearchSuggestionTool } from './search_suggestion_tool.js'; import { ReadNoteTool } from './read_note_tool.js'; import { NoteCreationTool } from './note_creation_tool.js'; import { NoteUpdateTool } from './note_update_tool.js'; import { ContentExtractionTool } from './content_extraction_tool.js'; import { RelationshipTool } from './relationship_tool.js'; +import { AttributeManagerTool } from './attribute_manager_tool.js'; +import { CalendarIntegrationTool } from './calendar_integration_tool.js'; +import { NoteSummarizationTool } from './note_summarization_tool.js'; import log from '../../log.js'; /** @@ -20,17 +26,25 @@ export async function initializeTools(): Promise { try { log.info('Initializing LLM tools...'); - // Register basic note search and read tools - toolRegistry.registerTool(new SearchNotesTool()); - toolRegistry.registerTool(new ReadNoteTool()); + // Register search and discovery tools + toolRegistry.registerTool(new SearchNotesTool()); // Semantic search + toolRegistry.registerTool(new KeywordSearchTool()); // Keyword-based search + toolRegistry.registerTool(new AttributeSearchTool()); // Attribute-specific search + toolRegistry.registerTool(new SearchSuggestionTool()); // Search syntax helper + toolRegistry.registerTool(new ReadNoteTool()); // Read note content // Register note creation and manipulation tools - toolRegistry.registerTool(new NoteCreationTool()); - toolRegistry.registerTool(new NoteUpdateTool()); + toolRegistry.registerTool(new NoteCreationTool()); // Create new notes + toolRegistry.registerTool(new NoteUpdateTool()); // Update existing notes + toolRegistry.registerTool(new NoteSummarizationTool()); // Summarize note content + + // Register attribute and relationship tools + toolRegistry.registerTool(new AttributeManagerTool()); // Manage note attributes + toolRegistry.registerTool(new RelationshipTool()); // Manage note relationships // Register content analysis tools - toolRegistry.registerTool(new ContentExtractionTool()); - toolRegistry.registerTool(new RelationshipTool()); + toolRegistry.registerTool(new ContentExtractionTool()); // Extract info from note content + toolRegistry.registerTool(new CalendarIntegrationTool()); // Calendar-related operations // Log registered tools const toolCount = toolRegistry.getAllTools().length;