aia 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5737dc3f15dc0372865893efba9c8e7bb6c4c41fc335476b638cb55a5dc4ad17
4
- data.tar.gz: 91545c6a73d8d6f61cbf878f35df375f3e700961d1d70b25bd3cb0aa0cc25251
3
+ metadata.gz: a080fd671c1e5e3f9420f036892afb69a8d4190745da8bbce82620356ae2ad10
4
+ data.tar.gz: ffe746cbcb3a24216f39f4ec220b962828ab925041fe6f97f006676988006690
5
5
  SHA512:
6
- metadata.gz: 7a276b2fdad405245fd29f8aba586504ad2a99cc4c8f3f570af1de58849c1ee2abda2f30c5a72fbd699db37f01b94e79711eb4833495cebeae33c9562d03033f
7
- data.tar.gz: 2c570b4339d3f24b6621378481393994272a0f0317ab4edb2ae9ca79007a81b288cc09eeae0f9326f72892ce742d41e9bbfe180de3a8bb6f652a1fc808d1e62f
6
+ metadata.gz: 6ac1c123e391a981800ac8777e31de96cc68af4747e0756902c3f3bd6eaa3fef4c23e425ff7d951c63f93ee5dbb85c1573f8434958dd3e6ce142579f52bf7bd6
7
+ data.tar.gz: ed7e48ffa949db795884eb0d552a3ac312079dce7152fd45643191f2907ff5a697d334cc26a492cee44a649420fecaf9d6063dfc72ff82e7bd226dc2a77e81c6
data/README.md CHANGED
@@ -31,48 +31,114 @@ TODO: don't forget to mention have access token (API keys) setup as envars for t
31
31
 
32
32
  ## Usage
33
33
 
34
- `aia prompt_id [context_file]*`
34
+ ```text
35
+ $ aia --help
36
+
37
+ aia v0.0.5
38
+
39
+ Usage: aia [options] prompt_id [context_file]* [-- external_options+]
40
+
41
+ Options
42
+ -------
43
+
44
+ Edit the Prompt File -e --edit
45
+ default: false
46
+
47
+ Turn On Debugging -d --debug
48
+ default: false
49
+
50
+ Be Verbose -v --verbose
51
+ default: false
35
52
 
36
- `prompt_id` is the basename of the `prompt_id.txt` file that is located in the `$PROMPTS_DIR` directory. There is also a `prompt_id.json` file saved in the same place to hold the last-used values (parameters) for the keywords (if any) found in your prompt file.
53
+ Print Version --version
54
+ default: false
55
+
56
+ Show Usage -h --help
57
+ default: false
58
+
59
+ Use Fuzzy Matching --fuzzy
60
+ default: false
61
+
62
+ Out FILENAME -o --output --no-output
63
+ default: ./temp.md
64
+
65
+ Log FILEPATH -l --log --no-log
66
+ default: $HOME/.prompts/_prompts.log
67
+
68
+ Format with Markdown -m --markdown --no-markdown --md --no-md
69
+ default: true
70
+ ```
37
71
 
38
- TODO: consider a config file.
39
- TODO: consider a --no-log option to turn off logging
72
+ Turn on `verbose` with `help` to see more usage information that includes system environment variables and external CLI tools that are used.
40
73
 
41
- The `_prompts.log` file is also located in the `$PROMPTS_DIR`
74
+ ```text
75
+ $ aia --help --verbose
76
+ ```
42
77
 
43
- The default output file is `temp.md` which is written to the current directory from which `aia` was executed.
78
+ ## System Environment Variables (envars)
44
79
 
80
+ From the verbose help text ...
45
81
 
46
82
  ```text
47
- $ aia -h
48
- Use generative AI with saved parameterized prompts
49
83
 
50
- Usage: aia [options] ...
84
+ System Environment Variables Used
85
+ ---------------------------------
51
86
 
52
- Where:
87
+ The OUTPUT and PROMPT_LOG envars can be overridden
88
+ by cooresponding options on the command line.
53
89
 
54
- Common Options Are:
55
- -h, --help show this message
56
- -v, --verbose enable verbose mode
57
- -d, --debug enable debug mode
58
- --version print the version: 1.2.0
90
+ Name Default Value
91
+ -------------- -------------------------
92
+ PROMPTS_DIR $HOME/.prompts_dir
93
+ AI_CLI_PROGRAM mods
94
+ EDITOR edit
95
+ MODS_MODEL gpt-4-1106-preview
96
+ OUTPUT ./temp.md
97
+ PROMPT_LOG $PROMPTS_DIR/_prompts.log
59
98
 
60
- Program Options Are:
61
- -f, --fuzzy Allow fuzzy matching
62
- -o, --output The output file
99
+ These two are required for access the OpenAI
100
+ services. The have the same value but different
101
+ programs use different envar names.
63
102
 
64
- AI Assistant (aia)
65
- ==================
103
+ To get an OpenAI access key/token (same thing)
104
+ you must first create an account at OpenAI.
105
+ Here is the link: https://platform.openai.com/docs/overview
106
+
107
+ OPENAI_ACCESS_TOKEN
108
+ OPENAI_API_KEY
109
+ ```
66
110
 
