gpterm 0.6.5 → 0.7.0

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: e024b6f050f77e130bee02f5a1e56df3bd510c79cd7dedb84ae26b196ae9d942
4
- data.tar.gz: dcbd7637acebad7bf14de4722eeb83a9bbb57cf5c52015529b07515e573dac3f
3
+ metadata.gz: a5d7e207cd381258fa26f30cad19add40fef4c0b3931971e9d7c131ec16734fd
4
+ data.tar.gz: d3c472cf2ac8f979616f2863310aa9fd8ea083384c85ac6a78eaba5397a15899
5
5
  SHA512:
6
- metadata.gz: 74f6fe4934dc5060c52085d2483a74dc98595c3208aa6917b45f2d47883c72c4b47e37670e9750745f7eb252233d636a2442afe0fa70f0b75b1970907b70780b
7
- data.tar.gz: e1bad1a098380d95e1855ddfd413b2300a9224e6bca5db6aad5f4d8938d7b0356e34e6bd9fc21d7d47544dcbe5c692d67912ac5fff1f801eb239ba0c0c3dffa6
6
+ metadata.gz: 984be150518b40b63ba73a873d0389801c5ab45069be710c2373baa5fe324c538694a5a040c57218388ea0200a3ec38a6d5c2e04d2db803a9e9d91f543781d0e
7
+ data.tar.gz: 461c55256fc8f71ac54422c95022d921a172f79ec4ef4e59dd31df2d5a123c3d61fcd3013fb5d597701a37285202596342c8b47d768756fb51c8ef485639a868
data/config/prompts.yml CHANGED
@@ -116,3 +116,5 @@ goal_commands: |
116
116
  - $$cannot_compute$$ - You cannot create a VALID response to this prompt. The user will be asked to provide a new prompt.
117
117
 
118
118
  COMMANDS:
119
+ refine_commands: |
120
+ The last response needs some changes. Please take into account this prompt from the user, then refine the commands you provided in the last response.
data/lib/app_config.rb ADDED
@@ -0,0 +1,69 @@
1
+ require 'colorize'
2
+ require 'yaml'
3
+
4
+ require_relative 'input'
5
+
6
+ module AppConfig
7
+ CONFIG_FILE = File.join(Dir.home, '.gpterm', 'config.yml').freeze
8
+
9
+ def self.load
10
+ # Check if the directory exists, if not, create it
11
+ unless File.directory?(File.dirname(CONFIG_FILE))
12
+ Dir.mkdir(File.dirname(CONFIG_FILE))
13
+ end
14
+
15
+ unless File.exist?(self::CONFIG_FILE)
16
+ puts 'Welcome to gpterm! It looks like this is your first time using this application.'.colorize(:magenta)
17
+
18
+ new_config = {}
19
+ puts "Before we get started, we need to configure the application. All the info you provide will be saved in #{self::CONFIG_FILE}.".colorize(:magenta)
20
+
21
+ puts "Enter your OpenAI API key's \"SECRET KEY\" value then hit return: ".colorize(:yellow)
22
+ new_config['openapi_key'] = Input.non_empty
23
+
24
+ puts "Your PATH environment variable is: #{ENV['PATH']}".colorize(:magenta)
25
+ puts 'Are you happy for your PATH to be sent to OpenAI to help with command generation? (Y/n then hit return) '.colorize(:yellow)
26
+
27
+ input = Input.yes_or_no
28
+
29
+ if input == 'y'
30
+ new_config['send_path'] = true
31
+ else
32
+ new_config['send_path'] = false
33
+ end
34
+
35
+ default_model = 'gpt-4-turbo-preview'
36
+
37
+ puts "The default model is #{default_model}. If you would like to change it please enter the name of your preferred model:".colorize(:yellow)
38
+ new_config['model'] = STDIN.gets.chomp.strip || default_model
39
+
40
+ self.save_config(new_config)
41
+
42
+ puts "Configuration saved to #{self::CONFIG_FILE}".colorize(:green)
43
+
44
+ new_config
45
+ else
46
+ self.load_config_from_file
47
+ end
48
+ end
49
+
50
+ def self.load_config_from_file
51
+ YAML.load_file(CONFIG_FILE)
52
+ end
53
+
54
+ def self.save_config(config)
55
+ File.write(CONFIG_FILE, config.to_yaml)
56
+ end
57
+
58
+ def self.add_openapi_key(config, openapi_key)
59
+ config['openapi_key'] = openapi_key
60
+ save_config(config)
61
+ end
62
+
63
+ def self.add_preset(config, preset_name, preset_prompt)
64
+ # This is a YAML file so we need to make sure the presets key exists
65
+ config['presets'] ||= {}
66
+ config['presets'][preset_name] = preset_prompt
67
+ save_config(config)
68
+ end
69
+ end
@@ -1,13 +1,12 @@
1
- require "openai"
2
1
  require 'yaml'
