legion-llm 0.6.29 → 0.6.30

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: f61fe00ccd51184aa08b50f2169e510db99ac249bcd71c61310b2705dda1e8d4
4
- data.tar.gz: 5fe4035e3d6ea242d52d681a7e44a11952ddd82b71aa009de628c18bae838965
3
+ metadata.gz: 9454e25248e8f8dcbd9e4805137dbac86dfcddb846d769515236632db12a1c66
4
+ data.tar.gz: 674ad6db37301f304bbbcfaa9ef3b396146b04a6e85955dab6a65a695a8456f2
5
5
  SHA512:
6
- metadata.gz: 7c91776441b4e6d943b4e33b2a1b6f69919215457889e88952b5ce79c3089dc783a5bfea24651c1ae844ec6b364c55b47889bf291035eedf388263bb59e364ee
7
- data.tar.gz: 953104b4e691e18686f3aa8af3e981f47b1c7fb76b2923c59ca5fa966d4a5aab5ab8f85fe89f612f1896d3f6af0f3ad8f86e75f5d25b591b28a06e5d401322c8
6
+ metadata.gz: cc94b57f194c4a6904a5da6b88eca9320676b34fff23207de83c30f474e4c0e73054935e5c84e4469c9beb8b20f12fac4e6233634876a2344819d221270a0600
7
+ data.tar.gz: 062ee727e7d2bb9e31d9a2ba4e64d6e2b2e8798cc571e0ba91406cdaef9780b5719db5e03430c3f37dc81ef3cb047b32713ce27e42e313cc4653e504e9c169e6
data/CHANGELOG.md CHANGED
@@ -2,8 +2,14 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.6.30] - 2026-04-10
6
+
5
7
  ### Added
6
- - Broker soft consumer in Providers module tries Identity::Broker before Settings for all provider credentials (Phase 8 Wave 2)
8
+ - `Legion::LLM::Pipeline::Steps::TriggerMatch`pipeline step that matches recent message words against `Legion::Tools::TriggerIndex` and populates `@triggered_tools`
9
+ - `tool_trigger` settings defaults (`scan_depth: 2`, `tool_limit: 10`) in `Legion::LLM::Settings`
10
+ - Trigger-matched tools are injected into the RubyLLM session in `inject_registry_tools` after always-loaded tools
11
+ - `:trigger_match` step inserted between `:rag_context` and `:tool_discovery` in `STEPS` and `PRE_PROVIDER_STEPS`
12
+ - `:trigger_match` added to all profile skip lists that skip `:tool_discovery` (`GAIA_SKIP`, `SYSTEM_SKIP`, `QUICK_REPLY_SKIP`, `SERVICE_SKIP`)
7
13
 
8
14
  ## [0.6.29] - 2026-04-09
9
15
 
@@ -19,6 +19,7 @@ module Legion
19
19
  :escalation_chain
20
20
  attr_accessor :tool_event_handler
21
21
 
22
+ include Steps::TriggerMatch
22
23
  include Steps::ToolDiscovery
23
24
  include Steps::ToolCalls
24
25
  include Steps::KnowledgeCapture
@@ -29,14 +30,14 @@ module Legion
29
30
 
30
31
  STEPS = %i[
31
32
  tracing_init idempotency conversation_uuid context_load
32
- rbac classification billing gaia_advisory tier_assignment rag_context tool_discovery
33
+ rbac classification billing gaia_advisory tier_assignment rag_context trigger_match tool_discovery
33
34
  routing request_normalization token_budget provider_call response_normalization
34
35
  debate confidence_scoring tool_calls context_store post_response knowledge_capture response_return
35
36
  ].freeze
36
37
 
37
38
  PRE_PROVIDER_STEPS = %i[
38
39
  tracing_init idempotency conversation_uuid context_load
39
- rbac classification billing gaia_advisory tier_assignment rag_context tool_discovery
40
+ rbac classification billing gaia_advisory tier_assignment rag_context trigger_match tool_discovery
40
41
  routing request_normalization token_budget
41
42
  ].freeze
42
43
 
@@ -62,6 +63,7 @@ module Legion
62
63
  @raw_response = nil
63
64
  @exchange_id = nil
64
65
  @discovered_tools = []
