aia 0.5.17 → 0.8.0

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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +1 -0
  3. data/.version +1 -2
  4. data/CHANGELOG.md +61 -22
  5. data/README.md +387 -227
  6. data/Rakefile +16 -5
  7. data/_notes.txt +231 -0
  8. data/bin/aia +3 -2
  9. data/examples/README.md +140 -0
  10. data/examples/headlines +21 -0
  11. data/justfile +16 -3
  12. data/lib/aia/ai_client_adapter.rb +210 -0
  13. data/lib/aia/chat_processor_service.rb +120 -0
  14. data/lib/aia/config.rb +473 -4
  15. data/lib/aia/context_manager.rb +58 -0
  16. data/lib/aia/directive_processor.rb +267 -0
  17. data/lib/aia/{tools/fzf.rb → fzf.rb} +9 -17
  18. data/lib/aia/history_manager.rb +85 -0
  19. data/lib/aia/prompt_handler.rb +178 -0
  20. data/lib/aia/session.rb +215 -0
  21. data/lib/aia/shell_command_executor.rb +109 -0
  22. data/lib/aia/ui_presenter.rb +110 -0
  23. data/lib/aia/utility.rb +24 -0
  24. data/lib/aia/version.rb +9 -6
  25. data/lib/aia.rb +57 -61
  26. data/lib/extensions/openstruct_merge.rb +44 -0
  27. metadata +43 -42
  28. data/LICENSE.txt +0 -21
  29. data/doc/aia_and_pre_compositional_prompts.md +0 -474
  30. data/lib/aia/clause.rb +0 -7
  31. data/lib/aia/cli.rb +0 -452
  32. data/lib/aia/directives.rb +0 -142
  33. data/lib/aia/dynamic_content.rb +0 -26
  34. data/lib/aia/logging.rb +0 -62
  35. data/lib/aia/main.rb +0 -265
  36. data/lib/aia/prompt.rb +0 -275
  37. data/lib/aia/tools/backend_common.rb +0 -58
  38. data/lib/aia/tools/client.rb +0 -197
  39. data/lib/aia/tools/editor.rb +0 -52
  40. data/lib/aia/tools/glow.rb +0 -90
  41. data/lib/aia/tools/llm.rb +0 -77
  42. data/lib/aia/tools/mods.rb +0 -100
  43. data/lib/aia/tools/sgpt.rb +0 -79
  44. data/lib/aia/tools/subl.rb +0 -68
  45. data/lib/aia/tools/vim.rb +0 -93
  46. data/lib/aia/tools.rb +0 -88
  47. data/lib/aia/user_query.rb +0 -21
  48. data/lib/core_ext/string_wrap.rb +0 -73
  49. data/lib/core_ext/tty-spinner_log.rb +0 -25
  50. data/man/aia.1 +0 -272
  51. data/man/aia.1.md +0 -236
