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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.vimproject +80 -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 +51 -1
  12. data/lib/scout/llm/agent/delegate.rb +39 -0
  13. data/lib/scout/llm/agent/iterate.rb +44 -0
  14. data/lib/scout/llm/agent.rb +42 -21
  15. data/lib/scout/llm/ask.rb +38 -6
  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 +23 -29
  19. data/lib/scout/llm/backends/openai.rb +34 -40
  20. data/lib/scout/llm/backends/responses.rb +158 -110
  21. data/lib/scout/llm/chat.rb +250 -94
  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 +66 -0
  27. data/lib/scout/llm/tools/knowledge_base.rb +158 -0
  28. data/lib/scout/llm/tools/mcp.rb +59 -0
  29. data/lib/scout/llm/tools/workflow.rb +69 -0
  30. data/lib/scout/llm/tools.rb +58 -143
  31. data/lib/scout-ai.rb +1 -0
  32. data/scout-ai.gemspec +31 -18
  33. data/scout_commands/agent/ask +28 -71
  34. data/scout_commands/documenter +148 -0
  35. data/scout_commands/llm/ask +2 -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_openai.rb +45 -6
  41. data/test/scout/llm/backends/test_responses.rb +124 -0
  42. data/test/scout/llm/test_agent.rb +0 -70
  43. data/test/scout/llm/test_ask.rb +3 -1
  44. data/test/scout/llm/test_chat.rb +43 -1
  45. data/test/scout/llm/test_mcp.rb +29 -0
  46. data/test/scout/llm/tools/test_knowledge_base.rb +22 -0
  47. data/test/scout/llm/tools/test_mcp.rb +11 -0
  48. data/test/scout/llm/tools/test_workflow.rb +39 -0
  49. metadata +56 -17
  50. data/README.rdoc +0 -18
  51. data/python/scout_ai/__pycache__/__init__.cpython-310.pyc +0 -0
  52. data/python/scout_ai/__pycache__/__init__.cpython-311.pyc +0 -0
  53. data/python/scout_ai/__pycache__/huggingface.cpython-310.pyc +0 -0
  54. data/python/scout_ai/__pycache__/huggingface.cpython-311.pyc +0 -0
  55. data/python/scout_ai/__pycache__/util.cpython-310.pyc +0 -0
  56. data/python/scout_ai/__pycache__/util.cpython-311.pyc +0 -0
  57. data/python/scout_ai/atcold/plot_lib.py +0 -141
  58. data/python/scout_ai/atcold/spiral.py +0 -27
  59. data/python/scout_ai/huggingface/train/__pycache__/__init__.cpython-310.pyc +0 -0
  60. data/python/scout_ai/huggingface/train/__pycache__/next_token.cpython-310.pyc +0 -0
  61. data/python/scout_ai/language_model.py +0 -70
  62. /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
@@ -224,14 +227,22 @@ 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
240
  jobs << job
230
241
 
231
242
  if message[:role] == 'inline_task'
232
- {role: 'inline_job', content: job.short_path}
243
+ {role: 'inline_job', content: job.path.find}
233
244
  else
234
- {role: 'job', content: job.short_path}
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
- {role: 'file', content: step.path.find}
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
- tool_call_id: id,
283
+ id: id,
271
284
  role: "tool",
272
- content: step.path.read
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 = (String === file and Open.exists?(file)) ? file : Path.setup($0.dup)
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 format model backend persist).include? role.to_s
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] == 'tool'
362
- workflow_name, task_name, *inputs = message[:content].strip.split(/\s+/)
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
- definition = LLM.task_tool_definition workflow, task_name, inputs
374
- tool_definitions[task_name] = [workflow, definition]
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[:content].strip.split(/\s+/)
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
- definition = LLM.association_tool_definition name
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 + ":\n\n" + message.to_json
490
+ when nil, ''
491
+ message[:role].to_s + ":"
414
492
  else
415
- message[:role].to_s + ":\n\n" + message[:content].to_s
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
- self.push({role: :assistant, content: self.ask(...)})
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 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]
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 [self.last]
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')
@@ -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