66
+ @triggered_tools = []
65
67
  @resolved_provider = nil
66
68
  @resolved_model = nil
67
69
  @confidence_score = nil
@@ -102,6 +104,20 @@ module Legion
102
104
  handle_exception(e, level: :warn, operation: 'llm.pipeline.inject_always_tool')
103
105
  end
104
106
 
107
+ # Trigger-matched tools — inject tools surfaced by trigger word matching
108
+ if @triggered_tools.any?
109
+ @triggered_tools.each do |tool_class|
110
+ adapter = ToolAdapter.new(tool_class)
111
+ next if injected_names.include?(adapter.name)
112
+
113
+ session.with_tool(adapter)
114
+ injected_names << adapter.name
115
+ rescue StandardError => e
116
+ @warnings << "Failed to inject triggered tool: #{e.message}"
117
+ handle_exception(e, level: :warn, operation: 'llm.pipeline.inject_triggered_tool')
118
+ end
119
+ end
120
+
105
121
  # Requested deferred tools — inject only if explicitly requested
106
122
  deferred = ::Legion::Tools::Registry.respond_to?(:deferred_tools) ? ::Legion::Tools::Registry.deferred_tools : []
107
123
  requested = requested_deferred_tool_names
@@ -121,6 +137,7 @@ module Legion
121
137
  log.info(
122
138
  "[llm][tools] inject request_id=#{@request.id} " \
123
139
  "always=#{::Legion::Tools::Registry.tools.size} " \
140
+ "triggered=#{@triggered_tools.size} " \
124
141
  "deferred_available=#{deferred.size} " \
125
142
  "requested_deferred=#{requested.size} " \
126
143
  "injected=#{injected_names.size} names=#{injected_names.first(25).join(',')}"
@@ -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 tool_discovery context_store post_response
9
+ billing gaia_advisory trigger_match 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 tool_discovery context_store
14
+ billing gaia_advisory rag_context trigger_match 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 tool_discovery confidence_scoring
20
+ gaia_advisory rag_context trigger_match tool_discovery confidence_scoring
21
21
  tool_calls context_store post_response knowledge_capture
22
22
  ].freeze
23
23
 
@@ -25,7 +25,7 @@ module Legion
25
25
 
26
26
  SERVICE_SKIP = %i[
27
27
  conversation_uuid context_load gaia_advisory
28
- rag_context tool_discovery confidence_scoring
28
+ rag_context trigger_match tool_discovery confidence_scoring
29
29
  tool_calls context_store knowledge_capture
30
30
  ].freeze
31
31
 
