scout-ai 0.2.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 +155 -9
- data/README.md +296 -0
- data/Rakefile +3 -0
- data/VERSION +1 -1
- data/bin/scout-ai +2 -0
- 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 +74 -0
- data/lib/scout/llm/agent/delegate.rb +39 -0
- data/lib/scout/llm/agent/iterate.rb +44 -0
- data/lib/scout/llm/agent.rb +51 -30
- data/lib/scout/llm/ask.rb +63 -21
- data/lib/scout/llm/backends/anthropic.rb +147 -0
- 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 +62 -35
- data/lib/scout/llm/backends/openai.rb +77 -33
- 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 +320 -0
- data/lib/scout/llm/chat.rb +703 -0
- data/lib/scout/llm/embed.rb +4 -4
- data/lib/scout/llm/mcp.rb +28 -0
- data/lib/scout/llm/parse.rb +71 -13
- 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 +112 -76
- 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 +4 -1
- data/python/scout_ai/__init__.py +35 -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/next_token.py +315 -0
- data/python/scout_ai/util.py +32 -0
- data/scout-ai.gemspec +143 -0
- data/scout_commands/agent/ask +89 -14
- data/scout_commands/agent/kb +15 -0
- data/scout_commands/documenter +148 -0
- data/scout_commands/llm/ask +71 -12
- data/scout_commands/llm/process +4 -2
- data/scout_commands/llm/server +319 -0
- data/share/server/chat.html +138 -0
- data/share/server/chat.js +468 -0
- data/test/data/cat.jpg +0 -0
- data/test/scout/llm/agent/test_chat.rb +14 -0
- data/test/scout/llm/backends/test_anthropic.rb +134 -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 +134 -10
- data/test/scout/llm/backends/test_responses.rb +239 -0
- data/test/scout/llm/test_agent.rb +0 -70
- data/test/scout/llm/test_ask.rb +4 -1
- data/test/scout/llm/test_chat.rb +256 -0
- data/test/scout/llm/test_mcp.rb +29 -0
- data/test/scout/llm/test_parse.rb +81 -2
- data/test/scout/llm/tools/test_call.rb +0 -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
- 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 +113 -7
- data/README.rdoc +0 -18
- data/questions/coach +0 -2
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
require_relative 'utils'
|
|
2
|
+
require_relative 'parse'
|
|
3
|
+
require_relative 'tools'
|
|
4
|
+
require 'shellwords'
|
|
5
|
+
|
|
6
|
+
module LLM
|
|
7
|
+
def self.content_tokens(message)
|
|
8
|
+
Shellwords.split(message[:content].strip)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.messages(question, role = nil)
|
|
12
|
+
default_role = "user"
|
|
13
|
+
|
|
14
|
+
if Array === question
|
|
15
|
+
return question.collect do |q|
|
|
16
|
+
if String === q
|
|
17
|
+
{role: default_role, content: q}
|
|
18
|
+
else
|
|
19
|
+
q
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
messages = []
|
|
25
|
+
current_role = default_role
|
|
26
|
+
current_content = ""
|
|
27
|
+
in_protected_block = false
|
|
28
|
+
protected_block_type = nil
|
|
29
|
+
protected_stack = []
|
|
30
|
+
|
|
31
|
+
role = default_role if role.nil?
|
|
32
|
+
|
|
33
|
+
file_lines = question.split("\n")
|
|
34
|
+
|
|
35
|
+
file_lines.each do |line|
|
|
36
|
+
stripped = line.strip
|
|
37
|
+
|
|
38
|
+
# Detect protected blocks
|
|
39
|
+
if stripped.start_with?("```")
|
|
40
|
+
if in_protected_block
|
|
41
|
+
in_protected_block = false
|
|
42
|
+
protected_block_type = nil
|
|
43
|
+
current_content << "\n" << line unless line.strip.empty?
|
|
44
|
+
else
|
|
45
|
+
in_protected_block = true
|
|
46
|
+
protected_block_type = :square
|
|
47
|
+
current_content << "\n" << line unless line.strip.empty?
|
|
48
|
+
end
|
|
49
|
+
next
|
|
50
|
+
elsif stripped.end_with?("]]") && in_protected_block && protected_block_type == :square
|
|
51
|
+
in_protected_block = false
|
|
52
|
+
protected_block_type = nil
|
|
53
|
+
line = line.sub("]]", "")
|
|
54
|
+
current_content << "\n" << line unless line.strip.empty?
|
|
55
|
+
next
|
|
56
|
+
elsif stripped.start_with?("[[")
|
|
57
|
+
in_protected_block = true
|
|
58
|
+
protected_block_type = :square
|
|
59
|
+
line = line.sub("[[", "")
|
|
60
|
+
current_content << "\n" << line unless line.strip.empty?
|
|
61
|
+
next
|
|
62
|
+
elsif stripped.end_with?("]]") && in_protected_block && protected_block_type == :square
|
|
63
|
+
in_protected_block = false
|
|
64
|
+
protected_block_type = nil
|
|
65
|
+
line = line.sub("]]", "")
|
|
66
|
+
current_content << "\n" << line unless line.strip.empty?
|
|
67
|
+
next
|
|
68
|
+
elsif stripped.match(/^.*:-- .* {{{/)
|
|
69
|
+
in_protected_block = true
|
|
70
|
+
protected_block_type = :square
|
|
71
|
+
line = line.sub(/^.*:-- (.*) {{{/, '<cmd_output cmd="\1">')
|
|
72
|
+
current_content << "\n" << line unless line.strip.empty?
|
|
73
|
+
next
|
|
74
|
+
elsif stripped.match(/^.*:--.* }}}/) && in_protected_block && protected_block_type == :square
|
|
75
|
+
in_protected_block = false
|
|
76
|
+
protected_block_type = nil
|
|
77
|
+
line = line.sub(/^.*:-- .* }}}.*/, "</cmd_output>")
|
|
78
|
+
current_content << "\n" << line unless line.strip.empty?
|
|
79
|
+
next
|
|
80
|
+
elsif in_protected_block
|
|
81
|
+
|
|
82
|
+
if protected_block_type == :xml
|
|
83
|
+
if stripped =~ %r{</(\w+)>}
|
|
84
|
+
closing_tag = $1
|
|
85
|
+
if protected_stack.last == closing_tag
|
|
86
|
+
protected_stack.pop
|
|
87
|
+
end
|
|
88
|
+
if protected_stack.empty?
|
|
89
|
+
in_protected_block = false
|
|
90
|
+
protected_block_type = nil
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
current_content << "\n" << line
|
|
95
|
+
next
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# XML-style tag handling (protected content)
|
|
99
|
+
if stripped =~ /^<(\w+)(\s+[^>]*)?>/
|
|
100
|
+
tag = $1
|
|
101
|
+
protected_stack.push(tag)
|
|
102
|
+
in_protected_block = true
|
|
103
|
+
protected_block_type = :xml
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Match a new message header
|
|
107
|
+
if line =~ /^([a-z0-9_]+):(.*)$/
|
|
108
|
+
role = $1
|
|
109
|
+
inline_content = $2.strip
|
|
110
|
+
|
|
111
|
+
current_content = current_content.strip if current_content
|
|
112
|
+
# Save current message if any
|
|
113
|
+
messages << { role: current_role, content: current_content }
|
|
114
|
+
|
|
115
|
+
if inline_content.empty?
|
|
116
|
+
# Block message
|
|
117
|
+
current_role = role
|
|
118
|
+
current_content = ""
|
|
119
|
+
else
|
|
120
|
+
# Inline message + next block is default role
|
|
121
|
+
messages << { role: role, content: inline_content }
|
|
122
|
+
current_role = 'user' if role == 'previous_response_id'
|
|
123
|
+
current_content = ""
|
|
124
|
+
end
|
|
125
|
+
else
|
|
126
|
+
if current_content.nil?
|
|
127
|
+
current_content = line
|
|
128
|
+
else
|
|
129
|
+
current_content << "\n" << line
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Final message
|
|
135
|
+
messages << { role: current_role || default_role, content: current_content.strip }
|
|
136
|
+
|
|
137
|
+
messages
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def self.find_file(file, original = nil, caller_lib_dir = Path.caller_lib_dir(nil, 'chats'))
|
|
141
|
+
path = Scout.chats[file]
|
|
142
|
+
original = original.find if Path === original
|
|
143
|
+
if original
|
|
144
|
+
relative = File.join(File.dirname(original), file)
|
|
145
|
+
relative_lib = File.join(caller_lib_dir, file)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
if Open.exist?(file)
|
|
149
|
+
file
|
|
150
|
+
elsif Open.remote?(file)
|
|
151
|
+
file
|
|
152
|
+
elsif relative && Open.exist?(relative)
|
|
153
|
+
relative
|
|
154
|
+
elsif relative_lib && Open.exist?(relative_lib)
|
|
155
|
+
relative_lib
|
|
156
|
+
elsif path.exists?
|
|
157
|
+
path
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def self.imports(messages, original = nil, caller_lib_dir = Path.caller_lib_dir(nil, 'chats'))
|
|
162
|
+
messages.collect do |message|
|
|
163
|
+
if message[:role] == 'import' || message[:role] == 'continue' || message[:role] == 'last'
|
|
164
|
+
file = message[:content].to_s.strip
|
|
165
|
+
found_file = find_file(file, original, caller_lib_dir)
|
|
166
|
+
raise "Import not found: #{file}" if found_file.nil?
|
|
167
|
+
|
|
168
|
+
new = LLM.messages Open.read(found_file)
|
|
169
|
+
|
|
170
|
+
new = if message[:role] == 'continue'
|
|
171
|
+
[new.reject{|msg| msg[:content].nil? || msg[:content].strip.empty? }.last]
|
|
172
|
+
elsif message[:role] == 'last'
|
|
173
|
+
[LLM.purge(new).reject{|msg| msg[:content].empty?}.last]
|
|
174
|
+
else
|
|
175
|
+
LLM.purge(new)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
LLM.chat new, found_file
|
|
179
|
+
else
|
|
180
|
+
message
|
|
181
|
+
end
|
|
182
|
+
end.flatten
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def self.files(messages, original = nil, caller_lib_dir = Path.caller_lib_dir(nil, 'chats'))
|
|
186
|
+
messages.collect do |message|
|
|
187
|
+
if message[:role] == 'file' || message[:role] == 'directory'
|
|
188
|
+
file = message[:content].to_s.strip
|
|
189
|
+
found_file = find_file(file, original, caller_lib_dir)
|
|
190
|
+
raise "File not found: #{file}" if found_file.nil?
|
|
191
|
+
|
|
192
|
+
target = found_file
|
|
193
|
+
|
|
194
|
+
if message[:role] == 'directory'
|
|
195
|
+
Path.setup target
|
|
196
|
+
target.glob('**/*').
|
|
197
|
+
reject{|file|
|
|
198
|
+
Open.directory?(file)
|
|
199
|
+
}.collect{|file|
|
|
200
|
+
files([{role: 'file', content: file}])
|
|
201
|
+
}
|
|
202
|
+
else
|
|
203
|
+
new = LLM.tag :file, Open.read(target), file
|
|
204
|
+
{role: 'user', content: new}
|
|
205
|
+
end
|
|
206
|
+
elsif message[:role] == 'pdf' || message[:role] == 'image'
|
|
207
|
+
file = message[:content].to_s.strip
|
|
208
|
+
found_file = find_file(file, original, caller_lib_dir)
|
|
209
|
+
raise "File not found: #{file}" if found_file.nil?
|
|
210
|
+
|
|
211
|
+
message[:content] = found_file
|
|
212
|
+
message
|
|
213
|
+
else
|
|
214
|
+
message
|
|
215
|
+
end
|
|
216
|
+
end.flatten
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def self.tasks(messages, original = nil)
|
|
220
|
+
jobs = []
|
|
221
|
+
new = messages.collect do |message|
|
|
222
|
+
if message[:role] == 'task' || message[:role] == 'inline_task'
|
|
223
|
+
info = message[:content].strip
|
|
224
|
+
|
|
225
|
+
workflow, task = info.split(" ").values_at 0, 1
|
|
226
|
+
|
|
227
|
+
options = IndiferentHash.parse_options info
|
|
228
|
+
jobname = options.delete :jobname
|
|
229
|
+
|
|
230
|
+
if String === workflow
|
|
231
|
+
workflow = begin
|
|
232
|
+
Kernel.const_get workflow
|
|
233
|
+
rescue
|
|
234
|
+
Workflow.require_workflow(workflow)
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
job = workflow.job(task, jobname, options)
|
|
239
|
+
|
|
240
|
+
jobs << job
|
|
241
|
+
|
|
242
|
+
if message[:role] == 'inline_task'
|
|
243
|
+
{role: 'inline_job', content: job.path.find}
|
|
244
|
+
else
|
|
245
|
+
{role: 'job', content: job.path.find}
|
|
246
|
+
end
|
|
247
|
+
else
|
|
248
|
+
message
|
|
249
|
+
end
|
|
250
|
+
end.flatten
|
|
251
|
+
|
|
252
|
+
Workflow.produce(jobs)
|
|
253
|
+
|
|
254
|
+
new
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def self.jobs(messages, original = nil)
|
|
258
|
+
messages.collect do |message|
|
|
259
|
+
if message[:role] == 'job' || message[:role] == 'inline_job'
|
|
260
|
+
file = message[:content].strip
|
|
261
|
+
|
|
262
|
+
step = Step.load file
|
|
263
|
+
|
|
264
|
+
id = step.short_path[0..39]
|
|
265
|
+
id = id.gsub('/','-')
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
if message[:role] == 'inline_job'
|
|
269
|
+
path = step.path
|
|
270
|
+
path = path.find if Path === path
|
|
271
|
+
{role: 'file', content: step.path}
|
|
272
|
+
else
|
|
273
|
+
tool_call = {
|
|
274
|
+
type: "function",
|
|
275
|
+
function: {
|
|
276
|
+
name: step.full_task_name.sub('#', '-'),
|
|
277
|
+
arguments: step.provided_inputs.to_json
|
|
278
|
+
},
|
|
279
|
+
id: id,
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
tool_output = {
|
|
283
|
+
id: id,
|
|
284
|
+
role: "tool",
|
|
285
|
+
content: Open.read(step.path)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
[
|
|
289
|
+
{role: 'function_call', content: tool_call.to_json},
|
|
290
|
+
{role: 'function_call_output', content: tool_output.to_json},
|
|
291
|
+
]
|
|
292
|
+
end
|
|
293
|
+
else
|
|
294
|
+
message
|
|
295
|
+
end
|
|
296
|
+
end.flatten
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def self.clear(messages)
|
|
300
|
+
new = []
|
|
301
|
+
|
|
302
|
+
messages.reverse.each do |message|
|
|
303
|
+
if message[:role].to_s == 'clear'
|
|
304
|
+
break
|
|
305
|
+
elsif message[:role].to_s == 'previous_response_id'
|
|
306
|
+
new << message
|
|
307
|
+
break
|
|
308
|
+
else
|
|
309
|
+
new << message
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
new.reverse
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def self.clean(messages)
|
|
317
|
+
messages.reject do |message|
|
|
318
|
+
((String === message[:content]) && message[:content].empty?) ||
|
|
319
|
+
message[:role] == 'skip'
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def self.indiferent(messages)
|
|
324
|
+
messages.collect{|msg| IndiferentHash.setup msg }
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def self.chat(file, original = nil)
|
|
328
|
+
original ||= (String === file and Open.exists?(file)) ? file : Path.setup($0.dup)
|
|
329
|
+
caller_lib_dir = Path.caller_lib_dir(nil, 'chats')
|
|
330
|
+
|
|
331
|
+
if Array === file
|
|
332
|
+
messages = self.messages file
|
|
333
|
+
messages = self.indiferent messages
|
|
334
|
+
messages = self.imports messages, original, caller_lib_dir
|
|
335
|
+
elsif Open.exists?(file)
|
|
336
|
+
messages = self.messages Open.read(file)
|
|
337
|
+
messages = self.indiferent messages
|
|
338
|
+
messages = self.imports messages, original, caller_lib_dir
|
|
339
|
+
else
|
|
340
|
+
messages = self.messages file
|
|
341
|
+
messages = self.indiferent messages
|
|
342
|
+
messages = self.imports messages, original, caller_lib_dir
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
messages = self.clear messages
|
|
346
|
+
messages = self.clean messages
|
|
347
|
+
messages = self.tasks messages
|
|
348
|
+
messages = self.jobs messages
|
|
349
|
+
messages = self.files messages, original, caller_lib_dir
|
|
350
|
+
|
|
351
|
+
messages
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def self.options(chat)
|
|
355
|
+
options = IndiferentHash.setup({})
|
|
356
|
+
sticky_options = IndiferentHash.setup({})
|
|
357
|
+
new = []
|
|
358
|
+
|
|
359
|
+
# Most options reset after an assistant reply, but not previous_response_id
|
|
360
|
+
chat.each do |info|
|
|
361
|
+
if Hash === info
|
|
362
|
+
role = info[:role].to_s
|
|
363
|
+
if %w(endpoint model backend persist agent).include? role.to_s
|
|
364
|
+
options[role] = info[:content]
|
|
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
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
if role == 'assistant'
|
|
394
|
+
options.clear
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
new << info
|
|
398
|
+
end
|
|
399
|
+
chat.replace new
|
|
400
|
+
sticky_options.merge options
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def self.tools(messages)
|
|
404
|
+
tool_definitions = IndiferentHash.setup({})
|
|
405
|
+
new = messages.collect do |message|
|
|
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)
|
|
426
|
+
inputs = nil if inputs.empty?
|
|
427
|
+
inputs = [] if inputs == ['none'] || inputs == ['noinputs']
|
|
428
|
+
if Open.remote? workflow_name
|
|
429
|
+
require 'rbbt'
|
|
430
|
+
require 'scout/offsite/ssh'
|
|
431
|
+
require 'rbbt/workflow/remote_workflow'
|
|
432
|
+
workflow = RemoteWorkflow.new workflow_name
|
|
433
|
+
else
|
|
434
|
+
workflow = Workflow.require_workflow workflow_name
|
|
435
|
+
end
|
|
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
|
|
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 = {}
|
|
454
|
+
else
|
|
455
|
+
message
|
|
456
|
+
end
|
|
457
|
+
end.compact.flatten
|
|
458
|
+
messages.replace new
|
|
459
|
+
tool_definitions
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
def self.associations(messages, kb = nil)
|
|
463
|
+
tool_definitions = {}
|
|
464
|
+
new = messages.collect do |message|
|
|
465
|
+
if message[:role] == 'association'
|
|
466
|
+
name, path, *options = content_tokens(message)
|
|
467
|
+
|
|
468
|
+
kb ||= KnowledgeBase.new Scout.var.Agent.Chat.knowledge_base
|
|
469
|
+
kb.register name, Path.setup(path), IndiferentHash.parse_options(message[:content])
|
|
470
|
+
|
|
471
|
+
tool_definitions.merge!(LLM.knowledge_base_tool_definition( kb, [name]))
|
|
472
|
+
next
|
|
473
|
+
elsif message[:role] == 'clear_associations'
|
|
474
|
+
tool_definitions = {}
|
|
475
|
+
else
|
|
476
|
+
message
|
|
477
|
+
end
|
|
478
|
+
end.compact.flatten
|
|
479
|
+
messages.replace new
|
|
480
|
+
tool_definitions
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def self.print(chat)
|
|
484
|
+
return chat if String === chat
|
|
485
|
+
"\n" + chat.collect do |message|
|
|
486
|
+
IndiferentHash.setup message
|
|
487
|
+
case message[:content]
|
|
488
|
+
when Hash, Array
|
|
489
|
+
message[:role].to_s + ":\n\n" + message[:content].to_json
|
|
490
|
+
when nil, ''
|
|
491
|
+
message[:role].to_s + ":"
|
|
492
|
+
else
|
|
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
|
|
498
|
+
end
|
|
499
|
+
end * "\n\n"
|
|
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
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
module Chat
|
|
511
|
+
extend Annotation
|
|
512
|
+
|
|
513
|
+
def message(role, content)
|
|
514
|
+
self.append({role: role.to_s, content: content})
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
def user(content)
|
|
518
|
+
message(:user, content)
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def system(content)
|
|
522
|
+
message(:system, content)
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
def assistant(content)
|
|
526
|
+
message(:assistant, content)
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
def import(file)
|
|
530
|
+
message(:import, file)
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
def import_last(file)
|
|
534
|
+
message(:last, file)
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
def file(file)
|
|
538
|
+
message(:file, file)
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
def directory(directory)
|
|
542
|
+
message(:directory, directory)
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
def continue(file)
|
|
546
|
+
message(:continue, file)
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
def format(format)
|
|
550
|
+
message(:format, format)
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
def tool(*parts)
|
|
554
|
+
content = parts * "\n"
|
|
555
|
+
message(:tool, content)
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
def task(workflow, task_name, inputs = {})
|
|
559
|
+
input_str = IndiferentHash.print_options inputs
|
|
560
|
+
content = [workflow, task_name, input_str]*" "
|
|
561
|
+
message(:task, content)
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
def inline_task(workflow, task_name, inputs = {})
|
|
565
|
+
input_str = IndiferentHash.print_options inputs
|
|
566
|
+
content = [workflow, task_name, input_str]*" "
|
|
567
|
+
message(:inline_task, content)
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
def job(step)
|
|
571
|
+
message(:job, step.path)
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
def inline_job(step)
|
|
575
|
+
message(:inline_job, step.path)
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def association(name, path, options = {})
|
|
580
|
+
options_str = IndiferentHash.print_options options
|
|
581
|
+
content = [name, path, options_str]*" "
|
|
582
|
+
message(:association, name)
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
def tag(content, name=nil, tag=:file, role=:user)
|
|
586
|
+
self.message role, LLM.tag(tag, content, name)
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def ask(...)
|
|
591
|
+
LLM.ask(LLM.chat(self), ...)
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
def chat(...)
|
|
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
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
def json(...)
|
|
606
|
+
self.format :json
|
|
607
|
+
output = ask(...)
|
|
608
|
+
obj = JSON.parse output
|
|
609
|
+
if (Hash === obj) and obj.keys == ['content']
|
|
610
|
+
obj['content']
|
|
611
|
+
else
|
|
612
|
+
obj
|
|
613
|
+
end
|
|
614
|
+
end
|
|
615
|
+
|
|
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
|
|
624
|
+
end
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
def branch
|
|
628
|
+
self.annotate self.dup
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
def option(name, value)
|
|
632
|
+
self.message 'option', [name, value] * " "
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
def endpoint(value)
|
|
636
|
+
option :endpoint, value
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
def model(value)
|
|
640
|
+
option :model, value
|
|
641
|
+
end
|
|
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
|
+
|
|
661
|
+
def shed
|
|
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)
|
|
678
|
+
end
|
|
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
|
|
699
|
+
def create_image(file, ...)
|
|
700
|
+
base64_image = LLM.image(LLM.chat(self), ...)
|
|
701
|
+
Open.write(file, Base64.decode(file_content), mode: 'wb')
|
|
702
|
+
end
|
|
703
|
+
end
|
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}"
|