ruby_llm-agents 0.3.4 → 0.3.5

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 (84) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +132 -1263
  3. data/app/controllers/concerns/ruby_llm/agents/filterable.rb +5 -1
  4. data/app/controllers/concerns/ruby_llm/agents/paginatable.rb +2 -1
  5. data/app/controllers/ruby_llm/agents/agents_controller.rb +21 -2
  6. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +80 -16
  7. data/app/controllers/ruby_llm/agents/executions_controller.rb +83 -5
  8. data/app/models/ruby_llm/agents/execution/analytics.rb +17 -27
  9. data/app/models/ruby_llm/agents/execution/scopes.rb +25 -0
  10. data/app/models/ruby_llm/agents/execution/workflow.rb +299 -0
  11. data/app/models/ruby_llm/agents/execution.rb +9 -1
  12. data/app/models/ruby_llm/agents/tenant_budget.rb +165 -0
  13. data/app/services/ruby_llm/agents/agent_registry.rb +118 -7
  14. data/app/views/layouts/{rubyllm → ruby_llm}/agents/application.html.erb +91 -29
  15. data/app/views/ruby_llm/agents/agents/_empty_state.html.erb +23 -0
  16. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +125 -0
  17. data/app/views/ruby_llm/agents/agents/index.html.erb +93 -0
  18. data/app/views/{rubyllm → ruby_llm}/agents/agents/show.html.erb +77 -20
  19. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +112 -0
  20. data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_execution_item.html.erb +7 -4
  21. data/app/views/ruby_llm/agents/dashboard/_tenant_budget.html.erb +115 -0
  22. data/app/views/{rubyllm → ruby_llm}/agents/dashboard/index.html.erb +9 -6
  23. data/app/views/{rubyllm → ruby_llm}/agents/executions/_execution.html.erb +1 -1
  24. data/app/views/{rubyllm → ruby_llm}/agents/executions/_filters.html.erb +39 -11
  25. data/app/views/{rubyllm → ruby_llm}/agents/executions/_list.html.erb +19 -9
  26. data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +101 -0
  27. data/app/views/ruby_llm/agents/executions/index.html.erb +88 -0
  28. data/app/views/{rubyllm → ruby_llm}/agents/executions/show.html.erb +137 -126
  29. data/app/views/{rubyllm → ruby_llm}/agents/shared/_breadcrumbs.html.erb +2 -2
  30. data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +251 -0
  31. data/app/views/{rubyllm → ruby_llm}/agents/shared/_filter_dropdown.html.erb +1 -1
  32. data/app/views/{rubyllm → ruby_llm}/agents/shared/_select_dropdown.html.erb +1 -1
  33. data/app/views/{rubyllm → ruby_llm}/agents/shared/_status_badge.html.erb +1 -1
  34. data/app/views/{rubyllm → ruby_llm}/agents/shared/_status_dot.html.erb +1 -1
  35. data/app/views/ruby_llm/agents/shared/_tenant_filter.html.erb +26 -0
  36. data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +61 -0
  37. data/lib/generators/ruby_llm_agents/multi_tenancy_generator.rb +97 -0
  38. data/lib/generators/ruby_llm_agents/templates/add_attempts_migration.rb.tt +3 -3
  39. data/lib/generators/ruby_llm_agents/templates/add_tenant_to_executions_migration.rb.tt +23 -0
  40. data/lib/generators/ruby_llm_agents/templates/add_tool_calls_migration.rb.tt +2 -2
  41. data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +38 -0
  42. data/lib/generators/ruby_llm_agents/templates/create_tenant_budgets_migration.rb.tt +45 -0
  43. data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +17 -5
  44. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +13 -0
  45. data/lib/ruby_llm/agents/alert_manager.rb +20 -16
  46. data/lib/ruby_llm/agents/base/caching.rb +4 -7
  47. data/lib/ruby_llm/agents/base/cost_calculation.rb +5 -3
  48. data/lib/ruby_llm/agents/base/execution.rb +61 -9
  49. data/lib/ruby_llm/agents/base/reliability_execution.rb +14 -9
  50. data/lib/ruby_llm/agents/base.rb +26 -0
  51. data/lib/ruby_llm/agents/budget_tracker.rb +250 -139
  52. data/lib/ruby_llm/agents/cache_helper.rb +98 -0
  53. data/lib/ruby_llm/agents/circuit_breaker.rb +48 -30
  54. data/lib/ruby_llm/agents/configuration.rb +40 -1
  55. data/lib/ruby_llm/agents/engine.rb +65 -1
  56. data/lib/ruby_llm/agents/inflections.rb +14 -0
  57. data/lib/ruby_llm/agents/instrumentation.rb +66 -0
  58. data/lib/ruby_llm/agents/reliability.rb +8 -2
  59. data/lib/ruby_llm/agents/version.rb +1 -1
  60. data/lib/ruby_llm/agents/workflow/instrumentation.rb +254 -0
  61. data/lib/ruby_llm/agents/workflow/parallel.rb +282 -0
  62. data/lib/ruby_llm/agents/workflow/pipeline.rb +306 -0
  63. data/lib/ruby_llm/agents/workflow/result.rb +390 -0
  64. data/lib/ruby_llm/agents/workflow/router.rb +429 -0
  65. data/lib/ruby_llm/agents/workflow.rb +232 -0
  66. data/lib/ruby_llm/agents.rb +1 -0
  67. metadata +50 -60
  68. data/app/views/rubyllm/agents/agents/index.html.erb +0 -20
  69. data/app/views/rubyllm/agents/dashboard/_agent_comparison.html.erb +0 -46
  70. data/app/views/rubyllm/agents/executions/index.html.erb +0 -28
  71. data/app/views/rubyllm/agents/executions/index.turbo_stream.erb +0 -18
  72. data/app/views/rubyllm/agents/shared/_executions_table.html.erb +0 -193
  73. /data/app/views/{rubyllm → ruby_llm}/agents/agents/_agent.html.erb +0 -0
  74. /data/app/views/{rubyllm → ruby_llm}/agents/agents/_version_comparison.html.erb +0 -0
  75. /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_action_center.html.erb +0 -0
  76. /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_alerts_feed.html.erb +0 -0
  77. /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_breaker_strip.html.erb +0 -0
  78. /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_budgets_bar.html.erb +0 -0
  79. /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_now_strip.html.erb +0 -0
  80. /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_top_errors.html.erb +0 -0
  81. /data/app/views/{rubyllm → ruby_llm}/agents/executions/dry_run.html.erb +0 -0
  82. /data/app/views/{rubyllm → ruby_llm}/agents/settings/show.html.erb +0 -0
  83. /data/app/views/{rubyllm → ruby_llm}/agents/shared/_nav_link.html.erb +0 -0
  84. /data/app/views/{rubyllm → ruby_llm}/agents/shared/_stat_card.html.erb +0 -0
