aia 0.3.4 → 0.3.19

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/cli.rb CHANGED
@@ -1,150 +1,207 @@
1
1
  # lib/aia/cli.rb
2
2
 
3
- module AIA::Cli
4
- def setup_cli_options(args)
5
- @arguments = args
6
- # TODO: consider a fixed config file: ~/,aia
7
- @options = {
8
- # Value
9
- edit?: [false, "-e --edit", "Edit the Prompt File"],
10
- debug?: [false, "-d --debug", "Turn On Debugging"],
11
- verbose?: [false, "-v --verbose", "Be Verbose"],
12
- version?: [false, "--version", "Print Version"],
13
- help?: [false, "-h --help", "Show Usage"],
14
- fuzzy?: [false, "--fuzzy", "Use Fuzzy Matching"],
15
- completion: [nil, "--completion", "Show completion script for bash|zsh|fish"],
16
- # TODO: Consider dropping output in favor of always
17
- # going to STDOUT so user can redirect or pipe somewhere else
18
- output: [OUTPUT,"-o --output --no-output", "Out FILENAME"],
19
- log: [PROMPT_LOG,"-l --log --no-log", "Log FILEPATH"],
20
- markdown?: [true, "-m --markdown --no-markdown --md --no-md", "Format with Markdown"],
21
- backend: [:mods, "-b --be --backend --no-backend", "Specify the backend prompt resolver"],
22
- }
23
-
24
- # Array(String)
25
- @extra_options = [] # intended for the backend AI processor
3
+ HOME = Pathname.new(ENV['HOME'])
4
+ MY_NAME = 'aia'
26
5
 
27
- build_reader_methods # for the @options keys
28
- process_arguments
29
- end
30
6
 
7
+ require 'hashie'
8
+ require 'pathname'
9
+ require 'yaml'
10
+ require 'toml-rb'
31
11
 
32
- def usage
33
- usage = "\n#{MY_NAME} v#{AIA::VERSION}\n\n"
34
- usage += "Usage: #{MY_NAME} [options] prompt_id [context_file]* [-- external_options+]\n\n"
35
- usage += usage_options
36
- usage += usage_options_details
37
- usage += "\n"
38
- usage += usage_notes if verbose?
39
-
40
- usage
41
- end
42
-
43
12
 
44
- def usage_options
45
- options = [
46
- "Options",
47
- "-------",
48
- ""
49
- ]
13
+ class AIA::Cli
14
+ CF_FORMATS = %w[yml yaml toml]
15
+ ENV_PREFIX = self.name.split('::').first.upcase + "_"
16
+ MAN_PAGE_PATH = Pathname.new(__dir__) + '../../man/aia.1'
17
+
18
+
19
+ def initialize(args)
20
+ args = args.split(' ') if args.is_a? String
21
+
22
+ setup_options_with_defaults(args) # 1. defaults
23
+ load_env_options # 2. over-ride with envars
24
+ process_command_line_arguments # 3. over-ride with command line options
25
+
26
+ # 4. over-ride everything with config file
27
+ load_config_file unless AIA.config.config_file.nil?
50
28
 
51
- max_size = @options.values.map{|o| o[2].size}.max + 2
29
+ convert_to_pathname_objects
52
30
 
53
- @options.values.each do |o|
54
- pad_size = max_size - o[2].size
55
- options << o[2] + (" "*pad_size) + o[1]
31
+ setup_prompt_manager
32
+
33
+ execute_immediate_commands
34
+ end
56
35
 
57
- default = o[0]
58
- default = "./" + default.basename.to_s if o[1].include?('output')
59
- default = default.is_a?(Pathname) ? "$HOME/" + default.relative_path_from(HOME).to_s : default
60
36
 
61
- options << " default: #{default}\n"
37
+ def convert_pathname_objects!(converting_to_pathname: true)
38
+ path_keys = AIA.config.keys.grep(/_(dir|file)\z/)
39
+ path_keys.each do |key|
40
+ case AIA.config[key]
41
+ when String
42
+ AIA.config[key] = string_to_pathname(AIA.config[key])
43
+ when Pathname
44
+ AIA.config[key] = pathname_to_string(AIA.config[key]) unless converting_to_pathname
45
+ end
46
+ end
47
+ end
48
+
49
+
50
+ def string_to_pathname(string)
51
+ ['~/', '$HOME/'].each do |prefix|
52
+ if string.start_with? prefix
53
+ string = string.gsub(prefix, HOME.to_s+'/')
54
+ break
55
+ end
62
56
  end
