ruby_llm-agents 0.4.0 → 1.0.0.beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +225 -34
  3. data/app/controllers/ruby_llm/agents/agents_controller.rb +136 -16
  4. data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +214 -0
  5. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +29 -9
  6. data/app/controllers/ruby_llm/agents/{settings_controller.rb → system_config_controller.rb} +3 -3
  7. data/app/controllers/ruby_llm/agents/tenants_controller.rb +109 -0
  8. data/app/controllers/ruby_llm/agents/workflows_controller.rb +355 -0
  9. data/app/helpers/ruby_llm/agents/application_helper.rb +25 -0
  10. data/app/models/ruby_llm/agents/api_configuration.rb +386 -0
  11. data/app/models/ruby_llm/agents/execution.rb +3 -0
  12. data/app/models/ruby_llm/agents/tenant_budget.rb +112 -14
  13. data/app/services/ruby_llm/agents/agent_registry.rb +51 -12
  14. data/app/views/layouts/ruby_llm/agents/application.html.erb +5 -30
  15. data/app/views/ruby_llm/agents/agents/_agent.html.erb +13 -1
  16. data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +235 -0
  17. data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +70 -0
  18. data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +152 -0
  19. data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +63 -0
  20. data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +108 -0
  21. data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +91 -0
  22. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +1 -1
  23. data/app/views/ruby_llm/agents/agents/index.html.erb +74 -9
  24. data/app/views/ruby_llm/agents/agents/show.html.erb +18 -378
  25. data/app/views/ruby_llm/agents/api_configurations/_api_key_field.html.erb +34 -0
  26. data/app/views/ruby_llm/agents/api_configurations/_form.html.erb +288 -0
  27. data/app/views/ruby_llm/agents/api_configurations/edit.html.erb +95 -0
  28. data/app/views/ruby_llm/agents/api_configurations/edit_tenant.html.erb +97 -0
  29. data/app/views/ruby_llm/agents/api_configurations/show.html.erb +211 -0
  30. data/app/views/ruby_llm/agents/api_configurations/tenant.html.erb +179 -0
  31. data/app/views/ruby_llm/agents/dashboard/_action_center.html.erb +1 -1
  32. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +269 -15
  33. data/app/views/ruby_llm/agents/executions/show.html.erb +98 -0
  34. data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +93 -0
  35. data/app/views/ruby_llm/agents/{settings → system_config}/show.html.erb +1 -1
  36. data/app/views/ruby_llm/agents/tenants/_form.html.erb +150 -0
  37. data/app/views/ruby_llm/agents/tenants/edit.html.erb +13 -0
  38. data/app/views/ruby_llm/agents/tenants/index.html.erb +129 -0
  39. data/app/views/ruby_llm/agents/tenants/show.html.erb +374 -0
  40. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +236 -0
  41. data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +76 -0
  42. data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +74 -0
  43. data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +108 -0
  44. data/app/views/ruby_llm/agents/workflows/show.html.erb +442 -0
  45. data/config/routes.rb +13 -1
  46. data/lib/generators/ruby_llm_agents/agent_generator.rb +56 -7
  47. data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +100 -0
  48. data/lib/generators/ruby_llm_agents/background_remover_generator.rb +110 -0
  49. data/lib/generators/ruby_llm_agents/embedder_generator.rb +107 -0
  50. data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +115 -0
  51. data/lib/generators/ruby_llm_agents/image_editor_generator.rb +108 -0
  52. data/lib/generators/ruby_llm_agents/image_generator_generator.rb +116 -0
  53. data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +178 -0
  54. data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +109 -0
  55. data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +103 -0
  56. data/lib/generators/ruby_llm_agents/image_variator_generator.rb +102 -0
  57. data/lib/generators/ruby_llm_agents/install_generator.rb +76 -4
  58. data/lib/generators/ruby_llm_agents/restructure_generator.rb +292 -0
  59. data/lib/generators/ruby_llm_agents/speaker_generator.rb +121 -0
  60. data/lib/generators/ruby_llm_agents/templates/add_execution_type_migration.rb.tt +8 -0
  61. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +99 -84
  62. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +42 -40
  63. data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +26 -0
  64. data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +50 -0
  65. data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +26 -0
  66. data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +20 -0
  67. data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +38 -0
  68. data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +139 -0
  69. data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +21 -0
  70. data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +20 -0
  71. data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +20 -0
  72. data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +49 -0
  73. data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +53 -0
  74. data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +44 -0
  75. data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +90 -0
  76. data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +41 -0
  77. data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +45 -0
  78. data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +35 -0
  79. data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +47 -0
  80. data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +50 -0
  81. data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +44 -0
  82. data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +38 -0
  83. data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +33 -0
  84. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +228 -0
  85. data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +131 -0
  86. data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +255 -0
  87. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +120 -0
  88. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +102 -0
  89. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +282 -0
  90. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +228 -0
  91. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +120 -0
  92. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +110 -0
  93. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +120 -0
  94. data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +212 -0
  95. data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +227 -0
  96. data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +251 -0
  97. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +300 -0
  98. data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +56 -0
  99. data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +51 -0
  100. data/lib/generators/ruby_llm_agents/transcriber_generator.rb +107 -0
  101. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +152 -1
  102. data/lib/ruby_llm/agents/audio/speaker.rb +553 -0
  103. data/lib/ruby_llm/agents/audio/transcriber.rb +669 -0
  104. data/lib/ruby_llm/agents/base_agent.rb +675 -0
  105. data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +181 -0
  106. data/lib/ruby_llm/agents/core/base/moderation_execution.rb +274 -0
  107. data/lib/ruby_llm/agents/core/base.rb +135 -0
  108. data/lib/ruby_llm/agents/core/configuration.rb +981 -0
  109. data/lib/ruby_llm/agents/core/errors.rb +150 -0
  110. data/lib/ruby_llm/agents/{instrumentation.rb → core/instrumentation.rb} +93 -4
  111. data/lib/ruby_llm/agents/core/llm_tenant.rb +358 -0
  112. data/lib/ruby_llm/agents/core/resolved_config.rb +348 -0
  113. data/lib/ruby_llm/agents/{version.rb → core/version.rb} +1 -1
  114. data/lib/ruby_llm/agents/dsl/base.rb +110 -0
  115. data/lib/ruby_llm/agents/dsl/caching.rb +142 -0
  116. data/lib/ruby_llm/agents/dsl/reliability.rb +307 -0
  117. data/lib/ruby_llm/agents/dsl.rb +41 -0
  118. data/lib/ruby_llm/agents/image/analyzer/dsl.rb +130 -0
  119. data/lib/ruby_llm/agents/image/analyzer/execution.rb +402 -0
  120. data/lib/ruby_llm/agents/image/analyzer.rb +90 -0
  121. data/lib/ruby_llm/agents/image/background_remover/dsl.rb +154 -0
  122. data/lib/ruby_llm/agents/image/background_remover/execution.rb +240 -0
  123. data/lib/ruby_llm/agents/image/background_remover.rb +89 -0
  124. data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +91 -0
  125. data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +165 -0
  126. data/lib/ruby_llm/agents/image/editor/dsl.rb +56 -0
  127. data/lib/ruby_llm/agents/image/editor/execution.rb +207 -0
  128. data/lib/ruby_llm/agents/image/editor.rb +92 -0
  129. data/lib/ruby_llm/agents/image/generator/active_storage_support.rb +127 -0
  130. data/lib/ruby_llm/agents/image/generator/content_policy.rb +95 -0
  131. data/lib/ruby_llm/agents/image/generator/pricing.rb +353 -0
  132. data/lib/ruby_llm/agents/image/generator/templates.rb +124 -0
  133. data/lib/ruby_llm/agents/image/generator.rb +455 -0
  134. data/lib/ruby_llm/agents/image/pipeline/dsl.rb +213 -0
  135. data/lib/ruby_llm/agents/image/pipeline/execution.rb +382 -0
  136. data/lib/ruby_llm/agents/image/pipeline.rb +97 -0
  137. data/lib/ruby_llm/agents/image/transformer/dsl.rb +148 -0
  138. data/lib/ruby_llm/agents/image/transformer/execution.rb +223 -0
  139. data/lib/ruby_llm/agents/image/transformer.rb +95 -0
  140. data/lib/ruby_llm/agents/image/upscaler/dsl.rb +83 -0
  141. data/lib/ruby_llm/agents/image/upscaler/execution.rb +219 -0
  142. data/lib/ruby_llm/agents/image/upscaler.rb +81 -0
  143. data/lib/ruby_llm/agents/image/variator/dsl.rb +62 -0
  144. data/lib/ruby_llm/agents/image/variator/execution.rb +189 -0
  145. data/lib/ruby_llm/agents/image/variator.rb +80 -0
  146. data/lib/ruby_llm/agents/{alert_manager.rb → infrastructure/alert_manager.rb} +17 -22
  147. data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +145 -0
  148. data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +149 -0
  149. data/lib/ruby_llm/agents/infrastructure/budget/forecaster.rb +68 -0
  150. data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +279 -0
  151. data/lib/ruby_llm/agents/infrastructure/budget_tracker.rb +275 -0
  152. data/lib/ruby_llm/agents/{execution_logger_job.rb → infrastructure/execution_logger_job.rb} +17 -1
  153. data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/executor.rb +2 -1
  154. data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/retry_strategy.rb +9 -3
  155. data/lib/ruby_llm/agents/{reliability.rb → infrastructure/reliability.rb} +11 -21
  156. data/lib/ruby_llm/agents/pipeline/builder.rb +215 -0
  157. data/lib/ruby_llm/agents/pipeline/context.rb +255 -0
  158. data/lib/ruby_llm/agents/pipeline/executor.rb +86 -0
  159. data/lib/ruby_llm/agents/pipeline/middleware/base.rb +124 -0
  160. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +95 -0
  161. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +171 -0
  162. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +415 -0
  163. data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +276 -0
  164. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +196 -0
  165. data/lib/ruby_llm/agents/pipeline.rb +68 -0
  166. data/lib/ruby_llm/agents/{engine.rb → rails/engine.rb} +79 -10
  167. data/lib/ruby_llm/agents/results/background_removal_result.rb +286 -0
  168. data/lib/ruby_llm/agents/{result.rb → results/base.rb} +73 -1
  169. data/lib/ruby_llm/agents/results/embedding_result.rb +243 -0
  170. data/lib/ruby_llm/agents/results/image_analysis_result.rb +314 -0
  171. data/lib/ruby_llm/agents/results/image_edit_result.rb +250 -0
  172. data/lib/ruby_llm/agents/results/image_generation_result.rb +346 -0
  173. data/lib/ruby_llm/agents/results/image_pipeline_result.rb +399 -0
  174. data/lib/ruby_llm/agents/results/image_transform_result.rb +251 -0
  175. data/lib/ruby_llm/agents/results/image_upscale_result.rb +255 -0
  176. data/lib/ruby_llm/agents/results/image_variation_result.rb +237 -0
  177. data/lib/ruby_llm/agents/results/moderation_result.rb +158 -0
  178. data/lib/ruby_llm/agents/results/speech_result.rb +338 -0
  179. data/lib/ruby_llm/agents/results/transcription_result.rb +408 -0
  180. data/lib/ruby_llm/agents/text/embedder.rb +444 -0
  181. data/lib/ruby_llm/agents/text/moderator.rb +237 -0
  182. data/lib/ruby_llm/agents/workflow/async.rb +220 -0
  183. data/lib/ruby_llm/agents/workflow/async_executor.rb +156 -0
  184. data/lib/ruby_llm/agents/{workflow.rb → workflow/orchestrator.rb} +6 -5
  185. data/lib/ruby_llm/agents/workflow/parallel.rb +34 -17
  186. data/lib/ruby_llm/agents/workflow/thread_pool.rb +185 -0
  187. data/lib/ruby_llm/agents.rb +86 -20
  188. metadata +189 -35
  189. data/lib/ruby_llm/agents/base/caching.rb +0 -40
  190. data/lib/ruby_llm/agents/base/cost_calculation.rb +0 -105
  191. data/lib/ruby_llm/agents/base/dsl.rb +0 -324
  192. data/lib/ruby_llm/agents/base/execution.rb +0 -283
  193. data/lib/ruby_llm/agents/base/reliability_dsl.rb +0 -82
  194. data/lib/ruby_llm/agents/base/reliability_execution.rb +0 -136
  195. data/lib/ruby_llm/agents/base/response_building.rb +0 -86
  196. data/lib/ruby_llm/agents/base/tool_tracking.rb +0 -57
  197. data/lib/ruby_llm/agents/base.rb +0 -209
  198. data/lib/ruby_llm/agents/budget_tracker.rb +0 -471
  199. data/lib/ruby_llm/agents/configuration.rb +0 -357
  200. /data/lib/ruby_llm/agents/{deprecations.rb → core/deprecations.rb} +0 -0
  201. /data/lib/ruby_llm/agents/{inflections.rb → core/inflections.rb} +0 -0
  202. /data/lib/ruby_llm/agents/{attempt_tracker.rb → infrastructure/attempt_tracker.rb} +0 -0
  203. /data/lib/ruby_llm/agents/{cache_helper.rb → infrastructure/cache_helper.rb} +0 -0
  204. /data/lib/ruby_llm/agents/{circuit_breaker.rb → infrastructure/circuit_breaker.rb} +0 -0
  205. /data/lib/ruby_llm/agents/{redactor.rb → infrastructure/redactor.rb} +0 -0
  206. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/breaker_manager.rb +0 -0
  207. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/execution_constraints.rb +0 -0
  208. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/fallback_routing.rb +0 -0