@@ -0,0 +1,251 @@
1
+ <%# Show tenant column when multi-tenancy is enabled and no specific tenant is selected %>
2
+ <% show_tenant_column = tenant_filter_enabled? && current_tenant_id.blank? %>
3
+
4
+ <% if executions.empty? %>
5
+ <p class="text-gray-500 dark:text-gray-400 text-center py-8">No executions found.</p>
6
+ <% else %>
7
+ <div class="overflow-x-auto">
8
+ <table class="min-w-full text-sm">
9
+ <thead class="bg-gray-50 dark:bg-gray-900/50">
10
+ <tr>
11
+ <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Status</th>
12
+ <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Agent</th>
13
+ <% if show_tenant_column %>
14
+ <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Tenant</th>
15
+ <% end %>
16
+ <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Model</th>
17
+ <th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Duration</th>
18
+ <th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Tokens</th>
19
+ <th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Cost</th>
20
+ <th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Time</th>
21
+ </tr>
22
+ </thead>
23
+ <tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-100 dark:divide-gray-700">
24
+ <% executions.each do |execution| %>
25
+ <%
26
+ is_workflow = execution.workflow_type.present?
27
+ children = execution.child_executions.sort_by(&:created_at)
28
+ has_children = children.any?
29
+ %>
30
+
31
+ <%# Parent/Main Row %>
32
+ <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
33
+ <%# Status %>
34
+ <td class="px-4 py-3 whitespace-nowrap">
35
+ <%= render "ruby_llm/agents/shared/status_badge", status: execution.status, size: :sm %>
36
+ </td>
37
+
38
+ <%# Agent Name with Workflow Badge %>
39
+ <td class="px-4 py-3 whitespace-nowrap">
40
+ <div class="flex items-center gap-2">
41
+ <%= link_to ruby_llm_agents.execution_path(execution), class: "font-medium text-gray-900 dark:text-gray-100 hover:text-blue-600 dark:hover:text-blue-400" do %>
42
+ <%= execution.agent_type.gsub(/Agent$/, "") %>
43
+ <% end %>
44
+ <% if is_workflow %>
45
+ <% badge_style = case execution.workflow_type
46
+ when "pipeline" then "text-indigo-600 dark:text-indigo-400"
47
+ when "parallel" then "text-cyan-600 dark:text-cyan-400"
48
+ when "router" then "text-amber-600 dark:text-amber-400"
49
+ end %>
50
+ <% badge_icon = case execution.workflow_type
51
+ when "pipeline" then "→"
52
+ when "parallel" then "⫴"
53
+ when "router" then "⑂"
54
+ end %>
55
+ <span class="text-xs <%= badge_style %>"><%= badge_icon %></span>
56
+ <% end %>
57
+ </div>
58
+ </td>
59
+
60
+ <%# Tenant (only when viewing all tenants) %>
61
+ <% if show_tenant_column %>
62
+ <td class="px-4 py-3 whitespace-nowrap">
63
+ <% if execution.tenant_id.present? %>
64
+ <a href="<%= url_for(request.query_parameters.merge(tenant_id: execution.tenant_id)) %>"
65
+ class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 hover:bg-blue-100 dark:hover:bg-blue-900/50">
66
+ <%= truncate(execution.tenant_id, length: 15) %>
67
+ </a>
68
+ <% else %>
69
+ <span class="text-gray-400 dark:text-gray-500 text-xs">—</span>
70
+ <% end %>
71
+ </td>
72
+ <% end %>
73
+
74
+ <%# Model %>
75
+ <td class="px-4 py-3 whitespace-nowrap text-gray-500 dark:text-gray-400 font-mono text-xs">
76
+ <%= execution.model_id %>
77
+ </td>
78
+
79
+ <%# Duration %>
80
+ <td class="px-4 py-3 whitespace-nowrap text-right text-gray-900 dark:text-gray-100 tabular-nums">
81
+ <%= execution.duration_ms ? "#{number_with_delimiter(execution.duration_ms)}ms" : "-" %>
82
+ </td>
83
+
84
+ <%# Tokens %>
85
+ <td class="px-4 py-3 whitespace-nowrap text-right text-gray-900 dark:text-gray-100 tabular-nums">
86
+ <%= number_with_delimiter(execution.total_tokens || 0) %>
87
+ </td>
88
+
89
+ <%# Cost %>
90
+ <td class="px-4 py-3 whitespace-nowrap text-right text-gray-900 dark:text-gray-100 tabular-nums">
91
+ $<%= number_with_precision(execution.total_cost || 0, precision: 4) %>
92
+ </td>
93
+
94
+ <%# Time %>
95
+ <td class="px-4 py-3 whitespace-nowrap text-right text-gray-500 dark:text-gray-400">
96
+ <%= time_ago_in_words(execution.created_at) %> ago
97
+ </td>
98
+ </tr>
99
+
100
+ <%# Error Row (if applicable) %>
101
+ <% if execution.status_error? && execution.error_message.present? %>
102
+ <tr class="bg-red-50/50 dark:bg-red-900/20">
103
+ <td></td>
104
+ <td colspan="<%= show_tenant_column ? 7 : 6 %>" class="px-4 py-2">
105
+ <p class="text-xs text-red-600 dark:text-red-400">
106
+ <span class="font-medium"><%= execution.error_class %>:</span>
107
+ <%= truncate(execution.error_message, length: 120) %>
108
+ </p>
109
+ </td>
110
+ </tr>
111
+ <% end %>
112
+
113
+ <%# Child Rows (for workflows) %>
114
+ <% if has_children %>
115
+ <% children.each_with_index do |child, index| %>
116
+ <% is_last = index == children.size - 1 %>
117
+ <tr class="bg-gray-50/50 dark:bg-gray-900/30 hover:bg-gray-100/50 dark:hover:bg-gray-800/50 transition-colors">
118
+ <%# Status with tree line %>
119
+ <td class="px-4 py-2 whitespace-nowrap">
120
+ <div class="flex items-center">
121
+ <span class="text-gray-300 dark:text-gray-600 mr-2 font-mono text-xs"><%= is_last ? "└─" : "├─" %></span>
122
+ <% case child.status
123
+ when "success" %>
124
+ <span class="text-green-600 dark:text-green-400">✓</span>
125
+ <% when "error" %>
126
+ <span class="text-red-600 dark:text-red-400">✗</span>
127
+ <% when "timeout" %>
128
+ <span class="text-orange-600 dark:text-orange-400">⏱</span>
129
+ <% when "running" %>
130
+ <span class="text-blue-600 dark:text-blue-400 animate-pulse">●</span>
131
+ <% else %>
132
+ <span class="text-gray-400">○</span>
133
+ <% end %>
134
+ </div>
135
+ </td>
136
+
137
+ <%# Step/Branch Name %>
138
+ <td class="px-4 py-2 whitespace-nowrap">
139
+ <%= link_to ruby_llm_agents.execution_path(child), class: "text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400" do %>
140
+ <% if execution.pipeline_workflow? %>
141
+ <span class="text-gray-400 dark:text-gray-500 text-xs"><%= index + 1 %>.</span>
142
+ <% end %>
143
+ <%= child.workflow_step || child.agent_type.gsub(/Agent$/, "") %>
144
+ <% end %>
145
+ </td>
146
+
147
+ <%# Tenant - empty for child rows %>
148
+ <% if show_tenant_column %>
149
+ <td class="px-4 py-2"></td>
150
+ <% end %>
151
+
152
+ <%# Model %>
153
+ <td class="px-4 py-2 whitespace-nowrap text-gray-400 dark:text-gray-500 font-mono text-xs">
154
+ <%= child.model_id %>
155
+ </td>
156
+
157
+ <%# Duration %>
158
+ <td class="px-4 py-2 whitespace-nowrap text-right text-gray-600 dark:text-gray-300 tabular-nums text-xs">
159
+ <%= child.duration_ms ? "#{number_with_delimiter(child.duration_ms)}ms" : "-" %>
160
+ </td>
161
+
162
+ <%# Tokens %>
163
+ <td class="px-4 py-2 whitespace-nowrap text-right text-gray-600 dark:text-gray-300 tabular-nums text-xs">
164
+ <%= number_with_delimiter(child.total_tokens || 0) %>
165
+ </td>
166
+
167
+ <%# Cost %>
168
+ <td class="px-4 py-2 whitespace-nowrap text-right text-gray-600 dark:text-gray-300 tabular-nums text-xs">
169
+ $<%= number_with_precision(child.total_cost || 0, precision: 4) %>
170
+ </td>
171
+
172
+ <%# Time - empty for children %>
173
+ <td class="px-4 py-2"></td>
174
+ </tr>
175
+
176
+ <%# Child Error Row %>
177
+ <% if child.status_error? && child.error_message.present? %>
178
+ <tr class="bg-red-50/30 dark:bg-red-900/10">
179
+ <td></td>
180
+ <td colspan="<%= show_tenant_column ? 7 : 6 %>" class="px-4 py-1.5 pl-8">
181
+ <p class="text-xs text-red-500 dark:text-red-400">
182
+ <%= truncate(child.error_message, length: 100) %>
183
+ </p>
184
+ </td>
185
+ </tr>
186
+ <% end %>
187
+ <% end %>
188
+ <% end %>
189
+ <% end %>
190
+ </tbody>
191
+ </table>
192
+ </div>
193
+
194
+ <%# Pagination %>
195
+ <% if pagination[:total_pages] > 1 %>
196
+ <%
197
+ current_page = pagination[:current_page]
198
+ total_pages = pagination[:total_pages]
199
+ total_count = pagination[:total_count]
200
+ per_page = pagination[:per_page]
201
+
202
+ from_record = ((current_page - 1) * per_page) + 1
203
+ to_record = [current_page * per_page, total_count].min
204
+ %>
205
+ <div class="mt-4 flex items-center justify-between border-t border-gray-100 dark:border-gray-700 pt-4">
206
+ <p class="text-sm text-gray-500 dark:text-gray-400">
207
+ Showing <%= from_record %>-<%= to_record %> of <%= number_with_delimiter(total_count) %> executions
208
+ </p>
209
+ <nav class="flex items-center space-x-1">
210
+ <% if current_page > 1 %>
211
+ <%= link_to "Previous", url_for(request.query_parameters.merge(page: current_page - 1)), class: "px-3 py-1.5 text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700" %>
212
+ <% else %>
213
+ <span class="px-3 py-1.5 text-sm font-medium text-gray-400 dark:text-gray-500 bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md cursor-not-allowed">Previous</span>
214
+ <% end %>
215
+
216
+ <%
217
+ window = 2
218
+ left_edge = 1
219
+ right_edge = 1
220
+
221
+ pages_to_show = []
222
+ (1..total_pages).each do |page|
223
+ if page <= left_edge ||
224
+ page > total_pages - right_edge ||
225
+ (page >= current_page - window && page <= current_page + window)
226
+ pages_to_show << page
227
+ elsif pages_to_show.last != :gap
228
+ pages_to_show << :gap
229
+ end
230
+ end
231
+ %>
232
+
233
+ <% pages_to_show.each do |page| %>
234
+ <% if page == :gap %>
235
+ <span class="px-2 py-1.5 text-sm text-gray-500 dark:text-gray-400">...</span>
236
+ <% elsif page == current_page %>
237
+ <span class="px-3 py-1.5 text-sm font-medium text-white bg-blue-600 border border-blue-600 rounded-md"><%= page %></span>
238
+ <% else %>
239
+ <%= link_to page, url_for(request.query_parameters.merge(page: page)), class: "px-3 py-1.5 text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700" %>
240
+ <% end %>
241
+ <% end %>
242
+
243
+ <% if current_page < total_pages %>
244
+ <%= link_to "Next", url_for(request.query_parameters.merge(page: current_page + 1)), class: "px-3 py-1.5 text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700" %>
245
+ <% else %>
246
+ <span class="px-3 py-1.5 text-sm font-medium text-gray-400 dark:text-gray-500 bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md cursor-not-allowed">Next</span>
247
+ <% end %>
248
+ </nav>
249
+ </div>
250
+ <% end %>
251
+ <% end %>
@@ -1,7 +1,7 @@
1
1
  <%
