aia 0.5.18 → 0.8.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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +1 -0
  3. data/.version +1 -1
  4. data/CHANGELOG.md +39 -5
  5. data/README.md +388 -219
  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/lib/aia/ai_client_adapter.rb +210 -0
  12. data/lib/aia/chat_processor_service.rb +120 -0
  13. data/lib/aia/config.rb +473 -4
  14. data/lib/aia/context_manager.rb +58 -0
  15. data/lib/aia/directive_processor.rb +267 -0
  16. data/lib/aia/{tools/fzf.rb → fzf.rb} +9 -17
  17. data/lib/aia/history_manager.rb +85 -0
  18. data/lib/aia/prompt_handler.rb +178 -0
  19. data/lib/aia/session.rb +215 -0
  20. data/lib/aia/shell_command_executor.rb +109 -0
  21. data/lib/aia/ui_presenter.rb +110 -0
  22. data/lib/aia/utility.rb +24 -0
  23. data/lib/aia/version.rb +9 -6
  24. data/lib/aia.rb +57 -61
  25. data/lib/extensions/openstruct_merge.rb +44 -0
  26. metadata +29 -42
  27. data/LICENSE.txt +0 -21
  28. data/doc/aia_and_pre_compositional_prompts.md +0 -474
  29. data/lib/aia/clause.rb +0 -7
  30. data/lib/aia/cli.rb +0 -452
  31. data/lib/aia/directives.rb +0 -142
  32. data/lib/aia/dynamic_content.rb +0 -26
  33. data/lib/aia/logging.rb +0 -62
  34. data/lib/aia/main.rb +0 -265
  35. data/lib/aia/prompt.rb +0 -275
  36. data/lib/aia/tools/backend_common.rb +0 -58
  37. data/lib/aia/tools/client.rb +0 -197
  38. data/lib/aia/tools/editor.rb +0 -52
  39. data/lib/aia/tools/glow.rb +0 -90
  40. data/lib/aia/tools/llm.rb +0 -77
  41. data/lib/aia/tools/mods.rb +0 -100
  42. data/lib/aia/tools/sgpt.rb +0 -79
  43. data/lib/aia/tools/subl.rb +0 -68
  44. data/lib/aia/tools/vim.rb +0 -93
  45. data/lib/aia/tools.rb +0 -88
  46. data/lib/aia/user_query.rb +0 -21
  47. data/lib/core_ext/string_wrap.rb +0 -73
  48. data/lib/core_ext/tty-spinner_log.rb +0 -25
  49. data/man/aia.1 +0 -272
  50. data/man/aia.1.md +0 -236
