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.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/.vimproject +155 -9
  3. data/README.md +296 -0
  4. data/Rakefile +3 -0
  5. data/VERSION +1 -1
  6. data/bin/scout-ai +2 -0
  7. data/doc/Agent.md +279 -0
  8. data/doc/Chat.md +258 -0
  9. data/doc/LLM.md +446 -0
  10. data/doc/Model.md +513 -0
  11. data/doc/RAG.md +129 -0
  12. data/lib/scout/llm/agent/chat.rb +74 -0
  13. data/lib/scout/llm/agent/delegate.rb +39 -0
  14. data/lib/scout/llm/agent/iterate.rb +44 -0
  15. data/lib/scout/llm/agent.rb +51 -30
  16. data/lib/scout/llm/ask.rb +63 -21
  17. data/lib/scout/llm/backends/anthropic.rb +147 -0
  18. data/lib/scout/llm/backends/bedrock.rb +129 -0
  19. data/lib/scout/llm/backends/huggingface.rb +6 -21
  20. data/lib/scout/llm/backends/ollama.rb +62 -35
  21. data/lib/scout/llm/backends/openai.rb +77 -33
  22. data/lib/scout/llm/backends/openwebui.rb +1 -1
  23. data/lib/scout/llm/backends/relay.rb +3 -2
  24. data/lib/scout/llm/backends/responses.rb +320 -0
  25. data/lib/scout/llm/chat.rb +703 -0
  26. data/lib/scout/llm/embed.rb +4 -4
  27. data/lib/scout/llm/mcp.rb +28 -0
  28. data/lib/scout/llm/parse.rb +71 -13
  29. data/lib/scout/llm/rag.rb +9 -0
  30. data/lib/scout/llm/tools/call.rb +66 -0
  31. data/lib/scout/llm/tools/knowledge_base.rb +158 -0
  32. data/lib/scout/llm/tools/mcp.rb +59 -0
  33. data/lib/scout/llm/tools/workflow.rb +69 -0
  34. data/lib/scout/llm/tools.rb +112 -76
  35. data/lib/scout/llm/utils.rb +17 -10
  36. data/lib/scout/model/base.rb +19 -0
  37. data/lib/scout/model/python/base.rb +25 -0
  38. data/lib/scout/model/python/huggingface/causal/next_token.rb +23 -0
  39. data/lib/scout/model/python/huggingface/causal.rb +29 -0
  40. data/lib/scout/model/python/huggingface/classification +0 -0
  41. data/lib/scout/model/python/huggingface/classification.rb +50 -0
  42. data/lib/scout/model/python/huggingface.rb +112 -0
  43. data/lib/scout/model/python/torch/dataloader.rb +57 -0
  44. data/lib/scout/model/python/torch/helpers.rb +84 -0
  45. data/lib/scout/model/python/torch/introspection.rb +34 -0
  46. data/lib/scout/model/python/torch/load_and_save.rb +47 -0
  47. data/lib/scout/model/python/torch.rb +94 -0
  48. data/lib/scout/model/util/run.rb +181 -0
  49. data/lib/scout/model/util/save.rb +81 -0
  50. data/lib/scout-ai.rb +4 -1
  51. data/python/scout_ai/__init__.py +35 -0
  52. data/python/scout_ai/huggingface/data.py +48 -0
  53. data/python/scout_ai/huggingface/eval.py +60 -0
  54. data/python/scout_ai/huggingface/model.py +29 -0
  55. data/python/scout_ai/huggingface/rlhf.py +83 -0
  56. data/python/scout_ai/huggingface/train/__init__.py +34 -0
  57. data/python/scout_ai/huggingface/train/next_token.py +315 -0
  58. data/python/scout_ai/util.py +32 -0
  59. data/scout-ai.gemspec +143 -0
  60. data/scout_commands/agent/ask +89 -14
  61. data/scout_commands/agent/kb +15 -0
  62. data/scout_commands/documenter +148 -0
  63. data/scout_commands/llm/ask +71 -12
  64. data/scout_commands/llm/process +4 -2
  65. data/scout_commands/llm/server +319 -0
  66. data/share/server/chat.html +138 -0
  67. data/share/server/chat.js +468 -0
  68. data/test/data/cat.jpg +0 -0
  69. data/test/scout/llm/agent/test_chat.rb +14 -0
  70. data/test/scout/llm/backends/test_anthropic.rb +134 -0
  71. data/test/scout/llm/backends/test_bedrock.rb +60 -0
  72. data/test/scout/llm/backends/test_huggingface.rb +3 -3
  73. data/test/scout/llm/backends/test_ollama.rb +48 -10
  74. data/test/scout/llm/backends/test_openai.rb +134 -10
  75. data/test/scout/llm/backends/test_responses.rb +239 -0
  76. data/test/scout/llm/test_agent.rb +0 -70
  77. data/test/scout/llm/test_ask.rb +4 -1
  78. data/test/scout/llm/test_chat.rb +256 -0
  79. data/test/scout/llm/test_mcp.rb +29 -0
  80. data/test/scout/llm/test_parse.rb +81 -2
  81. data/test/scout/llm/tools/test_call.rb +0 -0
  82. data/test/scout/llm/tools/test_knowledge_base.rb +22 -0
  83. data/test/scout/llm/tools/test_mcp.rb +11 -0
  84. data/test/scout/llm/tools/test_workflow.rb +39 -0
  85. data/test/scout/model/python/huggingface/causal/test_next_token.rb +59 -0
  86. data/test/scout/model/python/huggingface/test_causal.rb +33 -0
  87. data/test/scout/model/python/huggingface/test_classification.rb +30 -0
  88. data/test/scout/model/python/test_base.rb +44 -0
  89. data/test/scout/model/python/test_huggingface.rb +9 -0
  90. data/test/scout/model/python/test_torch.rb +71 -0
  91. data/test/scout/model/python/torch/test_helpers.rb +14 -0
  92. data/test/scout/model/test_base.rb +117 -0
  93. data/test/scout/model/util/test_save.rb +31 -0
  94. metadata +113 -7
  95. data/README.rdoc +0 -18
  96. 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
