ruby_llm-agents 0.3.3 → 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 (97) 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 +137 -9
  7. data/app/controllers/ruby_llm/agents/executions_controller.rb +83 -5
  8. data/app/models/ruby_llm/agents/execution/analytics.rb +103 -12
  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 +28 -59
  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/ruby_llm/agents/application.html.erb +430 -0
  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/ruby_llm/agents/agents/show.html.erb +775 -0
  19. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +112 -0
  20. data/app/views/ruby_llm/agents/dashboard/_budgets_bar.html.erb +75 -0
  21. data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_execution_item.html.erb +7 -4
  22. data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +84 -0
  23. data/app/views/ruby_llm/agents/dashboard/_tenant_budget.html.erb +115 -0
  24. data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +49 -0
  25. data/app/views/ruby_llm/agents/dashboard/index.html.erb +155 -0
  26. data/app/views/{rubyllm → ruby_llm}/agents/executions/_execution.html.erb +1 -1
  27. data/app/views/{rubyllm → ruby_llm}/agents/executions/_filters.html.erb +39 -11
  28. data/app/views/{rubyllm → ruby_llm}/agents/executions/_list.html.erb +19 -9
  29. data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +101 -0
  30. data/app/views/ruby_llm/agents/executions/index.html.erb +88 -0
  31. data/app/views/{rubyllm → ruby_llm}/agents/executions/show.html.erb +260 -200
  32. data/app/views/{rubyllm → ruby_llm}/agents/settings/show.html.erb +1 -1
  33. data/app/views/ruby_llm/agents/shared/_breadcrumbs.html.erb +48 -0
  34. data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +251 -0
  35. data/app/views/{rubyllm → ruby_llm}/agents/shared/_filter_dropdown.html.erb +1 -1
  36. data/app/views/ruby_llm/agents/shared/_nav_link.html.erb +27 -0
  37. data/app/views/{rubyllm → ruby_llm}/agents/shared/_select_dropdown.html.erb +1 -1
  38. data/app/views/{rubyllm → ruby_llm}/agents/shared/_status_badge.html.erb +1 -1
  39. data/app/views/{rubyllm → ruby_llm}/agents/shared/_status_dot.html.erb +1 -1
  40. data/app/views/ruby_llm/agents/shared/_tenant_filter.html.erb +26 -0
  41. data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +61 -0
  42. data/config/routes.rb +2 -0
  43. data/lib/generators/ruby_llm_agents/multi_tenancy_generator.rb +97 -0
  44. data/lib/generators/ruby_llm_agents/templates/add_attempts_migration.rb.tt +3 -3
  45. data/lib/generators/ruby_llm_agents/templates/add_tenant_to_executions_migration.rb.tt +23 -0
  46. data/lib/generators/ruby_llm_agents/templates/add_tool_calls_migration.rb.tt +2 -2
  47. data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +38 -0
  48. data/lib/generators/ruby_llm_agents/templates/create_tenant_budgets_migration.rb.tt +45 -0
  49. data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +17 -5
  50. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +13 -0
  51. data/lib/ruby_llm/agents/alert_manager.rb +20 -16
  52. data/lib/ruby_llm/agents/base/caching.rb +40 -0
  53. data/lib/ruby_llm/agents/base/cost_calculation.rb +105 -0
  54. data/lib/ruby_llm/agents/base/dsl.rb +261 -0
  55. data/lib/ruby_llm/agents/base/execution.rb +258 -0
  56. data/lib/ruby_llm/agents/base/reliability_execution.rb +136 -0
  57. data/lib/ruby_llm/agents/base/response_building.rb +86 -0
  58. data/lib/ruby_llm/agents/base/tool_tracking.rb +57 -0
  59. data/lib/ruby_llm/agents/base.rb +37 -801
  60. data/lib/ruby_llm/agents/budget_tracker.rb +250 -139
  61. data/lib/ruby_llm/agents/cache_helper.rb +98 -0
  62. data/lib/ruby_llm/agents/circuit_breaker.rb +48 -30
  63. data/lib/ruby_llm/agents/configuration.rb +40 -1
  64. data/lib/ruby_llm/agents/engine.rb +65 -1
  65. data/lib/ruby_llm/agents/inflections.rb +14 -0
  66. data/lib/ruby_llm/agents/instrumentation.rb +66 -0
  67. data/lib/ruby_llm/agents/reliability.rb +8 -2
  68. data/lib/ruby_llm/agents/version.rb +1 -1
  69. data/lib/ruby_llm/agents/workflow/instrumentation.rb +254 -0
  70. data/lib/ruby_llm/agents/workflow/parallel.rb +282 -0
  71. data/lib/ruby_llm/agents/workflow/pipeline.rb +306 -0
  72. data/lib/ruby_llm/agents/workflow/result.rb +390 -0
  73. data/lib/ruby_llm/agents/workflow/router.rb +429 -0
  74. data/lib/ruby_llm/agents/workflow.rb +232 -0
  75. data/lib/ruby_llm/agents.rb +1 -0
  76. metadata +57 -75
  77. data/app/channels/ruby_llm/agents/executions_channel.rb +0 -46
  78. data/app/javascript/ruby_llm/agents/controllers/filter_controller.js +0 -56
  79. data/app/javascript/ruby_llm/agents/controllers/index.js +0 -12
  80. data/app/javascript/ruby_llm/agents/controllers/refresh_controller.js +0 -83
  81. data/app/views/layouts/rubyllm/agents/application.html.erb +0 -626
  82. data/app/views/rubyllm/agents/agents/index.html.erb +0 -20
  83. data/app/views/rubyllm/agents/agents/show.html.erb +0 -772
  84. data/app/views/rubyllm/agents/dashboard/_budgets_bar.html.erb +0 -165
  85. data/app/views/rubyllm/agents/dashboard/_now_strip.html.erb +0 -10
  86. data/app/views/rubyllm/agents/dashboard/_now_strip_values.html.erb +0 -71
  87. data/app/views/rubyllm/agents/dashboard/index.html.erb +0 -197
  88. data/app/views/rubyllm/agents/executions/index.html.erb +0 -28
  89. data/app/views/rubyllm/agents/executions/index.turbo_stream.erb +0 -18
  90. data/app/views/rubyllm/agents/shared/_executions_table.html.erb +0 -193
  91. /data/app/views/{rubyllm → ruby_llm}/agents/agents/_agent.html.erb +0 -0
  92. /data/app/views/{rubyllm → ruby_llm}/agents/agents/_version_comparison.html.erb +0 -0
  93. /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_action_center.html.erb +0 -0
  94. /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_alerts_feed.html.erb +0 -0
  95. /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_breaker_strip.html.erb +0 -0
  96. /data/app/views/{rubyllm → ruby_llm}/agents/executions/dry_run.html.erb +0 -0
  97. /data/app/views/{rubyllm → ruby_llm}/agents/shared/_stat_card.html.erb +0 -0