@@ -0,0 +1,267 @@
1
+ # lib/aia/directive_processor.rb
2
+
3
+ module AIA
4
+ class DirectiveProcessor
5
+ EXCLUDED_METHODS = %w[ run initialize private? ]
6
+ @descriptions = {}
7
+ @aliases = {}
8
+
9
+ class << self
10
+ attr_reader :descriptions, :aliases
11
+
12
+ def desc(description, method_name = nil)
13
+ @last_description = description
14
+ @descriptions[method_name.to_s] = description if method_name
15
+ nil
16
+ end
17
+
18
+ def method_added(method_name)
19
+ if @last_description
20
+ @descriptions[method_name.to_s] = @last_description
21
+ @last_description = nil
22
+ end
23
+ super if defined?(super)
24
+ end
25
+
26
+ def build_aliases(private_methods)
27
+ private_methods.each do |method_name|
28
+ method = instance_method(method_name)
29
+
30
+ @aliases[method_name] = []
31
+
32
+ private_methods.each do |other_method_name|
33
+ next if method_name == other_method_name
34
+
35
+ other_method = instance_method(other_method_name)
36
+
37
+ if method == other_method
38
+ @aliases[method_name] << other_method_name
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ def initialize
46
+ @prefix_size = PromptManager::Prompt::DIRECTIVE_SIGNAL.size
47
+ @included_files = []
48
+ end
49
+
50
+ def directive?(a_string)
51
+ a_string.strip.start_with?(PromptManager::Prompt::DIRECTIVE_SIGNAL)
52
+ end
53
+
54
+ # Used with the chat loop to allow user to enter a single directive
55
+ def process(a_string, context_manager)
56
+ return a_string unless directive?(a_string)
57
+
58
+ key = a_string.strip
59
+ sans_prefix = key[@prefix_size..]
60
+ args = sans_prefix.split(' ')
61
+ method_name = args.shift.downcase
62
+
63
+ if EXCLUDED_METHODS.include?(method_name)
64
+ return "Error: #{method_name} is not a valid directive: #{key}"
65
+ elsif respond_to?(method_name, true)
66
+ return send(method_name, args)
67
+ else
68
+ return "Error: Unknown directive '#{key}'"
69
+ end
70
+ end
71
+
72
+ def run(directives)
73
+ return {} if directives.nil? || directives.empty?
74
+ directives.each do |key, _|
75
+ sans_prefix = key[@prefix_size..]
76
+ args = sans_prefix.split(' ')
77
+ method_name = args.shift.downcase
78
+
79
+ if EXCLUDED_METHODS.include?(method_name)
80
+ directives[key] = "Error: #{method_name} is not a valid directive: #{key}"
81
+ next
82
+ elsif respond_to?(method_name, true)
83
+ directives[key] = send(method_name, args)
84
+ else
85
+ directives[key] = "Error: Unknown directive '#{key}'"
86
+ end
87
+ end
88
+
89
+ directives
90
+ end
91
+
92
+
93
+ #####################################################
94
+ ## Directives are implemented as private methods
95
+ ## All directives return a String. It can be empty.
96
+ #
97
+ private
98
+
99
+ def private?(method_name)
100
+ !respond_to?(method_name) && respond_to?(method_name, true)
101
+ end
102
+
103
+ ################
104
+ ## Directives ##
105
+ ################
106
+
107
+ desc "Specify the next prompt ID to process after this one"
108
+ def next(args = [])
109
+ if args.empty?
110
+ ap AIA.config.next
111
+ else
112
+ AIA.config.next = args.shift
113
+ end
114
+ ''
115
+ end
116
+
117
+ desc "Specify a sequence pf prompt IDs to process after this one"
118
+ def pipeline(args = [])
119
+ if args.empty?
120
+ ap AIA.config.pipeline
121
+ else
122
+ AIA.config.pipeline += args.map {|id| id.gsub(',', '').strip}
123
+ end
124
+ ''
125
+ end
126
+
127
+ desc "Inserts the contents of a file Example: //include path/to/file"
128
+ def include(args)
129
+ file_path = args.shift
130
+
131
+ if @included_files.include?(file_path)
132
+ ""
133
+ else
134
+ if File.exist?(file_path) && File.readable?(file_path)
135
+ @included_files << file_path
136
+ File.read(file_path)
137
+ else
138
+ "Error: File '#{file_path}' is not accessible"
139
+ end
140
+ end
141
+ end
142
+ alias_method :include_file, :include
143
+ alias_method :import, :include
144
+
145
+ desc "Without arguments it will print a list of all config items and their values _or_ //config item (for one item's value) _or_ //config item = value (to set a value of an item)"
146
+ def config(args = [])
147
+ args = Array(args)
148
+
149
+ if args.empty?
150
+ ap AIA.config
151
+ ""
152
+ elsif args.length == 1
153
+ config_item = args.first
154
+ local_cfg = Hash.new
155
+ local_cfg[config_item] = AIA.config[config_item]
156
+ ap local_cfg
157
+ ""
158
+ else
159
+ config_item = args.shift
160
+ boolean = AIA.respond_to?("#{config_item}?")
161
+ new_value = args.join(' ').gsub('=', '').strip
162
+
163
+ if boolean
164
+ new_value = %w[true t yes y on 1 yea yeah yep yup].include?(new_value.downcase)
165
+ end
166
+
167
+ AIA.config[config_item] = new_value
168
+ ""
169
+ end
170
+ end
171
+ alias_method :cfg, :config
172
+
173
+ desc "Shortcut for //config top_p _and_ //config top_p = value"
174
+ def top_p(*args)
175
+ send(:config, args.prepend('top_p'))
176
+ end
177
+ alias_method :topp, :top_p
178
+
179
+ desc "Shortcut for //config model _and_ //config model = value"
180
+ def model(*args)
181
+ send(:config, args.prepend('model'))
182
+ end
183
+
184
+ desc "Shortcut for //config temperature _and_ //config temperature = value"
185
+ def temperature(*args)
186
+ send(:config, args.prepend('temperature'))
187
+ end
188
+ alias_method :temp, :temperature
189
+
190
+ desc "Clears the conversation history (aka context) same as //config clear = true"
191
+ def clear(args, context_manager)
192
+ if context_manager.nil?
193
+ return "Error: Context manager not available for //clear directive."
194
+ end
195
+ context_manager.clear_context
196
+ nil
197
+ end
198
+
199
+ desc "Shortcut for a one line of ruby code; result is added to the context"
200
+ def ruby(*args)
201
+ ruby_code = args.join(' ')
202
+
203
+ begin
204
+ String(eval(ruby_code))
205
+ rescue Exception => e
206
+ <<~ERROR
207
+ This ruby code failed: #{ruby_code}
208
+ #{e.message}
209
+ ERROR
210
+ end
211
+ end
212
+ alias_method :rb, :ruby
213
+
214
+ desc "Use the system's say command to speak text //say some text"
215
+ def say(*args)
216
+ `say #{args.join(' ')}`
217
+ ""
218
+ end
219
+
220
+ desc "Inserts an instruction to keep responses short and to the point."
221
+ def terse(*args)
222
+ AIA::Session::TERSE_PROMPT
223
+ end
224
+
225
+ desc "Display the ASCII art AIA robot."
226
+ def robot(*args)
227
+ AIA::Utility.robot
228
+ ""
229
+ end
230
+
231
+ desc "Generates this help content"
232
+ def help(*args)
233
+ puts
234
+ puts "Available Directives"
235
+ puts "===================="
236
+ puts
237
+
238
+ directives = self.class
239
+ .private_instance_methods(false)
240
+ .map(&:to_s)
241
+ .reject { |m| EXCLUDED_METHODS.include?(m) }
242
+ .sort
243
+
244
+ self.class.build_aliases(directives)
245
+
246
+ directives.each do |directive|
247
+ next unless self.class.descriptions[directive]
248
+
249
+ others = self.class.aliases[directive]
250
+
251
+ if others.empty?
252
+ others_line = ""
253
+ else
254
+ with_prefix = others.map{|m| PromptManager::Prompt::DIRECTIVE_SIGNAL + m}
255
+ others_line = "\tAliases:#{with_prefix.join(' ')}\n"
256
+ end
257
+
258
+ puts <<~TEXT
259
+ //#{directive} #{self.class.descriptions[directive]}
260
+ #{others_line}
261
+ TEXT
262
+ end
263
+
264
+ ""
265
+ end
266
+ end
267
+ end
@@ -4,16 +4,7 @@
4
4
  require 'shellwords'