3
2
 
4
- class Client
3
+ class CommandGenerator
5
4
  attr_reader :openai_client
6
5
  attr_reader :config
7
6
 
8
- def initialize(config)
7
+ def initialize(config, openai_client)
9
8
  @config = config
10
- @openai_client = OpenAI::Client.new(access_token: config["openapi_key"])
9
+ @openai_client = openai_client
11
10
  @prompts = YAML.load_file(File.join(__dir__, '..', 'config', 'prompts.yml'))
12
11
  end
13
12
 
@@ -74,6 +73,19 @@ class Client
74
73
  continue_conversation(goal_commands_prompt)
75
74
  end
76
75
 
76
+ def refine_last_response(prompt)
77
+ refinement_prompt = @prompts["refine_commands"]
78
+
79
+ refinement_prompt += <<~PROMPT
80
+
81
+ #{prompt}
82
+
83
+ COMMANDS:
84
+ PROMPT
85
+
86
+ continue_conversation(refinement_prompt)
87
+ end
88
+
77
89
  private
78
90
 
79
91
  def continue_conversation(prompt)
data/lib/gpterm.rb CHANGED
@@ -1,9 +1,11 @@
1
- require 'optparse'
2
1
  require 'colorize'
3
2
  require 'open3'
3
+ require "openai"
4
4
 
5
- require_relative 'config'
6
- require_relative 'client'
5
+ require_relative 'app_config'
6
+ require_relative 'command_generator'
7
+ require_relative 'parse_options'
8
+ require_relative 'input'
7
9
 
8
10
  # The colours work like this:
9
11
  # - Output from STDOUT or STDERR is default
@@ -14,9 +16,10 @@ require_relative 'client'
14
16
 
15
17
  class GPTerm
16
18
  def initialize
17
- @config = load_config
18
- @options = parse_options
19
- @client = Client.new(@config)
19
+ @config = AppConfig.load
20
+ @options = ParseOptions.call(@config)
21
+ @openai_client = OpenAI::Client.new(access_token: @config["openapi_key"])
22
+ @command_generator = CommandGenerator.new(@config, @openai_client)
20
23
  end
21
24
 
22
25
  def run
@@ -47,93 +50,35 @@ class GPTerm
47
50
  exit
48
51
  end
49
52
 
50
- # Ensures the user enters "y" or "n"
51
- def get_yes_or_no
52
- input = STDIN.gets.chomp.downcase
53
- while ['y', 'n'].include?(input) == false
54
- puts 'Please enter "y/Y" or "n/N":'.colorize(:yellow)
55
- input = STDIN.gets.chomp.downcase
56
- end
57
- input
58
- end
59
-
60
- # Ensures the user enters a non-empty value
61
- def get_non_empty_input
62
- input = STDIN.gets.chomp.strip
63
- while input.length == 0
64
- puts 'Please enter a non-empty value:'.colorize(:yellow)
65
- input = STDIN.gets.chomp.strip
66
- end
67
- input
68
- end
69
-
70
53
  def start_conversation(prompt)