63
57
 
64
- options.join("\n")
58
+ pathname = Pathname.new(string)
59
+ pathname.relative? ? Pathname.pwd + pathname : pathname
65
60
  end
66
61
 
67
62
 
68
- def usage_options_details
69
- <<~EOS
63
+ def pathname_to_string(pathname)
64
+ pathname.to_s
65
+ end
70
66
 
71
- Details
72
- -------
73
67
 
74
- Use (--help --verbose) or (-h -v) for verbose usage text.
68
+ def convert_to_pathname_objects
69
+ convert_pathname_objects!(converting_to_pathname: true)
70
+ end
75
71
 
76
- Use --completion bash|zsh|fish to show a script
77
- that will add prompt ID completion to your desired shell.
78
- You must copy the output from this option into a
79
- place where the function will be executed for
80
- your shell.
81
72
 
82
- EOS
73
+ def convert_from_pathname_objects
74
+ convert_pathname_objects!(converting_to_pathname: false)
83
75
  end
84
76
 
85
77
 
86
- def usage_notes
87
- <<~EOS
78
+ def load_env_options
79
+ known_keys = @options.keys
88
80
 
89
- #{usage_envars}
90
- #{AIA::External::HELP}
81
+ keys = ENV.keys
82
+ .select{|k| k.start_with?(ENV_PREFIX)}
83
+ .map{|k| k.gsub(ENV_PREFIX,'').downcase.to_sym}
91
84
 
92
- EOS
85
+ keys.each do |key|
86
+ envar_key = ENV_PREFIX + key.to_s.upcase
87
+ if known_keys.include?(key)
88
+ AIA.config[key] = ENV[envar_key]
89
+ elsif known_keys.include?("#{key}?".to_sym)
90
+ key = "#{key}?".to_sym
91
+ AIA.config[key] = %w[true t yes yea y 1].include?(ENV[envar_key].strip.downcase) ? true : false
92
+ else
93
+ # This is a new config key
94
+ AIA.config[key] = ENV[envar_key]
95
+ end
96
+ end
93
97
  end
94
98
 
95
99
 
96
- def usage_envars
97
- <<~EOS
98
- System Environment Variables Used
99
- ---------------------------------
100
+ def load_config_file
101
+ AIA.config.config_file = Pathname.new(AIA.config.config_file)
102
+ if AIA.config.config_file.exist?
103
+ AIA.config.merge! parse_config_file
104
+ else
105
+ abort "Config file does not exist: #{AIA.config.config_file}"
106
+ end
107
+ end
100
108
 
101
- The OUTPUT and PROMPT_LOG envars can be overridden
102
- by cooresponding options on the command line.
103
109
 
104
- Name Default Value
105
- -------------- -------------------------
106
- PROMPTS_DIR $HOME/.prompts_dir
107
- AI_CLI_PROGRAM mods
108
- EDITOR edit
109
- MODS_MODEL gpt-4-1106-preview
110
- OUTPUT ./temp.md
111
- PROMPT_LOG $PROMPTS_DIR/_prompts.log
110
+ def setup_options_with_defaults(args)
111
+ # TODO: This structure if flat; consider making it
112
+ # at least two levels totake advantage of
113
+ # YAML and TOML capabilities to isolate
114
+ # common options within a section.
115
+ #
116
+ @options = {
117
+ # Default
118
+ # Key Value, switches
119
+ arguments: [args], # NOTE: after process, prompt_id and context_files will be left
120
+ extra: [''], # SMELL: should be nil?
121
+ #
122
+ model: ["gpt-4-1106-preview", "--llm --model"],
123
+ #
124
+ dump: [nil, "--dump"],
125
+ completion: [nil, "--completion"],
126
+ #
127
+ edit?: [false, "-e --edit"],
128
+ debug?: [false, "-d --debug"],
129
+ verbose?: [false, "-v --verbose"],
130
+ version?: [false, "--version"],
131
+ help?: [false, "-h --help"],
132
+ fuzzy?: [false, "-f --fuzzy"],
133
+ search: [nil, "-s --search"],
134
+ markdown?: [true, "-m --markdown --no-markdown --md --no-md"],
135
+ #
136
+ # TODO: May have to process the
137
+ # "~" character and replace it with HOME
138
+ #
139
+ # TODO: Consider using standard suffix of _dif and _file
140
+ # to signal Pathname objects fo validation
141
+ #
142
+ config_file:[nil, "-c --config"],
143
+ prompts_dir:["~/.prompts", "-p --prompts"],
144
+ output_file:["temp.md", "-o --output --no-output"],
145
+ log_file: ["~/.prompts/_prompts.log", "-l --log --no-log"],
146
+ #
147
+ backend: ['mods', "-b --be --backend --no-backend"],
148
+ }
149
+
150
+ AIA.config = AIA::Config.new(@options.transform_values { |values| values.first })
151
+ end
112
152
 