2
2
  # Multi-select filter dropdown with Alpine.js
3
3
  # Usage:
4
- # render "rubyllm/agents/shared/filter_dropdown",
4
+ # render "ruby_llm/agents/shared/filter_dropdown",
5
5
  # name: "statuses[]",
6
6
  # filter_id: "statuses",
7
7
  # label: "Status",
@@ -1,7 +1,7 @@
1
1
  <%
2
2
  # Single-select filter dropdown with Alpine.js
3
3
  # Usage:
4
- # render "rubyllm/agents/shared/select_dropdown",
4
+ # render "ruby_llm/agents/shared/select_dropdown",
5
5
  # name: "days",
6
6
  # filter_id: "days",
7
7
  # options: [
@@ -1,6 +1,6 @@
1
1
  <%
2
2
  # Status badge with icon and color coding
3
- # Usage: render "rubyllm/agents/shared/status_badge", status: execution.status
3
+ # Usage: render "ruby_llm/agents/shared/status_badge", status: execution.status
4
4
  # Options: size: :sm (default), :md, :lg
5
5
 
6
6
  size = local_assigns[:size] || :sm
@@ -1,6 +1,6 @@
1
1
  <%
2
2
  # Simple status dot indicator
3
- # Usage: render "rubyllm/agents/shared/status_dot", status: execution.status
3
+ # Usage: render "ruby_llm/agents/shared/status_dot", status: execution.status
4
4
 
