ruby_llm-agents 0.3.1 → 0.3.4

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +88 -0
  3. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +68 -4
  4. data/app/models/ruby_llm/agents/execution/analytics.rb +114 -13
  5. data/app/models/ruby_llm/agents/execution/scopes.rb +10 -0
  6. data/app/models/ruby_llm/agents/execution.rb +26 -58
  7. data/app/views/layouts/rubyllm/agents/application.html.erb +103 -352
  8. data/app/views/rubyllm/agents/agents/_agent.html.erb +87 -0
  9. data/app/views/rubyllm/agents/agents/index.html.erb +2 -71
  10. data/app/views/rubyllm/agents/agents/show.html.erb +349 -416
  11. data/app/views/rubyllm/agents/dashboard/_action_center.html.erb +7 -7
  12. data/app/views/rubyllm/agents/dashboard/_agent_comparison.html.erb +46 -0
  13. data/app/views/rubyllm/agents/dashboard/_budgets_bar.html.erb +0 -90
  14. data/app/views/rubyllm/agents/dashboard/_execution_item.html.erb +54 -39
  15. data/app/views/rubyllm/agents/dashboard/_now_strip.html.erb +79 -5
  16. data/app/views/rubyllm/agents/dashboard/_top_errors.html.erb +49 -0
  17. data/app/views/rubyllm/agents/dashboard/index.html.erb +76 -151
  18. data/app/views/rubyllm/agents/executions/show.html.erb +256 -93
  19. data/app/views/rubyllm/agents/settings/show.html.erb +1 -1
  20. data/app/views/rubyllm/agents/shared/_breadcrumbs.html.erb +48 -0
  21. data/app/views/rubyllm/agents/shared/_nav_link.html.erb +27 -0
  22. data/config/routes.rb +2 -0
  23. data/lib/generators/ruby_llm_agents/templates/add_tool_calls_migration.rb.tt +28 -0
  24. data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +7 -0
  25. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +13 -0
  26. data/lib/ruby_llm/agents/base/caching.rb +43 -0
  27. data/lib/ruby_llm/agents/base/cost_calculation.rb +103 -0
  28. data/lib/ruby_llm/agents/base/dsl.rb +261 -0
  29. data/lib/ruby_llm/agents/base/execution.rb +206 -0
  30. data/lib/ruby_llm/agents/base/reliability_execution.rb +131 -0
  31. data/lib/ruby_llm/agents/base/response_building.rb +86 -0
  32. data/lib/ruby_llm/agents/base/tool_tracking.rb +57 -0
  33. data/lib/ruby_llm/agents/base.rb +19 -619
  34. data/lib/ruby_llm/agents/instrumentation.rb +36 -3
  35. data/lib/ruby_llm/agents/result.rb +235 -0
  36. data/lib/ruby_llm/agents/version.rb +1 -1
  37. data/lib/ruby_llm/agents.rb +1 -0
  38. metadata +15 -20
  39. data/app/channels/ruby_llm/agents/executions_channel.rb +0 -46
  40. data/app/javascript/ruby_llm/agents/controllers/filter_controller.js +0 -56
  41. data/app/javascript/ruby_llm/agents/controllers/index.js +0 -12
  42. data/app/javascript/ruby_llm/agents/controllers/refresh_controller.js +0 -83
  43. data/app/views/rubyllm/agents/dashboard/_now_strip_values.html.erb +0 -71
@@ -1,74 +1,153 @@
1
1
  <div id="execution-detail" data-execution-id="<%= @execution.id %>" data-status="<%= @execution.status %>">
2
- <div class="mb-6">
3
- <%= link_to ruby_llm_agents.executions_path, class: "inline-flex items-center text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200" do %>
4
- <svg
5
- class="w-4 h-4 mr-1"
6
- fill="none"
7
- stroke="currentColor"
8
- viewBox="0 0 24 24"
9
- >
10
- <path
11
- stroke-linecap="round"
12
- stroke-linejoin="round"
13
- stroke-width="2"
14
- d="M10 19l-7-7m0 0l7-7m-7 7h18"
15
- />
16
- </svg>
17
- Back to Executions
18
- <% end %>
19
- </div>
2
+ <%= render "rubyllm/agents/shared/breadcrumbs", items: [
3
+ { label: "Dashboard", path: ruby_llm_agents.root_path },
4
+ { label: "Executions", path: ruby_llm_agents.executions_path },
5
+ { label: "##{@execution.id}" }
6
+ ] %>
20
7
 
