aia 0.0.4 → 0.3.0

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.
@@ -0,0 +1,212 @@
1
+ # lib/aia/prompt_processing.rb
2
+
3
+ module AIA::PromptProcessing
4
+ KW_HISTORY_MAX = 5
5
+
6
+ # Fetch the first argument which should be the prompt id
7
+ def get_prompt
8
+ prompt_id = @arguments.shift
9
+
10
+ # TODO: or maybe go to a generic search and select process
11
+
12
+ abort("Please provide a prompt id") unless prompt_id
13
+
14
+ search_for_a_matching_prompt(prompt_id) unless existing_prompt?(prompt_id)
15
+ edit_prompt if edit?
16
+ end
17
+
18
+
19
+ # Check if a prompt with the given id already exists
20
+ def existing_prompt?(prompt_id)
21
+ @prompt = PromptManager::Prompt.get(id: prompt_id)
22
+
23
+ # FIXME: Kludge until prompt_manager is changed
24
+ # prompt_manager v0.3.0 now supports this feature.
25
+ # keeping the kludge in for legacy JSON files
26
+ # files which have not yet been reformatted.
27
+ @prompt.keywords.each do |kw|
28
+ if @prompt.parameters[kw].nil? || @prompt.parameters[kw].empty?
29
+ @prompt.parameters[kw] = []
30
+ else
31
+ @prompt.parameters[kw] = Array(@prompt.parameters[kw])
32
+ end
33
+ end
34
+
35
+ true
36
+ rescue ArgumentError
37
+ false
38
+ end
39
+
40
+
41
+ # Process the prompt's associated keywords and parameters
42
+ def process_prompt
43
+ unless @prompt.keywords.empty?
44
+ replace_keywords
45
+ @prompt.build
46
+ @prompt.save
47
+ end
48
+ end
49
+
50
+
51
+
52
+ def replace_keywords
53
+ puts
54
+ puts "ID: #{@prompt.id}"
55
+
56
+ show_prompt_without_comments
57
+
58
+ puts "\nPress up/down arrow to scroll through history."
59
+ puts "Type new input or edit the current input."
60
+ puts "Quit #{MY_NAME} with a CNTL-D or a CNTL-C"
61
+ puts
62
+ @prompt.keywords.each do |kw|
63
+ value = keyword_value(kw, @prompt.parameters[kw])
64
+
65
+ unless value.nil? || value.strip.empty?
66
+ value_inx = @prompt.parameters[kw].index(value)
67
+
68
+ if value_inx
69
+ @prompt.parameters[kw].delete_at(value_inx)
70
+ end
71
+
72
+ # The most recent value for this kw will always be
73
+ # in the last position
74
+ @prompt.parameters[kw] << value
75
+ @prompt.parameters[kw].shift if @prompt.parameters[kw].size > KW_HISTORY_MAX
76
+ end
77
+ end
78
+ end
79
+
80
+
81
+ # query the user for a value to the keyword allow the
82
+ # reuse of the previous value shown as the default
83
+ def keyword_value(kw, history_array)
84
+
85
+ Readline::HISTORY.clear
86
+ Array(history_array).each { |entry| Readline::HISTORY.push(entry) unless entry.nil? || entry.empty? }
87
+
88
+ puts "Parameter #{kw} ..."
89
+
90
+ begin
91
+ a_string = Readline.readline("\n-=> ", true)
92
+ rescue Interrupt
93
+ a_string = nil
94
+ end
95
+
96
+ if a_string.nil?
97
+ puts "okay. Come back soon."
98
+ exit
99
+ end
100
+
101
+ puts
102
+ a_string.empty? ? default : a_string
103
+ end
104
+
105
+
106
+ # Search for a prompt with a matching id or keyword
107
+ def search_for_a_matching_prompt(prompt_id)
108
+ # TODO: using the rgfzf version of the search_proc should only
109
+ # return a single prompt_id
110
+ found_prompts = PromptManager::Prompt.search(prompt_id)
111
+
112
+ if found_prompts.empty?
113
+ if edit?
114
+ create_prompt(prompt_id)
115
+ edit_prompt
116
+ else
117
+ abort <<~EOS
118
+
119
+ No prompts where found for: #{prompt_id}
120
+ To create a prompt with this ID use the --edit option
121
+ like this:
122
+ #{MY_NAME} #{prompt_id} --edit
123
+
124
+ EOS
125
+ end
126
+ else
127
+ prompt_id = 1 == found_prompts.size ? found_prompts.first : handle_multiple_prompts(found_prompts, prompt_id)
128
+ @prompt = PromptManager::Prompt.get(id: prompt_id)
129
+ end
130
+ end
131
+
132
+
133
+ def handle_multiple_prompts(found_these, while_looking_for_this)
134
+ raise ArgumentError, "Argument is not an Array" unless found_these.is_a?(Array)
135
+
136
+ # TODO: Make this a class constant for defaults; make the header content
137
+ # a parameter so it can be varied.
138
+ fzf_options = [
139
+ "--tabstop=2", # 2 soaces for a tab
140
+ "--header='Prompt IDs which contain: #{while_looking_for_this}\nPress ESC to cancel.'",
141
+ "--header-first",
142
+ "--prompt='Search term: '",
143
+ '--delimiter :',
144
+ "--preview 'cat $PROMPTS_DIR/{1}.txt'",
145
+ "--preview-window=down:50%:wrap"
146
+ ].join(' ')
147
+
148
+
149
+ # Create a temporary file to hold the list of strings
150
+ temp_file = Tempfile.new('fzf-input')
151
+
152
+ begin
153
+ # Write all strings to the temp file
154
+ temp_file.puts(found_these)
155
+ temp_file.close
156
+
157
+ # Execute fzf command-line utility to allow selection
158
+ selected = `cat #{temp_file.path} | fzf #{fzf_options}`.strip
159
+
160
+ # Check if fzf actually returned a string; if not, return nil
161
+ result = selected.empty? ? nil : selected
162
+ ensure
163
+ # Ensure that the tempfile is closed and unlinked
164
+ temp_file.unlink
165
+ end
166
+
167
+ exit unless result
168
+
169
+ result
170
+ end
171
+
172
+
173
+ def create_prompt(prompt_id)
174
+ @prompt = PromptManager::Prompt.create(id: prompt_id)
175
+ # TODO: consider a configurable prompt template
176
+ # ERB ???
177
+ end
178
+
179
+
180
+ def edit_prompt
181
+ `#{EDITOR} #{@prompt.path}`
182
+ @options[:edit?][0] = false
183
+ @prompt = PromptManager::Prompt.get(id: @prompt.id)
184
+ end
185
+
186
+
187
+ def show_prompt_without_comments
188
+ puts remove_comments.wrap(indent: 4)
189
+ end
190
+
191
+
192
+ def remove_comments
193
+ lines = @prompt.text
194
+ .split("\n")
195
+ .reject{|a_line| a_line.strip.start_with?('#')}
196
+
197
+ # Remove empty lines at the start of the prompt
198
+ #
199
+ lines = lines.drop_while(&:empty?)
200
+
201
+ # Drop all the lines at __END__ and after
202
+ #
203
+ logical_end_inx = lines.index("__END__")
204
+
205
+ if logical_end_inx
206
+ lines[0...logical_end_inx] # NOTE: ... means to not include last index
207
+ else
208
+ lines
209
+ end.join("\n")
210
+ end
211
+ end
212
+
data/lib/aia/version.rb CHANGED
@@ -1,5 +1,6 @@
1
+ # lib/aia/version.rb
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module AIA
4
- VERSION = "0.0.4"
5
+ VERSION = "0.3.0"
5
6
  end
