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 +4 -4
- data/CHANGELOG.md +7 -1
- data/lib/legion/llm/pipeline/executor.rb +19 -2
- data/lib/legion/llm/pipeline/profile.rb +4 -4
- data/lib/legion/llm/pipeline/steps/trigger_match.rb +111 -0
- data/lib/legion/llm/pipeline/steps.rb +1 -0
- data/lib/legion/llm/settings.rb +9 -1
- data/lib/legion/llm/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9454e25248e8f8dcbd9e4805137dbac86dfcddb846d769515236632db12a1c66
|
|
4
|
+
data.tar.gz: 674ad6db37301f304bbbcfaa9ef3b396146b04a6e85955dab6a65a695a8456f2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
-
|
|
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'
|
data/lib/legion/llm/settings.rb
CHANGED
|
@@ -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,
|
data/lib/legion/llm/version.rb
CHANGED
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.
|
|
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
|