scout-ai 1.0.0 → 1.1.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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.vimproject +87 -15
  3. data/README.md +296 -0
  4. data/Rakefile +2 -0
  5. data/VERSION +1 -1
  6. data/doc/Agent.md +279 -0
  7. data/doc/Chat.md +258 -0
  8. data/doc/LLM.md +446 -0
  9. data/doc/Model.md +513 -0
  10. data/doc/RAG.md +129 -0
  11. data/lib/scout/llm/agent/chat.rb +48 -1
  12. data/lib/scout/llm/agent/delegate.rb +51 -0
  13. data/lib/scout/llm/agent/iterate.rb +44 -0
  14. data/lib/scout/llm/agent.rb +43 -22
  15. data/lib/scout/llm/ask.rb +47 -7
  16. data/lib/scout/llm/backends/anthropic.rb +147 -0
  17. data/lib/scout/llm/backends/bedrock.rb +1 -1
  18. data/lib/scout/llm/backends/ollama.rb +27 -30
  19. data/lib/scout/llm/backends/openai.rb +36 -41
  20. data/lib/scout/llm/backends/responses.rb +166 -113
  21. data/lib/scout/llm/chat.rb +270 -102
  22. data/lib/scout/llm/embed.rb +4 -4
  23. data/lib/scout/llm/mcp.rb +28 -0
  24. data/lib/scout/llm/parse.rb +1 -0
  25. data/lib/scout/llm/rag.rb +9 -0
  26. data/lib/scout/llm/tools/call.rb +76 -0
  27. data/lib/scout/llm/tools/knowledge_base.rb +159 -0
  28. data/lib/scout/llm/tools/mcp.rb +59 -0
  29. data/lib/scout/llm/tools/workflow.rb +106 -0
  30. data/lib/scout/llm/tools.rb +98 -141
  31. data/lib/scout-ai.rb +1 -0
  32. data/scout-ai.gemspec +31 -18
  33. data/scout_commands/agent/ask +59 -78
  34. data/scout_commands/documenter +148 -0
  35. data/scout_commands/llm/ask +3 -2
  36. data/scout_commands/llm/server +319 -0
  37. data/share/server/chat.html +138 -0
  38. data/share/server/chat.js +468 -0
  39. data/test/scout/llm/backends/test_anthropic.rb +134 -0
  40. data/test/scout/llm/backends/test_ollama.rb +1 -1
  41. data/test/scout/llm/backends/test_openai.rb +45 -6
  42. data/test/scout/llm/backends/test_responses.rb +124 -0
  43. data/test/scout/llm/test_agent.rb +1 -93
  44. data/test/scout/llm/test_ask.rb +3 -1
  45. data/test/scout/llm/test_chat.rb +43 -1
  46. data/test/scout/llm/test_mcp.rb +29 -0
  47. data/test/scout/llm/tools/test_knowledge_base.rb +22 -0
  48. data/test/scout/llm/tools/test_mcp.rb +11 -0
  49. data/test/scout/llm/tools/test_workflow.rb +39 -0
  50. metadata +56 -17
  51. data/README.rdoc +0 -18
  52. data/python/scout_ai/__pycache__/__init__.cpython-310.pyc +0 -0
  53. data/python/scout_ai/__pycache__/__init__.cpython-311.pyc +0 -0
  54. data/python/scout_ai/__pycache__/huggingface.cpython-310.pyc +0 -0
  55. data/python/scout_ai/__pycache__/huggingface.cpython-311.pyc +0 -0
  56. data/python/scout_ai/__pycache__/util.cpython-310.pyc +0 -0
  57. data/python/scout_ai/__pycache__/util.cpython-311.pyc +0 -0
  58. data/python/scout_ai/atcold/plot_lib.py +0 -141
  59. data/python/scout_ai/atcold/spiral.py +0 -27
  60. data/python/scout_ai/huggingface/train/__pycache__/__init__.cpython-310.pyc +0 -0
  61. data/python/scout_ai/huggingface/train/__pycache__/next_token.cpython-310.pyc +0 -0
  62. data/python/scout_ai/language_model.py +0 -70
  63. /data/{python/scout_ai/atcold/__init__.py → test/scout/llm/tools/test_call.rb} +0 -0
@@ -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 = nil
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.strip }
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
- #current_role = default_role
122
+ current_role = 'user' if role == 'previous_response_id'
128
123
  current_content = ""
129
124
  end
130
125
  else
131
- current_content << "\n" << line
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
- 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
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 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
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
- raise "Import not found: #{file}"
175
+ LLM.purge(new)
162
176
  end
163
177
 
164
- if message[:role] == 'continue'
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
- 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
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 = 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
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
@@ -216,7 +219,7 @@ module LLM
216
219
  def self.tasks(messages, original = nil)
217
220
  jobs = []
218
221
  new = messages.collect do |message|
219
- if message[:role] == 'task' || message[:role] == 'inline_task'
222
+ if message[:role] == 'task' || message[:role] == 'inline_task' || message[:role] == 'exec_task'
220
223
  info = message[:content].strip
221
224
 
222
225
  workflow, task = info.split(" ").values_at 0, 1
