ruby_llm-agents 0.4.0 → 1.0.0.beta.1

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 (208) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +225 -34
  3. data/app/controllers/ruby_llm/agents/agents_controller.rb +136 -16
  4. data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +214 -0
  5. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +29 -9
  6. data/app/controllers/ruby_llm/agents/{settings_controller.rb → system_config_controller.rb} +3 -3
  7. data/app/controllers/ruby_llm/agents/tenants_controller.rb +109 -0
  8. data/app/controllers/ruby_llm/agents/workflows_controller.rb +355 -0
  9. data/app/helpers/ruby_llm/agents/application_helper.rb +25 -0
  10. data/app/models/ruby_llm/agents/api_configuration.rb +386 -0
  11. data/app/models/ruby_llm/agents/execution.rb +3 -0
  12. data/app/models/ruby_llm/agents/tenant_budget.rb +112 -14
  13. data/app/services/ruby_llm/agents/agent_registry.rb +51 -12
  14. data/app/views/layouts/ruby_llm/agents/application.html.erb +5 -30
  15. data/app/views/ruby_llm/agents/agents/_agent.html.erb +13 -1
  16. data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +235 -0
  17. data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +70 -0
  18. data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +152 -0
  19. data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +63 -0
  20. data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +108 -0
  21. data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +91 -0
  22. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +1 -1
  23. data/app/views/ruby_llm/agents/agents/index.html.erb +74 -9
  24. data/app/views/ruby_llm/agents/agents/show.html.erb +18 -378
  25. data/app/views/ruby_llm/agents/api_configurations/_api_key_field.html.erb +34 -0
  26. data/app/views/ruby_llm/agents/api_configurations/_form.html.erb +288 -0
  27. data/app/views/ruby_llm/agents/api_configurations/edit.html.erb +95 -0
  28. data/app/views/ruby_llm/agents/api_configurations/edit_tenant.html.erb +97 -0
  29. data/app/views/ruby_llm/agents/api_configurations/show.html.erb +211 -0
  30. data/app/views/ruby_llm/agents/api_configurations/tenant.html.erb +179 -0
  31. data/app/views/ruby_llm/agents/dashboard/_action_center.html.erb +1 -1
  32. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +269 -15
  33. data/app/views/ruby_llm/agents/executions/show.html.erb +98 -0
  34. data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +93 -0
  35. data/app/views/ruby_llm/agents/{settings → system_config}/show.html.erb +1 -1
  36. data/app/views/ruby_llm/agents/tenants/_form.html.erb +150 -0
  37. data/app/views/ruby_llm/agents/tenants/edit.html.erb +13 -0
  38. data/app/views/ruby_llm/agents/tenants/index.html.erb +129 -0
  39. data/app/views/ruby_llm/agents/tenants/show.html.erb +374 -0
  40. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +236 -0
  41. data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +76 -0
  42. data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +74 -0
  43. data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +108 -0
  44. data/app/views/ruby_llm/agents/workflows/show.html.erb +442 -0
  45. data/config/routes.rb +13 -1
  46. data/lib/generators/ruby_llm_agents/agent_generator.rb +56 -7
  47. data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +100 -0
  48. data/lib/generators/ruby_llm_agents/background_remover_generator.rb +110 -0
  49. data/lib/generators/ruby_llm_agents/embedder_generator.rb +107 -0
  50. data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +115 -0
  51. data/lib/generators/ruby_llm_agents/image_editor_generator.rb +108 -0
  52. data/lib/generators/ruby_llm_agents/image_generator_generator.rb +116 -0
  53. data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +178 -0
  54. data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +109 -0
  55. data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +103 -0
  56. data/lib/generators/ruby_llm_agents/image_variator_generator.rb +102 -0
  57. data/lib/generators/ruby_llm_agents/install_generator.rb +76 -4
  58. data/lib/generators/ruby_llm_agents/restructure_generator.rb +292 -0
  59. data/lib/generators/ruby_llm_agents/speaker_generator.rb +121 -0
  60. data/lib/generators/ruby_llm_agents/templates/add_execution_type_migration.rb.tt +8 -0
  61. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +99 -84
  62. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +42 -40
  63. data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +26 -0
  64. data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +50 -0
  65. data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +26 -0
  66. data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +20 -0
  67. data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +38 -0
  68. data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +139 -0
  69. data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +21 -0
  70. data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +20 -0
  71. data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +20 -0
  72. data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +49 -0
  73. data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +53 -0
  74. data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +44 -0
  75. data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +90 -0
  76. data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +41 -0
  77. data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +45 -0
  78. data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +35 -0
  79. data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +47 -0
  80. data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +50 -0
  81. data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +44 -0
  82. data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +38 -0
  83. data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +33 -0
  84. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +228 -0
  85. data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +131 -0
  86. data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +255 -0
  87. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +120 -0
  88. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +102 -0
  89. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +282 -0
  90. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +228 -0
  91. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +120 -0
  92. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +110 -0
  93. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +120 -0
  94. data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +212 -0
  95. data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +227 -0
  96. data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +251 -0
  97. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +300 -0
  98. data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +56 -0
  99. data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +51 -0
  100. data/lib/generators/ruby_llm_agents/transcriber_generator.rb +107 -0
  101. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +152 -1
  102. data/lib/ruby_llm/agents/audio/speaker.rb +553 -0
  103. data/lib/ruby_llm/agents/audio/transcriber.rb +669 -0
  104. data/lib/ruby_llm/agents/base_agent.rb +675 -0
  105. data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +181 -0
  106. data/lib/ruby_llm/agents/core/base/moderation_execution.rb +274 -0
  107. data/lib/ruby_llm/agents/core/base.rb +135 -0
  108. data/lib/ruby_llm/agents/core/configuration.rb +981 -0
  109. data/lib/ruby_llm/agents/core/errors.rb +150 -0
  110. data/lib/ruby_llm/agents/{instrumentation.rb → core/instrumentation.rb} +93 -4
  111. data/lib/ruby_llm/agents/core/llm_tenant.rb +358 -0
  112. data/lib/ruby_llm/agents/core/resolved_config.rb +348 -0
  113. data/lib/ruby_llm/agents/{version.rb → core/version.rb} +1 -1
  114. data/lib/ruby_llm/agents/dsl/base.rb +110 -0
  115. data/lib/ruby_llm/agents/dsl/caching.rb +142 -0
  116. data/lib/ruby_llm/agents/dsl/reliability.rb +307 -0
  117. data/lib/ruby_llm/agents/dsl.rb +41 -0
  118. data/lib/ruby_llm/agents/image/analyzer/dsl.rb +130 -0
  119. data/lib/ruby_llm/agents/image/analyzer/execution.rb +402 -0
  120. data/lib/ruby_llm/agents/image/analyzer.rb +90 -0
  121. data/lib/ruby_llm/agents/image/background_remover/dsl.rb +154 -0
  122. data/lib/ruby_llm/agents/image/background_remover/execution.rb +240 -0
  123. data/lib/ruby_llm/agents/image/background_remover.rb +89 -0
  124. data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +91 -0
  125. data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +165 -0
  126. data/lib/ruby_llm/agents/image/editor/dsl.rb +56 -0
  127. data/lib/ruby_llm/agents/image/editor/execution.rb +207 -0
  128. data/lib/ruby_llm/agents/image/editor.rb +92 -0
  129. data/lib/ruby_llm/agents/image/generator/active_storage_support.rb +127 -0
  130. data/lib/ruby_llm/agents/image/generator/content_policy.rb +95 -0
  131. data/lib/ruby_llm/agents/image/generator/pricing.rb +353 -0
  132. data/lib/ruby_llm/agents/image/generator/templates.rb +124 -0
  133. data/lib/ruby_llm/agents/image/generator.rb +455 -0
  134. data/lib/ruby_llm/agents/image/pipeline/dsl.rb +213 -0
  135. data/lib/ruby_llm/agents/image/pipeline/execution.rb +382 -0
  136. data/lib/ruby_llm/agents/image/pipeline.rb +97 -0
  137. data/lib/ruby_llm/agents/image/transformer/dsl.rb +148 -0
  138. data/lib/ruby_llm/agents/image/transformer/execution.rb +223 -0
  139. data/lib/ruby_llm/agents/image/transformer.rb +95 -0
  140. data/lib/ruby_llm/agents/image/upscaler/dsl.rb +83 -0
  141. data/lib/ruby_llm/agents/image/upscaler/execution.rb +219 -0
  142. data/lib/ruby_llm/agents/image/upscaler.rb +81 -0
  143. data/lib/ruby_llm/agents/image/variator/dsl.rb +62 -0
  144. data/lib/ruby_llm/agents/image/variator/execution.rb +189 -0
  145. data/lib/ruby_llm/agents/image/variator.rb +80 -0
  146. data/lib/ruby_llm/agents/{alert_manager.rb → infrastructure/alert_manager.rb} +17 -22
  147. data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +145 -0
  148. data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +149 -0
  149. data/lib/ruby_llm/agents/infrastructure/budget/forecaster.rb +68 -0
  150. data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +279 -0
  151. data/lib/ruby_llm/agents/infrastructure/budget_tracker.rb +275 -0
  152. data/lib/ruby_llm/agents/{execution_logger_job.rb → infrastructure/execution_logger_job.rb} +17 -1
  153. data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/executor.rb +2 -1
  154. data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/retry_strategy.rb +9 -3
  155. data/lib/ruby_llm/agents/{reliability.rb → infrastructure/reliability.rb} +11 -21
  156. data/lib/ruby_llm/agents/pipeline/builder.rb +215 -0
  157. data/lib/ruby_llm/agents/pipeline/context.rb +255 -0
  158. data/lib/ruby_llm/agents/pipeline/executor.rb +86 -0
  159. data/lib/ruby_llm/agents/pipeline/middleware/base.rb +124 -0
  160. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +95 -0
  161. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +171 -0
  162. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +415 -0
  163. data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +276 -0
  164. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +196 -0
  165. data/lib/ruby_llm/agents/pipeline.rb +68 -0
  166. data/lib/ruby_llm/agents/{engine.rb → rails/engine.rb} +79 -10
  167. data/lib/ruby_llm/agents/results/background_removal_result.rb +286 -0
  168. data/lib/ruby_llm/agents/{result.rb → results/base.rb} +73 -1
  169. data/lib/ruby_llm/agents/results/embedding_result.rb +243 -0
  170. data/lib/ruby_llm/agents/results/image_analysis_result.rb +314 -0
  171. data/lib/ruby_llm/agents/results/image_edit_result.rb +250 -0
  172. data/lib/ruby_llm/agents/results/image_generation_result.rb +346 -0
  173. data/lib/ruby_llm/agents/results/image_pipeline_result.rb +399 -0
  174. data/lib/ruby_llm/agents/results/image_transform_result.rb +251 -0
  175. data/lib/ruby_llm/agents/results/image_upscale_result.rb +255 -0
  176. data/lib/ruby_llm/agents/results/image_variation_result.rb +237 -0
  177. data/lib/ruby_llm/agents/results/moderation_result.rb +158 -0
  178. data/lib/ruby_llm/agents/results/speech_result.rb +338 -0
  179. data/lib/ruby_llm/agents/results/transcription_result.rb +408 -0
  180. data/lib/ruby_llm/agents/text/embedder.rb +444 -0
  181. data/lib/ruby_llm/agents/text/moderator.rb +237 -0
  182. data/lib/ruby_llm/agents/workflow/async.rb +220 -0
  183. data/lib/ruby_llm/agents/workflow/async_executor.rb +156 -0
  184. data/lib/ruby_llm/agents/{workflow.rb → workflow/orchestrator.rb} +6 -5
  185. data/lib/ruby_llm/agents/workflow/parallel.rb +34 -17
  186. data/lib/ruby_llm/agents/workflow/thread_pool.rb +185 -0
  187. data/lib/ruby_llm/agents.rb +86 -20
  188. metadata +189 -35
  189. data/lib/ruby_llm/agents/base/caching.rb +0 -40
  190. data/lib/ruby_llm/agents/base/cost_calculation.rb +0 -105
  191. data/lib/ruby_llm/agents/base/dsl.rb +0 -324
  192. data/lib/ruby_llm/agents/base/execution.rb +0 -283
  193. data/lib/ruby_llm/agents/base/reliability_dsl.rb +0 -82
  194. data/lib/ruby_llm/agents/base/reliability_execution.rb +0 -136
  195. data/lib/ruby_llm/agents/base/response_building.rb +0 -86
  196. data/lib/ruby_llm/agents/base/tool_tracking.rb +0 -57
  197. data/lib/ruby_llm/agents/base.rb +0 -209
  198. data/lib/ruby_llm/agents/budget_tracker.rb +0 -471
  199. data/lib/ruby_llm/agents/configuration.rb +0 -357
  200. /data/lib/ruby_llm/agents/{deprecations.rb → core/deprecations.rb} +0 -0
  201. /data/lib/ruby_llm/agents/{inflections.rb → core/inflections.rb} +0 -0
  202. /data/lib/ruby_llm/agents/{attempt_tracker.rb → infrastructure/attempt_tracker.rb} +0 -0
  203. /data/lib/ruby_llm/agents/{cache_helper.rb → infrastructure/cache_helper.rb} +0 -0
  204. /data/lib/ruby_llm/agents/{circuit_breaker.rb → infrastructure/circuit_breaker.rb} +0 -0
  205. /data/lib/ruby_llm/agents/{redactor.rb → infrastructure/redactor.rb} +0 -0
  206. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/breaker_manager.rb +0 -0
  207. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/execution_constraints.rb +0 -0
  208. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/fallback_routing.rb +0 -0
