ruby_llm-agents 0.3.4 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +132 -1263
  3. data/app/controllers/concerns/ruby_llm/agents/filterable.rb +5 -1
  4. data/app/controllers/concerns/ruby_llm/agents/paginatable.rb +2 -1
  5. data/app/controllers/ruby_llm/agents/agents_controller.rb +21 -2
  6. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +80 -16
  7. data/app/controllers/ruby_llm/agents/executions_controller.rb +83 -5
  8. data/app/models/ruby_llm/agents/execution/analytics.rb +17 -27
  9. data/app/models/ruby_llm/agents/execution/scopes.rb +25 -0
  10. data/app/models/ruby_llm/agents/execution/workflow.rb +299 -0
  11. data/app/models/ruby_llm/agents/execution.rb +9 -1
  12. data/app/models/ruby_llm/agents/tenant_budget.rb +165 -0
  13. data/app/services/ruby_llm/agents/agent_registry.rb +118 -7
  14. data/app/views/layouts/{rubyllm → ruby_llm}/agents/application.html.erb +91 -29
  15. data/app/views/ruby_llm/agents/agents/_empty_state.html.erb +23 -0
  16. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +125 -0
  17. data/app/views/ruby_llm/agents/agents/index.html.erb +93 -0
  18. data/app/views/{rubyllm → ruby_llm}/agents/agents/show.html.erb +77 -20
  19. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +112 -0
  20. data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_execution_item.html.erb +7 -4
  21. data/app/views/ruby_llm/agents/dashboard/_tenant_budget.html.erb +115 -0
  22. data/app/views/{rubyllm → ruby_llm}/agents/dashboard/index.html.erb +9 -6
  23. data/app/views/{rubyllm → ruby_llm}/agents/executions/_execution.html.erb +1 -1
  24. data/app/views/{rubyllm → ruby_llm}/agents/executions/_filters.html.erb +39 -11
  25. data/app/views/{rubyllm → ruby_llm}/agents/executions/_list.html.erb +19 -9
  26. data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +101 -0
  27. data/app/views/ruby_llm/agents/executions/index.html.erb +88 -0
  28. data/app/views/{rubyllm → ruby_llm}/agents/executions/show.html.erb +137 -126
  29. data/app/views/{rubyllm → ruby_llm}/agents/shared/_breadcrumbs.html.erb +2 -2
  30. data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +251 -0
  31. data/app/views/{rubyllm → ruby_llm}/agents/shared/_filter_dropdown.html.erb +1 -1
  32. data/app/views/{rubyllm → ruby_llm}/agents/shared/_select_dropdown.html.erb +1 -1
  33. data/app/views/{rubyllm → ruby_llm}/agents/shared/_status_badge.html.erb +1 -1
  34. data/app/views/{rubyllm → ruby_llm}/agents/shared/_status_dot.html.erb +1 -1
  35. data/app/views/ruby_llm/agents/shared/_tenant_filter.html.erb +26 -0
  36. data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +61 -0
  37. data/lib/generators/ruby_llm_agents/multi_tenancy_generator.rb +97 -0
  38. data/lib/generators/ruby_llm_agents/templates/add_attempts_migration.rb.tt +3 -3
  39. data/lib/generators/ruby_llm_agents/templates/add_tenant_to_executions_migration.rb.tt +23 -0
  40. data/lib/generators/ruby_llm_agents/templates/add_tool_calls_migration.rb.tt +2 -2
  41. data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +38 -0
  42. data/lib/generators/ruby_llm_agents/templates/create_tenant_budgets_migration.rb.tt +45 -0
  43. data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +17 -5
  44. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +13 -0
  45. data/lib/ruby_llm/agents/alert_manager.rb +20 -16
  46. data/lib/ruby_llm/agents/base/caching.rb +4 -7
  47. data/lib/ruby_llm/agents/base/cost_calculation.rb +5 -3
  48. data/lib/ruby_llm/agents/base/execution.rb +61 -9
  49. data/lib/ruby_llm/agents/base/reliability_execution.rb +14 -9
  50. data/lib/ruby_llm/agents/base.rb +26 -0
  51. data/lib/ruby_llm/agents/budget_tracker.rb +250 -139
  52. data/lib/ruby_llm/agents/cache_helper.rb +98 -0
  53. data/lib/ruby_llm/agents/circuit_breaker.rb +48 -30
  54. data/lib/ruby_llm/agents/configuration.rb +40 -1
  55. data/lib/ruby_llm/agents/engine.rb +65 -1
  56. data/lib/ruby_llm/agents/inflections.rb +14 -0
  57. data/lib/ruby_llm/agents/instrumentation.rb +66 -0
  58. data/lib/ruby_llm/agents/reliability.rb +8 -2
  59. data/lib/ruby_llm/agents/version.rb +1 -1
  60. data/lib/ruby_llm/agents/workflow/instrumentation.rb +254 -0
  61. data/lib/ruby_llm/agents/workflow/parallel.rb +282 -0
  62. data/lib/ruby_llm/agents/workflow/pipeline.rb +306 -0
  63. data/lib/ruby_llm/agents/workflow/result.rb +390 -0
  64. data/lib/ruby_llm/agents/workflow/router.rb +429 -0
  65. data/lib/ruby_llm/agents/workflow.rb +232 -0
  66. data/lib/ruby_llm/agents.rb +1 -0
  67. metadata +50 -60
  68. data/app/views/rubyllm/agents/agents/index.html.erb +0 -20
  69. data/app/views/rubyllm/agents/dashboard/_agent_comparison.html.erb +0 -46
  70. data/app/views/rubyllm/agents/executions/index.html.erb +0 -28
  71. data/app/views/rubyllm/agents/executions/index.turbo_stream.erb +0 -18
  72. data/app/views/rubyllm/agents/shared/_executions_table.html.erb +0 -193
  73. /data/app/views/{rubyllm → ruby_llm}/agents/agents/_agent.html.erb +0 -0
  74. /data/app/views/{rubyllm → ruby_llm}/agents/agents/_version_comparison.html.erb +0 -0
  75. /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_action_center.html.erb +0 -0
  76. /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_alerts_feed.html.erb +0 -0
  77. /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_breaker_strip.html.erb +0 -0
  78. /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_budgets_bar.html.erb +0 -0
  79. /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_now_strip.html.erb +0 -0
  80. /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_top_errors.html.erb +0 -0
  81. /data/app/views/{rubyllm → ruby_llm}/agents/executions/dry_run.html.erb +0 -0
  82. /data/app/views/{rubyllm → ruby_llm}/agents/settings/show.html.erb +0 -0
  83. /data/app/views/{rubyllm → ruby_llm}/agents/shared/_nav_link.html.erb +0 -0
  84. /data/app/views/{rubyllm → ruby_llm}/agents/shared/_stat_card.html.erb +0 -0
