bookwiz.io / lib / services / tool-executor.ts
tool-executor.ts
Raw
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<any> {
    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<any> {
    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<any> {
    // 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<string>() // 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**`
    }
  }
}