71
- message = @client.first_prompt(prompt)
54
+ shell_output = gather_information_from_shell(prompt)
72
55
 
73
- if message.downcase == '$$cannot_compute$$'
74
- exit_with_message('Sorry, a command could not be generated for that prompt. Try another.', :red)
75
- end
76
-
77
- if message.downcase == '$$no_gathering_needed$$'
78
- puts 'No information gathering needed'.colorize(:magenta)
79
- output = "No information gathering was needed."
80
- elsif message.downcase == '$$cannot_compute$$'
81
- exit_with_message('Sorry, a command could not be generated for that prompt. Try another.', :red)
82
- else
83
- puts 'Information gathering command:'.colorize(:magenta)
84
- puts message.gsub(/^/, "#{" $".colorize(:blue)} ")
85
- puts 'Do you want to execute this command? (Y/n then hit return)'.colorize(:yellow)
86
- continue = get_yes_or_no
56
+ offer_prompt_response = @command_generator.offer_information_prompt(shell_output, :shell_output_response)
87
57
 
88
- unless continue.downcase == 'y'
89
- exit
90
- end
91
-
92
- puts 'Running command...'
93
- output = `#{message}`
94
-
95
- if @config[:verbose]
96
- puts 'Output:'
97
- puts output
98
- end
99
- end
100
-
101
- output = @client.offer_information_prompt(output, :shell_output_response)
102
-
103
- while output.downcase != '$$no_more_information_needed$$'
58
+ while offer_prompt_response.downcase != '$$no_more_information_needed$$'
104
59
  puts "You have been asked to provide more information with this command:".colorize(:magenta)
105
- puts output.gsub(/^/, "#{" >".colorize(:blue)} ")
60
+ puts offer_prompt_response.gsub(/^/, "#{" >".colorize(:blue)} ")
106
61
  puts "What is your response? (Type 'skip' to skip this step and force the final command to be generated)".colorize(:yellow)
107
62
 
108
- response = get_non_empty_input
63
+ user_question_response = Input.non_empty
109
64
 
110
- if response.downcase == 'skip'
111
- output = '$$no_more_information_needed$$'
65
+ if user_question_response.downcase == 'skip'
66
+ offer_prompt_response = '$$no_more_information_needed$$'
67
+ puts 'Thanks, one moment please...'.colorize(:magenta)
112
68
  else
113
- output = @client.offer_information_prompt(response, :question_response)
69
+ offer_prompt_response = @command_generator.offer_information_prompt(user_question_response, :question_response)
114
70
  end
115
71
  end
116
72
 
117
- puts 'Requesting the next command...'.colorize(:magenta)
118
-
119
- message = @client.final_prompt(output)
120
-
121
- if message.downcase == '$$cannot_compute$$'
122
- exit_with_message('Sorry, a command could not be generated for that prompt. Try another.', :red)
123
- end
124
-
125
- puts 'Generated command to accomplish your goal:'.colorize(:magenta)
126
- puts message.gsub(/^/, "#{" $".colorize(:green)} ")
127
-
128
- puts 'Do you want to execute this command? (Y/n then hit return)'.colorize(:yellow)
73
+ puts 'Requesting the next command...'.colorize(:magenta) if @config[:verbose]
129
74
 
130
- continue = get_yes_or_no
75
+ # TODO: Some redundant info being passed here in the case of
76
+ # the user answering a question
77
+ goal_prompt_response = @command_generator.final_prompt(offer_prompt_response)
131
78
 
132
- unless continue.downcase == 'y'
133
- exit
134
- end
79
+ goal_prompt_response = run_refinement_loop(goal_prompt_response)
135
80
 
136
- commands = message.split("\n")
81
+ commands = goal_prompt_response.split("\n")
137
82
 
138
83
  commands.each do |command|
139
84
  stdout, stderr, exit_status = execute_shell_command(command)
@@ -149,101 +94,58 @@ class GPTerm
149
94
  end
150
95
  end
151
96
 
