ruby_llm-agents 0.5.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 (190) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +189 -31
  3. data/app/controllers/ruby_llm/agents/agents_controller.rb +136 -16
  4. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +29 -9
  5. data/app/controllers/ruby_llm/agents/workflows_controller.rb +355 -0
  6. data/app/helpers/ruby_llm/agents/application_helper.rb +25 -0
  7. data/app/models/ruby_llm/agents/execution.rb +3 -0
  8. data/app/models/ruby_llm/agents/tenant_budget.rb +58 -15
  9. data/app/services/ruby_llm/agents/agent_registry.rb +51 -12
  10. data/app/views/layouts/ruby_llm/agents/application.html.erb +2 -29
  11. data/app/views/ruby_llm/agents/agents/_agent.html.erb +13 -1
  12. data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +235 -0
  13. data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +70 -0
  14. data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +152 -0
  15. data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +63 -0
  16. data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +108 -0
  17. data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +91 -0
  18. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +1 -1
  19. data/app/views/ruby_llm/agents/agents/index.html.erb +74 -9
  20. data/app/views/ruby_llm/agents/agents/show.html.erb +18 -378
  21. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +269 -15
  22. data/app/views/ruby_llm/agents/executions/show.html.erb +16 -0
  23. data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +93 -0
  24. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +236 -0
  25. data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +76 -0
  26. data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +74 -0
  27. data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +108 -0
  28. data/app/views/ruby_llm/agents/workflows/show.html.erb +442 -0
  29. data/config/routes.rb +1 -0
  30. data/lib/generators/ruby_llm_agents/agent_generator.rb +56 -7
  31. data/lib/generators/ruby_llm_agents/background_remover_generator.rb +110 -0
  32. data/lib/generators/ruby_llm_agents/embedder_generator.rb +107 -0
  33. data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +115 -0
  34. data/lib/generators/ruby_llm_agents/image_editor_generator.rb +108 -0
  35. data/lib/generators/ruby_llm_agents/image_generator_generator.rb +116 -0
  36. data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +178 -0
  37. data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +109 -0
  38. data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +103 -0
  39. data/lib/generators/ruby_llm_agents/image_variator_generator.rb +102 -0
  40. data/lib/generators/ruby_llm_agents/install_generator.rb +76 -4
  41. data/lib/generators/ruby_llm_agents/restructure_generator.rb +292 -0
  42. data/lib/generators/ruby_llm_agents/speaker_generator.rb +121 -0
  43. data/lib/generators/ruby_llm_agents/templates/add_execution_type_migration.rb.tt +8 -0
  44. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +99 -84
  45. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +42 -40
  46. data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +26 -0
  47. data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +50 -0
  48. data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +26 -0
  49. data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +20 -0
  50. data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +38 -0
  51. data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +139 -0
  52. data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +21 -0
  53. data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +20 -0
  54. data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +20 -0
  55. data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +49 -0
  56. data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +53 -0
  57. data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +44 -0
  58. data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +41 -0
  59. data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +45 -0
  60. data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +35 -0
  61. data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +47 -0
  62. data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +50 -0
  63. data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +44 -0
  64. data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +38 -0
  65. data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +33 -0
  66. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +228 -0
  67. data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +131 -0
  68. data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +255 -0
  69. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +120 -0
  70. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +102 -0
  71. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +282 -0
  72. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +228 -0
  73. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +120 -0
  74. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +110 -0
  75. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +120 -0
  76. data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +212 -0
  77. data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +227 -0
  78. data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +251 -0
  79. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +300 -0
  80. data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +56 -0
  81. data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +51 -0
  82. data/lib/generators/ruby_llm_agents/transcriber_generator.rb +107 -0
  83. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +152 -1
  84. data/lib/ruby_llm/agents/audio/speaker.rb +553 -0
  85. data/lib/ruby_llm/agents/audio/transcriber.rb +669 -0
  86. data/lib/ruby_llm/agents/base_agent.rb +675 -0
  87. data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +181 -0
  88. data/lib/ruby_llm/agents/core/base/moderation_execution.rb +274 -0
  89. data/lib/ruby_llm/agents/core/base.rb +135 -0
  90. data/lib/ruby_llm/agents/core/configuration.rb +981 -0
  91. data/lib/ruby_llm/agents/core/errors.rb +150 -0
  92. data/lib/ruby_llm/agents/{instrumentation.rb → core/instrumentation.rb} +22 -1
  93. data/lib/ruby_llm/agents/core/llm_tenant.rb +358 -0
  94. data/lib/ruby_llm/agents/{version.rb → core/version.rb} +1 -1
  95. data/lib/ruby_llm/agents/dsl/base.rb +110 -0
  96. data/lib/ruby_llm/agents/dsl/caching.rb +142 -0
  97. data/lib/ruby_llm/agents/dsl/reliability.rb +307 -0
  98. data/lib/ruby_llm/agents/dsl.rb +41 -0
  99. data/lib/ruby_llm/agents/image/analyzer/dsl.rb +130 -0
  100. data/lib/ruby_llm/agents/image/analyzer/execution.rb +402 -0
  101. data/lib/ruby_llm/agents/image/analyzer.rb +90 -0
  102. data/lib/ruby_llm/agents/image/background_remover/dsl.rb +154 -0
  103. data/lib/ruby_llm/agents/image/background_remover/execution.rb +240 -0
  104. data/lib/ruby_llm/agents/image/background_remover.rb +89 -0
  105. data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +91 -0
  106. data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +165 -0
  107. data/lib/ruby_llm/agents/image/editor/dsl.rb +56 -0
  108. data/lib/ruby_llm/agents/image/editor/execution.rb +207 -0
  109. data/lib/ruby_llm/agents/image/editor.rb +92 -0
  110. data/lib/ruby_llm/agents/image/generator/active_storage_support.rb +127 -0
  111. data/lib/ruby_llm/agents/image/generator/content_policy.rb +95 -0
  112. data/lib/ruby_llm/agents/image/generator/pricing.rb +353 -0
  113. data/lib/ruby_llm/agents/image/generator/templates.rb +124 -0
  114. data/lib/ruby_llm/agents/image/generator.rb +455 -0
  115. data/lib/ruby_llm/agents/image/pipeline/dsl.rb +213 -0
  116. data/lib/ruby_llm/agents/image/pipeline/execution.rb +382 -0
  117. data/lib/ruby_llm/agents/image/pipeline.rb +97 -0
  118. data/lib/ruby_llm/agents/image/transformer/dsl.rb +148 -0
  119. data/lib/ruby_llm/agents/image/transformer/execution.rb +223 -0
  120. data/lib/ruby_llm/agents/image/transformer.rb +95 -0
  121. data/lib/ruby_llm/agents/image/upscaler/dsl.rb +83 -0
  122. data/lib/ruby_llm/agents/image/upscaler/execution.rb +219 -0
  123. data/lib/ruby_llm/agents/image/upscaler.rb +81 -0
  124. data/lib/ruby_llm/agents/image/variator/dsl.rb +62 -0
  125. data/lib/ruby_llm/agents/image/variator/execution.rb +189 -0
  126. data/lib/ruby_llm/agents/image/variator.rb +80 -0
  127. data/lib/ruby_llm/agents/{alert_manager.rb → infrastructure/alert_manager.rb} +17 -22
  128. data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +145 -0
  129. data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +149 -0
  130. data/lib/ruby_llm/agents/infrastructure/budget/forecaster.rb +68 -0
  131. data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +279 -0
  132. data/lib/ruby_llm/agents/infrastructure/budget_tracker.rb +275 -0
  133. data/lib/ruby_llm/agents/{execution_logger_job.rb → infrastructure/execution_logger_job.rb} +17 -1
  134. data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/executor.rb +2 -1
  135. data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/retry_strategy.rb +9 -3
  136. data/lib/ruby_llm/agents/{reliability.rb → infrastructure/reliability.rb} +11 -21
  137. data/lib/ruby_llm/agents/pipeline/builder.rb +215 -0
  138. data/lib/ruby_llm/agents/pipeline/context.rb +255 -0
  139. data/lib/ruby_llm/agents/pipeline/executor.rb +86 -0
  140. data/lib/ruby_llm/agents/pipeline/middleware/base.rb +124 -0
  141. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +95 -0
  142. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +171 -0
  143. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +415 -0
  144. data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +276 -0
  145. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +196 -0
  146. data/lib/ruby_llm/agents/pipeline.rb +68 -0
  147. data/lib/ruby_llm/agents/{engine.rb → rails/engine.rb} +79 -11
  148. data/lib/ruby_llm/agents/results/background_removal_result.rb +286 -0
  149. data/lib/ruby_llm/agents/{result.rb → results/base.rb} +73 -1
  150. data/lib/ruby_llm/agents/results/embedding_result.rb +243 -0
  151. data/lib/ruby_llm/agents/results/image_analysis_result.rb +314 -0
  152. data/lib/ruby_llm/agents/results/image_edit_result.rb +250 -0
  153. data/lib/ruby_llm/agents/results/image_generation_result.rb +346 -0
  154. data/lib/ruby_llm/agents/results/image_pipeline_result.rb +399 -0
  155. data/lib/ruby_llm/agents/results/image_transform_result.rb +251 -0
  156. data/lib/ruby_llm/agents/results/image_upscale_result.rb +255 -0
  157. data/lib/ruby_llm/agents/results/image_variation_result.rb +237 -0
  158. data/lib/ruby_llm/agents/results/moderation_result.rb +158 -0
  159. data/lib/ruby_llm/agents/results/speech_result.rb +338 -0
  160. data/lib/ruby_llm/agents/results/transcription_result.rb +408 -0
  161. data/lib/ruby_llm/agents/text/embedder.rb +444 -0
  162. data/lib/ruby_llm/agents/text/moderator.rb +237 -0
  163. data/lib/ruby_llm/agents/workflow/async.rb +220 -0
  164. data/lib/ruby_llm/agents/workflow/async_executor.rb +156 -0
  165. data/lib/ruby_llm/agents/{workflow.rb → workflow/orchestrator.rb} +6 -5
  166. data/lib/ruby_llm/agents/workflow/parallel.rb +34 -17
  167. data/lib/ruby_llm/agents/workflow/thread_pool.rb +185 -0
  168. data/lib/ruby_llm/agents.rb +86 -20
  169. metadata +172 -34
  170. data/lib/ruby_llm/agents/base/caching.rb +0 -40
  171. data/lib/ruby_llm/agents/base/cost_calculation.rb +0 -105
  172. data/lib/ruby_llm/agents/base/dsl.rb +0 -324
  173. data/lib/ruby_llm/agents/base/execution.rb +0 -366
  174. data/lib/ruby_llm/agents/base/reliability_dsl.rb +0 -82
  175. data/lib/ruby_llm/agents/base/reliability_execution.rb +0 -136
  176. data/lib/ruby_llm/agents/base/response_building.rb +0 -86
  177. data/lib/ruby_llm/agents/base/tool_tracking.rb +0 -57
  178. data/lib/ruby_llm/agents/base.rb +0 -210
  179. data/lib/ruby_llm/agents/budget_tracker.rb +0 -733
  180. data/lib/ruby_llm/agents/configuration.rb +0 -394
  181. /data/lib/ruby_llm/agents/{deprecations.rb → core/deprecations.rb} +0 -0
  182. /data/lib/ruby_llm/agents/{inflections.rb → core/inflections.rb} +0 -0
  183. /data/lib/ruby_llm/agents/{resolved_config.rb → core/resolved_config.rb} +0 -0
  184. /data/lib/ruby_llm/agents/{attempt_tracker.rb → infrastructure/attempt_tracker.rb} +0 -0
  185. /data/lib/ruby_llm/agents/{cache_helper.rb → infrastructure/cache_helper.rb} +0 -0
  186. /data/lib/ruby_llm/agents/{circuit_breaker.rb → infrastructure/circuit_breaker.rb} +0 -0
  187. /data/lib/ruby_llm/agents/{redactor.rb → infrastructure/redactor.rb} +0 -0
  188. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/breaker_manager.rb +0 -0
  189. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/execution_constraints.rb +0 -0
  190. /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
