ruby_llm-agents 1.3.4 → 2.1.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 (191) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +112 -336
  3. data/app/controllers/concerns/ruby_llm/agents/sortable.rb +0 -1
  4. data/app/controllers/ruby_llm/agents/agents_controller.rb +5 -56
  5. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +22 -106
  6. data/app/controllers/ruby_llm/agents/executions_controller.rb +4 -114
  7. data/app/controllers/ruby_llm/agents/tenants_controller.rb +30 -2
  8. data/app/helpers/ruby_llm/agents/application_helper.rb +19 -53
  9. data/app/models/ruby_llm/agents/execution/analytics.rb +13 -54
  10. data/app/models/ruby_llm/agents/execution/scopes.rb +61 -14
  11. data/app/models/ruby_llm/agents/execution.rb +52 -12
  12. data/app/models/ruby_llm/agents/execution_detail.rb +18 -0
  13. data/app/models/ruby_llm/agents/tenant/budgetable.rb +132 -24
  14. data/app/models/ruby_llm/agents/tenant/incrementable.rb +117 -0
  15. data/app/models/ruby_llm/agents/tenant/resettable.rb +128 -0
  16. data/app/models/ruby_llm/agents/tenant/trackable.rb +46 -12
  17. data/app/models/ruby_llm/agents/tenant.rb +2 -3
  18. data/app/models/ruby_llm/agents/tenant_budget.rb +6 -3
  19. data/app/services/ruby_llm/agents/agent_registry.rb +6 -112
  20. data/app/views/layouts/ruby_llm/agents/application.html.erb +89 -252
  21. data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +71 -218
  22. data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +20 -63
  23. data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +44 -131
  24. data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +16 -57
  25. data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +39 -104
  26. data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +29 -82
  27. data/app/views/ruby_llm/agents/agents/_empty_state.html.erb +4 -14
  28. data/app/views/ruby_llm/agents/agents/index.html.erb +105 -274
  29. data/app/views/ruby_llm/agents/agents/show.html.erb +248 -378
  30. data/app/views/ruby_llm/agents/dashboard/_action_center.html.erb +29 -52
  31. data/app/views/ruby_llm/agents/dashboard/_tenant_budget.html.erb +73 -99
  32. data/app/views/ruby_llm/agents/dashboard/index.html.erb +228 -433
  33. data/app/views/ruby_llm/agents/executions/_execution.html.erb +1 -1
  34. data/app/views/ruby_llm/agents/executions/_filters.html.erb +4 -25
  35. data/app/views/ruby_llm/agents/executions/_list.html.erb +111 -152
  36. data/app/views/ruby_llm/agents/executions/index.html.erb +5 -7
  37. data/app/views/ruby_llm/agents/executions/show.html.erb +526 -1037
  38. data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +5 -21
  39. data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +70 -191
  40. data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +16 -44
  41. data/app/views/ruby_llm/agents/shared/_select_dropdown.html.erb +12 -41
  42. data/app/views/ruby_llm/agents/shared/_status_badge.html.erb +11 -65
  43. data/app/views/ruby_llm/agents/shared/_tenant_filter.html.erb +6 -5
  44. data/app/views/ruby_llm/agents/system_config/show.html.erb +240 -351
  45. data/app/views/ruby_llm/agents/tenants/_form.html.erb +67 -77
  46. data/app/views/ruby_llm/agents/tenants/edit.html.erb +7 -9
  47. data/app/views/ruby_llm/agents/tenants/index.html.erb +100 -122
  48. data/app/views/ruby_llm/agents/tenants/show.html.erb +146 -336
  49. data/config/routes.rb +0 -13
  50. data/lib/generators/ruby_llm_agents/install_generator.rb +13 -17
  51. data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +2 -12
  52. data/lib/generators/ruby_llm_agents/restructure_generator.rb +0 -2
  53. data/lib/generators/ruby_llm_agents/templates/add_usage_counters_to_tenants_migration.rb.tt +37 -0
  54. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +1 -2
  55. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +1 -1
  56. data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +0 -1
  57. data/lib/generators/ruby_llm_agents/templates/create_execution_details_migration.rb.tt +27 -0
  58. data/lib/generators/ruby_llm_agents/templates/create_tenants_migration.rb.tt +25 -0
  59. data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +0 -1
  60. data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +33 -12
  61. data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +40 -71
  62. data/lib/generators/ruby_llm_agents/templates/remove_agent_version_migration.rb.tt +13 -0
  63. data/lib/generators/ruby_llm_agents/templates/remove_workflow_columns_migration.rb.tt +19 -0
  64. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +2 -4
  65. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +0 -1
  66. data/lib/generators/ruby_llm_agents/templates/split_execution_details_migration.rb.tt +232 -0
  67. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +77 -259
  68. data/lib/ruby_llm/agents/audio/speaker.rb +0 -1
  69. data/lib/ruby_llm/agents/audio/transcriber.rb +0 -1
  70. data/lib/ruby_llm/agents/base_agent.rb +54 -23
  71. data/lib/ruby_llm/agents/core/base/callbacks.rb +142 -0
  72. data/lib/ruby_llm/agents/core/base.rb +23 -55
  73. data/lib/ruby_llm/agents/core/configuration.rb +97 -117
  74. data/lib/ruby_llm/agents/core/errors.rb +0 -58
  75. data/lib/ruby_llm/agents/core/instrumentation.rb +157 -110
  76. data/lib/ruby_llm/agents/core/llm_tenant.rb +8 -7
  77. data/lib/ruby_llm/agents/core/version.rb +1 -1
  78. data/lib/ruby_llm/agents/dsl/base.rb +157 -17
  79. data/lib/ruby_llm/agents/dsl/caching.rb +33 -2
  80. data/lib/ruby_llm/agents/dsl/reliability.rb +148 -0
  81. data/lib/ruby_llm/agents/dsl.rb +1 -2
  82. data/lib/ruby_llm/agents/image/analyzer/execution.rb +1 -2
  83. data/lib/ruby_llm/agents/image/background_remover/execution.rb +1 -2
  84. data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +1 -13
  85. data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +2 -2
  86. data/lib/ruby_llm/agents/image/editor/dsl.rb +0 -14
  87. data/lib/ruby_llm/agents/image/editor/execution.rb +1 -10
  88. data/lib/ruby_llm/agents/image/editor.rb +0 -1
  89. data/lib/ruby_llm/agents/image/generator.rb +0 -21
  90. data/lib/ruby_llm/agents/image/pipeline/dsl.rb +0 -13
  91. data/lib/ruby_llm/agents/image/pipeline/execution.rb +0 -1
  92. data/lib/ruby_llm/agents/image/transformer/dsl.rb +0 -13
  93. data/lib/ruby_llm/agents/image/transformer/execution.rb +1 -10
  94. data/lib/ruby_llm/agents/image/transformer.rb +0 -1
  95. data/lib/ruby_llm/agents/image/upscaler/execution.rb +1 -2
  96. data/lib/ruby_llm/agents/image/variator/execution.rb +1 -2
  97. data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +78 -173
  98. data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +66 -2
  99. data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +0 -12
  100. data/lib/ruby_llm/agents/infrastructure/circuit_breaker.rb +10 -13
  101. data/lib/ruby_llm/agents/infrastructure/execution_logger_job.rb +8 -0
  102. data/lib/ruby_llm/agents/pipeline/context.rb +0 -1
  103. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +28 -4
  104. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +3 -10
  105. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +88 -55
  106. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +5 -41
  107. data/lib/ruby_llm/agents/rails/engine.rb +6 -6
  108. data/lib/ruby_llm/agents/results/base.rb +1 -49
  109. data/lib/ruby_llm/agents/text/embedder.rb +0 -1
  110. data/lib/ruby_llm/agents.rb +1 -9
  111. data/lib/tasks/ruby_llm_agents.rake +34 -0
  112. metadata +14 -83
  113. data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +0 -214
  114. data/app/controllers/ruby_llm/agents/workflows_controller.rb +0 -544
  115. data/app/mailers/ruby_llm/agents/alert_mailer.rb +0 -84
  116. data/app/mailers/ruby_llm/agents/application_mailer.rb +0 -28
  117. data/app/models/ruby_llm/agents/api_configuration.rb +0 -386
  118. data/app/models/ruby_llm/agents/execution/workflow.rb +0 -170
  119. data/app/models/ruby_llm/agents/tenant/configurable.rb +0 -135
  120. data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -98
  121. data/app/views/ruby_llm/agents/agents/_version_comparison.html.erb +0 -186
  122. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +0 -126
  123. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +0 -107
  124. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +0 -18
  125. data/app/views/ruby_llm/agents/api_configurations/_api_key_field.html.erb +0 -34
  126. data/app/views/ruby_llm/agents/api_configurations/_form.html.erb +0 -288
  127. data/app/views/ruby_llm/agents/api_configurations/edit.html.erb +0 -95
  128. data/app/views/ruby_llm/agents/api_configurations/edit_tenant.html.erb +0 -97
  129. data/app/views/ruby_llm/agents/api_configurations/show.html.erb +0 -214
  130. data/app/views/ruby_llm/agents/api_configurations/tenant.html.erb +0 -179
  131. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +0 -73
  132. data/app/views/ruby_llm/agents/dashboard/_alerts_feed.html.erb +0 -62
  133. data/app/views/ruby_llm/agents/dashboard/_breaker_strip.html.erb +0 -47
  134. data/app/views/ruby_llm/agents/dashboard/_budgets_bar.html.erb +0 -75
  135. data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +0 -56
  136. data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +0 -115
  137. data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +0 -59
  138. data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +0 -60
  139. data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +0 -86
  140. data/app/views/ruby_llm/agents/executions/dry_run.html.erb +0 -149
  141. data/app/views/ruby_llm/agents/shared/_breadcrumbs.html.erb +0 -48
  142. data/app/views/ruby_llm/agents/shared/_nav_link.html.erb +0 -27
  143. data/app/views/ruby_llm/agents/shared/_stat_card.html.erb +0 -14
  144. data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +0 -35
  145. data/app/views/ruby_llm/agents/workflows/_empty_state.html.erb +0 -22
  146. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +0 -228
  147. data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +0 -539
  148. data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +0 -76
  149. data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +0 -74
  150. data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +0 -108
  151. data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +0 -920
  152. data/app/views/ruby_llm/agents/workflows/index.html.erb +0 -179
  153. data/app/views/ruby_llm/agents/workflows/show.html.erb +0 -467
  154. data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +0 -100
  155. data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +0 -38
  156. data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +0 -48
  157. data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +0 -90
  158. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +0 -551
  159. data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +0 -181
  160. data/lib/ruby_llm/agents/core/base/moderation_execution.rb +0 -274
  161. data/lib/ruby_llm/agents/core/resolved_config.rb +0 -348
  162. data/lib/ruby_llm/agents/image/generator/content_policy.rb +0 -95
  163. data/lib/ruby_llm/agents/infrastructure/redactor.rb +0 -130
  164. data/lib/ruby_llm/agents/results/moderation_result.rb +0 -158
  165. data/lib/ruby_llm/agents/text/moderator.rb +0 -237
  166. data/lib/ruby_llm/agents/workflow/approval.rb +0 -205
  167. data/lib/ruby_llm/agents/workflow/approval_store.rb +0 -179
  168. data/lib/ruby_llm/agents/workflow/async.rb +0 -220
  169. data/lib/ruby_llm/agents/workflow/async_executor.rb +0 -156
  170. data/lib/ruby_llm/agents/workflow/dsl/executor.rb +0 -467
  171. data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +0 -244
  172. data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +0 -289
  173. data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +0 -107
  174. data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +0 -150
  175. data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +0 -187
  176. data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +0 -352
  177. data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +0 -415
  178. data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +0 -257
  179. data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +0 -317
  180. data/lib/ruby_llm/agents/workflow/dsl.rb +0 -576
  181. data/lib/ruby_llm/agents/workflow/instrumentation.rb +0 -249
  182. data/lib/ruby_llm/agents/workflow/notifiers/base.rb +0 -117
  183. data/lib/ruby_llm/agents/workflow/notifiers/email.rb +0 -117
  184. data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +0 -180
  185. data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +0 -121
  186. data/lib/ruby_llm/agents/workflow/notifiers.rb +0 -70
  187. data/lib/ruby_llm/agents/workflow/orchestrator.rb +0 -416
  188. data/lib/ruby_llm/agents/workflow/result.rb +0 -592
  189. data/lib/ruby_llm/agents/workflow/thread_pool.rb +0 -185
  190. data/lib/ruby_llm/agents/workflow/throttle_manager.rb +0 -206
  191. data/lib/ruby_llm/agents/workflow/wait_result.rb +0 -213
