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.
- checksums.yaml +4 -4
- data/.vimproject +87 -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 +48 -1
- data/lib/scout/llm/agent/delegate.rb +51 -0
- data/lib/scout/llm/agent/iterate.rb +44 -0
- data/lib/scout/llm/agent.rb +43 -22
- data/lib/scout/llm/ask.rb +47 -7
- 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 +27 -30
- data/lib/scout/llm/backends/openai.rb +36 -41
- data/lib/scout/llm/backends/responses.rb +166 -113
- data/lib/scout/llm/chat.rb +270 -102
- 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 +76 -0
- data/lib/scout/llm/tools/knowledge_base.rb +159 -0
- data/lib/scout/llm/tools/mcp.rb +59 -0
- data/lib/scout/llm/tools/workflow.rb +106 -0
- data/lib/scout/llm/tools.rb +98 -141
- data/lib/scout-ai.rb +1 -0
- data/scout-ai.gemspec +31 -18
- data/scout_commands/agent/ask +59 -78
- data/scout_commands/documenter +148 -0
- data/scout_commands/llm/ask +3 -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_ollama.rb +1 -1
- 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 +1 -93
- 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
|
|
@@ -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
|
-
|
|
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] == '
|
|
232
|
-
|
|
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.
|
|
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
|
-
|
|
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:
|
|
264
|
-
arguments: step.provided_inputs
|
|
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
|
-
|
|
271
|
-
|
|
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
|
|
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
|
|
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] == '
|
|
362
|
-
|
|
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
|
-
|
|
374
|
-
|
|
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
|
|
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
|
-
|
|
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 + "
|
|
502
|
+
when nil, ''
|
|
503
|
+
message[:role].to_s + ":"
|
|
414
504
|
else
|
|
415
|
-
|
|
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
|
-
|
|
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
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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 [
|
|
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')
|
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