@@ -0,0 +1,88 @@
1
+ <h2 class="text-2xl font-bold text-gray-900 dark:text-gray-100 mb-6">Executions</h2>
2
+
3
+ <div id="executions_content" x-data="{ activeTab: '<%= params[:execution_type] || 'all' %>', activeWorkflowType: '<%= params[:workflow_type_tab] %>' }">
4
+ <!-- Top-Level Execution Type Tabs -->
5
+ <div class="border-b border-gray-200 dark:border-gray-700 mb-4">
6
+ <nav class="-mb-px flex space-x-6" aria-label="Execution Types">
7
+ <button type="button" @click="activeTab = 'all'; activeWorkflowType = ''; submitFilters()"
8
+ :class="activeTab === 'all' ? 'border-blue-500 text-blue-600 dark:text-blue-400' : 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'"
9
+ class="whitespace-nowrap py-3 px-1 border-b-2 font-medium text-sm transition-colors">
10
+ All
11
+ </button>
12
+ <button type="button" @click="activeTab = 'agents'; activeWorkflowType = ''; submitFilters()"
13
+ :class="activeTab === 'agents' ? 'border-blue-500 text-blue-600 dark:text-blue-400' : 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'"
14
+ class="whitespace-nowrap py-3 px-1 border-b-2 font-medium text-sm transition-colors">
15
+ Agents Only
16
+ </button>
17
+ <button type="button" @click="activeTab = 'workflows'; activeWorkflowType = ''; submitFilters()"
18
+ :class="activeTab === 'workflows' ? 'border-blue-500 text-blue-600 dark:text-blue-400' : 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'"
19
+ class="whitespace-nowrap py-3 px-1 border-b-2 font-medium text-sm transition-colors">
20
+ Workflows
21
+ </button>
22
+ </nav>
23
+ </div>
24
+
25
+ <!-- Workflow Sub-tabs -->
26
+ <div x-show="activeTab === 'workflows'" x-cloak class="mb-4">
27
+ <div class="flex flex-wrap gap-2">
28
+ <button type="button" @click="activeWorkflowType = ''; submitFilters()"
29
+ :class="activeWorkflowType === '' ? 'bg-gray-900 dark:bg-gray-100 text-white dark:text-gray-900' : 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'"
30
+ class="px-3 py-1.5 rounded-md text-sm font-medium transition-colors">
31
+ All Workflows
32
+ </button>
33
+ <button type="button" @click="activeWorkflowType = 'pipeline'; submitFilters()"
34
+ :class="activeWorkflowType === 'pipeline' ? 'bg-indigo-600 text-white' : 'bg-indigo-100 dark:bg-indigo-900/50 text-indigo-700 dark:text-indigo-300 hover:bg-indigo-200 dark:hover:bg-indigo-800'"
35
+ class="px-3 py-1.5 rounded-md text-sm font-medium transition-colors inline-flex items-center gap-1.5">
36
+ <span aria-hidden="true">-></span> Pipeline
37
+ </button>
38
+ <button type="button" @click="activeWorkflowType = 'parallel'; submitFilters()"
39
+ :class="activeWorkflowType === 'parallel' ? 'bg-cyan-600 text-white' : 'bg-cyan-100 dark:bg-cyan-900/50 text-cyan-700 dark:text-cyan-300 hover:bg-cyan-200 dark:hover:bg-cyan-800'"
40
+ class="px-3 py-1.5 rounded-md text-sm font-medium transition-colors inline-flex items-center gap-1.5">
41
+ <span aria-hidden="true">//</span> Parallel
42
+ </button>
43
+ <button type="button" @click="activeWorkflowType = 'router'; submitFilters()"
44
+ :class="activeWorkflowType === 'router' ? 'bg-amber-600 text-white' : 'bg-amber-100 dark:bg-amber-900/50 text-amber-700 dark:text-amber-300 hover:bg-amber-200 dark:hover:bg-amber-800'"
45
+ class="px-3 py-1.5 rounded-md text-sm font-medium transition-colors inline-flex items-center gap-1.5">
46
+ <span aria-hidden="true">&lt;&gt;</span> Router
47
+ </button>
48
+ </div>
49
+ </div>
50
+
51
+ <!-- Hidden inputs for tab state -->
52
+ <input type="hidden" id="execution-type-filter" name="execution_type" x-model="activeTab" form="filters-form">
53
+ <input type="hidden" id="workflow-type-tab-filter" name="workflow_type_tab" x-model="activeWorkflowType" form="filters-form">
54
+
55
+ <%= render partial: "ruby_llm/agents/executions/filters", locals: { agent_types: @agent_types, model_ids: @model_ids, workflow_types: @workflow_types, filter_stats: @filter_stats } %>
56
+ <%= render partial: "ruby_llm/agents/executions/list", locals: { executions: @executions, pagination: @pagination, filter_stats: @filter_stats } %>
57
+ </div>
58
+
59
+ <script>
60
+ // Toggle attempts expansion (used by executions list)
61
+ function toggleAttempts(executionId) {
62
+ const attemptsRow = document.getElementById('attempts-row-' + executionId);
63
+ const expandBtn = document.querySelector(`button[data-execution-id="${executionId}"]`);
64
+
65
+ if (attemptsRow) {
66
+ const isHidden = attemptsRow.classList.contains('hidden');
67
+ attemptsRow.classList.toggle('hidden');
68
+
69
+ // Rotate the chevron
70
+ if (expandBtn) {
71
+ const svg = expandBtn.querySelector('svg');
72
+ if (svg) {
73
+ svg.classList.toggle('rotate-90', isHidden);
74
+ }
75
+ expandBtn.setAttribute('aria-expanded', isHidden ? 'true' : 'false');
76
+ }
77
+ }
78
+ }
79
+
80
+ // Submit filters when tabs change
81
+ function submitFilters() {
82
+ // Delay to allow Alpine to update x-model values
83
+ setTimeout(function() {
84
+ const form = document.getElementById('filters-form');
85
+ if (form) form.requestSubmit();
86
+ }, 50);
87
+ }
88
+ </script>
@@ -1,5 +1,5 @@
1
1
  <div id="execution-detail" data-execution-id="<%= @execution.id %>" data-status="<%= @execution.status %>">
