ruby_llm-agents 3.5.5 → 3.7.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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +21 -0
  3. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +155 -10
  4. data/app/controllers/ruby_llm/agents/executions_controller.rb +1 -3
  5. data/app/helpers/ruby_llm/agents/application_helper.rb +15 -28
  6. data/app/models/ruby_llm/agents/execution/replayable.rb +124 -0
  7. data/app/models/ruby_llm/agents/execution/scopes.rb +42 -1
  8. data/app/models/ruby_llm/agents/execution.rb +50 -1
  9. data/app/models/ruby_llm/agents/tenant/budgetable.rb +28 -4
  10. data/app/views/layouts/ruby_llm/agents/application.html.erb +41 -28
  11. data/app/views/ruby_llm/agents/agents/show.html.erb +16 -1
  12. data/app/views/ruby_llm/agents/dashboard/_top_tenants.html.erb +47 -0
  13. data/app/views/ruby_llm/agents/dashboard/index.html.erb +404 -107
  14. data/app/views/ruby_llm/agents/system_config/show.html.erb +0 -13
  15. data/lib/generators/ruby_llm_agents/rename_agent_generator.rb +53 -0
  16. data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +0 -15
  17. data/lib/generators/ruby_llm_agents/templates/rename_agent_migration.rb.tt +19 -0
  18. data/lib/ruby_llm/agents/agent_tool.rb +125 -0
  19. data/lib/ruby_llm/agents/audio/speaker.rb +5 -3
  20. data/lib/ruby_llm/agents/audio/speech_pricing.rb +63 -187
  21. data/lib/ruby_llm/agents/audio/transcriber.rb +5 -3
  22. data/lib/ruby_llm/agents/audio/transcription_pricing.rb +5 -7
  23. data/lib/ruby_llm/agents/base_agent.rb +144 -5
  24. data/lib/ruby_llm/agents/core/configuration.rb +178 -53
  25. data/lib/ruby_llm/agents/core/errors.rb +3 -77
  26. data/lib/ruby_llm/agents/core/instrumentation.rb +0 -17
  27. data/lib/ruby_llm/agents/core/version.rb +1 -1
  28. data/lib/ruby_llm/agents/dsl/base.rb +0 -8
  29. data/lib/ruby_llm/agents/dsl/queryable.rb +124 -0
  30. data/lib/ruby_llm/agents/dsl.rb +1 -0
  31. data/lib/ruby_llm/agents/eval/eval_result.rb +73 -0
  32. data/lib/ruby_llm/agents/eval/eval_run.rb +124 -0
  33. data/lib/ruby_llm/agents/eval/eval_suite.rb +264 -0
  34. data/lib/ruby_llm/agents/eval.rb +5 -0
  35. data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +2 -1
  36. data/lib/ruby_llm/agents/image/generator/pricing.rb +75 -217
  37. data/lib/ruby_llm/agents/image/generator.rb +5 -3
  38. data/lib/ruby_llm/agents/infrastructure/attempt_tracker.rb +8 -0
  39. data/lib/ruby_llm/agents/infrastructure/circuit_breaker.rb +4 -2
  40. data/lib/ruby_llm/agents/pipeline/builder.rb +43 -0
  41. data/lib/ruby_llm/agents/pipeline/context.rb +11 -1
  42. data/lib/ruby_llm/agents/pipeline/executor.rb +1 -25
  43. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +26 -1
  44. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +18 -0
  45. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +90 -0
  46. data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +29 -0
  47. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +11 -4
  48. data/lib/ruby_llm/agents/pipeline.rb +0 -92
  49. data/lib/ruby_llm/agents/results/background_removal_result.rb +11 -1
  50. data/lib/ruby_llm/agents/results/base.rb +23 -1
  51. data/lib/ruby_llm/agents/results/embedding_result.rb +14 -1
  52. data/lib/ruby_llm/agents/results/image_analysis_result.rb +11 -1
  53. data/lib/ruby_llm/agents/results/image_edit_result.rb +11 -1
  54. data/lib/ruby_llm/agents/results/image_generation_result.rb +12 -3
  55. data/lib/ruby_llm/agents/results/image_pipeline_result.rb +11 -1
  56. data/lib/ruby_llm/agents/results/image_transform_result.rb +11 -1
  57. data/lib/ruby_llm/agents/results/image_upscale_result.rb +11 -1
  58. data/lib/ruby_llm/agents/results/image_variation_result.rb +11 -1
  59. data/lib/ruby_llm/agents/results/speech_result.rb +20 -1
  60. data/lib/ruby_llm/agents/results/transcription_result.rb +20 -1
  61. data/lib/ruby_llm/agents/text/embedder.rb +23 -18
  62. data/lib/ruby_llm/agents.rb +73 -5
  63. data/lib/tasks/ruby_llm_agents.rake +21 -0
  64. metadata +11 -6
  65. data/lib/ruby_llm/agents/infrastructure/reliability/breaker_manager.rb +0 -80
  66. data/lib/ruby_llm/agents/infrastructure/reliability/execution_constraints.rb +0 -69
  67. data/lib/ruby_llm/agents/infrastructure/reliability/executor.rb +0 -125
  68. data/lib/ruby_llm/agents/infrastructure/reliability/fallback_routing.rb +0 -72
  69. data/lib/ruby_llm/agents/infrastructure/reliability/retry_strategy.rb +0 -82
