aia 0.3.20 → 0.4.2

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