@@ -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,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module RubyLlmAgents
6
+ # BackgroundRemover generator for creating new background removers
7
+ #
8
+ # Usage:
9
+ # rails generate ruby_llm_agents:background_remover Product
10
+ # rails generate ruby_llm_agents:background_remover Portrait --model segment-anything --alpha_matting
11
+ # rails generate ruby_llm_agents:background_remover Photo --refine_edges --return_mask
12
+ # rails generate ruby_llm_agents:background_remover Product --root=ai
13
+ #
14
+ # This will create:
15
+ # - app/{root}/image/background_removers/product_background_remover.rb
16
+ #
17
+ class BackgroundRemoverGenerator < ::Rails::Generators::NamedBase
18
+ source_root File.expand_path("templates", __dir__)
19
+
20
+ class_option :model, type: :string, default: "rembg",
21
+ desc: "The segmentation model to use"
22
+ class_option :output_format, type: :string, default: "png",
23
+ desc: "Output format (png, webp)"
24
+ class_option :refine_edges, type: :boolean, default: false,
25
+ desc: "Enable edge refinement"
26
+ class_option :alpha_matting, type: :boolean, default: false,
27
+ desc: "Enable alpha matting for better edges"
28
+ class_option :return_mask, type: :boolean, default: false,
29
+ desc: "Also return the segmentation mask"
30
+ class_option :cache, type: :string, default: nil,
31
+ desc: "Cache TTL (e.g., '1.hour', '1.day')"
32
+ class_option :root,
33
+ type: :string,
34
+ default: nil,
35
+ desc: "Root directory name (default: uses config or 'llm')"
36
+ class_option :namespace,
37
+ type: :string,
38
+ default: nil,
39
+ desc: "Root namespace (default: camelized root or config)"
40
+
41
+ def ensure_base_class_and_skill_file
42
+ @root_namespace = root_namespace
43
+ @image_namespace = "#{root_namespace}::Image"
44
+ removers_dir = "app/#{root_directory}/image/background_removers"
45
+
46
+ # Create directory if needed
47
+ empty_directory removers_dir
48
+
49
+ # Create base class if it doesn't exist
50
+ base_class_path = "#{removers_dir}/application_background_remover.rb"
51
+ unless File.exist?(File.join(destination_root, base_class_path))
52
+ template "application_background_remover.rb.tt", base_class_path
53
+ end
54
+
55
+ # Create skill file if it doesn't exist
56
+ skill_file_path = "#{removers_dir}/BACKGROUND_REMOVERS.md"
57
+ unless File.exist?(File.join(destination_root, skill_file_path))
58
+ template "skills/BACKGROUND_REMOVERS.md.tt", skill_file_path
59
+ end
60
+ end
61
+
62
+ def create_background_remover_file
63
+ @root_namespace = root_namespace
64
+ @image_namespace = "#{root_namespace}::Image"
65
+ remover_path = name.underscore
66
+ template "background_remover.rb.tt", "app/#{root_directory}/image/background_removers/#{remover_path}_background_remover.rb"
67
+ end
68
+
69
+ def show_usage
70
+ remover_class_name = name.split("/").map(&:camelize).join("::")
71
+ full_class_name = "#{root_namespace}::Image::#{remover_class_name}BackgroundRemover"
72
+ say ""
73
+ say "Background remover #{full_class_name} created!", :green
74
+ say ""
75
+ say "Usage:"
76
+ say " # Remove background from an image"
77
+ say " result = #{full_class_name}.call(image: 'photo.jpg')"
78
+ say " result.url # => 'https://...' (transparent PNG)"
79
+ say " result.has_alpha? # => true"
80
+ say ""
81
+ say " # Override settings at runtime"
82
+ say " result = #{full_class_name}.call("
83
+ say " image: 'portrait.jpg',"
84
+ say " alpha_matting: true,"
85
+ say " return_mask: true"
86
+ say " )"
87
+ say ""
88
+ say " # Access the segmentation mask"
89
+ say " result.mask_url # => 'https://...' (if return_mask was enabled)"
90
+ say ""
91
+ end
92
+
93
+ private
94
+
95
+ def root_directory
96
+ @root_directory ||= options[:root] || RubyLLM::Agents.configuration.root_directory
97
+ end
98
+
99
+ def root_namespace
100
+ @root_namespace ||= options[:namespace] || camelize(root_directory)
101
+ end
102
+
103
+ def camelize(str)
104
+ return "AI" if str.downcase == "ai"
105
+ return "ML" if str.downcase == "ml"
106
+ return "LLM" if str.downcase == "llm"
107
+ str.split(/[-_]/).map(&:capitalize).join
108
+ end
109
+ end
110
+ end