@@ -1,421 +1,291 @@
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(tab: params[:tab], subtab: params[:subtab]) },
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
- <%= render "ruby_llm/agents/shared/doc_link" %>
16
-
17
- <% if @agent_active %>
18
- <span
19
- class="
20
- inline-flex items-center px-2.5 py-0.5 rounded-full text-xs
21
- font-medium bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-300
22
- "
23
- >
24
- Active
25
- </span>
26
- <% else %>
27
- <span
28
- class="
29
- inline-flex items-center px-2.5 py-0.5 rounded-full text-xs
30
- font-medium bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400
31
- "
32
- >
33
- Deleted
34
- </span>
35
- <% end %>
36
-
1
+ <!-- Back link -->
2
+ <nav class="font-mono text-xs mb-6">
3
+ <%= link_to "← agents", ruby_llm_agents.agents_path(subtab: params[:subtab]), class: "text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300" %>
4
+ </nav>
5
+
6
+ <!-- ── agent header ──────────────── -->
7
+ <div class="flex items-start justify-between gap-8 mb-2">
8
+ <!-- Left: identity -->
9
+ <div class="min-w-0">
10
+ <div class="flex items-center gap-3 mb-1.5">
11
+ <span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono"><%= @agent_type.gsub(/Agent$/, '') %></span>
12
+ <div class="font-mono text-xs text-gray-400 dark:text-gray-500 flex items-center gap-1.5">
13
+ <span class="badge badge-sm <%= @agent_active ? 'badge-success' : 'badge-error' %>"><%= @agent_active ? 'active' : 'inactive' %></span>
37
14
  <% if @config %>