@@ -125,26 +125,32 @@ module RubyLLM
125
125
 
126
126
  # Returns the effective per-agent daily limit
127
127
  #
128
+ # Checks the current name and all aliases for matching limits.
129
+ #
128
130
  # @param agent_type [String] The agent class name
129
131
  # @return [Float, nil] The limit or nil if not set
130
132
  def effective_per_agent_daily(agent_type)
131
- limit = per_agent_daily&.dig(agent_type)
133
+ names = resolve_agent_names(agent_type)
134
+ limit = names.lazy.filter_map { |n| per_agent_daily&.dig(n) }.first
132
135
  return limit if limit.present?
133
136
  return nil unless inherit_global_defaults
134
137
 
135
- global_config&.dig(:per_agent_daily, agent_type)
138
+ names.lazy.filter_map { |n| global_config&.dig(:per_agent_daily, n) }.first
136
139
  end
137
140
 
138
141
  # Returns the effective per-agent monthly limit
139
142
  #
143
+ # Checks the current name and all aliases for matching limits.
144
+ #
140
145
  # @param agent_type [String] The agent class name
141
146
  # @return [Float, nil] The limit or nil if not set
142
147
  def effective_per_agent_monthly(agent_type)
143
- limit = per_agent_monthly&.dig(agent_type)
148
+ names = resolve_agent_names(agent_type)
149
+ limit = names.lazy.filter_map { |n| per_agent_monthly&.dig(n) }.first
144
150
  return limit if limit.present?
145
151
  return nil unless inherit_global_defaults
146
152
 
147
- global_config&.dig(:per_agent_monthly, agent_type)
153
+ names.lazy.filter_map { |n| global_config&.dig(:per_agent_monthly, n) }.first
148
154
  end
149
155
 
150
156
  # Budget status checks
@@ -352,6 +358,24 @@ module RubyLLM
352
358
  RubyLLM::Agents.configuration.budgets
353
359
  end
354
360
 
361
+ # Resolves all known names for an agent (current name + aliases)
362
+ #
363
+ # @param agent_type [String] The agent class name
364
+ # @return [Array<String>] All names to check
365
+ def resolve_agent_names(agent_type)
366
+ name = agent_type.to_s
367
+ klass = begin
368
+ name.constantize
369
+ rescue NameError
370
+ nil
371
+ end
372
+ if klass&.respond_to?(:all_agent_names)
373
+ klass.all_agent_names
374
+ else
375
+ [name]
376
+ end
377
+ end
378
+
355
379
  # Merges per-agent daily limits with global defaults
356
380
  #
357
381
  # @return [Hash]
@@ -7,6 +7,13 @@
7
7
 
8
8
  <title>RubyLLM Agents Dashboard</title>
9
9
 
10
+ <!-- Favicons -->
11
+ <link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96">
12
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg">
13
+ <link rel="shortcut icon" href="/favicon.ico">
14
+ <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
15
+ <link rel="manifest" href="/site.webmanifest">
16
+
10
17
  <!-- Prevent flash of wrong theme - must run before any rendering -->
11
18
  <script>
