ruby_llm-agents 1.0.0 → 1.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 (139) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/ruby_llm/agents/paginatable.rb +9 -3
  3. data/app/controllers/concerns/ruby_llm/agents/sortable.rb +58 -0
  4. data/app/controllers/ruby_llm/agents/agents_controller.rb +59 -16
  5. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +144 -20
  6. data/app/controllers/ruby_llm/agents/executions_controller.rb +13 -16
  7. data/app/controllers/ruby_llm/agents/workflows_controller.rb +279 -90
  8. data/app/helpers/ruby_llm/agents/application_helper.rb +100 -0
  9. data/app/mailers/ruby_llm/agents/alert_mailer.rb +84 -0
  10. data/app/mailers/ruby_llm/agents/application_mailer.rb +28 -0
  11. data/app/models/ruby_llm/agents/execution/analytics.rb +170 -20
  12. data/app/models/ruby_llm/agents/execution/scopes.rb +0 -31
  13. data/app/models/ruby_llm/agents/execution/workflow.rb +0 -129
  14. data/app/models/ruby_llm/agents/execution.rb +50 -14
  15. data/app/services/ruby_llm/agents/agent_registry.rb +18 -12
  16. data/app/views/layouts/ruby_llm/agents/application.html.erb +72 -76
  17. data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -12
  18. data/app/views/ruby_llm/agents/agents/_sortable_header.html.erb +56 -0
  19. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +5 -15
  20. data/app/views/ruby_llm/agents/agents/index.html.erb +271 -100
  21. data/app/views/ruby_llm/agents/agents/show.html.erb +1 -0
  22. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +107 -0
  23. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +18 -0
  24. data/app/views/ruby_llm/agents/api_configurations/show.html.erb +4 -1
  25. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +66 -359
  26. data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +56 -0
  27. data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +115 -0
  28. data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +35 -60
  29. data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +17 -6
  30. data/app/views/ruby_llm/agents/dashboard/index.html.erb +373 -72
  31. data/app/views/ruby_llm/agents/executions/_execution.html.erb +0 -1
  32. data/app/views/ruby_llm/agents/executions/_filters.html.erb +51 -39
  33. data/app/views/ruby_llm/agents/executions/_list.html.erb +53 -195
  34. data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +5 -20
  35. data/app/views/ruby_llm/agents/executions/index.html.erb +7 -83
  36. data/app/views/ruby_llm/agents/executions/show.html.erb +10 -20
  37. data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +2 -1
  38. data/app/views/ruby_llm/agents/shared/_doc_link.html.erb +12 -0
  39. data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +3 -15
  40. data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +1 -1
  41. data/app/views/ruby_llm/agents/shared/_select_dropdown.html.erb +1 -1
  42. data/app/views/ruby_llm/agents/shared/_sortable_header.html.erb +53 -0
  43. data/app/views/ruby_llm/agents/shared/_status_badge.html.erb +7 -0
  44. data/app/views/ruby_llm/agents/shared/_status_dot.html.erb +1 -1
  45. data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +9 -35
  46. data/app/views/ruby_llm/agents/system_config/show.html.erb +4 -1
  47. data/app/views/ruby_llm/agents/tenants/index.html.erb +4 -1
  48. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +7 -15
  49. data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +539 -0
  50. data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +920 -0
  51. data/app/views/ruby_llm/agents/workflows/index.html.erb +179 -0
  52. data/app/views/ruby_llm/agents/workflows/show.html.erb +164 -139
  53. data/config/routes.rb +1 -1
  54. data/lib/generators/ruby_llm_agents/agent_generator.rb +6 -36
  55. data/lib/generators/ruby_llm_agents/background_remover_generator.rb +7 -37
  56. data/lib/generators/ruby_llm_agents/embedder_generator.rb +5 -38
  57. data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +7 -37
  58. data/lib/generators/ruby_llm_agents/image_editor_generator.rb +7 -37
  59. data/lib/generators/ruby_llm_agents/image_generator_generator.rb +8 -41
  60. data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +18 -46
  61. data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +7 -37
  62. data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +7 -37
  63. data/lib/generators/ruby_llm_agents/image_variator_generator.rb +7 -37
  64. data/lib/generators/ruby_llm_agents/install_generator.rb +33 -56
  65. data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +480 -0
  66. data/lib/generators/ruby_llm_agents/restructure_generator.rb +2 -2
  67. data/lib/generators/ruby_llm_agents/speaker_generator.rb +8 -39
  68. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +5 -8
  69. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +40 -42
  70. data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +20 -22
  71. data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +24 -26
  72. data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +20 -22
  73. data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +19 -17
  74. data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +31 -33
  75. data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +125 -127
  76. data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +20 -18
  77. data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +19 -17
  78. data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +19 -17
  79. data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +38 -40
  80. data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +42 -44
  81. data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +48 -0
  82. data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +19 -21
  83. data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +19 -21
  84. data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +20 -22
  85. data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +15 -17
  86. data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +25 -27
  87. data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +19 -21
  88. data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +20 -22
  89. data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +17 -19
  90. data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +15 -17
  91. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +87 -24
  92. data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +21 -27
  93. data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +46 -54
  94. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +31 -39
  95. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +22 -28
  96. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +53 -63
  97. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +46 -56
  98. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +23 -31
  99. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +22 -30
  100. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +23 -31
  101. data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +38 -46
  102. data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +7 -7
  103. data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +59 -71
  104. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +274 -23
  105. data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +29 -31
  106. data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +28 -30
  107. data/lib/generators/ruby_llm_agents/transcriber_generator.rb +10 -43
  108. data/lib/ruby_llm/agents/core/configuration.rb +55 -43
  109. data/lib/ruby_llm/agents/core/version.rb +1 -1
  110. data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +26 -0
  111. data/lib/ruby_llm/agents/pipeline.rb +69 -0
  112. data/lib/ruby_llm/agents/workflow/approval.rb +205 -0
  113. data/lib/ruby_llm/agents/workflow/approval_store.rb +179 -0
  114. data/lib/ruby_llm/agents/workflow/dsl/executor.rb +467 -0
  115. data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +244 -0
  116. data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +289 -0
  117. data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +107 -0
  118. data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +150 -0
  119. data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +187 -0
  120. data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +352 -0
  121. data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +415 -0
  122. data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +257 -0
  123. data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +317 -0
  124. data/lib/ruby_llm/agents/workflow/dsl.rb +576 -0
  125. data/lib/ruby_llm/agents/workflow/instrumentation.rb +2 -7
  126. data/lib/ruby_llm/agents/workflow/notifiers/base.rb +117 -0
  127. data/lib/ruby_llm/agents/workflow/notifiers/email.rb +117 -0
  128. data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +180 -0
  129. data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +121 -0
  130. data/lib/ruby_llm/agents/workflow/notifiers.rb +70 -0
  131. data/lib/ruby_llm/agents/workflow/orchestrator.rb +190 -23
  132. data/lib/ruby_llm/agents/workflow/result.rb +202 -0
  133. data/lib/ruby_llm/agents/workflow/throttle_manager.rb +206 -0
  134. data/lib/ruby_llm/agents/workflow/wait_result.rb +213 -0
  135. metadata +37 -6
  136. data/app/views/ruby_llm/agents/dashboard/_execution_item.html.erb +0 -66
  137. data/lib/ruby_llm/agents/workflow/parallel.rb +0 -299
  138. data/lib/ruby_llm/agents/workflow/pipeline.rb +0 -306
  139. data/lib/ruby_llm/agents/workflow/router.rb +0 -429