38
- <span class="text-sm text-gray-500 dark:text-gray-400">
39
- v<%= @config[:version] %>
40
- </span>
41
- <% end %>
42
- </div>
43
-
44
- <% if @config %>
45
- <p class="text-gray-500 dark:text-gray-400 mt-1">
46
- <%= @config[:model] %>
15
+ <span class="text-gray-800 dark:text-gray-200"><%= @config[:model] %></span>
47
16
  <% if @config[:temperature] %>
48
- &middot; temp <%= @config[:temperature] %>
17
+ <span class="text-gray-300 dark:text-gray-700">&middot;</span>
18
+ <span>temp <%= @config[:temperature] %></span>
49
19
  <% end %>
50
20
  <% if @config[:timeout] %>
51
- &middot; timeout <%= @config[:timeout] %>s
21
+ <span class="text-gray-300 dark:text-gray-700">&middot;</span>
22
+ <span><%= @config[:timeout] %>s</span>
23
+ <% end %>
24
+ <% if @config[:cache_enabled] %>
25
+ <span class="text-gray-300 dark:text-gray-700">&middot;</span>
26
+ <%
27
+ ttl = @config[:cache_ttl]
28
+ cache_label = if ttl.is_a?(ActiveSupport::Duration)
29
+ secs = ttl.to_i
30
+ if secs >= 3600 then "#{secs / 3600}h"
31
+ elsif secs >= 60 then "#{secs / 60}m"
32
+ else "#{secs}s"
33
+ end
34
+ else
35
+ ttl.to_s
36
+ end
37
+ %>
38
+ <span>cache <%= cache_label %></span>
52
39
  <% end %>
53
40
  <% if @config[:dimensions] %>
54
- &middot; <%= @config[:dimensions] %> dims
41
+ <span class="text-gray-300 dark:text-gray-700">&middot;</span>
42
+ <span><%= @config[:dimensions] %> dims</span>
55
43
  <% end %>
56
44
  <% if @config[:voice] %>
57
- &middot; voice: <%= @config[:voice] %>
45
+ <span class="text-gray-300 dark:text-gray-700">&middot;</span>
46
+ <span>voice: <%= @config[:voice] %></span>
47
+ <% end %>
48
+ <% if @streaming_rate && @streaming_rate > 0 %>
49
+ <span class="text-gray-300 dark:text-gray-700">&middot;</span>
50
+ <span>streaming</span>
58
51
  <% end %>
