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,442 @@
1
+ <%= render "ruby_llm/agents/shared/breadcrumbs", items: [
2
+ { label: "Dashboard", path: ruby_llm_agents.root_path },
3
+ { label: "Workflows", path: ruby_llm_agents.agents_path(tab: "workflows") },
4
+ { label: @workflow_type.gsub(/Workflow$|Pipeline$|Parallel$|Router$/, '') }
5
+ ] %>
6
+
7
+ <!-- Header -->
8
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
9
+ <div class="flex items-start justify-between">
10
+ <div>
11
+ <div class="flex items-center space-x-3">
12
+ <%= render "ruby_llm/agents/shared/workflow_type_badge", workflow_type: @workflow_type_kind, size: :md %>
13
+ <h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">
14
+ <% name_parts = @workflow_type.gsub(/Workflow$|Pipeline$|Parallel$|Router$/, '').split('::') %>
15
+ <% if name_parts.length > 1 %>
16
+ <span class="text-gray-400 dark:text-gray-500 font-normal"><%= name_parts[0..-2].join('::') %>::</span>
17
+ <% end %>
18
+ <%= name_parts.last %>
19
+ </h1>
20
+
21
+ <% if @workflow_active %>
22
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-300">
23
+ Active
24
+ </span>
25
+ <% else %>
26
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400">
27
+ Deleted
28
+ </span>
29
+ <% end %>
30
+
31
+ <% if @config %>
32
+ <span class="text-sm text-gray-500 dark:text-gray-400">
33
+ v<%= @config[:version] %>
34
+ </span>
35
+ <% end %>
36
+ </div>
37
+
38
+ <% if @config && @config[:description].present? %>
39
+ <p class="text-gray-600 dark:text-gray-300 mt-2 max-w-2xl">
40
+ <%= @config[:description] %>
41
+ </p>
42
+ <% end %>
43
+
44
+ <!-- Workflow Summary Line -->
45
+ <div class="flex items-center gap-4 mt-3 text-sm text-gray-500 dark:text-gray-400">
46
+ <% if @workflow_type_kind == "pipeline" && @steps.present? %>
47
+ <span><%= @steps.size %> steps</span>
48
+ <% elsif @workflow_type_kind == "parallel" && @branches.present? %>
49
+ <span><%= @branches.size %> branches</span>
50
+ <% elsif @workflow_type_kind == "router" && @routes.present? %>
51
+ <span><%= @routes.size %> routes</span>
52
+ <% end %>
53
+
54
+ <% if @config && @config[:timeout] %>
55
+ <span class="text-gray-300 dark:text-gray-600">|</span>
56
+ <span>timeout <%= @config[:timeout] %>s</span>
57
+ <% end %>
58
+
59
+ <% if @config && @config[:max_cost] %>
60
+ <span class="text-gray-300 dark:text-gray-600">|</span>
61
+ <span>max cost $<%= @config[:max_cost] %></span>
62
+ <% end %>
63
+
64
+ <% if @workflow_type_kind == "router" && @config && @config[:classifier_model] %>
65
+ <span class="text-gray-300 dark:text-gray-600">|</span>
66
+ <span>classifier: <%= @config[:classifier_model] %></span>
67
+ <% end %>
68
+ </div>
69
+ </div>
70
+
71
+ <div class="text-right">
72
+ <p class="text-sm text-gray-500 dark:text-gray-400">
73
+ <%= number_with_delimiter(@stats[:count]) %> total executions
74
+ </p>
75
+
76
+ <div class="flex items-center justify-end gap-3 mt-1">
77
+ <% status_colors = {
78
+ "success" => "bg-green-500",
79
+ "error" => "bg-red-500",
80
+ "timeout" => "bg-yellow-500",
81
+ "running" => "bg-blue-500"
82
+ } %>
83
+
84
+ <% @status_distribution.each do |status, count| %>
85
+ <div class="flex items-center gap-1">
86
+ <span class="w-2 h-2 rounded-full <%= status_colors[status] || 'bg-gray-400' %> <%= status == 'running' ? 'animate-pulse' : '' %>"></span>
87
+ <span class="text-xs text-gray-600 dark:text-gray-400">
88
+ <%= number_with_delimiter(count) %>
89
+ </span>
90
+ </div>
91
+ <% end %>
92
+ </div>
93
+ </div>
94
+ </div>
95
+ </div>
96
+
97
+ <!-- Workflow Structure -->
98
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
99
+ <div class="flex items-center justify-between mb-4">
100
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">
101
+ Workflow Structure
102
+ </h3>
103
+
104
+ <% if @workflow_type_kind == "parallel" && @config %>
105
+ <span class="text-sm text-gray-500 dark:text-gray-400">
106
+ fail_fast: <%= @config[:fail_fast] ? 'on' : 'off' %>
107
+ </span>
108
+ <% elsif @workflow_type_kind == "router" && @config && @config[:classifier_model] %>
109
+ <span class="text-sm text-gray-500 dark:text-gray-400">
110
+ classifier: <%= @config[:classifier_model] %> @ <%= @config[:classifier_temperature] || 0.0 %>
111
+ </span>
112
+ <% end %>
113
+ </div>
114
+
115
+ <% case @workflow_type_kind %>
116
+ <% when "pipeline" %>
117
+ <%= render "ruby_llm/agents/workflows/structure_pipeline", steps: @steps || [] %>
118
+ <% when "parallel" %>
119
+ <%= render "ruby_llm/agents/workflows/structure_parallel", branches: @branches || [] %>
120
+ <% when "router" %>
121
+ <%= render "ruby_llm/agents/workflows/structure_router", routes: @routes || [] %>
122
+ <% else %>
123
+ <p class="text-gray-500 dark:text-gray-400 italic">
124
+ Workflow structure unavailable
125
+ </p>
126
+ <% end %>
127
+ </div>
128
+
129
+ <!-- Stats Grid -->
130
+ <% success_rate = @stats[:success_rate] || 0 %>
131
+ <% success_rate_color = success_rate >= 95 ? 'text-green-600' : success_rate >= 80 ? 'text-yellow-600' : 'text-red-600' %>
132
+
133
+ <div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4 mb-6">
134
+ <%= render "ruby_llm/agents/shared/stat_card",
135
+ title: "Executions",
136
+ value: number_with_delimiter(@stats[:count]),
137
+ subtitle: "Today: #{@stats_today[:count]}",
138
+ icon: "M13 10V3L4 14h7v7l9-11h-7z",
139
+ icon_color: "text-blue-500" %>
140
+
141
+ <%= render "ruby_llm/agents/shared/stat_card",
142
+ title: "Success Rate",
143
+ value: "#{success_rate}%",
144
+ subtitle: "Error rate: #{@stats[:error_rate] || 0}%",
145
+ icon: "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z",
146
+ icon_color: "text-green-500",
147
+ value_color: success_rate_color %>
148
+
149
+ <%= render "ruby_llm/agents/shared/stat_card",
150
+ title: "Total Cost",
151
+ value: "$#{number_with_precision(@stats[:total_cost] || 0, precision: 4)}",
152
+ subtitle: "Avg: $#{number_with_precision(@stats[:avg_cost] || 0, precision: 6)}",
153
+ icon: "M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z",
154
+ icon_color: "text-amber-500" %>
155
+
156
+ <%= render "ruby_llm/agents/shared/stat_card",
157
+ title: "Total Tokens",
158
+ value: number_with_delimiter(@stats[:total_tokens] || 0),
159
+ subtitle: "Avg: #{number_with_delimiter(@stats[:avg_tokens] || 0)}",
160
+ icon: "M7 20l4-16m2 16l4-16M6 9h14M4 15h14",
161
+ icon_color: "text-indigo-500" %>
162
+
163
+ <%= render "ruby_llm/agents/shared/stat_card",
164
+ title: "Avg Duration",
165
+ value: "#{number_with_delimiter(@stats[:avg_duration_ms] || 0)} ms",
166
+ icon: "M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z",
167
+ icon_color: "text-purple-500" %>
168
+ </div>
169
+
170
+ <!-- Step/Branch/Route Performance -->
171
+ <% if @step_stats.present? %>
172
+ <%= render "ruby_llm/agents/workflows/step_performance",
173
+ workflow_type: @workflow_type_kind,
174
+ step_stats: @step_stats,
175
+ route_distribution: @route_distribution %>
176
+ <% end %>
177
+
178
+ <!-- Charts Section -->
179
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
180
+ <!-- Executions Over Time -->
181
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 overflow-hidden">
182
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
183
+ Executions (30 days)
184
+ </h3>
185
+ <div id="executions-chart" style="height: 220px;"></div>
186
+ </div>
187
+
188
+ <!-- Cost Over Time -->
189
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 overflow-hidden">
190
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
191
+ Cost (30 days)
192
+ </h3>
193
+ <div id="cost-chart" style="height: 220px;"></div>
194
+ </div>
195
+ </div>
196
+
197
+ <script>
198
+ (function() {
199
+ const trendData = <%= raw @trend_data.to_json %>;
200
+ const categories = trendData.map(d => d.date.split('-').slice(1).join('/'));
201
+
202
+ // Executions chart
203
+ Highcharts.chart('executions-chart', {
204
+ chart: { type: 'areaspline', backgroundColor: 'transparent' },
205
+ xAxis: { categories: categories, labels: { style: { color: '#9CA3AF' } } },
206
+ yAxis: { min: 0, title: { text: null }, labels: { style: { color: '#9CA3AF' } } },
207
+ legend: { enabled: false },
208
+ plotOptions: { areaspline: { fillOpacity: 0.2, marker: { enabled: false } } },
209
+ series: [
210
+ { name: 'Success', color: '#10B981', data: trendData.map(d => d.count - (d.error_count || 0)) },
211
+ { name: 'Failed', color: '#EF4444', data: trendData.map(d => d.error_count || 0) }
212
+ ]
213
+ });
214
+
215
+ // Cost chart
216
+ Highcharts.chart('cost-chart', {
217
+ chart: { type: 'spline', backgroundColor: 'transparent' },
218
+ xAxis: { categories: categories, labels: { style: { color: '#9CA3AF' } } },
219
+ yAxis: { min: 0, title: { text: null }, labels: { style: { color: '#9CA3AF' }, format: '${value}' } },
220
+ legend: { enabled: false },
221
+ plotOptions: { spline: { marker: { enabled: false } } },
222
+ tooltip: { valuePrefix: '$' },
223
+ series: [{ name: 'Cost', color: '#10B981', data: trendData.map(d => parseFloat(d.total_cost) || 0) }]
224
+ });
225
+ })();
226
+ </script>
227
+
228
+ <!-- Finish Reason Distribution -->
229
+ <% if @finish_reason_distribution.present? && @finish_reason_distribution.any? %>
230
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4 mb-6">
231
+ <div class="flex items-center justify-between">
232
+ <p class="text-sm text-gray-500 dark:text-gray-400 uppercase">
233
+ Finish Reasons
234
+ </p>
235
+
236
+ <div class="flex flex-wrap gap-4">
237
+ <% finish_colors = {
238
+ 'stop' => '#10B981',
239
+ 'length' => '#F59E0B',
240
+ 'content_filter' => '#EF4444',
241
+ 'tool_calls' => '#3B82F6',
242
+ nil => '#6B7280'
243
+ } %>
244
+
245
+ <% @finish_reason_distribution.each do |reason, count| %>
246
+ <div class="flex items-center">
247
+ <span class="w-2 h-2 rounded-full mr-1.5" style="background-color: <%= finish_colors[reason] || '#6B7280' %>"></span>
248
+ <span class="text-sm text-gray-700 dark:text-gray-300">
249
+ <%= reason || 'unknown' %>
250
+ </span>
251
+ <span class="text-sm font-medium text-gray-900 dark:text-gray-100 ml-1">
252
+ (<%= number_with_delimiter(count) %>)
253
+ </span>
254
+ </div>
255
+ <% end %>
256
+ </div>
257
+ </div>
258
+ </div>
259
+ <% end %>
260
+
261
+ <% if @config %>
262
+ <!-- Configuration -->
263
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
264
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
265
+ Configuration
266
+ </h3>
267
+
268
+ <!-- Basic Configuration -->
269
+ <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-3">
270
+ Basic
271
+ </p>
272
+
273
+ <div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
274
+ <div>
275
+ <p class="text-sm text-gray-500 dark:text-gray-400">Version</p>
276
+ <p class="font-medium text-gray-900 dark:text-gray-100">
277
+ <%= @config[:version] || 'N/A' %>
278
+ </p>
279
+ </div>
280
+
281
+ <div>
282
+ <p class="text-sm text-gray-500 dark:text-gray-400">Timeout</p>
283
+ <p class="font-medium text-gray-900 dark:text-gray-100">
284
+ <%= @config[:timeout] ? "#{@config[:timeout]}s" : 'N/A' %>
285
+ </p>
286
+ </div>
287
+
288
+ <div>
289
+ <p class="text-sm text-gray-500 dark:text-gray-400">Max Cost</p>
290
+ <p class="font-medium text-gray-900 dark:text-gray-100">
291
+ <%= @config[:max_cost] ? "$#{@config[:max_cost]}" : 'N/A' %>
292
+ </p>
293
+ </div>
294
+
295
+ <% if @workflow_type_kind == "parallel" %>
296
+ <div>
297
+ <p class="text-sm text-gray-500 dark:text-gray-400">Fail Fast</p>
298
+ <p class="font-medium text-gray-900 dark:text-gray-100">
299
+ <%= @config[:fail_fast] ? 'Yes' : 'No' %>
300
+ </p>
301
+ </div>
302
+ <% end %>
303
+ </div>
304
+
305
+ <% if @workflow_type_kind == "router" && @config[:classifier_model] %>
306
+ <!-- Router Settings -->
307
+ <div class="border-t border-gray-100 dark:border-gray-700 pt-4">
308
+ <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-3">
309
+ Router Settings
310
+ </p>
311
+
312
+ <div class="grid grid-cols-2 md:grid-cols-4 gap-4">
313
+ <div>
314
+ <p class="text-sm text-gray-500 dark:text-gray-400">Classifier Model</p>
315
+ <p class="font-medium text-gray-900 dark:text-gray-100">
316
+ <%= @config[:classifier_model] %>
317
+ </p>
318
+ </div>
319
+
320
+ <div>
321
+ <p class="text-sm text-gray-500 dark:text-gray-400">Temperature</p>
322
+ <p class="font-medium text-gray-900 dark:text-gray-100">
323
+ <%= @config[:classifier_temperature] || 0.0 %>
324
+ </p>
325
+ </div>
326
+ </div>
327
+ </div>
328
+ <% end %>
329
+ </div>
330
+ <% end %>
331
+
332
+ <!-- Executions -->
333
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
334
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
335
+ Executions
336
+ </h3>
337
+
338
+ <div id="executions_table">
339
+ <%
340
+ has_filters = params[:statuses].present? || params[:versions].present? || params[:models].present? || params[:temperatures].present? || params[:days].present?
341
+ selected_statuses = params[:statuses].present? ? (params[:statuses].is_a?(Array) ? params[:statuses] : params[:statuses].split(",")) : []
342
+ selected_versions = params[:versions].present? ? (params[:versions].is_a?(Array) ? params[:versions] : params[:versions].split(",")) : []
343
+ selected_models = params[:models].present? ? (params[:models].is_a?(Array) ? params[:models] : params[:models].split(",")) : []
344
+ selected_temperatures = params[:temperatures].present? ? (params[:temperatures].is_a?(Array) ? params[:temperatures] : params[:temperatures].split(",")).map(&:to_s) : []
345
+
346
+ status_options = [
347
+ { value: "success", label: "Success", color: "bg-green-500" },
348
+ { value: "error", label: "Error", color: "bg-red-500" },
349
+ { value: "running", label: "Running", color: "bg-blue-500" },
350
+ { value: "timeout", label: "Timeout", color: "bg-yellow-500" }
351
+ ]
352
+ version_options = @versions.map { |v| { value: v.to_s, label: "v#{v}" } }
353
+ model_options = @models.map { |m| { value: m, label: m } }
354
+ temperature_options = @temperatures.map { |t| { value: t.to_s, label: t.to_s } }
355
+ days_options = [
356
+ { value: "", label: "All Time" },
357
+ { value: "1", label: "Today" },
358
+ { value: "7", label: "Last 7 Days" },
359
+ { value: "30", label: "Last 30 Days" }
360
+ ]
361
+ %>
362
+
363
+ <%= form_with url: ruby_llm_agents.workflow_path(@workflow_type), method: :get, local: true do |f| %>
364
+ <div class="flex flex-wrap items-center gap-3 mb-4 pb-4 border-b border-gray-100 dark:border-gray-700">
365
+ <%# Status Filter (Multi-select) %>
366
+ <%= render "ruby_llm/agents/shared/filter_dropdown",
367
+ name: "statuses[]",
368
+ filter_id: "statuses",
369
+ label: "Status",
370
+ all_label: "All Statuses",
371
+ options: status_options,
372
+ selected: selected_statuses %>
373
+
374
+ <%# Version Filter (Multi-select) %>
375
+ <% if @versions.any? %>
376
+ <%= render "ruby_llm/agents/shared/filter_dropdown",
377
+ name: "versions[]",
378
+ filter_id: "versions",
379
+ label: "Version",
380
+ all_label: "All Versions",
381
+ options: version_options,
382
+ selected: selected_versions,
383
+ icon: "M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" %>
384
+ <% end %>
385
+
386
+ <%# Model Filter (Multi-select) %>
387
+ <% if @models.length > 1 %>
388
+ <%= render "ruby_llm/agents/shared/filter_dropdown",
389
+ name: "models[]",
390
+ filter_id: "models",
391
+ label: "Model",
392
+ all_label: "All Models",
393
+ options: model_options,
394
+ selected: selected_models,
395
+ icon: "M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" %>
396
+ <% end %>
397
+
398
+ <%# Temperature Filter (Multi-select) %>
399
+ <% if @temperatures.length > 1 %>
400
+ <%= render "ruby_llm/agents/shared/filter_dropdown",
401
+ name: "temperatures[]",
402
+ filter_id: "temperatures",
403
+ label: "Temp",
404
+ all_label: "All Temps",
405
+ options: temperature_options,
406
+ selected: selected_temperatures,
407
+ icon: "M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" %>
408
+ <% end %>
409
+
410
+ <%# Time Range Filter (Single-select) %>
411
+ <%= render "ruby_llm/agents/shared/select_dropdown",
412
+ name: "days",
413
+ filter_id: "days",
414
+ options: days_options,
415
+ selected: params[:days].to_s,
416
+ icon: "M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" %>
417
+
418
+ <%# Clear Filters %>
419
+ <% if has_filters %>
420
+ <%= link_to ruby_llm_agents.workflow_path(@workflow_type),
421
+ class: "flex items-center gap-1 px-3 py-2 text-sm text-red-500 dark:text-red-400 hover:text-red-600 dark:hover:text-red-300 hover:bg-red-50 dark:hover:bg-red-900/30 rounded-lg transition-colors" do %>
422
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
423
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
424
+ </svg>
425
+ Clear
426
+ <% end %>
427
+ <% end %>
428
+
429
+ <%# Stats Summary (right aligned) %>
430
+ <div class="ml-auto flex items-center gap-4 text-sm text-gray-500 dark:text-gray-400">
431
+ <span><%= number_with_delimiter(@filter_stats[:total_count]) %> executions</span>
432
+ <span class="text-gray-300 dark:text-gray-600">|</span>
433
+ <span>$<%= number_with_precision(@filter_stats[:total_cost] || 0, precision: 4) %></span>
434
+ <span class="text-gray-300 dark:text-gray-600">|</span>
435
+ <span><%= number_with_delimiter(@filter_stats[:total_tokens] || 0) %> tokens</span>
436
+ </div>
437
+ </div>
438
+ <% end %>
439
+
440
+ <%= render "ruby_llm/agents/shared/executions_table", executions: @executions, pagination: @pagination %>
441
+ </div>
442
+ </div>
data/config/routes.rb CHANGED
@@ -5,6 +5,7 @@ RubyLLM::Agents::Engine.routes.draw do
5
5
  get "chart_data", to: "dashboard#chart_data"
