aia 0.3.20 → 0.4.2

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/main.rb CHANGED
@@ -4,7 +4,8 @@ module AIA ; end
4
4
 
5
5
  require_relative 'config'
6
6
  require_relative 'cli'
7
- require_relative 'prompt_processing'
7
+ require_relative 'directives'
8
+ require_relative 'prompt'
8
9
  require_relative 'logging'
9
10
  require_relative 'tools'
10
11
 
@@ -12,26 +13,58 @@ require_relative 'tools'
12
13
  # of a single class.
13
14
 
14
15
  class AIA::Main
15
- include AIA::PromptProcessing
16
16
 
17
- attr_accessor :logger, :tools
17
+ attr_accessor :logger, :tools, :backend
18
+
19
+ def initialize(args= ARGV)
20
+ AIA::Tools.load_tools
18
21
 
19
- def initialize(args= ARGV)
20
22
  AIA::Cli.new(args)
21
23
 
22
24
  @logger = AIA::Logging.new(AIA.config.log_file)
23
- AIA::Tools.load_tools
25
+
26
+ @logger.info(AIA.config) if AIA.config.debug? || AIA.config.verbose?
27
+
28
+ @prompt = AIA::Prompt.new.prompt
29
+
30
+ @engine = AIA::Directives.new(prompt: @prompt)
24
31
 
25
32
  # TODO: still should verify that the tools are ion the $PATH
26
33
  # tools.class.verify_tools
27
34
  end
28
35
 
29
36
 
37
+ def speak(what)
38
+ return unless AIA.config.speak?
39
+ # MacOS uses the say command
40
+ system "say #{Shellwords.escape(what)}"
41
+ end
42
+
43
+
44
+ # Function to setup the Reline history with a maximum depth
45
+ def setup_reline_history(max_history_size=5)
46
+ Reline::HISTORY.clear
47
+ # Reline::HISTORY.max_size = max_history_size
48
+ end
49
+
50
+
51
+ # Function to prompt the user with a question using reline
52
+ def ask_question_with_reline(prompt)
53
+ answer = Reline.readline(prompt)
54
+ Reline::HISTORY.push(answer) unless answer.nil? || Reline::HISTORY.to_a.include?(answer)
55
+ answer
56
+ rescue Interrupt
57
+ ''
58
+ end
59
+
60
+
30
61
  def call
31
- get_prompt
32
- process_prompt
33
-
34
- # send_prompt_to_external_command
62
+ @engine.execute_my_directives
63
+
64
+ if AIA.config.chat?
65
+ AIA.config.output_file = STDOUT
66
+ AIA.config.extra = "--quiet" if 'mods' == AIA.config.backend
67
+ end
35
68
 
36
69
  # TODO: the context_files left in the @arguments array
37
70
  # should be verified BEFORE asking the user for a
@@ -56,15 +89,63 @@ class AIA::Main
56
89
 
57
90
  abort "backend not found: #{AIA.config.backend}" if backend_klass.nil?
58
91
 
59
- backend = backend_klass.new(
60
- text: @prompt.to_s,
61
- files: AIA.config.arguments # FIXME: want validated context files
62
- )
92
+ the_prompt = @prompt.to_s
93
+
94
+ if AIA.config.terse?
95
+ the_prompt.prepend "Be terse in your response. "
96
+ end
97
+
98
+ @backend = backend_klass.new(
99
+ text: the_prompt,
100
+ files: AIA.config.arguments # FIXME: want validated context files
101
+ )
102
+
63
103
 
64
104
  result = backend.run
65
105
 
66
106
  AIA.config.output_file.write result
67
107
 
68
108
  logger.prompt_result(@prompt, result)
109
+
110
+
111
+ if AIA.config.chat?
112
+ setup_reline_history
113
+ speak result
114
+ lets_chat
115
+ end
116
+ end
117
+
118
+
119
+ def lets_chat
120
+ if 'mods' == AIA.config.backend
121
+ AIA.config.extra += " -C"
122
+ end
123
+
124
+ backend.text = ask_question_with_reline("\nFollow Up: ")
125
+
126
+ until backend.text.empty?
127
+ if AIA.config.terse?
128
+ backend.text.prepend "Be terse in your response. "
129
+ end
130
+
131
+ logger.info "Follow Up: #{backend.text}"
132
+ response = backend.run
133
+
134
+ speak response
135
+
136
+ puts "\nResponse: #{response}"
137
+ logger.info "Response: #{backend.run}"
138
+
139
+ # TODO: Allow user to enter a directive; loop
140
+ # until answer is not a directive
141
+ #
142
+ # while !directive do
143
+ backend.text = ask_question_with_reline("\nFollow Up: ")
144
+
145
+ speak backend.text
146
+
147
+ # execute the directive
148
+ # end
149
+ end
69
150
  end
