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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.vimproject +87 -15
  3. data/README.md +296 -0
  4. data/Rakefile +2 -0
  5. data/VERSION +1 -1
  6. data/doc/Agent.md +279 -0
  7. data/doc/Chat.md +258 -0
  8. data/doc/LLM.md +446 -0
  9. data/doc/Model.md +513 -0
  10. data/doc/RAG.md +129 -0
  11. data/lib/scout/llm/agent/chat.rb +48 -1
  12. data/lib/scout/llm/agent/delegate.rb +51 -0
  13. data/lib/scout/llm/agent/iterate.rb +44 -0
  14. data/lib/scout/llm/agent.rb +43 -22
  15. data/lib/scout/llm/ask.rb +47 -7
  16. data/lib/scout/llm/backends/anthropic.rb +147 -0
  17. data/lib/scout/llm/backends/bedrock.rb +1 -1
  18. data/lib/scout/llm/backends/ollama.rb +27 -30
  19. data/lib/scout/llm/backends/openai.rb +36 -41
  20. data/lib/scout/llm/backends/responses.rb +166 -113
  21. data/lib/scout/llm/chat.rb +270 -102
  22. data/lib/scout/llm/embed.rb +4 -4
  23. data/lib/scout/llm/mcp.rb +28 -0
  24. data/lib/scout/llm/parse.rb +1 -0
  25. data/lib/scout/llm/rag.rb +9 -0
  26. data/lib/scout/llm/tools/call.rb +76 -0
  27. data/lib/scout/llm/tools/knowledge_base.rb +159 -0
  28. data/lib/scout/llm/tools/mcp.rb +59 -0
  29. data/lib/scout/llm/tools/workflow.rb +106 -0
  30. data/lib/scout/llm/tools.rb +98 -141
  31. data/lib/scout-ai.rb +1 -0
  32. data/scout-ai.gemspec +31 -18
  33. data/scout_commands/agent/ask +59 -78
  34. data/scout_commands/documenter +148 -0
  35. data/scout_commands/llm/ask +3 -2
  36. data/scout_commands/llm/server +319 -0
  37. data/share/server/chat.html +138 -0
  38. data/share/server/chat.js +468 -0
  39. data/test/scout/llm/backends/test_anthropic.rb +134 -0
  40. data/test/scout/llm/backends/test_ollama.rb +1 -1
  41. data/test/scout/llm/backends/test_openai.rb +45 -6
  42. data/test/scout/llm/backends/test_responses.rb +124 -0
  43. data/test/scout/llm/test_agent.rb +1 -93
  44. data/test/scout/llm/test_ask.rb +3 -1
  45. data/test/scout/llm/test_chat.rb +43 -1
  46. data/test/scout/llm/test_mcp.rb +29 -0
  47. data/test/scout/llm/tools/test_knowledge_base.rb +22 -0
  48. data/test/scout/llm/tools/test_mcp.rb +11 -0
  49. data/test/scout/llm/tools/test_workflow.rb +39 -0
  50. metadata +56 -17
  51. data/README.rdoc +0 -18
  52. data/python/scout_ai/__pycache__/__init__.cpython-310.pyc +0 -0
  53. data/python/scout_ai/__pycache__/__init__.cpython-311.pyc +0 -0
  54. data/python/scout_ai/__pycache__/huggingface.cpython-310.pyc +0 -0
  55. data/python/scout_ai/__pycache__/huggingface.cpython-311.pyc +0 -0
  56. data/python/scout_ai/__pycache__/util.cpython-310.pyc +0 -0
  57. data/python/scout_ai/__pycache__/util.cpython-311.pyc +0 -0
  58. data/python/scout_ai/atcold/plot_lib.py +0 -141
  59. data/python/scout_ai/atcold/spiral.py +0 -27
  60. data/python/scout_ai/huggingface/train/__pycache__/__init__.cpython-310.pyc +0 -0
  61. data/python/scout_ai/huggingface/train/__pycache__/next_token.cpython-310.pyc +0 -0
  62. data/python/scout_ai/language_model.py +0 -70
  63. /data/{python/scout_ai/atcold/__init__.py → test/scout/llm/tools/test_call.rb} +0 -0
