aia 0.9.11 → 0.9.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.version +1 -1
- data/CHANGELOG.md +66 -2
- data/README.md +133 -4
- data/docs/advanced-prompting.md +721 -0
- data/docs/cli-reference.md +582 -0
- data/docs/configuration.md +347 -0
- data/docs/contributing.md +332 -0
- data/docs/directives-reference.md +490 -0
- data/docs/examples/index.md +277 -0
- data/docs/examples/mcp/index.md +479 -0
- data/docs/examples/prompts/analysis/index.md +78 -0
- data/docs/examples/prompts/automation/index.md +108 -0
- data/docs/examples/prompts/development/index.md +125 -0
- data/docs/examples/prompts/index.md +333 -0
- data/docs/examples/prompts/learning/index.md +127 -0
- data/docs/examples/prompts/writing/index.md +62 -0
- data/docs/examples/tools/index.md +292 -0
- data/docs/faq.md +414 -0
- data/docs/guides/available-models.md +366 -0
- data/docs/guides/basic-usage.md +477 -0
- data/docs/guides/chat.md +474 -0
- data/docs/guides/executable-prompts.md +417 -0
- data/docs/guides/first-prompt.md +454 -0
- data/docs/guides/getting-started.md +455 -0
- data/docs/guides/image-generation.md +507 -0
- data/docs/guides/index.md +46 -0
- data/docs/guides/models.md +507 -0
- data/docs/guides/tools.md +856 -0
- data/docs/index.md +173 -0
- data/docs/installation.md +238 -0
- data/docs/mcp-integration.md +612 -0
- data/docs/prompt_management.md +579 -0
- data/docs/security.md +629 -0
- data/docs/tools-and-mcp-examples.md +1186 -0
- data/docs/workflows-and-pipelines.md +563 -0
- data/examples/tools/mcp/github_mcp_server.json +11 -0
- data/examples/tools/mcp/imcp.json +7 -0
- data/lib/aia/chat_processor_service.rb +19 -3
- data/lib/aia/config/base.rb +224 -0
- data/lib/aia/config/cli_parser.rb +409 -0
- data/lib/aia/config/defaults.rb +88 -0
- data/lib/aia/config/file_loader.rb +131 -0
- data/lib/aia/config/validator.rb +184 -0
- data/lib/aia/config.rb +10 -860
- data/lib/aia/directive_processor.rb +27 -372
- data/lib/aia/directives/configuration.rb +114 -0
- data/lib/aia/directives/execution.rb +37 -0
- data/lib/aia/directives/models.rb +178 -0
- data/lib/aia/directives/registry.rb +120 -0
- data/lib/aia/directives/utility.rb +70 -0
- data/lib/aia/directives/web_and_file.rb +71 -0
- data/lib/aia/prompt_handler.rb +23 -3
- data/lib/aia/ruby_llm_adapter.rb +307 -128
- data/lib/aia/session.rb +27 -14
- data/lib/aia/utility.rb +12 -8
- data/lib/aia.rb +11 -2
- data/lib/extensions/ruby_llm/.irbrc +56 -0
- data/mkdocs.yml +165 -0
- metadata +77 -20
- /data/{images → docs/assets/images}/aia.png +0 -0
data/lib/aia/config.rb
CHANGED
@@ -5,876 +5,26 @@
|
|
5
5
|
# for the AIA application. It provides methods to parse command-line
|
6
6
|
# arguments, environment variables, and configuration files.
|
7
7
|
|
8
|
-
|
9
|
-
require 'toml-rb'
|
10
|
-
require 'date'
|
11
|
-
require 'erb'
|
12
|
-
require 'optparse'
|
13
|
-
require 'json'
|
14
|
-
require 'tempfile'
|
15
|
-
require 'fileutils'
|
8
|
+
require_relative 'config/base'
|
16
9
|
|
17
10
|
module AIA
|
18
11
|
class Config
|
19
|
-
|
20
|
-
adapter: 'ruby_llm', # 'ruby_llm' or ???
|
21
|
-
#
|
22
|
-
aia_dir: File.join(ENV['HOME'], '.aia'),
|
23
|
-
config_file: File.join(ENV['HOME'], '.aia', 'config.yml'),
|
24
|
-
out_file: 'temp.md',
|
25
|
-
log_file: File.join(ENV['HOME'], '.prompts', '_prompts.log'),
|
26
|
-
context_files: [],
|
27
|
-
#
|
28
|
-
prompts_dir: File.join(ENV['HOME'], '.prompts'),
|
29
|
-
prompt_extname: PromptManager::Storage::FileSystemAdapter::PROMPT_EXTENSION,
|
30
|
-
#
|
31
|
-
roles_prefix: 'roles',
|
32
|
-
roles_dir: File.join(ENV['HOME'], '.prompts', 'roles'),
|
33
|
-
role: '',
|
34
|
-
|
35
|
-
#
|
36
|
-
system_prompt: '',
|
37
|
-
|
38
|
-
# Tools
|
39
|
-
tools: '', # Comma-separated string of loaded tool names (set by adapter)
|
40
|
-
allowed_tools: nil, # nil means all tools are allowed; otherwise an Array of Strings which are the tool names
|
41
|
-
rejected_tools: nil, # nil means no tools are rejected
|
42
|
-
tool_paths: [], # Strings - absolute and relative to tools
|
43
|
-
|
44
|
-
# Flags
|
45
|
-
markdown: true,
|
46
|
-
shell: true,
|
47
|
-
erb: true,
|
48
|
-
chat: false,
|
49
|
-
clear: false,
|
50
|
-
terse: false,
|
51
|
-
verbose: false,
|
52
|
-
debug: $DEBUG_ME,
|
53
|
-
fuzzy: false,
|
54
|
-
speak: false,
|
55
|
-
append: false, # Default to not append to existing out_file
|
56
|
-
|
57
|
-
# workflow
|
58
|
-
pipeline: [],
|
59
|
-
|
60
|
-
# PromptManager::Prompt Tailoring
|
61
|
-
parameter_regex: PromptManager::Prompt.parameter_regex.to_s,
|
62
|
-
|
63
|
-
# LLM tuning parameters
|
64
|
-
temperature: 0.7,
|
65
|
-
max_tokens: 2048,
|
66
|
-
top_p: 1.0,
|
67
|
-
frequency_penalty: 0.0,
|
68
|
-
presence_penalty: 0.0,
|
69
|
-
|
70
|
-
# Audio Parameters
|
71
|
-
voice: 'alloy',
|
72
|
-
speak_command: 'afplay', # 'afplay' for audio files on MacOS
|
73
|
-
|
74
|
-
# Image Parameters
|
75
|
-
image_size: '1024x1024',
|
76
|
-
image_quality: 'standard',
|
77
|
-
image_style: 'vivid',
|
78
|
-
|
79
|
-
# Models
|
80
|
-
model: 'gpt-4o-mini',
|
81
|
-
speech_model: 'tts-1',
|
82
|
-
transcription_model: 'whisper-1',
|
83
|
-
embedding_model: 'text-embedding-ada-002',
|
84
|
-
image_model: 'dall-e-3',
|
85
|
-
|
86
|
-
# Model Regristery
|
87
|
-
refresh: 7, # days between refreshes of model info; 0 means every startup
|
88
|
-
last_refresh: Date.today - 1,
|
89
|
-
|
90
|
-
# Ruby libraries to require for Ruby binding
|
91
|
-
require_libs: [],
|
92
|
-
}).freeze
|
93
|
-
|
12
|
+
# Delegate all functionality to the modular config system
|
94
13
|
def self.setup
|
95
|
-
|
96
|
-
cli_config = cli_options
|
97
|
-
envar_config = envar_options(default_config, cli_config)
|
98
|
-
|
99
|
-
file = envar_config.config_file unless envar_config.config_file.nil?
|
100
|
-
file = cli_config.config_file unless cli_config.config_file.nil?
|
101
|
-
|
102
|
-
cf_config = cf_options(file)
|
103
|
-
|
104
|
-
config = OpenStruct.merge(
|
105
|
-
default_config,
|
106
|
-
cf_config || {},
|
107
|
-
envar_config || {},
|
108
|
-
cli_config || {}
|
109
|
-
)
|
110
|
-
|
111
|
-
tailor_the_config(config)
|
112
|
-
load_libraries(config)
|
113
|
-
load_tools(config)
|
114
|
-
|
115
|
-
if config.dump_file
|
116
|
-
dump_config(config, config.dump_file)
|
117
|
-
end
|
118
|
-
|
119
|
-
config
|
120
|
-
end
|
121
|
-
|
122
|
-
|
123
|
-
def self.tailor_the_config(config)
|
124
|
-
remaining_args = config.remaining_args.dup
|
125
|
-
config.remaining_args = nil
|
126
|
-
|
127
|
-
stdin_content = process_stdin_content
|
128
|
-
config.stdin_content = stdin_content if stdin_content && !stdin_content.strip.empty?
|
129
|
-
|
130
|
-
process_prompt_id_from_args(config, remaining_args)
|
131
|
-
validate_and_set_context_files(config, remaining_args)
|
132
|
-
handle_executable_prompt(config)
|
133
|
-
validate_required_prompt_id(config)
|
134
|
-
process_role_configuration(config)
|
135
|
-
handle_fuzzy_search_prompt_id(config)
|
136
|
-
normalize_boolean_flags(config)
|
137
|
-
handle_completion_script(config)
|
138
|
-
validate_final_prompt_requirements(config)
|
139
|
-
configure_prompt_manager(config)
|
140
|
-
prepare_pipeline(config)
|
141
|
-
validate_pipeline_prompts(config)
|
142
|
-
|
143
|
-
config
|
144
|
-
end
|
145
|
-
|
146
|
-
|
147
|
-
def self.load_libraries(config)
|
148
|
-
return if config.require_libs.empty?
|
149
|
-
|
150
|
-
exit_on_error = false
|
151
|
-
|
152
|
-
config.require_libs.each do |library|
|
153
|
-
begin
|
154
|
-
require(library)
|
155
|
-
rescue => e
|
156
|
-
STDERR.puts "Error loading library '#{library}' #{e.message}"
|
157
|
-
exit_on_error = true
|
158
|
-
end
|
159
|
-
end
|
160
|
-
|
161
|
-
exit(1) if exit_on_error
|
162
|
-
|
163
|
-
config
|
164
|
-
end
|
165
|
-
|
166
|
-
|
167
|
-
def self.load_tools(config)
|
168
|
-
return if config.tool_paths.empty?
|
169
|
-
|
170
|
-
require_all_tools(config)
|
171
|
-
|
172
|
-
config
|
173
|
-
end
|
174
|
-
|
175
|
-
|
176
|
-
def self.require_all_tools(config)
|
177
|
-
exit_on_error = false
|
178
|
-
|
179
|
-
config.tool_paths.each do |tool_path|
|
180
|
-
begin
|
181
|
-
# expands path based on PWD
|
182
|
-
absolute_tool_path = File.expand_path(tool_path)
|
183
|
-
require(absolute_tool_path)
|
184
|
-
rescue => e
|
185
|
-
STDERR.puts "Error loading tool '#{tool_path}' #{e.message}"
|
186
|
-
exit_on_error = true
|
187
|
-
end
|
188
|
-
end
|
189
|
-
|
190
|
-
exit(1) if exit_on_error
|
191
|
-
end
|
192
|
-
|
193
|
-
|
194
|
-
# envar values are always String object so need other config
|
195
|
-
# layers to know the prompter type for each key's value
|
196
|
-
def self.envar_options(default, cli_config)
|
197
|
-
config = OpenStruct.merge(default, cli_config)
|
198
|
-
envars = ENV.keys.select { |key, _| key.start_with?('AIA_') }
|
199
|
-
envars.each do |envar|
|
200
|
-
key = envar.sub(/^AIA_/, '').downcase.to_sym
|
201
|
-
value = ENV[envar]
|
202
|
-
|
203
|
-
value = case config[key]
|
204
|
-
when TrueClass, FalseClass
|
205
|
-
value.downcase == 'true'
|
206
|
-
when Integer
|
207
|
-
value.to_i
|
208
|
-
when Float
|
209
|
-
value.to_f
|
210
|
-
when Array
|
211
|
-
value.split(',').map(&:strip)
|
212
|
-
else
|
213
|
-
value # defaults to String
|
214
|
-
end
|
215
|
-
config[key] = value
|
216
|
-
end
|
217
|
-
|
218
|
-
config
|
219
|
-
end
|
220
|
-
|
221
|
-
|
222
|
-
def self.cli_options
|
223
|
-
config = OpenStruct.new
|
224
|
-
|
225
|
-
begin
|
226
|
-
opt_parser = create_option_parser(config)
|
227
|
-
opt_parser.parse!
|
228
|
-
rescue => e
|
229
|
-
STDERR.puts "ERROR: #{e.message}"
|
230
|
-
STDERR.puts " use --help for usage report"
|
231
|
-
exit 1
|
232
|
-
end
|
233
|
-
|
234
|
-
parse_remaining_arguments(opt_parser, config)
|
235
|
-
config
|
14
|
+
ConfigModules::Base.setup
|
236
15
|
end
|
237
16
|
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
setup_adapter_options(opts, config)
|
243
|
-
setup_model_options(opts, config)
|
244
|
-
setup_file_options(opts, config)
|
245
|
-
setup_prompt_options(opts, config)
|
246
|
-
setup_ai_parameters(opts, config)
|
247
|
-
setup_audio_image_options(opts, config)
|
248
|
-
setup_tool_options(opts, config)
|
249
|
-
setup_utility_options(opts, config)
|
250
|
-
end
|
251
|
-
end
|
252
|
-
|
253
|
-
def self.setup_banner(opts)
|
254
|
-
opts.banner = "Usage: aia [options] [PROMPT_ID] [CONTEXT_FILE]*\n" +
|
255
|
-
" aia --chat [PROMPT_ID] [CONTEXT_FILE]*\n" +
|
256
|
-
" aia --chat [CONTEXT_FILE]*"
|
257
|
-
end
|
258
|
-
|
259
|
-
def self.setup_mode_options(opts, config)
|
260
|
-
opts.on("--chat", "Begin a chat session with the LLM after processing all prompts in the pipeline.") do
|
261
|
-
config.chat = true
|
262
|
-
puts "Debug: Setting chat mode to true" if config.debug
|
263
|
-
end
|
264
|
-
|
265
|
-
opts.on("-f", "--fuzzy", "Use fuzzy matching for prompt search") do
|
266
|
-
unless system("which fzf > /dev/null 2>&1")
|
267
|
-
STDERR.puts "Error: 'fzf' is not installed. Please install 'fzf' to use the --fuzzy option."
|
268
|
-
exit 1
|
269
|
-
end
|
270
|
-
config.fuzzy = true
|
271
|
-
end
|
272
|
-
|
273
|
-
opts.on("--terse", "Adds a special instruction to the prompt asking the AI to keep responses short and to the point") do
|
274
|
-
config.terse = true
|
275
|
-
end
|
276
|
-
end
|
277
|
-
|
278
|
-
def self.setup_adapter_options(opts, config)
|
279
|
-
opts.on("--adapter ADAPTER", "Interface that adapts AIA to the LLM") do |adapter|
|
280
|
-
adapter.downcase!
|
281
|
-
valid_adapters = %w[ ruby_llm ] # NOTE: Add additional adapters here when needed
|
282
|
-
if valid_adapters.include? adapter
|
283
|
-
config.adapter = adapter
|
284
|
-
else
|
285
|
-
STDERR.puts "ERROR: Invalid adapter #{adapter} must be one of these: #{valid_adapters.join(', ')}"
|
286
|
-
exit 1
|
287
|
-
end
|
288
|
-
end
|
289
|
-
|
290
|
-
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|
|
291
|
-
list_available_models(query)
|
292
|
-
end
|
293
|
-
end
|
294
|
-
|
295
|
-
def self.setup_model_options(opts, config)
|
296
|
-
opts.on("-m MODEL", "--model MODEL", "Name of the LLM model to use") do |model|
|
297
|
-
config.model = model
|
298
|
-
end
|
299
|
-
|
300
|
-
opts.on("--sm", "--speech_model MODEL", "Speech model to use") do |model|
|
301
|
-
config.speech_model = model
|
302
|
-
end
|
303
|
-
|
304
|
-
opts.on("--tm", "--transcription_model MODEL", "Transcription model to use") do |model|
|
305
|
-
config.transcription_model = model
|
306
|
-
end
|
307
|
-
end
|
308
|
-
|
309
|
-
def self.setup_file_options(opts, config)
|
310
|
-
opts.on("-c", "--config_file FILE", "Load config file") do |file|
|
311
|
-
load_config_file(file, config)
|
312
|
-
end
|
313
|
-
|
314
|
-
opts.on("-o", "--[no-]out_file [FILE]", "Output file (default: temp.md)") do |file|
|
315
|
-
if file == false # --no-out_file was used
|
316
|
-
config.out_file = nil
|
317
|
-
elsif file.nil? # No argument provided
|
318
|
-
config.out_file = 'temp.md'
|
319
|
-
else # File name provided
|
320
|
-
config.out_file = File.expand_path(file, Dir.pwd)
|
321
|
-
end
|
322
|
-
end
|
323
|
-
|
324
|
-
opts.on("-a", "--[no-]append", "Append to output file instead of overwriting") do |append|
|
325
|
-
config.append = append
|
326
|
-
end
|
327
|
-
|
328
|
-
opts.on("-l", "--[no-]log_file [FILE]", "Log file") do |file|
|
329
|
-
config.log_file = file
|
330
|
-
end
|
331
|
-
|
332
|
-
opts.on("--md", "--[no-]markdown", "Format with Markdown") do |md|
|
333
|
-
config.markdown = md
|
334
|
-
end
|
335
|
-
end
|
336
|
-
|
337
|
-
def self.setup_prompt_options(opts, config)
|
338
|
-
opts.on("--prompts_dir DIR", "Directory containing prompt files") do |dir|
|
339
|
-
config.prompts_dir = dir
|
340
|
-
end
|
341
|
-
|
342
|
-
opts.on("--roles_prefix PREFIX", "Subdirectory name for role files (default: roles)") do |prefix|
|
343
|
-
config.roles_prefix = prefix
|
344
|
-
end
|
345
|
-
|
346
|
-
opts.on("-r", "--role ROLE_ID", "Role ID to prepend to prompt") do |role|
|
347
|
-
config.role = role
|
348
|
-
end
|
349
|
-
|
350
|
-
opts.on("-n", "--next PROMPT_ID", "Next prompt to process") do |next_prompt|
|
351
|
-
config.pipeline ||= []
|
352
|
-
config.pipeline << next_prompt
|
353
|
-
end
|
354
|
-
|
355
|
-
opts.on("-p PROMPTS", "--pipeline PROMPTS", "Pipeline of comma-seperated prompt IDs to process") do |pipeline|
|
356
|
-
config.pipeline ||= []
|
357
|
-
config.pipeline += pipeline.split(',').map(&:strip)
|
358
|
-
end
|
359
|
-
|
360
|
-
opts.on("-x", "--[no-]exec", "Used to designate an executable prompt file") do |value|
|
361
|
-
config.executable_prompt = value
|
362
|
-
end
|
363
|
-
|
364
|
-
opts.on("--system_prompt PROMPT_ID", "System prompt ID to use for chat sessions") do |prompt_id|
|
365
|
-
config.system_prompt = prompt_id
|
366
|
-
end
|
367
|
-
|
368
|
-
opts.on('--regex pattern', 'Regex pattern to extract parameters from prompt text') do |pattern|
|
369
|
-
config.parameter_regex = pattern
|
370
|
-
end
|
371
|
-
end
|
372
|
-
|
373
|
-
def self.setup_ai_parameters(opts, config)
|
374
|
-
opts.on("-t", "--temperature TEMP", Float, "Temperature for text generation") do |temp|
|
375
|
-
config.temperature = temp
|
376
|
-
end
|
377
|
-
|
378
|
-
opts.on("--max_tokens TOKENS", Integer, "Maximum tokens for text generation") do |tokens|
|
379
|
-
config.max_tokens = tokens
|
380
|
-
end
|
381
|
-
|
382
|
-
opts.on("--top_p VALUE", Float, "Top-p sampling value") do |value|
|
383
|
-
config.top_p = value
|
384
|
-
end
|
385
|
-
|
386
|
-
opts.on("--frequency_penalty VALUE", Float, "Frequency penalty") do |value|
|
387
|
-
config.frequency_penalty = value
|
388
|
-
end
|
389
|
-
|
390
|
-
opts.on("--presence_penalty VALUE", Float, "Presence penalty") do |value|
|
391
|
-
config.presence_penalty = value
|
392
|
-
end
|
393
|
-
end
|
394
|
-
|
395
|
-
def self.setup_audio_image_options(opts, config)
|
396
|
-
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
|
397
|
-
config.speak = true
|
398
|
-
end
|
399
|
-
|
400
|
-
opts.on("--voice VOICE", "Voice to use for speech") do |voice|
|
401
|
-
config.voice = voice
|
402
|
-
end
|
403
|
-
|
404
|
-
opts.on("--is", "--image_size SIZE", "Image size for image generation") do |size|
|
405
|
-
config.image_size = size
|
406
|
-
end
|
407
|
-
|
408
|
-
opts.on("--iq", "--image_quality QUALITY", "Image quality for image generation") do |quality|
|
409
|
-
config.image_quality = quality
|
410
|
-
end
|
411
|
-
|
412
|
-
opts.on("--style", "--image_style STYLE", "Style for image generation") do |style|
|
413
|
-
config.image_style = style
|
414
|
-
end
|
415
|
-
end
|
416
|
-
|
417
|
-
def self.setup_tool_options(opts, config)
|
418
|
-
opts.on("--rq LIBS", "--require LIBS", "Ruby libraries to require for Ruby directive") do |libs|
|
419
|
-
config.require_libs ||= []
|
420
|
-
config.require_libs += libs.split(',')
|
421
|
-
end
|
422
|
-
|
423
|
-
opts.on("--tools PATH_LIST", "Add a tool(s)") do |a_path_list|
|
424
|
-
process_tools_option(a_path_list, config)
|
425
|
-
end
|
426
|
-
|
427
|
-
opts.on("--at", "--allowed_tools TOOLS_LIST", "Allow only these tools to be used") do |tools_list|
|
428
|
-
process_allowed_tools_option(tools_list, config)
|
429
|
-
end
|
430
|
-
|
431
|
-
opts.on("--rt", "--rejected_tools TOOLS_LIST", "Reject these tools") do |tools_list|
|
432
|
-
process_rejected_tools_option(tools_list, config)
|
433
|
-
end
|
434
|
-
end
|
435
|
-
|
436
|
-
def self.setup_utility_options(opts, config)
|
437
|
-
opts.on("-d", "--debug", "Enable debug output") do
|
438
|
-
config.debug = $DEBUG_ME = true
|
439
|
-
end
|
440
|
-
|
441
|
-
opts.on("--no-debug", "Disable debug output") do
|
442
|
-
config.debug = $DEBUG_ME = false
|
443
|
-
end
|
444
|
-
|
445
|
-
opts.on("-v", "--[no-]verbose", "Be verbose") do |value|
|
446
|
-
config.verbose = value
|
447
|
-
end
|
448
|
-
|
449
|
-
opts.on("--refresh DAYS", Integer, "Refresh models database interval in days") do |days|
|
450
|
-
config.refresh = days || 0
|
451
|
-
end
|
452
|
-
|
453
|
-
opts.on("--dump FILE", "Dump config to file") do |file|
|
454
|
-
config.dump_file = file
|
455
|
-
end
|
456
|
-
|
457
|
-
opts.on("--completion SHELL", "Show completion script for bash|zsh|fish - default is nil") do |shell|
|
458
|
-
config.completion = shell
|
459
|
-
end
|
460
|
-
|
461
|
-
opts.on("--version", "Show version") do
|
462
|
-
puts AIA::VERSION
|
463
|
-
exit
|
464
|
-
end
|
465
|
-
|
466
|
-
opts.on("-h", "--help", "Prints this help") do
|
467
|
-
puts <<~HELP
|
468
|
-
|
469
|
-
AIA your AI Assistant
|
470
|
-
- designed for generative AI workflows,
|
471
|
-
- effortlessly manage AI prompts,
|
472
|
-
- integrate seamlessly with shell and embedded Ruby (ERB),
|
473
|
-
- run batch processes,
|
474
|
-
- engage in interactive chats,
|
475
|
-
- with user defined directives, tools and MCP clients.
|
476
|
-
|
477
|
-
HELP
|
478
|
-
|
479
|
-
puts opts
|
480
|
-
|
481
|
-
puts <<~EXTRA
|
482
|
-
|
483
|
-
Explore Further:
|
484
|
-
- AIA Report an Issue: https://github.com/MadBomber/aia/issues
|
485
|
-
- AIA Documentation: https://github.com/madbomber/aia/blob/main/README.md
|
486
|
-
- AIA GitHub Repository: https://github.com/MadBomber/aia
|
487
|
-
- PromptManager Docs: https://github.com/MadBomber/prompt_manager/blob/main/README.md
|
488
|
-
- ERB Documentation: https://rubyapi.org/o/erb
|
489
|
-
- RubyLLM Tool Docs: https://rubyllm.com/guides/tools
|
490
|
-
- MCP Client Docs: https://github.com/patvice/ruby_llm-mcp/blob/main/README.md
|
491
|
-
|
492
|
-
EXTRA
|
493
|
-
|
494
|
-
exit
|
495
|
-
end
|
496
|
-
end
|
497
|
-
|
498
|
-
def self.list_available_models(query)
|
499
|
-
# SMELL: mostly duplications the code in the vailable_models directive
|
500
|
-
# assumes that the adapter is for the ruby_llm gem
|
501
|
-
# should this be moved to the Utilities class as a common method?
|
502
|
-
|
503
|
-
if query.nil?
|
504
|
-
query = []
|
505
|
-
else
|
506
|
-
query = query.split(',')
|
507
|
-
end
|
508
|
-
|
509
|
-
header = "\nAvailable LLMs"
|
510
|
-
header += " for #{query.join(' and ')}" if query
|
511
|
-
|
512
|
-
puts header + ':'
|
513
|
-
puts
|
514
|
-
|
515
|
-
q1 = query.select{|q| q.include?('_to_')}.map{|q| ':'==q[0] ? q[1...] : q}
|
516
|
-
q2 = query.reject{|q| q.include?('_to_')}
|
517
|
-
|
518
|
-
counter = 0
|
519
|
-
|
520
|
-
RubyLLM.models.all.each do |llm|
|
521
|
-
inputs = llm.modalities.input.join(',')
|
522
|
-
outputs = llm.modalities.output.join(',')
|
523
|
-
entry = "- #{llm.id} (#{llm.provider}) #{inputs} to #{outputs}"
|
524
|
-
|
525
|
-
if query.nil? || query.empty?
|
526
|
-
counter += 1
|
527
|
-
puts entry
|
528
|
-
next
|
529
|
-
end
|
530
|
-
|
531
|
-
show_it = true
|
532
|
-
q1.each{|q| show_it &&= llm.modalities.send("#{q}?")}
|
533
|
-
q2.each{|q| show_it &&= entry.include?(q)}
|
534
|
-
|
535
|
-
if show_it
|
536
|
-
counter += 1
|
537
|
-
puts entry
|
538
|
-
end
|
539
|
-
end
|
540
|
-
|
541
|
-
puts if counter > 0
|
542
|
-
puts "#{counter} LLMs matching your query"
|
543
|
-
puts
|
544
|
-
|
545
|
-
exit
|
546
|
-
end
|
547
|
-
|
548
|
-
def self.load_config_file(file, config)
|
549
|
-
if File.exist?(file)
|
550
|
-
ext = File.extname(file).downcase
|
551
|
-
content = File.read(file)
|
552
|
-
|
553
|
-
# Process ERB if filename ends with .erb
|
554
|
-
if file.end_with?('.erb')
|
555
|
-
content = ERB.new(content).result
|
556
|
-
file = file.chomp('.erb')
|
557
|
-
File.write(file, content)
|
558
|
-
end
|
559
|
-
|
560
|
-
file_config = case ext
|
561
|
-
when '.yml', '.yaml'
|
562
|
-
YAML.safe_load(content, permitted_classes: [Symbol], symbolize_names: true)
|
563
|
-
when '.toml'
|
564
|
-
TomlRB.parse(content)
|
565
|
-
else
|
566
|
-
raise "Unsupported config file format: #{ext}"
|
567
|
-
end
|
568
|
-
|
569
|
-
file_config.each do |key, value|
|
570
|
-
config[key.to_sym] = value
|
571
|
-
end
|
572
|
-
else
|
573
|
-
raise "Config file not found: #{file}"
|
574
|
-
end
|
575
|
-
end
|
576
|
-
|
577
|
-
def self.process_tools_option(a_path_list, config)
|
578
|
-
config.tool_paths ||= []
|
579
|
-
|
580
|
-
if a_path_list.empty?
|
581
|
-
STDERR.puts "No list of paths for --tools option"
|
582
|
-
exit 1
|
583
|
-
else
|
584
|
-
paths = a_path_list.split(',').map(&:strip).uniq
|
585
|
-
end
|
586
|
-
|
587
|
-
paths.each do |a_path|
|
588
|
-
if File.exist?(a_path)
|
589
|
-
if File.file?(a_path)
|
590
|
-
if '.rb' == File.extname(a_path)
|
591
|
-
config.tool_paths << a_path
|
592
|
-
else
|
593
|
-
STDERR.puts "file should have *.rb extension: #{a_path}"
|
594
|
-
exit 1
|
595
|
-
end
|
596
|
-
elsif File.directory?(a_path)
|
597
|
-
rb_files = Dir.glob(File.join(a_path, '*.rb'))
|
598
|
-
config.tool_paths += rb_files
|
599
|
-
end
|
600
|
-
else
|
601
|
-
STDERR.puts "file/dir path is not valid: #{a_path}"
|
602
|
-
exit 1
|
603
|
-
end
|
604
|
-
end
|
605
|
-
|
606
|
-
config.tool_paths.uniq!
|
607
|
-
end
|
608
|
-
|
609
|
-
def self.process_allowed_tools_option(tools_list, config)
|
610
|
-
config.allowed_tools ||= []
|
611
|
-
if tools_list.empty?
|
612
|
-
STDERR.puts "No list of tool names provided for --allowed_tools option"
|
613
|
-
exit 1
|
614
|
-
else
|
615
|
-
config.allowed_tools += tools_list.split(',').map(&:strip)
|
616
|
-
config.allowed_tools.uniq!
|
617
|
-
end
|
618
|
-
end
|
619
|
-
|
620
|
-
def self.process_rejected_tools_option(tools_list, config)
|
621
|
-
config.rejected_tools ||= []
|
622
|
-
if tools_list.empty?
|
623
|
-
STDERR.puts "No list of tool names provided for --rejected_tools option"
|
624
|
-
exit 1
|
625
|
-
else
|
626
|
-
config.rejected_tools += tools_list.split(',').map(&:strip)
|
627
|
-
config.rejected_tools.uniq!
|
628
|
-
end
|
629
|
-
end
|
630
|
-
|
631
|
-
def self.process_stdin_content
|
632
|
-
stdin_content = ''
|
633
|
-
|
634
|
-
if !STDIN.tty? && !STDIN.closed?
|
635
|
-
begin
|
636
|
-
stdin_content << "\n" + STDIN.read
|
637
|
-
STDIN.reopen('/dev/tty') # Reopen STDIN for interactive use
|
638
|
-
rescue => _
|
639
|
-
# If we can't reopen, continue without error
|
640
|
-
end
|
641
|
-
end
|
642
|
-
|
643
|
-
stdin_content
|
644
|
-
end
|
645
|
-
|
646
|
-
def self.process_prompt_id_from_args(config, remaining_args)
|
647
|
-
return if remaining_args.empty?
|
648
|
-
|
649
|
-
maybe_id = remaining_args.first
|
650
|
-
maybe_id_plus = File.join(config.prompts_dir, maybe_id + config.prompt_extname)
|
651
|
-
|
652
|
-
if AIA.bad_file?(maybe_id) && AIA.good_file?(maybe_id_plus)
|
653
|
-
config.prompt_id = remaining_args.shift
|
654
|
-
end
|
655
|
-
end
|
656
|
-
|
657
|
-
def self.validate_and_set_context_files(config, remaining_args)
|
658
|
-
return if remaining_args.empty?
|
659
|
-
|
660
|
-
bad_files = remaining_args.reject { |filename| AIA.good_file?(filename) }
|
661
|
-
if bad_files.any?
|
662
|
-
STDERR.puts "Error: The following files do not exist: #{bad_files.join(', ')}"
|
663
|
-
exit 1
|
664
|
-
end
|
665
|
-
|
666
|
-
config.context_files ||= []
|
667
|
-
config.context_files += remaining_args
|
668
|
-
end
|
669
|
-
|
670
|
-
def self.handle_executable_prompt(config)
|
671
|
-
return unless config.executable_prompt && config.context_files && !config.context_files.empty?
|
672
|
-
|
673
|
-
config.executable_prompt_file = config.context_files.pop
|
674
|
-
end
|
675
|
-
|
676
|
-
def self.validate_required_prompt_id(config)
|
677
|
-
return unless config.prompt_id.nil? && !config.chat && !config.fuzzy
|
678
|
-
|
679
|
-
STDERR.puts "Error: A prompt ID is required unless using --chat, --fuzzy, or providing context files. Use -h or --help for help."
|
680
|
-
exit 1
|
681
|
-
end
|
682
|
-
|
683
|
-
def self.process_role_configuration(config)
|
684
|
-
return if config.role.empty?
|
685
|
-
|
686
|
-
unless config.roles_prefix.empty?
|
687
|
-
unless config.role.start_with?(config.roles_prefix)
|
688
|
-
config.role.prepend "#{config.roles_prefix}/"
|
689
|
-
end
|
690
|
-
end
|
691
|
-
|
692
|
-
config.roles_dir ||= File.join(config.prompts_dir, config.roles_prefix)
|
693
|
-
|
694
|
-
if config.prompt_id.nil? || config.prompt_id.empty?
|
695
|
-
if !config.role.nil? && !config.role.empty?
|
696
|
-
config.prompt_id = config.role
|
697
|
-
config.pipeline.prepend config.prompt_id
|
698
|
-
config.role = ''
|
699
|
-
end
|
700
|
-
end
|
701
|
-
end
|
702
|
-
|
703
|
-
def self.handle_fuzzy_search_prompt_id(config)
|
704
|
-
return unless config.fuzzy && config.prompt_id.empty?
|
705
|
-
|
706
|
-
# When fuzzy search is enabled but no prompt ID is provided,
|
707
|
-
# set a special value to trigger fuzzy search without an initial query
|
708
|
-
# SMELL: This feels like a cludge
|
709
|
-
config.prompt_id = '__FUZZY_SEARCH__'
|
710
|
-
end
|
711
|
-
|
712
|
-
def self.normalize_boolean_flags(config)
|
713
|
-
normalize_boolean_flag(config, :chat)
|
714
|
-
normalize_boolean_flag(config, :fuzzy)
|
715
|
-
end
|
716
|
-
|
717
|
-
def self.normalize_boolean_flag(config, flag)
|
718
|
-
return if [TrueClass, FalseClass].include?(config[flag].class)
|
719
|
-
|
720
|
-
config[flag] = if config[flag].nil? || config[flag].empty?
|
721
|
-
false
|
722
|
-
else
|
723
|
-
true
|
724
|
-
end
|
725
|
-
end
|
726
|
-
|
727
|
-
def self.handle_completion_script(config)
|
728
|
-
return unless config.completion
|
729
|
-
|
730
|
-
generate_completion_script(config.completion)
|
731
|
-
exit
|
732
|
-
end
|
733
|
-
|
734
|
-
def self.validate_final_prompt_requirements(config)
|
735
|
-
# Only require a prompt_id if we're not in chat mode, not using fuzzy search, and no context files
|
736
|
-
if !config.chat && !config.fuzzy && (config.prompt_id.nil? || config.prompt_id.empty?) && (!config.context_files || config.context_files.empty?)
|
737
|
-
STDERR.puts "Error: A prompt ID is required unless using --chat, --fuzzy, or providing context files. Use -h or --help for help."
|
738
|
-
exit 1
|
739
|
-
end
|
740
|
-
|
741
|
-
# If we're in chat mode with context files but no prompt_id, that's valid
|
742
|
-
# This is handled implicitly - no action needed
|
743
|
-
end
|
744
|
-
|
745
|
-
def self.configure_prompt_manager(config)
|
746
|
-
return unless config.parameter_regex
|
747
|
-
|
748
|
-
PromptManager::Prompt.parameter_regex = Regexp.new(config.parameter_regex)
|
749
|
-
end
|
750
|
-
|
751
|
-
def self.prepare_pipeline(config)
|
752
|
-
return if config.prompt_id.nil? || config.prompt_id.empty? || config.prompt_id == config.pipeline.first
|
753
|
-
|
754
|
-
config.pipeline.prepend config.prompt_id
|
755
|
-
end
|
756
|
-
|
757
|
-
def self.validate_pipeline_prompts(config)
|
758
|
-
return if config.pipeline.empty?
|
759
|
-
|
760
|
-
and_exit = false
|
761
|
-
|
762
|
-
config.pipeline.each do |prompt_id|
|
763
|
-
# Skip empty prompt IDs (can happen in chat-only mode)
|
764
|
-
next if prompt_id.nil? || prompt_id.empty?
|
765
|
-
|
766
|
-
prompt_file_path = File.join(config.prompts_dir, "#{prompt_id}.txt")
|
767
|
-
unless File.exist?(prompt_file_path)
|
768
|
-
STDERR.puts "Error: Prompt ID '#{prompt_id}' does not exist at #{prompt_file_path}"
|
769
|
-
and_exit = true
|
770
|
-
end
|
771
|
-
end
|
772
|
-
|
773
|
-
exit(1) if and_exit
|
774
|
-
end
|
775
|
-
|
776
|
-
def self.parse_remaining_arguments(opt_parser, config)
|
777
|
-
args = ARGV.dup
|
778
|
-
|
779
|
-
# Parse the command line arguments
|
780
|
-
begin
|
781
|
-
config.remaining_args = opt_parser.parse(args)
|
782
|
-
rescue OptionParser::InvalidOption => e
|
783
|
-
puts e.message
|
784
|
-
puts opt_parser
|
785
|
-
exit 1
|
786
|
-
end
|
787
|
-
end
|
788
|
-
|
789
|
-
|
790
|
-
def self.cf_options(file)
|
791
|
-
config = OpenStruct.new
|
792
|
-
|
793
|
-
if File.exist?(file)
|
794
|
-
content = read_and_process_config_file(file)
|
795
|
-
file_config = parse_config_content(content, File.extname(file).downcase)
|
796
|
-
apply_file_config_to_struct(config, file_config)
|
797
|
-
else
|
798
|
-
STDERR.puts "WARNING:Config file not found: #{file}"
|
799
|
-
end
|
800
|
-
|
801
|
-
normalize_last_refresh_date(config)
|
802
|
-
config
|
803
|
-
end
|
804
|
-
|
805
|
-
def self.read_and_process_config_file(file)
|
806
|
-
content = File.read(file)
|
807
|
-
|
808
|
-
# Process ERB if filename ends with .erb
|
809
|
-
if file.end_with?('.erb')
|
810
|
-
content = ERB.new(content).result
|
811
|
-
processed_file = file.chomp('.erb')
|
812
|
-
File.write(processed_file, content)
|
813
|
-
end
|
814
|
-
|
815
|
-
content
|
816
|
-
end
|
817
|
-
|
818
|
-
def self.parse_config_content(content, ext)
|
819
|
-
case ext
|
820
|
-
when '.yml', '.yaml'
|
821
|
-
YAML.safe_load(content, permitted_classes: [Symbol], symbolize_names: true)
|
822
|
-
when '.toml'
|
823
|
-
TomlRB.parse(content)
|
824
|
-
else
|
825
|
-
raise "Unsupported config file format: #{ext}"
|
826
|
-
end
|
827
|
-
end
|
828
|
-
|
829
|
-
def self.apply_file_config_to_struct(config, file_config)
|
830
|
-
file_config.each do |key, value|
|
831
|
-
config[key] = value
|
832
|
-
end
|
833
|
-
end
|
834
|
-
|
835
|
-
def self.normalize_last_refresh_date(config)
|
836
|
-
return unless config.last_refresh&.is_a?(String)
|
837
|
-
|
838
|
-
config.last_refresh = Date.strptime(config.last_refresh, '%Y-%m-%d')
|
839
|
-
end
|
840
|
-
|
841
|
-
|
842
|
-
def self.generate_completion_script(shell)
|
843
|
-
script_path = File.join(File.dirname(__FILE__), "aia_completion.#{shell}")
|
844
|
-
|
845
|
-
if File.exist?(script_path)
|
846
|
-
puts File.read(script_path)
|
17
|
+
# Maintain backward compatibility by delegating to Base module
|
18
|
+
def self.method_missing(method_name, *args, &block)
|
19
|
+
if ConfigModules::Base.respond_to?(method_name)
|
20
|
+
ConfigModules::Base.send(method_name, *args, &block)
|
847
21
|
else
|
848
|
-
|
22
|
+
super
|
849
23
|
end
|
850
24
|
end
|
851
25
|
|
852
|
-
|
853
|
-
|
854
|
-
# Implementation for config dump
|
855
|
-
ext = File.extname(file).downcase
|
856
|
-
|
857
|
-
config.last_refresh = config.last_refresh.to_s if config.last_refresh.is_a? Date
|
858
|
-
|
859
|
-
config_hash = config.to_h
|
860
|
-
|
861
|
-
# Remove prompt_id to prevent automatic initial pompting in --chat mode
|
862
|
-
config_hash.delete(:prompt_id)
|
863
|
-
|
864
|
-
# Remove dump_file key to prevent automatic exit on next load
|
865
|
-
config_hash.delete(:dump_file)
|
866
|
-
|
867
|
-
content = case ext
|
868
|
-
when '.yml', '.yaml'
|
869
|
-
YAML.dump(config_hash)
|
870
|
-
when '.toml'
|
871
|
-
TomlRB.dump(config_hash)
|
872
|
-
else
|
873
|
-
raise "Unsupported config file format: #{ext}"
|
874
|
-
end
|
875
|
-
|
876
|
-
File.write(file, content)
|
877
|
-
puts "Config successfully dumped to #{file}"
|
26
|
+
def self.respond_to_missing?(method_name, include_private = false)
|
27
|
+
ConfigModules::Base.respond_to?(method_name, include_private) || super
|
878
28
|
end
|
879
29
|
end
|
880
30
|
end
|