ruby_llm-agents 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/ruby_llm/agents/paginatable.rb +9 -3
  3. data/app/controllers/concerns/ruby_llm/agents/sortable.rb +58 -0
  4. data/app/controllers/ruby_llm/agents/agents_controller.rb +59 -16
  5. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +144 -20
  6. data/app/controllers/ruby_llm/agents/executions_controller.rb +13 -16
  7. data/app/controllers/ruby_llm/agents/workflows_controller.rb +279 -90
  8. data/app/helpers/ruby_llm/agents/application_helper.rb +100 -0
  9. data/app/mailers/ruby_llm/agents/alert_mailer.rb +84 -0
  10. data/app/mailers/ruby_llm/agents/application_mailer.rb +28 -0
  11. data/app/models/ruby_llm/agents/execution/analytics.rb +170 -20
  12. data/app/models/ruby_llm/agents/execution/scopes.rb +0 -31
  13. data/app/models/ruby_llm/agents/execution/workflow.rb +0 -129
  14. data/app/models/ruby_llm/agents/execution.rb +50 -14
  15. data/app/models/ruby_llm/agents/tenant/budgetable.rb +277 -0
  16. data/app/models/ruby_llm/agents/tenant/configurable.rb +135 -0
  17. data/app/models/ruby_llm/agents/tenant/trackable.rb +310 -0
  18. data/app/models/ruby_llm/agents/tenant.rb +146 -0
  19. data/app/models/ruby_llm/agents/tenant_budget.rb +12 -253
  20. data/app/services/ruby_llm/agents/agent_registry.rb +18 -12
  21. data/app/views/layouts/ruby_llm/agents/application.html.erb +72 -76
  22. data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -12
  23. data/app/views/ruby_llm/agents/agents/_sortable_header.html.erb +56 -0
  24. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +5 -15
  25. data/app/views/ruby_llm/agents/agents/index.html.erb +271 -100
  26. data/app/views/ruby_llm/agents/agents/show.html.erb +1 -0
  27. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +107 -0
  28. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +18 -0
  29. data/app/views/ruby_llm/agents/api_configurations/show.html.erb +4 -1
  30. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +66 -359
  31. data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +56 -0
  32. data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +115 -0
  33. data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +35 -60
  34. data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +17 -6
  35. data/app/views/ruby_llm/agents/dashboard/index.html.erb +373 -72
  36. data/app/views/ruby_llm/agents/executions/_execution.html.erb +0 -1
  37. data/app/views/ruby_llm/agents/executions/_filters.html.erb +51 -39
  38. data/app/views/ruby_llm/agents/executions/_list.html.erb +53 -195
  39. data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +5 -20
  40. data/app/views/ruby_llm/agents/executions/index.html.erb +7 -83
  41. data/app/views/ruby_llm/agents/executions/show.html.erb +10 -20
  42. data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +2 -1
  43. data/app/views/ruby_llm/agents/shared/_doc_link.html.erb +12 -0
  44. data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +3 -15
  45. data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +1 -1
  46. data/app/views/ruby_llm/agents/shared/_select_dropdown.html.erb +1 -1
  47. data/app/views/ruby_llm/agents/shared/_sortable_header.html.erb +53 -0
  48. data/app/views/ruby_llm/agents/shared/_status_badge.html.erb +7 -0
  49. data/app/views/ruby_llm/agents/shared/_status_dot.html.erb +1 -1
  50. data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +9 -35
  51. data/app/views/ruby_llm/agents/system_config/show.html.erb +4 -1
  52. data/app/views/ruby_llm/agents/tenants/index.html.erb +4 -1
  53. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +7 -15
  54. data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +539 -0
  55. data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +920 -0
  56. data/app/views/ruby_llm/agents/workflows/index.html.erb +179 -0
  57. data/app/views/ruby_llm/agents/workflows/show.html.erb +164 -139
  58. data/config/routes.rb +1 -1
  59. data/lib/generators/ruby_llm_agents/agent_generator.rb +6 -36
  60. data/lib/generators/ruby_llm_agents/background_remover_generator.rb +7 -37
  61. data/lib/generators/ruby_llm_agents/embedder_generator.rb +5 -38
  62. data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +7 -37
  63. data/lib/generators/ruby_llm_agents/image_editor_generator.rb +7 -37
  64. data/lib/generators/ruby_llm_agents/image_generator_generator.rb +8 -41
  65. data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +18 -46
  66. data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +7 -37
  67. data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +7 -37
  68. data/lib/generators/ruby_llm_agents/image_variator_generator.rb +7 -37
  69. data/lib/generators/ruby_llm_agents/install_generator.rb +33 -56
  70. data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +480 -0
  71. data/lib/generators/ruby_llm_agents/multi_tenancy_generator.rb +42 -22
  72. data/lib/generators/ruby_llm_agents/restructure_generator.rb +2 -2
  73. data/lib/generators/ruby_llm_agents/speaker_generator.rb +8 -39
  74. data/lib/generators/ruby_llm_agents/templates/add_tenant_to_executions_migration.rb.tt +13 -2
  75. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +5 -8
  76. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +40 -42
  77. data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +20 -22
  78. data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +24 -26
  79. data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +20 -22
  80. data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +19 -17
  81. data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +31 -33
  82. data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +125 -127
  83. data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +20 -18
  84. data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +19 -17
  85. data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +19 -17
  86. data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +38 -40
  87. data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +42 -44
  88. data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +48 -0
  89. data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +19 -21
  90. data/lib/generators/ruby_llm_agents/templates/create_tenant_budgets_migration.rb.tt +11 -0
  91. data/lib/generators/ruby_llm_agents/templates/create_tenants_migration.rb.tt +72 -0
  92. data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +19 -21
  93. data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +20 -22
  94. data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +15 -17
  95. data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +25 -27
  96. data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +19 -21
  97. data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +20 -22
  98. data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +17 -19
  99. data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +15 -17
  100. data/lib/generators/ruby_llm_agents/templates/rename_tenant_budgets_to_tenants_migration.rb.tt +34 -0
  101. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +87 -24
  102. data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +21 -27
  103. data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +46 -54
  104. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +31 -39
  105. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +22 -28
  106. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +53 -63
  107. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +46 -56
  108. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +23 -31
  109. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +22 -30
  110. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +23 -31
  111. data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +38 -46
  112. data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +7 -7
  113. data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +59 -71
  114. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +274 -23
  115. data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +29 -31
  116. data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +28 -30
  117. data/lib/generators/ruby_llm_agents/transcriber_generator.rb +10 -43
  118. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +26 -0
  119. data/lib/ruby_llm/agents/core/configuration.rb +55 -43
  120. data/lib/ruby_llm/agents/core/llm_tenant.rb +60 -60
  121. data/lib/ruby_llm/agents/core/version.rb +1 -1
  122. data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +26 -0
  123. data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +4 -2
  124. data/lib/ruby_llm/agents/pipeline.rb +69 -0
  125. data/lib/ruby_llm/agents/workflow/approval.rb +205 -0
  126. data/lib/ruby_llm/agents/workflow/approval_store.rb +179 -0
  127. data/lib/ruby_llm/agents/workflow/dsl/executor.rb +467 -0
  128. data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +244 -0
  129. data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +289 -0
  130. data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +107 -0
  131. data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +150 -0
  132. data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +187 -0
  133. data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +352 -0
  134. data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +415 -0
  135. data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +257 -0
  136. data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +317 -0
  137. data/lib/ruby_llm/agents/workflow/dsl.rb +576 -0
  138. data/lib/ruby_llm/agents/workflow/instrumentation.rb +2 -7
  139. data/lib/ruby_llm/agents/workflow/notifiers/base.rb +117 -0
  140. data/lib/ruby_llm/agents/workflow/notifiers/email.rb +117 -0
  141. data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +180 -0
  142. data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +121 -0
  143. data/lib/ruby_llm/agents/workflow/notifiers.rb +70 -0
  144. data/lib/ruby_llm/agents/workflow/orchestrator.rb +190 -23
  145. data/lib/ruby_llm/agents/workflow/result.rb +202 -0
  146. data/lib/ruby_llm/agents/workflow/throttle_manager.rb +206 -0
  147. data/lib/ruby_llm/agents/workflow/wait_result.rb +213 -0
  148. metadata +43 -6
  149. data/app/views/ruby_llm/agents/dashboard/_execution_item.html.erb +0 -66
  150. data/lib/ruby_llm/agents/workflow/parallel.rb +0 -299
  151. data/lib/ruby_llm/agents/workflow/pipeline.rb +0 -306
  152. data/lib/ruby_llm/agents/workflow/router.rb +0 -429
