ruby_llm-agents 1.3.4 → 2.1.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.
Files changed (191) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +112 -336
  3. data/app/controllers/concerns/ruby_llm/agents/sortable.rb +0 -1
  4. data/app/controllers/ruby_llm/agents/agents_controller.rb +5 -56
  5. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +22 -106
  6. data/app/controllers/ruby_llm/agents/executions_controller.rb +4 -114
  7. data/app/controllers/ruby_llm/agents/tenants_controller.rb +30 -2
  8. data/app/helpers/ruby_llm/agents/application_helper.rb +19 -53
  9. data/app/models/ruby_llm/agents/execution/analytics.rb +13 -54
  10. data/app/models/ruby_llm/agents/execution/scopes.rb +61 -14
  11. data/app/models/ruby_llm/agents/execution.rb +52 -12
  12. data/app/models/ruby_llm/agents/execution_detail.rb +18 -0
  13. data/app/models/ruby_llm/agents/tenant/budgetable.rb +132 -24
  14. data/app/models/ruby_llm/agents/tenant/incrementable.rb +117 -0
  15. data/app/models/ruby_llm/agents/tenant/resettable.rb +128 -0
  16. data/app/models/ruby_llm/agents/tenant/trackable.rb +46 -12
  17. data/app/models/ruby_llm/agents/tenant.rb +2 -3
  18. data/app/models/ruby_llm/agents/tenant_budget.rb +6 -3
  19. data/app/services/ruby_llm/agents/agent_registry.rb +6 -112
  20. data/app/views/layouts/ruby_llm/agents/application.html.erb +89 -252
  21. data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +71 -218
  22. data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +20 -63
  23. data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +44 -131
  24. data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +16 -57
  25. data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +39 -104
  26. data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +29 -82
  27. data/app/views/ruby_llm/agents/agents/_empty_state.html.erb +4 -14
  28. data/app/views/ruby_llm/agents/agents/index.html.erb +105 -274
  29. data/app/views/ruby_llm/agents/agents/show.html.erb +248 -378
  30. data/app/views/ruby_llm/agents/dashboard/_action_center.html.erb +29 -52
  31. data/app/views/ruby_llm/agents/dashboard/_tenant_budget.html.erb +73 -99
  32. data/app/views/ruby_llm/agents/dashboard/index.html.erb +228 -433
  33. data/app/views/ruby_llm/agents/executions/_execution.html.erb +1 -1
  34. data/app/views/ruby_llm/agents/executions/_filters.html.erb +4 -25
  35. data/app/views/ruby_llm/agents/executions/_list.html.erb +111 -152
  36. data/app/views/ruby_llm/agents/executions/index.html.erb +5 -7
  37. data/app/views/ruby_llm/agents/executions/show.html.erb +526 -1037
  38. data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +5 -21
  39. data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +70 -191
  40. data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +16 -44
  41. data/app/views/ruby_llm/agents/shared/_select_dropdown.html.erb +12 -41
  42. data/app/views/ruby_llm/agents/shared/_status_badge.html.erb +11 -65
  43. data/app/views/ruby_llm/agents/shared/_tenant_filter.html.erb +6 -5
  44. data/app/views/ruby_llm/agents/system_config/show.html.erb +240 -351
  45. data/app/views/ruby_llm/agents/tenants/_form.html.erb +67 -77
  46. data/app/views/ruby_llm/agents/tenants/edit.html.erb +7 -9
  47. data/app/views/ruby_llm/agents/tenants/index.html.erb +100 -122
  48. data/app/views/ruby_llm/agents/tenants/show.html.erb +146 -336
  49. data/config/routes.rb +0 -13
  50. data/lib/generators/ruby_llm_agents/install_generator.rb +13 -17
  51. data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +2 -12
  52. data/lib/generators/ruby_llm_agents/restructure_generator.rb +0 -2
  53. data/lib/generators/ruby_llm_agents/templates/add_usage_counters_to_tenants_migration.rb.tt +37 -0
  54. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +1 -2
  55. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +1 -1
  56. data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +0 -1
  57. data/lib/generators/ruby_llm_agents/templates/create_execution_details_migration.rb.tt +27 -0
  58. data/lib/generators/ruby_llm_agents/templates/create_tenants_migration.rb.tt +25 -0
  59. data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +0 -1
  60. data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +33 -12
  61. data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +40 -71
  62. data/lib/generators/ruby_llm_agents/templates/remove_agent_version_migration.rb.tt +13 -0
  63. data/lib/generators/ruby_llm_agents/templates/remove_workflow_columns_migration.rb.tt +19 -0
  64. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +2 -4
  65. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +0 -1
  66. data/lib/generators/ruby_llm_agents/templates/split_execution_details_migration.rb.tt +232 -0
  67. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +77 -259
  68. data/lib/ruby_llm/agents/audio/speaker.rb +0 -1
  69. data/lib/ruby_llm/agents/audio/transcriber.rb +0 -1
  70. data/lib/ruby_llm/agents/base_agent.rb +54 -23
  71. data/lib/ruby_llm/agents/core/base/callbacks.rb +142 -0
  72. data/lib/ruby_llm/agents/core/base.rb +23 -55
  73. data/lib/ruby_llm/agents/core/configuration.rb +97 -117
  74. data/lib/ruby_llm/agents/core/errors.rb +0 -58
  75. data/lib/ruby_llm/agents/core/instrumentation.rb +157 -110
  76. data/lib/ruby_llm/agents/core/llm_tenant.rb +8 -7
  77. data/lib/ruby_llm/agents/core/version.rb +1 -1
  78. data/lib/ruby_llm/agents/dsl/base.rb +157 -17
  79. data/lib/ruby_llm/agents/dsl/caching.rb +33 -2
  80. data/lib/ruby_llm/agents/dsl/reliability.rb +148 -0
  81. data/lib/ruby_llm/agents/dsl.rb +1 -2
  82. data/lib/ruby_llm/agents/image/analyzer/execution.rb +1 -2
  83. data/lib/ruby_llm/agents/image/background_remover/execution.rb +1 -2
  84. data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +1 -13
  85. data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +2 -2
  86. data/lib/ruby_llm/agents/image/editor/dsl.rb +0 -14
  87. data/lib/ruby_llm/agents/image/editor/execution.rb +1 -10
  88. data/lib/ruby_llm/agents/image/editor.rb +0 -1
  89. data/lib/ruby_llm/agents/image/generator.rb +0 -21
  90. data/lib/ruby_llm/agents/image/pipeline/dsl.rb +0 -13
  91. data/lib/ruby_llm/agents/image/pipeline/execution.rb +0 -1
  92. data/lib/ruby_llm/agents/image/transformer/dsl.rb +0 -13
  93. data/lib/ruby_llm/agents/image/transformer/execution.rb +1 -10
  94. data/lib/ruby_llm/agents/image/transformer.rb +0 -1
  95. data/lib/ruby_llm/agents/image/upscaler/execution.rb +1 -2
  96. data/lib/ruby_llm/agents/image/variator/execution.rb +1 -2
  97. data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +78 -173
  98. data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +66 -2
  99. data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +0 -12
  100. data/lib/ruby_llm/agents/infrastructure/circuit_breaker.rb +10 -13
  101. data/lib/ruby_llm/agents/infrastructure/execution_logger_job.rb +8 -0
  102. data/lib/ruby_llm/agents/pipeline/context.rb +0 -1
  103. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +28 -4
  104. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +3 -10
  105. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +88 -55
  106. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +5 -41
  107. data/lib/ruby_llm/agents/rails/engine.rb +6 -6
  108. data/lib/ruby_llm/agents/results/base.rb +1 -49
  109. data/lib/ruby_llm/agents/text/embedder.rb +0 -1
  110. data/lib/ruby_llm/agents.rb +1 -9
  111. data/lib/tasks/ruby_llm_agents.rake +34 -0
  112. metadata +14 -83
  113. data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +0 -214
  114. data/app/controllers/ruby_llm/agents/workflows_controller.rb +0 -544
  115. data/app/mailers/ruby_llm/agents/alert_mailer.rb +0 -84
  116. data/app/mailers/ruby_llm/agents/application_mailer.rb +0 -28
  117. data/app/models/ruby_llm/agents/api_configuration.rb +0 -386
  118. data/app/models/ruby_llm/agents/execution/workflow.rb +0 -170
  119. data/app/models/ruby_llm/agents/tenant/configurable.rb +0 -135
  120. data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -98
  121. data/app/views/ruby_llm/agents/agents/_version_comparison.html.erb +0 -186
  122. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +0 -126
  123. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +0 -107
  124. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +0 -18
  125. data/app/views/ruby_llm/agents/api_configurations/_api_key_field.html.erb +0 -34
  126. data/app/views/ruby_llm/agents/api_configurations/_form.html.erb +0 -288
  127. data/app/views/ruby_llm/agents/api_configurations/edit.html.erb +0 -95
  128. data/app/views/ruby_llm/agents/api_configurations/edit_tenant.html.erb +0 -97
  129. data/app/views/ruby_llm/agents/api_configurations/show.html.erb +0 -214
  130. data/app/views/ruby_llm/agents/api_configurations/tenant.html.erb +0 -179
  131. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +0 -73
  132. data/app/views/ruby_llm/agents/dashboard/_alerts_feed.html.erb +0 -62
  133. data/app/views/ruby_llm/agents/dashboard/_breaker_strip.html.erb +0 -47
  134. data/app/views/ruby_llm/agents/dashboard/_budgets_bar.html.erb +0 -75
  135. data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +0 -56
  136. data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +0 -115
  137. data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +0 -59
  138. data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +0 -60
  139. data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +0 -86
  140. data/app/views/ruby_llm/agents/executions/dry_run.html.erb +0 -149
  141. data/app/views/ruby_llm/agents/shared/_breadcrumbs.html.erb +0 -48
  142. data/app/views/ruby_llm/agents/shared/_nav_link.html.erb +0 -27
  143. data/app/views/ruby_llm/agents/shared/_stat_card.html.erb +0 -14
  144. data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +0 -35
  145. data/app/views/ruby_llm/agents/workflows/_empty_state.html.erb +0 -22
  146. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +0 -228
  147. data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +0 -539
  148. data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +0 -76
  149. data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +0 -74
  150. data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +0 -108
  151. data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +0 -920
  152. data/app/views/ruby_llm/agents/workflows/index.html.erb +0 -179
  153. data/app/views/ruby_llm/agents/workflows/show.html.erb +0 -467
  154. data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +0 -100
  155. data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +0 -38
  156. data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +0 -48
  157. data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +0 -90
  158. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +0 -551
  159. data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +0 -181
  160. data/lib/ruby_llm/agents/core/base/moderation_execution.rb +0 -274
  161. data/lib/ruby_llm/agents/core/resolved_config.rb +0 -348
  162. data/lib/ruby_llm/agents/image/generator/content_policy.rb +0 -95
  163. data/lib/ruby_llm/agents/infrastructure/redactor.rb +0 -130
  164. data/lib/ruby_llm/agents/results/moderation_result.rb +0 -158
  165. data/lib/ruby_llm/agents/text/moderator.rb +0 -237
  166. data/lib/ruby_llm/agents/workflow/approval.rb +0 -205
  167. data/lib/ruby_llm/agents/workflow/approval_store.rb +0 -179
  168. data/lib/ruby_llm/agents/workflow/async.rb +0 -220
  169. data/lib/ruby_llm/agents/workflow/async_executor.rb +0 -156
  170. data/lib/ruby_llm/agents/workflow/dsl/executor.rb +0 -467
  171. data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +0 -244
  172. data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +0 -289
  173. data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +0 -107
  174. data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +0 -150
  175. data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +0 -187
  176. data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +0 -352
  177. data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +0 -415
  178. data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +0 -257
  179. data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +0 -317
  180. data/lib/ruby_llm/agents/workflow/dsl.rb +0 -576
  181. data/lib/ruby_llm/agents/workflow/instrumentation.rb +0 -249
  182. data/lib/ruby_llm/agents/workflow/notifiers/base.rb +0 -117
  183. data/lib/ruby_llm/agents/workflow/notifiers/email.rb +0 -117
  184. data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +0 -180
  185. data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +0 -121
  186. data/lib/ruby_llm/agents/workflow/notifiers.rb +0 -70
  187. data/lib/ruby_llm/agents/workflow/orchestrator.rb +0 -416
  188. data/lib/ruby_llm/agents/workflow/result.rb +0 -592
  189. data/lib/ruby_llm/agents/workflow/thread_pool.rb +0 -185
  190. data/lib/ruby_llm/agents/workflow/throttle_manager.rb +0 -206
  191. data/lib/ruby_llm/agents/workflow/wait_result.rb +0 -213