12
19
  (function() {
@@ -152,8 +159,9 @@
152
159
  }
153
160
  </script>
154
161
 
155
- <!-- Highcharts for charts -->
156
- <script src="https://code.highcharts.com/highcharts.js"></script>
162
+ <!-- Highcharts for charts (primary CDN, fallback to jsDelivr) -->
163
+ <script src="https://code.highcharts.com/highcharts.js"
164
+ onerror="var s=document.createElement('script');s.src='https://cdn.jsdelivr.net/npm/highcharts@11.4.0/highcharts.min.js';s.onload=window.__initHighchartsDefaults;document.head.appendChild(s);"></script>
157
165
 
158
166
  <!-- Chart color helpers + Highcharts defaults -->
159
167
  <script>
@@ -164,32 +172,37 @@
164
172
  return document.documentElement.classList.contains('dark')
165
173
  ? 'rgba(' + darkR + ',' + darkG + ',' + darkB + ',' + alpha + ')' : lightRgba;
166
174
  }
167
- Highcharts.setOptions({
168
- credits: { enabled: false },
169
- chart: {
170
- backgroundColor: 'transparent',
171
- style: { fontFamily: 'ui-monospace, monospace' }
172
- },
173
- title: { text: null },
174
- xAxis: {
175
- labels: { style: { color: chartColor('#6B7280', '#928374') } },
176
- lineColor: chartColorAlpha('rgba(107, 114, 128, 0.1)', 146, 131, 116, 0.1),
177
- tickColor: chartColorAlpha('rgba(107, 114, 128, 0.1)', 146, 131, 116, 0.1)
178
- },
179
- yAxis: {
180
- labels: { style: { color: chartColor('#6B7280', '#928374') } },
181
- gridLineColor: chartColorAlpha('rgba(107, 114, 128, 0.1)', 146, 131, 116, 0.1)
182
- },
183
- legend: {
184
- itemStyle: { color: chartColor('#6B7280', '#928374') },
185
- itemHoverStyle: { color: chartColor('#9CA3AF', '#bdae93') }
186
- },
187
- tooltip: {
188
- backgroundColor: chartColorAlpha('rgba(0, 0, 0, 0.85)', 40, 40, 40, 0.95),
189
- borderColor: 'transparent',
190
- style: { color: chartColor('#E5E7EB', '#ebdbb2'), fontFamily: 'ui-monospace, monospace' }
191
- }
192
- });
175
+ window.__initHighchartsDefaults = function() {
176
+ if (typeof Highcharts === 'undefined') return;
177
+ Highcharts.setOptions({
178
+ accessibility: { enabled: false },
179
+ credits: { enabled: false },
180
+ chart: {
181
+ backgroundColor: 'transparent',
182
+ style: { fontFamily: 'ui-monospace, monospace' }
183
+ },
184
+ title: { text: null },
185
+ xAxis: {
186
+ labels: { style: { color: chartColor('#6B7280', '#928374') } },
187
+ lineColor: chartColorAlpha('rgba(107, 114, 128, 0.1)', 146, 131, 116, 0.1),
188
+ tickColor: chartColorAlpha('rgba(107, 114, 128, 0.1)', 146, 131, 116, 0.1)
189
+ },
190
+ yAxis: {
191
+ labels: { style: { color: chartColor('#6B7280', '#928374') } },
192
+ gridLineColor: chartColorAlpha('rgba(107, 114, 128, 0.1)', 146, 131, 116, 0.1)
193
+ },
194
+ legend: {
195
+ itemStyle: { color: chartColor('#6B7280', '#928374') },
196
+ itemHoverStyle: { color: chartColor('#9CA3AF', '#bdae93') }
197
+ },
198
+ tooltip: {
199
+ backgroundColor: chartColorAlpha('rgba(0, 0, 0, 0.85)', 40, 40, 40, 0.95),
200
+ borderColor: 'transparent',
201
+ style: { color: chartColor('#E5E7EB', '#ebdbb2'), fontFamily: 'ui-monospace, monospace' }
202
+ }
203
+ });
204
+ };
205
+ window.__initHighchartsDefaults();
193
206
  </script>
194
207
 
195
208
  <!-- Alpine.js -->
@@ -104,7 +104,9 @@
104
104
  const successData = trendData.map(d => [new Date(d.date).getTime(), d.count - d.errors]);