5
5
  config = case status.to_s
6
6
  when "running"
@@ -0,0 +1,26 @@
1
+ <% if tenant_filter_enabled? && available_tenants.any? %>
2
+ <div class="tenant-filter">
3
+ <%= form_with url: request.path, method: :get, local: true, class: "tenant-filter-form" do |f| %>
4
+ <label for="tenant_id">Tenant:</label>
5
+ <select name="tenant_id" id="tenant_id" onchange="this.form.submit()">
6
+ <option value="">All Tenants</option>
7
+ <% available_tenants.each do |tenant| %>
8
+ <option value="<%= tenant %>" <%= 'selected' if tenant == current_tenant_id %>>
9
+ <%= tenant %>
10
+ </option>
11
+ <% end %>
12
+ </select>
13
+
14
+ <%# Preserve other filter params %>
15
+ <% params.except(:tenant_id, :controller, :action).each do |key, value| %>
16
+ <% if value.is_a?(Array) %>
17
+ <% value.each do |v| %>
18
+ <input type="hidden" name="<%= key %>[]" value="<%= v %>">
19
+ <% end %>
20
+ <% else %>
21
+ <input type="hidden" name="<%= key %>" value="<%= value %>">
22
+ <% end %>
23
+ <% end %>
24
+ <% end %>
25
+ </div>
26
+ <% end %>
@@ -0,0 +1,61 @@
1
+ <%
2
+ # Workflow type badge with icon and color coding
3
+ # Usage: render "ruby_llm/agents/shared/workflow_type_badge", workflow_type: "pipeline"
4
+ # Options:
5
+ # size: :xs, :sm (default), :md
6
+ # show_label: true (default) or false for icon-only mode
7
+
8
+ workflow_type = local_assigns[:workflow_type]
9
+ size = local_assigns[:size] || :sm
10
+ show_label = local_assigns.fetch(:show_label, true)
11
+
12
+ config = case workflow_type.to_s
13
+ when "pipeline"
14
+ {
15
+ icon: "arrow-right",
16
+ label: "Pipeline",
17
+ bg: "bg-indigo-100 dark:bg-indigo-900/50",
18
+ text: "text-indigo-700 dark:text-indigo-300",
19
+ icon_char: "→"
20
+ }
21
+ when "parallel"
22
+ {
23
+ icon: "parallel",
24
+ label: "Parallel",
25
+ bg: "bg-cyan-100 dark:bg-cyan-900/50",
26
+ text: "text-cyan-700 dark:text-cyan-300",
27
+ icon_char: "⫿"
28
+ }
29
+ when "router"
30
+ {
31
+ icon: "router",
32
+ label: "Router",
33
+ bg: "bg-amber-100 dark:bg-amber-900/50",
34
+ text: "text-amber-700 dark:text-amber-300",
35
+ icon_char: "⌂"
36
+ }
37
+ else
38
+ {
39
+ icon: "workflow",
40
+ label: "Workflow",
41
+ bg: "bg-gray-100 dark:bg-gray-700",
42
+ text: "text-gray-700 dark:text-gray-300",
43
+ icon_char: "⚙"
44
+ }
45
+ end
46
+
47
+ size_classes = case size
48
+ when :xs
49
+ { badge: "px-1.5 py-0.5", icon: "text-[10px]", text: "text-[10px]" }
50
+ when :md
51
+ { badge: "px-2.5 py-1", icon: "text-sm", text: "text-sm" }
52
+ else # :sm
53
+ { badge: "px-2 py-0.5", icon: "text-xs", text: "text-xs" }
54
+ end
55
+ %>
56
+ <span class="inline-flex items-center gap-1 rounded-md font-medium <%= config[:bg] %> <%= config[:text] %> <%= size_classes[:badge] %>">
57
+ <span class="<%= size_classes[:icon] %>" aria-hidden="true"><%= config[:icon_char] %></span>
58
+ <% if show_label %>
59
+ <span class="<%= size_classes[:text] %>"><%= config[:label] %></span>
60
+ <% end %>
61
+ </span>
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module RubyLlmAgents
7
+ # Multi-tenancy generator for ruby_llm-agents
8
+ #
9
+ # Usage:
10
+ # rails generate ruby_llm_agents:multi_tenancy
11
+ #
12
+ # This will create migrations for:
13
+ # - ruby_llm_agents_tenant_budgets table for per-tenant budget configuration
14
+ # - Adding tenant_id column to ruby_llm_agents_executions
15
+ #
16
+ class MultiTenancyGenerator < ::Rails::Generators::Base
17
+ include ::ActiveRecord::Generators::Migration
18
+
19
+ source_root File.expand_path("templates", __dir__)
20
+
21
+ desc "Adds multi-tenancy support to RubyLLM::Agents"
22
+
23
+ def create_tenant_budgets_migration
24
+ if table_exists?(:ruby_llm_agents_tenant_budgets)
25
+ say_status :skip, "ruby_llm_agents_tenant_budgets table already exists", :yellow
26
+ return
27
+ end
28
+
29
+ migration_template(
30
+ "create_tenant_budgets_migration.rb.tt",
31
+ File.join(db_migrate_path, "create_ruby_llm_agents_tenant_budgets.rb")
32
+ )
33
+ end
34
+
35
+ def create_add_tenant_to_executions_migration
36
+ if column_exists?(:ruby_llm_agents_executions, :tenant_id)
37
+ say_status :skip, "tenant_id column already exists", :yellow
38
+ return
39
+ end
40
+
41
+ migration_template(
42
+ "add_tenant_to_executions_migration.rb.tt",
43
+ File.join(db_migrate_path, "add_tenant_id_to_ruby_llm_agents_executions.rb")
44
+ )
45
+ end
46
+
47
+ def show_post_install_message
48
+ say ""
49
+ say "Multi-tenancy migrations created!", :green
50
+ say ""
51
+ say "Next steps:"
52
+ say " 1. Run: rails db:migrate"
53
+ say " 2. Configure multi-tenancy in your initializer:"
54
+ say ""
55
+ say " RubyLLM::Agents.configure do |config|"
56
+ say " config.multi_tenancy_enabled = true"
57
+ say " config.tenant_resolver = -> { Current.tenant&.id }"
58
+ say " end"
59
+ say ""
60
+ say " 3. Set Current.tenant in your ApplicationController"
61
+ say ""
62
+ say " 4. Create tenant budgets:"
63
+ say ""
64
+ say " RubyLLM::Agents::TenantBudget.create!("
65
+ say " tenant_id: 'acme_corp',"
66
+ say " daily_limit: 50.0,"
67
+ say " monthly_limit: 500.0,"
68
+ say " enforcement: 'hard'"
69
+ say " )"
70
+ say ""
71
+ end
72
+
73
+ private
74
+
75
+ def migration_version
76
+ "[#{::ActiveRecord::VERSION::STRING.to_f}]"
77
+ end
78
+
79
+ def db_migrate_path
80
+ "db/migrate"
81
+ end
82
+
83
+ def table_exists?(table)
84
+ ActiveRecord::Base.connection.table_exists?(table)
85
+ rescue StandardError
86
+ false
87
+ end
88
+
89
+ def column_exists?(table, column)
90
+ return false unless ActiveRecord::Base.connection.table_exists?(table)
91
+
92
+ ActiveRecord::Base.connection.column_exists?(table, column)
93
+ rescue StandardError
94
+ false
95
+ end
96
+ end
97
+ end
@@ -8,8 +8,8 @@
8
8
  # Run with: rails db:migrate
