aia 0.0.3 → 0.0.5

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ac671687f74fc804d119d8492ba0975cee0808eeeb0e6a1bd8943076d70c0472
4
- data.tar.gz: f18d49026bfeae63f44c036d5f76b350c070a0817a8326fbf5ee0aa2ddce3e54
3
+ metadata.gz: a080fd671c1e5e3f9420f036892afb69a8d4190745da8bbce82620356ae2ad10
4
+ data.tar.gz: ffe746cbcb3a24216f39f4ec220b962828ab925041fe6f97f006676988006690
5
5
  SHA512:
6
- metadata.gz: 89fc8032f90b88538e109de44ffed06c87443bae64ce0f7cd7a450d549004ca8facc49de6456e8af3a5ae72775ef43f46e008b18d6f4a961b7cb815a9a3cc8bc
7
- data.tar.gz: 0f796a27dc7e8531f3752f24f9c2c442724cc2102d0c6b192ff249edce74cf5701475d99e1a62e339f9de12c83dd404f09ec0d99f9d3a5ba13ca066c2b6c4929
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
- require_relative '../lib/aia'
5
-
6
- # Create an instance of the Main class and run the program
7
- AIA::Main.new.call
4
+ require 'aia'
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.3"
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.3
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