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
@@ -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>
@@ -0,0 +1,75 @@
1
+ <%# Budget bars for daily and monthly spending %>
2
+ <% if budget_status[:enabled] %>
3
+ <div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-4 mb-6">
4
+ <div class="flex items-center justify-between mb-3">
5
+ <h3 class="text-sm font-semibold text-gray-700 dark:text-gray-200">Budget Usage</h3>
6
+ <span class="text-xs text-gray-500 dark:text-gray-400">
7
+ <%= budget_status[:enforcement] == :hard ? "Hard Enforcement" : "Soft Enforcement" %>
8
+ </span>
9
+ </div>
10
+
11
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
12
+ <%# Daily Budget %>
13
+ <% if budget_status[:global_daily] %>
14
+ <% daily = budget_status[:global_daily] %>
15
+ <% percentage = daily[:percentage_used] %>
16
+ <% bar_color = percentage >= 100 ? "bg-red-500" : (percentage >= 80 ? "bg-yellow-500" : "bg-green-500") %>
17
+ <div>
18
+ <div class="flex justify-between items-center mb-1">
19
+ <span class="text-xs font-medium text-gray-600 dark:text-gray-300">Daily</span>
20
+ <span class="text-xs text-gray-500 dark:text-gray-400">
21
+ $<%= number_with_precision(daily[:current], precision: 2) %> / $<%= number_with_precision(daily[:limit], precision: 2) %>
22
+ </span>
23
+ </div>
24
+ <div class="relative w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
25
+ <div class="absolute h-full <%= bar_color %> rounded-full transition-all duration-300" style="width: <%= [percentage, 100].min %>%"></div>
26
+ <%# Soft cap marker at 80% %>
27
+ <div class="absolute h-full w-0.5 bg-yellow-600 dark:bg-yellow-400" style="left: 80%"></div>
28
+ </div>
29
+ <div class="flex justify-between items-center mt-1">
30
+ <span class="text-xs <%= percentage >= 100 ? 'text-red-600 dark:text-red-400 font-medium' : 'text-gray-400 dark:text-gray-500' %>">
31
+ <%= percentage.round(1) %>% used
32
+ </span>
33
+ <span class="text-xs text-gray-400 dark:text-gray-500">
34
+ $<%= number_with_precision(daily[:remaining], precision: 2) %> remaining
35
+ </span>
36
+ </div>
37
+ </div>
38
+ <% end %>
39
+
40
+ <%# Monthly Budget %>
41
+ <% if budget_status[:global_monthly] %>
42
+ <% monthly = budget_status[:global_monthly] %>
43
+ <% percentage = monthly[:percentage_used] %>
44
+ <% bar_color = percentage >= 100 ? "bg-red-500" : (percentage >= 80 ? "bg-yellow-500" : "bg-green-500") %>
45
+ <div>
46
+ <div class="flex justify-between items-center mb-1">
47
+ <span class="text-xs font-medium text-gray-600 dark:text-gray-300">Monthly</span>
48
+ <span class="text-xs text-gray-500 dark:text-gray-400">
49
+ $<%= number_with_precision(monthly[:current], precision: 2) %> / $<%= number_with_precision(monthly[:limit], precision: 2) %>
50
+ </span>
51
+ </div>
52
+ <div class="relative w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
53
+ <div class="absolute h-full <%= bar_color %> rounded-full transition-all duration-300" style="width: <%= [percentage, 100].min %>%"></div>
54
+ <%# Soft cap marker at 80% %>
55
+ <div class="absolute h-full w-0.5 bg-yellow-600 dark:bg-yellow-400" style="left: 80%"></div>
56
+ </div>
57
+ <div class="flex justify-between items-center mt-1">
58
+ <span class="text-xs <%= percentage >= 100 ? 'text-red-600 dark:text-red-400 font-medium' : 'text-gray-400 dark:text-gray-500' %>">
59
+ <%= percentage.round(1) %>% used
60
+ </span>
61
+ <span class="text-xs text-gray-400 dark:text-gray-500">
62
+ $<%= number_with_precision(monthly[:remaining], precision: 2) %> remaining
63
+ </span>
64
+ </div>
65
+ </div>
66
+ <% end %>
67
+ </div>
68
+
69
+ <% unless budget_status[:global_daily] || budget_status[:global_monthly] %>
70
+ <p class="text-sm text-gray-500 dark:text-gray-400 text-center py-2">
71
+ No budget limits configured
72
+ </p>
73
+ <% end %>
74
+ </div>
75
+ <% end %>
@@ -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,84 @@
1
+ <div class="mb-6">
2
+ <div class="flex items-center justify-end mb-3">
3
+ <div class="flex space-x-1 bg-gray-100 dark:bg-gray-700 rounded-lg p-1">
4
+ <%= link_to "Today", ruby_llm_agents.root_path(range: "today"),
5
+ class: "px-3 py-1 text-xs font-medium rounded-md transition-colors #{@selected_range == 'today' ? 'bg-white dark:bg-gray-600 text-gray-900 dark:text-white shadow-sm' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'}" %>
6
+ <%= link_to "7 Days", ruby_llm_agents.root_path(range: "7d"),
7
+ class: "px-3 py-1 text-xs font-medium rounded-md transition-colors #{@selected_range == '7d' ? 'bg-white dark:bg-gray-600 text-gray-900 dark:text-white shadow-sm' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'}" %>
8
+ <%= link_to "30 Days", ruby_llm_agents.root_path(range: "30d"),
9
+ class: "px-3 py-1 text-xs font-medium rounded-md transition-colors #{@selected_range == '30d' ? 'bg-white dark:bg-gray-600 text-gray-900 dark:text-white shadow-sm' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'}" %>
10
+ </div>
11
+ </div>
12
+
13
+ <div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3">
14
+ <!-- Running -->
15
+ <div class="flex items-center bg-white dark:bg-gray-800 rounded-lg px-4 py-3 shadow-sm border border-gray-100 dark:border-gray-700">
16
+ <div class="flex-shrink-0 w-8 h-8 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center mr-3">
17
+ <span class="w-2.5 h-2.5 bg-blue-500 rounded-full animate-pulse"></span>
18
+ </div>
19
+ <div>
20
+ <p class="text-lg font-bold text-gray-900 dark:text-gray-100"><%= now_strip[:running] %></p>
21
+ <p class="text-xs text-gray-500 dark:text-gray-400">Running</p>
22
+ </div>
23
+ </div>
24
+
25
+ <!-- Success -->
26
+ <div class="flex items-center bg-white dark:bg-gray-800 rounded-lg px-4 py-3 shadow-sm border border-gray-100 dark:border-gray-700">
27
+ <div class="flex-shrink-0 w-8 h-8 bg-green-100 dark:bg-green-900 rounded-full flex items-center justify-center mr-3">
28
+ <span class="w-2.5 h-2.5 bg-green-500 rounded-full"></span>
29
+ </div>
30
+ <div>
31
+ <p class="text-lg font-bold text-green-600 dark:text-green-400"><%= now_strip[:success_today] %></p>
32
+ <p class="text-xs text-gray-500 dark:text-gray-400">Success</p>
33
+ </div>
34
+ </div>
35
+
36
+ <!-- Errors -->
37
+ <div class="flex items-center bg-white dark:bg-gray-800 rounded-lg px-4 py-3 shadow-sm border border-gray-100 dark:border-gray-700">
38
+ <div class="flex-shrink-0 w-8 h-8 <%= now_strip[:errors_today] > 0 ? 'bg-red-100 dark:bg-red-900' : 'bg-gray-100 dark:bg-gray-700' %> rounded-full flex items-center justify-center mr-3">
39
+ <span class="w-2.5 h-2.5 <%= now_strip[:errors_today] > 0 ? 'bg-red-500' : 'bg-gray-400' %> rounded-full"></span>
40
+ </div>
41
+ <div>
42
+ <p class="text-lg font-bold <%= now_strip[:errors_today] > 0 ? 'text-red-600 dark:text-red-400' : 'text-gray-900 dark:text-gray-100' %>"><%= now_strip[:errors_today] %></p>
43
+ <p class="text-xs text-gray-500 dark:text-gray-400">Errors</p>
44
+ </div>
45
+ </div>
46
+
47
+ <!-- Timeouts -->
48
+ <div class="flex items-center bg-white dark:bg-gray-800 rounded-lg px-4 py-3 shadow-sm border border-gray-100 dark:border-gray-700">
49
+ <div class="flex-shrink-0 w-8 h-8 <%= now_strip[:timeouts_today] > 0 ? 'bg-yellow-100 dark:bg-yellow-900' : 'bg-gray-100 dark:bg-gray-700' %> rounded-full flex items-center justify-center mr-3">
50
+ <span class="w-2.5 h-2.5 <%= now_strip[:timeouts_today] > 0 ? 'bg-yellow-500' : 'bg-gray-400' %> rounded-full"></span>
51
+ </div>
52
+ <div>
53
+ <p class="text-lg font-bold <%= now_strip[:timeouts_today] > 0 ? 'text-yellow-600 dark:text-yellow-400' : 'text-gray-900 dark:text-gray-100' %>"><%= now_strip[:timeouts_today] %></p>
54
+ <p class="text-xs text-gray-500 dark:text-gray-400">Timeouts</p>
55
+ </div>
56
+ </div>
57
+
58
+ <!-- Cost -->
59
+ <div class="flex items-center bg-white dark:bg-gray-800 rounded-lg px-4 py-3 shadow-sm border border-gray-100 dark:border-gray-700">
60
+ <div class="flex-shrink-0 w-8 h-8 bg-amber-100 dark:bg-amber-900 rounded-full flex items-center justify-center mr-3">
61
+ <svg class="w-4 h-4 text-amber-600 dark:text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
62
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
63
+ </svg>
64
+ </div>
65
+ <div>
66
+ <p class="text-lg font-bold text-gray-900 dark:text-gray-100">$<%= number_with_precision(now_strip[:cost_today], precision: 4) %></p>
67
+ <p class="text-xs text-gray-500 dark:text-gray-400">Cost</p>
68
+ </div>
69
+ </div>
70
+
71
+ <!-- Success Rate -->
72
+ <div class="flex items-center bg-white dark:bg-gray-800 rounded-lg px-4 py-3 shadow-sm border border-gray-100 dark:border-gray-700">
73
+ <div class="flex-shrink-0 w-8 h-8 bg-green-100 dark:bg-green-900 rounded-full flex items-center justify-center mr-3">
74
+ <svg class="w-4 h-4 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
75
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
76
+ </svg>
77
+ </div>
78
+ <div>
79
+ <p class="text-lg font-bold text-gray-900 dark:text-gray-100"><%= now_strip[:success_rate] %>%</p>
80
+ <p class="text-xs text-gray-500 dark:text-gray-400">Success Rate</p>
81
+ </div>
82
+ </div>
83
+ </div>
84
+ </div>
@@ -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 %>
@@ -0,0 +1,49 @@
1
+ <div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 h-full">
2
+ <div class="px-4 py-3 border-b border-gray-100 dark:border-gray-700">
3
+ <h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">Top Errors</h3>
4
+ </div>
5
+
6
+ <% if top_errors.any? %>
7
+ <div class="overflow-x-auto">
8
+ <table class="w-full text-sm">
9
+ <thead>
10
+ <tr class="text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
11
+ <th class="px-4 py-2">Error</th>
12
+ <th class="px-4 py-2 text-right">Count</th>
13
+ <th class="px-4 py-2 text-right">Last Seen</th>
14
+ </tr>
15
+ </thead>
16
+ <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
17
+ <% top_errors.first(5).each do |error| %>
18
+ <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
19
+ <td class="px-4 py-2">
20
+ <span class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate block max-w-[180px]" title="<%= error[:error_class] %>">
21
+ <%= error[:error_class].to_s.split("::").last %>
22
+ </span>
23
+ </td>
24
+ <td class="px-4 py-2 text-right">
25
+ <span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300">
26
+ <%= number_with_delimiter(error[:count]) %>
27
+ </span>
28
+ </td>
29
+ <td class="px-4 py-2 text-right text-xs text-gray-500 dark:text-gray-400">
30
+ <% if error[:last_seen] %>
31
+ <%= time_ago_in_words(error[:last_seen]) %>
32
+ <% else %>
33
+ -
34
+ <% end %>
35
+ </td>
36
+ </tr>
37
+ <% end %>
38
+ </tbody>
39
+ </table>
40
+ </div>
41
+ <% else %>
42
+ <div class="px-4 py-8 text-center text-green-500 dark:text-green-400">
43
+ <svg class="w-8 h-8 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
44
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
45
+ </svg>
46
+ <p class="text-sm">No errors</p>
47
+ </div>
48
+ <% end %>
49
+ </div>
@@ -0,0 +1,155 @@
1
+ <!-- Action Center (only when critical alerts exist) -->
2
+ <%= render partial: "ruby_llm/agents/dashboard/action_center", locals: { critical_alerts: @critical_alerts } %>
3
+
4
+ <!-- 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 } %>
9
+
10
+ <!-- Activity Chart -->
11
+ <div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 mb-6">
12
+ <div class="px-6 py-3 border-b border-gray-100 dark:border-gray-700">
13
+ <div class="flex items-center justify-between">
14
+ <h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">Activity</h3>
15
+ <span id="chart-totals" class="text-sm text-gray-500 dark:text-gray-400"></span>
16
+ </div>
17
+ </div>
18
+
19
+ <div id="activity-chart-container" class="p-4 h-[320px] sm:h-[380px]">
20
+ <div id="activity-chart" style="width: 100%; height: 100%;"></div>
21
+ </div>
22
+
23
+ <script>
24
+ (function() {
25
+ const range = '<%= @selected_range %>';
26
+ const chartUrl = '<%= ruby_llm_agents.chart_data_path %>?range=' + range;
27
+ const hourMs = 3600000;
28
+ const dayMs = 24 * hourMs;
29
+
30
+ function convertToDatetimePoints(data, range) {
31
+ const now = Date.now();
32
+ const numPoints = data.length;
33
+ if (range === 'today') {
34
+ return data.map((val, i) => [now - (numPoints - 1 - i) * hourMs, val]);
35
+ } else {
36
+ return data.map((val, i) => [now - (numPoints - 1 - i) * dayMs, val]);
37
+ }
38
+ }
39
+
40
+ function formatNumber(num) {
41
+ return num.toLocaleString();
42
+ }
43
+
44
+ function initChart() {
45
+ fetch(chartUrl)
46
+ .then(res => res.json())
47
+ .then(data => {
48
+ const now = Date.now();
49
+ const successData = convertToDatetimePoints(data.series[0].data, range);
50
+ const failedData = convertToDatetimePoints(data.series[1].data, range);
51
+ const costData = convertToDatetimePoints(data.series[2].data, range);
52
+
53
+ // Update totals in header
54
+ const totals = data.totals;
55
+ const totalExecs = totals.success + totals.failed;
56
+ document.getElementById('chart-totals').textContent =
57
+ formatNumber(totalExecs) + ' executions • $' + totals.cost.toFixed(2);
58
+
59
+ const timeRange = range === 'today' ? dayMs : (range === '7d' ? 7 * dayMs : 30 * dayMs);
60
+ const dateFormat = range === 'today' ? '%H:%M' : '%b %d';
61
+
62
+ Highcharts.chart('activity-chart', {
63
+ chart: { type: 'areaspline', backgroundColor: 'transparent' },
64
+ title: { text: null },
65
+ xAxis: {
66
+ type: 'datetime',
67
+ min: now - timeRange,
68
+ max: now,
69
+ labels: { style: { color: '#9CA3AF' }, format: '{value:' + dateFormat + '}' },
70
+ lineColor: 'rgba(156, 163, 175, 0.2)'
71
+ },
72
+ yAxis: [{
73
+ title: { text: null },
74
+ min: 0,
75
+ allowDecimals: false,
76
+ labels: { style: { color: '#9CA3AF' } },
77
+ gridLineColor: 'rgba(156, 163, 175, 0.2)'
78
+ }, {
79
+ title: { text: null },
80
+ min: 0,
81
+ labels: { style: { color: '#F59E0B' }, format: '${value}' },
82
+ opposite: true,
83
+ gridLineWidth: 0
84
+ }],
85
+ legend: {
86
+ enabled: true,
87
+ align: 'center',
88
+ verticalAlign: 'bottom',
89
+ itemStyle: { color: '#9CA3AF', fontWeight: 'normal' },
90
+ itemHoverStyle: { color: '#D1D5DB' }
91
+ },
92
+ credits: { enabled: false },
93
+ tooltip: {
94
+ shared: true,
95
+ backgroundColor: 'rgba(17, 24, 39, 0.95)',
96
+ borderColor: 'rgba(75, 85, 99, 0.5)',
97
+ style: { color: '#F3F4F6' },
98
+ xDateFormat: range === 'today' ? '%H:%M' : '%b %d'
99
+ },
100
+ plotOptions: {
101
+ areaspline: {
102
+ fillOpacity: 0.15,
103
+ lineWidth: 2,
104
+ marker: { enabled: false, states: { hover: { enabled: true, radius: 4 } } }
105
+ },
106
+ spline: {
107
+ lineWidth: 2,
108
+ marker: { enabled: false, states: { hover: { enabled: true, radius: 4 } } }
109
+ }
110
+ },
111
+ series: [
112
+ { name: 'Success', type: 'areaspline', color: '#10B981', data: successData, yAxis: 0 },
113
+ { name: 'Failed', type: 'areaspline', color: '#EF4444', data: failedData, yAxis: 0 },
114
+ { name: 'Cost', type: 'spline', color: '#F59E0B', data: costData, yAxis: 1, tooltip: { valuePrefix: '$' } }
115
+ ]
116
+ });
117
+ })
118
+ .catch(err => console.log('[RubyLLM::Agents] Chart error:', err));
119
+ }
120
+
121
+ if (document.readyState === 'loading') {
122
+ document.addEventListener('DOMContentLoaded', initChart);
123
+ } else {
124
+ initChart();
125
+ }
126
+ })();
127
+ </script>
128
+ </div>
129
+
130
+ <!-- Live Activity Feed -->
131
+ <div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 mb-6">
132
+ <div class="px-6 py-3 border-b border-gray-100 dark:border-gray-700">
133
+ <div class="flex justify-between items-center">
134
+ <h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">Recent Activity</h3>
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" %>
136
+ </div>
137
+ </div>
138
+ <div id="activity-feed" class="px-6 py-2">
139
+ <% if @recent_executions.any? %>
140
+ <% @recent_executions.first(5).each do |execution| %>
141
+ <%= render partial: "ruby_llm/agents/dashboard/execution_item", locals: { execution: execution } %>
142
+ <% end %>
143
+ <% else %>
144
+ <div class="py-8 text-center text-gray-500 dark:text-gray-400">
145
+ <p class="text-sm">No executions yet</p>
146
+ </div>
147
+ <% end %>
148
+ </div>
149
+ </div>
150
+
151
+ <!-- Agent Comparison + Top Errors -->
152
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
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 } %>
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 -->