legion-llm 0.6.16 → 0.6.17

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0fe6246015e9967dec5bbe428ab2cb5869b4a359c4a5b40c0a8206d0fe7b516d
4
- data.tar.gz: f803de963b1887d064506187c3d7c8fa00b437613f5456bcbf87adae1d53a3a4
3
+ metadata.gz: 85bd29db06078f900c983a6f722cda55d6c3c5c6a72c6e4fdb8c4295c3debddb
4
+ data.tar.gz: 855d18648dbe9d8e02e886011c51e35dd5f7d0828a25529428fb6f865c9b5f62
5
5
  SHA512:
6
- metadata.gz: e7f65ec573b55b840b7b07b9a9c83c7aa189696b162936e63ad65ddf508be3240eb056fc70d3bbbad532eb2f3db841c4facd3efb1df30e9bf816b4f37d8a33e0
7
- data.tar.gz: 6a823d728ab547b3ccab050d7a01e9825cb3764446eecb2f32b71ce1c7ff73f7da7f167e9198b9a45f4941b7ed8113c3aac3f1ca95c0a76b46a5b502dd713147
6
+ metadata.gz: 063beb13698b8645a8c365ccc1bde00d599b7653df47304d311b4c829b7de277e13e9443c5d693fe22668ee5855d3c2d33187eb8ed4af71dc0f744a59cdceaed
7
+ data.tar.gz: 4244f8d0af82a82c3a45cc483c9939fe492ef7350e43cdbf2f4d5b933210b6154150e038f02931f52943c5ad8918d6b6247d4c8debc29b083ea55336c5daf570
data/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ### Added
6
+ - `Legion::LLM::Pipeline::ToolAdapter` - wraps Tools::Base for RubyLLM sessions
7
+
8
+ ### Changed
9
+ - Renamed `McpToolAdapter` to `ToolAdapter` (backwards compat alias kept)
10
+ - Pipeline step `McpDiscovery` renamed to `ToolDiscovery`
11
+ - Executor reads from `Legion::Tools::Registry`
12
+ - `chat_single` wraps registry tools with ToolAdapter
13
+ - Routes: executor handles all tool injection, routes only pass client tools
14
+
15
+ ### Removed
16
+ - MCP server dependency for tool injection in routes
17
+
5
18
  ## [0.6.16] - 2026-04-03
6
19
 
7
20
  ### Fixed
@@ -18,7 +18,7 @@ module Legion
18
18
  :audit, :warnings, :discovered_tools, :confidence_score,
19
19
  :escalation_chain
20
20
 
21
- include Steps::McpDiscovery
21
+ include Steps::ToolDiscovery
22
22
  include Steps::ToolCalls
23
23
  include Steps::KnowledgeCapture
24
24
  include Steps::ConfidenceScoring
@@ -28,14 +28,14 @@ module Legion
28
28
 
29
29
  STEPS = %i[
30
30
  tracing_init idempotency conversation_uuid context_load
31
- rbac classification billing gaia_advisory tier_assignment rag_context mcp_discovery
31
+ rbac classification billing gaia_advisory tier_assignment rag_context tool_discovery
32
32
  routing request_normalization token_budget provider_call response_normalization
33
33
  debate confidence_scoring tool_calls context_store post_response knowledge_capture response_return
34
34
  ].freeze
35
35
 
36
36
  PRE_PROVIDER_STEPS = %i[
37
37
  tracing_init idempotency conversation_uuid context_load
38
- rbac classification billing gaia_advisory tier_assignment rag_context mcp_discovery
38
+ rbac classification billing gaia_advisory tier_assignment rag_context tool_discovery
39
39
  routing request_normalization token_budget
40
40
  ].freeze
41
41
 
@@ -66,6 +66,8 @@ module Legion
66
66
  legion_search_sessions
67
67
  ].freeze
68
68
 
69
+ private_constant :ALWAYS_LOADED_MCP_TOOLS
70
+
69
71
  ASYNC_THREAD_POOL = Concurrent::FixedThreadPool.new(4, fallback_policy: :caller_runs)
70
72
 
71
73
  def initialize(request)
@@ -104,16 +106,16 @@ module Legion
104
106
 
105
107
  private
106
108
 
107
- def inject_discovered_tools(session)
108
- server = mcp_server
109
- return unless server.respond_to?(:tool_registry)
109
+ def inject_registry_tools(session)
110
+ return unless defined?(::Legion::Tools::Registry)
110
111
 
111
112
  requested = requested_deferred_tool_names
113
+ always_loaded = always_loaded_tool_names
112
114
  injected_names = []
