scout-ai 1.0.1 → 1.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 +4 -4
- data/.vimproject +20 -2
- data/Rakefile +1 -0
- data/VERSION +1 -1
- data/bin/scout-ai +46 -0
- data/lib/scout/llm/agent/chat.rb +4 -7
- data/lib/scout/llm/agent/delegate.rb +12 -0
- data/lib/scout/llm/agent.rb +2 -2
- data/lib/scout/llm/ask.rb +18 -2
- data/lib/scout/llm/backends/huggingface.rb +0 -2
- data/lib/scout/llm/backends/ollama.rb +6 -6
- data/lib/scout/llm/backends/openai.rb +7 -4
- 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 +34 -18
- 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 -662
- data/lib/scout/llm/mcp.rb +1 -1
- data/lib/scout/llm/tools/call.rb +22 -1
- data/lib/scout/llm/tools/knowledge_base.rb +15 -14
- data/lib/scout/llm/tools/mcp.rb +4 -0
- data/lib/scout/llm/tools/workflow.rb +54 -15
- data/lib/scout/llm/tools.rb +42 -0
- data/lib/scout/llm/utils.rb +2 -17
- data/scout-ai.gemspec +13 -4
- data/scout_commands/agent/ask +36 -12
- data/scout_commands/llm/ask +17 -7
- data/scout_commands/llm/process +1 -1
- data/test/scout/llm/backends/test_anthropic.rb +2 -2
- data/test/scout/llm/backends/test_ollama.rb +1 -1
- 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 +2 -25
- data/test/scout/llm/test_chat.rb +2 -178
- metadata +25 -3
- data/lib/scout/llm/parse.rb +0 -91
data/lib/scout/llm/chat.rb
CHANGED
|
@@ -1,703 +1,67 @@
|
|
|
1
|
+
#require_relative 'parse'
|
|
1
2
|
require_relative 'utils'
|
|
2
|
-
require_relative 'parse'
|
|
3
3
|
require_relative 'tools'
|
|
4
|
-
|
|
4
|
+
require_relative 'chat/annotation'
|
|
5
|
+
require_relative 'chat/parse'
|
|
6
|
+
require_relative 'chat/process'
|
|
5
7
|
|
|
6
8
|
module LLM
|
|
7
|
-
def self.content_tokens(message)
|
|
8
|
-
Shellwords.split(message[:content].strip)
|
|
9
|
-
end
|
|
10
|
-
|
|
11
9
|
def self.messages(question, role = nil)
|
|
12
10
|
default_role = "user"
|
|
13
11
|
|
|
14
12
|
if Array === question
|
|
15
13
|
return question.collect do |q|
|
|
16
14
|
if String === q
|
|
17
|
-
{role: default_role, content: q}
|
|
15
|
+
{role: role || default_role, content: q}
|
|
18
16
|
else
|
|
19
17
|
q
|
|
20
18
|
end
|
|
21
19
|
end
|
|
22
20
|
end
|
|
23
21
|
|
|
24
|
-
|
|
25
|
-
current_role = default_role
|
|
26
|
-
current_content = ""
|
|
27
|
-
in_protected_block = false
|
|
28
|
-
protected_block_type = nil
|
|
29
|
-
protected_stack = []
|
|
30
|
-
|
|
31
|
-
role = default_role if role.nil?
|
|
32
|
-
|
|
33
|
-
file_lines = question.split("\n")
|
|
34
|
-
|
|
35
|
-
file_lines.each do |line|
|
|
36
|
-
stripped = line.strip
|
|
37
|
-
|
|
38
|
-
# Detect protected blocks
|
|
39
|
-
if stripped.start_with?("```")
|
|
40
|
-
if in_protected_block
|
|
41
|
-
in_protected_block = false
|
|
42
|
-
protected_block_type = nil
|
|
43
|
-
current_content << "\n" << line unless line.strip.empty?
|
|
44
|
-
else
|
|
45
|
-
in_protected_block = true
|
|
46
|
-
protected_block_type = :square
|
|
47
|
-
current_content << "\n" << line unless line.strip.empty?
|
|
48
|
-
end
|
|
49
|
-
next
|
|
50
|
-
elsif stripped.end_with?("]]") && in_protected_block && protected_block_type == :square
|
|
51
|
-
in_protected_block = false
|
|
52
|
-
protected_block_type = nil
|
|
53
|
-
line = line.sub("]]", "")
|
|
54
|
-
current_content << "\n" << line unless line.strip.empty?
|
|
55
|
-
next
|
|
56
|
-
elsif stripped.start_with?("[[")
|
|
57
|
-
in_protected_block = true
|
|
58
|
-
protected_block_type = :square
|
|
59
|
-
line = line.sub("[[", "")
|
|
60
|
-
current_content << "\n" << line unless line.strip.empty?
|
|
61
|
-
next
|
|
62
|
-
elsif stripped.end_with?("]]") && in_protected_block && protected_block_type == :square
|
|
63
|
-
in_protected_block = false
|
|
64
|
-
protected_block_type = nil
|
|
65
|
-
line = line.sub("]]", "")
|
|
66
|
-
current_content << "\n" << line unless line.strip.empty?
|
|
67
|
-
next
|
|
68
|
-
elsif stripped.match(/^.*:-- .* {{{/)
|
|
69
|
-
in_protected_block = true
|
|
70
|
-
protected_block_type = :square
|
|
71
|
-
line = line.sub(/^.*:-- (.*) {{{/, '<cmd_output cmd="\1">')
|
|
72
|
-
current_content << "\n" << line unless line.strip.empty?
|
|
73
|
-
next
|
|
74
|
-
elsif stripped.match(/^.*:--.* }}}/) && in_protected_block && protected_block_type == :square
|
|
75
|
-
in_protected_block = false
|
|
76
|
-
protected_block_type = nil
|
|
77
|
-
line = line.sub(/^.*:-- .* }}}.*/, "</cmd_output>")
|
|
78
|
-
current_content << "\n" << line unless line.strip.empty?
|
|
79
|
-
next
|
|
80
|
-
elsif in_protected_block
|
|
81
|
-
|
|
82
|
-
if protected_block_type == :xml
|
|
83
|
-
if stripped =~ %r{</(\w+)>}
|
|
84
|
-
closing_tag = $1
|
|
85
|
-
if protected_stack.last == closing_tag
|
|
86
|
-
protected_stack.pop
|
|
87
|
-
end
|
|
88
|
-
if protected_stack.empty?
|
|
89
|
-
in_protected_block = false
|
|
90
|
-
protected_block_type = nil
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
current_content << "\n" << line
|
|
95
|
-
next
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
# XML-style tag handling (protected content)
|
|
99
|
-
if stripped =~ /^<(\w+)(\s+[^>]*)?>/
|
|
100
|
-
tag = $1
|
|
101
|
-
protected_stack.push(tag)
|
|
102
|
-
in_protected_block = true
|
|
103
|
-
protected_block_type = :xml
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# Match a new message header
|
|
107
|
-
if line =~ /^([a-z0-9_]+):(.*)$/
|
|
108
|
-
role = $1
|
|
109
|
-
inline_content = $2.strip
|
|
110
|
-
|
|
111
|
-
current_content = current_content.strip if current_content
|
|
112
|
-
# Save current message if any
|
|
113
|
-
messages << { role: current_role, content: current_content }
|
|
114
|
-
|
|
115
|
-
if inline_content.empty?
|
|
116
|
-
# Block message
|
|
117
|
-
current_role = role
|
|
118
|
-
current_content = ""
|
|
119
|
-
else
|
|
120
|
-
# Inline message + next block is default role
|
|
121
|
-
messages << { role: role, content: inline_content }
|
|
122
|
-
current_role = 'user' if role == 'previous_response_id'
|
|
123
|
-
current_content = ""
|
|
124
|
-
end
|
|
125
|
-
else
|
|
126
|
-
if current_content.nil?
|
|
127
|
-
current_content = line
|
|
128
|
-
else
|
|
129
|
-
current_content << "\n" << line
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
# Final message
|
|
135
|
-
messages << { role: current_role || default_role, content: current_content.strip }
|
|
136
|
-
|
|
137
|
-
messages
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def self.find_file(file, original = nil, caller_lib_dir = Path.caller_lib_dir(nil, 'chats'))
|
|
141
|
-
path = Scout.chats[file]
|
|
142
|
-
original = original.find if Path === original
|
|
143
|
-
if original
|
|
144
|
-
relative = File.join(File.dirname(original), file)
|
|
145
|
-
relative_lib = File.join(caller_lib_dir, file)
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
if Open.exist?(file)
|
|
149
|
-
file
|
|
150
|
-
elsif Open.remote?(file)
|
|
151
|
-
file
|
|
152
|
-
elsif relative && Open.exist?(relative)
|
|
153
|
-
relative
|
|
154
|
-
elsif relative_lib && Open.exist?(relative_lib)
|
|
155
|
-
relative_lib
|
|
156
|
-
elsif path.exists?
|
|
157
|
-
path
|
|
158
|
-
end
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
def self.imports(messages, original = nil, caller_lib_dir = Path.caller_lib_dir(nil, 'chats'))
|
|
162
|
-
messages.collect do |message|
|
|
163
|
-
if message[:role] == 'import' || message[:role] == 'continue' || message[:role] == 'last'
|
|
164
|
-
file = message[:content].to_s.strip
|
|
165
|
-
found_file = find_file(file, original, caller_lib_dir)
|
|
166
|
-
raise "Import not found: #{file}" if found_file.nil?
|
|
167
|
-
|
|
168
|
-
new = LLM.messages Open.read(found_file)
|
|
169
|
-
|
|
170
|
-
new = if message[:role] == 'continue'
|
|
171
|
-
[new.reject{|msg| msg[:content].nil? || msg[:content].strip.empty? }.last]
|
|
172
|
-
elsif message[:role] == 'last'
|
|
173
|
-
[LLM.purge(new).reject{|msg| msg[:content].empty?}.last]
|
|
174
|
-
else
|
|
175
|
-
LLM.purge(new)
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
LLM.chat new, found_file
|
|
179
|
-
else
|
|
180
|
-
message
|
|
181
|
-
end
|
|
182
|
-
end.flatten
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
def self.files(messages, original = nil, caller_lib_dir = Path.caller_lib_dir(nil, 'chats'))
|
|
186
|
-
messages.collect do |message|
|
|
187
|
-
if message[:role] == 'file' || message[:role] == 'directory'
|
|
188
|
-
file = message[:content].to_s.strip
|
|
189
|
-
found_file = find_file(file, original, caller_lib_dir)
|
|
190
|
-
raise "File not found: #{file}" if found_file.nil?
|
|
191
|
-
|
|
192
|
-
target = found_file
|
|
193
|
-
|
|
194
|
-
if message[:role] == 'directory'
|
|
195
|
-
Path.setup target
|
|
196
|
-
target.glob('**/*').
|
|
197
|
-
reject{|file|
|
|
198
|
-
Open.directory?(file)
|
|
199
|
-
}.collect{|file|
|
|
200
|
-
files([{role: 'file', content: file}])
|
|
201
|
-
}
|
|
202
|
-
else
|
|
203
|
-
new = LLM.tag :file, Open.read(target), file
|
|
204
|
-
{role: 'user', content: new}
|
|
205
|
-
end
|
|
206
|
-
elsif message[:role] == 'pdf' || message[:role] == 'image'
|
|
207
|
-
file = message[:content].to_s.strip
|
|
208
|
-
found_file = find_file(file, original, caller_lib_dir)
|
|
209
|
-
raise "File not found: #{file}" if found_file.nil?
|
|
210
|
-
|
|
211
|
-
message[:content] = found_file
|
|
212
|
-
message
|
|
213
|
-
else
|
|
214
|
-
message
|
|
215
|
-
end
|
|
216
|
-
end.flatten
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
def self.tasks(messages, original = nil)
|
|
220
|
-
jobs = []
|
|
221
|
-
new = messages.collect do |message|
|
|
222
|
-
if message[:role] == 'task' || message[:role] == 'inline_task'
|
|
223
|
-
info = message[:content].strip
|
|
224
|
-
|
|
225
|
-
workflow, task = info.split(" ").values_at 0, 1
|
|
226
|
-
|
|
227
|
-
options = IndiferentHash.parse_options info
|
|
228
|
-
jobname = options.delete :jobname
|
|
229
|
-
|
|
230
|
-
if String === workflow
|
|
231
|
-
workflow = begin
|
|
232
|
-
Kernel.const_get workflow
|
|
233
|
-
rescue
|
|
234
|
-
Workflow.require_workflow(workflow)
|
|
235
|
-
end
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
job = workflow.job(task, jobname, options)
|
|
239
|
-
|
|
240
|
-
jobs << job
|
|
241
|
-
|
|
242
|
-
if message[:role] == 'inline_task'
|
|
243
|
-
{role: 'inline_job', content: job.path.find}
|
|
244
|
-
else
|
|
245
|
-
{role: 'job', content: job.path.find}
|
|
246
|
-
end
|
|
247
|
-
else
|
|
248
|
-
message
|
|
249
|
-
end
|
|
250
|
-
end.flatten
|
|
251
|
-
|
|
252
|
-
Workflow.produce(jobs)
|
|
253
|
-
|
|
254
|
-
new
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
def self.jobs(messages, original = nil)
|
|
258
|
-
messages.collect do |message|
|
|
259
|
-
if message[:role] == 'job' || message[:role] == 'inline_job'
|
|
260
|
-
file = message[:content].strip
|
|
261
|
-
|
|
262
|
-
step = Step.load file
|
|
263
|
-
|
|
264
|
-
id = step.short_path[0..39]
|
|
265
|
-
id = id.gsub('/','-')
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
if message[:role] == 'inline_job'
|
|
269
|
-
path = step.path
|
|
270
|
-
path = path.find if Path === path
|
|
271
|
-
{role: 'file', content: step.path}
|
|
272
|
-
else
|
|
273
|
-
tool_call = {
|
|
274
|
-
type: "function",
|
|
275
|
-
function: {
|
|
276
|
-
name: step.full_task_name.sub('#', '-'),
|
|
277
|
-
arguments: step.provided_inputs.to_json
|
|
278
|
-
},
|
|
279
|
-
id: id,
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
tool_output = {
|
|
283
|
-
id: id,
|
|
284
|
-
role: "tool",
|
|
285
|
-
content: Open.read(step.path)
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
[
|
|
289
|
-
{role: 'function_call', content: tool_call.to_json},
|
|
290
|
-
{role: 'function_call_output', content: tool_output.to_json},
|
|
291
|
-
]
|
|
292
|
-
end
|
|
293
|
-
else
|
|
294
|
-
message
|
|
295
|
-
end
|
|
296
|
-
end.flatten
|
|
297
|
-
end
|
|
298
|
-
|
|
299
|
-
def self.clear(messages)
|
|
300
|
-
new = []
|
|
301
|
-
|
|
302
|
-
messages.reverse.each do |message|
|
|
303
|
-
if message[:role].to_s == 'clear'
|
|
304
|
-
break
|
|
305
|
-
elsif message[:role].to_s == 'previous_response_id'
|
|
306
|
-
new << message
|
|
307
|
-
break
|
|
308
|
-
else
|
|
309
|
-
new << message
|
|
310
|
-
end
|
|
311
|
-
end
|
|
312
|
-
|
|
313
|
-
new.reverse
|
|
22
|
+
Chat.parse question
|
|
314
23
|
end
|
|
315
|
-
|
|
316
|
-
def self.clean(messages)
|
|
317
|
-
messages.reject do |message|
|
|
318
|
-
((String === message[:content]) && message[:content].empty?) ||
|
|
319
|
-
message[:role] == 'skip'
|
|
320
|
-
end
|
|
321
|
-
end
|
|
322
|
-
|
|
323
|
-
def self.indiferent(messages)
|
|
324
|
-
messages.collect{|msg| IndiferentHash.setup msg }
|
|
325
|
-
end
|
|
326
|
-
|
|
327
24
|
def self.chat(file, original = nil)
|
|
328
25
|
original ||= (String === file and Open.exists?(file)) ? file : Path.setup($0.dup)
|
|
329
26
|
caller_lib_dir = Path.caller_lib_dir(nil, 'chats')
|
|
330
27
|
|
|
331
|
-
if
|
|
332
|
-
messages = self.messages file
|
|
333
|
-
messages = self.indiferent messages
|
|
334
|
-
messages = self.imports messages, original, caller_lib_dir
|
|
335
|
-
elsif Open.exists?(file)
|
|
28
|
+
if String === file && Open.exists?(file)
|
|
336
29
|
messages = self.messages Open.read(file)
|
|
337
|
-
messages = self.indiferent messages
|
|
338
|
-
messages = self.imports messages, original, caller_lib_dir
|
|
339
30
|
else
|
|
340
31
|
messages = self.messages file
|
|
341
|
-
messages = self.indiferent messages
|
|
342
|
-
messages = self.imports messages, original, caller_lib_dir
|
|
343
32
|
end
|
|
344
33
|
|
|
345
|
-
messages =
|
|
346
|
-
messages =
|
|
347
|
-
messages = self.tasks messages
|
|
348
|
-
messages = self.jobs messages
|
|
349
|
-
messages = self.files messages, original, caller_lib_dir
|
|
350
|
-
|
|
351
|
-
messages
|
|
352
|
-
end
|
|
353
|
-
|
|
354
|
-
def self.options(chat)
|
|
355
|
-
options = IndiferentHash.setup({})
|
|
356
|
-
sticky_options = IndiferentHash.setup({})
|
|
357
|
-
new = []
|
|
358
|
-
|
|
359
|
-
# Most options reset after an assistant reply, but not previous_response_id
|
|
360
|
-
chat.each do |info|
|
|
361
|
-
if Hash === info
|
|
362
|
-
role = info[:role].to_s
|
|
363
|
-
if %w(endpoint model backend persist agent).include? role.to_s
|
|
364
|
-
options[role] = info[:content]
|
|
365
|
-
next
|
|
366
|
-
elsif %w(previous_response_id).include? role.to_s
|
|
367
|
-
sticky_options[role] = info[:content]
|
|
368
|
-
next
|
|
369
|
-
elsif %w(format).include? role.to_s
|
|
370
|
-
format = info[:content]
|
|
371
|
-
if Path.is_filename?(format)
|
|
372
|
-
file = find_file(format)
|
|
373
|
-
if file
|
|
374
|
-
format = Open.json(file)
|
|
375
|
-
end
|
|
376
|
-
end
|
|
377
|
-
options[role] = format
|
|
378
|
-
next
|
|
379
|
-
end
|
|
34
|
+
messages = Chat.indiferent messages
|
|
35
|
+
messages = Chat.imports messages, original, caller_lib_dir
|
|
380
36
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
options[key] = value
|
|
384
|
-
next
|
|
385
|
-
end
|
|
37
|
+
messages = Chat.clear messages
|
|
38
|
+
messages = Chat.clean messages
|
|
386
39
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
next
|
|
391
|
-
end
|
|
40
|
+
messages = Chat.tasks messages
|
|
41
|
+
messages = Chat.jobs messages
|
|
42
|
+
messages = Chat.files messages, original, caller_lib_dir
|
|
392
43
|
|
|
393
|
-
|
|
394
|
-
options.clear
|
|
395
|
-
end
|
|
396
|
-
end
|
|
397
|
-
new << info
|
|
398
|
-
end
|
|
399
|
-
chat.replace new
|
|
400
|
-
sticky_options.merge options
|
|
44
|
+
Chat.setup messages
|
|
401
45
|
end
|
|
402
46
|
|
|
403
|
-
def self.
|
|
404
|
-
|
|
405
|
-
new = messages.collect do |message|
|
|
406
|
-
if message[:role] == 'mcp'
|
|
407
|
-
url, *tools = content_tokens(message)
|
|
408
|
-
|
|
409
|
-
if url == 'stdio'
|
|
410
|
-
command = tools.shift
|
|
411
|
-
mcp_tool_definitions = LLM.mcp_tools(url, command: command, url: nil, type: :stdio)
|
|
412
|
-
else
|
|
413
|
-
mcp_tool_definitions = LLM.mcp_tools(url)
|
|
414
|
-
end
|
|
415
|
-
|
|
416
|
-
if tools.any?
|
|
417
|
-
tools.each do |tool|
|
|
418
|
-
tool_definitions[tool] = mcp_tool_definitions[tool]
|
|
419
|
-
end
|
|
420
|
-
else
|
|
421
|
-
tool_definitions.merge!(mcp_tool_definitions)
|
|
422
|
-
end
|
|
423
|
-
next
|
|
424
|
-
elsif message[:role] == 'tool'
|
|
425
|
-
workflow_name, task_name, *inputs = content_tokens(message)
|
|
426
|
-
inputs = nil if inputs.empty?
|
|
427
|
-
inputs = [] if inputs == ['none'] || inputs == ['noinputs']
|
|
428
|
-
if Open.remote? workflow_name
|
|
429
|
-
require 'rbbt'
|
|
430
|
-
require 'scout/offsite/ssh'
|
|
431
|
-
require 'rbbt/workflow/remote_workflow'
|
|
432
|
-
workflow = RemoteWorkflow.new workflow_name
|
|
433
|
-
else
|
|
434
|
-
workflow = Workflow.require_workflow workflow_name
|
|
435
|
-
end
|
|
436
|
-
|
|
437
|
-
if task_name
|
|
438
|
-
definition = LLM.task_tool_definition workflow, task_name, inputs
|
|
439
|
-
tool_definitions[task_name] = [workflow, definition]
|
|
440
|
-
else
|
|
441
|
-
tool_definitions.merge!(LLM.workflow_tools(workflow))
|
|
442
|
-
end
|
|
443
|
-
next
|
|
444
|
-
elsif message[:role] == 'kb'
|
|
445
|
-
knowledge_base_name, *databases = content_tokens(message)
|
|
446
|
-
databases = nil if databases.empty?
|
|
447
|
-
knowledge_base = KnowledgeBase.load knowledge_base_name
|
|
448
|
-
|
|
449
|
-
knowledge_base_definition = LLM.knowledge_base_tool_definition(knowledge_base, databases)
|
|
450
|
-
tool_definitions.merge!(knowledge_base_definition)
|
|
451
|
-
next
|
|
452
|
-
elsif message[:role] == 'clear_tools'
|
|
453
|
-
tool_definitions = {}
|
|
454
|
-
else
|
|
455
|
-
message
|
|
456
|
-
end
|
|
457
|
-
end.compact.flatten
|
|
458
|
-
messages.replace new
|
|
459
|
-
tool_definitions
|
|
460
|
-
end
|
|
461
|
-
|
|
462
|
-
def self.associations(messages, kb = nil)
|
|
463
|
-
tool_definitions = {}
|
|
464
|
-
new = messages.collect do |message|
|
|
465
|
-
if message[:role] == 'association'
|
|
466
|
-
name, path, *options = content_tokens(message)
|
|
467
|
-
|
|
468
|
-
kb ||= KnowledgeBase.new Scout.var.Agent.Chat.knowledge_base
|
|
469
|
-
kb.register name, Path.setup(path), IndiferentHash.parse_options(message[:content])
|
|
470
|
-
|
|
471
|
-
tool_definitions.merge!(LLM.knowledge_base_tool_definition( kb, [name]))
|
|
472
|
-
next
|
|
473
|
-
elsif message[:role] == 'clear_associations'
|
|
474
|
-
tool_definitions = {}
|
|
475
|
-
else
|
|
476
|
-
message
|
|
477
|
-
end
|
|
478
|
-
end.compact.flatten
|
|
479
|
-
messages.replace new
|
|
480
|
-
tool_definitions
|
|
481
|
-
end
|
|
482
|
-
|
|
483
|
-
def self.print(chat)
|
|
484
|
-
return chat if String === chat
|
|
485
|
-
"\n" + chat.collect do |message|
|
|
486
|
-
IndiferentHash.setup message
|
|
487
|
-
case message[:content]
|
|
488
|
-
when Hash, Array
|
|
489
|
-
message[:role].to_s + ":\n\n" + message[:content].to_json
|
|
490
|
-
when nil, ''
|
|
491
|
-
message[:role].to_s + ":"
|
|
492
|
-
else
|
|
493
|
-
if %w(option previous_response_id).include? message[:role].to_s
|
|
494
|
-
message[:role].to_s + ": " + message[:content].to_s
|
|
495
|
-
else
|
|
496
|
-
message[:role].to_s + ":\n\n" + message[:content].to_s
|
|
497
|
-
end
|
|
498
|
-
end
|
|
499
|
-
end * "\n\n"
|
|
500
|
-
end
|
|
501
|
-
|
|
502
|
-
def self.purge(chat)
|
|
503
|
-
chat.reject do |msg|
|
|
504
|
-
IndiferentHash.setup msg
|
|
505
|
-
msg[:role].to_s == 'previous_response_id'
|
|
506
|
-
end
|
|
507
|
-
end
|
|
508
|
-
end
|
|
509
|
-
|
|
510
|
-
module Chat
|
|
511
|
-
extend Annotation
|
|
512
|
-
|
|
513
|
-
def message(role, content)
|
|
514
|
-
self.append({role: role.to_s, content: content})
|
|
515
|
-
end
|
|
516
|
-
|
|
517
|
-
def user(content)
|
|
518
|
-
message(:user, content)
|
|
47
|
+
def self.options(...)
|
|
48
|
+
Chat.options(...)
|
|
519
49
|
end
|
|
520
50
|
|
|
521
|
-
def
|
|
522
|
-
|
|
51
|
+
def self.print(...)
|
|
52
|
+
Chat.print(...)
|
|
523
53
|
end
|
|
524
54
|
|
|
525
|
-
def
|
|
526
|
-
|
|
55
|
+
def self.tools(...)
|
|
56
|
+
Chat.tools(...)
|
|
527
57
|
end
|
|
528
58
|
|
|
529
|
-
def
|
|
530
|
-
|
|
59
|
+
def self.associations(...)
|
|
60
|
+
Chat.associations(...)
|
|
531
61
|
end
|
|
532
62
|
|
|
533
|
-
def
|
|
534
|
-
|
|
535
|
-
end
|
|
536
|
-
|
|
537
|
-
def file(file)
|
|
538
|
-
message(:file, file)
|
|
539
|
-
end
|
|
540
|
-
|
|
541
|
-
def directory(directory)
|
|
542
|
-
message(:directory, directory)
|
|
543
|
-
end
|
|
544
|
-
|
|
545
|
-
def continue(file)
|
|
546
|
-
message(:continue, file)
|
|
547
|
-
end
|
|
548
|
-
|
|
549
|
-
def format(format)
|
|
550
|
-
message(:format, format)
|
|
551
|
-
end
|
|
552
|
-
|
|
553
|
-
def tool(*parts)
|
|
554
|
-
content = parts * "\n"
|
|
555
|
-
message(:tool, content)
|
|
556
|
-
end
|
|
557
|
-
|
|
558
|
-
def task(workflow, task_name, inputs = {})
|
|
559
|
-
input_str = IndiferentHash.print_options inputs
|
|
560
|
-
content = [workflow, task_name, input_str]*" "
|
|
561
|
-
message(:task, content)
|
|
562
|
-
end
|
|
563
|
-
|
|
564
|
-
def inline_task(workflow, task_name, inputs = {})
|
|
565
|
-
input_str = IndiferentHash.print_options inputs
|
|
566
|
-
content = [workflow, task_name, input_str]*" "
|
|
567
|
-
message(:inline_task, content)
|
|
568
|
-
end
|
|
569
|
-
|
|
570
|
-
def job(step)
|
|
571
|
-
message(:job, step.path)
|
|
572
|
-
end
|
|
573
|
-
|
|
574
|
-
def inline_job(step)
|
|
575
|
-
message(:inline_job, step.path)
|
|
576
|
-
end
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
def association(name, path, options = {})
|
|
580
|
-
options_str = IndiferentHash.print_options options
|
|
581
|
-
content = [name, path, options_str]*" "
|
|
582
|
-
message(:association, name)
|
|
583
|
-
end
|
|
584
|
-
|
|
585
|
-
def tag(content, name=nil, tag=:file, role=:user)
|
|
586
|
-
self.message role, LLM.tag(tag, content, name)
|
|
587
|
-
end
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
def ask(...)
|
|
591
|
-
LLM.ask(LLM.chat(self), ...)
|
|
592
|
-
end
|
|
593
|
-
|
|
594
|
-
def chat(...)
|
|
595
|
-
response = ask(...)
|
|
596
|
-
if Array === response
|
|
597
|
-
current_chat.concat(response)
|
|
598
|
-
final(response)
|
|
599
|
-
else
|
|
600
|
-
current_chat.push({role: :assistant, content: response})
|
|
601
|
-
response
|
|
602
|
-
end
|
|
603
|
-
end
|
|
604
|
-
|
|
605
|
-
def json(...)
|
|
606
|
-
self.format :json
|
|
607
|
-
output = ask(...)
|
|
608
|
-
obj = JSON.parse output
|
|
609
|
-
if (Hash === obj) and obj.keys == ['content']
|
|
610
|
-
obj['content']
|
|
611
|
-
else
|
|
612
|
-
obj
|
|
613
|
-
end
|
|
614
|
-
end
|
|
615
|
-
|
|
616
|
-
def json_format(format, ...)
|
|
617
|
-
self.format format
|
|
618
|
-
output = ask(...)
|
|
619
|
-
obj = JSON.parse output
|
|
620
|
-
if (Hash === obj) and obj.keys == ['content']
|
|
621
|
-
obj['content']
|
|
622
|
-
else
|
|
623
|
-
obj
|
|
624
|
-
end
|
|
625
|
-
end
|
|
626
|
-
|
|
627
|
-
def branch
|
|
628
|
-
self.annotate self.dup
|
|
629
|
-
end
|
|
630
|
-
|
|
631
|
-
def option(name, value)
|
|
632
|
-
self.message 'option', [name, value] * " "
|
|
633
|
-
end
|
|
634
|
-
|
|
635
|
-
def endpoint(value)
|
|
636
|
-
option :endpoint, value
|
|
637
|
-
end
|
|
638
|
-
|
|
639
|
-
def model(value)
|
|
640
|
-
option :model, value
|
|
641
|
-
end
|
|
642
|
-
|
|
643
|
-
def image(file)
|
|
644
|
-
self.message :image, file
|
|
645
|
-
end
|
|
646
|
-
|
|
647
|
-
# Reporting
|
|
648
|
-
|
|
649
|
-
def print
|
|
650
|
-
LLM.print LLM.chat(self)
|
|
651
|
-
end
|
|
652
|
-
|
|
653
|
-
def final
|
|
654
|
-
LLM.purge(self).last
|
|
655
|
-
end
|
|
656
|
-
|
|
657
|
-
def purge
|
|
658
|
-
Chat.setup(LLM.purge(self))
|
|
659
|
-
end
|
|
660
|
-
|
|
661
|
-
def shed
|
|
662
|
-
self.annotate [final]
|
|
663
|
-
end
|
|
664
|
-
|
|
665
|
-
def answer
|
|
666
|
-
final[:content]
|
|
667
|
-
end
|
|
668
|
-
|
|
669
|
-
# Write and save
|
|
670
|
-
|
|
671
|
-
def save(path, force = true)
|
|
672
|
-
path = path.to_s if Symbol === path
|
|
673
|
-
if not (Open.exists?(path) || Path === path || Path.located?(path))
|
|
674
|
-
path = Scout.chats.find[path]
|
|
675
|
-
end
|
|
676
|
-
return if Open.exists?(path) && ! force
|
|
677
|
-
Open.write path, LLM.print(self)
|
|
678
|
-
end
|
|
679
|
-
|
|
680
|
-
def write(path, force = true)
|
|
681
|
-
path = path.to_s if Symbol === path
|
|
682
|
-
if not (Open.exists?(path) || Path === path || Path.located?(path))
|
|
683
|
-
path = Scout.chats.find[path]
|
|
684
|
-
end
|
|
685
|
-
return if Open.exists?(path) && ! force
|
|
686
|
-
Open.write path, self.print
|
|
687
|
-
end
|
|
688
|
-
|
|
689
|
-
def write_answer(path, force = true)
|
|
690
|
-
path = path.to_s if Symbol === path
|
|
691
|
-
if not (Open.exists?(path) || Path === path || Path.located?(path))
|
|
692
|
-
path = Scout.chats.find[path]
|
|
693
|
-
end
|
|
694
|
-
return if Open.exists?(path) && ! force
|
|
695
|
-
Open.write path, self.answer
|
|
696
|
-
end
|
|
697
|
-
|
|
698
|
-
# Image
|
|
699
|
-
def create_image(file, ...)
|
|
700
|
-
base64_image = LLM.image(LLM.chat(self), ...)
|
|
701
|
-
Open.write(file, Base64.decode(file_content), mode: 'wb')
|
|
63
|
+
def self.purge(...)
|
|
64
|
+
Chat.purge(...)
|
|
702
65
|
end
|
|
703
66
|
end
|
|
67
|
+
|