ruby_llm-agents 0.3.3 → 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 (97) 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 +137 -9
  7. data/app/controllers/ruby_llm/agents/executions_controller.rb +83 -5
  8. data/app/models/ruby_llm/agents/execution/analytics.rb +103 -12
  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 +28 -59
  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/ruby_llm/agents/application.html.erb +430 -0
  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/ruby_llm/agents/agents/show.html.erb +775 -0
  19. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +112 -0
  20. data/app/views/ruby_llm/agents/dashboard/_budgets_bar.html.erb +75 -0
  21. data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_execution_item.html.erb +7 -4
  22. data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +84 -0
  23. data/app/views/ruby_llm/agents/dashboard/_tenant_budget.html.erb +115 -0
  24. data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +49 -0
  25. data/app/views/ruby_llm/agents/dashboard/index.html.erb +155 -0
  26. data/app/views/{rubyllm → ruby_llm}/agents/executions/_execution.html.erb +1 -1
  27. data/app/views/{rubyllm → ruby_llm}/agents/executions/_filters.html.erb +39 -11
  28. data/app/views/{rubyllm → ruby_llm}/agents/executions/_list.html.erb +19 -9
  29. data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +101 -0
  30. data/app/views/ruby_llm/agents/executions/index.html.erb +88 -0
  31. data/app/views/{rubyllm → ruby_llm}/agents/executions/show.html.erb +260 -200
  32. data/app/views/{rubyllm → ruby_llm}/agents/settings/show.html.erb +1 -1
  33. data/app/views/ruby_llm/agents/shared/_breadcrumbs.html.erb +48 -0
  34. data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +251 -0
  35. data/app/views/{rubyllm → ruby_llm}/agents/shared/_filter_dropdown.html.erb +1 -1
  36. data/app/views/ruby_llm/agents/shared/_nav_link.html.erb +27 -0
  37. data/app/views/{rubyllm → ruby_llm}/agents/shared/_select_dropdown.html.erb +1 -1
  38. data/app/views/{rubyllm → ruby_llm}/agents/shared/_status_badge.html.erb +1 -1
  39. data/app/views/{rubyllm → ruby_llm}/agents/shared/_status_dot.html.erb +1 -1
  40. data/app/views/ruby_llm/agents/shared/_tenant_filter.html.erb +26 -0
  41. data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +61 -0
  42. data/config/routes.rb +2 -0
  43. data/lib/generators/ruby_llm_agents/multi_tenancy_generator.rb +97 -0
  44. data/lib/generators/ruby_llm_agents/templates/add_attempts_migration.rb.tt +3 -3
  45. data/lib/generators/ruby_llm_agents/templates/add_tenant_to_executions_migration.rb.tt +23 -0
  46. data/lib/generators/ruby_llm_agents/templates/add_tool_calls_migration.rb.tt +2 -2
  47. data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +38 -0
  48. data/lib/generators/ruby_llm_agents/templates/create_tenant_budgets_migration.rb.tt +45 -0
  49. data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +17 -5
  50. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +13 -0
  51. data/lib/ruby_llm/agents/alert_manager.rb +20 -16
  52. data/lib/ruby_llm/agents/base/caching.rb +40 -0
  53. data/lib/ruby_llm/agents/base/cost_calculation.rb +105 -0
  54. data/lib/ruby_llm/agents/base/dsl.rb +261 -0
  55. data/lib/ruby_llm/agents/base/execution.rb +258 -0
  56. data/lib/ruby_llm/agents/base/reliability_execution.rb +136 -0
  57. data/lib/ruby_llm/agents/base/response_building.rb +86 -0
  58. data/lib/ruby_llm/agents/base/tool_tracking.rb +57 -0
  59. data/lib/ruby_llm/agents/base.rb +37 -801
  60. data/lib/ruby_llm/agents/budget_tracker.rb +250 -139
  61. data/lib/ruby_llm/agents/cache_helper.rb +98 -0
  62. data/lib/ruby_llm/agents/circuit_breaker.rb +48 -30
  63. data/lib/ruby_llm/agents/configuration.rb +40 -1
  64. data/lib/ruby_llm/agents/engine.rb +65 -1
  65. data/lib/ruby_llm/agents/inflections.rb +14 -0
  66. data/lib/ruby_llm/agents/instrumentation.rb +66 -0
  67. data/lib/ruby_llm/agents/reliability.rb +8 -2
  68. data/lib/ruby_llm/agents/version.rb +1 -1
  69. data/lib/ruby_llm/agents/workflow/instrumentation.rb +254 -0
  70. data/lib/ruby_llm/agents/workflow/parallel.rb +282 -0
  71. data/lib/ruby_llm/agents/workflow/pipeline.rb +306 -0
  72. data/lib/ruby_llm/agents/workflow/result.rb +390 -0
  73. data/lib/ruby_llm/agents/workflow/router.rb +429 -0
  74. data/lib/ruby_llm/agents/workflow.rb +232 -0
  75. data/lib/ruby_llm/agents.rb +1 -0
  76. metadata +57 -75
  77. data/app/channels/ruby_llm/agents/executions_channel.rb +0 -46
  78. data/app/javascript/ruby_llm/agents/controllers/filter_controller.js +0 -56
  79. data/app/javascript/ruby_llm/agents/controllers/index.js +0 -12
  80. data/app/javascript/ruby_llm/agents/controllers/refresh_controller.js +0 -83
  81. data/app/views/layouts/rubyllm/agents/application.html.erb +0 -626
  82. data/app/views/rubyllm/agents/agents/index.html.erb +0 -20
  83. data/app/views/rubyllm/agents/agents/show.html.erb +0 -772
  84. data/app/views/rubyllm/agents/dashboard/_budgets_bar.html.erb +0 -165
  85. data/app/views/rubyllm/agents/dashboard/_now_strip.html.erb +0 -10
  86. data/app/views/rubyllm/agents/dashboard/_now_strip_values.html.erb +0 -71
  87. data/app/views/rubyllm/agents/dashboard/index.html.erb +0 -197
  88. data/app/views/rubyllm/agents/executions/index.html.erb +0 -28
  89. data/app/views/rubyllm/agents/executions/index.turbo_stream.erb +0 -18
  90. data/app/views/rubyllm/agents/shared/_executions_table.html.erb +0 -193
  91. /data/app/views/{rubyllm → ruby_llm}/agents/agents/_agent.html.erb +0 -0
  92. /data/app/views/{rubyllm → ruby_llm}/agents/agents/_version_comparison.html.erb +0 -0
  93. /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_action_center.html.erb +0 -0
  94. /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_alerts_feed.html.erb +0 -0
  95. /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_breaker_strip.html.erb +0 -0
  96. /data/app/views/{rubyllm → ruby_llm}/agents/executions/dry_run.html.erb +0 -0
  97. /data/app/views/{rubyllm → ruby_llm}/agents/shared/_stat_card.html.erb +0 -0
