aia 0.5.18 → 0.8.1

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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +1 -0
  3. data/.version +1 -1
  4. data/CHANGELOG.md +39 -5
  5. data/README.md +388 -219
  6. data/Rakefile +16 -5
  7. data/_notes.txt +231 -0
  8. data/bin/aia +3 -2
  9. data/examples/README.md +140 -0
  10. data/examples/headlines +21 -0
  11. data/lib/aia/ai_client_adapter.rb +210 -0
  12. data/lib/aia/chat_processor_service.rb +120 -0
  13. data/lib/aia/config.rb +473 -4
  14. data/lib/aia/context_manager.rb +58 -0
  15. data/lib/aia/directive_processor.rb +267 -0
  16. data/lib/aia/{tools/fzf.rb → fzf.rb} +9 -17
  17. data/lib/aia/history_manager.rb +85 -0
  18. data/lib/aia/prompt_handler.rb +178 -0
  19. data/lib/aia/session.rb +215 -0
  20. data/lib/aia/shell_command_executor.rb +109 -0
  21. data/lib/aia/ui_presenter.rb +110 -0
  22. data/lib/aia/utility.rb +24 -0
  23. data/lib/aia/version.rb +9 -6
  24. data/lib/aia.rb +57 -61
  25. data/lib/extensions/openstruct_merge.rb +44 -0
  26. metadata +29 -42
  27. data/LICENSE.txt +0 -21
  28. data/doc/aia_and_pre_compositional_prompts.md +0 -474
  29. data/lib/aia/clause.rb +0 -7
  30. data/lib/aia/cli.rb +0 -452
  31. data/lib/aia/directives.rb +0 -142
  32. data/lib/aia/dynamic_content.rb +0 -26
  33. data/lib/aia/logging.rb +0 -62
  34. data/lib/aia/main.rb +0 -265
  35. data/lib/aia/prompt.rb +0 -275
  36. data/lib/aia/tools/backend_common.rb +0 -58
  37. data/lib/aia/tools/client.rb +0 -197
  38. data/lib/aia/tools/editor.rb +0 -52
  39. data/lib/aia/tools/glow.rb +0 -90
  40. data/lib/aia/tools/llm.rb +0 -77
  41. data/lib/aia/tools/mods.rb +0 -100
  42. data/lib/aia/tools/sgpt.rb +0 -79
  43. data/lib/aia/tools/subl.rb +0 -68
  44. data/lib/aia/tools/vim.rb +0 -93
  45. data/lib/aia/tools.rb +0 -88
  46. data/lib/aia/user_query.rb +0 -21
  47. data/lib/core_ext/string_wrap.rb +0 -73
  48. data/lib/core_ext/tty-spinner_log.rb +0 -25
  49. data/man/aia.1 +0 -272
  50. data/man/aia.1.md +0 -236
