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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 221d8482faa90a3fa8e4bbefe90999838a56b96bc34d6506956f6b8ac90ab290
4
- data.tar.gz: a3c3ce4cde046e8cd1af18874e04da1698d4527549fc70df274d14c0830f3803
3
+ metadata.gz: 715bd8c0918939545eda0cf832d81aa23e69c807ed6055fdb4d95f4177c99449
4
+ data.tar.gz: 12e42d3d2fdc02c4ca7764a264af90631141952982d9726c02d0e9ea7de87a92
5
5
  SHA512:
6
- metadata.gz: fd2fdd7bce06cd6efa9a8b583072b947f14ffdaa5a1898b65f2027da8693c79bdd56b3553d04f6f6329f2f3a9d71c0699947f371aa5715708fd538a144819363
7
- data.tar.gz: 7c3c36ca8b7db0075b18ea656e3c13c9235d873362d8febbe663c96eb07a1fa562a16d754f1bfed42d909698b466c303d7395e79caa0093ad1d1008c64389f69
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::Ingest.ingest(
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
- metadata: { submitted_by: Legion::LLM::PublisherIdentity.requested_by }
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::Ingest)
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 = EnrichmentInjector.inject(
893
- system: @request.system,
894
- enrichments: @enrichments
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
- return false unless @request.respond_to?(:metadata)
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
- metadata = @request.metadata || {}
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
- Legion::LLM::Audit.emit_tools(
1430
- request_id: @request.id,
1431
- conversation_id: @request.conversation_id,
1432
- exchange_id: @exchange_id,
1433
- tool_name: tc_name,
1434
- tool_call: {
1435
- id: tc_id,
1436
- name: tc_name,
1437
- status: is_error ? :error : :success,
1438
- duration_ms: duration_ms,
1439
- started_at: started_at,
1440
- finished_at: finished_at
1441
- },
1442
- result: result_str[0, 4096],
1443
- caller: @request.caller,
1444
- classification: @request.classification,
1445
- tracing: @tracing,
1446
- timestamp: finished_at,
1447
- request_type: 'tool'
1448
- )
1449
- rescue StandardError => e
1450
- handle_exception(e, level: :warn, operation: 'llm.pipeline.publish_tool_audit', tool_name: tc_name)
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.5)
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:)
@@ -377,12 +377,13 @@ module Legion
377
377
  enabled: true,
378
378
  full_limit: 10,
379
379
  compact_limit: 5,
380
- min_confidence: 0.5,
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: 10,
485
- tool_limit: 25,
486
- local_tool_limit: 100
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',
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module LLM
5
- VERSION = '0.9.30'
5
+ VERSION = '0.9.32'
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.9.30
4
+ version: 0.9.32
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity