monadic-chat 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|