aia 0.3.19 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/aia/prompt.rb ADDED
@@ -0,0 +1,267 @@
1
+ # lib/aia/prompt.rb
2
+
3
+ require 'reline'
4
+
5
+ class AIA::Prompt
6
+ #
7
+ # used when no prompt_id is provided but there
8
+ # are extra parameters that need to be passed
9
+ # to the backend. For example "aia -- --settings"
10
+ #
11
+ class Fake
12
+ def id = '_fake_'
13
+ def path = '_fake_'
14
+ def keywords = []
15
+ def directives = []
16
+ def to_s = ''
17
+ end
18
+
19
+ KW_HISTORY_MAX = 5
20
+
21
+ attr_reader :prompt
22
+
23
+ # setting build: false supports unit testing.
24
+ def initialize(build: true)
25
+ get_prompt
26
+
27
+ process_prompt if build
28
+ end
29
+
30
+
31
+ # Fetch the first argument which should be the prompt id
32
+ def get_prompt
33
+ prompt_id = AIA.config.arguments.shift
34
+
35
+ unless prompt_id
36
+ if AIA.config.extra.empty?
37
+ abort("Please provide a prompt id")
38
+ else
39
+ @prompt = Fake.new
40
+ return
41
+ end
42
+ end
43
+
44
+ search_for_a_matching_prompt(prompt_id) unless existing_prompt?(prompt_id)
45
+ edit_prompt if AIA.config.edit?
46
+ end
47
+
48
+
49
+ # Check if a prompt with the given id already exists. If so, use it.
50
+ def existing_prompt?(prompt_id)
51
+ @prompt = PromptManager::Prompt.get(id: prompt_id)
52
+ true
53
+ rescue ArgumentError
54
+ false
55
+ end
56
+
57
+
58
+ # Process the prompt's associated keywords and parameters
59
+ def process_prompt
60
+ unless @prompt.keywords.empty?
61
+ replace_keywords
62
+ @prompt.build
63
+ @prompt.save
64
+ end
65
+ end
66
+
67
+
68
+ def replace_keywords
69
+ puts
70
+ puts "ID: #{@prompt.id}"
71
+
72
+ show_prompt_without_comments
73
+
74
+ puts "\nPress up/down arrow to scroll through history."
75
+ puts "Type new input or edit the current input."
76
+ puts "Quit #{MY_NAME} with a CNTL-D or a CNTL-C"
77
+ puts
78
+ @prompt.keywords.each do |kw|
79
+ value = keyword_value(kw, @prompt.parameters[kw])
80
+
81
+ unless value.nil? || value.strip.empty?
82
+ value_inx = @prompt.parameters[kw].index(value)
83
+
84
+ if value_inx
85
+ @prompt.parameters[kw].delete_at(value_inx)
86
+ end
87
+
88
+ # The most recent value for this kw will always be
89
+ # in the last position
90
+ @prompt.parameters[kw] << value
91
+ @prompt.parameters[kw].shift if @prompt.parameters[kw].size > KW_HISTORY_MAX
92
+ end
93
+ end
94
+ end
95
+
96
+
97
+ # Function to setup the Reline history with a maximum depth
98
+ def setup_reline_history(max_history_size=5)
99
+ Reline::HISTORY.clear
100
+ # Reline::HISTORY.max_size = max_history_size
101
+ end
102
+
103
+
104
+ # Function to prompt the user with a question using reline
105
+ def ask_question_with_reline(prompt)
106
+ answer = Reline.readline(prompt)
107
+ Reline::HISTORY.push(answer) unless answer.nil? || Reline::HISTORY.to_a.include?(answer)
108
+ answer
109
+ rescue Interrupt
110
+ ''
111
+ end
112
+
113
+
114
+ # query the user for a value to the keyword allow the
115
+ # reuse of the previous value shown as the default
116
+ #
117
+ # FIXME: Ruby v3.3.0 drops readline in favor or reline
118
+ # internally it redirects "require 'readline'" to Reline
119
+ # puts lipstick on the pig so that you can continue to
120
+ # use the Readline namespace
121
+ #
122
+ def keyword_value(kw, history_array)
123
+ setup_reline_history
124
+
125
+ default = history_array.last
126
+
127
+ Array(history_array).each { |entry| Reline::HISTORY.push(entry) unless entry.nil? || entry.empty? }
128
+
129
+ puts "Parameter #{kw} ..."
130
+
131
+ if default.empty?
132
+ user_prompt = "\n-=> "
133
+ else
134
+ user_prompt = "\n(#{default}) -=>"
135
+ end
136
+
137
+ a_string = ask_question_with_reline(user_prompt)
138
+
139
+ if a_string.nil?
140
+ puts "okay. Come back soon."
141
+ exit
142
+ end
143
+
144
+ puts
145
+ a_string.empty? ? default : a_string
146
+ end
147
+
148
+
149
+ # Search for a prompt with a matching id or keyword
150
+ def search_for_a_matching_prompt(prompt_id)
151
+ # TODO: using the rgfzf version of the search_proc should only
152
+ # return a single prompt_id
153
+ found_prompts = PromptManager::Prompt.search(prompt_id)
154
+
155
+ if found_prompts.empty?
156
+ if AIA.config.edit?
157
+ create_prompt(prompt_id)
158
+ edit_prompt
159
+ else
160
+ abort <<~EOS
161
+
162
+ No prompts where found for: #{prompt_id}
163
+ To create a prompt with this ID use the --edit option
164
+ like this:
165
+ #{MY_NAME} #{prompt_id} --edit
166
+
167
+ EOS
168
+ end
169
+ else
170
+ prompt_id = 1 == found_prompts.size ? found_prompts.first : handle_multiple_prompts(found_prompts, prompt_id)
171
+ @prompt = PromptManager::Prompt.get(id: prompt_id)
172
+ end
173
+ end
174
+
175
+
176
+ def handle_multiple_prompts(found_these, while_looking_for_this)
177
+ raise ArgumentError, "Argument is not an Array" unless found_these.is_a?(Array)
178
+
179
+ # TODO: Make this a class constant for defaults; make the header content
180
+ # a parameter so it can be varied.
181
+ fzf_options = [
182
+ "--tabstop=2", # 2 soaces for a tab
183
+ "--header='Prompt IDs which contain: #{while_looking_for_this}\nPress ESC to cancel.'",
184
+ "--header-first",
185
+ "--prompt='Search term: '",
186
+ '--delimiter :',
187
+ "--preview 'cat $PROMPTS_DIR/{1}.txt'",
188
+ "--preview-window=down:50%:wrap"
189
+ ].join(' ')
190
+
191
+
192
+ # Create a temporary file to hold the list of strings
193
+ temp_file = Tempfile.new('fzf-input')
194
+
195
+ begin
196
+ # Write all strings to the temp file
197
+ temp_file.puts(found_these)
198
+ temp_file.close
199
+
200
+ # Execute fzf command-line utility to allow selection
201
+ selected = `cat #{temp_file.path} | fzf #{fzf_options}`.strip
202
+
203
+ # Check if fzf actually returned a string; if not, return nil
204
+ result = selected.empty? ? nil : selected
205
+ ensure
206
+ # Ensure that the tempfile is closed and unlinked
207
+ temp_file.unlink
208
+ end
209
+
210
+ exit unless result
211
+
212
+ result
213
+ end
214
+
215
+
216
+ def create_prompt(prompt_id)
217
+ @prompt = PromptManager::Prompt.create(id: prompt_id)
218
+ # TODO: consider a configurable prompt template
219
+ # ERB ???
220
+ end
221
+
222
+
223
+ def edit_prompt
224
+ # FIXME: replace with the editor from the configuration
225
+
226
+ @editor = AIA::Subl.new(
227
+ file: @prompt.path
228
+ )
229
+
230
+ @editor.run # blocks until file is closed
231
+
232
+ AIA.config[:edit?] = false # turn off the --edit switch
233
+
234
+ # reload the edited prompt
235
+ @prompt = PromptManager::Prompt.get(id: @prompt.id)
236
+ end
237
+
238
+
239
+ def show_prompt_without_comments
240
+ puts remove_comments.wrap(indent: 4)
241
+ end
242
+
243
+
244
+ # removes comments and directives
245
+ def remove_comments
246
+ lines = @prompt.text
247
+ .split("\n")
248
+ .reject{|a_line|
249
+ a_line.strip.start_with?('#') ||
250
+ a_line.strip.start_with?('//')
251
+ }
252
+
253
+ # Remove empty lines at the start of the prompt
254
+ #
255
+ lines = lines.drop_while(&:empty?)
256
+
257
+ # Drop all the lines at __END__ and after
258
+ #
259
+ logical_end_inx = lines.index("__END__")
260
+
261
+ if logical_end_inx
262
+ lines[0...logical_end_inx] # NOTE: ... means to not include last index
263
+ else
264
+ lines
265
+ end.join("\n")
266
+ end
267
+ end
@@ -0,0 +1,76 @@
1
+ # aia/lib/aia/tools/backend_common.rb
2
+
3
+ # Used by both the AIA::Mods and AIA::Sgpt classes
4
+
5
+ module AIA::BackendCommon
6
+ attr_accessor :command, :text, :files, :parameters
7
+
8
+ def initialize(text: "", files: [])
9
+ @text = text
10
+ @files = files
11
+ @parameters = self.class::DEFAULT_PARAMETERS.dup
12
+ build_command
13
+ end
14
+
15
+ def sanitize(input)
16
+ Shellwords.escape(input)
17
+ end
18
+ def build_command
19
+ @parameters += " --model #{AIA.config.model} " if AIA.config.model
20
+ @parameters += AIA.config.extra
21
+
22
+ set_parameter_from_directives
23
+
24
+ @command = "#{meta.name} #{@parameters} "
25
+ @command += sanitize(text)
26
+
27
+ puts @command if AIA.config.debug?
28
+
29
+ @command
30
+ end
31
+
32
+ def set_parameter_from_directives
33
+ AIA.config.directives.each do |entry|
34
+ directive, value = entry
35
+ if self.class::DIRECTIVES.include?(directive)
36
+ @parameters += " --#{directive} #{sanitize(value)}" unless @parameters.include?(directive)
37
+ end
38
+ end
39
+ end
40
+
41
+ def run
42
+ case @files.size
43
+ when 0
44
+ @result = `#{build_command}`
45
+ when 1
46
+ @result = `#{build_command} < #{@files.first}`
47
+ else
48
+ create_temp_file_with_contexts
49
+ run_with_temp_file
50
+ clean_up_temp_file
51
+ end
52
+
53
+ @result
54
+ end
55
+
56
+ def create_temp_file_with_contexts
57
+ @temp_file = Tempfile.new("#{self.class::COMMAND_NAME}-context")
58
+
59
+ @files.each do |file|
60
+ content = File.read(file)
61
+ @temp_file.write(content)
62
+ @temp_file.write("\n")
63
+ end
64
+
65
+ @temp_file.close
66
+ end
67
+
68
+ def run_with_temp_file
69
+ command = "#{build_command} < #{@temp_file.path}"
70
+ @result = `#{command}`
71
+ end
72
+
73
+ def clean_up_temp_file
74
+ @temp_file.unlink if @temp_file
75
+ end
76
+ end
@@ -4,19 +4,21 @@
4
4
 
