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.
- checksums.yaml +4 -4
- data/.vimproject +80 -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 +51 -1
- data/lib/scout/llm/agent/delegate.rb +39 -0
- data/lib/scout/llm/agent/iterate.rb +44 -0
- data/lib/scout/llm/agent.rb +42 -21
- data/lib/scout/llm/ask.rb +38 -6
- 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 +23 -29
- data/lib/scout/llm/backends/openai.rb +34 -40
- data/lib/scout/llm/backends/responses.rb +158 -110
- data/lib/scout/llm/chat.rb +250 -94
- 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 +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 +58 -143
- data/lib/scout-ai.rb +1 -0
- data/scout-ai.gemspec +31 -18
- data/scout_commands/agent/ask +28 -71
- data/scout_commands/documenter +148 -0
- data/scout_commands/llm/ask +2 -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_openai.rb +45 -6
- data/test/scout/llm/backends/test_responses.rb +124 -0
- data/test/scout/llm/test_agent.rb +0 -70
- 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
@@ -0,0 +1,66 @@
|
|
1
|
+
module LLM
|
2
|
+
def self.call_id_name_and_arguments(tool_call)
|
3
|
+
tool_call_id = tool_call.dig("call_id") || tool_call.dig("id")
|
4
|
+
if tool_call['function']
|
5
|
+
function_name = tool_call.dig("function", "name")
|
6
|
+
function_arguments = tool_call.dig("function", "arguments")
|
7
|
+
else
|
8
|
+
function_name = tool_call.dig("name")
|
9
|
+
function_arguments = tool_call.dig("arguments")
|
10
|
+
end
|
11
|
+
|
12
|
+
function_arguments = JSON.parse(function_arguments, { symbolize_names: true }) if String === function_arguments
|
13
|
+
|
14
|
+
[tool_call_id, function_name, function_arguments]
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.process_calls(tools, calls, &block)
|
18
|
+
IndiferentHash.setup tools
|
19
|
+
calls.collect do |tool_call|
|
20
|
+
tool_call_id, function_name, function_arguments = call_id_name_and_arguments(tool_call)
|
21
|
+
|
22
|
+
obj, definition = tools[function_name]
|
23
|
+
|
24
|
+
function_response = case obj
|
25
|
+
when Proc
|
26
|
+
obj.call function_name, function_arguments
|
27
|
+
when Workflow
|
28
|
+
call_workflow(obj, function_name, function_arguments)
|
29
|
+
when KnowledgeBase
|
30
|
+
call_knowledge_base(obj, function_name, function_arguments)
|
31
|
+
else
|
32
|
+
if block_given?
|
33
|
+
block.call function_name, function_arguments
|
34
|
+
else
|
35
|
+
raise "Unkown executor #{Log.fingerprint obj} for function #{function_name}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
content = case function_response
|
40
|
+
when String
|
41
|
+
function_response
|
42
|
+
when nil
|
43
|
+
"success"
|
44
|
+
when Exception
|
45
|
+
{exception: function_response.message, stack: function_response.backtrace }.to_json
|
46
|
+
else
|
47
|
+
function_response.to_json
|
48
|
+
end
|
49
|
+
content = content.to_s if Numeric === content
|
50
|
+
|
51
|
+
response_message = {
|
52
|
+
id: tool_call_id,
|
53
|
+
role: "tool",
|
54
|
+
content: content
|
55
|
+
}
|
56
|
+
|
57
|
+
function_call = tool_call.dup
|
58
|
+
|
59
|
+
function_call['id'] = function_call.delete('call_id') if function_call.dig('call_id')
|
60
|
+
[
|
61
|
+
{role: "function_call", content: function_call.to_json},
|
62
|
+
{role: "function_call_output", content: response_message.to_json},
|
63
|
+
]
|
64
|
+
end.flatten
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
require 'scout/knowledge_base'
|
2
|
+
|
3
|
+
module LLM
|
4
|
+
def self.database_tool_definition(database, undirected = false, database_description = nil)
|
5
|
+
|
6
|
+
if undirected
|
7
|
+
properties = {
|
8
|
+
entities: {
|
9
|
+
type: "array",
|
10
|
+
items: { type: :string },
|
11
|
+
description: "Entities for which to find associations"
|
12
|
+
},
|
13
|
+
}
|
14
|
+
else
|
15
|
+
properties = {
|
16
|
+
entities: {
|
17
|
+
type: "array",
|
18
|
+
items: { type: :string },
|
19
|
+
description: "Source entities in the association, or target entities if 'reverse' is 'true'"
|
20
|
+
},
|
21
|
+
reverse: {
|
22
|
+
type: "boolean",
|
23
|
+
description: "Look for targets instead of sources, defaults to 'false'"
|
24
|
+
}
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
if database_description and not database_description.strip.empty?
|
29
|
+
description = <<-EOF
|
30
|
+
Find associations for a list of entities in database #{database}: #{database_description}
|
31
|
+
EOF
|
32
|
+
else
|
33
|
+
description = <<-EOF
|
34
|
+
Find associations for a list of entities in database #{database}.
|
35
|
+
EOF
|
36
|
+
end
|
37
|
+
|
38
|
+
if undirected
|
39
|
+
description += <<-EOF
|
40
|
+
Returns a list in the format entity~partner.
|
41
|
+
EOF
|
42
|
+
else
|
43
|
+
description += <<-EOF
|
44
|
+
Returns a list in the format source~target.
|
45
|
+
EOF
|
46
|
+
end
|
47
|
+
|
48
|
+
function = {
|
49
|
+
name: database,
|
50
|
+
description: description,
|
51
|
+
parameters: {
|
52
|
+
type: "object",
|
53
|
+
properties: properties,
|
54
|
+
required: ['entities']
|
55
|
+
}
|
56
|
+
}
|
57
|
+
|
58
|
+
IndiferentHash.setup function.merge(type: 'function', function: function)
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.database_details_tool_definition(database, undirected, fields)
|
62
|
+
|
63
|
+
if undirected
|
64
|
+
properties = {
|
65
|
+
associations: {
|
66
|
+
type: "array",
|
67
|
+
items: { type: :string },
|
68
|
+
description: "Associations in the form of source~target or target~source"
|
69
|
+
},
|
70
|
+
fields: {
|
71
|
+
type: "string",
|
72
|
+
enum: select_options,
|
73
|
+
description: "Limit the response to these detail fields fields"
|
74
|
+
},
|
75
|
+
}
|
76
|
+
else
|
77
|
+
properties = {
|
78
|
+
associations: {
|
79
|
+
type: "array",
|
80
|
+
items: { type: :string },
|
81
|
+
description: "Associations in the form of source~target"
|
82
|
+
},
|
83
|
+
}
|
84
|
+
end
|
85
|
+
|
86
|
+
if fields.length > 1
|
87
|
+
description = <<-EOF
|
88
|
+
Return details of association as a dictionary object.
|
89
|
+
Each key is an association and the value is an array with the values of the different fields you asked for, or for all fields otherwise.
|
90
|
+
The fields are: #{fields * ', '}.
|
91
|
+
Multiple values may be present and use the charater ';' to separate them.
|
92
|
+
EOF
|
93
|
+
else
|
94
|
+
properties.delete(:fields)
|
95
|
+
description = <<-EOF
|
96
|
+
Return the #{field} of association.
|
97
|
+
Multiple values may be present and use the charater ';' to separate them.
|
98
|
+
EOF
|
99
|
+
end
|
100
|
+
|
101
|
+
function = {
|
102
|
+
name: database + '_association_details',
|
103
|
+
description: description,
|
104
|
+
parameters: {
|
105
|
+
type: "object",
|
106
|
+
properties: properties,
|
107
|
+
required: ['associations']
|
108
|
+
}
|
109
|
+
}
|
110
|
+
|
111
|
+
IndiferentHash.setup function.merge(type: 'function', function: function)
|
112
|
+
end
|
113
|
+
|
114
|
+
|
115
|
+
def self.knowledge_base_tool_definition(knowledge_base, databases = nil)
|
116
|
+
databases ||= knowledge_base.all_databases
|
117
|
+
|
118
|
+
databases.inject({}){|tool_definitions,database|
|
119
|
+
database_description = knowledge_base.description(database)
|
120
|
+
undirected = knowledge_base.undirected(database)
|
121
|
+
definition = self.database_tool_definition(database, undirected, database_description)
|
122
|
+
tool_definitions.merge(database => [knowledge_base, definition])
|
123
|
+
if (fields = knowledge_base.get_database(database).fields).any?
|
124
|
+
details_definition = self.database_details_tool_definition(database, undirected, fields)
|
125
|
+
tool_definitions.merge(database + '_association_details' => [knowledge_base, details_definition])
|
126
|
+
end
|
127
|
+
}
|
128
|
+
end
|
129
|
+
|
130
|
+
def self.call_knowledge_base(knowledge_base, database, parameters={})
|
131
|
+
if database.end_with?('_association_details')
|
132
|
+
database = database.sub('_association_details', '')
|
133
|
+
associations, fields = IndiferentHash.process_options parameters, :associations, :fields
|
134
|
+
index = knowledge_base.get_index(database)
|
135
|
+
if fields
|
136
|
+
field_pos = fields.collect{|f| index.identify_field f }
|
137
|
+
associations.each_with_object({}) do |a,hash|
|
138
|
+
values = index[a]
|
139
|
+
next if values.nil?
|
140
|
+
hash[a] = values.values_at *field_pos
|
141
|
+
end
|
142
|
+
else
|
143
|
+
associations.each_with_object({}) do |a,hash|
|
144
|
+
values = index[a]
|
145
|
+
next if values.nil?
|
146
|
+
hash[a] = values
|
147
|
+
end
|
148
|
+
end
|
149
|
+
else
|
150
|
+
entities, reverse = IndiferentHash.process_options parameters, :entities, :reverse
|
151
|
+
if reverse
|
152
|
+
knowledge_base.parents(database, entities)
|
153
|
+
else
|
154
|
+
knowledge_base.children(database, entities)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require_relative '../utils'
|
2
|
+
require 'mcp_client'
|
3
|
+
|
4
|
+
module LLM
|
5
|
+
def self.mcp_tools(url, options = {})
|
6
|
+
if url == 'stdio'
|
7
|
+
client = MCPClient.create_client(mcp_server_configs: [options.merge(type: 'stdio')])
|
8
|
+
else
|
9
|
+
type = IndiferentHash.process_options options, :type,
|
10
|
+
type: (Open.remote?(url) ? :http : :stdio)
|
11
|
+
|
12
|
+
if url && Open.remote?(url)
|
13
|
+
token ||= LLM.get_url_config(:key, url, :mcp)
|
14
|
+
options[:headers] = { 'Authorization' => "Bearer #{token}" }
|
15
|
+
end
|
16
|
+
|
17
|
+
client = MCPClient.create_client(mcp_server_configs: [options.merge(type: 'http', url: url)])
|
18
|
+
end
|
19
|
+
|
20
|
+
tools = client.list_tools
|
21
|
+
|
22
|
+
tool_definitions = IndiferentHash.setup({})
|
23
|
+
tools.each do |tool|
|
24
|
+
name = tool.name
|
25
|
+
description = tool.description
|
26
|
+
schema = tool.schema
|
27
|
+
|
28
|
+
function = {
|
29
|
+
name: name,
|
30
|
+
description: description,
|
31
|
+
parameters: schema
|
32
|
+
}
|
33
|
+
|
34
|
+
definition = IndiferentHash.setup function.merge(type: 'function', function: function)
|
35
|
+
block = Proc.new do |name,params|
|
36
|
+
res = tool.server.call_tool(name, params)
|
37
|
+
if Hash === res && res['content']
|
38
|
+
res = res['content']
|
39
|
+
end
|
40
|
+
|
41
|
+
if Array === res and res.length == 1
|
42
|
+
res = res.first
|
43
|
+
end
|
44
|
+
|
45
|
+
if Hash === res && res['content']
|
46
|
+
res = res['content']
|
47
|
+
end
|
48
|
+
|
49
|
+
if Hash === res && res['text']
|
50
|
+
res = res['text']
|
51
|
+
end
|
52
|
+
|
53
|
+
res
|
54
|
+
end
|
55
|
+
tool_definitions[name] = [block, definition]
|
56
|
+
end
|
57
|
+
tool_definitions
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'scout/workflow'
|
2
|
+
module LLM
|
3
|
+
def self.task_tool_definition(workflow, task_name, inputs = nil)
|
4
|
+
task_info = workflow.task_info(task_name)
|
5
|
+
|
6
|
+
inputs = inputs.collect{|i| i.to_sym } if inputs
|
7
|
+
|
8
|
+
properties = task_info[:inputs].inject({}) do |acc,input|
|
9
|
+
next acc if inputs and not inputs.include?(input)
|
10
|
+
type = task_info[:input_types][input]
|
11
|
+
description = task_info[:input_descriptions][input]
|
12
|
+
|
13
|
+
type = :string if type == :text
|
14
|
+
type = :string if type == :select
|
15
|
+
type = :string if type == :path
|
16
|
+
type = :number if type == :float
|
17
|
+
|
18
|
+
acc[input] = {
|
19
|
+
"type": type,
|
20
|
+
"description": description
|
21
|
+
}
|
22
|
+
|
23
|
+
if input_options = task_info[:input_options][input]
|
24
|
+
if select_options = input_options[:select_options]
|
25
|
+
select_options = select_options.values if Hash === select_options
|
26
|
+
acc[input]["enum"] = select_options
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
acc
|
31
|
+
end
|
32
|
+
|
33
|
+
required_inputs = task_info[:inputs].select do |input|
|
34
|
+
next if inputs and not inputs.include?(input.to_sym)
|
35
|
+
task_info[:input_options].include?(input) && task_info[:input_options][input][:required]
|
36
|
+
end
|
37
|
+
|
38
|
+
function = {
|
39
|
+
name: task_name,
|
40
|
+
description: task_info[:description],
|
41
|
+
parameters: {
|
42
|
+
type: "object",
|
43
|
+
properties: properties,
|
44
|
+
required: required_inputs
|
45
|
+
}
|
46
|
+
}
|
47
|
+
|
48
|
+
IndiferentHash.setup function.merge(type: 'function', function: function)
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.workflow_tools(workflow, tasks = nil)
|
52
|
+
tasks = workflow.all_exports if tasks.nil?
|
53
|
+
tasks = workflow.all_tasks if tasks.empty?
|
54
|
+
|
55
|
+
tasks.inject({}){|tool_definitions,task_name|
|
56
|
+
definition = self.task_tool_definition(workflow, task_name)
|
57
|
+
tool_definitions.merge(task_name => [workflow, definition])
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.call_workflow(workflow, task_name, parameters={})
|
62
|
+
jobname = parameters.delete :jobname
|
63
|
+
if workflow.exec_exports.include? task_name.to_sym
|
64
|
+
workflow.job(task_name, jobname, parameters).exec
|
65
|
+
else
|
66
|
+
workflow.job(task_name, jobname, parameters).run
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/lib/scout/llm/tools.rb
CHANGED
@@ -1,18 +1,48 @@
|
|
1
|
-
require 'scout/workflow'
|
2
1
|
require 'scout/knowledge_base'
|
2
|
+
require_relative 'tools/mcp'
|
3
|
+
require_relative 'tools/workflow'
|
4
|
+
require_relative 'tools/knowledge_base'
|
5
|
+
require_relative 'tools/call'
|
3
6
|
module LLM
|
7
|
+
def self.call_tools(tool_calls, &block)
|
8
|
+
tool_calls.collect{|tool_call|
|
9
|
+
response_message = LLM.tool_response(tool_call, &block)
|
10
|
+
function_call = tool_call
|
11
|
+
function_call['id'] = tool_call.delete('call_id') if tool_call.dig('call_id')
|
12
|
+
[
|
13
|
+
{role: "function_call", content: tool_call.to_json},
|
14
|
+
{role: "function_call_output", content: response_message.to_json},
|
15
|
+
]
|
16
|
+
}.flatten
|
17
|
+
end
|
18
|
+
|
4
19
|
def self.tool_response(tool_call, &block)
|
5
|
-
tool_call_id = tool_call.dig("id")
|
6
|
-
|
7
|
-
|
20
|
+
tool_call_id = tool_call.dig("call_id") || tool_call.dig("id")
|
21
|
+
if tool_call['function']
|
22
|
+
function_name = tool_call.dig("function", "name")
|
23
|
+
function_arguments = tool_call.dig("function", "arguments")
|
24
|
+
else
|
25
|
+
function_name = tool_call.dig("name")
|
26
|
+
function_arguments = tool_call.dig("arguments")
|
27
|
+
end
|
28
|
+
|
8
29
|
function_arguments = JSON.parse(function_arguments, { symbolize_names: true }) if String === function_arguments
|
9
|
-
|
30
|
+
|
31
|
+
Log.high "Calling function #{function_name} with arguments #{Log.fingerprint function_arguments}"
|
32
|
+
|
33
|
+
function_response = begin
|
34
|
+
block.call function_name, function_arguments
|
35
|
+
rescue
|
36
|
+
$!
|
37
|
+
end
|
10
38
|
|
11
39
|
content = case function_response
|
12
40
|
when String
|
13
41
|
function_response
|
14
42
|
when nil
|
15
43
|
"success"
|
44
|
+
when Exception
|
45
|
+
{exception: function_response.message, stack: function_response.backtrace }.to_json
|
16
46
|
else
|
17
47
|
function_response.to_json
|
18
48
|
end
|
@@ -24,118 +54,6 @@ module LLM
|
|
24
54
|
}
|
25
55
|
end
|
26
56
|
|
27
|
-
def self.task_tool_definition(workflow, task_name, inputs = nil)
|
28
|
-
task_info = workflow.task_info(task_name)
|
29
|
-
|
30
|
-
inputs = inputs.collect{|i| i.to_sym } if inputs
|
31
|
-
|
32
|
-
properties = task_info[:inputs].inject({}) do |acc,input|
|
33
|
-
next acc if inputs and not inputs.include?(input)
|
34
|
-
type = task_info[:input_types][input]
|
35
|
-
description = task_info[:input_descriptions][input]
|
36
|
-
|
37
|
-
type = :string if type == :text
|
38
|
-
type = :string if type == :select
|
39
|
-
type = :string if type == :path
|
40
|
-
type = :number if type == :float
|
41
|
-
|
42
|
-
acc[input] = {
|
43
|
-
"type": type,
|
44
|
-
"description": description
|
45
|
-
}
|
46
|
-
|
47
|
-
if input_options = task_info[:input_options][input]
|
48
|
-
if select_options = input_options[:select_options]
|
49
|
-
select_options = select_options.values if Hash === select_options
|
50
|
-
acc[input]["enum"] = select_options
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
acc
|
55
|
-
end
|
56
|
-
|
57
|
-
required_inputs = task_info[:inputs].select do |input|
|
58
|
-
next if inputs and not inputs.include?(input.to_sym)
|
59
|
-
task_info[:input_options].include?(input) && task_info[:input_options][input][:required]
|
60
|
-
end
|
61
|
-
|
62
|
-
{
|
63
|
-
type: "function",
|
64
|
-
function: {
|
65
|
-
name: task_name,
|
66
|
-
description: task_info[:description],
|
67
|
-
parameters: {
|
68
|
-
type: "object",
|
69
|
-
properties: properties,
|
70
|
-
required: required_inputs
|
71
|
-
}
|
72
|
-
}
|
73
|
-
}
|
74
|
-
end
|
75
|
-
|
76
|
-
def self.workflow_tools(workflow, tasks = nil)
|
77
|
-
tasks = workflow.all_exports
|
78
|
-
tasks.collect{|task_name| self.task_tool_definition(workflow, task_name) }
|
79
|
-
end
|
80
|
-
|
81
|
-
def self.knowledge_base_tool_definition(knowledge_base)
|
82
|
-
|
83
|
-
databases = knowledge_base.all_databases.collect{|d| d.to_s }
|
84
|
-
|
85
|
-
properties = {
|
86
|
-
database: {
|
87
|
-
type: "string",
|
88
|
-
enum: databases,
|
89
|
-
description: "Database to traverse"
|
90
|
-
},
|
91
|
-
entities: {
|
92
|
-
type: "array",
|
93
|
-
items: { type: :string },
|
94
|
-
description: "Parent entities to find children for"
|
95
|
-
}
|
96
|
-
}
|
97
|
-
|
98
|
-
[{
|
99
|
-
type: "function",
|
100
|
-
function: {
|
101
|
-
name: 'children',
|
102
|
-
description: "Find the graph children for a list of entities in a format like parent~child. Returns a list.",
|
103
|
-
parameters: {
|
104
|
-
type: "object",
|
105
|
-
properties: properties,
|
106
|
-
required: ['database', 'entities']
|
107
|
-
}
|
108
|
-
}
|
109
|
-
}]
|
110
|
-
end
|
111
|
-
|
112
|
-
def self.association_tool_definition(name)
|
113
|
-
properties = {
|
114
|
-
entities: {
|
115
|
-
type: "array",
|
116
|
-
items: { type: :string },
|
117
|
-
description: "Source entities in the association, or target entities if 'reverse' it true."
|
118
|
-
},
|
119
|
-
reverse: {
|
120
|
-
type: "boolean",
|
121
|
-
description: "Look for targets instead of sources, defaults to 'false'."
|
122
|
-
}
|
123
|
-
}
|
124
|
-
|
125
|
-
{
|
126
|
-
type: "function",
|
127
|
-
function: {
|
128
|
-
name: name,
|
129
|
-
description: "Find associations for a list of entities. Returns a list in the format source~target.",
|
130
|
-
parameters: {
|
131
|
-
type: "object",
|
132
|
-
properties: properties,
|
133
|
-
required: ['entities']
|
134
|
-
}
|
135
|
-
}
|
136
|
-
}
|
137
|
-
end
|
138
|
-
|
139
57
|
def self.run_tools(messages)
|
140
58
|
messages.collect do |info|
|
141
59
|
IndiferentHash.setup(info)
|
@@ -156,7 +74,7 @@ module LLM
|
|
156
74
|
if message[:role] == 'function_call'
|
157
75
|
tool_call = JSON.parse(message[:content])
|
158
76
|
arguments = tool_call.delete('arguments') || {}
|
159
|
-
name = tool_call
|
77
|
+
name = tool_call[:name]
|
160
78
|
tool_call['type'] = 'function'
|
161
79
|
tool_call['function'] ||= {}
|
162
80
|
tool_call['function']['name'] ||= name
|
@@ -164,7 +82,7 @@ module LLM
|
|
164
82
|
{role: 'assistant', tool_calls: [tool_call]}
|
165
83
|
elsif message[:role] == 'function_call_output'
|
166
84
|
info = JSON.parse(message[:content])
|
167
|
-
id = info.delete('
|
85
|
+
id = info.delete('call_id') || info.dig('id')
|
168
86
|
info['role'] = 'tool'
|
169
87
|
info['tool_call_id'] = id
|
170
88
|
info
|
@@ -174,52 +92,49 @@ module LLM
|
|
174
92
|
end.flatten
|
175
93
|
end
|
176
94
|
|
177
|
-
def self.
|
95
|
+
def self.tools_to_anthropic(messages)
|
178
96
|
messages.collect do |message|
|
179
97
|
if message[:role] == 'function_call'
|
180
98
|
tool_call = JSON.parse(message[:content])
|
181
99
|
arguments = tool_call.delete('arguments') || {}
|
182
|
-
|
183
|
-
|
184
|
-
tool_call['
|
185
|
-
tool_call['
|
186
|
-
|
187
|
-
tool_call['function']['arguments'] ||= arguments
|
188
|
-
{role: 'assistant', tool_calls: [tool_call]}
|
100
|
+
name = tool_call[:name]
|
101
|
+
tool_call['type'] = 'tool_use'
|
102
|
+
tool_call['name'] ||= name
|
103
|
+
tool_call['input'] = arguments
|
104
|
+
{role: 'assistant', content: [tool_call]}
|
189
105
|
elsif message[:role] == 'function_call_output'
|
190
106
|
info = JSON.parse(message[:content])
|
191
|
-
id = info.delete('
|
192
|
-
info
|
193
|
-
info
|
107
|
+
id = info.delete('call_id') || info.delete('id')
|
108
|
+
info.delete "role"
|
109
|
+
info['tool_use_id'] = id
|
110
|
+
info['type'] = 'tool_result'
|
111
|
+
{role: 'user', content: [info]}
|
194
112
|
else
|
195
113
|
message
|
196
114
|
end
|
197
115
|
end.flatten
|
198
116
|
end
|
199
117
|
|
200
|
-
def self.
|
118
|
+
def self.tools_to_ollama(messages)
|
201
119
|
messages.collect do |message|
|
202
120
|
if message[:role] == 'function_call'
|
203
121
|
tool_call = JSON.parse(message[:content])
|
204
|
-
|
122
|
+
arguments = tool_call.delete('arguments') || {}
|
123
|
+
id = tool_call.delete('id')
|
124
|
+
name = tool_call.delete('name')
|
125
|
+
tool_call['type'] = 'function'
|
126
|
+
tool_call['function'] ||= {}
|
127
|
+
tool_call['function']['name'] ||= name
|
128
|
+
tool_call['function']['arguments'] ||= arguments
|
205
129
|
{role: 'assistant', tool_calls: [tool_call]}
|
206
130
|
elsif message[:role] == 'function_call_output'
|
207
131
|
info = JSON.parse(message[:content])
|
208
|
-
|
132
|
+
id = info.delete('id') || ''
|
133
|
+
info['role'] = 'tool'
|
209
134
|
info
|
210
135
|
else
|
211
136
|
message
|
212
137
|
end
|
213
138
|
end.flatten
|
214
139
|
end
|
215
|
-
|
216
|
-
def self.call_tools(tool_calls, &block)
|
217
|
-
tool_calls.collect{|tool_call|
|
218
|
-
response_message = LLM.tool_response(tool_call, &block)
|
219
|
-
[
|
220
|
-
{role: "function_call", content: tool_call.to_json},
|
221
|
-
{role: "function_call_output", content: response_message.to_json},
|
222
|
-
]
|
223
|
-
}.flatten
|
224
|
-
end
|
225
140
|
end
|