aia 0.0.4 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
-