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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.vimproject +80 -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 +51 -1
  12. data/lib/scout/llm/agent/delegate.rb +39 -0
  13. data/lib/scout/llm/agent/iterate.rb +44 -0
  14. data/lib/scout/llm/agent.rb +42 -21
  15. data/lib/scout/llm/ask.rb +38 -6
  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 +23 -29
  19. data/lib/scout/llm/backends/openai.rb +34 -40
  20. data/lib/scout/llm/backends/responses.rb +158 -110
  21. data/lib/scout/llm/chat.rb +250 -94
  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 +66 -0
  27. data/lib/scout/llm/tools/knowledge_base.rb +158 -0
  28. data/lib/scout/llm/tools/mcp.rb +59 -0
  29. data/lib/scout/llm/tools/workflow.rb +69 -0
  30. data/lib/scout/llm/tools.rb +58 -143
  31. data/lib/scout-ai.rb +1 -0
  32. data/scout-ai.gemspec +31 -18
  33. data/scout_commands/agent/ask +28 -71
  34. data/scout_commands/documenter +148 -0
  35. data/scout_commands/llm/ask +2 -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_openai.rb +45 -6
  41. data/test/scout/llm/backends/test_responses.rb +124 -0
  42. data/test/scout/llm/test_agent.rb +0 -70
  43. data/test/scout/llm/test_ask.rb +3 -1
  44. data/test/scout/llm/test_chat.rb +43 -1
  45. data/test/scout/llm/test_mcp.rb +29 -0
  46. data/test/scout/llm/tools/test_knowledge_base.rb +22 -0
  47. data/test/scout/llm/tools/test_mcp.rb +11 -0
  48. data/test/scout/llm/tools/test_workflow.rb +39 -0
  49. metadata +56 -17
  50. data/README.rdoc +0 -18
  51. data/python/scout_ai/__pycache__/__init__.cpython-310.pyc +0 -0
  52. data/python/scout_ai/__pycache__/__init__.cpython-311.pyc +0 -0
  53. data/python/scout_ai/__pycache__/huggingface.cpython-310.pyc +0 -0
  54. data/python/scout_ai/__pycache__/huggingface.cpython-311.pyc +0 -0
  55. data/python/scout_ai/__pycache__/util.cpython-310.pyc +0 -0
  56. data/python/scout_ai/__pycache__/util.cpython-311.pyc +0 -0
  57. data/python/scout_ai/atcold/plot_lib.py +0 -141
  58. data/python/scout_ai/atcold/spiral.py +0 -27
  59. data/python/scout_ai/huggingface/train/__pycache__/__init__.cpython-310.pyc +0 -0
  60. data/python/scout_ai/huggingface/train/__pycache__/next_token.cpython-310.pyc +0 -0
  61. data/python/scout_ai/language_model.py +0 -70
  62. /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
@@ -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,52 +92,49 @@ 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
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
data/lib/scout-ai.rb CHANGED
@@ -5,5 +5,6 @@ require 'scout/resource'
5
5
  Path.add_path :scout_ai_lib, File.join(Path.caller_lib_dir(__FILE__), "{TOPLEVEL}/{SUBPATH}")
6
6
 
7
7
  require 'scout/llm/ask'
8
+ require 'scout/llm/chat'
8
9
  require 'scout/llm/embed'
9
10
  require 'scout/llm/agent'