2
- <%= render "rubyllm/agents/shared/breadcrumbs", items: [
2
+ <%= render "ruby_llm/agents/shared/breadcrumbs", items: [
3
3
  { label: "Dashboard", path: ruby_llm_agents.root_path },
4
4
  { label: "Executions", path: ruby_llm_agents.executions_path },
5
5
  { label: "##{@execution.id}" }
@@ -30,7 +30,7 @@
30
30
  <h2 class="text-lg font-bold text-gray-900 dark:text-gray-100 truncate">
31
31
  <%= @execution.agent_type.gsub(/Agent$/, '') %>
32
32
  </h2>
33
- <%= render "rubyllm/agents/shared/status_badge", status: @execution.status, size: :md %>
33
+ <%= render "ruby_llm/agents/shared/status_badge", status: @execution.status, size: :md %>
34
34
  <% if secondary_badges.any? %>
35
35
  <div class="relative" x-data="{ showDetails: false }">
36
36
  <button
@@ -74,7 +74,6 @@
74
74
  <div class="flex items-center gap-3 flex-shrink-0">
75
75
  <%= button_to rerun_execution_path(@execution, dry_run: true),
76
76
  method: :post,
77
- data: { turbo: false },
78
77
  class: "inline-flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors",
79
78
  title: "Preview what would be sent without making an API call" do %>
80
79
  <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -115,7 +114,7 @@
115
114
  <%= @execution.agent_type.gsub(/Agent$/, '') %>
116
115
  </h2>
117
116
  <div class="flex flex-wrap items-center gap-2 mt-2">
118
- <%= render "rubyllm/agents/shared/status_badge", status: @execution.status, size: :md %>
117
+ <%= render "ruby_llm/agents/shared/status_badge", status: @execution.status, size: :md %>
119
118
  <% secondary_badges.each do |badge| %>
120
119
  <% badge_classes = case badge[:color]
121
120
  when 'cyan' then 'bg-cyan-100 dark:bg-cyan-900/50 text-cyan-800 dark:text-cyan-300'
@@ -146,7 +145,6 @@
146
145
  <div class="flex items-center gap-2">
147
146
  <%= button_to rerun_execution_path(@execution, dry_run: true),
148
147
  method: :post,
149
- data: { turbo: false },
150
148
  class: "flex-1 inline-flex items-center justify-center gap-1.5 px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors",
151
149
  title: "Preview what would be sent without making an API call" do %>
152
150
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -209,27 +207,124 @@
209
207
  </div>
210
208
  </div>
211
209
 
210
+ <!-- Workflow Summary Panel (for root workflow executions) -->
211
+ <% if @execution.respond_to?(:root_workflow?) && @execution.root_workflow? %>
212
+ <%= render "ruby_llm/agents/executions/workflow_summary", execution: @execution %>
213
+ <% end %>
214
+
215
+ <!-- Workflow Info (if applicable - for child workflow steps) -->
216
+ <% if (@execution.workflow_type.present? || @execution.workflow_step.present? || @execution.routed_to.present?) && !(@execution.respond_to?(:root_workflow?) && @execution.root_workflow?) %>
217
+ <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-4 sm:p-5 mb-6">
218
+ <div class="flex items-center gap-2 mb-4">
219
+ <% case @execution.workflow_type
220
+ when "pipeline" %>
221
+ <span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-sm font-medium bg-indigo-100 dark:bg-indigo-900/50 text-indigo-700 dark:text-indigo-300">
222
+ <span class="text-base">→</span> Pipeline Workflow
223
+ </span>
224
+ <% when "parallel" %>
225
+ <span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-sm font-medium bg-cyan-100 dark:bg-cyan-900/50 text-cyan-700 dark:text-cyan-300">
226
+ <span class="text-base">⫴</span> Parallel Workflow
227
+ </span>
228
+ <% when "router" %>
229
+ <span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-sm font-medium bg-amber-100 dark:bg-amber-900/50 text-amber-700 dark:text-amber-300">
230
+ <span class="text-base">⑂</span> Router Workflow
231
+ </span>
232
+ <% else %>
233
+ <% if @execution.workflow_step.present? %>
234
+ <span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-sm font-medium bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300">
235
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
236
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5l7 7-7 7M5 5l7 7-7 7"/>
237
+ </svg>
238
+ Workflow Step
239
+ </span>
240
+ <% end %>
241
+ <% end %>
242
+ <% if @execution.workflow_id.present? %>
243
+ <span class="text-xs text-gray-400 dark:text-gray-500 font-mono" title="Workflow ID: <%= @execution.workflow_id %>">
244
+ <%= @execution.workflow_id.to_s.truncate(12) %>
245
+ </span>
246
+ <% end %>
247
+ </div>
248
+
249
+ <div class="grid grid-cols-2 md:grid-cols-4 gap-4">
250
+ <% if @execution.workflow_step.present? %>
251
+ <div>
252
+ <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">Step Name</p>
253
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100"><%= @execution.workflow_step %></p>
254
+ </div>
255
+ <% end %>
256
+
257
+ <% if @execution.routed_to.present? %>
258
+ <div>
259
+ <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">Routed To</p>
260
+ <p class="text-sm font-medium text-amber-600 dark:text-amber-400"><%= @execution.routed_to %></p>
261
+ </div>
262
+ <% end %>
263
+
264
+ <% if @execution.classification_result.present? %>
265
+ <%
266
+ classification = if @execution.classification_result.is_a?(String)
267
+ begin
268
+ JSON.parse(@execution.classification_result)
269
+ rescue
270
+ {}
271
+ end
272
+ else
273
+ @execution.classification_result || {}
274
+ end
275
+ %>
276
+ <% if classification["method"].present? %>
277
+ <div>
278
+ <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">Classification</p>
279
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100">
280
+ <%= classification["method"] == "llm" ? "LLM" : "Rule-based" %>
281
+ <% if classification["classification_time_ms"].present? %>
282
+ <span class="text-xs text-gray-400 dark:text-gray-500">(<%= classification["classification_time_ms"] %>ms)</span>
283
+ <% end %>
284
+ </p>
285
+ </div>
286
+ <% end %>
287
+ <% if classification["classifier_model"].present? %>
288
+ <div>
289
+ <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">Classifier Model</p>
290
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100 font-mono"><%= classification["classifier_model"] %></p>
291
+ </div>
292
+ <% end %>
293
+ <% end %>
294
+ </div>
295
+
296
+ <% if @execution.parent_execution_id.present? %>
297
+ <div class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-700">
298
+ <span class="text-xs text-gray-500 dark:text-gray-400">Part of workflow:</span>
299
+ <%= link_to "##{@execution.parent_execution_id}",
300
+ ruby_llm_agents.execution_path(@execution.parent_execution_id),
301
+ class: "ml-2 text-blue-600 dark:text-blue-400 hover:underline font-mono text-sm" %>
302
+ </div>
303
+ <% end %>
304
+ </div>
305
+ <% end %>
306
+
212
307
  <!-- Stats Grid -->
213
308
  <div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
214
- <%= render "rubyllm/agents/shared/stat_card",
309
+ <%= render "ruby_llm/agents/shared/stat_card",
215
310
  title: "Model",
216
311
  value: @execution.model_id,
217
312
  icon: "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",
218
313
  icon_color: "text-blue-500" %>
219
314
 
220
- <%= render "rubyllm/agents/shared/stat_card",
315
+ <%= render "ruby_llm/agents/shared/stat_card",
221
316
  title: "Duration",
222
317
  value: "#{number_to_human_short(@execution.duration_ms || 0)} ms",
223
318
  icon: "M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z",
224
319
  icon_color: "text-purple-500" %>
225
320
 
226
- <%= render "rubyllm/agents/shared/stat_card",
321
+ <%= render "ruby_llm/agents/shared/stat_card",
227
322
  title: "Total Tokens",
228
323
  value: number_to_human_short(@execution.total_tokens || 0),
229
324
  icon: "M7 20l4-16m2 16l4-16M6 9h14M4 15h14",
230
325
  icon_color: "text-indigo-500" %>
231
326
 
232
- <%= render "rubyllm/agents/shared/stat_card",
327
+ <%= render "ruby_llm/agents/shared/stat_card",
233
328
  title: "Total Cost",
234
329
  value: number_to_human_short(@execution.total_cost || 0, prefix: "$", precision: 2),
235
330
  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",
@@ -297,7 +392,7 @@
297
392
  <div>
298
393
  <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Attempts</h3>
299
394
  <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">
300
- <%= @execution.attempts_count || @execution.attempts.size %> attempt(s)
395
+ <%= @execution.respond_to?(:attempts_count) && @execution.attempts_count ? @execution.attempts_count : @execution.attempts.size %> attempt(s)
301
396
  <% if @execution.used_fallback? %>
302
397
  · <span class="text-amber-500">Used fallback model</span>
303
398
  <% end %>
@@ -306,7 +401,7 @@
306
401
  <% end %>
307
402
  </p>
308
403
  </div>
309
- <% if @execution.chosen_model_id.present? && @execution.chosen_model_id != @execution.model_id %>
404
+ <% if @execution.used_fallback? %>
310
405
  <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-100 dark:bg-amber-900/30 text-amber-800 dark:text-amber-300">
311
406
  Fallback: <%= @execution.chosen_model_id %>
312
407
  </span>
@@ -428,46 +523,13 @@
428
523
  </div>
429
524
  <% end %>
430
525
 
431
- <!-- Masking Toggle -->
432
- <div class="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-xl p-4 mb-6">
433
- <div class="flex items-center justify-between">
434
- <div class="flex items-center gap-2">
435
- <svg class="w-5 h-5 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
436
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
437
- </svg>
438
- <span class="text-sm font-medium text-amber-800 dark:text-amber-200">Data Masking</span>
439
- </div>
440
- <div class="flex items-center gap-3">
441
- <span id="masking-status" class="text-xs text-amber-600 dark:text-amber-400">
442
- Sensitive data is hidden
443
- </span>
444
- <button
445
- type="button"
446
- id="toggle-masking-btn"
447
- onclick="toggleMasking()"
448
- class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md transition-colors bg-amber-100 dark:bg-amber-900/50 text-amber-700 dark:text-amber-300 hover:bg-amber-200 dark:hover:bg-amber-800/50"
449
- >
450
- <svg id="eye-icon" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
451
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
452
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
453
- </svg>
454
- <svg id="eye-off-icon" class="w-4 h-4 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
455
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"/>
456
- </svg>
457
- <span id="toggle-btn-text">Show Original</span>
458
- </button>
459
- </div>
460
- </div>
461
- </div>
462
-
463
526
  <!-- Parameters -->
464
527
  <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6">
465
528
  <div class="flex items-center justify-between mb-4">
466
529
  <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Parameters</h3>
467
530
  <button
468
531
  type="button"
469
- data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(redact_for_display(@execution.parameters || {}))) %>"
470
- data-copy-json-original="<%= Base64.strict_encode64(JSON.pretty_generate(@execution.parameters || {})) %>"
532
+ data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(@execution.parameters || {})) %>"
471
533
  class="copy-json-btn inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors"