67
- The AI cli program being used is: mods
111
+ ## External CLI Tools Used
112
+
113
+ From the verbose help text ...
114
+
115
+ ```text
116
+ External Tools Used
117
+ -------------------
68
118
 
69
- The defaul options to mods are:
70
- "-m gpt-4-1106-preview --no-limit -f"
119
+ To install the external CLI programs used by aia:
120
+ brew install fzf mods rg
121
+
122
+ fzf
123
+ Command-line fuzzy finder written in Go
124
+ https://github.com/junegunn/fzf
125
+
126
+ mods
127
+ AI on the command-line
128
+ https://github.com/charmbracelet/mods
129
+
130
+ rg
131
+ Search tool like grep and The Silver Searcher
132
+ https://github.com/BurntSushi/ripgrep
133
+
134
+ A text editor whose executable is setup in the
135
+ system environment variable 'EDITOR' like this:
136
+
137
+ export EDITOR="subl -w"
71
138
 
72
- You can pass additional CLI options to mods like this:
73
- "aia my options -- options for mods"
74
139
  ```
75
140
 
141
+
76
142
  ## Development
77
143
 
78
144
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/bin/aia CHANGED
@@ -1,7 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
- # aia
2
+ # bin/aia
3
3
 
4
4
  require 'aia'
