ruby_llm-agents 0.2.4 → 0.3.1

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 +413 -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 +143 -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 +597 -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,369 @@
1
+ <div class="space-y-6">
2
+ <!-- Header -->
3
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
4
+ <div class="flex items-center space-x-3">
5
+ <svg class="w-8 h-8 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
6
+ <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"/>
7
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
8
+ </svg>
9
+ <div>
10
+ <h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">Global Configuration</h1>
11
+ <p class="text-sm text-gray-500 dark:text-gray-400">Settings configured in <code class="bg-gray-100 dark:bg-gray-700 px-1 rounded">config/initializers/ruby_llm_agents.rb</code></p>
12
+ </div>
13
+ </div>
14
+ </div>
15
+
16
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
17
+ <!-- Model Defaults -->
18
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
19
+ <h3 class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-4">Model Defaults</h3>
20
+ <div class="space-y-4">
21
+ <div class="flex justify-between items-center">
22
+ <div>
23
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100">Default Model</p>
24
+ <p class="text-xs text-gray-500 dark:text-gray-400">Used when agents don't specify a model</p>
25
+ </div>
26
+ <code class="bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 px-2 py-1 rounded text-sm"><%= @config.default_model %></code>
27
+ </div>
28
+ <div class="flex justify-between items-center">
29
+ <div>
30
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100">Default Temperature</p>
31
+ <p class="text-xs text-gray-500 dark:text-gray-400">0.0 = deterministic, 2.0 = creative</p>
32
+ </div>
33
+ <code class="bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 px-2 py-1 rounded text-sm"><%= @config.default_temperature %></code>
34
+ </div>
35
+ <div class="flex justify-between items-center">
36
+ <div>
37
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100">Default Timeout</p>
38
+ <p class="text-xs text-gray-500 dark:text-gray-400">Per-request timeout in seconds</p>
39
+ </div>
40
+ <code class="bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 px-2 py-1 rounded text-sm"><%= @config.default_timeout %>s</code>
41
+ </div>
42
+ </div>
43
+ </div>
44
+
45
+ <!-- Execution Logging -->
46
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
47
+ <h3 class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-4">Execution Logging</h3>
48
+ <div class="space-y-4">
49
+ <div class="flex justify-between items-center">
50
+ <div>
51
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100">Async Logging</p>
52
+ <p class="text-xs text-gray-500 dark:text-gray-400">Log executions via background job</p>
53
+ </div>
54
+ <%= render_enabled_badge(@config.async_logging) %>
55
+ </div>
56
+ <div class="flex justify-between items-center">
57
+ <div>
58
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100">Retention Period</p>
59
+ <p class="text-xs text-gray-500 dark:text-gray-400">How long to keep execution records</p>
60
+ </div>
61
+ <code class="bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 px-2 py-1 rounded text-sm"><%= @config.retention_period.inspect %></code>
62
+ </div>
63
+ <div class="flex justify-between items-center">
64
+ <div>
65
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100">Job Retry Attempts</p>
66
+ <p class="text-xs text-gray-500 dark:text-gray-400">Retries for async logging job</p>
67
+ </div>
68
+ <code class="bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 px-2 py-1 rounded text-sm"><%= @config.job_retry_attempts %></code>
69
+ </div>
70
+ </div>
71
+ </div>
72
+
73
+ <!-- Caching -->
74
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
75
+ <h3 class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-4">Caching</h3>
76
+ <div class="space-y-4">
77
+ <div class="flex justify-between items-center">
78
+ <div>
79
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100">Cache Store</p>
80
+ <p class="text-xs text-gray-500 dark:text-gray-400">Used for response caching</p>
81
+ </div>
82
+ <code class="bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 px-2 py-1 rounded text-sm"><%= @config.cache_store.class.name %></code>
83
+ </div>
84
+ </div>
85
+ </div>
86
+
87
+ <!-- Anomaly Detection -->
88
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
89
+ <h3 class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-4">Anomaly Detection</h3>
90
+ <div class="space-y-4">
91
+ <div class="flex justify-between items-center">
92
+ <div>
93
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100">Cost Threshold</p>
94
+ <p class="text-xs text-gray-500 dark:text-gray-400">Executions above this trigger warnings</p>
95
+ </div>
96
+ <code class="bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 px-2 py-1 rounded text-sm">$<%= @config.anomaly_cost_threshold %></code>
97
+ </div>
98
+ <div class="flex justify-between items-center">
99
+ <div>
100
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100">Duration Threshold</p>
101
+ <p class="text-xs text-gray-500 dark:text-gray-400">Executions above this trigger warnings</p>
102
+ </div>
103
+ <code class="bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 px-2 py-1 rounded text-sm"><%= number_with_delimiter(@config.anomaly_duration_threshold) %>ms</code>
104
+ </div>
105
+ </div>
106
+ </div>
107
+
108
+ <!-- Dashboard Settings -->
109
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
110
+ <h3 class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-4">Dashboard Settings</h3>
111
+ <div class="space-y-4">
112
+ <div class="flex justify-between items-center">
113
+ <div>
114
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100">Records Per Page</p>
115
+ <p class="text-xs text-gray-500 dark:text-gray-400">Pagination limit for listings</p>
116
+ </div>
117
+ <code class="bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 px-2 py-1 rounded text-sm"><%= @config.per_page %></code>
118
+ </div>
119
+ <div class="flex justify-between items-center">
120
+ <div>
121
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100">Recent Executions Limit</p>
122
+ <p class="text-xs text-gray-500 dark:text-gray-400">Shown on dashboard home</p>
123
+ </div>
124
+ <code class="bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 px-2 py-1 rounded text-sm"><%= @config.recent_executions_limit %></code>
125
+ </div>
126
+ <div class="flex justify-between items-center">
127
+ <div>
128
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100">Parent Controller</p>
129
+ <p class="text-xs text-gray-500 dark:text-gray-400">Dashboard inherits from this</p>
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>
132
+ </div>
133
+ <div class="flex justify-between items-center">
134
+ <div>
135
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100">HTTP Basic Auth</p>
136
+ <p class="text-xs text-gray-500 dark:text-gray-400">Username/password protection</p>
137
+ </div>
138
+ <%= render_configured_badge(@config.basic_auth_username.present? && @config.basic_auth_password.present?) %>
139
+ </div>
140
+ <div class="flex justify-between items-center">
141
+ <div>
142
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100">Custom Auth</p>
143
+ <p class="text-xs text-gray-500 dark:text-gray-400">Lambda-based authentication</p>
144
+ </div>
145
+ <%= render_configured_badge(@config.dashboard_auth.present?) %>
146
+ </div>
147
+ </div>
148
+ </div>
149
+
150
+ <!-- Reliability Defaults -->
151
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
152
+ <h3 class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-4">Reliability Defaults</h3>
153
+ <% retries = @config.default_retries || {} %>
154
+ <% has_retries = (retries[:max] || 0) > 0 %>
155
+ <% has_fallbacks = @config.default_fallback_models.present? && @config.default_fallback_models.any? %>
156
+ <% has_total_timeout = @config.default_total_timeout.present? %>
157
+
158
+ <div class="space-y-4">
159
+ <div>
160
+ <div class="flex justify-between items-center">
161
+ <div>
162
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100">Default Retries</p>
163
+ <p class="text-xs text-gray-500 dark:text-gray-400">Applied to agents without retry config</p>
164
+ </div>
165
+ <%= render_enabled_badge(has_retries) %>
166
+ </div>
167
+ <% if has_retries %>
168
+ <p class="text-xs text-gray-600 dark:text-gray-400 mt-2 ml-4">
169
+ Max: <%= retries[:max] %> &middot;
170
+ Backoff: <%= retries[:backoff] %> &middot;
171
+ Base: <%= retries[:base] %>s &middot;
172
+ Max delay: <%= retries[:max_delay] %>s
173
+ </p>
174
+ <% end %>
175
+ </div>
176
+
177
+ <div>
178
+ <div class="flex justify-between items-center">
179
+ <div>
180
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100">Default Fallback Models</p>
181
+ <p class="text-xs text-gray-500 dark:text-gray-400">Tried when primary model fails</p>
182
+ </div>
183
+ <%= render_enabled_badge(has_fallbacks) %>
184
+ </div>
185
+ <% if has_fallbacks %>
186
+ <p class="text-xs text-gray-600 dark:text-gray-400 mt-2 ml-4">
187
+ <%= @config.default_fallback_models.join(" → ") %>
188
+ </p>
189
+ <% end %>
190
+ </div>
191
+
192
+ <div class="flex justify-between items-center">
193
+ <div>
194
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100">Default Total Timeout</p>
195
+ <p class="text-xs text-gray-500 dark:text-gray-400">Across all retry/fallback attempts</p>
196
+ </div>
197
+ <% if has_total_timeout %>
198
+ <code class="bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 px-2 py-1 rounded text-sm"><%= @config.default_total_timeout %>s</code>
199
+ <% else %>
200
+ <span class="text-xs text-gray-400 dark:text-gray-500">Not configured</span>
201
+ <% end %>
202
+ </div>
203
+ </div>
204
+ </div>
205
+
206
+ <!-- Governance - Budgets -->
207
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
208
+ <h3 class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-4">Governance - Budgets</h3>
209
+ <% budgets = @config.budgets || {} %>
210
+ <% budgets_enabled = @config.budgets_enabled? %>
211
+
212
+ <div class="space-y-4">
213
+ <div class="flex justify-between items-center">
214
+ <div>
215
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100">Budget Enforcement</p>
216
+ <p class="text-xs text-gray-500 dark:text-gray-400">Cost limits for agents</p>
217
+ </div>
218
+ <% if budgets_enabled %>
219
+ <span class="inline-flex items-center px-2 py-1 rounded text-xs font-medium <%= @config.budget_enforcement == :hard ? 'bg-red-100 dark:bg-red-900/50 text-red-700 dark:text-red-300' : 'bg-yellow-100 dark:bg-yellow-900/50 text-yellow-700 dark:text-yellow-300' %>">
220
+ <%= @config.budget_enforcement.to_s.titleize %>
221
+ </span>
222
+ <% else %>
223
+ <span class="text-xs text-gray-400 dark:text-gray-500">Disabled</span>
224
+ <% end %>
225
+ </div>
226
+
227
+ <% if budgets_enabled %>
228
+ <% if budgets[:global_daily] %>
229
+ <div class="flex justify-between items-center ml-4">
230
+ <p class="text-sm text-gray-600 dark:text-gray-400">Global Daily Limit</p>
231
+ <code class="bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 px-2 py-1 rounded text-sm">$<%= budgets[:global_daily] %></code>
232
+ </div>
233
+ <% end %>
234
+ <% if budgets[:global_monthly] %>
235
+ <div class="flex justify-between items-center ml-4">
236
+ <p class="text-sm text-gray-600 dark:text-gray-400">Global Monthly Limit</p>
237
+ <code class="bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 px-2 py-1 rounded text-sm">$<%= budgets[:global_monthly] %></code>
238
+ </div>
239
+ <% end %>
240
+ <% if budgets[:per_agent_daily].present? %>
241
+ <div class="ml-4">
242
+ <p class="text-sm text-gray-600 dark:text-gray-400 mb-1">Per-Agent Daily Limits</p>
243
+ <% budgets[:per_agent_daily].each do |agent, limit| %>
244
+ <div class="flex justify-between items-center ml-4 text-xs">
245
+ <span class="text-gray-500 dark:text-gray-400"><%= agent %></span>
246
+ <code class="bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 px-1 rounded">$<%= limit %></code>
247
+ </div>
248
+ <% end %>
249
+ </div>
250
+ <% end %>
251
+ <% if budgets[:per_agent_monthly].present? %>
252
+ <div class="ml-4">
253
+ <p class="text-sm text-gray-600 dark:text-gray-400 mb-1">Per-Agent Monthly Limits</p>
254
+ <% budgets[:per_agent_monthly].each do |agent, limit| %>
255
+ <div class="flex justify-between items-center ml-4 text-xs">
256
+ <span class="text-gray-500 dark:text-gray-400"><%= agent %></span>
257
+ <code class="bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 px-1 rounded">$<%= limit %></code>
258
+ </div>
259
+ <% end %>
260
+ </div>
261
+ <% end %>
262
+ <% end %>
263
+ </div>
264
+ </div>
265
+
266
+ <!-- Governance - Alerts -->
267
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
268
+ <h3 class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-4">Governance - Alerts</h3>
269
+ <% alerts = @config.alerts || {} %>
270
+ <% alerts_enabled = @config.alerts_enabled? %>
271
+
272
+ <div class="space-y-4">
273
+ <div class="flex justify-between items-center">
274
+ <div>
275
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100">Alerts</p>
276
+ <p class="text-xs text-gray-500 dark:text-gray-400">Notifications for important events</p>
277
+ </div>
278
+ <%= render_enabled_badge(alerts_enabled) %>
279
+ </div>
280
+
281
+ <% if alerts_enabled %>
282
+ <div class="flex justify-between items-center ml-4">
283
+ <p class="text-sm text-gray-600 dark:text-gray-400">Slack Webhook</p>
284
+ <%= render_configured_badge(alerts[:slack_webhook_url].present?) %>
285
+ </div>
286
+ <div class="flex justify-between items-center ml-4">
287
+ <p class="text-sm text-gray-600 dark:text-gray-400">Generic Webhook</p>
288
+ <%= render_configured_badge(alerts[:webhook_url].present?) %>
289
+ </div>
290
+ <div class="flex justify-between items-center ml-4">
291
+ <p class="text-sm text-gray-600 dark:text-gray-400">Custom Handler</p>
292
+ <%= render_configured_badge(alerts[:custom].present?) %>
293
+ </div>
294
+ <% if @config.alert_events.any? %>
295
+ <div class="ml-4">
296
+ <p class="text-sm text-gray-600 dark:text-gray-400 mb-2">Events</p>
297
+ <div class="flex flex-wrap gap-1">
298
+ <% @config.alert_events.each do |event| %>
299
+ <span class="inline-flex items-center px-2 py-0.5 rounded text-xs bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300">
300
+ <%= event %>
301
+ </span>
302
+ <% end %>
303
+ </div>
304
+ </div>
305
+ <% end %>
306
+ <% end %>
307
+ </div>
308
+ </div>
309
+
310
+ <!-- Data Handling -->
311
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
312
+ <h3 class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-4">Data Handling</h3>
313
+ <div class="space-y-4">
314
+ <div class="flex justify-between items-center">
315
+ <div>
316
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100">Persist Prompts</p>
317
+ <p class="text-xs text-gray-500 dark:text-gray-400">Store system/user prompts in executions</p>
318
+ </div>
319
+ <%= render_enabled_badge(@config.persist_prompts) %>
320
+ </div>
321
+ <div class="flex justify-between items-center">
322
+ <div>
323
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100">Persist Responses</p>
324
+ <p class="text-xs text-gray-500 dark:text-gray-400">Store LLM responses in executions</p>
325
+ </div>
326
+ <%= render_enabled_badge(@config.persist_responses) %>
327
+ </div>
328
+
329
+ <% redaction = @config.redaction || {} %>
330
+ <% redaction_enabled = redaction.present? %>
331
+ <div>
332
+ <div class="flex justify-between items-center">
333
+ <div>
334
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100">PII Redaction</p>
335
+ <p class="text-xs text-gray-500 dark:text-gray-400">Sanitize sensitive data before storing</p>
336
+ </div>
337
+ <%= render_enabled_badge(redaction_enabled) %>
338
+ </div>
339
+ <% if redaction_enabled %>
340
+ <div class="mt-2 ml-4 text-xs text-gray-600 dark:text-gray-400 space-y-1">
341
+ <p>Fields: <%= @config.redaction_fields.count %> patterns</p>
342
+ <p>Regex patterns: <%= @config.redaction_patterns.count %></p>
343
+ <p>Placeholder: <code class="bg-gray-100 dark:bg-gray-700 px-1 rounded"><%= @config.redaction_placeholder %></code></p>
344
+ <% if @config.redaction_max_value_length %>
345
+ <p>Max value length: <%= number_with_delimiter(@config.redaction_max_value_length) %> chars</p>
346
+ <% end %>
347
+ </div>
348
+ <% end %>
349
+ </div>
350
+ </div>
351
+ </div>
352
+ </div>
353
+
354
+ <!-- Info Box -->
355
+ <div class="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4 border border-blue-200 dark:border-blue-800">
356
+ <div class="flex">
357
+ <svg class="w-5 h-5 text-blue-400 mr-3 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
358
+ <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
359
+ </svg>
360
+ <div>
361
+ <p class="text-sm text-blue-700 dark:text-blue-300">
362
+ These settings are configured in your Rails initializer and cannot be changed at runtime.
363
+ To modify, edit <code class="bg-blue-100 dark:bg-blue-800 px-1 rounded">config/initializers/ruby_llm_agents.rb</code> and restart your application.
364
+ </p>
365
+ </div>
366
+ </div>
367
+ </div>
368
+ </div>
369
+
@@ -0,0 +1,121 @@
1
+ <%
2
+ # Multi-select filter dropdown with Alpine.js
3
+ # Usage:
4
+ # render "rubyllm/agents/shared/filter_dropdown",
5
+ # name: "statuses[]",
6
+ # filter_id: "statuses",
7
+ # label: "Status",
8
+ # all_label: "All Statuses",
9
+ # options: [
10
+ # { value: "success", label: "Success", color: "bg-green-500" },
11
+ # { value: "error", label: "Error", color: "bg-red-500" }
12
+ # ],
13
+ # selected: ["success"],
14
+ # icon: "M9.75 17L9 20l-1...", # optional SVG path
15
+ # width: "w-48", # optional, default w-48
16
+ # full_width: false # optional, for mobile
17
+
18
+ selected = local_assigns[:selected] || []
19
+ width = local_assigns[:width] || "w-48"
20
+ full_width = local_assigns[:full_width] || false
21
+ show_all = local_assigns.fetch(:show_all_option, true)
22
+ all_label = local_assigns[:all_label] || "All"
23
+
24
+ # Determine current label
25
+ current_label = if selected.empty?
26
+ label
27
+ elsif selected.length == 1
28
+ opt = options.find { |o| o[:value].to_s == selected.first.to_s }
29
+ opt ? opt[:label] : selected.first
30
+ else
31
+ "#{selected.length} #{label}"
32
+ end
33
+
34
+ # Get color for single selection (if applicable)
35
+ current_color = if selected.length == 1
36
+ opt = options.find { |o| o[:value].to_s == selected.first.to_s }
37
+ opt&.dig(:color)
38
+ end
39
+
40
+ has_selection = selected.any?
41
+ %>
42
+ <div class="relative" x-data="{ open: false }" @click.outside="open = false" data-filter="<%= filter_id %>">
43
+ <%# Trigger button %>
44
+ <button type="button" @click="open = !open"
45
+ class="<%= full_width ? 'w-full md:w-auto justify-between md:justify-start' : '' %> flex items-center gap-2 px-3 py-2 text-sm
46
+ bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg
47
+ hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors
48
+ <%= has_selection ? 'ring-2 ring-blue-500 dark:ring-offset-gray-900' : '' %>">
49
+ <div class="flex items-center gap-2">
50
+ <% if current_color.present? %>
51
+ <span class="w-2 h-2 rounded-full <%= current_color %> <%= current_color.include?('blue') && selected.first == 'running' ? 'animate-pulse' : '' %>"></span>
52
+ <% elsif local_assigns[:icon].present? %>
53
+ <svg class="w-4 h-4 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
54
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="<%= icon %>"/>
55
+ </svg>
56
+ <% elsif !has_selection && options.first&.dig(:color) %>
57
+ <span class="w-2 h-2 rounded-full bg-gray-400"></span>
58
+ <% end %>
59
+ <span class="text-gray-700 dark:text-gray-200"><%= current_label %></span>
60
+ </div>
61
+ <svg class="w-4 h-4 text-gray-400 dark:text-gray-500 flex-shrink-0 transition-transform" :class="{ 'rotate-180': open }" fill="none" stroke="currentColor" viewBox="0 0 24 24">
62
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
63
+ </svg>
64
+ </button>
65
+
66
+ <%# Dropdown menu %>
67
+ <div x-show="open" x-cloak x-transition:enter="transition ease-out duration-100"
68
+ x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100"
69
+ x-transition:leave="transition ease-in duration-75"
70
+ x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95"
71
+ class="absolute z-20 mt-1 <%= full_width ? 'left-0 right-0 md:left-auto md:right-auto md:min-w-max' : 'min-w-max' %>
72
+ bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-1
73
+ max-h-64 overflow-y-auto">
74
+
75
+ <% if show_all %>
76
+ <div class="px-3 py-2 border-b border-gray-100 dark:border-gray-700">
77
+ <label class="flex items-center gap-2 cursor-pointer">
78
+ <input type="checkbox"
79
+ class="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500 dark:bg-gray-700"
80
+ <%= selected.empty? ? 'checked' : '' %>
81
+ @change="
82
+ $el.closest('[data-filter]').querySelectorAll('.filter-cb').forEach(c => c.checked = false);
83
+ $el.closest('form').requestSubmit();
84
+ ">
85
+ <span class="text-sm font-medium text-gray-700 dark:text-gray-200"><%= all_label %></span>
86
+ </label>
87
+ </div>
88
+ <% end %>
89
+
90
+ <% options.each do |option| %>
91
+ <div class="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200
92
+ hover:bg-gray-50 dark:hover:bg-gray-700">
93
+ <%# Checkbox: toggle this item, dropdown stays open %>
94
+ <input type="checkbox"
95
+ name="<%= name %>"
96
+ value="<%= option[:value] %>"
97
+ class="filter-cb rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500 dark:bg-gray-700"
98
+ <%= selected.include?(option[:value].to_s) ? 'checked' : '' %>
99
+ @change="$el.closest('form').requestSubmit()">
100
+
101
+ <%# Label: click to select ONLY this item AND close dropdown %>
102
+ <span class="flex-1 flex items-center gap-2 cursor-pointer"
103
+ @click.prevent="
104
+ $el.closest('[data-filter]').querySelectorAll('.filter-cb').forEach(c => c.checked = false);
105
+ $el.previousElementSibling.checked = true;
106
+ open = false;
107
+ $el.closest('form').requestSubmit();
108
+ ">
109
+ <% if option[:color].present? %>
110
+ <span class="w-2 h-2 rounded-full <%= option[:color] %> <%= option[:color].include?('blue') && option[:value] == 'running' ? 'animate-pulse' : '' %>"></span>
111
+ <% elsif option[:icon].present? %>
112
+ <svg class="w-4 h-4 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
113
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="<%= option[:icon] %>"/>
114
+ </svg>
115
+ <% end %>
116
+ <%= option[:label] %>
117
+ </span>
118
+ </div>
119
+ <% end %>
120
+ </div>
121
+ </div>
@@ -0,0 +1,85 @@
1
+ <%
2
+ # Single-select filter dropdown with Alpine.js
3
+ # Usage:
4
+ # render "rubyllm/agents/shared/select_dropdown",
5
+ # name: "days",
6
+ # filter_id: "days",
7
+ # options: [
8
+ # { value: "", label: "All Time" },
9
+ # { value: "1", label: "Today" },
10
+ # { value: "7", label: "Last 7 Days" },
11
+ # { value: "30", label: "Last 30 Days" }
12
+ # ],
13
+ # selected: "7",
14
+ # icon: "M8 7V3m8 4V3...", # optional SVG path
15
+ # width: "w-40", # optional, default w-40
16
+ # full_width: false # optional, for mobile
17
+
18
+ selected = local_assigns[:selected].to_s
19
+ width = local_assigns[:width] || "w-40"
20
+ full_width = local_assigns[:full_width] || false
21
+
22
+ # Find current option
23
+ current_option = options.find { |o| o[:value].to_s == selected } || options.first
24
+ current_label = current_option[:label]
25
+
26
+ has_selection = selected.present?
27
+ %>
28
+ <div class="relative" x-data="{ open: false }" @click.outside="open = false" data-filter="<%= filter_id %>">
29
+ <%# Trigger button %>
30
+ <button type="button" @click="open = !open"
31
+ class="<%= full_width ? 'w-full md:w-auto justify-between md:justify-start' : '' %> flex items-center gap-2 px-3 py-2 text-sm
32
+ bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg
33
+ hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors
34
+ <%= has_selection ? 'ring-2 ring-blue-500 dark:ring-offset-gray-900' : '' %>">
35
+ <div class="flex items-center gap-2">
36
+ <% if local_assigns[:icon].present? %>
37
+ <svg class="w-4 h-4 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
38
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="<%= icon %>"/>
39
+ </svg>
40
+ <% end %>
41
+ <span class="text-gray-700 dark:text-gray-200"><%= current_label %></span>
42
+ </div>
43
+ <svg class="w-4 h-4 text-gray-400 dark:text-gray-500 flex-shrink-0 transition-transform" :class="{ 'rotate-180': open }" fill="none" stroke="currentColor" viewBox="0 0 24 24">
44
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
45
+ </svg>
46
+ </button>
47
+
48
+ <%# Dropdown menu %>
49
+ <div x-show="open" x-cloak x-transition:enter="transition ease-out duration-100"
50
+ x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100"
51
+ x-transition:leave="transition ease-in duration-75"
52
+ x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95"
53
+ class="absolute z-20 mt-1 <%= full_width ? 'left-0 right-0 md:left-auto md:right-auto md:min-w-max' : 'min-w-max' %>
54
+ bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-1">
55
+ <% options.each do |option| %>
56
+ <%
57
+ is_selected = option[:value].to_s == selected
58
+ item_classes = is_selected ?
59
+ "bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300" :
60
+ "text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700"
61
+ %>
62
+ <button type="button"
63
+ @click="
64
+ document.getElementById('filter_<%= filter_id %>').value = '<%= option[:value] %>';
65
+ open = false;
66
+ $el.closest('form').requestSubmit();
67
+ "
68
+ class="w-full flex items-center gap-2 px-3 py-2 text-sm text-left <%= item_classes %>">
69
+ <% if option[:icon].present? %>
70
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
71
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="<%= option[:icon] %>"/>
72
+ </svg>
73
+ <% end %>
74
+ <%= option[:label] %>
75
+ <% if is_selected %>
76
+ <svg class="w-4 h-4 ml-auto text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
77
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
78
+ </svg>
79
+ <% end %>
80
+ </button>
81
+ <% end %>
82
+ </div>
83
+
84
+ <input type="hidden" name="<%= name %>" value="<%= selected %>" id="filter_<%= filter_id %>">
85
+ </div>
data/config/routes.rb CHANGED
@@ -2,12 +2,19 @@
2
2
 
