import { FileOperationsService } from './file-operations' export interface ToolCall { id: string type: 'function' function: { name: string arguments: string } } export interface ToolResult { role: 'tool' tool_call_id: string name: string content: string } export class ToolExecutor { private fileService: FileOperationsService constructor(customSupabaseClient?: any) { this.fileService = new FileOperationsService(customSupabaseClient) } getToolDefinitions() { return [ { type: "function", function: { name: "search_files", description: "🔍 PRIMARY DISCOVERY TOOL: Use this FIRST for any user request involving specific files, characters, chapters, or content. Searches by name or content and returns file IDs needed for other tools. Essential for: questions about existing content, edit requests, file references. TIP: If this doesn't find what you're looking for, try list_files to see what's available, or try variations with extensions like 'overview.md' or partial matches.", parameters: { type: "object", properties: { query: { type: "string", description: "Search query to find files (e.g., 'Kris', 'chapter 1', 'character notes', 'outline'). Try different variations if first search fails." } }, required: ["query"] } } }, { type: "function", function: { name: "smart_search", description: "🎯 INTELLIGENT SEARCH: Use when regular search_files fails or you need to find files with variations. Tries multiple search strategies: exact match, partial match, with extensions, similar names, etc. Great for finding files when you know part of the name but search_files returned empty.", parameters: { type: "object", properties: { base_name: { type: "string", description: "Base name to search for (e.g., 'book-overview', 'character notes')" } }, required: ["base_name"] } } }, { type: "function", function: { name: "list_files", description: "📁 EXPLORATION TOOL: Use to see project structure and discover what files exist. Good for understanding organization before working with specific files, or when user asks 'what do I have' or wants to see their project.", parameters: { type: "object", properties: { folder_id: { type: "string", description: "Optional: ID of a specific folder to list. If not provided, lists root level." } }, required: [] } } }, { type: "function", function: { name: "read_file", description: "📖 CONTENT READER: Get complete file content for analysis, editing, or answering questions. ALWAYS use after search_files to get the actual content. CRITICAL: Only use file_id from search_files or list_files results, NEVER use file names as IDs.", parameters: { type: "object", properties: { file_id: { type: "string", description: "The UUID file ID from search_files or list_files results (NOT the file name)" } }, required: ["file_id"] } } }, { type: "function", function: { name: "update_file", description: "✏️ CONTENT EDITOR: Update/edit existing files with new content. Use after reading current content. CRITICAL: Only use file_id from search_files or list_files results, NEVER use file names as IDs.", parameters: { type: "object", properties: { file_id: { type: "string", description: "The UUID file ID from search_files or list_files results (NOT the file name)" }, new_content: { type: "string", description: "The new content for the file" }, change_summary: { type: "string", description: "Brief summary of what changes were made" } }, required: ["file_id", "new_content", "change_summary"] } } }, { type: "function", function: { name: "create_file", description: "📝 CONTENT CREATOR: Create new files when user wants to add content that doesn't exist yet. Use when user asks to create something new.", parameters: { type: "object", properties: { name: { type: "string", description: "Name of the new file (e.g., 'anya.md', 'chapter-2.md', 'character-notes.md'). Do NOT include folder paths." }, content: { type: "string", description: "Initial content for the new file" }, parent_folder_id: { type: "string", description: "Optional: ID of the parent folder. If not provided, creates in root." } }, required: ["name", "content"] } } }, { type: "function", function: { name: "create_folder", description: "📁 FOLDER CREATOR: Create new folders to organize files. Use before creating files that should be organized in folders. Essential for creating proper folder structure.", parameters: { type: "object", properties: { name: { type: "string", description: "Name of the new folder (e.g., 'characters', 'chapters', 'research')" }, parent_folder_id: { type: "string", description: "Optional: ID of the parent folder. If not provided, creates in root." } }, required: ["name"] } } }, { type: "function", function: { name: "delete_file", description: "🗑️ CONTENT REMOVER: Delete files or folders that are no longer needed. Use carefully and only when explicitly requested by user.", parameters: { type: "object", properties: { file_id: { type: "string", description: "The UUID file ID from search_files or list_files results (NOT the file name)" } }, required: ["file_id"] } } }, { type: "function", function: { name: "grep_files", description: "🔎 TEXT SEARCH TOOL: Search for specific text patterns within file contents. Use to find exact phrases, keywords, or patterns across your project. More precise than search_files for finding specific content within files.", parameters: { type: "object", properties: { pattern: { type: "string", description: "The text pattern or phrase to search for within files" }, file_filter: { type: "string", description: "Optional: Filter to specific file types or names (e.g., '*.md', 'chapter', etc.)" }, case_sensitive: { type: "boolean", description: "Optional: Whether the search should be case sensitive (default: false)" } }, required: ["pattern"] } } } ] } async executeToolCall(bookId: string, toolCall: ToolCall, userId?: string): Promise { const { name: toolName, arguments: toolArgs } = toolCall.function // Handle empty or invalid JSON arguments let args: any = {} if (toolArgs && toolArgs.trim()) { try { args = JSON.parse(toolArgs) } catch (error) { console.error('Failed to parse tool arguments:', toolArgs, error) const errorMessage = error instanceof Error ? error.message : 'Unknown parsing error' throw new Error(`Invalid tool arguments: ${errorMessage}`) } } // Execute the tool directly return await this.executeTool(toolName, args, bookId) } private async executeTool(toolName: string, args: any, bookId: string): Promise { let result: any switch (toolName) { case 'search_files': result = await this.fileService.searchFiles(bookId, args.query || '') break case 'smart_search': result = await this.smartSearch(bookId, args.base_name || '') break case 'read_file': result = await this.fileService.readFile(bookId, args.file_id) break case 'update_file': result = await this.fileService.updateFile(bookId, args.file_id, args.new_content, args.change_summary || 'Updated via AI') break case 'create_file': result = await this.fileService.createFile(bookId, args.name, args.content || '', args.parent_folder_id) break case 'create_folder': result = await this.fileService.createFolder(bookId, args.name, args.parent_folder_id) break case 'delete_file': result = await this.fileService.deleteFile(bookId, args.file_id) break case 'list_files': result = await this.fileService.listFiles(bookId, args.folder_id) break case 'grep_files': result = await this.fileService.grepFiles(bookId, args.pattern || '', args.file_filter || '', args.case_sensitive || false) break default: throw new Error(`Unknown tool: ${toolName}`) } // Dispatch event for file-modifying operations to trigger UI refresh if (this.isFileModifyingOperation(toolName)) { console.log(`🔄 ToolExecutor: Dispatching file operation completed event for ${toolName}`) if (typeof window !== 'undefined') { window.dispatchEvent(new CustomEvent('ai-file-operation-completed', { detail: { operation: toolName, bookId: bookId, result: result } })) } } return result } private isFileModifyingOperation(toolName: string): boolean { return ['update_file', 'create_file', 'create_folder', 'delete_file'].includes(toolName) } private async smartSearch(bookId: string, baseName: string): Promise { // Try multiple search strategies in order of specificity const searchVariations = [ baseName, // exact match `${baseName}.md`, // with .md extension `${baseName}.txt`, // with .txt extension baseName.replace(/-/g, ' '), // replace hyphens with spaces baseName.replace(/_/g, ' '), // replace underscores with spaces baseName.replace(/[-_]/g, ''), // remove separators ...baseName.split(/[-_\s]+/), // individual words ] let allResults: any[] = [] const foundFiles = new Set() // to avoid duplicates // Try each variation for (const query of searchVariations) { if (query.length < 2) continue // skip very short queries try { const results = await this.fileService.searchFiles(bookId, query) // Add new unique results for (const result of results) { if (!foundFiles.has(result.id)) { foundFiles.add(result.id) allResults.push({ ...result, search_query: query, // track which query found this match_type: query === baseName ? 'exact' : 'variation' }) } } // If we found exact matches, prioritize them if (query === baseName && results.length > 0) { break } } catch (error) { console.warn(`Search variation "${query}" failed:`, error) } } // If still no results, try listing files and fuzzy matching if (allResults.length === 0) { try { const allFiles = await this.fileService.listFiles(bookId) const fuzzyMatches = allFiles.filter((file: any) => { const fileName = file.name.toLowerCase() const searchTerm = baseName.toLowerCase() return fileName.includes(searchTerm) || searchTerm.split(/[-_\s]+/).some((word: string) => fileName.includes(word)) }) allResults = fuzzyMatches.map((file: any) => ({ ...file, search_query: 'fuzzy_match', match_type: 'fuzzy' })) } catch (error) { console.warn('Fuzzy matching failed:', error) } } return { original_query: baseName, total_found: allResults.length, files: allResults.slice(0, 10), // limit to 10 results search_strategies_used: searchVariations.slice(0, 5) } } getToolDescription(toolName: string, toolArgs: any): string { switch (toolName) { case 'search_files': return `🔍 Looking for: *"${toolArgs.query}"*` case 'smart_search': return `🎯 Intelligent search for: *"${toolArgs.base_name}"* (trying multiple variations)` case 'read_file': return `📖 Reading file...` case 'update_file': return `✏️ Updating file...` case 'create_file': return `📝 Creating: *"${toolArgs.name}"*` case 'create_folder': return `📁 Creating folder: *"${toolArgs.name}"*` case 'list_files': return `📁 Checking files...` case 'delete_file': return `🗑️ Deleting file...` case 'grep_files': return `🔎 Searching for: *"${toolArgs.pattern}"*` default: return `🛠️ Executing ${toolName}...` } } formatToolResult(toolName: string, toolResult: any): string { switch (toolName) { case 'search_files': if (Array.isArray(toolResult) && toolResult.length > 0) { const fileLinks = toolResult.map((f: any) => `[📄 ${f.name}](file://${f.id})`).join(', ') return `Found: ${fileLinks}` } else { return 'No files found.' } case 'smart_search': if (toolResult.total_found > 0) { const fileLinks = toolResult.files.map((f: any) => { const matchInfo = f.match_type === 'exact' ? '🎯' : f.match_type === 'fuzzy' ? '🔍' : '🔄' return `${matchInfo} [📄 ${f.name}](file://${f.id})` }).join(', ') return `Found ${toolResult.total_found} file(s): ${fileLinks}` } else { return `No files found for "${toolResult.original_query}". Tried variations: ${toolResult.search_strategies_used.join(', ')}` } case 'read_file': if (toolResult.content) { const preview = toolResult.content.length > 100 ? toolResult.content.substring(0, 100) + '...' : toolResult.content return `**[📄 ${toolResult.name}](file://${toolResult.id})**\n\`\`\`\n${preview}\n\`\`\`` } else { return `**[📄 ${toolResult.name}](file://${toolResult.id})** (empty)` } case 'update_file': return `✅ Updated [📄 ${toolResult.name}](file://${toolResult.id})` case 'create_file': return `✅ Created [📄 ${toolResult.name}](file://${toolResult.id})` case 'create_folder': return `✅ Created [📁 ${toolResult.name}](folder://${toolResult.id})` case 'list_files': if (Array.isArray(toolResult) && toolResult.length > 0) { const folders = toolResult.filter((item: any) => item.type === 'folder') const files = toolResult.filter((item: any) => item.type === 'file') let message = '' if (folders.length > 0) { const folderLinks = folders.map((f: any) => `[📁 ${f.name}](folder://${f.id})`).join(', ') message += `${folderLinks}\n` } if (files.length > 0) { const fileLinks = files.map((f: any) => `[📄 ${f.name}](file://${f.id})`).join(', ') message += `${fileLinks}` } return message } else { return 'Empty folder' } case 'delete_file': return `✅ Deleted ${toolResult.name}` case 'grep_files': if (Array.isArray(toolResult) && toolResult.length > 0) { const summary = toolResult.map((f: any) => `**[📄 ${f.name}](file://${f.id})** (${f.total_matches} match${f.total_matches > 1 ? 'es' : ''})` ).join(', ') let details = '' toolResult.forEach((file: any) => { details += `\n\n**[📄 ${file.name}](file://${file.id}):**\n` file.matches.slice(0, 3).forEach((match: any) => { details += `Line ${match.line_number}: \`${match.content}\`\n` }) if (file.matches.length > 3) { details += `... and ${file.matches.length - 3} more matches\n` } }) return `Found in: ${summary}${details}` } else { return 'No matches found.' } default: return `**Tool completed successfully**` } } }