5
5
 
6
6
  class AIA::Editor < AIA::Tools
7
+
8
+ meta(
9
+ name: 'editor',
10
+ role: :editor,
11
+ desc: "Your default system $EDITOR",
12
+ url: "unknown",
13
+ install: "should already be installed",
14
+ )
15
+
7
16
  DEFAULT_PARAMETERS = ""
8
17
 
9
18
  attr_accessor :command
10
19
 
11
20
 
12
- def initialize(file: "")
13
- super
14
-
15
- @role = :editor
16
- @description = "Your default system $EDITOR"
17
- @url = "unknown"
18
- @install = "should already be installed"
19
-
21
+ def initialize(file: "")
20
22
  @file = file
21
23
 
22
24
  discover_editor
@@ -39,7 +41,7 @@ class AIA::Editor < AIA::Tools
39
41
 
40
42
 
41
43
  def build_command
42
- @command = "#{name} #{DEFAULT_PARAMETERS} #{@file}"
44
+ @command = "#{meta.name} #{DEFAULT_PARAMETERS} #{@file}"
43
45
  end
44
46
 
45
47
 
@@ -1,115 +1,81 @@
1
1
  # lib/aia/tools/mods.rb
2
2
 
3
- class AIA::Mods < AIA::Tools
4
- DEFAULT_PARAMETERS = [
5
- "--no-limit" # no limit on input context
6
- ].join(' ').freeze
7
-
8
- attr_accessor :command, :extra_options, :text, :files
3
+ require_relative 'backend_common'
9
4
 