@@ -2,262 +2,21 @@
2
2
 
3
3
  module RubyLLM
4
4
  module Agents
5
- # Database-backed budget configuration for multi-tenant environments
5
+ # @deprecated Use {Tenant} instead. This class will be removed in a future major version.
6
6
  #
7
- # Stores per-tenant budget limits that override the global configuration.
8
- # Supports runtime updates without application restarts.
9
- # Supports cost-based (USD), token-based, and execution-based limits.
7
+ # TenantBudget is now an alias to Tenant for backward compatibility.
8
+ # All functionality has been moved to the Tenant model with organized concerns.
10
9
  #
11
- # @!attribute [rw] tenant_id
12
- # @return [String] Unique identifier for the tenant
13
- # @!attribute [rw] name
14
- # @return [String, nil] Human-readable name for the tenant
15
- # @!attribute [rw] daily_limit
16
- # @return [BigDecimal, nil] Daily budget limit in USD
17
- # @!attribute [rw] monthly_limit
18
- # @return [BigDecimal, nil] Monthly budget limit in USD
19
- # @!attribute [rw] daily_token_limit
20
- # @return [Integer, nil] Daily token limit (across all models)
21
- # @!attribute [rw] monthly_token_limit
22
- # @return [Integer, nil] Monthly token limit (across all models)
23
- # @!attribute [rw] daily_execution_limit
24
- # @return [Integer, nil] Daily execution/call limit
25
- # @!attribute [rw] monthly_execution_limit
26
- # @return [Integer, nil] Monthly execution/call limit
27
- # @!attribute [rw] per_agent_daily
28
- # @return [Hash] Per-agent daily cost limits: { "AgentName" => limit }
29
- # @!attribute [rw] per_agent_monthly
30
- # @return [Hash] Per-agent monthly cost limits: { "AgentName" => limit }
31
- # @!attribute [rw] enforcement
32
- # @return [String] Enforcement mode: "none", "soft", or "hard"
33
- # @!attribute [rw] inherit_global_defaults
34
- # @return [Boolean] Whether to fall back to global config for unset limits
35
- # @!attribute [rw] tenant_record
36
- # @return [ActiveRecord::Base, nil] Polymorphic association to tenant model
10
+ # @example Migration path
11
+ # # Old usage (still works)
12
+ # TenantBudget.for_tenant("acme_corp")
13
+ # TenantBudget.create!(tenant_id: "acme", daily_limit: 100)
37
14
  #
