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,112 @@
1
+ <div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 h-full" x-data="{ activeTab: 'agents' }">
2
+ <div class="px-4 py-3 border-b border-gray-100 dark:border-gray-700">
3
+ <div class="flex items-center justify-between">
4
+ <h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">Performance</h3>
5
+ <!-- Tab Buttons -->
6
+ <div class="flex space-x-1 bg-gray-100 dark:bg-gray-700 rounded-lg p-0.5">
7
+ <button type="button" @click="activeTab = 'agents'"
8
+ :class="activeTab === 'agents' ? 'bg-white dark:bg-gray-600 shadow-sm' : 'hover:bg-gray-50 dark:hover:bg-gray-600'"
9
+ class="px-3 py-1 text-xs font-medium rounded-md transition-colors text-gray-700 dark:text-gray-200">
10
+ Agents
11
+ </button>
12
+ <button type="button" @click="activeTab = 'workflows'"
13
+ :class="activeTab === 'workflows' ? 'bg-white dark:bg-gray-600 shadow-sm' : 'hover:bg-gray-50 dark:hover:bg-gray-600'"
14
+ class="px-3 py-1 text-xs font-medium rounded-md transition-colors text-gray-700 dark:text-gray-200">
15
+ Workflows
16
+ </button>
17
+ </div>
18
+ </div>
19
+ </div>
20
+
21
+ <!-- Agents Tab -->
22
+ <div x-show="activeTab === 'agents'">
23
+ <% if agent_stats&.any? %>
24
+ <div class="overflow-x-auto">
25
+ <table class="w-full text-sm">
26
+ <thead>
27
+ <tr class="text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
28
+ <th class="px-4 py-2">Agent</th>
29
+ <th class="px-4 py-2 text-right">Runs</th>
30
+ <th class="px-4 py-2 text-right">Cost</th>
31
+ <th class="px-4 py-2 text-right">Success</th>
32
+ </tr>
33
+ </thead>
34
+ <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
35
+ <% agent_stats.first(5).each do |agent| %>
36
+ <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
37
+ <td class="px-4 py-2">
38
+ <span class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate block max-w-[120px]" title="<%= agent[:agent_type] %>">
39
+ <%= agent[:agent_type].to_s.demodulize %>
40
+ </span>
41
+ </td>
42
+ <td class="px-4 py-2 text-right text-gray-600 dark:text-gray-300">
43
+ <%= number_with_delimiter(agent[:executions]) %>
44
+ </td>
45
+ <td class="px-4 py-2 text-right text-gray-600 dark:text-gray-300">
46
+ $<%= number_with_precision(agent[:total_cost], precision: 2) %>
47
+ </td>
48
+ <td class="px-4 py-2 text-right">
49
+ <span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium <%= agent[:success_rate] >= 95 ? 'bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300' : agent[:success_rate] >= 80 ? 'bg-yellow-100 dark:bg-yellow-900 text-yellow-700 dark:text-yellow-300' : 'bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300' %>">
50
+ <%= agent[:success_rate].round %>%
51
+ </span>
52
+ </td>
53
+ </tr>
54
+ <% end %>
55
+ </tbody>
56
+ </table>
57
+ </div>
58
+ <% else %>
59
+ <div class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
60
+ <p class="text-sm">No agent data yet</p>
61
+ </div>
62
+ <% end %>
63
+ </div>
64
+
65
+ <!-- Workflows Tab -->
66
+ <div x-show="activeTab === 'workflows'" x-cloak>
67
+ <% if @workflow_stats&.any? %>
68
+ <div class="overflow-x-auto">
69
+ <table class="w-full text-sm">
70
+ <thead>
71
+ <tr class="text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
72
+ <th class="px-4 py-2">Workflow</th>
73
+ <th class="px-4 py-2">Type</th>
74
+ <th class="px-4 py-2 text-right">Runs</th>
75
+ <th class="px-4 py-2 text-right">Cost</th>
76
+ <th class="px-4 py-2 text-right">Success</th>
77
+ </tr>
78
+ </thead>
79
+ <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
80
+ <% @workflow_stats.first(5).each do |workflow| %>
81
+ <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
82
+ <td class="px-4 py-2">
83
+ <span class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate block max-w-[100px]" title="<%= workflow[:agent_type] %>">
84
+ <%= workflow[:agent_type].to_s.demodulize.gsub(/Workflow$|Pipeline$|Parallel$|Router$/, '') %>
85
+ </span>
86
+ </td>
87
+ <td class="px-4 py-2">
88
+ <%= render "ruby_llm/agents/shared/workflow_type_badge", workflow_type: workflow[:workflow_type], size: :xs, show_label: false %>
89
+ </td>
90
+ <td class="px-4 py-2 text-right text-gray-600 dark:text-gray-300">
91
+ <%= number_with_delimiter(workflow[:executions]) %>
92
+ </td>
93
+ <td class="px-4 py-2 text-right text-gray-600 dark:text-gray-300">
94
+ $<%= number_with_precision(workflow[:total_cost], precision: 2) %>
95
+ </td>
96
+ <td class="px-4 py-2 text-right">
97
+ <span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium <%= workflow[:success_rate] >= 95 ? 'bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300' : workflow[:success_rate] >= 80 ? 'bg-yellow-100 dark:bg-yellow-900 text-yellow-700 dark:text-yellow-300' : 'bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300' %>">
98
+ <%= workflow[:success_rate].round %>%
99
+ </span>
100
+ </td>
101
+ </tr>
102
+ <% end %>
103
+ </tbody>
104
+ </table>
105
+ </div>
106
+ <% else %>
107
+ <div class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
108
+ <p class="text-sm">No workflow data yet</p>
109
+ </div>
110
+ <% end %>
111
+ </div>
112
+ </div>
@@ -1,11 +1,14 @@
1
1
  <div id="execution-<%= execution.id %>" class="py-2.5 hover:bg-gray-50 dark:hover:bg-gray-700/50 -mx-2 px-2 rounded-lg transition-colors">