472
534
  >
473
535
  <svg class="w-4 h-4 copy-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -479,8 +541,7 @@
479
541
  <span>Copy</span>
480
542
  </button>
481
543
  </div>
482
- <pre id="parameters-masked" class="maskable-content bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto font-mono"><%= highlight_json_redacted(@execution.parameters || {}) %></pre>
483
- <pre id="parameters-original" class="maskable-content hidden bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto font-mono"><%= highlight_json(@execution.parameters || {}) %></pre>
544
+ <pre class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto font-mono"><%= highlight_json(@execution.parameters || {}) %></pre>
484
545
  </div>
485
546
 
486
547
  <!-- Response -->
@@ -490,8 +551,7 @@
490
551
  <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Response</h3>
491
552
  <button
492
553
  type="button"
493
- data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(redact_for_display(@execution.response))) %>"
494
- data-copy-json-original="<%= Base64.strict_encode64(JSON.pretty_generate(@execution.response)) %>"
554
+ data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(@execution.response)) %>"
495
555
  class="copy-json-btn inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors"
496
556
  >
497
557
  <svg class="w-4 h-4 copy-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -503,30 +563,30 @@
503
563
  <span>Copy</span>
504
564
  </button>
505
565
  </div>
506
- <pre id="response-masked" class="maskable-content bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto max-h-96 font-mono"><%= highlight_json_redacted(@execution.response) %></pre>
507
- <pre id="response-original" class="maskable-content hidden bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto max-h-96 font-mono"><%= highlight_json(@execution.response) %></pre>
566
+ <pre class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto max-h-96 font-mono"><%= highlight_json(@execution.response) %></pre>
508
567
  </div>