38
- # @example Creating a tenant budget with cost, token, and execution limits
39
- # TenantBudget.create!(
40
- # tenant_id: "acme_corp",
41
- # name: "Acme Corporation",
42
- # daily_limit: 50.0, # USD
43
- # monthly_limit: 500.0, # USD
44
- # daily_token_limit: 1_000_000,
45
- # monthly_token_limit: 10_000_000,
46
- # daily_execution_limit: 500,
47
- # monthly_execution_limit: 10_000,
48
- # enforcement: "hard"
49
- # )
15
+ # # New usage (preferred)
16
+ # Tenant.for("acme_corp")
17
+ # Tenant.create!(tenant_id: "acme", daily_limit: 100)
50
18
  #
51
- # @example Fetching budget for a tenant object
52
- # budget = TenantBudget.for_tenant(organization)
53
- # budget.effective_daily_limit # => 50.0 (cost)
54
- # budget.effective_daily_token_limit # => 1_000_000 (tokens)
55
- # budget.effective_daily_execution_limit # => 500 (executions)
56
- #
57
- # @see RubyLLM::Agents::BudgetTracker
58
- # @see RubyLLM::Agents::LLMTenant
59
- # @api public
60
- class TenantBudget < ::ActiveRecord::Base
61
- self.table_name = "ruby_llm_agents_tenant_budgets"
62
-
63
- # Valid enforcement modes
64
- ENFORCEMENT_MODES = %w[none soft hard].freeze
65
-
66
- # Polymorphic association to the tenant model (e.g., Organization, Account)
67
- belongs_to :tenant_record, polymorphic: true, optional: true
68
-
69
- # Validations
70
- validates :tenant_id, presence: true, uniqueness: true
71
- validates :enforcement, inclusion: { in: ENFORCEMENT_MODES }, allow_nil: true
72
- validates :daily_limit, :monthly_limit,
73
- numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
74
- validates :daily_token_limit, :monthly_token_limit,
75
- numericality: { greater_than_or_equal_to: 0, only_integer: true }, allow_nil: true
76
- validates :daily_execution_limit, :monthly_execution_limit,
77
- numericality: { greater_than_or_equal_to: 0, only_integer: true }, allow_nil: true
78
-
79
- # Finds a budget for the given tenant
80
- #
81
- # @param tenant [String, Object] The tenant identifier string or object with llm_tenant_id
82
- # @return [TenantBudget, nil] The budget record or nil if not found
83
- def self.for_tenant(tenant)
84
- return nil if tenant.blank?
85
-
86
- if tenant.respond_to?(:llm_tenant_id)
87
- # Object with llm_tenant DSL - try polymorphic first, then tenant_id
88
- find_by(tenant_record: tenant) || find_by(tenant_id: tenant.llm_tenant_id)
89
- else
90
- # String tenant_id
91
- find_by(tenant_id: tenant.to_s)
92
- end
93
- end
94
-
95
- # Finds or creates a budget for the given tenant
96
- #
97
- # @param tenant_id [String] The tenant identifier
98
- # @param name [String, nil] Optional human-readable name
99
- # @return [TenantBudget] The budget record
100
- def self.for_tenant!(tenant_id, name: nil)
101
- find_or_create_by!(tenant_id: tenant_id) do |budget|
102
- budget.name = name
103
- end
104
- end
105
-
106
- # Returns the display name (name or tenant_id fallback)
107
- #
108
- # @return [String] The name to display
109
- def display_name
110
- name.presence || tenant_id
111
- end
112
-
113
- # Returns the effective daily limit, considering inheritance
114
- #
115
- # @return [Float, nil] The daily limit or nil if not set
116
- def effective_daily_limit
117
- return daily_limit if daily_limit.present?
118
- return nil unless inherit_global_defaults
119
-
120
- global_config&.dig(:global_daily)
121
- end
122
-
123
- # Returns the effective monthly limit, considering inheritance
124
- #
125
- # @return [Float, nil] The monthly limit or nil if not set
126
- def effective_monthly_limit
127
- return monthly_limit if monthly_limit.present?
128
- return nil unless inherit_global_defaults
129
-
130
- global_config&.dig(:global_monthly)
131
- end
132
-
133
- # Returns the effective per-agent daily limit
134
- #
135
- # @param agent_type [String] The agent class name
136
- # @return [Float, nil] The limit or nil if not set
137
- def effective_per_agent_daily(agent_type)
138
- limit = per_agent_daily&.dig(agent_type)
139
- return limit if limit.present?
140
- return nil unless inherit_global_defaults
141
-
142
- global_config&.dig(:per_agent_daily, agent_type)
143
- end
144
-
145
- # Returns the effective per-agent monthly limit
146
- #
147
- # @param agent_type [String] The agent class name
148
- # @return [Float, nil] The limit or nil if not set
149
- def effective_per_agent_monthly(agent_type)
150
- limit = per_agent_monthly&.dig(agent_type)
151
- return limit if limit.present?
152
- return nil unless inherit_global_defaults
153
-
154
- global_config&.dig(:per_agent_monthly, agent_type)
155
- end
156
-
157
- # Returns the effective daily token limit, considering inheritance
158
- #
159
- # @return [Integer, nil] The daily token limit or nil if not set
160
- def effective_daily_token_limit
161
- return daily_token_limit if daily_token_limit.present?
162
- return nil unless inherit_global_defaults
163
-
164
- global_config&.dig(:global_daily_tokens)
165
- end
166
-
167
- # Returns the effective monthly token limit, considering inheritance
168
- #
169
- # @return [Integer, nil] The monthly token limit or nil if not set
170
- def effective_monthly_token_limit
171
- return monthly_token_limit if monthly_token_limit.present?
172
- return nil unless inherit_global_defaults
173
-
174
- global_config&.dig(:global_monthly_tokens)
175
- end
176
-
177
- # Returns the effective daily execution limit, considering inheritance
178
- #
179
- # @return [Integer, nil] The daily execution limit or nil if not set
180
- def effective_daily_execution_limit
181
- return daily_execution_limit if daily_execution_limit.present?
182
- return nil unless inherit_global_defaults
183
-
184
- global_config&.dig(:global_daily_executions)
185
- end
186
-
187
- # Returns the effective monthly execution limit, considering inheritance
188
- #
189
- # @return [Integer, nil] The monthly execution limit or nil if not set
190
- def effective_monthly_execution_limit
191
- return monthly_execution_limit if monthly_execution_limit.present?
192
- return nil unless inherit_global_defaults
193
-
194
- global_config&.dig(:global_monthly_executions)
195
- end
196
-
197
- # Returns the effective enforcement mode
198
- #
199
- # @return [Symbol] :none, :soft, or :hard
200
- def effective_enforcement
201
- return enforcement.to_sym if enforcement.present?
202
- return :soft unless inherit_global_defaults
203
-
204
- RubyLLM::Agents.configuration.budget_enforcement
205
- end
206
-
207
- # Checks if budget enforcement is enabled for this tenant
208
- #
209
- # @return [Boolean] true if enforcement is :soft or :hard
210
- def budgets_enabled?
211
- effective_enforcement != :none
212
- end
213
-
214
- # Returns a hash suitable for BudgetTracker
215
- #
216
- # @return [Hash] Budget configuration hash
217
- def to_budget_config
218
- {
219
- enabled: budgets_enabled?,
220
- enforcement: effective_enforcement,
221
- # Cost limits
222
- global_daily: effective_daily_limit,
223
- global_monthly: effective_monthly_limit,
224
- per_agent_daily: merged_per_agent_daily,
225
- per_agent_monthly: merged_per_agent_monthly,
226
- # Token limits
227
- global_daily_tokens: effective_daily_token_limit,
228
- global_monthly_tokens: effective_monthly_token_limit,
229
- # Execution limits
230
- global_daily_executions: effective_daily_execution_limit,
231
- global_monthly_executions: effective_monthly_execution_limit
232
- }
233
- end
234
-
235
- private
236
-
237
- # Returns the global budgets configuration
238
- #
239
- # @return [Hash, nil] Global budget config
240
- def global_config
241
- RubyLLM::Agents.configuration.budgets
242
- end
243
-
244
- # Merges per-agent daily limits with global defaults
245
- #
246
- # @return [Hash] Merged per-agent daily limits
247
- def merged_per_agent_daily
248
- return per_agent_daily || {} unless inherit_global_defaults
249
-
250
- (global_config&.dig(:per_agent_daily) || {}).merge(per_agent_daily || {})
251
- end
252
-
253
- # Merges per-agent monthly limits with global defaults
254
- #
255
- # @return [Hash] Merged per-agent monthly limits
256
- def merged_per_agent_monthly
257
- return per_agent_monthly || {} unless inherit_global_defaults
258
-
259
- (global_config&.dig(:per_agent_monthly) || {}).merge(per_agent_monthly || {})
260
- end
261
- end
19
+ # @see Tenant
20
+ TenantBudget = Tenant
262
21
  end