2
- <%= link_to ruby_llm_agents.execution_path(execution), data: { turbo: false }, class: "block" do %>
2
+ <%= link_to ruby_llm_agents.execution_path(execution), class: "block" do %>
3
3
  <!-- Row 1: Status dot + Agent + Badges + Timestamp -->
4
4
  <div class="flex items-center justify-between gap-2">
5
5
  <div class="flex items-center gap-2 min-w-0">
6
- <%= render "rubyllm/agents/shared/status_dot", status: execution.status %>
6
+ <%= render "ruby_llm/agents/shared/status_dot", status: execution.status %>
7
+ <% if execution.respond_to?(:workflow_type) && execution.workflow_type.present? %>
8
+ <%= render "ruby_llm/agents/shared/workflow_type_badge", workflow_type: execution.workflow_type, size: :xs, show_label: false %>
9
+ <% end %>
7
10
  <span class="font-medium text-sm text-gray-900 dark:text-gray-100 truncate">
8
- <%= execution.agent_type.gsub(/Agent$/, '') %>
11
+ <%= execution.agent_type.gsub(/Agent$|Workflow$|Pipeline$|Parallel$|Router$/, '') %>
9
12
  </span>
10
13
  <span class="hidden sm:inline text-xs text-gray-400 dark:text-gray-500">v<%= execution.agent_version %></span>
11
14
  <% unless execution.status_running? %>
@@ -45,7 +48,7 @@
45
48
  $<%= number_with_precision(execution.total_cost || 0, precision: 4) %>
46
49
  <span class="mx-1.5 text-gray-300 dark:text-gray-600">·</span>
47
50
  <%= number_to_human_short(execution.duration_ms || 0) %>ms
48
- <% if execution.attempts_count && execution.attempts_count > 1 %>
51
+ <% if execution.respond_to?(:attempts_count) && execution.attempts_count && execution.attempts_count > 1 %>
49
52
  <span class="mx-1.5 text-gray-300 dark:text-gray-600">·</span>
50
53
  <span class="text-amber-600 dark:text-amber-400"><%= execution.attempts_count %> retries</span>
51
54
  <% end %>