5
-
6
- # Create an instance of the Main class and run the program
7
- AIA::Main.new.call
5
+ AIA.run
data/lib/aia/cli.rb ADDED
@@ -0,0 +1,209 @@
1
+ # lib/aia/cli.rb
2
+
3
+ module AIA::Cli
4
+ def setup_cli_options(args)
5
+ @arguments = args
6
+ @options = {
7
+ # Value
8
+ edit?: [false, "-e --edit", "Edit the Prompt File"],
9
+ debug?: [false, "-d --debug", "Turn On Debugging"],
10
+ verbose?: [false, "-v --verbose", "Be Verbose"],
11
+ version?: [false, "--version", "Print Version"],
12
+ help?: [false, "-h --help", "Show Usage"],
13
+ fuzzy?: [false, "--fuzzy", "Use Fuzzy Matching"],
14
+ # TODO: Consider dropping output in favor of always
15
+ # going to STDOUT so user can redirect or pipe somewhere else
16
+ output: [OUTPUT,"-o --output --no-output", "Out FILENAME"],
17
+ log: [PROMPT_LOG,"-l --log --no-log", "Log FILEPATH"],
18
+ markdown?: [true, "-m --markdown --no-markdown --md --no-md", "Format with Markdown"],
19
+ }
20
+
21
+ # Array(String)
22
+ @extra_options = [] # intended for the backend AI processor
23
+
24
+ build_reader_methods # for the @options keys
25
+ process_arguments
26
+ end
27
+
28
+
29
+ def usage
30
+ usage = "\n#{MY_NAME} v#{AIA::VERSION}\n\n"
31
+ usage += "Usage: #{MY_NAME} [options] prompt_id [context_file]* [-- external_options+]\n\n"
32
+ usage += usage_options
33
+ usage += "\n"
34
+ usage += usage_notes if verbose?
35
+
36
+ usage
37
+ end
38
+
39
+
40
+ def usage_options
41
+ options = [
42
+ "Options",
43
+ "-------",
44
+ ""
45
+ ]
46
+
47
+ max_size = @options.values.map{|o| o[2].size}.max + 2
48
+
49
+ @options.values.each do |o|
50
+ pad_size = max_size - o[2].size
51
+ options << o[2] + (" "*pad_size) + o[1]
52
+
53
+ default = o[0]
54
+ default = "./" + default.basename.to_s if o[1].include?('output')
55
+ default = default.is_a?(Pathname) ? "$HOME/" + default.relative_path_from(HOME).to_s : default
56
+
57
+ options << " default: #{default}\n"
58
+ end
59
+
60
+ options.join("\n")
61
+ end
62
+
63
+
64
+ def usage_notes
65
+ <<~EOS
66
+ #{usage_envars}
67
+ #{AIA::ExternalCommands::HELP}
68
+ EOS
69
+ end
70
+
71
+
72
+ def usage_envars
73
+ <<~EOS
74
+ System Environment Variables Used
75
+ ---------------------------------
76
+
77
+ The OUTPUT and PROMPT_LOG envars can be overridden
78
+ by cooresponding options on the command line.
79
+
80
+ Name Default Value
81
+ -------------- -------------------------
82
+ PROMPTS_DIR $HOME/.prompts_dir
83
+ AI_CLI_PROGRAM mods
84
+ EDITOR edit
85
+ MODS_MODEL gpt-4-1106-preview
86
+ OUTPUT ./temp.md
87
+ PROMPT_LOG $PROMPTS_DIR/_prompts.log
88
+
89
+ These two are required for access the OpenAI
90
+ services. The have the same value but different
91
+ programs use different envar names.
92
+
93
+ To get an OpenAI access key/token (same thing)
94
+ you must first create an account at OpenAI.
95
+ Here is the link: https://platform.openai.com/docs/overview
96
+
97
+ OPENAI_ACCESS_TOKEN
98
+ OPENAI_API_KEY
99
+
100
+ EOS
101
+ end
102
+
103
+
104
+ def build_reader_methods
105
+ @options.keys.each do |key|
106
+ define_singleton_method(key) do
107
+ @options[key][0]
108
+ end
109
+ end
110
+ end
111
+
112
+
113
+ def process_arguments
114
+ @options.keys.each do |option|
115
+ check_for option
116
+ end
117
+
118
+ # get the options meant for the backend AI command
119
+ extract_extra_options
120
+
121
+ bad_options = @arguments.select{|a| a.start_with?('-')}
122
+
123
+ unless bad_options.empty?
124
+ puts <<~EOS
125
+
126
+ ERROR: Unknown options: #{bad_options.join(' ')}
127
+
128
+ EOS
129
+
130
+ show_usage
131
+
132
+ exit
133
+ end
134
+ end
135
+
136
+
137
+ def check_for(option_sym)
138
+ boolean = option_sym.to_s.end_with?('?')
139
+ switches = @options[option_sym][1].split
140
+
141
+ switches.each do |switch|
142
+ if @arguments.include?(switch)
143
+ index = @arguments.index(switch)
144
+
145
+ if boolean
146
+ @options[option_sym][0] = switch.include?('-no-') ? false : true
147
+ @arguments.slice!(index,1)
148
+ else
149
+ if switch.include?('-no-')
150
+ @options[option_sym][0] = nil
151
+ @arguments.slice!(index,1)
152
+ else
153
+ @options[option_sym][0] = @arguments[index + 1]
154
+ @arguments.slice!(index,2)
155
+ end
156
+ end
157
+
158
+ break
159
+ end
160
+ end
161
+ end
162
+
163
+
164
+
165
+ def show_usage
166
+ @options[:help?][0] = false
167
+ puts usage
168
+ exit
169
+ end
170
+ alias_method :show_help, :show_usage
171
+
172
+
173
+ def show_version
174
+ puts AIA::VERSION
175
+ exit
176
+ end
177
+ end
178
+
179
+
180
+ __END__
181
+
182
+
183
+ # TODO: Consider using this history process to preload the default
184
+ # so that an up arrow will bring the previous answer into
185
+ # the read buffer for line editing.
186
+ # Instead of usin the .history file just push the default
187
+ # value from the JSON file.
188
+
189
+ while input = Readline.readline('> ', true)
190
+ # Skip empty entries and duplicates
191
+ if input.empty? || Readline::HISTORY.to_a[-2] == input
192
+ Readline::HISTORY.pop
193
+ end
194
+ break if input == 'exit'
195
+
196
+ # Do something with the input
197
+ puts "You entered: #{input}"
198
+
199
+ # Save the history in case you want to preserve it for the next sessions
200
+ File.open('.history', 'a') { |f| f.puts(input) }
201
+ end
202
+
203
+ # Load history from file at the beginning of the program
204
+ if File.exist?('.history')
205
+ File.readlines('.history').each do |line|
206
+ Readline::HISTORY.push(line.chomp)
207
+ end
208
+ end
209
+
@@ -0,0 +1,44 @@
1
+ # lib/aia/configuration.rb
2
+
3
+ HOME = Pathname.new(ENV['HOME'])
4
+ PROMPTS_DIR = Pathname.new(ENV['PROMPTS_DIR'] || (HOME + ".prompts_dir"))
5
+
6
+ AI_CLI_PROGRAM = "mods"
7
+ EDITOR = ENV['EDITOR'] || 'edit'
8
+ MY_NAME = "aia"
9
+ MODS_MODEL = ENV['MODS_MODEL'] || 'gpt-4-1106-preview'
10
+ OUTPUT = Pathname.pwd + "temp.md"
11
+ PROMPT_LOG = PROMPTS_DIR + "_prompts.log"
12
+
13
+
14
+ module AIA::Configuration
15
+ def setup_configuration
16
+ @prompt = nil
17
+
18
+ PromptManager::Prompt.storage_adapter =
19
+ PromptManager::Storage::FileSystemAdapter.config do |config|
20
+ config.prompts_dir = PROMPTS_DIR
21
+ config.prompt_extension = '.txt'
22
+ config.params_extension = '.json'
23
+ config.search_proc = nil
24
+ # TODO: add the rgfzz script for search_proc
25
+ end.new
26
+ end
27
+
28
+
29
+ # Get the additional CLI arguments intended for the
30
+ # backend gen-AI processor.
31
+ def extract_extra_options
32
+ extra_index = @arguments.index('--')
33
+ if extra_index.nil?
34
+ @extra_options = []
35
+ else
36
+ @extra_options = @arguments.slice!(extra_index..-1)[1..]
37
+ end
38
+ end
39
+
40
+
41
+
42
+
43
+
44
+ end
@@ -0,0 +1,116 @@
1
+ # lib/aia/external_commands.rb
2
+
3
+ # TODO: move stuff associated with the CLI options for
4
+ # external commands to this module.
5
+ # Is the EDITOR considered an external command? Yes.
6
+
7
+ module AIA::ExternalCommands
8
+ TOOLS = {
9
+ 'fzf' => [ 'Command-line fuzzy finder written in Go',
10
+ 'https://github.com/junegunn/fzf'],
11
+
12
+ 'mods' => [ 'AI on the command-line',
13
+ 'https://github.com/charmbracelet/mods'],
14
+
15
+ 'rg' => [ 'Search tool like grep and The Silver Searcher',
16
+ 'https://github.com/BurntSushi/ripgrep']
17
+ }
18
+
19
+
20
+ HELP = <<~EOS
21
+ External Tools Used
22
+ -------------------
23
+
24
+ To install the external CLI programs used by aia:
25
+ brew install #{TOOLS.keys.join(' ')}
26
+
27
+ #{TOOLS.to_a.map{|t| t.join("\n ") }.join("\n\n")}
28
+
29
+ A text editor whose executable is setup in the
30
+ system environment variable 'EDITOR' like this:
31
+
32
+ export EDITOR="#{ENV['EDITOR']}"
33
+
34
+ EOS
35
+
36
+
37
+ # Setup the AI CLI program with necessary variables
38
+ def setup_external_programs
39
+ verify_external_tools
40
+
41
+ ai_default_opts = "-m #{MODS_MODEL} --no-limit "
42
+ ai_default_opts += "-f " if markdown?
43
+ @ai_options = ai_default_opts.dup
44
+
45
+
46
+ @ai_options += @extra_options.join(' ')
47
+
48
+ @ai_command = "#{AI_CLI_PROGRAM} #{@ai_options} "
49
+ end
50
+
51
+
52
+ # Check if the external tools are present on the system
53
+ def verify_external_tools
54
+ missing_tools = []
55
+
56
+ TOOLS.each do |tool, url|
57
+ path = `which #{tool}`.chomp
58
+ if path.empty? || !File.executable?(path)
59
+ missing_tools << { name: tool, url: url }
60
+ end
61
+ end
62
+
63
+ if missing_tools.any?
64
+ puts format_missing_tools_response(missing_tools)
65
+ end
66
+ end
67
+
68
+
69
+ def format_missing_tools_response(missing_tools)
70
+ response = <<~EOS
71
+
72
+ WARNING: #{MY_NAME} makes use of a few external CLI tools.
73
+ #{MY_NAME} may not respond as designed without these.
74
+
75
+ The following tools are missing on your system:
76
+
77
+ EOS
78
+
79
+ missing_tools.each do |tool|
80
+ response << " #{tool[:name]}: install from #{tool[:url]}\n"
81
+ end
82
+
83
+ response
84
+ end
85
+
86
+
87
+ # Build the command to interact with the AI CLI program
88
+ def build_command
89
+ command = @ai_command + %Q["#{@prompt.to_s}"]
90
+
91
+ @arguments.each do |input_file|
92
+ file_path = Pathname.new(input_file)
93
+ abort("File does not exist: #{input_file}") unless file_path.exist?
94
+ command += " < #{input_file}"
95
+ end
96
+
97
+ command
98
+ end
99
+
100
+
101
+ # Execute the command and log the results
102
+ def send_prompt_to_external_command
103
+ command = build_command
104
+
105
+ puts command if verbose?
106
+ @result = `#{command}`
107
+
108
+ if output.nil?
109
+ puts @result
110
+ else
111
+ output.write @result
112
+ end
113
+
114
+ @result
115
+ end
116
+ end
@@ -0,0 +1,22 @@
1
+ # lib/aia/logging.rb
2
+
3
+ module AIA::Logging
4
+ def log_result
5
+ return if log.nil?
6
+
7
+ f = File.open(log, "ab")
8
+
9
+ f.write <<~EOS
10
+ =======================================
11
+ == #{Time.now}
12
+ == #{@prompt.path}
13
+
14
+ PROMPT:
15
+ #{@prompt}
16
+
17
+ RESULT:
18
+ #{@result}
19
+
20
+ EOS
21
+ end
22
+ end
data/lib/aia/main.rb ADDED
@@ -0,0 +1,39 @@
1
+ # lib/aia/main.rb
2
+
3
+ module AIA ; end
4
+
5
+ require_relative 'configuration'
6
+
7
+ require_relative 'cli'
8
+ require_relative 'prompt_processing'
9
+ require_relative 'external_commands'
10
+ require_relative 'logging'
11
+
12
+ # Everything is being handled within the context
13
+ # of a single class.
14
+
15
+ class AIA::Main
16
+ include AIA::Configuration
17
+ include AIA::Cli
18
+ include AIA::PromptProcessing
19
+ include AIA::ExternalCommands
20
+ include AIA::Logging
21
+
22
+
23
+ def initialize(args= ARGV)
24
+ setup_configuration
25
+ setup_cli_options(args)
26
+ setup_external_programs
27
+ end
28
+
29
+
30
+ def call
31
+ show_usage if help?
32
+ show_version if version?
33
+
34
+ get_prompt
35
+ process_prompt
36
+ send_prompt_to_external_command
37
+ log_result unless log.nil?
38
+ end
39
+ end
@@ -0,0 +1,158 @@
1
+ # lib/aia/prompt_processing.rb
2
+
3
+ module AIA::PromptProcessing
4
+
5
+ # Fetch the first argument which should be the prompt id
6
+ def get_prompt
7
+ prompt_id = @arguments.shift
8
+
9
+ # TODO: or maybe go to a generic search and select process
10
+
11
+ abort("Please provide a prompt id") unless prompt_id
12
+
13
+ search_for_a_matching_prompt(prompt_id) unless existing_prompt?(prompt_id)
14
+ edit_prompt if edit?
15
+ end
16
+
17
+
18
+ # Check if a prompt with the given id already exists
19
+ def existing_prompt?(prompt_id)
20
+ @prompt = PromptManager::Prompt.get(id: prompt_id)
21
+ true
22
+ rescue ArgumentError
23
+ false
24
+ end
25
+
26
+
27
+ # Process the prompt's associated keywords and parameters
28
+ def process_prompt
29
+ unless @prompt.keywords.empty?
30
+ replace_keywords
31
+ @prompt.build
32
+ @prompt.save
33
+ end
34
+ end
35
+
36
+
37
+
38
+ def replace_keywords
39
+ print "\nQuit #{MY_NAME} with a CNTL-D or a CNTL-C\n\n"
40
+
41
+ defaults = @prompt.parameters
42
+
43
+ @prompt.keywords.each do |kw|
44
+ defaults[kw] = keyword_value(kw, defaults[kw])
45
+ end
46
+
47
+ @prompt.parameters = defaults
48
+ end
49
+
50
+
51
+
52
+
53
+ # query the user for a value to the keyword allow the
54
+ # reuse of the previous value shown as the default
55
+ def keyword_value(kw, default)
56
+ label = "Default: "
57
+ puts "Parameter #{kw} ..."
58
+ default_wrapped = default.wrap(indent: label.size)
59
+ default_wrapped[0..label.size] = label
60
+ puts default_wrapped
61
+
62
+ begin
63
+ a_string = Readline.readline("\n-=> ", false)
64
+ rescue Interrupt
65
+ a_string = nil
66
+ end
67
+
68
+ if a_string.nil?
69
+ puts "okay. Come back soon."
70
+ exit
71
+ end
72
+
73
+ puts
74
+ a_string.empty? ? default : a_string
75
+ end
76
+
77
+
78
+ # Search for a prompt with a matching id or keyword
79
+ def search_for_a_matching_prompt(prompt_id)
80
+ # TODO: using the rgfzf version of the search_proc should only
81
+ # return a single prompt_id
82
+ found_prompts = PromptManager::Prompt.search(prompt_id)
83
+
84
+ if found_prompts.empty?
85
+ if edit?
86
+ create_prompt(prompt_id)
87
+ edit_prompt
88
+ else
89
+ abort <<~EOS
90
+
91
+ No prompts where found for: #{prompt_id}
92
+ To create a prompt with this ID use the --edit option
93
+ like this:
94
+ #{MY_NAME} #{prompt_id} --edit
95
+
96
+ EOS
97
+ end
98
+ else
99
+ prompt_id = 1 == found_prompts.size ? found_prompts.first : handle_multiple_prompts(found_prompts, prompt_id)
100
+ @prompt = PromptManager::Prompt.get(id: prompt_id)
101
+ end
102
+ end
103
+
104
+
105
+ def handle_multiple_prompts(found_these, while_looking_for_this)
106
+ raise ArgumentError, "Argument is not an Array" unless found_these.is_a?(Array)
107
+
108
+ # TODO: Make this a class constant for defaults; make the header content
109
+ # a parameter so it can be varied.
110
+ fzf_options = [
111
+ "--tabstop=2", # 2 soaces for a tab
112
+ "--header='Prompt IDs which contain: #{while_looking_for_this}\nPress ESC to cancel.'",
113
+ "--header-first",
114
+ "--prompt='Search term: '",
115
+ '--delimiter :',
116
+ "--preview 'cat $PROMPTS_DIR/{1}.txt'",
117
+ "--preview-window=down:50%:wrap"
118
+ ].join(' ')
119
+
120
+
121
+ # Create a temporary file to hold the list of strings
122
+ temp_file = Tempfile.new('fzf-input')
123
+
124
+ begin
125
+ # Write all strings to the temp file
126
+ temp_file.puts(found_these)
127
+ temp_file.close
128
+
129
+ # Execute fzf command-line utility to allow selection
130
+ selected = `cat #{temp_file.path} | fzf #{fzf_options}`.strip
131
+
132
+ # Check if fzf actually returned a string; if not, return nil
133
+ result = selected.empty? ? nil : selected
134
+ ensure
135
+ # Ensure that the tempfile is closed and unlinked
136
+ temp_file.unlink
137
+ end
138
+
139
+ exit unless result
140
+
141
+ result
142
+ end
143
+
144
+
145
+ def create_prompt(prompt_id)
146
+ @prompt = PromptManager::Prompt.create(id: prompt_id)
147
+ # TODO: consider a configurable prompt template
148
+ # ERB ???
149
+ end
150
+
151
+
152
+ def edit_prompt
153
+ `#{EDITOR} #{@prompt.path}`
154
+ @options[:edit?][0] = false
155
+ @prompt = PromptManager::Prompt.get(id: @prompt.id)
156
+ end
157
+
158
+ end
data/lib/aia/version.rb CHANGED
@@ -1,5 +1,6 @@
1
+ # lib/aia/version.rb
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module AIA
4
- VERSION = "0.0.4"
5
+ VERSION = "0.0.5"
5
6
  end