509
568
  <% end %>
510
569
 
511
570
  <!-- Tool Calls -->
512
- <% if @execution.tool_calls.present? && @execution.tool_calls.any? %>
513
- <% tool_call_count = @execution.tool_calls.size %>
514
- <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6" x-data="{ expanded: <%= tool_call_count <= 3 %> }">
515
- <div class="flex items-center justify-between mb-4">
516
- <div class="flex items-center gap-2">
517
- <svg class="w-5 h-5 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
518
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
519
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
520
- </svg>
521
- <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Tool Calls</h3>
522
- <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-300">
523
- <%= tool_call_count %>
524
- </span>
525
- </div>
571
+ <% tool_calls = @execution.tool_calls || [] %>
572
+ <% tool_call_count = tool_calls.size %>
573
+ <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6" x-data="{ expanded: <%= tool_call_count <= 3 && tool_call_count > 0 %> }">
574
+ <div class="flex items-center justify-between mb-4">
575
+ <div class="flex items-center gap-2">
576
+ <svg class="w-5 h-5 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
577
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
578
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
579
+ </svg>
580
+ <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Tool Calls</h3>
581
+ <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-300">
582
+ <%= tool_call_count %>
583
+ </span>
584
+ </div>
585
+ <% if tool_call_count > 0 %>
526
586
  <div class="flex items-center gap-2">