6
6
 
7
7
  resources :agents, only: [:index, :show]
8
+ resources :workflows, only: [:show]
8
9
 
9
10
  resources :executions, only: [:index, :show] do
10
11
  collection do
@@ -16,7 +17,18 @@ RubyLLM::Agents::Engine.routes.draw do
16
17
  end
17
18
  end
18
19
 
20
+ resources :tenants, only: [:index, :show, :edit, :update]
21
+
22
+ # Global API Configuration
23
+ resource :api_configuration, only: [:show, :edit, :update]
24
+
25
+ # Tenant API Configurations
26
+ get "tenants/:tenant_id/api_configuration", to: "api_configurations#tenant", as: :tenant_api_configuration
27
+ get "tenants/:tenant_id/api_configuration/edit", to: "api_configurations#edit_tenant", as: :edit_tenant_api_configuration
28
+ patch "tenants/:tenant_id/api_configuration", to: "api_configurations#update_tenant"
29
+ post "api_configuration/test_connection", to: "api_configurations#test_connection", as: :test_api_connection
30
+
19
31
  # Redirect old analytics route to dashboard
20
32
  get "analytics", to: redirect("/")
21
- resource :settings, only: [:show]
33
+ resource :system_config, only: [:show], controller: "system_config"
22
34
  end
