aia 0.3.19 → 0.4.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.
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