ruby_llm_mesh 0.1.0 → 2.2.0
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 +58 -0
- data/CODE_OF_CONDUCT.md +77 -3
- data/CONTRIBUTING.md +7 -1
- data/PRIVACY.md +56 -0
- data/README.md +104 -79
- data/Rakefile +28 -0
- data/ext/chimera_core/Cargo.toml +14 -0
- data/ext/chimera_core/src/lib.rs +194 -0
- data/lib/ruby_llm_mesh/budget.rb +150 -0
- data/lib/ruby_llm_mesh/cache/memory_store.rb +47 -0
- data/lib/ruby_llm_mesh/cache/redis_store.rb +110 -0
- data/lib/ruby_llm_mesh/cache/semantic_cache.rb +126 -0
- data/lib/ruby_llm_mesh/circuit_breaker.rb +11 -1
- data/lib/ruby_llm_mesh/configuration.rb +57 -1
- data/lib/ruby_llm_mesh/errors.rb +11 -0
- data/lib/ruby_llm_mesh/mesh/health_monitor.rb +104 -0
- data/lib/ruby_llm_mesh/mesh/peer_registry.rb +112 -0
- data/lib/ruby_llm_mesh/native_core.rb +199 -0
- data/lib/ruby_llm_mesh/providers/base.rb +5 -1
- data/lib/ruby_llm_mesh/providers/local_node.rb +37 -5
- data/lib/ruby_llm_mesh/response.rb +6 -3
- data/lib/ruby_llm_mesh/router.rb +133 -8
- data/lib/ruby_llm_mesh/sovereign_mesh.rb +138 -0
- data/lib/ruby_llm_mesh/version.rb +1 -1
- data/lib/ruby_llm_mesh.rb +42 -1
- data/ruby_llm_mesh.gemspec +21 -8
- data/sig/ruby_llm_mesh.rbs +30 -1
- metadata +35 -6
data/lib/ruby_llm_mesh/router.rb
CHANGED
|
@@ -5,7 +5,8 @@ module RubyLlmMesh
|
|
|
5
5
|
PROVIDER_MAP = {
|
|
6
6
|
openai: Providers::Openai,
|
|
7
7
|
anthropic: Providers::Anthropic,
|
|
8
|
-
local_node: Providers::LocalNode
|
|
8
|
+
local_node: Providers::LocalNode,
|
|
9
|
+
local_mesh: Providers::LocalNode # alias — multi-peer aware via PeerRegistry
|
|
9
10
|
}.freeze
|
|
10
11
|
|
|
11
12
|
class << self
|
|
@@ -19,16 +20,43 @@ module RubyLlmMesh
|
|
|
19
20
|
def reset_circuit_breaker!
|
|
20
21
|
@circuit_breaker = nil
|
|
21
22
|
end
|
|
23
|
+
|
|
24
|
+
def semantic_cache
|
|
25
|
+
Cache::SemanticCache.instance
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def peer_registry
|
|
29
|
+
Mesh::PeerRegistry.instance
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def health_monitor
|
|
33
|
+
Mesh::HealthMonitor.instance
|
|
34
|
+
end
|
|
22
35
|
end
|
|
23
36
|
|
|
24
|
-
def initialize(config: RubyLlmMesh.configuration, circuit_breaker: self.class.circuit_breaker
|
|
37
|
+
def initialize(config: RubyLlmMesh.configuration, circuit_breaker: self.class.circuit_breaker,
|
|
38
|
+
semantic_cache: nil, budget: nil)
|
|
25
39
|
@config = config
|
|
26
40
|
@circuit_breaker = circuit_breaker
|
|
41
|
+
@semantic_cache = semantic_cache
|
|
42
|
+
@budget = budget
|
|
27
43
|
end
|
|
28
44
|
|
|
29
45
|
def complete(prompt:, providers: nil, fallback: nil, system: nil, model: nil, **options)
|
|
30
46
|
raise ArgumentError, "prompt is required" if prompt.nil? || prompt.to_s.strip.empty?
|
|
31
47
|
|
|
48
|
+
skip_cache = options.delete(:skip_cache)
|
|
49
|
+
cache = resolve_cache
|
|
50
|
+
unless skip_cache
|
|
51
|
+
cached = cache&.lookup(prompt, system: system)
|
|
52
|
+
if cached
|
|
53
|
+
log(:info, "Semantic cache hit")
|
|
54
|
+
return cached
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
maybe_refresh_peer_health!
|
|
59
|
+
|
|
32
60
|
provider_list = Array(providers || @config.default_providers).map(&:to_sym)
|
|
33
61
|
raise ArgumentError, "providers list cannot be empty" if provider_list.empty?
|
|
34
62
|
|
|
@@ -37,37 +65,66 @@ module RubyLlmMesh
|
|
|
37
65
|
attempted = 0
|
|
38
66
|
|
|
39
67
|
provider_list.each_with_index do |provider_name, index|
|
|
68
|
+
circuit_key = circuit_key_for(provider_name)
|
|
69
|
+
|
|
40
70
|
unless PROVIDER_MAP.key?(provider_name)
|
|
41
71
|
errors[provider_name] = ProviderError.new("Unknown provider: #{provider_name}", provider: provider_name)
|
|
72
|
+
break unless use_fallback
|
|
42
73
|
next
|
|
43
74
|
end
|
|
44
75
|
|
|
45
|
-
unless @circuit_breaker.allow?(
|
|
76
|
+
unless @circuit_breaker.allow?(circuit_key)
|
|
46
77
|
errors[provider_name] = CircuitOpenError.new(
|
|
47
78
|
"Circuit open for #{provider_name}",
|
|
48
79
|
provider: provider_name
|
|
49
80
|
)
|
|
50
81
|
log(:warn, "Skipping #{provider_name} — circuit open")
|
|
82
|
+
break unless use_fallback
|
|
51
83
|
next
|
|
52
84
|
end
|
|
53
85
|
|
|
54
86
|
begin
|
|
55
87
|
attempted += 1
|
|
56
88
|
log(:info, "Routing to #{provider_name}")
|
|
89
|
+
budget = resolve_budget
|
|
90
|
+
estimated_tokens = Budget.estimate_tokens(prompt: prompt, system: system, max_tokens: options[:max_tokens])
|
|
91
|
+
estimated_usd = Budget.estimate_usd(tokens: estimated_tokens, model: model, config: @config)
|
|
92
|
+
budget.check!(estimated_tokens: estimated_tokens, estimated_usd: estimated_usd)
|
|
93
|
+
|
|
57
94
|
provider = PROVIDER_MAP[provider_name].new(@config)
|
|
58
|
-
response =
|
|
59
|
-
|
|
60
|
-
|
|
95
|
+
response = with_retries(provider_name) do
|
|
96
|
+
provider.complete(prompt: prompt, system: system, model: model, **options)
|
|
97
|
+
end
|
|
98
|
+
budget.consume!(usage: response.usage, provider: provider_name, model: response.model || model)
|
|
99
|
+
@circuit_breaker.record_success(circuit_key)
|
|
100
|
+
|
|
101
|
+
result = Response.new(
|
|
61
102
|
content: response.content,
|
|
62
103
|
provider: response.provider,
|
|
63
104
|
model: response.model,
|
|
64
105
|
usage: response.usage,
|
|
65
106
|
raw: response.raw,
|
|
66
107
|
latency_ms: response.latency_ms,
|
|
67
|
-
fallback_used: index.positive
|
|
108
|
+
fallback_used: index.positive?,
|
|
109
|
+
cache_hit: false
|
|
68
110
|
)
|
|
111
|
+
cache&.store_response(prompt, result, system: system) unless skip_cache
|
|
112
|
+
return result
|
|
113
|
+
rescue BudgetExceededError
|
|
114
|
+
raise
|
|
115
|
+
rescue AuthenticationError => e
|
|
116
|
+
@circuit_breaker.record_failure(circuit_key)
|
|
117
|
+
errors[provider_name] = e
|
|
118
|
+
log(:error, "#{provider_name} authentication failed: #{e.message}")
|
|
119
|
+
break unless use_fallback
|
|
120
|
+
rescue RateLimitError => e
|
|
121
|
+
# Trip circuit immediately so subsequent requests skip this provider
|
|
122
|
+
force_open_circuit!(circuit_key)
|
|
123
|
+
errors[provider_name] = e
|
|
124
|
+
log(:error, "#{provider_name} rate limited: #{e.message}")
|
|
125
|
+
break unless use_fallback
|
|
69
126
|
rescue ProviderError => e
|
|
70
|
-
@circuit_breaker.record_failure(
|
|
127
|
+
@circuit_breaker.record_failure(circuit_key)
|
|
71
128
|
errors[provider_name] = e
|
|
72
129
|
log(:error, "#{provider_name} failed: #{e.message}")
|
|
73
130
|
break unless use_fallback
|
|
@@ -79,6 +136,74 @@ module RubyLlmMesh
|
|
|
79
136
|
|
|
80
137
|
private
|
|
81
138
|
|
|
139
|
+
def resolve_cache
|
|
140
|
+
return @semantic_cache if @semantic_cache
|
|
141
|
+
return nil unless @config.semantic_cache_enabled
|
|
142
|
+
|
|
143
|
+
Cache::SemanticCache.instance(config: @config)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def resolve_budget
|
|
147
|
+
return @budget if @budget
|
|
148
|
+
|
|
149
|
+
Budget.instance(config: @config)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def circuit_key_for(provider_name)
|
|
153
|
+
provider_name == :local_mesh ? :local_node : provider_name
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def maybe_refresh_peer_health!
|
|
157
|
+
return unless @config.peer_discovery_enabled
|
|
158
|
+
|
|
159
|
+
monitor = Mesh::HealthMonitor.instance(config: @config)
|
|
160
|
+
monitor.start!
|
|
161
|
+
# Opportunistic sync check so first request after enable sees fresh state
|
|
162
|
+
monitor.check_all!
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def force_open_circuit!(circuit_key)
|
|
166
|
+
threshold = @config.circuit_failure_threshold
|
|
167
|
+
threshold.times { @circuit_breaker.record_failure(circuit_key) }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def with_retries(provider_name)
|
|
171
|
+
attempts = 1 + [Integer(@config.max_retries || 0), 0].max
|
|
172
|
+
last_error = nil
|
|
173
|
+
|
|
174
|
+
attempts.times do |index|
|
|
175
|
+
return yield
|
|
176
|
+
rescue AuthenticationError, BudgetExceededError
|
|
177
|
+
raise
|
|
178
|
+
rescue RateLimitError, TimeoutError, ProviderError => error
|
|
179
|
+
last_error = error
|
|
180
|
+
remaining = attempts - index - 1
|
|
181
|
+
unless retryable_error?(error) && remaining.positive?
|
|
182
|
+
raise error
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
delay = retry_delay(index)
|
|
186
|
+
log(:warn, "#{provider_name} attempt #{index + 1}/#{attempts} failed (#{error.class}): #{error.message}; retrying in #{delay}s")
|
|
187
|
+
sleep(delay) if delay.positive?
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
raise last_error
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def retryable_error?(error)
|
|
194
|
+
return false if error.is_a?(AuthenticationError)
|
|
195
|
+
return true if error.is_a?(TimeoutError) || error.is_a?(RateLimitError)
|
|
196
|
+
return false unless error.is_a?(ProviderError)
|
|
197
|
+
|
|
198
|
+
status = error.status
|
|
199
|
+
status.nil? || status >= 500 || status == 429
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def retry_delay(attempt)
|
|
203
|
+
base = Float(@config.retry_backoff || 0)
|
|
204
|
+
base * (2**attempt)
|
|
205
|
+
end
|
|
206
|
+
|
|
82
207
|
def log(level, message)
|
|
83
208
|
logger = @config.logger
|
|
84
209
|
return unless logger
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module RubyLlmMesh
|
|
6
|
+
# Sovereign mesh orchestrator — native P2P node + cloud failover strategies.
|
|
7
|
+
class SovereignMesh
|
|
8
|
+
STRATEGIES = %i[auto p2p_mesh cloud openai anthropic local_node local_mesh].freeze
|
|
9
|
+
|
|
10
|
+
def initialize(config: RubyLlmMesh.configuration, router: nil)
|
|
11
|
+
@config = config
|
|
12
|
+
@router = router || Router.new(config: config)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def boot!(port: @config.mesh_port)
|
|
16
|
+
NativeCore.start_node(port)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def shutdown!
|
|
20
|
+
NativeCore.stop_node
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def alive?
|
|
24
|
+
NativeCore.node_alive?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Execute an intent with a routing strategy.
|
|
28
|
+
#
|
|
29
|
+
# Strategies:
|
|
30
|
+
# - +:auto+ — native mesh when alive (or auto-boot), else cloud ladder
|
|
31
|
+
# - +:p2p_mesh+ — native chimera_core only
|
|
32
|
+
# - +:cloud+ — real HTTP via +fallback_providers+ / +default_providers+
|
|
33
|
+
# - +:openai+ / +:anthropic+ / +:local_node+ / +:local_mesh+ — single-provider cloud/local
|
|
34
|
+
def execute(intent:, strategy: :auto, **options)
|
|
35
|
+
raise ArgumentError, "intent is required" if intent.nil? || intent.to_s.strip.empty?
|
|
36
|
+
|
|
37
|
+
strategy = strategy.to_sym
|
|
38
|
+
unless STRATEGIES.include?(strategy)
|
|
39
|
+
raise ArgumentError, "Unknown strategy: #{strategy.inspect} (expected one of #{STRATEGIES.join(', ')})"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
43
|
+
result = case strategy
|
|
44
|
+
when :p2p_mesh
|
|
45
|
+
execute_p2p(intent)
|
|
46
|
+
when :cloud
|
|
47
|
+
execute_cloud(intent, **options)
|
|
48
|
+
when :openai, :anthropic, :local_node, :local_mesh
|
|
49
|
+
execute_cloud(intent, providers: [strategy], **options)
|
|
50
|
+
else # :auto
|
|
51
|
+
execute_auto(intent, **options)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
latency_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000).round
|
|
55
|
+
normalize_result(result, latency_ms: latency_ms, strategy: strategy)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def execute_auto(intent, **options)
|
|
61
|
+
if @config.auto_boot_mesh && !NativeCore.node_alive?
|
|
62
|
+
NativeCore.start_node(@config.mesh_port)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
if NativeCore.node_alive?
|
|
66
|
+
begin
|
|
67
|
+
return execute_p2p(intent)
|
|
68
|
+
rescue ProviderError
|
|
69
|
+
# fall through to cloud
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
execute_cloud(intent, **options)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def execute_p2p(intent)
|
|
77
|
+
unless NativeCore.node_alive?
|
|
78
|
+
booted = NativeCore.start_node(@config.mesh_port)
|
|
79
|
+
unless booted && NativeCore.node_alive?
|
|
80
|
+
raise ProviderError.new(
|
|
81
|
+
"Native mesh node is not alive (compile chimera_core or enable fallback)",
|
|
82
|
+
provider: :p2p_mesh
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
payload = NativeCore.execute_wasm_payload(intent)
|
|
88
|
+
if payload["ok"] == false
|
|
89
|
+
raise ProviderError.new(
|
|
90
|
+
payload["error"] || "native execute failed",
|
|
91
|
+
provider: :p2p_mesh,
|
|
92
|
+
body: payload
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
Response.new(
|
|
97
|
+
content: payload["output"].to_s,
|
|
98
|
+
provider: :p2p_mesh,
|
|
99
|
+
model: payload["engine"] || "chimera_core",
|
|
100
|
+
usage: {},
|
|
101
|
+
raw: payload,
|
|
102
|
+
latency_ms: nil,
|
|
103
|
+
fallback_used: false,
|
|
104
|
+
cache_hit: false
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def execute_cloud(intent, providers: nil, **options)
|
|
109
|
+
providers ||= @config.fallback_providers
|
|
110
|
+
providers = Array(providers).map(&:to_sym)
|
|
111
|
+
providers = @config.default_providers if providers.empty?
|
|
112
|
+
|
|
113
|
+
@router.complete(
|
|
114
|
+
prompt: intent,
|
|
115
|
+
providers: providers,
|
|
116
|
+
fallback: options.key?(:fallback) ? options[:fallback] : @config.fallback,
|
|
117
|
+
system: options[:system],
|
|
118
|
+
model: options[:model],
|
|
119
|
+
**options.reject { |k, _| %i[system model fallback providers].include?(k) }
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def normalize_result(response, latency_ms:, strategy:)
|
|
124
|
+
return response if response.is_a?(Response) && response.latency_ms
|
|
125
|
+
|
|
126
|
+
Response.new(
|
|
127
|
+
content: response.content,
|
|
128
|
+
provider: response.provider,
|
|
129
|
+
model: response.model,
|
|
130
|
+
usage: response.usage,
|
|
131
|
+
raw: (response.raw.is_a?(Hash) ? response.raw : { data: response.raw }).merge("strategy" => strategy.to_s),
|
|
132
|
+
latency_ms: response.latency_ms || latency_ms,
|
|
133
|
+
fallback_used: response.fallback_used,
|
|
134
|
+
cache_hit: response.cache_hit
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
data/lib/ruby_llm_mesh.rb
CHANGED
|
@@ -5,14 +5,22 @@ require_relative "ruby_llm_mesh/errors"
|
|
|
5
5
|
require_relative "ruby_llm_mesh/configuration"
|
|
6
6
|
require_relative "ruby_llm_mesh/response"
|
|
7
7
|
require_relative "ruby_llm_mesh/circuit_breaker"
|
|
8
|
+
require_relative "ruby_llm_mesh/budget"
|
|
8
9
|
require_relative "ruby_llm_mesh/providers/base"
|
|
9
10
|
require_relative "ruby_llm_mesh/providers/openai"
|
|
10
11
|
require_relative "ruby_llm_mesh/providers/anthropic"
|
|
11
12
|
require_relative "ruby_llm_mesh/providers/local_node"
|
|
12
|
-
require_relative "ruby_llm_mesh/router"
|
|
13
13
|
require_relative "ruby_llm_mesh/rag/chunker"
|
|
14
14
|
require_relative "ruby_llm_mesh/rag/embeddings"
|
|
15
15
|
require_relative "ruby_llm_mesh/rag/tools"
|
|
16
|
+
require_relative "ruby_llm_mesh/cache/memory_store"
|
|
17
|
+
require_relative "ruby_llm_mesh/cache/redis_store"
|
|
18
|
+
require_relative "ruby_llm_mesh/cache/semantic_cache"
|
|
19
|
+
require_relative "ruby_llm_mesh/mesh/peer_registry"
|
|
20
|
+
require_relative "ruby_llm_mesh/mesh/health_monitor"
|
|
21
|
+
require_relative "ruby_llm_mesh/native_core"
|
|
22
|
+
require_relative "ruby_llm_mesh/router"
|
|
23
|
+
require_relative "ruby_llm_mesh/sovereign_mesh"
|
|
16
24
|
|
|
17
25
|
# Optional Rails integration — only load railtie when Rails is already present
|
|
18
26
|
if defined?(Rails::Railtie)
|
|
@@ -28,6 +36,23 @@ module RubyLlmMesh
|
|
|
28
36
|
def chat(**)
|
|
29
37
|
complete(**)
|
|
30
38
|
end
|
|
39
|
+
|
|
40
|
+
# Sovereign mesh entrypoint — native P2P and/or real cloud failover.
|
|
41
|
+
def execute(intent:, strategy: :auto, **options)
|
|
42
|
+
SovereignMesh.new.execute(intent: intent, strategy: strategy, **options)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def boot_mesh!(port: configuration.mesh_port)
|
|
46
|
+
NativeCore.start_node(port)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def mesh_alive?
|
|
50
|
+
NativeCore.node_alive?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def budget_status
|
|
54
|
+
Budget.instance(config: configuration).status
|
|
55
|
+
end
|
|
31
56
|
end
|
|
32
57
|
end
|
|
33
58
|
|
|
@@ -37,6 +62,10 @@ module AiAgentRouter
|
|
|
37
62
|
RubyLlmMesh.complete(...)
|
|
38
63
|
end
|
|
39
64
|
|
|
65
|
+
def self.execute(...)
|
|
66
|
+
RubyLlmMesh.execute(...)
|
|
67
|
+
end
|
|
68
|
+
|
|
40
69
|
def self.configure(&)
|
|
41
70
|
RubyLlmMesh.configure(&)
|
|
42
71
|
end
|
|
@@ -44,4 +73,16 @@ module AiAgentRouter
|
|
|
44
73
|
def self.configuration
|
|
45
74
|
RubyLlmMesh.configuration
|
|
46
75
|
end
|
|
76
|
+
|
|
77
|
+
def self.boot_mesh!(...)
|
|
78
|
+
RubyLlmMesh.boot_mesh!(...)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.mesh_alive?
|
|
82
|
+
RubyLlmMesh.mesh_alive?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def self.budget_status
|
|
86
|
+
RubyLlmMesh.budget_status
|
|
87
|
+
end
|
|
47
88
|
end
|
data/ruby_llm_mesh.gemspec
CHANGED
|
@@ -8,11 +8,15 @@ Gem::Specification.new do |spec|
|
|
|
8
8
|
spec.authors = ["theworker02"]
|
|
9
9
|
spec.email = ["theworker02@users.noreply.github.com"]
|
|
10
10
|
|
|
11
|
-
spec.summary = "
|
|
11
|
+
spec.summary = "Sovereign multi-provider AI mesh with native FFI core, circuit-breaking, and cloud failover for Ruby & Rails"
|
|
12
12
|
spec.description = <<~DESC
|
|
13
|
-
ruby_llm_mesh (AiAgentRouter) is a
|
|
14
|
-
OpenAI, Anthropic, and local node runtimes with
|
|
15
|
-
fallback ladders,
|
|
13
|
+
ruby_llm_mesh (AiAgentRouter) is a Ruby gem that routes AI intents across a native
|
|
14
|
+
chimera_core mesh (Rust FFI), OpenAI, Anthropic, and local node runtimes with
|
|
15
|
+
automatic circuit-breaking, fallback ladders, optional semantic caching, peer health
|
|
16
|
+
monitoring, lightweight RAG helpers, and optional ActiveRecord hooks.
|
|
17
|
+
|
|
18
|
+
Gem name uses underscores (ruby_llm_mesh) to match the GitHub repository and
|
|
19
|
+
RubyGems listing at https://rubygems.org/gems/ruby_llm_mesh.
|
|
16
20
|
DESC
|
|
17
21
|
spec.homepage = "https://github.com/theworker02/ruby_llm_mesh"
|
|
18
22
|
spec.license = "MIT"
|
|
@@ -29,13 +33,14 @@ Gem::Specification.new do |spec|
|
|
|
29
33
|
spec.files = Dir.chdir(__dir__) do
|
|
30
34
|
`git ls-files -z`.split("\x0").reject do |f|
|
|
31
35
|
f.start_with?(*%w[test/ spec/ features/ .git .github docs/]) ||
|
|
32
|
-
f.end_with?(".gem")
|
|
36
|
+
f.end_with?(".gem") ||
|
|
37
|
+
f.include?("/target/")
|
|
33
38
|
end
|
|
34
39
|
end
|
|
35
|
-
# Ensure packaging works before the first commit
|
|
36
40
|
if spec.files.empty?
|
|
37
41
|
spec.files = Dir[
|
|
38
42
|
"lib/**/*",
|
|
43
|
+
"ext/**/*",
|
|
39
44
|
"sig/**/*",
|
|
40
45
|
"exe/**/*",
|
|
41
46
|
"assets/**/*",
|
|
@@ -43,14 +48,22 @@ Gem::Specification.new do |spec|
|
|
|
43
48
|
"README*",
|
|
44
49
|
"CHANGELOG*",
|
|
45
50
|
"CODE_OF_CONDUCT*",
|
|
46
|
-
"
|
|
47
|
-
|
|
51
|
+
"PRIVACY*",
|
|
52
|
+
"CONTRIBUTING*",
|
|
53
|
+
"*.gemspec",
|
|
54
|
+
"Rakefile"
|
|
55
|
+
].reject { |f| f.include?("/target/") || f.end_with?(".gem") }
|
|
48
56
|
end
|
|
49
57
|
|
|
50
58
|
spec.bindir = "exe"
|
|
51
59
|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
52
60
|
spec.require_paths = ["lib"]
|
|
53
61
|
|
|
62
|
+
# Optional native compile — documented via `rake compile`; gem works without it.
|
|
63
|
+
# spec.extensions = [] # cargo-based; use rake compile instead of mkmf
|
|
64
|
+
|
|
65
|
+
spec.add_dependency "ffi", "~> 1.17"
|
|
66
|
+
|
|
54
67
|
spec.add_development_dependency "minitest", "~> 5.25"
|
|
55
68
|
spec.add_development_dependency "rake", "~> 13.2"
|
|
56
69
|
spec.add_development_dependency "webmock", "~> 3.24"
|
data/sig/ruby_llm_mesh.rbs
CHANGED
|
@@ -8,6 +8,7 @@ module RubyLlmMesh
|
|
|
8
8
|
attr_accessor fallback: bool
|
|
9
9
|
attr_accessor timeout: Integer
|
|
10
10
|
attr_accessor max_retries: Integer
|
|
11
|
+
attr_accessor retry_backoff: Float
|
|
11
12
|
attr_accessor openai_api_key: String?
|
|
12
13
|
attr_accessor openai_base_url: String
|
|
13
14
|
attr_accessor openai_model: String
|
|
@@ -19,6 +20,12 @@ module RubyLlmMesh
|
|
|
19
20
|
attr_accessor circuit_failure_threshold: Integer
|
|
20
21
|
attr_accessor circuit_reset_timeout: Integer
|
|
21
22
|
attr_accessor logger: untyped
|
|
23
|
+
attr_accessor mesh_port: Integer
|
|
24
|
+
attr_accessor fallback_providers: Array[Symbol]
|
|
25
|
+
attr_accessor auto_boot_mesh: bool
|
|
26
|
+
attr_accessor semantic_cache_enabled: bool
|
|
27
|
+
attr_accessor peer_discovery_enabled: bool
|
|
28
|
+
attr_accessor peer_urls: Array[String]
|
|
22
29
|
end
|
|
23
30
|
|
|
24
31
|
def self.configuration: () -> Configuration
|
|
@@ -31,6 +38,9 @@ module RubyLlmMesh
|
|
|
31
38
|
**untyped
|
|
32
39
|
) -> Response
|
|
33
40
|
def self.chat: (**untyped) -> Response
|
|
41
|
+
def self.execute: (intent: String, ?strategy: Symbol, **untyped) -> Response
|
|
42
|
+
def self.boot_mesh!: (?port: Integer) -> bool
|
|
43
|
+
def self.mesh_alive?: () -> bool
|
|
34
44
|
|
|
35
45
|
class Response
|
|
36
46
|
attr_reader content: String
|
|
@@ -40,6 +50,7 @@ module RubyLlmMesh
|
|
|
40
50
|
attr_reader raw: untyped
|
|
41
51
|
attr_reader latency_ms: Integer?
|
|
42
52
|
attr_reader fallback_used: bool
|
|
53
|
+
attr_reader cache_hit: bool
|
|
43
54
|
|
|
44
55
|
def initialize: (
|
|
45
56
|
content: String,
|
|
@@ -48,7 +59,8 @@ module RubyLlmMesh
|
|
|
48
59
|
?usage: Hash[untyped, untyped],
|
|
49
60
|
?raw: untyped,
|
|
50
61
|
?latency_ms: Integer?,
|
|
51
|
-
?fallback_used: bool
|
|
62
|
+
?fallback_used: bool,
|
|
63
|
+
?cache_hit: bool
|
|
52
64
|
) -> void
|
|
53
65
|
def text: () -> String
|
|
54
66
|
def to_s: () -> String
|
|
@@ -66,6 +78,20 @@ module RubyLlmMesh
|
|
|
66
78
|
) -> Response
|
|
67
79
|
end
|
|
68
80
|
|
|
81
|
+
class SovereignMesh
|
|
82
|
+
def execute: (intent: String, ?strategy: Symbol, **untyped) -> Response
|
|
83
|
+
def boot!: (?port: Integer) -> bool
|
|
84
|
+
def alive?: () -> bool
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
module NativeCore
|
|
88
|
+
def self.available?: () -> bool
|
|
89
|
+
def self.start_node: (?Integer) -> bool
|
|
90
|
+
def self.stop_node: () -> bool
|
|
91
|
+
def self.node_alive?: () -> bool
|
|
92
|
+
def self.execute_wasm_payload: (String) -> Hash[String, untyped]
|
|
93
|
+
end
|
|
94
|
+
|
|
69
95
|
class CircuitBreaker
|
|
70
96
|
def allow?: (Symbol) -> bool
|
|
71
97
|
def record_success: (Symbol) -> void
|
|
@@ -77,6 +103,9 @@ end
|
|
|
77
103
|
|
|
78
104
|
module AiAgentRouter
|
|
79
105
|
def self.complete: (**untyped) -> RubyLlmMesh::Response
|
|
106
|
+
def self.execute: (**untyped) -> RubyLlmMesh::Response
|
|
80
107
|
def self.configure: () { (RubyLlmMesh::Configuration) -> void } -> void
|
|
81
108
|
def self.configuration: () -> RubyLlmMesh::Configuration
|
|
109
|
+
def self.boot_mesh!: (**untyped) -> bool
|
|
110
|
+
def self.mesh_alive?: () -> bool
|
|
82
111
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby_llm_mesh
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 2.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- theworker02
|
|
@@ -9,6 +9,20 @@ bindir: exe
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: ffi
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.17'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.17'
|
|
12
26
|
- !ruby/object:Gem::Dependency
|
|
13
27
|
name: minitest
|
|
14
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -52,9 +66,13 @@ dependencies:
|
|
|
52
66
|
- !ruby/object:Gem::Version
|
|
53
67
|
version: '3.24'
|
|
54
68
|
description: |
|
|
55
|
-
ruby_llm_mesh (AiAgentRouter) is a
|
|
56
|
-
OpenAI, Anthropic, and local node runtimes with
|
|
57
|
-
fallback ladders,
|
|
69
|
+
ruby_llm_mesh (AiAgentRouter) is a Ruby gem that routes AI intents across a native
|
|
70
|
+
chimera_core mesh (Rust FFI), OpenAI, Anthropic, and local node runtimes with
|
|
71
|
+
automatic circuit-breaking, fallback ladders, optional semantic caching, peer health
|
|
72
|
+
monitoring, lightweight RAG helpers, and optional ActiveRecord hooks.
|
|
73
|
+
|
|
74
|
+
Gem name uses underscores (ruby_llm_mesh) to match the GitHub repository and
|
|
75
|
+
RubyGems listing at https://rubygems.org/gems/ruby_llm_mesh.
|
|
58
76
|
email:
|
|
59
77
|
- theworker02@users.noreply.github.com
|
|
60
78
|
executables: []
|
|
@@ -66,14 +84,24 @@ files:
|
|
|
66
84
|
- CONTRIBUTING.md
|
|
67
85
|
- Gemfile
|
|
68
86
|
- LICENSE.txt
|
|
87
|
+
- PRIVACY.md
|
|
69
88
|
- README.md
|
|
70
89
|
- Rakefile
|
|
71
90
|
- assets/logo.png
|
|
91
|
+
- ext/chimera_core/Cargo.toml
|
|
92
|
+
- ext/chimera_core/src/lib.rs
|
|
72
93
|
- lib/ruby_llm_mesh.rb
|
|
73
94
|
- lib/ruby_llm_mesh/active_record/acts_as_ai_agent.rb
|
|
95
|
+
- lib/ruby_llm_mesh/budget.rb
|
|
96
|
+
- lib/ruby_llm_mesh/cache/memory_store.rb
|
|
97
|
+
- lib/ruby_llm_mesh/cache/redis_store.rb
|
|
98
|
+
- lib/ruby_llm_mesh/cache/semantic_cache.rb
|
|
74
99
|
- lib/ruby_llm_mesh/circuit_breaker.rb
|
|
75
100
|
- lib/ruby_llm_mesh/configuration.rb
|
|
76
101
|
- lib/ruby_llm_mesh/errors.rb
|
|
102
|
+
- lib/ruby_llm_mesh/mesh/health_monitor.rb
|
|
103
|
+
- lib/ruby_llm_mesh/mesh/peer_registry.rb
|
|
104
|
+
- lib/ruby_llm_mesh/native_core.rb
|
|
77
105
|
- lib/ruby_llm_mesh/providers/anthropic.rb
|
|
78
106
|
- lib/ruby_llm_mesh/providers/base.rb
|
|
79
107
|
- lib/ruby_llm_mesh/providers/local_node.rb
|
|
@@ -84,6 +112,7 @@ files:
|
|
|
84
112
|
- lib/ruby_llm_mesh/railtie.rb
|
|
85
113
|
- lib/ruby_llm_mesh/response.rb
|
|
86
114
|
- lib/ruby_llm_mesh/router.rb
|
|
115
|
+
- lib/ruby_llm_mesh/sovereign_mesh.rb
|
|
87
116
|
- lib/ruby_llm_mesh/version.rb
|
|
88
117
|
- ruby_llm_mesh.gemspec
|
|
89
118
|
- sig/ruby_llm_mesh.rbs
|
|
@@ -114,6 +143,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
114
143
|
requirements: []
|
|
115
144
|
rubygems_version: 4.0.16
|
|
116
145
|
specification_version: 4
|
|
117
|
-
summary:
|
|
118
|
-
for Ruby & Rails
|
|
146
|
+
summary: Sovereign multi-provider AI mesh with native FFI core, circuit-breaking,
|
|
147
|
+
and cloud failover for Ruby & Rails
|
|
119
148
|
test_files: []
|