scout-ai 1.0.1 → 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 +8 -1
- data/VERSION +1 -1
- data/lib/scout/llm/agent/chat.rb +2 -5
- data/lib/scout/llm/agent/delegate.rb +12 -0
- data/lib/scout/llm/agent.rb +2 -2
- data/lib/scout/llm/ask.rb +9 -1
- data/lib/scout/llm/backends/ollama.rb +6 -3
- data/lib/scout/llm/backends/openai.rb +3 -2
- data/lib/scout/llm/backends/responses.rb +9 -4
- data/lib/scout/llm/chat.rb +22 -10
- data/lib/scout/llm/tools/call.rb +11 -1
- data/lib/scout/llm/tools/knowledge_base.rb +15 -14
- data/lib/scout/llm/tools/workflow.rb +52 -15
- data/lib/scout/llm/tools.rb +42 -0
- data/scout-ai.gemspec +2 -2
- data/scout_commands/agent/ask +36 -12
- data/scout_commands/llm/ask +1 -0
- data/test/scout/llm/backends/test_ollama.rb +1 -1
- data/test/scout/llm/test_agent.rb +1 -23
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cc083461a140b5149d6965f5aa635e651015786cfe312f220a33967fa10c52cb
|
|
4
|
+
data.tar.gz: 694ac0ff0b626d99ee993c77a1dba12022f9e7a545bbab67b94627a4698ade09
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ef373e303af56478ecde91faf87093226936e3537b9cb3346192e230af43cc8350091e589080b7c4e32993bf0cbcc43ef380582a4d38b82ca5da33bc60754dfd
|
|
7
|
+
data.tar.gz: cccb6e93d1b827a02f5b9159a11eb7839c11c48b0cdd5a0096644b19479e0609d9d641efff286a545625fc17863e2f106faa2ea775e7cde121f313260fdef779
|
data/.vimproject
CHANGED
|
@@ -6,6 +6,13 @@ scout-ai=$PWD filter="*.rb *.rake Rakefile *.rdoc *.R *.sh *.js *.haml *.sass *.
|
|
|
6
6
|
scout-ai
|
|
7
7
|
}
|
|
8
8
|
chats=chats filter="*"{
|
|
9
|
+
|
|
10
|
+
test_tool
|
|
11
|
+
|
|
12
|
+
ask_agent
|
|
13
|
+
|
|
14
|
+
test_ollama_tool
|
|
15
|
+
|
|
9
16
|
test_github
|
|
10
17
|
|
|
11
18
|
test_stdio
|
|
@@ -103,8 +110,8 @@ scout-ai=$PWD filter="*.rb *.rake Rakefile *.rdoc *.R *.sh *.js *.haml *.sass *.
|
|
|
103
110
|
|
|
104
111
|
backends=backends{
|
|
105
112
|
openai.rb
|
|
106
|
-
anthropic.rb
|
|
107
113
|
responses.rb
|
|
114
|
+
anthropic.rb
|
|
108
115
|
ollama.rb
|
|
109
116
|
bedrock.rb
|
|
110
117
|
openwebui.rb
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0
|
|
1
|
+
1.1.0
|
data/lib/scout/llm/agent/chat.rb
CHANGED
|
@@ -9,6 +9,8 @@ module LLM
|
|
|
9
9
|
(@current_chat || start_chat).annotate chat unless Chat === chat
|
|
10
10
|
@current_chat = chat
|
|
11
11
|
else
|
|
12
|
+
start_chat = self.start_chat
|
|
13
|
+
Chat.setup(start_chat) unless Chat === start_chat
|
|
12
14
|
@current_chat = start_chat.branch
|
|
13
15
|
end
|
|
14
16
|
end
|
|
@@ -25,11 +27,6 @@ module LLM
|
|
|
25
27
|
self.ask(current_chat, ...)
|
|
26
28
|
end
|
|
27
29
|
|
|
28
|
-
def chat(model = nil, options = {})
|
|
29
|
-
new = self.ask(current_chat, model, options.merge(return_messages: true))
|
|
30
|
-
current_chat.concat(new)
|
|
31
|
-
new.last['content']
|
|
32
|
-
end
|
|
33
30
|
|
|
34
31
|
def chat(model = nil, options = {})
|
|
35
32
|
response = ask(current_chat, model, options.merge(return_messages: true))
|
|
@@ -7,6 +7,13 @@ module LLM
|
|
|
7
7
|
|
|
8
8
|
block ||= Proc.new do |name, parameters|
|
|
9
9
|
message = parameters[:message]
|
|
10
|
+
new_conversation = parameters[:new_conversation]
|
|
11
|
+
Log.medium "Delegated to #{agent}: " + Log.fingerprint(message)
|
|
12
|
+
if new_conversation
|
|
13
|
+
agent.start
|
|
14
|
+
else
|
|
15
|
+
agent.purge
|
|
16
|
+
end
|
|
10
17
|
agent.user message
|
|
11
18
|
agent.chat
|
|
12
19
|
end
|
|
@@ -15,6 +22,11 @@ module LLM
|
|
|
15
22
|
message: {
|
|
16
23
|
"type": :string,
|
|
17
24
|
"description": "Message to pass to the agent"
|
|
25
|
+
},
|
|
26
|
+
new_conversation: {
|
|
27
|
+
"type": :boolean,
|
|
28
|
+
"description": "Erase conversation history and start a new conversation with this message",
|
|
29
|
+
"default": false
|
|
18
30
|
}
|
|
19
31
|
}
|
|
20
32
|
|
data/lib/scout/llm/agent.rb
CHANGED
|
@@ -49,7 +49,7 @@ You have access to the following databases associating entities:
|
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
# function: takes an array of messages and calls LLM.ask with them
|
|
52
|
-
def ask(messages,
|
|
52
|
+
def ask(messages, options = {})
|
|
53
53
|
messages = [messages] unless messages.is_a? Array
|
|
54
54
|
model ||= @model if model
|
|
55
55
|
|
|
@@ -87,7 +87,7 @@ You have access to the following databases associating entities:
|
|
|
87
87
|
|
|
88
88
|
workflow = Workflow.require_workflow workflow_path if workflow_path.exists?
|
|
89
89
|
knowledge_base = KnowledgeBase.new knowledge_base_path if knowledge_base_path.exists?
|
|
90
|
-
chat = LLM.chat
|
|
90
|
+
chat = Chat.setup LLM.chat(chat_path.find) if chat_path.exists?
|
|
91
91
|
|
|
92
92
|
LLM::Agent.new workflow: workflow, knowledge_base: knowledge_base, start_chat: chat
|
|
93
93
|
end
|
data/lib/scout/llm/ask.rb
CHANGED
|
@@ -41,7 +41,11 @@ module LLM
|
|
|
41
41
|
raise "Endpoint not found #{endpoint}"
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
Log.high Log.color :green, "Asking #{endpoint || 'client'}:\n" + LLM.print(messages)
|
|
45
|
+
tools = options[:tools]
|
|
46
|
+
Log.high "Tools: #{Log.fingerprint tools.keys}}" if tools
|
|
47
|
+
|
|
48
|
+
res = Persist.persist(endpoint, :json, prefix: "LLM ask", other: options.merge(messages: messages), persist: persist) do
|
|
45
49
|
backend = IndiferentHash.process_options options, :backend
|
|
46
50
|
backend ||= Scout::Config.get :backend, :ask, :llm, env: 'ASK_BACKEND,LLM_BACKEND', default: :openai
|
|
47
51
|
|
|
@@ -71,6 +75,10 @@ module LLM
|
|
|
71
75
|
raise "Unknown backend: #{backend}"
|
|
72
76
|
end
|
|
73
77
|
end
|
|
78
|
+
|
|
79
|
+
Log.high Log.color :blue, "Response:\n" + LLM.print(res)
|
|
80
|
+
|
|
81
|
+
res
|
|
74
82
|
end
|
|
75
83
|
|
|
76
84
|
def self.workflow_ask(workflow, question, options = {})
|
|
@@ -81,10 +81,11 @@ module LLM
|
|
|
81
81
|
tools.merge!(LLM.associations messages)
|
|
82
82
|
|
|
83
83
|
if tools.any?
|
|
84
|
-
parameters[:tools] =
|
|
84
|
+
parameters[:tools] = LLM.tool_definitions_to_ollama tools
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
-
Log.low "Calling
|
|
87
|
+
Log.low "Calling ollama #{url}: #{Log.fingerprint(parameters.except(:tools))}}"
|
|
88
|
+
Log.medium "Tools: #{Log.fingerprint tools.keys}}" if tools
|
|
88
89
|
|
|
89
90
|
parameters[:messages] = LLM.tools_to_ollama messages
|
|
90
91
|
|
|
@@ -93,7 +94,9 @@ module LLM
|
|
|
93
94
|
response = self.process_response client.chat(parameters), tools, &block
|
|
94
95
|
|
|
95
96
|
res = if response.last[:role] == 'function_call_output'
|
|
96
|
-
response + self.ask(messages + response, original_options.except(:tool_choice).merge(return_messages: true, tools: tools), &block)
|
|
97
|
+
#response + self.ask(messages + response, original_options.except(:tool_choice).merge(return_messages: true, tools: tools), &block)
|
|
98
|
+
# This version seems to keep the original message from getting forgotten
|
|
99
|
+
response + self.ask(response + messages, original_options.except(:tool_choice).merge(return_messages: true, tools: tools), &block)
|
|
97
100
|
else
|
|
98
101
|
response
|
|
99
102
|
end
|
|
@@ -84,12 +84,13 @@ module LLM
|
|
|
84
84
|
tools.merge!(LLM.associations messages)
|
|
85
85
|
|
|
86
86
|
if tools.any?
|
|
87
|
-
parameters[:tools] =
|
|
87
|
+
parameters[:tools] = LLM.tool_definitions_to_openai tools
|
|
88
88
|
end
|
|
89
89
|
|
|
90
90
|
messages = self.process_input messages
|
|
91
91
|
|
|
92
|
-
Log.
|
|
92
|
+
Log.debug "Calling openai #{url}: #{Log.fingerprint(parameters.except(:tools))}}"
|
|
93
|
+
Log.high "Tools: #{Log.fingerprint tools.keys}}" if tools
|
|
93
94
|
|
|
94
95
|
parameters[:messages] = LLM.tools_to_openai messages
|
|
95
96
|
|
|
@@ -45,13 +45,15 @@ module LLM
|
|
|
45
45
|
#end
|
|
46
46
|
|
|
47
47
|
def self.tools_to_responses(messages)
|
|
48
|
+
last_id = nil
|
|
48
49
|
messages.collect do |message|
|
|
49
50
|
if message[:role] == 'function_call'
|
|
50
51
|
info = JSON.parse(message[:content])
|
|
51
52
|
IndiferentHash.setup info
|
|
52
53
|
name = info[:name] || IndiferentHash.dig(info,:function, :name)
|
|
53
54
|
IndiferentHash.setup info
|
|
54
|
-
id = info[:id]
|
|
55
|
+
id = last_id = info[:id] || "fc_#{rand(1000).to_s}"
|
|
56
|
+
id = id.sub(/^fc_/, '')
|
|
55
57
|
IndiferentHash.setup({
|
|
56
58
|
"type" => "function_call",
|
|
57
59
|
"status" => "completed",
|
|
@@ -62,7 +64,8 @@ module LLM
|
|
|
62
64
|
elsif message[:role] == 'function_call_output'
|
|
63
65
|
info = JSON.parse(message[:content])
|
|
64
66
|
IndiferentHash.setup info
|
|
65
|
-
id = info[:id]
|
|
67
|
+
id = info[:id] || last_id
|
|
68
|
+
id = id.sub(/^fc_/, '')
|
|
66
69
|
{ # append result message
|
|
67
70
|
"type" => "function_call_output",
|
|
68
71
|
"output" => info[:content],
|
|
@@ -232,11 +235,13 @@ module LLM
|
|
|
232
235
|
tools.merge!(LLM.associations messages)
|
|
233
236
|
|
|
234
237
|
if tools.any?
|
|
235
|
-
parameters[:tools] =
|
|
238
|
+
parameters[:tools] = LLM.tool_definitions_to_reponses tools
|
|
236
239
|
end
|
|
237
240
|
|
|
238
241
|
parameters['previous_response_id'] = previous_response_id if String === previous_response_id
|
|
239
|
-
|
|
242
|
+
|
|
243
|
+
Log.low "Calling responses #{url}: #{Log.fingerprint(parameters.except(:tools))}}"
|
|
244
|
+
Log.medium "Tools: #{Log.fingerprint tools.keys}}" if tools
|
|
240
245
|
|
|
241
246
|
messages = self.process_input messages
|
|
242
247
|
input = []
|
data/lib/scout/llm/chat.rb
CHANGED
|
@@ -219,7 +219,7 @@ module LLM
|
|
|
219
219
|
def self.tasks(messages, original = nil)
|
|
220
220
|
jobs = []
|
|
221
221
|
new = messages.collect do |message|
|
|
222
|
-
if message[:role] == 'task' || message[:role] == 'inline_task'
|
|
222
|
+
if message[:role] == 'task' || message[:role] == 'inline_task' || message[:role] == 'exec_task'
|
|
223
223
|
info = message[:content].strip
|
|
224
224
|
|
|
225
225
|
workflow, task = info.split(" ").values_at 0, 1
|
|
@@ -237,9 +237,15 @@ module LLM
|
|
|
237
237
|
|
|
238
238
|
job = workflow.job(task, jobname, options)
|
|
239
239
|
|
|
240
|
-
jobs << job
|
|
240
|
+
jobs << job unless message[:role] == 'exec_task'
|
|
241
241
|
|
|
242
|
-
if message[:role] == '
|
|
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'
|
|
243
249
|
{role: 'inline_job', content: job.path.find}
|
|
244
250
|
else
|
|
245
251
|
{role: 'job', content: job.path.find}
|
|
@@ -264,25 +270,31 @@ module LLM
|
|
|
264
270
|
id = step.short_path[0..39]
|
|
265
271
|
id = id.gsub('/','-')
|
|
266
272
|
|
|
267
|
-
|
|
268
273
|
if message[:role] == 'inline_job'
|
|
269
274
|
path = step.path
|
|
270
275
|
path = path.find if Path === path
|
|
271
276
|
{role: 'file', content: step.path}
|
|
272
277
|
else
|
|
278
|
+
|
|
279
|
+
function_name = step.full_task_name.sub('#', '-')
|
|
280
|
+
function_name = step.task_name
|
|
273
281
|
tool_call = {
|
|
274
|
-
type: "function",
|
|
275
282
|
function: {
|
|
276
|
-
name:
|
|
277
|
-
arguments: step.provided_inputs
|
|
283
|
+
name: function_name,
|
|
284
|
+
arguments: step.provided_inputs
|
|
278
285
|
},
|
|
279
286
|
id: id,
|
|
280
287
|
}
|
|
281
288
|
|
|
289
|
+
content = if step.done?
|
|
290
|
+
Open.read(step.path)
|
|
291
|
+
elsif step.error?
|
|
292
|
+
step.exception
|
|
293
|
+
end
|
|
294
|
+
|
|
282
295
|
tool_output = {
|
|
283
296
|
id: id,
|
|
284
|
-
|
|
285
|
-
content: Open.read(step.path)
|
|
297
|
+
content: content
|
|
286
298
|
}
|
|
287
299
|
|
|
288
300
|
[
|
|
@@ -490,7 +502,7 @@ module LLM
|
|
|
490
502
|
when nil, ''
|
|
491
503
|
message[:role].to_s + ":"
|
|
492
504
|
else
|
|
493
|
-
if %w(option previous_response_id).include? message[:role].to_s
|
|
505
|
+
if %w(option previous_response_id function_call function_call_output).include? message[:role].to_s
|
|
494
506
|
message[:role].to_s + ": " + message[:content].to_s
|
|
495
507
|
else
|
|
496
508
|
message[:role].to_s + ":\n\n" + message[:content].to_s
|
data/lib/scout/llm/tools/call.rb
CHANGED
|
@@ -19,8 +19,16 @@ module LLM
|
|
|
19
19
|
calls.collect do |tool_call|
|
|
20
20
|
tool_call_id, function_name, function_arguments = call_id_name_and_arguments(tool_call)
|
|
21
21
|
|
|
22
|
+
function_arguments = IndiferentHash.setup function_arguments
|
|
23
|
+
|
|
22
24
|
obj, definition = tools[function_name]
|
|
23
25
|
|
|
26
|
+
definition = obj if Hash === obj
|
|
27
|
+
|
|
28
|
+
defaults = definition[:parameters][:defaults] if definition && definition[:parameters]
|
|
29
|
+
function_arguments = function_arguments.merge(defaults) if defaults
|
|
30
|
+
|
|
31
|
+
Log.high "Calling #{function_name} (#{Log.fingerprint function_arguments}): "
|
|
24
32
|
function_response = case obj
|
|
25
33
|
when Proc
|
|
26
34
|
obj.call function_name, function_arguments
|
|
@@ -32,7 +40,7 @@ module LLM
|
|
|
32
40
|
if block_given?
|
|
33
41
|
block.call function_name, function_arguments
|
|
34
42
|
else
|
|
35
|
-
|
|
43
|
+
ParameterException.new "Tool or function not found '#{function_name}'. Called with parameters #{Log.fingerprint function_arguments}" if obj.nil? && definition.nil?
|
|
36
44
|
end
|
|
37
45
|
end
|
|
38
46
|
|
|
@@ -48,6 +56,8 @@ module LLM
|
|
|
48
56
|
end
|
|
49
57
|
content = content.to_s if Numeric === content
|
|
50
58
|
|
|
59
|
+
Log.high "Called #{function_name}: " + Log.fingerprint(content)
|
|
60
|
+
|
|
51
61
|
response_message = {
|
|
52
62
|
id: tool_call_id,
|
|
53
63
|
role: "tool",
|
|
@@ -68,9 +68,9 @@ Returns a list in the format source~target.
|
|
|
68
68
|
description: "Associations in the form of source~target or target~source"
|
|
69
69
|
},
|
|
70
70
|
fields: {
|
|
71
|
-
type: "
|
|
72
|
-
|
|
73
|
-
description: "Limit the response to these
|
|
71
|
+
type: "array",
|
|
72
|
+
items: { type: :string },
|
|
73
|
+
description: "Limit the response to these fields"
|
|
74
74
|
},
|
|
75
75
|
}
|
|
76
76
|
else
|
|
@@ -93,22 +93,22 @@ Multiple values may be present and use the charater ';' to separate them.
|
|
|
93
93
|
else
|
|
94
94
|
properties.delete(:fields)
|
|
95
95
|
description = <<-EOF
|
|
96
|
-
Return the #{
|
|
96
|
+
Return the #{fields.first} of association.
|
|
97
97
|
Multiple values may be present and use the charater ';' to separate them.
|
|
98
98
|
EOF
|
|
99
99
|
end
|
|
100
100
|
|
|
101
101
|
function = {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
102
|
+
name: database.to_s + '_association_details',
|
|
103
|
+
description: description,
|
|
104
|
+
parameters: {
|
|
105
|
+
type: "object",
|
|
106
|
+
properties: properties,
|
|
107
|
+
required: ['associations']
|
|
108
|
+
}
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
IndiferentHash.setup function
|
|
111
|
+
IndiferentHash.setup function
|
|
112
112
|
end
|
|
113
113
|
|
|
114
114
|
|
|
@@ -119,11 +119,12 @@ Multiple values may be present and use the charater ';' to separate them.
|
|
|
119
119
|
database_description = knowledge_base.description(database)
|
|
120
120
|
undirected = knowledge_base.undirected(database)
|
|
121
121
|
definition = self.database_tool_definition(database, undirected, database_description)
|
|
122
|
-
tool_definitions.merge(database => [knowledge_base, definition])
|
|
122
|
+
tool_definitions.merge!(database => [knowledge_base, definition])
|
|
123
123
|
if (fields = knowledge_base.get_database(database).fields).any?
|
|
124
124
|
details_definition = self.database_details_tool_definition(database, undirected, fields)
|
|
125
|
-
tool_definitions.merge(database + '_association_details' => [knowledge_base, details_definition])
|
|
125
|
+
tool_definitions.merge!(database.to_s + '_association_details' => [knowledge_base, details_definition])
|
|
126
126
|
end
|
|
127
|
+
tool_definitions
|
|
127
128
|
}
|
|
128
129
|
end
|
|
129
130
|
|
|
@@ -2,11 +2,25 @@ require 'scout/workflow'
|
|
|
2
2
|
module LLM
|
|
3
3
|
def self.task_tool_definition(workflow, task_name, inputs = nil)
|
|
4
4
|
task_info = workflow.task_info(task_name)
|
|
5
|
+
return nil if task_info.nil?
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
if inputs
|
|
8
|
+
names = []
|
|
9
|
+
defaults = {}
|
|
10
|
+
|
|
11
|
+
inputs.each do |i|
|
|
12
|
+
if String === i && i.include?('=')
|
|
13
|
+
name,_ , value = i.partition("=")
|
|
14
|
+
defaults[name] = value
|
|
15
|
+
else
|
|
16
|
+
names << i.to_sym
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
end
|
|
7
21
|
|
|
8
22
|
properties = task_info[:inputs].inject({}) do |acc,input|
|
|
9
|
-
next acc if
|
|
23
|
+
next acc if names and not names.include?(input)
|
|
10
24
|
type = task_info[:input_types][input]
|
|
11
25
|
description = task_info[:input_descriptions][input]
|
|
12
26
|
|
|
@@ -14,12 +28,17 @@ module LLM
|
|
|
14
28
|
type = :string if type == :select
|
|
15
29
|
type = :string if type == :path
|
|
16
30
|
type = :number if type == :float
|
|
31
|
+
type = :array if type.to_s.end_with?('_array')
|
|
17
32
|
|
|
18
33
|
acc[input] = {
|
|
19
34
|
"type": type,
|
|
20
35
|
"description": description
|
|
21
36
|
}
|
|
22
37
|
|
|
38
|
+
if type == :array
|
|
39
|
+
acc[input]['items'] = {type: :string}
|
|
40
|
+
end
|
|
41
|
+
|
|
23
42
|
if input_options = task_info[:input_options][input]
|
|
24
43
|
if select_options = input_options[:select_options]
|
|
25
44
|
select_options = select_options.values if Hash === select_options
|
|
@@ -31,7 +50,7 @@ module LLM
|
|
|
31
50
|
end
|
|
32
51
|
|
|
33
52
|
required_inputs = task_info[:inputs].select do |input|
|
|
34
|
-
next if
|
|
53
|
+
next if names and not names.include?(input.to_sym)
|
|
35
54
|
task_info[:input_options].include?(input) && task_info[:input_options][input][:required]
|
|
36
55
|
end
|
|
37
56
|
|
|
@@ -41,29 +60,47 @@ module LLM
|
|
|
41
60
|
parameters: {
|
|
42
61
|
type: "object",
|
|
43
62
|
properties: properties,
|
|
44
|
-
required: required_inputs
|
|
63
|
+
required: required_inputs,
|
|
45
64
|
}
|
|
46
65
|
}
|
|
47
66
|
|
|
48
|
-
|
|
67
|
+
function[:parameters][:defaults] = defaults if defaults
|
|
68
|
+
|
|
69
|
+
#IndiferentHash.setup function.merge(type: 'function', function: function)
|
|
70
|
+
IndiferentHash.setup function
|
|
49
71
|
end
|
|
50
72
|
|
|
51
73
|
def self.workflow_tools(workflow, tasks = nil)
|
|
52
|
-
|
|
53
|
-
|
|
74
|
+
if Array === workflow
|
|
75
|
+
workflow.inject({}){|tool_definitions,wf| tool_definitions.merge(workflow_tools(wf, tasks)) }
|
|
54
76
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
77
|
+
else
|
|
78
|
+
tasks = workflow.all_exports if tasks.nil?
|
|
79
|
+
tasks = workflow.all_tasks if tasks.empty?
|
|
80
|
+
|
|
81
|
+
tasks.inject({}){|tool_definitions,task_name|
|
|
82
|
+
definition = self.task_tool_definition(workflow, task_name)
|
|
83
|
+
next if definition.nil?
|
|
84
|
+
tool_definitions.merge(task_name => [workflow, definition])
|
|
85
|
+
}
|
|
86
|
+
end
|
|
59
87
|
end
|
|
60
88
|
|
|
61
89
|
def self.call_workflow(workflow, task_name, parameters={})
|
|
62
90
|
jobname = parameters.delete :jobname
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
workflow.
|
|
91
|
+
begin
|
|
92
|
+
exec_type = parameters[:exec_type]
|
|
93
|
+
job = workflow.job(task_name, jobname, parameters)
|
|
94
|
+
if workflow.exec_exports.include?(task_name.to_sym) || parameters[:exec_type].to_s == 'exec'
|
|
95
|
+
job.exec
|
|
96
|
+
else
|
|
97
|
+
raise ScoutException, 'Potential recursive call' if parameters[:allow_recursive] != 'true' &&
|
|
98
|
+
(job.running? and job.info[:pid] == Process.pid)
|
|
99
|
+
|
|
100
|
+
job.run
|
|
101
|
+
end
|
|
102
|
+
rescue ScoutException
|
|
103
|
+
return $!
|
|
67
104
|
end
|
|
68
105
|
end
|
|
69
106
|
end
|
data/lib/scout/llm/tools.rb
CHANGED
|
@@ -137,4 +137,46 @@ module LLM
|
|
|
137
137
|
end
|
|
138
138
|
end.flatten
|
|
139
139
|
end
|
|
140
|
+
|
|
141
|
+
def self.tool_definitions_to_reponses(tools)
|
|
142
|
+
tools.values.collect do |obj,definition|
|
|
143
|
+
definition = obj if Hash === obj
|
|
144
|
+
definition
|
|
145
|
+
|
|
146
|
+
definition = case definition[:function]
|
|
147
|
+
when Hash
|
|
148
|
+
definition.merge(definition.delete :function)
|
|
149
|
+
else
|
|
150
|
+
definition
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
definition = IndiferentHash.add_defaults definition, type: :function
|
|
154
|
+
|
|
155
|
+
definition[:parameters].delete :defaults if definition[:parameters]
|
|
156
|
+
|
|
157
|
+
definition
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
class << self
|
|
162
|
+
alias tool_definitions_to_openai tool_definitions_to_reponses
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def self.tool_definitions_to_ollama(tools)
|
|
166
|
+
tools.values.collect do |obj,definition|
|
|
167
|
+
definition = obj if Hash === obj
|
|
168
|
+
definition = IndiferentHash.setup definition
|
|
169
|
+
|
|
170
|
+
definition = case definition[:function]
|
|
171
|
+
when Hash
|
|
172
|
+
definition
|
|
173
|
+
else
|
|
174
|
+
{type: :function, function: definition}
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
definition = IndiferentHash.add_defaults definition, type: :function
|
|
178
|
+
|
|
179
|
+
definition
|
|
180
|
+
end
|
|
181
|
+
end
|
|
140
182
|
end
|
data/scout-ai.gemspec
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
|
3
3
|
# Instead, edit Juwelier::Tasks in Rakefile, and run 'rake gemspec'
|
|
4
4
|
# -*- encoding: utf-8 -*-
|
|
5
|
-
# stub: scout-ai 1.0
|
|
5
|
+
# stub: scout-ai 1.1.0 ruby lib
|
|
6
6
|
|
|
7
7
|
Gem::Specification.new do |s|
|
|
8
8
|
s.name = "scout-ai".freeze
|
|
9
|
-
s.version = "1.0
|
|
9
|
+
s.version = "1.1.0".freeze
|
|
10
10
|
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
|
|
12
12
|
s.require_paths = ["lib".freeze]
|
data/scout_commands/agent/ask
CHANGED
|
@@ -41,13 +41,12 @@ agent_name, *question_parts = ARGV
|
|
|
41
41
|
|
|
42
42
|
question = question_parts * " "
|
|
43
43
|
|
|
44
|
-
file, chat, inline, template, dry_run, imports = IndiferentHash.process_options options, :file, :chat, :inline, :template, :dry_run, :imports
|
|
44
|
+
file, chat, inline, template, dry_run, imports, endpoint, model = IndiferentHash.process_options options, :file, :chat, :inline, :template, :dry_run, :imports, :endpoint, :model
|
|
45
45
|
|
|
46
46
|
file = Path.setup(file) if file
|
|
47
47
|
|
|
48
48
|
imports = imports.split(/,\s*/) if imports
|
|
49
49
|
|
|
50
|
-
|
|
51
50
|
agent_name ||= 'default'
|
|
52
51
|
|
|
53
52
|
agent_file = Scout.workflows[agent_name]
|
|
@@ -71,6 +70,9 @@ else
|
|
|
71
70
|
#raise ParameterException agent_file
|
|
72
71
|
end
|
|
73
72
|
|
|
73
|
+
agent.other_options[:endpoint] = endpoint if endpoint
|
|
74
|
+
agent.other_options[:model] = model if model
|
|
75
|
+
|
|
74
76
|
if template
|
|
75
77
|
if Open.exists?(template)
|
|
76
78
|
template_question = Open.read(template)
|
|
@@ -98,24 +100,43 @@ elsif file
|
|
|
98
100
|
end
|
|
99
101
|
|
|
100
102
|
if chat
|
|
101
|
-
conversation = Open.exist?(chat)? LLM.chat(chat) : []
|
|
103
|
+
#conversation = Open.exist?(chat)? LLM.chat(chat) : []
|
|
104
|
+
#convo_options = LLM.options conversation
|
|
105
|
+
#conversation = question.empty? ? conversation : conversation + LLM.chat(question)
|
|
106
|
+
|
|
107
|
+
#if dry_run
|
|
108
|
+
# ppp LLM.print conversation
|
|
109
|
+
# exit 0
|
|
110
|
+
#end
|
|
111
|
+
|
|
112
|
+
#new = agent.ask(conversation, convo_options.merge(options.merge(return_messages: true)))
|
|
113
|
+
#conversation = Open.read(chat) + LLM.print(new)
|
|
114
|
+
#Open.write(chat, conversation)
|
|
115
|
+
|
|
116
|
+
conversation = Open.exist?(chat)? LLM.chat(chat) : []
|
|
102
117
|
convo_options = LLM.options conversation
|
|
103
|
-
|
|
118
|
+
agent.start(Chat.setup(conversation)) if conversation.any?
|
|
119
|
+
agent.current_chat.concat LLM.chat(question) if question && ! question.empty?
|
|
120
|
+
imports.each{|import| agent.import import } if imports
|
|
104
121
|
|
|
105
122
|
if dry_run
|
|
106
123
|
ppp LLM.print conversation
|
|
107
124
|
exit 0
|
|
108
125
|
end
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
126
|
+
|
|
127
|
+
new = agent.ask(agent.current_chat, convo_options.merge(options.merge(return_messages: true)))
|
|
128
|
+
conversation += LLM.chat(question) if question
|
|
129
|
+
conversation += new
|
|
130
|
+
|
|
131
|
+
Open.open(chat, mode: 'a'){|f| f.puts LLM.print(new) }
|
|
132
|
+
puts LLM.purge(new).last[:content]
|
|
112
133
|
elsif inline
|
|
113
134
|
|
|
114
135
|
file = Open.read inline
|
|
115
136
|
|
|
116
137
|
new_file = ""
|
|
117
138
|
while true
|
|
118
|
-
pre, question, post =
|
|
139
|
+
pre, question, post =
|
|
119
140
|
file.partition(/^\s*#\s*ask:(?:.*?)(?=^\s*[^\s#])/smu)
|
|
120
141
|
|
|
121
142
|
break if post.empty?
|
|
@@ -124,7 +145,7 @@ elsif inline
|
|
|
124
145
|
new_file << question
|
|
125
146
|
clean_question = question.gsub('#', '').gsub(/\s+/,' ').sub(/.*ask:\s*/,'').strip
|
|
126
147
|
chat = [
|
|
127
|
-
{role: :system, content: "Write a succint reply with no commentary and no formatting."},
|
|
148
|
+
{role: :system, content: "Write a succint reply with no commentary and no formatting."},
|
|
128
149
|
{role: :user, content: "Find the following question as a comment in the file give a response to be placed inline: #{question}"},
|
|
129
150
|
LLM.tag('file', file, inline)
|
|
130
151
|
]
|
|
@@ -139,7 +160,10 @@ elsif inline
|
|
|
139
160
|
new_file << file
|
|
140
161
|
Open.write(inline, new_file)
|
|
141
162
|
else
|
|
142
|
-
conversation = Chat.setup(LLM.chat question)
|
|
143
|
-
imports.each{|import| conversation.import import } if imports
|
|
144
|
-
puts agent.ask(conversation, nil, options)
|
|
163
|
+
#conversation = Chat.setup(LLM.chat question)
|
|
164
|
+
#imports.each{|import| conversation.import import } if imports
|
|
165
|
+
#puts agent.ask(conversation, nil, options)
|
|
166
|
+
agent.current_chat.concat LLM.chat(question)
|
|
167
|
+
imports.each{|import| agent.import import } if imports
|
|
168
|
+
puts agent.chat
|
|
145
169
|
end
|
data/scout_commands/llm/ask
CHANGED
|
@@ -82,7 +82,7 @@ What is the weather in London. Should I take an umbrella?
|
|
|
82
82
|
]
|
|
83
83
|
|
|
84
84
|
sss 0
|
|
85
|
-
respose = LLM::OLlama.ask prompt, model: '
|
|
85
|
+
respose = LLM::OLlama.ask prompt, model: 'gpt-oss', url: 'http://localhost:3330', tool_choice: 'required', tools: tools do |name,arguments|
|
|
86
86
|
"It's raining cats and dogs"
|
|
87
87
|
end
|
|
88
88
|
|
|
@@ -3,7 +3,7 @@ require File.expand_path(__FILE__).sub(%r(.*/test/), '').sub(/test_(.*)\.rb/,'\1
|
|
|
3
3
|
|
|
4
4
|
require 'rbbt-util'
|
|
5
5
|
class TestLLMAgent < Test::Unit::TestCase
|
|
6
|
-
def
|
|
6
|
+
def test_system
|
|
7
7
|
TmpFile.with_dir do |dir|
|
|
8
8
|
kb = KnowledgeBase.new dir
|
|
9
9
|
kb.format = {"Person" => "Alias"}
|
|
@@ -13,32 +13,10 @@ class TestLLMAgent < Test::Unit::TestCase
|
|
|
13
13
|
|
|
14
14
|
agent = LLM::Agent.new knowledge_base: kb
|
|
15
15
|
|
|
16
|
-
agent.system = ""
|
|
17
|
-
|
|
18
16
|
sss 0
|
|
19
17
|
ppp agent.ask "Who is Miguel's brother-in-law. Brother in law is your spouses sibling or your sibling's spouse"
|
|
20
18
|
#ppp agent.ask "Who is Guille's brother-in-law. Brother in law is your spouses sibling or your sibling's spouse"
|
|
21
19
|
end
|
|
22
20
|
end
|
|
23
|
-
|
|
24
|
-
def _test_system_gepeto
|
|
25
|
-
TmpFile.with_dir do |dir|
|
|
26
|
-
Scout::Config.set(:backend, :ollama, :ask)
|
|
27
|
-
kb = KnowledgeBase.new dir
|
|
28
|
-
kb.format = {"Person" => "Alias"}
|
|
29
|
-
kb.register :brothers, datafile_test(:person).brothers, undirected: true
|
|
30
|
-
kb.register :marriages, datafile_test(:person).marriages, undirected: true, source: "=>Alias", target: "=>Alias"
|
|
31
|
-
kb.register :parents, datafile_test(:person).parents
|
|
32
|
-
|
|
33
|
-
agent = LLM::Agent.new knowledge_base: kb, model: 'mistral', url: "https://gepeto.bsc.es/"
|
|
34
|
-
|
|
35
|
-
agent.system = ""
|
|
36
|
-
|
|
37
|
-
sss 0
|
|
38
|
-
ppp agent.ask "Who is Guille's brother-in-law. Brother in law is your spouses sibling or your sibling's spouse"
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
|
|
43
21
|
end
|
|
44
22
|
|