@@ -0,0 +1,111 @@
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 TriggerMatch
10
+ include Legion::Logging::Helper
11
+
12
+ def step_trigger_match
13
+ start_time = nil
14
+ return unless defined?(::Legion::Tools::TriggerIndex)
15
+ return if ::Legion::Tools::TriggerIndex.empty?
16
+
17
+ start_time = ::Time.now
18
+
19
+ text = extract_recent_text
20
+ word_set = normalize_message_words(text)
21
+ return if word_set.empty?
22
+
23
+ matched, per_word = ::Legion::Tools::TriggerIndex.match(word_set)
24
+ subtract_always_loaded(matched)
25
+ return if matched.empty?
26
+
27
+ limit = trigger_tool_limit
28
+ @triggered_tools = if matched.size <= limit
29
+ matched.to_a
30
+ else
31
+ rank_and_cap(matched, per_word, limit)
32
+ end
33
+
34
+ if @triggered_tools.any?
35
+ names = @triggered_tools.map(&:tool_name)
36
+ @enrichments['tool:trigger_match'] = {
37
+ content: "#{@triggered_tools.size} tools matched via trigger words",
38
+ data: { tool_count: @triggered_tools.size, tool_names: names },
39
+ timestamp: ::Time.now
40
+ }
41
+ end
42
+
43
+ record_trigger_match_timeline(@triggered_tools.size, start_time)
44
+ rescue StandardError => e
45
+ @warnings << "Trigger match error: #{e.message}"
46
+ handle_exception(e, level: :warn, operation: 'llm.pipeline.steps.trigger_match')
47
+ record_trigger_match_timeline(0, start_time)
48
+ end
49
+
50
+ private
51
+
52
+ def extract_recent_text
53
+ depth = trigger_scan_depth
54
+ messages = @request.messages.last(depth)
55
+ messages.map do |msg|
56
+ if msg.is_a?(Hash)
57
+ msg[:content] || msg['content'] || msg.to_s
58
+ else
59
+ msg.to_s
60
+ end
61
+ end.join(' ')
62
+ end
63
+
64
+ def normalize_message_words(text)
65
+ return Set.new if text.nil? || text.empty?
66
+
67
+ text.downcase.gsub(/[^a-z ]/, ' ').split.to_set
68
+ end
69
+
70
+ def rank_and_cap(matched, per_word, limit)
71
+ scores = Hash.new(0)
72
+ per_word.each_value do |tools|
73
+ tools.each { |tool| scores[tool] += 1 }
74
+ end
75
+ matched.to_a
76
+ .sort_by { |tool| [-scores[tool], tool.tool_name] }
77
+ .first(limit)
78
+ end
79
+
80
+ def subtract_always_loaded(matched)
81
+ return unless defined?(::Legion::Tools::Registry) &&
82
+ ::Legion::Tools::Registry.respond_to?(:always_loaded_names)
83
+
84
+ always = ::Legion::Tools::Registry.always_loaded_names
85
+ matched.reject! { |tool| always.include?(tool.tool_name) }
86
+ end
87
+
88
+ def trigger_scan_depth
89
+ Legion::Settings.dig(:llm, :tool_trigger, :scan_depth) || 2
90
+ end
91
+
92
+ def trigger_tool_limit
93
+ Legion::Settings.dig(:llm, :tool_trigger, :tool_limit) || 10
94
+ end
95
+
96
+ def record_trigger_match_timeline(count, start_time = nil)
97
+ return unless @timeline.respond_to?(:record)
98
+
99
+ duration = start_time ? ((::Time.now - start_time) * 1000).to_i : 0
100
+ @timeline.record(
101
+ category: :enrichment, key: 'tool:trigger_match',
102
+ direction: :inbound, detail: "#{count} tools matched via trigger words",
103
+ from: 'trigger_index', to: 'pipeline',
104
+ duration_ms: duration
105
+ )
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -18,6 +18,7 @@ require_relative 'steps/gaia_advisory'
18
18
  require_relative 'steps/tier_assigner'
19
19
  require_relative 'steps/post_response'
20
20
  require_relative 'steps/tool_discovery'
21
+ require_relative 'steps/trigger_match'
21
22
  require_relative 'steps/tool_calls'
22
23
  require_relative 'steps/rag_context'
23
24
  require_relative 'steps/rag_guard'
@@ -33,7 +33,8 @@ module Legion
33
33
  telemetry: telemetry_defaults,
34
34
  context_curation: context_curation_defaults,
35
35
  debate: debate_defaults,
36
- provider_layer: provider_layer_defaults
36
+ provider_layer: provider_layer_defaults,
37
+ tool_trigger: tool_trigger_defaults
37
38
  }
38
39
  end
39
40
 
@@ -226,6 +227,13 @@ module Legion
226
227
  }
227
228
  end
228
229
 
230
+ def self.tool_trigger_defaults
231
+ {
232
+ scan_depth: 2,
233
+ tool_limit: 10
234
+ }
235
+ end
236
+
229
237
  def self.debate_defaults
230
238
  {
231
239
  enabled: false,
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module LLM
5
- VERSION = '0.6.29'
5
+ VERSION = '0.6.30'
6
6
  end
7
7
  end
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.29
4
+ version: 0.6.30
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -300,6 +300,7 @@ files:
300
300
  - lib/legion/llm/pipeline/steps/token_budget.rb
301
301
  - lib/legion/llm/pipeline/steps/tool_calls.rb
302
302
  - lib/legion/llm/pipeline/steps/tool_discovery.rb
303
+ - lib/legion/llm/pipeline/steps/trigger_match.rb
303
304
  - lib/legion/llm/pipeline/timeline.rb
304
305
  - lib/legion/llm/pipeline/tool_adapter.rb
305
306
  - lib/legion/llm/pipeline/tool_dispatcher.rb