3
3
  RubyLLM::Agents::Engine.routes.draw do
4
4
  root to: "dashboard#index"
5
+ get "chart_data", to: "dashboard#chart_data"
5
6
 
6
7
  resources :agents, only: [:index, :show]
7
8
 
8
9
  resources :executions, only: [:index, :show] do
9
10
  collection do
10
11
  get :search
12
+ get :export
13
+ end
14
+ member do
15
+ post :rerun
11
16
  end
12
17
  end
18
+
19
+ resource :settings, only: [:show]
13
20
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Migration to add retry/fallback attempt tracking to executions
4
+ #
5
+ # This migration adds columns for storing attempt details when using
6
+ # reliability features (retries, fallbacks, circuit breakers).
7
+ #
8
+ # Run with: rails db:migrate
9
+ class AddAttemptsToRubyLLMAgentsExecutions < ActiveRecord::Migration<%= migration_version %>
10
+ def change
11
+ # Add attempts JSONB array for storing per-attempt details
12
+ add_column :ruby_llm_agents_executions, :attempts, :jsonb, null: false, default: []
13
+
14
+ # Add counter for quick access to attempt count
15
+ add_column :ruby_llm_agents_executions, :attempts_count, :integer, null: false, default: 0
16
+
17
+ # Add chosen model (the model that successfully completed the request)
18
+ add_column :ruby_llm_agents_executions, :chosen_model_id, :string
19
+
20
+ # Add fallback chain (list of models that were configured to try)
21
+ add_column :ruby_llm_agents_executions, :fallback_chain, :jsonb, null: false, default: []
22
+
23
+ # Add indexes for common queries
24
+ add_index :ruby_llm_agents_executions, :attempts_count
25
+ add_index :ruby_llm_agents_executions, :chosen_model_id
26
+ end
27
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Migration to add caching tracking columns to executions
4
+ #
5
+ # These columns help track cache effectiveness and cost savings
6
+ # from cached responses.
7
+ #
8
+ # Run with: rails db:migrate
9
+ class AddCachingToRubyLLMAgentsExecutions < ActiveRecord::Migration<%= migration_version %>
10
+ def change
11
+ # Cache hit tracking
12
+ add_column :ruby_llm_agents_executions, :cache_hit, :boolean, default: false
13
+
14
+ # Cache key for debugging cache misses
15
+ add_column :ruby_llm_agents_executions, :response_cache_key, :string
16
+
17
+ # When was this response originally cached
18
+ add_column :ruby_llm_agents_executions, :cached_at, :datetime
19
+
20
+ # Index for cache analytics
21
+ add_index :ruby_llm_agents_executions, :response_cache_key
22
+ end
23
+ end