263
22
  end
@@ -21,6 +21,12 @@ module RubyLLM
21
21
  #
22
22
  # @api public
23
23
  class AgentRegistry
24
+ # Base workflow classes to exclude from listings
25
+ # These are abstract parent classes, not concrete workflows
26
+ BASE_WORKFLOW_CLASSES = [
27
+ "RubyLLM::Agents::Workflow"
28
+ ].freeze
29
+
24
30
  class << self
25
31
  # Returns all unique agent type names
26
32
  #
@@ -72,7 +78,10 @@ module RubyLLM
72
78
  transcribers = RubyLLM::Agents::Transcriber.descendants.map(&:name).compact
73
79
  image_generators = RubyLLM::Agents::ImageGenerator.descendants.map(&:name).compact
74
80
 
75
- (agents + workflows + embedders + moderators + speakers + transcribers + image_generators).uniq
81
+ all_agents = (agents + workflows + embedders + moderators + speakers + transcribers + image_generators).uniq
82
+
83
+ # Filter out base workflow classes
84
+ all_agents.reject { |name| BASE_WORKFLOW_CLASSES.include?(name) }
76
85
  rescue StandardError => e
77
86
  Rails.logger.error("[RubyLLM::Agents] Error loading agents from file system: #{e.message}")
78
87
  []