10
- # TODO: put the prompt text to be resolved into a
11
- # temporary text file then cat that file into mods.
12
- # This will keep from polluting the CLI history with
13
- # lots of text
5
+ class AIA::Mods < AIA::Tools
6
+ include AIA::BackendCommon
14
7
 
8
+ meta(
9
+ name: 'mods',
10
+ role: :backend,
11
+ desc: 'AI on the command-line',
12
+ url: 'https://github.com/charmbracelet/mods',
13
+ install: 'brew install mods',
14
+ )
15
15
 
16
- def initialize(
17
- extra_options: "", # everything after -- on command line
18
- text: "", # prompt text after keyword replacement
19
- files: [] # context file paths (Array of Pathname)
20
- )
21
- super
22
- @role = :gen_ai
23
- @description = 'AI on the command-line'
24
- @url = 'https://github.com/charmbracelet/mods'
25
16
 
26
- @extra_options = extra_options
27
- @text = text
28
- @files = files
29
-
30
- build_command
31
- end
32
-
33
-
34
- def build_command
35
- parameters = DEFAULT_PARAMETERS.dup + " "
36
- parameters += "-f " if ::AIA.config.markdown?
37
- parameters += "-m #{AIA.config.model} " if ::AIA.config.model
38
- parameters += @extra_options
39
- @command = "mods #{parameters} "
40
- @command += %Q["#{@text}"] # TODO: consider using the pipeline
41
-
42
- @files.each {|f| @command += " < #{f}" }
43
-
44
- @command
45
- end
17
+ DEFAULT_PARAMETERS = [
18
+ # "--no-cache", # do not save prompt and response
19
+ "--no-limit" # no limit on input context
20
+ ].join(' ').freeze
46
21
 