data/lib/aia.rb CHANGED
@@ -1,397 +1,20 @@
1
1
  # lib/aia.rb
2
2
 
3
- require 'amazing_print'
4
3
  require 'pathname'
5
4
  require 'readline'
6
5
  require 'tempfile'
7
6
 
8
-
9
- require 'debug_me'
10
- include DebugMe
11
-
12
- $DEBUG_ME = true # ARGV.include?("--debug") || ARGV.include?("-d")
13
-
14
7
  require 'prompt_manager'
15
8
  require 'prompt_manager/storage/file_system_adapter'
16
9
 
17
10
  require_relative "aia/version"
11
+ require_relative "aia/main"
18
12
  require_relative "core_ext/string_wrap"
19
13
 
20
14
  module AIA
21
- class Main
22
- HOME = Pathname.new(ENV['HOME'])
23
- PROMPTS_DIR = Pathname.new(ENV['PROMPTS_DIR'] || (HOME + ".prompts_dir"))
24
-
25
- AI_CLI_PROGRAM = "mods"
26
- EDITOR = ENV['EDITOR'] || 'edit'
27
- MY_NAME = Pathname.new(__FILE__).basename.to_s.split('.')[0]
28
- MODS_MODEL = ENV['MODS_MODEL'] || 'gpt-4-1106-preview'
29
- OUTPUT = Pathname.pwd + "temp.md"
30
- PROMPT_LOG = PROMPTS_DIR + "_prompts.log"
31
-
32
-
33
- # TODO: write the usage text
34
- USAGE = <<~EOUSAGE
35
- AI Assistant (aia)
36
- ==================
37
-
38
- The AI cli program being used is: #{AI_CLI_PROGRAM}
39
-
40
- You can pass additional CLI options to #{AI_CLI_PROGRAM} like this:
41
- "#{MY_NAME} my options -- options for #{AI_CLI_PROGRAM}"
42
- EOUSAGE
43
-
44
-
45
- def initialize(args= ARGV)
46
- @prompt = nil
47
- @arguments = args
48
- @options = {
49
- edit?: false,
50
- debug?: false,
51
- verbose?: false,
52
- version?: false,
53
- help?: false,
54
- fuzzy?: false,
55
- markdown?: true,
56
- output: OUTPUT,
57
- log: PROMPT_LOG,
58
- }
59
- @extra_options = [] # intended for the backend AI processor
60
-
61
- build_reader_methods # for the @options keys
62
- process_arguments
63
-
64
- PromptManager::Prompt.storage_adapter =
65
- PromptManager::Storage::FileSystemAdapter.config do |config|
66
- config.prompts_dir = PROMPTS_DIR
67
- config.prompt_extension = '.txt'
68
- config.params_extension = '.json'
69
- config.search_proc = nil
70
- # TODO: add the rgfzz script for search_proc
71
- end.new
72
-
73
- setup_cli_program
74
- end
75
-
76
-
77
- def build_reader_methods
78
- @options.keys.each do |key|
79
- define_singleton_method(key) do
80
- @options[key]
81
- end
82
- end
83
- end
84
-
85
-
86
- def process_arguments
87
- @options.keys.each do |option|
88
- check_for option
89
- end
90
-
91
- # get the options meant for the backend AI command
92
- extract_extra_options
93
-
94
- bad_options = @arguments.select{|a| a.start_with?('-')}
95
-
96
- unless bad_options.empty?
97
- puts <<~EOS
98
-
99
- ERROR: Unknown options: #{bad_options.join(' ')}
100
-
101
- EOS
102
-
103
- show_usage
104
-
105
- exit
106
- end
107
- end
108
-
109
-
110
- def check_for(an_option)
111
- switches = [
112
- "--#{an_option}".gsub('?',''), # Dropping ? in case of a boolean
113
- "--no-#{an_option}".gsub('?',''),
114
- "-#{an_option.to_s[0]}" # SMELL: -v for both --verbose and --version
115
- ]
116
-
117
- process_option(an_option, switches)
118
- end
119
-
120
-
121
- def process_option(option_sym, switches)
122
- boolean = option_sym.to_s.end_with?('?')
123
-
124
- switches.each do |switch|
125
- if @arguments.include?(switch)
126
- index = @arguments.index(switch)
127
-
128
- if boolean
129
- @options[option_sym] = switch.include?('-no-') ? false : true
130
- @arguments.slice!(index,1)
131
- else
132
- if switch.include?('-no-')
133
- @option[option_sym] = nil
134
- @arguments.slice!(index,1)
135
- else
136
- @option[option_sym] = @arguments[index + 1]
137
- @arguments.slice!(index,2)
138
- end
139
- end
140
-
141
- break
142
- end
143
- end
144
- end
145
-
146
-
147
- def show_usage
148
- puts USAGE
149
- exit
150
- end
151
-
152
-
153
- def show_version
154
- puts VERSION
155
- exit
156
- end
157
-
158
-
159
- def call
160
- show_usage if help?
161
- show_version if version?
162
-
163
- prompt_id = get_prompt_id
164
-
165
- search_for_a_matching_prompt(prompt_id) unless existing_prompt?(prompt_id)
166
- process_prompt
167
- execute_and_log_command(build_command)
168
- end
169
-
170
-
171
- ####################################################
172
- private
173
-
174
- # Setup the AI CLI program with necessary variables
175
- def setup_cli_program
176
-
177
- ai_default_opts = "-m #{MODS_MODEL} --no-limit "
178
- ai_default_opts += "-f " if markdown?
179
- @ai_options = ai_default_opts.dup
180
-
181
-
182
- @ai_options += @extra_options.join(' ')
183
-
184
- @ai_command = "#{AI_CLI_PROGRAM} #{@ai_options} "
185
- end
186
-
187
-
188
- # Get the additional CLI arguments intended for the
189
- # backend gen-AI processor.
190
- def extract_extra_options
191
- extra_index = @arguments.index('--')
192
- if extra_index.nil?
193
- @extra_options = []
194
- else
195
- @extra_options = @arguments.slice!(extra_index..-1)[1..]
196
- end
197
- end
198
-
199
-
200
- # Fetch the first argument which should be the prompt id
201
- def get_prompt_id
202
- prompt_id = @arguments.shift
203
-
204
- # TODO: or maybe go to a search and select process
205
-
206
- abort("Please provide a prompt id") unless prompt_id
207
- prompt_id
208
- end
209
-
210
-
211
- # Check if a prompt with the given id already exists
212
- def existing_prompt?(prompt_id)
213
- @prompt = PromptManager::Prompt.get(id: prompt_id)
214
- true
215
- rescue ArgumentError
216
- false
217
- end
218
-
219
-
220
- # Process the prompt's associated keywords and parameters
221
- def process_prompt
222
- unless @prompt.keywords.empty?
223
- replace_keywords
224
- @prompt.build
225
- @prompt.save
226
- end
227
- end
228
-
229
-
230
- def replace_keywords
231
- print "\nQuit #{MY_NAME} with a CNTL-D or a CNTL-C\n\n"
232
-
233
- defaults = @prompt.parameters
234
-
235
- @prompt.keywords.each do |kw|
236
- defaults[kw] = keyword_value(kw, defaults[kw])
237
- end
238
-
239
- @prompt.parameters = defaults
240
- end
241
-
242
-
243
- # query the user for a value to the keyword allow the
244
- # reuse of the previous value shown as the default
245
- def keyword_value(kw, default)
246
- label = "Default: "
247
- puts "Parameter #{kw} ..."
248
- default_wrapped = default.wrap(indent: label.size)
249
- default_wrapped[0..label.size] = label
250
- puts default_wrapped
251
-
252
- begin
253
- a_string = Readline.readline("\n-=> ", false)
254
- rescue Interrupt
255
- a_string = nil
256
- end
257
-
258
- if a_string.nil?
259
- puts "okay. Come back soon."
260
- exit
261
- end
262
-
263
-
264
- puts
265
- a_string.empty? ? default : a_string
266
- end
267
-
268
-
269
- # Search for a prompt with a matching id or keyword
270
- def search_for_a_matching_prompt(prompt_id)
271
- # TODO: using the rgfzf version of the search_proc should only
272
- # return a single prompt_id
273
- found_prompts = PromptManager::Prompt.search(prompt_id)
274
- prompt_id = found_prompts.size == 1 ? found_prompts.first : handle_multiple_prompts(found_prompts, prompt_id)
275
- @prompt = PromptManager::Prompt.get(id: prompt_id)
276
- end
277
-
278
-
279
- def handle_multiple_prompts(found_these, while_looking_for_this)
280
- raise ArgumentError, "Argument is not an Array" unless found_these.is_a?(Array)
281
-
282
- # TODO: Make this a class constant for defaults; make the header content
283
- # a parameter so it can be varied.
284
- fzf_options = [
285
- "--tabstop=2", # 2 soaces for a tab
286
- "--header='Prompt IDs which contain: #{while_looking_for_this}\nPress ESC to cancel.'",
287
- "--header-first",
288
- "--prompt='Search term: '",
289
- '--delimiter :',
290
- "--preview 'cat $PROMPTS_DIR/{1}.txt'",
291
- "--preview-window=down:50%:wrap"
292
- ].join(' ')
293
-
294
-
295
- # Create a temporary file to hold the list of strings
296
- temp_file = Tempfile.new('fzf-input')
297
-
298
- begin
299
- # Write all strings to the temp file
300
- temp_file.puts(found_these)
301
- temp_file.close
302
-
303
- # Execute fzf command-line utility to allow selection
304
- selected = `cat #{temp_file.path} | fzf #{fzf_options}`.strip
305
-
306
- # Check if fzf actually returned a string; if not, return nil
307
- result = selected.empty? ? nil : selected
308
- ensure
309
- # Ensure that the tempfile is closed and unlinked
310
- temp_file.unlink
311
- end
312
-
313
- exit unless result
314
-
315
- result
316
- end
317
-
318
-
319
- # Build the command to interact with the AI CLI program
320
- def build_command
321
- command = @ai_command + %Q["#{@prompt.to_s}"]
322
-
323
- @arguments.each do |input_file|
324
- file_path = Pathname.new(input_file)
325
- abort("File does not exist: #{input_file}") unless file_path.exist?
326
- command += " < #{input_file}"
327
- end
328
-
329
- command
330
- end
331
-
332
-
333
- # Execute the command and log the results
334
- def execute_and_log_command(command)
335
- puts command if verbose?
336
- result = `#{command}`
337
- output.write result
338
-
339
- write_to_log(result) unless log.nil?
340
- end
341
-
342
-
343
- def write_to_log(answer)
344
- f = File.open(log, "ab")
345
-
346
- f.write <<~EOS
347
- =======================================
348
- == #{Time.now}
349
- == #{@prompt.path}
350
-
351
- PROMPT:
352
- #{@prompt}
353
-
354
- RESULT:
355
- #{answer}
356
-
357
- EOS
358
- end
15
+ def self.run(args=ARGV)
16
+ args = args.split(' ') if args.is_a?(String)
17
+ AIA::Main.new(args).call
359
18
  end