@@ -0,0 +1,355 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ # Controller for viewing workflow details and per-workflow analytics
6
+ #
7
+ # Provides detailed views for individual workflows including structure
8
+ # visualization, per-step/branch performance, route distribution,
9
+ # and execution history.
10
+ #
11
+ # @see AgentRegistry For workflow discovery
12
+ # @see Paginatable For pagination implementation
13
+ # @see Filterable For filter parsing and validation
14
+ # @api private
15
+ class WorkflowsController < ApplicationController
16
+ include Paginatable
17
+ include Filterable
18
+
19
+ # Shows detailed view for a specific workflow
20
+ #
21
+ # Loads workflow configuration (if class exists), statistics,
22
+ # filtered executions, chart data, and step-level analytics.
23
+ # Works for both active workflows and deleted workflows with history.
24
+ #
25
+ # @return [void]
26
+ def show
27
+ @workflow_type = CGI.unescape(params[:id])
28
+ @workflow_class = AgentRegistry.find(@workflow_type)
29
+ @workflow_active = @workflow_class.present?
30
+
31
+ # Determine workflow type from class or execution history
32
+ @workflow_type_kind = detect_workflow_type_kind
33
+
34
+ load_workflow_stats
35
+ load_filter_options
36
+ load_filtered_executions
37
+ load_chart_data
38
+ load_step_stats
39
+
40
+ if @workflow_class
41
+ load_workflow_config
42
+ end
43
+ rescue StandardError => e
44
+ Rails.logger.error("[RubyLLM::Agents] Error loading workflow #{@workflow_type}: #{e.message}")
45
+ redirect_to ruby_llm_agents.agents_path, alert: "Error loading workflow details"
46
+ end
47
+
48
+ private
49
+
50
+ # Detects the workflow type kind (pipeline, parallel, router)
51
+ #
52
+ # @return [String, nil] The workflow type kind
53
+ def detect_workflow_type_kind
54
+ if @workflow_class
55
+ ancestors = @workflow_class.ancestors.map { |a| a.name.to_s }
56
+ if ancestors.include?("RubyLLM::Agents::Workflow::Pipeline")
57
+ "pipeline"
58
+ elsif ancestors.include?("RubyLLM::Agents::Workflow::Parallel")
59
+ "parallel"
60
+ elsif ancestors.include?("RubyLLM::Agents::Workflow::Router")
61
+ "router"
62
+ end
63
+ else
64
+ # Fallback to execution history
65
+ Execution.by_agent(@workflow_type)
66
+ .where.not(workflow_type: nil)
67
+ .pluck(:workflow_type)
68
+ .first
69
+ end
70
+ end
71
+
72
+ # Loads all-time and today's statistics for the workflow
73
+ #
74
+ # @return [void]
75
+ def load_workflow_stats
76
+ @stats = Execution.stats_for(@workflow_type, period: :all_time)
77
+ @stats_today = Execution.stats_for(@workflow_type, period: :today)
78
+
79
+ # Additional stats for new schema fields
80
+ workflow_scope = Execution.by_agent(@workflow_type)
81
+ @cache_hit_rate = workflow_scope.cache_hit_rate
82
+ @streaming_rate = workflow_scope.streaming_rate
83
+ @avg_ttft = workflow_scope.avg_time_to_first_token
84
+ end
85
+
86
+ # Loads available filter options from execution history
87
+ #
88
+ # @return [void]
89
+ def load_filter_options
90
+ filter_data = Execution.by_agent(@workflow_type)
91
+ .where.not(agent_version: nil)
92
+ .or(Execution.by_agent(@workflow_type).where.not(model_id: nil))
93
+ .or(Execution.by_agent(@workflow_type).where.not(temperature: nil))
94
+ .pluck(:agent_version, :model_id, :temperature)
95
+
96
+ @versions = filter_data.map(&:first).compact.uniq.sort.reverse
97
+ @models = filter_data.map { |d| d[1] }.compact.uniq.sort
98
+ @temperatures = filter_data.map(&:last).compact.uniq.sort
99
+ end
100
+
101
+ # Loads paginated and filtered executions with statistics
102
+ #
103
+ # @return [void]
104
+ def load_filtered_executions
105
+ base_scope = build_filtered_scope
106
+ result = paginate(base_scope)
107
+ @executions = result[:records]
108
+ @pagination = result[:pagination]
109
+
110
+ @filter_stats = {
111
+ total_count: result[:pagination][:total_count],
112
+ total_cost: base_scope.sum(:total_cost),
113
+ total_tokens: base_scope.sum(:total_tokens)
114
+ }
115
+ end
116
+
117
+ # Builds a filtered scope for the current workflow's executions
118
+ #
119
+ # @return [ActiveRecord::Relation] Filtered execution scope
120
+ def build_filtered_scope
121
+ scope = Execution.by_agent(@workflow_type)
122
+
123
+ # Apply status filter with validation
124
+ statuses = parse_array_param(:statuses)
125
+ scope = apply_status_filter(scope, statuses) if statuses.any?
126
+
127
+ # Apply version filter
128
+ versions = parse_array_param(:versions)
129
+ scope = scope.where(agent_version: versions) if versions.any?
130
+
131
+ # Apply model filter
132
+ models = parse_array_param(:models)
133
+ scope = scope.where(model_id: models) if models.any?
134
+
135
+ # Apply temperature filter
136
+ temperatures = parse_array_param(:temperatures)
137
+ scope = scope.where(temperature: temperatures) if temperatures.any?
138
+
139
+ # Apply time range filter with validation
140
+ days = parse_days_param
141
+ scope = apply_time_filter(scope, days)
142
+
143
+ scope
144
+ end
145
+
146
+ # Loads chart data for workflow performance visualization
147
+ #
148
+ # @return [void]
149
+ def load_chart_data
150
+ @trend_data = Execution.trend_analysis(agent_type: @workflow_type, days: 30)
151
+ @status_distribution = Execution.by_agent(@workflow_type).group(:status).count
152
+ @finish_reason_distribution = Execution.by_agent(@workflow_type).finish_reason_distribution
153
+ end
154
+
155
+ # Loads per-step/branch statistics for workflow analytics
156
+ #
157
+ # @return [void]
158
+ def load_step_stats
159
+ @step_stats = calculate_step_stats
160
+ @route_distribution = calculate_route_distribution if @workflow_type_kind == "router"
161
+ end
162
+
163
+ # Calculates per-step/branch performance statistics
164
+ #
165
+ # @return [Array<Hash>] Array of step stats hashes
166
+ def calculate_step_stats
167
+ # Get root workflow executions
168
+ root_executions = Execution.by_agent(@workflow_type)
169
+ .root_executions
170
+ .where("created_at > ?", 30.days.ago)
171
+ .pluck(:id)
172
+
173
+ return [] if root_executions.empty?
174
+
175
+ # Aggregate child execution stats by workflow_step
176
+ child_stats = Execution.where(parent_execution_id: root_executions)
177
+ .group(:workflow_step)
178
+ .select(
179
+ "workflow_step",
180
+ "COUNT(*) as execution_count",
181
+ "AVG(duration_ms) as avg_duration_ms",
182
+ "SUM(total_cost) as total_cost",
183
+ "AVG(total_cost) as avg_cost",
184
+ "SUM(total_tokens) as total_tokens",
185
+ "AVG(total_tokens) as avg_tokens",
186
+ "SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as success_count",
187
+ "SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as error_count"
188
+ )
189
+
190
+ # Get agent type mappings for each step
191
+ step_agent_map = Execution.where(parent_execution_id: root_executions)
192
+ .where.not(workflow_step: nil)
193
+ .group(:workflow_step)
194
+ .pluck(:workflow_step, Arel.sql("MAX(agent_type)"))
195
+ .to_h
196
+
197
+ child_stats.map do |row|
198
+ next if row.workflow_step.blank?
199
+
200
+ execution_count = row.execution_count.to_i
201
+ success_count = row.success_count.to_i
202
+ success_rate = execution_count > 0 ? (success_count.to_f / execution_count * 100).round(1) : 0
203
+
204
+ {
205
+ name: row.workflow_step,
206
+ agent_type: step_agent_map[row.workflow_step],
207
+ execution_count: execution_count,
208
+ success_rate: success_rate,
209
+ avg_duration_ms: row.avg_duration_ms.to_f.round(0),
210
+ total_cost: row.total_cost.to_f.round(4),
211
+ avg_cost: row.avg_cost.to_f.round(6),
212
+ total_tokens: row.total_tokens.to_i,
213
+ avg_tokens: row.avg_tokens.to_f.round(0)
214
+ }
215
+ end.compact
216
+ end
217
+
218
+ # Calculates route distribution for router workflows
219
+ #
220
+ # @return [Hash] Route distribution data
221
+ def calculate_route_distribution
222
+ # Get route distribution from routed_to field
223
+ distribution = Execution.by_agent(@workflow_type)
224
+ .where("created_at > ?", 30.days.ago)
225
+ .where.not(routed_to: nil)
226
+ .group(:routed_to)
227
+ .count
228
+
229
+ total = distribution.values.sum
230
+ return {} if total.zero?
231
+
232
+ # Add percentage and sorting
233
+ distribution.transform_values do |count|
234
+ {
235
+ count: count,
236
+ percentage: (count.to_f / total * 100).round(1)
237
+ }
238
+ end.sort_by { |_k, v| -v[:count] }.to_h
239
+ end
240
+
241
+ # Loads the current workflow class configuration
242
+ #
243
+ # @return [void]
244
+ def load_workflow_config
245
+ @config = {
246
+ # Basic configuration
247
+ version: safe_call(@workflow_class, :version),
248
+ description: safe_call(@workflow_class, :description),
249
+ timeout: safe_call(@workflow_class, :timeout),
250
+ max_cost: safe_call(@workflow_class, :max_cost)
251
+ }
252
+
253
+ # Workflow-specific configuration
254
+ case @workflow_type_kind
255
+ when "pipeline"
256
+ load_pipeline_config
257
+ when "parallel"
258
+ load_parallel_config
259
+ when "router"
260
+ load_router_config
261
+ end
262
+ end
263
+
264
+ # Loads pipeline-specific configuration
265
+ #
266
+ # @return [void]
267
+ def load_pipeline_config
268
+ @steps = extract_steps(@workflow_class)
269
+ @config[:steps_count] = @steps.size
270
+ end
271
+
272
+ # Loads parallel-specific configuration
273
+ #
274
+ # @return [void]
275
+ def load_parallel_config
276
+ @branches = extract_branches(@workflow_class)
277
+ @config[:branches_count] = @branches.size
278
+ @config[:fail_fast] = safe_call(@workflow_class, :fail_fast?)
279
+ end
280
+
281
+ # Loads router-specific configuration
282
+ #
283
+ # @return [void]
284
+ def load_router_config
285
+ @routes = extract_routes(@workflow_class)
286
+ @config[:routes_count] = @routes.size
287
+ @config[:classifier_model] = safe_call(@workflow_class, :classifier_model)
288
+ @config[:classifier_temperature] = safe_call(@workflow_class, :classifier_temperature)
289
+ end
290
+
291
+ # Extracts steps from a pipeline workflow class
292
+ #
293
+ # @param klass [Class] The workflow class
294
+ # @return [Array<Hash>] Array of step hashes
295
+ def extract_steps(klass)
296
+ return [] unless klass.respond_to?(:steps)
297
+
298
+ klass.steps.map do |name, config|
299
+ {
300
+ name: name,
301
+ agent: config[:agent]&.name,
302
+ optional: config[:continue_on_error] || false
303
+ }
304
+ end
305
+ end
306
+
307
+ # Extracts branches from a parallel workflow class
308
+ #
309
+ # @param klass [Class] The workflow class
310
+ # @return [Array<Hash>] Array of branch hashes
311
+ def extract_branches(klass)
312
+ return [] unless klass.respond_to?(:branches)
313
+
314
+ klass.branches.map do |name, config|
315
+ {
316
+ name: name,
317
+ agent: config[:agent]&.name,
318
+ optional: config[:optional] || false
319
+ }
320
+ end
321
+ end
322
+
323
+ # Extracts routes from a router workflow class
324
+ #
325
+ # @param klass [Class] The workflow class
326
+ # @return [Array<Hash>] Array of route hashes
327
+ def extract_routes(klass)
328
+ return [] unless klass.respond_to?(:routes)
329
+
330
+ klass.routes.map do |name, config|
331
+ {
332
+ name: name,
333
+ agent: config[:agent]&.name,
334
+ description: config[:description],
335
+ default: config[:default] || false
336
+ }
337
+ end
338
+ end
339
+
340
+ # Safely calls a method on a class, returning nil if method doesn't exist
341
+ #
342
+ # @param klass [Class, nil] The class to call the method on
343
+ # @param method_name [Symbol] The method to call
344
+ # @return [Object, nil] The result or nil
345
+ def safe_call(klass, method_name)
346
+ return nil unless klass
347
+ return nil unless klass.respond_to?(method_name)
348
+
349
+ klass.public_send(method_name)
350
+ rescue StandardError
351
+ nil
352
+ end
353
+ end
354
+ end
355
+ end
@@ -24,6 +24,31 @@ module RubyLLM
24
24
  RubyLLM::Agents::Engine.routes.url_helpers