47
22
 
48
- def run
49
- `#{command}`
50
- end
23
+ DIRECTIVES = %w[
24
+ api
25
+ fanciness
26
+ http-proxy
27
+ max-retries
28
+ max-tokens
29
+ no-cache
30
+ no-limit
31
+ quiet
32
+ raw
33
+ status-text
34
+ temp
35
+ title
36
+ topp
37
+ ]
51
38
  end
52
39
 
53
40
  __END__
54
41
 
55
42
 
56
- # Execute the command and log the results
57
- def send_prompt_to_external_command
58
- command = build_command
59
-
60
- puts command if verbose?
61
- @result = `#{command}`
62
-
63
- if @output.nil?
64
- puts @result
65
- else
66
- @output.write @result
67
- end
68
-
69
- @result
70
- end
71
-
72
-
73
-
74
-
75
-
76
43
  ##########################################################
77
44
 
78
-
79
45
  GPT on the command line. Built for pipelines.
80
46
 
81
47
  Usage:
82
48
  mods [OPTIONS] [PREFIX TERM]
83
49
 
84
50
  Options:
85
- -m, --model Default model (gpt-3.5-turbo, gpt-4, ggml-gpt4all-j...).
86
- -a, --api OpenAI compatible REST API (openai, localai).
87
- -x, --http-proxy HTTP proxy to use for API requests.
88
- -f, --format Ask for the response to be formatted as markdown unless otherwise set.
89
- -r, --raw Render output as raw text when connected to a TTY.
90
- -P, --prompt Include the prompt from the arguments and stdin, truncate stdin to specified number of lines.
91
- -p, --prompt-args Include the prompt from the arguments in the response.
92
- -c, --continue Continue from the last response or a given save title.
93
- -C, --continue-last Continue from the last response.
94
- -l, --list Lists saved conversations.
95
- -t, --title Saves the current conversation with the given title.
96
- -d, --delete Deletes a saved conversation with the given title or ID.
97
- -s, --show Show a saved conversation with the given title or ID.
98
- -S, --show-last Show a the last saved conversation.
99
- -q, --quiet Quiet mode (hide the spinner while loading and stderr messages for success).
100
- -h, --help Show help and exit.
101
- -v, --version Show version and exit.
102
- --max-retries Maximum number of times to retry API calls.
103
- --no-limit Turn off the client-side limit on the size of the input into the model.
104
- --max-tokens Maximum number of tokens in response.
105
- --temp Temperature (randomness) of results, from 0.0 to 2.0.
106
- --topp TopP, an alternative to temperature that narrows response, from 0.0 to 1.0.
107
- --fanciness Your desired level of fanciness.
108
- --status-text Text to show while generating.
109
- --no-cache Disables caching of the prompt/response.
110
- --reset-settings Backup your old settings file and reset everything to the defaults.
111
- --settings Open settings in your $EDITOR.
112
- --dirs Print the directories in which mods store its data
51
+ -m, --model Default model (gpt-3.5-turbo, gpt-4, ggml-gpt4all-j...).
52
+ -a, --api OpenAI compatible REST API (openai, localai).
53
+ -x, --http-proxy HTTP proxy to use for API requests.
54
+ -f, --format Ask for the response to be formatted as markdown unless otherwise set.
55
+ -r, --raw Render output as raw text when connected to a TTY.
56
+ -P, --prompt Include the prompt from the arguments and stdin, truncate stdin to specified number of lines.
57
+ -p, --prompt-args Include the prompt from the arguments in the response.
58
+ -c, --continue Continue from the last response or a given save title.
59
+ -C, --continue-last Continue from the last response.
60
+ -l, --list Lists saved conversations.
61
+ -t, --title Saves the current conversation with the given title.
62
+ -d, --delete Deletes a saved conversation with the given title or ID.
63
+ -s, --show Show a saved conversation with the given title or ID.
64
+ -S, --show-last Show a the last saved conversation.
65
+ -q, --quiet Quiet mode (hide the spinner while loading and stderr messages for success).
66
+ -h, --help Show help and exit.
67
+ -v, --version Show version and exit.
68
+ --max-retries Maximum number of times to retry API calls.
69
+ --no-limit Turn off the client-side limit on the size of the input into the model.
70
+ --max-tokens Maximum number of tokens in response.
71
+ --temp Temperature (randomness) of results, from 0.0 to 2.0.
72
+ --topp TopP, an alternative to temperature that narrows response, from 0.0 to 1.0.
73
+ --fanciness Your desired level of fanciness.
74
+ --status-text Text to show while generating.
75
+ --no-cache Disables caching of the prompt/response.
76
+ --reset-settings Backup your old settings file and reset everything to the defaults.
77
+ --settings Open settings in your $EDITOR.
78
+ --dirs Print the directories in which mods store its data
113
79
 