113
115
 
114
- server.tool_registry.each do |mcp_tool_class|
115
- adapter = McpToolAdapter.new(mcp_tool_class)
116
- next unless ALWAYS_LOADED_MCP_TOOLS.include?(adapter.name) || requested.include?(adapter.name)
116
+ ::Legion::Tools::Registry.tools.each do |tool_class|
117
+ adapter = ToolAdapter.new(tool_class)
118
+ next unless always_loaded.include?(adapter.name) || requested.include?(adapter.name)
117
119
 
118
120
  session.with_tool(adapter)
119
121
  injected_names << adapter.name
@@ -123,8 +125,8 @@ module Legion
123
125
  end
124
126
 
125
127
  log.info(
126
- "[llm][mcp] inject request_id=#{@request.id} " \
127
- "always_loaded=#{ALWAYS_LOADED_MCP_TOOLS.size} requested_deferred=#{requested.size} " \
128
+ "[llm][tools] inject request_id=#{@request.id} " \
129
+ "always_loaded=#{always_loaded.size} requested_deferred=#{requested.size} " \
128
130
  "injected=#{injected_names.size} names=#{injected_names.first(25).join(',')}"
129
131
  )
130
132
  rescue StandardError => e
@@ -132,6 +134,18 @@ module Legion
132
134
  handle_exception(e, level: :warn, operation: 'llm.pipeline.inject_tools')
133
135
  end
134
136
 
137
+ # Backwards compatibility alias
138
+ alias inject_discovered_tools inject_registry_tools
139
+
140
+ def always_loaded_tool_names
141
+ return ALWAYS_LOADED_MCP_TOOLS unless defined?(::Legion::Tools::Registry)
142
+
143
+ names = ::Legion::Tools::Registry.always_loaded_names.map { |name| name.to_s.tr('.', '_') }
144
+ names.any? ? names : ALWAYS_LOADED_MCP_TOOLS
145
+ rescue StandardError
146
+ ALWAYS_LOADED_MCP_TOOLS
147
+ end
148
+
135
149
  def execute_steps
136
150
  executed = 0
137
151
  skipped = 0
@@ -678,8 +692,7 @@ module Legion
678
692
  session.with_tool(tool)
679
693
  end
680
694
 
681
- ToolRegistry.tools.each { |tool| session.with_tool(tool) } if defined?(ToolRegistry)
682
- inject_discovered_tools(session)
695
+ inject_registry_tools(session)
683
696
  end
684
697
 
685
698
  def apply_ruby_llm_instructions(session)
@@ -864,23 +877,6 @@ module Legion
864
877
  )
865
878
  end
866
879
 
867
- def mcp_server
868
- return ::Legion::MCP.server if defined?(::Legion::MCP) && ::Legion::MCP.respond_to?(:server)
869
-
870
- require 'legion/mcp'
871
- return unless defined?(::Legion::MCP) && ::Legion::MCP.respond_to?(:server)
872
-
873
- ::Legion::MCP.server
874
- rescue LoadError => e
875
- @warnings << "MCP unavailable: #{e.message}"
876
- handle_exception(e, level: :debug, operation: 'llm.pipeline.mcp_server.require')
877
- nil
878
- rescue StandardError => e
879
- @warnings << "MCP server load error: #{e.message}"
880
- handle_exception(e, level: :warn, operation: 'llm.pipeline.mcp_server')
881
- nil
882
- end
883
-
884
880
  def requested_deferred_tool_names
885
881
  metadata = @request.metadata || {}
886
882
  requested = metadata[:requested_tools] || metadata['requested_tools'] || []
@@ -6,18 +6,18 @@ module Legion
6
6
  module Profile
7
7
  GAIA_SKIP = %i[
8
8
  idempotency conversation_uuid context_load rbac classification
9
- billing gaia_advisory mcp_discovery context_store post_response
9
+ billing gaia_advisory tool_discovery context_store post_response
10
10
  ].freeze
11
11
 
12
12
  SYSTEM_SKIP = %i[
13
13
  idempotency conversation_uuid context_load rbac classification
14
- billing gaia_advisory rag_context mcp_discovery context_store
14
+ billing gaia_advisory rag_context tool_discovery context_store
15
15
  post_response
16
16
  ].freeze
17
17
 
18
18
  QUICK_REPLY_SKIP = %i[
19
19
  idempotency conversation_uuid context_load classification
20
- gaia_advisory rag_context mcp_discovery confidence_scoring
20
+ gaia_advisory rag_context tool_discovery confidence_scoring
21
21
  tool_calls context_store post_response knowledge_capture
22
22
  ].freeze