@@ -1,366 +1,73 @@
1
- <div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 h-full" x-data="{ activeTab: 'agents' }">
1
+ <%
2
+ # Combine all stats into a single sorted leaderboard
3
+ all_stats = [
4
+ @agent_stats,
5
+ @embedder_stats,
6
+ @transcriber_stats,
7
+ @speaker_stats,
8
+ @image_generator_stats,
9
+ @moderator_stats,
10
+ @workflow_stats
11
+ ].flatten.compact
12
+ .select { |a| a[:executions].to_i > 0 }
13
+ .sort_by { |a| -a[:executions].to_i }
14
+ .first(10)
15
+ %>
16
+
17
+ <div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 h-full">
2
18
  <div class="px-4 py-3 border-b border-gray-100 dark:border-gray-700">
3
- <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
19
+ <div class="flex items-center justify-between">
4
20
  <h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">Performance</h3>
5
- <!-- Scrollable Tab Buttons -->
6
- <div class="overflow-x-auto -mx-4 px-4 sm:mx-0 sm:px-0">
7
- <div class="flex space-x-1 bg-gray-100 dark:bg-gray-700 rounded-lg p-0.5 min-w-max">
8
- <button type="button" @click="activeTab = 'agents'"
9
- :class="activeTab === 'agents' ? 'bg-white dark:bg-gray-600 shadow-sm' : 'hover:bg-gray-50 dark:hover:bg-gray-600'"
10
- class="px-2 py-1 text-xs font-medium rounded-md transition-colors text-gray-700 dark:text-gray-200 whitespace-nowrap">
11
- <span class="hidden sm:inline">Agents</span>
12
- <span class="sm:hidden">🤖</span>
13
- </button>
14
- <button type="button" @click="activeTab = 'embedders'"
15
- :class="activeTab === 'embedders' ? 'bg-white dark:bg-gray-600 shadow-sm' : 'hover:bg-gray-50 dark:hover:bg-gray-600'"
16
- class="px-2 py-1 text-xs font-medium rounded-md transition-colors text-gray-700 dark:text-gray-200 whitespace-nowrap">
17
- <span class="hidden sm:inline">Embedders</span>
18
- <span class="sm:hidden">📊</span>
19
- </button>
20
- <button type="button" @click="activeTab = 'transcribers'"
21
- :class="activeTab === 'transcribers' ? 'bg-white dark:bg-gray-600 shadow-sm' : 'hover:bg-gray-50 dark:hover:bg-gray-600'"
22
- class="px-2 py-1 text-xs font-medium rounded-md transition-colors text-gray-700 dark:text-gray-200 whitespace-nowrap">
23
- <span class="hidden sm:inline">Transcribers</span>
24
- <span class="sm:hidden">🎤</span>
25
- </button>
26
- <button type="button" @click="activeTab = 'speakers'"
27
- :class="activeTab === 'speakers' ? 'bg-white dark:bg-gray-600 shadow-sm' : 'hover:bg-gray-50 dark:hover:bg-gray-600'"
28
- class="px-2 py-1 text-xs font-medium rounded-md transition-colors text-gray-700 dark:text-gray-200 whitespace-nowrap">
29
- <span class="hidden sm:inline">Speakers</span>
30
- <span class="sm:hidden">🔊</span>
31
- </button>
32
- <button type="button" @click="activeTab = 'image_generators'"
33
- :class="activeTab === 'image_generators' ? 'bg-white dark:bg-gray-600 shadow-sm' : 'hover:bg-gray-50 dark:hover:bg-gray-600'"
34
- class="px-2 py-1 text-xs font-medium rounded-md transition-colors text-gray-700 dark:text-gray-200 whitespace-nowrap">
35
- <span class="hidden sm:inline">Image Gen</span>
36
- <span class="sm:hidden">🎨</span>
37
- </button>
38
- <button type="button" @click="activeTab = 'moderators'"
39
- :class="activeTab === 'moderators' ? 'bg-white dark:bg-gray-600 shadow-sm' : 'hover:bg-gray-50 dark:hover:bg-gray-600'"
40
- class="px-2 py-1 text-xs font-medium rounded-md transition-colors text-gray-700 dark:text-gray-200 whitespace-nowrap">
41
- <span class="hidden sm:inline">Moderators</span>
42
- <span class="sm:hidden">🛡️</span>
43
- </button>
44
- <button type="button" @click="activeTab = 'workflows'"
45
- :class="activeTab === 'workflows' ? 'bg-white dark:bg-gray-600 shadow-sm' : 'hover:bg-gray-50 dark:hover:bg-gray-600'"
46
- class="px-2 py-1 text-xs font-medium rounded-md transition-colors text-gray-700 dark:text-gray-200 whitespace-nowrap">
47
- <span class="hidden sm:inline">Workflows</span>
48
- <span class="sm:hidden">⚙️</span>
49
- </button>
50
- </div>
51
- </div>
21
+ <span class="text-xs text-gray-500 dark:text-gray-400">Top 10 by runs</span>
52
22
  </div>
