scout-ai 0.2.0 → 1.0.0
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 +91 -10
- data/Rakefile +1 -0
- data/VERSION +1 -1
- data/bin/scout-ai +2 -0
- data/lib/scout/llm/agent/chat.rb +24 -0
- data/lib/scout/llm/agent.rb +13 -13
- data/lib/scout/llm/ask.rb +26 -16
- data/lib/scout/llm/backends/bedrock.rb +129 -0
- data/lib/scout/llm/backends/huggingface.rb +6 -21
- data/lib/scout/llm/backends/ollama.rb +69 -36
- data/lib/scout/llm/backends/openai.rb +85 -35
- data/lib/scout/llm/backends/openwebui.rb +1 -1
- data/lib/scout/llm/backends/relay.rb +3 -2
- data/lib/scout/llm/backends/responses.rb +272 -0
- data/lib/scout/llm/chat.rb +547 -0
- data/lib/scout/llm/parse.rb +70 -13
- data/lib/scout/llm/tools.rb +126 -5
- data/lib/scout/llm/utils.rb +17 -10
- data/lib/scout/model/base.rb +19 -0
- data/lib/scout/model/python/base.rb +25 -0
- data/lib/scout/model/python/huggingface/causal/next_token.rb +23 -0
- data/lib/scout/model/python/huggingface/causal.rb +29 -0
- data/lib/scout/model/python/huggingface/classification +0 -0
- data/lib/scout/model/python/huggingface/classification.rb +50 -0
- data/lib/scout/model/python/huggingface.rb +112 -0
- data/lib/scout/model/python/torch/dataloader.rb +57 -0
- data/lib/scout/model/python/torch/helpers.rb +84 -0
- data/lib/scout/model/python/torch/introspection.rb +34 -0
- data/lib/scout/model/python/torch/load_and_save.rb +47 -0
- data/lib/scout/model/python/torch.rb +94 -0
- data/lib/scout/model/util/run.rb +181 -0
- data/lib/scout/model/util/save.rb +81 -0
- data/lib/scout-ai.rb +3 -1
- data/python/scout_ai/__init__.py +35 -0
- data/python/scout_ai/__pycache__/__init__.cpython-310.pyc +0 -0
- data/python/scout_ai/__pycache__/__init__.cpython-311.pyc +0 -0
- data/python/scout_ai/__pycache__/huggingface.cpython-310.pyc +0 -0
- data/python/scout_ai/__pycache__/huggingface.cpython-311.pyc +0 -0
- data/python/scout_ai/__pycache__/util.cpython-310.pyc +0 -0
- data/python/scout_ai/__pycache__/util.cpython-311.pyc +0 -0
- data/python/scout_ai/atcold/__init__.py +0 -0
- data/python/scout_ai/atcold/plot_lib.py +141 -0
- data/python/scout_ai/atcold/spiral.py +27 -0
- data/python/scout_ai/huggingface/data.py +48 -0
- data/python/scout_ai/huggingface/eval.py +60 -0
- data/python/scout_ai/huggingface/model.py +29 -0
- data/python/scout_ai/huggingface/rlhf.py +83 -0
- data/python/scout_ai/huggingface/train/__init__.py +34 -0
- data/python/scout_ai/huggingface/train/__pycache__/__init__.cpython-310.pyc +0 -0
- data/python/scout_ai/huggingface/train/__pycache__/next_token.cpython-310.pyc +0 -0
- data/python/scout_ai/huggingface/train/next_token.py +315 -0
- data/python/scout_ai/language_model.py +70 -0
- data/python/scout_ai/util.py +32 -0
- data/scout-ai.gemspec +130 -0
- data/scout_commands/agent/ask +133 -15
- data/scout_commands/agent/kb +15 -0
- data/scout_commands/llm/ask +71 -12
- data/scout_commands/llm/process +4 -2
- data/test/data/cat.jpg +0 -0
- data/test/scout/llm/agent/test_chat.rb +14 -0
- data/test/scout/llm/backends/test_bedrock.rb +60 -0
- data/test/scout/llm/backends/test_huggingface.rb +3 -3
- data/test/scout/llm/backends/test_ollama.rb +48 -10
- data/test/scout/llm/backends/test_openai.rb +96 -11
- data/test/scout/llm/backends/test_responses.rb +115 -0
- data/test/scout/llm/test_ask.rb +1 -0
- data/test/scout/llm/test_chat.rb +214 -0
- data/test/scout/llm/test_parse.rb +81 -2
- data/test/scout/model/python/huggingface/causal/test_next_token.rb +59 -0
- data/test/scout/model/python/huggingface/test_causal.rb +33 -0
- data/test/scout/model/python/huggingface/test_classification.rb +30 -0
- data/test/scout/model/python/test_base.rb +44 -0
- data/test/scout/model/python/test_huggingface.rb +9 -0
- data/test/scout/model/python/test_torch.rb +71 -0
- data/test/scout/model/python/torch/test_helpers.rb +14 -0
- data/test/scout/model/test_base.rb +117 -0
- data/test/scout/model/util/test_save.rb +31 -0
- metadata +72 -5
- data/questions/coach +0 -2
@@ -0,0 +1,547 @@
|
|
1
|
+
require_relative 'utils'
|
2
|
+
require_relative 'parse'
|
3
|
+
require_relative 'tools'
|
4
|
+
|
5
|
+
module LLM
|
6
|
+
def self.messages(question, role = nil)
|
7
|
+
default_role = "user"
|
8
|
+
|
9
|
+
if Array === question
|
10
|
+
return question.collect do |q|
|
11
|
+
if String === q
|
12
|
+
{role: default_role, content: q}
|
13
|
+
else
|
14
|
+
q
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
messages = []
|
20
|
+
current_role = nil
|
21
|
+
current_content = ""
|
22
|
+
in_protected_block = false
|
23
|
+
protected_block_type = nil
|
24
|
+
protected_stack = []
|
25
|
+
|
26
|
+
role = default_role if role.nil?
|
27
|
+
|
28
|
+
file_lines = question.split("\n")
|
29
|
+
|
30
|
+
file_lines.each do |line|
|
31
|
+
stripped = line.strip
|
32
|
+
|
33
|
+
# Detect protected blocks
|
34
|
+
if stripped.start_with?("```")
|
35
|
+
if in_protected_block
|
36
|
+
in_protected_block = false
|
37
|
+
protected_block_type = nil
|
38
|
+
current_content << "\n" << line unless line.strip.empty?
|
39
|
+
else
|
40
|
+
in_protected_block = true
|
41
|
+
protected_block_type = :square
|
42
|
+
current_content << "\n" << line unless line.strip.empty?
|
43
|
+
end
|
44
|
+
next
|
45
|
+
elsif stripped.start_with?("---")
|
46
|
+
if in_protected_block
|
47
|
+
in_protected_block = false
|
48
|
+
protected_block_type = nil
|
49
|
+
current_content << "\n" << line unless line.strip.empty?
|
50
|
+
else
|
51
|
+
in_protected_block = true
|
52
|
+
protected_block_type = :square
|
53
|
+
current_content << "\n" << line unless line.strip.empty?
|
54
|
+
end
|
55
|
+
next
|
56
|
+
elsif stripped.end_with?("]]") && in_protected_block && protected_block_type == :square
|
57
|
+
in_protected_block = false
|
58
|
+
protected_block_type = nil
|
59
|
+
line = line.sub("]]", "")
|
60
|
+
current_content << "\n" << line unless line.strip.empty?
|
61
|
+
next
|
62
|
+
elsif stripped.start_with?("[[")
|
63
|
+
in_protected_block = true
|
64
|
+
protected_block_type = :square
|
65
|
+
line = line.sub("[[", "")
|
66
|
+
current_content << "\n" << line unless line.strip.empty?
|
67
|
+
next
|
68
|
+
elsif stripped.end_with?("]]") && in_protected_block && protected_block_type == :square
|
69
|
+
in_protected_block = false
|
70
|
+
protected_block_type = nil
|
71
|
+
line = line.sub("]]", "")
|
72
|
+
current_content << "\n" << line unless line.strip.empty?
|
73
|
+
next
|
74
|
+
elsif stripped.match(/^.*:-- .* {{{/)
|
75
|
+
in_protected_block = true
|
76
|
+
protected_block_type = :square
|
77
|
+
line = line.sub(/^.*:-- (.*) {{{/, '<cmd_output cmd="\1">')
|
78
|
+
current_content << "\n" << line unless line.strip.empty?
|
79
|
+
next
|
80
|
+
elsif stripped.match(/^.*:--.* }}}/) && in_protected_block && protected_block_type == :square
|
81
|
+
in_protected_block = false
|
82
|
+
protected_block_type = nil
|
83
|
+
line = line.sub(/^.*:-- .* }}}.*/, "</cmd_output>")
|
84
|
+
current_content << "\n" << line unless line.strip.empty?
|
85
|
+
next
|
86
|
+
elsif in_protected_block
|
87
|
+
|
88
|
+
if protected_block_type == :xml
|
89
|
+
if stripped =~ %r{</(\w+)>}
|
90
|
+
closing_tag = $1
|
91
|
+
if protected_stack.last == closing_tag
|
92
|
+
protected_stack.pop
|
93
|
+
end
|
94
|
+
if protected_stack.empty?
|
95
|
+
in_protected_block = false
|
96
|
+
protected_block_type = nil
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
current_content << "\n" << line
|
101
|
+
next
|
102
|
+
end
|
103
|
+
|
104
|
+
# XML-style tag handling (protected content)
|
105
|
+
if stripped =~ /^<(\w+)(\s+[^>]*)?>/
|
106
|
+
tag = $1
|
107
|
+
protected_stack.push(tag)
|
108
|
+
in_protected_block = true
|
109
|
+
protected_block_type = :xml
|
110
|
+
end
|
111
|
+
|
112
|
+
# Match a new message header
|
113
|
+
if line =~ /^([a-z0-9_]+):(.*)$/
|
114
|
+
role = $1
|
115
|
+
inline_content = $2.strip
|
116
|
+
|
117
|
+
# Save current message if any
|
118
|
+
messages << { role: current_role, content: current_content.strip }
|
119
|
+
|
120
|
+
if inline_content.empty?
|
121
|
+
# Block message
|
122
|
+
current_role = role
|
123
|
+
current_content = ""
|
124
|
+
else
|
125
|
+
# Inline message + next block is default role
|
126
|
+
messages << { role: role, content: inline_content }
|
127
|
+
#current_role = default_role
|
128
|
+
current_content = ""
|
129
|
+
end
|
130
|
+
else
|
131
|
+
current_content << "\n" << line
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Final message
|
136
|
+
messages << { role: current_role || default_role, content: current_content.strip }
|
137
|
+
|
138
|
+
messages
|
139
|
+
end
|
140
|
+
|
141
|
+
def self.imports(messages, original = nil, caller_lib_dir = Path.caller_lib_dir(nil, 'chats'))
|
142
|
+
messages.collect do |message|
|
143
|
+
if message[:role] == 'import' || message[:role] == 'continue'
|
144
|
+
file = message[:content].to_s.strip
|
145
|
+
path = Scout.chats[file]
|
146
|
+
original = original.find if Path === original
|
147
|
+
if original
|
148
|
+
relative = File.join(File.dirname(original), file)
|
149
|
+
relative_lib = File.join(caller_lib_dir, file)
|
150
|
+
end
|
151
|
+
|
152
|
+
new = if Open.exist?(file)
|
153
|
+
LLM.chat file
|
154
|
+
elsif relative && Open.exist?(relative)
|
155
|
+
LLM.chat relative
|
156
|
+
elsif relative_lib && Open.exist?(relative_lib)
|
157
|
+
LLM.chat relative_lib
|
158
|
+
elsif path.exists?
|
159
|
+
LLM.chat path
|
160
|
+
else
|
161
|
+
raise "Import not found: #{file}"
|
162
|
+
end
|
163
|
+
|
164
|
+
if message[:role] == 'continue'
|
165
|
+
new.last
|
166
|
+
else
|
167
|
+
new
|
168
|
+
end
|
169
|
+
else
|
170
|
+
message
|
171
|
+
end
|
172
|
+
end.flatten
|
173
|
+
end
|
174
|
+
|
175
|
+
def self.files(messages, original = nil, caller_lib_dir = Path.caller_lib_dir(nil, 'chats'))
|
176
|
+
messages.collect do |message|
|
177
|
+
if message[:role] == 'file' || message[:role] == 'directory'
|
178
|
+
file = message[:content].strip
|
179
|
+
path = Scout.chats[file]
|
180
|
+
original = original.find if Path === original
|
181
|
+
if original
|
182
|
+
relative = File.join(File.dirname(original), file)
|
183
|
+
relative_lib = File.join(caller_lib_dir, file)
|
184
|
+
end
|
185
|
+
|
186
|
+
target = if Open.exist?(file)
|
187
|
+
file
|
188
|
+
elsif relative && Open.exist?(relative)
|
189
|
+
relative
|
190
|
+
elsif relative_lib && Open.exist?(relative_lib)
|
191
|
+
relative_lib
|
192
|
+
elsif path.exists?
|
193
|
+
path
|
194
|
+
else
|
195
|
+
raise "File not found: #{file}"
|
196
|
+
end
|
197
|
+
|
198
|
+
if message[:role] == 'directory'
|
199
|
+
Path.setup target
|
200
|
+
target.glob('**/*').
|
201
|
+
reject{|file|
|
202
|
+
Open.directory?(file)
|
203
|
+
}.collect{|file|
|
204
|
+
files([{role: 'file', content: file}])
|
205
|
+
}
|
206
|
+
else
|
207
|
+
new = LLM.tag :file, Open.read(target), file
|
208
|
+
{role: 'user', content: new}
|
209
|
+
end
|
210
|
+
else
|
211
|
+
message
|
212
|
+
end
|
213
|
+
end.flatten
|
214
|
+
end
|
215
|
+
|
216
|
+
def self.tasks(messages, original = nil)
|
217
|
+
jobs = []
|
218
|
+
new = messages.collect do |message|
|
219
|
+
if message[:role] == 'task' || message[:role] == 'inline_task'
|
220
|
+
info = message[:content].strip
|
221
|
+
|
222
|
+
workflow, task = info.split(" ").values_at 0, 1
|
223
|
+
|
224
|
+
options = IndiferentHash.parse_options info
|
225
|
+
jobname = options.delete :jobname
|
226
|
+
|
227
|
+
job = Workflow.require_workflow(workflow).job(task, jobname, options)
|
228
|
+
|
229
|
+
jobs << job
|
230
|
+
|
231
|
+
if message[:role] == 'inline_task'
|
232
|
+
{role: 'inline_job', content: job.short_path}
|
233
|
+
else
|
234
|
+
{role: 'job', content: job.short_path}
|
235
|
+
end
|
236
|
+
else
|
237
|
+
message
|
238
|
+
end
|
239
|
+
end.flatten
|
240
|
+
|
241
|
+
Workflow.produce(jobs)
|
242
|
+
|
243
|
+
new
|
244
|
+
end
|
245
|
+
|
246
|
+
def self.jobs(messages, original = nil)
|
247
|
+
messages.collect do |message|
|
248
|
+
if message[:role] == 'job' || message[:role] == 'inline_job'
|
249
|
+
file = message[:content].strip
|
250
|
+
|
251
|
+
step = Step.load file
|
252
|
+
|
253
|
+
id = step.short_path[0..39]
|
254
|
+
id = id.gsub('/','-')
|
255
|
+
|
256
|
+
|
257
|
+
if message[:role] == 'inline_job'
|
258
|
+
{role: 'file', content: step.path.find}
|
259
|
+
else
|
260
|
+
tool_call = {
|
261
|
+
type: "function",
|
262
|
+
function: {
|
263
|
+
name: step.full_task_name.sub('#', '-'),
|
264
|
+
arguments: step.provided_inputs.to_json
|
265
|
+
},
|
266
|
+
id: id,
|
267
|
+
}
|
268
|
+
|
269
|
+
tool_output = {
|
270
|
+
tool_call_id: id,
|
271
|
+
role: "tool",
|
272
|
+
content: step.path.read
|
273
|
+
}
|
274
|
+
|
275
|
+
[
|
276
|
+
{role: 'function_call', content: tool_call.to_json},
|
277
|
+
{role: 'function_call_output', content: tool_output.to_json},
|
278
|
+
]
|
279
|
+
end
|
280
|
+
else
|
281
|
+
message
|
282
|
+
end
|
283
|
+
end.flatten
|
284
|
+
end
|
285
|
+
|
286
|
+
def self.clear(messages)
|
287
|
+
new = []
|
288
|
+
|
289
|
+
messages.reverse.each do |message|
|
290
|
+
if message[:role] == 'clear'
|
291
|
+
break
|
292
|
+
else
|
293
|
+
new << message
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
new.reverse
|
298
|
+
end
|
299
|
+
|
300
|
+
def self.clean(messages)
|
301
|
+
messages.reject do |message|
|
302
|
+
message[:content] && message[:content].empty?
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
def self.indiferent(messages)
|
307
|
+
messages.collect{|msg| IndiferentHash.setup msg }
|
308
|
+
end
|
309
|
+
|
310
|
+
def self.chat(file)
|
311
|
+
original = (String === file and Open.exists?(file)) ? file : Path.setup($0.dup)
|
312
|
+
caller_lib_dir = Path.caller_lib_dir(nil, 'chats')
|
313
|
+
|
314
|
+
if Array === file
|
315
|
+
messages = self.messages file
|
316
|
+
messages = self.indiferent messages
|
317
|
+
messages = self.imports messages, original, caller_lib_dir
|
318
|
+
elsif Open.exists?(file)
|
319
|
+
messages = self.messages Open.read(file)
|
320
|
+
messages = self.indiferent messages
|
321
|
+
messages = self.imports messages, original, caller_lib_dir
|
322
|
+
else
|
323
|
+
messages = self.messages file
|
324
|
+
messages = self.indiferent messages
|
325
|
+
messages = self.imports messages, original, caller_lib_dir
|
326
|
+
end
|
327
|
+
|
328
|
+
messages = self.clear messages
|
329
|
+
messages = self.clean messages
|
330
|
+
messages = self.tasks messages
|
331
|
+
messages = self.jobs messages
|
332
|
+
messages = self.files messages, original, caller_lib_dir
|
333
|
+
|
334
|
+
messages
|
335
|
+
end
|
336
|
+
|
337
|
+
def self.options(chat)
|
338
|
+
options = IndiferentHash.setup({})
|
339
|
+
new = []
|
340
|
+
chat.each do |info|
|
341
|
+
if Hash === info
|
342
|
+
role = info[:role].to_s
|
343
|
+
if %w(endpoint format model backend persist).include? role.to_s
|
344
|
+
options[role] = info[:content]
|
345
|
+
next
|
346
|
+
end
|
347
|
+
|
348
|
+
if role == 'assistant'
|
349
|
+
options.clear
|
350
|
+
end
|
351
|
+
end
|
352
|
+
new << info
|
353
|
+
end
|
354
|
+
chat.replace new
|
355
|
+
options
|
356
|
+
end
|
357
|
+
|
358
|
+
def self.tools(messages)
|
359
|
+
tool_definitions = {}
|
360
|
+
new = messages.collect do |message|
|
361
|
+
if message[:role] == 'tool'
|
362
|
+
workflow_name, task_name, *inputs = message[:content].strip.split(/\s+/)
|
363
|
+
inputs = nil if inputs.empty?
|
364
|
+
inputs = [] if inputs == ['none'] || inputs == ['noinputs']
|
365
|
+
if Open.remote? workflow_name
|
366
|
+
require 'rbbt'
|
367
|
+
require 'scout/offsite/ssh'
|
368
|
+
require 'rbbt/workflow/remote_workflow'
|
369
|
+
workflow = RemoteWorkflow.new workflow_name
|
370
|
+
else
|
371
|
+
workflow = Workflow.require_workflow workflow_name
|
372
|
+
end
|
373
|
+
definition = LLM.task_tool_definition workflow, task_name, inputs
|
374
|
+
tool_definitions[task_name] = [workflow, definition]
|
375
|
+
next
|
376
|
+
else
|
377
|
+
message
|
378
|
+
end
|
379
|
+
end.compact.flatten
|
380
|
+
messages.replace new
|
381
|
+
tool_definitions
|
382
|
+
end
|
383
|
+
|
384
|
+
def self.associations(messages)
|
385
|
+
tool_definitions = {}
|
386
|
+
kb = nil
|
387
|
+
new = messages.collect do |message|
|
388
|
+
if message[:role] == 'association'
|
389
|
+
name, path, *options = message[:content].strip.split(/\s+/)
|
390
|
+
|
391
|
+
kb ||= KnowledgeBase.new Scout.var.Agent.Chat.knowledge_base
|
392
|
+
kb.register name, Path.setup(path), IndiferentHash.parse_options(message[:content])
|
393
|
+
|
394
|
+
definition = LLM.association_tool_definition name
|
395
|
+
tool_definitions[name] = [kb, definition]
|
396
|
+
next
|
397
|
+
else
|
398
|
+
message
|
399
|
+
end
|
400
|
+
end.compact.flatten
|
401
|
+
messages.replace new
|
402
|
+
tool_definitions
|
403
|
+
end
|
404
|
+
|
405
|
+
def self.print(chat)
|
406
|
+
return chat if String === chat
|
407
|
+
chat.collect do |message|
|
408
|
+
IndiferentHash.setup message
|
409
|
+
case message[:content]
|
410
|
+
when Hash, Array
|
411
|
+
message[:role].to_s + ":\n\n" + message[:content].to_json
|
412
|
+
when nil
|
413
|
+
message[:role].to_s + ":\n\n" + message.to_json
|
414
|
+
else
|
415
|
+
message[:role].to_s + ":\n\n" + message[:content].to_s
|
416
|
+
end
|
417
|
+
end * "\n\n"
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
module Chat
|
422
|
+
extend Annotation
|
423
|
+
|
424
|
+
def message(role, content)
|
425
|
+
self.append({role: role.to_s, content: content})
|
426
|
+
end
|
427
|
+
|
428
|
+
def user(content)
|
429
|
+
message(:user, content)
|
430
|
+
end
|
431
|
+
|
432
|
+
def system(content)
|
433
|
+
message(:system, content)
|
434
|
+
end
|
435
|
+
|
436
|
+
def assistant(content)
|
437
|
+
message(:assistant, content)
|
438
|
+
end
|
439
|
+
|
440
|
+
def import(file)
|
441
|
+
message(:import, file)
|
442
|
+
end
|
443
|
+
|
444
|
+
def file(file)
|
445
|
+
message(:file, file)
|
446
|
+
end
|
447
|
+
|
448
|
+
def directory(directory)
|
449
|
+
message(:directory, directory)
|
450
|
+
end
|
451
|
+
|
452
|
+
def continue(file)
|
453
|
+
message(:continue, file)
|
454
|
+
end
|
455
|
+
|
456
|
+
def format(format)
|
457
|
+
message(:format, format)
|
458
|
+
end
|
459
|
+
|
460
|
+
def tool(*parts)
|
461
|
+
content = parts * "\n"
|
462
|
+
message(:tool, content)
|
463
|
+
end
|
464
|
+
|
465
|
+
def task(workflow, task_name, inputs = {})
|
466
|
+
input_str = IndiferentHash.print_options inputs
|
467
|
+
content = [workflow, task_name, input_str]*" "
|
468
|
+
message(:task, content)
|
469
|
+
end
|
470
|
+
|
471
|
+
def inline_task(workflow, task_name, inputs = {})
|
472
|
+
input_str = IndiferentHash.print_options inputs
|
473
|
+
content = [workflow, task_name, input_str]*" "
|
474
|
+
message(:inline_task, content)
|
475
|
+
end
|
476
|
+
|
477
|
+
def job(step)
|
478
|
+
message(:job, step.path)
|
479
|
+
end
|
480
|
+
|
481
|
+
def association(name, path, options = {})
|
482
|
+
options_str = IndiferentHash.print_options options
|
483
|
+
content = [name, path, options_str]*" "
|
484
|
+
message(:association, name)
|
485
|
+
end
|
486
|
+
|
487
|
+
def tag(content, name=nil, tag=:file, role=:user)
|
488
|
+
self.message role, LLM.tag(tag, content, name)
|
489
|
+
end
|
490
|
+
|
491
|
+
def ask(...)
|
492
|
+
LLM.ask(LLM.chat(self), ...)
|
493
|
+
end
|
494
|
+
|
495
|
+
def chat(...)
|
496
|
+
self.push({role: :assistant, content: self.ask(...)})
|
497
|
+
end
|
498
|
+
|
499
|
+
def json(...)
|
500
|
+
self.format :json
|
501
|
+
output = ask(...)
|
502
|
+
obj = JSON.parse output
|
503
|
+
if (Hash === obj) and obj.keys == ['content']
|
504
|
+
obj['content']
|
505
|
+
else
|
506
|
+
obj
|
507
|
+
end
|
508
|
+
end
|
509
|
+
|
510
|
+
def print
|
511
|
+
LLM.print LLM.chat(self)
|
512
|
+
end
|
513
|
+
|
514
|
+
def write(path, force = true)
|
515
|
+
path = path.to_s if Symbol === path
|
516
|
+
if not (Open.exists?(path) || Path === path || Path.located?(path))
|
517
|
+
path = Rbbt.chats.find[path]
|
518
|
+
end
|
519
|
+
return if Open.exists?(path) && ! force
|
520
|
+
Open.write path, self.print
|
521
|
+
end
|
522
|
+
|
523
|
+
def branch
|
524
|
+
self.annotate self.dup
|
525
|
+
end
|
526
|
+
|
527
|
+
def option(name, value)
|
528
|
+
self.message name, value
|
529
|
+
end
|
530
|
+
|
531
|
+
def endpoint(value)
|
532
|
+
option :endpoint, value
|
533
|
+
end
|
534
|
+
|
535
|
+
def model(value)
|
536
|
+
option :model, value
|
537
|
+
end
|
538
|
+
|
539
|
+
def shed
|
540
|
+
self.annotate [self.last]
|
541
|
+
end
|
542
|
+
|
543
|
+
def create_image(file, ...)
|
544
|
+
base64_image = LLM.image(LLM.chat(self), ...)
|
545
|
+
Open.write(file, Base64.decode(file_content), mode: 'wb')
|
546
|
+
end
|
547
|
+
end
|
data/lib/scout/llm/parse.rb
CHANGED
@@ -1,4 +1,33 @@
|
|
1
1
|
module LLM
|
2
|
+
def self.process_inside(inside)
|
3
|
+
header, content = inside.match(/([^\n]*)\n(.*)/).values_at 1, 2
|
4
|
+
if header.empty?
|
5
|
+
content
|
6
|
+
else
|
7
|
+
action, _sep, rest = header.partition /\s/
|
8
|
+
case action
|
9
|
+
when 'import'
|
10
|
+
when 'cmd'
|
11
|
+
title = rest.strip.empty? ? content : rest
|
12
|
+
tag('file', title, CMD.cmd(content).read)
|
13
|
+
when 'file'
|
14
|
+
file = content
|
15
|
+
title = rest.strip.empty? ? file : rest
|
16
|
+
tag(action, title, Open.read(file))
|
17
|
+
when 'directory'
|
18
|
+
directory = content
|
19
|
+
title = rest.strip.empty? ? directory : rest
|
20
|
+
directory_content = Dir.glob(File.join(directory, '**/*')).collect do |file|
|
21
|
+
file_title = Misc.path_relative_to(directory, file)
|
22
|
+
tag('file', file_title, Open.read(file) )
|
23
|
+
end * "\n"
|
24
|
+
tag(action, title, directory_content )
|
25
|
+
else
|
26
|
+
tag(action, rest, content)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
2
31
|
def self.parse(question, role = nil)
|
3
32
|
role = :user if role.nil?
|
4
33
|
|
@@ -12,21 +41,49 @@ module LLM
|
|
12
41
|
inside = m[2]
|
13
42
|
post = m[3]
|
14
43
|
messages = parse(pre, role)
|
15
|
-
|
16
|
-
messages
|
44
|
+
|
45
|
+
messages = [{role: role, content: ''}] if messages.empty?
|
46
|
+
messages.last[:content] += process_inside inside
|
47
|
+
|
48
|
+
last = parse(post, messages.last[:role])
|
49
|
+
|
50
|
+
messages.concat last
|
51
|
+
|
52
|
+
messages
|
53
|
+
elsif m = question.match(/(.*?)(```.*?```)(.*)/m)
|
54
|
+
pre = m[1]
|
55
|
+
inside = m[2]
|
56
|
+
post = m[3]
|
57
|
+
messages = parse(pre, role)
|
58
|
+
|
59
|
+
messages = [{role: role, content: ''}] if messages.empty?
|
60
|
+
messages.last[:content] += inside
|
61
|
+
|
62
|
+
last = parse(post, messages.last[:role])
|
63
|
+
|
64
|
+
if last.first[:role] == messages.last[:role]
|
65
|
+
m = last.shift
|
66
|
+
messages.last[:content] += m[:content]
|
67
|
+
end
|
68
|
+
|
69
|
+
messages.concat last
|
70
|
+
|
71
|
+
messages
|
17
72
|
else
|
18
|
-
question.
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
q = line
|
73
|
+
chunks = question.scan(/(.*?)^(\w+):(.*?)(?=^\w+:|\z)/m)
|
74
|
+
|
75
|
+
if chunks.any?
|
76
|
+
messages = []
|
77
|
+
messages << {role: role, content: chunks.first.first} if chunks.first and not chunks.first.first.empty?
|
78
|
+
chunks.collect do |pre,role,text|
|
79
|
+
messages << {role: role, content: text.strip}
|
26
80
|
end
|
27
|
-
|
28
|
-
|
29
|
-
|
81
|
+
messages
|
82
|
+
elsif question.strip.empty?
|
83
|
+
[]
|
84
|
+
else
|
85
|
+
[{role: role, content: question}]
|
86
|
+
end
|
30
87
|
end
|
31
88
|
end
|
32
89
|
end
|