59
- </p>
60
- <% if @config[:description].present? %>
61
- <p class="text-gray-600 dark:text-gray-300 mt-2">
62
- <%= @config[:description] %>
63
- </p>
64
- <% end %>
65
- <% end %>
66
- </div>
67
-
68
- <div class="text-right">
69
- <p class="text-sm text-gray-500 dark:text-gray-400">
70
- <%= number_with_delimiter(@stats[:count]) %> total executions
71
- </p>
72
-
73
- <div class="flex items-center justify-end gap-3 mt-1">
74
- <% status_colors = {
75
- "success" => "bg-green-500",
76
- "error" => "bg-red-500",
77
- "timeout" => "bg-yellow-500",
78
- "running" => "bg-blue-500"
79
- } %>
80
-
81
- <% @status_distribution.each do |status, count| %>
82
- <div class="flex items-center gap-1">
83
- <span class="w-2 h-2 rounded-full <%= status_colors[status] || 'bg-gray-400' %> <%= status == 'running' ? 'animate-pulse' : '' %>"></span>
84
-
85
- <span class="text-xs text-gray-600 dark:text-gray-400">
86
- <%= number_with_delimiter(count) %>
87
- </span>
88
- </div>
89
52
  <% end %>
90
53
  </div>
54
+ <%= render "ruby_llm/agents/shared/doc_link" %>
91
55
  </div>
56
+ <% if @config && @config[:description].present? %>
57
+ <p class="font-mono text-xs text-gray-500 dark:text-gray-400"><%= @config[:description] %></p>
58
+ <% end %>
92
59
  </div>
93
- </div>
94
-
95
- <!-- Circuit Breaker Status (only for base agents with reliability enabled) -->
96
- <% if @config && @agent_type_kind == "agent" && @circuit_breaker_status.present? %>
97
- <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4 mb-6">
98
- <div class="flex items-center justify-between mb-3">
99
- <h3
100
- class="
101
- text-sm font-medium text-gray-700 dark:text-gray-300 uppercase
102
- tracking-wider
103
- "
104
- >
105
- Circuit Breaker Status
106
- </h3>
107
60
 
108
- <span class="text-xs text-gray-400 dark:text-gray-500">
109
- Auto-refreshes every 30s
110
- </span>
61
+ <!-- Right: stats -->
62
+ <div class="flex-shrink-0 text-right font-mono text-xs text-gray-400 dark:text-gray-500 space-y-1">
63
+ <% success_rate = @stats[:success_rate] || 0 %>
64
+ <div class="flex items-center justify-end gap-x-4">
65
+ <span><span class="text-gray-800 dark:text-gray-200"><%= number_with_delimiter(@stats[:count]) %></span> runs</span>
66
+ <span class="<%= success_rate >= 95 ? 'text-green-500' : success_rate >= 80 ? 'text-yellow-500' : 'text-red-500' %>"><%= success_rate %>% ok</span>
67
+ <span><span class="text-gray-800 dark:text-gray-200">$<%= number_with_precision(@stats[:total_cost] || 0, precision: 4) %></span> cost</span>
111
68
  </div>
112
-
113
- <div class="flex flex-wrap gap-3">
114
- <% @circuit_breaker_status.each do |model_id, status| %>
115
- <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' %>">
116
- <% if status[:open] %>
117
- <!-- Open/Tripped indicator -->
118
- <span class="relative flex h-3 w-3">
119
- <span
120
- class="
121
- animate-ping absolute inline-flex h-full w-full
122
- rounded-full bg-red-400 opacity-75
123
- "
124
- ></span>
125
-
126
- <span class="relative inline-flex rounded-full h-3 w-3 bg-red-500"></span>
127
- </span>
128
- <span class="text-sm font-medium text-red-700 dark:text-red-300">
129
- <%= model_id %>
130
- </span>
131
- <span class="text-xs text-red-500 dark:text-red-400 ml-1">
132
- OPEN
133
- </span>
134
- <% if status[:cooldown_remaining] %>
135
- <span class="text-xs text-red-400 dark:text-red-500 ml-1">
136
- (resets in <%= status[:cooldown_remaining] %>s)
137
- </span>
138
- <% end %>
139
- <% else %>
140
- <!-- Closed/Healthy indicator -->
141
- <span class="inline-flex rounded-full h-3 w-3 bg-green-500"></span>
142
- <span class="text-sm font-medium text-green-700 dark:text-green-300">
143
- <%= model_id %>
144
- </span>
145
- <span class="text-xs text-green-500 dark:text-green-400 ml-1">
146
- OK
147
- </span>
148
- <% if status[:failure_count] && status[:failure_count] > 0 %>
149
- <span class="text-xs text-yellow-500 dark:text-yellow-400 ml-1">
150
- (<%= status[:failure_count] %>/<%= status[:threshold] %>
151
- failures)
152
- </span>
153
- <% end %>
154
- <% end %>
155
- </div>
69
+ <div class="flex items-center justify-end gap-x-4 text-gray-500 dark:text-gray-600">
70
+ <span><span class="text-gray-600 dark:text-gray-400"><%= number_with_delimiter(@stats[:total_tokens] || 0) %></span> tokens</span>
71
+ <span><span class="text-gray-600 dark:text-gray-400"><%= number_with_delimiter(@stats[:avg_duration_ms] || 0) %></span>ms avg</span>
72
+ <% if (@config && @config[:cache_enabled]) || @cache_hit_rate > 0 %>
73
+ <span><span class="text-gray-600 dark:text-gray-400"><%= @cache_hit_rate %>%</span> cache</span>
156
74
  <% end %>
157
75
  </div>
