monadic-chat 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/CHANGELOG.md +9 -0
  4. data/Gemfile +4 -0
  5. data/Gemfile.lock +172 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +652 -0
  8. data/Rakefile +12 -0
  9. data/apps/chat/chat.json +4 -0
  10. data/apps/chat/chat.md +42 -0
  11. data/apps/chat/chat.rb +79 -0
  12. data/apps/code/code.json +4 -0
  13. data/apps/code/code.md +42 -0
  14. data/apps/code/code.rb +77 -0
  15. data/apps/novel/novel.json +4 -0
  16. data/apps/novel/novel.md +36 -0
  17. data/apps/novel/novel.rb +77 -0
  18. data/apps/translate/translate.json +4 -0
  19. data/apps/translate/translate.md +37 -0
  20. data/apps/translate/translate.rb +81 -0
  21. data/assets/github.css +1036 -0
  22. data/assets/pigments-default.css +69 -0
  23. data/bin/monadic-chat +122 -0
  24. data/doc/img/code-example-time-html.png +0 -0
  25. data/doc/img/code-example-time.png +0 -0
  26. data/doc/img/example-translation.png +0 -0
  27. data/doc/img/how-research-mode-works.svg +1 -0
  28. data/doc/img/input-acess-token.png +0 -0
  29. data/doc/img/langacker-2001.svg +41 -0
  30. data/doc/img/linguistic-html.png +0 -0
  31. data/doc/img/monadic-chat-main-menu.png +0 -0
  32. data/doc/img/monadic-chat.svg +13 -0
  33. data/doc/img/readme-example-beatles-html.png +0 -0
  34. data/doc/img/readme-example-beatles.png +0 -0
  35. data/doc/img/research-mode-template.svg +198 -0
  36. data/doc/img/select-app-menu.png +0 -0
  37. data/doc/img/select-feature-menu.png +0 -0
  38. data/doc/img/state-monad.svg +154 -0
  39. data/doc/img/syntree-sample.png +0 -0
  40. data/lib/monadic_app.rb +115 -0
  41. data/lib/monadic_chat/console.rb +29 -0
  42. data/lib/monadic_chat/formatting.rb +110 -0
  43. data/lib/monadic_chat/helper.rb +72 -0
  44. data/lib/monadic_chat/interaction.rb +41 -0
  45. data/lib/monadic_chat/internals.rb +269 -0
  46. data/lib/monadic_chat/menu.rb +189 -0
  47. data/lib/monadic_chat/open_ai.rb +150 -0
  48. data/lib/monadic_chat/parameters.rb +109 -0
  49. data/lib/monadic_chat/version.rb +5 -0
  50. data/lib/monadic_chat.rb +190 -0
  51. data/monadic_chat.gemspec +54 -0
  52. data/samples/linguistic/linguistic.json +17 -0
  53. data/samples/linguistic/linguistic.md +39 -0
  54. data/samples/linguistic/linguistic.rb +74 -0
  55. metadata +343 -0
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ class MonadicApp
4
+ ##################################################
5
+ # methods for showing menu and menu items
6
+ ##################################################
7
+
8
+ def show_menu
9
+ clear_screen
10
+ print TTY::Cursor.save
11
+ parameter = PROMPT_SYSTEM.select("Select function:", per_page: 10, cycle: true, filter: true, default: 1, show_help: :never) do |menu|
12
+ menu.choice "#{BULLET} #{PASTEL.bold("cancel/return/escape")} cancel this menu", "cancel"
13
+ menu.choice "#{BULLET} #{PASTEL.bold("params/settings/config")} show and change values of parameters", "params"
14
+ menu.choice "#{BULLET} #{PASTEL.bold("data/context")} show currrent contextual info", "data"
15
+ menu.choice "#{BULLET} #{PASTEL.bold("html")} view contextual info on the web browser", "html"
16
+ menu.choice "#{BULLET} #{PASTEL.bold("reset")} reset context to original state", "reset"
17
+ menu.choice "#{BULLET} #{PASTEL.bold("save")} save current contextual info to file", "save"
18
+ menu.choice "#{BULLET} #{PASTEL.bold("load")} load current contextual info from file", "load"
19
+ menu.choice "#{BULLET} #{PASTEL.bold("clear/clean")} clear screen", "clear"
20
+ menu.choice "#{BULLET} #{PASTEL.bold("readme/documentation")} open readme/documentation", "readme"
21
+ menu.choice "#{BULLET} #{PASTEL.bold("exit/bye/quit")} go back to main menu", "exit"
22
+ end
23
+
24
+ print TTY::Cursor.restore
25
+ print TTY::Cursor.clear_screen_down
26
+ print TTY::Cursor.restore
27
+
28
+ case parameter
29
+ when "cancel"
30
+ return true
31
+ when "params"
32
+ change_parameter
33
+ when "data"
34
+ show_data
35
+ when "html"
36
+ set_html
37
+ when "reset"
38
+ reset
39
+ when "save"
40
+ save_data
41
+ when "load"
42
+ load_data
43
+ when "clear"
44
+ clear_screen
45
+ print TTY::Cursor.clear_screen_down
46
+ when "readme"
47
+ MonadicChat.open_readme
48
+ when "exit"
49
+ return false
50
+ end
51
+ true
52
+ end
53
+
54
+ def reset
55
+ @params = @params_original.dup
56
+
57
+ case @method
58
+ when "completions"
59
+ @template = @template_original.dup
60
+ when "chat/completions"
61
+ @template = JSON.parse @template_original
62
+ end
63
+
64
+ if @placeholders.empty?
65
+ print PROMPT_SYSTEM.prefix
66
+ print "Context and parameters have been reset.\n"
67
+ else
68
+ fulfill_placeholders
69
+ end
70
+ end
71
+
72
+ def ask_retrial(input, message = nil)
73
+ print PROMPT_SYSTEM.prefix
74
+ print " Error: #{message.capitalize}\n" if message
75
+ retrial = PROMPT_USER.select("Do you want to try again?",
76
+ show_help: :never) do |menu|
77
+ menu.choice "Yes", "yes"
78
+ menu.choice "No", "no"
79
+ menu.choice "Show current contextual data", "show"
80
+ end
81
+ case retrial
82
+ when "yes"
83
+ input
84
+ when "no"
85
+ user_input
86
+ when "show"
87
+ show_data
88
+ ask_retrial(input)
89
+ end
90
+ end
91
+
92
+ def check_file(path)
93
+ dirname = File.dirname(File.expand_path(path))
94
+ path == "" || (/\.json\z/ =~ path.strip && Dir.exist?(dirname)) ? true : false
95
+ end
96
+
97
+ def save_data
98
+ input = ""
99
+ loop do
100
+ print TTY::Cursor.save
101
+ path = PROMPT_SYSTEM.readline("Enter the file path for the JSON file (including the file name and .json extension): ")
102
+ if check_file(path)
103
+ input = path
104
+ break
105
+ else
106
+ print TTY::Cursor.restore
107
+ print TTY::Cursor.clear_screen_down
108
+ end
109
+ end
110
+ print TTY::Cursor.save
111
+
112
+ return if input.to_s == ""
113
+
114
+ filepath = File.expand_path(input.strip)
115
+
116
+ if File.exist? filepath
117
+ overwrite = PROMPT_SYSTEM.select("#{filepath} already exists.\nOverwrite?",
118
+ show_help: :never) do |menu|
119
+ menu.choice "Yes", "yes"
120
+ menu.choice "No", "no"
121
+ end
122
+ return if overwrite == "no"
123
+ end
124
+
125
+ FileUtils.touch(filepath)
126
+ unless File.exist? filepath
127
+ print "File cannot be created\n"
128
+ save_data
129
+ end
130
+
131
+ begin
132
+ File.open(filepath, "w") do |f|
133
+ case @method
134
+ when "completions"
135
+ m = /\n\n```json\s*(\{.+\})\s*```\n\n/m.match(@template)
136
+ f.write JSON.pretty_generate(JSON.parse(m[1]))
137
+ when "chat/completions"
138
+ f.write JSON.pretty_generate(@template)
139
+ end
140
+
141
+ print "Data has been saved successfully\n"
142
+ end
143
+ true
144
+ rescue StandardError
145
+ print "Error: Something went wrong"
146
+ false
147
+ end
148
+ end
149
+
150
+ def load_data
151
+ input = ""
152
+ loop do
153
+ print TTY::Cursor.save
154
+ path = PROMPT_SYSTEM.readline("Enter the file path for the JSON file (press Enter to cancel): ")
155
+ if check_file(path)
156
+ input = path
157
+ break
158
+ else
159
+ print TTY::Cursor.restore
160
+ print TTY::Cursor.clear_screen_down
161
+ end
162
+ end
163
+ print TTY::Cursor.save
164
+
165
+ return if input.to_s == ""
166
+
167
+ begin
168
+ filepath = File.expand_path(input.strip)
169
+ json = File.read(filepath)
170
+ data = JSON.parse(json)
171
+ case @method
172
+ when "completions"
173
+ raise unless data["mode"] == self.class.name.downcase.split("::")[-1]
174
+
175
+ new_template = @template.sub(/\n\n```json\s*\{.+\}\s*```\n\n/m, "\n\n```json\n#{JSON.pretty_generate(data).strip}\n```\n\n")
176
+ @template = new_template
177
+ when "chat/completions"
178
+ raise unless data["messages"] && data["messages"][0]["role"]
179
+
180
+ @template["messages"] = data["messages"]
181
+ end
182
+ print "Data has been loaded successfully\n"
183
+ true
184
+ rescue StandardError
185
+ print "The data structure is not valid for this app\n"
186
+ false
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "http"
4
+ require "oj"
5
+ require "net/http"
6
+ require "uri"
7
+ require "strscan"
8
+ require "parallel"
9
+ require "tty-progressbar"
10
+
11
+ Oj.mimic_JSON
12
+
13
+ module OpenAI
14
+ def self.model_name(research_mode: false)
15
+ research_mode ? "text-davinci-003" : "gpt-3.5-turbo"
16
+ end
17
+
18
+ def self.model_to_method(model)
19
+ {
20
+ "text-davinci-003" => "completions",
21
+ "gpt-3.5-turbo" => "chat/completions",
22
+ "gpt-3.5-turbo-0301" => "chat/completions"
23
+ }[model]
24
+ end
25
+
26
+ def self.query(access_token, mode, method, timeout_sec = 60, query = {}, &block)
27
+ target_uri = "https://api.openai.com/v1/#{method}"
28
+ headers = {
29
+ "Content-Type" => "application/json",
30
+ "Authorization" => "Bearer #{access_token}"
31
+ }
32
+ headers["Accept"] = "text/event-stream" if query["stream"]
33
+ http = HTTP.headers(headers)
34
+
35
+ case mode
36
+ when "post"
37
+ res = http.timeout(timeout_sec).post(target_uri, json: query)
38
+ when "get"
39
+ res = http.timeout(timeout_sec).get(target_uri)
40
+ end
41
+
42
+ if query["stream"]
43
+ json = nil
44
+ res.body.each do |chunk|
45
+ chunk.split("\n\n").each do |data|
46
+ content = data.strip[6..]
47
+ break if content == "[DONE]"
48
+
49
+ stream = JSON.parse(content)
50
+ fragment = case method
51
+ when "completions"
52
+ stream["choices"][0]["text"]
53
+ when "chat/completions"
54
+ stream["choices"][0]["delta"]["content"] || ""
55
+ end
56
+ block&.call fragment
57
+ if !json
58
+ json = stream
59
+ else
60
+ case method
61
+ when "completions"
62
+ json["choices"][0]["text"] << fragment
63
+ when "chat/completions"
64
+ json["choices"][0]["text"] ||= +""
65
+ json["choices"][0]["text"] << fragment
66
+ end
67
+ end
68
+ end
69
+ end
70
+ json
71
+ else
72
+ JSON.parse res.body
73
+ end
74
+ end
75
+
76
+ def self.models(access_token)
77
+ res = query(access_token, "get", "models")
78
+ res.fetch("data", []).sort_by { |m| -m["created"] }
79
+ end
80
+
81
+ class Completion
82
+ attr_reader :access_token
83
+
84
+ def initialize(access_token, tmp_file: nil)
85
+ @access_token = access_token
86
+ @tmp_file = tmp_file
87
+ end
88
+
89
+ def models
90
+ OpenAI.models(@access_token)
91
+ end
92
+
93
+ def run(params, num_retry: 1, &block)
94
+ method = OpenAI.model_to_method(params["model"])
95
+
96
+ response = OpenAI.query(@access_token, "post", method, 60, params, &block)
97
+ if response["error"]
98
+ raise response["error"]["message"]
99
+ elsif response["choices"][0]["finish_reason"] == "length"
100
+ raise "finished because of length"
101
+ end
102
+
103
+ case method
104
+ when "completions"
105
+ get_json response ["choices"][0]["text"]
106
+ when "chat/completions"
107
+ response ["choices"][0]["text"]
108
+ end
109
+ rescue StandardError => e
110
+ case num_retry
111
+ when 0
112
+ raise e
113
+ else
114
+ run(params, num_retry: num_retry - 1, &block)
115
+ end
116
+ end
117
+
118
+ def get_json(data)
119
+ case data
120
+ when %r{<JSON>\n*(\{.+\})\n*</JSON>}m
121
+ json = Regexp.last_match(1).gsub(/\r\n?/, "\n").gsub(/\r\n/) { "\n" }
122
+ res = JSON.parse(json)
123
+ when /(\{.+\})/m
124
+ json = Regexp.last_match(1).gsub(/\r\n?/, "\n").gsub(/\r\n/) { "\n" }
125
+ res = JSON.parse(json)
126
+ else
127
+ res = data
128
+ end
129
+ File.open(@tmp_file, "w") { |f| f.write json } if @tmp_file
130
+ res
131
+ end
132
+
133
+ def run_iteration(params, prompts, template, replace_key = "{{PROMPT}}", num_retry: 0)
134
+ bar = TTY::ProgressBar.new("[:bar] :current/:total :total_byte :percent ET::elapsed ETA::eta",
135
+ total: prompts.size,
136
+ bar_format: :box)
137
+ bar.start
138
+ json = ""
139
+ prompts.each do |prompt|
140
+ params["prompt"] = template.sub(replace_key, prompt)
141
+ res = run(params, num_retry: num_retry)
142
+ json = JSON.pretty_generate(res)
143
+ bar.advance(1)
144
+ template = template.sub(/\n\n```json.+?```\n\n/m, "\n\n```json\n#{json}\n```\n\n")
145
+ end
146
+ bar.finish
147
+ JSON.parse(json)
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ class MonadicApp
4
+ ##################################################
5
+ # methods for parametter setting
6
+ ##################################################
7
+
8
+ def change_parameter
9
+ parameter = PROMPT_SYSTEM.select("Select the parmeter to be set:", per_page: 7, cycle: true, show_help: :never, filter: true, default: 1) do |menu|
10
+ menu.choice "#{BULLET} Cancel", "cancel"
11
+ menu.choice "#{BULLET} model: #{@params["model"]}", "model"
12
+ menu.choice "#{BULLET} max_tokens: #{@params["max_tokens"]}", "max_tokens"
13
+ menu.choice "#{BULLET} temperature: #{@params["temperature"]}", "temperature"
14
+ menu.choice "#{BULLET} top_p: #{@params["top_p"]}", "top_p"
15
+ menu.choice "#{BULLET} frequency_penalty: #{@params["frequency_penalty"]}", "frequency_penalty"
16
+ menu.choice "#{BULLET} presence_penalty: #{@params["presence_penalty"]}", "presence_penalty"
17
+ end
18
+ return if parameter == "cancel"
19
+
20
+ case parameter
21
+ when "model"
22
+ value = change_model
23
+ case @method
24
+ when "completions"
25
+ @template = @template_original.dup
26
+ when "chat/completions"
27
+ @template = JSON.parse @template_original
28
+ end
29
+ when "max_tokens"
30
+ value = change_max_tokens
31
+ when "temperature"
32
+ value = change_temperature
33
+ when "top_p"
34
+ value = change_top_p
35
+ when "frequency_penalty"
36
+ value = change_frequency_penalty
37
+ when "presence_penalty"
38
+ value = change_presence_penalty
39
+ end
40
+ @params[parameter] = value if value
41
+ print "Parameter #{parameter} has been set to #{PASTEL.green(value)}\n" if value
42
+ end
43
+
44
+ def change_max_tokens
45
+ PROMPT_SYSTEM.ask("Set value of max tokens [1000 to 8000]:", convert: :int) do |q|
46
+ q.in "1000-8000"
47
+ q.messages[:range?] = "Value out of expected range [1000 to 2048]"
48
+ end
49
+ end
50
+
51
+ def change_temperature
52
+ PROMPT_SYSTEM.ask("Set value of temperature [0.0 to 1.0]:", convert: :float) do |q|
53
+ q.in "0.0-1.0"
54
+ q.messages[:range?] = "Value out of expected range [0.0 to 1.0]"
55
+ end
56
+ end
57
+
58
+ def change_top_p
59
+ PROMPT_SYSTEM.ask("Set value of top_p [0.0 to 1.0]:", convert: :float) do |q|
60
+ q.in "0.0-1.0"
61
+ q.messages[:range?] = "Value out of expected range [0.0 to 1.0]"
62
+ end
63
+ end
64
+
65
+ def change_frequency_penalty
66
+ PROMPT_SYSTEM.ask("Set value of frequency penalty [-2.0 to 2.0]:", convert: :float) do |q|
67
+ q.in "-2.0-2.0"
68
+ q.messages[:range?] = "Value out of expected range [-2.0 to 2.0]"
69
+ end
70
+ end
71
+
72
+ def change_presence_penalty
73
+ PROMPT_SYSTEM.ask("Set value of presence penalty [-2.0 to 2.0]:", convert: :float) do |q|
74
+ q.in "-2.0-2.0"
75
+ q.messages[:range?] = "Value out of expected range [-2.0 to 2.0]"
76
+ end
77
+ end
78
+
79
+ def change_model
80
+ model = PROMPT_SYSTEM.select("Select a model:", per_page: 10, cycle: false, show_help: :never, filter: true, default: 1) do |menu|
81
+ menu.choice "#{BULLET} Cancel", "cancel"
82
+ TTY::Cursor.save
83
+ print SPINNER
84
+ models = @completion.models
85
+ go_up_and_clear
86
+ TTY::Cursor.restore
87
+ TTY::Cursor.restore
88
+ models.filter { |m| OpenAI.model_to_method(m["id"]) == @method }.sort_by { |m| -m["created"] }.each do |m|
89
+ menu.choice "#{BULLET} #{m["id"]}", m["id"]
90
+ end
91
+ end
92
+ if model == "cancel"
93
+ nil
94
+ else
95
+ model
96
+ end
97
+ end
98
+
99
+ def show_params
100
+ params_md = "# Current Parameter Values\n\n"
101
+ @params.each do |key, val|
102
+ next if /\A(?:prompt|stream|logprobs|echo|stop)\z/ =~ key
103
+
104
+ params_md += "- #{key}: #{val}\n"
105
+ end
106
+ print prompt_system, "\n"
107
+ print "#{TTY::Markdown.parse(params_md, indent: 0).strip}\n\n"
108
+ end
109
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MonadicChat
4
+ VERSION = "0.1.1"
5
+ end
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-cursor"
4
+ require "tty-screen"
5
+ require "tty-markdown"
6
+ require "tty-prompt"
7
+ require "tty-box"
8
+ require "pastel"
9
+ require "oj"
10
+ require "kramdown"
11
+ require "rouge"
12
+ require "launchy"
13
+ require "io/console"
14
+ require "readline"
15
+
16
+ require_relative "./monadic_chat/version"
17
+ require_relative "./monadic_chat/open_ai"
18
+ require_relative "./monadic_chat/helper"
19
+
20
+ Oj.mimic_JSON
21
+
22
+ module MonadicChat
23
+ CONFIG = File.join(Dir.home, "monadic_chat.conf")
24
+ NUM_RETRY = 2
25
+ MIN_LENGTH = 5
26
+ TIMEOUT_SEC = 120
27
+ TITLE_WIDTH = 72
28
+
29
+ APPS_DIR = File.absolute_path(File.join(__dir__, "..", "apps"))
30
+ APPS_DIR_LIST = Dir.entries(APPS_DIR)
31
+ .reject { |entry| /\A\./ =~ entry || /\A_/ =~ entry.split("/").last }
32
+ .map { |entry| File.join(APPS_DIR, entry) }
33
+ templates = {}
34
+ APPS_DIR_LIST.each do |app|
35
+ basename = File.basename(app, ".*")
36
+ normal_mode_template = File.absolute_path(File.join(app, "#{basename}.json"))
37
+ templates["normal/#{basename}"] = normal_mode_template if File.exist? normal_mode_template
38
+ research_mode_template = File.absolute_path(File.join(app, "#{basename}.md"))
39
+ templates["research/#{basename}"] = research_mode_template if File.exist? research_mode_template
40
+ end
41
+ APPS = APPS_DIR_LIST.map { |dir| File.basename(dir, ".*") }
42
+
43
+ TEMPLATES = templates
44
+
45
+ PASTEL = Pastel.new
46
+
47
+ TEMP_HTML = File.join(Dir.home, "monadic_chat.html")
48
+ TEMP_JSON = File.join(Dir.home, "monadic_chat.json")
49
+
50
+ style = +File.read(File.join(__dir__, "..", "assets", "github.css")).gsub(".markdown-") { "" }
51
+ style << File.read(File.join(__dir__, "..", "assets", "pigments-default.css"))
52
+ style << <<~CSS
53
+ body {
54
+ margin: 50px;
55
+ font-family: "Helvetica Neue", Arial, "Hiragino Kaku Gothic ProN", "Hiragino Sans", Meiryo, sans-serif;
56
+ color: #333
57
+ }
58
+ .monadic_user{
59
+ display:inline-block;
60
+ padding-left: 0.5em;
61
+ padding-right: 0.5em;
62
+ font-weight: bold;
63
+ background-color: #c8e5ff;
64
+ margin-bottom: 0.5em;
65
+ }
66
+ .monadic_chat {
67
+ display:inline-block;
68
+ padding-left: 0.5em;
69
+ padding-right: 0.5em;
70
+ font-weight: bold;
71
+ background-color: #ffcaca;
72
+ margin-bottom: 0.5em;
73
+ }
74
+ .monadic_system {
75
+ display:inline-block;
76
+ padding-left: 0.5em;
77
+ padding-right: 0.5em;
78
+ font-weight: bold;
79
+ background-color: #c4ffcb;
80
+ margin-bottom: 0.5em;
81
+ }
82
+ .monadic_gray {
83
+ display:inline-block;
84
+ font-weight: bold;
85
+ color: #999;
86
+ margin-bottom: 0.5em;
87
+ }
88
+ .monadic_app {
89
+ display:inline-block;
90
+ font-weight: bold;
91
+ color: #EB742B;
92
+ margin-bottom: 0.5em;
93
+ }
94
+ CSS
95
+ GITHUB_STYLE = style
96
+
97
+ def self.require_apps
98
+ MonadicChat::APPS_DIR_LIST.each do |app_dir|
99
+ basename = app_dir.split("/").last
100
+ require "#{app_dir}/#{basename}"
101
+ end
102
+ end
103
+
104
+ def self.open_readme
105
+ url = "https://github.com/yohasebe/monadic-chat/"
106
+ shellscript = <<~SHELL
107
+ if [[ "$OSTYPE" == "darwin"* ]]; then
108
+ open "#{url}"
109
+ elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
110
+ if command -v xdg-open >/dev/null 2>&1; then
111
+ xdg-open "#{url}"
112
+ else
113
+ echo "#{url}"
114
+ fi
115
+ else
116
+ echo "#{url}"
117
+ fi
118
+ SHELL
119
+ `#{shellscript}`
120
+ end
121
+
122
+ def self.authenticate(overwrite: false)
123
+ check = lambda do |token|
124
+ print "Checking configuration #{SPINNER} "
125
+ begin
126
+ raise if OpenAI.models(token).empty?
127
+
128
+ print "success\n"
129
+ OpenAI::Completion.new(token, tmp_file: TEMP_JSON)
130
+ rescue StandardError
131
+ print "failure.\n"
132
+ authenticate(overwrite: true)
133
+ end
134
+ end
135
+
136
+ access_token = ENV["OPENAI_API_KEY"]
137
+ if overwrite
138
+ access_token = nil
139
+ access_token ||= PROMPT_SYSTEM.ask(" Input your OpenAI access token:")
140
+
141
+ check.call(access_token)
142
+
143
+ File.open(CONFIG, "w") do |f|
144
+ config = { "access_token" => access_token }
145
+ f.write(JSON.pretty_generate(config))
146
+ print "New access token has been saved to #{CONFIG}\n"
147
+ end
148
+ elsif File.exist?(CONFIG)
149
+ json = File.read(CONFIG)
150
+ config = JSON.parse(json)
151
+ access_token = config["access_token"]
152
+ check.call(access_token)
153
+ else
154
+ access_token ||= PROMPT_SYSTEM.ask(" Input your OpenAI access token:")
155
+ check.call(access_token)
156
+ File.open(CONFIG, "w") do |f|
157
+ config = { "access_token" => access_token }
158
+ f.write(JSON.pretty_generate(config))
159
+ print "Access token has been saved to #{CONFIG}\n"
160
+ end
161
+ end
162
+ end
163
+
164
+ def self.prompt_system
165
+ box_width = 8
166
+ name = "System".center(box_width, " ")
167
+ color = "green"
168
+ "\n#{PASTEL.send(:"on_#{color}", name)}"
169
+ end
170
+
171
+ def self.prompt_user
172
+ box_width = 6
173
+ color = "blue"
174
+ name = "User".center(box_width, " ")
175
+ "\n#{PASTEL.send(:"on_#{color}", name)}"
176
+ end
177
+
178
+ def self.prompt_assistant
179
+ box_width = 5
180
+ color = "red"
181
+ name = "GPT".center(box_width, " ")
182
+ "\n#{PASTEL.send(:"on_#{color}", name)}"
183
+ end
184
+
185
+ PROMPT_USER = TTY::PromptX.new(active_color: :blue, prefix: prompt_user)
186
+ PROMPT_SYSTEM = TTY::PromptX.new(active_color: :blue, prefix: "#{prompt_system} ")
187
+ PROMPT_ASSISTANT = TTY::PromptX.new(active_color: :red, prefix: "#{prompt_assistant} ")
188
+ SPINNER = "▹▹▹▹"
189
+ BULLET = "\e[33m●\e[0m"
190
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/monadic_chat/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "monadic-chat"
7
+ spec.version = MonadicChat::VERSION
8
+ spec.authors = ["yohasebe"]
9
+ spec.email = ["yohasebe@gmail.com"]
10
+
11
+ spec.summary = "Highly configurable CLI client app for OpenAI chat/text-completion API"
12
+ spec.description = <<~DESC
13
+ Monadic Chat is a command-line client application program that uses OpenAI's Text Completion API and Chat API to enable chat-style conversations with OpenAI's artificial intelligence system in a ChatGPT-like style.
14
+ DESC
15
+ spec.homepage = "https://github.com/yohasebe/monadic-chat"
16
+ spec.license = "MIT"
17
+ spec.required_ruby_version = ">= 2.6.0"
18
+
19
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
20
+
21
+ spec.metadata["homepage_uri"] = spec.homepage
22
+ spec.metadata["source_code_uri"] = "https://github.com/yohasebe/monadic-chat"
23
+ spec.metadata["changelog_uri"] = "https://github.com/yohasebe/monadic-chat/CHANGELOG.md"
24
+
25
+ # Specify which files should be added to the gem when it is released.
26
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
27
+ spec.files = Dir.chdir(__dir__) do
28
+ `git ls-files -z`.split("\x0").reject do |f|
29
+ (f == __FILE__) || f.match(%r{\A(?:(?:test|spec|features)/|\.(?:git|circleci)|appveyor)})
30
+ end
31
+ end
32
+ spec.bindir = "bin"
33
+ spec.executables = ["monadic-chat"]
34
+ spec.require_paths = ["lib"]
35
+
36
+ spec.add_development_dependency "bundler"
37
+ spec.add_development_dependency "rake"
38
+ spec.add_development_dependency "rspec"
39
+ spec.add_development_dependency "solargraph"
40
+
41
+ spec.add_dependency "http"
42
+ spec.add_dependency "kramdown"
43
+ spec.add_dependency "launchy"
44
+ spec.add_dependency "oj"
45
+ spec.add_dependency "parallel"
46
+ spec.add_dependency "pastel"
47
+ spec.add_dependency "rouge"
48
+ spec.add_dependency "tty-box"
49
+ spec.add_dependency "tty-cursor"
50
+ spec.add_dependency "tty-markdown"
51
+ spec.add_dependency "tty-progressbar"
52
+ spec.add_dependency "tty-prompt"
53
+ spec.add_dependency "tty-screen"
54
+ end