9
9
  class AddAttemptsToRubyLLMAgentsExecutions < ActiveRecord::Migration<%= migration_version %>
10
10
  def change
11
- # Add attempts JSONB array for storing per-attempt details
12
- add_column :ruby_llm_agents_executions, :attempts, :jsonb, null: false, default: []
11
+ # Add attempts JSON array for storing per-attempt details
12
+ add_column :ruby_llm_agents_executions, :attempts, :json, null: false, default: []
13
13
 
14
14
  # Add counter for quick access to attempt count
15
15
  add_column :ruby_llm_agents_executions, :attempts_count, :integer, null: false, default: 0
@@ -18,7 +18,7 @@ class AddAttemptsToRubyLLMAgentsExecutions < ActiveRecord::Migration<%= migratio
18
18
  add_column :ruby_llm_agents_executions, :chosen_model_id, :string
19
19
 
20
20
  # Add fallback chain (list of models that were configured to try)
21
- add_column :ruby_llm_agents_executions, :fallback_chain, :jsonb, null: false, default: []
21
+ add_column :ruby_llm_agents_executions, :fallback_chain, :json, null: false, default: []
22
22
 
23
23
  # Add indexes for common queries
24
24
  add_index :ruby_llm_agents_executions, :attempts_count
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Migration to add tenant_id column to executions for multi-tenancy support
4
+ #
5
+ # This migration adds a tenant_id column to track which tenant each execution
6
+ # belongs to, enabling:
7
+ # - Filtering executions by tenant
8
+ # - Tenant-scoped analytics and reporting
9
+ # - Per-tenant budget tracking and circuit breakers
10
+ #
11
+ # Run with: rails db:migrate
12
+ class AddTenantIdToRubyLLMAgentsExecutions < ActiveRecord::Migration<%= migration_version %>
13
+ def change
14
+ # Add tenant_id column (nullable for backward compatibility)
15
+ add_column :ruby_llm_agents_executions, :tenant_id, :string
16
+
17
+ # Add indexes for efficient tenant-scoped queries
18
+ add_index :ruby_llm_agents_executions, :tenant_id
19
+ add_index :ruby_llm_agents_executions, [:tenant_id, :created_at]
20
+ add_index :ruby_llm_agents_executions, [:tenant_id, :agent_type]
21
+ add_index :ruby_llm_agents_executions, [:tenant_id, :status]
22
+ end
23
+ end
@@ -15,9 +15,9 @@
15
15
  # Run with: rails db:migrate