data/lib/aia.rb CHANGED
@@ -1,397 +1,20 @@
1
1
  # lib/aia.rb
2
2
 
3
- require 'amazing_print'
4
3
  require 'pathname'
5
4
  require 'readline'
6
5
  require 'tempfile'
7
6
 
8
-
9
- require 'debug_me'
10
- include DebugMe
11
-
12
- $DEBUG_ME = true # ARGV.include?("--debug") || ARGV.include?("-d")
13
-
14
7
  require 'prompt_manager'
15
8
  require 'prompt_manager/storage/file_system_adapter'
16
9
 
17
10
  require_relative "aia/version"
11
+ require_relative "aia/main"
18
12
  require_relative "core_ext/string_wrap"
19
13
 
20
14
  module AIA
21
- class Main
22
- HOME = Pathname.new(ENV['HOME'])
23
- PROMPTS_DIR = Pathname.new(ENV['PROMPTS_DIR'] || (HOME + ".prompts_dir"))
24
-
25
- AI_CLI_PROGRAM = "mods"
26
- EDITOR = ENV['EDITOR'] || 'edit'
27
- MY_NAME = Pathname.new(__FILE__).basename.to_s.split('.')[0]
28
- MODS_MODEL = ENV['MODS_MODEL'] || 'gpt-4-1106-preview'
29
- OUTPUT = Pathname.pwd + "temp.md"
30
- PROMPT_LOG = PROMPTS_DIR + "_prompts.log"
31
-
32
-
33
- # TODO: write the usage text
34
- USAGE = <<~EOUSAGE
35
- AI Assistant (aia)
36
- ==================
37
-
38
- The AI cli program being used is: #{AI_CLI_PROGRAM}
39
-
40
- You can pass additional CLI options to #{AI_CLI_PROGRAM} like this:
41
- "#{MY_NAME} my options -- options for #{AI_CLI_PROGRAM}"
42
- EOUSAGE
43
-
44
-
45
- def initialize(args= ARGV)
46
- @prompt = nil
47
- @arguments = args
48
- @options = {
49
- edit?: false,
50
- debug?: false,
51
- verbose?: false,
52
- version?: false,
53
- help?: false,
54
- fuzzy?: false,
55
- markdown?: true,
56
- output: OUTPUT,
57
- log: PROMPT_LOG,
58
- }
59
- @extra_options = [] # intended for the backend AI processor
60
-
61
- build_reader_methods # for the @options keys
62
- process_arguments
63
-
64
- PromptManager::Prompt.storage_adapter =
65
- PromptManager::Storage::FileSystemAdapter.config do |config|
66
- config.prompts_dir = PROMPTS_DIR
67
- config.prompt_extension = '.txt'
68
- config.params_extension = '.json'
69
- config.search_proc = nil
70
- # TODO: add the rgfzz script for search_proc
71
- end.new
72
-
73
- setup_cli_program
74
- end
75
-
76
-
77
- def build_reader_methods
78
- @options.keys.each do |key|
79
- define_singleton_method(key) do
80
- @options[key]
81
- end
82
- end
83
- end
84
-
85
-
86
- def process_arguments
87
- @options.keys.each do |option|
88
- check_for option
89
- end
90
-
91
- # get the options meant for the backend AI command
92
- extract_extra_options
93
-
94
- bad_options = @arguments.select{|a| a.start_with?('-')}
95
-
96
- unless bad_options.empty?
97
- puts <<~EOS
98
-
99
- ERROR: Unknown options: #{bad_options.join(' ')}
100
-
101
- EOS
102
-
103
- show_usage
104
-
105
- exit
106
- end
107
- end
108
-
109
-
110
- def check_for(an_option)
111
- switches = [
112
- "--#{an_option}".gsub('?',''), # Dropping ? in case of a boolean
113
- "--no-#{an_option}".gsub('?',''),
114
- "-#{an_option.to_s[0]}" # SMELL: -v for both --verbose and --version
115
- ]
116
-
117
- process_option(an_option, switches)
118
- end
119
-
120
-
121
- def process_option(option_sym, switches)
122
- boolean = option_sym.to_s.end_with?('?')
123
-
124
- switches.each do |switch|
125
- if @arguments.include?(switch)
126
- index = @arguments.index(switch)
127
-
128
- if boolean
129
- @options[option_sym] = switch.include?('-no-') ? false : true
130
- @arguments.slice!(index,1)
131
- else
132
- if switch.include?('-no-')
133
- @option[option_sym] = nil
134
- @arguments.slice!(index,1)
135
- else
136
- @option[option_sym] = @arguments[index + 1]
137
- @arguments.slice!(index,2)
138
- end
139
- end
140
-
141
- break
142
- end
143
- end
144
- end
145
-
146
-
147
- def show_usage
148
- puts USAGE
149
- exit
150
- end
151
-
152
-
153
- def show_version
154
- puts VERSION
155
- exit
156
- end
157
-
158
-
159
- def call
160
- show_usage if help?
161
- show_version if version?
162
-
163
- prompt_id = get_prompt_id
164
-
165
- search_for_a_matching_prompt(prompt_id) unless existing_prompt?(prompt_id)
166
- process_prompt
167
- execute_and_log_command(build_command)
168
- end
169
-
170
-
171
- ####################################################
172
- private
173
-
174
- # Setup the AI CLI program with necessary variables
175
- def setup_cli_program
176
-
177
- ai_default_opts = "-m #{MODS_MODEL} --no-limit "
178
- ai_default_opts += "-f " if markdown?
179
- @ai_options = ai_default_opts.dup
180
-
181
-
182
- @ai_options += @extra_options.join(' ')
183
-
184
- @ai_command = "#{AI_CLI_PROGRAM} #{@ai_options} "
185
- end
186
-
187
-
188
- # Get the additional CLI arguments intended for the
189
- # backend gen-AI processor.
190
- def extract_extra_options
191
- extra_index = @arguments.index('--')
192
- if extra_index.nil?
193
- @extra_options = []
194
- else
195
- @extra_options = @arguments.slice!(extra_index..-1)[1..]
196
- end
197
- end
198
-
199
-
200
- # Fetch the first argument which should be the prompt id
201
- def get_prompt_id
202
- prompt_id = @arguments.shift
203
-
204
- # TODO: or maybe go to a search and select process
205
-
206
- abort("Please provide a prompt id") unless prompt_id
207
- prompt_id
208
- end
209
-
210
-
211
- # Check if a prompt with the given id already exists
212
- def existing_prompt?(prompt_id)
213
- @prompt = PromptManager::Prompt.get(id: prompt_id)
214
- true
215
- rescue ArgumentError
216
- false
217
- end
218
-
219
-
220
- # Process the prompt's associated keywords and parameters
221
- def process_prompt
222
- unless @prompt.keywords.empty?
223
- replace_keywords
224
- @prompt.build
225
- @prompt.save
226
- end
227
- end
228
-
229
-
230
- def replace_keywords
231
- print "\nQuit #{MY_NAME} with a CNTL-D or a CNTL-C\n\n"
232
-
233
- defaults = @prompt.parameters
234
-
235
- @prompt.keywords.each do |kw|
236
- defaults[kw] = keyword_value(kw, defaults[kw])
237
- end
238
-
239
- @prompt.parameters = defaults
240
- end
241
-
242
-
243
- # query the user for a value to the keyword allow the
244
- # reuse of the previous value shown as the default
245
- def keyword_value(kw, default)
246
- label = "Default: "
247
- puts "Parameter #{kw} ..."
248
- default_wrapped = default.wrap(indent: label.size)
249
- default_wrapped[0..label.size] = label
250
- puts default_wrapped
251
-
252
- begin
253
- a_string = Readline.readline("\n-=> ", false)
254
- rescue Interrupt
255
- a_string = nil
256
- end
257
-
258
- if a_string.nil?
259
- puts "okay. Come back soon."
260
- exit
261
- end
262
-
263
-
264
- puts
265
- a_string.empty? ? default : a_string
266
- end
267
-
268
-
269
- # Search for a prompt with a matching id or keyword
270
- def search_for_a_matching_prompt(prompt_id)
271
- # TODO: using the rgfzf version of the search_proc should only
272
- # return a single prompt_id
273
- found_prompts = PromptManager::Prompt.search(prompt_id)
274
- prompt_id = found_prompts.size == 1 ? found_prompts.first : handle_multiple_prompts(found_prompts, prompt_id)
275
- @prompt = PromptManager::Prompt.get(id: prompt_id)
276
- end
277
-
278
-
279
- def handle_multiple_prompts(found_these, while_looking_for_this)
280
- raise ArgumentError, "Argument is not an Array" unless found_these.is_a?(Array)
281
-
282
- # TODO: Make this a class constant for defaults; make the header content
283
- # a parameter so it can be varied.
284
- fzf_options = [
285
- "--tabstop=2", # 2 soaces for a tab
286
- "--header='Prompt IDs which contain: #{while_looking_for_this}\nPress ESC to cancel.'",
287
- "--header-first",
288
- "--prompt='Search term: '",
289
- '--delimiter :',
290
- "--preview 'cat $PROMPTS_DIR/{1}.txt'",
291
- "--preview-window=down:50%:wrap"
292
- ].join(' ')
293
-
294
-
295
- # Create a temporary file to hold the list of strings
296
- temp_file = Tempfile.new('fzf-input')
297
-
298
- begin
299
- # Write all strings to the temp file
300
- temp_file.puts(found_these)
301
- temp_file.close
302
-
303
- # Execute fzf command-line utility to allow selection
304
- selected = `cat #{temp_file.path} | fzf #{fzf_options}`.strip
305
-
306
- # Check if fzf actually returned a string; if not, return nil
307
- result = selected.empty? ? nil : selected
308
- ensure
309
- # Ensure that the tempfile is closed and unlinked
310
- temp_file.unlink
311
- end
312
-
313
- exit unless result
314
-
315
- result
316
- end
317
-
318
-
319
- # Build the command to interact with the AI CLI program
320
- def build_command
321
- command = @ai_command + %Q["#{@prompt.to_s}"]
322
-
323
- @arguments.each do |input_file|
324
- file_path = Pathname.new(input_file)
325
- abort("File does not exist: #{input_file}") unless file_path.exist?
326
- command += " < #{input_file}"
327
- end
328
-
329
- command
330
- end
331
-
332
-
333
- # Execute the command and log the results
334
- def execute_and_log_command(command)
335
- puts command if verbose?
336
- result = `#{command}`
337
- output.write result
338
-
339
- write_to_log(result) unless log.nil?
340
- end
341
-
342
-
343
- def write_to_log(answer)
344
- f = File.open(log, "ab")
345
-
346
- f.write <<~EOS
347
- =======================================
348
- == #{Time.now}
349
- == #{@prompt.path}
350
-
351
- PROMPT:
352
- #{@prompt}
353
-
354
- RESULT:
355
- #{answer}
356
-
357
- EOS
358
- end
15
+ def self.run(args=ARGV)
16
+ args = args.split(' ') if args.is_a?(String)
17
+ AIA::Main.new(args).call
359
18
  end
360
19
  end
361
20
 
362
-
363
- # Create an instance of the Main class and run the program
364
- AIA::Main.new.call if $PROGRAM_NAME == __FILE__
365
-
366
-
367
- __END__
368
-
369
-
370
- # TODO: Consider using this history process to preload the default
371
- # so that an up arrow will bring the previous answer into
372
- # the read buffer for line editing.
373
- # Instead of usin the .history file just push the default
374
- # value from the JSON file.
375
-
376
- while input = Readline.readline('> ', true)
377
- # Skip empty entries and duplicates
378
- if input.empty? || Readline::HISTORY.to_a[-2] == input
379
- Readline::HISTORY.pop
380
- end
381
- break if input == 'exit'
382
-
383
- # Do something with the input
384
- puts "You entered: #{input}"
385
-
386
- # Save the history in case you want to preserve it for the next sessions
387
- File.open('.history', 'a') { |f| f.puts(input) }
388
- end
389
-
390
- # Load history from file at the beginning of the program
391
- if File.exist?('.history')
392
- File.readlines('.history').each do |line|
393
- Readline::HISTORY.push(line.chomp)
394
- end
395
- end
396
-
397
-