scout-ai 1.1.0 → 1.1.2
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 +4 -4
- data/.vimproject +12 -1
- data/Rakefile +2 -0
- data/VERSION +1 -1
- data/bin/scout-ai +46 -0
- data/lib/scout/llm/agent/chat.rb +2 -2
- data/lib/scout/llm/ask.rb +10 -2
- data/lib/scout/llm/backends/huggingface.rb +0 -2
- data/lib/scout/llm/backends/ollama.rb +0 -3
- data/lib/scout/llm/backends/openai.rb +4 -2
- data/lib/scout/llm/backends/openwebui.rb +1 -4
- data/lib/scout/llm/backends/relay.rb +1 -3
- data/lib/scout/llm/backends/responses.rb +25 -14
- data/lib/scout/llm/chat/annotation.rb +195 -0
- data/lib/scout/llm/chat/parse.rb +139 -0
- data/lib/scout/llm/chat/process/clear.rb +29 -0
- data/lib/scout/llm/chat/process/files.rb +96 -0
- data/lib/scout/llm/chat/process/options.rb +52 -0
- data/lib/scout/llm/chat/process/tools.rb +173 -0
- data/lib/scout/llm/chat/process.rb +16 -0
- data/lib/scout/llm/chat.rb +26 -674
- data/lib/scout/llm/mcp.rb +1 -1
- data/lib/scout/llm/tools/call.rb +11 -0
- data/lib/scout/llm/tools/mcp.rb +4 -0
- data/lib/scout/llm/tools/workflow.rb +3 -1
- data/lib/scout/llm/utils.rb +2 -17
- data/scout-ai.gemspec +13 -3
- data/scout_commands/llm/ask +16 -7
- data/scout_commands/llm/process +1 -1
- data/test/scout/llm/backends/test_anthropic.rb +2 -2
- data/test/scout/llm/backends/test_responses.rb +9 -9
- data/test/scout/llm/chat/test_parse.rb +126 -0
- data/test/scout/llm/chat/test_process.rb +123 -0
- data/test/scout/llm/test_agent.rb +1 -2
- data/test/scout/llm/test_chat.rb +2 -178
- metadata +38 -2
- data/lib/scout/llm/parse.rb +0 -91
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: aaebf68f2d4bcbd388e292dbdd536558e0b33daf744845bc20fd7bf92e3693f4
|
|
4
|
+
data.tar.gz: 46d9aafabb314aaca7056e2982d911a69c165da1dfc668fd0b3755e43bd2b3a5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3ce6e03f60080d5f103eddd7e14df23fe90ee8f446563084fbfc30eebfdabbcb193cc061b22058bf9560eb335586f678cdb81954726b83523030e9cd25d0e335
|
|
7
|
+
data.tar.gz: a6d7c13bf096be0b1947d64461acfaaa38ad94bafa33112b28bd167b832fcb16fa5b913fbd16699be058d67e7ebb38efee1322237ec8c90010fef73222b3f882
|
data/.vimproject
CHANGED
|
@@ -6,6 +6,7 @@ scout-ai=$PWD filter="*.rb *.rake Rakefile *.rdoc *.R *.sh *.js *.haml *.sass *.
|
|
|
6
6
|
scout-ai
|
|
7
7
|
}
|
|
8
8
|
chats=chats filter="*"{
|
|
9
|
+
parse
|
|
9
10
|
|
|
10
11
|
test_tool
|
|
11
12
|
|
|
@@ -98,7 +99,6 @@ scout-ai=$PWD filter="*.rb *.rake Rakefile *.rdoc *.R *.sh *.js *.haml *.sass *.
|
|
|
98
99
|
scout=scout{
|
|
99
100
|
llm=llm{
|
|
100
101
|
utils.rb
|
|
101
|
-
parse.rb
|
|
102
102
|
tools.rb
|
|
103
103
|
tools=tools{
|
|
104
104
|
mcp.rb
|
|
@@ -107,6 +107,17 @@ scout-ai=$PWD filter="*.rb *.rake Rakefile *.rdoc *.R *.sh *.js *.haml *.sass *.
|
|
|
107
107
|
call.rb
|
|
108
108
|
}
|
|
109
109
|
chat.rb
|
|
110
|
+
chat=chat{
|
|
111
|
+
annotation.rb
|
|
112
|
+
parse.rb
|
|
113
|
+
process.rb
|
|
114
|
+
process=process{
|
|
115
|
+
tools.rb
|
|
116
|
+
files.rb
|
|
117
|
+
clear.rb
|
|
118
|
+
options.rb
|
|
119
|
+
}
|
|
120
|
+
}
|
|
110
121
|
|
|
111
122
|
backends=backends{
|
|
112
123
|
openai.rb
|
data/Rakefile
CHANGED
|
@@ -18,7 +18,9 @@ Juwelier::Tasks.new do |gem|
|
|
|
18
18
|
# dependencies defined in Gemfile
|
|
19
19
|
gem.add_runtime_dependency 'scout-rig', '>= 0'
|
|
20
20
|
gem.add_runtime_dependency 'ruby-openai', '>= 0'
|
|
21
|
+
gem.add_runtime_dependency 'ollama-ai', '>= 0'
|
|
21
22
|
gem.add_runtime_dependency 'ruby-mcp-client', '>= 0'
|
|
23
|
+
gem.add_runtime_dependency 'hnswlib', '>= 0'
|
|
22
24
|
end
|
|
23
25
|
Juwelier::RubygemsDotOrgTasks.new
|
|
24
26
|
require 'rake/testtask'
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.1.
|
|
1
|
+
1.1.2
|
data/bin/scout-ai
CHANGED
|
@@ -2,6 +2,52 @@
|
|
|
2
2
|
|
|
3
3
|
$LOAD_PATH.unshift File.join(__dir__, '../lib')
|
|
4
4
|
|
|
5
|
+
if _i = ARGV.index("--log")
|
|
6
|
+
require 'scout/log'
|
|
7
|
+
log = ARGV[_i+1]
|
|
8
|
+
Log.severity = log.to_i
|
|
9
|
+
ARGV.delete "--log"
|
|
10
|
+
ARGV.delete log
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
dev_dir = nil
|
|
14
|
+
if _i = ARGV.index("--dev")
|
|
15
|
+
dev_dir = ARGV[_i+1]
|
|
16
|
+
ARGV.delete "--dev"
|
|
17
|
+
ARGV.delete dev_dir
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
if dev_dir.nil?
|
|
21
|
+
_s = nil
|
|
22
|
+
ARGV.each_with_index do |s,i|
|
|
23
|
+
if s.match(/^--dev(?:=(.*))?/)
|
|
24
|
+
dev_dir = $1
|
|
25
|
+
_s = s
|
|
26
|
+
next
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
ARGV.delete _s if _s
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
if dev_dir.nil? && ENV["SCOUT_DEV"]
|
|
33
|
+
dev_dir = ENV["SCOUT_DEV"]
|
|
34
|
+
ARGV.delete "--dev"
|
|
35
|
+
ARGV.delete dev_dir
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
if dev_dir
|
|
39
|
+
['scout-*/lib'].each do |pattern|
|
|
40
|
+
Dir.glob(File.join(File.expand_path(dev_dir), pattern)).each do |f|
|
|
41
|
+
$LOAD_PATH.unshift f
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
['rbbt-*/lib'].each do |pattern|
|
|
45
|
+
Dir.glob(File.join(File.expand_path(dev_dir), pattern)).each do |f|
|
|
46
|
+
$LOAD_PATH.unshift f
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
5
51
|
require 'scout-ai'
|
|
6
52
|
|
|
7
53
|
load Scout.bin.scout.find
|
data/lib/scout/llm/agent/chat.rb
CHANGED
|
@@ -28,8 +28,8 @@ module LLM
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
def chat(
|
|
32
|
-
response = ask(current_chat,
|
|
31
|
+
def chat(options = {})
|
|
32
|
+
response = ask(current_chat, options.merge(return_messages: true))
|
|
33
33
|
if Array === response
|
|
34
34
|
current_chat.concat(response)
|
|
35
35
|
current_chat.answer
|
data/lib/scout/llm/ask.rb
CHANGED
|
@@ -34,16 +34,24 @@ module LLM
|
|
|
34
34
|
|
|
35
35
|
endpoint, persist = IndiferentHash.process_options options, :endpoint, :persist, persist: true
|
|
36
36
|
|
|
37
|
-
endpoint ||= Scout::Config.get :endpoint, :ask, :llm, env: 'ASK_ENDPOINT,LLM_ENDPOINT'
|
|
37
|
+
endpoint ||= Scout::Config.get :endpoint, :ask, :llm, env: 'ASK_ENDPOINT,LLM_ENDPOINT,ENDPOINT,LLM,ASK'
|
|
38
38
|
if endpoint && Scout.etc.AI[endpoint].exists?
|
|
39
39
|
options = IndiferentHash.add_defaults options, Scout.etc.AI[endpoint].yaml
|
|
40
40
|
elsif endpoint && endpoint != ""
|
|
41
41
|
raise "Endpoint not found #{endpoint}"
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
if options[:backend].to_s == 'responses'
|
|
45
|
+
messages = Chat.clear(messages, 'previous_response_id')
|
|
46
|
+
else
|
|
47
|
+
messages = Chat.clean(messages, 'previous_response_id')
|
|
48
|
+
options.delete :previous_response_id
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
Log.high Log.color :green, "Asking #{endpoint || 'client'}: #{options[:previous_response_id]}\n" + LLM.print(messages)
|
|
45
52
|
tools = options[:tools]
|
|
46
53
|
Log.high "Tools: #{Log.fingerprint tools.keys}}" if tools
|
|
54
|
+
Log.debug "#{Log.fingerprint tools}}" if tools
|
|
47
55
|
|
|
48
56
|
res = Persist.persist(endpoint, :json, prefix: "LLM ask", other: options.merge(messages: messages), persist: persist) do
|
|
49
57
|
backend = IndiferentHash.process_options options, :backend
|
|
@@ -58,8 +58,10 @@ module LLM
|
|
|
58
58
|
model ||= LLM.get_url_config(:model, url, :openai_ask, :ask, :openai, env: 'OPENAI_MODEL', default: "gpt-4.1")
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
-
case format
|
|
62
|
-
when
|
|
61
|
+
case format
|
|
62
|
+
when Hash
|
|
63
|
+
options[:response_format] = format
|
|
64
|
+
when 'json', 'json_object', :json, :json_object
|
|
63
65
|
options[:response_format] = {type: 'json_object'}
|
|
64
66
|
else
|
|
65
67
|
options[:response_format] = {type: format}
|
|
@@ -106,35 +106,44 @@ module LLM
|
|
|
106
106
|
def self.process_input(messages)
|
|
107
107
|
messages = self.tools_to_responses messages
|
|
108
108
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
res = []
|
|
110
|
+
messages.each do |message|
|
|
111
|
+
message = IndiferentHash.setup(message)
|
|
112
|
+
|
|
113
|
+
role = message[:role]
|
|
114
|
+
|
|
115
|
+
case role.to_s
|
|
116
|
+
when 'image'
|
|
112
117
|
path = message[:content]
|
|
113
118
|
path = LLM.find_file path
|
|
114
119
|
if Open.remote?(path)
|
|
115
|
-
{role: :user, content: {type: :input_image, image_url: path }}
|
|
120
|
+
res << {role: :user, content: {type: :input_image, image_url: path }}
|
|
116
121
|
elsif Open.exists?(path)
|
|
117
122
|
path = self.encode_image(path)
|
|
118
|
-
{role: :user, content: [{type: :input_image, image_url: path }]}
|
|
123
|
+
res << {role: :user, content: [{type: :input_image, image_url: path }]}
|
|
119
124
|
else
|
|
120
|
-
raise
|
|
125
|
+
raise "Image does not exist in #{path}"
|
|
121
126
|
end
|
|
122
|
-
|
|
127
|
+
when 'pdf'
|
|
123
128
|
path = original_path = message[:content]
|
|
124
129
|
if Open.remote?(path)
|
|
125
|
-
{role: :user, content: {type: :input_file, file_url: path }}
|
|
130
|
+
res << {role: :user, content: {type: :input_file, file_url: path }}
|
|
126
131
|
elsif Open.exists?(path)
|
|
127
132
|
data = self.encode_pdf(path)
|
|
128
|
-
{role: :user, content: [{type: :input_file, file_data: data, filename: File.basename(path) }]}
|
|
133
|
+
res << {role: :user, content: [{type: :input_file, file_data: data, filename: File.basename(path) }]}
|
|
129
134
|
else
|
|
130
|
-
raise
|
|
135
|
+
raise "PDF does not exist in #{path}"
|
|
131
136
|
end
|
|
132
|
-
|
|
133
|
-
{role: :tool, content: {type: "web_search_preview"} }
|
|
137
|
+
when 'websearch'
|
|
138
|
+
res << {role: :tool, content: {type: "web_search_preview"} }
|
|
139
|
+
when 'previous_response_id'
|
|
140
|
+
res = []
|
|
134
141
|
else
|
|
135
|
-
message
|
|
142
|
+
res << message
|
|
136
143
|
end
|
|
137
|
-
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
res
|
|
138
147
|
end
|
|
139
148
|
|
|
140
149
|
def self.process_format(format)
|
|
@@ -190,6 +199,7 @@ module LLM
|
|
|
190
199
|
|
|
191
200
|
messages = LLM.chat(question)
|
|
192
201
|
options = options.merge LLM.options messages
|
|
202
|
+
|
|
193
203
|
|
|
194
204
|
client, url, key, model, log_errors, return_messages, format, websearch, previous_response_id, tools, = IndiferentHash.process_options options,
|
|
195
205
|
:client, :url, :key, :model, :log_errors, :return_messages, :format, :websearch, :previous_response_id, :tools,
|
|
@@ -244,6 +254,7 @@ module LLM
|
|
|
244
254
|
Log.medium "Tools: #{Log.fingerprint tools.keys}}" if tools
|
|
245
255
|
|
|
246
256
|
messages = self.process_input messages
|
|
257
|
+
|
|
247
258
|
input = []
|
|
248
259
|
messages.each do |message|
|
|
249
260
|
parameters[:tools] ||= []
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
require 'scout/annotation'
|
|
2
|
+
module Chat
|
|
3
|
+
extend Annotation
|
|
4
|
+
|
|
5
|
+
def message(role, content)
|
|
6
|
+
self.append({role: role.to_s, content: content})
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def user(content)
|
|
10
|
+
message(:user, content)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def system(content)
|
|
14
|
+
message(:system, content)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def assistant(content)
|
|
18
|
+
message(:assistant, content)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def import(file)
|
|
22
|
+
message(:import, file)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def import_last(file)
|
|
26
|
+
message(:last, file)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def file(file)
|
|
30
|
+
message(:file, file)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def directory(directory)
|
|
34
|
+
message(:directory, directory)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def continue(file)
|
|
38
|
+
message(:continue, file)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def format(format)
|
|
42
|
+
message(:format, format)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def tool(*parts)
|
|
46
|
+
content = parts * "\n"
|
|
47
|
+
message(:tool, content)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def task(workflow, task_name, inputs = {})
|
|
51
|
+
input_str = IndiferentHash.print_options inputs
|
|
52
|
+
content = [workflow, task_name, input_str]*" "
|
|
53
|
+
message(:task, content)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def inline_task(workflow, task_name, inputs = {})
|
|
57
|
+
input_str = IndiferentHash.print_options inputs
|
|
58
|
+
content = [workflow, task_name, input_str]*" "
|
|
59
|
+
message(:inline_task, content)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def job(step)
|
|
63
|
+
message(:job, step.path)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def inline_job(step)
|
|
67
|
+
message(:inline_job, step.path)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def association(name, path, options = {})
|
|
72
|
+
options_str = IndiferentHash.print_options options
|
|
73
|
+
content = [name, path, options_str]*" "
|
|
74
|
+
message(:association, name)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def tag(content, name=nil, tag=:file, role=:user)
|
|
78
|
+
self.message role, LLM.tag(tag, content, name)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def ask(...)
|
|
83
|
+
LLM.ask(LLM.chat(self), ...)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def chat(...)
|
|
87
|
+
response = ask(...)
|
|
88
|
+
if Array === response
|
|
89
|
+
current_chat.concat(response)
|
|
90
|
+
final(response)
|
|
91
|
+
else
|
|
92
|
+
current_chat.push({role: :assistant, content: response})
|
|
93
|
+
response
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def json(...)
|
|
98
|
+
self.format :json
|
|
99
|
+
output = ask(...)
|
|
100
|
+
obj = JSON.parse output
|
|
101
|
+
if (Hash === obj) and obj.keys == ['content']
|
|
102
|
+
obj['content']
|
|
103
|
+
else
|
|
104
|
+
obj
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def json_format(format, ...)
|
|
109
|
+
self.format format
|
|
110
|
+
output = ask(...)
|
|
111
|
+
obj = JSON.parse output
|
|
112
|
+
if (Hash === obj) and obj.keys == ['content']
|
|
113
|
+
obj['content']
|
|
114
|
+
else
|
|
115
|
+
obj
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def branch
|
|
120
|
+
self.annotate self.dup
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def option(name, value)
|
|
124
|
+
self.message 'option', [name, value] * " "
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def endpoint(value)
|
|
128
|
+
option :endpoint, value
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def model(value)
|
|
132
|
+
option :model, value
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def image(file)
|
|
136
|
+
self.message :image, file
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Reporting
|
|
140
|
+
|
|
141
|
+
def print
|
|
142
|
+
LLM.print LLM.chat(self)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def final
|
|
146
|
+
LLM.purge(self).last
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def purge
|
|
150
|
+
Chat.setup(LLM.purge(self))
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def shed
|
|
154
|
+
self.annotate [final]
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def answer
|
|
158
|
+
final[:content]
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Write and save
|
|
162
|
+
|
|
163
|
+
def save(path, force = true)
|
|
164
|
+
path = path.to_s if Symbol === path
|
|
165
|
+
if not (Open.exists?(path) || Path === path || Path.located?(path))
|
|
166
|
+
path = Scout.chats.find[path]
|
|
167
|
+
end
|
|
168
|
+
return if Open.exists?(path) && ! force
|
|
169
|
+
Open.write path, LLM.print(self)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def write(path, force = true)
|
|
173
|
+
path = path.to_s if Symbol === path
|
|
174
|
+
if not (Open.exists?(path) || Path === path || Path.located?(path))
|
|
175
|
+
path = Scout.chats.find[path]
|
|
176
|
+
end
|
|
177
|
+
return if Open.exists?(path) && ! force
|
|
178
|
+
Open.write path, self.print
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def write_answer(path, force = true)
|
|
182
|
+
path = path.to_s if Symbol === path
|
|
183
|
+
if not (Open.exists?(path) || Path === path || Path.located?(path))
|
|
184
|
+
path = Scout.chats.find[path]
|
|
185
|
+
end
|
|
186
|
+
return if Open.exists?(path) && ! force
|
|
187
|
+
Open.write path, self.answer
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Image
|
|
191
|
+
def create_image(file, ...)
|
|
192
|
+
base64_image = LLM.image(LLM.chat(self), ...)
|
|
193
|
+
Open.write(file, Base64.decode(file_content), mode: 'wb')
|
|
194
|
+
end
|
|
195
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
module Chat
|
|
2
|
+
def self.parse(text, role = nil)
|
|
3
|
+
default_role = "user"
|
|
4
|
+
|
|
5
|
+
messages = []
|
|
6
|
+
current_role = role || default_role
|
|
7
|
+
current_content = ""
|
|
8
|
+
in_protected_block = false
|
|
9
|
+
protected_block_type = nil
|
|
10
|
+
protected_stack = []
|
|
11
|
+
|
|
12
|
+
role = default_role if role.nil?
|
|
13
|
+
|
|
14
|
+
file_lines = text.split("\n")
|
|
15
|
+
|
|
16
|
+
file_lines.each do |line|
|
|
17
|
+
stripped = line.strip
|
|
18
|
+
|
|
19
|
+
# Detect protected blocks
|
|
20
|
+
if stripped.start_with?("```")
|
|
21
|
+
if in_protected_block
|
|
22
|
+
in_protected_block = false
|
|
23
|
+
protected_block_type = nil
|
|
24
|
+
current_content << "\n" << line unless line.strip.empty?
|
|
25
|
+
else
|
|
26
|
+
in_protected_block = true
|
|
27
|
+
protected_block_type = :square
|
|
28
|
+
current_content << "\n" << line unless line.strip.empty?
|
|
29
|
+
end
|
|
30
|
+
next
|
|
31
|
+
elsif stripped.end_with?("]]") && in_protected_block && protected_block_type == :square
|
|
32
|
+
in_protected_block = false
|
|
33
|
+
protected_block_type = nil
|
|
34
|
+
line = line.sub("]]", "")
|
|
35
|
+
current_content << "\n" << line unless line.strip.empty?
|
|
36
|
+
next
|
|
37
|
+
elsif stripped.start_with?("[[")
|
|
38
|
+
in_protected_block = true
|
|
39
|
+
protected_block_type = :square
|
|
40
|
+
line = line.sub("[[", "")
|
|
41
|
+
current_content << "\n" << line unless line.strip.empty?
|
|
42
|
+
next
|
|
43
|
+
elsif stripped.end_with?("]]") && in_protected_block && protected_block_type == :square
|
|
44
|
+
in_protected_block = false
|
|
45
|
+
protected_block_type = nil
|
|
46
|
+
line = line.sub("]]", "")
|
|
47
|
+
current_content << "\n" << line unless line.strip.empty?
|
|
48
|
+
next
|
|
49
|
+
elsif stripped.match(/^.*:-- .* {{{/)
|
|
50
|
+
in_protected_block = true
|
|
51
|
+
protected_block_type = :square
|
|
52
|
+
line = line.sub(/^.*:-- (.*) {{{.*/, '<cmd_output cmd="\1">')
|
|
53
|
+
current_content << "\n" << line unless line.strip.empty?
|
|
54
|
+
next
|
|
55
|
+
elsif stripped.match(/^.*:--.* }}}/) && in_protected_block && protected_block_type == :square
|
|
56
|
+
in_protected_block = false
|
|
57
|
+
protected_block_type = nil
|
|
58
|
+
line = line.sub(/^.*:-- .* }}}.*/, "</cmd_output>")
|
|
59
|
+
current_content << "\n" << line unless line.strip.empty?
|
|
60
|
+
next
|
|
61
|
+
elsif in_protected_block
|
|
62
|
+
|
|
63
|
+
if protected_block_type == :xml
|
|
64
|
+
if stripped =~ %r{</(\w+)>}
|
|
65
|
+
closing_tag = $1
|
|
66
|
+
if protected_stack.last == closing_tag
|
|
67
|
+
protected_stack.pop
|
|
68
|
+
end
|
|
69
|
+
if protected_stack.empty?
|
|
70
|
+
in_protected_block = false
|
|
71
|
+
protected_block_type = nil
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
current_content << "\n" << line
|
|
76
|
+
next
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# XML-style tag handling (protected content)
|
|
80
|
+
if stripped =~ /^<(\w+)(\s+[^>]*)?>/
|
|
81
|
+
tag = $1
|
|
82
|
+
protected_stack.push(tag)
|
|
83
|
+
in_protected_block = true
|
|
84
|
+
protected_block_type = :xml
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Match a new message header
|
|
88
|
+
if line =~ /^([a-z0-9_]+):(.*)$/
|
|
89
|
+
role = $1
|
|
90
|
+
inline_content = $2.strip
|
|
91
|
+
|
|
92
|
+
current_content = current_content.strip if current_content
|
|
93
|
+
# Save current message if any
|
|
94
|
+
messages << { role: current_role, content: current_content }
|
|
95
|
+
|
|
96
|
+
if inline_content.empty?
|
|
97
|
+
# Block message
|
|
98
|
+
current_role = role
|
|
99
|
+
current_content = ""
|
|
100
|
+
else
|
|
101
|
+
# Inline message + next block is default role
|
|
102
|
+
messages << { role: role, content: inline_content }
|
|
103
|
+
current_role = 'user' if role == 'previous_response_id'
|
|
104
|
+
current_content = ""
|
|
105
|
+
end
|
|
106
|
+
else
|
|
107
|
+
if current_content.nil?
|
|
108
|
+
current_content = line
|
|
109
|
+
else
|
|
110
|
+
current_content += "\n" + line
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Final message
|
|
116
|
+
messages << { role: current_role || default_role, content: current_content.strip }
|
|
117
|
+
|
|
118
|
+
messages
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def self.print(chat)
|
|
122
|
+
return chat if String === chat
|
|
123
|
+
"\n" + chat.collect do |message|
|
|
124
|
+
IndiferentHash.setup message
|
|
125
|
+
case message[:content]
|
|
126
|
+
when Hash, Array
|
|
127
|
+
message[:role].to_s + ":\n\n" + message[:content].to_json
|
|
128
|
+
when nil, ''
|
|
129
|
+
message[:role].to_s + ":"
|
|
130
|
+
else
|
|
131
|
+
if %w(option previous_response_id function_call function_call_output).include? message[:role].to_s
|
|
132
|
+
message[:role].to_s + ": " + message[:content].to_s
|
|
133
|
+
else
|
|
134
|
+
message[:role].to_s + ":\n\n" + message[:content].to_s
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end * "\n\n"
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Chat
|
|
2
|
+
def self.clear(messages, role = 'clear')
|
|
3
|
+
new = []
|
|
4
|
+
|
|
5
|
+
messages.reverse.each do |message|
|
|
6
|
+
if message[:role].to_s == role
|
|
7
|
+
break
|
|
8
|
+
else
|
|
9
|
+
new << message
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
new.reverse
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.clean(messages, role = 'skip')
|
|
17
|
+
messages.reject do |message|
|
|
18
|
+
((String === message[:content]) && message[:content].empty?) ||
|
|
19
|
+
message[:role] == role
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.purge(chat)
|
|
24
|
+
chat.reject do |msg|
|
|
25
|
+
IndiferentHash.setup msg
|
|
26
|
+
msg[:role].to_s == 'previous_response_id'
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|