53
23
  </div>
54
24
 
55
- <!-- Agents Tab -->
56
- <div x-show="activeTab === 'agents'">
57
- <% if @agent_stats&.any? %>
58
- <div class="overflow-x-auto">
59
- <table class="w-full text-sm">
60
- <thead>
61
- <tr class="text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
62
- <th class="px-4 py-2">Agent</th>
63
- <th class="px-4 py-2 text-right">Runs</th>
64
- <th class="px-4 py-2 text-right">Cost</th>
65
- <th class="px-4 py-2 text-right">Success</th>
66
- </tr>
67
- </thead>
68
- <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
69
- <% @agent_stats.first(5).each do |agent| %>
70
- <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
71
- <td class="px-4 py-2">
72
- <span class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate block max-w-[120px]" title="<%= agent[:agent_type] %>">
73
- <%= agent[:agent_type].to_s.demodulize %>
74
- </span>
75
- </td>
76
- <td class="px-4 py-2 text-right text-gray-600 dark:text-gray-300">
77
- <%= number_with_delimiter(agent[:executions]) %>
78
- </td>
79
- <td class="px-4 py-2 text-right text-gray-600 dark:text-gray-300">
80
- $<%= number_with_precision(agent[:total_cost], precision: 2) %>
81
- </td>
82
- <td class="px-4 py-2 text-right">
83
- <span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium <%= agent[:success_rate] >= 95 ? 'bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300' : agent[:success_rate] >= 80 ? 'bg-yellow-100 dark:bg-yellow-900 text-yellow-700 dark:text-yellow-300' : 'bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300' %>">
84
- <%= agent[:success_rate].round %>%
85
- </span>
86
- </td>
87
- </tr>
88
- <% end %>
89
- </tbody>
90
- </table>
91
- </div>
92
- <% else %>
93
- <div class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
94
- <p class="text-sm">No agent data yet</p>
95
- </div>
96
- <% end %>
97
- </div>
98
-
99
- <!-- Embedders Tab -->
100
- <div x-show="activeTab === 'embedders'" x-cloak>
101
- <% if @embedder_stats&.any? %>
102
- <div class="overflow-x-auto">
103
- <table class="w-full text-sm">
104
- <thead>
105
- <tr class="text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
106
- <th class="px-4 py-2">Embedder</th>
107
- <th class="px-4 py-2 text-right">Runs</th>
108
- <th class="px-4 py-2 text-right">Cost</th>
109
- <th class="px-4 py-2 text-right">Success</th>
110
- </tr>
111
- </thead>
112
- <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
113
- <% @embedder_stats.first(5).each do |embedder| %>
114
- <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
115
- <td class="px-4 py-2">
116
- <span class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate block max-w-[120px]" title="<%= embedder[:agent_type] %>">
117
- <%= embedder[:agent_type].to_s.demodulize %>
118
- </span>
119
- </td>
120
- <td class="px-4 py-2 text-right text-gray-600 dark:text-gray-300">
121
- <%= number_with_delimiter(embedder[:executions]) %>
122
- </td>
123
- <td class="px-4 py-2 text-right text-gray-600 dark:text-gray-300">
124
- $<%= number_with_precision(embedder[:total_cost], precision: 2) %>
125
- </td>
126
- <td class="px-4 py-2 text-right">
127
- <span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium <%= embedder[:success_rate] >= 95 ? 'bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300' : embedder[:success_rate] >= 80 ? 'bg-yellow-100 dark:bg-yellow-900 text-yellow-700 dark:text-yellow-300' : 'bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300' %>">
128
- <%= embedder[:success_rate].round %>%
129
- </span>
130
- </td>
131
- </tr>
132
- <% end %>
133
- </tbody>
134
- </table>
135
- </div>
136
- <% else %>
137
- <div class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
138
- <p class="text-sm">No embedder data yet</p>
139
- </div>
140
- <% end %>
141
- </div>
142
-
143
- <!-- Transcribers Tab -->
144
- <div x-show="activeTab === 'transcribers'" x-cloak>
145
- <% if @transcriber_stats&.any? %>
146
- <div class="overflow-x-auto">
147
- <table class="w-full text-sm">
148
- <thead>
149
- <tr class="text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
150
- <th class="px-4 py-2">Transcriber</th>
151
- <th class="px-4 py-2 text-right">Runs</th>
152
- <th class="px-4 py-2 text-right">Cost</th>
153
- <th class="px-4 py-2 text-right">Success</th>
154
- </tr>
155
- </thead>
156
- <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
157
- <% @transcriber_stats.first(5).each do |transcriber| %>
158
- <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
159
- <td class="px-4 py-2">
160
- <span class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate block max-w-[120px]" title="<%= transcriber[:agent_type] %>">
161
- <%= transcriber[:agent_type].to_s.demodulize %>
162
- </span>
163
- </td>
164
- <td class="px-4 py-2 text-right text-gray-600 dark:text-gray-300">
165
- <%= number_with_delimiter(transcriber[:executions]) %>
166
- </td>
167
- <td class="px-4 py-2 text-right text-gray-600 dark:text-gray-300">
168
- $<%= number_with_precision(transcriber[:total_cost], precision: 2) %>
169
- </td>
170
- <td class="px-4 py-2 text-right">
171
- <span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium <%= transcriber[:success_rate] >= 95 ? 'bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300' : transcriber[:success_rate] >= 80 ? 'bg-yellow-100 dark:bg-yellow-900 text-yellow-700 dark:text-yellow-300' : 'bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300' %>">
172
- <%= transcriber[:success_rate].round %>%
173
- </span>
174
- </td>
175
- </tr>
176
- <% end %>
177
- </tbody>
178
- </table>
179
- </div>
180
- <% else %>
181
- <div class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
182
- <p class="text-sm">No transcriber data yet</p>
183
- </div>
184
- <% end %>
185
- </div>
186
-
187
- <!-- Speakers Tab -->
188
- <div x-show="activeTab === 'speakers'" x-cloak>
189
- <% if @speaker_stats&.any? %>
190
- <div class="overflow-x-auto">
191
- <table class="w-full text-sm">
192
- <thead>
193
- <tr class="text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
194
- <th class="px-4 py-2">Speaker</th>
195
- <th class="px-4 py-2 text-right">Runs</th>
196
- <th class="px-4 py-2 text-right">Cost</th>
197
- <th class="px-4 py-2 text-right">Success</th>
198
- </tr>
199
- </thead>
200
- <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
201
- <% @speaker_stats.first(5).each do |speaker| %>
202
- <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
203
- <td class="px-4 py-2">
204
- <span class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate block max-w-[120px]" title="<%= speaker[:agent_type] %>">
205
- <%= speaker[:agent_type].to_s.demodulize %>
206
- </span>
207
- </td>
208
- <td class="px-4 py-2 text-right text-gray-600 dark:text-gray-300">
209
- <%= number_with_delimiter(speaker[:executions]) %>
210
- </td>
211
- <td class="px-4 py-2 text-right text-gray-600 dark:text-gray-300">
212
- $<%= number_with_precision(speaker[:total_cost], precision: 2) %>
213
- </td>
214
- <td class="px-4 py-2 text-right">
215
- <span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium <%= speaker[:success_rate] >= 95 ? 'bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300' : speaker[:success_rate] >= 80 ? 'bg-yellow-100 dark:bg-yellow-900 text-yellow-700 dark:text-yellow-300' : 'bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300' %>">
216
- <%= speaker[:success_rate].round %>%
217
- </span>
218
- </td>
219
- </tr>
220
- <% end %>
221
- </tbody>
222
- </table>
223
- </div>
224
- <% else %>
225
- <div class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
226
- <p class="text-sm">No speaker data yet</p>
227
- </div>
228
- <% end %>
229
- </div>
230
-
231
- <!-- Image Generators Tab -->
232
- <div x-show="activeTab === 'image_generators'" x-cloak>
233
- <% if @image_generator_stats&.any? %>
234
- <div class="overflow-x-auto">
235
- <table class="w-full text-sm">
236
- <thead>
237
- <tr class="text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
238
- <th class="px-4 py-2">Image Generator</th>
239
- <th class="px-4 py-2 text-right">Runs</th>
240
- <th class="px-4 py-2 text-right">Cost</th>
241
- <th class="px-4 py-2 text-right">Success</th>
242
- </tr>
243
- </thead>
244
- <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
245
- <% @image_generator_stats.first(5).each do |generator| %>
246
- <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
247
- <td class="px-4 py-2">
248
- <span class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate block max-w-[120px]" title="<%= generator[:agent_type] %>">
249
- <%= generator[:agent_type].to_s.demodulize %>
250
- </span>
251
- </td>
252
- <td class="px-4 py-2 text-right text-gray-600 dark:text-gray-300">
253
- <%= number_with_delimiter(generator[:executions]) %>
254
- </td>
255
- <td class="px-4 py-2 text-right text-gray-600 dark:text-gray-300">
256
- $<%= number_with_precision(generator[:total_cost], precision: 2) %>
257
- </td>
258
- <td class="px-4 py-2 text-right">
259
- <span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium <%= generator[:success_rate] >= 95 ? 'bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300' : generator[:success_rate] >= 80 ? 'bg-yellow-100 dark:bg-yellow-900 text-yellow-700 dark:text-yellow-300' : 'bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300' %>">
260
- <%= generator[:success_rate].round %>%
261
- </span>
262
- </td>
263
- </tr>
264
- <% end %>
265
- </tbody>
266
- </table>
267
- </div>
268
- <% else %>
269
- <div class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
270
- <p class="text-sm">No image generator data yet</p>
271
- </div>
272
- <% end %>
273
- </div>
274
-
275
- <!-- Moderators Tab -->
276
- <div x-show="activeTab === 'moderators'" x-cloak>
277
- <% if @moderator_stats&.any? %>
278
- <div class="overflow-x-auto">
279
- <table class="w-full text-sm">
280
- <thead>
281
- <tr class="text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
282
- <th class="px-4 py-2">Moderator</th>
283
- <th class="px-4 py-2 text-right">Runs</th>
284
- <th class="px-4 py-2 text-right">Cost</th>
285
- <th class="px-4 py-2 text-right">Success</th>
286
- </tr>
287
- </thead>
288
- <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
289
- <% @moderator_stats.first(5).each do |moderator| %>
290
- <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
291
- <td class="px-4 py-2">
292
- <span class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate block max-w-[120px]" title="<%= moderator[:agent_type] %>">
293
- <%= moderator[:agent_type].to_s.demodulize %>
294
- </span>
295
- </td>
296
- <td class="px-4 py-2 text-right text-gray-600 dark:text-gray-300">
297
- <%= number_with_delimiter(moderator[:executions]) %>
298
- </td>
299
- <td class="px-4 py-2 text-right text-gray-600 dark:text-gray-300">
300
- $<%= number_with_precision(moderator[:total_cost], precision: 2) %>
301
- </td>
302
- <td class="px-4 py-2 text-right">
303
- <span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium <%= moderator[:success_rate] >= 95 ? 'bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300' : moderator[:success_rate] >= 80 ? 'bg-yellow-100 dark:bg-yellow-900 text-yellow-700 dark:text-yellow-300' : 'bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300' %>">
304
- <%= moderator[:success_rate].round %>%
305
- </span>
306
- </td>
307
- </tr>
308
- <% end %>
309
- </tbody>
310
- </table>
311
- </div>
312
- <% else %>
313
- <div class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
314
- <p class="text-sm">No moderator data yet</p>
315
- </div>
316
- <% end %>
317
- </div>
318
-
319
- <!-- Workflows Tab -->
320
- <div x-show="activeTab === 'workflows'" x-cloak>
321
- <% if @workflow_stats&.any? %>
322
- <div class="overflow-x-auto">
323
- <table class="w-full text-sm">
324
- <thead>
325
- <tr class="text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
326
- <th class="px-4 py-2">Workflow</th>
327
- <th class="px-4 py-2">Type</th>
328
- <th class="px-4 py-2 text-right">Runs</th>
329
- <th class="px-4 py-2 text-right">Cost</th>
330
- <th class="px-4 py-2 text-right">Success</th>
25
+ <% if all_stats.empty? %>
26
+ <div class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
27
+ <p class="text-sm">No performance data yet</p>
28
+ </div>
29
+ <% else %>
30
+ <div class="overflow-x-auto">
31
+ <table class="w-full text-sm">
32
+ <thead>
33
+ <tr class="text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
34
+ <th class="px-4 py-2 w-8">#</th>
35
+ <th class="px-4 py-2">Agent</th>
36
+ <th class="px-4 py-2 text-right">Runs</th>
37
+ <th class="px-4 py-2 text-right">Cost</th>
38
+ <th class="px-4 py-2 text-right">Success</th>
39
+ </tr>
40
+ </thead>
41
+ <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
42
+ <% all_stats.each_with_index do |item, index| %>
43
+ <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
44
+ <td class="px-4 py-2">
45
+ <span class="text-xs font-medium text-gray-400 dark:text-gray-500"><%= index + 1 %></span>
46
+ </td>
47
+ <td class="px-4 py-2">
48
+ <span class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate max-w-[140px] block" title="<%= item[:agent_type] %>">
49
+ <% if item[:is_workflow] %>
50
+ <%= item[:agent_type].to_s.demodulize.gsub(/Workflow$|Pipeline$|Parallel$|Router$/, '') %>
51
+ <% else %>
52
+ <%= item[:agent_type].to_s.demodulize %>
53
+ <% end %>
54
+ </span>
55
+ </td>
56
+ <td class="px-4 py-2 text-right text-gray-600 dark:text-gray-300">
57
+ <%= number_with_delimiter(item[:executions]) %>
58
+ </td>
59
+ <td class="px-4 py-2 text-right text-gray-600 dark:text-gray-300">
60
+ $<%= number_with_precision(item[:total_cost], precision: 2) %>
61
+ </td>
62
+ <td class="px-4 py-2 text-right">
63
+ <span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium <%= item[:success_rate] >= 95 ? 'bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300' : item[:success_rate] >= 80 ? 'bg-yellow-100 dark:bg-yellow-900 text-yellow-700 dark:text-yellow-300' : 'bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300' %>">
64
+ <%= item[:success_rate].round %>%
65
+ </span>
66
+ </td>
331
67
  </tr>
