monadic-chat 0.1.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/.rspec +3 -0
- data/CHANGELOG.md +9 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +172 -0
- data/LICENSE.txt +21 -0
- data/README.md +652 -0
- data/Rakefile +12 -0
- data/apps/chat/chat.json +4 -0
- data/apps/chat/chat.md +42 -0
- data/apps/chat/chat.rb +79 -0
- data/apps/code/code.json +4 -0
- data/apps/code/code.md +42 -0
- data/apps/code/code.rb +77 -0
- data/apps/novel/novel.json +4 -0
- data/apps/novel/novel.md +36 -0
- data/apps/novel/novel.rb +77 -0
- data/apps/translate/translate.json +4 -0
- data/apps/translate/translate.md +37 -0
- data/apps/translate/translate.rb +81 -0
- data/assets/github.css +1036 -0
- data/assets/pigments-default.css +69 -0
- data/bin/monadic-chat +122 -0
- data/doc/img/code-example-time-html.png +0 -0
- data/doc/img/code-example-time.png +0 -0
- data/doc/img/example-translation.png +0 -0
- data/doc/img/how-research-mode-works.svg +1 -0
- data/doc/img/input-acess-token.png +0 -0
- data/doc/img/langacker-2001.svg +41 -0
- data/doc/img/linguistic-html.png +0 -0
- data/doc/img/monadic-chat-main-menu.png +0 -0
- data/doc/img/monadic-chat.svg +13 -0
- data/doc/img/readme-example-beatles-html.png +0 -0
- data/doc/img/readme-example-beatles.png +0 -0
- data/doc/img/research-mode-template.svg +198 -0
- data/doc/img/select-app-menu.png +0 -0
- data/doc/img/select-feature-menu.png +0 -0
- data/doc/img/state-monad.svg +154 -0
- data/doc/img/syntree-sample.png +0 -0
- data/lib/monadic_app.rb +115 -0
- data/lib/monadic_chat/console.rb +29 -0
- data/lib/monadic_chat/formatting.rb +110 -0
- data/lib/monadic_chat/helper.rb +72 -0
- data/lib/monadic_chat/interaction.rb +41 -0
- data/lib/monadic_chat/internals.rb +269 -0
- data/lib/monadic_chat/menu.rb +189 -0
- data/lib/monadic_chat/open_ai.rb +150 -0
- data/lib/monadic_chat/parameters.rb +109 -0
- data/lib/monadic_chat/version.rb +5 -0
- data/lib/monadic_chat.rb +190 -0
- data/monadic_chat.gemspec +54 -0
- data/samples/linguistic/linguistic.json +17 -0
- data/samples/linguistic/linguistic.md +39 -0
- data/samples/linguistic/linguistic.rb +74 -0
- metadata +343 -0
data/lib/monadic_app.rb
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "./monadic_chat"
|
4
|
+
require_relative "./monadic_chat/console"
|
5
|
+
require_relative "./monadic_chat/formatting"
|
6
|
+
require_relative "./monadic_chat/interaction"
|
7
|
+
require_relative "./monadic_chat/menu"
|
8
|
+
require_relative "./monadic_chat/parameters"
|
9
|
+
require_relative "./monadic_chat/internals"
|
10
|
+
|
11
|
+
Thread.abort_on_exception = false
|
12
|
+
|
13
|
+
class MonadicApp
|
14
|
+
include MonadicChat
|
15
|
+
attr_reader :template
|
16
|
+
|
17
|
+
def initialize(params, template, placeholders, prop_accumulated, prop_newdata, update_proc)
|
18
|
+
@threads = Thread::Queue.new
|
19
|
+
@responses = Thread::Queue.new
|
20
|
+
@placeholders = placeholders
|
21
|
+
@prop_accumulated = prop_accumulated
|
22
|
+
@prop_newdata = prop_newdata
|
23
|
+
@completion = nil
|
24
|
+
@update_proc = update_proc
|
25
|
+
@params_original = params
|
26
|
+
@params = @params_original.dup
|
27
|
+
@template_original = File.read(template)
|
28
|
+
@method = OpenAI.model_to_method @params["model"]
|
29
|
+
|
30
|
+
case @method
|
31
|
+
when "completions"
|
32
|
+
@template = @template_original.dup
|
33
|
+
when "chat/completions"
|
34
|
+
@template = JSON.parse @template_original
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
##################################################
|
39
|
+
# methods for running monadic app
|
40
|
+
##################################################
|
41
|
+
|
42
|
+
def parse(input = nil)
|
43
|
+
loop do
|
44
|
+
case input
|
45
|
+
when TrueClass
|
46
|
+
input = user_input
|
47
|
+
next
|
48
|
+
when /\A\s*(?:help|menu|commands?|\?|h)\s*\z/i
|
49
|
+
return true unless show_menu
|
50
|
+
when /\A\s*(?:bye|exit|quit)\s*\z/i
|
51
|
+
break
|
52
|
+
when /\A\s*(?:reset)\s*\z/i
|
53
|
+
reset
|
54
|
+
when /\A\s*(?:data|context)\s*\z/i
|
55
|
+
show_data
|
56
|
+
when /\A\s*(?:html)\s*\z/i
|
57
|
+
set_html
|
58
|
+
when /\A\s*(?:save)\s*\z/i
|
59
|
+
save_data
|
60
|
+
when /\A\s*(?:load)\s*\z/i
|
61
|
+
load_data
|
62
|
+
when /\A\s*(?:clear|clean)\s*\z/i
|
63
|
+
clear_screen
|
64
|
+
when /\A\s*(?:params?|parameters?|config|configuration)\s*\z/i
|
65
|
+
change_parameter
|
66
|
+
else
|
67
|
+
if input && confirm_query(input)
|
68
|
+
begin
|
69
|
+
case @method
|
70
|
+
when "completions"
|
71
|
+
bind_research_mode(input, num_retry: NUM_RETRY)
|
72
|
+
when "chat/completions"
|
73
|
+
bind_normal_mode(input, num_retry: NUM_RETRY)
|
74
|
+
end
|
75
|
+
rescue StandardError => e
|
76
|
+
input = ask_retrial(input, e.message)
|
77
|
+
next
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
if input.to_s == ""
|
82
|
+
input = false
|
83
|
+
clear_screen
|
84
|
+
end
|
85
|
+
input = user_input
|
86
|
+
end
|
87
|
+
rescue MonadicError
|
88
|
+
false
|
89
|
+
end
|
90
|
+
|
91
|
+
def banner(title, desc, color)
|
92
|
+
screen_width = TTY::Screen.width - 2
|
93
|
+
width = screen_width < TITLE_WIDTH ? screen_width : TITLE_WIDTH
|
94
|
+
title = PASTEL.bold.send(color.to_sym, title.center(width, " "))
|
95
|
+
desc = desc.center(width, " ")
|
96
|
+
padding = "".center(width, " ")
|
97
|
+
banner = TTY::Box.frame "#{padding}\n#{title}\n#{desc}\n#{padding}"
|
98
|
+
print "\n", banner.strip, "\n"
|
99
|
+
end
|
100
|
+
|
101
|
+
def run
|
102
|
+
banner("MONADIC::CHAT / #{self.class.name}", self.class::DESC, self.class::COLOR)
|
103
|
+
show_greet
|
104
|
+
|
105
|
+
if @placeholders.empty?
|
106
|
+
parse(user_input)
|
107
|
+
else
|
108
|
+
loadfile = PROMPT_SYSTEM.select("\nLoad saved file? (Make sure the file is saved by the same app)", default: 2, show_help: :never) do |menu|
|
109
|
+
menu.choice "Yes", "yes"
|
110
|
+
menu.choice "No", "no"
|
111
|
+
end
|
112
|
+
parse(user_input) if loadfile == "yes" && load_data || fulfill_placeholders
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class MonadicApp
|
4
|
+
##################################################
|
5
|
+
# methods for manipulating terminal screen
|
6
|
+
##################################################
|
7
|
+
def count_lines_below
|
8
|
+
screen_height = TTY::Screen.height
|
9
|
+
vpos = Cursor.pos[:row]
|
10
|
+
screen_height - vpos
|
11
|
+
end
|
12
|
+
|
13
|
+
def go_up_and_clear
|
14
|
+
print TTY::Cursor.up
|
15
|
+
print TTY::Cursor.clear_screen_down
|
16
|
+
print TTY::Cursor.up
|
17
|
+
end
|
18
|
+
|
19
|
+
def clear_screen
|
20
|
+
print "\e[2J\e[f"
|
21
|
+
end
|
22
|
+
|
23
|
+
def ask_clear
|
24
|
+
PROMPT_SYSTEM.readline(PASTEL.red("Press Enter to clear screen"))
|
25
|
+
print TTY::Cursor.up
|
26
|
+
print TTY::Cursor.clear_screen_down
|
27
|
+
clear_screen
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class MonadicApp
|
4
|
+
##################################################
|
5
|
+
# methods for formatting and presenting
|
6
|
+
##################################################
|
7
|
+
def format_data
|
8
|
+
contextual = []
|
9
|
+
accumulated = []
|
10
|
+
|
11
|
+
objectify.each do |key, val|
|
12
|
+
next if %w[prompt response].include? key
|
13
|
+
|
14
|
+
if (@method == "completions" && key == @prop_accumulated) ||
|
15
|
+
(@method == "chat/completions" && key == "messages")
|
16
|
+
val = val.map do |v|
|
17
|
+
case @method
|
18
|
+
when "completions"
|
19
|
+
if v.instance_of?(String)
|
20
|
+
v.sub(/\s+###\s*$/m, "")
|
21
|
+
else
|
22
|
+
v.map { |role, text| "#{role.strip.capitalize}: #{text.sub(/\s+###\s*$/m, "")}" }
|
23
|
+
end
|
24
|
+
when "chat/completions"
|
25
|
+
"#{v["role"].capitalize}: #{v["content"]}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
accumulated << val.join("\n\n")
|
29
|
+
else
|
30
|
+
contextual << "- **#{key.split("_").map(&:capitalize).join(" ")}**: #{val.to_s.strip}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
h1 = "# Monadic :: Chat / #{self.class.name}"
|
35
|
+
contextual.map!(&:strip).unshift "## Contextual Data\n" unless contextual.empty?
|
36
|
+
accum_label = @prop_accumulated.split("_").map(&:capitalize).join(" ")
|
37
|
+
accumulated.map!(&:strip).unshift "## #{accum_label}\n" unless accumulated.empty?
|
38
|
+
"#{h1}\n\n#{contextual.join("\n")}\n\n#{accumulated.join("\n")}"
|
39
|
+
end
|
40
|
+
|
41
|
+
def show_data
|
42
|
+
print PROMPT_SYSTEM.prefix
|
43
|
+
|
44
|
+
wait
|
45
|
+
|
46
|
+
res = format_data
|
47
|
+
print "\n#{TTY::Markdown.parse(res, indent: 0)}"
|
48
|
+
end
|
49
|
+
|
50
|
+
def set_html
|
51
|
+
print PROMPT_SYSTEM.prefix
|
52
|
+
|
53
|
+
wait
|
54
|
+
|
55
|
+
print "HTML is ready\n"
|
56
|
+
show_html
|
57
|
+
end
|
58
|
+
|
59
|
+
def add_to_html(text, filepath)
|
60
|
+
text = text.gsub(/(?<![\\>\s])(?!\n[\n<])\n/m) { "<br/>\n" }
|
61
|
+
text = text.gsub(/~~~(.+?)~~~/m) do
|
62
|
+
m = Regexp.last_match
|
63
|
+
"~~~#{m[1].gsub("<br/>\n") { "\n" }}~~~"
|
64
|
+
end
|
65
|
+
text = text.gsub(/`(.+?)`/) do
|
66
|
+
m = Regexp.last_match
|
67
|
+
"`#{m[1].gsub("<br/>\n") { "\n" }}`"
|
68
|
+
end
|
69
|
+
|
70
|
+
`touch #{filepath}` unless File.exist?(filepath)
|
71
|
+
File.open(filepath, "w") do |f|
|
72
|
+
html = <<~HTML
|
73
|
+
<!doctype html>
|
74
|
+
<html lang="en">
|
75
|
+
<head>
|
76
|
+
<meta charset="utf-8">
|
77
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
78
|
+
<style type="text/css">
|
79
|
+
#{GITHUB_STYLE}
|
80
|
+
</style>
|
81
|
+
<title>Monadic Chat</title>
|
82
|
+
</head>
|
83
|
+
<body>
|
84
|
+
#{Kramdown::Document.new(text, syntax_highlighter: :rouge, syntax_highlighter_ops: {}).to_html}
|
85
|
+
</body>
|
86
|
+
<script src="https://code.jquery.com/jquery-3.6.3.min.js"></script>
|
87
|
+
<script src="https://code.jquery.com/ui/1.13.2/jquery-ui.min.js"></script>
|
88
|
+
<script>
|
89
|
+
$(window).on("load", function() {
|
90
|
+
$("html, body").animate({ scrollTop: $(document).height() }, 500);
|
91
|
+
});
|
92
|
+
</script>
|
93
|
+
</html>
|
94
|
+
HTML
|
95
|
+
f.write html
|
96
|
+
end
|
97
|
+
Launchy.open(filepath)
|
98
|
+
end
|
99
|
+
|
100
|
+
def show_html
|
101
|
+
res = format_data.sub(%r{::(.+?)/(.+?)\b}) do
|
102
|
+
" <span class='monadic_gray'>::</span> <span class='monadic_app'>#{Regexp.last_match(1)}</span> <span class='monadic_gray'>/</span> #{Regexp.last_match(2)}"
|
103
|
+
end
|
104
|
+
res = res.gsub("```") { "~~~" }
|
105
|
+
.gsub(/^(system):/i) { "<span class='monadic_system'> #{Regexp.last_match(1)} </span><br />" }
|
106
|
+
.gsub(/^(user):/i) { "<span class='monadic_user'> #{Regexp.last_match(1)} </span><br />" }
|
107
|
+
.gsub(/^(assistant|gpt):/i) { "<span class='monadic_chat'> #{Regexp.last_match(1)} </span><br />" }
|
108
|
+
add_to_html(res, TEMP_HTML)
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Cursor
|
4
|
+
class << self
|
5
|
+
def pos
|
6
|
+
res = +""
|
7
|
+
$stdin.raw do |stdin|
|
8
|
+
$stdout << "\e[6n"
|
9
|
+
$stdout.flush
|
10
|
+
while (c = stdin.getc) != "R"
|
11
|
+
res << c if c
|
12
|
+
end
|
13
|
+
end
|
14
|
+
m = res.match(/(?<row>\d+);(?<column>\d+)/)
|
15
|
+
{ row: Integer(m[:row]), column: Integer(m[:column]) }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module TTY
|
21
|
+
class PromptX < Prompt
|
22
|
+
attr_reader :prefix
|
23
|
+
|
24
|
+
def initialize(active_color:, prefix:, history: true)
|
25
|
+
@interrupt = lambda do
|
26
|
+
print TTY::Cursor.clear_screen_down
|
27
|
+
print "\e[2J\e[f"
|
28
|
+
res = TTY::Prompt.new.yes?("Quit the app?")
|
29
|
+
exit if res
|
30
|
+
end
|
31
|
+
|
32
|
+
super(active_color: active_color, prefix: prefix, interrupt: @interrupt)
|
33
|
+
@history = history
|
34
|
+
@prefix = prefix
|
35
|
+
end
|
36
|
+
|
37
|
+
def readline(text = "")
|
38
|
+
puts @prefix
|
39
|
+
begin
|
40
|
+
Readline.readline(text, @history)
|
41
|
+
rescue Interrupt
|
42
|
+
@interrupt.call
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
module Markdown
|
48
|
+
# Converts a Kramdown::Document tree to a terminal friendly output
|
49
|
+
class Converter < ::Kramdown::Converter::Base
|
50
|
+
def convert_p(ell, opts)
|
51
|
+
indent = SPACE * @current_indent
|
52
|
+
result = []
|
53
|
+
|
54
|
+
result << indent unless %i[blockquote li].include?(opts[:parent].type)
|
55
|
+
|
56
|
+
opts[:indent] = @current_indent
|
57
|
+
opts[:indent] = 0 if opts[:parent].type == :blockquote
|
58
|
+
|
59
|
+
content = inner(ell, opts)
|
60
|
+
|
61
|
+
symbols = %q{[-!$%^&*()_+|~=`{}\[\]:";'<>?,.\/]}
|
62
|
+
# result << content.join.gsub(/(?<!#{symbols})\n(?!#{symbols})/m) { " " }.gsub(/ +/) { " " }
|
63
|
+
result << content.join.gsub(/(?<!#{symbols})\n(?!#{symbols})/m) { "" }
|
64
|
+
result << NEWLINE unless result.last.to_s.end_with?(NEWLINE)
|
65
|
+
result
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class MonadicError < StandardError
|
72
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class MonadicApp
|
4
|
+
##################################################
|
5
|
+
# methods for user interaction
|
6
|
+
##################################################
|
7
|
+
|
8
|
+
def user_input(text = "")
|
9
|
+
if count_lines_below < 1
|
10
|
+
ask_clear
|
11
|
+
user_input
|
12
|
+
else
|
13
|
+
res = PROMPT_USER.readline(text)
|
14
|
+
print TTY::Cursor.clear_line_after
|
15
|
+
res == "" ? nil : res
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def show_greet
|
20
|
+
current_mode = case @method
|
21
|
+
when "completions"
|
22
|
+
PASTEL.red("Research")
|
23
|
+
when "chat/completions"
|
24
|
+
PASTEL.green("Normal")
|
25
|
+
end
|
26
|
+
greet_md = <<~GREET
|
27
|
+
- You are currently in **#{current_mode}** mode
|
28
|
+
- Type **help** or **menu** to see available commands
|
29
|
+
GREET
|
30
|
+
print PROMPT_SYSTEM.prefix
|
31
|
+
print "\n#{TTY::Markdown.parse(greet_md, indent: 0).strip}\n"
|
32
|
+
end
|
33
|
+
|
34
|
+
def confirm_query(input)
|
35
|
+
if input.size < MIN_LENGTH
|
36
|
+
PROMPT_SYSTEM.yes?("Would you like to proceed with this (very short) prompt?")
|
37
|
+
else
|
38
|
+
true
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,269 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class MonadicApp
|
4
|
+
##################################################
|
5
|
+
# methods for preparation and updating
|
6
|
+
##################################################
|
7
|
+
|
8
|
+
def fulfill_placeholders
|
9
|
+
input = nil
|
10
|
+
replacements = []
|
11
|
+
mode = :replace
|
12
|
+
|
13
|
+
@placeholders.each do |key, val|
|
14
|
+
if key == "mode"
|
15
|
+
mode = val
|
16
|
+
next
|
17
|
+
end
|
18
|
+
|
19
|
+
input = if mode == :replace
|
20
|
+
val
|
21
|
+
else
|
22
|
+
PROMPT_SYSTEM.readline("#{val}: ")
|
23
|
+
end
|
24
|
+
|
25
|
+
unless input
|
26
|
+
replacements.clear
|
27
|
+
break
|
28
|
+
end
|
29
|
+
replacements << [key, input]
|
30
|
+
end
|
31
|
+
if replacements.empty?
|
32
|
+
false
|
33
|
+
else
|
34
|
+
replacements.each do |key, value|
|
35
|
+
case @method
|
36
|
+
when "completions"
|
37
|
+
@template.gsub!(key, value)
|
38
|
+
when "chat/completions"
|
39
|
+
@template["messages"][0]["content"].gsub!(key, value)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
true
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def wait
|
47
|
+
return self if @threads.empty?
|
48
|
+
|
49
|
+
print TTY::Cursor.save
|
50
|
+
message = PASTEL.red "Processing contextual data #{SPINNER} "
|
51
|
+
print message
|
52
|
+
|
53
|
+
TIMEOUT_SEC.times do |i|
|
54
|
+
raise MonadicError, "Error: something went wrong" if i + 1 == TIMEOUT_SEC
|
55
|
+
|
56
|
+
break if @threads.empty?
|
57
|
+
|
58
|
+
sleep 1
|
59
|
+
end
|
60
|
+
print TTY::Cursor.restore
|
61
|
+
print TTY::Cursor.clear_char(message.size)
|
62
|
+
|
63
|
+
self
|
64
|
+
end
|
65
|
+
|
66
|
+
def objectify
|
67
|
+
case @method
|
68
|
+
when "completions"
|
69
|
+
m = /\n\n```json\s*(\{.+\})\s*```\n\n/m.match(@template)
|
70
|
+
json = m[1].gsub(/(?!\\\\\\)\\\\"/) { '\\\"' }
|
71
|
+
JSON.parse(json)
|
72
|
+
when "chat/completions"
|
73
|
+
@template
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def prepare_params(input)
|
78
|
+
params = @params.dup
|
79
|
+
case @method
|
80
|
+
when "completions"
|
81
|
+
template = @template.dup.sub("{{PROMPT}}", input).sub("{{MAX_TOKENS}}", (@params["max_tokens"] / 2).to_s)
|
82
|
+
params["prompt"] = template
|
83
|
+
when "chat/completions"
|
84
|
+
@template["messages"] << { "role" => "user", "content" => input }
|
85
|
+
params["messages"] = @template["messages"]
|
86
|
+
end
|
87
|
+
params
|
88
|
+
end
|
89
|
+
|
90
|
+
def update_template(res)
|
91
|
+
case @method
|
92
|
+
when "completions"
|
93
|
+
updated = @update_proc.call(res)
|
94
|
+
json = updated.to_json.strip
|
95
|
+
@template.sub!(/\n\n```json.+```\n\n/m, "\n\n```json\n#{json}\n```\n\n")
|
96
|
+
when "chat/completions"
|
97
|
+
@template["messages"] << { "role" => "assistant", "content" => res }
|
98
|
+
@template["messages"] = @update_proc.call(@template["messages"])
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
##################################################
|
103
|
+
# functions for binding data
|
104
|
+
##################################################
|
105
|
+
|
106
|
+
def bind_normal_mode(input, num_retry: 0)
|
107
|
+
print PROMPT_ASSISTANT.prefix, "\n"
|
108
|
+
print TTY::Cursor.save
|
109
|
+
|
110
|
+
wait
|
111
|
+
|
112
|
+
params = prepare_params(input)
|
113
|
+
print TTY::Cursor.save
|
114
|
+
|
115
|
+
escaping = +""
|
116
|
+
last_chunk = +""
|
117
|
+
response = +""
|
118
|
+
spinning = false
|
119
|
+
res = @completion.run(params, num_retry: num_retry) do |chunk|
|
120
|
+
if escaping
|
121
|
+
chunk = escaping + chunk
|
122
|
+
escaping = ""
|
123
|
+
end
|
124
|
+
|
125
|
+
if /(?:\\\z)/ =~ chunk
|
126
|
+
escaping += chunk
|
127
|
+
next
|
128
|
+
else
|
129
|
+
chunk = chunk.gsub('\\n', "\n")
|
130
|
+
response << chunk
|
131
|
+
end
|
132
|
+
|
133
|
+
if count_lines_below > 1
|
134
|
+
print PASTEL.magenta(last_chunk)
|
135
|
+
elsif !spinning
|
136
|
+
print PASTEL.red SPINNER
|
137
|
+
spinning = true
|
138
|
+
end
|
139
|
+
|
140
|
+
last_chunk = chunk
|
141
|
+
end
|
142
|
+
|
143
|
+
print TTY::Cursor.restore
|
144
|
+
print TTY::Cursor.clear_screen_down
|
145
|
+
|
146
|
+
text = response.gsub(/(?<![\\>\s])(?!\n[\n<])\n/m) { "{{NEWLINE}}\n" }
|
147
|
+
text = text.gsub(/```(.+?)```/m) do
|
148
|
+
m = Regexp.last_match
|
149
|
+
"```#{m[1].gsub("{{NEWLINE}}\n") { "\n" }}```"
|
150
|
+
end
|
151
|
+
text = text.gsub(/`(.+?)`/) do
|
152
|
+
m = Regexp.last_match
|
153
|
+
"`#{m[1].gsub("{{NEWLINE}}\n") { "\n" }}`"
|
154
|
+
end
|
155
|
+
|
156
|
+
text = text.gsub(/(?!\\\\)\\/) { "" }
|
157
|
+
print TTY::Markdown.parse(text).gsub("{{NEWLINE}}") { "\n" }.strip
|
158
|
+
print "\n"
|
159
|
+
|
160
|
+
update_template(res)
|
161
|
+
end
|
162
|
+
|
163
|
+
def bind_research_mode(input, num_retry: 0)
|
164
|
+
print PROMPT_ASSISTANT.prefix, "\n"
|
165
|
+
|
166
|
+
wait
|
167
|
+
|
168
|
+
params = prepare_params(input)
|
169
|
+
print TTY::Cursor.save
|
170
|
+
|
171
|
+
@threads << true
|
172
|
+
Thread.new do
|
173
|
+
response_all_shown = false
|
174
|
+
key_start = /"#{@prop_newdata}":\s*"/
|
175
|
+
key_finish = /\s+###\s*"/m
|
176
|
+
started = false
|
177
|
+
escaping = +""
|
178
|
+
last_chunk = +""
|
179
|
+
finished = false
|
180
|
+
response = +""
|
181
|
+
spinning = false
|
182
|
+
res = @completion.run(params, num_retry: num_retry) do |chunk|
|
183
|
+
if finished && !response_all_shown
|
184
|
+
response_all_shown = true
|
185
|
+
@responses << response.sub(/\s+###\s*".*/m, "")
|
186
|
+
if spinning
|
187
|
+
TTY::Cursor.backword(" ▹▹▹▹▹ ".size)
|
188
|
+
TTY::Cursor.clear_char(" ▹▹▹▹▹ ".size)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
unless finished
|
193
|
+
if escaping
|
194
|
+
chunk = escaping + chunk
|
195
|
+
escaping = ""
|
196
|
+
end
|
197
|
+
|
198
|
+
if /(?:\\\z)/ =~ chunk
|
199
|
+
escaping += chunk
|
200
|
+
next
|
201
|
+
else
|
202
|
+
chunk = chunk.gsub('\\n', "\n")
|
203
|
+
response << chunk
|
204
|
+
end
|
205
|
+
|
206
|
+
if started && !finished
|
207
|
+
if key_finish =~ response
|
208
|
+
finished = true
|
209
|
+
else
|
210
|
+
if count_lines_below > 1
|
211
|
+
print PASTEL.magenta(last_chunk)
|
212
|
+
elsif !spinning
|
213
|
+
print PASTEL.red SPINNER
|
214
|
+
spinning = true
|
215
|
+
end
|
216
|
+
last_chunk = chunk
|
217
|
+
end
|
218
|
+
elsif !started && !finished && key_start =~ response
|
219
|
+
started = true
|
220
|
+
response = +""
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
unless response_all_shown
|
226
|
+
if spinning
|
227
|
+
TTY::Cursor.backword(SPINNER.size)
|
228
|
+
TTY::Cursor.clear_char(SPINNER.size)
|
229
|
+
end
|
230
|
+
@responses << response.sub(/\s+###\s*".*/m, "")
|
231
|
+
end
|
232
|
+
|
233
|
+
update_template(res)
|
234
|
+
@threads.clear
|
235
|
+
rescue StandardError => e
|
236
|
+
@threads.clear
|
237
|
+
@responses << <<~ERROR
|
238
|
+
Error: something went wrong in a thread"
|
239
|
+
#{e.message}
|
240
|
+
#{e.backtrace}
|
241
|
+
ERROR
|
242
|
+
end
|
243
|
+
|
244
|
+
loop do
|
245
|
+
if @responses.empty?
|
246
|
+
sleep 1
|
247
|
+
else
|
248
|
+
print TTY::Cursor.restore
|
249
|
+
print TTY::Cursor.clear_screen_down
|
250
|
+
text = @responses.pop
|
251
|
+
|
252
|
+
text = text.gsub(/(?<![\\>\s])(?!\n[\n<])\n/m) { "{{NEWLINE}}\n" }
|
253
|
+
text = text.gsub(/```(.+?)```/m) do
|
254
|
+
m = Regexp.last_match
|
255
|
+
"```#{m[1].gsub("{{NEWLINE}}\n") { "\n" }}```"
|
256
|
+
end
|
257
|
+
text = text.gsub(/`(.+?)`/) do
|
258
|
+
m = Regexp.last_match
|
259
|
+
"`#{m[1].gsub("{{NEWLINE}}\n") { "\n" }}`"
|
260
|
+
end
|
261
|
+
|
262
|
+
text = text.gsub(/(?!\\\\)\\/) { "" }
|
263
|
+
print TTY::Markdown.parse(text).gsub("{{NEWLINE}}") { "\n" }.strip
|
264
|
+
print "\n"
|
265
|
+
break
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|