158
-
159
- <% if @circuit_breaker_status.values.any? { |s| s[:open] } %>
160
- <p class="mt-3 text-xs text-gray-500 dark:text-gray-400">
161
- Open circuit breakers will skip the model and try fallbacks (if
162
- configured). They automatically reset after the cooldown period.
163
- </p>
76
+ <% if @stats_today[:count] > 0 %>
77
+ <% today_success_rate = @stats_today[:success_rate] || 0 %>
78
+ <div class="flex items-center justify-end gap-x-4 pt-1 border-t border-gray-200 dark:border-gray-800">
79
+ <span class="text-gray-300 dark:text-gray-700">today</span>
80
+ <span><span class="text-gray-800 dark:text-gray-200"><%= number_with_delimiter(@stats_today[:count]) %></span> runs</span>
81
+ <span class="<%= today_success_rate >= 95 ? 'text-green-500' : today_success_rate >= 80 ? 'text-yellow-500' : 'text-red-500' %>"><%= today_success_rate %>% ok</span>
82
+ <span><span class="text-gray-800 dark:text-gray-200">$<%= number_with_precision(@stats_today[:total_cost] || 0, precision: 4) %></span> cost</span>
83
+ <span><span class="text-gray-800 dark:text-gray-200"><%= number_with_delimiter(@stats_today[:avg_duration_ms] || 0) %></span>ms</span>
84
+ </div>
164
85
  <% end %>
165
86
  </div>
166
- <% end %>
167
-
168
- <!-- Stats Grid -->
169
- <% success_rate = @stats[:success_rate] || 0 %>
170
- <% success_rate_color = success_rate >= 95 ? 'text-green-600' : success_rate >= 80 ? 'text-yellow-600' : 'text-red-600' %>
171
-
172
- <div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4 mb-6">
173
- <%= render "ruby_llm/agents/shared/stat_card",
174
- title: "Executions",
175
- value: number_with_delimiter(@stats[:count]),
176
- subtitle: "Today: #{@stats_today[:count]}",
177
- icon: "M13 10V3L4 14h7v7l9-11h-7z",
178
- icon_color: "text-blue-500" %>
179
-
180
- <%= render "ruby_llm/agents/shared/stat_card",
181
- title: "Success Rate",
182
- value: "#{success_rate}%",
183
- subtitle: "Error rate: #{@stats[:error_rate] || 0}%",
184
- icon: "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z",
185
- icon_color: "text-green-500",
186
- value_color: success_rate_color %>
187
-
188
- <%= render "ruby_llm/agents/shared/stat_card",
189
- title: "Total Cost",
190
- value: "$#{number_with_precision(@stats[:total_cost] || 0, precision: 4)}",
191
- subtitle: "Avg: $#{number_with_precision(@stats[:avg_cost] || 0, precision: 6)}",
192
- 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",
193
- icon_color: "text-amber-500" %>
194
-
195
- <%= render "ruby_llm/agents/shared/stat_card",
196
- title: "Total Tokens",
197
- value: number_with_delimiter(@stats[:total_tokens] || 0),
198
- subtitle: "Avg: #{number_with_delimiter(@stats[:avg_tokens] || 0)}",
199
- icon: "M7 20l4-16m2 16l4-16M6 9h14M4 15h14",
200
- icon_color: "text-indigo-500" %>
201
-
202
- <%= render "ruby_llm/agents/shared/stat_card",
203
- title: "Avg Duration",
204
- value: "#{number_with_delimiter(@stats[:avg_duration_ms] || 0)} ms",
205
- icon: "M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z",
206
- icon_color: "text-purple-500" %>
207
-
208
- <%= render "ruby_llm/agents/shared/stat_card",
209
- title: "Cache Hit Rate",
210
- value: "#{@cache_hit_rate}%",
211
- 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",
212
- icon_color: "text-purple-500" %>
213
87
  </div>
88
+ <div class="border-t border-gray-200 dark:border-gray-800 mb-2"></div>
214
89
 
215
- <!-- Charts Section -->
216
- <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
217
- <!-- Executions Over Time -->
218
- <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 overflow-hidden">
219
- <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
220
- Executions (30 days)
221
- </h3>
222
- <div id="executions-chart" style="height: 220px;"></div>
223
- </div>
224
-
225
- <!-- Cost Over Time -->
226
- <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 overflow-hidden">
227
- <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
228
- Cost (30 days)
229
- </h3>
230
- <div id="cost-chart" style="height: 220px;"></div>
231
- </div>
90
+ <!-- 30d trend chart -->
91
+ <div class="flex items-center gap-3 mt-6 mb-2">
92
+ <span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">last 30 days</span>
93
+ <div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
232
94
  </div>
233
-
95
+ <div id="agent-trend-chart" style="width: 100%; height: 160px;"></div>
234
96
  <script>
