legion-llm 0.6.5 → 0.6.6
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 +10 -0
- data/lib/legion/llm/embeddings.rb +13 -4
- data/lib/legion/llm/pipeline/executor.rb +24 -1
- data/lib/legion/llm/pipeline/mcp_tool_adapter.rb +47 -0
- data/lib/legion/llm/pipeline/steps/mcp_discovery.rb +44 -21
- data/lib/legion/llm/pipeline.rb +1 -0
- 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: 56b729952a9d16d1b1ab83a669d86d7be4b32b760e3d96e57e19bec1c6158fff
|
|
4
|
+
data.tar.gz: 85336f8cbe3224d03bdc2c93f923711ae081e15fbea41ff4eee0e45a45a99faf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6bfbe51c0ebb5ece47ebe20e17d925051dcd22ce5a084040f20207b9498597fb2b763aa68933a23aeacbd82d5d064365dd71f3dadaca7ddda4dd282e17d12d90
|
|
7
|
+
data.tar.gz: 7e42e9ee15e04a3ca99d1b00a1355d8c261d3231b006b160e741da581a520787fa3a171afad44b25db28afeec92419fde0aab9508a33a872c9e6a304859ab46e
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.6.6] - 2026-04-01
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- `McpToolAdapter` — wraps MCP server tool classes as RubyLLM::Tool instances for LLM session injection
|
|
9
|
+
- Pipeline `McpDiscovery` step discovers both server-side (Legion::MCP::Server) and client-side (MCP::Client::Pool) tools
|
|
10
|
+
- Tool name sanitization: dots replaced with underscores for Bedrock compatibility (`[a-zA-Z0-9_-]+`)
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Skip RubyLLM-based embedding health check for Azure provider since it uses direct HTTP with SNI host injection
|
|
14
|
+
|
|
5
15
|
## [0.6.5] - 2026-04-01
|
|
6
16
|
|
|
7
17
|
### Fixed
|
|
@@ -316,18 +316,27 @@ module Legion
|
|
|
316
316
|
raise 'Azure OpenAI embedding not configured (llm.providers.azure.api_base required)' unless api_base
|
|
317
317
|
|
|
318
318
|
host = URI.parse(api_base).host
|
|
319
|
-
|
|
319
|
+
target_ip = ip
|
|
320
320
|
path = "/openai/deployments/#{model}/embeddings?api-version=2024-02-01"
|
|
321
|
+
Legion::Logging.info "Azure embed connecting to #{host}:443 (ip_override=#{target_ip.inspect})" if defined?(Legion::Logging)
|
|
321
322
|
|
|
322
323
|
require 'net/http'
|
|
323
|
-
|
|
324
|
+
require 'openssl'
|
|
325
|
+
http = Net::HTTP.new(host, 443)
|
|
324
326
|
http.use_ssl = true
|
|
325
|
-
http.
|
|
327
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
328
|
+
http.open_timeout = 10
|
|
326
329
|
http.read_timeout = 30
|
|
327
330
|
|
|
331
|
+
# When an IP override is set, resolve the FQDN to the private endpoint IP
|
|
332
|
+
# while keeping the FQDN as SNI for TLS handshake
|
|
333
|
+
if target_ip
|
|
334
|
+
addr = Addrinfo.tcp(target_ip, 443)
|
|
335
|
+
http.ipaddr = addr.ip_address
|
|
336
|
+
end
|
|
337
|
+
|
|
328
338
|
req = Net::HTTP::Post.new(path)
|
|
329
339
|
req['Content-Type'] = 'application/json'
|
|
330
|
-
req['Host'] = host
|
|
331
340
|
req['api-key'] = api_key
|
|
332
341
|
body = { input: input }
|
|
333
342
|
body[:dimensions] = dimensions || TARGET_DIMENSION
|
|
@@ -76,6 +76,22 @@ module Legion
|
|
|
76
76
|
|
|
77
77
|
private
|
|
78
78
|
|
|
79
|
+
def inject_discovered_tools(session)
|
|
80
|
+
return unless defined?(::Legion::MCP) && ::Legion::MCP.respond_to?(:server)
|
|
81
|
+
|
|
82
|
+
server = ::Legion::MCP.server
|
|
83
|
+
return unless server.respond_to?(:tool_registry)
|
|
84
|
+
|
|
85
|
+
server.tool_registry.each do |mcp_tool_class|
|
|
86
|
+
adapter = McpToolAdapter.new(mcp_tool_class)
|
|
87
|
+
session.with_tool(adapter)
|
|
88
|
+
rescue StandardError => e
|
|
89
|
+
@warnings << "Failed to inject tool: #{e.message}"
|
|
90
|
+
end
|
|
91
|
+
rescue StandardError => e
|
|
92
|
+
@warnings << "Tool injection error: #{e.message}"
|
|
93
|
+
end
|
|
94
|
+
|
|
79
95
|
def execute_steps
|
|
80
96
|
executed = 0
|
|
81
97
|
skipped = 0
|
|
@@ -403,7 +419,13 @@ module Legion
|
|
|
403
419
|
session.with_tool(tool) if tool.is_a?(Class)
|
|
404
420
|
end
|
|
405
421
|
|
|
406
|
-
|
|
422
|
+
if defined?(ToolRegistry)
|
|
423
|
+
ToolRegistry.tools.each do |t|
|
|
424
|
+
Legion::Logging.fatal("Injecting ToolRegistry tool: #{t.class} #{t.respond_to?(:tool_name) ? t.tool_name : t}") if defined?(Legion::Logging)
|
|
425
|
+
session.with_tool(t)
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
inject_discovered_tools(session)
|
|
407
429
|
|
|
408
430
|
injected_system = EnrichmentInjector.inject(
|
|
409
431
|
system: @request.system,
|
|
@@ -545,6 +567,7 @@ module Legion
|
|
|
545
567
|
|
|
546
568
|
(@request.tools || []).each { |tool| session.with_tool(tool) if tool.is_a?(Class) }
|
|
547
569
|
ToolRegistry.tools.each { |t| session.with_tool(t) } if defined?(ToolRegistry)
|
|
570
|
+
inject_discovered_tools(session)
|
|
548
571
|
|
|
549
572
|
messages = @request.messages
|
|
550
573
|
prior = messages.size > 1 ? messages[0..-2] : []
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ruby_llm'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module LLM
|
|
7
|
+
module Pipeline
|
|
8
|
+
class McpToolAdapter < RubyLLM::Tool
|
|
9
|
+
def initialize(mcp_tool_class)
|
|
10
|
+
@mcp_tool_class = mcp_tool_class
|
|
11
|
+
raw_name = mcp_tool_class.respond_to?(:tool_name) ? mcp_tool_class.tool_name : mcp_tool_class.name.to_s
|
|
12
|
+
@tool_name = raw_name.tr('.', '_')
|
|
13
|
+
@tool_desc = mcp_tool_class.respond_to?(:description) ? mcp_tool_class.description.to_s : ''
|
|
14
|
+
@tool_schema = mcp_tool_class.respond_to?(:input_schema) ? mcp_tool_class.input_schema : nil
|
|
15
|
+
super()
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def name
|
|
19
|
+
@tool_name
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def description
|
|
23
|
+
@tool_desc
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def params_schema
|
|
27
|
+
return @params_schema if defined?(@params_schema)
|
|
28
|
+
|
|
29
|
+
@params_schema = (RubyLLM::Utils.deep_stringify_keys(@tool_schema) if @tool_schema.is_a?(Hash))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def execute(**)
|
|
33
|
+
result = @mcp_tool_class.call(**)
|
|
34
|
+
if result.is_a?(Hash) && result[:content]
|
|
35
|
+
result[:content].map { |c| c[:text] || c['text'] }.compact.join("\n")
|
|
36
|
+
elsif result.is_a?(String)
|
|
37
|
+
result
|
|
38
|
+
else
|
|
39
|
+
result.to_s
|
|
40
|
+
end
|
|
41
|
+
rescue StandardError => e
|
|
42
|
+
"Tool error: #{e.message}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -7,35 +7,22 @@ module Legion
|
|
|
7
7
|
module McpDiscovery
|
|
8
8
|
def step_mcp_discovery
|
|
9
9
|
@discovered_tools ||= []
|
|
10
|
-
|
|
11
|
-
unless defined?(::Legion::MCP::Client::Pool)
|
|
12
|
-
@warnings << 'MCP Client unavailable for tool discovery'
|
|
13
|
-
record_mcp_timeline(0)
|
|
14
|
-
return
|
|
15
|
-
end
|
|
16
|
-
|
|
17
10
|
start_time = Time.now
|
|
18
|
-
mcp_tools = ::Legion::MCP::Client::Pool.all_tools
|
|
19
11
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
name: tool[:name],
|
|
23
|
-
description: tool[:description],
|
|
24
|
-
parameters: tool[:input_schema],
|
|
25
|
-
source: tool[:source]
|
|
26
|
-
}
|
|
27
|
-
end
|
|
12
|
+
discover_server_tools
|
|
13
|
+
discover_client_tools
|
|
28
14
|
|
|
29
|
-
|
|
30
|
-
|
|
15
|
+
total = @discovered_tools.size
|
|
16
|
+
if total.positive?
|
|
17
|
+
sources = @discovered_tools.filter_map { |t| t.dig(:source, :server) || t.dig(:source, :type) }.uniq
|
|
31
18
|
@enrichments['mcp:tool_discovery'] = {
|
|
32
|
-
content: "#{
|
|
33
|
-
data: { tool_count:
|
|
19
|
+
content: "#{total} tools from #{sources.length} sources",
|
|
20
|
+
data: { tool_count: total, sources: sources },
|
|
34
21
|
timestamp: Time.now
|
|
35
22
|
}
|
|
36
23
|
end
|
|
37
24
|
|
|
38
|
-
record_mcp_timeline(
|
|
25
|
+
record_mcp_timeline(total, start_time)
|
|
39
26
|
rescue StandardError => e
|
|
40
27
|
@warnings << "MCP discovery error: #{e.message}"
|
|
41
28
|
record_mcp_timeline(0)
|
|
@@ -43,6 +30,42 @@ module Legion
|
|
|
43
30
|
|
|
44
31
|
private
|
|
45
32
|
|
|
33
|
+
def discover_server_tools
|
|
34
|
+
return unless defined?(::Legion::MCP) && ::Legion::MCP.respond_to?(:server)
|
|
35
|
+
|
|
36
|
+
server = ::Legion::MCP.server
|
|
37
|
+
return unless server.respond_to?(:tool_registry)
|
|
38
|
+
|
|
39
|
+
server.tool_registry.each do |tool_class|
|
|
40
|
+
name = tool_class.respond_to?(:tool_name) ? tool_class.tool_name : tool_class.name
|
|
41
|
+
desc = tool_class.respond_to?(:description) ? tool_class.description : ''
|
|
42
|
+
schema = tool_class.respond_to?(:input_schema) ? tool_class.input_schema : {}
|
|
43
|
+
@discovered_tools << {
|
|
44
|
+
name: name,
|
|
45
|
+
description: desc,
|
|
46
|
+
parameters: schema,
|
|
47
|
+
source: { type: :server, server: 'legion' }
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
rescue StandardError => e
|
|
51
|
+
@warnings << "Server tool discovery error: #{e.message}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def discover_client_tools
|
|
55
|
+
return unless defined?(::Legion::MCP::Client::Pool)
|
|
56
|
+
|
|
57
|
+
::Legion::MCP::Client::Pool.all_tools.each do |tool|
|
|
58
|
+
@discovered_tools << {
|
|
59
|
+
name: tool[:name],
|
|
60
|
+
description: tool[:description],
|
|
61
|
+
parameters: tool[:input_schema],
|
|
62
|
+
source: tool[:source]
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
rescue StandardError => e
|
|
66
|
+
@warnings << "Client tool discovery error: #{e.message}"
|
|
67
|
+
end
|
|
68
|
+
|
|
46
69
|
def record_mcp_timeline(count, start_time = nil)
|
|
47
70
|
duration = start_time ? ((Time.now - start_time) * 1000).to_i : 0
|
|
48
71
|
@timeline.record(
|
data/lib/legion/llm/pipeline.rb
CHANGED
|
@@ -11,6 +11,7 @@ require_relative 'pipeline/gaia_caller'
|
|
|
11
11
|
require_relative 'pipeline/tool_dispatcher'
|
|
12
12
|
require_relative 'pipeline/enrichment_injector'
|
|
13
13
|
require_relative 'pipeline/steps'
|
|
14
|
+
require_relative 'pipeline/mcp_tool_adapter'
|
|
14
15
|
require_relative 'pipeline/executor'
|
|
15
16
|
|
|
16
17
|
module Legion
|
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.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -265,6 +265,7 @@ files:
|
|
|
265
265
|
- lib/legion/llm/pipeline/enrichment_injector.rb
|
|
266
266
|
- lib/legion/llm/pipeline/executor.rb
|
|
267
267
|
- lib/legion/llm/pipeline/gaia_caller.rb
|
|
268
|
+
- lib/legion/llm/pipeline/mcp_tool_adapter.rb
|
|
268
269
|
- lib/legion/llm/pipeline/profile.rb
|
|
269
270
|
- lib/legion/llm/pipeline/request.rb
|
|
270
271
|
- lib/legion/llm/pipeline/response.rb
|