@@ -0,0 +1,115 @@
1
+ <%# Tenant Budget Widget - shows budget limits and usage for current tenant %>
2
+ <%# @param tenant_budget [Hash] Budget data from controller %>
3
+
4
+ <% if tenant_budget.present? %>
5
+ <div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 mb-6">
6
+ <div class="px-6 py-3 border-b border-gray-100 dark:border-gray-700">
7
+ <div class="flex items-center justify-between">
8
+ <div class="flex items-center gap-2">
9
+ <svg class="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
10
+ <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" />
11
+ </svg>
12
+ <h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">
13
+ Tenant Budget: <%= tenant_budget[:tenant_id] %>
14
+ </h3>
15
+ </div>
16
+ <%# Enforcement badge %>
17
+ <% enforcement = tenant_budget[:enforcement].to_s %>
18
+ <% badge_class = case enforcement
19
+ when "hard" then "bg-red-100 dark:bg-red-900/50 text-red-700 dark:text-red-300"
20
+ when "soft" then "bg-yellow-100 dark:bg-yellow-900/50 text-yellow-700 dark:text-yellow-300"
21
+ else "bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400"
22
+ end %>
23
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium <%= badge_class %>">
24
+ <%= enforcement.capitalize %> Enforcement
25
+ </span>
26
+ </div>
27
+ </div>
28
+
29
+ <div class="p-6">
30
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
31
+ <%# Daily Budget %>
32
+ <div>
33
+ <div class="flex items-center justify-between mb-2">
34
+ <span class="text-sm font-medium text-gray-700 dark:text-gray-300">Daily Budget</span>
35
+ <span class="text-sm text-gray-500 dark:text-gray-400">
36
+ $<%= number_with_precision(tenant_budget[:daily_spend], precision: 2) %>
37
+ <% if tenant_budget[:daily_limit] %>
38
+ / $<%= number_with_precision(tenant_budget[:daily_limit], precision: 2) %>
39
+ <% else %>
40
+ (no limit)
41
+ <% end %>
42
+ </span>
43
+ </div>
44
+ <% if tenant_budget[:daily_limit] %>
45
+ <% daily_pct = [tenant_budget[:daily_percentage], 100].min %>
46
+ <% bar_color = if daily_pct >= 100
47
+ "bg-red-500"
48
+ elsif daily_pct >= 80
49
+ "bg-yellow-500"
50
+ else
51
+ "bg-green-500"
52
+ end %>
53
+ <div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2.5">
54
+ <div class="<%= bar_color %> h-2.5 rounded-full transition-all duration-300" style="width: <%= daily_pct %>%"></div>
55
+ </div>
56
+ <p class="text-xs text-gray-500 dark:text-gray-400 mt-1"><%= tenant_budget[:daily_percentage] %>% used</p>
57
+ <% else %>
58
+ <div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2.5">
59
+ <div class="bg-gray-400 h-2.5 rounded-full" style="width: 0%"></div>
60
+ </div>
61
+ <p class="text-xs text-gray-500 dark:text-gray-400 mt-1">No daily limit configured</p>
62
+ <% end %>
63
+ </div>
64
+
65
+ <%# Monthly Budget %>
66
+ <div>
67
+ <div class="flex items-center justify-between mb-2">
68
+ <span class="text-sm font-medium text-gray-700 dark:text-gray-300">Monthly Budget</span>
69
+ <span class="text-sm text-gray-500 dark:text-gray-400">
70
+ $<%= number_with_precision(tenant_budget[:monthly_spend], precision: 2) %>
71
+ <% if tenant_budget[:monthly_limit] %>
72
+ / $<%= number_with_precision(tenant_budget[:monthly_limit], precision: 2) %>
73
+ <% else %>
74
+ (no limit)
75
+ <% end %>
76
+ </span>
77
+ </div>
78
+ <% if tenant_budget[:monthly_limit] %>
79
+ <% monthly_pct = [tenant_budget[:monthly_percentage], 100].min %>
80
+ <% bar_color = if monthly_pct >= 100
81
+ "bg-red-500"
82
+ elsif monthly_pct >= 80
83
+ "bg-yellow-500"
84
+ else
85
+ "bg-blue-500"
86
+ end %>
87
+ <div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2.5">
88
+ <div class="<%= bar_color %> h-2.5 rounded-full transition-all duration-300" style="width: <%= monthly_pct %>%"></div>
89
+ </div>
90
+ <p class="text-xs text-gray-500 dark:text-gray-400 mt-1"><%= tenant_budget[:monthly_percentage] %>% used</p>
91
+ <% else %>
92
+ <div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2.5">
93
+ <div class="bg-gray-400 h-2.5 rounded-full" style="width: 0%"></div>
94
+ </div>
95
+ <p class="text-xs text-gray-500 dark:text-gray-400 mt-1">No monthly limit configured</p>
96
+ <% end %>
97
+ </div>
98
+ </div>
99
+
100
+ <%# Per-agent limits if configured %>
101
+ <% if tenant_budget[:per_agent_daily].present? %>
102
+ <div class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-700">
103
+ <p class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-2">Per-Agent Daily Limits</p>
104
+ <div class="flex flex-wrap gap-2">
105
+ <% tenant_budget[:per_agent_daily].each do |agent, limit| %>
106
+ <span class="inline-flex items-center px-2 py-1 rounded text-xs bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300">
107
+ <%= agent.to_s.gsub(/Agent$/, "") %>: $<%= number_with_precision(limit, precision: 2) %>
108
+ </span>
109
+ <% end %>
110
+ </div>
111
+ </div>
112
+ <% end %>
113
+ </div>
114
+ </div>
115
+ <% end %>
@@ -1,8 +1,11 @@
1
1
  <!-- Action Center (only when critical alerts exist) -->