21
8
  <!-- Header -->
22
- <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6">
23
- <div class="flex items-center justify-between">
24
- <div>
25
- <div class="flex items-center gap-3">
26
- <h2 class="text-xl font-bold text-gray-900 dark:text-gray-100">
27
- <%= @execution.agent_type.gsub(/Agent$/, '') %>
28
- </h2>
29
-
30
- <%= render "rubyllm/agents/shared/status_badge", status: @execution.status, size: :md %>
31
-
32
- <% if @execution.streaming? %>
33
- <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-cyan-100 dark:bg-cyan-900/50 text-cyan-800 dark:text-cyan-300">
34
- Streaming
35
- </span>
36
- <% end %>
37
-
38
- <% if @execution.cache_hit %>
39
- <span class="inline-flex items-center px-2.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">
40
- Cached
41
- </span>
42
- <% end %>
9
+ <%
10
+ # Collect secondary badges
11
+ secondary_badges = []
12
+ secondary_badges << { label: "Stream", color: "cyan" } if @execution.streaming?
13
+ secondary_badges << { label: "Cached", color: "purple" } if @execution.cache_hit
14
+ if @execution.finish_reason.present?
15
+ finish_color = case @execution.finish_reason
16
+ when 'stop' then 'green'
17
+ when 'length' then 'yellow'
18
+ when 'content_filter' then 'red'
19
+ when 'tool_calls' then 'blue'
20
+ else 'gray'
21
+ end
22
+ secondary_badges << { label: @execution.finish_reason, color: finish_color }
23
+ end
24
+ secondary_badges << { label: "Rate Limited", color: "orange" } if @execution.respond_to?(:rate_limited?) && @execution.rate_limited?
25
+ %>
26
+ <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">
27
+ <!-- Desktop: Row 1 - Agent name + badges + buttons + date -->
28
+ <div class="hidden sm:flex sm:items-center sm:justify-between gap-4">
29
+ <div class="flex items-center gap-3 min-w-0">
30
+ <h2 class="text-lg font-bold text-gray-900 dark:text-gray-100 truncate">
31
+ <%= @execution.agent_type.gsub(/Agent$/, '') %>
32
+ </h2>
33
+ <%= render "rubyllm/agents/shared/status_badge", status: @execution.status, size: :md %>
34
+ <% if secondary_badges.any? %>
35
+ <div class="relative" x-data="{ showDetails: false }">
36
+ <button
37
+ type="button"
38
+ @mouseenter="showDetails = true"
39
+ @mouseleave="showDetails = false"
40
+ class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
41
+ >
42
+ +<%= secondary_badges.size %>
43
+ <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
44
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
45
+ </svg>
46
+ </button>
47
+ <div
48
+ x-show="showDetails"
49
+ x-cloak
50
+ x-transition:enter="transition ease-out duration-100"
51
+ x-transition:enter-start="opacity-0 scale-95"
52
+ x-transition:enter-end="opacity-100 scale-100"
53
+ class="absolute left-0 mt-1 z-10 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 p-2 min-w-max"
54
+ >
55
+ <div class="flex flex-wrap gap-1.5">
56
+ <% secondary_badges.each do |badge| %>
57
+ <% badge_classes = case badge[:color]
58
+ when 'cyan' then 'bg-cyan-100 dark:bg-cyan-900/50 text-cyan-800 dark:text-cyan-300'
59
+ when 'purple' then 'bg-purple-100 dark:bg-purple-900/50 text-purple-800 dark:text-purple-300'
60
+ when 'green' then 'bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-300'
61
+ when 'yellow' then 'bg-yellow-100 dark:bg-yellow-900/50 text-yellow-800 dark:text-yellow-300'
62
+ when 'red' then 'bg-red-100 dark:bg-red-900/50 text-red-800 dark:text-red-300'
63
+ when 'blue' then 'bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-300'
64
+ when 'orange' then 'bg-orange-100 dark:bg-orange-900/50 text-orange-800 dark:text-orange-300'
65
+ else 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300'
66
+ end %>
67
+ <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium <%= badge_classes %>"><%= badge[:label] %></span>
68
+ <% end %>
69
+ </div>
70
+ </div>
71
+ </div>
72
+ <% end %>
73
+ </div>
74
+ <div class="flex items-center gap-3 flex-shrink-0">
75
+ <%= button_to rerun_execution_path(@execution, dry_run: true),
76
+ method: :post,
77
+ data: { turbo: false },
78
+ 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
+ title: "Preview what would be sent without making an API call" do %>
80
+ <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
81
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
82
+ <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"/>
83
+ </svg>
84
+ Dry Run
85
+ <% end %>
86
+ <button
87
+ type="button"
88
+ onclick="confirmRerun()"
89
+ class="inline-flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors"
90
+ title="Re-execute this agent with the same parameters"
91
+ >
92
+ <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
93
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
94
+ </svg>
95
+ Rerun
96
+ </button>
97
+ <span class="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap"><%= @execution.created_at.strftime("%b %d, %H:%M") %></span>
98
+ </div>
99
+ </div>
43
100
 