@@ -0,0 +1,76 @@
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
+ function_arguments = IndiferentHash.setup function_arguments
23
+
24
+ obj, definition = tools[function_name]
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}): "
32
+ function_response = case obj
33
+ when Proc
34
+ obj.call function_name, function_arguments
35
+ when Workflow
36
+ call_workflow(obj, function_name, function_arguments)
37
+ when KnowledgeBase
38
+ call_knowledge_base(obj, function_name, function_arguments)
39
+ else
40
+ if block_given?
41
+ block.call function_name, function_arguments
42
+ else
43
+ ParameterException.new "Tool or function not found '#{function_name}'. Called with parameters #{Log.fingerprint function_arguments}" if obj.nil? && definition.nil?
44
+ end
45
+ end
46
+
47
+ content = case function_response
48
+ when String
49
+ function_response
50
+ when nil
51
+ "success"
52
+ when Exception
53
+ {exception: function_response.message, stack: function_response.backtrace }.to_json
54
+ else
55
+ function_response.to_json
56
+ end
57
+ content = content.to_s if Numeric === content
58
+
59
+ Log.high "Called #{function_name}: " + Log.fingerprint(content)
60
+
61
+ response_message = {
62
+ id: tool_call_id,
63
+ role: "tool",
64
+ content: content
65
+ }
66
+
67
+ function_call = tool_call.dup
68
+
69
+ function_call['id'] = function_call.delete('call_id') if function_call.dig('call_id')
70
+ [
71
+ {role: "function_call", content: function_call.to_json},
72
+ {role: "function_call_output", content: response_message.to_json},
73
+ ]
74
+ end.flatten
75
+ end
76
+ end
@@ -0,0 +1,159 @@
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: "array",
72
+ items: { type: :string },
73
+ description: "Limit the response to these 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 #{fields.first} 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.to_s + '_association_details',
103
+ description: description,
104
+ parameters: {
105
+ type: "object",
106
+ properties: properties,
107
+ required: ['associations']
108
+ }
109
+ }
110
+
111
+ IndiferentHash.setup 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.to_s + '_association_details' => [knowledge_base, details_definition])
126
+ end
127
+ tool_definitions
128
+ }
129
+ end
130
+
131
+ def self.call_knowledge_base(knowledge_base, database, parameters={})
132
+ if database.end_with?('_association_details')
133
+ database = database.sub('_association_details', '')
134
+ associations, fields = IndiferentHash.process_options parameters, :associations, :fields
135
+ index = knowledge_base.get_index(database)
136
+ if fields
137
+ field_pos = fields.collect{|f| index.identify_field f }
138
+ associations.each_with_object({}) do |a,hash|
139
+ values = index[a]
140
+ next if values.nil?
141
+ hash[a] = values.values_at *field_pos
142
+ end
143
+ else
144
+ associations.each_with_object({}) do |a,hash|
145
+ values = index[a]
146
+ next if values.nil?
147
+ hash[a] = values
148
+ end
149
+ end
150
+ else
151
+ entities, reverse = IndiferentHash.process_options parameters, :entities, :reverse
152
+ if reverse
153
+ knowledge_base.parents(database, entities)
154
+ else
155
+ knowledge_base.children(database, entities)
156
+ end
157
+ end
158
+ end
159
+ 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,106 @@
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
+ return nil if task_info.nil?
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
21
+
22
+ properties = task_info[:inputs].inject({}) do |acc,input|
23
+ next acc if names and not names.include?(input)
24
+ type = task_info[:input_types][input]
25
+ description = task_info[:input_descriptions][input]
26
+
27
+ type = :string if type == :text
28
+ type = :string if type == :select
29
+ type = :string if type == :path
30
+ type = :number if type == :float
31
+ type = :array if type.to_s.end_with?('_array')
32
+
33
+ acc[input] = {
34
+ "type": type,
35
+ "description": description
36
+ }
37
+
38
+ if type == :array
39
+ acc[input]['items'] = {type: :string}
40
+ end
41
+
42
+ if input_options = task_info[:input_options][input]
43
+ if select_options = input_options[:select_options]
44
+ select_options = select_options.values if Hash === select_options
45
+ acc[input]["enum"] = select_options
46
+ end
47
+ end
48
+
49
+ acc
50
+ end
51
+
52
+ required_inputs = task_info[:inputs].select do |input|
53
+ next if names and not names.include?(input.to_sym)
54
+ task_info[:input_options].include?(input) && task_info[:input_options][input][:required]
55
+ end
56
+
57
+ function = {
58
+ name: task_name,
59
+ description: task_info[:description],
60
+ parameters: {
61
+ type: "object",
62
+ properties: properties,
63
+ required: required_inputs,
64
+ }
65
+ }
66
+
67
+ function[:parameters][:defaults] = defaults if defaults
68
+
69
+ #IndiferentHash.setup function.merge(type: 'function', function: function)
70
+ IndiferentHash.setup function
71
+ end
72
+
73
+ def self.workflow_tools(workflow, tasks = nil)
74
+ if Array === workflow
75
+ workflow.inject({}){|tool_definitions,wf| tool_definitions.merge(workflow_tools(wf, tasks)) }
76
+
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
87
+ end
88
+
89
+ def self.call_workflow(workflow, task_name, parameters={})
90
+ jobname = parameters.delete :jobname
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 $!
104
+ end
105
+ end
106
+ end
@@ -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
- function_name = tool_call.dig("function", "name")
7
- function_arguments = tool_call.dig("function", "arguments")
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
- function_response = block.call function_name, function_arguments
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.delete('name')
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('id') || ''
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,38 +92,45 @@ module LLM
174
92
  end.flatten
175
93
  end
176
94
 
177
- def self.tools_to_ollama(messages)
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
- id = tool_call.delete('id')
183
- name = tool_call.delete('name')
184
- tool_call['type'] = 'function'
185
- tool_call['function'] ||= {}
186
- tool_call['function']['name'] ||= name
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('id') || ''
192
- info['role'] = 'tool'
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.tools_to_responses(messages)
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
- tool_call['function']['arguments'] = (tool_call['function']['arguments'] || {}).to_json
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
- info["tool_call_id"] = info['id']
132
+ id = info.delete('id') || ''
133
+ info['role'] = 'tool'
209
134
  info
210
135
  else
211
136
  message
@@ -213,13 +138,45 @@ module LLM
213
138
  end.flatten
214
139
  end
215
140
 
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
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
224
181
  end
225
182
  end