113
- These two are required for access the OpenAI
114
- services. The have the same value but different
115
- programs use different envar names.
116
153
 
117
- To get an OpenAI access key/token (same thing)
118
- you must first create an account at OpenAI.
119
- Here is the link: https://platform.openai.com/docs/overview
154
+ def arguments
155
+ AIA.config.arguments
156
+ end
120
157
 
121
- OPENAI_ACCESS_TOKEN
122
- OPENAI_API_KEY
123
158
 
124
- EOS
159
+ def execute_immediate_commands
160
+ show_usage if AIA.config.help?
161
+ show_version if AIA.config.version?
162
+ dump_config_file if AIA.config.dump
163
+ show_completion if AIA.config.completion
125
164
  end
126
165
 
127
166
 
128
- def build_reader_methods
129
- @options.keys.each do |key|
130
- define_singleton_method(key) do
131
- @options[key][0]
132
- end
167
+ def dump_config_file
168
+ a_hash = prepare_config_as_hash
169
+
170
+ case AIA.config.dump.downcase
171
+ when 'yml', 'yaml'
172
+ puts YAML.dump(a_hash)
173
+ when 'toml'
174
+ puts TomlRB.dump(a_hash)
175
+ else
176
+ abort "Invalid config file format request. Only #{CF_FORMATS.join(', ')} are supported."
133
177
  end
178
+
179
+ exit
180
+ end
181
+
182
+
183
+ def prepare_config_as_hash
184
+ convert_from_pathname_objects
185
+
186
+ a_hash = AIA.config.to_h
187
+ a_hash['dump'] = nil
188
+
189
+ a_hash.delete('arguments')
190
+ a_hash.delete('config_file')
191
+
192
+ a_hash
134
193
  end
135
194
 
136
195
 
137
- def process_arguments
196
+ def process_command_line_arguments
138
197
  @options.keys.each do |option|
139
198
  check_for option
140
199
  end
141
200
 
142
- show_completion unless @options[:completion].first.nil?
143
-
144
201
  # get the options meant for the backend AI command
145
202
  extract_extra_options
146
203
 
147
- bad_options = @arguments.select{|a| a.start_with?('-')}
204
+ bad_options = arguments.select{|a| a.start_with?('-')}
148
205
 
149
206
  unless bad_options.empty?
150
207
  puts <<~EOS
@@ -161,23 +218,26 @@ module AIA::Cli
161
218
 
162
219
 
163
220
  def check_for(option_sym)
164
- boolean = option_sym.to_s.end_with?('?')
165
- switches = @options[option_sym][1].split
221
+ # sometimes @options has stuff that is not a command line option
222
+ return if @options[option_sym].nil? || @options[option_sym].size <= 1
223
+
224
+ boolean = option_sym.to_s.end_with?('?')
225
+ switches = @options[option_sym][1].split
166
226
 
167
227
  switches.each do |switch|
168
- if @arguments.include?(switch)
169
- index = @arguments.index(switch)
228
+ if arguments.include?(switch)
229
+ index = arguments.index(switch)
170
230
 
171
231
  if boolean
172
- @options[option_sym][0] = switch.include?('-no-') ? false : true
173
- @arguments.slice!(index,1)
232
+ AIA.config[option_sym] = switch.include?('-no-') ? false : true
233
+ arguments.slice!(index,1)
174
234
  else