25
25
  end
26
26
 
27
+ # Returns the URL for "All Tenants" (clears tenant filter)
28
+ #
29
+ # Handles two scenarios:
30
+ # 1. Query param routes - removes tenant_id from query params
31
+ # 2. Path-based tenant routes - navigates to equivalent global route
32
+ #
33
+ # @return [String] URL without tenant filtering
34
+ def all_tenants_url
35
+ # Map tenant-specific path routes to their global equivalents
36
+ tenant_route_mappings = {
37
+ "tenant" => ruby_llm_agents.api_configuration_path,
38
+ "edit_tenant" => ruby_llm_agents.edit_api_configuration_path
39
+ }
40
+
41
+ # Check if current action has a global equivalent
42
+ if tenant_route_mappings.key?(action_name)
43
+ base_path = tenant_route_mappings[action_name]
44
+ query = request.query_parameters.except("tenant_id")
45
+ query.any? ? "#{base_path}?#{query.to_query}" : base_path
46
+ else
47
+ # For query param routes, just remove tenant_id
48
+ url_for(request.query_parameters.except("tenant_id"))
49
+ end
50
+ end
51
+
27
52
  # Formats large numbers with human-readable suffixes (K, M, B)
28
53
  #
29
54
  # @param number [Numeric, nil] The number to format