2
- <%= render partial: "rubyllm/agents/dashboard/action_center", locals: { critical_alerts: @critical_alerts } %>
2
+ <%= render partial: "ruby_llm/agents/dashboard/action_center", locals: { critical_alerts: @critical_alerts } %>
3
3
 
4
4
  <!-- Now Strip -->
5
- <%= render partial: "rubyllm/agents/dashboard/now_strip", locals: { now_strip: @now_strip } %>
5
+ <%= render partial: "ruby_llm/agents/dashboard/now_strip", locals: { now_strip: @now_strip } %>
6
+
7
+ <!-- Tenant Budget (when viewing specific tenant) -->
8
+ <%= render partial: "ruby_llm/agents/dashboard/tenant_budget", locals: { tenant_budget: @tenant_budget } %>
6
9
 
7
10
  <!-- Activity Chart -->
8
11
  <div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 mb-6">
@@ -129,13 +132,13 @@
129
132
  <div class="px-6 py-3 border-b border-gray-100 dark:border-gray-700">
130
133
  <div class="flex justify-between items-center">
131
134
  <h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">Recent Activity</h3>
132
- <%= link_to "View All", ruby_llm_agents.executions_path, data: { turbo: false }, class: "text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 text-sm font-medium" %>
135
+ <%= link_to "View All", ruby_llm_agents.executions_path, class: "text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 text-sm font-medium" %>
133
136
  </div>
134
137
  </div>
135
138
  <div id="activity-feed" class="px-6 py-2">
136
139
  <% if @recent_executions.any? %>
137
140
  <% @recent_executions.first(5).each do |execution| %>
138
- <%= render partial: "rubyllm/agents/dashboard/execution_item", locals: { execution: execution } %>
141
+ <%= render partial: "ruby_llm/agents/dashboard/execution_item", locals: { execution: execution } %>
139
142
  <% end %>
140
143
  <% else %>
141
144
  <div class="py-8 text-center text-gray-500 dark:text-gray-400">
@@ -147,6 +150,6 @@
147
150
 
148
151
  <!-- Agent Comparison + Top Errors -->
149
152
  <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
150
- <%= render partial: "rubyllm/agents/dashboard/agent_comparison", locals: { agent_stats: @agent_stats } %>
151
- <%= render partial: "rubyllm/agents/dashboard/top_errors", locals: { top_errors: @top_errors } %>
153
+ <%= render partial: "ruby_llm/agents/dashboard/agent_comparison", locals: { agent_stats: @agent_stats } %>
154
+ <%= render partial: "ruby_llm/agents/dashboard/top_errors", locals: { top_errors: @top_errors } %>
152
155
  </div>
@@ -1,4 +1,4 @@
1
- <%= link_to ruby_llm_agents.execution_path(execution), data: { turbo: false }, class: "block px-6 py-4 hover:bg-gray-50 transition-colors" do %>
1
+ <%= link_to ruby_llm_agents.execution_path(execution), class: "block px-6 py-4 hover:bg-gray-50 transition-colors" do %>
2
2
  <div class="flex items-center justify-between">
3
3
  <div class="flex items-center space-x-4">
4
4
  <!-- Status Badge -->
@@ -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 %>