44
- <% if @execution.finish_reason.present? %>
45
- <% finish_colors = {
46
- 'stop' => 'bg-green-100 text-green-800 dark:bg-green-900/50 dark:text-green-300',
47
- 'length' => 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/50 dark:text-yellow-300',
48
- 'content_filter' => 'bg-red-100 text-red-800 dark:bg-red-900/50 dark:text-red-300',
49
- 'tool_calls' => 'bg-blue-100 text-blue-800 dark:bg-blue-900/50 dark:text-blue-300'
50
- } %>
51
- <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium <%= finish_colors[@execution.finish_reason] || 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300' %>">
52
- <%= @execution.finish_reason %>
53
- </span>
54
- <% end %>
55
- </div>
101
+ <!-- Desktop: Row 2 - Info line + relative time -->
102
+ <div class="hidden sm:flex sm:items-center sm:justify-between mt-1.5">
103
+ <p class="text-xs text-gray-500 dark:text-gray-400">
104
+ #<%= @execution.id %> · v<%= @execution.agent_version %>
105
+ <% if @execution.model_provider.present? %>
106
+ · <%= @execution.model_provider %>
107
+ <% end %>
108
+ </p>
109
+ <span class="text-xs text-gray-400 dark:text-gray-500"><%= time_ago_in_words(@execution.created_at) %> ago</span>
110
+ </div>
56
111
 
57
- <p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
58
- Execution #<%= @execution.id %> · v<%= @execution.agent_version %>
59
- <% if @execution.model_provider.present? %>
60
- · <%= @execution.model_provider %>
61
- <% end %>
62
- </p>
112
+ <!-- Mobile: Stacked layout -->
113
+ <div class="sm:hidden">
114
+ <h2 class="text-lg font-bold text-gray-900 dark:text-gray-100 truncate">
115
+ <%= @execution.agent_type.gsub(/Agent$/, '') %>
116
+ </h2>
117
+ <div class="flex flex-wrap items-center gap-2 mt-2">
118
+ <%= render "rubyllm/agents/shared/status_badge", status: @execution.status, size: :md %>
119
+ <% secondary_badges.each do |badge| %>
120
+ <% badge_classes = case badge[:color]
121
+ when 'cyan' then 'bg-cyan-100 dark:bg-cyan-900/50 text-cyan-800 dark:text-cyan-300'
122
+ when 'purple' then 'bg-purple-100 dark:bg-purple-900/50 text-purple-800 dark:text-purple-300'
123
+ when 'green' then 'bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-300'
124
+ when 'yellow' then 'bg-yellow-100 dark:bg-yellow-900/50 text-yellow-800 dark:text-yellow-300'
125
+ when 'red' then 'bg-red-100 dark:bg-red-900/50 text-red-800 dark:text-red-300'
126
+ when 'blue' then 'bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-300'
127
+ when 'orange' then 'bg-orange-100 dark:bg-orange-900/50 text-orange-800 dark:text-orange-300'
128
+ else 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300'
129
+ end %>
130
+ <span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-medium <%= badge_classes %>"><%= badge[:label] %></span>
131
+ <% end %>
63
132
  </div>
133
+ <p class="text-xs text-gray-500 dark:text-gray-400 mt-2">
134
+ #<%= @execution.id %> · v<%= @execution.agent_version %>
135
+ <% if @execution.model_provider.present? %>
136
+ · <%= @execution.model_provider %>
137
+ <% end %>
138
+ </p>
64
139
 
