Module: Legion::LLM::Tools::Dispatcher
- Extended by:
- Legion::Logging::Helper
- Defined in:
- lib/legion/llm/tools/dispatcher.rb
Class Method Summary collapse
- .check_override(tool_name) ⇒ Object
- .check_registry_override(tool_name) ⇒ Object
- .check_settings_override(tool_name) ⇒ Object
- .dispatch(tool_call:, source:, exchange_id: nil) ⇒ Object
- .dispatch_builtin(_tool_call, _source) ⇒ Object
- .dispatch_client(tool_call, source) ⇒ Object
- .dispatch_extension(tool_call, source) ⇒ Object
- .dispatch_mcp(tool_call, source) ⇒ Object
- .dispatch_registry(tool_call, source) ⇒ Object
- .extract_content(result) ⇒ Object
- .result_error?(result) ⇒ Boolean
- .symbolize_keys(value) ⇒ Object
Class Method Details
.check_override(tool_name) ⇒ Object
53 54 55 56 57 58 |
# File 'lib/legion/llm/tools/dispatcher.rb', line 53 def check_override(tool_name) registry_override = check_registry_override(tool_name) return registry_override if registry_override check_settings_override(tool_name) end |
.check_registry_override(tool_name) ⇒ Object
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/legion/llm/tools/dispatcher.rb', line 60 def check_registry_override(tool_name) return nil unless Legion::Settings::Extensions.respond_to?(:find_tool) entry = Legion::Settings::Extensions.find_tool(tool_name) return nil unless entry if entry[:tool_class] { type: :registry, tool_class: entry[:tool_class] } elsif entry[:extension] && entry[:runner] && entry[:function] { type: :extension, lex: entry[:extension], runner: entry[:runner], function: entry[:function] } end rescue StandardError => e handle_exception(e, level: :debug, operation: 'llm.tools.dispatcher.check_registry_override', tool_name: tool_name) nil end |
.check_settings_override(tool_name) ⇒ Object
76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/legion/llm/tools/dispatcher.rb', line 76 def check_settings_override(tool_name) overrides = Legion::LLM::Settings.global_value(:mcp, :overrides) return nil unless overrides.is_a?(Hash) override = overrides[tool_name] return nil unless override { type: :extension, lex: override[:lex] || override['lex'], runner: override[:runner] || override['runner'], function: override[:function] || override['function'] } end |
.dispatch(tool_call:, source:, exchange_id: nil) ⇒ Object
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 |
# File 'lib/legion/llm/tools/dispatcher.rb', line 14 def dispatch(tool_call:, source:, exchange_id: nil) start_time = Time.now log.debug "[llm][tools] action=dispatch.enter tool=#{tool_call[:name]} source_type=#{source[:type]} exchange_id=#{exchange_id}" if source[:type] == :mcp override = check_override(tool_call[:name]) if override overridden_source = source source = override.merge(overridden_from: overridden_source) end end result = case source[:type] when :mcp dispatch_mcp(tool_call, source) when :extension dispatch_extension(tool_call, source) when :registry dispatch_registry(tool_call, source) when :client dispatch_client(tool_call, source) when :builtin dispatch_builtin(tool_call, source) else { status: :error, error: "Unknown tool source type: #{source[:type]}" } end duration_ms = ((Time.now - start_time) * 1000).to_i log.debug "[llm][tools] action=dispatch.complete tool=#{tool_call[:name]} status=#{result[:status]} duration_ms=#{duration_ms}" result.merge( source: source, exchange_id: exchange_id, duration_ms: duration_ms ) rescue StandardError => e handle_exception(e, level: :warn, operation: 'llm.tools.dispatcher.dispatch_tool_call', tool_name: tool_call[:name]) { status: :error, error: e., source: source, exchange_id: exchange_id } end |
.dispatch_builtin(_tool_call, _source) ⇒ Object
133 134 135 |
# File 'lib/legion/llm/tools/dispatcher.rb', line 133 def dispatch_builtin(_tool_call, _source) { status: :passthrough, result: nil } end |
.dispatch_client(tool_call, source) ⇒ Object
123 124 125 126 127 128 129 130 131 |
# File 'lib/legion/llm/tools/dispatcher.rb', line 123 def dispatch_client(tool_call, source) return { status: :error, result: "Tool #{tool_call[:name]} is not executable server-side." } unless source[:executable] require 'legion/llm/api/native/helpers' helper = Object.new.extend(Legion::LLM::API::Native::ClientToolMethods) result = helper.send(:dispatch_client_tool, tool_call[:name].to_s, **symbolize_keys(tool_call[:arguments] || {})) { status: :success, result: result } end |
.dispatch_extension(tool_call, source) ⇒ Object
101 102 103 104 105 106 107 108 109 110 |
# File 'lib/legion/llm/tools/dispatcher.rb', line 101 def dispatch_extension(tool_call, source) log.debug "[llm][tools] action=dispatch_extension tool=#{tool_call[:name]} lex=#{source[:lex]} runner=#{source[:runner]}" segments = (source[:lex] || '').delete_prefix('lex-').split('-') runner_path = (%w[Legion Extensions] + segments.map(&:capitalize) + ['Runners', source[:runner]]).join('::') runner = Kernel.const_get(runner_path) fn = source[:function].to_sym result = runner.send(fn, **(tool_call[:arguments] || {})) { status: :success, result: result } end |
.dispatch_mcp(tool_call, source) ⇒ Object
91 92 93 94 95 96 97 98 99 |
# File 'lib/legion/llm/tools/dispatcher.rb', line 91 def dispatch_mcp(tool_call, source) log.debug "[llm][tools] action=dispatch_mcp tool=#{tool_call[:name]} server=#{source[:server]}" conn = ::Legion::MCP::Client::Pool.connection_for(source[:server]) raise "No connection for MCP server: #{source[:server]}" unless conn raw = conn.call_tool(name: tool_call[:name], arguments: tool_call[:arguments] || {}) content = raw[:content]&.map { |c| c[:text] || c['text'] }&.join("\n") { status: raw[:error] ? :error : :success, result: content } end |
.dispatch_registry(tool_call, source) ⇒ Object
112 113 114 115 116 117 118 119 120 121 |
# File 'lib/legion/llm/tools/dispatcher.rb', line 112 def dispatch_registry(tool_call, source) log.debug "[llm][tools] action=dispatch_registry tool=#{tool_call[:name]}" tool_class = source[:tool_class] raise "No registry tool class for #{tool_call[:name]}" unless tool_class.respond_to?(:call) args = symbolize_keys(tool_call[:arguments] || {}) args = Interceptor.intercept(tool_call[:name], **args) result = tool_class.call(**args) { status: result_error?(result) ? :error : :success, result: extract_content(result) } end |
.extract_content(result) ⇒ Object
151 152 153 154 155 156 157 158 159 160 161 162 163 |
# File 'lib/legion/llm/tools/dispatcher.rb', line 151 def extract_content(result) if result.respond_to?(:content) && result.content.is_a?(Array) result.content.filter_map { |c| c[:text] || c['text'] || c.to_s }.join("\n") elsif result.is_a?(Hash) && result[:content].is_a?(Array) result[:content].filter_map { |c| c[:text] || c['text'] }.join("\n") elsif result.is_a?(Hash) Legion::JSON.dump(result) elsif result.is_a?(String) result else result.to_s end end |
.result_error?(result) ⇒ Boolean
165 166 167 |
# File 'lib/legion/llm/tools/dispatcher.rb', line 165 def result_error?(result) result.is_a?(Hash) && (result[:error] || result['error']) end |
.symbolize_keys(value) ⇒ Object
137 138 139 140 141 142 143 144 145 146 147 148 149 |
# File 'lib/legion/llm/tools/dispatcher.rb', line 137 def symbolize_keys(value) case value when Hash value.to_h do |key, nested| normalized_key = key.respond_to?(:to_sym) ? key.to_sym : key [normalized_key, symbolize_keys(nested)] end when Array value.map { |entry| symbolize_keys(entry) } else value end end |