ask-ai 0.0.1
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 +7 -0
- data/bin/aa +5 -0
- data/config/config.yml +1 -0
- data/files/context.jsonl +0 -0
- data/files/context_file.txt +0 -0
- data/lib/context.rb +79 -0
- data/lib/handle_args.rb +61 -0
- data/lib/help.rb +48 -0
- data/lib/main.rb +168 -0
- data/lib/prompt.rb +102 -0
- metadata +66 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f66251ade1f9986ae077dc13cb177f5f651b5c3638115fb7b5a5ef54d9510972
|
4
|
+
data.tar.gz: ec6673e17d5cdd7e6e202ad33d87f30b4e132afdbde347c67917aee6a7483291
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c4c71fbb474ebbea5684fc849007f04af964c105ae9a8827790da443a4c45168b59ba5c49b90644bdd8d3f46ca03d5ad31535bf244eace5b00d017f43aae36e1
|
7
|
+
data.tar.gz: 69dea278d20a873ba3a91f93dd41e97d148e754cf654ac32c6ca3ad168bc57a66bd3816db23aa38362bf81a18be3453d08e2201d1ec134ed1230b5a792f70000
|
data/bin/aa
ADDED
data/config/config.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
|
data/files/context.jsonl
ADDED
File without changes
|
File without changes
|
data/lib/context.rb
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
class Context
|
2
|
+
def self.load_context()
|
3
|
+
if File.exist?(CONTEXT_PATH)
|
4
|
+
conversation = File.readlines(CONTEXT_PATH).map { |line| JSON.parse(line) }
|
5
|
+
else
|
6
|
+
conversation = []
|
7
|
+
end
|
8
|
+
|
9
|
+
if conversation.length > 0
|
10
|
+
context_as_string = "This is our previous conversation:\n"
|
11
|
+
else
|
12
|
+
context_as_string = ""
|
13
|
+
end
|
14
|
+
|
15
|
+
if conversation.length > 0
|
16
|
+
conversation.each_with_index do |v, i|
|
17
|
+
context_as_string += "My #{i + 1} input was: #{v['input']}\nYour #{i + 1} response was: #{v['response']}\n"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
return context_as_string
|
22
|
+
end
|
23
|
+
|
24
|
+
## Here we save the context to a file.
|
25
|
+
## Max 10 previous Q / A to save tokens.
|
26
|
+
def self.save_context(context)
|
27
|
+
tmp_arr = []
|
28
|
+
unless File.exist?(CONTEXT_PATH)
|
29
|
+
File.open(CONTEXT_PATH, "w") {}
|
30
|
+
end
|
31
|
+
File.readlines(CONTEXT_PATH).map { |line| tmp_arr.push(JSON.parse(line)) }
|
32
|
+
if tmp_arr.length > 9
|
33
|
+
tmp_arr.shift()
|
34
|
+
end
|
35
|
+
File.truncate(CONTEXT_PATH, 0)
|
36
|
+
tmp_arr.each { |line| File.open(CONTEXT_PATH, "a") { |file| file.write("#{line.to_json}\n") } }
|
37
|
+
File.open(CONTEXT_PATH, "a") { |file| file.write("#{context.to_json}\n") }
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.delete_context()
|
41
|
+
puts "Deleting previous context."
|
42
|
+
File.truncate(CONTEXT_PATH, 0)
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.save_context_file(file_path)
|
46
|
+
unless file_path.nil?
|
47
|
+
file_in = File.open(file_path, 'r')
|
48
|
+
file_out = File.open(CONTEXT_FILE_PATH, 'w')
|
49
|
+
char_count = 0
|
50
|
+
file_in.each do |line|
|
51
|
+
char_count += line.length
|
52
|
+
file_out.write(line)
|
53
|
+
end
|
54
|
+
|
55
|
+
if char_count > 10000
|
56
|
+
puts "Warning: The file you are trying to feed to the API is #{char_count} characters long. This consumes a lot of tokens."
|
57
|
+
end
|
58
|
+
else
|
59
|
+
puts "No file path given."
|
60
|
+
end
|
61
|
+
rescue Errno::ENOENT
|
62
|
+
puts "No file at '#{file_path}' found."
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.load_context_file()
|
66
|
+
file = File.open(CONTEXT_FILE_PATH, 'r')
|
67
|
+
file_as_string = ""
|
68
|
+
file.each do |line|
|
69
|
+
file_as_string += line
|
70
|
+
end
|
71
|
+
|
72
|
+
return file_as_string
|
73
|
+
rescue Errno::ENOENT
|
74
|
+
puts "No file at '#{CONTEXT_FILE_PATH}' found."
|
75
|
+
puts "Load a file with 'aa -lf <file_path>'"
|
76
|
+
return ""
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
data/lib/handle_args.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
class HandleArgs
|
2
|
+
def self.permitted_options()
|
3
|
+
## TODO: Add more options.
|
4
|
+
## -t --translate
|
5
|
+
## -temp --temperature (need to handle value after input)
|
6
|
+
|
7
|
+
[
|
8
|
+
'-h',
|
9
|
+
'--help',
|
10
|
+
'-v',
|
11
|
+
'--version',
|
12
|
+
'-f',
|
13
|
+
'--file',
|
14
|
+
'-lf',
|
15
|
+
'--loadfile',
|
16
|
+
'-d',
|
17
|
+
'--delete',
|
18
|
+
'-c',
|
19
|
+
'--conversation',
|
20
|
+
'--finetune',
|
21
|
+
'-w', '--whisper',
|
22
|
+
'-t', '--translate',
|
23
|
+
'-i', '--interactive',
|
24
|
+
'--key',
|
25
|
+
]
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.handle_args()
|
29
|
+
p_args = HandleArgs.permitted_options()
|
30
|
+
## This is the hash that will be returned.
|
31
|
+
args_hash = {}
|
32
|
+
|
33
|
+
## This handles args then called as ruby script.
|
34
|
+
ARGV.each_with_index do |arg, i|
|
35
|
+
#puts "Arg: #{arg}"
|
36
|
+
|
37
|
+
if arg.start_with?('-')
|
38
|
+
## This is an option.
|
39
|
+
if p_args.include?(arg)
|
40
|
+
## This is a permitted / available option.
|
41
|
+
#puts "Option: #{arg}"
|
42
|
+
args_hash["option_#{i}"] = arg
|
43
|
+
else
|
44
|
+
## This is an unknown option.
|
45
|
+
## TODO: Handle unknown option. display help? discard?
|
46
|
+
puts "Unknown option: #{arg}"
|
47
|
+
args_hash["option_#{i}"] = arg
|
48
|
+
end
|
49
|
+
else
|
50
|
+
## This is input.
|
51
|
+
## This if statement append all 'input' args to one string.
|
52
|
+
if args_hash["input"].nil?
|
53
|
+
args_hash["input"] = arg
|
54
|
+
else
|
55
|
+
args_hash["input"] = "#{args_hash['input']} #{arg}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
return args_hash
|
60
|
+
end
|
61
|
+
end
|
data/lib/help.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
class Help
|
4
|
+
def self.display_help()
|
5
|
+
puts "Usage: aa [options] [input]"
|
6
|
+
puts " -lf, --loadfile <path>: Load file into context"
|
7
|
+
puts " -f, --file: Read from context file"
|
8
|
+
puts " -c, --conversation: Append to conversation (max 10 Questions / Answers pairs saved)"
|
9
|
+
puts " -d, --delete: Delete conversation"
|
10
|
+
puts " -i, --interactive: Interactive mode, always a conversation. Clear context with 'clear' (exit with 'exit' or 'quit')"
|
11
|
+
puts " -w, --whisper <path>: Transcribe audio file"
|
12
|
+
puts " -t, --translate <path>: Translate audio file"
|
13
|
+
puts "\n Options:"
|
14
|
+
puts " --config: Edit config file"
|
15
|
+
puts " -v, --version: Display version"
|
16
|
+
puts " -h, --help: Display this help message"
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.display_api_key()
|
21
|
+
puts "You need to set your API key in the file: ./config/config.yml"
|
22
|
+
puts "Create the file if it doesn't exist."
|
23
|
+
puts "Add the following line to the file:"
|
24
|
+
puts " OPENAI_API_KEY: <your API key>"
|
25
|
+
puts "You can get your API key from: https://openai.com/"
|
26
|
+
end
|
27
|
+
|
28
|
+
## This don't work yet. Need to rework the way we handle args.
|
29
|
+
def self.display_help_file()
|
30
|
+
## TODO: How to work with files
|
31
|
+
end
|
32
|
+
|
33
|
+
## This don't work yet. Need to rework the way we handle args.
|
34
|
+
def self.display_help_conversation()
|
35
|
+
## TODO: How to work with conversation
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.display_version()
|
39
|
+
spec = Gem::Specification::load("aa.gemspec")
|
40
|
+
puts "Version: #{spec.version}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.display_usage()
|
44
|
+
puts "Usage: ./main.rb [options] [input]"
|
45
|
+
|
46
|
+
puts "There are two types of options, flags and arguments."
|
47
|
+
end
|
48
|
+
end
|
data/lib/main.rb
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
require 'yaml'
|
5
|
+
require 'readline'
|
6
|
+
require 'io/console'
|
7
|
+
[
|
8
|
+
'./prompt.rb',
|
9
|
+
'./handle_args.rb',
|
10
|
+
'./help.rb',
|
11
|
+
'./context.rb'
|
12
|
+
].each { |f| require_relative f }
|
13
|
+
|
14
|
+
CONTEXT_PATH = File.expand_path("./../files/context.jsonl", __dir__)
|
15
|
+
FILE_PATH = File.expand_path("./../files/", __dir__)
|
16
|
+
CONFIG_PATH = File.expand_path("./../config/config.yml", __dir__)
|
17
|
+
CONTEXT_FILE_PATH = File.expand_path("./../files/context_file.txt", __dir__)
|
18
|
+
class Main
|
19
|
+
|
20
|
+
def self.run()
|
21
|
+
config = load_env()
|
22
|
+
|
23
|
+
## This does not work. Need to fix this.
|
24
|
+
if config.nil? || config['OPENAI_API_KEY'].nil?
|
25
|
+
puts "No API key found."
|
26
|
+
Help.display_api_key()
|
27
|
+
puts 'If you want to set your API key now, type (y)es and press enter.'
|
28
|
+
#puts "If you have an API key you can set it here."
|
29
|
+
#puts "Enter API key: (or press enter to exit)"
|
30
|
+
|
31
|
+
while input = Readline.readline("> ", true) do
|
32
|
+
if input.empty?
|
33
|
+
puts "Exiting."
|
34
|
+
exit
|
35
|
+
elsif input == "y" || input == "yes" || input == "Y" || input == "Yes"
|
36
|
+
set_key()
|
37
|
+
exit
|
38
|
+
else
|
39
|
+
puts 'Invalid input (y)es or press enter to exit.'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context = Context.load_context()
|
45
|
+
options_and_input = HandleArgs.handle_args()
|
46
|
+
options = options_and_input.select { |k, v| k.start_with?("option_") }
|
47
|
+
input = options_and_input["input"]
|
48
|
+
|
49
|
+
halt_options = ["-h", "--help", "-v", "--version", "--key"]
|
50
|
+
|
51
|
+
## Hack... Need to fix this.
|
52
|
+
if options.empty?
|
53
|
+
options = { "option_0" => "simple" }
|
54
|
+
end
|
55
|
+
|
56
|
+
options.each do |k, v|
|
57
|
+
if halt_options.include?(v)
|
58
|
+
## Options that halt the program.
|
59
|
+
case v
|
60
|
+
when "-h", "--help"
|
61
|
+
Help.display_help()
|
62
|
+
exit
|
63
|
+
when "-v", "--version"
|
64
|
+
Help.display_version()
|
65
|
+
exit
|
66
|
+
|
67
|
+
when "--key"
|
68
|
+
set_key(api_key: nil)
|
69
|
+
else
|
70
|
+
Help.display_help()
|
71
|
+
exit
|
72
|
+
end
|
73
|
+
else
|
74
|
+
## Options that don't halt the program.
|
75
|
+
case v
|
76
|
+
when "-f", "--file"
|
77
|
+
file_as_string = Context.load_context_file()
|
78
|
+
if file_as_string.empty?
|
79
|
+
exit
|
80
|
+
end
|
81
|
+
Prompt.stream_prompt(input, file_as_string)
|
82
|
+
when "-lf", "--loadfile"
|
83
|
+
puts "Loading File #{input}"
|
84
|
+
Context.save_context_file(input)
|
85
|
+
when "-d", "--delete"
|
86
|
+
if input.nil?
|
87
|
+
Context.delete_context()
|
88
|
+
else
|
89
|
+
Context.delete_context()
|
90
|
+
Context.save_context(Prompt.stream_prompt(input, context))
|
91
|
+
end
|
92
|
+
when "-c", "--conversation"
|
93
|
+
puts input
|
94
|
+
Context.save_context(Prompt.stream_prompt(input, context))
|
95
|
+
when "-w", "--whisper"
|
96
|
+
puts Prompt.whisper_transcribe(input)
|
97
|
+
when "-t", "--translate"
|
98
|
+
puts Prompt.whisper_translate(input)
|
99
|
+
when "-i", "--interactive"
|
100
|
+
puts "Interactive mode..."
|
101
|
+
puts "Type 'exit' or 'quit' to exit."
|
102
|
+
puts "Type 'clear' to clear context."
|
103
|
+
|
104
|
+
while input = Readline.readline("\n> ", true) do
|
105
|
+
if (input == "exit" || input == "quit")
|
106
|
+
break
|
107
|
+
end
|
108
|
+
if input == "clear"
|
109
|
+
puts "Clearing context..."
|
110
|
+
Context.delete_context()
|
111
|
+
next
|
112
|
+
end
|
113
|
+
options_and_input = HandleArgs.handle_args()
|
114
|
+
context = Context.load_context()
|
115
|
+
puts "\n"
|
116
|
+
Context.save_context(Prompt.stream_prompt(input, context))
|
117
|
+
puts "\n"
|
118
|
+
end
|
119
|
+
puts "Exiting..."
|
120
|
+
#Context.delete_context()
|
121
|
+
when "simple"
|
122
|
+
if !input.nil?
|
123
|
+
Prompt.stream_prompt(input)
|
124
|
+
else
|
125
|
+
puts "No input given."
|
126
|
+
Help.display_help()
|
127
|
+
end
|
128
|
+
else
|
129
|
+
Help.display_help()
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
def self.set_key(api_key: nil)
|
138
|
+
if api_key.nil?
|
139
|
+
puts "Setting API key..."
|
140
|
+
puts "Enter API key: (or press enter to exit)"
|
141
|
+
|
142
|
+
while input = Readline.readline("> ", true) do
|
143
|
+
if input.empty?
|
144
|
+
puts "Exiting."
|
145
|
+
exit
|
146
|
+
else
|
147
|
+
api_key = input.strip
|
148
|
+
break
|
149
|
+
end
|
150
|
+
end
|
151
|
+
puts "Saving API key..."
|
152
|
+
end
|
153
|
+
|
154
|
+
FileUtils.mkdir_p(File.dirname(CONFIG_PATH))
|
155
|
+
File.open(CONFIG_PATH, "w") do |f|
|
156
|
+
f.write(YAML.dump({ "OPENAI_API_KEY" => api_key }))
|
157
|
+
end
|
158
|
+
puts "API key saved."
|
159
|
+
end
|
160
|
+
|
161
|
+
def self.load_env()
|
162
|
+
YAML.load(File.read(CONFIG_PATH))
|
163
|
+
|
164
|
+
rescue Errno::ENOENT
|
165
|
+
puts "No config.yml found."
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
data/lib/prompt.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
require "openai"
|
2
|
+
|
3
|
+
class Prompt
|
4
|
+
|
5
|
+
## Streams the response, VERY NICE
|
6
|
+
def self.stream_prompt(input, conversation = '', temp = 0.7)
|
7
|
+
if conversation.length == 0
|
8
|
+
conversation += input
|
9
|
+
else
|
10
|
+
conversation += "\n My question: #{input}"
|
11
|
+
end
|
12
|
+
|
13
|
+
response = ''
|
14
|
+
client.chat(
|
15
|
+
parameters: {
|
16
|
+
model: "gpt-3.5-turbo",
|
17
|
+
messages: [{ role: "user", content: conversation}],
|
18
|
+
temperature: temp, ## Should be a parameter
|
19
|
+
stream: proc do |chunk, _bytesize|
|
20
|
+
response += chunk.dig("choices", 0, "delta", "content") unless chunk.dig("choices", 0, "delta", "content").nil?
|
21
|
+
print chunk.dig("choices", 0, "delta", "content")
|
22
|
+
end
|
23
|
+
}
|
24
|
+
)
|
25
|
+
context = {
|
26
|
+
"input" => input,
|
27
|
+
"response" => response,
|
28
|
+
}
|
29
|
+
return context
|
30
|
+
end
|
31
|
+
|
32
|
+
## Not implemented only scaffolding
|
33
|
+
def self.file_finetune()
|
34
|
+
return
|
35
|
+
client.files.upload(parameters: { file: "./test.json", purpose: "fine-tune" })
|
36
|
+
client.files.lisr
|
37
|
+
client.files.retrieve(id: "file-123")
|
38
|
+
client.files.content(id: "file-123")
|
39
|
+
client.files.delete(id: "file-123")
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.whisper_translate(file_path)
|
43
|
+
if (file_path.nil? || !file_path.end_with?(*['.mp3', '.wav', '.m4a', '.webm', '.mpeg', '.mpga']))
|
44
|
+
puts "No file given or wrong file type"
|
45
|
+
exit
|
46
|
+
else
|
47
|
+
size = File.size(file_path).to_f / 2**20
|
48
|
+
if size > 24
|
49
|
+
warning("The file is above the maximum size of 25MB")
|
50
|
+
exit
|
51
|
+
else
|
52
|
+
response = client.audio.translate(
|
53
|
+
parameters: {
|
54
|
+
model: "whisper-1",
|
55
|
+
file: File.open(file_path, "rb"),
|
56
|
+
})
|
57
|
+
if (response["text"].nil? || response["text"].empty?)
|
58
|
+
puts "No text found"
|
59
|
+
exit
|
60
|
+
end
|
61
|
+
puts response["text"]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
rescue Errno::ENOENT => e
|
65
|
+
puts "File not found"
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.whisper_transcribe(file_path)
|
69
|
+
if (file_path.nil? || !file_path.end_with?(*['.mp3', '.wav', '.m4a', '.webm', '.mpeg', '.mpga']))
|
70
|
+
puts "No file given"
|
71
|
+
exit
|
72
|
+
else
|
73
|
+
size = File.size(file_path).to_f / 2**20
|
74
|
+
if size > 24
|
75
|
+
warning("The file is above the maximum size of 25MB, this may take")
|
76
|
+
exit
|
77
|
+
else
|
78
|
+
response = client.audio.transcribe(
|
79
|
+
parameters: {
|
80
|
+
model: "whisper-1",
|
81
|
+
file: File.open(file_path, "rb"),
|
82
|
+
})
|
83
|
+
if (response["text"].nil? || response["text"].empty?)
|
84
|
+
puts "No text found"
|
85
|
+
exit
|
86
|
+
end
|
87
|
+
puts response["text"]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
rescue Errno::ENOENT => e
|
91
|
+
puts "File not found"
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def self.client()
|
97
|
+
conf = YAML.load(File.read(CONFIG_PATH))
|
98
|
+
key = conf["OPENAI_API_KEY"]
|
99
|
+
|
100
|
+
OpenAI::Client.new(access_token: key)
|
101
|
+
end
|
102
|
+
end
|
metadata
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ask-ai
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Oskar Franck
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-09-22 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: ruby-openai
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 5.1.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 5.1.0
|
27
|
+
description: A CLI tool for using OpenAI's API. The idea is to make it easy to use
|
28
|
+
the API with an option to pass files for help with diffrent tasks.
|
29
|
+
email: contact.oskarfranck.se
|
30
|
+
executables:
|
31
|
+
- aa
|
32
|
+
extensions: []
|
33
|
+
extra_rdoc_files: []
|
34
|
+
files:
|
35
|
+
- bin/aa
|
36
|
+
- config/config.yml
|
37
|
+
- files/context.jsonl
|
38
|
+
- files/context_file.txt
|
39
|
+
- lib/context.rb
|
40
|
+
- lib/handle_args.rb
|
41
|
+
- lib/help.rb
|
42
|
+
- lib/main.rb
|
43
|
+
- lib/prompt.rb
|
44
|
+
homepage:
|
45
|
+
licenses: []
|
46
|
+
metadata: {}
|
47
|
+
post_install_message:
|
48
|
+
rdoc_options: []
|
49
|
+
require_paths:
|
50
|
+
- lib
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
requirements: []
|
62
|
+
rubygems_version: 3.3.7
|
63
|
+
signing_key:
|
64
|
+
specification_version: 4
|
65
|
+
summary: A simple CLI for OpenAI's GPT-3 API.
|
66
|
+
test_files: []
|