152
- def load_config
153
- unless File.exist?(AppConfig::CONFIG_FILE)
154
- puts 'Welcome to gpterm! It looks like this is your first time using this application.'.colorize(:magenta)
155
-
156
- new_config = {}
157
- puts "Before we get started, we need to configure the application. All the info you provide will be saved in #{AppConfig::CONFIG_FILE}.".colorize(:magenta)
97
+ def gather_information_from_shell(prompt)
98
+ info_prompt_response = @command_generator.first_prompt(prompt)
158
99
 
159
- puts "Enter your OpenAI API key's \"SECRET KEY\" value then hit return: ".colorize(:yellow)
160
- new_config['openapi_key'] = get_non_empty_input
161
-
162
- puts "Your PATH environment variable is: #{ENV['PATH']}".colorize(:magenta)
163
- puts 'Are you happy for your PATH to be sent to OpenAI to help with command generation? (Y/n then hit return) '.colorize(:yellow)
100
+ if info_prompt_response.downcase == '$$no_gathering_needed$$'
101
+ puts 'No information gathering needed'.colorize(:magenta) if @config[:verbose]
102
+ shell_output = nil
103
+ else
104
+ info_prompt_response = run_refinement_loop(info_prompt_response, 'gather more information for your request')
164
105
 
165
- input = get_yes_or_no
106
+ puts 'Running command...' if @config[:verbose]
107
+ shell_output = `#{info_prompt_response}`
166
108
 
167
- if input == 'y'
168
- new_config['send_path'] = true
169
- else
170
- new_config['send_path'] = false
109
+ if @config[:verbose]
110
+ puts 'Shell output:'
111
+ puts shell_output
171
112
  end
172
113
 
173
- default_model = 'gpt-4-turbo-preview'
114
+ end
115
+ shell_output
116
+ end
174
117
 
175
- puts "The default model is #{default_model}. If you would like to change it please enter the name of your preferred model:".colorize(:yellow)
176
- new_config['model'] = STDIN.gets.chomp.strip || default_model
118
+ def run_refinement_loop(original_response, purpose = 'accomplish your goal')
119
+ if original_response.downcase == '$$cannot_compute$$'
120
+ exit_with_message('Sorry, a command could not be generated for that prompt. Try another.', :red)
121
+ end
177
122
 
178
- AppConfig.save_config(new_config)
123
+ refined_response = original_response.dup
179
124
 
180
- puts "Configuration saved to #{AppConfig::CONFIG_FILE}".colorize(:green)
125
+ puts "The following command(s) were generated to #{purpose}:".colorize(:magenta)
126
+ puts refined_response.gsub(/^/, "#{" $".colorize(:green)} ")
127
+ puts 'Do you want to execute this/these command(s), or refine them with another prompt? (Y/n/r then hit return)'.colorize(:yellow)
128
+ continue = Input.yes_no_or_refine
181
129
 
182
- new_config
183
- else
184
- AppConfig.load_config
185
- end
186
- end
130
+ while continue.downcase == 'r'
131
+ puts 'Please enter a new prompt:'.colorize(:yellow)
132
+ new_prompt = Input.non_empty
133
+ refined_response = @command_generator.refine_last_response(new_prompt)
187
134
 