@@ -90,10 +99,13 @@ module RubyLLM
90
99
 
91
100
  # Eager loads all agent and workflow files to register descendants
92
101
  #
102
+ # Uses the configured autoload paths from RubyLLM::Agents.configuration
103
+ # to ensure agents are discovered in the correct directories.
104
+ #
93
105
  # @return [void]
94
106
  def eager_load_agents!
95
- %w[agents workflows embedders moderators speakers transcribers image_generators].each do |dir|
96
- path = Rails.root.join("app", dir)
107
+ RubyLLM::Agents.configuration.all_autoload_paths.each do |relative_path|
108
+ path = Rails.root.join(relative_path)
97
109
  next unless path.exist?
98
110
 
99
111
  Dir.glob(path.join("**", "*.rb")).each do |file|
@@ -207,18 +219,12 @@ module RubyLLM
207
219
  # Detects the specific workflow type from class hierarchy
208
220
  #
209
221
  # @param agent_class [Class, nil] The agent class
210
- # @return [String, nil] "pipeline", "parallel", "router", or nil
222
+ # @return [String, nil] "workflow" for DSL workflows, or nil
211
223
  def detect_workflow_type(agent_class)
212
224
  return nil unless agent_class
213
225
 
214
- ancestors = agent_class.ancestors.map { |a| a.name.to_s }
215
-
216
- if ancestors.include?("RubyLLM::Agents::Workflow::Pipeline")
217
- "pipeline"
218
- elsif ancestors.include?("RubyLLM::Agents::Workflow::Parallel")
219
- "parallel"
220
- elsif ancestors.include?("RubyLLM::Agents::Workflow::Router")
221
- "router"
226
+ if agent_class.respond_to?(:step_configs) && agent_class.step_configs.any?
227
+ "workflow"
222
228
  end
223
229
  end
224
230
 
