legion-llm 0.9.30 → 0.9.32
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 +18 -0
- data/lib/legion/llm/hooks/reflection.rb +3 -3
- data/lib/legion/llm/inference/executor.rb +62 -30
- data/lib/legion/llm/inference/steps/rag_context.rb +25 -2
- data/lib/legion/llm/settings.rb +7 -5
- data/lib/legion/llm/tools/special.rb +46 -1
- data/lib/legion/llm/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 715bd8c0918939545eda0cf832d81aa23e69c807ed6055fdb4d95f4177c99449
|
|
4
|
+
data.tar.gz: 12e42d3d2fdc02c4ca7764a264af90631141952982d9726c02d0e9ea7de87a92
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6f2fd1a0ea8b18ed222f2713adb4f4a48ce57e90d5a3ac2242f7ae648ed297b6375d1143c0e29777e4c5ade60e479e09997c2af04cf0e1d3b81225ef3a14276f
|
|
7
|
+
data.tar.gz: 41d0daa21a98518c4192881bd3231ca1e308b6231d06e22b6102531feee4f6aef1ced50d9e43b1f65b7e8f344e95406ebb682bf445d356011aafd6d4ce241a37
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Legion LLM Changelog
|
|
2
2
|
|
|
3
|
+
## [0.9.31] - 2026-05-18
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Tools: `legion_list_all_tools` pinned special tool for full extension tool discovery with extension/deferred filters
|
|
7
|
+
- RAG: debug logging for per-result confidence scores and content_type distribution
|
|
8
|
+
- RAG: `exclude_source_agents` setting to filter noisy bulk-ingest sources from context injection
|
|
9
|
+
- Settings: `client_tool_passthrough` configurable via `Legion::Settings[:llm][:tool_trigger][:client_tool_passthrough]`
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- RAG: `min_confidence` default raised from 0.5 to 0.85 (reduces irrelevant context)
|
|
13
|
+
- RAG: default exclusion list: teams-api-ingest, unknown, teams-entity-extractor, legion-interlink
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Executor: enrichment injection (system prompt + RAG) cached on first tool loop pass; subsequent rounds reuse cache
|
|
17
|
+
- Client tool passthrough: per-request explicit false now correctly disables (was broken by || on false values)
|
|
18
|
+
- Tool audit publishing deferred to post-metering flush (async, non-blocking)
|
|
19
|
+
|
|
20
|
+
|
|
3
21
|
## [0.9.30] - 2026-05-16
|
|
4
22
|
|
|
5
23
|
### Fixed
|
|
@@ -176,13 +176,13 @@ module Legion
|
|
|
176
176
|
)
|
|
177
177
|
log.info("[llm][reflection] published via=transport model=#{model} type=#{entry[:type]}")
|
|
178
178
|
elsif apollo_direct?
|
|
179
|
-
Legion::Extensions::Apollo::Runners::
|
|
179
|
+
Legion::Extensions::Apollo::Runners::Request.ingest(
|
|
180
180
|
content: entry[:content],
|
|
181
181
|
content_type: entry[:type].to_s,
|
|
182
182
|
knowledge_domain: 'reflection',
|
|
183
183
|
confidence: entry[:confidence],
|
|
184
184
|
source_agent: "llm:#{model}",
|
|
185
|
-
|
|
185
|
+
source_channel: 'reflection_hook'
|
|
186
186
|
)
|
|
187
187
|
log.info("[llm][reflection] published via=direct model=#{model} type=#{entry[:type]}")
|
|
188
188
|
end
|
|
@@ -236,7 +236,7 @@ module Legion
|
|
|
236
236
|
private_class_method :apollo_transport?
|
|
237
237
|
|
|
238
238
|
def apollo_direct?
|
|
239
|
-
defined?(Legion::Extensions::Apollo::Runners::
|
|
239
|
+
defined?(Legion::Extensions::Apollo::Runners::Request)
|
|
240
240
|
end
|
|
241
241
|
private_class_method :apollo_direct?
|
|
242
242
|
end
|
|
@@ -102,6 +102,7 @@ module Legion
|
|
|
102
102
|
@sticky_turn_snapshot = nil
|
|
103
103
|
@pending_tool_history = Concurrent::Array.new
|
|
104
104
|
@pending_tool_history_mutex = Mutex.new
|
|
105
|
+
@deferred_tool_audits = []
|
|
105
106
|
@injected_tool_map = {}
|
|
106
107
|
@native_tool_source_map = {}
|
|
107
108
|
@freshly_triggered_keys = []
|
|
@@ -889,10 +890,14 @@ module Legion
|
|
|
889
890
|
end
|
|
890
891
|
|
|
891
892
|
def native_dispatch_options
|
|
892
|
-
injected_system =
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
893
|
+
injected_system = if @native_tool_loop_round.to_i.positive?
|
|
894
|
+
@cached_injected_system
|
|
895
|
+
else
|
|
896
|
+
@cached_injected_system = EnrichmentInjector.inject(
|
|
897
|
+
system: @request.system,
|
|
898
|
+
enrichments: @enrichments
|
|
899
|
+
)
|
|
900
|
+
end
|
|
896
901
|
|
|
897
902
|
options = {
|
|
898
903
|
system: injected_system,
|
|
@@ -940,11 +945,13 @@ module Legion
|
|
|
940
945
|
end
|
|
941
946
|
|
|
942
947
|
def client_tool_passthrough_enabled?
|
|
943
|
-
|
|
948
|
+
if @request.respond_to?(:metadata)
|
|
949
|
+
metadata = @request.metadata || {}
|
|
950
|
+
value = metadata.key?(:client_tool_passthrough) ? metadata[:client_tool_passthrough] : metadata['client_tool_passthrough']
|
|
951
|
+
return value if [true, false].include?(value)
|
|
952
|
+
end
|
|
944
953
|
|
|
945
|
-
|
|
946
|
-
value = metadata[:client_tool_passthrough] || metadata['client_tool_passthrough']
|
|
947
|
-
value == true
|
|
954
|
+
Legion::LLM::Settings.value(:tool_trigger, :client_tool_passthrough) != false
|
|
948
955
|
end
|
|
949
956
|
|
|
950
957
|
def non_executable_client_tool?(definition)
|
|
@@ -1426,28 +1433,52 @@ module Legion
|
|
|
1426
1433
|
end
|
|
1427
1434
|
|
|
1428
1435
|
def publish_tool_audit(tc_id, tc_name, result_str, is_error, duration_ms, started_at, finished_at)
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1436
|
+
@deferred_tool_audits << {
|
|
1437
|
+
tc_id: tc_id, tc_name: tc_name, result_str: result_str,
|
|
1438
|
+
is_error: is_error, duration_ms: duration_ms,
|
|
1439
|
+
started_at: started_at, finished_at: finished_at
|
|
1440
|
+
}
|
|
1441
|
+
end
|
|
1442
|
+
|
|
1443
|
+
def flush_deferred_tool_audits
|
|
1444
|
+
return if @deferred_tool_audits.empty?
|
|
1445
|
+
|
|
1446
|
+
audits = @deferred_tool_audits.dup
|
|
1447
|
+
@deferred_tool_audits.clear
|
|
1448
|
+
|
|
1449
|
+
request_id = @request.id
|
|
1450
|
+
conversation_id = @request.conversation_id
|
|
1451
|
+
exchange_id = @exchange_id
|
|
1452
|
+
caller_data = @request.caller
|
|
1453
|
+
classification = @request.classification
|
|
1454
|
+
tracing = @tracing
|
|
1455
|
+
|
|
1456
|
+
Concurrent::Promises.future do
|
|
1457
|
+
audits.each do |audit|
|
|
1458
|
+
Legion::LLM::Audit.emit_tools(
|
|
1459
|
+
request_id: request_id,
|
|
1460
|
+
conversation_id: conversation_id,
|
|
1461
|
+
exchange_id: exchange_id,
|
|
1462
|
+
tool_name: audit[:tc_name],
|
|
1463
|
+
tool_call: {
|
|
1464
|
+
id: audit[:tc_id],
|
|
1465
|
+
name: audit[:tc_name],
|
|
1466
|
+
status: audit[:is_error] ? :error : :success,
|
|
1467
|
+
duration_ms: audit[:duration_ms],
|
|
1468
|
+
started_at: audit[:started_at],
|
|
1469
|
+
finished_at: audit[:finished_at]
|
|
1470
|
+
},
|
|
1471
|
+
result: audit[:result_str][0, 4096],
|
|
1472
|
+
caller: caller_data,
|
|
1473
|
+
classification: classification,
|
|
1474
|
+
tracing: tracing,
|
|
1475
|
+
timestamp: audit[:finished_at],
|
|
1476
|
+
request_type: 'tool'
|
|
1477
|
+
)
|
|
1478
|
+
rescue StandardError => e
|
|
1479
|
+
Legion::Logging.log.warn("[llm][pipeline] publish_tool_audit failed tool=#{audit[:tc_name]}: #{e.message}")
|
|
1480
|
+
end
|
|
1481
|
+
end
|
|
1451
1482
|
end
|
|
1452
1483
|
|
|
1453
1484
|
def tool_call_field(tool_call, field)
|
|
@@ -1642,6 +1673,7 @@ module Legion
|
|
|
1642
1673
|
routing_reason: @audit.dig(:'routing:provider_selection', :data, :reason)
|
|
1643
1674
|
)
|
|
1644
1675
|
Steps::Metering.publish_or_spool(event)
|
|
1676
|
+
flush_deferred_tool_audits
|
|
1645
1677
|
rescue StandardError => e
|
|
1646
1678
|
@warnings << "metering error: #{e.message}"
|
|
1647
1679
|
handle_exception(e, level: :warn, operation: 'llm.pipeline.step_metering')
|
|
@@ -89,6 +89,13 @@ module Legion
|
|
|
89
89
|
return
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
+
scores = entries.filter_map { |e| e[:confidence] || e[:distance] }.map { |s| s.is_a?(Numeric) ? s.round(3) : s }
|
|
93
|
+
log.debug(
|
|
94
|
+
"[llm][steps][rag_context] action=results_scored request_id=#{@request&.id || 'unknown'} " \
|
|
95
|
+
"strategy=#{strategy} count=#{entries.size} scores=#{scores.inspect} " \
|
|
96
|
+
"types=#{entries.map { |e| e[:content_type] }.compact.tally.inspect}"
|
|
97
|
+
)
|
|
98
|
+
|
|
92
99
|
@enrichments['rag:context_retrieval'] = {
|
|
93
100
|
content: "#{Legion::LLM::Settings.config_value(result, :count)} entries retrieved via #{strategy}",
|
|
94
101
|
data: { entries: entries, strategy: strategy, count: Legion::LLM::Settings.config_value(result, :count) },
|
|
@@ -160,14 +167,30 @@ module Legion
|
|
|
160
167
|
def apollo_retrieve(query:, strategy:)
|
|
161
168
|
full_limit = rag_setting(:full_limit, 10)
|
|
162
169
|
compact_limit = rag_setting(:compact_limit, 5)
|
|
163
|
-
confidence = rag_setting(:min_confidence, 0.
|
|
170
|
+
confidence = rag_setting(:min_confidence, 0.85)
|
|
164
171
|
limit = apply_gaia_context_limit(strategy == :rag_compact ? compact_limit : full_limit,
|
|
165
172
|
strategy: strategy)
|
|
166
173
|
log_step_debug(:rag_context, :apollo_query, strategy: strategy, limit: limit, min_confidence: confidence)
|
|
167
174
|
|
|
168
175
|
general = apollo_retrieve_general(query: query, limit: limit, confidence: confidence)
|
|
169
176
|
history = apollo_retrieve_conversation_history(query: query, limit: limit, confidence: confidence)
|
|
170
|
-
merge_apollo_results(general, history)
|
|
177
|
+
result = merge_apollo_results(general, history)
|
|
178
|
+
filter_excluded_source_agents(result)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def filter_excluded_source_agents(result)
|
|
182
|
+
excluded = Array(rag_setting(:exclude_source_agents, []))
|
|
183
|
+
return result if excluded.empty?
|
|
184
|
+
|
|
185
|
+
entries = Array(result[:entries])
|
|
186
|
+
filtered = entries.reject { |e| excluded.include?(e[:source_agent].to_s) }
|
|
187
|
+
if filtered.size < entries.size
|
|
188
|
+
log.debug(
|
|
189
|
+
"[llm][steps][rag_context] action=source_agent_filter excluded=#{entries.size - filtered.size} " \
|
|
190
|
+
"remaining=#{filtered.size} agents=#{excluded.inspect}"
|
|
191
|
+
)
|
|
192
|
+
end
|
|
193
|
+
result.merge(entries: filtered, count: filtered.size)
|
|
171
194
|
end
|
|
172
195
|
|
|
173
196
|
def apollo_retrieve_general(query:, limit:, confidence:)
|
data/lib/legion/llm/settings.rb
CHANGED
|
@@ -377,12 +377,13 @@ module Legion
|
|
|
377
377
|
enabled: true,
|
|
378
378
|
full_limit: 10,
|
|
379
379
|
compact_limit: 5,
|
|
380
|
-
min_confidence: 0.
|
|
380
|
+
min_confidence: 0.85,
|
|
381
381
|
utilization_compact_threshold: 0.7,
|
|
382
382
|
utilization_skip_threshold: 0.9,
|
|
383
383
|
conversation_history_enabled: false,
|
|
384
384
|
trivial_max_chars: 20,
|
|
385
|
-
trivial_patterns: %w[hello hi hey ping pong test ok okay yes no thanks thank]
|
|
385
|
+
trivial_patterns: %w[hello hi hey ping pong test ok okay yes no thanks thank],
|
|
386
|
+
exclude_source_agents: %w[teams-api-ingest unknown teams-entity-extractor legion-interlink]
|
|
386
387
|
}
|
|
387
388
|
end
|
|
388
389
|
|
|
@@ -481,9 +482,10 @@ module Legion
|
|
|
481
482
|
|
|
482
483
|
def self.tool_trigger_defaults
|
|
483
484
|
{
|
|
484
|
-
scan_depth:
|
|
485
|
-
tool_limit:
|
|
486
|
-
local_tool_limit:
|
|
485
|
+
scan_depth: 10,
|
|
486
|
+
tool_limit: 25,
|
|
487
|
+
local_tool_limit: 100,
|
|
488
|
+
client_tool_passthrough: false
|
|
487
489
|
}
|
|
488
490
|
end
|
|
489
491
|
|
|
@@ -16,6 +16,7 @@ module Legion
|
|
|
16
16
|
extend Legion::Logging::Helper
|
|
17
17
|
|
|
18
18
|
LIST_SPECIAL_TOOLS_NAME = 'legion_list_special_tools'
|
|
19
|
+
LIST_ALL_TOOLS_NAME = 'legion_list_all_tools'
|
|
19
20
|
DEFAULT_TIMEOUT_MS = 120_000
|
|
20
21
|
MAX_TIMEOUT_MS = 600_000
|
|
21
22
|
PYTHON_PACKAGES = %w[
|
|
@@ -34,7 +35,7 @@ module Legion
|
|
|
34
35
|
module_function
|
|
35
36
|
|
|
36
37
|
def pinned_definitions
|
|
37
|
-
definitions = [special_tools_definition, ruby_definition]
|
|
38
|
+
definitions = [special_tools_definition, all_tools_definition, ruby_definition]
|
|
38
39
|
definitions.concat(python_definitions) if python_available?
|
|
39
40
|
definitions
|
|
40
41
|
end
|
|
@@ -43,6 +44,8 @@ module Legion
|
|
|
43
44
|
case normalize_tool_name(tool_name)
|
|
44
45
|
when LIST_SPECIAL_TOOLS_NAME
|
|
45
46
|
{ status: :success, result: Legion::JSON.dump(inventory) }
|
|
47
|
+
when LIST_ALL_TOOLS_NAME
|
|
48
|
+
{ status: :success, result: Legion::JSON.dump(all_tools_inventory(**args)) }
|
|
46
49
|
when 'ruby'
|
|
47
50
|
dispatch_runtime('ruby', ruby_path, **args)
|
|
48
51
|
when 'python', 'python3'
|
|
@@ -65,6 +68,32 @@ module Legion
|
|
|
65
68
|
}
|
|
66
69
|
end
|
|
67
70
|
|
|
71
|
+
def all_tools_inventory(**args)
|
|
72
|
+
tools = settings_extensions_tools
|
|
73
|
+
extension_filter = args[:extension] || args['extension']
|
|
74
|
+
deferred_filter = args.key?(:deferred) ? args[:deferred] : args['deferred']
|
|
75
|
+
|
|
76
|
+
if extension_filter
|
|
77
|
+
normalized_filter = extension_filter.to_s.tr('-', '_').delete_prefix('lex_')
|
|
78
|
+
tools = tools.select { |t| t[:extension].to_s.tr('-', '_').delete_prefix('lex_').include?(normalized_filter) }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
tools = tools.select { |t| t[:deferred] == deferred_filter } unless deferred_filter.nil?
|
|
82
|
+
|
|
83
|
+
grouped = tools.group_by { |t| t[:extension] || 'unknown' }
|
|
84
|
+
{
|
|
85
|
+
total: tools.size,
|
|
86
|
+
extensions: grouped.transform_values do |ext_tools|
|
|
87
|
+
ext_tools.group_by { |t| t[:runner] || 'default' }.transform_values do |runner_tools|
|
|
88
|
+
runner_tools.map { |t| { name: t[:name], description: t[:description], deferred: t[:deferred] } }
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
}
|
|
92
|
+
rescue StandardError => e
|
|
93
|
+
handle_exception(e, level: :warn, handled: true, operation: 'llm.tools.special.all_tools_inventory')
|
|
94
|
+
{ total: 0, extensions: {}, error: e.message }
|
|
95
|
+
end
|
|
96
|
+
|
|
68
97
|
def python_available?
|
|
69
98
|
!python_path.to_s.empty?
|
|
70
99
|
end
|
|
@@ -115,6 +144,22 @@ module Legion
|
|
|
115
144
|
)
|
|
116
145
|
end
|
|
117
146
|
|
|
147
|
+
def all_tools_definition
|
|
148
|
+
Types::ToolDefinition.build(
|
|
149
|
+
name: LIST_ALL_TOOLS_NAME,
|
|
150
|
+
description: 'List ALL registered Legion tools from all loaded extensions, grouped by extension and runner. ' \
|
|
151
|
+
'Use this to discover what tools are available for a specific domain (e.g. Teams, Apollo, identity).',
|
|
152
|
+
parameters: {
|
|
153
|
+
type: 'object',
|
|
154
|
+
properties: {
|
|
155
|
+
extension: { type: 'string', description: 'Filter by extension name (e.g. "microsoft_teams", "apollo"). Omit for all.' },
|
|
156
|
+
deferred: { type: 'boolean', description: 'Filter by deferred status. Omit for all.' }
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
source: { type: :special, handler: :all_tools_inventory, pinned: true }
|
|
160
|
+
)
|
|
161
|
+
end
|
|
162
|
+
|
|
118
163
|
def ruby_definition
|
|
119
164
|
Types::ToolDefinition.build(
|
|
120
165
|
name: 'ruby',
|
data/lib/legion/llm/version.rb
CHANGED