@@ -3,13 +3,15 @@
3
3
  selected_agents = params[:agent_types].present? ? (params[:agent_types].is_a?(Array) ? params[:agent_types] : params[:agent_types].split(",")) : []
4
4
  selected_statuses = params[:statuses].present? ? (params[:statuses].is_a?(Array) ? params[:statuses] : params[:statuses].split(",")) : []
5
5
  selected_models = params[:model_ids].present? ? (params[:model_ids].is_a?(Array) ? params[:model_ids] : params[:model_ids].split(",")) : []
6
+ selected_workflows = params[:workflow_types].present? ? (params[:workflow_types].is_a?(Array) ? params[:workflow_types] : params[:workflow_types].split(",")) : []
6
7
 
7
- has_filters = selected_agents.any? || selected_statuses.any? || params[:days].present? || selected_models.any?
8
+ has_filters = selected_agents.any? || selected_statuses.any? || params[:days].present? || selected_models.any? || selected_workflows.any?
8
9
  active_filter_count = [
9
10
  selected_agents.any? ? 1 : 0,
10
11
  selected_statuses.any? ? 1 : 0,
11
12
  params[:days].present? ? 1 : 0,
12
- selected_models.any? ? 1 : 0
13
+ selected_models.any? ? 1 : 0,
14
+ selected_workflows.any? ? 1 : 0
13
15
  ].sum
14
16
 