@@ -224,14 +227,28 @@ module LLM
224
227
  options = IndiferentHash.parse_options info
225
228
  jobname = options.delete :jobname
226
229
 
227
- job = Workflow.require_workflow(workflow).job(task, jobname, options)
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
- jobs << job
240
+ jobs << job unless message[:role] == 'exec_task'
230
241
 
231
- if message[:role] == 'inline_task'
232
- {role: 'inline_job', content: job.short_path}
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}
233
250
  else
234
- {role: 'job', content: job.short_path}
251
+ {role: 'job', content: job.path.find}
235
252
  end
236
253
  else
237
254
  message
@@ -253,23 +270,31 @@ module LLM
253
270
  id = step.short_path[0..39]
254
271
  id = id.gsub('/','-')
255
272
 
256
-
257
273
  if message[:role] == 'inline_job'
258
- {role: 'file', content: step.path.find}
274
+ path = step.path
275
+ path = path.find if Path === path
276
+ {role: 'file', content: step.path}
259
277
  else
278
+
279
+ function_name = step.full_task_name.sub('#', '-')
280
+ function_name = step.task_name
260
281
  tool_call = {
261
- type: "function",
262
282
  function: {
263
- name: step.full_task_name.sub('#', '-'),
264
- arguments: step.provided_inputs.to_json
283
+ name: function_name,
284
+ arguments: step.provided_inputs
265
285
  },
266
286
  id: id,
267
287
  }
268
288
 
289
+ content = if step.done?
290
+ Open.read(step.path)
291
+ elsif step.error?
292
+ step.exception
293
+ end
294
+
269
295
  tool_output = {
270
- tool_call_id: id,
271
- role: "tool",
272
- content: step.path.read
296
+ id: id,
297
+ content: content
273
298
  }
274
299
 
275
300
  [
@@ -287,7 +312,10 @@ module LLM
287
312
  new = []
288
313
 
289
314
  messages.reverse.each do |message|
290
- if message[:role] == 'clear'
315
+ if message[:role].to_s == 'clear'
316
+ break
317
+ elsif message[:role].to_s == 'previous_response_id'
318
+ new << message
291
319
  break
292
320
  else
293
321
  new << message
@@ -299,7 +327,8 @@ module LLM
299
327
 
300
328
  def self.clean(messages)
301
329
  messages.reject do |message|
302
- message[:content] && message[:content].empty?
330
+ ((String === message[:content]) && message[:content].empty?) ||
331
+ message[:role] == 'skip'
303
332
  end
304
333
  end
305
334
 
@@ -307,8 +336,8 @@ module LLM
307
336
  messages.collect{|msg| IndiferentHash.setup msg }
308
337
  end
309
338
 
310
- def self.chat(file)
311
- original = (String === file and Open.exists?(file)) ? file : Path.setup($0.dup)
339
+ def self.chat(file, original = nil)
340
+ original ||= (String === file and Open.exists?(file)) ? file : Path.setup($0.dup)
312
341
  caller_lib_dir = Path.caller_lib_dir(nil, 'chats')
313
342
 
314
343
  if Array === file
@@ -336,13 +365,41 @@ module LLM
336
365
 
337
366
  def self.options(chat)
338
367
  options = IndiferentHash.setup({})
368
+ sticky_options = IndiferentHash.setup({})
339
369
  new = []
370
+
371
+ # Most options reset after an assistant reply, but not previous_response_id
340
372
  chat.each do |info|
341
373
  if Hash === info
342
374
  role = info[:role].to_s
343
- if %w(endpoint format model backend persist).include? role.to_s
375
+ if %w(endpoint model backend persist agent).include? role.to_s
344
376
  options[role] = info[:content]
345
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
346
403
  end
347
404
 
348
405
  if role == 'assistant'
@@ -352,14 +409,32 @@ module LLM
352
409
  new << info
353
410
  end
354
411
  chat.replace new
355
- options
412
+ sticky_options.merge options
356
413
  end
357
414
 
358
415
  def self.tools(messages)
359
- tool_definitions = {}
416
+ tool_definitions = IndiferentHash.setup({})
360
417
  new = messages.collect do |message|
361
- if message[:role] == 'tool'
362
- workflow_name, task_name, *inputs = message[:content].strip.split(/\s+/)
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)
363
438
  inputs = nil if inputs.empty?
364
439
  inputs = [] if inputs == ['none'] || inputs == ['noinputs']
365
440
  if Open.remote? workflow_name
@@ -370,9 +445,24 @@ module LLM
370
445
  else
371
446
  workflow = Workflow.require_workflow workflow_name
372
447
  end
373
- definition = LLM.task_tool_definition workflow, task_name, inputs
374
- tool_definitions[task_name] = [workflow, definition]
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)
375
463
  next
464
+ elsif message[:role] == 'clear_tools'
465
+ tool_definitions = {}
376
466
  else
377
467
  message
378
468
  end
@@ -381,19 +471,19 @@ module LLM
381
471
  tool_definitions
382
472
  end
383
473
 
384
- def self.associations(messages)
474
+ def self.associations(messages, kb = nil)
385
475
  tool_definitions = {}