@@ -0,0 +1,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ # Controller for managing API configurations
6
+ #
7
+ # Provides CRUD operations for global and tenant-specific API
8
+ # configurations, including API keys and connection settings.
9
+ #
10
+ # @see ApiConfiguration
11
+ # @api private
12
+ class ApiConfigurationsController < ApplicationController
13
+ before_action :ensure_table_exists
14
+ before_action :set_global_config, only: [:show, :edit, :update]
15
+ before_action :set_tenant_config, only: [:tenant, :edit_tenant, :update_tenant]
16
+
17
+ # Displays the global API configuration
18
+ #
19
+ # @return [void]
20
+ def show
21
+ @resolved = ApiConfiguration.resolve
22
+ @provider_statuses = @resolved.provider_statuses_with_source
23
+ end
24
+
25
+ # Renders the edit form for global configuration
26
+ #
27
+ # @return [void]
28
+ def edit
29
+ @resolved = ApiConfiguration.resolve
30
+ end
31
+
32
+ # Updates the global API configuration
33
+ #
34
+ # @return [void]
35
+ def update
36
+ if @config.update(api_configuration_params)
37
+ log_configuration_change(@config, "global")
38
+ redirect_to edit_api_configuration_path, notice: "API configuration updated successfully"
39
+ else
40
+ render :edit, status: :unprocessable_entity
41
+ end
42
+ end
43
+
44
+ # Displays tenant-specific API configuration
45
+ #
46
+ # @return [void]
47
+ def tenant
48
+ @resolved = ApiConfiguration.resolve(tenant_id: params[:tenant_id])
49
+ @provider_statuses = @resolved.provider_statuses_with_source
50
+ @tenant_budget = TenantBudget.for_tenant(params[:tenant_id])
51
+ end
52
+
53
+ # Renders the edit form for tenant configuration
54
+ #
55
+ # @return [void]
56
+ def edit_tenant
57
+ @resolved = ApiConfiguration.resolve(tenant_id: @tenant_id)
58
+ end
59
+
60
+ # Updates a tenant-specific API configuration
61
+ #
62
+ # @return [void]
63
+ def update_tenant
64
+ if @config.update(api_configuration_params)
65
+ log_configuration_change(@config, "tenant:#{params[:tenant_id]}")
66
+ redirect_to edit_tenant_api_configuration_path(params[:tenant_id]),
67
+ notice: "Tenant API configuration updated successfully"
68
+ else
69
+ render :edit_tenant, status: :unprocessable_entity
70
+ end
71
+ end
72
+
73
+ # Tests API key validity for a specific provider
74
+ # (Optional - can be used for AJAX validation)
75
+ #
76
+ # @return [void]
77
+ def test_connection
78
+ provider = params[:provider]
79
+ api_key = params[:api_key]
80
+
81
+ result = test_provider_connection(provider, api_key)
82
+
83
+ render json: {
84
+ success: result[:success],
85
+ message: result[:message],
86
+ models: result[:models]
87
+ }
88
+ rescue StandardError => e
89
+ render json: { success: false, message: e.message }
90
+ end
91
+
92
+ private
93
+
94
+ # Ensures the api_configurations table exists
95
+ def ensure_table_exists
96
+ return if ApiConfiguration.table_exists?
97
+
98
+ flash[:alert] = "API configurations table not found. Run the generator: rails generate ruby_llm_agents:api_configuration"
99
+ redirect_to root_path
100
+ end
101
+
102
+ # Sets the global configuration (creates if not exists)
103
+ def set_global_config
104
+ @config = ApiConfiguration.global
105
+ end
106
+
107
+ # Sets the tenant-specific configuration (creates if not exists)
108
+ def set_tenant_config
109
+ @tenant_id = params[:tenant_id]
110
+ raise ActionController::RoutingError, "Tenant ID required" if @tenant_id.blank?
111
+
112
+ @config = ApiConfiguration.for_tenant!(@tenant_id)
113
+ end
114
+
115
+ # Strong parameters for API configuration
116
+ #
117
+ # @return [ActionController::Parameters]
118
+ def api_configuration_params
119
+ params.require(:api_configuration).permit(
120
+ # API Keys
121
+ :openai_api_key,
122
+ :anthropic_api_key,
123
+ :gemini_api_key,
124
+ :deepseek_api_key,
125
+ :mistral_api_key,
126
+ :perplexity_api_key,
127
+ :openrouter_api_key,
128
+ :gpustack_api_key,
129
+ :xai_api_key,
130
+ :ollama_api_key,
131
+ # AWS Bedrock
132
+ :bedrock_api_key,
133
+ :bedrock_secret_key,
134
+ :bedrock_session_token,
135
+ :bedrock_region,
136
+ # Vertex AI
137
+ :vertexai_credentials,
138
+ :vertexai_project_id,
139
+ :vertexai_location,
140
+ # Endpoints
141
+ :openai_api_base,
142
+ :gemini_api_base,
143
+ :ollama_api_base,
144
+ :gpustack_api_base,
145
+ :xai_api_base,
146
+ # OpenAI Options
147
+ :openai_organization_id,
148
+ :openai_project_id,
149
+ # Default Models
150
+ :default_model,
151
+ :default_embedding_model,
152
+ :default_image_model,
153
+ :default_moderation_model,
154
+ # Connection Settings
155
+ :request_timeout,
156
+ :max_retries,
157
+ :retry_interval,
158
+ :retry_backoff_factor,
159
+ :retry_interval_randomness,
160
+ :http_proxy,
161
+ # Inheritance
162
+ :inherit_global_defaults
163
+ ).tap do |permitted|
164
+ # Remove blank API keys to prevent overwriting with empty values
165
+ # This allows users to submit forms without touching existing keys
166
+ ApiConfiguration::API_KEY_ATTRIBUTES.each do |key|
167
+ permitted.delete(key) if permitted[key].blank?
168
+ end
169
+ end
170
+ end
171
+
172
+ # Logs configuration changes for audit purposes
173
+ #
174
+ # @param config [ApiConfiguration] The configuration that changed
175
+ # @param scope [String] The scope identifier
176
+ def log_configuration_change(config, scope)
177
+ changed_fields = config.previous_changes.keys.reject { |k| k.end_with?("_at") }
178
+ return if changed_fields.empty?
179
+
180
+ # Mask sensitive fields in the log
181
+ masked_changes = changed_fields.map do |field|
182
+ if field.include?("api_key") || field.include?("secret") || field.include?("credentials")
183
+ "#{field}: [REDACTED]"
184
+ else
185
+ "#{field}: #{config.previous_changes[field].last}"
186
+ end
187
+ end
188
+
189
+ Rails.logger.info(
190
+ "[RubyLLM::Agents] API configuration updated for #{scope}: #{masked_changes.join(', ')}"
191
+ )
192
+ end
193
+
194
+ # Tests connection to a specific provider
195
+ #
196
+ # @param provider [String] Provider key
197
+ # @param api_key [String] API key to test
198
+ # @return [Hash] Test result with success, message, and models
199
+ def test_provider_connection(provider, api_key)
200
+ # This is a placeholder - actual implementation would depend on
201
+ # RubyLLM's ability to list models or make a test request
202
+ case provider
203
+ when "openai"
204
+ # Example: Try to list models
205
+ { success: true, message: "Connection successful", models: [] }
206
+ when "anthropic"
207
+ { success: true, message: "Connection successful", models: [] }
208
+ else
209
+ { success: false, message: "Provider not supported for testing" }
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
@@ -53,10 +53,19 @@ module RubyLLM
53
53
  end
