ruby_llm-agents 0.3.4 → 0.3.6

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 (86) 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 +23 -3
  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 +119 -7
  14. data/app/views/layouts/{rubyllm → ruby_llm}/agents/application.html.erb +91 -29
  15. data/app/views/{rubyllm → ruby_llm}/agents/agents/_agent.html.erb +13 -2
  16. data/app/views/ruby_llm/agents/agents/_empty_state.html.erb +23 -0
  17. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +136 -0
  18. data/app/views/ruby_llm/agents/agents/index.html.erb +93 -0
  19. data/app/views/{rubyllm → ruby_llm}/agents/agents/show.html.erb +82 -20
  20. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +112 -0
  21. data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_execution_item.html.erb +7 -4
  22. data/app/views/ruby_llm/agents/dashboard/_tenant_budget.html.erb +115 -0
  23. data/app/views/{rubyllm → ruby_llm}/agents/dashboard/index.html.erb +9 -6
  24. data/app/views/{rubyllm → ruby_llm}/agents/executions/_execution.html.erb +1 -1
  25. data/app/views/{rubyllm → ruby_llm}/agents/executions/_filters.html.erb +39 -11
  26. data/app/views/{rubyllm → ruby_llm}/agents/executions/_list.html.erb +19 -9
  27. data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +101 -0
  28. data/app/views/ruby_llm/agents/executions/index.html.erb +88 -0
  29. data/app/views/{rubyllm → ruby_llm}/agents/executions/show.html.erb +137 -126
  30. data/app/views/{rubyllm → ruby_llm}/agents/shared/_breadcrumbs.html.erb +2 -2
  31. data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +251 -0
  32. data/app/views/{rubyllm → ruby_llm}/agents/shared/_filter_dropdown.html.erb +1 -1
  33. data/app/views/{rubyllm → ruby_llm}/agents/shared/_select_dropdown.html.erb +1 -1
  34. data/app/views/{rubyllm → ruby_llm}/agents/shared/_status_badge.html.erb +1 -1
  35. data/app/views/{rubyllm → ruby_llm}/agents/shared/_status_dot.html.erb +1 -1
  36. data/app/views/ruby_llm/agents/shared/_tenant_filter.html.erb +26 -0
  37. data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +61 -0
  38. data/lib/generators/ruby_llm_agents/agent_generator.rb +9 -4
  39. data/lib/generators/ruby_llm_agents/multi_tenancy_generator.rb +97 -0
  40. data/lib/generators/ruby_llm_agents/templates/add_attempts_migration.rb.tt +3 -3
  41. data/lib/generators/ruby_llm_agents/templates/add_tenant_to_executions_migration.rb.tt +23 -0
  42. data/lib/generators/ruby_llm_agents/templates/add_tool_calls_migration.rb.tt +2 -2
  43. data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +38 -0
  44. data/lib/generators/ruby_llm_agents/templates/create_tenant_budgets_migration.rb.tt +45 -0
  45. data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +17 -5
  46. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +13 -0
  47. data/lib/ruby_llm/agents/alert_manager.rb +20 -16
  48. data/lib/ruby_llm/agents/base/caching.rb +4 -7
  49. data/lib/ruby_llm/agents/base/cost_calculation.rb +5 -3
  50. data/lib/ruby_llm/agents/base/dsl.rb +11 -0
  51. data/lib/ruby_llm/agents/base/execution.rb +62 -9
  52. data/lib/ruby_llm/agents/base/reliability_execution.rb +14 -9
  53. data/lib/ruby_llm/agents/base.rb +26 -0
  54. data/lib/ruby_llm/agents/budget_tracker.rb +250 -139
  55. data/lib/ruby_llm/agents/cache_helper.rb +98 -0
  56. data/lib/ruby_llm/agents/circuit_breaker.rb +48 -30
  57. data/lib/ruby_llm/agents/configuration.rb +40 -1
  58. data/lib/ruby_llm/agents/engine.rb +65 -1
  59. data/lib/ruby_llm/agents/inflections.rb +14 -0
  60. data/lib/ruby_llm/agents/instrumentation.rb +66 -0
  61. data/lib/ruby_llm/agents/reliability.rb +8 -2
  62. data/lib/ruby_llm/agents/version.rb +1 -1
  63. data/lib/ruby_llm/agents/workflow/instrumentation.rb +254 -0
  64. data/lib/ruby_llm/agents/workflow/parallel.rb +282 -0
  65. data/lib/ruby_llm/agents/workflow/pipeline.rb +306 -0
  66. data/lib/ruby_llm/agents/workflow/result.rb +390 -0
  67. data/lib/ruby_llm/agents/workflow/router.rb +429 -0
  68. data/lib/ruby_llm/agents/workflow.rb +248 -0
  69. data/lib/ruby_llm/agents.rb +1 -0
  70. metadata +50 -60
  71. data/app/views/rubyllm/agents/agents/index.html.erb +0 -20
  72. data/app/views/rubyllm/agents/dashboard/_agent_comparison.html.erb +0 -46
  73. data/app/views/rubyllm/agents/executions/index.html.erb +0 -28
  74. data/app/views/rubyllm/agents/executions/index.turbo_stream.erb +0 -18
  75. data/app/views/rubyllm/agents/shared/_executions_table.html.erb +0 -193
  76. /data/app/views/{rubyllm → ruby_llm}/agents/agents/_version_comparison.html.erb +0 -0
  77. /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_action_center.html.erb +0 -0
  78. /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_alerts_feed.html.erb +0 -0
  79. /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_breaker_strip.html.erb +0 -0
  80. /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_budgets_bar.html.erb +0 -0
  81. /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_now_strip.html.erb +0 -0
  82. /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_top_errors.html.erb +0 -0
  83. /data/app/views/{rubyllm → ruby_llm}/agents/executions/dry_run.html.erb +0 -0
  84. /data/app/views/{rubyllm → ruby_llm}/agents/settings/show.html.erb +0 -0
  85. /data/app/views/{rubyllm → ruby_llm}/agents/shared/_nav_link.html.erb +0 -0
  86. /data/app/views/{rubyllm → ruby_llm}/agents/shared/_stat_card.html.erb +0 -0