@@ -7,9 +7,10 @@ module RubyLlmAgents
7
7
  #
8
8
  # Usage:
9
9
  # rails generate ruby_llm_agents:agent SearchIntent query:required limit:10
10
+ # rails generate ruby_llm_agents:agent SearchIntent query:required --root=ai
10
11
  #
11
12
  # This will create:
12
- # - app/agents/search_intent_agent.rb
13
+ # - app/{root}/agents/search_intent_agent.rb
13
14
  #
14
15
  # Parameter syntax:
15
16
  # name - Optional parameter
@@ -27,28 +28,76 @@ module RubyLlmAgents
27
28
  desc: "The temperature setting (0.0-1.0)"
28
29
  class_option :cache, type: :string, default: nil,
29
30
  desc: "Cache TTL (e.g., '1.hour', '30.minutes')"
31
+ class_option :root,
32
+ type: :string,
33
+ default: nil,
34
+ desc: "Root directory name (default: uses config or 'llm')"
35
+ class_option :namespace,
36
+ type: :string,
37
+ default: nil,
38
+ desc: "Root namespace (default: camelized root or config)"
39
+
40
+ def ensure_base_class_and_skill_file
41
+ @root_namespace = root_namespace
42
+ agents_dir = "app/#{root_directory}/agents"
43
+
44
+ # Create directory if needed
45
+ empty_directory agents_dir
46
+
47
+ # Create base class if it doesn't exist
48
+ base_class_path = "#{agents_dir}/application_agent.rb"
49
+ unless File.exist?(File.join(destination_root, base_class_path))
50
+ template "application_agent.rb.tt", base_class_path
51
+ end
52
+
53
+ # Create skill file if it doesn't exist
54
+ skill_file_path = "#{agents_dir}/AGENTS.md"
55
+ unless File.exist?(File.join(destination_root, skill_file_path))
56
+ template "skills/AGENTS.md.tt", skill_file_path
57
+ end
58
+ end
30
59
 
