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
@@ -19,13 +19,10 @@ module RubyLLM
19
19
  "dashboard/index" => "Dashboard",
20
20
  "agents/index" => "Agent-DSL",
21
21
  "agents/show" => "Agent-DSL",
22
- "workflows/index" => "Workflows",
23
- "workflows/show" => "Workflows",
24
22
  "executions/index" => "Execution-Tracking",
25
23
  "executions/show" => "Execution-Tracking",
26
24
  "tenants/index" => "Multi-Tenancy",
27
- "system_config/show" => "Configuration",
28
- "api_configurations/show" => "Configuration"
25
+ "system_config/show" => "Configuration"
29
26
  }.freeze
30
27
 
31
28
  # Returns the documentation URL for the current page or a specific page key
@@ -59,27 +56,11 @@ module RubyLLM
59
56
 
60
57
  # Returns the URL for "All Tenants" (clears tenant filter)
61
58
  #
62
- # Handles two scenarios:
63
- # 1. Query param routes - removes tenant_id from query params
64
- # 2. Path-based tenant routes - navigates to equivalent global route
59
+ # Removes tenant_id from query params to show unfiltered results.
65
60
  #
66
61
  # @return [String] URL without tenant filtering
67
62
  def all_tenants_url
68
- # Map tenant-specific path routes to their global equivalents
69
- tenant_route_mappings = {
70
- "tenant" => ruby_llm_agents.api_configuration_path,
71
- "edit_tenant" => ruby_llm_agents.edit_api_configuration_path
72
- }
73
-
74
- # Check if current action has a global equivalent
75
- if tenant_route_mappings.key?(action_name)
76
- base_path = tenant_route_mappings[action_name]
77
- query = request.query_parameters.except("tenant_id")
78
- query.any? ? "#{base_path}?#{query.to_query}" : base_path
79
- else
80
- # For query param routes, just remove tenant_id
81
- url_for(request.query_parameters.except("tenant_id"))
82
- end
63
+ url_for(request.query_parameters.except("tenant_id"))
83
64
  end
84
65
 
85
66
  # Formats large numbers with human-readable suffixes (K, M, B)
@@ -121,7 +102,7 @@ module RubyLLM
121
102
  # @return [ActiveSupport::SafeBuffer] HTML badge element
122
103
  def render_enabled_badge(enabled)
123
104
  if enabled
124
- '<span class="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-green-100 dark:bg-green-900/50 text-green-700 dark:text-green-300">Enabled</span>'.html_safe
105
+ '<span class="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-green-100 dark:bg-green-500/20 text-green-700 dark:text-green-300">Enabled</span>'.html_safe
125
106
  else
126
107
  '<span class="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400">Disabled</span>'.html_safe
127
108
  end
@@ -133,7 +114,7 @@ module RubyLLM
133
114
  # @return [ActiveSupport::SafeBuffer] HTML badge element
134
115
  def render_configured_badge(configured)
135
116
  if configured
136
- '<span class="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-green-100 dark:bg-green-900/50 text-green-700 dark:text-green-300">Configured</span>'.html_safe
117
+ '<span class="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-green-100 dark:bg-green-500/20 text-green-700 dark:text-green-300">Configured</span>'.html_safe
137
118
  else
138
119
  '<span class="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400">Not configured</span>'.html_safe
139
120
  end
@@ -254,7 +235,7 @@ module RubyLLM
254
235
  end
255
236
 
256
237
  if is_improvement
