ruby_llm-agents 0.2.4 → 0.3.0

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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +273 -0
  3. data/app/channels/ruby_llm/agents/executions_channel.rb +24 -1
  4. data/app/controllers/concerns/ruby_llm/agents/filterable.rb +81 -0
  5. data/app/controllers/concerns/ruby_llm/agents/paginatable.rb +51 -0
  6. data/app/controllers/ruby_llm/agents/agents_controller.rb +228 -59
  7. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +167 -12
  8. data/app/controllers/ruby_llm/agents/executions_controller.rb +189 -31
  9. data/app/controllers/ruby_llm/agents/settings_controller.rb +20 -0
  10. data/app/helpers/ruby_llm/agents/application_helper.rb +307 -7
  11. data/app/models/ruby_llm/agents/execution/analytics.rb +224 -20
  12. data/app/models/ruby_llm/agents/execution/metrics.rb +41 -25
  13. data/app/models/ruby_llm/agents/execution/scopes.rb +234 -14
  14. data/app/models/ruby_llm/agents/execution.rb +259 -16
  15. data/app/services/ruby_llm/agents/agent_registry.rb +49 -12
  16. data/app/views/layouts/rubyllm/agents/application.html.erb +351 -85
  17. data/app/views/rubyllm/agents/agents/_version_comparison.html.erb +186 -0
  18. data/app/views/rubyllm/agents/agents/show.html.erb +233 -10
  19. data/app/views/rubyllm/agents/dashboard/_action_center.html.erb +62 -0
  20. data/app/views/rubyllm/agents/dashboard/_alerts_feed.html.erb +62 -0
  21. data/app/views/rubyllm/agents/dashboard/_breaker_strip.html.erb +47 -0
  22. data/app/views/rubyllm/agents/dashboard/_budgets_bar.html.erb +165 -0
  23. data/app/views/rubyllm/agents/dashboard/_now_strip.html.erb +10 -0
  24. data/app/views/rubyllm/agents/dashboard/_now_strip_values.html.erb +71 -0
  25. data/app/views/rubyllm/agents/dashboard/index.html.erb +215 -109
  26. data/app/views/rubyllm/agents/executions/_filters.html.erb +152 -155
  27. data/app/views/rubyllm/agents/executions/_list.html.erb +103 -12
  28. data/app/views/rubyllm/agents/executions/dry_run.html.erb +149 -0
  29. data/app/views/rubyllm/agents/executions/index.html.erb +17 -72
  30. data/app/views/rubyllm/agents/executions/index.turbo_stream.erb +16 -2
  31. data/app/views/rubyllm/agents/executions/show.html.erb +693 -14
  32. data/app/views/rubyllm/agents/settings/show.html.erb +369 -0
  33. data/app/views/rubyllm/agents/shared/_filter_dropdown.html.erb +121 -0
  34. data/app/views/rubyllm/agents/shared/_select_dropdown.html.erb +85 -0
  35. data/config/routes.rb +7 -0
  36. data/lib/generators/ruby_llm_agents/templates/add_attempts_migration.rb.tt +27 -0
  37. data/lib/generators/ruby_llm_agents/templates/add_caching_migration.rb.tt +23 -0
  38. data/lib/generators/ruby_llm_agents/templates/add_finish_reason_migration.rb.tt +19 -0
  39. data/lib/generators/ruby_llm_agents/templates/add_routing_migration.rb.tt +19 -0
  40. data/lib/generators/ruby_llm_agents/templates/add_streaming_migration.rb.tt +8 -0
  41. data/lib/generators/ruby_llm_agents/templates/add_tracing_migration.rb.tt +34 -0
  42. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +66 -4
  43. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +53 -6
  44. data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +139 -8
  45. data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +38 -1
  46. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +78 -0
  47. data/lib/ruby_llm/agents/alert_manager.rb +207 -0
  48. data/lib/ruby_llm/agents/attempt_tracker.rb +295 -0
  49. data/lib/ruby_llm/agents/base.rb +580 -112
  50. data/lib/ruby_llm/agents/budget_tracker.rb +360 -0
  51. data/lib/ruby_llm/agents/circuit_breaker.rb +197 -0
  52. data/lib/ruby_llm/agents/configuration.rb +279 -1
  53. data/lib/ruby_llm/agents/engine.rb +58 -6
  54. data/lib/ruby_llm/agents/execution_logger_job.rb +17 -6
  55. data/lib/ruby_llm/agents/inflections.rb +13 -2
  56. data/lib/ruby_llm/agents/instrumentation.rb +538 -87
  57. data/lib/ruby_llm/agents/redactor.rb +130 -0
  58. data/lib/ruby_llm/agents/reliability.rb +185 -0
  59. data/lib/ruby_llm/agents/version.rb +3 -1
  60. data/lib/ruby_llm/agents.rb +52 -0
  61. metadata +41 -2
  62. data/app/controllers/ruby_llm/agents/application_controller.rb +0 -37