527
587
  <button
528
588
  type="button"
529
- data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(@execution.tool_calls)) %>"
589
+ data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(tool_calls)) %>"
530
590
  class="copy-json-btn inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors"
531
591
  >
532
592
  <svg class="w-4 h-4 copy-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -543,10 +603,12 @@
543
603
  </button>
544
604
  <% end %>
545
605
  </div>
546
- </div>
606
+ <% end %>
607
+ </div>
547
608
 
609
+ <% if tool_call_count > 0 %>
548
610
  <div class="space-y-4" x-show="expanded" <%= tool_call_count > 3 ? 'x-cloak' : '' %>>
549
- <% @execution.tool_calls.each_with_index do |tool_call, index| %>
611
+ <% tool_calls.each_with_index do |tool_call, index| %>
550
612
  <%
551
613
  # Handle both symbol and string keys
552
614
  tool_id = tool_call['id'] || tool_call[:id]
@@ -583,8 +645,10 @@
583
645
  </div>
584
646
  <% end %>
585
647
  </div>
586
- </div>
587
- <% end %>
648
+ <% else %>
649
+ <p class="text-sm text-gray-400 dark:text-gray-500 italic">No tool calls were made during this execution.</p>
650
+ <% end %>
651
+ </div>
588
652
 
589
653
  <!-- Metadata -->