data/lib/aia/cli.rb DELETED
@@ -1,452 +0,0 @@
1
- # lib/aia/cli.rb
2
-
3
- HOME = Pathname.new(ENV['HOME'])
4
- MY_NAME = 'aia'
5
-
6
-
7
- require 'hashie'
8
- require 'pathname'
9
- require 'yaml'
10
- require 'toml-rb'
11
-
12
-
13
- class AIA::Cli
14
- CF_FORMATS = %w[yml yaml toml]
15
- ENV_PREFIX = self.name.split('::').first.upcase + "_"
16
- MAN_PAGE_PATH = Pathname.new(__dir__) + '../../man/aia.1'
17
-
18
-
19
- def initialize(args)
20
- args = args.split(' ') if args.is_a? String
21
-
22
- setup_options_with_defaults(args) # 1. defaults
23
- load_env_options # 2. over-ride with envars
24
- process_command_line_arguments # 3. over-ride with command line options
25
-
26
- # 4. over-ride everything with config file
27
- load_config_file unless AIA.config.config_file.nil?
28
-
29
- convert_to_pathname_objects
30
- error_on_invalid_option_combinations
31
- setup_prompt_manager
32
- execute_immediate_commands
33
- end
34
-
35
-
36
- def convert_pathname_objects!(converting_to_pathname: true)
37
- path_keys = AIA.config.keys.grep(/_(dir|file)\z/)
38
- path_keys.each do |key|
39
- case AIA.config[key]
40
- when String
41
- AIA.config[key] = string_to_pathname(AIA.config[key])
42
- when Pathname
43
- AIA.config[key] = pathname_to_string(AIA.config[key]) unless converting_to_pathname
44
- end
45
- end
46
- end
47
-
48
-
49
- def error_on_invalid_option_combinations
50
- # --chat is intended as an interactive exchange
51
- if AIA.config.chat?
52
- unless AIA.config.next.empty?
53
- abort "ERROR: Cannot use --next with --chat"
54
- end
55
- unless STDOUT == AIA.config.out_file
56
- abort "ERROR: Cannot use --out_file with --chat"
57
- end
58
- unless AIA.config.pipeline.empty?
59
- abort "ERROR: Cannot use --pipeline with --chat"
60
- end
61
- end
62
-
63
- # --next says which prompt to process next
64
- # but --pipeline gives an entire sequence of prompts for processing
65
- unless AIA.config.next.empty?
66
- unless AIA.config.pipeline.empty?
67
- abort "ERROR: Cannot use --pipeline with --next"
68
- end
69
- end
70
- end
71
-
72
- def string_to_pathname(string)
73
- ['~/', '$HOME/'].each do |prefix|
74
- if string.start_with? prefix
75
- string = string.gsub(prefix, HOME.to_s+'/')
76
- break
77
- end
78
- end
79
-
80
- pathname = Pathname.new(string)
81
- pathname.relative? ? Pathname.pwd + pathname : pathname
82
- end
83
-
84
-
85
- def pathname_to_string(pathname)
86
- pathname.to_s
87
- end
88
-
89
-
90
- def convert_to_pathname_objects
91
- convert_pathname_objects!(converting_to_pathname: true)
92
- end
93
-
94
-
95
- def convert_from_pathname_objects
96
- convert_pathname_objects!(converting_to_pathname: false)
97
- end
98
-
99
-
100
- def load_env_options
101
- known_keys = @options.keys
102
-
103
- keys = ENV.keys
104
- .select{|k| k.start_with?(ENV_PREFIX)}
105
- .map{|k| k.gsub(ENV_PREFIX,'').downcase.to_sym}
106
-
107
- keys.each do |key|
108
- envar_key = ENV_PREFIX + key.to_s.upcase
109
- if known_keys.include?(key)
110
- AIA.config[key] = ENV[envar_key]
111
- elsif known_keys.include?("#{key}?".to_sym)
112
- key = "#{key}?".to_sym
113
- AIA.config[key] = %w[true t yes yea y 1].include?(ENV[envar_key].strip.downcase) ? true : false
114
- else
115
- # This is a new config key
116
- AIA.config[key] = ENV[envar_key]
117
- end
118
- end
119
- end
120
-
121
-
122
- def replace_erb_in_config_file
123
- content = Pathname.new(AIA.config.config_file).read
124
- content = ERB.new(content).result(binding)
125
- AIA.config.config_file = AIA.config.config_file.to_s.gsub('.erb', '')
126
- Pathname.new(AIA.config.config_file).write content
127
- end
128
-
129
-
130
- def load_config_file
131
- if AIA.config.config_file.to_s.end_with?(".erb")
132
- replace_erb_in_config_file
133
- end
134
-
135
- AIA.config.config_file = Pathname.new(AIA.config.config_file)
136
- if AIA.config.config_file.exist?
137
- AIA.config.merge! parse_config_file
138
- else
139
- abort "Config file does not exist: #{AIA.config.config_file}"
140
- end
141
- end
142
-
143
-
144
- def setup_options_with_defaults(args)
145
- # TODO: This structure if flat; consider making it
146
- # at least two levels totake advantage of
147
- # YAML and TOML capabilities to isolate
148
- # common options within a section.
149
- #
150
- @options = {
151
- # Default
152
- # Key Value, switches
153
- arguments: [args], # NOTE: after process, prompt_id and context_files will be left
154
- directives: [[]], # an empty Array as the default value
155
- extra: [''], #
156
- #
157
- model: ["gpt-4-1106-preview", "--llm --model"],
158
- speech_model: ["tts-1", "--sm --speech_model"],
159
- voice: ["alloy", "--voice"],
160
- #
161
- transcription_model: ["wisper-1", "--tm --transcription_model"],
162
- #
163
- dump_file: [nil, "--dump"],
164
- completion: [nil, "--completion"],
165
- #
166
- chat?: [false, "--chat"],
167
- debug?: [false, "-d --debug"],
168
- edit?: [false, "-e --edit"],
169
- erb?: [false, "--erb"],
170
- fuzzy?: [false, "-f --fuzzy"],
171
- help?: [false, "-h --help"],
172
- markdown?: [true, "-m --markdown --no-markdown --md --no-md"],
173
- render?: [false, "--render"],
174
- shell?: [false, "--shell"],
175
- speak?: [false, "--speak"],
176
- terse?: [false, "--terse"],
177
- verbose?: [false, "-v --verbose"],
178
- version?: [false, "--version"],
179
- #
180
- next: ['', "-n --next"],
181
- pipeline: [[], "--pipeline"],
182
- role: ['', "-r --role"],
183
- #
184
- config_file:[nil, "-c --config_file"],
185
- prompts_dir:["~/.prompts", "-p --prompts_dir"],
186
- roles_dir: ["~/.prompts/roles", "--roles_dir"],
187
- out_file: [STDOUT, "-o --out_file --no-out_file"],
188
- log_file: ["~/.prompts/_prompts.log", "-l --log_file --no-log_file"],
189
- #
190
- backend: ['mods', "-b --be --backend --no-backend"],
191
- #
192
- # text2image related ...
193
- #
194
- image_size: ['', '--is --image_size'],
195
- image_quality: ['', '--iq --image_quality'],
196
- }
197
-
198
- AIA.config = AIA::Config.new(@options.transform_values { |values| values.first })
199
- end
200
-
201
-
202
- def arguments
203
- AIA.config.arguments
204
- end
205
-
206
-
207
- def execute_immediate_commands
208
- show_usage if AIA.config.help?
209
- show_version if AIA.config.version?
210
- dump_config_file if AIA.config.dump_file
211
- show_completion if AIA.config.completion
212
- end
213
-
214
-
215
- def dump_config_file
216
- a_hash = prepare_config_as_hash
217
-
218
- dump_file = Pathname.new AIA.config.dump_file
219
- extname = dump_file.extname.to_s.downcase
220
-
221
- case extname
222
- when '.yml', '.yaml'
223
- dump_file.write YAML.dump(a_hash)
224
- when '.toml'
225
- dump_file.write TomlRB.dump(a_hash)
226
- else
227
- abort "Invalid config file format (#{extname}) request. Only #{CF_FORMATS.join(', ')} are supported."
228
- end
229
-
230
- exit
231
- end
232
-
233
-
234
- def prepare_config_as_hash
235
- convert_from_pathname_objects
236
-
237
- a_hash = AIA.config.to_h
238
- a_hash['dump'] = nil
239
-
240
- %w[ arguments config_file dump_file ].each do |unwanted_key|
241
- a_hash.delete(unwanted_key)
242
- end
243
-
244
- a_hash
245
- end
246
-
247
-
248
- def process_command_line_arguments
249
- # get the options meant for the backend AI command
250
- # doing this first in case there are any options that conflict
251
- # between frontend and backend.
252
- extract_extra_options
253
-
254
- @options.keys.each do |option|
255
- check_for option
256
- end
257
-
258
- bad_options = arguments.select{|a| a.start_with?('-')}
259
-
260
- unless bad_options.empty?
261
- puts <<~EOS
262
-
263
- ERROR: Unknown options: #{bad_options.join(' ')}
264
-
265
- EOS
266
-
267
- show_error_usage
268
-
269
- exit
270
- end
271
-
272
- # After all other arguments
273
- # are processed, check for role parameter.
274
- check_for_role_parameter
275
- end
276
-
277
-
278
- def check_for(option_sym)
279
- # sometimes @options has stuff that is not a command line option
280
- return if @options[option_sym].nil? || @options[option_sym].size <= 1
281
-
282
- boolean = option_sym.to_s.end_with?('?')
283
- switches = @options[option_sym][1].split
284
-
285
- switches.each do |switch|
286
- if arguments.include?(switch)
287
- index = arguments.index(switch)
288
-
289
- if boolean
290
- AIA.config[option_sym] = switch.include?('-no-') ? false : true
291
- arguments.slice!(index,1)
292
- else
293
- if switch.include?('-no-')
294
- AIA.config[option_sym] = switch.include?('out_file') ? STDOUT : nil
295
- arguments.slice!(index,1)
296
- else
297
- value = arguments[index + 1]
298
- if value.nil? || value.start_with?('-')
299
- abort "ERROR: #{option_sym} requires a parameter value"
300
- elsif "--pipeline" == switch
301
- prompt_sequence = value.split(',')
302
- AIA.config[option_sym] = prompt_sequence
303
- arguments.slice!(index,2)
304
- else
305
- AIA.config[option_sym] = value
306
- arguments.slice!(index,2)
307
- end
308
- end
309
- end
310
-
311
- break
312
- end
313
- end
314
- end
315
-
316
-
317
- def check_for_role_parameter
318
- role = AIA.config.role
319
- return if role.empty?
320
-
321
- role_path = string_to_pathname(AIA.config.roles_dir) + "#{role}.txt"
322
-
323
- unless role_path.exist?
324
- puts "Role prompt '#{role}' not found. Invoking fzf to choose a role..."
325
- invoke_fzf_to_choose_role
326
- end
327
- end
328
-
329
-
330
- def invoke_fzf_to_choose_role
331
- roles_path = string_to_pathname AIA.config.roles_dir
332
-
333
- available_roles = roles_path
334
- .children
335
- .select { |f| '.txt' == f.extname}
336
- .map{|role| role.basename.to_s.gsub('.txt','')}
337
-
338
- fzf = AIA::Fzf.new(
339
- list: available_roles,
340
- directory: roles_path,
341
- prompt: 'Select Role:',
342
- extension: '.txt'
343
- )
344
-
345
- chosen_role = fzf.run
346
-
347
- if chosen_role.nil?
348
- abort("No role selected. Exiting...")
349
- else
350
- AIA.config.role = chosen_role
351
- puts "Role changed to '#{chosen_role}'."
352
- end
353
- end
354
-
355
-
356
- def show_error_usage
357
- puts <<~ERROR_USAGE
358
-
359
- Usage: aia [options] PROMPT_ID [CONTEXT_FILE(s)] [-- EXTERNAL_OPTIONS]"
360
- Try 'aia --help' for more information."
361
-
362
- ERROR_USAGE
363
- end
364
-
365
-
366
- # aia usage is maintained in a man page
367
- def show_usage
368
- @options[:help?][0] = false
369
- puts `man #{MAN_PAGE_PATH}`
370
- show_verbose_usage if AIA.config.verbose?
371
- exit
372
- end
373
- alias_method :show_help, :show_usage
374
-
375
-
376
- def show_verbose_usage
377
- puts <<~EOS
378
-
379
- ======================================
380
- == Currently selected Backend: #{AIA.config.backend} ==
381
- ======================================
382
-
383
- EOS
384
- puts `mods --help` if "mods" == AIA.config.backend
385
- puts `sgpt --help` if "sgpt" == AIA.config.backend
386
- puts
387
- end
388
- # alias_method :show_verbose_help, :show_verbose_usage
389
-
390
-
391
- def show_completion
392
- shell = AIA.config.completion
393
- script = Pathname.new(__dir__) + "aia_completion.#{shell}"
394
-
395
- if script.exist?
396
- puts
397
- puts script.read
398
- puts
399
- else
400
- STDERR.puts <<~EOS
401
-
402
- ERROR: The shell '#{shell}' is not supported.
403
-
404
- EOS
405
- end
406
-
407
- exit
408
- end
409
-
410
-
411
- def show_version
412
- puts AIA::VERSION
413
- exit
414
- end
415
-
416
-
417
- def setup_prompt_manager
418
- @prompt = nil
419
-
420
- PromptManager::Prompt.storage_adapter =
421
- PromptManager::Storage::FileSystemAdapter.config do |config|
422
- config.prompts_dir = AIA.config.prompts_dir
423
- config.prompt_extension = '.txt'
424
- config.params_extension = '.json'
425
- config.search_proc = nil
426
- # TODO: add the rgfzf script for search_proc
427
- end.new
428
- end
429
-
430
-
431
- # Get the additional CLI arguments intended for the
432
- # backend gen-AI processor.
433
- def extract_extra_options
434
- extra_index = arguments.index('--')
435
-
436
- if extra_index
437
- AIA.config.extra = arguments.slice!(extra_index..-1)[1..].join(' ')
438
- end
439
- end
440
-
441
-
442
- def parse_config_file
443
- case AIA.config.config_file.extname.downcase
444
- when '.yaml', '.yml'
445
- YAML.safe_load(AIA.config.config_file.read)
446
- when '.toml'
447
- TomlRB.parse(AIA.config.config_file.read)
448
- else
449
- abort "Unsupported config file type: #{AIA.config.config_file.extname}"
450
- end
451
- end
452
- end
@@ -1,142 +0,0 @@
1
- # lib/aia/directives.rb
2
-
3
- require 'hashie'
4
-
5
- =begin
6
- AIA.config.directives is an Array of Arrays. An
7
- entry looks like this:
8
- [directive, parameters]
9
- where both are String objects
10
- =end
11
-
12
-
13
- class AIA::Directives
14
- def execute_my_directives
15
- return if AIA.config.directives.nil? || AIA.config.directives.empty?
16
-
17
- result = ""
18
- not_mine = []
19
-
20
- AIA.config.directives.each do |entry|
21
- directive = entry[0].to_sym
22
- parameters = entry[1]
23
-
24
- if respond_to? directive
25
- output = send(directive, parameters)
26
- result << "#{output}\n" unless output.nil?
27
- else
28
- not_mine << entry
29
- end
30
- end
31
-
32
- AIA.config.directives = not_mine
33
-
34
- result.empty? ? nil : result
35
- end
36
-
37
-
38
- def box(what)
39
- f = what[0]
40
- bar = "#{f}"*what.size
41
- puts "#{bar}\n#{what}\n#{bar}"
42
- end
43
-
44
-
45
- # Allows a prompt to change its configuration environment
46
- def config(what)
47
- parts = what.split(' ')
48
- item = parts.shift
49
- parts.shift if %w[:= =].include? parts[0]
50
-
51
- if '<<' == parts[0]
52
- parts.shift
53
- value = parts.join
54
- if AIA.config(item).is_a?(Array)
55
- AIA.config[item] << value
56
- else
57
- AIA.config[item] = [ value ]
58
- end
59
- else
60
- value = parts.join
61
- if item.end_with?('?')
62
- AIA.config[item] = %w[1 y yea yes t true].include?(value.downcase)
63
- elsif item.end_with?('_file')
64
- if "STDOUT" == value.upcase
65
- AIA.config[item] = STDOUT
66
- elsif "STDERR" == value.upcase
67
- AIA.config[item] = STDERR
68
- else
69
- AIA.config[item] = value.start_with?('/') ?
70
- Pathname.new(value) :
71
- Pathname.pwd + value
72
- end
73
- elsif %w[next pipeline].include? item.downcase
74
- pipeline(value)
75
- else
76
- AIA.config[item] = value
77
- end
78
- end
79
-
80
- nil
81
- end
82
-
83
-
84
- # TODO: we need a way to submit CLI arguments into
85
- # the next prompt(s) from the main prompt.
86
- # currently the config for subsequent prompts
87
- # is expected to be set within those prompts.
88
- # Maybe something like:
89
- # //next prompt_id CLI args
90
- # This would mean that the pipeline would be:
91
- # //pipeline id1 cli args, id2 cli args, id3 cli args
92
- #
93
-
94
- # TODO: Change AIA.config.pipline Array to be an Array of arrays
95
- # where each entry is:
96
- # [prompt_id, cli_args]
97
- # This means that:
98
- # entry = AIA.config.pipeline.shift
99
- # entry.is_A?(Sring) ? 'old format' : 'new format'
100
- #
101
-
102
- # //next id
103
- # //pipeline id1,id2, id3 , id4
104
- def pipeline(what)
105
- return if what.empty?
106
- AIA.config.pipeline << what.split(',').map(&:strip)
107
- AIA.config.pipeline.flatten!
108
- end
109
- alias_method :next, :pipeline
110
-
111
- # when path_to_file is relative it will be
112
- # relative to the PWD.
113
- #
114
- # TODO: Consider an AIA_INCLUDE_DIR --include_dir
115
- # option to be used for all relative include paths
116
- #
117
- def include(path_to_file)
118
- path = Pathname.new path_to_file
119
- if path.exist? && path.readable?
120
- content = path.readlines.reject do |a_line|
121
- a_line.strip.start_with?(AIA::Prompt::COMMENT_SIGNAL) ||
122
- a_line.strip.start_with?(AIA::Prompt::DIRECTIVE_SIGNAL)
123
- end.join("\n")
124
- else
125
- abort "ERROR: could not include #{path_to_file}"
126
- end
127
-
128
- content
129
- end
130
-
131
-
132
- def shell(command)
133
- `#{command}`
134
- end
135
-
136
-
137
- def ruby(code)
138
- output = eval(code)
139
-
140
- output.is_a?(String) ? output : nil
141
- end
142
- end
@@ -1,26 +0,0 @@
1
- # aia/lib/aia/dynamic_content.rb
2
-
3
- require 'erb'
4
-
5
- module AIA::DynamicContent
6
-
7
- # inserts environment variables (envars) and dynamic content into a prompt
8
- # replaces patterns like $HOME and ${HOME} with the value of ENV['HOME']
9
- # replaces patterns like $(shell command) with the output of the shell command
10
- #
11
- def render_env(a_string)
12
- a_string.gsub(/\$(\w+|\{\w+\})/) do |match|
13
- ENV[match.tr('$', '').tr('{}', '')]
14
- end.gsub(/\$\((.*?)\)/) do |match|
15
- `#{match[2..-2]}`.chomp
16
- end
17
- end
18
-
19
-
20
- # Need to use instance variables in assignments
21
- # to maintain binding from one follow up prompt
22
- # to another.
23
- def render_erb(the_prompt_text)
24
- ERB.new(the_prompt_text).result(binding)
25
- end
26
- end
data/lib/aia/logging.rb DELETED
@@ -1,62 +0,0 @@
1
- # lib/aia/logging.rb
2
-
3
- require 'logger'
4
-
5
- class AIA::Logging
6
- attr_accessor :logger
7
-
8
- def initialize(log_file_path)
9
- @logger = if log_file_path
10
- Logger.new(
11
- log_file_path, # path/to/file
12
- 'weekly', # rotation interval
13
- 'a' # append to existing file
14
- )
15
- else
16
- # SMELL: Looks like you get logging whether you want it or not
17
- # TODO: when path is nil create a fake logger
18
- # that does nothing
19
- Logger.new(STDOUT) # Fall back to standard output if path is nil or invalid
20
- end
21
-
22
- configure_logger
23
- end
24
-
25
- def prompt_result(prompt, result)
26
- logger.info( <<~EOS
27
- PROMPT ID #{prompt.id}
28
- PATH: #{prompt.path}
29
- KEYWORDS: #{prompt.keywords.join(', ')}
30
-
31
- #{prompt.to_s}
32
-
33
- RESULT:
34
- #{result}
35
-
36
-
37
- EOS
38
- )
39
- rescue StandardError => e
40
- logger.error("Failed to log the result. Error: #{e.message}")
41
- end
42
-
43
-
44
- def debug(msg) = logger.debug(msg)
45
- def info(msg) = logger.info(msg)
46
- def warn(msg) = logger.warn(msg)
47
- def error(msg) = logger.error(msg)
48
- def fatal(msg) = logger.fatal(msg)
49
-
50
- private
51
-
52
- def configure_logger
53
- @logger.formatter = proc do |severity, datetime, _progname, msg|
54
- formatted_datetime = datetime.strftime("%Y-%m-%d %H:%M:%S")
55
- "[#{formatted_datetime}] #{severity}: #{msg}\n"
56
- end
57
- @logger.level = Logger::DEBUG
58
- end
59
- end
60
-
61
-
62
-