175
235
  if switch.include?('-no-')
176
- @options[option_sym][0] = nil
177
- @arguments.slice!(index,1)
236
+ AIA.config[option_sym] = switch.include?('output') ? STDOUT : nil
237
+ arguments.slice!(index,1)
178
238
  else
179
- @options[option_sym][0] = @arguments[index + 1]
180
- @arguments.slice!(index,2)
239
+ AIA.config[option_sym] = arguments[index + 1]
240
+ arguments.slice!(index,2)
181
241
  end
182
242
  end
183
243
 
@@ -186,17 +246,33 @@ module AIA::Cli
186
246
  end
187
247
  end
188
248
 
189
-
249
+ # aia usage is maintained in a man page
190
250
  def show_usage
191
251
  @options[:help?][0] = false
192
- puts usage
252
+ puts `man #{MAN_PAGE_PATH}`
253
+ show_verbose_usage if AIA.config.verbose?
193
254
  exit
194
255
  end
195
256
  alias_method :show_help, :show_usage
196
257
 
197
258
 
259
+ def show_verbose_usage
260
+ puts <<~EOS
261
+
262
+ ======================================
263
+ == Currently selected Backend: #{AIA.config.backend} ==
264
+ ======================================
265
+
266
+ EOS
267
+ puts `mods --help` if "mods" == AIA.config.backend
268
+ puts `sgpt --help` if "sgpt" == AIA.config.backend
269
+ puts
270
+ end
271
+ # alias_method :show_verbose_help, :show_verbose_usage
272
+
273
+
198
274
  def show_completion
199
- shell = @options[:completion].first
275
+ shell = AIA.config.completion
200
276
  script = Pathname.new(__dir__) + "aia_completion.#{shell}"
201
277
 
202
278
  if script.exist?
@@ -206,12 +282,11 @@ module AIA::Cli
206
282
  else
207
283
  STDERR.puts <<~EOS
208
284
 
209
- ERRORL The shell '#{shell}' is not supported.
285
+ ERROR: The shell '#{shell}' is not supported.
210
286
 
211
287
  EOS
212
288
  end
213
289
 
214
-
215
290
  exit
216
291
  end
217
292
 
@@ -220,4 +295,42 @@ module AIA::Cli
220
295
  puts AIA::VERSION
221
296
  exit
222
297
  end
298
+
299
+
300
+ def setup_prompt_manager
301
+ @prompt = nil
302
+
303
+ PromptManager::Prompt.storage_adapter =
304
+ PromptManager::Storage::FileSystemAdapter.config do |config|
305
+ config.prompts_dir = AIA.config.prompts_dir
306
+ config.prompt_extension = '.txt'
307
+ config.params_extension = '.json'
308
+ config.search_proc = nil
309
+ # TODO: add the rgfzf script for search_proc
310
+ end.new
311
+ end
312
+
313
+
314
+ # Get the additional CLI arguments intended for the
315
+ # backend gen-AI processor.
316
+ def extract_extra_options
317
+ extra_index = arguments.index('--')
318
+
319
+ if extra_index
320
+ AIA.config.extra = arguments.slice!(extra_index..-1)[1..].join(' ')
321
+ end
322
+ end
323
+
324
+
325
+ def parse_config_file
326
+ case AIA.config.config_file.extname.downcase
327
+ when '.yaml', '.yml'
328
+ YAML.safe_load(AIA.config.config_file.read)
329
+ when '.toml'
330
+ TomlRB.parse(AIA.config.config_file.read)
331
+ else
332
+ abort "Unsupported config file type: #{AIA.config.config_file.extname}"
333
+ end
334
+ end
223
335
  end
336
+
data/lib/aia/config.rb ADDED
@@ -0,0 +1,7 @@
1
+ # aia/lib/aia/config.rb
2
+
3
+ require 'hashie'
4
+
5
+ class AIA::Config < Hashie::Mash
6
+ disable_warnings
7
+ end
data/lib/aia/logging.rb CHANGED
@@ -1,22 +1,59 @@
1
1
  # lib/aia/logging.rb
2
2
 