70
151
  end
data/lib/aia/prompt.rb ADDED
@@ -0,0 +1,279 @@
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
+ if AIA.config.role.empty?
26
+ @role = nil
27
+ else
28
+ AIA.config.arguments.prepend AIA.config.role
29
+ get_prompt
30
+ @role = @prompt.dup
31
+ end
32
+
33
+ get_prompt
34
+
35
+ unless @role.nil?
36
+ @prompt.text.prepend @role.text
37
+ end
38
+
39
+ process_prompt if build
40
+ end
41
+
42
+
43
+ # Fetch the first argument which should be the prompt id
44
+ def get_prompt
45
+ prompt_id = AIA.config.arguments.shift
46
+
47
+ unless prompt_id
48
+ if AIA.config.extra.empty?
49
+ abort("Please provide a prompt id")
50
+ else
51
+ @prompt = Fake.new
52
+ return
53
+ end
54
+ end
55
+
56
+ search_for_a_matching_prompt(prompt_id) unless existing_prompt?(prompt_id)
57
+ edit_prompt if AIA.config.edit?
58
+ end
59
+
60
+
61
+ # Check if a prompt with the given id already exists. If so, use it.
62
+ def existing_prompt?(prompt_id)
63
+ @prompt = PromptManager::Prompt.get(id: prompt_id)
64
+ true
65
+ rescue ArgumentError
66
+ false
67
+ end
68
+
69
+
70
+ # Process the prompt's associated keywords and parameters
71
+ def process_prompt
72
+ unless @prompt.keywords.empty?
73
+ replace_keywords
74
+ @prompt.build
75
+ @prompt.save
76
+ end
77
+ end
78
+
79
+
80
+ def replace_keywords
81
+ puts
82
+ puts "ID: #{@prompt.id}"
83
+
84
+ show_prompt_without_comments
85
+
86
+ puts "\nPress up/down arrow to scroll through history."
87
+ puts "Type new input or edit the current input."
88
+ puts "Quit #{MY_NAME} with a CNTL-D or a CNTL-C"
89
+ puts
90
+ @prompt.keywords.each do |kw|
91
+ value = keyword_value(kw, @prompt.parameters[kw])
92
+
93
+ unless value.nil? || value.strip.empty?
94
+ value_inx = @prompt.parameters[kw].index(value)
95
+
96
+ if value_inx
97
+ @prompt.parameters[kw].delete_at(value_inx)
98
+ end
99
+
100
+ # The most recent value for this kw will always be
101
+ # in the last position
102
+ @prompt.parameters[kw] << value
103
+ @prompt.parameters[kw].shift if @prompt.parameters[kw].size > KW_HISTORY_MAX
104
+ end
105
+ end
106
+ end
107
+
108
+
109
+ # Function to setup the Reline history with a maximum depth
110
+ def setup_reline_history(max_history_size=5)
111
+ Reline::HISTORY.clear
112
+ # Reline::HISTORY.max_size = max_history_size
113
+ end
114
+
115
+
116
+ # Function to prompt the user with a question using reline
117
+ def ask_question_with_reline(prompt)
118
+ answer = Reline.readline(prompt)
119
+ Reline::HISTORY.push(answer) unless answer.nil? || Reline::HISTORY.to_a.include?(answer)
120
+ answer
121
+ rescue Interrupt
122
+ ''
123
+ end
124
+
125
+
126
+ # query the user for a value to the keyword allow the
127
+ # reuse of the previous value shown as the default
128
+ #
129
+ # FIXME: Ruby v3.3.0 drops readline in favor or reline
130
+ # internally it redirects "require 'readline'" to Reline
131
+ # puts lipstick on the pig so that you can continue to
132
+ # use the Readline namespace
133
+ #
134
+ def keyword_value(kw, history_array)
135
+ setup_reline_history
136
+
137
+ default = history_array.last
138
+
139
+ Array(history_array).each { |entry| Reline::HISTORY.push(entry) unless entry.nil? || entry.empty? }
140
+
141
+ puts "Parameter #{kw} ..."
142
+
143
+ if default&.empty?
144
+ user_prompt = "\n-=> "
145
+ else
146
+ user_prompt = "\n(#{default}) -=>"
147
+ end
148
+
149
+ a_string = ask_question_with_reline(user_prompt)
150
+
151
+ if a_string.nil?
152
+ puts "okay. Come back soon."
153
+ exit
154
+ end
155
+
156
+ puts
157
+ a_string.empty? ? default : a_string
158
+ end
159
+
160
+
161
+ # Search for a prompt with a matching id or keyword
162
+ def search_for_a_matching_prompt(prompt_id)
163
+ # TODO: using the rgfzf version of the search_proc should only
164
+ # return a single prompt_id
165
+ found_prompts = PromptManager::Prompt.search(prompt_id)
166
+
167
+ if found_prompts.empty?
168
+ if AIA.config.edit?
169
+ create_prompt(prompt_id)
170
+ edit_prompt
171
+ else
172
+ abort <<~EOS
173
+
174
+ No prompts where found for: #{prompt_id}
175
+ To create a prompt with this ID use the --edit option
176
+ like this:
177
+ #{MY_NAME} #{prompt_id} --edit
178
+
179
+ EOS
180
+ end
181
+ else
182
+ prompt_id = 1 == found_prompts.size ? found_prompts.first : handle_multiple_prompts(found_prompts, prompt_id)
183
+ @prompt = PromptManager::Prompt.get(id: prompt_id)
184
+ end
185
+ end
186
+
187
+
188
+ def handle_multiple_prompts(found_these, while_looking_for_this)
189
+ raise ArgumentError, "Argument is not an Array" unless found_these.is_a?(Array)
190
+
191
+ # TODO: Make this a class constant for defaults; make the header content
192
+ # a parameter so it can be varied.
193
+ fzf_options = [
194
+ "--tabstop=2", # 2 soaces for a tab
195
+ "--header='Prompt IDs which contain: #{while_looking_for_this}\nPress ESC to cancel.'",
196
+ "--header-first",
197
+ "--prompt='Search term: '",
198
+ '--delimiter :',
199
+ "--preview 'cat $PROMPTS_DIR/{1}.txt'",
200
+ "--preview-window=down:50%:wrap"
201
+ ].join(' ')
202
+
203
+
204
+ # Create a temporary file to hold the list of strings
205
+ temp_file = Tempfile.new('fzf-input')
206
+
207
+ begin
208
+ # Write all strings to the temp file
209
+ temp_file.puts(found_these)
210
+ temp_file.close
211
+
212
+ # Execute fzf command-line utility to allow selection
213
+ selected = `cat #{temp_file.path} | fzf #{fzf_options}`.strip
214
+
215
+ # Check if fzf actually returned a string; if not, return nil
216
+ result = selected.empty? ? nil : selected
217
+ ensure
218
+ # Ensure that the tempfile is closed and unlinked
219
+ temp_file.unlink
220
+ end
221
+
222
+ exit unless result
223
+
224
+ result
225
+ end
226
+
227
+
228
+ def create_prompt(prompt_id)
229
+ @prompt = PromptManager::Prompt.create(id: prompt_id)
230
+ # TODO: consider a configurable prompt template
231
+ # ERB ???
232
+ end
233
+
234
+
235
+ def edit_prompt
236
+ # FIXME: replace with the editor from the configuration
237
+
238
+ @editor = AIA::Subl.new(
239
+ file: @prompt.path
240
+ )
241
+
242
+ @editor.run # blocks until file is closed
243
+
244
+ AIA.config[:edit?] = false # turn off the --edit switch
245
+
246
+ # reload the edited prompt
247
+ @prompt = PromptManager::Prompt.get(id: @prompt.id)
248
+ end
249
+
250
+
251
+ def show_prompt_without_comments
252
+ puts remove_comments.wrap(indent: 4)
253
+ end
254
+
255
+
256
+ # removes comments and directives
257
+ def remove_comments
258
+ lines = @prompt.text
259
+ .split("\n")
260
+ .reject{|a_line|
261
+ a_line.strip.start_with?('#') ||
262
+ a_line.strip.start_with?('//')
263
+ }
264
+
265
+ # Remove empty lines at the start of the prompt
266
+ #
267
+ lines = lines.drop_while(&:empty?)
268
+
269
+ # Drop all the lines at __END__ and after
270
+ #
271
+ logical_end_inx = lines.index("__END__")
272
+
273
+ if logical_end_inx
274
+ lines[0...logical_end_inx] # NOTE: ... means to not include last index
275
+ else
276
+ lines
277
+ end.join("\n")
278
+ end
279
+ 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