Module: LLM
- Defined in:
- lib/scout/llm/ask.rb,
lib/scout/llm/rag.rb,
lib/scout/llm/chat.rb,
lib/scout/llm/agent.rb,
lib/scout/llm/embed.rb,
lib/scout/llm/tools.rb,
lib/scout/llm/utils.rb,
lib/scout/llm/tools/mcp.rb,
lib/scout/llm/agent/chat.rb,
lib/scout/llm/tools/call.rb,
lib/scout/llm/agent/iterate.rb,
lib/scout/llm/backends/vllm.rb,
lib/scout/llm/agent/delegate.rb,
lib/scout/llm/backends/relay.rb,
lib/scout/llm/tools/workflow.rb,
lib/scout/llm/backends/ollama.rb,
lib/scout/llm/backends/openai.rb,
lib/scout/llm/backends/bedrock.rb,
lib/scout/llm/backends/default.rb,
lib/scout/llm/backends/anthropic.rb,
lib/scout/llm/backends/openwebui.rb,
lib/scout/llm/backends/responses.rb,
lib/scout/llm/backends/huggingface.rb,
lib/scout/llm/tools/knowledge_base.rb
Defined Under Namespace
Modules: Anthropic, AnthropicMethods, Backend, Bedrock, Huggingface, HuggingfaceMethods, OLlama, OLlamaMethods, OpenAI, OpenAIMethods, OpenWebUI, OpenWebUIMethods, Relay, Responses, ResponsesMethods, VLLM, VLLMMethods Classes: Agent, RAG
Constant Summary collapse
- BACKENDS =
IndiferentHash.setup({})
Instance Attribute Summary collapse
-
#max_content_length ⇒ Object
Returns the value of attribute max_content_length.
Class Method Summary collapse
- .agent ⇒ Object
- .ask(question, options = {}, &block) ⇒ Object
- .associations ⇒ Object
- .call_id_name_and_arguments(tool_call) ⇒ Object
- .call_knowledge_base(knowledge_base, database, parameters = {}) ⇒ Object
- .call_tools(tool_calls, &block) ⇒ Object
- .call_workflow(workflow, task_name, parameters = {}) ⇒ Object
- .chat(file = [], original = nil) ⇒ Object
- .database_details_tool_definition(database, undirected, fields) ⇒ Object
- .database_tool_definition(database, undirected = false, database_description = nil) ⇒ Object
- .embed(text, options = {}) ⇒ Object
- .get_url_config(key, url = nil, *tokens) ⇒ Object
- .get_url_server_tokens(url, prefix = nil) ⇒ Object
- .knowledge_base_ask(knowledge_base, question, options = {}) ⇒ Object
- .knowledge_base_tool_definition(knowledge_base, databases = nil) ⇒ Object
- .load_agent ⇒ Object
- .mcp_tools(url, options = {}) ⇒ Object
- .messages(question, role = nil) ⇒ Object
- .options ⇒ Object
- .print ⇒ Object
- .process_calls(tools, calls, &block) ⇒ Object
- .purge ⇒ Object
- .register_backend(name, mod) ⇒ Object
- .run_tools(messages) ⇒ Object
- .task_tool_definition(workflow, task_name, inputs = nil) ⇒ Object
- .tool_response(tool_call, &block) ⇒ Object
- .tools ⇒ Object
- .workflow_ask(workflow, question, options = {}) ⇒ Object
- .workflow_tools(workflow, tasks = nil) ⇒ Object
Instance Method Summary collapse
Instance Attribute Details
#max_content_length ⇒ Object
Returns the value of attribute max_content_length.
3 4 5 |
# File 'lib/scout/llm/tools/call.rb', line 3 def max_content_length @max_content_length end |
Class Method Details
.agent ⇒ Object
4 5 6 |
# File 'lib/scout/llm/agent.rb', line 4 def self.agent(...) LLM::Agent.new(...) end |
.ask(question, options = {}, &block) ⇒ Object
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
# File 'lib/scout/llm/ask.rb', line 12 def self.ask(question, = {}, &block) = LLM.chat(question) = IndiferentHash.add_defaults LLM.(), endpoint, persist = IndiferentHash. , :endpoint, :persist, persist: true endpoint ||= Scout::Config.get :endpoint, :ask, :llm, env: 'ASK_ENDPOINT,LLM_ENDPOINT,ENDPOINT,LLM,ASK' if endpoint && Scout.etc.AI[endpoint].find_with_extension(:yaml).exists? = IndiferentHash.add_defaults , Scout.etc.AI[endpoint].yaml elsif endpoint && endpoint != "" raise "Endpoint not found #{endpoint}" end agent_name = IndiferentHash. , :agent agent_name = nil if %(none false nil).include?(agent_name.to_s) if agent_name [:endpoint] ||= endpoint agent = LLM::Agent.load_agent agent_name agent.follow res = agent.ask return res end = Chat.() [:current_meta] = if and .any? if [:backend].to_s == 'responses' && [:previous_response].to_s != 'false' = Chat.clear(, 'previous_response_id') else = Chat.clean(, 'previous_response_id') .delete :previous_response_id end Log.high Log.color :green, "Asking #{endpoint || [:endpoint] || 'client'}: #{[:previous_response_id]}\n" + Chat.print_brief() tools = [:tools] Log.medium "Tools: #{Log.fingerprint tools.keys}" if tools Log.debug "#{Log.fingerprint tools}}" if tools res = Persist.persist(endpoint, :json, prefix: "LLM ask", other: .merge(messages: ), persist: persist, dir: Scout.var.cache.ask) do backend = IndiferentHash. , :backend backend ||= Scout::Config.get :backend, :ask, :llm, env: 'ASK_BACKEND,LLM_BACKEND', default: :responses case backend when :openai, "openai" require_relative 'backends/openai' LLM::OpenAI.ask(, , &block) when :anthropic, "anthropic" require_relative 'backends/anthropic' LLM::Anthropic.ask(, , &block) when :responses, "responses" require_relative 'backends/responses' LLM::Responses.ask(, , &block) when :ollama, "ollama" require_relative 'backends/ollama' LLM::OLlama.ask(, , &block) when :vllm, "vllm" require_relative 'backends/vllm' LLM::VLLM.ask(, , &block) when :openwebui, "openwebui" require_relative 'backends/openwebui' LLM::OpenWebUI.ask(, , &block) when :huggingface, "huggingface" require_relative 'backends/huggingface' LLM::Huggingface.ask(, , &block) when :relay, "relay" require_relative 'backends/relay' LLM::Relay.ask(, , &block) when :bedrock, "bedrock" require_relative 'backends/bedrock' LLM::Bedrock.ask(, , &block) else mod = BACKENDS[backend] raise "Unknown backend: #{backend}" if mod.nil? mod.ask(, , &block) end end Chat.setup res if Array === res Log.high Log.color :blue, "Response:\n" + Chat.print_brief(res, %w(meta assistant)) if Array === res res end |
.associations ⇒ Object
59 60 61 |
# File 'lib/scout/llm/chat.rb', line 59 def self.associations(...) Chat.associations(...) end |
.call_id_name_and_arguments(tool_call) ⇒ Object
5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# File 'lib/scout/llm/tools/call.rb', line 5 def self.call_id_name_and_arguments(tool_call) tool_call_id = tool_call.dig("call_id") || tool_call.dig("id") || tool_call.dig('tool_call_id') if tool_call['function'] function_name = tool_call.dig("function", "name") function_arguments = tool_call.dig("function", "arguments") else function_name = tool_call.dig("name") function_arguments = tool_call.dig("arguments") end function_arguments = JSON.parse(function_arguments, { symbolize_names: true }) if String === function_arguments [tool_call_id, function_name, function_arguments] end |
.call_knowledge_base(knowledge_base, database, parameters = {}) ⇒ Object
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
# File 'lib/scout/llm/tools/knowledge_base.rb', line 131 def self.call_knowledge_base(knowledge_base, database, parameters={}) if database.end_with?('_association_details') database = database.sub('_association_details', '') associations, fields = IndiferentHash. parameters, :associations, :fields # Dumb associations = JSON.parse associations if String === associations index = knowledge_base.get_index(database) if fields field_pos = fields.collect{|f| index.identify_field f } associations.each_with_object({}) do |a,hash| values = index[a] next if values.nil? hash[a] = values.values_at *field_pos end else associations.each_with_object({}) do |a,hash| values = index[a] next if values.nil? hash[a] = values.to_hash end end else entities, reverse = IndiferentHash. parameters, :entities, :reverse # Dumb entities = JSON.parse entities if String === entities if reverse knowledge_base.parents(database, entities) else knowledge_base.children(database, entities) end end end |
.call_tools(tool_calls, &block) ⇒ Object
7 8 9 10 11 12 13 14 15 16 17 |
# File 'lib/scout/llm/tools.rb', line 7 def self.call_tools(tool_calls, &block) tool_calls.collect{|tool_call| = LLM.tool_response(tool_call, &block) function_call = tool_call function_call['id'] = tool_call.delete('call_id') if tool_call.dig('call_id') [ {role: "function_call", content: tool_call.to_json}, {role: "function_call_output", content: .to_json}, ] }.flatten end |
.call_workflow(workflow, task_name, parameters = {}) ⇒ Object
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
# File 'lib/scout/llm/tools/workflow.rb', line 98 def self.call_workflow(workflow, task_name, parameters={}) parameters = {} if parameters.nil? jobname, return_path, exec_type, allow_recursive = IndiferentHash. parameters, :jobname, :return_path, :exec_type, :allow_recursive begin job = workflow.job(task_name.to_sym, jobname, parameters) if workflow.exec_exports.include?(task_name.to_sym) || exec_type.to_s == 'exec' job.exec else if return_path job.run(true) job.path else raise ScoutException, 'Potential recursive call' if allow_recursive != 'true' && (job.running? and job.info[:pid] == Process.pid) job end end rescue ScoutException return $! end end |
.chat(file = [], original = nil) ⇒ Object
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
# File 'lib/scout/llm/chat.rb', line 24 def self.chat(file = [], original = nil) original ||= (String === file and Open.exists?(file)) ? file : Path.setup($0.dup) caller_lib_dir = Path.caller_lib_dir(nil, 'chats') if Path.is_filename? file = self. Open.read(file), file else = self. file end = Chat.indiferent = Chat.imports , original, caller_lib_dir = Chat.clear = Chat.clean = Chat.tasks = Chat.jobs = Chat.files , original, caller_lib_dir Chat.setup end |
.database_details_tool_definition(database, undirected, fields) ⇒ Object
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
# File 'lib/scout/llm/tools/knowledge_base.rb', line 61 def self.database_details_tool_definition(database, undirected, fields) if undirected properties = { associations: { type: "array", items: { type: :string }, description: "Associations in the form of source~target or target~source" }, fields: { type: "array", items: { type: :string }, description: "Limit the response to these fields" }, } else properties = { associations: { type: "array", items: { type: :string }, description: "Associations in the form of source~target" }, } end if fields.length > 1 description = <<-EOF Return details of association as a dictionary object. 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. The fields are: #{fields * ', '}. Multiple values may be present and use the charater ";" to separate them. EOF else properties.delete(:fields) description = <<-EOF Return the #{fields.first} of association. Multiple values may be present and use the charater ";" to separate them. EOF end function = { name: database.to_s + '_association_details', description: description, parameters: { type: "object", properties: properties, required: ['associations'] } } IndiferentHash.setup function end |
.database_tool_definition(database, undirected = false, database_description = nil) ⇒ Object
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
# File 'lib/scout/llm/tools/knowledge_base.rb', line 4 def self.database_tool_definition(database, undirected = false, database_description = nil) if undirected properties = { entities: { type: "array", items: { type: :string }, description: "Entities for which to find associations" }, } else properties = { entities: { type: "array", items: { type: :string }, description: 'Source entities in the association, or target entities if "reverse" is "true"' }, reverse: { type: "boolean", description: 'Look for targets instead of sources, defaults to "false"' } } end if database_description and not database_description.strip.empty? description = <<-EOF Find associations for a list of entities in database #{database}: #{database_description} EOF else description = <<-EOF Find associations for a list of entities in database #{database}. EOF end if undirected description += <<-EOF Returns a list in the format entity~partner. EOF else description += <<-EOF Returns a list in the format source~target. EOF end function = { name: database, description: description, parameters: { type: "object", properties: properties, required: ['entities'] } } IndiferentHash.setup function.merge(type: 'function', function: function) end |
.embed(text, options = {}) ⇒ Object
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
# File 'lib/scout/llm/embed.rb', line 4 def self.(text, = {}) endpoint = IndiferentHash. , :endpoint endpoint ||= Scout::Config.get :endpoint, :embed, :llm, env: 'EMBED_ENDPOINT,LLM_ENDPOINT', default: :openai if endpoint && Scout.etc.AI[endpoint].exists? = IndiferentHash.add_defaults , Scout.etc.AI[endpoint].yaml end backend = IndiferentHash. , :backend backend ||= Scout::Config.get :backend, :embed, :llm, env: 'EMBED_BACKEND,LLM_BACKEND', default: :openai case backend when :openai, "openai" require_relative 'backends/openai' LLM::OpenAI.(text, ) when :responses, "responses" require_relative 'backends/responses' LLM::OpenAI.(text, ) when :ollama, "ollama" require_relative 'backends/ollama' LLM::OLlama.(text, ) when :openwebui, "openwebui" require_relative 'backends/openwebui' LLM::OpenWebUI.(text, ) when :huggingface, "huggingface" require_relative 'backends/huggingface' LLM::Huggingface.(text, ) when :relay, "relay" require_relative 'backends/relay' LLM::Relay.(text, ) else raise "Unknown backend: #{backend}" end end |
.get_url_config(key, url = nil, *tokens) ⇒ Object
15 16 17 18 19 20 21 22 23 24 |
# File 'lib/scout/llm/utils.rb', line 15 def self.get_url_config(key, url = nil, *tokens) hash = tokens.pop if Hash === tokens.last if url url_tokens = tokens.inject([]){|acc,prefix| acc.concat(get_url_server_tokens(url, prefix))} all_tokens = url_tokens + tokens else all_tokens = tokens end Scout::Config.get(key, *all_tokens, hash) end |
.get_url_server_tokens(url, prefix = nil) ⇒ Object
2 3 4 5 6 7 8 9 10 11 12 13 |
# File 'lib/scout/llm/utils.rb', line 2 def self.get_url_server_tokens(url, prefix=nil) return get_url_server_tokens(url).collect{|e| prefix.to_s + "." + e } if prefix server = url.match(/(?:https?:\/\/)?([^\/:]*)/)[1] || "NOSERVER" parts = server.split(".") parts.pop if parts.last.length <= 3 combinations = [] (1..parts.length).each do |l| parts.each_cons(l){|p| combinations << p*"."} end (parts + combinations + [server]).uniq end |
.knowledge_base_ask(knowledge_base, question, options = {}) ⇒ Object
103 104 105 106 107 108 109 110 111 |
# File 'lib/scout/llm/ask.rb', line 103 def self.knowledge_base_ask(knowledge_base, question, = {}) knowledge_base_tools = LLM.knowledge_base_tool_definition(knowledge_base) self.ask(question, .merge(tools: knowledge_base_tools)) do |task_name,parameters| parameters = IndiferentHash.setup(parameters) database, entities = parameters.values_at "database", "entities" Log.info "Finding #{entities} children in #{database}" knowledge_base.children(database, entities).collect{|e| e.sub('~', '=>')} end end |
.knowledge_base_tool_definition(knowledge_base, databases = nil) ⇒ Object
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/scout/llm/tools/knowledge_base.rb', line 115 def self.knowledge_base_tool_definition(knowledge_base, databases = nil) databases ||= knowledge_base.all_databases databases.inject({}){|tool_definitions,database| database_description = knowledge_base.description(database) undirected = knowledge_base.undirected(database) definition = self.database_tool_definition(database, undirected, database_description) tool_definitions.merge!(database => [knowledge_base, definition]) if (fields = knowledge_base.get_database(database).fields).any? details_definition = self.database_details_tool_definition(database, undirected, fields) tool_definitions.merge!(database.to_s + '_association_details' => [knowledge_base, details_definition]) end tool_definitions } end |
.load_agent ⇒ Object
8 9 10 |
# File 'lib/scout/llm/agent.rb', line 8 def self.load_agent(...) LLM::Agent.load_agent(...) end |
.mcp_tools(url, options = {}) ⇒ Object
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
# File 'lib/scout/llm/tools/mcp.rb', line 5 def self.mcp_tools(url, = {}) timeout = Scout::Config.get :timeout, :mcp, :tools = IndiferentHash.add_defaults , read_timeout: timeout.to_i if timeout && timeout != "" if url == 'stdio' client = MCPClient.create_client(mcp_server_configs: [.merge(type: 'stdio')]) else type = IndiferentHash. , :type, type: (Open.remote?(url) ? :http : :stdio) if url && Open.remote?(url) token ||= LLM.get_url_config(:key, url, :mcp) [:headers] = { 'Authorization' => "Bearer #{token}" } end client = MCPClient.create_client(mcp_server_configs: [.merge(type: 'http', url: url)]) end tools = client.list_tools tool_definitions = IndiferentHash.setup({}) tools.each do |tool| name = tool.name description = tool.description schema = tool.schema function = { name: name, description: description, parameters: schema } definition = IndiferentHash.setup function.merge(type: 'function', function: function) block = Proc.new do |name,params| res = tool.server.call_tool(name, params) if Hash === res && res['content'] res = res['content'] end if Array === res and res.length == 1 res = res.first end if Hash === res && res['content'] res = res['content'] end if Hash === res && res['text'] res = res['text'] end res end tool_definitions[name] = [block, definition] end tool_definitions end |
.messages(question, role = nil) ⇒ Object
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
# File 'lib/scout/llm/chat.rb', line 9 def self.(question, role = nil) default_role = "user" if Array === question return question.collect do |q| if String === q {role: role || default_role, content: q} else q end end end Chat.parse question end |
.options ⇒ Object
47 48 49 |
# File 'lib/scout/llm/chat.rb', line 47 def self.(...) Chat.(...) end |
.print ⇒ Object
51 52 53 |
# File 'lib/scout/llm/chat.rb', line 51 def self.print(...) Chat.print(...) end |
.process_calls(tools, calls, &block) ⇒ Object
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
# File 'lib/scout/llm/tools/call.rb', line 20 def self.process_calls(tools, calls, &block) max_content_length = LLM.max_content_length IndiferentHash.setup tools tool_call_content = calls.collect do |tool_call| tool_call = IndiferentHash.setup tool_call tool_call_id, function_name, function_arguments = call_id_name_and_arguments(tool_call) raise "No tool_call_id in #{ tool_call}" if tool_call_id.nil? function_arguments = IndiferentHash.setup function_arguments obj, definition = tools[function_name] definition = obj if Hash === obj defaults = definition[:parameters][:defaults] if definition && definition[:parameters] function_arguments = function_arguments.merge(defaults) if defaults Log.high "Calling #{function_name} (#{Log.fingerprint function_arguments}): " function_response = case obj when Proc obj.call function_name, function_arguments when Workflow call_workflow(obj, function_name, function_arguments) when KnowledgeBase call_knowledge_base(obj, function_name, function_arguments.dup) else if block_given? block.call function_name, function_arguments else ParameterException.new "Tool or function not found '#{function_name}'. Called with parameters #{Log.fingerprint function_arguments}" if obj.nil? && definition.nil? end end content = case function_response when Step function_response when String function_response when nil "success" when Exception {exception: function_response., stack: function_response.backtrace }.to_json else function_response.to_json end content = content.to_s if Numeric === content function_call = tool_call.dup function_call = {'name' => tool_call['name']}.merge tool_call.except('name') function_call['id'] = function_call.delete('call_id') if function_call.dig('call_id') [ function_name, function_arguments, tool_call_id, IndiferentHash.setup({role: "function_call", content: function_call.to_json}), content ] end jobs = tool_call_content.collect{|p| p.last }.select{|c| Step === c } if jobs.reject{|job| job.done? }.any? begin Workflow.produce jobs rescue end end tool_call_content.collect do |function_name,function_arguments,tool_call_id,tool_call,content| if Step === content step = content if content.done? content = content.load.to_json if content.done? elsif content.error? && content.exception content = {exception: content.exception., stack: content.exception.backtrace }.to_json else begin content = content.run.to_json rescue content = {exception: $!., stack: $!.backtrace }.to_json end end else step = nil end if (String === content) && content.length > max_content_length exception_msg = "Function #{function_name} #{tool_call_id} (#{Log.fingerprint function_arguments}) was executed successfully, but it returned #{content.length} characters, which is more than the maximum of #{max_content_length}. To protect the model context window this result was not returned." exception_msg += " The results was persisted at '#{step.path}'." if step Log.high exception_msg content = {exception: exception_msg, stack: caller}.to_json end Log.high "Called #{function_name} #{tool_call_id} (#{Log.fingerprint function_arguments}): " + Log.fingerprint(content) = { name: function_name, content: content, id: tool_call_id } [ tool_call, IndiferentHash.setup({role: "function_call_output", content: .to_json}) ] end.flatten end |
.purge ⇒ Object
63 64 65 |
# File 'lib/scout/llm/chat.rb', line 63 def self.purge(...) Chat.purge(...) end |
.register_backend(name, mod) ⇒ Object
8 9 10 |
# File 'lib/scout/llm/ask.rb', line 8 def self.register_backend(name, mod) BACKENDS[name] = mod end |
.run_tools(messages) ⇒ Object
57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
# File 'lib/scout/llm/tools.rb', line 57 def self.run_tools() .collect do |info| IndiferentHash.setup(info) role = info[:role] if role == 'cmd' { role: 'tool', content: CMD.cmd(info[:content]).read } else info end end end |
.task_tool_definition(workflow, task_name, inputs = nil) ⇒ Object
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
# File 'lib/scout/llm/tools/workflow.rb', line 3 def self.task_tool_definition(workflow, task_name, inputs = nil) task_info = workflow.task_info(task_name) return nil if task_info.nil? if inputs names = [] defaults = {} inputs.each do |i| if String === i && i.include?('=') name,_ , value = i.partition("=") defaults[name] = value else names << i.to_sym end end end properties = task_info[:inputs].inject({}) do |acc,input| next acc if names and not names.include?(input) type = task_info[:input_types][input] description = task_info[:input_descriptions][input] type = :string if type == :text type = :string if type == :select type = :string if type == :path type = :number if type == :float type = :array if type.to_s.end_with?('_array') acc[input] = { type: type, description: description || '' } if type == :array acc[input]['items'] = {type: :string} end if = task_info[:input_options][input] if = [:select_options] = .values if Hash === acc[input]["enum"] = end end acc end if not workflow.exec_exports.include?(task_name.to_sym) properties[:return_path] = { type: 'boolean', description: 'Instead of the result of the job, return the path were it is persisted' } end required_inputs = task_info[:inputs].select do |input| next if names and not names.include?(input.to_sym) task_info[:input_options].include?(input) && task_info[:input_options][input][:required] end function = { name: task_name, description: task_info[:description] || '', parameters: { type: "object", properties: properties, required: required_inputs, } } function[:parameters][:defaults] = defaults if defaults #IndiferentHash.setup function.merge(type: 'function', function: function) IndiferentHash.setup function end |
.tool_response(tool_call, &block) ⇒ Object
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
# File 'lib/scout/llm/tools.rb', line 19 def self.tool_response(tool_call, &block) tool_call_id = tool_call.dig("call_id") || tool_call.dig("id") if tool_call['function'] function_name = tool_call.dig("function", "name") function_arguments = tool_call.dig("function", "arguments") else function_name = tool_call.dig("name") function_arguments = tool_call.dig("arguments") end function_arguments = JSON.parse(function_arguments, { symbolize_names: true }) if String === function_arguments Log.high "Calling function #{function_name} with arguments #{Log.fingerprint function_arguments}" function_response = begin block.call function_name, function_arguments rescue $! end content = case function_response when String function_response when nil "success" when Exception {exception: function_response., stack: function_response.backtrace }.to_json else function_response.to_json end content = content.to_s if Numeric === content { id: tool_call_id, role: "tool", content: content } end |
.tools ⇒ Object
55 56 57 |
# File 'lib/scout/llm/chat.rb', line 55 def self.tools(...) Chat.tools(...) end |
.workflow_ask(workflow, question, options = {}) ⇒ Object
96 97 98 99 100 101 |
# File 'lib/scout/llm/ask.rb', line 96 def self.workflow_ask(workflow, question, = {}) workflow_tools = LLM.workflow_tools(workflow) self.ask(question, .merge(tools: workflow_tools)) do |task_name,parameters| workflow.job(task_name, parameters).run end end |
.workflow_tools(workflow, tasks = nil) ⇒ Object
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/scout/llm/tools/workflow.rb', line 81 def self.workflow_tools(workflow, tasks = nil) if Array === workflow workflow.inject({}){|tool_definitions,wf| tool_definitions.merge(workflow_tools(wf, tasks)) } else tasks = workflow.all_exports if tasks.nil? tasks = workflow.all_tasks if tasks.empty? && workflow.all_tasks tasks = [] if tasks.nil? tasks.inject({}){|tool_definitions,task_name| definition = self.task_tool_definition(workflow, task_name) next if definition.nil? tool_definitions.merge(task_name => [workflow, definition]) } end end |
Instance Method Details
#delegate(agent, name, description, &block) ⇒ Object
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
# File 'lib/scout/llm/agent/delegate.rb', line 96 def delegate(agent, name, description, &block) @other_options[:tools] ||= {} task_name = "hand_off_to_#{name}".to_sym block ||= Proc.new do |name, parameters| = parameters[:message] new_conversation = parameters[:new_conversation] Log.medium "Delegated to #{agent}: " + Log.fingerprint() if new_conversation agent.start else agent.purge end agent.user agent.chat end properties = { message: { "type": :string, "description": "Message to pass to the agent" }, new_conversation: { "type": :boolean, "description": "Erase conversation history and start a new conversation with this message", "default": false } } required_inputs = [:message] function = { name: task_name, description: description, parameters: { type: "object", properties: properties, required: required_inputs } } definition = IndiferentHash.setup function.merge(type: 'function', function: function) @other_options[:tools][task_name] = [block, definition] end |