360
19
  end
361
20
 
362
-
363
- # Create an instance of the Main class and run the program
364
- AIA::Main.new.call if $PROGRAM_NAME == __FILE__
365
-
366
-
367
- __END__
368
-
369
-
370
- # TODO: Consider using this history process to preload the default
371
- # so that an up arrow will bring the previous answer into
372
- # the read buffer for line editing.
373
- # Instead of usin the .history file just push the default
374
- # value from the JSON file.
375
-
376
- while input = Readline.readline('> ', true)
377
- # Skip empty entries and duplicates
378
- if input.empty? || Readline::HISTORY.to_a[-2] == input
379
- Readline::HISTORY.pop
380
- end
381
- break if input == 'exit'
382
-
383
- # Do something with the input
384
- puts "You entered: #{input}"
385
-
386
- # Save the history in case you want to preserve it for the next sessions
387
- File.open('.history', 'a') { |f| f.puts(input) }
388
- end
389
-
390
- # Load history from file at the beginning of the program
391
- if File.exist?('.history')
392
- File.readlines('.history').each do |line|
393
- Readline::HISTORY.push(line.chomp)
394
- end
395
- end
396
-
397
-
@@ -0,0 +1,126 @@
1
+ ## Suggested Refactoring into Modules
2
+
3
+ ### ConfigurationModule
4
+
5
+ This module could encapsulate all the constants and environment-dependent settings.
6
+
7
+ ```ruby
8
+ module Configuration
9
+ HOME = Pathname.new(ENV['HOME'])
10
+ PROMPTS_DIR = Pathname.new(ENV['PROMPTS_DIR'] || (HOME + ".prompts_dir"))
11
+ AI_CLI_PROGRAM = "mods"
12
+ EDITOR = ENV['EDITOR'] || 'edit'
13
+ MY_NAME = Pathname.new(__FILE__).basename.to_s.split('.')[0]
14
+ MODS_MODEL = ENV['MODS_MODEL'] || 'gpt-4-1106-preview'
15
+ OUTPUT = Pathname.pwd + "temp.md"
16
+ PROMPT_LOG = PROMPTS_DIR + "_prompts.log"
17
+ USAGE = <<~EOUSAGE
18
+ AI Assistant (aia)
19
+ ==================
20
+ The AI cli program being used is: #{AI_CLI_PROGRAM}
21
+ You can pass additional CLI options to #{AI_CLI_PROGRAM} like this:
22
+ "#{MY_NAME} my options -- options for #{AI_CLI_PROGRAM}"
23
+ EOUSAGE
24
+ end
25
+ ```
26
+
27
+ ### OptionParsingModule
28
+
29
+ This module could manage the parsing of command-line arguments and configuring the options for the application.
30
+
31
+ ```ruby
32
+ module OptionParsing
33
+ def build_reader_methods
34
+ # ... method definition ...
35
+ end
36
+
37
+ def process_arguments
38
+ # ... method definition ...
39
+ end
40
+
41
+ def check_for(an_option)
42
+ # ... method definition ...
43
+ end
44
+
45
+ def process_option(option_sym, switches)
46
+ # ... method definition ...
47
+ end
48
+ end
49
+ ```
50
+
51
+ ### CommandLineInterfaceModule
52
+
53
+ This module would manage interactions with the command-line interface including editing of prompts and selection processes.
54
+
55
+ ```ruby
56
+ module CommandLineInterface
57
+ def keyword_value(kw, default)
58
+ # ... method definition ...
59
+ end
60
+
61
+ def handle_multiple_prompts(found_these, while_looking_for_this)
62
+ # ... method definition ...
63
+ end
64
+ end
65
+ ```
66
+
67
+ ### LoggingModule
68
+
69
+ Responsible for logging the results of the command.
70
+
71
+ ```ruby
72
+ module Logging
73
+ def write_to_log(answer)
74
+ # ... method definition ...
75
+ end
76
+ end
77
+ ```
78
+
79
+ ### AICommandModule
80
+
81
+ Manages the building and execution of the AI CLI command.
82
+
83
+ ```ruby
84
+ module AICommand
85
+ def setup_cli_program
86
+ # ... method definition ...
87
+ end
88
+
89
+ def build_command
90
+ # ... method definition ...
91
+ end
92
+
93
+ def execute_and_log_command(command)
94
+ # ... method definition ...
95
+ end
96
+ end
97
+ ```
98
+
99
+ ### PromptProcessingModule
100
+
101
+ Handles prompt retrieval, existing check, and keyword processing.
102
+
103
+ ```ruby
104
+ module PromptProcessing
105
+ def existing_prompt?(prompt_id)
106
+ # ... method definition ...
107
+ end
108
+
109
+ def process_prompt
110
+ # ... method definition ...
111
+ end
112
+
113
+ def replace_keywords
114
+ # ... method definition ...
115
+ end
116
+
117
+ def search_for_a_matching_prompt(prompt_id)
118
+ # ... method definition ...
119
+ end
120
+ end
121
+ ```
122
+
123
+ Each module should only contain the methods relevant to that module's purpose. After defining these modules, they can be included in the `AIA::Main` class where appropriate. Note that the method `get_prompt_id` didn't fit neatly into one of the outlined modules; it may remain in the main class or be included in a module if additional context becomes available or if it can be logically grouped with similar methods.
124
+
125
+ The `__END__` block and the Readline history management could be encapsulated into a separate module for terminal interactions if that block grows in complexity or moves out of the overall class definition.
126
+
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aia
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dewayne VanHoozer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-11-24 00:00:00.000000000 Z
11
+ date: 2023-11-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: prompt_manager
@@ -103,8 +103,15 @@ files:
103
103
  - bin/aia
104
104
  - bin/aia_completion.sh
105
105
  - lib/aia.rb
106
+ - lib/aia/cli.rb
107
+ - lib/aia/configuration.rb
108
+ - lib/aia/external_commands.rb
109
+ - lib/aia/logging.rb
110
+ - lib/aia/main.rb
111
+ - lib/aia/prompt_processing.rb
106
112
  - lib/aia/version.rb
107
113
  - lib/core_ext/string_wrap.rb
114
+ - lib/modularization_plan.md
108
115
  homepage: https://github.com/MadBomber/aia
109
116
  licenses:
110
117
  - MIT