aia 0.3.20 → 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
@@ -1,6 +1,9 @@
1
1
  # lib/aia/tools/mods.rb
2
2
 
3
+ require_relative 'backend_common'
4
+
3
5
  class AIA::Mods < AIA::Tools
6
+ include AIA::BackendCommon
4
7
 
5
8
  meta(
6
9
  name: 'mods',
@@ -12,141 +15,67 @@ class AIA::Mods < AIA::Tools
12
15
 
13
16
 
14
17
  DEFAULT_PARAMETERS = [
18
+ # "--no-cache", # do not save prompt and response
15
19
  "--no-limit" # no limit on input context
16
20
  ].join(' ').freeze
17
21
 
18
- attr_accessor :command, :text, :files
19
-
20
-
21
- def initialize(
22
- text: "", # prompt text after keyword replacement
23
- files: [] # context file paths (Array of Pathname)
24
- )
25
-
26
- @text = text
27
- @files = files
28
-
29
- build_command
30
- end
31
-
32
-
33
- def sanitize(input)
34
- Shellwords.escape(input)
35
- end
36
-
37
-
38
- def build_command
39
- parameters = DEFAULT_PARAMETERS.dup + " "
40
- parameters += "-f " if AIA.config.markdown?
41
- parameters += "-m #{AIA.config.model} " if AIA.config.model
42
- parameters += AIA.config.extra
43
- @command = "mods #{parameters} "
44
- @command += sanitize(@text)
45
-
46
- # context = @files.join(' ')
47
- #
48
- # unless context.empty?
49
- # if @files.size > 1
50
- # # FIXME: This syntax breaks mods which does not know how
51
- # # to read the temporary file descriptor created
52
- # # by the shell
53
- # @command += " <(cat #{context})"
54
- # else
55
- # @command += " < #{context}"
56
- # end
57
- # end
58
-
59
- @command
60
- end
61
-
62
22
 
63
- def run
64
- case @files.size
65
- when 0
66
- @result = `#{build_command}`
67
- when 1
68
- @result = `#{build_command} < #{@files.first}`
69
- else
70
- create_temp_file_with_contexts
71
- run_mods_with_temp_file
72
- clean_up_temp_file
73
- end
74
-
75
- @result
76
- end
77
-
78
-
79
- # Create a temporary file that concatenates all contexts,
80
- # to be used as STDIN for the 'mods' utility
81
- def create_temp_file_with_contexts
82
- @temp_file = Tempfile.new('mods-context')
83
-
84
- @files.each do |file|
85
- content = File.read(file)
86
- @temp_file.write(content)
87
- @temp_file.write("\n")
88
- end
89
-
90
- @temp_file.close
91
- end
92
-
93
-
94
- # Run 'mods' with the temporary file as STDIN
95
- def run_mods_with_temp_file
96
- command = "#{build_command} < #{@temp_file.path}"
97
- @result = `#{command}`
98
- end
99
-
100
-
101
- # Clean up the temporary file after use
102
- def clean_up_temp_file
103
- @temp_file.unlink if @temp_file
104
- 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
+ ]
105
38
  end
106
39
 
107
40
  __END__
108
41
 
109
42
 
110
-
111
-
112
-
113
43
  ##########################################################
114
44
 
115
-
116
45
  GPT on the command line. Built for pipelines.
117
46
 
118
47
  Usage:
119
48
  mods [OPTIONS] [PREFIX TERM]
120
49
 
121
50
  Options:
122
- -m, --model Default model (gpt-3.5-turbo, gpt-4, ggml-gpt4all-j...).
123
- -a, --api OpenAI compatible REST API (openai, localai).
124
- -x, --http-proxy HTTP proxy to use for API requests.
125
- -f, --format Ask for the response to be formatted as markdown unless otherwise set.
126
- -r, --raw Render output as raw text when connected to a TTY.
127
- -P, --prompt Include the prompt from the arguments and stdin, truncate stdin to specified number of lines.
128
- -p, --prompt-args Include the prompt from the arguments in the response.
129
- -c, --continue Continue from the last response or a given save title.
130
- -C, --continue-last Continue from the last response.
131
- -l, --list Lists saved conversations.
132
- -t, --title Saves the current conversation with the given title.
133
- -d, --delete Deletes a saved conversation with the given title or ID.
134
- -s, --show Show a saved conversation with the given title or ID.
135
- -S, --show-last Show a the last saved conversation.
136
- -q, --quiet Quiet mode (hide the spinner while loading and stderr messages for success).
137
- -h, --help Show help and exit.
138
- -v, --version Show version and exit.
139
- --max-retries Maximum number of times to retry API calls.
140
- --no-limit Turn off the client-side limit on the size of the input into the model.
141
- --max-tokens Maximum number of tokens in response.
142
- --temp Temperature (randomness) of results, from 0.0 to 2.0.
143
- --topp TopP, an alternative to temperature that narrows response, from 0.0 to 1.0.
144
- --fanciness Your desired level of fanciness.
145
- --status-text Text to show while generating.
146
- --no-cache Disables caching of the prompt/response.
147
- --reset-settings Backup your old settings file and reset everything to the defaults.
148
- --settings Open settings in your $EDITOR.
149
- --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
150
79
 
151
80
  Example:
152
81
  # Editorialize your video files
@@ -1,6 +1,9 @@
1
1
  # lib/aia/tools/sgpt.rb
2
2
 
3
+ require_relative 'backend_common'
4
+
3
5
  class AIA::Sgpt < AIA::Tools
6
+ include AIA::BackendCommon
4
7
 
5
8
  meta(
6
9
  name: 'sgpt',
@@ -10,9 +13,22 @@ class AIA::Sgpt < AIA::Tools
10
13
  install: "pip install shell-gpt",
11
14
  )
12
15
 
13
- def initialize
14
- # TODO: something
15
- end
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
+ ]
16
32
  end
17
33
 
18
34
  __END__
data/lib/aia/tools.rb CHANGED
@@ -5,6 +5,8 @@ require 'hashie'
5
5
  class AIA::Tools
6
6
  @@catalog = []
7
7
 
8
+ def meta = self.class::meta
9
+
8
10
  class << self
9
11
  def inherited(subclass)
10
12
  subclass_meta = Hashie::Mash.new(klass: subclass)
data/lib/aia.rb CHANGED
@@ -5,7 +5,7 @@ include DebugMe
5
5
 
6
6
  require 'hashie'
7
7
  require 'pathname'
8
- require 'readline'
8
+ require 'reline'
9
9
  require 'shellwords'
10
10
  require 'tempfile'
11
11