@@ -1,386 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyLLM
4
- module Agents
5
- # Database-backed API configuration for LLM providers
6
- #
7
- # Stores API keys (encrypted at rest) and configuration options that can be
8
- # managed via the dashboard UI. Supports both global settings and per-tenant
9
- # overrides.
10
- #
11
- # Resolution priority: per-tenant DB > global DB > config file (RubyLLM.configure)
12
- #
13
- # @!attribute [rw] scope_type
14
- # @return [String] Either 'global' or 'tenant'
15
- # @!attribute [rw] scope_id
16
- # @return [String, nil] Tenant ID when scope_type='tenant'
17
- #
18
- # @example Setting global API keys
19
- # config = ApiConfiguration.global
20
- # config.update!(
21
- # openai_api_key: "sk-...",
22
- # anthropic_api_key: "sk-ant-..."
23
- # )
24
- #
25
- # @example Setting tenant-specific configuration
26
- # tenant_config = ApiConfiguration.for_tenant!("acme_corp")
27
- # tenant_config.update!(
28
- # anthropic_api_key: "sk-ant-tenant-specific...",
29
- # default_model: "claude-sonnet-4-20250514"
30
- # )
31
- #
32
- # @example Resolving configuration for a tenant
33
- # resolved = ApiConfiguration.resolve(tenant_id: "acme_corp")
34
- # resolved.apply_to_ruby_llm! # Apply to RubyLLM.configuration
35
- #
36
- # @see ResolvedConfig
37
- # @api public
38
- class ApiConfiguration < ::ActiveRecord::Base
39
- self.table_name = "ruby_llm_agents_api_configurations"
40
-
41
- # Valid scope types
42
- SCOPE_TYPES = %w[global tenant].freeze
43
-
44
- # All API key attributes that should be encrypted
45
- API_KEY_ATTRIBUTES = %i[
46
- openai_api_key
47
- anthropic_api_key
48
- gemini_api_key
49
- deepseek_api_key
50
- mistral_api_key
51
- perplexity_api_key
52
- openrouter_api_key
53
- gpustack_api_key
54
- xai_api_key
55
- ollama_api_key
56
- bedrock_api_key
57
- bedrock_secret_key
58
- bedrock_session_token
59
- vertexai_credentials
60
- ].freeze
61
-
62
- # All endpoint attributes
63
- ENDPOINT_ATTRIBUTES = %i[
64
- openai_api_base
65
- gemini_api_base
66
- ollama_api_base
67
- gpustack_api_base
68
- xai_api_base
69
- ].freeze
70
-
71
- # All default model attributes
72
- MODEL_ATTRIBUTES = %i[
73
- default_model
74
- default_embedding_model
75
- default_image_model
76
- default_moderation_model
77
- ].freeze
78
-
79
- # Connection settings attributes
80
- CONNECTION_ATTRIBUTES = %i[
81
- request_timeout
82
- max_retries
83
- retry_interval
84
- retry_backoff_factor
85
- retry_interval_randomness
86
- http_proxy
87
- ].freeze
88
-
89
- # All configurable attributes (excluding API keys)
90
- NON_KEY_ATTRIBUTES = (
91
- ENDPOINT_ATTRIBUTES +
92
- MODEL_ATTRIBUTES +
93
- CONNECTION_ATTRIBUTES +
94
- %i[
95
- openai_organization_id
96
- openai_project_id
97
- bedrock_region
98
- vertexai_project_id
99
- vertexai_location
100
- ]
101
- ).freeze
102
-
103
- # Encrypt all API keys using Rails encrypted attributes
104
- # Requires Rails encryption to be configured (rails credentials:edit)
105
- API_KEY_ATTRIBUTES.each do |key_attr|
106
- encrypts key_attr, deterministic: false
107
- end
108
-
109
- # Validations
110
- validates :scope_type, presence: true, inclusion: { in: SCOPE_TYPES }
111
- validates :scope_id, uniqueness: { scope: :scope_type }, allow_nil: true
112
- validate :validate_scope_consistency
113
-
114
- # Scopes
115
- scope :global_config, -> { where(scope_type: "global", scope_id: nil) }
116
- scope :for_scope, ->(type, id) { where(scope_type: type, scope_id: id) }
117
- scope :tenant_configs, -> { where(scope_type: "tenant") }
118
-
119
- # Provider configuration mappings for display
120
- PROVIDERS = {
121
- openai: {
122
- name: "OpenAI",
123
- key_attr: :openai_api_key,
124
- base_attr: :openai_api_base,
125
- extra_attrs: [:openai_organization_id, :openai_project_id],
126
- capabilities: ["Chat", "Embeddings", "Images", "Moderation"]
127
- },
128
- anthropic: {
129
- name: "Anthropic",
130
- key_attr: :anthropic_api_key,
131
- capabilities: ["Chat"]
132
- },
133
- gemini: {
134
- name: "Google Gemini",
135
- key_attr: :gemini_api_key,
136
- base_attr: :gemini_api_base,
137
- capabilities: ["Chat", "Embeddings", "Images"]
138
- },
139
- deepseek: {
140
- name: "DeepSeek",
141
- key_attr: :deepseek_api_key,
142
- capabilities: ["Chat"]
143
- },
144
- mistral: {
145
- name: "Mistral",
146
- key_attr: :mistral_api_key,
147
- capabilities: ["Chat", "Embeddings"]
148
- },
149
- perplexity: {
150
- name: "Perplexity",
151
- key_attr: :perplexity_api_key,
152
- capabilities: ["Chat"]
153
- },
154
- openrouter: {
155
- name: "OpenRouter",
156
- key_attr: :openrouter_api_key,
157
- capabilities: ["Chat"]
158
- },
159
- gpustack: {
160
- name: "GPUStack",
161
- key_attr: :gpustack_api_key,
162
- base_attr: :gpustack_api_base,
163
- capabilities: ["Chat"]
164
- },
165
- xai: {
166
- name: "xAI",
167
- key_attr: :xai_api_key,
168
- base_attr: :xai_api_base,
169
- capabilities: ["Chat"]
170
- },
171
- ollama: {
172
- name: "Ollama",
173
- key_attr: :ollama_api_key,
174
- base_attr: :ollama_api_base,
175
- capabilities: ["Chat", "Embeddings"]
176
- },
177
- bedrock: {
178
- name: "AWS Bedrock",
179
- key_attr: :bedrock_api_key,
180
- extra_attrs: [:bedrock_secret_key, :bedrock_session_token, :bedrock_region],
181
- capabilities: ["Chat", "Embeddings"]
182
- },
183
- vertexai: {
184
- name: "Google Vertex AI",
185
- key_attr: :vertexai_credentials,
186
- extra_attrs: [:vertexai_project_id, :vertexai_location],
187
- capabilities: ["Chat", "Embeddings"]
188
- }
189
- }.freeze
190
-
191
- class << self
192
- # Finds or creates the global configuration
193
- #
194
- # @return [ApiConfiguration] The global configuration record
195
- def global
196
- global_config.first_or_create!
197
- end
198
-
199
- # Finds a tenant-specific configuration
200
- #
201
- # @param tenant_id [String] The tenant identifier
202
- # @return [ApiConfiguration, nil] The tenant configuration or nil
203
- def for_tenant(tenant_id)
204
- return nil if tenant_id.blank?
205
-
206
- for_scope("tenant", tenant_id).first
207
- end
208
-
209
- # Finds or creates a tenant-specific configuration
210
- #
211
- # @param tenant_id [String] The tenant identifier
212
- # @return [ApiConfiguration] The tenant configuration record
213
- def for_tenant!(tenant_id)
214
- raise ArgumentError, "tenant_id cannot be blank" if tenant_id.blank?
215
-
216
- for_scope("tenant", tenant_id).first_or_create!(
217
- scope_type: "tenant",
218
- scope_id: tenant_id
219
- )
220
- end
221
-
222
- # Resolves the effective configuration for a given tenant
223
- #
224
- # Creates a ResolvedConfig that combines tenant config > global DB > RubyLLM config
225
- #
226
- # @param tenant_id [String, nil] Optional tenant identifier
227
- # @return [ResolvedConfig] The resolved configuration
228
- def resolve(tenant_id: nil)
229
- tenant_config = tenant_id.present? ? for_tenant(tenant_id) : nil
230
- global = global_config.first
231
-
232
- RubyLLM::Agents::ResolvedConfig.new(
233
- tenant_config: tenant_config,
234
- global_config: global,
235
- ruby_llm_config: ruby_llm_current_config
236
- )
237
- end
238
-
239
- # Attempts to get the current RubyLLM configuration object
240
- # Gets the current RubyLLM configuration object
241
- #
242
- # @return [Object, nil] The RubyLLM config object or nil
243
- def ruby_llm_current_config
244
- return nil unless defined?(::RubyLLM)
245
- return nil unless RubyLLM.respond_to?(:config)
246
-
247
- RubyLLM.config
248
- rescue StandardError
249
- nil
250
- end
251
-
252
- # Checks if the table exists (for graceful degradation)
253
- #
254
- # @return [Boolean]
255
- def table_exists?
256
- connection.table_exists?(table_name)
257
- rescue ActiveRecord::NoDatabaseError, ActiveRecord::StatementInvalid
258
- false
259
- end
260
- end
261
-
262
- # Checks if a specific attribute has a value set
263
- #
264
- # @param attr_name [Symbol, String] The attribute name
265
- # @return [Boolean]
266
- def has_value?(attr_name)
267
- value = send(attr_name)
268
- value.present?
269
- rescue NoMethodError
270
- false
271
- end
272
-
273
- # Returns a masked version of an API key for display
274
- #
275
- # @param attr_name [Symbol, String] The API key attribute name
276
- # @return [String, nil] Masked key like "sk-ab****wxyz" or nil
277
- def masked_key(attr_name)
278
- value = send(attr_name)
279
- return nil if value.blank?
280
-
281
- mask_string(value)
282
- end
283
-
284
- # Returns the source of this configuration
285
- #
286
- # @return [String] "global" or "tenant:ID"
287
- def source_label
288
- scope_type == "global" ? "Global" : "Tenant: #{scope_id}"
289
- end
290
-
291
- # Converts this configuration to a hash suitable for RubyLLM
292
- #
293
- # @return [Hash] Configuration hash with non-nil values
294
- def to_ruby_llm_config
295
- {}.tap do |config|
296
- # API keys
297
- config[:openai_api_key] = openai_api_key if openai_api_key.present?
298
- config[:anthropic_api_key] = anthropic_api_key if anthropic_api_key.present?
299
- config[:gemini_api_key] = gemini_api_key if gemini_api_key.present?
300
- config[:deepseek_api_key] = deepseek_api_key if deepseek_api_key.present?
301
- config[:mistral_api_key] = mistral_api_key if mistral_api_key.present?
302
- config[:perplexity_api_key] = perplexity_api_key if perplexity_api_key.present?
303
- config[:openrouter_api_key] = openrouter_api_key if openrouter_api_key.present?
304
- config[:gpustack_api_key] = gpustack_api_key if gpustack_api_key.present?
305
- config[:xai_api_key] = xai_api_key if xai_api_key.present?
306
- config[:ollama_api_key] = ollama_api_key if ollama_api_key.present?
307
-
308
- # Bedrock
309
- config[:bedrock_api_key] = bedrock_api_key if bedrock_api_key.present?
310
- config[:bedrock_secret_key] = bedrock_secret_key if bedrock_secret_key.present?
311
- config[:bedrock_session_token] = bedrock_session_token if bedrock_session_token.present?
312
- config[:bedrock_region] = bedrock_region if bedrock_region.present?
313
-
314
- # Vertex AI
315
- config[:vertexai_credentials] = vertexai_credentials if vertexai_credentials.present?
316
- config[:vertexai_project_id] = vertexai_project_id if vertexai_project_id.present?
317
- config[:vertexai_location] = vertexai_location if vertexai_location.present?
318
-
319
- # Endpoints
320
- config[:openai_api_base] = openai_api_base if openai_api_base.present?
321
- config[:gemini_api_base] = gemini_api_base if gemini_api_base.present?
322
- config[:ollama_api_base] = ollama_api_base if ollama_api_base.present?
323
- config[:gpustack_api_base] = gpustack_api_base if gpustack_api_base.present?
324
- config[:xai_api_base] = xai_api_base if xai_api_base.present?
325
-
326
- # OpenAI options
327
- config[:openai_organization_id] = openai_organization_id if openai_organization_id.present?
328
- config[:openai_project_id] = openai_project_id if openai_project_id.present?
329
-
330
- # Default models
331
- config[:default_model] = default_model if default_model.present?
332
- config[:default_embedding_model] = default_embedding_model if default_embedding_model.present?
333
- config[:default_image_model] = default_image_model if default_image_model.present?
334
- config[:default_moderation_model] = default_moderation_model if default_moderation_model.present?
335
-
336
- # Connection settings
337
- config[:request_timeout] = request_timeout if request_timeout.present?
338
- config[:max_retries] = max_retries if max_retries.present?
339
- config[:retry_interval] = retry_interval if retry_interval.present?
340
- config[:retry_backoff_factor] = retry_backoff_factor if retry_backoff_factor.present?
341
- config[:retry_interval_randomness] = retry_interval_randomness if retry_interval_randomness.present?
342
- config[:http_proxy] = http_proxy if http_proxy.present?
343
- end
344
- end
345
-
346
- # Returns provider status information for display
347
- #
348
- # @return [Array<Hash>] Array of provider status hashes
349
- def provider_statuses
350
- PROVIDERS.map do |key, info|
351
- key_value = send(info[:key_attr])
352
- {
353
- key: key,
354
- name: info[:name],
355
- configured: key_value.present?,
356
- masked_key: key_value.present? ? mask_string(key_value) : nil,
357
- capabilities: info[:capabilities],
358
- has_base_url: info[:base_attr].present? && send(info[:base_attr]).present?
359
- }
360
- end
361
- end
362
-
363
- private
364
-
365
- # Validates scope consistency
366
- def validate_scope_consistency
367
- if scope_type == "global" && scope_id.present?
368
- errors.add(:scope_id, "must be nil for global scope")
369
- elsif scope_type == "tenant" && scope_id.blank?
370
- errors.add(:scope_id, "must be present for tenant scope")
371
- end
372
- end
373
-
374
- # Masks a string for display (shows first 2 and last 4 chars)
375
- #
376
- # @param value [String] The string to mask
377
- # @return [String] Masked string
378
- def mask_string(value)
379
- return nil if value.blank?
380
- return "****" if value.length <= 8
381
-
382
- "#{value[0..1]}****#{value[-4..]}"
383
- end
384
- end
385
- end
386
- end
@@ -1,170 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyLLM
4
- module Agents
5
- class Execution
6
- # Workflow concern for workflow-related methods and aggregate calculations
7
- #
8
- # Provides instance methods for determining workflow type, calculating
9
- # aggregate statistics across child executions, and retrieving workflow
10
- # step/branch information.
11
- #
12
- # @see RubyLLM::Agents::Execution
13
- # @api public
14
- module Workflow
15
- extend ActiveSupport::Concern
16
-
17
- # Returns whether this is a workflow execution (has workflow_type)
18
- #
19
- # @return [Boolean] true if this is a workflow execution
20
- def workflow?
21
- workflow_type.present?
22
- end
23
-
24
- # Returns whether this is a root workflow execution (top-level)
25
- #
26
- # @return [Boolean] true if this is a workflow with no parent
27
- def root_workflow?
28
- workflow? && root?
29
- end
30
-
31
- # Returns all workflow steps/branches ordered by creation time
32
- #
33
- # @return [ActiveRecord::Relation] Child executions for this workflow
34
- def workflow_steps
35
- child_executions.order(:created_at)
36
- end
37
-
38
- # Returns the count of child workflow steps
39
- #
40
- # @return [Integer] Number of child executions
41
- def workflow_steps_count
42
- child_executions.count
43
- end
44
-
45
- # @!group Aggregate Statistics
46
-
47
- # Returns aggregate stats for all child executions
48
- #
49
- # @return [Hash] Aggregated metrics including cost, tokens, duration
50
- def workflow_aggregate_stats
51
- return @workflow_aggregate_stats if defined?(@workflow_aggregate_stats)
52
-
53
- children = child_executions.to_a
54
- return empty_aggregate_stats if children.empty?
55
-
56
- @workflow_aggregate_stats = {
57
- total_cost: children.sum { |c| c.total_cost || 0 },
58
- total_tokens: children.sum { |c| c.total_tokens || 0 },
59
- input_tokens: children.sum { |c| c.input_tokens || 0 },
60
- output_tokens: children.sum { |c| c.output_tokens || 0 },
61
- total_duration_ms: children.sum { |c| c.duration_ms || 0 },
62
- wall_clock_ms: calculate_wall_clock_duration(children),
63
- steps_count: children.size,
64
- successful_count: children.count(&:status_success?),
65
- failed_count: children.count(&:status_error?),
66
- timeout_count: children.count(&:status_timeout?),
67
- running_count: children.count(&:status_running?),
68
- success_rate: calculate_success_rate(children),
69
- models_used: children.map(&:model_id).uniq.compact
70
- }
71
- end
72
-
73
- # Returns aggregate total cost across all child executions
74
- #
75
- # @return [Float] Total cost in USD
76
- def workflow_total_cost
77
- workflow_aggregate_stats[:total_cost]
78
- end
79
-
80
- # Returns aggregate total tokens across all child executions
81
- #
82
- # @return [Integer] Total tokens used
83
- def workflow_total_tokens
84
- workflow_aggregate_stats[:total_tokens]
85
- end
86
-
87
- # Returns the wall-clock duration (from first start to last completion)
88
- #
89
- # @return [Integer, nil] Duration in milliseconds
90
- def workflow_wall_clock_ms
91
- workflow_aggregate_stats[:wall_clock_ms]
92
- end
93
-
94
- # Returns the sum of all step durations (may exceed wall-clock for parallel)
95
- #
96
- # @return [Integer] Sum of all durations in milliseconds
97
- def workflow_sum_duration_ms
98
- workflow_aggregate_stats[:total_duration_ms]
99
- end
100
-
101
- # Returns the overall workflow status based on child executions
102
- #
103
- # @return [Symbol] :success, :error, :timeout, :running, or :pending
104
- def workflow_overall_status
105
- stats = workflow_aggregate_stats
106
- return :pending if stats[:steps_count].zero?
107
- return :running if stats[:running_count] > 0
108
- return :error if stats[:failed_count] > 0
109
- return :timeout if stats[:timeout_count] > 0
110
-
111
- :success
112
- end
113
-
114
- # @!endgroup
115
-
116
- private
117
-
118
- # Returns empty aggregate stats hash
119
- #
120
- # @return [Hash] Empty stats with zero values
121
- def empty_aggregate_stats
122
- {
123
- total_cost: 0,
124
- total_tokens: 0,
125
- input_tokens: 0,
126
- output_tokens: 0,
127
- total_duration_ms: 0,
128
- wall_clock_ms: nil,
129
- steps_count: 0,
130
- successful_count: 0,
131
- failed_count: 0,
132
- timeout_count: 0,
133
- running_count: 0,
134
- success_rate: 0.0,
135
- models_used: []
136
- }
137
- end
138
-
139
- # Calculates wall-clock duration from child executions
140
- #
141
- # @param children [Array<Execution>] Child executions
142
- # @return [Integer, nil] Duration in milliseconds
143
- def calculate_wall_clock_duration(children)
144
- started_times = children.map(&:started_at).compact
145
- completed_times = children.map(&:completed_at).compact
146
-
147
- return nil if started_times.empty? || completed_times.empty?
148
-
149
- first_start = started_times.min
150
- last_complete = completed_times.max
151
-
152
- ((last_complete - first_start) * 1000).round
153
- end
154
-
155
- # Calculates success rate from children
156
- #
157
- # @param children [Array<Execution>] Child executions
158
- # @return [Float] Success rate as percentage
159
- def calculate_success_rate(children)
160
- return 0.0 if children.empty?
161
-
162
- completed = children.reject(&:status_running?)
163
- return 0.0 if completed.empty?
164
-
165
- (completed.count(&:status_success?).to_f / completed.size * 100).round(1)
166
- end
167
- end
168
- end
169
- end
170
- end
@@ -1,135 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "active_support/concern"
4
-
5
- module RubyLLM
6
- module Agents
7
- class Tenant
8
- # Manages API configuration for a tenant.
9
- #
10
- # Links to the ApiConfiguration model to provide per-tenant API keys
11
- # and settings. Supports inheritance from global configuration when
12
- # tenant-specific settings are not defined.
13
- #
14
- # @example Accessing tenant API keys
15
- # tenant = Tenant.for("acme_corp")
16
- # tenant.api_key_for(:openai) # => "sk-..."
17
- # tenant.has_custom_api_keys? # => true
18
- #
19
- # @example Getting effective configuration
20
- # config = tenant.effective_api_configuration
21
- # config.apply_to_ruby_llm!
22
- #
23
- # @see ApiConfiguration
24
- # @api public
25
- module Configurable
26
- extend ActiveSupport::Concern
27
-
28
- included do
29
- # Link to tenant-specific API configuration
30
- has_one :api_configuration,
31
- -> { where(scope_type: "tenant") },
32
- class_name: "RubyLLM::Agents::ApiConfiguration",
33
- foreign_key: :scope_id,
34
- primary_key: :tenant_id,
35
- dependent: :destroy
36
- end
37
-
38
- # Get API key for a specific provider
39
- #
40
- # @param provider [Symbol] Provider name (:openai, :anthropic, :gemini, etc.)
41
- # @return [String, nil] The API key or nil if not configured
42
- #
43
- # @example
44
- # tenant.api_key_for(:openai) # => "sk-abc123..."
45
- # tenant.api_key_for(:anthropic) # => "sk-ant-xyz..."
46
- def api_key_for(provider)
47
- attr_name = "#{provider}_api_key"
48
- api_configuration&.send(attr_name) if api_configuration&.respond_to?(attr_name)
49
- end
50
-
51
- # Check if tenant has custom API keys configured
52
- #
53
- # @return [Boolean] true if tenant has an ApiConfiguration record
54
- def has_custom_api_keys?
55
- api_configuration.present?
56
- end
57
-
58
- # Get effective API configuration for this tenant
59
- #
60
- # Returns the resolved configuration that combines tenant-specific
61
- # settings with global defaults.
62
- #
63
- # @return [ResolvedConfig] The resolved configuration
64
- #
65
- # @example
66
- # config = tenant.effective_api_configuration
67
- # config.openai_api_key # Tenant's key or global fallback
68
- def effective_api_configuration
69
- ApiConfiguration.resolve(tenant_id: tenant_id)
70
- end
71
-
72
- # Get or create the API configuration for this tenant
73
- #
74
- # @return [ApiConfiguration] The tenant's API configuration record
75
- def api_configuration!
76
- api_configuration || create_api_configuration!(
77
- scope_type: "tenant",
78
- scope_id: tenant_id
79
- )
80
- end
81
-
82
- # Configure API settings for this tenant
83
- #
84
- # @yield [config] Block to configure the API settings
85
- # @yieldparam config [ApiConfiguration] The configuration to modify
86
- # @return [ApiConfiguration] The saved configuration
87
- #
88
- # @example
89
- # tenant.configure_api do |config|
90
- # config.openai_api_key = "sk-..."
91
- # config.default_model = "gpt-4o"
92
- # end
93
- def configure_api(&block)
94
- config = api_configuration!
95
- yield(config) if block_given?
96
- config.save!
97
- config
98
- end
99
-
100
- # Check if a specific provider is configured for this tenant
101
- #
102
- # @param provider [Symbol] Provider name
103
- # @return [Boolean] true if the provider has an API key set
104
- def provider_configured?(provider)
105
- api_key_for(provider).present?
106
- end
107
-
108
- # Get all configured providers for this tenant
109
- #
110
- # @return [Array<Symbol>] List of configured provider symbols
111
- def configured_providers
112
- return [] unless api_configuration
113
-
114
- ApiConfiguration::PROVIDERS.keys.select do |provider|
115
- provider_configured?(provider)
116
- end
117
- end
118
-
119
- # Get the default model for this tenant
120
- #
121
- # @return [String, nil] The default model or nil
122
- def default_model
123
- api_configuration&.default_model
124
- end
125
-
126
- # Get the default embedding model for this tenant
127
- #
128
- # @return [String, nil] The default embedding model or nil
129
- def default_embedding_model
130
- api_configuration&.default_embedding_model
131
- end
132
- end
133
- end
134
- end
135
- end