Module: AIA::ConfigModules::CLIParser
- Defined in:
- lib/aia/config/cli_parser.rb
Class Method Summary collapse
- .cli_options ⇒ Object
- .create_option_parser(config) ⇒ Object
- .list_available_models(query) ⇒ Object
- .list_available_role_names(prompts_dir, roles_prefix) ⇒ Object
- .list_available_roles ⇒ Object
- .parse_models_with_roles(model_string) ⇒ Object
- .parse_remaining_arguments(opt_parser, config) ⇒ Object
- .process_allowed_tools_option(tools_list, config) ⇒ Object
- .process_rejected_tools_option(tools_list, config) ⇒ Object
- .process_tools_option(a_path_list, config) ⇒ Object
- .setup_adapter_options(opts, config) ⇒ Object
- .setup_ai_parameters(opts, config) ⇒ Object
- .setup_audio_image_options(opts, config) ⇒ Object
- .setup_banner(opts) ⇒ Object
- .setup_file_options(opts, config) ⇒ Object
- .setup_mode_options(opts, config) ⇒ Object
- .setup_model_options(opts, config) ⇒ Object
- .setup_prompt_options(opts, config) ⇒ Object
- .setup_tool_options(opts, config) ⇒ Object
- .setup_utility_options(opts, config) ⇒ Object
- .validate_role_exists(role_id) ⇒ Object
Class Method Details
.cli_options ⇒ Object
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
# File 'lib/aia/config/cli_parser.rb', line 10 def config = OpenStruct.new begin opt_parser = create_option_parser(config) opt_parser.parse! rescue => e STDERR.puts "ERROR: #{e.message}" STDERR.puts " use --help for usage report" exit 1 end parse_remaining_arguments(opt_parser, config) config end |
.create_option_parser(config) ⇒ Object
26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
# File 'lib/aia/config/cli_parser.rb', line 26 def create_option_parser(config) OptionParser.new do |opts| (opts) (opts, config) (opts, config) (opts, config) (opts, config) (opts, config) setup_ai_parameters(opts, config) (opts, config) (opts, config) (opts, config) end end |
.list_available_models(query) ⇒ Object
413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 |
# File 'lib/aia/config/cli_parser.rb', line 413 def list_available_models(query) # SMELL: mostly duplications the code in the vailable_models directive # assumes that the adapter is for the ruby_llm gem # should this be moved to the Utilities class as a common method? if query.nil? query = [] else query = query.split(',') end header = "\nAvailable LLMs" header += " for #{query.join(' and ')}" if query puts header + ':' puts q1 = query.select{|q| q.include?('_to_')}.map{|q| ':'==q[0] ? q[1...] : q} q2 = query.reject{|q| q.include?('_to_')} counter = 0 RubyLLM.models.all.each do |llm| inputs = llm.modalities.input.join(',') outputs = llm.modalities.output.join(',') entry = "- #{llm.id} (#{llm.provider}) #{inputs} to #{outputs}" if query.nil? || query.empty? counter += 1 puts entry next end show_it = true q1.each{|q| show_it &&= llm.modalities.send("#{q}?")} q2.each{|q| show_it &&= entry.include?(q)} if show_it counter += 1 puts entry end end puts if counter > 0 puts "#{counter} LLMs matching your query" puts exit end |
.list_available_role_names(prompts_dir, roles_prefix) ⇒ Object
403 404 405 406 407 408 409 410 411 |
# File 'lib/aia/config/cli_parser.rb', line 403 def list_available_role_names(prompts_dir, roles_prefix) roles_dir = File.join(prompts_dir, roles_prefix) return [] unless Dir.exist?(roles_dir) # Find all .txt files recursively, preserving paths Dir.glob("**/*.txt", base: roles_dir) .map { |f| f.chomp('.txt') } .sort end |
.list_available_roles ⇒ Object
382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 |
# File 'lib/aia/config/cli_parser.rb', line 382 def list_available_roles prompts_dir = ENV.fetch('AIA_PROMPTS_DIR', File.join(ENV['HOME'], '.prompts')) roles_prefix = ENV.fetch('AIA_ROLES_PREFIX', 'roles') roles_dir = File.join(prompts_dir, roles_prefix) if Dir.exist?(roles_dir) roles = list_available_role_names(prompts_dir, roles_prefix) if roles.empty? puts "No role files found in #{roles_dir}" puts "Create .txt files in this directory to define roles." else puts "Available roles in #{roles_dir}:" roles.each { |role| puts " - #{role}" } end else puts "No roles directory found at #{roles_dir}" puts "Create this directory and add role files to use roles." end end |
.parse_models_with_roles(model_string) ⇒ Object
304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 |
# File 'lib/aia/config/cli_parser.rb', line 304 def parse_models_with_roles(model_string) models = [] model_counts = Hash.new(0) model_string.split(',').each do |spec| spec.strip! # Validate syntax if spec =~ /^=|=$/ raise ArgumentError, "Invalid model syntax: '#{spec}'. Expected format: MODEL[=ROLE]" end if spec.include?('=') # Explicit role: "model=role" or "provider/model=role" model_name, role_name = spec.split('=', 2) model_name.strip! role_name.strip! # Validate role file exists (fail fast) validate_role_exists(role_name) # Track instance count for duplicates model_counts[model_name] += 1 instance = model_counts[model_name] models << { model: model_name, role: role_name, instance: instance, internal_id: instance > 1 ? "#{model_name}##{instance}" : model_name } else # No explicit role, will use default from -r/--role model_counts[spec] += 1 instance = model_counts[spec] models << { model: spec, role: nil, instance: instance, internal_id: instance > 1 ? "#{spec}##{instance}" : spec } end end models end |
.parse_remaining_arguments(opt_parser, config) ⇒ Object
463 464 465 466 467 468 469 470 471 472 473 474 |
# File 'lib/aia/config/cli_parser.rb', line 463 def parse_remaining_arguments(opt_parser, config) args = ARGV.dup # Parse the command line arguments begin config.remaining_args = opt_parser.parse(args) rescue OptionParser::InvalidOption => e puts e. puts opt_parser exit 1 end end |
.process_allowed_tools_option(tools_list, config) ⇒ Object
508 509 510 511 512 513 514 515 516 517 |
# File 'lib/aia/config/cli_parser.rb', line 508 def process_allowed_tools_option(tools_list, config) config.allowed_tools ||= [] if tools_list.empty? STDERR.puts "No list of tool names provided for --allowed_tools option" exit 1 else config.allowed_tools += tools_list.split(',').map(&:strip) config.allowed_tools.uniq! end end |
.process_rejected_tools_option(tools_list, config) ⇒ Object
519 520 521 522 523 524 525 526 527 528 |
# File 'lib/aia/config/cli_parser.rb', line 519 def process_rejected_tools_option(tools_list, config) config.rejected_tools ||= [] if tools_list.empty? STDERR.puts "No list of tool names provided for --rejected_tools option" exit 1 else config.rejected_tools += tools_list.split(',').map(&:strip) config.rejected_tools.uniq! end end |
.process_tools_option(a_path_list, config) ⇒ Object
476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 |
# File 'lib/aia/config/cli_parser.rb', line 476 def process_tools_option(a_path_list, config) config.tool_paths ||= [] if a_path_list.empty? STDERR.puts "No list of paths for --tools option" exit 1 else paths = a_path_list.split(',').map(&:strip).uniq end paths.each do |a_path| if File.exist?(a_path) if File.file?(a_path) if '.rb' == File.extname(a_path) config.tool_paths << a_path else STDERR.puts "file should have *.rb extension: #{a_path}" exit 1 end elsif File.directory?(a_path) rb_files = Dir.glob(File.join(a_path, '*.rb')) config.tool_paths += rb_files end else STDERR.puts "file/dir path is not valid: #{a_path}" exit 1 end end config.tool_paths.uniq! end |
.setup_adapter_options(opts, config) ⇒ Object
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/aia/config/cli_parser.rb', line 66 def (opts, config) opts.on("--adapter ADAPTER", "Interface that adapts AIA to the LLM") do |adapter| adapter.downcase! valid_adapters = %w[ ruby_llm ] # NOTE: Add additional adapters here when needed if valid_adapters.include? adapter config.adapter = adapter else STDERR.puts "ERROR: Invalid adapter #{adapter} must be one of these: #{valid_adapters.join(', ')}" exit 1 end end opts.on('--available_models [QUERY]', 'List (then exit) available models that match the optional query - a comma separated list of AND components like: openai,mini') do |query| list_available_models(query) end end |
.setup_ai_parameters(opts, config) ⇒ Object
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 |
# File 'lib/aia/config/cli_parser.rb', line 170 def setup_ai_parameters(opts, config) opts.on("-t", "--temperature TEMP", Float, "Temperature for text generation") do |temp| config.temperature = temp end opts.on("--max_tokens TOKENS", Integer, "Maximum tokens for text generation") do |tokens| config.max_tokens = tokens end opts.on("--top_p VALUE", Float, "Top-p sampling value") do |value| config.top_p = value end opts.on("--frequency_penalty VALUE", Float, "Frequency penalty") do |value| config.frequency_penalty = value end opts.on("--presence_penalty VALUE", Float, "Presence penalty") do |value| config.presence_penalty = value end end |
.setup_audio_image_options(opts, config) ⇒ Object
192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 |
# File 'lib/aia/config/cli_parser.rb', line 192 def (opts, config) opts.on("--speak", "Simple implementation. Uses the speech model to convert text to audio, then plays the audio. Fun with --chat. Supports configuration of speech model and voice.") do config.speak = true end opts.on("--voice VOICE", "Voice to use for speech") do |voice| config.voice = voice end opts.on("--is", "--image_size SIZE", "Image size for image generation") do |size| config.image_size = size end opts.on("--iq", "--image_quality QUALITY", "Image quality for image generation") do |quality| config.image_quality = quality end opts.on("--style", "--image_style STYLE", "Style for image generation") do |style| config.image_style = style end end |
.setup_banner(opts) ⇒ Object
41 42 43 44 45 |
# File 'lib/aia/config/cli_parser.rb', line 41 def (opts) opts. = "Usage: aia [options] [PROMPT_ID] [CONTEXT_FILE]*\n" + " aia --chat [PROMPT_ID] [CONTEXT_FILE]*\n" + " aia --chat [CONTEXT_FILE]*" end |
.setup_file_options(opts, config) ⇒ Object
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 |
# File 'lib/aia/config/cli_parser.rb', line 106 def (opts, config) opts.on("-c", "--config_file FILE", "Load config file") do |file| FileLoader.load_config_file(file, config) end opts.on("-o", "--[no-]out_file [FILE]", "Output file (default: temp.md)") do |file| if file == false # --no-out_file was used config.out_file = nil elsif file.nil? # No argument provided config.out_file = 'temp.md' else # File name provided config.out_file = File.(file, Dir.pwd) end end opts.on("-a", "--[no-]append", "Append to output file instead of overwriting") do |append| config.append = append end opts.on("-l", "--[no-]log_file [FILE]", "Log file") do |file| config.log_file = file end opts.on("--md", "--[no-]markdown", "Format with Markdown") do |md| config.markdown = md end end |
.setup_mode_options(opts, config) ⇒ Object
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
# File 'lib/aia/config/cli_parser.rb', line 47 def (opts, config) opts.on("--chat", "Begin a chat session with the LLM after processing all prompts in the pipeline.") do config.chat = true puts "Debug: Setting chat mode to true" if config.debug end opts.on("-f", "--fuzzy", "Use fuzzy matching for prompt search") do unless system("which fzf > /dev/null 2>&1") STDERR.puts "Error: 'fzf' is not installed. Please install 'fzf' to use the --fuzzy option." exit 1 end config.fuzzy = true end opts.on("--terse", "Adds a special instruction to the prompt asking the AI to keep responses short and to the point") do config.terse = true end end |
.setup_model_options(opts, config) ⇒ Object
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
# File 'lib/aia/config/cli_parser.rb', line 83 def (opts, config) opts.on("-m MODEL", "--model MODEL", "Name of the LLM model(s) to use. Format: MODEL[=ROLE][,MODEL[=ROLE]]...") do |model| config.model = parse_models_with_roles(model) end opts.on("--[no-]consensus", "Enable/disable consensus mode for multi-model responses (default: show individual responses)") do |consensus| config.consensus = consensus end opts.on("--list-roles", "List available role files and exit") do list_available_roles exit 0 end opts.on("--sm", "--speech_model MODEL", "Speech model to use") do |model| config.speech_model = model end opts.on("--tm", "--transcription_model MODEL", "Transcription model to use") do |model| config.transcription_model = model end end |
.setup_prompt_options(opts, config) ⇒ Object
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/aia/config/cli_parser.rb', line 134 def (opts, config) opts.on("--prompts_dir DIR", "Directory containing prompt files") do |dir| config.prompts_dir = dir end opts.on("--roles_prefix PREFIX", "Subdirectory name for role files (default: roles)") do |prefix| config.roles_prefix = prefix end opts.on("-r", "--role ROLE_ID", "Role ID to prepend to prompt") do |role| config.role = role end opts.on("-n", "--next PROMPT_ID", "Next prompt to process") do |next_prompt| config.pipeline ||= [] config.pipeline << next_prompt end opts.on("-p PROMPTS", "--pipeline PROMPTS", "Pipeline of comma-seperated prompt IDs to process") do |pipeline| config.pipeline ||= [] config.pipeline += pipeline.split(',').map(&:strip) end opts.on("-x", "--[no-]exec", "Used to designate an executable prompt file") do |value| config.executable_prompt = value end opts.on("--system_prompt PROMPT_ID", "System prompt ID to use for chat sessions") do |prompt_id| config.system_prompt = prompt_id end opts.on('--regex pattern', 'Regex pattern to extract parameters from prompt text') do |pattern| config.parameter_regex = pattern end end |
.setup_tool_options(opts, config) ⇒ Object
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 |
# File 'lib/aia/config/cli_parser.rb', line 214 def (opts, config) opts.on("--rq LIBS", "--require LIBS", "Ruby libraries to require for Ruby directive") do |libs| config.require_libs ||= [] config.require_libs += libs.split(',') end opts.on("--tools PATH_LIST", "Add a tool(s)") do |a_path_list| process_tools_option(a_path_list, config) end opts.on("--at", "--allowed_tools TOOLS_LIST", "Allow only these tools to be used") do |tools_list| process_allowed_tools_option(tools_list, config) end opts.on("--rt", "--rejected_tools TOOLS_LIST", "Reject these tools") do |tools_list| process_rejected_tools_option(tools_list, config) end end |
.setup_utility_options(opts, config) ⇒ Object
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 |
# File 'lib/aia/config/cli_parser.rb', line 233 def (opts, config) opts.on("-d", "--debug", "Enable debug output") do config.debug = $DEBUG_ME = true end opts.on("--no-debug", "Disable debug output") do config.debug = $DEBUG_ME = false end opts.on("-v", "--[no-]verbose", "Be verbose") do |value| config.verbose = value end opts.on("--refresh DAYS", Integer, "Refresh models database interval in days") do |days| config.refresh = days || 0 end opts.on("--dump FILE", "Dump config to file") do |file| config.dump_file = file end opts.on("--completion SHELL", "Show completion script for bash|zsh|fish - default is nil") do |shell| config.completion = shell end opts.on("--metrics", "Display token usage in chat mode") do config.show_metrics = true end opts.on("--cost", "Include cost calculations with metrics (requires --metrics)") do config.show_cost = true config.show_metrics = true # Automatically enable metrics when cost is requested end opts.on("--version", "Show version") do puts AIA::VERSION exit end opts.on("-h", "--help", "Prints this help") do puts "\n AIA your AI Assistant\n - designed for generative AI workflows,\n - effortlessly manage AI prompts,\n - integrate seamlessly with shell and embedded Ruby (ERB),\n - run batch processes,\n - engage in interactive chats,\n - with user defined directives, tools and MCP clients.\n\n HELP\n\n puts opts\n\n puts <<~EXTRA\n\n Explore Further:\n - AIA Report an Issue: https://github.com/MadBomber/aia/issues\n - AIA Documentation: https://github.com/MadBomber/aia/blob/main/README.md\n - AIA GitHub Repository: https://github.com/MadBomber/aia\n - PromptManager Docs: https://github.com/MadBomber/prompt_manager/blob/main/README.md\n - ERB Documentation: https://rubyapi.org/o/erb\n - RubyLLM Tool Docs: https://rubyllm.com/guides/tools\n - MCP Client Docs: https://github.com/patvice/ruby_llm-mcp/blob/main/README.md\n\n EXTRA\n\n exit\n end\nend\n" |
.validate_role_exists(role_id) ⇒ Object
352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 |
# File 'lib/aia/config/cli_parser.rb', line 352 def validate_role_exists(role_id) # Get prompts_dir from defaults or environment prompts_dir = ENV.fetch('AIA_PROMPTS_DIR', File.join(ENV['HOME'], '.prompts')) roles_prefix = ENV.fetch('AIA_ROLES_PREFIX', 'roles') # Build role file path unless role_id.start_with?(roles_prefix) role_id = "#{roles_prefix}/#{role_id}" end role_file_path = File.join(prompts_dir, "#{role_id}.txt") unless File.exist?(role_file_path) available_roles = list_available_role_names(prompts_dir, roles_prefix) error_msg = "Role file not found: #{role_file_path}\n\n" if available_roles.empty? error_msg += "No roles directory found at #{File.join(prompts_dir, roles_prefix)}\n" error_msg += "Create the directory and add role files to use this feature." else error_msg += "Available roles:\n" error_msg += available_roles.map { |r| " - #{r}" }.join("\n") error_msg += "\n\nCreate the role file or use an existing role." end raise ArgumentError, error_msg end end |