15
17
  status_options = [
@@ -33,11 +35,23 @@
33
35
  short_name = m.split("/").last.split(":").first # Handle "provider/model:version" format
34
36
  { value: m, label: short_name }
35
37
  end || []
38
+
39
+ # Build workflow type options with icons
40
+ workflow_type_labels = {
41
+ "pipeline" => { label: "Pipeline", icon: "→" },
42
+ "parallel" => { label: "Parallel", icon: "⫴" },
43
+ "router" => { label: "Router", icon: "⑂" }
44
+ }
45
+ workflow_options = [{ value: "single", label: "Single Agent" }]
46
+ workflow_options += (local_assigns[:workflow_types] || []).map do |wt|
47
+ info = workflow_type_labels[wt] || { label: wt.titleize, icon: "" }
48
+ { value: wt, label: "#{info[:icon]} #{info[:label]}".strip }
49
+ end
36
50
  %>
37
51
  <div id="filters-container" class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-4 mb-6"
38
52
  x-data="{ mobileOpen: false }">
39
53
  <%= form_with url: ruby_llm_agents.executions_path, method: :get,
40
- data: { turbo_stream: true, turbo_action: "advance" },
54
+ local: true,
41
55
  id: "filters-form" do |f| %>
42
56
 
43
57
  <%# Mobile: Toggle button (only shows on small screens) %>
@@ -71,7 +85,7 @@
71
85
  <div class="flex flex-col md:flex-row md:items-center gap-3">
72
86
  <%# Status Filter %>
73
87
  <div class="md:w-auto">
74
- <%= render "rubyllm/agents/shared/filter_dropdown",
88
+ <%= render "ruby_llm/agents/shared/filter_dropdown",
75
89
  name: "statuses[]",
76
90
  filter_id: "statuses",
77
91
  label: "Status",
@@ -84,7 +98,7 @@
84
98
 
85
99
  <%# Time Range Filter %>
86
100
  <div class="md:w-auto">
87
- <%= render "rubyllm/agents/shared/select_dropdown",
101
+ <%= render "ruby_llm/agents/shared/select_dropdown",
88
102
  name: "days",
89
103
  filter_id: "days",
90
104
  options: time_options,
@@ -97,7 +111,7 @@
97
111
  <%# Model Filter %>
98
112
  <% if model_options.any? %>
99
113
  <div class="md:w-auto">
100
- <%= render "rubyllm/agents/shared/filter_dropdown",
114
+ <%= render "ruby_llm/agents/shared/filter_dropdown",
101
115
  name: "model_ids[]",
102
116
  filter_id: "model_ids",
103
117
  label: "Model",
@@ -112,7 +126,7 @@
112
126
  <%# Agent Types Filter %>
113
127
  <% if agent_types.any? %>
114
128
  <div class="md:w-auto">
115
- <%= render "rubyllm/agents/shared/filter_dropdown",
129
+ <%= render "ruby_llm/agents/shared/filter_dropdown",
116
130
  name: "agent_types[]",
117
131
  filter_id: "agent_types",
118
132
  label: "Agents",
@@ -125,6 +139,22 @@
125
139
  </div>
126
140
  <% end %>
127
141
 
142
+ <%# Workflow Type Filter %>
143
+ <% if workflow_options.length > 1 %>
144
+ <div class="md:w-auto">
145
+ <%= render "ruby_llm/agents/shared/filter_dropdown",
146
+ name: "workflow_types[]",
147
+ filter_id: "workflow_types",
148
+ label: "Type",
149
+ all_label: "All Types",
150
+ options: workflow_options,
151
+ selected: selected_workflows,
152
+ icon: "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",
153
+ width: "w-44",
154
+ full_width: true %>
155
+ </div>
156
+ <% end %>
157
+
128
158
  <%# Spacer (desktop only) %>
129
159
  <div class="hidden md:block flex-1"></div>
130
160
 
@@ -144,7 +174,6 @@
144
174
  <div class="flex items-center gap-2 md:gap-1">
145
175
  <% if has_filters %>
146
176
  <%= link_to ruby_llm_agents.executions_path,
147
- data: { turbo_stream: true, turbo_action: "advance" },
148
177
  class: "flex-1 md:flex-initial flex items-center justify-center gap-2 px-3 py-2 md:p-2 text-sm md:text-base text-red-600 md:text-gray-400 dark:text-red-400 md:dark:text-gray-400 bg-red-50 md:bg-transparent dark:bg-red-900/20 md:dark:bg-transparent hover:text-red-500 hover:bg-red-100 md:hover:bg-red-50 dark:hover:bg-red-900/30 md:dark:hover:bg-red-900/20 rounded-lg transition-colors",
149
178
  title: "Clear filters" do %>
150
179
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -153,10 +182,9 @@
153
182
  <span class="md:hidden">Clear</span>
154
183
  <% end %>
155
184
  <% end %>
156
- <%= link_to ruby_llm_agents.export_executions_path(agent_types: selected_agents.presence, statuses: selected_statuses.presence, days: params[:days].presence, model_ids: selected_models.presence),
185
+ <%= link_to ruby_llm_agents.export_executions_path(agent_types: selected_agents.presence, statuses: selected_statuses.presence, days: params[:days].presence, model_ids: selected_models.presence, workflow_types: selected_workflows.presence),
157
186
  class: "flex-1 md:flex-initial flex items-center justify-center gap-2 px-3 py-2 md:p-2 text-sm md:text-base text-gray-600 md:text-gray-400 dark:text-gray-300 md:dark:text-gray-400 bg-gray-50 md:bg-transparent dark:bg-gray-700 md:dark:bg-transparent hover:text-gray-600 md:hover:text-gray-600 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-600 md:dark:hover:bg-gray-700 rounded-lg transition-colors",
158
- title: "Export CSV",
159
- data: { turbo: false } do %>
187
+ title: "Export CSV" do %>
160
188
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
161
189
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
162
190
  </svg>
@@ -94,7 +94,7 @@
94
94
 
95
95
  <tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-100 dark:divide-gray-700">
96
96
  <% executions.each do |execution| %>
97
- <% has_attempts = execution.attempts.present? && execution.attempts.size > 0 %>
97
+ <% has_attempts = execution.respond_to?(:attempts) && execution.attempts.present? && execution.attempts.size > 0 %>
98
98
  <tr
99
99
  class="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
100
100
  id="execution-row-<%= execution.id %>"
@@ -118,13 +118,23 @@
118
118
  </td>
119
119
 
120
120
  <td class="px-4 py-3 whitespace-nowrap cursor-pointer" onclick="window.location='<%= ruby_llm_agents.execution_path(execution) %>'">
121
- <span class="text-sm font-medium text-gray-900 dark:text-gray-100">
122
- <%= execution.agent_type.gsub(/Agent$/, '') %>
123
- </span>
121
+ <div class="flex items-center gap-2">
122
+ <% if execution.respond_to?(:workflow_type) && execution.workflow_type.present? %>
123
+ <%= render "ruby_llm/agents/shared/workflow_type_badge", workflow_type: execution.workflow_type, size: :xs, show_label: false %>
124
+ <% end %>
125
+ <span class="text-sm font-medium text-gray-900 dark:text-gray-100">
126
+ <%= execution.agent_type.gsub(/Agent$|Workflow$|Pipeline$|Parallel$|Router$/, '') %>
127
+ </span>
128
+ </div>
129
+ <% if execution.respond_to?(:parent_execution_id) && execution.parent_execution_id.present? %>
130
+ <span class="text-xs text-gray-400 dark:text-gray-500 ml-5 block">
131
+ child of #<%= execution.parent_execution_id %>
132
+ </span>
133
+ <% end %>
124
134
  </td>
125
135
 
126
136
  <td class="px-4 py-3 whitespace-nowrap cursor-pointer" onclick="window.location='<%= ruby_llm_agents.execution_path(execution) %>'">
127
- <%= render "rubyllm/agents/shared/status_badge", status: execution.status %>
137
+ <%= render "ruby_llm/agents/shared/status_badge", status: execution.status %>
128
138
  </td>
129
139
 
130
140
  <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400 cursor-pointer" onclick="window.location='<%= ruby_llm_agents.execution_path(execution) %>'">
@@ -133,7 +143,7 @@
133
143
 
134
144
  <!-- Attempts Count -->
135
145
  <td class="px-4 py-3 whitespace-nowrap text-sm text-center cursor-pointer" onclick="window.location='<%= ruby_llm_agents.execution_path(execution) %>'">
136
- <% attempts_count = execution.attempts_count || (execution.attempts&.size || 1) %>
146
+ <% attempts_count = (execution.respond_to?(:attempts_count) && execution.attempts_count) || (execution.respond_to?(:attempts) && execution.attempts&.size) || 1 %>
137
147
  <% if attempts_count > 1 %>
138
148
  <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200">
139
149
  <%= attempts_count %> attempts
@@ -257,7 +267,7 @@
257
267
 
258
268
  <nav class="flex items-center space-x-1">
259
269
  <% if current_page > 1 %>
260
- <%= link_to "Previous", url_for(page: current_page - 1), data: { turbo_frame: "executions_content", turbo_action: "advance" }, 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" %>
270
+ <%= link_to "Previous", url_for(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" %>
261
271
  <% else %>
262
272
  <span
263
273
  class="
@@ -297,12 +307,12 @@
297
307
  <%= page %>
298
308
  </span>
299
309
  <% else %>
300
- <%= link_to page, url_for(page: page), data: { turbo_frame: "executions_content", turbo_action: "advance" }, 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" %>
310
+ <%= link_to page, url_for(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" %>
301
311
  <% end %>
302
312
  <% end %>
303
313
 
304
314
  <% if current_page < total_pages %>
305
- <%= link_to "Next", url_for(page: current_page + 1), data: { turbo_frame: "executions_content", turbo_action: "advance" }, 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" %>
315
+ <%= link_to "Next", url_for(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" %>
306
316
  <% else %>
307
317
  <span
308
318
  class="
@@ -0,0 +1,101 @@
1
+ <%# Simplified Workflow Summary - Clean table approach %>
2
+ <%# @param execution [RubyLLM::Agents::Execution] The workflow execution to display %>
3
+
4
+ <% if execution.root_workflow? %>
5
+ <% stats = execution.workflow_aggregate_stats %>
6
+ <% steps = execution.workflow_steps.to_a %>
7
+
8
+ <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden mb-6">
9
+ <!-- Header -->
10
+ <div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
11
+ <div class="flex items-center gap-2">
12
+ <% case execution.workflow_type
13
+ when "pipeline" %>
14
+ <span class="text-indigo-600 dark:text-indigo-400">→</span>
15
+ <span class="font-medium text-gray-900 dark:text-gray-100">Pipeline</span>
16
+ <span class="text-gray-500 dark:text-gray-400">· <%= stats[:steps_count] %> steps</span>
17
+ <% when "parallel" %>
18
+ <span class="text-cyan-600 dark:text-cyan-400">⫴</span>
19
+ <span class="font-medium text-gray-900 dark:text-gray-100">Parallel</span>
20
+ <span class="text-gray-500 dark:text-gray-400">· <%= stats[:steps_count] %> branches</span>
21
+ <% when "router" %>
22
+ <span class="text-amber-600 dark:text-amber-400">⑂</span>
23
+ <span class="font-medium text-gray-900 dark:text-gray-100">Router</span>
24
+ <span class="text-gray-500 dark:text-gray-400">→ <%= execution.routed_to %></span>
25
+ <% end %>
26
+ </div>
27
+
28
+ <% overall_status = execution.workflow_overall_status %>
29
+ <%= render "ruby_llm/agents/shared/status_badge", status: overall_status.to_s, size: :sm %>
30
+ </div>
31
+
32
+ <!-- Table -->
33
+ <div class="overflow-x-auto">
34
+ <table class="min-w-full text-sm">
35
+ <thead class="bg-gray-50 dark:bg-gray-900/50">
36
+ <tr>
37
+ <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
38
+ <%= execution.pipeline_workflow? ? "Step" : execution.parallel_workflow? ? "Branch" : "Route" %>
39
+ </th>
40
+ <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Status</th>
41
+ <th class="px-4 py-2 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Duration</th>
42
+ <th class="px-4 py-2 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Tokens</th>
43
+ <th class="px-4 py-2 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Cost</th>
44
+ </tr>
45
+ </thead>
46
+ <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
47
+ <% steps.each_with_index do |step, index| %>
48
+ <tr class="hover:bg-gray-50 dark:hover:bg-gray-800/50">
49
+ <td class="px-4 py-2">
50
+ <%= link_to ruby_llm_agents.execution_path(step.id), class: "text-blue-600 dark:text-blue-400 hover:underline" do %>
51
+ <% if execution.pipeline_workflow? %>
52
+ <span class="text-gray-400 dark:text-gray-500"><%= index + 1 %>.</span>
53
+ <% end %>
54
+ <%= step.workflow_step || step.agent_type.gsub(/Agent$/, "") %>
55
+ <% end %>
56
+ </td>
57
+ <td class="px-4 py-2">
58
+ <% case step.status
59
+ when "success" %>
60
+ <span class="text-green-600 dark:text-green-400">✓</span>
61
+ <% when "error" %>
62
+ <span class="text-red-600 dark:text-red-400">✗</span>
63
+ <% when "timeout" %>
64
+ <span class="text-orange-600 dark:text-orange-400">⏱</span>
65
+ <% when "running" %>
66
+ <span class="text-blue-600 dark:text-blue-400 animate-pulse">●</span>
67
+ <% else %>
68
+ <span class="text-gray-400">○</span>
69
+ <% end %>
70
+ </td>
71
+ <td class="px-4 py-2 text-right text-gray-600 dark:text-gray-300 tabular-nums">
72
+ <%= step.duration_ms ? "#{number_with_delimiter(step.duration_ms)}ms" : "-" %>
73
+ </td>
74
+ <td class="px-4 py-2 text-right text-gray-600 dark:text-gray-300 tabular-nums">
75
+ <%= number_with_delimiter(step.total_tokens || 0) %>
76
+ </td>
77
+ <td class="px-4 py-2 text-right text-gray-600 dark:text-gray-300 tabular-nums">
78
+ $<%= number_with_precision(step.total_cost || 0, precision: 4) %>
79
+ </td>
80
+ </tr>
81
+ <% end %>
82
+ </tbody>
83
+ <tfoot class="bg-gray-50 dark:bg-gray-900/50 font-medium">
84
+ <tr>
85
+ <td class="px-4 py-2 text-gray-700 dark:text-gray-300">Total</td>
86
+ <td class="px-4 py-2"></td>
87
+ <td class="px-4 py-2 text-right text-gray-700 dark:text-gray-300 tabular-nums">
88
+ <%= stats[:wall_clock_ms] ? "#{number_with_delimiter(stats[:wall_clock_ms])}ms" : "-" %>
89
+ </td>
90
+ <td class="px-4 py-2 text-right text-gray-700 dark:text-gray-300 tabular-nums">
91
+ <%= number_with_delimiter(stats[:total_tokens]) %>
92
+ </td>
93
+ <td class="px-4 py-2 text-right text-gray-700 dark:text-gray-300 tabular-nums">
94
+ $<%= number_with_precision(stats[:total_cost], precision: 4) %>
95
+ </td>
96
+ </tr>
97
+ </tfoot>
98
+ </table>
99
+ </div>
100
+ </div>
101
+ <% end %>
@@ -0,0 +1,88 @@
1
+ <h2 class="text-2xl font-bold text-gray-900 dark:text-gray-100 mb-6">Executions</h2>
2
+
3
+ <div id="executions_content" x-data="{ activeTab: '<%= params[:execution_type] || 'all' %>', activeWorkflowType: '<%= params[:workflow_type_tab] %>' }">
4
+ <!-- Top-Level Execution Type Tabs -->
5
+ <div class="border-b border-gray-200 dark:border-gray-700 mb-4">
6
+ <nav class="-mb-px flex space-x-6" aria-label="Execution Types">
7
+ <button type="button" @click="activeTab = 'all'; activeWorkflowType = ''; submitFilters()"
8
+ :class="activeTab === 'all' ? 'border-blue-500 text-blue-600 dark:text-blue-400' : 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'"
9
+ class="whitespace-nowrap py-3 px-1 border-b-2 font-medium text-sm transition-colors">
10
+ All
11
+ </button>
12
+ <button type="button" @click="activeTab = 'agents'; activeWorkflowType = ''; submitFilters()"
13
+ :class="activeTab === 'agents' ? 'border-blue-500 text-blue-600 dark:text-blue-400' : 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'"
14
+ class="whitespace-nowrap py-3 px-1 border-b-2 font-medium text-sm transition-colors">
15
+ Agents Only
16
+ </button>
17
+ <button type="button" @click="activeTab = 'workflows'; activeWorkflowType = ''; submitFilters()"
18
+ :class="activeTab === 'workflows' ? 'border-blue-500 text-blue-600 dark:text-blue-400' : 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'"
19
+ class="whitespace-nowrap py-3 px-1 border-b-2 font-medium text-sm transition-colors">
20
+ Workflows
21
+ </button>
22
+ </nav>
23
+ </div>
24
+
25
+ <!-- Workflow Sub-tabs -->
26
+ <div x-show="activeTab === 'workflows'" x-cloak class="mb-4">
27
+ <div class="flex flex-wrap gap-2">
28
+ <button type="button" @click="activeWorkflowType = ''; submitFilters()"
29
+ :class="activeWorkflowType === '' ? 'bg-gray-900 dark:bg-gray-100 text-white dark:text-gray-900' : 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'"
30
+ class="px-3 py-1.5 rounded-md text-sm font-medium transition-colors">
31
+ All Workflows
32
+ </button>
33
+ <button type="button" @click="activeWorkflowType = 'pipeline'; submitFilters()"
34
+ :class="activeWorkflowType === 'pipeline' ? 'bg-indigo-600 text-white' : 'bg-indigo-100 dark:bg-indigo-900/50 text-indigo-700 dark:text-indigo-300 hover:bg-indigo-200 dark:hover:bg-indigo-800'"
35
+ class="px-3 py-1.5 rounded-md text-sm font-medium transition-colors inline-flex items-center gap-1.5">
36
+ <span aria-hidden="true">-></span> Pipeline
37
+ </button>
38
+ <button type="button" @click="activeWorkflowType = 'parallel'; submitFilters()"
39
+ :class="activeWorkflowType === 'parallel' ? 'bg-cyan-600 text-white' : 'bg-cyan-100 dark:bg-cyan-900/50 text-cyan-700 dark:text-cyan-300 hover:bg-cyan-200 dark:hover:bg-cyan-800'"
40
+ class="px-3 py-1.5 rounded-md text-sm font-medium transition-colors inline-flex items-center gap-1.5">
41
+ <span aria-hidden="true">//</span> Parallel
42
+ </button>
43
+ <button type="button" @click="activeWorkflowType = 'router'; submitFilters()"
44
+ :class="activeWorkflowType === 'router' ? 'bg-amber-600 text-white' : 'bg-amber-100 dark:bg-amber-900/50 text-amber-700 dark:text-amber-300 hover:bg-amber-200 dark:hover:bg-amber-800'"
45
+ class="px-3 py-1.5 rounded-md text-sm font-medium transition-colors inline-flex items-center gap-1.5">
46
+ <span aria-hidden="true">&lt;&gt;</span> Router
47
+ </button>
48
+ </div>
49
+ </div>
50
+
51
+ <!-- Hidden inputs for tab state -->
52
+ <input type="hidden" id="execution-type-filter" name="execution_type" x-model="activeTab" form="filters-form">
53
+ <input type="hidden" id="workflow-type-tab-filter" name="workflow_type_tab" x-model="activeWorkflowType" form="filters-form">
54
+
55
+ <%= render partial: "ruby_llm/agents/executions/filters", locals: { agent_types: @agent_types, model_ids: @model_ids, workflow_types: @workflow_types, filter_stats: @filter_stats } %>
56
+ <%= render partial: "ruby_llm/agents/executions/list", locals: { executions: @executions, pagination: @pagination, filter_stats: @filter_stats } %>
57
+ </div>
58
+
59
+ <script>
60
+ // Toggle attempts expansion (used by executions list)
61
+ function toggleAttempts(executionId) {
62
+ const attemptsRow = document.getElementById('attempts-row-' + executionId);
63
+ const expandBtn = document.querySelector(`button[data-execution-id="${executionId}"]`);
64
+
65
+ if (attemptsRow) {
66
+ const isHidden = attemptsRow.classList.contains('hidden');
67
+ attemptsRow.classList.toggle('hidden');
68
+
69
+ // Rotate the chevron
70
+ if (expandBtn) {
71
+ const svg = expandBtn.querySelector('svg');
72
+ if (svg) {
73
+ svg.classList.toggle('rotate-90', isHidden);
74
+ }
75
+ expandBtn.setAttribute('aria-expanded', isHidden ? 'true' : 'false');
76
+ }
77
+ }
78
+ }
79
+
80
+ // Submit filters when tabs change
81
+ function submitFilters() {
82
+ // Delay to allow Alpine to update x-model values
83
+ setTimeout(function() {
84
+ const form = document.getElementById('filters-form');
85
+ if (form) form.requestSubmit();
86
+ }, 50);
87
+ }
88
+ </script>