65
- <div class="flex items-center gap-4">
66
- <!-- Rerun Buttons -->
140
+ <!-- Mobile: Date + Buttons -->
141
+ <div class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-700">
142
+ <p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
143
+ <%= @execution.created_at.strftime("%b %d, %Y at %H:%M") %>
144
+ <span class="text-gray-400 dark:text-gray-500">· <%= time_ago_in_words(@execution.created_at) %> ago</span>
145
+ </p>
67
146
  <div class="flex items-center gap-2">
68
147
  <%= button_to rerun_execution_path(@execution, dry_run: true),
69
148
  method: :post,
70
149
  data: { turbo: false },
71
- class: "inline-flex items-center gap-1.5 px-3 py-1.5 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",
150
+ 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",
72
151
  title: "Preview what would be sent without making an API call" do %>
73
152
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
74
153
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
@@ -76,11 +155,10 @@
76
155
  </svg>
77
156
  Dry Run
78
157
  <% end %>
79
-
80
158
  <button
81
159
  type="button"
82
160
  onclick="confirmRerun()"
83
- class="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors"
161
+ class="flex-1 inline-flex items-center justify-center gap-1.5 px-3 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors"
84
162
  title="Re-execute this agent with the same parameters"
85
163
  >
86
164
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -89,14 +167,6 @@
89
167
  Rerun
90
168
  </button>
91
169
  </div>
92
-
93
- <div class="text-right text-sm text-gray-500 dark:text-gray-400">
94
- <p><%= @execution.created_at.strftime("%b %d, %Y at %H:%M") %></p>
95
-
96
- <p class="text-xs text-gray-400 dark:text-gray-500">
97
- <%= time_ago_in_words(@execution.created_at) %> ago
98
- </p>
99
- </div>
100
170
  </div>
101
171
  </div>
102
172
  </div>
@@ -438,28 +508,116 @@
438
508
  </div>
439
509
  <% end %>
440
510
 
441
- <!-- Metadata -->
442
- <% if @execution.metadata.present? && @execution.metadata.any? %>
443
- <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6">
511
+ <!-- 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 %> }">
444
515
  <div class="flex items-center justify-between mb-4">
445
- <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Metadata</h3>
446
- <button
447
- type="button"
448
- data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(redact_for_display(@execution.metadata))) %>"
449
- data-copy-json-original="<%= Base64.strict_encode64(JSON.pretty_generate(@execution.metadata)) %>"
450
- 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"
451
- >
452
- <svg class="w-4 h-4 copy-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
453
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
454
- </svg>
455
- <svg class="w-4 h-4 check-icon hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
456
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
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"/>
457
520
  </svg>
458
- <span>Copy</span>
459
- </button>
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>
526
+ <div class="flex items-center gap-2">
527
+ <button
528
+ type="button"
529
+ data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(@execution.tool_calls)) %>"
530
+ 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
+ >
532
+ <svg class="w-4 h-4 copy-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
533
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
534
+ </svg>
535
+ <svg class="w-4 h-4 check-icon hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
536
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
537
+ </svg>
538
+ <span>Copy</span>
539
+ </button>
540
+ <% if tool_call_count > 3 %>
541
+ <button type="button" @click="expanded = !expanded" class="text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
542
+ <span x-text="expanded ? 'Collapse' : 'Expand'">Expand</span>
543
+ </button>
544
+ <% end %>
545
+ </div>
546
+ </div>
547
+
548
+ <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| %>
550
+ <%
551
+ # Handle both symbol and string keys
552
+ tool_id = tool_call['id'] || tool_call[:id]
553
+ tool_name = tool_call['name'] || tool_call[:name]
554
+ tool_args = tool_call['arguments'] || tool_call[:arguments] || {}
555
+ %>
556
+ <div class="border border-gray-100 dark:border-gray-700 rounded-lg overflow-hidden">
557
+ <!-- Tool Call Header -->
558
+ <div class="bg-gray-50 dark:bg-gray-900/50 px-4 py-3 flex items-center justify-between">
559
+ <div class="flex items-center gap-3">
560
+ <span class="inline-flex items-center justify-center w-6 h-6 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 text-xs font-medium">
561
+ <%= index + 1 %>
562
+ </span>
563
+ <code class="text-sm font-semibold text-gray-900 dark:text-gray-100"><%= tool_name %></code>
564
+ </div>
565
+ <% if tool_id.present? %>
566
+ <span class="text-xs text-gray-400 dark:text-gray-500 font-mono truncate max-w-xs" title="<%= tool_id %>">
567
+ <%= tool_id.to_s.truncate(24) %>
568
+ </span>
569
+ <% end %>
570
+ </div>
571
+
572
+ <!-- Tool Call Arguments -->
573
+ <% if tool_args.present? && tool_args.any? %>
574
+ <div class="px-4 py-3">
575
+ <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-2">Arguments</p>
576
+ <pre class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-3 text-sm overflow-x-auto font-mono"><%= highlight_json(tool_args) %></pre>
577
+ </div>
578
+ <% else %>
579
+ <div class="px-4 py-3">
580
+ <p class="text-xs text-gray-400 dark:text-gray-500 italic">No arguments</p>
581
+ </div>
582
+ <% end %>
583
+ </div>
584
+ <% end %>
585
+ </div>
586
+ </div>
587
+ <% end %>
588
+
589
+ <!-- Metadata -->
590
+ <% if @execution.metadata.present? && @execution.metadata.any? %>
591
+ <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: false }">
592
+ <div class="flex items-center justify-between">
593
+ <div class="flex items-center gap-2">
594
+ <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Metadata</h3>
595
+ <span class="text-xs text-gray-400 dark:text-gray-500">(<%= @execution.metadata.keys.count %> keys)</span>
596
+ </div>
597
+ <div class="flex items-center gap-2">
598
+ <button
599
+ 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)) %>"
602
+ 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
+ >
604
+ <svg class="w-4 h-4 copy-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
605
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
606
+ </svg>
607
+ <svg class="w-4 h-4 check-icon hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
608
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
609
+ </svg>
610
+ <span>Copy</span>
611
+ </button>
612
+ <button type="button" @click="expanded = !expanded" class="text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
613
+ <span x-text="expanded ? 'Collapse' : 'Expand'">Expand</span>
614
+ </button>
615
+ </div>
616
+ </div>
617
+ <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>
460
620
  </div>