257
- content_tag(:span, class: "inline-flex items-center gap-1 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") do
238
+ content_tag(:span, class: "inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium text-green-700 dark:text-green-300 bg-green-100 dark:bg-green-500/20 rounded-full") do
258
239
  safe_join([
259
240
  content_tag(:svg, class: "w-3 h-3", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24") do
260
241
  content_tag(:path, nil, "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "2", d: "M5 10l7-7m0 0l7 7m-7-7v18")
@@ -263,7 +244,7 @@ module RubyLLM
263
244
  ])
264
245
  end
265
246
  elsif is_regression
266
- content_tag(:span, class: "inline-flex items-center gap-1 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") do
247
+ content_tag(:span, class: "inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium text-red-700 dark:text-red-300 bg-red-100 dark:bg-red-500/20 rounded-full") do
267
248
  safe_join([
268
249
  content_tag(:svg, class: "w-3 h-3", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24") do
269
250
  content_tag(:path, nil, "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "2", d: "M19 14l-7 7m0 0l-7-7m7 7V3")
@@ -306,31 +287,16 @@ module RubyLLM
306
287
 
307
288
  # Returns human-readable display name for time range
308
289
  #
309
- # @param range [String] Range parameter (today, 7d, 30d, 60d, 90d, or custom YYYY-MM-DD_YYYY-MM-DD)
290
+ # @param range [String] Range parameter (today, 7d, 30d)
310
291
  # @return [String] Human-readable range name
311
292
  # @example
312
- # range_display_name("7d") #=> "Last 7 Days"
313
- # range_display_name("2024-01-01_2024-01-15") #=> "Jan 1 - Jan 15"
293
+ # range_display_name("7d") #=> "7 Days"
314
294
  def range_display_name(range)
315
295
  case range
316
296
  when "today" then "Today"
317
- when "7d" then "Last 7 Days"
318
- when "30d" then "Last 30 Days"
319
- when "60d" then "Last 60 Days"
320
- when "90d" then "Last 90 Days"
321
- else
322
- if range&.include?("_")
323
- from_str, to_str = range.split("_")
324
- from_date = Date.parse(from_str) rescue nil
325
- to_date = Date.parse(to_str) rescue nil
326
- if from_date && to_date
327
- "#{from_date.strftime('%b %-d')} - #{to_date.strftime('%b %-d')}"
328
- else
329
- "Custom Range"
330
- end
331
- else
332
- "Today"
333
- end
297
+ when "7d" then "7 Days"
298
+ when "30d" then "30 Days"
299
+ else "Today"
334
300
  end
335
301
  end
336
302
 
@@ -391,7 +357,7 @@ module RubyLLM
391
357
  # @return [ActiveSupport::SafeBuffer] HTML summary banner
392
358
  def comparison_summary_badge(improvements_count, regressions_count, v2_label)
393
359
  if improvements_count >= 3 && regressions_count == 0
394
- content_tag(:span, class: "inline-flex items-center gap-1 px-3 py-1 text-sm font-medium text-green-700 dark:text-green-300 bg-green-100 dark:bg-green-900/50 rounded-lg") do
360
+ content_tag(:span, class: "inline-flex items-center gap-1 px-3 py-1 text-sm font-medium text-green-700 dark:text-green-300 bg-green-100 dark:bg-green-500/20 rounded-lg") do
395
361
  safe_join([
396
362
  content_tag(:svg, class: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24") do
397
363
  content_tag(:path, nil, "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "2", d: "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z")
@@ -400,7 +366,7 @@ module RubyLLM
400
366
  ])
401
367
  end
402
368
  elsif regressions_count >= 3 && improvements_count == 0
403
- content_tag(:span, class: "inline-flex items-center gap-1 px-3 py-1 text-sm font-medium text-red-700 dark:text-red-300 bg-red-100 dark:bg-red-900/50 rounded-lg") do
369
+ content_tag(:span, class: "inline-flex items-center gap-1 px-3 py-1 text-sm font-medium text-red-700 dark:text-red-300 bg-red-100 dark:bg-red-500/20 rounded-lg") do
404
370
  safe_join([
405
371
  content_tag(:svg, class: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24") do
406
372
  content_tag(:path, nil, "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "2", d: "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z")
@@ -409,7 +375,7 @@ module RubyLLM
409
375
  ])
410
376
  end
411
377
  elsif improvements_count > 0 || regressions_count > 0
412
- content_tag(:span, class: "inline-flex items-center gap-1 px-3 py-1 text-sm font-medium text-amber-700 dark:text-amber-300 bg-amber-100 dark:bg-amber-900/50 rounded-lg") do
378
+ content_tag(:span, class: "inline-flex items-center gap-1 px-3 py-1 text-sm font-medium text-amber-700 dark:text-amber-300 bg-amber-100 dark:bg-amber-500/20 rounded-lg") do
413
379
  safe_join([
414
380
  content_tag(:svg, class: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24") do
415
381
  content_tag(:path, nil, "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "2", d: "M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4")
@@ -552,14 +518,14 @@ module RubyLLM
552
518
 
553
519
  escaped_value = ERB::Util.html_escape(token[:value])
554
520
  if is_key
555
- result << %(<span class="text-purple-600">#{escaped_value}</span>)
521
+ result << %(<span class="text-purple-600 dark:text-purple-400">#{escaped_value}</span>)
556
522
  else
557
- result << %(<span class="text-green-600">#{escaped_value}</span>)
523
+ result << %(<span class="text-green-600 dark:text-green-400">#{escaped_value}</span>)
558
524
  end
559
525
  when :number
560
- result << %(<span class="text-blue-600">#{token[:value]}</span>)
526
+ result << %(<span class="text-blue-600 dark:text-blue-400">#{token[:value]}</span>)
561
527
  when :boolean
562
- result << %(<span class="text-amber-600">#{token[:value]}</span>)
528
+ result << %(<span class="text-amber-600 dark:text-amber-400">#{token[:value]}</span>)
563
529
  when :null
564
530
  result << %(<span class="text-gray-400">#{token[:value]}</span>)
565
531
  else
@@ -82,58 +82,6 @@ module RubyLLM
82
82
  end
83
83
 
84
84
  # Compares performance between two agent versions
85
- #
86
- # @param agent_type [String] The agent class name
87
- # @param version1 [String] First version to compare (baseline)
88
- # @param version2 [String] Second version to compare
89
- # @param period [Symbol] Time scope for comparison
90
- # @return [Hash] Comparison data with stats for each version and improvement percentages
91
- def compare_versions(agent_type, version1, version2, period: :this_week)
92
- base_scope = by_agent(agent_type).public_send(period)
93
-
94
- v1_stats = stats_for_scope(base_scope.by_version(version1))
95
- v2_stats = stats_for_scope(base_scope.by_version(version2))
96
-
97
- {
98
- agent_type: agent_type,
99
- period: period,
100
- version1: { version: version1, **v1_stats },
101
- version2: { version: version2, **v2_stats },
102
- improvements: {
103
- cost_change_pct: percent_change(v1_stats[:avg_cost], v2_stats[:avg_cost]),
104
- token_change_pct: percent_change(v1_stats[:avg_tokens], v2_stats[:avg_tokens]),
105
- speed_change_pct: percent_change(v1_stats[:avg_duration_ms], v2_stats[:avg_duration_ms])
106
- }
107
- }
108
- end
109
-
110
- # Returns daily trend data for a specific agent version
111
- #
112
- # Used for sparkline charts in version comparison.
113
- #
114
- # @param agent_type [String] The agent class name
115
- # @param version [String] The version to analyze
116
- # @param days [Integer] Number of days to analyze
117
- # @return [Array<Hash>] Daily metrics sorted oldest to newest
118
- def version_trend_data(agent_type, version, days: 14)
119
- scope = by_agent(agent_type).by_version(version)
120
-
121
- (0...days).map do |days_ago|
122
- date = days_ago.days.ago.to_date
123
- day_scope = scope.where(created_at: date.beginning_of_day..date.end_of_day)
124
- count = day_scope.count
125
-
126
- {
127
- date: date,
128
- count: count,
129
- success_rate: calculate_success_rate(day_scope),
130
- avg_cost: count > 0 ? ((day_scope.total_cost_sum || 0) / count).round(6) : 0,
131
- avg_duration_ms: day_scope.avg_duration&.round || 0,
132
- avg_tokens: day_scope.avg_tokens&.round || 0
133
- }
134
- end.reverse
135
- end
136
-
137
85
  # Analyzes trends over a time period
138
86
  #
139
87
  # @param agent_type [String, nil] Filter to specific agent, or nil for all
@@ -526,9 +474,18 @@ module RubyLLM
526
474
 
527
475
  # Average time to first token for streaming executions
528
476
  #
477
+ # time_to_first_token_ms is stored in metadata JSON, so we use
478
+ # Ruby-level calculation instead of SQL aggregation.
479
+ #
529
480
  # @return [Integer, nil] Average TTFT in milliseconds, or nil if no data
530
481
  def avg_time_to_first_token
531
- streaming.where.not(time_to_first_token_ms: nil).average(:time_to_first_token_ms)&.round(0)
482
+ ttft_values = streaming
483
+ .where("metadata IS NOT NULL")
484
+ .pluck(:metadata)
485
+ .filter_map { |m| m&.dig("time_to_first_token_ms") }
486
+ return nil if ttft_values.empty?
487
+
488
+ (ttft_values.sum.to_f / ttft_values.size).round(0)
532
489
  end
533
490
 
534
491
  # Finish reason distribution
@@ -540,9 +497,11 @@ module RubyLLM
540
497
 
541
498
  # Rate limited execution count
542
499
  #
500
+ # rate_limited is stored in metadata JSON
501
+ #
543
502
  # @return [Integer] Number of executions that were rate limited
544
503
  def rate_limited_count
545
- where(rate_limited: true).count
504
+ metadata_true("rate_limited").count
546
505
  end
547
506
 
548
507
  # Rate limited rate percentage
@@ -79,17 +79,11 @@ module RubyLLM
79
79
  # @param agent_type [String] The agent class name
80
80
  # @return [ActiveRecord::Relation]
81
81
 
82
- # @!method by_version(version)
83
- # Filters to a specific agent version
84
- # @param version [String] The version string
85
- # @return [ActiveRecord::Relation]
86
-
87
82
  # @!method by_model(model_id)
88
83
  # Filters to a specific LLM model
89
84
  # @param model_id [String] The model identifier
90
85
  # @return [ActiveRecord::Relation]
91
86
  scope :by_agent, ->(agent_type) { where(agent_type: agent_type.to_s) }
92
- scope :by_version, ->(version) { where(agent_version: version.to_s) }
93
87
  scope :by_model, ->(model_id) { where(model_id: model_id.to_s) }
94
88
 
95
89
  # @!endgroup
@@ -142,15 +136,25 @@ module RubyLLM
142
136
  # @!group Parameter Scopes
143
137
 
144
138
  # @!method with_parameter(key, value = nil)
145
- # Filters by JSONB parameter key/value
139
+ # Filters by parameter key/value in the execution_details table
146
140
  # @param key [String, Symbol] Parameter key to check
147
141
  # @param value [Object, nil] Optional value to match
148
142
  # @return [ActiveRecord::Relation]
149
143
  scope :with_parameter, ->(key, value = nil) do
150
- if value
151
- where("parameters @> ?", { key => value }.to_json)
144
+ detail_table = RubyLLM::Agents::ExecutionDetail.table_name
145
+ joined = joins(:detail)
146
+ if connection.adapter_name.downcase.include?("sqlite")
147
+ if value
148
+ joined.where("json_extract(#{detail_table}.parameters, ?) = ?", "$.#{key}", value.to_s)
149
+ else
150
+ joined.where("json_extract(#{detail_table}.parameters, ?) IS NOT NULL", "$.#{key}")
151
+ end
152
152
  else
153
- where("parameters ? :key", key: key.to_s)
153
+ if value
154
+ joined.where("#{detail_table}.parameters @> ?", { key => value }.to_json)
155
+ else
156
+ joined.where("#{detail_table}.parameters ? :key", key: key.to_s)
157
+ end
154
158
  end
155
159
  end
156
160
 
@@ -196,10 +200,12 @@ module RubyLLM
196
200
  # @!method rate_limited
197
201
  # Returns executions that were rate limited
198
202
  # @return [ActiveRecord::Relation]
199
- scope :with_fallback, -> { where.not(fallback_reason: nil) }
200
- scope :retryable_errors, -> { where(retryable: true) }
201
- scope :rate_limited, -> { where(rate_limited: true) }
202
- scope :by_fallback_reason, ->(reason) { where(fallback_reason: reason) }
203
+ #
204
+ # Note: fallback_reason, retryable, and rate_limited are stored in metadata JSON
205
+ scope :with_fallback, -> { metadata_present("fallback_reason") }
206
+ scope :retryable_errors, -> { metadata_true("retryable") }
207
+ scope :rate_limited, -> { metadata_true("rate_limited") }
208
+ scope :by_fallback_reason, ->(reason) { metadata_value("fallback_reason", reason) }
203
209
 
204
210
  # @!endgroup
205
211
 
@@ -269,6 +275,47 @@ module RubyLLM
269
275
  # They can be called on scoped relations.
270
276
 
271
277
  class_methods do
278
+ # Database-agnostic JSON metadata queries
279
+ # These fields (fallback_reason, retryable, rate_limited, etc.) are stored
280
+ # in the metadata JSON column rather than as direct columns.
281
+
282
+ # Queries for metadata key presence (IS NOT NULL)
283
+ #
284
+ # @param key [String] The metadata key
285
+ # @return [ActiveRecord::Relation]
286
+ def metadata_present(key)
287
+ if connection.adapter_name.downcase.include?("sqlite")
288
+ where("json_extract(metadata, ?) IS NOT NULL", "$.#{key}")
289
+ else
290
+ where("metadata->>? IS NOT NULL", key.to_s)
291
+ end
292
+ end
293
+
294
+ # Queries for metadata boolean value being true
295
+ #
296
+ # @param key [String] The metadata key
297
+ # @return [ActiveRecord::Relation]
298
+ def metadata_true(key)
299
+ if connection.adapter_name.downcase.include?("sqlite")
300
+ where("json_extract(metadata, ?) = 1", "$.#{key}")
301
+ else
302
+ where("metadata @> ?", { key.to_s => true }.to_json)
303
+ end
304
+ end
305
+
306
+ # Queries for metadata key matching a specific value
307
+ #
308
+ # @param key [String] The metadata key
309
+ # @param value [Object] The value to match
310
+ # @return [ActiveRecord::Relation]
311
+ def metadata_value(key, value)
312
+ if connection.adapter_name.downcase.include?("sqlite")
313
+ where("json_extract(metadata, ?) = ?", "$.#{key}", value.to_s)
314
+ else
315
+ where("metadata->>? = ?", key.to_s, value.to_s)
316
+ end
317
+ end
318
+
272
319
  # Returns sum of total_cost for the current scope
273
320
  #
274
321
  # @return [Float, nil] Total cost in USD
@@ -8,8 +8,6 @@ module RubyLLM
8
8
  #
9
9
  # @!attribute [rw] agent_type
10
10
  # @return [String] Full class name of the agent (e.g., "SearchAgent")
11
- # @!attribute [rw] agent_version
12
- # @return [String] Version string for cache invalidation
13
11
  # @!attribute [rw] model_id
14
12
  # @return [String] LLM model identifier used
15
13
  # @!attribute [rw] temperature
@@ -37,7 +35,7 @@ module RubyLLM
37
35
  # @!attribute [rw] parameters
38
36
  # @return [Hash] Sanitized parameters passed to the agent
39
37
  # @!attribute [rw] metadata
40
- # @return [Hash] Custom metadata from execution_metadata hook
38
+ # @return [Hash] Custom metadata from metadata hook
41
39
  # @!attribute [rw] error_class
42
40
  # @return [String, nil] Exception class name if failed
43
41
  # @!attribute [rw] error_message
@@ -51,7 +49,6 @@ module RubyLLM
51
49
  include Execution::Metrics
52
50
  include Execution::Scopes
53
51
  include Execution::Analytics
54
- include Execution::Workflow
55
52
 
56
53
  # Status enum
57
54
  # - running: execution in progress
@@ -72,20 +69,25 @@ module RubyLLM
72
69
  has_many :child_executions, class_name: "RubyLLM::Agents::Execution",
73
70
  foreign_key: :parent_execution_id, dependent: :nullify, inverse_of: :parent_execution
74
71
 
75
- # Polymorphic association to tenant model (for llm_tenant DSL)
76
- belongs_to :tenant_record, polymorphic: true, optional: true
72
+ # Detail record for large payloads (prompts, responses, tool calls, etc.)
73
+ has_one :detail, class_name: "RubyLLM::Agents::ExecutionDetail",
74
+ foreign_key: :execution_id, dependent: :destroy
75
+
76
+ # Delegations so existing code keeps working transparently
77
+ delegate :system_prompt, :user_prompt, :response, :error_message,
78
+ :messages_summary, :tool_calls, :attempts, :fallback_chain,
79
+ :parameters, :routed_to, :classification_result,
80
+ :cached_at, :cache_creation_tokens,
81
+ to: :detail, prefix: false, allow_nil: true
77
82
 
78
83
  # Validations
79
84
  validates :agent_type, :model_id, :started_at, presence: true
80
85
  validates :status, inclusion: { in: statuses.keys }
81
- validates :agent_version, presence: true
82
86
  validates :temperature, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 2 }, allow_nil: true
83
87
  validates :input_tokens, :output_tokens, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
84
88
  validates :duration_ms, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
85
89
  validates :input_cost, :output_cost, :total_cost, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
86
90
  validates :finish_reason, inclusion: { in: FINISH_REASONS }, allow_nil: true
87
- validates :fallback_reason, inclusion: { in: FALLBACK_REASONS }, allow_nil: true
88
- validates :time_to_first_token_ms, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
89
91
 
90
92
  before_save :calculate_total_tokens, if: -> { input_tokens_changed? || output_tokens_changed? }
91
93
  before_save :calculate_total_cost, if: -> { input_cost_changed? || output_cost_changed? }
@@ -205,7 +207,41 @@ module RubyLLM
205
207
  #
206
208
  # @return [Boolean] true if rate limiting occurred
207
209
  def rate_limited?
208
- rate_limited == true
210
+ metadata&.dig("rate_limited") == true
211
+ end
212
+
213
+ # Convenience accessors for niche fields stored in metadata JSON
214
+ %w[span_id response_cache_key fallback_reason].each do |field|
215
+ define_method(field) { metadata&.dig(field) }
216
+ define_method(:"#{field}=") { |val| self.metadata = (metadata || {}).merge(field => val) }
217
+ end
218
+
219
+ %w[time_to_first_token_ms].each do |field|
220
+ define_method(field) { metadata&.dig(field)&.to_i }
221
+ define_method(:"#{field}=") { |val| self.metadata = (metadata || {}).merge(field => val) }
222
+ end
223
+
224
+ def retryable
225
+ metadata&.dig("retryable")
226
+ end
227
+
228
+ def retryable=(val)
229
+ self.metadata = (metadata || {}).merge("retryable" => val)
230
+ end
231
+
232
+ def rate_limited
233
+ metadata&.dig("rate_limited")
234
+ end
235
+
236
+ def rate_limited=(val)
237
+ self.metadata = (metadata || {}).merge("rate_limited" => val)
238
+ end
239
+
240
+ # Convenience method to access tenant_record through the tenant
241
+ def tenant_record
242
+ return nil unless tenant_id.present?
243
+
244
+ Tenant.find_by(tenant_id: tenant_id)&.tenant_record
209
245
  end
210
246
 
211
247
  # Returns whether this execution used streaming
@@ -323,14 +359,18 @@ module RubyLLM
323
359
 
324
360
  # Resolves model info for cost calculation
325
361
  #
362
+ # Uses Models.find (local registry lookup) rather than Models.resolve
363
+ # because cost calculation only needs pricing data, not a provider instance.
364
+ # Models.resolve requires API keys to instantiate the provider, which may
365
+ # not be available in background jobs or instrumentation contexts.
366
+ #
326
367
  # @param lookup_model_id [String, nil] The model identifier (defaults to self.model_id)
327
368
  # @return [Object, nil] Model info or nil
328
369
  def resolve_model_info(lookup_model_id = nil)
329
370
  lookup_model_id ||= model_id
330
371
  return nil unless lookup_model_id
331
372
 
332
- model, _provider = RubyLLM::Models.resolve(lookup_model_id)
333
- model
373
+ RubyLLM::Models.find(lookup_model_id)
334
374
  rescue StandardError
335
375
  nil
336
376
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ # Stores large payload data for an execution (prompts, responses, tool calls, etc.)
6
+ #
7
+ # Separated from {Execution} to keep the main table lean for analytics queries.
8
+ # Only created when there is detail data to store.
9
+ #
10
+ # @see Execution
11
+ # @api public
12
+ class ExecutionDetail < ::ActiveRecord::Base
13
+ self.table_name = "ruby_llm_agents_execution_details"
14
+
15
+ belongs_to :execution, class_name: "RubyLLM::Agents::Execution"
16
+ end
17
+ end
18
+ end