ruby_llm-agents 0.5.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 (190) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +189 -31
  3. data/app/controllers/ruby_llm/agents/agents_controller.rb +136 -16
  4. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +29 -9
  5. data/app/controllers/ruby_llm/agents/workflows_controller.rb +355 -0
  6. data/app/helpers/ruby_llm/agents/application_helper.rb +25 -0
  7. data/app/models/ruby_llm/agents/execution.rb +3 -0
  8. data/app/models/ruby_llm/agents/tenant_budget.rb +58 -15
  9. data/app/services/ruby_llm/agents/agent_registry.rb +51 -12
  10. data/app/views/layouts/ruby_llm/agents/application.html.erb +2 -29
  11. data/app/views/ruby_llm/agents/agents/_agent.html.erb +13 -1
  12. data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +235 -0
  13. data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +70 -0
  14. data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +152 -0
  15. data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +63 -0
  16. data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +108 -0
  17. data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +91 -0
  18. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +1 -1
  19. data/app/views/ruby_llm/agents/agents/index.html.erb +74 -9
  20. data/app/views/ruby_llm/agents/agents/show.html.erb +18 -378
  21. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +269 -15
  22. data/app/views/ruby_llm/agents/executions/show.html.erb +16 -0
  23. data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +93 -0
  24. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +236 -0
  25. data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +76 -0
  26. data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +74 -0
  27. data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +108 -0
  28. data/app/views/ruby_llm/agents/workflows/show.html.erb +442 -0
  29. data/config/routes.rb +1 -0
  30. data/lib/generators/ruby_llm_agents/agent_generator.rb +56 -7
  31. data/lib/generators/ruby_llm_agents/background_remover_generator.rb +110 -0
  32. data/lib/generators/ruby_llm_agents/embedder_generator.rb +107 -0
  33. data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +115 -0
  34. data/lib/generators/ruby_llm_agents/image_editor_generator.rb +108 -0
  35. data/lib/generators/ruby_llm_agents/image_generator_generator.rb +116 -0
  36. data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +178 -0
  37. data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +109 -0
  38. data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +103 -0
  39. data/lib/generators/ruby_llm_agents/image_variator_generator.rb +102 -0
  40. data/lib/generators/ruby_llm_agents/install_generator.rb +76 -4
  41. data/lib/generators/ruby_llm_agents/restructure_generator.rb +292 -0
  42. data/lib/generators/ruby_llm_agents/speaker_generator.rb +121 -0
  43. data/lib/generators/ruby_llm_agents/templates/add_execution_type_migration.rb.tt +8 -0
  44. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +99 -84
  45. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +42 -40
  46. data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +26 -0
  47. data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +50 -0
  48. data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +26 -0
  49. data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +20 -0
  50. data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +38 -0
  51. data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +139 -0
  52. data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +21 -0
  53. data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +20 -0
  54. data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +20 -0
  55. data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +49 -0
  56. data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +53 -0
  57. data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +44 -0
  58. data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +41 -0
  59. data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +45 -0
  60. data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +35 -0
  61. data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +47 -0
  62. data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +50 -0
  63. data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +44 -0
  64. data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +38 -0
  65. data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +33 -0
  66. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +228 -0
  67. data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +131 -0
  68. data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +255 -0
  69. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +120 -0
  70. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +102 -0
  71. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +282 -0
  72. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +228 -0
  73. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +120 -0
  74. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +110 -0
  75. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +120 -0
  76. data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +212 -0
  77. data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +227 -0
  78. data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +251 -0
  79. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +300 -0
  80. data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +56 -0
  81. data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +51 -0
  82. data/lib/generators/ruby_llm_agents/transcriber_generator.rb +107 -0
  83. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +152 -1
  84. data/lib/ruby_llm/agents/audio/speaker.rb +553 -0
  85. data/lib/ruby_llm/agents/audio/transcriber.rb +669 -0
  86. data/lib/ruby_llm/agents/base_agent.rb +675 -0
  87. data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +181 -0
  88. data/lib/ruby_llm/agents/core/base/moderation_execution.rb +274 -0
  89. data/lib/ruby_llm/agents/core/base.rb +135 -0
  90. data/lib/ruby_llm/agents/core/configuration.rb +981 -0
  91. data/lib/ruby_llm/agents/core/errors.rb +150 -0
  92. data/lib/ruby_llm/agents/{instrumentation.rb → core/instrumentation.rb} +22 -1
  93. data/lib/ruby_llm/agents/core/llm_tenant.rb +358 -0
  94. data/lib/ruby_llm/agents/{version.rb → core/version.rb} +1 -1
  95. data/lib/ruby_llm/agents/dsl/base.rb +110 -0
  96. data/lib/ruby_llm/agents/dsl/caching.rb +142 -0
  97. data/lib/ruby_llm/agents/dsl/reliability.rb +307 -0
  98. data/lib/ruby_llm/agents/dsl.rb +41 -0
  99. data/lib/ruby_llm/agents/image/analyzer/dsl.rb +130 -0
  100. data/lib/ruby_llm/agents/image/analyzer/execution.rb +402 -0
  101. data/lib/ruby_llm/agents/image/analyzer.rb +90 -0
  102. data/lib/ruby_llm/agents/image/background_remover/dsl.rb +154 -0
  103. data/lib/ruby_llm/agents/image/background_remover/execution.rb +240 -0
  104. data/lib/ruby_llm/agents/image/background_remover.rb +89 -0
  105. data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +91 -0
  106. data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +165 -0
  107. data/lib/ruby_llm/agents/image/editor/dsl.rb +56 -0
  108. data/lib/ruby_llm/agents/image/editor/execution.rb +207 -0
  109. data/lib/ruby_llm/agents/image/editor.rb +92 -0
  110. data/lib/ruby_llm/agents/image/generator/active_storage_support.rb +127 -0
  111. data/lib/ruby_llm/agents/image/generator/content_policy.rb +95 -0
  112. data/lib/ruby_llm/agents/image/generator/pricing.rb +353 -0
  113. data/lib/ruby_llm/agents/image/generator/templates.rb +124 -0
  114. data/lib/ruby_llm/agents/image/generator.rb +455 -0
  115. data/lib/ruby_llm/agents/image/pipeline/dsl.rb +213 -0
  116. data/lib/ruby_llm/agents/image/pipeline/execution.rb +382 -0
  117. data/lib/ruby_llm/agents/image/pipeline.rb +97 -0
  118. data/lib/ruby_llm/agents/image/transformer/dsl.rb +148 -0
  119. data/lib/ruby_llm/agents/image/transformer/execution.rb +223 -0
  120. data/lib/ruby_llm/agents/image/transformer.rb +95 -0
  121. data/lib/ruby_llm/agents/image/upscaler/dsl.rb +83 -0
  122. data/lib/ruby_llm/agents/image/upscaler/execution.rb +219 -0
  123. data/lib/ruby_llm/agents/image/upscaler.rb +81 -0
  124. data/lib/ruby_llm/agents/image/variator/dsl.rb +62 -0
  125. data/lib/ruby_llm/agents/image/variator/execution.rb +189 -0
  126. data/lib/ruby_llm/agents/image/variator.rb +80 -0
  127. data/lib/ruby_llm/agents/{alert_manager.rb → infrastructure/alert_manager.rb} +17 -22
  128. data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +145 -0
  129. data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +149 -0
  130. data/lib/ruby_llm/agents/infrastructure/budget/forecaster.rb +68 -0
  131. data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +279 -0
  132. data/lib/ruby_llm/agents/infrastructure/budget_tracker.rb +275 -0
  133. data/lib/ruby_llm/agents/{execution_logger_job.rb → infrastructure/execution_logger_job.rb} +17 -1
  134. data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/executor.rb +2 -1
  135. data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/retry_strategy.rb +9 -3
  136. data/lib/ruby_llm/agents/{reliability.rb → infrastructure/reliability.rb} +11 -21
  137. data/lib/ruby_llm/agents/pipeline/builder.rb +215 -0
  138. data/lib/ruby_llm/agents/pipeline/context.rb +255 -0
  139. data/lib/ruby_llm/agents/pipeline/executor.rb +86 -0
  140. data/lib/ruby_llm/agents/pipeline/middleware/base.rb +124 -0
  141. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +95 -0
  142. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +171 -0
  143. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +415 -0
  144. data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +276 -0
  145. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +196 -0
  146. data/lib/ruby_llm/agents/pipeline.rb +68 -0
  147. data/lib/ruby_llm/agents/{engine.rb → rails/engine.rb} +79 -11
  148. data/lib/ruby_llm/agents/results/background_removal_result.rb +286 -0
  149. data/lib/ruby_llm/agents/{result.rb → results/base.rb} +73 -1
  150. data/lib/ruby_llm/agents/results/embedding_result.rb +243 -0
  151. data/lib/ruby_llm/agents/results/image_analysis_result.rb +314 -0
  152. data/lib/ruby_llm/agents/results/image_edit_result.rb +250 -0
  153. data/lib/ruby_llm/agents/results/image_generation_result.rb +346 -0
  154. data/lib/ruby_llm/agents/results/image_pipeline_result.rb +399 -0
  155. data/lib/ruby_llm/agents/results/image_transform_result.rb +251 -0
  156. data/lib/ruby_llm/agents/results/image_upscale_result.rb +255 -0
  157. data/lib/ruby_llm/agents/results/image_variation_result.rb +237 -0
  158. data/lib/ruby_llm/agents/results/moderation_result.rb +158 -0
  159. data/lib/ruby_llm/agents/results/speech_result.rb +338 -0
  160. data/lib/ruby_llm/agents/results/transcription_result.rb +408 -0
  161. data/lib/ruby_llm/agents/text/embedder.rb +444 -0
  162. data/lib/ruby_llm/agents/text/moderator.rb +237 -0
  163. data/lib/ruby_llm/agents/workflow/async.rb +220 -0
  164. data/lib/ruby_llm/agents/workflow/async_executor.rb +156 -0
  165. data/lib/ruby_llm/agents/{workflow.rb → workflow/orchestrator.rb} +6 -5
  166. data/lib/ruby_llm/agents/workflow/parallel.rb +34 -17
  167. data/lib/ruby_llm/agents/workflow/thread_pool.rb +185 -0
  168. data/lib/ruby_llm/agents.rb +86 -20
  169. metadata +172 -34
  170. data/lib/ruby_llm/agents/base/caching.rb +0 -40
  171. data/lib/ruby_llm/agents/base/cost_calculation.rb +0 -105
  172. data/lib/ruby_llm/agents/base/dsl.rb +0 -324
  173. data/lib/ruby_llm/agents/base/execution.rb +0 -366
  174. data/lib/ruby_llm/agents/base/reliability_dsl.rb +0 -82
  175. data/lib/ruby_llm/agents/base/reliability_execution.rb +0 -136
  176. data/lib/ruby_llm/agents/base/response_building.rb +0 -86
  177. data/lib/ruby_llm/agents/base/tool_tracking.rb +0 -57
  178. data/lib/ruby_llm/agents/base.rb +0 -210
  179. data/lib/ruby_llm/agents/budget_tracker.rb +0 -733
  180. data/lib/ruby_llm/agents/configuration.rb +0 -394
  181. /data/lib/ruby_llm/agents/{deprecations.rb → core/deprecations.rb} +0 -0
  182. /data/lib/ruby_llm/agents/{inflections.rb → core/inflections.rb} +0 -0
  183. /data/lib/ruby_llm/agents/{resolved_config.rb → core/resolved_config.rb} +0 -0
  184. /data/lib/ruby_llm/agents/{attempt_tracker.rb → infrastructure/attempt_tracker.rb} +0 -0
  185. /data/lib/ruby_llm/agents/{cache_helper.rb → infrastructure/cache_helper.rb} +0 -0
  186. /data/lib/ruby_llm/agents/{circuit_breaker.rb → infrastructure/circuit_breaker.rb} +0 -0
  187. /data/lib/ruby_llm/agents/{redactor.rb → infrastructure/redactor.rb} +0 -0
  188. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/breaker_manager.rb +0 -0
  189. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/execution_constraints.rb +0 -0
  190. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/fallback_routing.rb +0 -0
