Class: Clacky::Tools::FileReader
- Defined in:
- lib/clacky/tools/file_reader.rb
Constant Summary collapse
- MAX_TEXT_FILE_SIZE =
Maximum text file size (1MB)
1 * 1024 * 1024
- MAX_CONTENT_CHARS =
Maximum content size to return (~10,000 tokens = ~40,000 characters)
40_000- MAX_LINE_CHARS =
Maximum characters per line (prevent single huge lines from bloating tokens)
1000
Instance Method Summary collapse
- #execute(path:, max_lines: 500, start_line: nil, end_line: nil, working_dir: nil) ⇒ Object
- #format_call(args) ⇒ Object
- #format_result(result) ⇒ Object
-
#format_result_for_llm(result) ⇒ Object
Format result for LLM - handles both text and binary (image/PDF) content This method is called by the agent to format tool results before sending to LLM.
Methods inherited from Base
#category, #description, #name, #parameters, #to_function_definition
Instance Method Details
#execute(path:, max_lines: 500, start_line: nil, end_line: nil, working_dir: nil) ⇒ Object
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 |
# File 'lib/clacky/tools/file_reader.rb', line 47 def execute(path:, max_lines: 500, start_line: nil, end_line: nil, working_dir: nil) # Expand path relative to working_dir when provided = (path, working_dir: working_dir) unless File.exist?() return { path: , content: nil, error: "File not found: #{}" } end # If path is a directory, list its first-level contents (similar to filetree) if File.directory?() return list_directory_contents() end unless File.file?() return { path: , content: nil, error: "Path is not a file: #{}" } end begin # Check if file is binary if Utils::FileProcessor.binary_file_path?() return handle_binary_file() end # Check text file size (only for non-binary files) file_size = File.size() if file_size > MAX_TEXT_FILE_SIZE return { path: , content: nil, size_bytes: file_size, error: "Text file too large: #{format_file_size(file_size)} (max: #{format_file_size(MAX_TEXT_FILE_SIZE)}). Please use grep tool to search within this file instead." } end # Read text file with optional line range all_lines = File.readlines() total_lines = all_lines.size # Calculate start index (convert 1-indexed to 0-indexed) start_idx = start_line ? [start_line - 1, 0].max : 0 # Calculate end index based on parameters if end_line # User specified end_line directly end_idx = [end_line - 1, total_lines - 1].min elsif start_line # start_line + max_lines - 1 (relative to start_line, inclusive) calculated_end_line = start_line + max_lines - 1 end_idx = [calculated_end_line - 1, total_lines - 1].min else # Read from beginning with max_lines limit end_idx = [max_lines - 1, total_lines - 1].min end # Check if start_line exceeds file length first if start_idx >= total_lines return { path: , content: nil, lines_read: 0, error: "Invalid line range: start_line #{start_line} exceeds total lines (#{total_lines})" } end # Validate range if start_idx > end_idx return { path: , content: nil, lines_read: 0, error: "Invalid line range: start_line #{start_line} > end_line #{end_line || (start_line + max_lines)}" } end lines = all_lines[start_idx..end_idx] || [] # Truncate individual lines that are too long lines = lines.map do |line| if line.length > MAX_LINE_CHARS line[0...MAX_LINE_CHARS] + "... [Line truncated - #{line.length} chars]\n" else line end end content = lines.join truncated = end_idx < (total_lines - 1) # Truncate total content if it exceeds maximum size if content.length > MAX_CONTENT_CHARS content = content[0...MAX_CONTENT_CHARS] + "\n\n[Content truncated - exceeded #{MAX_CONTENT_CHARS} characters (~10,000 tokens)]" + "\nUse start_line/end_line parameters to read specific sections, or grep tool to search for keywords." truncated = true end { path: , content: content, lines_read: lines.size, total_lines: total_lines, truncated: truncated, start_line: start_line, end_line: end_line, error: nil } rescue StandardError => e { path: , content: nil, error: "Error reading file: #{e.}" } end end |
#format_call(args) ⇒ Object
170 171 172 173 |
# File 'lib/clacky/tools/file_reader.rb', line 170 def format_call(args) path = args[:path] || args['path'] "Read(#{Utils::PathHelper.safe_basename(path)})" end |
#format_result(result) ⇒ Object
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 |
# File 'lib/clacky/tools/file_reader.rb', line 175 def format_result(result) return result[:error] if result[:error] # Handle directory listing if result[:is_directory] || result['is_directory'] entries = result[:entries_count] || result['entries_count'] || 0 dirs = result[:directories_count] || result['directories_count'] || 0 files = result[:files_count] || result['files_count'] || 0 return "Listed #{entries} entries (#{dirs} directories, #{files} files)" end # Handle binary file if result[:binary] || result['binary'] format_type = result[:format] || result['format'] || 'unknown' size = result[:size_bytes] || result['size_bytes'] || 0 # Check if it has base64 data (LLM-compatible format) if result[:base64_data] || result['base64_data'] size_warning = size > Utils::FileProcessor::MAX_FILE_SIZE ? " (WARNING: large file)" : "" return "Binary file (#{format_type}, #{format_file_size(size)}) - sent to LLM#{size_warning}" else return "Binary file (#{format_type}, #{format_file_size(size)}) - cannot be read as text" end end # Handle text file reading lines = result[:lines_read] || result['lines_read'] || 0 truncated = result[:truncated] || result['truncated'] "Read #{lines} lines#{truncated ? ' (truncated)' : ''}" end |
#format_result_for_llm(result) ⇒ Object
Format result for LLM - handles both text and binary (image/PDF) content This method is called by the agent to format tool results before sending to LLM
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 |
# File 'lib/clacky/tools/file_reader.rb', line 208 def format_result_for_llm(result) # For LLM-compatible binary files with base64 data, return as content blocks if result[:binary] && result[:base64_data] # Create a text description description = "File: #{result[:path]}\nType: #{result[:format]}\nSize: #{format_file_size(result[:size_bytes])}" # Add size warning for large files if result[:size_bytes] > Utils::FileProcessor::MAX_FILE_SIZE description += "\nWARNING: Large file (>#{Utils::FileProcessor::MAX_FILE_SIZE / 1024}KB) - may consume significant tokens" end # For images, return content blocks Array (same pattern as browser tool) # so build_success_result passes it through as-is instead of JSON.generate-ing it. if result[:mime_type]&.start_with?("image/") return [ { type: "text", text: description }, { type: "image_url", image_url: { url: "data:#{result[:mime_type]};base64,#{result[:base64_data]}" } } ] end # For PDFs and other binary formats, just return metadata with base64 return { type: "document", path: result[:path], format: result[:format], size_bytes: result[:size_bytes], mime_type: result[:mime_type], base64_data: result[:base64_data], description: description } end # For error cases, return hash as-is return result if result[:error] || result[:content].nil? # For directory listings, return as-is (no raw file content to preserve) return result if result[:is_directory] # For plain text files: return a plain string so the agent sends it # directly to the LLM without JSON-encoding (avoids \" / \n escaping). header = "File: #{result[:path]}" header += " (lines #{result[:start_line]}-#{result[:end_line]})" if result[:start_line] header += " [#{result[:lines_read]}/#{result[:total_lines]} lines]" header += " [TRUNCATED]" if result[:truncated] "#{header}\n\n#{result[:content]}" end |