188
- def parse_options
189
- options = {}
190
- subcommands = {
191
- 'preset' => {
192
- option_parser: OptionParser.new do |opts|
193
- opts.banner = "gpterm preset <name> <prompt>"
194
- end,
195
- argument_parser: ->(args) {
196
- if args.length < 2
197
- options[:prompt] = @config['presets'][args[0]]
198
- else
199
- options[:preset_prompt] = [args[0], args[1]]
200
- end
201
- }
202
- },
203
- 'config' => {
204
- option_parser: OptionParser.new do |opts|
205
- opts.banner = "gpterm config [--openapi_key <value>|--send_path <true|false>]"
206
- opts.on("--openapi_key VALUE", "Set the OpenAI API key") do |v|
207
- AppConfig.add_openapi_key(@config, v)
208
- exit_with_message("OpenAI API key saved")
209
- end
210
- opts.on("--send_path", "Send the PATH environment variable to OpenAI") do
211
- @config['send_path'] = true
212
- AppConfig.save_config(@config)
213
- exit_with_message("Your PATH environment variable will be sent to OpenAI to help with command generation")
214
- end
215
- end
216
- }
217
- }
218
-
219
- main = OptionParser.new do |opts|
220
- opts.banner = "Usage:"
221
- opts.banner += "\n\ngpterm <prompt> [options] [subcommand [options]]"
222
- opts.banner += "\n\nSubcommands:"
223
- subcommands.each do |name, subcommand|
224
- opts.banner += "\n #{name} - #{subcommand[:option_parser].banner}"
225
- end
226
- opts.banner += "\n\nOptions:"
227
- opts.on("-v", "--verbose", "Run verbosely") do |v|
228
- options[:verbose] = true
135
+ if refined_response.downcase == '$$cannot_compute$$'
136
+ exit_with_message('Sorry, a command could not be generated for that prompt. Try another.', :red)
229
137
  end
230
- end
231
138
 
232
- command = ARGV.shift
139
+ puts "The following command(s) were generated to #{purpose}:".colorize(:magenta)
140
+ puts refined_response.gsub(/^/, "#{" $".colorize(:blue)} ")
141
+ puts 'Do you want to execute this/these command(s), or refine them with another prompt? (Y/n/r then hit return)'.colorize(:yellow)
142
+ continue = Input.yes_no_or_refine
143
+ end
233
144
 
234
- main.order!
235
- if subcommands.key?(command)
236
- subcommands[command][:option_parser].parse!
237
- subcommands[command][:argument_parser].call(ARGV) if subcommands[command][:argument_parser]
238
- elsif command == 'help'
239
- exit_with_message(main)
240
- elsif command
241
- options[:prompt] = command
242
- else
243
- puts 'Enter a prompt to generate text from:'.colorize(:yellow)
244
- options[:prompt] = get_non_empty_input
145
+ unless continue.downcase == 'y'
146
+ exit
245
147
  end
246
148
 
247
- options
149
+ refined_response
248
150
  end
249
151
  end