@@ -0,0 +1,149 @@
1
+ <div class="mb-6">
2
+ <%= link_to ruby_llm_agents.execution_path(@execution), class: "inline-flex items-center text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200" do %>
3
+ <svg
4
+ class="w-4 h-4 mr-1"
5
+ fill="none"
6
+ stroke="currentColor"
7
+ viewBox="0 0 24 24"
8
+ >
9
+ <path
10
+ stroke-linecap="round"
11
+ stroke-linejoin="round"
12
+ stroke-width="2"
13
+ d="M10 19l-7-7m0 0l7-7m-7 7h18"
14
+ />
15
+ </svg>
16
+ Back to Execution
17
+ <% end %>
18
+ </div>
19
+
20
+ <!-- Header -->
21
+ <div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-xl p-6 mb-6">
22
+ <div class="flex items-center gap-3 mb-2">
23
+ <svg class="w-6 h-6 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
24
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
25
+ <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"/>
26
+ </svg>
27
+ <h2 class="text-xl font-bold text-blue-900 dark:text-blue-100">Dry Run Preview</h2>
28
+ </div>
29
+ <p class="text-sm text-blue-700 dark:text-blue-300">
30
+ This is a preview of what would be sent to the LLM. No API call was made and no execution was recorded.
31
+ </p>
32
+ </div>
33
+
34
+ <!-- Agent Info -->
35
+ <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6">
36
+ <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-4">Agent Configuration</h3>
37
+
38
+ <div class="grid grid-cols-2 md:grid-cols-4 gap-4">
39
+ <div>
40
+ <p class="text-xs text-gray-500 dark:text-gray-400">Agent</p>
41
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100"><%= @dry_run_result[:agent] || @execution.agent_type %></p>
42
+ </div>
43
+ <div>
44
+ <p class="text-xs text-gray-500 dark:text-gray-400">Model</p>
45
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100"><%= @dry_run_result[:model] || 'N/A' %></p>
46
+ </div>
47
+ <div>
48
+ <p class="text-xs text-gray-500 dark:text-gray-400">Temperature</p>
49
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100"><%= @dry_run_result[:temperature] || 'N/A' %></p>
50
+ </div>
51
+ <div>
52
+ <p class="text-xs text-gray-500 dark:text-gray-400">Version</p>
53
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100"><%= @dry_run_result[:version] || 'N/A' %></p>
54
+ </div>
55
+ </div>
56
+ </div>
57
+
58
+ <!-- System Prompt -->
59
+ <% if @dry_run_result[:system_prompt].present? %>
60
+ <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6">
61
+ <div class="flex items-center justify-between mb-4">
62
+ <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">System Prompt</h3>
63
+ <button
64
+ type="button"
65
+ onclick="copyToClipboard(this, 'system-prompt-content')"
66
+ class="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"
67
+ >
68
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
69
+ <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"/>
70
+ </svg>
71
+ <span>Copy</span>
72
+ </button>
73
+ </div>
74
+ <pre id="system-prompt-content" class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto whitespace-pre-wrap"><%= @dry_run_result[:system_prompt] %></pre>
75
+ </div>
76
+ <% end %>
77
+
78
+ <!-- User Prompt -->
79
+ <% if @dry_run_result[:user_prompt].present? %>
80
+ <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6">
81
+ <div class="flex items-center justify-between mb-4">
82
+ <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">User Prompt</h3>
83
+ <button
84
+ type="button"
85
+ onclick="copyToClipboard(this, 'user-prompt-content')"
86
+ class="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"
87
+ >
88
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
89
+ <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"/>
90
+ </svg>
91
+ <span>Copy</span>
92
+ </button>
93
+ </div>
94
+ <pre id="user-prompt-content" class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto whitespace-pre-wrap"><%= @dry_run_result[:user_prompt] %></pre>
95
+ </div>
96
+ <% end %>
97
+
98
+ <!-- Parameters -->
99
+ <% if @dry_run_result[:parameters].present? %>
100
+ <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6">
101
+ <div class="flex items-center justify-between mb-4">
102
+ <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Parameters</h3>
103
+ </div>
104
+ <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(@dry_run_result[:parameters]) %></pre>
105
+ </div>
106
+ <% end %>
107
+
108
+ <!-- Actions -->
109
+ <div class="flex items-center gap-4">
110
+ <%= link_to execution_path(@execution), class: "inline-flex items-center gap-1.5 px-4 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" do %>
111
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
112
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
113
+ </svg>
114
+ Back to Execution
115
+ <% end %>
116
+
117
+ <%= button_to rerun_execution_path(@execution),
118
+ method: :post,
119
+ class: "inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors",
120
+ data: { confirm: "This will make a real API call and create a new execution. Are you sure?" } do %>
121
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
122
+ <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"/>
123
+ </svg>
124
+ Execute for Real
125
+ <% end %>
126
+ </div>
127
+
128
+ <script>
129
+ function copyToClipboard(button, elementId) {
130
+ const content = document.getElementById(elementId).textContent;
131
+ const span = button.querySelector('span');
132
+
133
+ navigator.clipboard.writeText(content).then(function() {
134
+ span.textContent = 'Copied!';
135
+ button.classList.add('text-green-600');
136
+
137
+ setTimeout(function() {
138
+ span.textContent = 'Copy';
139
+ button.classList.remove('text-green-600');
140
+ }, 2000);
141
+ }).catch(function(err) {
142
+ console.error('Failed to copy:', err);
143
+ span.textContent = 'Failed';
144
+ setTimeout(function() {
145
+ span.textContent = 'Copy';
146
+ }, 2000);
147
+ });
148
+ }
149
+ </script>
@@ -1,83 +1,28 @@
1
1
  <h2 class="text-2xl font-bold text-gray-900 dark:text-gray-100 mb-6">Executions</h2>
