legion-llm 0.6.17 → 0.6.18
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 +3 -0
- data/CLAUDE.md +6 -2
- data/lib/legion/llm/pipeline/executor.rb +22 -39
- data/lib/legion/llm/pipeline/mcp_tool_adapter.rb +3 -67
- data/lib/legion/llm/pipeline/tool_adapter.rb +14 -1
- data/lib/legion/llm/routes.rb +0 -11
- data/lib/legion/llm/version.rb +1 -1
- data/lib/legion/llm.rb +3 -6
- metadata +1 -2
- data/lib/legion/llm/tool_registry.rb +0 -40
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 83b1bf0eb338e47eb37627eb7004afb30b3fba91627577034554827edd797bcf
|
|
4
|
+
data.tar.gz: 54ba4b787dfb7ebbbe6ef10f363cb237075f5b37f74467c9408db723d9a36c48
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1a546dca02b403bc025621c1a980cf081114c383f97bd3ce3eef245a7b3314cdaacc7328cbbbfd7879cbf95dd53069e48b9af8f2401099e68966e0ee4e0b624f
|
|
7
|
+
data.tar.gz: ddb6aabeca04cdbdacfd2412ee2713dfaecc4b17911b55246ec680538874df7725eabef8768fe69c8202952d6a3f14d0a0ae27696cd64ebf2727679f7947cd61
|
data/CHANGELOG.md
CHANGED
data/CLAUDE.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
Core LegionIO gem providing LLM capabilities to all extensions. Wraps ruby_llm to provide a consistent interface for chat, embeddings, tool use, and agents across multiple providers (Bedrock, Anthropic, OpenAI, Gemini, Ollama). Includes a dynamic weighted routing engine that dispatches requests across local, fleet, and cloud tiers based on caller intent, priority rules, time schedules, cost multipliers, and real-time provider health.
|
|
9
9
|
|
|
10
10
|
**GitHub**: https://github.com/LegionIO/legion-llm
|
|
11
|
-
**Version**: 0.
|
|
11
|
+
**Version**: 0.6.18
|
|
12
12
|
**License**: Apache-2.0
|
|
13
13
|
|
|
14
14
|
## Architecture
|
|
@@ -61,8 +61,12 @@ Legion::LLM (lib/legion/llm.rb)
|
|
|
61
61
|
│ ├── Timeline # Ordered event recording with participant tracking
|
|
62
62
|
│ ├── Executor # 18-step pipeline skeleton with profile-aware execution
|
|
63
63
|
│ ├── Steps/
|
|
64
|
-
│ │
|
|
64
|
+
│ │ ├── Metering # Metering event builder (absorbed from lex-llm-gateway)
|
|
65
|
+
│ │ └── ToolDiscovery # Step 9 — formerly McpDiscovery; renamed to ToolDiscovery (McpDiscovery kept as backwards alias)
|
|
65
66
|
│ └── Executor#call_stream # Streaming variant: pre-provider steps, yield chunks, post-provider steps
|
|
67
|
+
│
|
|
68
|
+
│ Note: Legion::LLM::ToolRegistry was removed. Tool registration now lives in Legion::Tools::Registry (LegionIO gem).
|
|
69
|
+
│ McpToolAdapter renamed to ToolAdapter; McpToolAdapter kept as a backwards-compatible alias.
|
|
66
70
|
├── CostEstimator # Model cost estimation with fuzzy pricing (absorbed from lex-llm-gateway)
|
|
67
71
|
├── Fleet # Fleet RPC dispatch (absorbed from lex-llm-gateway)
|
|
68
72
|
│ ├── Dispatcher # Fleet dispatch with timeout and availability checks
|
|
@@ -45,29 +45,6 @@ module Legion
|
|
|
45
45
|
|
|
46
46
|
ASYNC_SAFE_STEPS = %i[post_response knowledge_capture response_return].freeze
|
|
47
47
|
|
|
48
|
-
ALWAYS_LOADED_MCP_TOOLS = %w[
|
|
49
|
-
legion_do
|
|
50
|
-
legion_get_status
|
|
51
|
-
legion_run_task
|
|
52
|
-
legion_describe_runner
|
|
53
|
-
legion_list_extensions
|
|
54
|
-
legion_get_extension
|
|
55
|
-
legion_list_tasks
|
|
56
|
-
legion_get_task
|
|
57
|
-
legion_get_task_logs
|
|
58
|
-
legion_query_knowledge
|
|
59
|
-
legion_knowledge_health
|
|
60
|
-
legion_knowledge_context
|
|
61
|
-
legion_list_workers
|
|
62
|
-
legion_show_worker
|
|
63
|
-
legion_mesh_status
|
|
64
|
-
legion_list_peers
|
|
65
|
-
legion_tools
|
|
66
|
-
legion_search_sessions
|
|
67
|
-
].freeze
|
|
68
|
-
|
|
69
|
-
private_constant :ALWAYS_LOADED_MCP_TOOLS
|
|
70
|
-
|
|
71
48
|
ASYNC_THREAD_POOL = Concurrent::FixedThreadPool.new(4, fallback_policy: :caller_runs)
|
|
72
49
|
|
|
73
50
|
def initialize(request)
|
|
@@ -109,24 +86,39 @@ module Legion
|
|
|
109
86
|
def inject_registry_tools(session)
|
|
110
87
|
return unless defined?(::Legion::Tools::Registry)
|
|
111
88
|
|
|
112
|
-
requested = requested_deferred_tool_names
|
|
113
|
-
always_loaded = always_loaded_tool_names
|
|
114
89
|
injected_names = []
|
|
115
90
|
|
|
91
|
+
# Always-loaded tools — inject all unconditionally
|
|
116
92
|
::Legion::Tools::Registry.tools.each do |tool_class|
|
|
117
93
|
adapter = ToolAdapter.new(tool_class)
|
|
118
|
-
next unless always_loaded.include?(adapter.name) || requested.include?(adapter.name)
|
|
119
|
-
|
|
120
94
|
session.with_tool(adapter)
|
|
121
95
|
injected_names << adapter.name
|
|
122
96
|
rescue StandardError => e
|
|
123
|
-
@warnings << "Failed to inject tool: #{e.message}"
|
|
124
|
-
handle_exception(e, level: :warn, operation: 'llm.pipeline.
|
|
97
|
+
@warnings << "Failed to inject always tool: #{e.message}"
|
|
98
|
+
handle_exception(e, level: :warn, operation: 'llm.pipeline.inject_always_tool')
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Requested deferred tools — inject only if explicitly requested
|
|
102
|
+
deferred = ::Legion::Tools::Registry.respond_to?(:deferred_tools) ? ::Legion::Tools::Registry.deferred_tools : []
|
|
103
|
+
requested = requested_deferred_tool_names
|
|
104
|
+
if requested.any?
|
|
105
|
+
deferred.each do |tool_class|
|
|
106
|
+
adapter = ToolAdapter.new(tool_class)
|
|
107
|
+
next unless requested.include?(adapter.name)
|
|
108
|
+
|
|
109
|
+
session.with_tool(adapter)
|
|
110
|
+
injected_names << adapter.name
|
|
111
|
+
rescue StandardError => e
|
|
112
|
+
@warnings << "Failed to inject deferred tool: #{e.message}"
|
|
113
|
+
handle_exception(e, level: :warn, operation: 'llm.pipeline.inject_deferred_tool')
|
|
114
|
+
end
|
|
125
115
|
end
|
|
126
116
|
|
|
127
117
|
log.info(
|
|
128
118
|
"[llm][tools] inject request_id=#{@request.id} " \
|
|
129
|
-
"
|
|
119
|
+
"always=#{::Legion::Tools::Registry.tools.size} " \
|
|
120
|
+
"deferred_available=#{deferred.size} " \
|
|
121
|
+
"requested_deferred=#{requested.size} " \
|
|
130
122
|
"injected=#{injected_names.size} names=#{injected_names.first(25).join(',')}"
|
|
131
123
|
)
|
|
132
124
|
rescue StandardError => e
|
|
@@ -137,15 +129,6 @@ module Legion
|
|
|
137
129
|
# Backwards compatibility alias
|
|
138
130
|
alias inject_discovered_tools inject_registry_tools
|
|
139
131
|
|
|
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
|
-
|
|
149
132
|
def execute_steps
|
|
150
133
|
executed = 0
|
|
151
134
|
skipped = 0
|
|
@@ -1,69 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
require
|
|
5
|
-
|
|
6
|
-
module Legion
|
|
7
|
-
module LLM
|
|
8
|
-
module Pipeline
|
|
9
|
-
class McpToolAdapter < RubyLLM::Tool
|
|
10
|
-
include Legion::Logging::Helper
|
|
11
|
-
|
|
12
|
-
def initialize(mcp_tool_class)
|
|
13
|
-
@mcp_tool_class = mcp_tool_class
|
|
14
|
-
raw_name = mcp_tool_class.respond_to?(:tool_name) ? mcp_tool_class.tool_name : mcp_tool_class.name.to_s
|
|
15
|
-
@tool_name = raw_name.tr('.', '_')
|
|
16
|
-
@tool_desc = mcp_tool_class.respond_to?(:description) ? mcp_tool_class.description.to_s : ''
|
|
17
|
-
@tool_schema = mcp_tool_class.respond_to?(:input_schema) ? mcp_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 = @mcp_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.mcp_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
|
-
end
|
|
68
|
-
end
|
|
69
|
-
end
|
|
3
|
+
# Backwards-compatibility shim — the implementation moved to tool_adapter.rb.
|
|
4
|
+
# Callers that require this path directly will still find McpToolAdapter via the alias.
|
|
5
|
+
require_relative 'tool_adapter'
|
|
@@ -9,10 +9,12 @@ module Legion
|
|
|
9
9
|
class ToolAdapter < RubyLLM::Tool
|
|
10
10
|
include Legion::Logging::Helper
|
|
11
11
|
|
|
12
|
+
MAX_TOOL_NAME_LENGTH = 64
|
|
13
|
+
|
|
12
14
|
def initialize(tool_class)
|
|
13
15
|
@tool_class = tool_class
|
|
14
16
|
raw_name = tool_class.respond_to?(:tool_name) ? tool_class.tool_name : tool_class.name.to_s
|
|
15
|
-
@tool_name = raw_name
|
|
17
|
+
@tool_name = sanitize_tool_name(raw_name)
|
|
16
18
|
@tool_desc = tool_class.respond_to?(:description) ? tool_class.description.to_s : ''
|
|
17
19
|
@tool_schema = tool_class.respond_to?(:input_schema) ? tool_class.input_schema : nil
|
|
18
20
|
super()
|
|
@@ -63,6 +65,17 @@ module Legion
|
|
|
63
65
|
def summarize_payload(payload)
|
|
64
66
|
payload.to_s[0, 200].inspect
|
|
65
67
|
end
|
|
68
|
+
|
|
69
|
+
# Bedrock constraints: [a-zA-Z0-9_-]+ and max 64 chars.
|
|
70
|
+
# Falls back to a stable name derived from the class object_id if sanitization yields
|
|
71
|
+
# an empty string (e.g. all chars stripped), ensuring the result always satisfies the
|
|
72
|
+
# at-least-one-character requirement.
|
|
73
|
+
def sanitize_tool_name(raw)
|
|
74
|
+
name = raw.tr('.', '_')
|
|
75
|
+
name = name.gsub(/[^a-zA-Z0-9_-]/, '') # strip ?, !, etc.
|
|
76
|
+
name = name[0, MAX_TOOL_NAME_LENGTH] if name.length > MAX_TOOL_NAME_LENGTH
|
|
77
|
+
name.empty? ? "tool_#{@tool_class.object_id}" : name
|
|
78
|
+
end
|
|
66
79
|
end
|
|
67
80
|
|
|
68
81
|
# Backwards compatibility alias
|
data/lib/legion/llm/routes.rb
CHANGED
|
@@ -12,17 +12,6 @@ require 'open3'
|
|
|
12
12
|
require 'time'
|
|
13
13
|
require 'legion/logging/helper'
|
|
14
14
|
|
|
15
|
-
begin
|
|
16
|
-
require 'legion/cli/chat/tools/search_traces'
|
|
17
|
-
if defined?(Legion::LLM::ToolRegistry) && defined?(Legion::CLI::Chat::Tools::SearchTraces)
|
|
18
|
-
Legion::LLM::ToolRegistry.register(Legion::CLI::Chat::Tools::SearchTraces)
|
|
19
|
-
end
|
|
20
|
-
rescue LoadError => e
|
|
21
|
-
if defined?(Legion::Logging) && Legion::Logging.respond_to?(:log_exception)
|
|
22
|
-
Legion::Logging.log_exception(e, payload_summary: 'SearchTraces not available for API', component_type: :api)
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
15
|
module Legion
|
|
27
16
|
module LLM
|
|
28
17
|
module Routes
|
data/lib/legion/llm/version.rb
CHANGED
data/lib/legion/llm.rb
CHANGED
|
@@ -32,7 +32,6 @@ require_relative 'llm/scheduling'
|
|
|
32
32
|
require_relative 'llm/off_peak'
|
|
33
33
|
require_relative 'llm/cost_tracker'
|
|
34
34
|
require_relative 'llm/token_tracker'
|
|
35
|
-
require_relative 'llm/tool_registry'
|
|
36
35
|
require_relative 'llm/override_confidence'
|
|
37
36
|
require_relative 'llm/routes'
|
|
38
37
|
|
|
@@ -108,7 +107,7 @@ module Legion
|
|
|
108
107
|
# for automatic metering and fleet dispatch
|
|
109
108
|
def chat(model: nil, provider: nil, intent: nil, tier: nil, escalate: nil,
|
|
110
109
|
max_escalations: nil, quality_check: nil, message: nil, **kwargs, &)
|
|
111
|
-
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
110
|
+
started_at = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
112
111
|
log_inference_request(
|
|
113
112
|
request_type: :chat,
|
|
114
113
|
requested_model: model,
|
|
@@ -154,7 +153,7 @@ module Legion
|
|
|
154
153
|
# Send a single message — daemon-first, falls through to direct on unavailability.
|
|
155
154
|
def ask(message:, model: nil, provider: nil, intent: nil, tier: nil,
|
|
156
155
|
context: {}, identity: nil, &)
|
|
157
|
-
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
156
|
+
started_at = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
158
157
|
log_inference_request(
|
|
159
158
|
request_type: :ask,
|
|
160
159
|
requested_model: model,
|
|
@@ -367,7 +366,7 @@ module Legion
|
|
|
367
366
|
end
|
|
368
367
|
|
|
369
368
|
def elapsed_ms_since(started_at)
|
|
370
|
-
((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000).round
|
|
369
|
+
((::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - started_at) * 1000).round
|
|
371
370
|
end
|
|
372
371
|
|
|
373
372
|
def inference_input_payload(message:, messages:)
|
|
@@ -690,8 +689,6 @@ module Legion
|
|
|
690
689
|
def adapted_registry_tools
|
|
691
690
|
tool_classes = if defined?(::Legion::Tools::Registry)
|
|
692
691
|
::Legion::Tools::Registry.tools
|
|
693
|
-
elsif defined?(::Legion::LLM::ToolRegistry)
|
|
694
|
-
::Legion::LLM::ToolRegistry.tools
|
|
695
692
|
else
|
|
696
693
|
return []
|
|
697
694
|
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.
|
|
4
|
+
version: 0.6.18
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -308,7 +308,6 @@ files:
|
|
|
308
308
|
- lib/legion/llm/shadow_eval.rb
|
|
309
309
|
- lib/legion/llm/structured_output.rb
|
|
310
310
|
- lib/legion/llm/token_tracker.rb
|
|
311
|
-
- lib/legion/llm/tool_registry.rb
|
|
312
311
|
- lib/legion/llm/transport/exchanges/audit.rb
|
|
313
312
|
- lib/legion/llm/transport/exchanges/escalation.rb
|
|
314
313
|
- lib/legion/llm/transport/messages/audit_event.rb
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'legion/logging/helper'
|
|
4
|
-
|
|
5
|
-
module Legion
|
|
6
|
-
module LLM
|
|
7
|
-
module ToolRegistry
|
|
8
|
-
extend Legion::Logging::Helper
|
|
9
|
-
|
|
10
|
-
@tools = []
|
|
11
|
-
@mutex = Mutex.new
|
|
12
|
-
|
|
13
|
-
class << self
|
|
14
|
-
def register(tool_class)
|
|
15
|
-
registered = @mutex.synchronize do
|
|
16
|
-
next false if @tools.include?(tool_class)
|
|
17
|
-
|
|
18
|
-
@tools << tool_class
|
|
19
|
-
true
|
|
20
|
-
end
|
|
21
|
-
if registered
|
|
22
|
-
log.info("[llm][tools] registered class=#{tool_class}")
|
|
23
|
-
else
|
|
24
|
-
log.debug("[llm][tools] already_registered class=#{tool_class}")
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def tools
|
|
29
|
-
@mutex.synchronize { @tools.dup }
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def clear
|
|
33
|
-
count = @mutex.synchronize { @tools.size }
|
|
34
|
-
@mutex.synchronize { @tools.clear }
|
|
35
|
-
log.info("[llm][tools] registry_cleared count=#{count}")
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|