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
@@ -84,7 +84,7 @@
84
84
  }
85
85
 
86
86
  // Initialize on page load
87
- document.addEventListener('turbo:load', function() {
87
+ document.addEventListener('DOMContentLoaded', function() {
88
88
  updateClock();
89
89
 
90
90
  // Handle data-href clickable rows (semantic alternative to onclick)
@@ -96,9 +96,6 @@
96
96
  });
97
97
  });
98
98
  });
99
-
100
- // Also run on DOMContentLoaded for non-Turbo loads
101
- document.addEventListener('DOMContentLoaded', updateClock);
102
99
  </script>
103
100
 
104
101
  <!-- Theme Switcher (Alpine.js) -->
@@ -188,34 +185,22 @@
188
185
  @apply bg-orange-100 dark:bg-orange-900/50 text-orange-700 dark:text-orange-300;
189
186
  }
190
187
 
191
- /* Turbo loading states */
192
- turbo-frame[busy] {
193
- position: relative;
194
- opacity: 0.6;
195
- pointer-events: none;
188
+ /* Workflow type badges */
189
+ .badge-pipeline {
190
+ @apply bg-indigo-100 dark:bg-indigo-900/50 text-indigo-700 dark:text-indigo-300;
196
191
  }
197
- turbo-frame[busy]::after {
198
- content: '';
199
- position: absolute;
200
- top: 50%;
201
- left: 50%;
202
- width: 24px;
203
- height: 24px;
204
- margin: -12px 0 0 -12px;
205
- border: 2px solid #e5e7eb;
206
- border-top-color: #3b82f6;
207
- border-radius: 50%;
208
- animation: spin 0.8s linear infinite;
192
+ .badge-parallel {
193
+ @apply bg-cyan-100 dark:bg-cyan-900/50 text-cyan-700 dark:text-cyan-300;
209
194
  }
210
- @keyframes spin {
211
- to { transform: rotate(360deg); }
195
+ .badge-router {
196
+ @apply bg-amber-100 dark:bg-amber-900/50 text-amber-700 dark:text-amber-300;
212
197
  }
213
198
 
214
- /* Form submitting state */
215
- form[data-turbo-submitting] {
216
- opacity: 0.6;
217
- pointer-events: none;
199
+ /* Alpine.js utilities */
200
+ [x-cloak] {
201
+ display: none !important;
218
202
  }
203
+
219
204
  </style>
220
205
  </head>
221
206
 
@@ -250,7 +235,7 @@
250
235
  %>
251
236
  <nav class="hidden md:flex items-center space-x-1">
252
237
  <% nav_items.each do |item| %>
253
- <%= render "rubyllm/agents/shared/nav_link", path: item[:path], label: item[:label], icon: item[:icon], mobile: false %>
238
+ <%= render "ruby_llm/agents/shared/nav_link", path: item[:path], label: item[:label], icon: item[:icon], mobile: false %>
254
239
  <% end %>
255
240
  </nav>
256
241
  </div>
@@ -261,6 +246,57 @@
261
246
  dark:text-gray-400
262
247
  "
263
248
  >
249
+ <%# Tenant Selector Dropdown %>
250
+ <% if tenant_filter_enabled? && available_tenants.any? %>
251
+ <div x-data="{ open: false }" class="relative">
252
+ <button
253
+ @click="open = !open"
254
+ @click.outside="open = false"
255
+ type="button"
256
+ class="flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium rounded-md border transition-colors
257
+ <%= current_tenant_id.present? ?
258
+ 'bg-blue-50 dark:bg-blue-900/30 border-blue-200 dark:border-blue-800 text-blue-700 dark:text-blue-300' :
259
+ 'bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600' %>"
260
+ >
261
+ <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
262
+ <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" />
263
+ </svg>
264
+ <span class="hidden sm:inline"><%= current_tenant_id.present? ? current_tenant_id : 'All Tenants' %></span>
265
+ <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
266
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
267
+ </svg>
268
+ </button>
269
+
270
+ <div
271
+ x-show="open"
272
+ x-cloak
273
+ x-transition:enter="transition ease-out duration-100"
274
+ x-transition:enter-start="opacity-0 scale-95"
275
+ x-transition:enter-end="opacity-100 scale-100"
276
+ x-transition:leave="transition ease-in duration-75"
277
+ x-transition:leave-start="opacity-100 scale-100"
278
+ x-transition:leave-end="opacity-0 scale-95"
279
+ class="absolute right-0 mt-1 w-48 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50 py-1"
280
+ >
281
+ <a
282
+ href="<%= url_for(request.query_parameters.except('tenant_id').merge(tenant_id: nil)) %>"
283
+ class="block px-3 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 <%= 'bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300' if current_tenant_id.blank? %>"
284
+ >
285
+ All Tenants
286
+ </a>
287
+ <div class="border-t border-gray-100 dark:border-gray-700 my-1"></div>
288
+ <% available_tenants.each do |tenant| %>
289
+ <a
290
+ href="<%= url_for(request.query_parameters.merge(tenant_id: tenant)) %>"
291
+ class="block px-3 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 <%= 'bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300' if tenant == current_tenant_id %>"
292
+ >
293
+ <%= tenant %>
294
+ </a>
295
+ <% end %>
296
+ </div>
297
+ </div>
298
+ <% end %>
299
+
264
300
  <span id="live-clock" class="tabular-nums"></span>
265
301
 
266
302
  <button
@@ -327,12 +363,38 @@
327
363
  >
328
364
  <nav class="max-w-7xl mx-auto px-4 py-3 space-y-1">
329
365
  <% nav_items.each do |item| %>
330
- <%= render "rubyllm/agents/shared/nav_link", path: item[:path], label: item[:label], icon: item[:icon], mobile: true %>
366
+ <%= render "ruby_llm/agents/shared/nav_link", path: item[:path], label: item[:label], icon: item[:icon], mobile: true %>
331
367
  <% end %>
332
368
  </nav>
333
369
  </div>
334
370
  </header>
335
371
 
372
+ <%# Tenant Context Badge - shows when viewing specific tenant %>
373
+ <% if tenant_filter_enabled? && current_tenant_id.present? %>
374
+ <div class="bg-blue-50 dark:bg-blue-900/20 border-b border-blue-100 dark:border-blue-900/50">
375
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-2">
376
+ <div class="flex items-center justify-between">
377
+ <div class="flex items-center gap-2 text-sm text-blue-700 dark:text-blue-300">
378
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
379
+ <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" />
380
+ </svg>
381
+ <span>Viewing tenant:</span>
382
+ <span class="font-semibold"><%= current_tenant_id %></span>
383
+ </div>
384
+ <a
385
+ href="<%= url_for(request.query_parameters.except('tenant_id')) %>"
386
+ class="flex items-center gap-1 text-sm text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-200"
387
+ >
388
+ <span>Clear filter</span>
389
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
390
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
391
+ </svg>
392
+ </a>
393
+ </div>
394
+ </div>
395
+ </div>
396
+ <% end %>
397
+
336
398
  <!-- Main content -->
337
399
  <main class="max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-8 h-full">
338
400
  <%= yield %>
@@ -0,0 +1,23 @@
1
+ <%
2
+ type = local_assigns[:type] || "agents"
3
+ is_workflow = type == "workflows"
4
+ %>
5
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-8 text-center">
6
+ <svg class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
7
+ <% if is_workflow %>
8
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="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"/>
9
+ <% else %>
10
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
11
+ <% end %>
12
+ </svg>
13
+ <h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-gray-100">
14
+ No <%= type %> found
15
+ </h3>
16
+ <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
17
+ <% if is_workflow %>
18
+ Create a workflow by subclassing <code class="bg-gray-100 dark:bg-gray-700 dark:text-gray-200 px-1 rounded">Pipeline</code>, <code class="bg-gray-100 dark:bg-gray-700 dark:text-gray-200 px-1 rounded">Parallel</code>, or <code class="bg-gray-100 dark:bg-gray-700 dark:text-gray-200 px-1 rounded">Router</code>
19
+ <% else %>
20
+ Create an agent by running <code class="bg-gray-100 dark:bg-gray-700 dark:text-gray-200 px-1 rounded">rails g ruby_llm_agents:agent YourAgentName</code>
21
+ <% end %>
22
+ </p>
23
+ </div>
@@ -0,0 +1,125 @@
1
+ <%
2
+ workflow_colors = {
3
+ "pipeline" => { border: "border-l-indigo-500", icon_bg: "bg-indigo-100 dark:bg-indigo-900/50", icon_text: "text-indigo-600 dark:text-indigo-300" },
4
+ "parallel" => { border: "border-l-cyan-500", icon_bg: "bg-cyan-100 dark:bg-cyan-900/50", icon_text: "text-cyan-600 dark:text-cyan-300" },
5
+ "router" => { border: "border-l-amber-500", icon_bg: "bg-amber-100 dark:bg-amber-900/50", icon_text: "text-amber-600 dark:text-amber-300" }
6
+ }
7
+ colors = workflow_colors[workflow[:workflow_type]] || { border: "border-l-gray-400", icon_bg: "bg-gray-100", icon_text: "text-gray-600" }
8
+
9
+ child_label = case workflow[:workflow_type]
10
+ when "pipeline" then "steps"
11
+ when "parallel" then "branches"
12
+ when "router" then "routes"
13
+ else "children"
14
+ end
15
+ %>
16
+
17
+ <div x-data="{ expanded: false }" class="block bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-md transition-shadow border-l-4 <%= colors[:border] %>">
18
+ <%= link_to ruby_llm_agents.agent_path(workflow[:name]), class: "block p-4 sm:p-5" do %>
19
+ <!-- Header Row -->
20
+ <div class="flex items-center justify-between gap-2">
21
+ <div class="flex items-center gap-2 min-w-0">
22
+ <%= render "ruby_llm/agents/shared/workflow_type_badge", workflow_type: workflow[:workflow_type], size: :sm %>
23
+ <h3 class="font-semibold text-gray-900 dark:text-gray-100 truncate">
24
+ <%= workflow[:name].gsub(/Workflow$/, '').gsub(/Pipeline$|Parallel$|Router$/, '') %>
25
+ </h3>
26
+ <span class="hidden sm:inline text-sm text-gray-500 dark:text-gray-400">v<%= workflow[:version] %></span>
27
+ <% if workflow[:active] %>
28
+ <span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-300">Active</span>
29
+ <% else %>
30
+ <span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400">Deleted</span>
31
+ <% end %>
32
+ </div>
33
+ <span class="hidden sm:block text-sm text-gray-500 dark:text-gray-400">
34
+ <%= workflow[:workflow_children]&.size || 0 %> <%= child_label %>
35
+ </span>
36
+ </div>
37
+
38
+ <!-- Stats Row -->
39
+ <div class="mt-2 sm:mt-3 sm:border-t sm:border-gray-100 sm:dark:border-gray-700 sm:pt-3">
40
+ <% success_rate = workflow[:success_rate] || 0 %>
41
+ <!-- Mobile: compact inline -->
42
+ <div class="sm:hidden text-xs text-gray-500 dark:text-gray-400">
43
+ <%= number_with_delimiter(workflow[:execution_count]) %> runs
44
+ <span class="mx-1 text-gray-300 dark:text-gray-600">-</span>
45
+ $<%= number_with_precision(workflow[:total_cost] || 0, precision: 2) %>
46
+ <span class="mx-1 text-gray-300 dark:text-gray-600">-</span>
47
+ <span class="<%= success_rate >= 95 ? 'text-green-600 dark:text-green-400' : success_rate >= 80 ? 'text-yellow-600 dark:text-yellow-400' : 'text-red-600 dark:text-red-400' %>">
48
+ <%= success_rate %>%
49
+ </span>
50
+ </div>
51
+
52
+ <!-- Desktop: full stats with icons -->
53
+ <div class="hidden sm:flex items-center justify-between text-sm">
54
+ <div class="flex items-center space-x-6">
55
+ <div class="flex items-center text-gray-600 dark:text-gray-300">
56
+ <svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
57
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
58
+ </svg>
59
+ <span><%= number_with_delimiter(workflow[:execution_count]) %> executions</span>
60
+ </div>
61
+ <div class="flex items-center text-gray-600 dark:text-gray-300">
62
+ <svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
63
+ <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" />
64
+ </svg>
65
+ <span>$<%= number_with_precision(workflow[:total_cost] || 0, precision: 4) %></span>
66
+ </div>
67
+ <div class="flex items-center">
68
+ <% if success_rate >= 95 %>
69
+ <span class="w-2 h-2 rounded-full bg-green-500 mr-1.5"></span>
70
+ <span class="text-green-600 dark:text-green-400"><%= success_rate %>%</span>
71
+ <% elsif success_rate >= 80 %>
72
+ <span class="w-2 h-2 rounded-full bg-yellow-500 mr-1.5"></span>
73
+ <span class="text-yellow-600 dark:text-yellow-400"><%= success_rate %>%</span>
74
+ <% else %>
75
+ <span class="w-2 h-2 rounded-full bg-red-500 mr-1.5"></span>
76
+ <span class="text-red-600 dark:text-red-400"><%= success_rate %>%</span>
77
+ <% end %>
78
+ </div>
79
+ </div>
80
+ <div class="text-gray-400 dark:text-gray-500">
81
+ <% if workflow[:last_executed] %>
82
+ <%= time_ago_in_words(workflow[:last_executed]) %> ago
83
+ <% else %>
84
+ Never executed
85
+ <% end %>
86
+ </div>
87
+ </div>
88
+ </div>
89
+ <% end %>
90
+
91
+ <!-- Expandable Child Agents Section -->
92
+ <% if workflow[:workflow_children].present? && workflow[:workflow_children].any? %>
93
+ <div class="border-t border-gray-100 dark:border-gray-700 px-4 sm:px-5 py-2">
94
+ <button type="button" @click.prevent="expanded = !expanded" class="w-full flex items-center justify-between text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300">
95
+ <span class="font-medium">
96
+ <%= child_label.capitalize %> (<%= workflow[:workflow_children].size %>)
97
+ </span>
98
+ <svg class="w-4 h-4 transition-transform duration-200" :class="{ 'rotate-180': expanded }" fill="none" stroke="currentColor" viewBox="0 0 24 24">
99
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
100
+ </svg>
101
+ </button>
102
+
103
+ <div x-show="expanded" x-cloak x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 -translate-y-1" x-transition:enter-end="opacity-100 translate-y-0" class="mt-2 space-y-1">
104
+ <% workflow[:workflow_children].each_with_index do |child, index| %>
105
+ <div class="flex items-center gap-2 text-sm py-1.5 px-2 rounded bg-gray-50 dark:bg-gray-900/50">
106
+ <span class="w-5 h-5 flex items-center justify-center rounded-full <%= colors[:icon_bg] %> text-xs <%= colors[:icon_text] %>">
107
+ <%= index + 1 %>
108
+ </span>
109
+ <span class="font-medium text-gray-700 dark:text-gray-300"><%= child[:name] %></span>
110
+ <span class="text-gray-400 dark:text-gray-500">-></span>
111
+ <span class="text-gray-600 dark:text-gray-400 font-mono text-xs"><%= child[:agent] %></span>
112
+ <% if child[:optional] %>
113
+ <span class="text-xs text-gray-400 dark:text-gray-500 italic">(optional)</span>
114
+ <% end %>
115
+ <% if child[:description].present? %>
116
+ <span class="text-xs text-gray-400 dark:text-gray-500 truncate max-w-xs" title="<%= child[:description] %>">
117
+ - <%= child[:description].truncate(30) %>
118
+ </span>
119
+ <% end %>
120
+ </div>
121
+ <% end %>
122
+ </div>
123
+ </div>
124
+ <% end %>
125
+ </div>
@@ -0,0 +1,93 @@
1
+ <div class="mb-6">
2
+ <h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">Agents & Workflows</h1>
3
+ <p class="text-gray-500 dark:text-gray-400 mt-1">All available agents and workflows with execution statistics</p>
4
+ </div>
5
+
6
+ <div x-data="{ activeTab: 'agents', activeSubTab: null }">
7
+ <!-- Tab Navigation -->
8
+ <div class="border-b border-gray-200 dark:border-gray-700 mb-6">
9
+ <nav class="-mb-px flex space-x-6" aria-label="Tabs">
10
+ <!-- Agents Tab -->
11
+ <button type="button" @click="activeTab = 'agents'; activeSubTab = null"
12
+ :class="activeTab === 'agents' ? 'border-blue-500 text-blue-600 dark:text-blue-400' : 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300'"
13
+ class="whitespace-nowrap py-3 px-1 border-b-2 font-medium text-sm flex items-center gap-2 transition-colors">
14
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
15
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
16
+ </svg>
17
+ Agents
18
+ <span class="inline-flex items-center justify-center px-2 py-0.5 rounded-full text-xs font-medium"
19
+ :class="activeTab === 'agents' ? 'bg-blue-100 dark:bg-blue-900/50 text-blue-600 dark:text-blue-300' : 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400'">
20
+ <%= @agent_count %>
21
+ </span>
22
+ </button>
23
+
24
+ <!-- Workflows Tab -->
25
+ <button type="button" @click="activeTab = 'workflows'; activeSubTab = null"
26
+ :class="activeTab === 'workflows' ? 'border-blue-500 text-blue-600 dark:text-blue-400' : 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300'"
27
+ class="whitespace-nowrap py-3 px-1 border-b-2 font-medium text-sm flex items-center gap-2 transition-colors">
28
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
29
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="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"/>
30
+ </svg>
31
+ Workflows
32
+ <span class="inline-flex items-center justify-center px-2 py-0.5 rounded-full text-xs font-medium"
33
+ :class="activeTab === 'workflows' ? 'bg-blue-100 dark:bg-blue-900/50 text-blue-600 dark:text-blue-300' : 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400'">
34
+ <%= @workflow_count %>
35
+ </span>
36
+ </button>
37
+ </nav>
38
+ </div>
39
+
40
+ <!-- Workflow Sub-tabs -->
41
+ <div x-show="activeTab === 'workflows'" x-cloak class="mb-4">
42
+ <div class="flex flex-wrap gap-2">
43
+ <button type="button" @click="activeSubTab = null"
44
+ :class="activeSubTab === null ? 'bg-gray-900 dark:bg-gray-100 text-white dark:text-gray-900' : 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'"
45
+ class="px-3 py-1.5 rounded-md text-sm font-medium transition-colors">
46
+ All (<%= @workflow_count %>)
47
+ </button>
48
+ <button type="button" @click="activeSubTab = 'pipeline'"
49
+ :class="activeSubTab === 'pipeline' ? 'bg-indigo-600 text-white' : 'bg-indigo-100 dark:bg-indigo-900/50 text-indigo-700 dark:text-indigo-300 hover:bg-indigo-200 dark:hover:bg-indigo-800'"
50
+ class="px-3 py-1.5 rounded-md text-sm font-medium transition-colors inline-flex items-center gap-1.5">
51
+ <span aria-hidden="true">-></span> Pipeline (<%= @workflows_by_type[:pipeline].size %>)
52
+ </button>
53
+ <button type="button" @click="activeSubTab = 'parallel'"
54
+ :class="activeSubTab === 'parallel' ? 'bg-cyan-600 text-white' : 'bg-cyan-100 dark:bg-cyan-900/50 text-cyan-700 dark:text-cyan-300 hover:bg-cyan-200 dark:hover:bg-cyan-800'"
55
+ class="px-3 py-1.5 rounded-md text-sm font-medium transition-colors inline-flex items-center gap-1.5">
56
+ <span aria-hidden="true">//</span> Parallel (<%= @workflows_by_type[:parallel].size %>)
57
+ </button>
58
+ <button type="button" @click="activeSubTab = 'router'"
59
+ :class="activeSubTab === 'router' ? 'bg-amber-600 text-white' : 'bg-amber-100 dark:bg-amber-900/50 text-amber-700 dark:text-amber-300 hover:bg-amber-200 dark:hover:bg-amber-800'"
60
+ class="px-3 py-1.5 rounded-md text-sm font-medium transition-colors inline-flex items-center gap-1.5">
61
+ <span aria-hidden="true">&lt;&gt;</span> Router (<%= @workflows_by_type[:router].size %>)
62
+ </button>
63
+ </div>
64
+ </div>
65
+
66
+ <!-- Agents Content -->
67
+ <div x-show="activeTab === 'agents'">
68
+ <% if @agents.empty? %>
69
+ <%= render "ruby_llm/agents/agents/empty_state", type: "agents" %>
70
+ <% else %>
71
+ <div class="space-y-3 sm:space-y-4">
72
+ <% @agents.each do |agent| %>
73
+ <%= render partial: "ruby_llm/agents/agents/agent", locals: { agent: agent } %>
74
+ <% end %>
75
+ </div>
76
+ <% end %>
77
+ </div>
78
+
79
+ <!-- Workflows Content -->
80
+ <div x-show="activeTab === 'workflows'" x-cloak>
81
+ <% if @workflows.empty? %>
82
+ <%= render "ruby_llm/agents/agents/empty_state", type: "workflows" %>
83
+ <% else %>
84
+ <div class="space-y-3 sm:space-y-4">
85
+ <% @workflows.each do |workflow| %>
86
+ <div x-show="activeSubTab === null || activeSubTab === '<%= workflow[:workflow_type] %>'">
87
+ <%= render partial: "ruby_llm/agents/agents/workflow", locals: { workflow: workflow } %>
88
+ </div>
89
+ <% end %>
90
+ </div>
91
+ <% end %>
92
+ </div>
93
+ </div>
@@ -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$/, '') }
@@ -153,14 +153,14 @@
153
153
  <% success_rate_color = success_rate >= 95 ? 'text-green-600' : success_rate >= 80 ? 'text-yellow-600' : 'text-red-600' %>
154
154
 
155
155
  <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",
156
+ <%= render "ruby_llm/agents/shared/stat_card",
157
157
  title: "Executions",
158
158
  value: number_with_delimiter(@stats[:count]),
159
159
  subtitle: "Today: #{@stats_today[:count]}",
160
160
  icon: "M13 10V3L4 14h7v7l9-11h-7z",
161
161
  icon_color: "text-blue-500" %>
162
162
 
163
- <%= render "rubyllm/agents/shared/stat_card",
163
+ <%= render "ruby_llm/agents/shared/stat_card",
164
164
  title: "Success Rate",
165
165
  value: "#{success_rate}%",
166
166
  subtitle: "Error rate: #{@stats[:error_rate] || 0}%",
@@ -168,27 +168,27 @@
168
168
  icon_color: "text-green-500",
169
169
  value_color: success_rate_color %>
170
170
 
171
- <%= render "rubyllm/agents/shared/stat_card",
171
+ <%= render "ruby_llm/agents/shared/stat_card",
172
172
  title: "Total Cost",
173
173
  value: "$#{number_with_precision(@stats[:total_cost] || 0, precision: 4)}",
174
174
  subtitle: "Avg: $#{number_with_precision(@stats[:avg_cost] || 0, precision: 6)}",
175
175
  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
176
  icon_color: "text-amber-500" %>
177
177
 
178
- <%= render "rubyllm/agents/shared/stat_card",
178
+ <%= render "ruby_llm/agents/shared/stat_card",
179
179
  title: "Total Tokens",
180
180
  value: number_with_delimiter(@stats[:total_tokens] || 0),
181
181
  subtitle: "Avg: #{number_with_delimiter(@stats[:avg_tokens] || 0)}",
182
182
  icon: "M7 20l4-16m2 16l4-16M6 9h14M4 15h14",
183
183
  icon_color: "text-indigo-500" %>
184
184
 
185
- <%= render "rubyllm/agents/shared/stat_card",
185
+ <%= render "ruby_llm/agents/shared/stat_card",
186
186
  title: "Avg Duration",
187
187
  value: "#{number_with_delimiter(@stats[:avg_duration_ms] || 0)} ms",
188
188
  icon: "M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z",
189
189
  icon_color: "text-purple-500" %>
190
190
 
191
- <%= render "rubyllm/agents/shared/stat_card",
191
+ <%= render "ruby_llm/agents/shared/stat_card",
192
192
  title: "Cache Hit Rate",
193
193
  value: "#{@cache_hit_rate}%",
194
194
  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 +284,7 @@
284
284
  <% end %>
285
285
 
286
286
  <!-- Version Comparison -->
287
- <%= render partial: "rubyllm/agents/agents/version_comparison",
287
+ <%= render partial: "ruby_llm/agents/agents/version_comparison",
288
288
  locals: { versions: @versions, version_comparison: @version_comparison } %>
289
289
 
290
290
  <% if @config %>
@@ -341,8 +341,9 @@
341
341
 
342
342
  <!-- Reliability Configuration -->
343
343
  <% retries_config = @config[:retries] || {}
344
+ fallback_models = Array(@config[:fallback_models]).compact
344
345
  has_retries = (retries_config[:max] || 0) > 0
345
- has_fallbacks = @config[:fallback_models].present? && @config[:fallback_models].any?
346
+ has_fallbacks = fallback_models.any?
346
347
  has_total_timeout = @config[:total_timeout].present?
347
348
  has_circuit_breaker = @config[:circuit_breaker].present?
348
349
  has_any_reliability = has_retries || has_fallbacks || has_total_timeout || has_circuit_breaker %>
@@ -446,7 +447,7 @@
446
447
 
447
448
  <% if has_fallbacks %>
448
449
  <p class="text-xs text-gray-600 dark:text-gray-400 mt-1">
449
- <%= @config[:fallback_models].join(" → ") %>
450
+ <%= fallback_models.join(" → ") %>
450
451
  </p>
451
452
  <% else %>
452
453
  <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">
@@ -601,6 +602,63 @@
601
602
  </div>
602
603
  </div>
603
604
  <% end %>
605
+
606
+ <!-- Available Tools -->
607
+ <%
608
+ class_tools = @agent_class.respond_to?(:tools) ? (@agent_class.tools || []) : []
609
+ has_dynamic_tools = @agent_class.instance_methods(false).include?(:tools)
610
+ %>
611
+ <div class="border-t border-gray-100 dark:border-gray-700 pt-4">
612
+ <p
613
+ class="
614
+ text-xs text-gray-500 dark:text-gray-400 uppercase
615
+ tracking-wider mb-3
616
+ "
617
+ >
618
+ Available Tools
619
+ <% if class_tools.any? %>
620
+ <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">
621
+ <%= class_tools.size %>
622
+ </span>
623
+ <% elsif has_dynamic_tools %>
624
+ <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">
625
+ Dynamic
626
+ </span>
627
+ <% else %>
628
+ <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">
629
+ 0
630
+ </span>
631
+ <% end %>
632
+ </p>
633
+
634
+ <% if class_tools.any? %>
635
+ <div class="space-y-2">
636
+ <% class_tools.each do |tool_class| %>
637
+ <div class="flex items-center text-sm">
638
+ <code
639
+ class="
640
+ bg-gray-100 dark:bg-gray-700 dark:text-gray-200 px-2
641
+ py-0.5 rounded font-mono
642
+ "
643
+ >
644
+ <%= tool_class.respond_to?(:tool_name) ? tool_class.tool_name : tool_class.name.demodulize %>
645
+ </code>
646
+ </div>
647
+ <% end %>
648
+ </div>
649
+ <% elsif has_dynamic_tools %>
650
+ <p class="text-sm text-purple-600 dark:text-purple-400">
651
+ This agent configures tools dynamically at runtime based on context.
652
+ </p>
653
+ <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">
654
+ Tools vary per execution based on feature flags and configuration.
655
+ </p>
656
+ <% else %>
657
+ <p class="text-sm text-gray-400 dark:text-gray-500 italic">
658
+ No tools configured for this agent.
659
+ </p>
660
+ <% end %>
661
+ </div>
604
662
  </div>
605
663
  <% end %>
606
664
 
@@ -610,7 +668,7 @@
610
668
  Executions
611
669
  </h3>
612
670
 
613
- <%= turbo_frame_tag "executions_table" do %>
671
+ <div id="executions_table">
614
672
  <%
615
673
  has_filters = params[:statuses].present? || params[:versions].present? || params[:models].present? || params[:temperatures].present? || params[:days].present?
616
674
  selected_statuses = params[:statuses].present? ? (params[:statuses].is_a?(Array) ? params[:statuses] : params[:statuses].split(",")) : []
@@ -635,10 +693,10 @@
635
693
  ]
636
694
  %>
637
695
 
638
- <%= form_with url: ruby_llm_agents.agent_path(@agent_type), method: :get, data: { turbo_frame: "executions_table" } do |f| %>
696
+ <%= form_with url: ruby_llm_agents.agent_path(@agent_type), method: :get, local: true do |f| %>
639
697
  <div class="flex flex-wrap items-center gap-3 mb-4 pb-4 border-b border-gray-100 dark:border-gray-700">
640
698
  <%# Status Filter (Multi-select) %>
641
- <%= render "rubyllm/agents/shared/filter_dropdown",
699
+ <%= render "ruby_llm/agents/shared/filter_dropdown",
642
700
  name: "statuses[]",
643
701
  filter_id: "statuses",
644
702
  label: "Status",
@@ -648,7 +706,7 @@
648
706
 
649
707
  <%# Version Filter (Multi-select) %>
650
708
  <% if @versions.any? %>
651
- <%= render "rubyllm/agents/shared/filter_dropdown",
709
+ <%= render "ruby_llm/agents/shared/filter_dropdown",
652
710
  name: "versions[]",
653
711
  filter_id: "versions",
654
712
  label: "Version",
@@ -660,7 +718,7 @@
660
718
 
661
719
  <%# Model Filter (Multi-select) %>
662
720
  <% if @models.length > 1 %>
663
- <%= render "rubyllm/agents/shared/filter_dropdown",
721
+ <%= render "ruby_llm/agents/shared/filter_dropdown",
664
722
  name: "models[]",
665
723
  filter_id: "models",
666
724
  label: "Model",
@@ -672,7 +730,7 @@
672
730
 
673
731
  <%# Temperature Filter (Multi-select) %>
674
732
  <% if @temperatures.length > 1 %>
675
- <%= render "rubyllm/agents/shared/filter_dropdown",
733
+ <%= render "ruby_llm/agents/shared/filter_dropdown",
676
734
  name: "temperatures[]",
677
735
  filter_id: "temperatures",
678
736
  label: "Temp",
@@ -683,7 +741,7 @@
683
741
  <% end %>
684
742
 
685
743
  <%# Time Range Filter (Single-select) %>
686
- <%= render "rubyllm/agents/shared/select_dropdown",
744
+ <%= render "ruby_llm/agents/shared/select_dropdown",
687
745
  name: "days",
688
746
  filter_id: "days",
689
747
  options: days_options,
@@ -693,7 +751,6 @@
693
751
  <%# Clear Filters %>
694
752
  <% if has_filters %>
695
753
  <%= link_to ruby_llm_agents.agent_path(@agent_type),
696
- data: { turbo_frame: "executions_table" },
697
754
  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
755
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
699
756
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
@@ -713,6 +770,6 @@
713
770
  </div>
714
771
  <% end %>
715
772
 
716
- <%= render "rubyllm/agents/shared/executions_table", executions: @executions, pagination: @pagination %>
717
- <% end %>
773
+ <%= render "ruby_llm/agents/shared/executions_table", executions: @executions, pagination: @pagination %>
774
+ </div>
718
775
  </div>