16
16
  class AddToolCallsToRubyLLMAgentsExecutions < ActiveRecord::Migration<%= migration_version %>
17
17
  def change
18
- # Add tool_calls JSONB array for storing tool call details
18
+ # Add tool_calls JSON array for storing tool call details
19
19
  # Each tool call contains: id, name, arguments
20
- add_column :ruby_llm_agents_executions, :tool_calls, :jsonb, null: false, default: []
20
+ add_column :ruby_llm_agents_executions, :tool_calls, :json, null: false, default: []
21
21
 
22
22
  # Add counter for quick access to tool call count
23
23
  add_column :ruby_llm_agents_executions, :tool_calls_count, :integer, null: false, default: 0
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Migration to add workflow orchestration columns to executions
4
+ #
5
+ # This migration adds columns for tracking workflow executions (Pipeline,
6
+ # Parallel, Router patterns) and linking child executions to their
7
+ # parent workflow.
8
+ #
9
+ # Workflow patterns supported:
10
+ # - Pipeline: Sequential execution with data flowing between steps
11
+ # - Parallel: Concurrent execution with result aggregation
12
+ # - Router: Conditional dispatch based on classification
13
+ #
14
+ # Run with: rails db:migrate
15
+ class AddWorkflowToRubyLLMAgentsExecutions < ActiveRecord::Migration<%= migration_version %>
16
+ def change
17
+ # Unique identifier for the workflow execution
18
+ # All steps/branches share the same workflow_id
19
+ add_column :ruby_llm_agents_executions, :workflow_id, :string
20
+
21
+ # Type of workflow: "pipeline", "parallel", "router", or nil for regular agents
22
+ add_column :ruby_llm_agents_executions, :workflow_type, :string
23
+
24
+ # Name of the step/branch within the workflow
25
+ add_column :ruby_llm_agents_executions, :workflow_step, :string
26
+
27
+ # For routers: the route that was selected
28
+ add_column :ruby_llm_agents_executions, :routed_to, :string
29
+
30
+ # For routers: classification details (route, method, time)
31
+ add_column :ruby_llm_agents_executions, :classification_result, :json
32
+
33
+ # Add indexes for efficient querying
34
+ add_index :ruby_llm_agents_executions, :workflow_id
35
+ add_index :ruby_llm_agents_executions, :workflow_type
36
+ add_index :ruby_llm_agents_executions, [:workflow_id, :workflow_step]
37
+ end
38
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Migration to create the tenant_budgets table for multi-tenancy support
4
+ #
5
+ # This table stores per-tenant budget configuration, allowing different
6
+ # tenants to have their own budget limits and enforcement modes.
7
+ #
8
+ # Features:
9
+ # - Per-tenant daily and monthly budget limits
10
+ # - Per-agent budget limits within a tenant
11
+ # - Configurable enforcement mode (none, soft, hard)
12
+ # - Option to inherit global defaults for unset limits
13
+ #
14
+ # Run with: rails db:migrate
15
+ class CreateRubyLLMAgentsTenantBudgets < ActiveRecord::Migration<%= migration_version %>
16
+ def change
17
+ create_table :ruby_llm_agents_tenant_budgets do |t|
18
+ # Unique identifier for the tenant (e.g., organization ID, workspace ID)
19
+ t.string :tenant_id, null: false
20
+
21
+ # Global budget limits for this tenant
22
+ t.decimal :daily_limit, precision: 12, scale: 6
23
+ t.decimal :monthly_limit, precision: 12, scale: 6
24
+
25
+ # Per-agent budget limits (JSON hash)
26
+ # Format: { "AgentName" => limit_value }
27
+ t.json :per_agent_daily, null: false, default: {}
28
+ t.json :per_agent_monthly, null: false, default: {}
29
+
30
+ # Enforcement mode for this tenant: "none", "soft", or "hard"
31
+ # - none: no enforcement, only tracking
32
+ # - soft: log warnings when limits exceeded
33
+ # - hard: block execution when limits exceeded
34
+ t.string :enforcement, default: "soft"
35
+
36
+ # Whether to inherit from global config for unset limits
37
+ t.boolean :inherit_global_defaults, default: true
38
+
39
+ t.timestamps
40
+ end
41
+
42
+ # Ensure unique tenant IDs
43
+ add_index :ruby_llm_agents_tenant_budgets, :tenant_id, unique: true
44
+ end
45
+ end
@@ -54,10 +54,10 @@ class CreateRubyLLMAgentsExecutions < ActiveRecord::Migration<%= migration_versi
54
54
  t.decimal :output_cost, precision: 12, scale: 6