461
- <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>
462
- <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>
463
621
  </div>
464
622
  <% end %>
465
623
 
@@ -504,6 +662,7 @@
504
662
  <span id="system-prompt-toggle">Expand</span>
505
663
  </button>
506
664
  </div>
665
+ <p id="system-prompt-preview" class="text-sm text-gray-600 dark:text-gray-300 font-mono bg-gray-50 dark:bg-gray-900 rounded-lg p-3 truncate"><%= @execution.system_prompt.truncate(150) %></p>
507
666
  <pre id="system-prompt-content" class="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 whitespace-pre-wrap"><%= @execution.system_prompt %></pre>
508
667
  </div>
509
668
  <% end %>
@@ -517,6 +676,7 @@
517
676
  <span id="user-prompt-toggle">Expand</span>
518
677
  </button>
519
678
  </div>
679
+ <p id="user-prompt-preview" class="text-sm text-gray-600 dark:text-gray-300 font-mono bg-gray-50 dark:bg-gray-900 rounded-lg p-3 truncate"><%= @execution.user_prompt.truncate(150) %></p>
520
680
  <pre id="user-prompt-content" class="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 whitespace-pre-wrap"><%= @execution.user_prompt %></pre>
521
681
  </div>
522
682
  <% end %>
@@ -753,9 +913,12 @@
753
913
  // Prompt toggle function
754
914
  function togglePrompt(type) {
755
915
  const content = document.getElementById(type + '-prompt-content');
916
+ const preview = document.getElementById(type + '-prompt-preview');
756
917
  const toggle = document.getElementById(type + '-prompt-toggle');
757
- content.classList.toggle('hidden');
758
- toggle.textContent = content.classList.contains('hidden') ? 'Expand' : 'Collapse';
918
+ const isHidden = content.classList.contains('hidden');
919
+ content.classList.toggle('hidden', !isHidden);
920
+ if (preview) preview.classList.toggle('hidden', isHidden);
921
+ toggle.textContent = isHidden ? 'Collapse' : 'Expand';
759
922
  }
760
923
 
761
924
  // Masking state (persisted in localStorage)
@@ -858,8 +1021,8 @@
858
1021
  }
859
1022
  });
860
1023
 
861
- // Diagnostics panel toggle
862
- let diagnosticsExpanded = localStorage.getItem('ruby_llm_agents_diagnostics_expanded') === 'true';
1024
+ // Diagnostics panel toggle (default to expanded)
1025
+ let diagnosticsExpanded = localStorage.getItem('ruby_llm_agents_diagnostics_expanded') !== 'false';
863
1026
 