data/lib/input.rb ADDED
@@ -0,0 +1,31 @@
1
+ module Input
2
+ # Ensures the user enters a non-empty value
3
+ def self.non_empty
4
+ input = STDIN.gets.chomp.strip
5
+ while input.length == 0
6
+ puts 'Please enter a non-empty value:'.colorize(:yellow)
7
+ input = STDIN.gets.chomp.strip
8
+ end
9
+ input
10
+ end
11
+
12
+ # Ensures the user enters "y" or "n"
13
+ def self.yes_or_no
14
+ input = STDIN.gets.chomp.downcase
15
+ while ['y', 'n'].include?(input) == false
16
+ puts 'Please enter "y/Y" or "n/N":'.colorize(:yellow)
17
+ input = STDIN.gets.chomp.downcase
18
+ end
19
+ input
20
+ end
21
+
22
+ # Ensures the user enters "y" or "n"
23
+ def self.yes_no_or_refine
24
+ input = STDIN.gets.chomp.downcase
25
+ while ['y', 'n', 'r'].include?(input) == false
26
+ puts 'Please enter "y/Y", "n/N" or "r/R":'.colorize(:yellow)
27
+ input = STDIN.gets.chomp.downcase
28
+ end
29
+ input
30
+ end
31
+ end
@@ -0,0 +1,65 @@
1
+ require 'optparse'
2
+
3
+ class ParseOptions
4
+ def self.call(config)
5
+ options = {}
6
+ subcommands = {
7
+ 'preset' => {
8
+ option_parser: OptionParser.new do |opts|
9
+ opts.banner = "gpterm preset <name> <prompt>"
10
+ end,
11
+ argument_parser: ->(args) {
12
+ if args.length < 2
13
+ options[:prompt] = config['presets'][args[0]]
14
+ else
15
+ options[:preset_prompt] = [args[0], args[1]]
16
+ end
17
+ }
18
+ },
19
+ 'config' => {
20
+ option_parser: OptionParser.new do |opts|
21
+ opts.banner = "gpterm config [--openapi_key <value>|--send_path <true|false>]"
22
+ opts.on("--openapi_key VALUE", "Set the OpenAI API key") do |v|
23
+ AppConfig.add_openapi_key(config, v)
24
+ exit_with_message("OpenAI API key saved")
25
+ end
26
+ opts.on("--send_path", "Send the PATH environment variable to OpenAI") do
27
+ config['send_path'] = true
28
+ AppConfig.save_config(config)
29
+ exit_with_message("Your PATH environment variable will be sent to OpenAI to help with command generation")
30
+ end
31
+ end
32
+ }
33
+ }
34
+
35
+ main = OptionParser.new do |opts|
36
+ opts.banner = "Usage:"
37
+ opts.banner += "\n\ngpterm <prompt> [options] [subcommand [options]]"
38
+ opts.banner += "\n\nSubcommands:"
39
+ subcommands.each do |name, subcommand|
40
+ opts.banner += "\n #{name} - #{subcommand[:option_parser].banner}"
41
+ end
42
+ opts.banner += "\n\nOptions:"
43
+ opts.on("-v", "--verbose", "Run verbosely") do |v|
44
+ options[:verbose] = true
45
+ end
46
+ end
47
+
48
+ command = ARGV.shift
49
+
50
+ main.order!
51
+ if subcommands.key?(command)
52
+ subcommands[command][:option_parser].parse!
53
+ subcommands[command][:argument_parser].call(ARGV) if subcommands[command][:argument_parser]
54
+ elsif command == 'help'
55
+ exit_with_message(main)
56
+ elsif command
57
+ options[:prompt] = command
58
+ else
59
+ puts 'Enter a prompt to generate text from:'.colorize(:yellow)
60
+ options[:prompt] = Input.non_empty
61
+ end
62
+
63
+ options
64
+ end
65
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gpterm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.5
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dan Hough
@@ -51,9 +51,11 @@ files:
51
51
  - README.md
52
52
  - bin/gpterm
53
53
  - config/prompts.yml
54
- - lib/client.rb
55
- - lib/config.rb
54
+ - lib/app_config.rb
55
+ - lib/command_generator.rb
56
56
  - lib/gpterm.rb
57
+ - lib/input.rb
58
+ - lib/parse_options.rb
57
59
  homepage: https://github.com/basicallydan/gpterm
58
60
  licenses:
59
61
  - MIT
data/lib/config.rb DELETED
@@ -1,38 +0,0 @@
1
- require 'yaml'
2
-
3
- module AppConfig
4
- CONFIG_FILE = File.join(Dir.home, '.gpterm', 'config.yml').freeze
5
-
6
- # Check if the directory exists, if not, create it
7
- unless File.directory?(File.dirname(CONFIG_FILE))
8
- Dir.mkdir(File.dirname(CONFIG_FILE))
9
- end
10
-
11
- def self.load_config
12
- YAML.load_file(CONFIG_FILE)
13
- rescue Errno::ENOENT
14
- default_config
15
- end
16
-
17
- def self.save_config(config)
18
- File.write(CONFIG_FILE, config.to_yaml)
19
- end
20
-
21
- def self.add_openapi_key(config, openapi_key)
22
- config['openapi_key'] = openapi_key
23
- save_config(config)
24
- end
25
-
26
- def self.add_preset(config, preset_name, preset_prompt)
27
- # This is a YAML file so we need to make sure the presets key exists
28
- config['presets'] ||= {}
29
- config['presets'][preset_name] = preset_prompt
30
- save_config(config)
31
- end
32
-
33
- def self.default_config
34
- {
35
- 'openapi_key' => ''
36
- }
37
- end
38
- end