Module: Legion::LLM::Tools::Special
- Extended by:
- Legion::Logging::Helper
- Defined in:
- lib/legion/llm/tools/special.rb
Constant Summary collapse
- LIST_SPECIAL_TOOLS_NAME =
'legion_list_special_tools'- LIST_ALL_TOOLS_NAME =
'legion_list_all_tools'- DEFAULT_TIMEOUT_MS =
120_000- MAX_TIMEOUT_MS =
600_000- PYTHON_PACKAGES =
%w[ python-pptx python-docx openpyxl pandas pillow requests lxml PyYAML tabulate markdown ].freeze
Class Method Summary collapse
- .all_tools_definition ⇒ Object
- .all_tools_inventory(**args) ⇒ Object
- .array_args(args) ⇒ Object
- .dispatch(tool_name, **args) ⇒ Object
- .dispatch_runtime(runtime_name, executable, **args) ⇒ Object
- .executable_file?(path) ⇒ Boolean
- .executable_from_path(command) ⇒ Object
- .inventory ⇒ Object
- .legionio_packaged_ruby?(path) ⇒ Boolean
- .normalize_inventory_entry(entry) ⇒ Object
- .normalize_tool_name(tool_name) ⇒ Object
- .path_ruby ⇒ Object
- .pinned_definitions ⇒ Object
- .pip_candidates_for(bin_dir) ⇒ Object
- .pip_path ⇒ Object
- .pip_schema ⇒ Object
- .process_cwd(args) ⇒ Object
- .process_ruby_path ⇒ Object
- .process_stdin(args) ⇒ Object
- .python_available? ⇒ Boolean
- .python_definitions ⇒ Object
- .python_path ⇒ Object
- .python_venv_dir ⇒ Object
- .reset_runtime_cache! ⇒ Object
- .ruby_definition ⇒ Object
- .ruby_path ⇒ Object
- .run_process(executable, argv, **args) ⇒ Object
- .runtime_argv(runtime_name, **args) ⇒ Object
- .runtime_inventory ⇒ Object
- .runtime_schema(language) ⇒ Object
- .settings_extensions_tools ⇒ Object
- .special_tool_summaries ⇒ Object
- .special_tools_definition ⇒ Object
- .timeout_ms(args) ⇒ Object
Class Method Details
.all_tools_definition ⇒ Object
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
# File 'lib/legion/llm/tools/special.rb', line 147 def all_tools_definition Types::ToolDefinition.build( name: LIST_ALL_TOOLS_NAME, description: 'List ALL registered Legion tools from all loaded extensions, grouped by extension and runner. ' \ 'Use this to discover what tools are available for a specific domain (e.g. Teams, Apollo, identity).', parameters: { type: 'object', properties: { extension: { type: 'string', description: 'Filter by extension name (e.g. "microsoft_teams", "apollo"). Omit for all.' }, deferred: { type: 'boolean', description: 'Filter by deferred status. Omit for all.' } } }, source: { type: :special, handler: :all_tools_inventory, pinned: true } ) end |
.all_tools_inventory(**args) ⇒ Object
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 |
# File 'lib/legion/llm/tools/special.rb', line 71 def all_tools_inventory(**args) tools = settings_extensions_tools extension_filter = args[:extension] || args['extension'] deferred_filter = args.key?(:deferred) ? args[:deferred] : args['deferred'] if extension_filter normalized_filter = extension_filter.to_s.tr('-', '_').delete_prefix('lex_') tools = tools.select { |t| t[:extension].to_s.tr('-', '_').delete_prefix('lex_').include?(normalized_filter) } end tools = tools.select { |t| t[:deferred] == deferred_filter } unless deferred_filter.nil? grouped = tools.group_by { |t| t[:extension] || 'unknown' } { total: tools.size, extensions: grouped.transform_values do |ext_tools| ext_tools.group_by { |t| t[:runner] || 'default' }.transform_values do |runner_tools| runner_tools.map { |t| { name: t[:name], description: t[:description], deferred: t[:deferred] } } end end } rescue StandardError => e handle_exception(e, level: :warn, handled: true, operation: 'llm.tools.special.all_tools_inventory') { total: 0, extensions: {}, error: e. } end |
.array_args(args) ⇒ Object
299 300 301 302 303 304 305 306 307 308 309 |
# File 'lib/legion/llm/tools/special.rb', line 299 def array_args(args) raw = args[:args] || args['args'] case raw when Array raw.map(&:to_s) when String Shellwords.split(raw) else [] end end |
.dispatch(tool_name, **args) ⇒ Object
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
# File 'lib/legion/llm/tools/special.rb', line 43 def dispatch(tool_name, **args) case normalize_tool_name(tool_name) when LIST_SPECIAL_TOOLS_NAME { status: :success, result: Legion::JSON.dump(inventory) } when LIST_ALL_TOOLS_NAME { status: :success, result: Legion::JSON.dump(all_tools_inventory(**args)) } when 'ruby' dispatch_runtime('ruby', ruby_path, **args) when 'python', 'python3' dispatch_runtime('python', python_path, **args) when 'pip', 'pip3' dispatch_runtime('pip', pip_path, **args) else { status: :error, result: "Unknown Legion special tool: #{tool_name}" } end rescue StandardError => e handle_exception(e, level: :warn, handled: true, operation: 'llm.tools.special.dispatch', tool_name: tool_name) { status: :error, result: e. } end |
.dispatch_runtime(runtime_name, executable, **args) ⇒ Object
277 278 279 280 281 282 283 284 285 286 287 288 |
# File 'lib/legion/llm/tools/special.rb', line 277 def dispatch_runtime(runtime_name, executable, **args) return { status: :error, result: "#{runtime_name} runtime is unavailable." } unless executable_file?(executable) argv = runtime_argv(runtime_name, **args) return { status: :error, result: "#{runtime_name} tool requires `code`, `command`, or `args`." } if argv.empty? output, status = run_process(executable, argv, **args) result = "command=#{Shellwords.join([executable, *argv])}\nexit=#{status.exitstatus}\n#{output}" { status: status.success? ? :success : :error, result: result } rescue Timeout::Error { status: :error, result: "#{runtime_name} tool timed out after #{timeout_ms(args)}ms." } end |
.executable_file?(path) ⇒ Boolean
360 361 362 |
# File 'lib/legion/llm/tools/special.rb', line 360 def executable_file?(path) !path.to_s.empty? && File.file?(path.to_s) && File.executable?(path.to_s) end |
.executable_from_path(command) ⇒ Object
353 354 355 356 357 358 |
# File 'lib/legion/llm/tools/special.rb', line 353 def executable_from_path(command) ENV.fetch('PATH', '').split(File::PATH_SEPARATOR).filter_map do |dir| path = File.join(dir, command) path if executable_file?(path) end.first end |
.inventory ⇒ Object
63 64 65 66 67 68 69 |
# File 'lib/legion/llm/tools/special.rb', line 63 def inventory { special_tools: special_tool_summaries, settings_extensions_tools: settings_extensions_tools, runtime: runtime_inventory } end |
.legionio_packaged_ruby?(path) ⇒ Boolean
348 349 350 351 |
# File 'lib/legion/llm/tools/special.rb', line 348 def legionio_packaged_ruby?(path) normalized = path.to_s normalized.include?('/Cellar/legionio/') || normalized.include?('/libexec/bin/ruby') end |
.normalize_inventory_entry(entry) ⇒ Object
236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 |
# File 'lib/legion/llm/tools/special.rb', line 236 def normalize_inventory_entry(entry) return unless entry.respond_to?(:transform_keys) normalized = entry.transform_keys { |key| key.respond_to?(:to_sym) ? key.to_sym : key } name = normalized[:name].to_s return if name.empty? { name: name, description: normalized[:description].to_s, deferred: normalized[:deferred] == true, extension: normalized[:extension], runner: normalized[:runner], function: normalized[:function], trigger_words: Array(normalized[:trigger_words]).map(&:to_s), parameters: normalized[:input_schema] || normalized[:parameters] || {}, source: normalized[:tool_class] ? 'registry' : 'extension' }.compact end |
.normalize_tool_name(tool_name) ⇒ Object
364 365 366 |
# File 'lib/legion/llm/tools/special.rb', line 364 def normalize_tool_name(tool_name) tool_name.to_s.tr('.', '_') end |
.path_ruby ⇒ Object
344 345 346 |
# File 'lib/legion/llm/tools/special.rb', line 344 def path_ruby executable_from_path('ruby') end |
.pinned_definitions ⇒ Object
37 38 39 40 41 |
# File 'lib/legion/llm/tools/special.rb', line 37 def pinned_definitions definitions = [special_tools_definition, all_tools_definition, ruby_definition] definitions.concat(python_definitions) if python_available? definitions end |
.pip_candidates_for(bin_dir) ⇒ Object
334 335 336 337 338 |
# File 'lib/legion/llm/tools/special.rb', line 334 def pip_candidates_for(bin_dir) return [] if bin_dir.to_s.empty? [File.join(bin_dir, 'pip'), File.join(bin_dir, 'pip3')] end |
.pip_path ⇒ Object
117 118 119 120 121 122 123 124 |
# File 'lib/legion/llm/tools/special.rb', line 117 def pip_path @pip_path ||= begin candidates = [] candidates.concat(pip_candidates_for(File.dirname(python_path))) if python_available? candidates.concat(pip_candidates_for(File.join(python_venv_dir, 'bin'))) candidates.compact.uniq.find { |path| executable_file?(path) } end end |
.pip_schema ⇒ Object
203 204 205 206 207 208 209 210 211 212 213 214 |
# File 'lib/legion/llm/tools/special.rb', line 203 def pip_schema { type: 'object', properties: { command: { type: 'string', description: 'pip arguments, such as `install pandas` or `list`.' }, args: { type: 'array', items: { type: 'string' }, description: 'pip argv entries.' }, cwd: { type: 'string', description: 'Working directory. Defaults to the current process directory.' }, timeout: { type: 'integer', description: 'Timeout in milliseconds.' }, stdin: { type: 'string', description: 'Optional stdin content.' } } } end |
.process_cwd(args) ⇒ Object
317 318 319 320 |
# File 'lib/legion/llm/tools/special.rb', line 317 def process_cwd(args) cwd = args[:cwd] || args['cwd'] cwd.to_s.empty? ? Dir.pwd : cwd.to_s end |
.process_ruby_path ⇒ Object
340 341 342 |
# File 'lib/legion/llm/tools/special.rb', line 340 def process_ruby_path RbConfig.ruby end |
.process_stdin(args) ⇒ Object
322 323 324 325 |
# File 'lib/legion/llm/tools/special.rb', line 322 def process_stdin(args) stdin = args[:stdin] || args['stdin'] stdin.nil? ? '' : stdin.to_s end |
.python_available? ⇒ Boolean
97 98 99 |
# File 'lib/legion/llm/tools/special.rb', line 97 def python_available? !python_path.to_s.empty? end |
.python_definitions ⇒ Object
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 |
# File 'lib/legion/llm/tools/special.rb', line 172 def python_definitions [ Types::ToolDefinition.build( name: 'python', description: "Run Python with the Legion-managed Python environment from `legionio setup python`. Current path: #{python_path}.", parameters: runtime_schema('Python'), source: { type: :special, handler: :python_runtime, pinned: true, executable: python_path } ), Types::ToolDefinition.build( name: 'pip', description: "Run pip inside the Legion-managed Python environment from `legionio setup python`. Current path: #{pip_path || 'unavailable'}.", parameters: pip_schema, source: { type: :special, handler: :pip_runtime, pinned: true, executable: pip_path } ) ] end |
.python_path ⇒ Object
108 109 110 111 112 113 114 115 |
# File 'lib/legion/llm/tools/special.rb', line 108 def python_path @python_path ||= begin candidates = [] candidates << ENV.fetch('LEGION_PYTHON', nil) candidates << File.join(python_venv_dir, 'bin', 'python3') candidates.compact.find { |path| executable_file?(path) } end end |
.python_venv_dir ⇒ Object
131 132 133 |
# File 'lib/legion/llm/tools/special.rb', line 131 def python_venv_dir ENV['LEGION_PYTHON_VENV'] || File.('~/.legionio/python') end |
.reset_runtime_cache! ⇒ Object
126 127 128 129 |
# File 'lib/legion/llm/tools/special.rb', line 126 def reset_runtime_cache! @python_path = nil @pip_path = nil end |
.ruby_definition ⇒ Object
163 164 165 166 167 168 169 170 |
# File 'lib/legion/llm/tools/special.rb', line 163 def ruby_definition Types::ToolDefinition.build( name: 'ruby', description: "Run Ruby with the current Legion Ruby environment. Current path: #{ruby_path}.", parameters: runtime_schema('Ruby'), source: { type: :special, handler: :ruby_runtime, pinned: true, executable: ruby_path } ) end |
.ruby_path ⇒ Object
101 102 103 104 105 106 |
# File 'lib/legion/llm/tools/special.rb', line 101 def ruby_path process_path = process_ruby_path return process_path if legionio_packaged_ruby?(process_path) && executable_file?(process_path) path_ruby || process_path end |
.run_process(executable, argv, **args) ⇒ Object
311 312 313 314 315 |
# File 'lib/legion/llm/tools/special.rb', line 311 def run_process(executable, argv, **args) Timeout.timeout(timeout_ms(args) / 1000.0) do Open3.capture2e(executable, *argv, chdir: process_cwd(args), stdin_data: process_stdin(args)) end end |
.runtime_argv(runtime_name, **args) ⇒ Object
290 291 292 293 294 295 296 297 |
# File 'lib/legion/llm/tools/special.rb', line 290 def runtime_argv(runtime_name, **args) code = args[:code] || args['code'] return [runtime_name == 'python' ? '-c' : '-e', code.to_s, *array_args(args)] unless code.to_s.empty? command = args[:command] || args['command'] command_args = command.to_s.empty? ? [] : Shellwords.split(command.to_s) command_args + array_args(args) end |
.runtime_inventory ⇒ Object
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 |
# File 'lib/legion/llm/tools/special.rb', line 256 def runtime_inventory { ruby: { path: ruby_path, path_ruby: path_ruby, process_ruby: process_ruby_path, description: RUBY_DESCRIPTION, bundler_version: defined?(Bundler) ? Bundler::VERSION : nil, bundle_gemfile: ENV.fetch('BUNDLE_GEMFILE', nil), bundle_bin_path: ENV.fetch('BUNDLE_BIN_PATH', nil) }.compact, python: { available: python_available?, path: python_path, venv_dir: python_venv_dir, pip: pip_path, default_packages: PYTHON_PACKAGES }.compact } end |
.runtime_schema(language) ⇒ Object
189 190 191 192 193 194 195 196 197 198 199 200 201 |
# File 'lib/legion/llm/tools/special.rb', line 189 def runtime_schema(language) { type: 'object', properties: { code: { type: 'string', description: "#{language} code to execute with -e/-c." }, command: { type: 'string', description: "#{language} command arguments, such as a script path plus args." }, args: { type: 'array', items: { type: 'string' }, description: 'Additional argv entries.' }, cwd: { type: 'string', description: 'Working directory. Defaults to the current process directory.' }, timeout: { type: 'integer', description: 'Timeout in milliseconds.' }, stdin: { type: 'string', description: 'Optional stdin content.' } } } end |
.settings_extensions_tools ⇒ Object
227 228 229 230 231 232 233 234 |
# File 'lib/legion/llm/tools/special.rb', line 227 def settings_extensions_tools return [] unless defined?(Legion::Settings::Extensions) && Legion::Settings::Extensions.respond_to?(:tools) Array(Legion::Settings::Extensions.tools).filter_map { |entry| normalize_inventory_entry(entry) } rescue StandardError => e handle_exception(e, level: :warn, handled: true, operation: 'llm.tools.special.settings_extensions_inventory') [] end |
.special_tool_summaries ⇒ Object
216 217 218 219 220 221 222 223 224 225 |
# File 'lib/legion/llm/tools/special.rb', line 216 def special_tool_summaries pinned_definitions.map do |definition| { name: definition.name, description: definition.description, parameters: definition.parameters, source: 'legion-special' } end end |
.special_tools_definition ⇒ Object
135 136 137 138 139 140 141 142 143 144 145 |
# File 'lib/legion/llm/tools/special.rb', line 135 def special_tools_definition Types::ToolDefinition.build( name: LIST_SPECIAL_TOOLS_NAME, description: 'Show all Legion special tools available to this LLM from the current Legion::Settings::Extensions inventory.', parameters: { type: 'object', properties: {} }, source: { type: :special, handler: :settings_extensions_inventory, pinned: true } ) end |
.timeout_ms(args) ⇒ Object
327 328 329 330 331 332 |
# File 'lib/legion/llm/tools/special.rb', line 327 def timeout_ms(args) requested = (args[:timeout] || args['timeout'] || DEFAULT_TIMEOUT_MS).to_i return DEFAULT_TIMEOUT_MS unless requested.positive? [requested, MAX_TIMEOUT_MS].min end |