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,98 +0,0 @@
1
- <%= link_to ruby_llm_agents.agent_path(ERB::Util.url_encode(agent[:name]), tab: local_assigns[:current_tab], subtab: local_assigns[:current_subtab]), class: "block bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-md transition-shadow relative z-10", data: { turbo: false }, style: "pointer-events: auto;" do %>
2
- <div class="p-4 sm:p-5">
3
- <!-- Row 1: Agent name + badge + model/timestamp -->
4
- <div class="flex items-center justify-between gap-2">
5
- <div class="flex items-center gap-2 min-w-0">
6
- <h3 class="font-semibold text-gray-900 dark:text-gray-100 truncate">
7
- <% name_parts = agent[:name].gsub(/Agent$/, '').split('::') %>
8
- <% if name_parts.length > 1 %>
9
- <span class="text-gray-400 dark:text-gray-500 font-normal"><%= name_parts[0..-2].join('::') %>::</span>
10
- <% end %>
11
- <%= name_parts.last %>
12
- </h3>
13
- <span class="hidden sm:inline text-sm text-gray-500 dark:text-gray-400">v<%= agent[:version] %></span>
14
- <% if agent[:active] %>
15
- <span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-300">Active</span>
16
- <% else %>
17
- <span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400">Deleted</span>
18
- <% end %>
19
- </div>
20
- <!-- Desktop: model -->
21
- <span class="hidden sm:block text-sm text-gray-500 dark:text-gray-400"><%= agent[:model] %></span>
22
- <!-- Mobile: last executed -->
23
- <span class="sm:hidden text-xs text-gray-400 dark:text-gray-500 whitespace-nowrap">
24
- <% if agent[:last_executed] %>
25
- <%= time_ago_in_words(agent[:last_executed]) %> ago
26
- <% else %>
27
- Never
28
- <% end %>
29
- </span>
30
- </div>
31
-
32
- <!-- Description -->
33
- <% if agent[:description].present? %>
34
- <p class="mt-1 text-sm text-gray-500 dark:text-gray-400 line-clamp-2">
35
- <%= agent[:description] %>
36
- </p>
37
- <% end %>
38
-
39
- <!-- Row 2: Stats -->
40
- <div class="mt-2 sm:mt-3 sm:border-t sm:border-gray-100 sm:dark:border-gray-700 sm:pt-3">
41
- <!-- Mobile: compact inline -->
42
- <% success_rate = agent[:success_rate] || 0 %>
43
- <div class="sm:hidden text-xs text-gray-500 dark:text-gray-400">
44
- <%= number_with_delimiter(agent[:execution_count]) %> runs
45
- <span class="mx-1 text-gray-300 dark:text-gray-600">·</span>
46
- $<%= number_with_precision(agent[:total_cost] || 0, precision: 2) %>
47
- <span class="mx-1 text-gray-300 dark:text-gray-600">·</span>
48
- <span class="<%= success_rate >= 95 ? 'text-green-600 dark:text-green-400' : success_rate >= 80 ? 'text-yellow-600 dark:text-yellow-400' : 'text-red-600 dark:text-red-400' %>">
49
- <%= success_rate %>%
50
- </span>
51
- </div>
52
-
53
- <!-- Desktop: full stats with icons -->
54
- <div class="hidden sm:flex items-center justify-between text-sm">
55
- <div class="flex items-center space-x-6">
56
- <!-- Executions -->
57
- <div class="flex items-center text-gray-600 dark:text-gray-300">
58
- <svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
59
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
60
- </svg>
61
- <span><%= number_with_delimiter(agent[:execution_count]) %> executions</span>
62
- </div>
63
-
64
- <!-- Cost -->
65
- <div class="flex items-center text-gray-600 dark:text-gray-300">
66
- <svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
67
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="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" />
68
- </svg>
69
- <span>$<%= number_with_precision(agent[:total_cost] || 0, precision: 4) %></span>
70
- </div>
71
-
72
- <!-- Success Rate -->
73
- <div class="flex items-center">
74
- <% if success_rate >= 95 %>
75
- <span class="w-2 h-2 rounded-full bg-green-500 mr-1.5"></span>
76
- <span class="text-green-600 dark:text-green-400"><%= success_rate %>%</span>
77
- <% elsif success_rate >= 80 %>
78
- <span class="w-2 h-2 rounded-full bg-yellow-500 mr-1.5"></span>
79
- <span class="text-yellow-600 dark:text-yellow-400"><%= success_rate %>%</span>
80
- <% else %>
81
- <span class="w-2 h-2 rounded-full bg-red-500 mr-1.5"></span>
82
- <span class="text-red-600 dark:text-red-400"><%= success_rate %>%</span>
83
- <% end %>
84
- </div>
85
- </div>
86
-
87
- <!-- Last Executed -->
88
- <div class="text-gray-400 dark:text-gray-500">
89
- <% if agent[:last_executed] %>
90
- <%= time_ago_in_words(agent[:last_executed]) %> ago
91
- <% else %>
92
- Never executed
93
- <% end %>
94
- </div>
95
- </div>
96
- </div>
97
- </div>
98
- <% end %>
@@ -1,186 +0,0 @@
1
- <%# Collapsible version comparison UI - simplified for quick scanning %>
2
- <% if versions.size >= 2 %>
3
- <%
4
- v1 = version_comparison&.dig(:v1)
5
- v2 = version_comparison&.dig(:v2)
6
- data = version_comparison&.dig(:data) || {}
7
- v1_stats = data[:v1] || {}
8
- v2_stats = data[:v2] || {}
9
-
10
- # Calculate metrics with changes
11
- metrics = [
12
- { name: "Success Rate", v1: v1_stats[:success_rate] || 0, v2: v2_stats[:success_rate] || 0, format: :pct, better: :higher },
13
- { name: "Avg Cost", v1: v1_stats[:avg_cost] || 0, v2: v2_stats[:avg_cost] || 0, format: :cost, better: :lower },
14
- { name: "Avg Tokens", v1: v1_stats[:avg_tokens] || 0, v2: v2_stats[:avg_tokens] || 0, format: :num, better: :lower },
15
- { name: "Avg Duration", v1: v1_stats[:avg_duration_ms] || 0, v2: v2_stats[:avg_duration_ms] || 0, format: :ms, better: :lower },
16
- { name: "Executions", v1: v1_stats[:count] || 0, v2: v2_stats[:count] || 0, format: :num, better: :higher }
17
- ]
18
-
19
- # Count improvements/regressions for summary
20
- improvements = 0
21
- regressions = 0
22
- metrics.each do |m|
23
- next if m[:v1].zero? && m[:v2].zero?
24
- if m[:better] == :higher
25
- improvements += 1 if m[:v2] > m[:v1]
26
- regressions += 1 if m[:v2] < m[:v1]
27
- else
28
- improvements += 1 if m[:v2] < m[:v1]
29
- regressions += 1 if m[:v2] > m[:v1]
30
- end
31
- end
32
- %>
33
-
34
- <div class="bg-white dark:bg-gray-800 rounded-lg shadow mb-6">
35
- <!-- Collapsible Header -->
36
- <button
37
- type="button"
38
- onclick="toggleVersionComparison()"
39
- class="w-full px-6 py-4 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors rounded-lg"
40
- >
41
- <div class="flex items-center gap-3">
42
- <svg id="version-chevron" class="w-5 h-5 text-gray-400 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
43
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
44
- </svg>
45
- <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Version Comparison</h3>
46
- <span class="text-sm text-gray-500 dark:text-gray-400">v<%= v1 %> → v<%= v2 %></span>
47
- </div>
48
-
49
- <!-- Quick Summary Pills -->
50
- <div class="flex items-center gap-2">
51
- <% if improvements > 0 %>
52
- <span class="px-2 py-0.5 text-xs font-medium text-green-700 dark:text-green-300 bg-green-100 dark:bg-green-900/50 rounded-full">
53
- <%= improvements %> improved
54
- </span>
55
- <% end %>
56
- <% if regressions > 0 %>
57
- <span class="px-2 py-0.5 text-xs font-medium text-red-700 dark:text-red-300 bg-red-100 dark:bg-red-900/50 rounded-full">
58
- <%= regressions %> regressed
59
- </span>
60
- <% end %>
61
- <% if improvements == 0 && regressions == 0 %>
62
- <span class="px-2 py-0.5 text-xs font-medium text-gray-600 dark:text-gray-400 bg-gray-100 dark:bg-gray-700 rounded-full">
63
- No changes
64
- </span>
65
- <% end %>
66
- </div>
67
- </button>
68
-
69
- <!-- Collapsible Content -->
70
- <div id="version-comparison-content" class="hidden border-t border-gray-100 dark:border-gray-700">
71
- <div class="p-6">
72
- <!-- Version Selectors -->
73
- <div class="flex items-center gap-4 mb-4">
74
- <select id="compare-v1" onchange="updateVersionComparison()" class="flex-1 px-3 py-2 text-sm bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg dark:text-gray-200">
75
- <% versions.each do |ver| %>
76
- <option value="<%= ver %>" <%= ver == v1 ? 'selected' : '' %>>v<%= ver %></option>
77
- <% end %>
78
- </select>
79
- <span class="text-gray-400">→</span>
80
- <select id="compare-v2" onchange="updateVersionComparison()" class="flex-1 px-3 py-2 text-sm bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg dark:text-gray-200">
81
- <% versions.each do |ver| %>
82
- <option value="<%= ver %>" <%= ver == v2 ? 'selected' : '' %>>v<%= ver %></option>
83
- <% end %>
84
- </select>
85
- </div>
86
-
87
- <% if version_comparison && version_comparison[:data] %>
88
- <!-- Simple Comparison Table -->
89
- <table class="w-full text-sm">
90
- <thead>
91
- <tr class="text-xs text-gray-500 dark:text-gray-400 uppercase">
92
- <th class="text-left py-2">Metric</th>
93
- <th class="text-right py-2">v<%= v1 %></th>
94
- <th class="text-right py-2">v<%= v2 %></th>
95
- <th class="text-right py-2">Change</th>
96
- </tr>
97
- </thead>
98
- <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
99
- <% metrics.each do |m| %>
100
- <%
101
- # Format values
102
- case m[:format]
103
- when :pct
104
- f_v1 = "#{m[:v1].round(1)}%"
105
- f_v2 = "#{m[:v2].round(1)}%"
106
- diff = m[:v2] - m[:v1]
107
- change = "#{diff >= 0 ? '+' : ''}#{diff.round(1)}pp"
108
- when :cost
109
- f_v1 = "$#{number_with_precision(m[:v1], precision: 4)}"
110
- f_v2 = "$#{number_with_precision(m[:v2], precision: 4)}"
111
- pct = m[:v1] > 0 ? ((m[:v2] - m[:v1]) / m[:v1] * 100) : 0
112
- change = "#{pct >= 0 ? '+' : ''}#{pct.round(1)}%"
113
- when :ms
114
- f_v1 = "#{number_with_delimiter(m[:v1].round)}ms"
115
- f_v2 = "#{number_with_delimiter(m[:v2].round)}ms"
116
- pct = m[:v1] > 0 ? ((m[:v2] - m[:v1]) / m[:v1] * 100) : 0
117
- change = "#{pct >= 0 ? '+' : ''}#{pct.round(1)}%"
118
- else
119
- f_v1 = number_with_delimiter(m[:v1].round)
120
- f_v2 = number_with_delimiter(m[:v2].round)
121
- pct = m[:v1] > 0 ? ((m[:v2] - m[:v1]) / m[:v1] * 100) : 0
122
- change = "#{pct >= 0 ? '+' : ''}#{pct.round(1)}%"
123
- end
124
-
125
- # Determine if change is good/bad
126
- is_better = m[:better] == :higher ? m[:v2] > m[:v1] : m[:v2] < m[:v1]
127
- is_worse = m[:better] == :higher ? m[:v2] < m[:v1] : m[:v2] > m[:v1]
128
- change_class = is_better ? "text-green-600 dark:text-green-400" : is_worse ? "text-red-600 dark:text-red-400" : "text-gray-500"
129
- %>
130
- <tr>
131
- <td class="py-2 text-gray-700 dark:text-gray-300"><%= m[:name] %></td>
132
- <td class="py-2 text-right text-gray-900 dark:text-gray-100 font-medium"><%= f_v1 %></td>
133
- <td class="py-2 text-right text-gray-900 dark:text-gray-100 font-medium"><%= f_v2 %></td>
134
- <td class="py-2 text-right font-medium <%= change_class %>"><%= change %></td>
135
- </tr>
136
- <% end %>
137
- </tbody>
138
- </table>
139
-
140
- <p class="mt-4 text-xs text-gray-400 dark:text-gray-500 text-center">
141
- Based on this month's data
142
- </p>
143
- <% else %>
144
- <p class="text-sm text-gray-500 dark:text-gray-400 text-center py-4">
145
- No data available for selected versions
146
- </p>
147
- <% end %>
148
- </div>
149
- </div>
150
- </div>
151
-
152
- <script>
153
- function toggleVersionComparison() {
154
- const content = document.getElementById('version-comparison-content');
155
- const chevron = document.getElementById('version-chevron');
156
- content.classList.toggle('hidden');
157
- chevron.classList.toggle('-rotate-180');
158
- }
159
-
160
- function updateVersionComparison() {
161
- const v1 = document.getElementById('compare-v1').value;
162
- const v2 = document.getElementById('compare-v2').value;
163
- const url = new URL(window.location.href);
164
- url.searchParams.set('compare_v1', v1);
165
- url.searchParams.set('compare_v2', v2);
166
- window.location.href = url.toString();
167
- }
168
-
169
- // Restore collapsed state from localStorage
170
- document.addEventListener('DOMContentLoaded', function() {
171
- const expanded = localStorage.getItem('version_comparison_expanded');
172
- if (expanded === 'true') {
173
- document.getElementById('version-comparison-content').classList.remove('hidden');
174
- document.getElementById('version-chevron').classList.add('-rotate-180');
175
- }
176
- });
177
-
178
- // Save state when toggling
179
- const originalToggle = toggleVersionComparison;
180
- toggleVersionComparison = function() {
181
- originalToggle();
182
- const isExpanded = !document.getElementById('version-comparison-content').classList.contains('hidden');
183
- localStorage.setItem('version_comparison_expanded', isExpanded);
184
- };
185
- </script>
186
- <% end %>
@@ -1,126 +0,0 @@
1
- <%
2
- # All workflows use unified emerald color scheme
3
- colors = { border: "border-l-emerald-500", icon_bg: "bg-emerald-100 dark:bg-emerald-900/50", icon_text: "text-emerald-600 dark:text-emerald-300" }
4
- child_label = "steps"
5
- %>
6
-
7
- <div x-data="{ expanded: false }" class="block bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-md transition-shadow border-l-4 <%= colors[:border] %>">
8
- <%= link_to ruby_llm_agents.workflow_path(ERB::Util.url_encode(workflow[:name]), tab: local_assigns[:current_tab]), class: "block p-4 sm:p-5 relative z-10", data: { turbo: false }, style: "pointer-events: auto;" do %>
9
- <!-- Header Row -->
10
- <div class="flex items-center justify-between gap-2">
11
- <div class="flex items-center gap-2 min-w-0">
12
- <%= render "ruby_llm/agents/shared/workflow_type_badge", workflow_type: workflow[:workflow_type], size: :sm %>
13
- <h3 class="font-semibold text-gray-900 dark:text-gray-100 truncate">
14
- <% name_parts = workflow[:name].split('::') %>
15
- <% if name_parts.length > 1 %>
16
- <span class="text-gray-400 dark:text-gray-500 font-normal"><%= name_parts[0..-2].join('::') %>::</span>
17
- <% end %>
18
- <%= name_parts.last %>
19
- </h3>
20
- <span class="hidden sm:inline text-sm text-gray-500 dark:text-gray-400">v<%= workflow[:version] %></span>
21
- <% if workflow[:active] %>
22
- <span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-300">Active</span>
23
- <% else %>
24
- <span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400">Deleted</span>
25
- <% end %>
26
- </div>
27
- <span class="hidden sm:block text-sm text-gray-500 dark:text-gray-400">
28
- <%= workflow[:workflow_children]&.size || 0 %> <%= child_label %>
29
- </span>
30
- </div>
31
-
32
- <!-- Description -->
33
- <% if workflow[:description].present? %>
34
- <p class="mt-1 text-sm text-gray-500 dark:text-gray-400 line-clamp-2">
35
- <%= workflow[:description] %>
36
- </p>
37
- <% end %>
38
-
39
- <!-- Stats Row -->
40
- <div class="mt-2 sm:mt-3 sm:border-t sm:border-gray-100 sm:dark:border-gray-700 sm:pt-3">
41
- <% success_rate = workflow[:success_rate] || 0 %>
42
- <!-- Mobile: compact inline -->
43
- <div class="sm:hidden text-xs text-gray-500 dark:text-gray-400">
44
- <%= number_with_delimiter(workflow[:execution_count]) %> runs
45
- <span class="mx-1 text-gray-300 dark:text-gray-600">-</span>
46
- $<%= number_with_precision(workflow[:total_cost] || 0, precision: 2) %>
47
- <span class="mx-1 text-gray-300 dark:text-gray-600">-</span>
48
- <span class="<%= success_rate >= 95 ? 'text-green-600 dark:text-green-400' : success_rate >= 80 ? 'text-yellow-600 dark:text-yellow-400' : 'text-red-600 dark:text-red-400' %>">
49
- <%= success_rate %>%
50
- </span>
51
- </div>
52
-
53
- <!-- Desktop: full stats with icons -->
54
- <div class="hidden sm:flex items-center justify-between text-sm">
55
- <div class="flex items-center space-x-6">
56
- <div class="flex items-center text-gray-600 dark:text-gray-300">
57
- <svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
58
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
59
- </svg>
60
- <span><%= number_with_delimiter(workflow[:execution_count]) %> executions</span>
61
- </div>
62
- <div class="flex items-center text-gray-600 dark:text-gray-300">
63
- <svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
64
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="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" />
65
- </svg>
66
- <span>$<%= number_with_precision(workflow[:total_cost] || 0, precision: 4) %></span>
67
- </div>
68
- <div class="flex items-center">
69
- <% if success_rate >= 95 %>
70
- <span class="w-2 h-2 rounded-full bg-green-500 mr-1.5"></span>
71
- <span class="text-green-600 dark:text-green-400"><%= success_rate %>%</span>
72
- <% elsif success_rate >= 80 %>
73
- <span class="w-2 h-2 rounded-full bg-yellow-500 mr-1.5"></span>
74
- <span class="text-yellow-600 dark:text-yellow-400"><%= success_rate %>%</span>
75
- <% else %>
76
- <span class="w-2 h-2 rounded-full bg-red-500 mr-1.5"></span>
77
- <span class="text-red-600 dark:text-red-400"><%= success_rate %>%</span>
78
- <% end %>
79
- </div>
80
- </div>
81
- <div class="text-gray-400 dark:text-gray-500">
82
- <% if workflow[:last_executed] %>
83
- <%= time_ago_in_words(workflow[:last_executed]) %> ago
84
- <% else %>
85
- Never executed
86
- <% end %>
87
- </div>
88
- </div>
89
- </div>
90
- <% end %>
91
-
92
- <!-- Expandable Child Agents Section -->
93
- <% if workflow[:workflow_children].present? && workflow[:workflow_children].any? %>
94
- <div class="border-t border-gray-100 dark:border-gray-700 px-4 sm:px-5 py-2">
95
- <button type="button" @click.prevent="expanded = !expanded" class="w-full flex items-center justify-between text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300">
96
- <span class="font-medium">
97
- <%= child_label.capitalize %> (<%= workflow[:workflow_children].size %>)
98
- </span>
99
- <svg class="w-4 h-4 transition-transform duration-200" :class="{ 'rotate-180': expanded }" fill="none" stroke="currentColor" viewBox="0 0 24 24">
100
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
101
- </svg>
102
- </button>
103
-
104
- <div x-show="expanded" x-cloak x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 -translate-y-1" x-transition:enter-end="opacity-100 translate-y-0" class="mt-2 space-y-1">
105
- <% workflow[:workflow_children].each_with_index do |child, index| %>
106
- <div class="flex items-center gap-2 text-sm py-1.5 px-2 rounded bg-gray-50 dark:bg-gray-900/50">
107
- <span class="w-5 h-5 flex items-center justify-center rounded-full <%= colors[:icon_bg] %> text-xs <%= colors[:icon_text] %>">
108
- <%= index + 1 %>
109
- </span>
110
- <span class="font-medium text-gray-700 dark:text-gray-300"><%= child[:name] %></span>
111
- <span class="text-gray-400 dark:text-gray-500">-></span>
112
- <span class="text-gray-600 dark:text-gray-400 font-mono text-xs"><%= child[:agent] %></span>
113
- <% if child[:optional] %>
114
- <span class="text-xs text-gray-400 dark:text-gray-500 italic">(optional)</span>
115
- <% end %>
116
- <% if child[:description].present? %>
117
- <span class="text-xs text-gray-400 dark:text-gray-500 truncate max-w-xs" title="<%= child[:description] %>">
118
- - <%= child[:description].truncate(30) %>
119
- </span>
120
- <% end %>
121
- </div>
122
- <% end %>
123
- </div>
124
- </div>
125
- <% end %>
126
- </div>
@@ -1,107 +0,0 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title><%= @title %></title>
7
- <style>
8
- body {
9
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
10
- line-height: 1.6;
11
- color: #333;
12
- max-width: 600px;
13
- margin: 0 auto;
14
- padding: 20px;
15
- }
16
- .header {
17
- border-left: 4px solid <%= @color %>;
18
- padding-left: 16px;
19
- margin-bottom: 24px;
20
- }
21
- .title {
22
- font-size: 24px;
23
- font-weight: 600;
24
- margin: 0 0 8px 0;
25
- }
26
- .severity {
27
- display: inline-block;
28
- padding: 4px 12px;
29
- border-radius: 4px;
30
- font-size: 12px;
31
- font-weight: 600;
32
- text-transform: uppercase;
33
- color: white;
34
- background-color: <%= @color %>;
35
- }
36
- .timestamp {
37
- color: #666;
38
- font-size: 14px;
39
- margin-top: 8px;
40
- }
41
- .details {
42
- background-color: #f8f9fa;
43
- border-radius: 8px;
44
- padding: 16px;
45
- margin-top: 24px;
46
- }
47
- .details-title {
48
- font-size: 14px;
49
- font-weight: 600;
50
- text-transform: uppercase;
51
- color: #666;
52
- margin: 0 0 12px 0;
53
- }
54
- .detail-row {
55
- display: flex;
56
- padding: 8px 0;
57
- border-bottom: 1px solid #e9ecef;
58
- }
59
- .detail-row:last-child {
60
- border-bottom: none;
61
- }
62
- .detail-key {
63
- font-weight: 500;
64
- color: #495057;
65
- min-width: 140px;
66
- }
67
- .detail-value {
68
- color: #212529;
69
- word-break: break-word;
70
- }
71
- .footer {
72
- margin-top: 32px;
73
- padding-top: 16px;
74
- border-top: 1px solid #e9ecef;
75
- font-size: 12px;
76
- color: #666;
77
- }
78
- </style>
79
- </head>
80
- <body>
81
- <div class="header">
82
- <h1 class="title"><%= @title %></h1>
83
- <span class="severity"><%= @severity %></span>
84
- <div class="timestamp">
85
- <%= @timestamp.strftime("%B %d, %Y at %I:%M %p %Z") %>
86
- </div>
87
- </div>
88
-
89
- <% if @payload.present? %>
90
- <div class="details">
91
- <h2 class="details-title">Event Details</h2>
92
- <% @payload.except(:event).each do |key, value| %>
93
- <div class="detail-row">
94
- <div class="detail-key"><%= key.to_s.titleize %></div>
95
- <div class="detail-value"><%= value.is_a?(Hash) ? value.to_json : value %></div>
96
- </div>
97
- <% end %>
98
- </div>
99
- <% end %>
100
-
101
- <div class="footer">
102
- This alert was sent by RubyLLM::Agents.
103
- <br>
104
- Event type: <code><%= @event %></code>
105
- </div>
106
- </body>
107
- </html>
@@ -1,18 +0,0 @@
1
- RubyLLM::Agents Alert
2
- =====================
3
-
4
- <%= @title %>
5
- Severity: <%= @severity %>
6
- Time: <%= @timestamp.strftime("%B %d, %Y at %I:%M %p %Z") %>
7
-
8
- <% if @payload.present? %>
9
- Event Details
10
- -------------
11
- <% @payload.except(:event).each do |key, value| %>
12
- <%= key.to_s.titleize %>: <%= value.is_a?(Hash) ? value.to_json : value %>
13
- <% end %>
14
- <% end %>
15
-
16
- ---
17
- This alert was sent by RubyLLM::Agents.
18
- Event type: <%= @event %>
@@ -1,34 +0,0 @@
1
- <% # Partial for API key input field %>
2
- <% # Local variables: f (form builder), config (ApiConfiguration), resolved (ResolvedConfig, optional), attr (symbol), label (string), placeholder (string, optional), hint (string, optional) %>
3
- <% resolved = local_assigns[:resolved] %>
4
- <% config_value = resolved&.ruby_llm_value_for(attr) %>
5
- <% db_value = config.send(attr) %>
6
-
7
- <div>
8
- <%= f.label attr, label, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
9
- <div class="mt-1 relative rounded-md shadow-sm">
10
- <%= f.password_field attr,
11
- class: "block w-full px-3 py-2 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm pr-10",
12
- placeholder: config.has_value?(attr) ? "[Key set - leave blank to keep]" : (local_assigns[:placeholder] || "Enter API key..."),
13
- autocomplete: "off",
14
- value: db_value,
15
- data: { key_field: attr, config_value: config_value, db_value: db_value } %>
16
- <% if config.has_value?(attr) %>
17
- <div class="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
18
- <svg class="h-5 w-5 text-green-500" fill="currentColor" viewBox="0 0 20 20">
19
- <path fill-rule="evenodd" 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" clip-rule="evenodd"/>
20
- </svg>
21
- </div>
22
- <% end %>
23
- </div>
24
- <% if local_assigns[:hint] %>
25
- <p class="mt-1 text-xs text-gray-500 dark:text-gray-400"><%= hint %></p>
26
- <% end %>
27
- <% if config_value.present? %>
28
- <p class="mt-1 text-xs text-gray-500 dark:text-gray-400 config-value-hint"
29
- data-full-value="<%= config_value %>"
30
- data-masked-value="<%= resolved.mask_string(config_value) %>">
31
- From config: <code class="bg-gray-100 dark:bg-gray-700 px-1 rounded config-value-display"><%= resolved.mask_string(config_value) %></code>
32
- </p>
33
- <% end %>
34
- </div>