864
1027
  function toggleDiagnostics() {
865
1028
  diagnosticsExpanded = !diagnosticsExpanded;
@@ -128,7 +128,7 @@
128
128
  <p class="text-sm font-medium text-gray-900 dark:text-gray-100">Parent Controller</p>
129
129
  <p class="text-xs text-gray-500 dark:text-gray-400">Dashboard inherits from this</p>
130
130
  </div>
131
- <code class="bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 px-2 py-1 rounded text-sm text-xs"><%= @config.dashboard_parent_controller %></code>
131
+ <code class="bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 px-2 py-1 rounded text-xs"><%= @config.dashboard_parent_controller %></code>
132
132
  </div>
133
133
  <div class="flex justify-between items-center">
134
134
  <div>
@@ -0,0 +1,48 @@
1
+ <%
2
+ # Breadcrumb navigation partial
3
+ # Usage:
4
+ # render "rubyllm/agents/shared/breadcrumbs", items: [
5
+ # { label: "Dashboard", path: ruby_llm_agents.root_path },
6
+ # { label: "Executions", path: ruby_llm_agents.executions_path },
7
+ # { label: "#123" } # Current page (no path)
8
+ # ]
9
+ #
10
+ # Or with simple array:
11
+ # render "rubyllm/agents/shared/breadcrumbs", items: [
12
+ # ["Dashboard", ruby_llm_agents.root_path],
13
+ # ["Executions", ruby_llm_agents.executions_path],
14
+ # ["#123"]
15
+ # ]
16
+
17
+ items = local_assigns[:items] || []
18
+ %>
19
+ <nav class="flex items-center text-sm mb-4" aria-label="Breadcrumb">
20
+ <ol class="flex items-center space-x-1">
21
+ <% items.each_with_index do |item, index| %>
22
+ <%
23
+ # Normalize item format
24
+ if item.is_a?(Array)
25
+ label = item[0]
26
+ path = item[1]
27
+ else
28
+ label = item[:label]
29
+ path = item[:path]
30
+ end
31
+ is_last = index == items.length - 1
32
+ %>
33
+ <li class="flex items-center">
34
+ <% if index > 0 %>
35
+ <svg class="w-4 h-4 text-gray-400 dark:text-gray-500 mx-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
36
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
37
+ </svg>
38
+ <% end %>
39
+
40
+ <% if path.present? && !is_last %>
41
+ <%= link_to label, path, class: "text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 transition-colors" %>
42
+ <% else %>
43
+ <span class="text-gray-900 dark:text-gray-100 font-medium"><%= label %></span>
44
+ <% end %>
45
+ </li>
46
+ <% end %>
47
+ </ol>
48
+ </nav>
@@ -0,0 +1,27 @@
1
+ <%# Reusable navigation link for desktop and mobile %>
2
+ <%
3
+ # Determine if link is active
4
+ is_active = if path == ruby_llm_agents.root_path
5
+ current_page?(path)
6
+ elsif path == ruby_llm_agents.agents_path
7
+ request.path.start_with?(path)
8
+ else
9
+ current_page?(path)
10
+ end
11
+
12
+ # Style classes
13
+ base_classes = mobile ? "flex items-center px-3 py-2 text-base font-medium rounded-md" : "inline-flex items-center px-3 py-1.5 text-sm font-medium rounded-md"
14
+ active_classes = "bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-gray-100"
15
+ inactive_classes = "text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-700"
16
+ icon_classes = mobile ? "w-5 h-5 mr-3" : "w-4 h-4 mr-1.5"
17
+
18
+ link_options = { class: "#{base_classes} #{is_active ? active_classes : inactive_classes}" }
19
+ link_options["x-on:click"] = "mobileMenuOpen = false" if mobile
20
+ %>
21
+
22
+ <%= link_to path, **link_options do %>
23
+ <svg class="<%= icon_classes %>" fill="none" stroke="currentColor" viewBox="0 0 24 24">
24
+ <%= icon.html_safe %>
25
+ </svg>
26
+ <%= label %>
27
+ <% end %>
data/config/routes.rb CHANGED
@@ -16,5 +16,7 @@ RubyLLM::Agents::Engine.routes.draw do
16
16
  end
17
17
  end
18
18
 
19
+ # Redirect old analytics route to dashboard
20
+ get "analytics", to: redirect("/")
19
21
  resource :settings, only: [:show]
20
22
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Migration to add dedicated tool_calls column to executions
4
+ #
5
+ # This migration adds a dedicated column for storing tool call data,
6
+ # enabling easier querying and display of tool/function calls made
7
+ # during agent execution.
8
+ #
9
+ # Tool call structure:
10
+ # [
11
+ # { "id": "call_abc123", "name": "search", "arguments": { "query": "..." } },
12
+ # { "id": "call_def456", "name": "calculate", "arguments": { ... } }
13
+ # ]
14
+ #
15
+ # Run with: rails db:migrate
16
+ class AddToolCallsToRubyLLMAgentsExecutions < ActiveRecord::Migration<%= migration_version %>
17
+ def change
18
+ # Add tool_calls JSONB array for storing tool call details
19
+ # Each tool call contains: id, name, arguments
20
+ add_column :ruby_llm_agents_executions, :tool_calls, :jsonb, null: false, default: []
21
+
22
+ # Add counter for quick access to tool call count
23
+ add_column :ruby_llm_agents_executions, :tool_calls_count, :integer, null: false, default: 0
24
+
25
+ # Add index for querying executions that have tool calls
26
+ add_index :ruby_llm_agents_executions, :tool_calls_count
27
+ end
28
+ end
@@ -67,6 +67,10 @@ class CreateRubyLLMAgentsExecutions < ActiveRecord::Migration<%= migration_versi
67
67
  t.text :system_prompt
68
68
  t.text :user_prompt
69
69
 
70
+ # Tool calls tracking
71
+ t.jsonb :tool_calls, null: false, default: []
72
+ t.integer :tool_calls_count, null: false, default: 0
73
+
70
74
  t.timestamps
71
75
  end
72
76
 
@@ -89,6 +93,9 @@ class CreateRubyLLMAgentsExecutions < ActiveRecord::Migration<%= migration_versi
89
93
  # Caching index
90
94
  add_index :ruby_llm_agents_executions, :response_cache_key
91
95
 
96
+ # Tool calls index
97
+ add_index :ruby_llm_agents_executions, :tool_calls_count
98
+
92
99
  # Foreign keys for execution hierarchy
93
100
  add_foreign_key :ruby_llm_agents_executions, :ruby_llm_agents_executions,
94
101
  column: :parent_execution_id, on_delete: :nullify
@@ -107,6 +107,19 @@ module RubyLlmAgents
107
107
  )