data/lib/aia/main.rb DELETED
@@ -1,265 +0,0 @@
1
- # lib/aia/main.rb
2
-
3
- module AIA ; end
4
-
5
- require_relative 'config'
6
- require_relative 'cli'
7
- require_relative 'directives'
8
- require_relative 'dynamic_content'
9
- require_relative 'prompt'
10
- require_relative 'logging'
11
- require_relative 'tools'
12
- require_relative 'user_query'
13
-
14
- # Everything is being handled within the context
15
- # of a single class.
16
-
17
- class AIA::Main
18
- SPINNER_FORMAT = :bouncing_ball
19
-
20
- include AIA::DynamicContent
21
- include AIA::UserQuery
22
-
23
- attr_accessor :logger, :tools, :backend, :directive_output, :piped_content
24
-
25
- attr_reader :spinner
26
-
27
- def initialize(args= ARGV)
28
- unless $stdin.tty?
29
- @piped_content = $stdin.readlines.join.chomp
30
- $stdin.reopen("/dev/tty")
31
- end
32
-
33
- @directive_output = ""
34
- AIA::Tools.load_tools
35
-
36
- AIA.client = AIA::Client.new
37
-
38
- AIA::Cli.new(args)
39
-
40
- if AIA.config.debug?
41
- debug_me('== CONFIG AFTER CLI =='){[
42
- "AIA.config"
43
- ]}
44
- end
45
-
46
- @spinner = TTY::Spinner.new(":spinner :title", format: SPINNER_FORMAT)
47
- spinner.update(title: "composing response ... ")
48
-
49
- @logger = AIA::Logging.new(AIA.config.log_file)
50
-
51
- @logger.info(AIA.config) if AIA.config.debug? || AIA.config.verbose?
52
-
53
-
54
- @directives_processor = AIA::Directives.new
55
-
56
- @prompt = AIA::Prompt.new.prompt
57
-
58
- @prompt.text += piped_content unless piped_content.nil?
59
-
60
- # TODO: still should verify that the tools are ion the $PATH
61
- # tools.class.verify_tools
62
- end
63
-
64
-
65
- # Function to setup the Reline history with a maximum depth
66
- def setup_reline_history(max_history_size=5)
67
- Reline::HISTORY.clear
68
- # Reline::HISTORY.max_size = max_history_size
69
- end
70
-
71
-
72
- # This will be recursive with the new options
73
- # --next and --pipeline
74
- def call
75
- @directive_output = @directives_processor.execute_my_directives
76
-
77
- if AIA.config.chat?
78
- AIA.config.out_file = STDOUT
79
- AIA.config.extra = "--quiet" if 'mods' == AIA.config.backend
80
- end
81
-
82
- # TODO: the context_files left in the @arguments array
83
- # should be verified BEFORE asking the user for a
84
- # prompt keyword or process the prompt. Do not
85
- # want invalid files to make it this far.
86
-
87
- found = AIA::Tools
88
- .search_for(
89
- name: AIA.config.backend,
90
- role: :backend
91
- )
92
-
93
- if found.empty?
94
- abort "There are no :backend tools named #{AIA.config.backend}"
95
- end
96
-
97
- if found.size > 1
98
- abort "There are #{found.size} :backend tools with the name #{AIAA.config.backend}"
99
- end
100
-
101
- backend_klass = found.first.klass
102
-
103
- abort "backend not found: #{AIA.config.backend}" if backend_klass.nil?
104
-
105
- the_prompt = @prompt.to_s
106
-
107
- the_prompt.prepend(@directive_output + "\n") unless @directive_output.nil? || @directive_output.empty?
108
-
109
- if AIA.config.terse?
110
- the_prompt.prepend "Be terse in your response. "
111
- end
112
-
113
- @backend = backend_klass.new(
114
- text: the_prompt,
115
- files: AIA.config.arguments # FIXME: want validated context files
116
- )
117
-
118
- result = get_and_display_result(the_prompt)
119
-
120
- AIA.speak(result) if AIA.config.speak?
121
-
122
- logger.prompt_result(@prompt, result)
123
-
124
- if AIA.config.chat?
125
- setup_reline_history
126
- AIA.speak result
127
- lets_chat
128
- end
129
-
130
- return if AIA.config.next.empty? && AIA.config.pipeline.empty?
131
-
132
- keep_going(result) unless AIA.config.pipeline.empty?
133
- end
134
-
135
-
136
- # The AIA.config.pipeline is NOT empty, so feed this result
137
- # into the next prompt within the pipeline.
138
- #
139
- def keep_going(result)
140
- temp_file = Tempfile.new('aia_pipeline')
141
- temp_file.write(result)
142
- temp_file.close
143
-
144
- AIA.config.directives = []
145
- AIA.config.model = ""
146
- AIA.config.arguments = [
147
- AIA.config.pipeline.shift,
148
- temp_file.path,
149
- # TODO: additional arguments from the pipeline
150
- ]
151
- AIA.config.next = ""
152
-
153
- AIA.config.files = [temp_file.path]
154
-
155
- @prompt = AIA::Prompt.new.prompt
156
- call # Recurse! until the AIA.config.pipeline is emplty
157
- puts
158
- ensure
159
- temp_file.unlink
160
- end
161
-
162
-
163
- def get_and_display_result(the_prompt_text)
164
- spinner.auto_spin if AIA.config.verbose?
165
-
166
- backend.text = the_prompt_text
167
- result = backend.run
168
-
169
- if AIA.config.verbose?
170
- spinner.success "Done."
171
- end
172
-
173
- AIA.config.out_file.write "\nResponse:\n"
174
-
175
- if STDOUT == AIA.config.out_file
176
- if AIA.config.render?
177
- AIA::Glow.new(content: result).run
178
- else
179
- result = result.wrap(indent: 2)
180
- AIA.config.out_file.write result
181
- end
182
- else
183
- AIA.config.out_file.write result
184
- if AIA.config.render?
185
- AIA::Glow.new(file_path: AIA.config.out_file).run
186
- end
187
- end
188
-
189
- result
190
- end
191
-
192
-
193
- def log_the_follow_up(the_prompt_text, result)
194
- logger.info "Follow Up:\n#{the_prompt_text}"
195
- logger.info "Response:\n#{result}"
196
- end
197
-
198
-
199
- def add_continue_option
200
- if 'mods' == AIA.config.backend
201
- continue_option = " -C"
202
- AIA.config.extra += continue_option unless AIA.config.extra.include?(continue_option)
203
- end
204
- end
205
-
206
-
207
- def insert_terse_phrase(a_string)
208
- if AIA.config.terse?
209
- a_string.prepend "Be terse in your response. "
210
- end
211
-
212
- a_string
213
- end
214
-
215
-
216
- def handle_directives(the_prompt_text)
217
- signal = PromptManager::Prompt::DIRECTIVE_SIGNAL
218
- result = the_prompt_text.start_with?(signal)
219
-
220
- if result
221
- parts = the_prompt_text[signal.size..].split(' ')
222
- directive = parts.shift
223
- parameters = parts.join(' ')
224
- AIA.config.directives << [directive, parameters]
225
-
226
- @directive_output = @directives_processor.execute_my_directives
227
- else
228
- @directive_output = ""
229
- end
230
-
231
- result
232
- end
233
-
234
-
235
- def lets_chat
236
- add_continue_option
237
-
238
- the_prompt_text = ask_question_with_reline("\nFollow Up: ")
239
-
240
- until the_prompt_text.empty?
241
- the_prompt_text = render_erb(the_prompt_text) if AIA.config.erb?
242
- the_prompt_text = render_env(the_prompt_text) if AIA.config.shell?
243
-
244
- if handle_directives(the_prompt_text)
245
- if @directive_output.nil? || @directive_output.empty?
246
- # Do nothing
247
- else
248
- the_prompt_text = @directive_output
249
- the_prompt_text = render_erb(the_prompt_text) if AIA.config.erb?
250
- the_prompt_text = render_env(the_prompt_text) if AIA.config.shell?
251
- result = get_and_display_result(the_prompt_text)
252
- log_the_follow_up(the_prompt_text, result)
253
- AIA.speak result
254
- end
255
- else
256
- the_prompt_text = insert_terse_phrase(the_prompt_text)
257
- result = get_and_display_result(the_prompt_text)
258
- log_the_follow_up(the_prompt_text, result)
259
- AIA.speak result
260
- end
261
-
262
- the_prompt_text = ask_question_with_reline("\nFollow Up: ")
263
- end
264
- end
265
- end
data/lib/aia/prompt.rb DELETED
@@ -1,275 +0,0 @@
1
- # lib/aia/prompt.rb
2
-
3
- require 'reline'
4
-
5
- require_relative 'dynamic_content'
6
- require_relative 'user_query'
7
-
8
- class AIA::Prompt
9
- include AIA::DynamicContent
10
- include AIA::UserQuery
11
-
12
- #
13
- # used when no prompt_id is provided but there
14
- # are extra parameters that need to be passed
15
- # to the backend. For example "aia -- --settings"
16
- #
17
- class Fake
18
- def id = '_fake_'
19
- def path = '_fake_'
20
- def text = ''
21
- def keywords = []
22
- def directives = []
23
- def to_s = ''
24
- end
25
-
26
- KW_HISTORY_MAX = 5
27
- COMMENT_SIGNAL = '#'
28
- DIRECTIVE_SIGNAL = "//"
29
-
30
- attr_reader :prompt
31
-
32
- # setting build: false supports unit testing.
33
- def initialize(build: true)
34
- if AIA.config.role.empty?
35
- @role = nil
36
- else
37
- @role = (AIA.config.roles_dir + "#{AIA.config.role}.txt").read
38
- end
39
-
40
- get_prompt
41
-
42
- @prompt_text_before_role = @prompt.text.dup
43
-
44
- unless @role.nil?
45
- @prompt.text.prepend @role
46
- end
47
-
48
- if build
49
- @prompt.text = render_erb(@prompt.text) if AIA.config.erb?
50
- @prompt.text = render_env(@prompt.text) if AIA.config.shell?
51
- process_prompt
52
- end
53
-
54
- AIA.config.directives = @prompt.directives
55
- end
56
-
57
-
58
- # Fetch the first argument which should be the prompt id
59
- def get_prompt
60
- prompt_id = AIA.config.arguments.shift
61
-
62
- unless prompt_id
63
- if AIA.config.extra.empty?
64
- abort("Please provide a prompt id")
65
- else
66
- @prompt = Fake.new
67
- return
68
- end
69
- end
70
-
71
- search_for_a_matching_prompt(prompt_id) unless existing_prompt?(prompt_id)
72
- edit_prompt if AIA.config.edit?
73
- end
74
-
75
-
76
- # Check if a prompt with the given id already exists. If so, use it.
77
- def existing_prompt?(prompt_id)
78
- @prompt = PromptManager::Prompt.get(id: prompt_id)
79
- true
80
- rescue ArgumentError
81
- false
82
- end
83
-
84
-
85
- # Process the prompt's associated keywords and parameters
86
- def process_prompt
87
- unless @prompt.keywords.empty?
88
- replace_keywords
89
- @prompt.build
90
- save(@prompt_text_before_role)
91
- end
92
- end
93
-
94
-
95
- def save(original_text)
96
- temp_text = @prompt.text
97
- @prompt.text = original_text
98
- @prompt.save
99
- @prompt.text = temp_text
100
- end
101
-
102
-
103
- def replace_keywords
104
- puts
105
- puts "ID: #{@prompt.id}"
106
-
107
- show_prompt_without_comments
108
-
109
- puts "\nPress up/down arrow to scroll through history."
110
- puts "Type new input or edit the current input."
111
- puts "Quit #{MY_NAME} with a CNTL-D or a CNTL-C"
112
- puts
113
- @prompt.keywords.each do |kw|
114
- value = keyword_value(kw, @prompt.parameters[kw])
115
-
116
- unless value.nil? || value.strip.empty?
117
- value_inx = @prompt.parameters[kw].index(value)
118
-
119
- if value_inx
120
- @prompt.parameters[kw].delete_at(value_inx)
121
- end
122
-
123
- # The most recent value for this kw will always be
124
- # in the last position
125
- @prompt.parameters[kw] << value
126
- @prompt.parameters[kw].shift if @prompt.parameters[kw].size > KW_HISTORY_MAX
127
- end
128
- end
129
- end
130
-
131
-
132
- # Function to setup the Reline history with a maximum depth
133
- def setup_reline_history(max_history_size=5)
134
- Reline::HISTORY.clear
135
- # Reline::HISTORY.max_size = max_history_size
136
- end
137
-
138
-
139
- # query the user for a value to the keyword allow the
140
- # reuse of the previous value shown as the default
141
- #
142
- # FIXME: Ruby v3.3.0 drops readline in favor or reline
143
- # internally it redirects "require 'readline'" to Reline
144
- # puts lipstick on the pig so that you can continue to
145
- # use the Readline namespace
146
- #
147
- def keyword_value(kw, history_array)
148
- setup_reline_history
149
-
150
- default = history_array.last
151
-
152
- Array(history_array).each { |entry| Reline::HISTORY.push(entry) unless entry.nil? || entry.empty? }
153
-
154
- puts "Parameter #{kw} ..."
155
-
156
- if default&.empty?
157
- user_prompt = "\n-=> "
158
- else
159
- user_prompt = "\n(#{default}) -=> "
160
- end
161
-
162
- a_string = ask_question_with_reline(user_prompt)
163
-
164
- if a_string.nil?
165
- puts "okay. Come back soon."
166
- exit
167
- end
168
-
169
- puts
170
- a_string.empty? ? default : a_string
171
- end
172
-
173
-
174
- # Search for a prompt with a matching id or keyword
175
- def search_for_a_matching_prompt(prompt_id)
176
- # TODO: using the rgfzf version of the search_proc should only
177
- # return a single prompt_id
178
- found_prompts = PromptManager::Prompt.search(prompt_id)
179
-
180
- if found_prompts.empty?
181
- if AIA.config.edit?
182
- create_prompt(prompt_id)
183
- edit_prompt
184
- else
185
- abort <<~EOS
186
-
187
- No prompts where found for: #{prompt_id}
188
- To create a prompt with this ID use the --edit option
189
- like this:
190
- #{MY_NAME} #{prompt_id} --edit
191
-
192
- EOS
193
- end
194
- else
195
- prompt_id = 1 == found_prompts.size ? found_prompts.first : handle_multiple_prompts(found_prompts, prompt_id)
196
- @prompt = PromptManager::Prompt.get(id: prompt_id)
197
- end
198
- end
199
-
200
-
201
- def handle_multiple_prompts(found_these, while_looking_for_this)
202
- raise ArgumentError, "Argument is not an Array" unless found_these.is_a?(Array)
203
-
204
- # Create an instance of AIA::Fzf with appropriate parameters
205
- fzf_instance = AIA::Fzf.new(
206
- list: found_these,
207
- directory: AIA.config.prompts_dir, # Assuming this is the correct directory
208
- query: while_looking_for_this,
209
- subject: 'Prompt IDs',
210
- prompt: 'Select one: '
211
- )
212
-
213
- # Run the fzf instance and get the selected result
214
- result = fzf_instance.run
215
-
216
- exit unless result
217
-
218
- result
219
- end
220
-
221
-
222
-
223
-
224
- def create_prompt(prompt_id)
225
- @prompt = PromptManager::Prompt.create(id: prompt_id)
226
- # TODO: consider a configurable prompt template
227
- # ERB ???
228
- end
229
-
230
-
231
- def edit_prompt
232
- # FIXME: replace with the editor from the configuration
233
-
234
- @editor = AIA::Subl.new(
235
- file: @prompt.path
236
- )
237
-
238
- @editor.run # blocks until file is closed
239
-
240
- AIA.config[:edit?] = false # turn off the --edit switch
241
-
242
- # reload the edited prompt
243
- @prompt = PromptManager::Prompt.get(id: @prompt.id)
244
- end
245
-
246
-
247
- def show_prompt_without_comments
248
- puts remove_comments.wrap(indent: 4)
249
- end
250
-
251
-
252
- # removes comments and directives
253
- def remove_comments
254
- lines = @prompt.text
255
- .split("\n")
256
- .reject{|a_line|
257
- a_line.strip.start_with?('#') ||
258
- a_line.strip.start_with?('//')
259
- }
260
-
261
- # Remove empty lines at the start of the prompt
262
- #
263
- lines = lines.drop_while(&:empty?)
264
-
265
- # Drop all the lines at __END__ and after
266
- #
267
- logical_end_inx = lines.index("__END__")
268
-
269
- if logical_end_inx
270
- lines[0...logical_end_inx] # NOTE: ... means to not include last index
271
- else
272
- lines
273
- end.join("\n")
274
- end
275
- end
@@ -1,58 +0,0 @@
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
-
16
- def sanitize(input)
17
- Shellwords.escape(input)
18
- end
19
-
20
-
21
- def build_command
22
- @parameters += " --model #{AIA.config.model} " if AIA.config.model
23
- @parameters += AIA.config.extra
24
-
25
- set_parameter_from_directives
26
-
27
- @command = "#{meta.name} #{@parameters} "
28
- @command += sanitize(text)
29
-
30
- puts @command if AIA.config.debug?
31
-
32
- @command
33
- end
34
-
35
-
36
- def set_parameter_from_directives
37
- AIA.config.directives.each do |entry|
38
- directive, value = entry
39
- if self.class::DIRECTIVES.include?(directive)
40
- @parameters += " --#{directive} #{sanitize(value)}" unless @parameters.include?(directive)
41
- end
42
- end
43
- end
44
-
45
-
46
- def run
47
- case @files.size
48
- when 0
49
- @result = `#{build_command}`
50
- when 1
51
- @result = `#{build_command} < #{@files.first}`
52
- else
53
- @result = %x[cat #{@files.join(' ')} | #{build_command}]
54
- end
55
-
56
- @result
57
- end
58
- end