5
5
  require 'tempfile'
6
6
 
7
- class AIA::Fzf < AIA::Tools
8
-
9
- meta(
10
- name: 'fzf',
11
- role: :search_tool,
12
- desc: "A command-line fuzzy finder",
13
- url: "https://github.com/junegunn/fzf",
14
- install: "brew install fzf",
15
- )
16
-
7
+ class AIA::Fzf
17
8
  DEFAULT_PARAMETERS = %w[
18
9
  --tabstop=2
19
10
  --header-first
@@ -39,26 +30,27 @@ class AIA::Fzf < AIA::Tools
39
30
  @subject = subject
40
31
  @prompt = prompt
41
32
  @extension = extension
42
-
33
+
43
34
  build_command
44
35
  end
45
36
 
46
37
 
47
38
  def build_command
48
39
  fzf_options = DEFAULT_PARAMETERS.dup
49
- fzf_options << "--header='#{subject} which contain: #{query}\\nPress ESC to cancel.'"
40
+ fzf_options << "--header='#{subject} which contain: #{query}\nPress ESC to cancel.'"
50
41
  fzf_options << "--preview='cat #{directory}/{1}#{extension}'"
51
42
  fzf_options << "--prompt=#{Shellwords.escape(prompt)}"
52
-
53
- fzf_command = "#{meta.name} #{fzf_options.join(' ')}"
43
+
44
+ fzf_command = "fzf #{fzf_options.join(' ')}"
54
45
 
55
46
  @command = "cat #{tempfile_path} | #{fzf_command}"
56
47
  end
57
-
48
+
58
49
 
59
50
  def run
60
- puts "Executing: #{@command}"
61
- selected = `#{@command}`
51
+ # puts "Executing: #{@command}"
52
+ selected = `#{@command}`.strip
53
+
62
54
  selected.strip.empty? ? nil : selected.strip
63
55
  ensure
64
56
  unlink_tempfile
@@ -0,0 +1,85 @@
1
+ # lib/aia/history_manager.rb
2
+
3
+ require 'json'
4
+ require 'fileutils'
5
+
6
+ module AIA
7
+ class HistoryManager
8
+ MAX_VARIABLE_HISTORY = 5
9
+
10
+ # prompt is PromptManager::Prompt instance
11
+ def initialize(prompt:)
12
+ @prompt = prompt
13
+ @history = []
14
+ end
15
+
16
+
17
+ def history
18
+ @history
19
+ end
20
+
21
+
22
+ def history=(new_history)
23
+ @history = new_history
24
+ end
25
+
26
+
27
+ def setup_variable_history(history_values)
28
+ Reline::HISTORY.clear
29
+ history_values.each do |value|
30
+ Reline::HISTORY.push(value) unless value.nil? || value.empty?
31
+ end
32
+ end
33
+
34
+
35
+ def get_variable_history(variable, value = '')
36
+ return if value.nil? || value.empty?
37
+
38
+ values = @prompt.parameters[variable]
39
+ if values.include?(value)
40
+ values.delete(value)
41
+ end
42
+
43
+ values << value
44
+
45
+ if values.size > MAX_VARIABLE_HISTORY
46
+ values.shift
47
+ end
48
+
49
+ @prompt.parameters[variable] = values
50
+ end
51
+
52
+
53
+ def request_variable_value(variable_name:, history_values: [])
54
+ setup_variable_history(history_values) # Setup Reline's history for completion
55
+
56
+ default_value = history_values.last || ''
57
+ question = "Value for #{variable_name} (#{default_value}): "
58
+
59
+ # Ensure Reline is writing to stdout explicitly for this interaction
60
+ Reline.output = $stdout
61
+
62
+ # Store the original prompt proc to restore later
63
+ original_prompt_proc = Reline.line_editor.prompt_proc
64
+
65
+ # Note: Temporarily setting prompt_proc might not be needed if passing prompt to readline works.
66
+ # Reline.line_editor.prompt_proc = ->(context) { [question] }
67
+
68
+ begin
69
+ input = Reline.readline(question, true)
70
+ return default_value if input.nil? # Handle Ctrl+D -> use default
71
+
72
+ chosen_value = input.strip.empty? ? default_value : input.strip
73
+ # Update the persistent history for this variable
74
+ get_variable_history(variable_name, chosen_value)
75
+ return chosen_value
76
+ rescue Interrupt
77
+ puts "\nVariable input interrupted."
78
+ exit(1) # Exit cleanly on Ctrl+C
79
+ ensure
80
+ # Restore the original prompt proc
81
+ Reline.line_editor.prompt_proc = original_prompt_proc
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,178 @@
1
+ # lib/aia/prompt_handler.rb
2
+
3
+ require 'prompt_manager'
4
+ require 'prompt_manager/storage/file_system_adapter'
5
+ require 'erb'
6
+
7
+
8
+ module AIA
9
+ class PromptHandler
10
+ def initialize
11
+ @prompts_dir = AIA.config.prompts_dir
12
+ @roles_dir = AIA.config.roles_dir # A sub-directory of @prompts_dir
13
+ @directive_processor = AIA::DirectiveProcessor.new
14
+
15
+ PromptManager::Prompt.storage_adapter =
16
+ PromptManager::Storage::FileSystemAdapter.config do |c|
17
+ c.prompts_dir = @prompts_dir
18
+ c.prompt_extension = '.txt' # default
19
+ c.params_extension = '.json' # default
20
+ end.new
21
+ end
22
+
23
+
24
+ def get_prompt(prompt_id, role_id = '')
25
+ prompt = fetch_prompt(prompt_id)
26
+
27
+ unless role_id.empty?
28
+ role_prompt = fetch_role(role_id)
29
+ prompt.text.prepend(role_prompt.text)
30
+ end
31
+
32
+ prompt
33
+ end
34
+
35
+ def fetch_prompt(prompt_id)
36
+ # Special case for fuzzy search without an initial query
37
+ if prompt_id == '__FUZZY_SEARCH__'
38
+ return fuzzy_search_prompt('')
39
+ end
40
+
41
+ # First check if the prompt file exists to avoid ArgumentError from PromptManager
42
+ prompt_file_path = File.join(@prompts_dir, "#{prompt_id}.txt")
43
+ if File.exist?(prompt_file_path)
44
+ prompt = PromptManager::Prompt.new(
45
+ id: prompt_id,
46
+ directives_processor: @directive_processor,
47
+ external_binding: binding,
48
+ erb_flag: AIA.config.erb,
49
+ envar_flag: AIA.config.shell
50
+ )
51
+
52
+ return prompt if prompt
53
+ else
54
+ puts "Warning: Invalid prompt ID or file not found: #{prompt_id}"
55
+ end
56
+
57
+ handle_missing_prompt(prompt_id)
58
+ end
59
+
60
+ def handle_missing_prompt(prompt_id)
61
+ if AIA.config.fuzzy
62
+ return fuzzy_search_prompt(prompt_id)
63
+ elsif AIA.config.fuzzy
64
+ puts "Warning: Fuzzy search is enabled but Fzf tool is not available."
65
+ raise "Error: Could not find prompt with ID: #{prompt_id}"
66
+ else
67
+ raise "Error: Could not find prompt with ID: #{prompt_id}"
68
+ end
69
+ end
70
+
71
+ def fuzzy_search_prompt(prompt_id)
72
+ new_prompt_id = search_prompt_id_with_fzf(prompt_id)
73
+
74
+ if new_prompt_id.nil? || new_prompt_id.empty?
75
+ raise "Error: Could not find prompt with ID: #{prompt_id} even with fuzzy search"
76
+ end
77
+
78
+ prompt = PromptManager::Prompt.new(
79
+ id: new_prompt_id,
80
+ directives_processor: @directive_processor,
81
+ external_binding: binding,
82
+ erb_flag: AIA.config.erb,
83
+ envar_flag: AIA.config.shell
84
+ )
85
+
86
+ raise "Error: Could not find prompt with ID: #{prompt_id} even with fuzzy search" if prompt.nil?
87
+
88
+ prompt
89
+ end
90
+
91
+ def fetch_role(role_id)
92
+ # Prepend roles_prefix if not already present
93
+ unless role_id.start_with?(AIA.config.roles_prefix)
94
+ role_id = "#{AIA.config.roles_prefix}/#{role_id}"
95
+ end
96
+
97
+ # NOTE: roles_prefix is a sub-directory of the prompts directory
98
+ role_file_path = File.join(@prompts_dir, "#{role_id}.txt")
99
+
100
+ if File.exist?(role_file_path)
101
+ role_prompt = PromptManager::Prompt.new(
102
+ id: role_id,
103
+ directives_processor: @directive_processor,
104
+ external_binding: binding,
105
+ erb_flag: AIA.config.erb,
106
+ envar_flag: AIA.config.shell
107
+ )
108
+ return role_prompt if role_prompt
109
+ else
110
+ puts "Warning: Invalid role ID or file not found: #{role_id}"
111
+ end
112
+
113
+ handle_missing_role(role_id)
114
+ end
115
+
116
+ def handle_missing_role(role_id)
117
+ if AIA.config.fuzzy
118
+ return fuzzy_search_role(role_id)
119
+ else
120
+ raise "Error: Could not find role with ID: #{role_id}"
121
+ end
122
+ end
123
+
124
+ def fuzzy_search_role(role_id)
125
+ new_role_id = search_role_id_with_fzf(role_id)
126
+ if new_role_id.nil? || new_role_id.empty?
127
+ raise "Error: Could not find role with ID: #{role_id} even with fuzzy search"
128
+ end
129
+
130
+ role_prompt = PromptManager::Prompt.new(
131
+ id: new_role_id,
132
+ directives_processor: @directive_processor,
133
+ external_binding: binding,
134
+ erb_flag: AIA.config.erb,
135
+ envar_flag: AIA.config.shell
136
+ )
137
+
138
+ raise "Error: Could not find role with ID: #{role_id} even with fuzzy search" if role_prompt.nil?
139
+ role_prompt
140
+ end
141
+
142
+
143
+ def search_prompt_id_with_fzf(initial_query)
144
+ prompt_files = Dir.glob(File.join(@prompts_dir, "*.txt")).map { |file| File.basename(file, ".txt") }
145
+ fzf = AIA::Fzf.new(
146
+ list: prompt_files,
147
+ directory: @prompts_dir,
148
+ query: initial_query,
149
+ subject: 'Prompt IDs',
150
+ prompt: 'Select a prompt ID:'
151
+ )
152
+ fzf.run || (raise "No prompt ID selected")
153
+ end
154
+
155
+ def search_role_id_with_fzf(initial_query)
156
+ role_files = Dir.glob(File.join(@roles_dir, "*.txt")).map { |file| File.basename(file, ".txt") }
157
+ fzf = AIA::Fzf.new(
158
+ list: role_files,
159
+ directory: @prompts_dir,
160
+ query: initial_query,
161
+ subject: 'Role IDs',
162
+ prompt: 'Select a role ID:'
163
+ )
164
+
165
+ role = fzf.run
166
+
167
+ if role.nil? || role.empty?
168
+ raise "No role ID selected"
169
+ end
170
+
171
+ unless role.start_with?(AIA.config.role_prefix)
172
+ role = AIA.config.role_prefix + '/' + role
173
+ end
174
+
175
+ role
176
+ end
177
+ end
178
+ end