3
- module AIA::Logging
4
- def log_result
5
- return if log.nil?
6
-
7
- f = File.open(log, "ab")
3
+ require 'logger'
8
4
 
9
- f.write <<~EOS
10
- =======================================
11
- == #{Time.now}
12
- == #{@prompt.path}
5
+ class AIA::Logging
6
+ attr_accessor :logger
13
7
 
14
- PROMPT:
15
- #{@prompt}
8
+ def initialize(log_file_path)
9
+ @logger = if log_file_path
10
+ Logger.new(
11
+ log_file_path, # path/to/file
12
+ 'weekly', # rotation interval
13
+ 'a' # append to existing file
14
+ )
15
+ else
16
+ Logger.new(STDOUT) # Fall back to standard output if path is nil or invalid
17
+ end
18
+
19
+ configure_logger
20
+ end
21
+
22
+ def prompt_result(prompt, result)
23
+ logger.info( <<~EOS
24
+ PROMPT ID #{prompt.id}
25
+ PATH: #{prompt.path}
26
+ KEYWORDS: #{prompt.keywords.join(', ')}
27
+
28
+ #{prompt.to_s}
16
29
 
17
30
  RESULT:
18
- #{@result}
31
+ #{result}
32
+
19
33
 
20
34
  EOS
35
+ )
36
+ rescue StandardError => e
37
+ logger.error("Failed to log the result. Error: #{e.message}")
38
+ end
39
+
40
+
41
+ def debug(msg) = logger.debug(msg)
42
+ def info(msg) = logger.info(msg)
43
+ def warn(msg) = logger.warn(msg)
44
+ def error(msg) = logger.error(msg)
45
+ def fatal(msg) = logger.fatal(msg)
46
+
47
+ private
48
+
49
+ def configure_logger
50
+ @logger.formatter = proc do |severity, datetime, _progname, msg|
51
+ formatted_datetime = datetime.strftime("%Y-%m-%d %H:%M:%S")
52
+ "[#{formatted_datetime}] #{severity}: #{msg}\n"
53
+ end
54
+ @logger.level = Logger::DEBUG
21
55
  end
22
56
  end
57
+
58
+
59
+
data/lib/aia/main.rb CHANGED
@@ -2,38 +2,52 @@
2
2
 
3
3
  module AIA ; end
4
4
 
5
- require_relative 'configuration'
6
-
5
+ require_relative 'config'
7
6
  require_relative 'cli'
8
7
  require_relative 'prompt_processing'
9
- require_relative 'external'
10
8
  require_relative 'logging'
9
+ require_relative 'tools'
11
10
 
12
11
  # Everything is being handled within the context
13
12
  # of a single class.
14
13
 
15
14
  class AIA::Main
16
- include AIA::Configuration
17
- include AIA::Cli
18
15
  include AIA::PromptProcessing
19
- include AIA::External
20
- include AIA::Logging
21
16
 
17
+ attr_accessor :logger, :tools
22
18
 
23
19
  def initialize(args= ARGV)
24
- setup_configuration
25
- setup_cli_options(args)
26
- setup_external_programs
20
+ AIA::Cli.new(args)
21
+
22
+ @logger = AIA::Logging.new(AIA.config.log_file)
23
+ @tools = AIA::Tools.new
24
+
25
+ tools.class.verify_tools
27
26
  end
28
27
 
29
28
 
30
29
  def call
31
- show_usage if help?
32
- show_version if version?
33
-
34
30
  get_prompt
35
31
  process_prompt
36
- send_prompt_to_external_command
37
- log_result unless log.nil?
32
+
33
+ # send_prompt_to_external_command
34
+
35
+ # TODO: the context_files left in the @arguments array
36
+ # should be verified BEFORE asking the user for a
37
+ # prompt keyword or process the prompt. Do not
38
+ # want invalid files to make it this far.
39
+
40
+
41
+ mods = AIA::Mods.new(
42
+ extra_options: AIA.config.extra,
43
+ text: @prompt.to_s,
44
+ files: AIA.config.arguments # FIXME: want validated context files
45
+ )
46
+
47
+ result = mods.run
48
+
49
+ AIA.config.output_file.write result
50
+
51
+ logger.prompt_result(@prompt, result)
38
52
  end
39
53
  end