23
23
 
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/logging/helper'
4
+
5
+ module Legion
6
+ module LLM
7
+ module Pipeline
8
+ module Steps
9
+ module ToolDiscovery
10
+ include Legion::Logging::Helper
11
+
12
+ def step_tool_discovery
13
+ @discovered_tools ||= []
14
+ start_time = Time.now
15
+
16
+ discover_registry_tools
17
+ discover_client_tools
18
+
19
+ total = @discovered_tools.size
20
+ if total.positive?
21
+ sources = @discovered_tools.filter_map { |t| t.dig(:source, :server) || t.dig(:source, :type) }.uniq
22
+ @enrichments['tool:discovery'] = {
23
+ content: "#{total} tools from #{sources.length} sources",
24
+ data: { tool_count: total, sources: sources },
25
+ timestamp: Time.now
26
+ }
27
+ end
28
+
29
+ record_tool_discovery_timeline(total, start_time)
30
+ rescue StandardError => e
31
+ @warnings << "Tool discovery error: #{e.message}"
32
+ handle_exception(e, level: :warn, operation: 'llm.pipeline.steps.tool_discovery')
33
+ record_tool_discovery_timeline(0)
34
+ end
35
+
36
+ # Backwards compatibility alias — step name used in STEPS array is tool_discovery
37
+ alias step_mcp_discovery step_tool_discovery
38
+
39
+ private
40
+
41
+ def discover_registry_tools
42
+ return unless defined?(::Legion::Tools::Registry)
43
+
44
+ ::Legion::Tools::Registry.tools.each do |tool_class|
45
+ name = tool_class.respond_to?(:tool_name) ? tool_class.tool_name : tool_class.name
46
+ desc = tool_class.respond_to?(:description) ? tool_class.description : ''
47
+ schema = tool_class.respond_to?(:input_schema) ? tool_class.input_schema : {}
48
+ @discovered_tools << {
49
+ name: name,
50
+ description: desc,
51
+ parameters: schema,
52
+ source: { type: :registry, server: 'legion' }
53
+ }
54
+ end
55
+
56
+ log.info(
57
+ "[llm][tools] discover request_id=#{@request.id} " \
58
+ "registry_tools=#{::Legion::Tools::Registry.tools.size}"
59
+ )
60
+ rescue StandardError => e
61
+ @warnings << "Registry tool discovery error: #{e.message}"
62
+ handle_exception(e, level: :warn, operation: 'llm.pipeline.steps.tool_discovery.registry')
63
+ end
64
+
65
+ def discover_client_tools
66
+ return unless defined?(::Legion::MCP::Client::Pool)
67
+
68
+ ::Legion::MCP::Client::Pool.all_tools.each do |tool|
69
+ @discovered_tools << {
70
+ name: tool[:name],
71
+ description: tool[:description],
72
+ parameters: tool[:input_schema],
73
+ source: tool[:source]
74
+ }
75
+ end
76
+ rescue StandardError => e
77
+ @warnings << "Client tool discovery error: #{e.message}"
78
+ handle_exception(e, level: :warn, operation: 'llm.pipeline.steps.tool_discovery.client')
79
+ end
80
+
81
+ def record_tool_discovery_timeline(count, start_time = nil)
82
+ duration = start_time ? ((Time.now - start_time) * 1000).to_i : 0
83
+ @timeline.record(
84
+ category: :enrichment, key: 'tool:discovery',
85
+ direction: :inbound, detail: "#{count} tools discovered",
86
+ from: 'tool_registry', to: 'pipeline',
87
+ duration_ms: duration
88
+ )
89
+ end
90
+ end
91
+
92
+ # Backwards compatibility alias
93
+ McpDiscovery = ToolDiscovery
94
+ end
95
+ end
96
+ end
97
+ end
@@ -17,7 +17,7 @@ require_relative 'steps/billing'
17
17
  require_relative 'steps/gaia_advisory'
18
18
  require_relative 'steps/tier_assigner'
19
19
  require_relative 'steps/post_response'
20
- require_relative 'steps/mcp_discovery'
20
+ require_relative 'steps/tool_discovery'
21
21
  require_relative 'steps/tool_calls'
22
22
  require_relative 'steps/rag_context'