108
108
  end
109
109
 
110
+ def create_add_tool_calls_migration
111
+ # Check if columns already exist
112
+ if column_exists?(:ruby_llm_agents_executions, :tool_calls)
113
+ say_status :skip, "tool_calls column already exists", :yellow
114
+ return
115
+ end
116
+
117
+ migration_template(
118
+ "add_tool_calls_migration.rb.tt",
119
+ File.join(db_migrate_path, "add_tool_calls_to_ruby_llm_agents_executions.rb")
120
+ )
121
+ end
122
+
110
123
  def show_post_upgrade_message
111
124
  say ""
112
125
  say "RubyLLM::Agents upgrade migration created!", :green
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ class Base
6
+ # Cache management for agent responses
7
+ #
8
+ # Handles cache key generation and store access for
9
+ # caching agent execution results.
10
+ module Caching
11
+ # Returns the configured cache store
12
+ #
13
+ # @return [ActiveSupport::Cache::Store] The cache store
14
+ def cache_store
15
+ RubyLLM::Agents.configuration.cache_store
16
+ end
17
+
18
+ # Generates the full cache key for this agent invocation
19
+ #
20
+ # @return [String] Cache key in format "ruby_llm_agent/ClassName/version/hash"
21
+ def cache_key
22
+ ["ruby_llm_agent", self.class.name, self.class.version, cache_key_hash].join("/")
23
+ end
24
+
25
+ # Generates a hash of the cache key data
26
+ #
27
+ # @return [String] SHA256 hex digest of the cache key data
28
+ def cache_key_hash
29
+ Digest::SHA256.hexdigest(cache_key_data.to_json)
30
+ end
31
+
32
+ # Returns data to include in cache key generation
33
+ #
34
+ # Override to customize what parameters affect cache invalidation.
35
+ #
36
+ # @return [Hash] Data to hash for cache key
37
+ def cache_key_data
38
+ @options.except(:skip_cache, :dry_run, :with)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end