@@ -0,0 +1,236 @@
1
+ <%
2
+ # Step/Branch/Route Performance Analytics
3
+ # Shows per-step metrics in a table format
4
+ workflow_type = local_assigns[:workflow_type]
5
+ step_stats = local_assigns[:step_stats] || []
6
+ route_distribution = local_assigns[:route_distribution] || {}
7
+
8
+ # Determine labels based on workflow type
9
+ section_title = case workflow_type
10
+ when "pipeline" then "Step Performance"
11
+ when "parallel" then "Branch Performance"
12
+ when "router" then "Route Distribution"
13
+ else "Step Performance"
14
+ end
15
+
16
+ column_label = case workflow_type
17
+ when "pipeline" then "Step"
18
+ when "parallel" then "Branch"
19
+ when "router" then "Route"
20
+ else "Step"
21
+ end
22
+
23
+ # Calculate max values for bar charts
24
+ max_duration = step_stats.map { |s| s[:avg_duration_ms] }.compact.max || 1
25
+ max_cost = step_stats.map { |s| s[:avg_cost] }.compact.max || 0.001
26
+ %>
27
+
28
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
29
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
30
+ <%= section_title %> <span class="text-sm font-normal text-gray-500 dark:text-gray-400">(last 30 days)</span>
31
+ </h3>
32
+
33
+ <% if workflow_type == "router" && route_distribution.present? %>
34
+ <!-- Route Distribution Bar Chart -->
35
+ <div class="mb-6">
36
+ <div class="space-y-3">
37
+ <% route_distribution.each do |route_name, data| %>
38
+ <div class="flex items-center gap-3">
39
+ <span class="text-sm font-medium text-gray-700 dark:text-gray-300 w-24 truncate" title="<%= route_name %>">
40
+ <%= route_name %>
41
+ </span>
42
+ <div class="flex-1 h-6 bg-gray-100 dark:bg-gray-700 rounded-lg overflow-hidden">
43
+ <div class="h-full bg-amber-500 dark:bg-amber-400 rounded-lg transition-all duration-300"
44
+ style="width: <%= data[:percentage] %>%">
45
+ </div>
46
+ </div>
47
+ <span class="text-sm text-gray-600 dark:text-gray-400 w-24 text-right">
48
+ <%= data[:count] %> (<%= data[:percentage] %>%)
49
+ </span>
50
+ </div>
51
+ <% end %>
52
+ </div>
53
+ </div>
54
+ <% end %>
55
+
56
+ <% if step_stats.any? %>
57
+ <!-- Performance Table -->
58
+ <div class="overflow-x-auto">
59
+ <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
60
+ <thead>
61
+ <tr>
62
+ <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
63
+ <%= column_label %>
64
+ </th>
65
+ <th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
66
+ Executions
67
+ </th>
68
+ <th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
69
+ Success
70
+ </th>
71
+ <th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
72
+ Avg Duration
73
+ </th>
74
+ <th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
75
+ Avg Cost
76
+ </th>
77
+ <th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
78
+ Avg Tokens
79
+ </th>
80
+ <% if workflow_type == "parallel" %>
81
+ <th scope="col" class="px-4 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
82
+ Notes
83
+ </th>
84
+ <% end %>
85
+ </tr>
86
+ </thead>
87
+ <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
88
+ <%
89
+ # For parallel workflows, find fastest/slowest
90
+ durations = step_stats.map { |s| s[:avg_duration_ms] }.compact
91
+ min_duration = durations.min
92
+ max_duration_val = durations.max
93
+ %>
94
+
95
+ <% step_stats.each do |stat| %>
96
+ <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
97
+ <td class="px-4 py-3">
98
+ <div class="flex items-center gap-2">
99
+ <span class="font-medium text-gray-900 dark:text-gray-100">
100
+ <%= stat[:name] %>
101
+ </span>
102
+ <% if stat[:agent_type].present? %>
103
+ <span class="text-xs text-gray-400 dark:text-gray-500 font-mono">
104
+ (<%= stat[:agent_type].gsub(/Agent$/, '') %>)
105
+ </span>
106
+ <% end %>
107
+ </div>
108
+ </td>
109
+ <td class="px-4 py-3 text-right text-sm text-gray-600 dark:text-gray-300">
110
+ <%= number_with_delimiter(stat[:execution_count]) %>
111
+ </td>
112
+ <td class="px-4 py-3 text-right">
113
+ <% rate = stat[:success_rate] %>
114
+ <span class="text-sm font-medium <%= rate >= 95 ? 'text-green-600 dark:text-green-400' : rate >= 80 ? 'text-yellow-600 dark:text-yellow-400' : 'text-red-600 dark:text-red-400' %>">
115
+ <%= rate %>%
116
+ </span>
117
+ </td>
118
+ <td class="px-4 py-3 text-right">
119
+ <div class="flex items-center justify-end gap-2">
120
+ <div class="w-16 h-2 bg-gray-100 dark:bg-gray-700 rounded-full overflow-hidden">
121
+ <div class="h-full bg-purple-500 rounded-full"
122
+ style="width: <%= max_duration > 0 ? (stat[:avg_duration_ms].to_f / max_duration * 100).round : 0 %>%">
123
+ </div>
124
+ </div>
125
+ <span class="text-sm text-gray-600 dark:text-gray-300 w-20 text-right">
126
+ <%= number_with_delimiter(stat[:avg_duration_ms]) %> ms
127
+ </span>
128
+ </div>
129
+ </td>
130
+ <td class="px-4 py-3 text-right">
131
+ <div class="flex items-center justify-end gap-2">
132
+ <div class="w-12 h-2 bg-gray-100 dark:bg-gray-700 rounded-full overflow-hidden">
133
+ <div class="h-full bg-amber-500 rounded-full"
134
+ style="width: <%= max_cost > 0 ? (stat[:avg_cost].to_f / max_cost * 100).round : 0 %>%">
135
+ </div>
136
+ </div>
137
+ <span class="text-sm text-gray-600 dark:text-gray-300 w-16 text-right">
138
+ $<%= number_with_precision(stat[:avg_cost], precision: 4) %>
139
+ </span>
140
+ </div>
141
+ </td>
142
+ <td class="px-4 py-3 text-right text-sm text-gray-600 dark:text-gray-300">
143
+ <%= number_with_delimiter(stat[:avg_tokens]) %>
144
+ </td>
145
+ <% if workflow_type == "parallel" %>
146
+ <td class="px-4 py-3 text-center">
147
+ <% if step_stats.size > 1 %>
148
+ <% if stat[:avg_duration_ms] == min_duration %>
149
+ <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 dark:bg-green-900/50 text-green-700 dark:text-green-300">
150
+ fastest
151
+ </span>
152
+ <% elsif stat[:avg_duration_ms] == max_duration_val && min_duration != max_duration_val %>
153
+ <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 dark:bg-yellow-900/50 text-yellow-700 dark:text-yellow-300">
154
+ slowest
155
+ </span>
156
+ <% end %>
157
+ <% end %>
158
+ </td>
159
+ <% end %>
160
+ </tr>
161
+ <% end %>
162
+
163
+ <!-- Totals Row -->
164
+ <%
165
+ total_executions = step_stats.sum { |s| s[:execution_count] }
166
+ total_cost = step_stats.sum { |s| s[:total_cost] }
167
+ total_tokens = step_stats.sum { |s| s[:total_tokens] }
168
+ avg_duration_all = step_stats.any? ? (step_stats.sum { |s| s[:avg_duration_ms] * s[:execution_count] }.to_f / total_executions).round : 0
169
+ avg_cost_all = total_executions > 0 ? total_cost / total_executions : 0
170
+ avg_tokens_all = total_executions > 0 ? total_tokens / total_executions : 0
171
+
172
+ # Calculate overall success rate
173
+ total_success = step_stats.sum { |s| (s[:success_rate] * s[:execution_count] / 100.0).round }
174
+ overall_success_rate = total_executions > 0 ? (total_success.to_f / total_executions * 100).round(1) : 0
175
+ %>
176
+ <tr class="bg-gray-50 dark:bg-gray-700/50 font-medium">
177
+ <td class="px-4 py-3 text-gray-700 dark:text-gray-300">
178
+ Totals
179
+ </td>
180
+ <td class="px-4 py-3 text-right text-sm text-gray-700 dark:text-gray-300">
181
+ <%= number_with_delimiter(total_executions) %>
182
+ </td>
183
+ <td class="px-4 py-3 text-right">
184
+ <span class="text-sm <%= overall_success_rate >= 95 ? 'text-green-600 dark:text-green-400' : overall_success_rate >= 80 ? 'text-yellow-600 dark:text-yellow-400' : 'text-red-600 dark:text-red-400' %>">
185
+ <%= overall_success_rate %>%
186
+ </span>
187
+ </td>
188
+ <td class="px-4 py-3 text-right text-sm text-gray-700 dark:text-gray-300">
189
+ <%= number_with_delimiter(avg_duration_all) %> ms
190
+ </td>
191
+ <td class="px-4 py-3 text-right text-sm text-gray-700 dark:text-gray-300">
192
+ $<%= number_with_precision(avg_cost_all, precision: 4) %>
193
+ </td>
194
+ <td class="px-4 py-3 text-right text-sm text-gray-700 dark:text-gray-300">
195
+ <%= number_with_delimiter(avg_tokens_all.round) %>
196
+ </td>
197
+ <% if workflow_type == "parallel" %>
198
+ <td class="px-4 py-3"></td>
199
+ <% end %>
200
+ </tr>
201
+ </tbody>
202
+ </table>
203
+ </div>
204
+
205
+ <% if workflow_type == "parallel" && step_stats.size > 1 %>
206
+ <%
207
+ sum_duration = step_stats.sum { |s| s[:avg_duration_ms] }
208
+ wall_clock = step_stats.map { |s| s[:avg_duration_ms] }.max
209
+ time_saved = sum_duration - wall_clock
210
+ %>
211
+ <div class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-700">
212
+ <p class="text-sm text-gray-500 dark:text-gray-400">
213
+ Wall-clock avg: <span class="font-medium text-gray-700 dark:text-gray-300"><%= number_with_delimiter(wall_clock) %> ms</span>
214
+ <span class="text-gray-400 dark:text-gray-500 mx-2">|</span>
215
+ Parallel saves <span class="font-medium text-green-600 dark:text-green-400"><%= number_with_delimiter(time_saved) %> ms</span> vs sequential
216
+ </p>
217
+ </div>
218
+ <% end %>
219
+
220
+ <% if workflow_type == "router" && route_distribution.present? %>
221
+ <%
222
+ # Calculate classification cost if we have stats
223
+ # This is approximate since we don't have exact classification costs
224
+ %>
225
+ <div class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-700">
226
+ <p class="text-sm text-gray-500 dark:text-gray-400">
227
+ Route distribution based on <%= route_distribution.values.sum { |v| v[:count] } %> classified requests
228
+ </p>
229
+ </div>
230
+ <% end %>
231
+ <% else %>
232
+ <p class="text-gray-500 dark:text-gray-400 italic py-4">
233
+ No <%= column_label.downcase %> performance data available for the last 30 days.
234
+ </p>
235
+ <% end %>
236
+ </div>
@@ -0,0 +1,76 @@
1
+ <%
2
+ # Parallel workflow structure visualization
3
+ # Shows branches executing concurrently
4
+ branches = local_assigns[:branches] || []
5
+ %>
6
+
7
+ <% if branches.any? %>
8
+ <!-- Visual Parallel Flow -->
9
+ <div class="py-4">
10
+ <div class="flex items-stretch gap-4">
11
+ <!-- Parallel Symbol -->
12
+ <div class="flex items-center">
13
+ <span class="text-3xl font-light text-cyan-500 dark:text-cyan-400">⫿</span>
14
+ </div>
15
+
16
+ <!-- Branches Container -->
17
+ <div class="flex-1 border-l-4 border-cyan-300 dark:border-cyan-600 pl-4">
18
+ <div class="space-y-2">
19
+ <% branches.each do |branch| %>
20
+ <div class="flex items-center gap-3 py-2 px-3 rounded-lg bg-cyan-50 dark:bg-cyan-900/20 border border-cyan-200 dark:border-cyan-800 <%= branch[:optional] ? 'border-dashed' : '' %>">
21
+ <span class="font-semibold text-cyan-700 dark:text-cyan-300 min-w-28">
22
+ <%= branch[:name] %>
23
+ </span>
24
+ <span class="text-cyan-400 dark:text-cyan-500">-></span>
25
+ <code class="text-sm font-mono text-gray-600 dark:text-gray-400">
26
+ <%= branch[:agent] %>
27
+ </code>
28
+ <% if branch[:optional] %>
29
+ <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 ml-auto">
30
+ optional
31
+ </span>
32
+ <% end %>
33
+ </div>
34
+ <% end %>
35
+ </div>
36
+ </div>
37
+ </div>
38
+
39
+ <p class="text-sm text-gray-500 dark:text-gray-400 mt-4 ml-12">
40
+ All branches execute concurrently
41
+ </p>
42
+ </div>
43
+
44
+ <!-- Branch Details Table -->
45
+ <div class="border-t border-gray-100 dark:border-gray-700 pt-4 mt-4">
46
+ <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-3">
47
+ Branch Details
48
+ </p>
49
+
50
+ <div class="space-y-2">
51
+ <% branches.each_with_index do |branch, index| %>
52
+ <div class="flex items-center gap-3 py-2 px-3 rounded-lg bg-gray-50 dark:bg-gray-900/50">
53
+ <span class="w-6 h-6 flex items-center justify-center rounded-full bg-cyan-100 dark:bg-cyan-900/50 text-xs font-medium text-cyan-600 dark:text-cyan-300">
54
+ <%= index + 1 %>
55
+ </span>
56
+ <span class="font-medium text-gray-700 dark:text-gray-300 min-w-24">
57
+ <%= branch[:name] %>
58
+ </span>
59
+ <span class="text-gray-400 dark:text-gray-500 text-sm">-></span>
60
+ <code class="text-sm font-mono text-gray-600 dark:text-gray-400 bg-gray-100 dark:bg-gray-800 px-2 py-0.5 rounded">
61
+ <%= branch[:agent] %>
62
+ </code>
63
+ <% if branch[:optional] %>
64
+ <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400">
65
+ optional
66
+ </span>
67
+ <% end %>
68
+ </div>
69
+ <% end %>
70
+ </div>
71
+ </div>
72
+ <% else %>
73
+ <p class="text-gray-500 dark:text-gray-400 italic py-4">
74
+ No branches defined for this parallel workflow.
75
+ </p>
76
+ <% end %>
@@ -0,0 +1,74 @@
1
+ <%
2
+ # Pipeline workflow structure visualization
3
+ # Shows steps in sequential order with arrows between them
4
+ steps = local_assigns[:steps] || []
5
+ %>
6
+
7
+ <% if steps.any? %>
8
+ <!-- Visual Pipeline Flow -->
9
+ <div class="py-4 overflow-x-auto">
10
+ <div class="flex items-center justify-center gap-2 min-w-max px-4">
11
+ <% steps.each_with_index do |step, index| %>
12
+ <!-- Step Box -->
13
+ <div class="flex flex-col items-center">
14
+ <div class="w-28 h-16 flex flex-col items-center justify-center rounded-lg border-2 border-indigo-200 dark:border-indigo-700 bg-indigo-50 dark:bg-indigo-900/30 <%= step[:optional] ? 'border-dashed' : '' %>">
15
+ <span class="text-sm font-semibold text-indigo-700 dark:text-indigo-300 truncate max-w-24 px-1">
16
+ <%= step[:name] %>
17
+ </span>
18
+ </div>
19
+ <span class="text-xs text-gray-500 dark:text-gray-400 mt-1 truncate max-w-28" title="<%= step[:agent] %>">
20
+ <%= step[:agent]&.gsub(/Agent$/, '') %>
21
+ </span>
22
+ <% if step[:optional] %>
23
+ <span class="text-[10px] text-gray-400 dark:text-gray-500 italic">(optional)</span>
24
+ <% end %>
25
+ </div>
26
+
27
+ <!-- Arrow between steps -->
28
+ <% unless index == steps.length - 1 %>
29
+ <div class="flex items-center text-indigo-400 dark:text-indigo-500 -mt-6">
30
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
31
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"/>
32
+ </svg>
33
+ </div>
34
+ <% end %>
35
+ <% end %>
36
+ </div>
37
+ </div>
38
+
39
+ <!-- Step Details Table -->
40
+ <div class="border-t border-gray-100 dark:border-gray-700 pt-4 mt-4">
41
+ <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-3">
42
+ Step Details
43
+ </p>
44
+
45
+ <div class="space-y-2">
46
+ <% steps.each_with_index do |step, index| %>
47
+ <div class="flex items-center gap-3 py-2 px-3 rounded-lg bg-gray-50 dark:bg-gray-900/50">
48
+ <span class="w-6 h-6 flex items-center justify-center rounded-full bg-indigo-100 dark:bg-indigo-900/50 text-xs font-medium text-indigo-600 dark:text-indigo-300">
49
+ <%= index + 1 %>
50
+ </span>
51
+ <span class="font-medium text-gray-700 dark:text-gray-300 min-w-24">
52
+ <%= step[:name] %>
53
+ </span>
54
+ <span class="text-gray-400 dark:text-gray-500 text-sm">-></span>
55
+ <code class="text-sm font-mono text-gray-600 dark:text-gray-400 bg-gray-100 dark:bg-gray-800 px-2 py-0.5 rounded">
56
+ <%= step[:agent] %>
57
+ </code>
58
+ <% if step[:optional] %>
59
+ <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400">
60
+ optional
61
+ </span>
62
+ <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 dark:bg-yellow-900/50 text-yellow-700 dark:text-yellow-300">
63
+ continue_on_error
64
+ </span>
65
+ <% end %>
66
+ </div>
67
+ <% end %>
68
+ </div>
69
+ </div>
70
+ <% else %>
71
+ <p class="text-gray-500 dark:text-gray-400 italic py-4">
72
+ No steps defined for this pipeline workflow.
73
+ </p>
74
+ <% end %>
@@ -0,0 +1,108 @@
1
+ <%
2
+ # Router workflow structure visualization
3
+ # Shows classifier branching to different routes
4
+ routes = local_assigns[:routes] || []
5
+ default_route = routes.find { |r| r[:default] }
6
+ %>
7
+
8
+ <% if routes.any? %>
9
+ <!-- Visual Router Flow -->
10
+ <div class="py-4">
11
+ <div class="flex items-start gap-6">
12
+ <!-- Classifier Box -->
13
+ <div class="flex flex-col items-center">
14
+ <div class="w-24 h-12 flex items-center justify-center rounded-lg border-2 border-amber-300 dark:border-amber-600 bg-amber-50 dark:bg-amber-900/30">
15
+ <span class="text-sm font-semibold text-amber-700 dark:text-amber-300">classify</span>
16
+ </div>
17
+ </div>
18
+
19
+ <!-- Routes Fan-out -->
20
+ <div class="flex-1 relative">
21
+ <!-- Connection Lines (visual only) -->
22
+ <div class="absolute left-0 top-0 bottom-0 w-8 flex flex-col justify-around items-end">
23
+ <% routes.each do |route| %>
24
+ <div class="text-amber-400 dark:text-amber-500">
25
+ <svg class="w-6 h-4" viewBox="0 0 24 16" fill="none" stroke="currentColor" stroke-width="2">
26
+ <path d="M0 8 L12 8 L18 8" stroke-linecap="round"/>
27
+ <path d="M14 4 L18 8 L14 12" stroke-linecap="round" stroke-linejoin="round"/>
28
+ </svg>
29
+ </div>
30
+ <% end %>
31
+ </div>
32
+
33
+ <!-- Route Boxes -->
34
+ <div class="space-y-2 ml-10">
35
+ <% routes.each do |route| %>
36
+ <div class="flex items-center gap-3 py-2 px-3 rounded-lg bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 <%= route[:default] ? 'border-dashed' : '' %>">
37
+ <span class="font-semibold text-amber-700 dark:text-amber-300 min-w-24">
38
+ <%= route[:name] %>
39
+ </span>
40
+ <span class="text-amber-400 dark:text-amber-500">-></span>
41
+ <code class="text-sm font-mono text-gray-600 dark:text-gray-400">
42
+ <%= route[:agent] %>
43
+ </code>
44
+ <% if route[:description].present? %>
45
+ <span class="text-xs text-gray-500 dark:text-gray-400 italic truncate max-w-xs ml-2" title="<%= route[:description] %>">
46
+ "<%= route[:description].truncate(40) %>"
47
+ </span>
48
+ <% end %>
49
+ <% if route[:default] %>
50
+ <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 ml-auto">
51
+ fallback
52
+ </span>
53
+ <% end %>
54
+ </div>
55
+ <% end %>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ </div>
60
+
61
+ <!-- Route Details Table -->
62
+ <div class="border-t border-gray-100 dark:border-gray-700 pt-4 mt-4">
63
+ <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-3">
64
+ Route Details
65
+ </p>
66
+
67
+ <div class="space-y-2">
68
+ <% routes.each do |route| %>
69
+ <div class="flex items-start gap-3 py-2 px-3 rounded-lg bg-gray-50 dark:bg-gray-900/50">
70
+ <span class="w-6 h-6 flex items-center justify-center rounded-full <%= route[:default] ? 'bg-gray-200 dark:bg-gray-700' : 'bg-amber-100 dark:bg-amber-900/50' %> text-xs font-medium <%= route[:default] ? 'text-gray-600 dark:text-gray-400' : 'text-amber-600 dark:text-amber-300' %> mt-0.5">
71
+ <% if route[:default] %>
72
+ *
73
+ <% else %>
74
+ <svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
75
+ <path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"/>
76
+ </svg>
77
+ <% end %>
78
+ </span>
79
+ <div class="flex-1">
80
+ <div class="flex items-center gap-3">
81
+ <span class="font-medium text-gray-700 dark:text-gray-300">
82
+ <%= route[:name] %>
83
+ </span>
84
+ <span class="text-gray-400 dark:text-gray-500 text-sm">-></span>
85
+ <code class="text-sm font-mono text-gray-600 dark:text-gray-400 bg-gray-100 dark:bg-gray-800 px-2 py-0.5 rounded">
86
+ <%= route[:agent] %>
87
+ </code>
88
+ <% if route[:default] %>
89
+ <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400">
90
+ fallback route
91
+ </span>
92
+ <% end %>
93
+ </div>
94
+ <% if route[:description].present? %>
95
+ <p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
96
+ <%= route[:description] %>
97
+ </p>
98
+ <% end %>
99
+ </div>
100
+ </div>
101
+ <% end %>
102
+ </div>
103
+ </div>
104
+ <% else %>
105
+ <p class="text-gray-500 dark:text-gray-400 italic py-4">
106
+ No routes defined for this router workflow.
107
+ </p>
108
+ <% end %>