scout-ai 1.0.0 → 1.0.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 +80 -15
- data/README.md +296 -0
- data/Rakefile +2 -0
- data/VERSION +1 -1
- data/doc/Agent.md +279 -0
- data/doc/Chat.md +258 -0
- data/doc/LLM.md +446 -0
- data/doc/Model.md +513 -0
- data/doc/RAG.md +129 -0
- data/lib/scout/llm/agent/chat.rb +51 -1
- data/lib/scout/llm/agent/delegate.rb +39 -0
- data/lib/scout/llm/agent/iterate.rb +44 -0
- data/lib/scout/llm/agent.rb +42 -21
- data/lib/scout/llm/ask.rb +38 -6
- data/lib/scout/llm/backends/anthropic.rb +147 -0
- data/lib/scout/llm/backends/bedrock.rb +1 -1
- data/lib/scout/llm/backends/ollama.rb +23 -29
- data/lib/scout/llm/backends/openai.rb +34 -40
- data/lib/scout/llm/backends/responses.rb +158 -110
- data/lib/scout/llm/chat.rb +250 -94
- data/lib/scout/llm/embed.rb +4 -4
- data/lib/scout/llm/mcp.rb +28 -0
- data/lib/scout/llm/parse.rb +1 -0
- data/lib/scout/llm/rag.rb +9 -0
- data/lib/scout/llm/tools/call.rb +66 -0
- data/lib/scout/llm/tools/knowledge_base.rb +158 -0
- data/lib/scout/llm/tools/mcp.rb +59 -0
- data/lib/scout/llm/tools/workflow.rb +69 -0
- data/lib/scout/llm/tools.rb +58 -143
- data/lib/scout-ai.rb +1 -0
- data/scout-ai.gemspec +31 -18
- data/scout_commands/agent/ask +28 -71
- data/scout_commands/documenter +148 -0
- data/scout_commands/llm/ask +2 -2
- data/scout_commands/llm/server +319 -0
- data/share/server/chat.html +138 -0
- data/share/server/chat.js +468 -0
- data/test/scout/llm/backends/test_anthropic.rb +134 -0
- data/test/scout/llm/backends/test_openai.rb +45 -6
- data/test/scout/llm/backends/test_responses.rb +124 -0
- data/test/scout/llm/test_agent.rb +0 -70
- data/test/scout/llm/test_ask.rb +3 -1
- data/test/scout/llm/test_chat.rb +43 -1
- data/test/scout/llm/test_mcp.rb +29 -0
- data/test/scout/llm/tools/test_knowledge_base.rb +22 -0
- data/test/scout/llm/tools/test_mcp.rb +11 -0
- data/test/scout/llm/tools/test_workflow.rb +39 -0
- metadata +56 -17
- data/README.rdoc +0 -18
- 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/plot_lib.py +0 -141
- data/python/scout_ai/atcold/spiral.py +0 -27
- 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/language_model.py +0 -70
- /data/{python/scout_ai/atcold/__init__.py → test/scout/llm/tools/test_call.rb} +0 -0
data/lib/scout/llm/chat.rb
CHANGED
@@ -1,8 +1,13 @@
|
|
1
1
|
require_relative 'utils'
|
2
2
|
require_relative 'parse'
|
3
3
|
require_relative 'tools'
|
4
|
+
require 'shellwords'
|
4
5
|
|
5
6
|
module LLM
|
7
|
+
def self.content_tokens(message)
|
8
|
+
Shellwords.split(message[:content].strip)
|
9
|
+
end
|
10
|
+
|
6
11
|
def self.messages(question, role = nil)
|
7
12
|
default_role = "user"
|
8
13
|
|
@@ -17,7 +22,7 @@ module LLM
|
|
17
22
|
end
|
18
23
|
|
19
24
|
messages = []
|
20
|
-
current_role =
|
25
|
+
current_role = default_role
|
21
26
|
current_content = ""
|
22
27
|
in_protected_block = false
|
23
28
|
protected_block_type = nil
|
@@ -42,17 +47,6 @@ module LLM
|
|
42
47
|
current_content << "\n" << line unless line.strip.empty?
|
43
48
|
end
|
44
49
|
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
50
|
elsif stripped.end_with?("]]") && in_protected_block && protected_block_type == :square
|
57
51
|
in_protected_block = false
|
58
52
|
protected_block_type = nil
|
@@ -114,8 +108,9 @@ module LLM
|
|
114
108
|
role = $1
|
115
109
|
inline_content = $2.strip
|
116
110
|
|
111
|
+
current_content = current_content.strip if current_content
|
117
112
|
# Save current message if any
|
118
|
-
messages << { role: current_role, content: current_content
|
113
|
+
messages << { role: current_role, content: current_content }
|
119
114
|
|
120
115
|
if inline_content.empty?
|
121
116
|
# Block message
|
@@ -124,11 +119,15 @@ module LLM
|
|
124
119
|
else
|
125
120
|
# Inline message + next block is default role
|
126
121
|
messages << { role: role, content: inline_content }
|
127
|
-
|
122
|
+
current_role = 'user' if role == 'previous_response_id'
|
128
123
|
current_content = ""
|
129
124
|
end
|
130
125
|
else
|
131
|
-
current_content
|
126
|
+
if current_content.nil?
|
127
|
+
current_content = line
|
128
|
+
else
|
129
|
+
current_content << "\n" << line
|
130
|
+
end
|
132
131
|
end
|
133
132
|
end
|
134
133
|
|
@@ -138,34 +137,45 @@ module LLM
|
|
138
137
|
messages
|
139
138
|
end
|
140
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
|
+
|
141
161
|
def self.imports(messages, original = nil, caller_lib_dir = Path.caller_lib_dir(nil, 'chats'))
|
142
162
|
messages.collect do |message|
|
143
|
-
if message[:role] == 'import' || message[:role] == 'continue'
|
163
|
+
if message[:role] == 'import' || message[:role] == 'continue' || message[:role] == 'last'
|
144
164
|
file = message[:content].to_s.strip
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
relative_lib = File.join(caller_lib_dir, file)
|
150
|
-
end
|
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)
|
151
169
|
|
152
|
-
new = if
|
153
|
-
|
154
|
-
elsif
|
155
|
-
LLM.
|
156
|
-
elsif relative_lib && Open.exist?(relative_lib)
|
157
|
-
LLM.chat relative_lib
|
158
|
-
elsif path.exists?
|
159
|
-
LLM.chat path
|
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]
|
160
174
|
else
|
161
|
-
|
175
|
+
LLM.purge(new)
|
162
176
|
end
|
163
177
|
|
164
|
-
|
165
|
-
new.last
|
166
|
-
else
|
167
|
-
new
|
168
|
-
end
|
178
|
+
LLM.chat new, found_file
|
169
179
|
else
|
170
180
|
message
|
171
181
|
end
|
@@ -175,25 +185,11 @@ module LLM
|
|
175
185
|
def self.files(messages, original = nil, caller_lib_dir = Path.caller_lib_dir(nil, 'chats'))
|
176
186
|
messages.collect do |message|
|
177
187
|
if message[:role] == 'file' || message[:role] == 'directory'
|
178
|
-
file = message[:content].strip
|
179
|
-
|
180
|
-
|
181
|
-
if original
|
182
|
-
relative = File.join(File.dirname(original), file)
|
183
|
-
relative_lib = File.join(caller_lib_dir, file)
|
184
|
-
end
|
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?
|
185
191
|
|
186
|
-
target =
|
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
|
192
|
+
target = found_file
|
197
193
|
|
198
194
|
if message[:role] == 'directory'
|
199
195
|
Path.setup target
|
@@ -207,6 +203,13 @@ module LLM
|
|
207
203
|
new = LLM.tag :file, Open.read(target), file
|
208
204
|
{role: 'user', content: new}
|
209
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
|
210
213
|
else
|
211
214
|
message
|
212
215
|
end
|
@@ -224,14 +227,22 @@ module LLM
|
|
224
227
|
options = IndiferentHash.parse_options info
|
225
228
|
jobname = options.delete :jobname
|
226
229
|
|
227
|
-
|
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)
|
228
239
|
|
229
240
|
jobs << job
|
230
241
|
|
231
242
|
if message[:role] == 'inline_task'
|
232
|
-
{role: 'inline_job', content: job.
|
243
|
+
{role: 'inline_job', content: job.path.find}
|
233
244
|
else
|
234
|
-
{role: 'job', content: job.
|
245
|
+
{role: 'job', content: job.path.find}
|
235
246
|
end
|
236
247
|
else
|
237
248
|
message
|
@@ -255,7 +266,9 @@ module LLM
|
|
255
266
|
|
256
267
|
|
257
268
|
if message[:role] == 'inline_job'
|
258
|
-
|
269
|
+
path = step.path
|
270
|
+
path = path.find if Path === path
|
271
|
+
{role: 'file', content: step.path}
|
259
272
|
else
|
260
273
|
tool_call = {
|
261
274
|
type: "function",
|
@@ -267,9 +280,9 @@ module LLM
|
|
267
280
|
}
|
268
281
|
|
269
282
|
tool_output = {
|
270
|
-
|
283
|
+
id: id,
|
271
284
|
role: "tool",
|
272
|
-
content: step.path
|
285
|
+
content: Open.read(step.path)
|
273
286
|
}
|
274
287
|
|
275
288
|
[
|
@@ -287,7 +300,10 @@ module LLM
|
|
287
300
|
new = []
|
288
301
|
|
289
302
|
messages.reverse.each do |message|
|
290
|
-
if message[:role] == 'clear'
|
303
|
+
if message[:role].to_s == 'clear'
|
304
|
+
break
|
305
|
+
elsif message[:role].to_s == 'previous_response_id'
|
306
|
+
new << message
|
291
307
|
break
|
292
308
|
else
|
293
309
|
new << message
|
@@ -299,7 +315,8 @@ module LLM
|
|
299
315
|
|
300
316
|
def self.clean(messages)
|
301
317
|
messages.reject do |message|
|
302
|
-
message[:content] && message[:content].empty?
|
318
|
+
((String === message[:content]) && message[:content].empty?) ||
|
319
|
+
message[:role] == 'skip'
|
303
320
|
end
|
304
321
|
end
|
305
322
|
|
@@ -307,8 +324,8 @@ module LLM
|
|
307
324
|
messages.collect{|msg| IndiferentHash.setup msg }
|
308
325
|
end
|
309
326
|
|
310
|
-
def self.chat(file)
|
311
|
-
original
|
327
|
+
def self.chat(file, original = nil)
|
328
|
+
original ||= (String === file and Open.exists?(file)) ? file : Path.setup($0.dup)
|
312
329
|
caller_lib_dir = Path.caller_lib_dir(nil, 'chats')
|
313
330
|
|
314
331
|
if Array === file
|
@@ -336,13 +353,41 @@ module LLM
|
|
336
353
|
|
337
354
|
def self.options(chat)
|
338
355
|
options = IndiferentHash.setup({})
|
356
|
+
sticky_options = IndiferentHash.setup({})
|
339
357
|
new = []
|
358
|
+
|
359
|
+
# Most options reset after an assistant reply, but not previous_response_id
|
340
360
|
chat.each do |info|
|
341
361
|
if Hash === info
|
342
362
|
role = info[:role].to_s
|
343
|
-
if %w(endpoint
|
363
|
+
if %w(endpoint model backend persist agent).include? role.to_s
|
344
364
|
options[role] = info[:content]
|
345
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
|
380
|
+
|
381
|
+
if role.to_s == 'option'
|
382
|
+
key, _, value = info[:content].partition(" ")
|
383
|
+
options[key] = value
|
384
|
+
next
|
385
|
+
end
|
386
|
+
|
387
|
+
if role.to_s == 'sticky_option'
|
388
|
+
key, _, value = info[:content].partition(" ")
|
389
|
+
sticky_options[key] = value
|
390
|
+
next
|
346
391
|
end
|
347
392
|
|
348
393
|
if role == 'assistant'
|
@@ -352,14 +397,32 @@ module LLM
|
|
352
397
|
new << info
|
353
398
|
end
|
354
399
|
chat.replace new
|
355
|
-
options
|
400
|
+
sticky_options.merge options
|
356
401
|
end
|
357
402
|
|
358
403
|
def self.tools(messages)
|
359
|
-
tool_definitions = {}
|
404
|
+
tool_definitions = IndiferentHash.setup({})
|
360
405
|
new = messages.collect do |message|
|
361
|
-
if message[:role] == '
|
362
|
-
|
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)
|
363
426
|
inputs = nil if inputs.empty?
|
364
427
|
inputs = [] if inputs == ['none'] || inputs == ['noinputs']
|
365
428
|
if Open.remote? workflow_name
|
@@ -370,9 +433,24 @@ module LLM
|
|
370
433
|
else
|
371
434
|
workflow = Workflow.require_workflow workflow_name
|
372
435
|
end
|
373
|
-
|
374
|
-
|
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
|
375
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 = {}
|
376
454
|
else
|
377
455
|
message
|
378
456
|
end
|
@@ -381,19 +459,19 @@ module LLM
|
|
381
459
|
tool_definitions
|
382
460
|
end
|
383
461
|
|
384
|
-
def self.associations(messages)
|
462
|
+
def self.associations(messages, kb = nil)
|
385
463
|
tool_definitions = {}
|
386
|
-
kb = nil
|
387
464
|
new = messages.collect do |message|
|
388
465
|
if message[:role] == 'association'
|
389
|
-
name, path, *options = message
|
466
|
+
name, path, *options = content_tokens(message)
|
390
467
|
|
391
468
|
kb ||= KnowledgeBase.new Scout.var.Agent.Chat.knowledge_base
|
392
469
|
kb.register name, Path.setup(path), IndiferentHash.parse_options(message[:content])
|
393
470
|
|
394
|
-
|
395
|
-
tool_definitions[name] = [kb, definition]
|
471
|
+
tool_definitions.merge!(LLM.knowledge_base_tool_definition( kb, [name]))
|
396
472
|
next
|
473
|
+
elsif message[:role] == 'clear_associations'
|
474
|
+
tool_definitions = {}
|
397
475
|
else
|
398
476
|
message
|
399
477
|
end
|
@@ -404,18 +482,29 @@ module LLM
|
|
404
482
|
|
405
483
|
def self.print(chat)
|
406
484
|
return chat if String === chat
|
407
|
-
chat.collect do |message|
|
485
|
+
"\n" + chat.collect do |message|
|
408
486
|
IndiferentHash.setup message
|
409
487
|
case message[:content]
|
410
488
|
when Hash, Array
|
411
489
|
message[:role].to_s + ":\n\n" + message[:content].to_json
|
412
|
-
when nil
|
413
|
-
message[:role].to_s + "
|
490
|
+
when nil, ''
|
491
|
+
message[:role].to_s + ":"
|
414
492
|
else
|
415
|
-
|
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
|
416
498
|
end
|
417
499
|
end * "\n\n"
|
418
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
|
419
508
|
end
|
420
509
|
|
421
510
|
module Chat
|
@@ -441,6 +530,10 @@ module Chat
|
|
441
530
|
message(:import, file)
|
442
531
|
end
|
443
532
|
|
533
|
+
def import_last(file)
|
534
|
+
message(:last, file)
|
535
|
+
end
|
536
|
+
|
444
537
|
def file(file)
|
445
538
|
message(:file, file)
|
446
539
|
end
|
@@ -478,6 +571,11 @@ module Chat
|
|
478
571
|
message(:job, step.path)
|
479
572
|
end
|
480
573
|
|
574
|
+
def inline_job(step)
|
575
|
+
message(:inline_job, step.path)
|
576
|
+
end
|
577
|
+
|
578
|
+
|
481
579
|
def association(name, path, options = {})
|
482
580
|
options_str = IndiferentHash.print_options options
|
483
581
|
content = [name, path, options_str]*" "
|
@@ -488,12 +586,20 @@ module Chat
|
|
488
586
|
self.message role, LLM.tag(tag, content, name)
|
489
587
|
end
|
490
588
|
|
589
|
+
|
491
590
|
def ask(...)
|
492
591
|
LLM.ask(LLM.chat(self), ...)
|
493
592
|
end
|
494
593
|
|
495
594
|
def chat(...)
|
496
|
-
|
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
|
497
603
|
end
|
498
604
|
|
499
605
|
def json(...)
|
@@ -507,17 +613,15 @@ module Chat
|
|
507
613
|
end
|
508
614
|
end
|
509
615
|
|
510
|
-
def
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
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
|
518
624
|
end
|
519
|
-
return if Open.exists?(path) && ! force
|
520
|
-
Open.write path, self.print
|
521
625
|
end
|
522
626
|
|
523
627
|
def branch
|
@@ -525,7 +629,7 @@ module Chat
|
|
525
629
|
end
|
526
630
|
|
527
631
|
def option(name, value)
|
528
|
-
self.message name, value
|
632
|
+
self.message 'option', [name, value] * " "
|
529
633
|
end
|
530
634
|
|
531
635
|
def endpoint(value)
|
@@ -536,10 +640,62 @@ module Chat
|
|
536
640
|
option :model, value
|
537
641
|
end
|
538
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
|
+
|
539
661
|
def shed
|
540
|
-
self.annotate [
|
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)
|
541
678
|
end
|
542
|
-
|
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
|
543
699
|
def create_image(file, ...)
|
544
700
|
base64_image = LLM.image(LLM.chat(self), ...)
|
545
701
|
Open.write(file, Base64.decode(file_content), mode: 'wb')
|
data/lib/scout/llm/embed.rb
CHANGED
@@ -1,8 +1,4 @@
|
|
1
1
|
require 'scout'
|
2
|
-
require_relative 'backends/ollama'
|
3
|
-
require_relative 'backends/openai'
|
4
|
-
require_relative 'backends/openwebui'
|
5
|
-
require_relative 'backends/relay'
|
6
2
|
|
7
3
|
module LLM
|
8
4
|
def self.embed(text, options = {})
|
@@ -17,12 +13,16 @@ module LLM
|
|
17
13
|
|
18
14
|
case backend
|
19
15
|
when :openai, "openai"
|
16
|
+
require_relative 'backends/openai'
|
20
17
|
LLM::OpenAI.embed(text, options)
|
21
18
|
when :ollama, "ollama"
|
19
|
+
require_relative 'backends/ollama'
|
22
20
|
LLM::OLlama.embed(text, options)
|
23
21
|
when :openwebui, "openwebui"
|
22
|
+
require_relative 'backends/openwebui'
|
24
23
|
LLM::OpenWebUI.embed(text, options)
|
25
24
|
when :relay, "relay"
|
25
|
+
require_relative 'backends/relay'
|
26
26
|
LLM::Relay.embed(text, options)
|
27
27
|
else
|
28
28
|
raise "Unknown backend: #{backend}"
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'mcp'
|
2
|
+
|
3
|
+
module Workflow
|
4
|
+
def mcp(*tasks)
|
5
|
+
tasks = tasks.flatten.compact
|
6
|
+
tasks = self.tasks.keys if tasks.empty?
|
7
|
+
|
8
|
+
tools = tasks.collect do |task,inputs=nil|
|
9
|
+
tool_definition = LLM.task_tool_definition(self, task, inputs)[:function]
|
10
|
+
description = tool_definition[:description]
|
11
|
+
input_schema = tool_definition[:parameters].slice(:properties, :required)
|
12
|
+
annotations = tool_definition.slice(:title)
|
13
|
+
annotations[:read_only_hint] = true
|
14
|
+
annotations[:destructive_hint] = false
|
15
|
+
annotations[:idempotent_hint] = true
|
16
|
+
annotations[:open_world_hint] = false
|
17
|
+
MCP::Tool.define(name:task, description: description, input_schema: input_schema, annotations:annotations) do |parameters,context|
|
18
|
+
self.job(name, parameters)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
MCP::Server.new(
|
23
|
+
name: self.name,
|
24
|
+
version: "1.0.0",
|
25
|
+
tools: tools
|
26
|
+
)
|
27
|
+
end
|
28
|
+
end
|
data/lib/scout/llm/parse.rb
CHANGED