aia 0.3.4 → 0.3.19

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