@@ -215,10 +215,9 @@
215
215
  nav_items = [
216
216
  { path: ruby_llm_agents.root_path, label: "Dashboard", icon: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />' },
217
217
  { path: ruby_llm_agents.agents_path, label: "Agents", icon: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="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" />' },
218
+ { path: ruby_llm_agents.workflows_path, label: "Workflows", icon: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zm0 8a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zm12 0a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z"/>' },
218
219
  { path: ruby_llm_agents.executions_path, label: "Executions", icon: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />' },
219
- { path: ruby_llm_agents.tenants_path, label: "Tenants", icon: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />' },
220
- { path: ruby_llm_agents.system_config_path, label: "System Config", icon: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />' },
221
- { path: ruby_llm_agents.api_configuration_path, label: "API Keys", icon: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />' }
220
+ { path: ruby_llm_agents.tenants_path, label: "Tenants", icon: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />' }
222
221
  ]
223
222
  %>
224
223
  <nav class="hidden md:flex items-center space-x-1">
@@ -234,56 +233,55 @@
234
233
  dark:text-gray-400
235
234
  "
236
235
  >
237
- <%# Tenant Selector Dropdown %>
238
- <% if tenant_filter_enabled? && available_tenants.any? %>
239
- <div x-data="{ open: false }" class="relative">
240
- <button
241
- @click="open = !open"
242
- @click.outside="open = false"
243
- type="button"
244
- class="flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium rounded-md border transition-colors
245
- <%= current_tenant_id.present? ?
246
- 'bg-blue-50 dark:bg-blue-900/30 border-blue-200 dark:border-blue-800 text-blue-700 dark:text-blue-300' :
247
- 'bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600' %>"
248
- >
249
- <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
250
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
236
+ <%# Settings Dropdown %>
237
+ <div x-data="{ settingsOpen: false }" class="relative hidden md:block">
238
+ <button
239
+ @click="settingsOpen = !settingsOpen"
240
+ @click.outside="settingsOpen = false"
241
+ type="button"
242
+ class="p-2 rounded-md text-gray-500 hover:text-gray-700 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-gray-200 dark:hover:bg-gray-700 transition-colors"
243
+ aria-label="Settings"
244
+ >
245
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
246
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
247
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
248
+ </svg>
249
+ </button>
250
+
251
+ <div
252
+ x-show="settingsOpen"
253
+ x-cloak
254
+ x-transition:enter="transition ease-out duration-100"
255
+ x-transition:enter-start="opacity-0 scale-95"
256
+ x-transition:enter-end="opacity-100 scale-100"
257
+ x-transition:leave="transition ease-in duration-75"
258
+ x-transition:leave-start="opacity-100 scale-100"
259
+ x-transition:leave-end="opacity-0 scale-95"
260
+ class="absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50 py-1"
261
+ >
262
+ <a href="<%= ruby_llm_agents.system_config_path %>"
263
+ class="flex items-center gap-2 px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 <%= 'bg-gray-100 dark:bg-gray-700' if request.path == ruby_llm_agents.system_config_path %>">
264
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
265
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"/>
251
266
  </svg>
252
- <span class="hidden sm:inline"><%= current_tenant_id.present? ? current_tenant_id : 'All Tenants' %></span>
253
- <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
254
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
267
+ System Config
268
+ </a>
269
+ <a href="<%= ruby_llm_agents.api_configuration_path %>"
270
+ class="flex items-center gap-2 px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 <%= 'bg-gray-100 dark:bg-gray-700' if request.path == ruby_llm_agents.api_configuration_path %>">
271
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
272
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"/>
255
273
  </svg>
256
- </button>
257
-
258
- <div
259
- x-show="open"
260
- x-cloak
261
- x-transition:enter="transition ease-out duration-100"
262
- x-transition:enter-start="opacity-0 scale-95"
263
- x-transition:enter-end="opacity-100 scale-100"
264
- x-transition:leave="transition ease-in duration-75"
265
- x-transition:leave-start="opacity-100 scale-100"
266
- x-transition:leave-end="opacity-0 scale-95"
267
- class="absolute right-0 mt-1 w-48 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50 py-1"
268
- >
269
- <a
270
- href="<%= all_tenants_url %>"
271
- class="block px-3 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 <%= 'bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300' if current_tenant_id.blank? %>"
272
- >
273
- All Tenants
274
- </a>
275
- <div class="border-t border-gray-100 dark:border-gray-700 my-1"></div>
276
- <% available_tenants.each do |tenant| %>
277
- <a
278
- href="<%= url_for(request.query_parameters.merge(tenant_id: tenant)) %>"
279
- class="block px-3 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 <%= 'bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300' if tenant == current_tenant_id %>"
280
- >
281
- <%= tenant %>
282
- </a>
283
- <% end %>
284
- </div>
274
+ API Keys
275
+ </a>
276
+ <div class="border-t border-gray-100 dark:border-gray-700 my-1"></div>
277
+ <a href="#" class="flex items-center gap-2 px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700">
278
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
279
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/>
280
+ </svg>
281
+ Logout
282
+ </a>
285
283
  </div>
286
- <% end %>
284
+ </div>
287
285
 
288
286
  <!-- Mobile menu button -->
289
287
  <button
@@ -340,35 +338,33 @@
340
338
  <% nav_items.each do |item| %>
341
339
  <%= render "ruby_llm/agents/shared/nav_link", path: item[:path], label: item[:label], icon: item[:icon], mobile: true %>
342
340
  <% end %>
343
- </nav>
344
- </div>
345
- </header>
346
341
 
347
- <%# Tenant Context Badge - shows when viewing specific tenant %>
348
- <% if tenant_filter_enabled? && current_tenant_id.present? %>
349
- <div class="bg-blue-50 dark:bg-blue-900/20 border-b border-blue-100 dark:border-blue-900/50">
350
- <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-2">
351
- <div class="flex items-center justify-between">
352
- <div class="flex items-center gap-2 text-sm text-blue-700 dark:text-blue-300">
353
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
354
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
355
- </svg>
356
- <span>Viewing tenant:</span>
357
- <span class="font-semibold"><%= current_tenant_id %></span>
358
- </div>
359
- <a
360
- href="<%= url_for(request.query_parameters.except('tenant_id')) %>"
361
- class="flex items-center gap-1 text-sm text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-200"
362
- >
363
- <span>Clear filter</span>
364
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
365
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
366
- </svg>
367
- </a>
342
+ <%# Settings section divider %>
343
+ <div class="border-t border-gray-200 dark:border-gray-700 my-2 pt-2">
344
+ <span class="px-3 text-xs font-semibold text-gray-400 dark:text-gray-500 uppercase tracking-wider">Settings</span>
368
345
  </div>
369
- </div>
346
+
347
+ <%= render "ruby_llm/agents/shared/nav_link",
348
+ path: ruby_llm_agents.system_config_path,
349
+ label: "System Config",
350
+ icon: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"/>',
351
+ mobile: true %>
352
+
353
+ <%= render "ruby_llm/agents/shared/nav_link",
354
+ path: ruby_llm_agents.api_configuration_path,
355
+ label: "API Keys",
356
+ icon: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"/>',
357
+ mobile: true %>
358
+
359
+ <a href="#" class="flex items-center px-3 py-2 text-sm font-medium text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md">
360
+ <svg class="w-5 h-5 mr-3 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
361
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/>
362
+ </svg>
363
+ Logout
364
+ </a>
365
+ </nav>
370
366
  </div>
371
- <% end %>
367
+ </header>
372
368
 
373
369
  <!-- Main content -->
374
370
  <main class="max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-8 h-full">
@@ -16,18 +16,6 @@
16
16
  <% else %>
17
17
  <span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400">Deleted</span>
18
18
  <% end %>
19
- <% if agent[:agent_type] && agent[:agent_type] != "agent" %>
20
- <span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium
21
- <%= case agent[:agent_type]
22
- when 'embedder' then 'bg-purple-100 dark:bg-purple-900/50 text-purple-800 dark:text-purple-300'
23
- when 'moderator' then 'bg-orange-100 dark:bg-orange-900/50 text-orange-800 dark:text-orange-300'
24
- when 'speaker' then 'bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-300'
25
- when 'transcriber' then 'bg-pink-100 dark:bg-pink-900/50 text-pink-800 dark:text-pink-300'
26
- else ''
27
- end %>">
28
- <%= agent[:agent_type].capitalize %>
29
- </span>
30
- <% end %>
31
19
  </div>
32
20
  <!-- Desktop: model -->
33
21
  <span class="hidden sm:block text-sm text-gray-500 dark:text-gray-400"><%= agent[:model] %></span>
@@ -0,0 +1,56 @@
1
+ <%#
2
+ Sortable column header component for agents table
3
+
4
+ Creates a clickable table header that toggles sort direction and
5
+ displays visual feedback for the current sort state.
6
+
7
+ Usage:
8
+ render "ruby_llm/agents/agents/sortable_header",
9
+ column: "name",
10
+ label: "Name",
11
+ current_sort: @sort_params[:column],
12
+ current_direction: @sort_params[:direction],
13
+ align: "right",
14
+ th_class: "hidden lg:table-cell"
15
+
16
+ Parameters:
17
+ column - (String) The column name used in sort URL parameter
18
+ label - (String) Display text for the header
19
+ current_sort - (String) Currently active sort column
20
+ current_direction - (String) Current sort direction ('asc' or 'desc')
21
+ align - (String) Text alignment: 'left' (default), 'center', or 'right'
22
+ th_class - (String) Additional CSS classes for the th element (e.g., responsive hiding)
23
+ %>
24
+ <%
25
+ align = local_assigns[:align] || "left"
26
+ th_class = local_assigns[:th_class] || ""
27
+ is_active = column == current_sort
28
+ next_direction = is_active && current_direction == "asc" ? "desc" : "asc"
29
+ sort_url = url_for(request.query_parameters.merge(sort: column, direction: next_direction))
30
+
31
+ align_class = case align
32
+ when "right" then "text-right justify-end"
33
+ when "center" then "text-center justify-center"
34
+ else "text-left justify-start"
35
+ end
36
+ %>
37
+ <th scope="col" class="px-4 py-3 <%= th_class %>">
38
+ <a href="<%= sort_url %>"
39
+ class="group inline-flex items-center gap-1 <%= align_class %> w-full
40
+ text-xs font-semibold uppercase tracking-wider
41
+ <%= is_active ? 'text-blue-600 dark:text-blue-400' : 'text-gray-500 dark:text-gray-400' %>
42
+ hover:text-blue-600 dark:hover:text-blue-400 transition-colors">
43
+ <span><%= label %></span>
44
+ <span class="<%= is_active ? 'opacity-100' : 'opacity-0 group-hover:opacity-50' %> transition-opacity">
45
+ <% if is_active && current_direction == "asc" %>
46
+ <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
47
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"/>
48
+ </svg>
49
+ <% else %>
50
+ <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
51
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
52
+ </svg>
53
+ <% end %>
54
+ </span>
55
+ </a>
56
+ </th>
@@ -1,27 +1,17 @@
1
1
  <%
2
- workflow_colors = {
3
- "pipeline" => { border: "border-l-indigo-500", icon_bg: "bg-indigo-100 dark:bg-indigo-900/50", icon_text: "text-indigo-600 dark:text-indigo-300" },
4
- "parallel" => { border: "border-l-cyan-500", icon_bg: "bg-cyan-100 dark:bg-cyan-900/50", icon_text: "text-cyan-600 dark:text-cyan-300" },
5
- "router" => { border: "border-l-amber-500", icon_bg: "bg-amber-100 dark:bg-amber-900/50", icon_text: "text-amber-600 dark:text-amber-300" }
6
- }
7
- colors = workflow_colors[workflow[:workflow_type]] || { border: "border-l-gray-400", icon_bg: "bg-gray-100", icon_text: "text-gray-600" }
8
-
9
- child_label = case workflow[:workflow_type]
10
- when "pipeline" then "steps"
11
- when "parallel" then "branches"
12
- when "router" then "routes"
13
- else "children"
14
- end
2
+ # All workflows use unified emerald color scheme
3
+ colors = { border: "border-l-emerald-500", icon_bg: "bg-emerald-100 dark:bg-emerald-900/50", icon_text: "text-emerald-600 dark:text-emerald-300" }
4
+ child_label = "steps"
15
5
  %>
16
6
 
17
7
  <div x-data="{ expanded: false }" class="block bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-md transition-shadow border-l-4 <%= colors[:border] %>">
18
- <%= link_to ruby_llm_agents.workflow_path(ERB::Util.url_encode(workflow[:name]), tab: local_assigns[:current_tab], subtab: local_assigns[:current_subtab]), class: "block p-4 sm:p-5 relative z-10", data: { turbo: false }, style: "pointer-events: auto;" do %>
8
+ <%= link_to ruby_llm_agents.workflow_path(ERB::Util.url_encode(workflow[:name]), tab: local_assigns[:current_tab]), class: "block p-4 sm:p-5 relative z-10", data: { turbo: false }, style: "pointer-events: auto;" do %>
19
9
  <!-- Header Row -->
20
10
  <div class="flex items-center justify-between gap-2">
21
11
  <div class="flex items-center gap-2 min-w-0">
22
12
  <%= render "ruby_llm/agents/shared/workflow_type_badge", workflow_type: workflow[:workflow_type], size: :sm %>
23
13
  <h3 class="font-semibold text-gray-900 dark:text-gray-100 truncate">
24
- <% name_parts = workflow[:name].gsub(/Workflow$/, '').gsub(/Pipeline$|Parallel$|Router$/, '').split('::') %>
14
+ <% name_parts = workflow[:name].split('::') %>
25
15
  <% if name_parts.length > 1 %>
26
16
  <span class="text-gray-400 dark:text-gray-500 font-normal"><%= name_parts[0..-2].join('::') %>::</span>
27
17
  <% end %>