23
23
  require_relative 'steps/rag_guard'
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ruby_llm'
4
+ require 'legion/logging/helper'
5
+
6
+ module Legion
7
+ module LLM
8
+ module Pipeline
9
+ class ToolAdapter < RubyLLM::Tool
10
+ include Legion::Logging::Helper
11
+
12
+ def initialize(tool_class)
13
+ @tool_class = tool_class
14
+ raw_name = tool_class.respond_to?(:tool_name) ? tool_class.tool_name : tool_class.name.to_s
15
+ @tool_name = raw_name.tr('.', '_')
16
+ @tool_desc = tool_class.respond_to?(:description) ? tool_class.description.to_s : ''
17
+ @tool_schema = tool_class.respond_to?(:input_schema) ? tool_class.input_schema : nil
18
+ super()
19
+ end
20
+
21
+ def name
22
+ @tool_name
23
+ end
24
+
25
+ def description
26
+ @tool_desc
27
+ end
28
+
29
+ def params_schema
30
+ return @params_schema if defined?(@params_schema)
31
+
32
+ @params_schema = (RubyLLM::Utils.deep_stringify_keys(@tool_schema) if @tool_schema.is_a?(Hash))
33
+ end
34
+
35
+ def execute(**args)
36
+ log.info("[llm][tools] adapter.execute name=#{@tool_name} arguments=#{summarize_payload(args)}")
37
+ result = @tool_class.call(**args)
38
+ content = extract_content(result)
39
+ log.info("[llm][tools] adapter.result name=#{@tool_name} output=#{summarize_payload(content)}")
40
+ content
41
+ rescue StandardError => e
42
+ handle_exception(e, level: :warn, operation: 'llm.pipeline.tool_adapter.execute', tool_name: @tool_name)
43
+ "Tool error: #{e.message}"
44
+ end
45
+
46
+ private
47
+
48
+ def extract_content(result)
49
+ # MCP::Tool::Response — has .content array of {type: 'text', text: '...'}
50
+ if result.respond_to?(:content) && result.content.is_a?(Array)
51
+ result.content.filter_map { |c| c[:text] || c['text'] || c.to_s }.join("\n")
52
+ elsif result.is_a?(Hash) && result[:content].is_a?(Array)
53
+ result[:content].filter_map { |c| c[:text] || c['text'] }.join("\n")
54
+ elsif result.is_a?(Hash)
55
+ Legion::JSON.dump(result)
56
+ elsif result.is_a?(String)
57
+ result
58
+ else
59
+ result.to_s
60
+ end
61
+ end
62
+
63
+ def summarize_payload(payload)
64
+ payload.to_s[0, 200].inspect
65
+ end
66
+ end
67
+
68
+ # Backwards compatibility alias
69
+ McpToolAdapter = ToolAdapter
70
+ end
71
+ end
72
+ end
@@ -11,7 +11,7 @@ require_relative 'pipeline/gaia_caller'
11
11
  require_relative 'pipeline/tool_dispatcher'
12
12
  require_relative 'pipeline/enrichment_injector'
13
13
  require_relative 'pipeline/steps'
14
- require_relative 'pipeline/mcp_tool_adapter'
14
+ require_relative 'pipeline/tool_adapter'
15
15
  require_relative 'pipeline/executor'
16
16
 
17
17
  module Legion
@@ -214,34 +214,6 @@ module Legion
214
214
  end
215
215
  # rubocop:enable Metrics/BlockLength
216
216
 
217
- define_method(:cached_mcp_tools) do
218
- @@llm_cached_mcp_tools ||= begin # rubocop:disable Style/ClassVars
219
- all = []
220
- if defined?(Legion::MCP) && Legion::MCP.respond_to?(:server)
221
- Legion::MCP.server
222
- if defined?(Legion::MCP::Server) && Legion::MCP::Server.respond_to?(:tool_registry)
223
- require 'legion/llm/pipeline/mcp_tool_adapter' unless defined?(Legion::LLM::Pipeline::McpToolAdapter)
224
- Legion::MCP::Server.tool_registry.each do |tc|
225
- all << Legion::LLM::Pipeline::McpToolAdapter.new(tc)
226
- rescue StandardError
227
- nil
228
- end
229
- end
230
- end
231
- always_names = %w[legion_do legion_get_status legion_run_task legion_describe_runner
232
- legion_list_extensions legion_get_extension legion_list_tasks
233
- legion_get_task legion_get_task_logs legion_query_knowledge
234
- legion_knowledge_health legion_knowledge_context legion_list_workers
235
- legion_show_worker legion_mesh_status legion_list_peers
236
- legion_tools legion_search_sessions]
237
- {
238
- always: all.select { |t| always_names.include?(t.name) }.freeze,
239
- deferred: all.reject { |t| always_names.include?(t.name) }.freeze,
240
- all: all.freeze
241
- }.freeze
242
- end
243
- end
244
-
245
217
  define_method(:extract_tool_calls) do |pipeline_response|
