scout-ai 0.2.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 +155 -9
- data/README.md +296 -0
- data/Rakefile +3 -0
- data/VERSION +1 -1
- data/bin/scout-ai +2 -0
- 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 +74 -0
- data/lib/scout/llm/agent/delegate.rb +39 -0
- data/lib/scout/llm/agent/iterate.rb +44 -0
- data/lib/scout/llm/agent.rb +51 -30
- data/lib/scout/llm/ask.rb +63 -21
- data/lib/scout/llm/backends/anthropic.rb +147 -0
- data/lib/scout/llm/backends/bedrock.rb +129 -0
- data/lib/scout/llm/backends/huggingface.rb +6 -21
- data/lib/scout/llm/backends/ollama.rb +62 -35
- data/lib/scout/llm/backends/openai.rb +77 -33
- data/lib/scout/llm/backends/openwebui.rb +1 -1
- data/lib/scout/llm/backends/relay.rb +3 -2
- data/lib/scout/llm/backends/responses.rb +320 -0
- data/lib/scout/llm/chat.rb +703 -0
- data/lib/scout/llm/embed.rb +4 -4
- data/lib/scout/llm/mcp.rb +28 -0
- data/lib/scout/llm/parse.rb +71 -13
- 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 +112 -76
- data/lib/scout/llm/utils.rb +17 -10
- data/lib/scout/model/base.rb +19 -0
- data/lib/scout/model/python/base.rb +25 -0
- data/lib/scout/model/python/huggingface/causal/next_token.rb +23 -0
- data/lib/scout/model/python/huggingface/causal.rb +29 -0
- data/lib/scout/model/python/huggingface/classification +0 -0
- data/lib/scout/model/python/huggingface/classification.rb +50 -0
- data/lib/scout/model/python/huggingface.rb +112 -0
- data/lib/scout/model/python/torch/dataloader.rb +57 -0
- data/lib/scout/model/python/torch/helpers.rb +84 -0
- data/lib/scout/model/python/torch/introspection.rb +34 -0
- data/lib/scout/model/python/torch/load_and_save.rb +47 -0
- data/lib/scout/model/python/torch.rb +94 -0
- data/lib/scout/model/util/run.rb +181 -0
- data/lib/scout/model/util/save.rb +81 -0
- data/lib/scout-ai.rb +4 -1
- data/python/scout_ai/__init__.py +35 -0
- data/python/scout_ai/huggingface/data.py +48 -0
- data/python/scout_ai/huggingface/eval.py +60 -0
- data/python/scout_ai/huggingface/model.py +29 -0
- data/python/scout_ai/huggingface/rlhf.py +83 -0
- data/python/scout_ai/huggingface/train/__init__.py +34 -0
- data/python/scout_ai/huggingface/train/next_token.py +315 -0
- data/python/scout_ai/util.py +32 -0
- data/scout-ai.gemspec +143 -0
- data/scout_commands/agent/ask +89 -14
- data/scout_commands/agent/kb +15 -0
- data/scout_commands/documenter +148 -0
- data/scout_commands/llm/ask +71 -12
- data/scout_commands/llm/process +4 -2
- data/scout_commands/llm/server +319 -0
- data/share/server/chat.html +138 -0
- data/share/server/chat.js +468 -0
- data/test/data/cat.jpg +0 -0
- data/test/scout/llm/agent/test_chat.rb +14 -0
- data/test/scout/llm/backends/test_anthropic.rb +134 -0
- data/test/scout/llm/backends/test_bedrock.rb +60 -0
- data/test/scout/llm/backends/test_huggingface.rb +3 -3
- data/test/scout/llm/backends/test_ollama.rb +48 -10
- data/test/scout/llm/backends/test_openai.rb +134 -10
- data/test/scout/llm/backends/test_responses.rb +239 -0
- data/test/scout/llm/test_agent.rb +0 -70
- data/test/scout/llm/test_ask.rb +4 -1
- data/test/scout/llm/test_chat.rb +256 -0
- data/test/scout/llm/test_mcp.rb +29 -0
- data/test/scout/llm/test_parse.rb +81 -2
- data/test/scout/llm/tools/test_call.rb +0 -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
- data/test/scout/model/python/huggingface/causal/test_next_token.rb +59 -0
- data/test/scout/model/python/huggingface/test_causal.rb +33 -0
- data/test/scout/model/python/huggingface/test_classification.rb +30 -0
- data/test/scout/model/python/test_base.rb +44 -0
- data/test/scout/model/python/test_huggingface.rb +9 -0
- data/test/scout/model/python/test_torch.rb +71 -0
- data/test/scout/model/python/torch/test_helpers.rb +14 -0
- data/test/scout/model/test_base.rb +117 -0
- data/test/scout/model/util/test_save.rb +31 -0
- metadata +113 -7
- data/README.rdoc +0 -18
- data/questions/coach +0 -2
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
module LLM
|
|
2
|
+
class Agent
|
|
3
|
+
def start_chat
|
|
4
|
+
@start_chat ||= Chat.setup([])
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def start(chat=nil)
|
|
8
|
+
if chat
|
|
9
|
+
(@current_chat || start_chat).annotate chat unless Chat === chat
|
|
10
|
+
@current_chat = chat
|
|
11
|
+
else
|
|
12
|
+
@current_chat = start_chat.branch
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def current_chat
|
|
17
|
+
@current_chat ||= start
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def method_missing(name,...)
|
|
21
|
+
current_chat.send(name, ...)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def respond(...)
|
|
25
|
+
self.ask(current_chat, ...)
|
|
26
|
+
end
|
|
27
|
+
|
|
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
|
+
|
|
34
|
+
def chat(model = nil, options = {})
|
|
35
|
+
response = ask(current_chat, model, options.merge(return_messages: true))
|
|
36
|
+
if Array === response
|
|
37
|
+
current_chat.concat(response)
|
|
38
|
+
current_chat.answer
|
|
39
|
+
else
|
|
40
|
+
current_chat.push({role: :assistant, content: response})
|
|
41
|
+
response
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def json(...)
|
|
47
|
+
current_chat.format :json
|
|
48
|
+
output = ask(current_chat, ...)
|
|
49
|
+
obj = JSON.parse output
|
|
50
|
+
if (Hash === obj) and obj.keys == ['content']
|
|
51
|
+
obj['content']
|
|
52
|
+
else
|
|
53
|
+
obj
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def json_format(format, ...)
|
|
58
|
+
current_chat.format format
|
|
59
|
+
output = ask(current_chat, ...)
|
|
60
|
+
obj = JSON.parse output
|
|
61
|
+
if (Hash === obj) and obj.keys == ['content']
|
|
62
|
+
obj['content']
|
|
63
|
+
else
|
|
64
|
+
obj
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def get_previous_response_id
|
|
69
|
+
msg = current_chat.reverse.find{|msg| msg[:role].to_sym == :previous_response_id }
|
|
70
|
+
msg.nil? ? nil : msg['content']
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module LLM
|
|
2
|
+
class Agent
|
|
3
|
+
|
|
4
|
+
def delegate(agent, name, description, &block)
|
|
5
|
+
@other_options[:tools] ||= {}
|
|
6
|
+
task_name = "hand_off_to_#{name}"
|
|
7
|
+
|
|
8
|
+
block ||= Proc.new do |name, parameters|
|
|
9
|
+
message = parameters[:message]
|
|
10
|
+
agent.user message
|
|
11
|
+
agent.chat
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
properties = {
|
|
15
|
+
message: {
|
|
16
|
+
"type": :string,
|
|
17
|
+
"description": "Message to pass to the agent"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
required_inputs = [:message]
|
|
22
|
+
|
|
23
|
+
function = {
|
|
24
|
+
name: task_name,
|
|
25
|
+
description: description,
|
|
26
|
+
parameters: {
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: properties,
|
|
29
|
+
required: required_inputs
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
definition = IndiferentHash.setup function.merge(type: 'function', function: function)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@other_options[:tools][task_name] = [block, definition]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module LLM
|
|
2
|
+
class Agent
|
|
3
|
+
|
|
4
|
+
def iterate(prompt = nil, &block)
|
|
5
|
+
self.endpoint :responses
|
|
6
|
+
self.user prompt if prompt
|
|
7
|
+
|
|
8
|
+
obj = self.json_format({
|
|
9
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
10
|
+
"type": "object",
|
|
11
|
+
"properties": {
|
|
12
|
+
"content": {
|
|
13
|
+
"type": "array",
|
|
14
|
+
"items": { "type": "string" }
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"required": ["content"],
|
|
18
|
+
"additionalProperties": false
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
self.option :format, :text
|
|
22
|
+
|
|
23
|
+
list = Hash === obj ? obj['content'] : obj
|
|
24
|
+
|
|
25
|
+
list.each &block
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def iterate_dictionary(prompt = nil, &block)
|
|
29
|
+
self.endpoint :responses
|
|
30
|
+
self.user prompt if prompt
|
|
31
|
+
|
|
32
|
+
dict = self.json_format({
|
|
33
|
+
name: 'dictionary',
|
|
34
|
+
type: 'object',
|
|
35
|
+
properties: {},
|
|
36
|
+
additionalProperties: {type: :string}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
self.option :format, :text
|
|
40
|
+
|
|
41
|
+
dict.each &block
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
data/lib/scout/llm/agent.rb
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
require_relative 'ask'
|
|
2
2
|
|
|
3
3
|
module LLM
|
|
4
|
+
def self.agent(...)
|
|
5
|
+
LLM::Agent.new(...)
|
|
6
|
+
end
|
|
7
|
+
|
|
4
8
|
class Agent
|
|
5
|
-
attr_accessor :
|
|
6
|
-
def initialize(
|
|
7
|
-
@system = system
|
|
9
|
+
attr_accessor :workflow, :knowledge_base, :start_chat, :process_exception, :other_options
|
|
10
|
+
def initialize(workflow: nil, knowledge_base: nil, start_chat: nil, **kwargs)
|
|
8
11
|
@workflow = workflow
|
|
12
|
+
@workflow = Workflow.require_workflow @workflow if String === @workflow
|
|
9
13
|
@knowledge_base = knowledge_base
|
|
10
|
-
@
|
|
11
|
-
@
|
|
14
|
+
@other_options = IndiferentHash.setup(kwargs.dup)
|
|
15
|
+
@start_chat = start_chat
|
|
12
16
|
end
|
|
13
17
|
|
|
14
18
|
def format_message(message, prefix = "user")
|
|
@@ -19,17 +23,17 @@ module LLM
|
|
|
19
23
|
|
|
20
24
|
def system_prompt
|
|
21
25
|
system = @system
|
|
26
|
+
system = [] if system.nil?
|
|
22
27
|
system = [system] unless system.nil? || system.is_a?(Array)
|
|
28
|
+
system = [] if system.nil?
|
|
23
29
|
|
|
24
|
-
if @knowledge_base
|
|
30
|
+
if @knowledge_base and @knowledge_base.all_databases.any?
|
|
25
31
|
system << <<-EOF
|
|
26
32
|
You have access to the following databases associating entities:
|
|
27
33
|
EOF
|
|
28
34
|
|
|
29
35
|
knowledge_base.all_databases.each do |database|
|
|
30
|
-
system <<
|
|
31
|
-
* #{database}: #{knowledge_base.source(database)} => #{knowledge_base.target(database)}
|
|
32
|
-
EOF
|
|
36
|
+
system << knowledge_base.markdown(database)
|
|
33
37
|
end
|
|
34
38
|
end
|
|
35
39
|
|
|
@@ -45,34 +49,51 @@ You have access to the following databases associating entities:
|
|
|
45
49
|
end
|
|
46
50
|
|
|
47
51
|
# function: takes an array of messages and calls LLM.ask with them
|
|
48
|
-
def ask(messages, model = nil)
|
|
52
|
+
def ask(messages, model = nil, options = {})
|
|
49
53
|
messages = [messages] unless messages.is_a? Array
|
|
50
|
-
model ||= @model
|
|
51
|
-
|
|
52
|
-
tools = []
|
|
53
|
-
tools += LLM.workflow_tools(workflow) if workflow
|
|
54
|
-
tools += LLM.knowledge_base_tool_definition(knowledge_base) if knowledge_base
|
|
54
|
+
model ||= @model if model
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
56
|
+
tools = options[:tools] || {}
|
|
57
|
+
tools = tools.merge @other_options[:tools] if @other_options[:tools]
|
|
58
|
+
options[:tools] = tools
|
|
59
|
+
begin
|
|
60
|
+
if workflow || knowledge_base
|
|
61
|
+
tools.merge!(LLM.workflow_tools(workflow)) if workflow
|
|
62
|
+
tools.merge!(LLM.knowledge_base_tool_definition(knowledge_base)) if knowledge_base and knowledge_base.all_databases.any?
|
|
63
|
+
options[:tools] = tools
|
|
64
|
+
LLM.ask messages, @other_options.merge(log_errors: true).merge(options)
|
|
63
65
|
else
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
LLM.ask messages, @other_options.merge(log_errors: true).merge(options)
|
|
67
|
+
end
|
|
68
|
+
rescue
|
|
69
|
+
exception = $!
|
|
70
|
+
if Proc === self.process_exception
|
|
71
|
+
try_again = self.process_exception.call exception
|
|
72
|
+
if try_again
|
|
73
|
+
retry
|
|
71
74
|
else
|
|
72
|
-
raise
|
|
75
|
+
raise exception
|
|
73
76
|
end
|
|
77
|
+
else
|
|
78
|
+
raise exception
|
|
74
79
|
end
|
|
75
80
|
end
|
|
76
81
|
end
|
|
82
|
+
|
|
83
|
+
def self.load_from_path(path, workflow: nil, knowledge_base: nil, chat: nil)
|
|
84
|
+
workflow_path = path['workflow.rb'].find
|
|
85
|
+
knowledge_base_path = path['knowledge_base']
|
|
86
|
+
chat_path = path['start_chat']
|
|
87
|
+
|
|
88
|
+
workflow = Workflow.require_workflow workflow_path if workflow_path.exists?
|
|
89
|
+
knowledge_base = KnowledgeBase.new knowledge_base_path if knowledge_base_path.exists?
|
|
90
|
+
chat = LLM.chat chat_path if chat_path.exists?
|
|
91
|
+
|
|
92
|
+
LLM::Agent.new workflow: workflow, knowledge_base: knowledge_base, start_chat: chat
|
|
93
|
+
end
|
|
77
94
|
end
|
|
78
95
|
end
|
|
96
|
+
|
|
97
|
+
require_relative 'agent/chat'
|
|
98
|
+
require_relative 'agent/iterate'
|
|
99
|
+
require_relative 'agent/delegate'
|
data/lib/scout/llm/ask.rb
CHANGED
|
@@ -1,33 +1,75 @@
|
|
|
1
1
|
require 'scout'
|
|
2
|
-
require_relative '
|
|
3
|
-
require_relative 'backends/ollama'
|
|
4
|
-
require_relative 'backends/openwebui'
|
|
5
|
-
require_relative 'backends/relay'
|
|
2
|
+
require_relative 'chat'
|
|
6
3
|
|
|
7
4
|
module LLM
|
|
8
5
|
def self.ask(question, options = {}, &block)
|
|
9
|
-
|
|
6
|
+
messages = LLM.chat(question)
|
|
7
|
+
options = IndiferentHash.add_defaults LLM.options(messages), options
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
agent = IndiferentHash.process_options options, :agent
|
|
10
|
+
|
|
11
|
+
if agent
|
|
12
|
+
agent_file = Scout.workflows[agent]
|
|
13
|
+
|
|
14
|
+
agent_file = Scout.chats[agent] unless agent_file.exists?
|
|
15
|
+
|
|
16
|
+
agent_file = agent_file.find_with_extension('rb') unless agent_file.exists?
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if agent_file.exists?
|
|
20
|
+
if agent_file.directory?
|
|
21
|
+
if agent_file.agent.find_with_extension('rb').exists?
|
|
22
|
+
agent = load agent_file.agent.find_with_extension('rb')
|
|
23
|
+
else
|
|
24
|
+
agent = LLM::Agent.load_from_path agent_file
|
|
25
|
+
end
|
|
26
|
+
else
|
|
27
|
+
agent = load agent_file
|
|
28
|
+
end
|
|
29
|
+
else
|
|
30
|
+
raise "Agent not found: #{agent}"
|
|
31
|
+
end
|
|
32
|
+
return agent.ask(question, options)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
endpoint, persist = IndiferentHash.process_options options, :endpoint, :persist, persist: true
|
|
36
|
+
|
|
37
|
+
endpoint ||= Scout::Config.get :endpoint, :ask, :llm, env: 'ASK_ENDPOINT,LLM_ENDPOINT'
|
|
12
38
|
if endpoint && Scout.etc.AI[endpoint].exists?
|
|
13
39
|
options = IndiferentHash.add_defaults options, Scout.etc.AI[endpoint].yaml
|
|
40
|
+
elsif endpoint && endpoint != ""
|
|
41
|
+
raise "Endpoint not found #{endpoint}"
|
|
14
42
|
end
|
|
15
43
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
44
|
+
Persist.persist(endpoint, :json, prefix: "LLM ask", other: options.merge(messages: messages), persist: persist) do
|
|
45
|
+
backend = IndiferentHash.process_options options, :backend
|
|
46
|
+
backend ||= Scout::Config.get :backend, :ask, :llm, env: 'ASK_BACKEND,LLM_BACKEND', default: :openai
|
|
47
|
+
|
|
48
|
+
case backend
|
|
49
|
+
when :openai, "openai"
|
|
50
|
+
require_relative 'backends/openai'
|
|
51
|
+
LLM::OpenAI.ask(messages, options, &block)
|
|
52
|
+
when :anthropic, "anthropic"
|
|
53
|
+
require_relative 'backends/anthropic'
|
|
54
|
+
LLM::Anthropic.ask(messages, options, &block)
|
|
55
|
+
when :responses, "responses"
|
|
56
|
+
require_relative 'backends/responses'
|
|
57
|
+
LLM::Responses.ask(messages, options, &block)
|
|
58
|
+
when :ollama, "ollama"
|
|
59
|
+
require_relative 'backends/ollama'
|
|
60
|
+
LLM::OLlama.ask(messages, options, &block)
|
|
61
|
+
when :openwebui, "openwebui"
|
|
62
|
+
require_relative 'backends/openwebui'
|
|
63
|
+
LLM::OpenWebUI.ask(messages, options, &block)
|
|
64
|
+
when :relay, "relay"
|
|
65
|
+
require_relative 'backends/relay'
|
|
66
|
+
LLM::Relay.ask(messages, options, &block)
|
|
67
|
+
when :bedrock, "bedrock"
|
|
68
|
+
require_relative 'backends/bedrock'
|
|
69
|
+
LLM::Bedrock.ask(messages, options, &block)
|
|
70
|
+
else
|
|
71
|
+
raise "Unknown backend: #{backend}"
|
|
72
|
+
end
|
|
31
73
|
end
|
|
32
74
|
end
|
|
33
75
|
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
require 'scout'
|
|
2
|
+
require 'anthropic'
|
|
3
|
+
require_relative '../chat'
|
|
4
|
+
|
|
5
|
+
module LLM
|
|
6
|
+
module Anthropic
|
|
7
|
+
|
|
8
|
+
def self.client(url = nil, key = nil, log_errors = false, request_timeout: 1200)
|
|
9
|
+
url ||= Scout::Config.get(:url, :openai_ask, :ask, :anthropic, env: 'ANTHROPIC_URL')
|
|
10
|
+
key ||= LLM.get_url_config(:key, url, :openai_ask, :ask, :anthropic, env: 'ANTHROPIC_KEY')
|
|
11
|
+
Object::Anthropic::Client.new(access_token:key, log_errors: log_errors, uri_base: url, request_timeout: request_timeout)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.process_input(messages)
|
|
15
|
+
messages.collect do |message|
|
|
16
|
+
if message[:role] == 'image'
|
|
17
|
+
Log.warn "Endpoint 'anthropic' does not support images, try 'responses': #{message[:content]}"
|
|
18
|
+
next
|
|
19
|
+
else
|
|
20
|
+
message
|
|
21
|
+
end
|
|
22
|
+
end.flatten.compact
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.process_response(response, tools, &block)
|
|
26
|
+
Log.debug "Respose: #{Log.fingerprint response}"
|
|
27
|
+
|
|
28
|
+
response['content'].collect do |output|
|
|
29
|
+
case output['type']
|
|
30
|
+
when 'text'
|
|
31
|
+
IndiferentHash.setup({role: :assistant, content: output['text']})
|
|
32
|
+
when 'reasoning'
|
|
33
|
+
next
|
|
34
|
+
when 'tool_use'
|
|
35
|
+
LLM.process_calls(tools, [output], &block)
|
|
36
|
+
when 'web_search_call'
|
|
37
|
+
next
|
|
38
|
+
else
|
|
39
|
+
eee response
|
|
40
|
+
eee output
|
|
41
|
+
raise
|
|
42
|
+
end
|
|
43
|
+
end.compact.flatten
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def self.ask(question, options = {}, &block)
|
|
48
|
+
original_options = options.dup
|
|
49
|
+
|
|
50
|
+
messages = LLM.chat(question)
|
|
51
|
+
options = options.merge LLM.options messages
|
|
52
|
+
|
|
53
|
+
options = IndiferentHash.add_defaults options, max_tokens: 1000
|
|
54
|
+
|
|
55
|
+
client, url, key, model, log_errors, return_messages, format, tool_choice_next, previous_response_id, tools = IndiferentHash.process_options options,
|
|
56
|
+
:client, :url, :key, :model, :log_errors, :return_messages, :format, :tool_choice_next, :previous_response_id, :tools,
|
|
57
|
+
log_errors: true, tool_choice_next: :none
|
|
58
|
+
|
|
59
|
+
if client.nil?
|
|
60
|
+
url ||= Scout::Config.get(:url, :openai_ask, :ask, :anthropic, env: 'ANTHROPIC_URL')
|
|
61
|
+
key ||= LLM.get_url_config(:key, url, :openai_ask, :ask, :anthropic, env: 'ANTHROPIC_KEY')
|
|
62
|
+
client = self.client url, key, log_errors
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
if model.nil?
|
|
66
|
+
url ||= Scout::Config.get(:url, :openai_ask, :ask, :anthropic, env: 'ANTHROPIC_URL')
|
|
67
|
+
model ||= LLM.get_url_config(:model, url, :openai_ask, :ask, :anthropic, env: 'ANTHROPIC_MODEL', default: "claude-sonnet-4-20250514")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
case format.to_sym
|
|
71
|
+
when :json, :json_object
|
|
72
|
+
options[:response_format] = {type: 'json_object'}
|
|
73
|
+
else
|
|
74
|
+
options[:response_format] = {type: format}
|
|
75
|
+
end if format
|
|
76
|
+
|
|
77
|
+
parameters = options.merge(model: model)
|
|
78
|
+
|
|
79
|
+
# Process tools
|
|
80
|
+
|
|
81
|
+
case tools
|
|
82
|
+
when Array
|
|
83
|
+
tools = tools.inject({}) do |acc,definition|
|
|
84
|
+
IndiferentHash.setup definition
|
|
85
|
+
name = definition.dig('name') || definition.dig('function', 'name')
|
|
86
|
+
acc.merge(name => definition)
|
|
87
|
+
end
|
|
88
|
+
when nil
|
|
89
|
+
tools = {}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
tools.merge!(LLM.tools messages)
|
|
93
|
+
tools.merge!(LLM.associations messages)
|
|
94
|
+
|
|
95
|
+
if tools.any?
|
|
96
|
+
parameters[:tools] = tools.values.collect{|obj,definition| Hash === obj ? obj : definition}
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
parameters[:tools] = parameters[:tools].collect do |info|
|
|
100
|
+
IndiferentHash.setup(info)
|
|
101
|
+
info[:type] = 'custom' if info[:type] == 'function'
|
|
102
|
+
info[:input_schema] = info.delete('parameters') if info["parameters"]
|
|
103
|
+
info
|
|
104
|
+
end if parameters[:tools]
|
|
105
|
+
|
|
106
|
+
messages = self.process_input messages
|
|
107
|
+
|
|
108
|
+
Log.low "Calling anthropic #{url}: #{Log.fingerprint parameters}}"
|
|
109
|
+
|
|
110
|
+
parameters[:messages] = LLM.tools_to_anthropic messages
|
|
111
|
+
|
|
112
|
+
response = self.process_response client.messages(parameters: parameters), tools, &block
|
|
113
|
+
|
|
114
|
+
res = if response.last[:role] == 'function_call_output'
|
|
115
|
+
#response + self.ask(messages + response, original_options.merge(tool_choice: tool_choice_next, return_messages: true, tools: tools ), &block)
|
|
116
|
+
response + self.ask(messages + response, original_options.merge(return_messages: true, tools: tools ), &block)
|
|
117
|
+
else
|
|
118
|
+
response
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
if return_messages
|
|
122
|
+
res
|
|
123
|
+
else
|
|
124
|
+
res.last['content']
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def self.embed(text, options = {})
|
|
129
|
+
|
|
130
|
+
client, url, key, model, log_errors = IndiferentHash.process_options options, :client, :url, :key, :model, :log_errors
|
|
131
|
+
|
|
132
|
+
if client.nil?
|
|
133
|
+
url ||= Scout::Config.get(:url, :openai_embed, :embed, :anthropic, env: 'ANTHROPIC_URL')
|
|
134
|
+
key ||= LLM.get_url_config(:key, url, :openai_embed, :embed, :anthropic, env: 'ANTHROPIC_KEY')
|
|
135
|
+
client = self.client url, key, log_errors
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
if model.nil?
|
|
139
|
+
url ||= Scout::Config.get(:url, :openai_embed, :embed, :anthropic, env: 'ANTHROPIC_URL')
|
|
140
|
+
model ||= LLM.get_url_config(:model, url, :openai_embed, :embed, :anthropic, env: 'ANTHROPIC_MODEL', default: "gpt-3.5-turbo")
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
response = client.embeddings(parameters: {input: text, model: model})
|
|
144
|
+
response.dig('data', 0, 'embedding')
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
require 'scout'
|
|
2
|
+
require 'aws-sdk-bedrockruntime'
|
|
3
|
+
require_relative '../parse'
|
|
4
|
+
require_relative '../tools'
|
|
5
|
+
require_relative '../utils'
|
|
6
|
+
|
|
7
|
+
module LLM
|
|
8
|
+
module Bedrock
|
|
9
|
+
def self.client(region, access_key, secret_key)
|
|
10
|
+
|
|
11
|
+
credentials = Aws::Credentials.new(access_key, secret_key) if access_key and secret_key
|
|
12
|
+
options = {}
|
|
13
|
+
options[:region] = region if region
|
|
14
|
+
options[:credentials] = credentials if credentials
|
|
15
|
+
Aws::BedrockRuntime::Client.new(options)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.messages_to_prompt(messages)
|
|
19
|
+
system = []
|
|
20
|
+
user = []
|
|
21
|
+
messages.each do |info|
|
|
22
|
+
role, content = info.values_at :role, :content
|
|
23
|
+
if role.to_s == 'system'
|
|
24
|
+
system << content
|
|
25
|
+
else
|
|
26
|
+
user << content
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
[system*"\n", user*"\n"]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.ask(question, options = {}, &block)
|
|
33
|
+
client, region, access_key, secret_key, type = IndiferentHash.process_options options, :client, :region, :access_key, :secret_key, :type
|
|
34
|
+
|
|
35
|
+
model_options = IndiferentHash.pull_keys options, :model
|
|
36
|
+
model = IndiferentHash.process_options model_options, :model
|
|
37
|
+
|
|
38
|
+
if client.nil?
|
|
39
|
+
region ||= Scout::Config.get(:region, :bedrock_ask, :ask, :bedrock, env: 'AWS_REGION')
|
|
40
|
+
access_key ||= LLM.get_url_config(:access_key, nil, :bedrock_ask, :ask, :bedrock, env: 'AWS_ACCESS_KEY_ID')
|
|
41
|
+
secret_key ||= LLM.get_url_config(:secret_key, nil, :bedrock_ask, :ask, :bedrock, env: 'AWS_SECRET_ACCESS_KEY')
|
|
42
|
+
client = self.client(region, access_key, secret_key)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
model ||= Scout::Config.get(:model, :bedrock_ask, :ask, :bedrock, env: 'BEDROCK_MODEL_ID')
|
|
46
|
+
type ||= Scout::Config.get(:type, model, default: :messages)
|
|
47
|
+
|
|
48
|
+
role, previous_response_id, tools = IndiferentHash.process_options options, :role, :previous_response_id, :tools
|
|
49
|
+
messages = LLM.parse(question, role)
|
|
50
|
+
|
|
51
|
+
case type.to_sym
|
|
52
|
+
when :messages
|
|
53
|
+
body = model_options.merge({
|
|
54
|
+
system: messages.select{|m| m[:role] == 'system'}.collect{|m| m[:content]}*"\n",
|
|
55
|
+
messages: messages.select{|m| m[:role] == 'user'}
|
|
56
|
+
})
|
|
57
|
+
when :prompt
|
|
58
|
+
system, user = messages_to_prompt messages
|
|
59
|
+
body = model_options.merge({
|
|
60
|
+
prompt: user
|
|
61
|
+
})
|
|
62
|
+
else
|
|
63
|
+
raise "Unkown type #{type}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
Log.debug "Calling bedrock with model: #{model} parameters: #{Log.fingerprint body}"
|
|
67
|
+
|
|
68
|
+
response = client.invoke_model(
|
|
69
|
+
model_id: model,
|
|
70
|
+
content_type: 'application/json',
|
|
71
|
+
body: body.to_json
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
result = JSON.parse(response.body.string)
|
|
75
|
+
Log.debug "Response: #{Log.fingerprint result}"
|
|
76
|
+
message = result
|
|
77
|
+
tool_calls = message.dig('content').select{|m| m['tool_calls']}
|
|
78
|
+
|
|
79
|
+
while tool_calls && tool_calls.any?
|
|
80
|
+
messages << message
|
|
81
|
+
|
|
82
|
+
cpus = Scout::Config.get :cpus, :tool_calling, default: 3
|
|
83
|
+
tool_calls.each do |tool_call|
|
|
84
|
+
response_message = LLM.tool_response(tool_call, &block)
|
|
85
|
+
messages << response_message
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
body[:messages] = messages.compact
|
|
89
|
+
Log.debug "Calling bedrock with parameters: #{Log.fingerprint body}"
|
|
90
|
+
response = client.invoke_model(
|
|
91
|
+
model_id: model,
|
|
92
|
+
content_type: 'application/json',
|
|
93
|
+
body: body.to_json
|
|
94
|
+
)
|
|
95
|
+
result = JSON.parse(response.body.string)
|
|
96
|
+
Log.debug "Response: #{Log.fingerprint result}"
|
|
97
|
+
|
|
98
|
+
message = result
|
|
99
|
+
tool_calls = message.dig('content').select{|m| m['tool_calls']}
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
message.dig('content').collect{|m|
|
|
103
|
+
m['text']
|
|
104
|
+
} * "\n"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def self.embed(text, options = {})
|
|
108
|
+
client, region, access_key, secret_key, model = IndiferentHash.process_options options, :client, :region, :access_key, :secret_key, :model
|
|
109
|
+
|
|
110
|
+
if client.nil?
|
|
111
|
+
region ||= Scout::Config.get(:region, :bedrock_embed, :embed, :bedrock, env: 'AWS_REGION')
|
|
112
|
+
access_key ||= LLM.get_url_config(:access_key, nil, :bedrock_embed, :embed, :bedrock, env: 'AWS_ACCESS_KEY_ID')
|
|
113
|
+
secret_key ||= LLM.get_url_config(:secret_key, nil, :bedrock_embed, :embed, :bedrock, env: 'AWS_SECRET_ACCESS_KEY')
|
|
114
|
+
client = self.client(region, access_key, secret_key)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
model ||= Scout::Config.get(:model, :bedrock_embed, :embed, :bedrock, env: 'BEDROCK_EMBED_MODEL_ID', default: 'amazon.titan-embed-text-v1')
|
|
118
|
+
|
|
119
|
+
response = client.invoke_model(
|
|
120
|
+
model_id: model,
|
|
121
|
+
content_type: 'application/json',
|
|
122
|
+
body: { inputText: text }.to_json
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
result = JSON.parse(response.body.string)
|
|
126
|
+
result['embedding']
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|