386
- kb = nil
387
476
  new = messages.collect do |message|
388
477
  if message[:role] == 'association'
389
- name, path, *options = message[:content].strip.split(/\s+/)
478
+ name, path, *options = content_tokens(message)
390
479
 
391
480
  kb ||= KnowledgeBase.new Scout.var.Agent.Chat.knowledge_base
392
481
  kb.register name, Path.setup(path), IndiferentHash.parse_options(message[:content])
393
482
 
394
- definition = LLM.association_tool_definition name
395
- tool_definitions[name] = [kb, definition]
483
+ tool_definitions.merge!(LLM.knowledge_base_tool_definition( kb, [name]))
396
484
  next
485
+ elsif message[:role] == 'clear_associations'
486
+ tool_definitions = {}
397
487
  else
398
488
  message
399
489
  end
@@ -404,18 +494,29 @@ module LLM
404
494
 
405
495
  def self.print(chat)
406
496
  return chat if String === chat
407
- chat.collect do |message|
497
+ "\n" + chat.collect do |message|
408
498
  IndiferentHash.setup message
409
499
  case message[:content]
410
500
  when Hash, Array
411
501
  message[:role].to_s + ":\n\n" + message[:content].to_json
412
- when nil
413
- message[:role].to_s + ":\n\n" + message.to_json
502
+ when nil, ''
503
+ message[:role].to_s + ":"
414
504
  else
415
- message[:role].to_s + ":\n\n" + message[:content].to_s
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
416
510
  end
417
511
  end * "\n\n"
418
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
419
520
  end
420
521
 
421
522
  module Chat
@@ -441,6 +542,10 @@ module Chat
441
542
  message(:import, file)
442
543
  end
443
544
 
545
+ def import_last(file)
546
+ message(:last, file)
547
+ end
548
+
444
549
  def file(file)
445
550
  message(:file, file)
446
551
  end
@@ -478,6 +583,11 @@ module Chat
478
583
  message(:job, step.path)
479
584
  end
480
585
 
586
+ def inline_job(step)
587
+ message(:inline_job, step.path)
588
+ end
589
+
590
+
481
591
  def association(name, path, options = {})
482
592
  options_str = IndiferentHash.print_options options
483
593
  content = [name, path, options_str]*" "
@@ -488,12 +598,20 @@ module Chat
488
598
  self.message role, LLM.tag(tag, content, name)
489
599
  end
490
600
 
601
+
491
602
  def ask(...)
492
603
  LLM.ask(LLM.chat(self), ...)
493
604
  end
494
605
 
495
606
  def chat(...)
496
- self.push({role: :assistant, content: self.ask(...)})
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
+ end
497
615
  end
498
616
 
499
617
  def json(...)
@@ -507,17 +625,15 @@ module Chat
507
625
  end
508
626
  end
509
627
 
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]
628
+ def json_format(format, ...)
629
+ self.format format
630
+ output = ask(...)
631
+ obj = JSON.parse output
632
+ if (Hash === obj) and obj.keys == ['content']
633
+ obj['content']
634
+ else
635
+ obj
518
636
  end
519
- return if Open.exists?(path) && ! force
520
- Open.write path, self.print
521
637
  end
522
638
 
523
639
  def branch
@@ -525,7 +641,7 @@ module Chat
525
641
  end
526
642
 
527
643
  def option(name, value)
528
- self.message name, value
644
+ self.message 'option', [name, value] * " "
529
645
  end
530
646
 
531
647
  def endpoint(value)
@@ -536,10 +652,62 @@ module Chat
536
652
  option :model, value
537
653
  end
538
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
668
+
669
+ def purge
670
+ Chat.setup(LLM.purge(self))
671
+ end
672
+
539
673
  def shed
540
- self.annotate [self.last]
674
+ self.annotate [final]
675
+ end
676
+
677
+ def answer
678
+ final[:content]
679
+ end
680
+
681
+ # Write and save
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)
541
690
  end
542
-
691
+
692
+ def write(path, force = true)
693
+ path = path.to_s if Symbol === path
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
699
+ end
700
+
701
+ def write_answer(path, force = true)
702
+ path = path.to_s if Symbol === path
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
708
+ end
709
+
710
+ # Image
543
711
  def create_image(file, ...)
544
712
  base64_image = LLM.image(LLM.chat(self), ...)
545
713
  Open.write(file, Base64.decode(file_content), mode: 'wb')
@@ -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
@@ -1,3 +1,4 @@
1
+ require 'scout/llm/utils'
1
2
  module LLM
2
3
  def self.process_inside(inside)
3
4
  header, content = inside.match(/([^\n]*)\n(.*)/).values_at 1, 2
data/lib/scout/llm/rag.rb CHANGED
@@ -12,5 +12,14 @@ module LLM
12
12
  end
13
13
  t
14
14
  end
15
+
16
+ def self.load(path, dim)
17
+ require 'hnswlib'
18
+
19
+ u = Hnswlib::HierarchicalNSW.new(space: 'l2', dim: dim)
20
+ u.load_index(path)
21
+
22
+ u
23
+ end
15
24
  end
16
25
  end