@@ -0,0 +1,775 @@
1
+ <%= render "ruby_llm/agents/shared/breadcrumbs", items: [
2
+ { label: "Dashboard", path: ruby_llm_agents.root_path },
3
+ { label: "Agents", path: ruby_llm_agents.agents_path },
4
+ { label: @agent_type.gsub(/Agent$/, '') }
5
+ ] %>
6
+
7
+ <!-- Header -->
8
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
9
+ <div class="flex items-start justify-between">
10
+ <div>
11
+ <div class="flex items-center space-x-3">
12
+ <h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">
13
+ <%= @agent_type.gsub(/Agent$/, '') %>
14
+ </h1>
15
+
16
+ <% if @agent_active %>
17
+ <span
18
+ class="
19
+ inline-flex items-center px-2.5 py-0.5 rounded-full text-xs
20
+ font-medium bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-300
21
+ "
22
+ >
23
+ Active
24
+ </span>
25
+ <% else %>
26
+ <span
27
+ class="
28
+ inline-flex items-center px-2.5 py-0.5 rounded-full text-xs
29
+ font-medium bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400
30
+ "
31
+ >
32
+ Deleted
33
+ </span>
34
+ <% end %>
35
+
36
+ <% if @config %>
37
+ <span class="text-sm text-gray-500 dark:text-gray-400">
38
+ v<%= @config[:version] %>
39
+ </span>
40
+ <% end %>
41
+ </div>
42
+
43
+ <% if @config %>
44
+ <p class="text-gray-500 dark:text-gray-400 mt-1">
45
+ <%= @config[:model] %> &middot; temp <%= @config[:temperature] %>
46
+ &middot; timeout <%= @config[:timeout] %>s
47
+ </p>
48
+ <% end %>
49
+ </div>
50
+
51
+ <div class="text-right">
52
+ <p class="text-sm text-gray-500 dark:text-gray-400">
53
+ <%= number_with_delimiter(@stats[:count]) %> total executions
54
+ </p>
55
+
56
+ <div class="flex items-center justify-end gap-3 mt-1">
57
+ <% status_colors = {
58
+ "success" => "bg-green-500",
59
+ "error" => "bg-red-500",
60
+ "timeout" => "bg-yellow-500",
61
+ "running" => "bg-blue-500"
62
+ } %>
63
+
64
+ <% @status_distribution.each do |status, count| %>
65
+ <div class="flex items-center gap-1">
66
+ <span class="w-2 h-2 rounded-full <%= status_colors[status] || 'bg-gray-400' %> <%= status == 'running' ? 'animate-pulse' : '' %>"></span>
67
+
68
+ <span class="text-xs text-gray-600 dark:text-gray-400">
69
+ <%= number_with_delimiter(count) %>
70
+ </span>
71
+ </div>
72
+ <% end %>
73
+ </div>
74
+ </div>
75
+ </div>
76
+ </div>
77
+
78
+ <!-- Circuit Breaker Status (if reliability enabled) -->
79
+ <% if @config && @circuit_breaker_status.present? %>
80
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4 mb-6">
81
+ <div class="flex items-center justify-between mb-3">
82
+ <h3
83
+ class="
84
+ text-sm font-medium text-gray-700 dark:text-gray-300 uppercase
85
+ tracking-wider
86
+ "
87
+ >
88
+ Circuit Breaker Status
89
+ </h3>
90
+
91
+ <span class="text-xs text-gray-400 dark:text-gray-500">
92
+ Auto-refreshes every 30s
93
+ </span>
94
+ </div>
95
+
96
+ <div class="flex flex-wrap gap-3">
97
+ <% @circuit_breaker_status.each do |model_id, status| %>
98
+ <div class="flex items-center gap-2 px-3 py-2 rounded-lg border <%= status[:open] ? 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800' : 'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800' %>">
99
+ <% if status[:open] %>
100
+ <!-- Open/Tripped indicator -->
101
+ <span class="relative flex h-3 w-3">
102
+ <span
103
+ class="
104
+ animate-ping absolute inline-flex h-full w-full
105
+ rounded-full bg-red-400 opacity-75
106
+ "
107
+ ></span>
108
+
109
+ <span class="relative inline-flex rounded-full h-3 w-3 bg-red-500"></span>
110
+ </span>
111
+ <span class="text-sm font-medium text-red-700 dark:text-red-300">
112
+ <%= model_id %>
113
+ </span>
114
+ <span class="text-xs text-red-500 dark:text-red-400 ml-1">
115
+ OPEN
116
+ </span>
117
+ <% if status[:cooldown_remaining] %>
118
+ <span class="text-xs text-red-400 dark:text-red-500 ml-1">
119
+ (resets in <%= status[:cooldown_remaining] %>s)
120
+ </span>
121
+ <% end %>
122
+ <% else %>
123
+ <!-- Closed/Healthy indicator -->
124
+ <span class="inline-flex rounded-full h-3 w-3 bg-green-500"></span>
125
+ <span class="text-sm font-medium text-green-700 dark:text-green-300">
126
+ <%= model_id %>
127
+ </span>
128
+ <span class="text-xs text-green-500 dark:text-green-400 ml-1">
129
+ OK
130
+ </span>
131
+ <% if status[:failure_count] && status[:failure_count] > 0 %>
132
+ <span class="text-xs text-yellow-500 dark:text-yellow-400 ml-1">
133
+ (<%= status[:failure_count] %>/<%= status[:threshold] %>
134
+ failures)
135
+ </span>
136
+ <% end %>
137
+ <% end %>
138
+ </div>
139
+ <% end %>
140
+ </div>
141
+
142
+ <% if @circuit_breaker_status.values.any? { |s| s[:open] } %>
143
+ <p class="mt-3 text-xs text-gray-500 dark:text-gray-400">
144
+ Open circuit breakers will skip the model and try fallbacks (if
145
+ configured). They automatically reset after the cooldown period.
146
+ </p>
147
+ <% end %>
148
+ </div>
149
+ <% end %>
150
+
151
+ <!-- Stats Grid -->
152
+ <% success_rate = @stats[:success_rate] || 0 %>
153
+ <% success_rate_color = success_rate >= 95 ? 'text-green-600' : success_rate >= 80 ? 'text-yellow-600' : 'text-red-600' %>
154
+
155
+ <div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4 mb-6">
156
+ <%= render "ruby_llm/agents/shared/stat_card",
157
+ title: "Executions",
158
+ value: number_with_delimiter(@stats[:count]),
159
+ subtitle: "Today: #{@stats_today[:count]}",
160
+ icon: "M13 10V3L4 14h7v7l9-11h-7z",
161
+ icon_color: "text-blue-500" %>
162
+
163
+ <%= render "ruby_llm/agents/shared/stat_card",
164
+ title: "Success Rate",
165
+ value: "#{success_rate}%",
166
+ subtitle: "Error rate: #{@stats[:error_rate] || 0}%",
167
+ icon: "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z",
168
+ icon_color: "text-green-500",
169
+ value_color: success_rate_color %>
170
+
171
+ <%= render "ruby_llm/agents/shared/stat_card",
172
+ title: "Total Cost",
173
+ value: "$#{number_with_precision(@stats[:total_cost] || 0, precision: 4)}",
174
+ subtitle: "Avg: $#{number_with_precision(@stats[:avg_cost] || 0, precision: 6)}",
175
+ icon: "M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z",
176
+ icon_color: "text-amber-500" %>
177
+
178
+ <%= render "ruby_llm/agents/shared/stat_card",
179
+ title: "Total Tokens",
180
+ value: number_with_delimiter(@stats[:total_tokens] || 0),
181
+ subtitle: "Avg: #{number_with_delimiter(@stats[:avg_tokens] || 0)}",
182
+ icon: "M7 20l4-16m2 16l4-16M6 9h14M4 15h14",
183
+ icon_color: "text-indigo-500" %>
184
+
185
+ <%= render "ruby_llm/agents/shared/stat_card",
186
+ title: "Avg Duration",
187
+ value: "#{number_with_delimiter(@stats[:avg_duration_ms] || 0)} ms",
188
+ icon: "M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z",
189
+ icon_color: "text-purple-500" %>
190
+
191
+ <%= render "ruby_llm/agents/shared/stat_card",
192
+ title: "Cache Hit Rate",
193
+ value: "#{@cache_hit_rate}%",
194
+ icon: "M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4",
195
+ icon_color: "text-purple-500" %>
196
+ </div>
197
+
198
+ <!-- Charts Section -->
199
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
200
+ <!-- Executions Over Time -->
201
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 overflow-hidden">
202
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
203
+ Executions (30 days)
204
+ </h3>
205
+ <div id="executions-chart" style="height: 220px;"></div>
206
+ </div>
207
+
208
+ <!-- Cost Over Time -->
209
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 overflow-hidden">
210
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
211
+ Cost (30 days)
212
+ </h3>
213
+ <div id="cost-chart" style="height: 220px;"></div>
214
+ </div>
215
+ </div>
216
+
217
+ <script>
218
+ (function() {
219
+ const trendData = <%= raw @trend_data.to_json %>;
220
+ const categories = trendData.map(d => d.date.split('-').slice(1).join('/'));
221
+
222
+ // Executions chart
223
+ Highcharts.chart('executions-chart', {
224
+ chart: { type: 'areaspline', backgroundColor: 'transparent' },
225
+ xAxis: { categories: categories, labels: { style: { color: '#9CA3AF' } } },
226
+ yAxis: { min: 0, title: { text: null }, labels: { style: { color: '#9CA3AF' } } },
227
+ legend: { enabled: false },
228
+ plotOptions: { areaspline: { fillOpacity: 0.2, marker: { enabled: false } } },
229
+ series: [
230
+ { name: 'Success', color: '#10B981', data: trendData.map(d => d.count - (d.error_count || 0)) },
231
+ { name: 'Failed', color: '#EF4444', data: trendData.map(d => d.error_count || 0) }
232
+ ]
233
+ });
234
+
235
+ // Cost chart
236
+ Highcharts.chart('cost-chart', {
237
+ chart: { type: 'spline', backgroundColor: 'transparent' },
238
+ xAxis: { categories: categories, labels: { style: { color: '#9CA3AF' } } },
239
+ yAxis: { min: 0, title: { text: null }, labels: { style: { color: '#9CA3AF' }, format: '${value}' } },
240
+ legend: { enabled: false },
241
+ plotOptions: { spline: { marker: { enabled: false } } },
242
+ tooltip: { valuePrefix: '$' },
243
+ series: [{ name: 'Cost', color: '#10B981', data: trendData.map(d => parseFloat(d.total_cost) || 0) }]
244
+ });
245
+ })();
246
+ </script>
247
+
248
+ <!-- Finish Reason Distribution -->
249
+ <% if @finish_reason_distribution.present? && @finish_reason_distribution.any? %>
250
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4 mb-6">
251
+ <div class="flex items-center justify-between">
252
+ <p class="text-sm text-gray-500 dark:text-gray-400 uppercase">
253
+ Finish Reasons
254
+ </p>
255
+
256
+ <div class="flex flex-wrap gap-4">
257
+ <% finish_colors = {
258
+ 'stop' => '#10B981',
259
+ 'length' => '#F59E0B',
260
+ 'content_filter' => '#EF4444',
261
+ 'tool_calls' => '#3B82F6',
262
+ nil => '#6B7280'
263
+ } %>
264
+
265
+ <% @finish_reason_distribution.each do |reason, count| %>
266
+ <div class="flex items-center">
267
+ <span
268
+ class="w-2 h-2 rounded-full mr-1.5"
269
+ style="background-color: <%= finish_colors[reason] || '#6B7280' %>"
270
+ ></span>
271
+
272
+ <span class="text-sm text-gray-700 dark:text-gray-300">
273
+ <%= reason || 'unknown' %>
274
+ </span>
275
+
276
+ <span class="text-sm font-medium text-gray-900 dark:text-gray-100 ml-1">
277
+ (<%= number_with_delimiter(count) %>)
278
+ </span>
279
+ </div>
280
+ <% end %>
281
+ </div>
282
+ </div>
283
+ </div>
284
+ <% end %>
285
+
286
+ <!-- Version Comparison -->
287
+ <%= render partial: "ruby_llm/agents/agents/version_comparison",
288
+ locals: { versions: @versions, version_comparison: @version_comparison } %>
289
+
290
+ <% if @config %>
291
+ <!-- Configuration -->
292
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
293
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
294
+ Configuration
295
+ </h3>
296
+
297
+ <!-- Basic Configuration -->
298
+ <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-3">
299
+ Basic
300
+ </p>
301
+
302
+ <div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
303
+ <div>
304
+ <p class="text-sm text-gray-500 dark:text-gray-400">Model</p>
305
+
306
+ <p class="font-medium text-gray-900 dark:text-gray-100">
307
+ <%= @config[:model] %>
308
+ </p>
309
+ </div>
310
+
311
+ <div>
312
+ <p class="text-sm text-gray-500 dark:text-gray-400">Temperature</p>
313
+
314
+ <p class="font-medium text-gray-900 dark:text-gray-100">
315
+ <%= @config[:temperature] %>
316
+ </p>
317
+ </div>
318
+
319
+ <div>
320
+ <p class="text-sm text-gray-500 dark:text-gray-400">Timeout</p>
321
+
322
+ <p class="font-medium text-gray-900 dark:text-gray-100">
323
+ <%= @config[:timeout] %> seconds
324
+ </p>
325
+ </div>
326
+
327
+ <div>
328
+ <p class="text-sm text-gray-500 dark:text-gray-400">Cache</p>
329
+
330
+ <p class="font-medium text-gray-900 dark:text-gray-100">
331
+ <% if @config[:cache_enabled] %>
332
+ Enabled (
333
+ <%= @config[:cache_ttl].inspect %>
334
+ )
335
+ <% else %>
336
+ <span class="text-gray-400 dark:text-gray-500">Disabled</span>
337
+ <% end %>
338
+ </p>
339
+ </div>
340
+ </div>
341
+
342
+ <!-- Reliability Configuration -->
343
+ <% retries_config = @config[:retries] || {}
344
+ fallback_models = Array(@config[:fallback_models]).compact
345
+ has_retries = (retries_config[:max] || 0) > 0
346
+ has_fallbacks = fallback_models.any?
347
+ has_total_timeout = @config[:total_timeout].present?
348
+ has_circuit_breaker = @config[:circuit_breaker].present?
349
+ has_any_reliability = has_retries || has_fallbacks || has_total_timeout || has_circuit_breaker %>
350
+
351
+ <div class="border-t border-gray-100 dark:border-gray-700 pt-4 mb-6">
352
+ <p
353
+ class="
354
+ text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider
355
+ mb-3
356
+ "
357
+ >
358
+ Reliability
359
+ </p>
360
+
361
+ <% if has_any_reliability %>
362
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
363
+ <!-- Retries -->
364
+ <div class="flex items-start gap-3 p-3 rounded-lg <%= has_retries ? 'bg-green-50 dark:bg-green-900/20' : 'bg-gray-50 dark:bg-gray-700/50' %>">
365
+ <div class="flex-shrink-0 mt-0.5">
366
+ <% if has_retries %>
367
+ <svg
368
+ class="w-5 h-5 text-green-500"
369
+ fill="currentColor"
370
+ viewBox="0 0 20 20"
371
+ >
372
+ <path
373
+ fill-rule="evenodd"
374
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
375
+ clip-rule="evenodd"
376
+ />
377
+ </svg>
378
+ <% else %>
379
+ <svg
380
+ class="w-5 h-5 text-gray-400"
381
+ fill="currentColor"
382
+ viewBox="0 0 20 20"
383
+ >
384
+ <path
385
+ fill-rule="evenodd"
386
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
387
+ clip-rule="evenodd"
388
+ />
389
+ </svg>
390
+ <% end %>
391
+ </div>
392
+
393
+ <div>
394
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100">
395
+ Retries
396
+ </p>
397
+
398
+ <% if has_retries %>
399
+ <p class="text-xs text-gray-600 dark:text-gray-400 mt-1">
400
+ Max: <%= retries_config[:max] %> &middot; Backoff:
401
+ <%= retries_config[:backoff] %> &middot; Base:
402
+ <%= retries_config[:base] %>s &middot; Max delay:
403
+ <%= retries_config[:max_delay] %>s
404
+ </p>
405
+ <% else %>
406
+ <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">
407
+ Not configured
408
+ </p>
409
+ <% end %>
410
+ </div>
411
+ </div>
412
+
413
+ <!-- Fallback Models -->
414
+ <div class="flex items-start gap-3 p-3 rounded-lg <%= has_fallbacks ? 'bg-green-50 dark:bg-green-900/20' : 'bg-gray-50 dark:bg-gray-700/50' %>">
415
+ <div class="flex-shrink-0 mt-0.5">
416
+ <% if has_fallbacks %>
417
+ <svg
418
+ class="w-5 h-5 text-green-500"
419
+ fill="currentColor"
420
+ viewBox="0 0 20 20"
421
+ >
422
+ <path
423
+ fill-rule="evenodd"
424
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
425
+ clip-rule="evenodd"
426
+ />
427
+ </svg>
428
+ <% else %>
429
+ <svg
430
+ class="w-5 h-5 text-gray-400"
431
+ fill="currentColor"
432
+ viewBox="0 0 20 20"
433
+ >
434
+ <path
435
+ fill-rule="evenodd"
436
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
437
+ clip-rule="evenodd"
438
+ />
439
+ </svg>
440
+ <% end %>
441
+ </div>
442
+
443
+ <div>
444
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100">
445
+ Fallback Models
446
+ </p>
447
+
448
+ <% if has_fallbacks %>
449
+ <p class="text-xs text-gray-600 dark:text-gray-400 mt-1">
450
+ <%= fallback_models.join(" → ") %>
451
+ </p>
452
+ <% else %>
453
+ <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">
454
+ Not configured
455
+ </p>
456
+ <% end %>
457
+ </div>
458
+ </div>
459
+
460
+ <!-- Total Timeout -->
461
+ <div class="flex items-start gap-3 p-3 rounded-lg <%= has_total_timeout ? 'bg-green-50 dark:bg-green-900/20' : 'bg-gray-50 dark:bg-gray-700/50' %>">
462
+ <div class="flex-shrink-0 mt-0.5">
463
+ <% if has_total_timeout %>
464
+ <svg
465
+ class="w-5 h-5 text-green-500"
466
+ fill="currentColor"
467
+ viewBox="0 0 20 20"
468
+ >
469
+ <path
470
+ fill-rule="evenodd"
471
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
472
+ clip-rule="evenodd"
473
+ />
474
+ </svg>
475
+ <% else %>
476
+ <svg
477
+ class="w-5 h-5 text-gray-400"
478
+ fill="currentColor"
479
+ viewBox="0 0 20 20"
480
+ >
481
+ <path
482
+ fill-rule="evenodd"
483
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
484
+ clip-rule="evenodd"
485
+ />
486
+ </svg>
487
+ <% end %>
488
+ </div>
489
+
490
+ <div>
491
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100">
492
+ Total Timeout
493
+ </p>
494
+
495
+ <% if has_total_timeout %>
496
+ <p class="text-xs text-gray-600 dark:text-gray-400 mt-1">
497
+ <%= @config[:total_timeout] %> seconds across all attempts
498
+ </p>
499
+ <% else %>
500
+ <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">
501
+ Not configured
502
+ </p>
503
+ <% end %>
504
+ </div>
505
+ </div>
506
+
507
+ <!-- Circuit Breaker -->
508
+ <div class="flex items-start gap-3 p-3 rounded-lg <%= has_circuit_breaker ? 'bg-green-50 dark:bg-green-900/20' : 'bg-gray-50 dark:bg-gray-700/50' %>">
509
+ <div class="flex-shrink-0 mt-0.5">
510
+ <% if has_circuit_breaker %>
511
+ <svg
512
+ class="w-5 h-5 text-green-500"
513
+ fill="currentColor"
514
+ viewBox="0 0 20 20"
515
+ >
516
+ <path
517
+ fill-rule="evenodd"
518
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
519
+ clip-rule="evenodd"
520
+ />
521
+ </svg>
522
+ <% else %>
523
+ <svg
524
+ class="w-5 h-5 text-gray-400"
525
+ fill="currentColor"
526
+ viewBox="0 0 20 20"
527
+ >
528
+ <path
529
+ fill-rule="evenodd"
530
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
531
+ clip-rule="evenodd"
532
+ />
533
+ </svg>
534
+ <% end %>
535
+ </div>
536
+
537
+ <div>
538
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100">
539
+ Circuit Breaker
540
+ </p>
541
+
542
+ <% if has_circuit_breaker %>
543
+ <% cb = @config[:circuit_breaker] %>
544
+ <p class="text-xs text-gray-600 dark:text-gray-400 mt-1">
545
+ Opens after <%= cb[:errors] %> errors within
546
+ <%= cb[:within] %>s &middot; Cooldown: <%= cb[:cooldown] %>s
547
+ </p>
548
+ <% else %>
549
+ <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">
550
+ Not configured
551
+ </p>
552
+ <% end %>
553
+ </div>
554
+ </div>
555
+ </div>
556
+ <% else %>
557
+ <p class="text-sm text-gray-400 dark:text-gray-500">
558
+ No reliability features configured
559
+ </p>
560
+ <% end %>
561
+ </div>
562
+
563
+ <!-- Parameters -->
564
+ <% if @config[:params].present? && @config[:params].any? %>
565
+ <div class="border-t border-gray-100 dark:border-gray-700 pt-4">
566
+ <p
567
+ class="
568
+ text-xs text-gray-500 dark:text-gray-400 uppercase
569
+ tracking-wider mb-3
570
+ "
571
+ >
572
+ Parameters
573
+ </p>
574
+
575
+ <div class="space-y-2">
576
+ <% @config[:params].each do |name, opts| %>
577
+ <div class="flex items-center text-sm">
578
+ <code
579
+ class="
580
+ bg-gray-100 dark:bg-gray-700 dark:text-gray-200 px-2
581
+ py-0.5 rounded font-mono
582
+ "
583
+ >
584
+ <%= name %>
585
+ </code>
586
+
587
+ <% if opts[:required] %>
588
+ <span class="ml-2 text-xs text-red-500 dark:text-red-400 font-medium">
589
+ required
590
+ </span>
591
+ <% elsif opts[:default].present? %>
592
+ <span class="ml-2 text-xs text-gray-400 dark:text-gray-500">
593
+ default: <%= opts[:default].inspect %>
594
+ </span>
595
+ <% else %>
596
+ <span class="ml-2 text-xs text-gray-400 dark:text-gray-500">
597
+ optional
598
+ </span>
599
+ <% end %>
600
+ </div>
601
+ <% end %>
602
+ </div>
603
+ </div>
604
+ <% end %>
605
+
606
+ <!-- Available Tools -->
607
+ <%
608
+ class_tools = @agent_class.respond_to?(:tools) ? (@agent_class.tools || []) : []
609
+ has_dynamic_tools = @agent_class.instance_methods(false).include?(:tools)
610
+ %>
611
+ <div class="border-t border-gray-100 dark:border-gray-700 pt-4">
612
+ <p
613
+ class="
614
+ text-xs text-gray-500 dark:text-gray-400 uppercase
615
+ tracking-wider mb-3
616
+ "
617
+ >
618
+ Available Tools
619
+ <% if class_tools.any? %>
620
+ <span class="inline-flex items-center ml-2 px-1.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-300">
621
+ <%= class_tools.size %>
622
+ </span>
623
+ <% elsif has_dynamic_tools %>
624
+ <span class="inline-flex items-center ml-2 px-1.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 dark:bg-purple-900/50 text-purple-800 dark:text-purple-300">
625
+ Dynamic
626
+ </span>
627
+ <% else %>
628
+ <span class="inline-flex items-center ml-2 px-1.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400">
629
+ 0
630
+ </span>
631
+ <% end %>
632
+ </p>
633
+
634
+ <% if class_tools.any? %>
635
+ <div class="space-y-2">
636
+ <% class_tools.each do |tool_class| %>
637
+ <div class="flex items-center text-sm">
638
+ <code
639
+ class="
640
+ bg-gray-100 dark:bg-gray-700 dark:text-gray-200 px-2
641
+ py-0.5 rounded font-mono
642
+ "
643
+ >
644
+ <%= tool_class.respond_to?(:tool_name) ? tool_class.tool_name : tool_class.name.demodulize %>
645
+ </code>
646
+ </div>
647
+ <% end %>
648
+ </div>
649
+ <% elsif has_dynamic_tools %>
650
+ <p class="text-sm text-purple-600 dark:text-purple-400">
651
+ This agent configures tools dynamically at runtime based on context.
652
+ </p>
653
+ <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">
654
+ Tools vary per execution based on feature flags and configuration.
655
+ </p>
656
+ <% else %>
657
+ <p class="text-sm text-gray-400 dark:text-gray-500 italic">
658
+ No tools configured for this agent.
659
+ </p>
660
+ <% end %>
661
+ </div>
662
+ </div>
663
+ <% end %>
664
+
665
+ <!-- Executions -->
666
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
667
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
668
+ Executions
669
+ </h3>
670
+
671
+ <div id="executions_table">
672
+ <%
673
+ has_filters = params[:statuses].present? || params[:versions].present? || params[:models].present? || params[:temperatures].present? || params[:days].present?
674
+ selected_statuses = params[:statuses].present? ? (params[:statuses].is_a?(Array) ? params[:statuses] : params[:statuses].split(",")) : []
675
+ selected_versions = params[:versions].present? ? (params[:versions].is_a?(Array) ? params[:versions] : params[:versions].split(",")) : []
676
+ selected_models = params[:models].present? ? (params[:models].is_a?(Array) ? params[:models] : params[:models].split(",")) : []
677
+ selected_temperatures = params[:temperatures].present? ? (params[:temperatures].is_a?(Array) ? params[:temperatures] : params[:temperatures].split(",")).map(&:to_s) : []
678
+
679
+ status_options = [
680
+ { value: "success", label: "Success", color: "bg-green-500" },
681
+ { value: "error", label: "Error", color: "bg-red-500" },
682
+ { value: "running", label: "Running", color: "bg-blue-500" },
683
+ { value: "timeout", label: "Timeout", color: "bg-yellow-500" }
684
+ ]
685
+ version_options = @versions.map { |v| { value: v.to_s, label: "v#{v}" } }
686
+ model_options = @models.map { |m| { value: m, label: m } }
687
+ temperature_options = @temperatures.map { |t| { value: t.to_s, label: t.to_s } }
688
+ days_options = [
689
+ { value: "", label: "All Time" },
690
+ { value: "1", label: "Today" },
691
+ { value: "7", label: "Last 7 Days" },
692
+ { value: "30", label: "Last 30 Days" }
693
+ ]
694
+ %>
695
+
696
+ <%= form_with url: ruby_llm_agents.agent_path(@agent_type), method: :get, local: true do |f| %>
697
+ <div class="flex flex-wrap items-center gap-3 mb-4 pb-4 border-b border-gray-100 dark:border-gray-700">
698
+ <%# Status Filter (Multi-select) %>
699
+ <%= render "ruby_llm/agents/shared/filter_dropdown",
700
+ name: "statuses[]",
701
+ filter_id: "statuses",
702
+ label: "Status",
703
+ all_label: "All Statuses",
704
+ options: status_options,
705
+ selected: selected_statuses %>
706
+
707
+ <%# Version Filter (Multi-select) %>
708
+ <% if @versions.any? %>
709
+ <%= render "ruby_llm/agents/shared/filter_dropdown",
710
+ name: "versions[]",
711
+ filter_id: "versions",
712
+ label: "Version",
713
+ all_label: "All Versions",
714
+ options: version_options,
715
+ selected: selected_versions,
716
+ icon: "M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" %>
717
+ <% end %>
718
+
719
+ <%# Model Filter (Multi-select) %>
720
+ <% if @models.length > 1 %>
721
+ <%= render "ruby_llm/agents/shared/filter_dropdown",
722
+ name: "models[]",
723
+ filter_id: "models",
724
+ label: "Model",
725
+ all_label: "All Models",
726
+ options: model_options,
727
+ selected: selected_models,
728
+ 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" %>
729
+ <% end %>
730
+
731
+ <%# Temperature Filter (Multi-select) %>
732
+ <% if @temperatures.length > 1 %>
733
+ <%= render "ruby_llm/agents/shared/filter_dropdown",
734
+ name: "temperatures[]",
735
+ filter_id: "temperatures",
736
+ label: "Temp",
737
+ all_label: "All Temps",
738
+ options: temperature_options,
739
+ selected: selected_temperatures,
740
+ icon: "M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" %>
741
+ <% end %>
742
+
743
+ <%# Time Range Filter (Single-select) %>
744
+ <%= render "ruby_llm/agents/shared/select_dropdown",
745
+ name: "days",
746
+ filter_id: "days",
747
+ options: days_options,
748
+ selected: params[:days].to_s,
749
+ icon: "M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" %>
750
+
751
+ <%# Clear Filters %>
752
+ <% if has_filters %>
753
+ <%= link_to ruby_llm_agents.agent_path(@agent_type),
754
+ class: "flex items-center gap-1 px-3 py-2 text-sm text-red-500 dark:text-red-400 hover:text-red-600 dark:hover:text-red-300 hover:bg-red-50 dark:hover:bg-red-900/30 rounded-lg transition-colors" do %>
755
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
756
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
757
+ </svg>
758
+ Clear
759
+ <% end %>
760
+ <% end %>
761
+
762
+ <%# Stats Summary (right aligned) %>
763
+ <div class="ml-auto flex items-center gap-4 text-sm text-gray-500 dark:text-gray-400">
764
+ <span><%= number_with_delimiter(@filter_stats[:total_count]) %> executions</span>
765
+ <span class="text-gray-300 dark:text-gray-600">|</span>
766
+ <span>$<%= number_with_precision(@filter_stats[:total_cost] || 0, precision: 4) %></span>
767
+ <span class="text-gray-300 dark:text-gray-600">|</span>
768
+ <span><%= number_with_delimiter(@filter_stats[:total_tokens] || 0) %> tokens</span>
769
+ </div>
770
+ </div>
771
+ <% end %>
772
+
773
+ <%= render "ruby_llm/agents/shared/executions_table", executions: @executions, pagination: @pagination %>
774
+ </div>
775
+ </div>