114
80
  Example:
115
81
  # Editorialize your video files
@@ -1,13 +1,34 @@
1
1
  # lib/aia/tools/sgpt.rb
2
2
 
3
+ require_relative 'backend_common'
4
+
3
5
  class AIA::Sgpt < AIA::Tools
4
- def initialize
5
- super
6
- @role = :backend
7
- @desc = "shell-gpt"
8
- @url = "https://github.com/TheR1D/shell_gpt"
9
- @install = "pip install shell-gpt"
10
- end
6
+ include AIA::BackendCommon
7
+
8
+ meta(
9
+ name: 'sgpt',
10
+ role: :backend,
11
+ desc: "shell-gpt",
12
+ url: "https://github.com/TheR1D/shell_gpt",
13
+ install: "pip install shell-gpt",
14
+ )
15
+
16
+
17
+ DEFAULT_PARAMETERS = [
18
+ # "--verbose", # enable verbose logging (if applicable)
19
+ # Add default parameters here
20
+ ].join(' ').freeze
21
+
22
+ DIRECTIVES = %w[
23
+ model
24
+ temperature
25
+ max_tokens
26
+ top_p
27
+ frequency_penalty
28
+ presence_penalty
29
+ stop_sequence
30
+ api_key
31
+ ]
11
32
  end
