ruby_llm-agents 3.7.2 → 3.8.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/app/controllers/ruby_llm/agents/agents_controller.rb +14 -141
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +12 -166
- data/app/controllers/ruby_llm/agents/executions_controller.rb +1 -1
- data/app/helpers/ruby_llm/agents/application_helper.rb +38 -0
- data/app/models/ruby_llm/agents/execution/analytics.rb +302 -103
- data/app/models/ruby_llm/agents/execution.rb +76 -54
- data/app/models/ruby_llm/agents/execution_detail.rb +2 -0
- data/app/models/ruby_llm/agents/tenant.rb +39 -0
- data/app/services/ruby_llm/agents/agent_registry.rb +98 -0
- data/app/views/ruby_llm/agents/executions/_list.html.erb +3 -17
- data/lib/generators/ruby_llm_agents/templates/add_dashboard_performance_indexes_migration.rb.tt +23 -0
- data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +3 -0
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +25 -0
- data/lib/ruby_llm/agents/base_agent.rb +7 -1
- data/lib/ruby_llm/agents/core/configuration.rb +1 -0
- data/lib/ruby_llm/agents/core/instrumentation.rb +15 -19
- data/lib/ruby_llm/agents/core/version.rb +1 -1
- data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +4 -4
- data/lib/ruby_llm/agents/infrastructure/budget_tracker.rb +19 -11
- data/lib/ruby_llm/agents/pipeline/builder.rb +8 -4
- data/lib/ruby_llm/agents/pipeline/context.rb +43 -1
- data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +6 -4
- data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +6 -4
- data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +26 -75
- data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +6 -6
- data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +23 -27
- data/lib/ruby_llm/agents/providers/inception/capabilities.rb +107 -0
- data/lib/ruby_llm/agents/providers/inception/chat.rb +17 -0
- data/lib/ruby_llm/agents/providers/inception/configuration.rb +9 -0
- data/lib/ruby_llm/agents/providers/inception/models.rb +38 -0
- data/lib/ruby_llm/agents/providers/inception/registry.rb +45 -0
- data/lib/ruby_llm/agents/providers/inception.rb +50 -0
- data/lib/ruby_llm/agents/results/base.rb +4 -2
- data/lib/ruby_llm/agents/results/image_analysis_result.rb +4 -2
- data/lib/ruby_llm/agents/text/embedder.rb +4 -0
- data/lib/ruby_llm/agents.rb +4 -0
- metadata +8 -1
|
@@ -170,7 +170,8 @@ module RubyLLM
|
|
|
170
170
|
# @return [Array<Hash>] Middleware entries
|
|
171
171
|
def global_middleware_entries
|
|
172
172
|
RubyLLM::Agents.configuration.middleware_stack
|
|
173
|
-
rescue
|
|
173
|
+
rescue => e
|
|
174
|
+
Rails.logger.debug("[RubyLLM::Agents::Pipeline] Failed to load global middleware: #{e.message}") if defined?(Rails) && Rails.logger
|
|
174
175
|
[]
|
|
175
176
|
end
|
|
176
177
|
|
|
@@ -182,7 +183,8 @@ module RubyLLM
|
|
|
182
183
|
return [] unless agent_class&.respond_to?(:agent_middleware)
|
|
183
184
|
|
|
184
185
|
agent_class.agent_middleware
|
|
185
|
-
rescue
|
|
186
|
+
rescue => e
|
|
187
|
+
Rails.logger.debug("[RubyLLM::Agents::Pipeline] Failed to load agent middleware: #{e.message}") if defined?(Rails) && Rails.logger
|
|
186
188
|
[]
|
|
187
189
|
end
|
|
188
190
|
|
|
@@ -207,7 +209,8 @@ module RubyLLM
|
|
|
207
209
|
# @return [Boolean]
|
|
208
210
|
def budgets_enabled?
|
|
209
211
|
RubyLLM::Agents.configuration.budgets_enabled?
|
|
210
|
-
rescue
|
|
212
|
+
rescue => e
|
|
213
|
+
Rails.logger.debug("[RubyLLM::Agents::Pipeline] Failed to check budgets_enabled: #{e.message}") if defined?(Rails) && Rails.logger
|
|
211
214
|
false
|
|
212
215
|
end
|
|
213
216
|
|
|
@@ -248,7 +251,8 @@ module RubyLLM
|
|
|
248
251
|
|
|
249
252
|
(retries.is_a?(Integer) && retries.positive?) ||
|
|
250
253
|
(fallbacks.is_a?(Array) && fallbacks.any?)
|
|
251
|
-
rescue
|
|
254
|
+
rescue => e
|
|
255
|
+
Rails.logger.debug("[RubyLLM::Agents::Pipeline] Failed to check reliability_enabled: #{e.message}") if defined?(Rails) && Rails.logger
|
|
252
256
|
false
|
|
253
257
|
end
|
|
254
258
|
end
|
|
@@ -136,6 +136,24 @@ module RubyLLM
|
|
|
136
136
|
(@input_tokens || 0) + (@output_tokens || 0)
|
|
137
137
|
end
|
|
138
138
|
|
|
139
|
+
# Returns a RubyLLM interface scoped to tenant API keys when present.
|
|
140
|
+
#
|
|
141
|
+
# When tenant API keys are stored on this context (by the Tenant middleware),
|
|
142
|
+
# returns a RubyLLM::Context with a cloned config that has tenant-specific
|
|
143
|
+
# keys applied. This avoids mutating global RubyLLM configuration, making
|
|
144
|
+
# multi-tenant LLM calls thread-safe.
|
|
145
|
+
#
|
|
146
|
+
# When no tenant API keys are present, returns the RubyLLM module directly
|
|
147
|
+
# (which uses the global configuration).
|
|
148
|
+
#
|
|
149
|
+
# @return [RubyLLM::Context, RubyLLM] Scoped context or global module
|
|
150
|
+
def llm
|
|
151
|
+
api_keys = self[:tenant_api_keys]
|
|
152
|
+
return RubyLLM if api_keys.nil? || api_keys.empty?
|
|
153
|
+
|
|
154
|
+
@llm_context ||= build_llm_context(api_keys)
|
|
155
|
+
end
|
|
156
|
+
|
|
139
157
|
# Custom metadata storage - read
|
|
140
158
|
#
|
|
141
159
|
# @param key [Symbol, String] The metadata key
|
|
@@ -217,6 +235,24 @@ module RubyLLM
|
|
|
217
235
|
|
|
218
236
|
private
|
|
219
237
|
|
|
238
|
+
# Builds a RubyLLM::Context with tenant-specific API keys
|
|
239
|
+
#
|
|
240
|
+
# Clones the global RubyLLM config and overlays tenant API keys,
|
|
241
|
+
# then wraps it in a RubyLLM::Context for thread-safe per-request use.
|
|
242
|
+
#
|
|
243
|
+
# @param api_keys [Hash] Provider => key mappings (e.g., {openai: "sk-..."})
|
|
244
|
+
# @return [RubyLLM::Context] Context with tenant-scoped configuration
|
|
245
|
+
def build_llm_context(api_keys)
|
|
246
|
+
config = RubyLLM.config.dup
|
|
247
|
+
api_keys.each do |provider, key|
|
|
248
|
+
next if key.nil? || (key.respond_to?(:empty?) && key.empty?)
|
|
249
|
+
|
|
250
|
+
setter = "#{provider}_api_key="
|
|
251
|
+
config.public_send(setter, key) if config.respond_to?(setter)
|
|
252
|
+
end
|
|
253
|
+
RubyLLM::Context.new(config)
|
|
254
|
+
end
|
|
255
|
+
|
|
220
256
|
# Extracts agent_type from the agent class
|
|
221
257
|
#
|
|
222
258
|
# @param agent_class [Class] The agent class
|
|
@@ -227,7 +263,13 @@ module RubyLLM
|
|
|
227
263
|
if agent_class.respond_to?(:agent_type)
|
|
228
264
|
agent_class.agent_type
|
|
229
265
|
else
|
|
230
|
-
|
|
266
|
+
if defined?(RubyLLM::Agents::Deprecations)
|
|
267
|
+
RubyLLM::Agents::Deprecations.warn(
|
|
268
|
+
"#{agent_class.name || agent_class} does not define `agent_type`. " \
|
|
269
|
+
"Guessing from class name. Define `self.agent_type` to silence this warning.",
|
|
270
|
+
caller
|
|
271
|
+
)
|
|
272
|
+
end
|
|
231
273
|
infer_agent_type(agent_class)
|
|
232
274
|
end
|
|
233
275
|
end
|
|
@@ -64,8 +64,8 @@ module RubyLLM
|
|
|
64
64
|
tenant_id: context.tenant_id
|
|
65
65
|
}.merge(extras)
|
|
66
66
|
)
|
|
67
|
-
rescue
|
|
68
|
-
|
|
67
|
+
rescue => e
|
|
68
|
+
debug("Budget notification failed: #{e.message}")
|
|
69
69
|
end
|
|
70
70
|
|
|
71
71
|
# Returns whether budgets are enabled globally
|
|
@@ -73,7 +73,8 @@ module RubyLLM
|
|
|
73
73
|
# @return [Boolean]
|
|
74
74
|
def budgets_enabled?
|
|
75
75
|
global_config.budgets_enabled?
|
|
76
|
-
rescue
|
|
76
|
+
rescue => e
|
|
77
|
+
debug("Failed to check budgets_enabled config: #{e.message}")
|
|
77
78
|
false
|
|
78
79
|
end
|
|
79
80
|
|
|
@@ -104,7 +105,8 @@ module RubyLLM
|
|
|
104
105
|
emit_budget_notification("ruby_llm_agents.budget.exceeded", context)
|
|
105
106
|
raise
|
|
106
107
|
rescue => e
|
|
107
|
-
error
|
|
108
|
+
# Log at error level so unexpected failures are visible in logs
|
|
109
|
+
error("Budget check failed: #{e.class}: #{e.message}")
|
|
108
110
|
end
|
|
109
111
|
|
|
110
112
|
# Records spend after execution
|
|
@@ -73,8 +73,8 @@ module RubyLLM
|
|
|
73
73
|
agent_type: @agent_class&.name,
|
|
74
74
|
cache_key: cache_key
|
|
75
75
|
)
|
|
76
|
-
rescue
|
|
77
|
-
|
|
76
|
+
rescue => e
|
|
77
|
+
debug("Cache notification failed: #{e.message}")
|
|
78
78
|
end
|
|
79
79
|
|
|
80
80
|
# Returns whether caching is enabled for this agent
|
|
@@ -89,7 +89,8 @@ module RubyLLM
|
|
|
89
89
|
# @return [ActiveSupport::Cache::Store, nil]
|
|
90
90
|
def cache_store
|
|
91
91
|
global_config.cache_store
|
|
92
|
-
rescue
|
|
92
|
+
rescue => e
|
|
93
|
+
debug("Failed to access cache_store config: #{e.message}")
|
|
93
94
|
nil
|
|
94
95
|
end
|
|
95
96
|
|
|
@@ -148,7 +149,8 @@ module RubyLLM
|
|
|
148
149
|
else
|
|
149
150
|
input.to_json
|
|
150
151
|
end
|
|
151
|
-
rescue
|
|
152
|
+
rescue => e
|
|
153
|
+
debug("Failed to serialize input for cache key: #{e.message}")
|
|
152
154
|
input.to_s
|
|
153
155
|
end
|
|
154
156
|
|
|
@@ -176,8 +176,8 @@ module RubyLLM
|
|
|
176
176
|
else
|
|
177
177
|
RubyLLM::Agents::ExecutionDetail.create!(detail_attrs.merge(execution_id: execution.id))
|
|
178
178
|
end
|
|
179
|
-
rescue
|
|
180
|
-
#
|
|
179
|
+
rescue => e
|
|
180
|
+
debug("Failed to store error detail: #{e.message}")
|
|
181
181
|
end
|
|
182
182
|
rescue => e
|
|
183
183
|
error("CRITICAL: Failed emergency status update for execution #{execution&.id}: #{e.message}")
|
|
@@ -205,8 +205,8 @@ module RubyLLM
|
|
|
205
205
|
tenant_id: context.tenant_id,
|
|
206
206
|
execution_id: context.execution_id
|
|
207
207
|
)
|
|
208
|
-
rescue
|
|
209
|
-
|
|
208
|
+
rescue => e
|
|
209
|
+
debug("Start notification failed: #{e.message}")
|
|
210
210
|
end
|
|
211
211
|
|
|
212
212
|
# Emits an AS::Notification for execution completion or error
|
|
@@ -242,8 +242,8 @@ module RubyLLM
|
|
|
242
242
|
error_class: context.error&.class&.name,
|
|
243
243
|
error_message: context.error&.message
|
|
244
244
|
)
|
|
245
|
-
rescue
|
|
246
|
-
|
|
245
|
+
rescue => e
|
|
246
|
+
debug("Complete notification failed: #{e.message}")
|
|
247
247
|
end
|
|
248
248
|
|
|
249
249
|
# Builds data for initial running execution record
|
|
@@ -321,7 +321,8 @@ module RubyLLM
|
|
|
321
321
|
|
|
322
322
|
context_meta = begin
|
|
323
323
|
context.metadata.dup
|
|
324
|
-
rescue
|
|
324
|
+
rescue => e
|
|
325
|
+
debug("Failed to read context metadata: #{e.message}")
|
|
325
326
|
{}
|
|
326
327
|
end
|
|
327
328
|
context_meta.transform_keys!(&:to_s)
|
|
@@ -414,72 +415,19 @@ module RubyLLM
|
|
|
414
415
|
error("Failed to record execution: #{e.message}")
|
|
415
416
|
end
|
|
416
417
|
|
|
417
|
-
# Builds execution data hash
|
|
418
|
+
# Builds execution data hash for the legacy single-step persistence path.
|
|
419
|
+
#
|
|
420
|
+
# Composes from build_running_execution_data and build_completion_data
|
|
421
|
+
# to avoid duplication.
|
|
418
422
|
#
|
|
419
423
|
# @param context [Context] The execution context
|
|
420
424
|
# @param status [String] "success" or "error"
|
|
421
|
-
# @return [Hash] Execution data
|
|
425
|
+
# @return [Hash] Execution data with _detail_data for detail record
|
|
422
426
|
def build_execution_data(context, status)
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
merged_metadata = agent_meta.transform_keys(&:to_s)
|
|
426
|
-
|
|
427
|
-
context_meta = begin
|
|
428
|
-
context.metadata.dup
|
|
429
|
-
rescue
|
|
430
|
-
{}
|
|
431
|
-
end
|
|
432
|
-
context_meta.transform_keys!(&:to_s)
|
|
433
|
-
merged_metadata.merge!(context_meta)
|
|
434
|
-
|
|
435
|
-
if context.cached? && context[:cache_key]
|
|
436
|
-
merged_metadata["response_cache_key"] = context[:cache_key]
|
|
437
|
-
end
|
|
438
|
-
|
|
439
|
-
data = {
|
|
440
|
-
agent_type: context.agent_class&.name,
|
|
441
|
-
model_id: context.model,
|
|
442
|
-
status: determine_status(context, status),
|
|
443
|
-
duration_ms: context.duration_ms,
|
|
444
|
-
started_at: context.started_at,
|
|
445
|
-
completed_at: context.completed_at,
|
|
446
|
-
cache_hit: context.cached?,
|
|
447
|
-
input_tokens: context.input_tokens || 0,
|
|
448
|
-
output_tokens: context.output_tokens || 0,
|
|
449
|
-
total_cost: context.total_cost || 0,
|
|
450
|
-
attempts_count: context.attempts_made,
|
|
451
|
-
metadata: merged_metadata
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
# Extract tracing fields from agent metadata to dedicated columns
|
|
455
|
-
if agent_meta.any?
|
|
456
|
-
data[:trace_id] = agent_meta[:trace_id] if agent_meta[:trace_id]
|
|
457
|
-
data[:request_id] = agent_meta[:request_id] if agent_meta[:request_id]
|
|
458
|
-
data[:parent_execution_id] = agent_meta[:parent_execution_id] if agent_meta[:parent_execution_id]
|
|
459
|
-
data[:root_execution_id] = agent_meta[:root_execution_id] if agent_meta[:root_execution_id]
|
|
460
|
-
end
|
|
461
|
-
|
|
462
|
-
# Add tenant_id only if multi-tenancy is enabled and tenant is set
|
|
463
|
-
if global_config.multi_tenancy_enabled? && context.tenant_id.present?
|
|
464
|
-
data[:tenant_id] = context.tenant_id
|
|
465
|
-
end
|
|
466
|
-
|
|
467
|
-
# Error class on execution
|
|
468
|
-
if context.error
|
|
469
|
-
data[:error_class] = context.error.class.name
|
|
470
|
-
end
|
|
471
|
-
|
|
472
|
-
# Tool calls count on execution
|
|
473
|
-
if context[:tool_calls].present?
|
|
474
|
-
data[:tool_calls_count] = context[:tool_calls].size
|
|
475
|
-
end
|
|
476
|
-
|
|
477
|
-
# Attempts count on execution
|
|
478
|
-
if context[:reliability_attempts].present?
|
|
479
|
-
data[:attempts_count] = context[:reliability_attempts].size
|
|
480
|
-
end
|
|
427
|
+
data = build_running_execution_data(context)
|
|
428
|
+
.merge(build_completion_data(context, determine_status(context, status)))
|
|
481
429
|
|
|
482
|
-
#
|
|
430
|
+
# Build detail data for separate creation
|
|
483
431
|
detail_data = {parameters: sanitize_parameters(context)}
|
|
484
432
|
if global_config.persist_prompts
|
|
485
433
|
exec_opts = context.options[:options] || {}
|
|
@@ -494,11 +442,9 @@ module RubyLLM
|
|
|
494
442
|
detail_data[:response] = serialize_response(context)
|
|
495
443
|
end
|
|
496
444
|
|
|
497
|
-
# Persist audio data for Speaker executions
|
|
498
445
|
maybe_persist_audio_response(context, detail_data)
|
|
499
446
|
|
|
500
447
|
data[:_detail_data] = detail_data
|
|
501
|
-
|
|
502
448
|
data
|
|
503
449
|
end
|
|
504
450
|
|
|
@@ -527,7 +473,8 @@ module RubyLLM
|
|
|
527
473
|
|
|
528
474
|
params = begin
|
|
529
475
|
context.agent_instance.send(:options)
|
|
530
|
-
rescue
|
|
476
|
+
rescue => e
|
|
477
|
+
debug("Failed to extract agent options: #{e.message}")
|
|
531
478
|
{}
|
|
532
479
|
end
|
|
533
480
|
params = params.dup
|
|
@@ -689,7 +636,8 @@ module RubyLLM
|
|
|
689
636
|
else
|
|
690
637
|
cfg.track_executions
|
|
691
638
|
end
|
|
692
|
-
rescue
|
|
639
|
+
rescue => e
|
|
640
|
+
debug("Failed to check tracking config: #{e.message}")
|
|
693
641
|
false
|
|
694
642
|
end
|
|
695
643
|
|
|
@@ -698,7 +646,8 @@ module RubyLLM
|
|
|
698
646
|
# @return [Boolean]
|
|
699
647
|
def track_cache_hits?
|
|
700
648
|
global_config.respond_to?(:track_cache_hits) && global_config.track_cache_hits
|
|
701
|
-
rescue
|
|
649
|
+
rescue => e
|
|
650
|
+
debug("Failed to check track_cache_hits config: #{e.message}")
|
|
702
651
|
false
|
|
703
652
|
end
|
|
704
653
|
|
|
@@ -707,7 +656,8 @@ module RubyLLM
|
|
|
707
656
|
# @return [Boolean]
|
|
708
657
|
def async_logging?
|
|
709
658
|
global_config.async_logging && defined?(Infrastructure::ExecutionLoggerJob)
|
|
710
|
-
rescue
|
|
659
|
+
rescue => e
|
|
660
|
+
debug("Failed to check async_logging config: #{e.message}")
|
|
711
661
|
false
|
|
712
662
|
end
|
|
713
663
|
|
|
@@ -722,7 +672,8 @@ module RubyLLM
|
|
|
722
672
|
@_assistant_prompt_column_exists = begin
|
|
723
673
|
defined?(RubyLLM::Agents::ExecutionDetail) &&
|
|
724
674
|
RubyLLM::Agents::ExecutionDetail.column_names.include?("assistant_prompt")
|
|
725
|
-
rescue
|
|
675
|
+
rescue => e
|
|
676
|
+
debug("Failed to check assistant_prompt column: #{e.message}")
|
|
726
677
|
false
|
|
727
678
|
end
|
|
728
679
|
end
|
|
@@ -237,7 +237,7 @@ module RubyLLM
|
|
|
237
237
|
return false if attempt_index >= max_retries
|
|
238
238
|
return false if total_deadline && Time.current > total_deadline
|
|
239
239
|
# Don't retry if fallback models are available — move to next model instead
|
|
240
|
-
return false if
|
|
240
|
+
return false if fallback_models?(config)
|
|
241
241
|
|
|
242
242
|
retryable_error?(error, config)
|
|
243
243
|
end
|
|
@@ -256,7 +256,7 @@ module RubyLLM
|
|
|
256
256
|
#
|
|
257
257
|
# @param config [Hash] The reliability configuration
|
|
258
258
|
# @return [Boolean]
|
|
259
|
-
def
|
|
259
|
+
def fallback_models?(config)
|
|
260
260
|
fallbacks = config[:fallback_models]
|
|
261
261
|
fallbacks.is_a?(Array) && fallbacks.any?
|
|
262
262
|
end
|
|
@@ -317,8 +317,8 @@ module RubyLLM
|
|
|
317
317
|
event,
|
|
318
318
|
{agent_type: @agent_class&.name}.merge(extras)
|
|
319
319
|
)
|
|
320
|
-
rescue
|
|
321
|
-
|
|
320
|
+
rescue => e
|
|
321
|
+
debug("Reliability notification failed: #{e.message}")
|
|
322
322
|
end
|
|
323
323
|
|
|
324
324
|
# Sleeps without blocking other fibers when in async context
|
|
@@ -333,8 +333,8 @@ module RubyLLM
|
|
|
333
333
|
else
|
|
334
334
|
sleep(seconds)
|
|
335
335
|
end
|
|
336
|
-
rescue
|
|
337
|
-
|
|
336
|
+
rescue => e
|
|
337
|
+
debug("Async sleep failed, falling back to regular sleep: #{e.message}")
|
|
338
338
|
sleep(seconds)
|
|
339
339
|
end
|
|
340
340
|
end
|
|
@@ -112,7 +112,7 @@ module RubyLLM
|
|
|
112
112
|
ensure_tenant_for_model!(tenant_object)
|
|
113
113
|
else
|
|
114
114
|
# For hash-based or string tenants, ensure a minimal record exists
|
|
115
|
-
|
|
115
|
+
find_or_create_tenant!(context.tenant_id)
|
|
116
116
|
end
|
|
117
117
|
rescue => e
|
|
118
118
|
# Don't fail the execution if tenant record creation fails
|
|
@@ -145,6 +145,18 @@ module RubyLLM
|
|
|
145
145
|
enforcement: options[:enforcement]&.to_s || "soft",
|
|
146
146
|
inherit_global_defaults: options.fetch(:inherit_global, true)
|
|
147
147
|
)
|
|
148
|
+
rescue ActiveRecord::RecordNotUnique
|
|
149
|
+
# Race condition: another thread created the record — safe to ignore
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Finds or creates a tenant record, handling race conditions
|
|
153
|
+
#
|
|
154
|
+
# @param tenant_id [String] The tenant identifier
|
|
155
|
+
def find_or_create_tenant!(tenant_id)
|
|
156
|
+
RubyLLM::Agents::Tenant.find_or_create_by!(tenant_id: tenant_id)
|
|
157
|
+
rescue ActiveRecord::RecordNotUnique
|
|
158
|
+
# Another thread/process created the record — just find it
|
|
159
|
+
RubyLLM::Agents::Tenant.find_by!(tenant_id: tenant_id)
|
|
148
160
|
end
|
|
149
161
|
|
|
150
162
|
# Checks if the tenants table exists (memoized)
|
|
@@ -154,7 +166,8 @@ module RubyLLM
|
|
|
154
166
|
return @tenant_table_exists if defined?(@tenant_table_exists)
|
|
155
167
|
|
|
156
168
|
@tenant_table_exists = ::ActiveRecord::Base.connection.table_exists?(:ruby_llm_agents_tenants)
|
|
157
|
-
rescue
|
|
169
|
+
rescue => e
|
|
170
|
+
debug("Failed to check tenant table existence: #{e.message}")
|
|
158
171
|
@tenant_table_exists = false
|
|
159
172
|
end
|
|
160
173
|
|
|
@@ -178,7 +191,11 @@ module RubyLLM
|
|
|
178
191
|
apply_tenant_object_api_keys!(context)
|
|
179
192
|
end
|
|
180
193
|
|
|
181
|
-
#
|
|
194
|
+
# Stores tenant API keys on the context for thread-safe per-request use.
|
|
195
|
+
#
|
|
196
|
+
# Instead of mutating the global RubyLLM configuration (which is not
|
|
197
|
+
# thread-safe), keys are stored on the context. The Pipeline::Context#llm
|
|
198
|
+
# method creates a scoped RubyLLM::Context with these keys when needed.
|
|
182
199
|
#
|
|
183
200
|
# @param context [Context] The execution context
|
|
184
201
|
def apply_tenant_object_api_keys!(context)
|
|
@@ -188,34 +205,12 @@ module RubyLLM
|
|
|
188
205
|
api_keys = tenant_object.llm_api_keys
|
|
189
206
|
return if api_keys.blank?
|
|
190
207
|
|
|
191
|
-
|
|
208
|
+
context[:tenant_api_keys] = api_keys
|
|
192
209
|
rescue => e
|
|
193
210
|
# Log but don't fail if API key extraction fails
|
|
194
211
|
warn_api_key_error("tenant object", e)
|
|
195
212
|
end
|
|
196
213
|
|
|
197
|
-
# Applies a hash of API keys to RubyLLM configuration
|
|
198
|
-
#
|
|
199
|
-
# @param api_keys [Hash] Hash of provider => key mappings
|
|
200
|
-
def apply_api_keys_to_ruby_llm(api_keys)
|
|
201
|
-
RubyLLM.configure do |config|
|
|
202
|
-
api_keys.each do |provider, key|
|
|
203
|
-
next if key.blank?
|
|
204
|
-
|
|
205
|
-
setter = api_key_setter_for(provider)
|
|
206
|
-
config.public_send(setter, key) if config.respond_to?(setter)
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
# Returns the setter method name for a provider's API key
|
|
212
|
-
#
|
|
213
|
-
# @param provider [Symbol, String] Provider name (e.g., :openai, :anthropic)
|
|
214
|
-
# @return [String] Setter method name (e.g., "openai_api_key=")
|
|
215
|
-
def api_key_setter_for(provider)
|
|
216
|
-
"#{provider}_api_key="
|
|
217
|
-
end
|
|
218
|
-
|
|
219
214
|
# Logs a warning about API key resolution failure
|
|
220
215
|
#
|
|
221
216
|
# @param source [String] Source that failed
|
|
@@ -236,7 +231,8 @@ module RubyLLM
|
|
|
236
231
|
return nil unless tenant.respond_to?(:llm_config)
|
|
237
232
|
|
|
238
233
|
tenant.llm_config
|
|
239
|
-
rescue
|
|
234
|
+
rescue => e
|
|
235
|
+
debug("Failed to extract tenant config: #{e.message}")
|
|
240
236
|
nil
|
|
241
237
|
end
|
|
242
238
|
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
module Providers
|
|
6
|
+
class Inception
|
|
7
|
+
# Determines capabilities and pricing for Inception Mercury models.
|
|
8
|
+
#
|
|
9
|
+
# Mercury models are diffusion LLMs with text-only I/O.
|
|
10
|
+
# Pricing is per million tokens.
|
|
11
|
+
#
|
|
12
|
+
# Models:
|
|
13
|
+
# - mercury-2: Reasoning dLLM, function calling, structured output
|
|
14
|
+
# - mercury: Base chat dLLM, function calling, structured output
|
|
15
|
+
# - mercury-coder-small: Fast coding model
|
|
16
|
+
# - mercury-edit: Code editing/FIM model
|
|
17
|
+
module Capabilities
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
REASONING_MODELS = %w[mercury-2].freeze
|
|
21
|
+
CODER_MODELS = %w[mercury-coder-small mercury-edit].freeze
|
|
22
|
+
FUNCTION_CALLING_MODELS = %w[mercury-2 mercury].freeze
|
|
23
|
+
|
|
24
|
+
def context_window_for(_model_id)
|
|
25
|
+
128_000
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def max_tokens_for(_model_id)
|
|
29
|
+
32_000
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def input_price_for(_model_id)
|
|
33
|
+
0.25
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def output_price_for(model_id)
|
|
37
|
+
if CODER_MODELS.include?(model_id)
|
|
38
|
+
1.00
|
|
39
|
+
else
|
|
40
|
+
0.75
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def supports_vision?(_model_id)
|
|
45
|
+
false
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def supports_functions?(model_id)
|
|
49
|
+
FUNCTION_CALLING_MODELS.include?(model_id)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def supports_json_mode?(model_id)
|
|
53
|
+
FUNCTION_CALLING_MODELS.include?(model_id)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def format_display_name(model_id)
|
|
57
|
+
case model_id
|
|
58
|
+
when "mercury-2" then "Mercury 2"
|
|
59
|
+
when "mercury" then "Mercury"
|
|
60
|
+
when "mercury-coder-small" then "Mercury Coder Small"
|
|
61
|
+
when "mercury-edit" then "Mercury Edit"
|
|
62
|
+
else
|
|
63
|
+
model_id.split("-").map(&:capitalize).join(" ")
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def model_type(model_id)
|
|
68
|
+
if CODER_MODELS.include?(model_id)
|
|
69
|
+
"code"
|
|
70
|
+
else
|
|
71
|
+
"chat"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def model_family(_model_id)
|
|
76
|
+
:mercury
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def modalities_for(_model_id)
|
|
80
|
+
{input: ["text"], output: ["text"]}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def capabilities_for(model_id)
|
|
84
|
+
caps = ["streaming"]
|
|
85
|
+
if FUNCTION_CALLING_MODELS.include?(model_id)
|
|
86
|
+
caps << "function_calling"
|
|
87
|
+
caps << "structured_output"
|
|
88
|
+
end
|
|
89
|
+
caps << "reasoning" if REASONING_MODELS.include?(model_id)
|
|
90
|
+
caps
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def pricing_for(model_id)
|
|
94
|
+
{
|
|
95
|
+
text_tokens: {
|
|
96
|
+
standard: {
|
|
97
|
+
input_per_million: input_price_for(model_id),
|
|
98
|
+
output_per_million: output_price_for(model_id)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
module Providers
|
|
6
|
+
class Inception
|
|
7
|
+
# Chat methods for Inception Mercury API.
|
|
8
|
+
# Mercury uses standard OpenAI chat format.
|
|
9
|
+
module Chat
|
|
10
|
+
def format_role(role)
|
|
11
|
+
role.to_s
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Extends RubyLLM::Configuration with Inception API key support.
|
|
4
|
+
# This allows users to configure: config.inception_api_key = ENV['INCEPTION_API_KEY']
|
|
5
|
+
module RubyLLM
|
|
6
|
+
class Configuration
|
|
7
|
+
attr_accessor :inception_api_key unless method_defined?(:inception_api_key)
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
module Providers
|
|
6
|
+
class Inception
|
|
7
|
+
# Parses model metadata from the Inception /models API endpoint.
|
|
8
|
+
# Response format is OpenAI-compatible.
|
|
9
|
+
module Models
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def parse_list_models_response(response, slug, capabilities)
|
|
13
|
+
Array(response.body["data"]).map do |model_data|
|
|
14
|
+
model_id = model_data["id"]
|
|
15
|
+
|
|
16
|
+
::RubyLLM::Model::Info.new(
|
|
17
|
+
id: model_id,
|
|
18
|
+
name: capabilities.format_display_name(model_id),
|
|
19
|
+
provider: slug,
|
|
20
|
+
family: "mercury",
|
|
21
|
+
created_at: model_data["created"] ? Time.at(model_data["created"]) : nil,
|
|
22
|
+
context_window: capabilities.context_window_for(model_id),
|
|
23
|
+
max_output_tokens: capabilities.max_tokens_for(model_id),
|
|
24
|
+
modalities: capabilities.modalities_for(model_id),
|
|
25
|
+
capabilities: capabilities.capabilities_for(model_id),
|
|
26
|
+
pricing: capabilities.pricing_for(model_id),
|
|
27
|
+
metadata: {
|
|
28
|
+
object: model_data["object"],
|
|
29
|
+
owned_by: model_data["owned_by"]
|
|
30
|
+
}.compact
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|