54
54
  end
55
55
 
56
- # Builds per-agent comparison statistics
56
+ # Builds per-agent comparison statistics for all agent types
57
+ #
58
+ # Creates separate instance variables for each agent type:
59
+ # - @agent_stats: Base agents
60
+ # - @embedder_stats: Embedders
61
+ # - @transcriber_stats: Transcribers
62
+ # - @speaker_stats: Speakers
63
+ # - @image_generator_stats: Image generators
64
+ # - @moderator_stats: Moderators
65
+ # - @workflow_stats: Workflows
57
66
  #
58
67
  # @param base_scope [ActiveRecord::Relation] Base scope to filter from
59
- # @return [Array<Hash>] Array of all stats sorted by cost descending
68
+ # @return [Array<Hash>] Array of base agent stats (for backward compatibility)
60
69
  def build_agent_comparison(base_scope = Execution)
61
70
  scope = base_scope.last_n_days(@days)
62
71
  agent_types = scope.distinct.pluck(:agent_type)
@@ -67,26 +76,37 @@ module RubyLLM
67
76
  total_cost = agent_scope.sum(:total_cost) || 0
68
77
  successful = agent_scope.successful.count
69
78
 
70
- # Detect if this is a workflow
79
+ # Detect agent type using AgentRegistry
71
80
  agent_class = AgentRegistry.find(agent_type)