@@ -1,4 +1,4 @@
1
- <%= render "rubyllm/agents/shared/breadcrumbs", items: [
1
+ <%= render "ruby_llm/agents/shared/breadcrumbs", items: [
2
2
  { label: "Dashboard", path: ruby_llm_agents.root_path },
3
3
  { label: "Agents", path: ruby_llm_agents.agents_path },
4
4
  { label: @agent_type.gsub(/Agent$/, '') }
@@ -45,6 +45,11 @@
45
45
  <%= @config[:model] %> &middot; temp <%= @config[:temperature] %>
46
46
  &middot; timeout <%= @config[:timeout] %>s
47
47
  </p>
48
+ <% if @config[:description].present? %>
49
+ <p class="text-gray-600 dark:text-gray-300 mt-2">
50
+ <%= @config[:description] %>
51
+ </p>
52
+ <% end %>
48
53
  <% end %>
49
54
  </div>
50
55
 
@@ -153,14 +158,14 @@
153
158
  <% success_rate_color = success_rate >= 95 ? 'text-green-600' : success_rate >= 80 ? 'text-yellow-600' : 'text-red-600' %>
154
159
 
155
160
  <div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4 mb-6">
156
- <%= render "rubyllm/agents/shared/stat_card",
161
+ <%= render "ruby_llm/agents/shared/stat_card",
157
162
  title: "Executions",
158
163
  value: number_with_delimiter(@stats[:count]),
159
164
  subtitle: "Today: #{@stats_today[:count]}",
160
165
  icon: "M13 10V3L4 14h7v7l9-11h-7z",
161
166
  icon_color: "text-blue-500" %>
162
167
 
163
- <%= render "rubyllm/agents/shared/stat_card",
168
+ <%= render "ruby_llm/agents/shared/stat_card",
164
169
  title: "Success Rate",
165
170
  value: "#{success_rate}%",
166
171
  subtitle: "Error rate: #{@stats[:error_rate] || 0}%",
@@ -168,27 +173,27 @@
168
173
  icon_color: "text-green-500",
169
174
  value_color: success_rate_color %>
170
175
 
171
- <%= render "rubyllm/agents/shared/stat_card",
176
+ <%= render "ruby_llm/agents/shared/stat_card",
172
177
  title: "Total Cost",
173
178
  value: "$#{number_with_precision(@stats[:total_cost] || 0, precision: 4)}",
174
179
  subtitle: "Avg: $#{number_with_precision(@stats[:avg_cost] || 0, precision: 6)}",
175
180
  icon: "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",
176
181
  icon_color: "text-amber-500" %>
177
182
 
178
- <%= render "rubyllm/agents/shared/stat_card",
183
+ <%= render "ruby_llm/agents/shared/stat_card",
179
184
  title: "Total Tokens",
180
185
  value: number_with_delimiter(@stats[:total_tokens] || 0),
181
186
  subtitle: "Avg: #{number_with_delimiter(@stats[:avg_tokens] || 0)}",
182
187
  icon: "M7 20l4-16m2 16l4-16M6 9h14M4 15h14",
183
188
  icon_color: "text-indigo-500" %>
184
189
 
185
- <%= render "rubyllm/agents/shared/stat_card",
190
+ <%= render "ruby_llm/agents/shared/stat_card",
186
191
  title: "Avg Duration",
187
192
  value: "#{number_with_delimiter(@stats[:avg_duration_ms] || 0)} ms",
188
193
  icon: "M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z",
189
194
  icon_color: "text-purple-500" %>
190
195
 
191
- <%= render "rubyllm/agents/shared/stat_card",
196
+ <%= render "ruby_llm/agents/shared/stat_card",
192
197
  title: "Cache Hit Rate",
193
198
  value: "#{@cache_hit_rate}%",
194
199
  icon: "M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4",
@@ -284,7 +289,7 @@
284
289
  <% end %>
285
290
 
286
291
  <!-- Version Comparison -->
287
- <%= render partial: "rubyllm/agents/agents/version_comparison",
292
+ <%= render partial: "ruby_llm/agents/agents/version_comparison",
288
293
  locals: { versions: @versions, version_comparison: @version_comparison } %>
289
294
 
290
295
  <% if @config %>
@@ -341,8 +346,9 @@
341
346
 
342
347
  <!-- Reliability Configuration -->
343
348
  <% retries_config = @config[:retries] || {}
349
+ fallback_models = Array(@config[:fallback_models]).compact
344
350
  has_retries = (retries_config[:max] || 0) > 0
345
- has_fallbacks = @config[:fallback_models].present? && @config[:fallback_models].any?
351
+ has_fallbacks = fallback_models.any?
346
352
  has_total_timeout = @config[:total_timeout].present?
347
353
  has_circuit_breaker = @config[:circuit_breaker].present?
348
354
  has_any_reliability = has_retries || has_fallbacks || has_total_timeout || has_circuit_breaker %>
@@ -446,7 +452,7 @@
446
452
 
447
453
  <% if has_fallbacks %>
448
454
  <p class="text-xs text-gray-600 dark:text-gray-400 mt-1">
449
- <%= @config[:fallback_models].join(" → ") %>
455
+ <%= fallback_models.join(" → ") %>
450
456
  </p>
451
457
  <% else %>
452
458
  <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">
@@ -601,6 +607,63 @@
601
607
  </div>
602
608
  </div>
603
609
  <% end %>
610
+
611
+ <!-- Available Tools -->
612
+ <%
613
+ class_tools = @agent_class.respond_to?(:tools) ? (@agent_class.tools || []) : []
614
+ has_dynamic_tools = @agent_class.instance_methods(false).include?(:tools)
615
+ %>
616
+ <div class="border-t border-gray-100 dark:border-gray-700 pt-4">
617
+ <p
618
+ class="
619
+ text-xs text-gray-500 dark:text-gray-400 uppercase
620
+ tracking-wider mb-3
621
+ "
622
+ >
623
+ Available Tools
624
+ <% if class_tools.any? %>
625
+ <span class="inline-flex items-center ml-2 px-1.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-300">
626
+ <%= class_tools.size %>
627
+ </span>
628
+ <% elsif has_dynamic_tools %>
629
+ <span class="inline-flex items-center ml-2 px-1.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 dark:bg-purple-900/50 text-purple-800 dark:text-purple-300">
630
+ Dynamic
631
+ </span>
632
+ <% else %>
633
+ <span class="inline-flex items-center ml-2 px-1.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400">
634
+ 0
635
+ </span>
636
+ <% end %>
637
+ </p>
638
+
639
+ <% if class_tools.any? %>
640
+ <div class="space-y-2">
641
+ <% class_tools.each do |tool_class| %>
642
+ <div class="flex items-center text-sm">
643
+ <code
644
+ class="
645
+ bg-gray-100 dark:bg-gray-700 dark:text-gray-200 px-2
646
+ py-0.5 rounded font-mono
647
+ "
648
+ >
649
+ <%= tool_class.respond_to?(:tool_name) ? tool_class.tool_name : tool_class.name.demodulize %>
650
+ </code>
651
+ </div>
652
+ <% end %>
653
+ </div>
654
+ <% elsif has_dynamic_tools %>
655
+ <p class="text-sm text-purple-600 dark:text-purple-400">
656
+ This agent configures tools dynamically at runtime based on context.
657
+ </p>
658
+ <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">
659
+ Tools vary per execution based on feature flags and configuration.
660
+ </p>
661
+ <% else %>
662
+ <p class="text-sm text-gray-400 dark:text-gray-500 italic">
663
+ No tools configured for this agent.
664
+ </p>
665
+ <% end %>
666
+ </div>
604
667
  </div>
605
668
  <% end %>
606
669
 
@@ -610,7 +673,7 @@
610
673
  Executions
611
674
  </h3>
612
675
 
613
- <%= turbo_frame_tag "executions_table" do %>
676
+ <div id="executions_table">
614
677
  <%
615
678
  has_filters = params[:statuses].present? || params[:versions].present? || params[:models].present? || params[:temperatures].present? || params[:days].present?
616
679
  selected_statuses = params[:statuses].present? ? (params[:statuses].is_a?(Array) ? params[:statuses] : params[:statuses].split(",")) : []
@@ -635,10 +698,10 @@
635
698
  ]
636
699
  %>
637
700
 
638
- <%= form_with url: ruby_llm_agents.agent_path(@agent_type), method: :get, data: { turbo_frame: "executions_table" } do |f| %>
701
+ <%= form_with url: ruby_llm_agents.agent_path(@agent_type), method: :get, local: true do |f| %>
639
702
  <div class="flex flex-wrap items-center gap-3 mb-4 pb-4 border-b border-gray-100 dark:border-gray-700">
640
703
  <%# Status Filter (Multi-select) %>
641
- <%= render "rubyllm/agents/shared/filter_dropdown",
704
+ <%= render "ruby_llm/agents/shared/filter_dropdown",
642
705
  name: "statuses[]",
643
706
  filter_id: "statuses",
644
707
  label: "Status",
@@ -648,7 +711,7 @@
648
711
 
649
712
  <%# Version Filter (Multi-select) %>
650
713
  <% if @versions.any? %>
651
- <%= render "rubyllm/agents/shared/filter_dropdown",
714
+ <%= render "ruby_llm/agents/shared/filter_dropdown",
652
715
  name: "versions[]",
653
716
  filter_id: "versions",
654
717
  label: "Version",
@@ -660,7 +723,7 @@
660
723
 
661
724
  <%# Model Filter (Multi-select) %>
662
725
  <% if @models.length > 1 %>
663
- <%= render "rubyllm/agents/shared/filter_dropdown",
726
+ <%= render "ruby_llm/agents/shared/filter_dropdown",
664
727
  name: "models[]",
665
728
  filter_id: "models",
666
729
  label: "Model",
@@ -672,7 +735,7 @@
672
735
 
673
736
  <%# Temperature Filter (Multi-select) %>
674
737
  <% if @temperatures.length > 1 %>
675
- <%= render "rubyllm/agents/shared/filter_dropdown",
738
+ <%= render "ruby_llm/agents/shared/filter_dropdown",
676
739
  name: "temperatures[]",
677
740
  filter_id: "temperatures",
678
741
  label: "Temp",
@@ -683,7 +746,7 @@
683
746
  <% end %>
684
747
 
685
748
  <%# Time Range Filter (Single-select) %>
686
- <%= render "rubyllm/agents/shared/select_dropdown",
749
+ <%= render "ruby_llm/agents/shared/select_dropdown",
687
750
  name: "days",
688
751
  filter_id: "days",
689
752
  options: days_options,
@@ -693,7 +756,6 @@
693
756
  <%# Clear Filters %>
694
757
  <% if has_filters %>
695
758
  <%= link_to ruby_llm_agents.agent_path(@agent_type),
696
- data: { turbo_frame: "executions_table" },
697
759
  class: "flex items-center gap-1 px-3 py-2 text-sm text-red-500 dark:text-red-400 hover:text-red-600 dark:hover:text-red-300 hover:bg-red-50 dark:hover:bg-red-900/30 rounded-lg transition-colors" do %>
698
760
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
699
761
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
@@ -713,6 +775,6 @@
713
775
  </div>
714
776
  <% end %>
715
777
 
716
- <%= render "rubyllm/agents/shared/executions_table", executions: @executions, pagination: @pagination %>
717
- <% end %>
778
+ <%= render "ruby_llm/agents/shared/executions_table", executions: @executions, pagination: @pagination %>
779
+ </div>
718
780
  </div>
@@ -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 -->