31
60
  def create_agent_file
32
- # Support nested paths: "chat/support" -> "app/agents/chat/support_agent.rb"
61
+ # Support nested paths: "chat/support" -> "app/{root}/agents/chat/support_agent.rb"
33
62
  # Rails' class_name handles namespacing: "chat/support" -> "Chat::Support"
63
+ @root_namespace = root_namespace
34
64
  agent_path = name.underscore
35
- template "agent.rb.tt", "app/agents/#{agent_path}_agent.rb"
65
+ template "agent.rb.tt", "app/#{root_directory}/agents/#{agent_path}_agent.rb"
36
66
  end
37
67
 
38
68
  def show_usage
39
69
  # Build full class name from path (e.g., "chat/support" -> "Chat::Support")
40
- full_class_name = name.split('/').map(&:camelize).join("::")
70
+ agent_class_name = name.split("/").map(&:camelize).join("::")
71
+ full_class_name = "#{root_namespace}::#{agent_class_name}Agent"
41
72
  say ""
42
- say "Agent #{full_class_name}Agent created!", :green
73
+ say "Agent #{full_class_name} created!", :green
43
74
  say ""
44
75
  say "Usage:"
45
- say " #{full_class_name}Agent.call(#{usage_params})"
46
- say " #{full_class_name}Agent.call(#{usage_params}, dry_run: true)"
76
+ say " #{full_class_name}.call(#{usage_params})"
77
+ say " #{full_class_name}.call(#{usage_params}, dry_run: true)"
47
78
  say ""