@@ -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 :system, :workflow, :knowledge_base
6
- def initialize(system = nil, workflow: nil, knowledge_base: nil, model: nil, **kwargs)
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
- @model = model
11
- @other_options = kwargs
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 << <<-EOF.strip + (knowledge_base.undirected(database) ? ". Undirected" : "")
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
- LLM.ask prompt(messages), @other_options.merge(model: model, log_errors: true, tools: tools) do |name,parameters|
57
- case name
58
- when 'children'
59
- parameters = IndiferentHash.setup(parameters)
60
- database, entities = parameters.values_at "database", "entities"
61
- Log.high "Finding #{entities} children in #{database}"
62
- knowledge_base.children(database, entities).target
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
- if workflow
65
- begin
66
- Log.high "Calling #{workflow}##{name} with #{Log.fingerprint parameters}"
67
- workflow.job(name, parameters).run
68
- rescue
69
- $!.message
70
- end
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 "What?"
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 'backends/openai'
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
- endpoint = IndiferentHash.process_options options, :endpoint
6
+ messages = LLM.chat(question)
7
+ options = IndiferentHash.add_defaults LLM.options(messages), options
10
8
 
11
- endpoint ||= Scout::Config.get :endpoint, :ask, :llm, env: 'ASK_ENDPOINT,LLM_ENDPOINT', default: :openai
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
- backend = IndiferentHash.process_options options, :backend
17
- backend ||= Scout::Config.get :backend, :ask, :llm, env: 'ASK_BACKEND,LLM_BACKEND', default: :openai
18
-
19
-
20
- case backend
21
- when :openai, "openai"
22
- LLM::OpenAI.ask(question, options, &block)
23
- when :ollama, "ollama"
24
- LLM::OLlama.ask(question, options, &block)
25
- when :openwebui, "openwebui"
26
- LLM::OpenWebUI.ask(question, options, &block)
27
- when :relay, "relay"
28
- LLM::Relay.ask(question, options, &block)
29
- else
30
- raise "Unknown backend: #{backend}"
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