590
654
  <% if @execution.metadata.present? && @execution.metadata.any? %>
@@ -597,8 +661,7 @@
597
661
  <div class="flex items-center gap-2">
598
662
  <button
599
663
  type="button"
600
- data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(redact_for_display(@execution.metadata))) %>"
601
- data-copy-json-original="<%= Base64.strict_encode64(JSON.pretty_generate(@execution.metadata)) %>"
664
+ data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(@execution.metadata)) %>"
602
665
  class="copy-json-btn inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors"
603
666
  >
604
667
  <svg class="w-4 h-4 copy-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -615,8 +678,7 @@
615
678
  </div>
616
679
  </div>
617
680
  <div x-show="expanded" x-cloak class="mt-4">
618
- <pre id="metadata-masked" class="maskable-content bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto font-mono"><%= highlight_json_redacted(@execution.metadata) %></pre>
619
- <pre id="metadata-original" class="maskable-content hidden bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto font-mono"><%= highlight_json(@execution.metadata) %></pre>
681
+ <pre class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto font-mono"><%= highlight_json(@execution.metadata) %></pre>
620
682
  </div>
621
683
  </div>
622
684
  <% end %>
@@ -921,61 +983,10 @@
921
983
  toggle.textContent = isHidden ? 'Collapse' : 'Expand';