55
55
  t.decimal :total_cost, precision: 12, scale: 6
56
56
 
57
- # Data (JSONB for PostgreSQL, JSON for others)
58
- t.jsonb :parameters, null: false, default: {}
59
- t.jsonb :response, default: {}
60
- t.jsonb :metadata, null: false, default: {}
57
+ # Data (JSON - works with PostgreSQL, MySQL, SQLite3)
58
+ t.json :parameters, null: false, default: {}
59
+ t.json :response, default: {}
60
+ t.json :metadata, null: false, default: {}
61
61
 
62
62
  # Error tracking
63
63
  t.string :error_class
@@ -68,9 +68,16 @@ class CreateRubyLLMAgentsExecutions < ActiveRecord::Migration<%= migration_versi
68
68
  t.text :user_prompt
69
69
 
70
70
  # Tool calls tracking
71
- t.jsonb :tool_calls, null: false, default: []
71
+ t.json :tool_calls, null: false, default: []
72
72
  t.integer :tool_calls_count, null: false, default: 0
73
73
 
74
+ # Workflow orchestration
75
+ t.string :workflow_id
76
+ t.string :workflow_type
77
+ t.string :workflow_step
78
+ t.string :routed_to
79
+ t.json :classification_result
80
+
74
81
  t.timestamps
75
82
  end
76
83
 
@@ -96,6 +103,11 @@ class CreateRubyLLMAgentsExecutions < ActiveRecord::Migration<%= migration_versi
96
103
  # Tool calls index
97
104
  add_index :ruby_llm_agents_executions, :tool_calls_count
98
105
 
106
+ # Workflow indexes
107
+ add_index :ruby_llm_agents_executions, :workflow_id
108
+ add_index :ruby_llm_agents_executions, :workflow_type
109
+ add_index :ruby_llm_agents_executions, [:workflow_id, :workflow_step]
110
+
99
111
  # Foreign keys for execution hierarchy
100
112
  add_foreign_key :ruby_llm_agents_executions, :ruby_llm_agents_executions,
101
113
  column: :parent_execution_id, on_delete: :nullify