12
33
 
13
34
  __END__
@@ -1,6 +1,16 @@
1
1
  # lib/aia/tools/subl.rb
2
2
 
3
3
  class AIA::Subl < AIA::Tools
4
+
5
+ meta(
6
+ name: 'subl',
7
+ role: :editor,
8
+ desc: "Sublime Text Editor",
9
+ url: "https://www.sublimetext.com/",
10
+ install: "echo 'Download from website'",
11
+ )
12
+
13
+
4
14
  DEFAULT_PARAMETERS = [
5
15
  "--new-window", # Open a new window
6
16
  "--wait", # Wait for the files to be closed before returning
@@ -10,13 +20,6 @@ class AIA::Subl < AIA::Tools
10
20
 
11
21
 
12
22
  def initialize(file: "")
13
- super
14
-
15
- @role = :editor
16
- @desc = "Sublime Text Editor"
17
- @url = "https://www.sublimetext.com/"
18
- @install = "echo 'Download from website'"
19
-
20
23
  @file = file
21
24
 
22
25
  build_command
@@ -24,7 +27,7 @@ class AIA::Subl < AIA::Tools
24
27
 
25
28
 
26
29
  def build_command
27
- @command = "#{name} #{DEFAULT_PARAMETERS} #{@file}"
30
+ @command = "#{meta.name} #{DEFAULT_PARAMETERS} #{@file}"
28
31
  end
29
32
 
30
33
 
data/lib/aia/tools/vim.rb CHANGED
@@ -1,6 +1,15 @@
1
1
  # lib/aia/tools/vim.rb
2
2
 
3
3
  class AIA::Vim < AIA::Tools
4
+
5
+ meta(
6
+ name: 'vim',
7
+ role: :editor,
8
+ desc: "Vi IMproved (VIM)",
9
+ url: "https://www.vim.org",
10
+ install: "brew install vim",
11
+ )
12
+
4
13
  DEFAULT_PARAMETERS = [
5
14
  " ", # no parameters
6
15
  ].join(' ')
@@ -9,13 +18,6 @@ class AIA::Vim < AIA::Tools
9
18
 
10
19
 
11
20
  def initialize(file: "")
12
- super
13
-
14
- @role = :editor
15
- @description = "Vi IMproved (VIM)"
16
- @url = "https://www.vim.org"
17
- @install = "brew install vim"
18
-
19
21
  @file = file
20
22
 
21
23
  build_command
@@ -23,7 +25,7 @@ class AIA::Vim < AIA::Tools
23
25
 
24
26
 
25
27
  def build_command
26
- @command = "#{name} #{DEFAULT_PARAMETERS} #{@file}"
28
+ @command = "#{meta.name} #{DEFAULT_PARAMETERS} #{@file}"
27
29
  end
28
30
 
29
31