246
218
  tools_data = pipeline_response.tools
247
219
  return [] unless tools_data.is_a?(Array) && !tools_data.empty?
@@ -505,16 +477,7 @@ module Legion
505
477
  build_client_tool_class(ts[:name].to_s, ts[:description].to_s, ts[:parameters] || ts[:input_schema])
506
478
  end
507
479
 
508
- # Inject daemon MCP tools alongside client tools
509
- all_tools = tool_declarations.dup
510
- begin
511
- mcp_tools = cached_mcp_tools
512
- mcp_to_inject = Array(requested_tools).empty? ? mcp_tools[:always] : mcp_tools[:all]
513
- all_tools.concat(mcp_to_inject) if mcp_to_inject&.any?
514
- log.unknown "[llm][api] inference tools client=#{tool_declarations.size} mcp=#{mcp_to_inject&.size || 0} total=#{all_tools.size}"
515
- rescue StandardError => e
516
- handle_exception(e, level: :warn, operation: 'llm.routes.mcp_tool_injection', request_id: request_id)
517
- end
480
+ log.unknown "[llm][api] inference tools client=#{tool_declarations.size}"
518
481
 
519
482
  streaming = body[:stream] == true && request.preferred_type.to_s.include?('text/event-stream')
520
483
  normalized_caller = caller_context.respond_to?(:transform_keys) ? caller_context.transform_keys(&:to_sym) : {}
@@ -541,7 +504,7 @@ module Legion
541
504
  messages: messages,
542
505
  system: body[:system],
543
506
  routing: { provider: provider, model: model },
544
- tools: all_tools,
507
+ tools: tool_declarations,
545
508
  caller: effective_caller,
546
509
  conversation_id: conversation_id,
547
510
  metadata: { requested_tools: requested_tools },
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module LLM
5
- VERSION = '0.6.16'
5
+ VERSION = '0.6.17'
6
6
  end
7
7
  end
data/lib/legion/llm.rb CHANGED
@@ -644,7 +644,7 @@ module Legion
644
644
 
645
645
  def chat_single(model:, provider:, intent:, tier:, message: nil, **kwargs, &block)
646
646
  explicit_tools = kwargs.delete(:tools)
647
- tools = explicit_tools || ToolRegistry.tools
647
+ tools = explicit_tools || adapted_registry_tools
648
648
  tools = nil if tools.empty?
649
649
 
650
650
  if (intent || tier) && Router.routing_enabled?
@@ -687,6 +687,26 @@ module Legion
687
687
  response
688
688
  end
689
689
 
690
+ def adapted_registry_tools
691
+ tool_classes = if defined?(::Legion::Tools::Registry)
692
+ ::Legion::Tools::Registry.tools
693
+ elsif defined?(::Legion::LLM::ToolRegistry)
694
+ ::Legion::LLM::ToolRegistry.tools
695
+ else
696
+ return []
697
+ end
698
+
699
+ tool_classes.map do |tool_class|
700
+ Pipeline::ToolAdapter.new(tool_class)
701
+ rescue StandardError => e
702
+ handle_exception(e, level: :warn, operation: 'llm.adapted_registry_tools', tool_class: tool_class.to_s)
703
+ nil
704
+ end.compact
705
+ rescue StandardError => e
706
+ handle_exception(e, level: :warn, operation: 'llm.adapted_registry_tools')
707
+ []
708
+ end
709
+
690
710
  def try_defer(intent:, urgency:, model:, provider:, message:, **)
691
711
  return nil unless Scheduling.enabled? && Scheduling.should_defer?(intent: intent || :normal, urgency: urgency)
692
712
  return nil unless defined?(Batch) && Batch.enabled?
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legion-llm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.16
4
+ version: 0.6.17
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -287,7 +287,9 @@ files:
287
287
  - lib/legion/llm/pipeline/steps/tier_assigner.rb
288
288
  - lib/legion/llm/pipeline/steps/token_budget.rb
289
289
  - lib/legion/llm/pipeline/steps/tool_calls.rb
290
+ - lib/legion/llm/pipeline/steps/tool_discovery.rb
290
291
  - lib/legion/llm/pipeline/timeline.rb
292
+ - lib/legion/llm/pipeline/tool_adapter.rb
291
293
  - lib/legion/llm/pipeline/tool_dispatcher.rb
292
294
  - lib/legion/llm/pipeline/tracing.rb
293
295
  - lib/legion/llm/provider_registry.rb