332
- </thead>
333
- <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
334
- <% @workflow_stats.first(5).each do |workflow| %>
335
- <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
336
- <td class="px-4 py-2">
337
- <span class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate block max-w-[100px]" title="<%= workflow[:agent_type] %>">
338
- <%= workflow[:agent_type].to_s.demodulize.gsub(/Workflow$|Pipeline$|Parallel$|Router$/, '') %>
339
- </span>
340
- </td>
341
- <td class="px-4 py-2">
342
- <%= render "ruby_llm/agents/shared/workflow_type_badge", workflow_type: workflow[:workflow_type], size: :xs, show_label: false %>
343
- </td>
344
- <td class="px-4 py-2 text-right text-gray-600 dark:text-gray-300">
345
- <%= number_with_delimiter(workflow[:executions]) %>
346
- </td>
347
- <td class="px-4 py-2 text-right text-gray-600 dark:text-gray-300">
348
- $<%= number_with_precision(workflow[:total_cost], precision: 2) %>
349
- </td>
350
- <td class="px-4 py-2 text-right">
351
- <span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium <%= workflow[:success_rate] >= 95 ? 'bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300' : workflow[:success_rate] >= 80 ? 'bg-yellow-100 dark:bg-yellow-900 text-yellow-700 dark:text-yellow-300' : 'bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300' %>">
352
- <%= workflow[:success_rate].round %>%
353
- </span>
354
- </td>
355
- </tr>
356
- <% end %>
357
- </tbody>
358
- </table>
359
- </div>
360
- <% else %>
361
- <div class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
362
- <p class="text-sm">No workflow data yet</p>
363
- </div>
364
- <% end %>
365
- </div>
68
+ <% end %>
69
+ </tbody>
70
+ </table>
71
+ </div>
72
+ <% end %>
366
73
  </div>