48
79
  end
49
80
 
50
81
  private
51
82
 
83
+ def root_directory
84
+ @root_directory ||= options[:root] || RubyLLM::Agents.configuration.root_directory
85
+ end
86
+
87
+ def root_namespace
88
+ @root_namespace ||= options[:namespace] || camelize(root_directory)
89
+ end
90
+
91
+ def camelize(str)
92
+ # Handle special cases for common abbreviations
93
+ return "AI" if str.downcase == "ai"
94
+ return "ML" if str.downcase == "ml"
95
+ return "LLM" if str.downcase == "llm"
96
+
97
+ # Standard camelization
98
+ str.split(/[-_]/).map(&:capitalize).join
99
+ end
100
+
52
101
  def parsed_params
53
102
  @parsed_params ||= params.map do |param|
54
103
  name, modifier = param.split(":")
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module RubyLlmAgents
7
+ # API Configuration generator for ruby_llm-agents
8
+ #
9
+ # Usage:
10
+ # rails generate ruby_llm_agents:api_configuration
11
+ #
12
+ # This will create migrations for:
13
+ # - ruby_llm_agents_api_configurations table for storing API keys and settings
14
+ #
15
+ # API keys are encrypted at rest using Rails encrypted attributes.
16
+ # Supports both global configuration and per-tenant overrides.
17
+ #
18
+ class ApiConfigurationGenerator < ::Rails::Generators::Base
19
+ include ::ActiveRecord::Generators::Migration
20
+
21
+ source_root File.expand_path("templates", __dir__)
22
+
23
+ desc "Adds database-backed API configuration support to RubyLLM::Agents"
24
+
25
+ def create_api_configurations_migration
26
+ if table_exists?(:ruby_llm_agents_api_configurations)
27
+ say_status :skip, "ruby_llm_agents_api_configurations table already exists", :yellow
28
+ return
29
+ end
30
+
31
+ migration_template(
32
+ "create_api_configurations_migration.rb.tt",
33
+ File.join(db_migrate_path, "create_ruby_llm_agents_api_configurations.rb")
34
+ )
35
+ end
36
+
37
+ def show_post_install_message
38
+ say ""
39
+ say "API Configuration migration created!", :green
40
+ say ""
41
+ say "Next steps:"
42
+ say " 1. Ensure Rails encryption is configured (if not already):"
43
+ say ""
44
+ say " bin/rails db:encryption:init"
45
+ say ""
46
+ say " Then add the generated keys to your credentials or environment."
47
+ say ""
48
+ say " 2. Run the migration:"
49
+ say ""
50
+ say " rails db:migrate"
51
+ say ""
52
+ say " 3. Access the API Configuration UI:"
53
+ say ""
54
+ say " Navigate to /agents/api_configuration in your browser"
55
+ say ""
56
+ say " 4. (Optional) Configure API keys programmatically:"
57
+ say ""
58
+ say " # Set global configuration"
59
+ say " config = RubyLLM::Agents::ApiConfiguration.global"
60
+ say " config.update!("
61
+ say " openai_api_key: 'sk-...',"
62
+ say " anthropic_api_key: 'sk-ant-...'"
63
+ say " )"
64
+ say ""
65
+ say " # Set tenant-specific configuration"
66
+ say " tenant_config = RubyLLM::Agents::ApiConfiguration.for_tenant!('acme_corp')"
67
+ say " tenant_config.update!("
68
+ say " openai_api_key: 'sk-tenant-specific-key',"
69
+ say " inherit_global_defaults: true"
70
+ say " )"
71
+ say ""
72
+ say "Configuration Resolution Priority:"
73
+ say " 1. Per-tenant database configuration (if multi-tenancy enabled)"
74
+ say " 2. Global database configuration"
75
+ say " 3. RubyLLM.configure block settings"
76
+ say ""
77
+ say "Security Notes:"
78
+ say " - API keys are encrypted at rest using Rails encrypted attributes"
79
+ say " - Keys are masked in the UI (e.g., sk-ab****wxyz)"
80
+ say " - Dashboard authentication inherits from your authenticate_dashboard! method"
81
+ say ""
82
+ end
83
+
84
+ private
85
+
86
+ def migration_version
87
+ "[#{::ActiveRecord::VERSION::STRING.to_f}]"
88
+ end
89
+
90
+ def db_migrate_path
91
+ "db/migrate"
92
+ end
93
+
94
+ def table_exists?(table)
95
+ ActiveRecord::Base.connection.table_exists?(table)
96
+ rescue StandardError
97
+ false
98
+ end
99
+ end
100
+ end