105
105
  const errorData = trendData.map(d => [new Date(d.date).getTime(), d.errors]);
106
106
 
107
- Highcharts.chart('agent-trend-chart', {
107
+ function renderChart() {
108
+ window.__initHighchartsDefaults && window.__initHighchartsDefaults();
109
+ Highcharts.chart('agent-trend-chart', {
108
110
  chart: { type: 'areaspline', backgroundColor: 'transparent', spacing: [5, 0, 0, 0] },
109
111
  title: { text: null },
110
112
  xAxis: {
@@ -159,6 +161,19 @@
159
161
  }
160
162
  ]
161
163
  });
164
+ }
165
+
166
+ function waitForHighcharts(attempts) {
167
+ if (typeof Highcharts !== 'undefined') {
168
+ renderChart();
169
+ } else if (attempts > 0) {
170
+ setTimeout(function() { waitForHighcharts(attempts - 1); }, 100);
171
+ }
172
+ }
173
+
174
+ document.readyState === 'loading'
175
+ ? document.addEventListener('DOMContentLoaded', function() { waitForHighcharts(50); })
176
+ : waitForHighcharts(50);
162
177
  })();
163
178
  </script>
164
179
 
@@ -0,0 +1,47 @@
1
+ <%# Top Tenants Widget - shows top tenants by monthly spend %>
2
+ <%# @param top_tenants [Array<Hash>, nil] Tenant data from controller %>
3
+
4
+ <% if top_tenants.present? %>
5
+ <div class="mt-8">
6
+ <div class="flex items-center gap-3 mb-3">
7
+ <span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">tenants</span>
8
+ <div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
9
+ <%= link_to "all →", ruby_llm_agents.tenants_path, class: "text-[10px] font-mono text-gray-400 dark:text-gray-600 hover:text-gray-600 dark:hover:text-gray-400" %>
10
+ </div>
11
+
12
+ <div class="font-mono text-xs space-y-px">
13
+ <% top_tenants.each do |tenant| %>
14
+ <div class="group flex items-center gap-3 py-1.5 px-2 -mx-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800/50 cursor-pointer"
15
+ onclick="window.location='<%= ruby_llm_agents.tenant_path(tenant[:id]) %>'">
16
+ <span class="w-32 truncate text-gray-900 dark:text-gray-200"><%= tenant[:name] %></span>
17
+ <span class="text-gray-500 dark:text-gray-400">$<%= number_with_precision(tenant[:monthly_spend], precision: 2) %></span>
18
+ <% if tenant[:monthly_limit] && tenant[:monthly_limit] > 0 %>
19
+ <%
20
+ pct = [tenant[:monthly_percentage], 100].min
21
+ bar_color = if pct >= 100 then "bg-red-500"
22
+ elsif pct >= 80 then "bg-yellow-500"
23
+ else "bg-blue-500"
24
+ end
25
+ %>
26
+ <div class="w-20 bg-gray-200 dark:bg-gray-800 rounded-full h-1 hidden sm:block">
27
+ <div class="<%= bar_color %> h-1 rounded-full transition-all" style="width: <%= pct %>%"></div>
28
+ </div>
29
+ <span class="w-10 text-right text-gray-400 dark:text-gray-600 hidden sm:inline"><%= tenant[:monthly_percentage] %>%</span>
30
+ <% else %>
31
+ <span class="text-gray-400 dark:text-gray-600 hidden sm:inline">(no limit)</span>
32
+ <% end %>
33
+ <%
34
+ enforcement = tenant[:enforcement].to_s
35
+ badge_class = case enforcement
36
+ when "hard" then "text-red-400 dark:text-red-500/70"
37
+ when "soft" then "text-yellow-400 dark:text-yellow-500/70"
38
+ else "text-gray-400 dark:text-gray-600"
39
+ end
40
+ %>
41
+ <span class="<%= badge_class %> hidden md:inline"><%= enforcement %></span>
42
+ <span class="ml-auto text-gray-400 dark:text-gray-600"><%= number_with_delimiter(tenant[:monthly_executions]) %><span class="text-gray-400 dark:text-gray-600">r</span></span>
43
+ </div>
44
+ <% end %>
45
+ </div>
46
+ </div>
47
+ <% end %>