@@ -0,0 +1,56 @@
1
+ <div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 h-full">
2
+ <div class="px-4 py-3 border-b border-gray-100 dark:border-gray-700">
3
+ <div class="flex items-center justify-between">
4
+ <h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">Model Performance</h3>
5
+ <span class="text-xs text-gray-500 dark:text-gray-400">By cost</span>
6
+ </div>
7
+ </div>
8
+
9
+ <% if model_stats.any? %>
10
+ <div class="overflow-x-auto">
11
+ <table class="w-full text-sm">
12
+ <thead>
13
+ <tr class="text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
14
+ <th class="px-4 py-2">Model</th>
15
+ <th class="px-4 py-2 text-right">Runs</th>
16
+ <th class="px-4 py-2 text-right">$/1K tok</th>
17
+ <th class="px-4 py-2 text-right">Avg Time</th>
18
+ <th class="px-4 py-2 text-right">Success</th>
19
+ </tr>
20
+ </thead>
21
+ <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
22
+ <% model_stats.first(8).each do |model| %>
23
+ <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
24
+ <td class="px-4 py-2">
25
+ <span class="font-mono text-xs font-medium text-gray-900 dark:text-gray-100 truncate max-w-[120px] block" title="<%= model[:model_id] %>">
26
+ <%= model[:model_id].to_s.split('/').last.truncate(20) %>
27
+ </span>
28
+ </td>
29
+ <td class="px-4 py-2 text-right text-gray-600 dark:text-gray-300">
30
+ <%= number_with_delimiter(model[:executions]) %>
31
+ </td>
32
+ <td class="px-4 py-2 text-right text-gray-600 dark:text-gray-300">
33
+ $<%= number_with_precision(model[:cost_per_1k_tokens], precision: 4) %>
34
+ </td>
35
+ <td class="px-4 py-2 text-right text-gray-600 dark:text-gray-300">
36
+ <%= format_duration_ms(model[:avg_duration_ms]) %>
37
+ </td>
38
+ <td class="px-4 py-2 text-right">
39
+ <span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium <%= model[:success_rate] >= 95 ? 'bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300' : model[:success_rate] >= 80 ? 'bg-yellow-100 dark:bg-yellow-900 text-yellow-700 dark:text-yellow-300' : 'bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300' %>">
40
+ <%= model[:success_rate].round %>%
41
+ </span>
42
+ </td>
43
+ </tr>
44
+ <% end %>
45
+ </tbody>
46
+ </table>
47
+ </div>
48
+ <% else %>
49
+ <div class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
50
+ <svg class="w-8 h-8 mx-auto mb-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
51
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"/>
52
+ </svg>
53
+ <p class="text-sm">No model data yet</p>
54
+ </div>
55
+ <% end %>
56
+ </div>
@@ -0,0 +1,115 @@
1
+ <div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 h-full">
2
+ <div class="px-4 py-3 border-b border-gray-100 dark:border-gray-700">
3
+ <div class="flex items-center justify-between">
4
+ <h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">Cost by Model</h3>
5
+ <span class="text-xs text-gray-500 dark:text-gray-400">
6
+ $<%= number_with_precision(model_stats.sum { |m| m[:total_cost] }, precision: 2) %> total
7
+ </span>
8
+ </div>
9
+ </div>
10
+
11
+ <% if model_stats.any? && model_stats.sum { |m| m[:total_cost] } > 0 %>
12
+ <div class="p-4">
13
+ <div id="model-cost-chart" style="width: 100%; height: 200px;"></div>
14
+
15
+ <!-- Legend -->
16
+ <div class="mt-4 space-y-2">
17
+ <% colors = ['#6366F1', '#8B5CF6', '#EC4899', '#F59E0B', '#10B981', '#3B82F6', '#EF4444', '#6B7280'] %>
18
+ <% model_stats.first(5).each_with_index do |model, i| %>
19
+ <div class="flex items-center justify-between text-sm">
20
+ <div class="flex items-center gap-2">
21
+ <span class="w-3 h-3 rounded-full flex-shrink-0" style="background-color: <%= colors[i % colors.length] %>"></span>
22
+ <span class="text-gray-700 dark:text-gray-300 truncate max-w-[140px]" title="<%= model[:model_id] %>">
23
+ <%= model[:model_id].to_s.split('/').last.truncate(18) %>
24
+ </span>
25
+ </div>
26
+ <div class="flex items-center gap-2">
27
+ <span class="text-gray-900 dark:text-gray-100 font-medium">$<%= number_with_precision(model[:total_cost], precision: 2) %></span>
28
+ <span class="text-gray-500 dark:text-gray-400 text-xs w-12 text-right"><%= model[:cost_percentage] %>%</span>
29
+ </div>
30
+ </div>
31
+ <% end %>
32
+ <% if model_stats.length > 5 %>
33
+ <% other_cost = model_stats[5..].sum { |m| m[:total_cost] } %>
34
+ <% other_percentage = model_stats[5..].sum { |m| m[:cost_percentage] } %>
35
+ <div class="flex items-center justify-between text-sm">
36
+ <div class="flex items-center gap-2">
37
+ <span class="w-3 h-3 rounded-full flex-shrink-0 bg-gray-400"></span>
38
+ <span class="text-gray-700 dark:text-gray-300">Other (<%= model_stats.length - 5 %> models)</span>
39
+ </div>
40
+ <div class="flex items-center gap-2">
41
+ <span class="text-gray-900 dark:text-gray-100 font-medium">$<%= number_with_precision(other_cost, precision: 2) %></span>
42
+ <span class="text-gray-500 dark:text-gray-400 text-xs w-12 text-right"><%= other_percentage.round(1) %>%</span>
43
+ </div>
44
+ </div>
45
+ <% end %>
46
+ </div>
47
+ </div>
48
+
49
+ <script>
50
+ (function() {
51
+ const colors = ['#6366F1', '#8B5CF6', '#EC4899', '#F59E0B', '#10B981', '#3B82F6', '#EF4444', '#6B7280'];
52
+ const data = [
53
+ <% model_stats.first(5).each_with_index do |model, i| %>
54
+ { name: '<%= model[:model_id].to_s.split('/').last.truncate(15).gsub("'", "\\'") %>', y: <%= model[:total_cost] %>, color: colors[<%= i %>] },
55
+ <% end %>
56
+ <% if model_stats.length > 5 %>
57
+ { name: 'Other', y: <%= model_stats[5..].sum { |m| m[:total_cost] } %>, color: '#9CA3AF' },
58
+ <% end %>
59
+ ];
60
+
61
+ function initChart() {
62
+ if (typeof Highcharts === 'undefined') {
63
+ setTimeout(initChart, 100);
64
+ return;
65
+ }
66
+
67
+ Highcharts.chart('model-cost-chart', {
68
+ chart: {
69
+ type: 'pie',
70
+ backgroundColor: 'transparent',
71
+ spacing: [0, 0, 0, 0]
72
+ },
73
+ title: { text: null },
74
+ credits: { enabled: false },
75
+ tooltip: {
76
+ backgroundColor: 'rgba(17, 24, 39, 0.95)',
77
+ borderColor: 'transparent',
78
+ borderRadius: 8,
79
+ style: { color: '#F3F4F6', fontSize: '12px' },
80
+ pointFormat: '<b>${point.y:.2f}</b> ({point.percentage:.1f}%)'
81
+ },
82
+ plotOptions: {
83
+ pie: {
84
+ innerSize: '60%',
85
+ dataLabels: { enabled: false },
86
+ borderWidth: 0,
87
+ states: {
88
+ hover: { brightness: 0.1 }
89
+ }
90
+ }
91
+ },
92
+ series: [{
93
+ name: 'Cost',
94
+ data: data
95
+ }]
96
+ });
97
+ }
98
+
99
+ if (document.readyState === 'loading') {
100
+ document.addEventListener('DOMContentLoaded', initChart);
101
+ } else {
102
+ initChart();
103
+ }
104
+ })();
105
+ </script>
106
+ <% else %>
107
+ <div class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
108
+ <svg class="w-8 h-8 mx-auto mb-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
109
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M11 3.055A9.001 9.001 0 1020.945 13H11V3.055z"/>
110
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20.488 9H15V3.512A9.025 9.025 0 0120.488 9z"/>
111
+ </svg>
112
+ <p class="text-sm">No cost data yet</p>
113
+ </div>
114
+ <% end %>
115
+ </div>