72
- is_workflow = agent_class&.ancestors&.any? { |a| a.name&.include?("Workflow") }
73
- workflow_type = is_workflow ? detect_workflow_type(agent_class) : nil
81
+ detected_type = AgentRegistry.send(:detect_agent_type, agent_class)
82
+
83
+ # Get workflow type if applicable
84
+ workflow_type = detected_type == "workflow" ? detect_workflow_type(agent_class) : nil
74
85
 
75
86
  {
76
87
  agent_type: agent_type,
88
+ detected_type: detected_type,
77
89
  executions: count,
78
90
  total_cost: total_cost,
79
91
  avg_cost: count > 0 ? (total_cost / count).round(6) : 0,
80
92
  avg_duration_ms: agent_scope.average(:duration_ms)&.round || 0,
81
93
  success_rate: count > 0 ? (successful.to_f / count * 100).round(1) : 0,
82
- is_workflow: is_workflow,
94
+ is_workflow: detected_type == "workflow",
83
95
  workflow_type: workflow_type
84
96
  }
85
97
  end.sort_by { |a| -(a[:total_cost] || 0) }
86
98
 
87
- # Split into agents and workflows for tabbed display
88
- @workflow_stats = all_stats.select { |a| a[:is_workflow] }
89
- all_stats.reject { |a| a[:is_workflow] }
99
+ # Split stats by agent type for 7-tab display
100
+ @agent_stats = all_stats.select { |a| a[:detected_type] == "agent" }
101
+ @embedder_stats = all_stats.select { |a| a[:detected_type] == "embedder" }
102
+ @transcriber_stats = all_stats.select { |a| a[:detected_type] == "transcriber" }
103
+ @speaker_stats = all_stats.select { |a| a[:detected_type] == "speaker" }
104
+ @image_generator_stats = all_stats.select { |a| a[:detected_type] == "image_generator" }
105
+ @moderator_stats = all_stats.select { |a| a[:detected_type] == "moderator" }
106
+ @workflow_stats = all_stats.select { |a| a[:detected_type] == "workflow" }
107
+
108
+ # Return base agents for backward compatibility
109
+ @agent_stats
90
110
  end