922
984
  }
923
985
 
924
- // Masking state (persisted in localStorage)
925
- let isMasked = localStorage.getItem('ruby_llm_agents_masking') !== 'false';
926
-
927
- function toggleMasking() {
928
- isMasked = !isMasked;
929
- localStorage.setItem('ruby_llm_agents_masking', isMasked);
930
- updateMaskingUI();
931
- }
932
-
933
- function updateMaskingUI() {
934
- const status = document.getElementById('masking-status');
935
- const btnText = document.getElementById('toggle-btn-text');
936
- const eyeIcon = document.getElementById('eye-icon');
937
- const eyeOffIcon = document.getElementById('eye-off-icon');
938
-
939
- // Update all maskable content sections
940
- document.querySelectorAll('[id$="-masked"]').forEach(function(el) {
941
- el.classList.toggle('hidden', !isMasked);
942
- });
943
- document.querySelectorAll('[id$="-original"]').forEach(function(el) {
944
- el.classList.toggle('hidden', isMasked);
945
- });
946
-
947
- // Update status and button
948
- if (isMasked) {
949
- status.textContent = 'Sensitive data is hidden';
950
- btnText.textContent = 'Show Original';
951
- eyeIcon.classList.remove('hidden');
952
- eyeOffIcon.classList.add('hidden');
953
- } else {
954
- status.textContent = 'Showing original data';
955
- btnText.textContent = 'Hide Sensitive';
956
- eyeIcon.classList.add('hidden');
957
- eyeOffIcon.classList.remove('hidden');
958
- }
959
-
960
- // Update copy buttons to use appropriate data
961
- document.querySelectorAll('.copy-json-btn').forEach(function(button) {
962
- const maskedData = button.getAttribute('data-copy-json');
963
- const originalData = button.getAttribute('data-copy-json-original');
964
- if (originalData) {
965
- button.setAttribute('data-active-json', isMasked ? maskedData : originalData);
966
- }
967
- });
968
- }
969
-
970
986
  document.addEventListener('DOMContentLoaded', function() {
971
- // Initialize masking UI on page load
972
- updateMaskingUI();
973
-
974
987
  document.querySelectorAll('.copy-json-btn').forEach(function(button) {
975
988
  button.addEventListener('click', function() {
976
- // Use active JSON (considers masking state) or fall back to default
977
- const activeData = this.getAttribute('data-active-json') || this.getAttribute('data-copy-json');
978
- const jsonText = atob(activeData);
989
+ const jsonText = atob(this.getAttribute('data-copy-json'));
979
990
  const span = this.querySelector('span');
980
991
  const copyIcon = this.querySelector('.copy-icon');
981
992
  const checkIcon = this.querySelector('.check-icon');
@@ -1,14 +1,14 @@
1
1
  <%
2
2
  # Breadcrumb navigation partial
3
3
  # Usage:
4
- # render "rubyllm/agents/shared/breadcrumbs", items: [
4
+ # render "ruby_llm/agents/shared/breadcrumbs", items: [
5
5
  # { label: "Dashboard", path: ruby_llm_agents.root_path },
6
6
  # { label: "Executions", path: ruby_llm_agents.executions_path },
7
7
  # { label: "#123" } # Current page (no path)
8
8
  # ]
9
9
  #
10
10
  # Or with simple array:
11
- # render "rubyllm/agents/shared/breadcrumbs", items: [
11
+ # render "ruby_llm/agents/shared/breadcrumbs", items: [
12
12
  # ["Dashboard", ruby_llm_agents.root_path],
13
13
  # ["Executions", ruby_llm_agents.executions_path],
14
14
  # ["#123"]