scout-ai 1.1.0 → 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 +12 -1
- data/Rakefile +1 -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 -4
- 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 +25 -3
- data/lib/scout/llm/parse.rb +0 -91
data/lib/scout/llm/chat.rb
CHANGED
|
@@ -1,715 +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' || message[:role] == 'exec_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 unless message[:role] == 'exec_task'
|
|
241
|
-
|
|
242
|
-
if message[:role] == 'exec_task'
|
|
243
|
-
begin
|
|
244
|
-
{role: 'user', content: job.exec}
|
|
245
|
-
rescue
|
|
246
|
-
{role: 'exec_job', content: $!}
|
|
247
|
-
end
|
|
248
|
-
elsif message[:role] == 'inline_task'
|
|
249
|
-
{role: 'inline_job', content: job.path.find}
|
|
250
|
-
else
|
|
251
|
-
{role: 'job', content: job.path.find}
|
|
252
|
-
end
|
|
253
|
-
else
|
|
254
|
-
message
|
|
255
|
-
end
|
|
256
|
-
end.flatten
|
|
257
|
-
|
|
258
|
-
Workflow.produce(jobs)
|
|
259
|
-
|
|
260
|
-
new
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
def self.jobs(messages, original = nil)
|
|
264
|
-
messages.collect do |message|
|
|
265
|
-
if message[:role] == 'job' || message[:role] == 'inline_job'
|
|
266
|
-
file = message[:content].strip
|
|
267
|
-
|
|
268
|
-
step = Step.load file
|
|
269
|
-
|
|
270
|
-
id = step.short_path[0..39]
|
|
271
|
-
id = id.gsub('/','-')
|
|
272
|
-
|
|
273
|
-
if message[:role] == 'inline_job'
|
|
274
|
-
path = step.path
|
|
275
|
-
path = path.find if Path === path
|
|
276
|
-
{role: 'file', content: step.path}
|
|
277
|
-
else
|
|
278
|
-
|
|
279
|
-
function_name = step.full_task_name.sub('#', '-')
|
|
280
|
-
function_name = step.task_name
|
|
281
|
-
tool_call = {
|
|
282
|
-
function: {
|
|
283
|
-
name: function_name,
|
|
284
|
-
arguments: step.provided_inputs
|
|
285
|
-
},
|
|
286
|
-
id: id,
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
content = if step.done?
|
|
290
|
-
Open.read(step.path)
|
|
291
|
-
elsif step.error?
|
|
292
|
-
step.exception
|
|
293
|
-
end
|
|
294
|
-
|
|
295
|
-
tool_output = {
|
|
296
|
-
id: id,
|
|
297
|
-
content: content
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
[
|
|
301
|
-
{role: 'function_call', content: tool_call.to_json},
|
|
302
|
-
{role: 'function_call_output', content: tool_output.to_json},
|
|
303
|
-
]
|
|
304
|
-
end
|
|
305
|
-
else
|
|
306
|
-
message
|
|
307
|
-
end
|
|
308
|
-
end.flatten
|
|
309
|
-
end
|
|
310
|
-
|
|
311
|
-
def self.clear(messages)
|
|
312
|
-
new = []
|
|
313
|
-
|
|
314
|
-
messages.reverse.each do |message|
|
|
315
|
-
if message[:role].to_s == 'clear'
|
|
316
|
-
break
|
|
317
|
-
elsif message[:role].to_s == 'previous_response_id'
|
|
318
|
-
new << message
|
|
319
|
-
break
|
|
320
|
-
else
|
|
321
|
-
new << message
|
|
322
|
-
end
|
|
323
|
-
end
|
|
324
|
-
|
|
325
|
-
new.reverse
|
|
326
|
-
end
|
|
327
|
-
|
|
328
|
-
def self.clean(messages)
|
|
329
|
-
messages.reject do |message|
|
|
330
|
-
((String === message[:content]) && message[:content].empty?) ||
|
|
331
|
-
message[:role] == 'skip'
|
|
332
|
-
end
|
|
333
|
-
end
|
|
334
|
-
|
|
335
|
-
def self.indiferent(messages)
|
|
336
|
-
messages.collect{|msg| IndiferentHash.setup msg }
|
|
22
|
+
Chat.parse question
|
|
337
23
|
end
|
|
338
|
-
|
|
339
24
|
def self.chat(file, original = nil)
|
|
340
25
|
original ||= (String === file and Open.exists?(file)) ? file : Path.setup($0.dup)
|
|
341
26
|
caller_lib_dir = Path.caller_lib_dir(nil, 'chats')
|
|
342
27
|
|
|
343
|
-
if
|
|
344
|
-
messages = self.messages file
|
|
345
|
-
messages = self.indiferent messages
|
|
346
|
-
messages = self.imports messages, original, caller_lib_dir
|
|
347
|
-
elsif Open.exists?(file)
|
|
28
|
+
if String === file && Open.exists?(file)
|
|
348
29
|
messages = self.messages Open.read(file)
|
|
349
|
-
messages = self.indiferent messages
|
|
350
|
-
messages = self.imports messages, original, caller_lib_dir
|
|
351
30
|
else
|
|
352
31
|
messages = self.messages file
|
|
353
|
-
messages = self.indiferent messages
|
|
354
|
-
messages = self.imports messages, original, caller_lib_dir
|
|
355
|
-
end
|
|
356
|
-
|
|
357
|
-
messages = self.clear messages
|
|
358
|
-
messages = self.clean messages
|
|
359
|
-
messages = self.tasks messages
|
|
360
|
-
messages = self.jobs messages
|
|
361
|
-
messages = self.files messages, original, caller_lib_dir
|
|
362
|
-
|
|
363
|
-
messages
|
|
364
|
-
end
|
|
365
|
-
|
|
366
|
-
def self.options(chat)
|
|
367
|
-
options = IndiferentHash.setup({})
|
|
368
|
-
sticky_options = IndiferentHash.setup({})
|
|
369
|
-
new = []
|
|
370
|
-
|
|
371
|
-
# Most options reset after an assistant reply, but not previous_response_id
|
|
372
|
-
chat.each do |info|
|
|
373
|
-
if Hash === info
|
|
374
|
-
role = info[:role].to_s
|
|
375
|
-
if %w(endpoint model backend persist agent).include? role.to_s
|
|
376
|
-
options[role] = info[:content]
|
|
377
|
-
next
|
|
378
|
-
elsif %w(previous_response_id).include? role.to_s
|
|
379
|
-
sticky_options[role] = info[:content]
|
|
380
|
-
next
|
|
381
|
-
elsif %w(format).include? role.to_s
|
|
382
|
-
format = info[:content]
|
|
383
|
-
if Path.is_filename?(format)
|
|
384
|
-
file = find_file(format)
|
|
385
|
-
if file
|
|
386
|
-
format = Open.json(file)
|
|
387
|
-
end
|
|
388
|
-
end
|
|
389
|
-
options[role] = format
|
|
390
|
-
next
|
|
391
|
-
end
|
|
392
|
-
|
|
393
|
-
if role.to_s == 'option'
|
|
394
|
-
key, _, value = info[:content].partition(" ")
|
|
395
|
-
options[key] = value
|
|
396
|
-
next
|
|
397
|
-
end
|
|
398
|
-
|
|
399
|
-
if role.to_s == 'sticky_option'
|
|
400
|
-
key, _, value = info[:content].partition(" ")
|
|
401
|
-
sticky_options[key] = value
|
|
402
|
-
next
|
|
403
|
-
end
|
|
404
|
-
|
|
405
|
-
if role == 'assistant'
|
|
406
|
-
options.clear
|
|
407
|
-
end
|
|
408
|
-
end
|
|
409
|
-
new << info
|
|
410
|
-
end
|
|
411
|
-
chat.replace new
|
|
412
|
-
sticky_options.merge options
|
|
413
|
-
end
|
|
414
|
-
|
|
415
|
-
def self.tools(messages)
|
|
416
|
-
tool_definitions = IndiferentHash.setup({})
|
|
417
|
-
new = messages.collect do |message|
|
|
418
|
-
if message[:role] == 'mcp'
|
|
419
|
-
url, *tools = content_tokens(message)
|
|
420
|
-
|
|
421
|
-
if url == 'stdio'
|
|
422
|
-
command = tools.shift
|
|
423
|
-
mcp_tool_definitions = LLM.mcp_tools(url, command: command, url: nil, type: :stdio)
|
|
424
|
-
else
|
|
425
|
-
mcp_tool_definitions = LLM.mcp_tools(url)
|
|
426
|
-
end
|
|
427
|
-
|
|
428
|
-
if tools.any?
|
|
429
|
-
tools.each do |tool|
|
|
430
|
-
tool_definitions[tool] = mcp_tool_definitions[tool]
|
|
431
|
-
end
|
|
432
|
-
else
|
|
433
|
-
tool_definitions.merge!(mcp_tool_definitions)
|
|
434
|
-
end
|
|
435
|
-
next
|
|
436
|
-
elsif message[:role] == 'tool'
|
|
437
|
-
workflow_name, task_name, *inputs = content_tokens(message)
|
|
438
|
-
inputs = nil if inputs.empty?
|
|
439
|
-
inputs = [] if inputs == ['none'] || inputs == ['noinputs']
|
|
440
|
-
if Open.remote? workflow_name
|
|
441
|
-
require 'rbbt'
|
|
442
|
-
require 'scout/offsite/ssh'
|
|
443
|
-
require 'rbbt/workflow/remote_workflow'
|
|
444
|
-
workflow = RemoteWorkflow.new workflow_name
|
|
445
|
-
else
|
|
446
|
-
workflow = Workflow.require_workflow workflow_name
|
|
447
|
-
end
|
|
448
|
-
|
|
449
|
-
if task_name
|
|
450
|
-
definition = LLM.task_tool_definition workflow, task_name, inputs
|
|
451
|
-
tool_definitions[task_name] = [workflow, definition]
|
|
452
|
-
else
|
|
453
|
-
tool_definitions.merge!(LLM.workflow_tools(workflow))
|
|
454
|
-
end
|
|
455
|
-
next
|
|
456
|
-
elsif message[:role] == 'kb'
|
|
457
|
-
knowledge_base_name, *databases = content_tokens(message)
|
|
458
|
-
databases = nil if databases.empty?
|
|
459
|
-
knowledge_base = KnowledgeBase.load knowledge_base_name
|
|
460
|
-
|
|
461
|
-
knowledge_base_definition = LLM.knowledge_base_tool_definition(knowledge_base, databases)
|
|
462
|
-
tool_definitions.merge!(knowledge_base_definition)
|
|
463
|
-
next
|
|
464
|
-
elsif message[:role] == 'clear_tools'
|
|
465
|
-
tool_definitions = {}
|
|
466
|
-
else
|
|
467
|
-
message
|
|
468
|
-
end
|
|
469
|
-
end.compact.flatten
|
|
470
|
-
messages.replace new
|
|
471
|
-
tool_definitions
|
|
472
|
-
end
|
|
473
|
-
|
|
474
|
-
def self.associations(messages, kb = nil)
|
|
475
|
-
tool_definitions = {}
|
|
476
|
-
new = messages.collect do |message|
|
|
477
|
-
if message[:role] == 'association'
|
|
478
|
-
name, path, *options = content_tokens(message)
|
|
479
|
-
|
|
480
|
-
kb ||= KnowledgeBase.new Scout.var.Agent.Chat.knowledge_base
|
|
481
|
-
kb.register name, Path.setup(path), IndiferentHash.parse_options(message[:content])
|
|
482
|
-
|
|
483
|
-
tool_definitions.merge!(LLM.knowledge_base_tool_definition( kb, [name]))
|
|
484
|
-
next
|
|
485
|
-
elsif message[:role] == 'clear_associations'
|
|
486
|
-
tool_definitions = {}
|
|
487
|
-
else
|
|
488
|
-
message
|
|
489
|
-
end
|
|
490
|
-
end.compact.flatten
|
|
491
|
-
messages.replace new
|
|
492
|
-
tool_definitions
|
|
493
|
-
end
|
|
494
|
-
|
|
495
|
-
def self.print(chat)
|
|
496
|
-
return chat if String === chat
|
|
497
|
-
"\n" + chat.collect do |message|
|
|
498
|
-
IndiferentHash.setup message
|
|
499
|
-
case message[:content]
|
|
500
|
-
when Hash, Array
|
|
501
|
-
message[:role].to_s + ":\n\n" + message[:content].to_json
|
|
502
|
-
when nil, ''
|
|
503
|
-
message[:role].to_s + ":"
|
|
504
|
-
else
|
|
505
|
-
if %w(option previous_response_id function_call function_call_output).include? message[:role].to_s
|
|
506
|
-
message[:role].to_s + ": " + message[:content].to_s
|
|
507
|
-
else
|
|
508
|
-
message[:role].to_s + ":\n\n" + message[:content].to_s
|
|
509
|
-
end
|
|
510
|
-
end
|
|
511
|
-
end * "\n\n"
|
|
512
|
-
end
|
|
513
|
-
|
|
514
|
-
def self.purge(chat)
|
|
515
|
-
chat.reject do |msg|
|
|
516
|
-
IndiferentHash.setup msg
|
|
517
|
-
msg[:role].to_s == 'previous_response_id'
|
|
518
|
-
end
|
|
519
|
-
end
|
|
520
|
-
end
|
|
521
|
-
|
|
522
|
-
module Chat
|
|
523
|
-
extend Annotation
|
|
524
|
-
|
|
525
|
-
def message(role, content)
|
|
526
|
-
self.append({role: role.to_s, content: content})
|
|
527
|
-
end
|
|
528
|
-
|
|
529
|
-
def user(content)
|
|
530
|
-
message(:user, content)
|
|
531
|
-
end
|
|
532
|
-
|
|
533
|
-
def system(content)
|
|
534
|
-
message(:system, content)
|
|
535
|
-
end
|
|
536
|
-
|
|
537
|
-
def assistant(content)
|
|
538
|
-
message(:assistant, content)
|
|
539
|
-
end
|
|
540
|
-
|
|
541
|
-
def import(file)
|
|
542
|
-
message(:import, file)
|
|
543
|
-
end
|
|
544
|
-
|
|
545
|
-
def import_last(file)
|
|
546
|
-
message(:last, file)
|
|
547
|
-
end
|
|
548
|
-
|
|
549
|
-
def file(file)
|
|
550
|
-
message(:file, file)
|
|
551
|
-
end
|
|
552
|
-
|
|
553
|
-
def directory(directory)
|
|
554
|
-
message(:directory, directory)
|
|
555
|
-
end
|
|
556
|
-
|
|
557
|
-
def continue(file)
|
|
558
|
-
message(:continue, file)
|
|
559
|
-
end
|
|
560
|
-
|
|
561
|
-
def format(format)
|
|
562
|
-
message(:format, format)
|
|
563
|
-
end
|
|
564
|
-
|
|
565
|
-
def tool(*parts)
|
|
566
|
-
content = parts * "\n"
|
|
567
|
-
message(:tool, content)
|
|
568
|
-
end
|
|
569
|
-
|
|
570
|
-
def task(workflow, task_name, inputs = {})
|
|
571
|
-
input_str = IndiferentHash.print_options inputs
|
|
572
|
-
content = [workflow, task_name, input_str]*" "
|
|
573
|
-
message(:task, content)
|
|
574
|
-
end
|
|
575
|
-
|
|
576
|
-
def inline_task(workflow, task_name, inputs = {})
|
|
577
|
-
input_str = IndiferentHash.print_options inputs
|
|
578
|
-
content = [workflow, task_name, input_str]*" "
|
|
579
|
-
message(:inline_task, content)
|
|
580
|
-
end
|
|
581
|
-
|
|
582
|
-
def job(step)
|
|
583
|
-
message(:job, step.path)
|
|
584
|
-
end
|
|
585
|
-
|
|
586
|
-
def inline_job(step)
|
|
587
|
-
message(:inline_job, step.path)
|
|
588
|
-
end
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
def association(name, path, options = {})
|
|
592
|
-
options_str = IndiferentHash.print_options options
|
|
593
|
-
content = [name, path, options_str]*" "
|
|
594
|
-
message(:association, name)
|
|
595
|
-
end
|
|
596
|
-
|
|
597
|
-
def tag(content, name=nil, tag=:file, role=:user)
|
|
598
|
-
self.message role, LLM.tag(tag, content, name)
|
|
599
|
-
end
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
def ask(...)
|
|
603
|
-
LLM.ask(LLM.chat(self), ...)
|
|
604
|
-
end
|
|
605
|
-
|
|
606
|
-
def chat(...)
|
|
607
|
-
response = ask(...)
|
|
608
|
-
if Array === response
|
|
609
|
-
current_chat.concat(response)
|
|
610
|
-
final(response)
|
|
611
|
-
else
|
|
612
|
-
current_chat.push({role: :assistant, content: response})
|
|
613
|
-
response
|
|
614
32
|
end
|
|
615
|
-
end
|
|
616
33
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
output = ask(...)
|
|
620
|
-
obj = JSON.parse output
|
|
621
|
-
if (Hash === obj) and obj.keys == ['content']
|
|
622
|
-
obj['content']
|
|
623
|
-
else
|
|
624
|
-
obj
|
|
625
|
-
end
|
|
626
|
-
end
|
|
34
|
+
messages = Chat.indiferent messages
|
|
35
|
+
messages = Chat.imports messages, original, caller_lib_dir
|
|
627
36
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
output = ask(...)
|
|
631
|
-
obj = JSON.parse output
|
|
632
|
-
if (Hash === obj) and obj.keys == ['content']
|
|
633
|
-
obj['content']
|
|
634
|
-
else
|
|
635
|
-
obj
|
|
636
|
-
end
|
|
637
|
-
end
|
|
638
|
-
|
|
639
|
-
def branch
|
|
640
|
-
self.annotate self.dup
|
|
641
|
-
end
|
|
642
|
-
|
|
643
|
-
def option(name, value)
|
|
644
|
-
self.message 'option', [name, value] * " "
|
|
645
|
-
end
|
|
646
|
-
|
|
647
|
-
def endpoint(value)
|
|
648
|
-
option :endpoint, value
|
|
649
|
-
end
|
|
650
|
-
|
|
651
|
-
def model(value)
|
|
652
|
-
option :model, value
|
|
653
|
-
end
|
|
654
|
-
|
|
655
|
-
def image(file)
|
|
656
|
-
self.message :image, file
|
|
657
|
-
end
|
|
658
|
-
|
|
659
|
-
# Reporting
|
|
660
|
-
|
|
661
|
-
def print
|
|
662
|
-
LLM.print LLM.chat(self)
|
|
663
|
-
end
|
|
664
|
-
|
|
665
|
-
def final
|
|
666
|
-
LLM.purge(self).last
|
|
667
|
-
end
|
|
37
|
+
messages = Chat.clear messages
|
|
38
|
+
messages = Chat.clean messages
|
|
668
39
|
|
|
669
|
-
|
|
670
|
-
Chat.
|
|
671
|
-
|
|
40
|
+
messages = Chat.tasks messages
|
|
41
|
+
messages = Chat.jobs messages
|
|
42
|
+
messages = Chat.files messages, original, caller_lib_dir
|
|
672
43
|
|
|
673
|
-
|
|
674
|
-
self.annotate [final]
|
|
44
|
+
Chat.setup messages
|
|
675
45
|
end
|
|
676
46
|
|
|
677
|
-
def
|
|
678
|
-
|
|
47
|
+
def self.options(...)
|
|
48
|
+
Chat.options(...)
|
|
679
49
|
end
|
|
680
50
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
def save(path, force = true)
|
|
684
|
-
path = path.to_s if Symbol === path
|
|
685
|
-
if not (Open.exists?(path) || Path === path || Path.located?(path))
|
|
686
|
-
path = Scout.chats.find[path]
|
|
687
|
-
end
|
|
688
|
-
return if Open.exists?(path) && ! force
|
|
689
|
-
Open.write path, LLM.print(self)
|
|
51
|
+
def self.print(...)
|
|
52
|
+
Chat.print(...)
|
|
690
53
|
end
|
|
691
54
|
|
|
692
|
-
def
|
|
693
|
-
|
|
694
|
-
if not (Open.exists?(path) || Path === path || Path.located?(path))
|
|
695
|
-
path = Scout.chats.find[path]
|
|
696
|
-
end
|
|
697
|
-
return if Open.exists?(path) && ! force
|
|
698
|
-
Open.write path, self.print
|
|
55
|
+
def self.tools(...)
|
|
56
|
+
Chat.tools(...)
|
|
699
57
|
end
|
|
700
58
|
|
|
701
|
-
def
|
|
702
|
-
|
|
703
|
-
if not (Open.exists?(path) || Path === path || Path.located?(path))
|
|
704
|
-
path = Scout.chats.find[path]
|
|
705
|
-
end
|
|
706
|
-
return if Open.exists?(path) && ! force
|
|
707
|
-
Open.write path, self.answer
|
|
59
|
+
def self.associations(...)
|
|
60
|
+
Chat.associations(...)
|
|
708
61
|
end
|
|
709
62
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
base64_image = LLM.image(LLM.chat(self), ...)
|
|
713
|
-
Open.write(file, Base64.decode(file_content), mode: 'wb')
|
|
63
|
+
def self.purge(...)
|
|
64
|
+
Chat.purge(...)
|
|
714
65
|
end
|
|
715
66
|
end
|
|
67
|
+
|