91
111
 
92
112
  # Detects workflow type from class hierarchy
@@ -2,14 +2,14 @@
2
2
 
3
3
  module RubyLLM
4
4
  module Agents
5
- # Controller for displaying global configuration settings
5
+ # Controller for displaying system configuration
6
6
  #
7
7
  # Shows all configuration options from RubyLLM::Agents.configuration
8
8
  # in a read-only dashboard view.
9
9
  #
10
10
  # @api private
11
- class SettingsController < ApplicationController
12
- # Displays the global configuration settings
11
+ class SystemConfigController < ApplicationController
12
+ # Displays the system configuration
13
13
  #
14
14
  # @return [void]
15
15
  def show
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ # Controller for managing tenant budgets
6
+ #
7
+ # Provides CRUD operations for viewing and editing tenant budget
8
+ # configurations, including cost limits and token limits.
9
+ #
10
+ # @see TenantBudget For budget configuration model
11
+ # @api private
12
+ class TenantsController < ApplicationController
13
+ # Lists all tenant budgets
14
+ #
15
+ # @return [void]
16
+ def index
17
+ @tenants = TenantBudget.order(:name, :tenant_id)
18
+ end
19
+
20
+ # Shows a single tenant's budget details
21
+ #
22
+ # @return [void]
23
+ def show
24
+ @tenant = TenantBudget.find(params[:id])
25
+ @executions = tenant_executions(@tenant.tenant_id).recent.limit(10)
26
+ @usage_stats = calculate_usage_stats(@tenant)
27
+ end
28
+
29
+ # Renders the edit form for a tenant budget
30
+ #
31
+ # @return [void]
32
+ def edit
33
+ @tenant = TenantBudget.find(params[:id])
34
+ end
35
+
36
+ # Updates a tenant budget
37
+ #
38
+ # @return [void]
39
+ def update
40
+ @tenant = TenantBudget.find(params[:id])
41
+ if @tenant.update(tenant_params)
42
+ redirect_to tenant_path(@tenant), notice: "Tenant updated successfully"
43
+ else
44
+ render :edit, status: :unprocessable_entity
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ # Strong parameters for tenant budget
51
+ #
52
+ # @return [ActionController::Parameters] Permitted parameters
53
+ def tenant_params
54
+ params.require(:tenant_budget).permit(
55
+ :name, :daily_limit, :monthly_limit,
56
+ :daily_token_limit, :monthly_token_limit,
57
+ :enforcement
58
+ )
59
+ end
60
+
61
+ # Returns executions scoped to a specific tenant
62
+ #
63
+ # @param tenant_id [String] The tenant identifier
64
+ # @return [ActiveRecord::Relation] Executions for the tenant
65
+ def tenant_executions(tenant_id)
66
+ Execution.by_tenant(tenant_id)
67
+ end
68
+
69
+ # Calculates usage statistics for a tenant
70
+ #
71
+ # @param tenant [TenantBudget] The tenant budget record
72
+ # @return [Hash] Usage statistics
73
+ def calculate_usage_stats(tenant)
74
+ scope = tenant_executions(tenant.tenant_id)
75
+ today_scope = scope.where("created_at >= ?", Time.current.beginning_of_day)
76
+ month_scope = scope.where("created_at >= ?", Time.current.beginning_of_month)
77
+
78
+ daily_spend = today_scope.sum(:total_cost) || 0
79
+ monthly_spend = month_scope.sum(:total_cost) || 0
80
+ daily_tokens = today_scope.sum(:total_tokens) || 0
81
+ monthly_tokens = month_scope.sum(:total_tokens) || 0
82
+
83
+ {
84
+ daily_spend: daily_spend,
85
+ monthly_spend: monthly_spend,
86
+ daily_tokens: daily_tokens,
87
+ monthly_tokens: monthly_tokens,
88
+ daily_spend_percentage: percentage_used(daily_spend, tenant.effective_daily_limit),
89
+ monthly_spend_percentage: percentage_used(monthly_spend, tenant.effective_monthly_limit),
90
+ daily_token_percentage: percentage_used(daily_tokens, tenant.effective_daily_token_limit),
91
+ monthly_token_percentage: percentage_used(monthly_tokens, tenant.effective_monthly_token_limit),
92
+ total_executions: scope.count,
93
+ total_cost: scope.sum(:total_cost) || 0,
94
+ total_tokens: scope.sum(:total_tokens) || 0
95
+ }
96
+ end
97
+
98
+ # Calculates percentage used
99
+ #
100
+ # @param current [Numeric] Current usage
101
+ # @param limit [Numeric, nil] The limit
102
+ # @return [Float] Percentage used (0-100+)
103
+ def percentage_used(current, limit)
104
+ return 0 if limit.nil? || limit.to_f <= 0
105
+ (current.to_f / limit.to_f * 100).round(1)
106
+ end
107
+ end
108
+ end
109
+ end