235
97
  (function() {
236
- const trendData = <%= raw @trend_data.to_json %>;
237
- const categories = trendData.map(d => d.date.split('-').slice(1).join('/'));
238
-
239
- // Executions chart
240
- Highcharts.chart('executions-chart', {
241
- chart: { type: 'areaspline', backgroundColor: 'transparent' },
242
- xAxis: { categories: categories, labels: { style: { color: '#9CA3AF' } } },
243
- yAxis: { min: 0, title: { text: null }, labels: { style: { color: '#9CA3AF' } } },
98
+ const trendData = <%= raw @trend_data.map { |d|
99
+ { date: d[:date] || d["date"], count: d[:count] || d["count"] || 0,
100
+ cost: (d[:total_cost] || d["total_cost"] || 0).to_f.round(6),
101
+ errors: d[:error_count] || d["error_count"] || 0 }
102
+ }.to_json %>;
103
+
104
+ const successData = trendData.map(d => [new Date(d.date).getTime(), d.count - d.errors]);
105
+ const errorData = trendData.map(d => [new Date(d.date).getTime(), d.errors]);
106
+
107
+ Highcharts.chart('agent-trend-chart', {
108
+ chart: { type: 'areaspline', backgroundColor: 'transparent', spacing: [5, 0, 0, 0] },
109
+ title: { text: null },
110
+ xAxis: {
111
+ type: 'datetime',
112
+ labels: { style: { color: '#6B7280', fontSize: '9px', fontFamily: 'ui-monospace, monospace' }, format: '{value:%b %d}' },
113
+ lineColor: 'transparent',
114
+ tickLength: 0,
115
+ gridLineWidth: 0
116
+ },
117
+ yAxis: {
118
+ title: { text: null },
119
+ min: 0,
120
+ allowDecimals: false,
121
+ labels: { style: { color: '#6B7280', fontSize: '9px', fontFamily: 'ui-monospace, monospace' } },
122
+ gridLineColor: 'rgba(107, 114, 128, 0.08)'
123
+ },
244
124
  legend: { enabled: false },
245
- plotOptions: { areaspline: { fillOpacity: 0.2, marker: { enabled: false } } },
125
+ credits: { enabled: false },
126
+ tooltip: {
127
+ backgroundColor: 'rgba(0, 0, 0, 0.85)',
128
+ borderColor: 'transparent',
129
+ borderRadius: 3,
130
+ style: { color: '#E5E7EB', fontSize: '10px', fontFamily: 'ui-monospace, monospace' },
131
+ shared: true,
132
+ formatter: function() {
133
+ let html = '<span style="color:#9CA3AF">' + Highcharts.dateFormat('%b %d', this.x) + '</span>';
134
+ let cost = trendData.find(d => new Date(d.date).getTime() === this.x);
135
+ this.points.forEach(p => html += '<br/>' + p.series.name + ': <b>' + p.y + '</b>');
136
+ if (cost) html += '<br/>cost: <b>$' + cost.cost.toFixed(4) + '</b>';
137
+ return html;
138
+ }
139
+ },
140
+ plotOptions: {
141
+ areaspline: {
142
+ stacking: 'normal',
143
+ lineWidth: 1.5,
144
+ marker: { enabled: false, states: { hover: { enabled: true, radius: 2 } } }
145
+ }
146
+ },
246
147
  series: [
247
- { name: 'Success', color: '#10B981', data: trendData.map(d => d.count - (d.error_count || 0)) },
248
- { name: 'Failed', color: '#EF4444', data: trendData.map(d => d.error_count || 0) }
148
+ {
149
+ name: 'errors',
150
+ data: errorData,
151
+ color: '#EF4444',
152
+ fillColor: { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, stops: [[0, 'rgba(239, 68, 68, 0.08)'], [1, 'rgba(239, 68, 68, 0)']] }
153
+ },
154
+ {
155
+ name: 'success',
156
+ data: successData,
157
+ color: '#10B981',
158
+ fillColor: { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, stops: [[0, 'rgba(16, 185, 129, 0.08)'], [1, 'rgba(16, 185, 129, 0)']] }
159
+ }
249
160
  ]
250
161
  });
251
-
252
- // Cost chart
253
- Highcharts.chart('cost-chart', {
254
- chart: { type: 'spline', backgroundColor: 'transparent' },
255
- xAxis: { categories: categories, labels: { style: { color: '#9CA3AF' } } },
256
- yAxis: { min: 0, title: { text: null }, labels: { style: { color: '#9CA3AF' }, format: '${value}' } },
257
- legend: { enabled: false },
258
- plotOptions: { spline: { marker: { enabled: false } } },
259
- tooltip: { valuePrefix: '$' },
260
- series: [{ name: 'Cost', color: '#10B981', data: trendData.map(d => parseFloat(d.total_cost) || 0) }]
261
- });
262
162
  })();
263
163
  </script>
264
164
 
265
- <!-- Finish Reason Distribution -->
266
- <% if @finish_reason_distribution.present? && @finish_reason_distribution.any? %>
267
- <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4 mb-6">
268
- <div class="flex items-center justify-between">
269
- <p class="text-sm text-gray-500 dark:text-gray-400 uppercase">
270
- Finish Reasons
271
- </p>
272
-
273
- <div class="flex flex-wrap gap-4">
274
- <% finish_colors = {
275
- 'stop' => '#10B981',
276
- 'length' => '#F59E0B',
277
- 'content_filter' => '#EF4444',
278
- 'tool_calls' => '#3B82F6',
279
- nil => '#6B7280'
280
- } %>
281
-
282
- <% @finish_reason_distribution.each do |reason, count| %>
283
- <div class="flex items-center">
284
- <span
285
- class="w-2 h-2 rounded-full mr-1.5"
286
- style="background-color: <%= finish_colors[reason] || '#6B7280' %>"
287
- ></span>
288
-
289
- <span class="text-sm text-gray-700 dark:text-gray-300">
290
- <%= reason || 'unknown' %>
291
- </span>
165
+ <!-- ── tools ──────────────────────── -->
166
+ <% if @config && @agent_type_kind == "agent" %>
167
+ <%
168
+ class_tools = @agent_class.respond_to?(:tools) ? (@agent_class.tools || []) : []
169
+ has_dynamic_tools = @agent_class.instance_methods(false).include?(:tools)
170
+ %>
171
+ <% if class_tools.any? || has_dynamic_tools %>
172
+ <div class="flex items-center gap-3 mt-6 mb-3">
173
+ <span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">tools (<%= class_tools.size %>)</span>
174
+ <div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
175
+ </div>
176
+ <div class="flex flex-wrap gap-1.5 font-mono text-xs">
177
+ <% class_tools.each do |tool_class| %>
178
+ <span class="badge badge-cyan"><%= tool_class.respond_to?(:tool_name) ? tool_class.tool_name : tool_class.name.demodulize %></span>
179
+ <% end %>
180
+ <% if has_dynamic_tools %>
181
+ <span class="badge badge-purple">dynamic</span>
182
+ <% end %>
183
+ </div>
184
+ <% end %>
292
185
 
293
- <span class="text-sm font-medium text-gray-900 dark:text-gray-100 ml-1">
294
- (<%= number_with_delimiter(count) %>)
295
- </span>
296
- </div>
297
- <% end %>
298
- </div>
186
+ <% if @config[:params].present? && @config[:params].any? %>
187
+ <div class="mt-3">
188
+ <span class="text-[10px] text-gray-400 dark:text-gray-600 uppercase tracking-wider font-mono">parameters</span>
299
189
  </div>
300
- </div>
190
+ <div class="grid grid-cols-2 sm:grid-cols-3 gap-x-6 gap-y-0.5 font-mono text-xs mt-1.5">
191
+ <% @config[:params].each do |name, opts| %>
192
+ <div class="flex items-center gap-2 py-0.5">
193
+ <span class="text-gray-900 dark:text-gray-200"><%= name %></span>
194
+ <% if opts[:required] %>
195
+ <span class="text-red-500">required</span>
196
+ <% elsif opts[:default].present? %>
197
+ <span class="text-gray-400 dark:text-gray-600">= <%= opts[:default].inspect %></span>
198
+ <% else %>
199
+ <span class="text-gray-400 dark:text-gray-600">optional</span>
200
+ <% end %>
201
+ </div>
202
+ <% end %>
203
+ </div>
204
+ <% end %>
301
205
  <% end %>
302
206
 
303
- <!-- Version Comparison -->
304
- <%= render partial: "ruby_llm/agents/agents/version_comparison",
305
- locals: { versions: @versions, version_comparison: @version_comparison } %>
207
+ <!-- ── model health ──────────────── -->
208
+ <%
209
+ if @config && @agent_type_kind == "agent"
210
+ retries_config = @config[:retries] || {}
211
+ fallback_models = Array(@config[:fallback_models]).compact
212
+ has_retries = (retries_config[:max] || 0) > 0
213
+ has_fallbacks = fallback_models.any?
214
+ has_circuit_breaker = @config[:circuit_breaker].present?
215
+ has_any_reliability = has_retries || has_fallbacks || has_circuit_breaker
216
+ show_model_health = has_any_reliability || @circuit_breaker_status.present?
217
+ end
218
+ %>
219
+ <% if show_model_health %>
220
+ <div class="flex items-center gap-3 mt-6 mb-3">
221
+ <span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">model health</span>
222
+ <div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
223
+ </div>
224
+ <% if has_any_reliability %>
225
+ <div class="font-mono text-xs text-gray-400 dark:text-gray-500 mb-2 flex flex-wrap gap-x-1.5 gap-y-0.5">
226
+ <% if has_retries %>
227
+ <span>retries: <%= retries_config[:max] %> max</span>
228
+ <% end %>
229
+ <% if has_retries && (has_fallbacks || has_circuit_breaker) %>
230
+ <span class="text-gray-300 dark:text-gray-700">&middot;</span>
231
+ <% end %>
232
+ <% if has_fallbacks %>
233
+ <span>fallbacks: <%= fallback_models.join(" → ") %></span>
234
+ <% end %>
235
+ <% if has_fallbacks && has_circuit_breaker %>
236
+ <span class="text-gray-300 dark:text-gray-700">&middot;</span>
237
+ <% end %>
238
+ <% if has_circuit_breaker %>
239
+ <% cb = @config[:circuit_breaker] %>
240
+ <span>pauses after <%= cb[:errors] %> errors in <%= cb[:within] %>s</span>
241
+ <% end %>
242
+ </div>
243
+ <% end %>
244
+ <% if @circuit_breaker_status.present? %>
245
+ <div class="font-mono text-xs">
246
+ <% @circuit_breaker_status.each do |model_id, status| %>
247
+ <div class="flex items-center gap-3 py-1 px-2 -mx-2">
248
+ <span class="w-1.5 h-1.5 rounded-full flex-shrink-0 <%= status[:open] ? 'bg-red-500 animate-pulse' : 'bg-green-500' %>"></span>
249
+ <span class="text-gray-900 dark:text-gray-200"><%= model_id %></span>
250
+ <% if status[:open] %>
251
+ <span class="text-red-500">paused</span>
252
+ <% if status[:cooldown_remaining] %>
253
+ <span class="text-gray-400 dark:text-gray-600">retrying in <%= status[:cooldown_remaining] %>s</span>
254
+ <% end %>
255
+ <% else %>
256
+ <span class="text-green-500">healthy</span>
257
+ <% if status[:failure_count] && status[:failure_count] > 0 %>
258
+ <span class="text-yellow-500"><%= status[:failure_count] %>/<%= status[:threshold] %> failures before pause</span>
259
+ <% end %>
260
+ <% end %>
261
+ </div>
262
+ <% end %>
263
+ </div>
264
+ <% end %>
265
+ <% end %>
306
266
 
307
- <% if @config && @agent_type_kind %>
267
+ <!-- ── config (non-agent types only) ── -->
268
+ <% if @config && @agent_type_kind && @agent_type_kind != "agent" %>
269
+ <div class="flex items-center gap-3 mt-6 mb-3">
270
+ <span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">config</span>
271
+ <div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
272
+ </div>
308
273
  <%= render "ruby_llm/agents/agents/config_#{@agent_type_kind}", config: @config %>
309
274
  <% end %>
310
275
 
311
- <!-- Executions -->
312
- <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
313
- <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
314
- Executions
315
- </h3>
316
-
317
- <div id="executions_table">
318
- <%
319
- has_filters = params[:statuses].present? || params[:versions].present? || params[:models].present? || params[:temperatures].present? || params[:days].present?
320
- selected_statuses = params[:statuses].present? ? (params[:statuses].is_a?(Array) ? params[:statuses] : params[:statuses].split(",")) : []
321
- selected_versions = params[:versions].present? ? (params[:versions].is_a?(Array) ? params[:versions] : params[:versions].split(",")) : []
322
- selected_models = params[:models].present? ? (params[:models].is_a?(Array) ? params[:models] : params[:models].split(",")) : []
323
- selected_temperatures = params[:temperatures].present? ? (params[:temperatures].is_a?(Array) ? params[:temperatures] : params[:temperatures].split(",")).map(&:to_s) : []
324
-
325
- status_options = [
326
- { value: "success", label: "Success", color: "bg-green-500" },
327
- { value: "error", label: "Error", color: "bg-red-500" },
328
- { value: "running", label: "Running", color: "bg-blue-500" },
329
- { value: "timeout", label: "Timeout", color: "bg-yellow-500" }
330
- ]
331
- version_options = @versions.map { |v| { value: v.to_s, label: "v#{v}" } }
332
- model_options = @models.map { |m| { value: m, label: m } }
333
- temperature_options = @temperatures.map { |t| { value: t.to_s, label: t.to_s } }
334
- days_options = [
335
- { value: "", label: "All Time" },
336
- { value: "1", label: "Today" },
337
- { value: "7", label: "Last 7 Days" },
338
- { value: "30", label: "Last 30 Days" }
339
- ]
340
- %>
341
-
342
- <%= form_with url: ruby_llm_agents.agent_path(@agent_type), method: :get, local: true do |f| %>
343
- <div class="flex flex-wrap items-center gap-3 mb-4 pb-4 border-b border-gray-100 dark:border-gray-700">
344
- <%# Status Filter (Multi-select) %>
345
- <%= render "ruby_llm/agents/shared/filter_dropdown",
346
- name: "statuses[]",
347
- filter_id: "statuses",
348
- label: "Status",
349
- all_label: "All Statuses",
350
- options: status_options,
351
- selected: selected_statuses %>
352
-
353
- <%# Version Filter (Multi-select) %>
354
- <% if @versions.any? %>
355
- <%= render "ruby_llm/agents/shared/filter_dropdown",
356
- name: "versions[]",
357
- filter_id: "versions",
358
- label: "Version",
359
- all_label: "All Versions",
360
- options: version_options,
361
- selected: selected_versions,
362
- 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" %>
363
- <% end %>
364
-
365
- <%# Model Filter (Multi-select) %>
366
- <% if @models.length > 1 %>
367
- <%= render "ruby_llm/agents/shared/filter_dropdown",
368
- name: "models[]",
369
- filter_id: "models",
370
- label: "Model",
371
- all_label: "All Models",
372
- options: model_options,
373
- selected: selected_models,
374
- 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" %>
375
- <% end %>
376
-
377
- <%# Temperature Filter (Multi-select) %>
378
- <% if @temperatures.length > 1 %>
379
- <%= render "ruby_llm/agents/shared/filter_dropdown",
380
- name: "temperatures[]",
381
- filter_id: "temperatures",
382
- label: "Temp",
383
- all_label: "All Temps",
384
- options: temperature_options,
385
- selected: selected_temperatures,
386
- 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" %>
387
- <% end %>
388
-
389
- <%# Time Range Filter (Single-select) %>
390
- <%= render "ruby_llm/agents/shared/select_dropdown",
391
- name: "days",
392
- filter_id: "days",
393
- options: days_options,
394
- selected: params[:days].to_s,
395
- 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" %>
276
+ <!-- ── executions ──────────────────── -->
277
+ <div class="flex items-center gap-3 mt-6 mb-3">
278
+ <span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">recent executions</span>
279
+ <div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
280
+ </div>
396
281
 
397
- <%# Clear Filters %>
398
- <% if has_filters %>
399
- <%= link_to ruby_llm_agents.agent_path(@agent_type),
400
- 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 %>
401
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
402
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
403
- </svg>
404
- Clear
405
- <% end %>
406
- <% end %>
282
+ <%= render "ruby_llm/agents/shared/executions_table", executions: @executions, pagination: @pagination, hide_agent_column: true %>
407
283
 
408
- <%# Stats Summary (right aligned) %>
409
- <div class="ml-auto flex items-center gap-4 text-sm text-gray-500 dark:text-gray-400">
410
- <span><%= number_with_delimiter(@filter_stats[:total_count]) %> executions</span>
411
- <span class="text-gray-300 dark:text-gray-600">|</span>
412
- <span>$<%= number_with_precision(@filter_stats[:total_cost] || 0, precision: 4) %></span>
413
- <span class="text-gray-300 dark:text-gray-600">|</span>
414
- <span><%= number_with_delimiter(@filter_stats[:total_tokens] || 0) %> tokens</span>
415
- </div>
416
- </div>
284
+ <% if @stats[:count] > 0 %>
285
+ <div class="mt-3 font-mono text-xs">
286
+ <%= link_to ruby_llm_agents.executions_path("agent_types[]": @agent_type),
287
+ class: "text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300" do %>
288
+ show all <%= number_with_delimiter(@stats[:count]) %> executions
417
289
  <% end %>
418
-
419
- <%= render "ruby_llm/agents/shared/executions_table", executions: @executions, pagination: @pagination %>
420
290
  </div>
421
- </div>
291
+ <% end %>