2
2
 
3
3
  <%= turbo_frame_tag "executions_content" do %>
4
- <%= render partial: "rubyllm/agents/executions/filters", locals: { agent_types: @agent_types, filter_stats: @filter_stats } %>
4
+ <%= render partial: "rubyllm/agents/executions/filters", locals: { agent_types: @agent_types, model_ids: @model_ids, filter_stats: @filter_stats } %>
5
5
  <%= render partial: "rubyllm/agents/executions/list", locals: { executions: @executions, pagination: @pagination, filter_stats: @filter_stats } %>
6
6
  <% end %>
7
7
 
8
8
  <script>
9
- function toggleDropdown(button) {
10
- document.querySelectorAll('.filter-dropdown .dropdown-menu').forEach(menu => {
11
- if (menu !== button.nextElementSibling) {
12
- menu.classList.add('hidden');
9
+ // Toggle attempts expansion (used by executions list)
10
+ function toggleAttempts(executionId) {
11
+ const attemptsRow = document.getElementById('attempts-row-' + executionId);
12
+ const expandBtn = document.querySelector(`button[data-execution-id="${executionId}"]`);
13
+
14
+ if (attemptsRow) {
15
+ const isHidden = attemptsRow.classList.contains('hidden');
16
+ attemptsRow.classList.toggle('hidden');
17
+
18
+ // Rotate the chevron
19
+ if (expandBtn) {
20
+ const svg = expandBtn.querySelector('svg');
21
+ if (svg) {
22
+ svg.classList.toggle('rotate-90', isHidden);
23
+ }
24
+ expandBtn.setAttribute('aria-expanded', isHidden ? 'true' : 'false');
13
25
  }
14
- });
15
- button.nextElementSibling.classList.toggle('hidden');
16
- }
17
-
18
- function selectSingleFilter(name, value, label) {
19
- document.getElementById('filter_' + name).value = value;
20
- // Update button label
21
- const dropdown = document.querySelector(`[data-filter="${name}"]`);
22
- dropdown.querySelector('.dropdown-label').textContent = label;
23
- // Update ring
24
- const button = dropdown.querySelector('button');
25
- if (value) {
26
- button.classList.add('ring-2', 'ring-blue-500', 'ring-offset-1');
27
- } else {
28
- button.classList.remove('ring-2', 'ring-blue-500', 'ring-offset-1');
29
- }
30
- // Close dropdown and submit
31
- dropdown.querySelector('.dropdown-menu').classList.add('hidden');
32
- document.getElementById('filters-form').requestSubmit();
33
- }
34
-
35
- function toggleAllOptions(checkbox, filterName) {
36
- const dropdown = document.querySelector(`[data-filter="${filterName}"]`);
37
- const checkboxes = dropdown.querySelectorAll('.filter-checkbox');
38
-
39
- if (checkbox.checked) {
40
- // Uncheck all specific options
41
- checkboxes.forEach(cb => cb.checked = false);
42
- updateMultiSelect(filterName);
43
26
  }
44
27
  }
45
-
46
- function updateMultiSelect(filterName) {
47
- const dropdown = document.querySelector(`[data-filter="${filterName}"]`);
48
- const checkboxes = dropdown.querySelectorAll('.filter-checkbox:checked');
49
- const selectAllCheckbox = dropdown.querySelector('.select-all-checkbox');
50
- const button = dropdown.querySelector('button');
51
- const label = dropdown.querySelector('.dropdown-label');
52
-
53
- const count = checkboxes.length;
54
-
55
- // Update "All" checkbox
56
- selectAllCheckbox.checked = (count === 0);
57
-
58
- // Update label
59
- if (count === 0) {
60
- label.textContent = filterName === 'agent_types' ? 'All Agents' : 'All Statuses';
61
- button.classList.remove('ring-2', 'ring-blue-500', 'ring-offset-1');
62
- } else if (count === 1) {
63
- const value = checkboxes[0].value;
64
- label.textContent = filterName === 'agent_types' ? value.replace(/Agent$/, '') : value.charAt(0).toUpperCase() + value.slice(1);
65
- button.classList.add('ring-2', 'ring-blue-500', 'ring-offset-1');
66
- } else {
67
- label.textContent = `${count} ${filterName === 'agent_types' ? 'Agents' : 'Statuses'}`;
68
- button.classList.add('ring-2', 'ring-blue-500', 'ring-offset-1');
69
- }
70
-
71
- // Submit form
72
- document.getElementById('filters-form').requestSubmit();
73
- }
74
-
75
- // Close dropdowns when clicking outside
76
- document.addEventListener('click', function(e) {
77
- if (!e.target.closest('.filter-dropdown')) {
78
- document.querySelectorAll('.filter-dropdown .dropdown-menu').forEach(menu => {
79
- menu.classList.add('hidden');
80
- });
81
- }
82
- });
83
28
  </script>
@@ -1,4 +1,18 @@
1
- <%= turbo_stream.replace "executions_content" do %>
2
- <%= render partial: "rubyllm/agents/executions/filters", locals: { agent_types: @agent_types, filter_stats: @filter_stats } %>
1
+ <%# Only update the stats (not the whole filter container to preserve Alpine.js state) %>
2
+ <%= turbo_stream.replace "filter-stats" do %>
3
+ <div id="filter-stats" class="flex items-center justify-between md:justify-end gap-3 py-2 px-3 md:p-0 bg-gray-50 md:bg-transparent dark:bg-gray-700/50 md:dark:bg-transparent rounded-lg md:rounded-none">
4
+ <span class="text-xs text-gray-500 dark:text-gray-400 md:hidden">Results</span>
5
+ <div class="flex items-center gap-3 text-xs text-gray-500 dark:text-gray-400">
6
+ <span class="tabular-nums"><%= number_to_human_short(@filter_stats[:total_count]) %><span class="md:hidden"> executions</span></span>
7
+ <span class="text-gray-300 dark:text-gray-600">|</span>
8
+ <span class="tabular-nums"><%= number_to_human_short(@filter_stats[:total_cost], prefix: "$", precision: 2) %></span>
9
+ <span class="hidden md:inline text-gray-300 dark:text-gray-600">|</span>
10
+ <span class="hidden md:inline tabular-nums"><%= number_to_human_short(@filter_stats[:total_tokens]) %>t</span>
11
+ </div>
12
+ </div>
13
+ <% end %>
14
+
15
+ <%# Update the executions list %>
16
+ <%= turbo_stream.replace "executions-list" do %>
3
17
  <%= render partial: "rubyllm/agents/executions/list", locals: { executions: @executions, pagination: @pagination, filter_stats: @filter_stats } %>
4
18
  <% end %>