ruby_llm-agents 1.0.0 → 1.2.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 (152) 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/models/ruby_llm/agents/tenant/budgetable.rb +277 -0
  16. data/app/models/ruby_llm/agents/tenant/configurable.rb +135 -0
  17. data/app/models/ruby_llm/agents/tenant/trackable.rb +310 -0
  18. data/app/models/ruby_llm/agents/tenant.rb +146 -0
  19. data/app/models/ruby_llm/agents/tenant_budget.rb +12 -253
  20. data/app/services/ruby_llm/agents/agent_registry.rb +18 -12
  21. data/app/views/layouts/ruby_llm/agents/application.html.erb +72 -76
  22. data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -12
  23. data/app/views/ruby_llm/agents/agents/_sortable_header.html.erb +56 -0
  24. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +5 -15
  25. data/app/views/ruby_llm/agents/agents/index.html.erb +271 -100
  26. data/app/views/ruby_llm/agents/agents/show.html.erb +1 -0
  27. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +107 -0
  28. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +18 -0
  29. data/app/views/ruby_llm/agents/api_configurations/show.html.erb +4 -1
  30. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +66 -359
  31. data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +56 -0
  32. data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +115 -0
  33. data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +35 -60
  34. data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +17 -6
  35. data/app/views/ruby_llm/agents/dashboard/index.html.erb +373 -72
  36. data/app/views/ruby_llm/agents/executions/_execution.html.erb +0 -1
  37. data/app/views/ruby_llm/agents/executions/_filters.html.erb +51 -39
  38. data/app/views/ruby_llm/agents/executions/_list.html.erb +53 -195
  39. data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +5 -20
  40. data/app/views/ruby_llm/agents/executions/index.html.erb +7 -83
  41. data/app/views/ruby_llm/agents/executions/show.html.erb +10 -20
  42. data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +2 -1
  43. data/app/views/ruby_llm/agents/shared/_doc_link.html.erb +12 -0
  44. data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +3 -15
  45. data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +1 -1
  46. data/app/views/ruby_llm/agents/shared/_select_dropdown.html.erb +1 -1
  47. data/app/views/ruby_llm/agents/shared/_sortable_header.html.erb +53 -0
  48. data/app/views/ruby_llm/agents/shared/_status_badge.html.erb +7 -0
  49. data/app/views/ruby_llm/agents/shared/_status_dot.html.erb +1 -1
  50. data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +9 -35
  51. data/app/views/ruby_llm/agents/system_config/show.html.erb +4 -1
  52. data/app/views/ruby_llm/agents/tenants/index.html.erb +4 -1
  53. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +7 -15
  54. data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +539 -0
  55. data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +920 -0
  56. data/app/views/ruby_llm/agents/workflows/index.html.erb +179 -0
  57. data/app/views/ruby_llm/agents/workflows/show.html.erb +164 -139
  58. data/config/routes.rb +1 -1
  59. data/lib/generators/ruby_llm_agents/agent_generator.rb +6 -36
  60. data/lib/generators/ruby_llm_agents/background_remover_generator.rb +7 -37
  61. data/lib/generators/ruby_llm_agents/embedder_generator.rb +5 -38
  62. data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +7 -37
  63. data/lib/generators/ruby_llm_agents/image_editor_generator.rb +7 -37
  64. data/lib/generators/ruby_llm_agents/image_generator_generator.rb +8 -41
  65. data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +18 -46
  66. data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +7 -37
  67. data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +7 -37
  68. data/lib/generators/ruby_llm_agents/image_variator_generator.rb +7 -37
  69. data/lib/generators/ruby_llm_agents/install_generator.rb +33 -56
  70. data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +480 -0
  71. data/lib/generators/ruby_llm_agents/multi_tenancy_generator.rb +42 -22
  72. data/lib/generators/ruby_llm_agents/restructure_generator.rb +2 -2
  73. data/lib/generators/ruby_llm_agents/speaker_generator.rb +8 -39
  74. data/lib/generators/ruby_llm_agents/templates/add_tenant_to_executions_migration.rb.tt +13 -2
  75. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +5 -8
  76. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +40 -42
  77. data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +20 -22
  78. data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +24 -26
  79. data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +20 -22
  80. data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +19 -17
  81. data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +31 -33
  82. data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +125 -127
  83. data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +20 -18
  84. data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +19 -17
  85. data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +19 -17
  86. data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +38 -40
  87. data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +42 -44
  88. data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +48 -0
  89. data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +19 -21
  90. data/lib/generators/ruby_llm_agents/templates/create_tenant_budgets_migration.rb.tt +11 -0
  91. data/lib/generators/ruby_llm_agents/templates/create_tenants_migration.rb.tt +72 -0
  92. data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +19 -21
  93. data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +20 -22
  94. data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +15 -17
  95. data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +25 -27
  96. data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +19 -21
  97. data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +20 -22
  98. data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +17 -19
  99. data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +15 -17
  100. data/lib/generators/ruby_llm_agents/templates/rename_tenant_budgets_to_tenants_migration.rb.tt +34 -0
  101. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +87 -24
  102. data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +21 -27
  103. data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +46 -54
  104. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +31 -39
  105. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +22 -28
  106. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +53 -63
  107. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +46 -56
  108. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +23 -31
  109. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +22 -30
  110. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +23 -31
  111. data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +38 -46
  112. data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +7 -7
  113. data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +59 -71
  114. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +274 -23
  115. data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +29 -31
  116. data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +28 -30
  117. data/lib/generators/ruby_llm_agents/transcriber_generator.rb +10 -43
  118. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +26 -0
  119. data/lib/ruby_llm/agents/core/configuration.rb +55 -43
  120. data/lib/ruby_llm/agents/core/llm_tenant.rb +60 -60
  121. data/lib/ruby_llm/agents/core/version.rb +1 -1
  122. data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +26 -0
  123. data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +4 -2
  124. data/lib/ruby_llm/agents/pipeline.rb +69 -0
  125. data/lib/ruby_llm/agents/workflow/approval.rb +205 -0
  126. data/lib/ruby_llm/agents/workflow/approval_store.rb +179 -0
  127. data/lib/ruby_llm/agents/workflow/dsl/executor.rb +467 -0
  128. data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +244 -0
  129. data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +289 -0
  130. data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +107 -0
  131. data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +150 -0
  132. data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +187 -0
  133. data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +352 -0
  134. data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +415 -0
  135. data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +257 -0
  136. data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +317 -0
  137. data/lib/ruby_llm/agents/workflow/dsl.rb +576 -0
  138. data/lib/ruby_llm/agents/workflow/instrumentation.rb +2 -7
  139. data/lib/ruby_llm/agents/workflow/notifiers/base.rb +117 -0
  140. data/lib/ruby_llm/agents/workflow/notifiers/email.rb +117 -0
  141. data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +180 -0
  142. data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +121 -0
  143. data/lib/ruby_llm/agents/workflow/notifiers.rb +70 -0
  144. data/lib/ruby_llm/agents/workflow/orchestrator.rb +190 -23
  145. data/lib/ruby_llm/agents/workflow/result.rb +202 -0
  146. data/lib/ruby_llm/agents/workflow/throttle_manager.rb +206 -0
  147. data/lib/ruby_llm/agents/workflow/wait_result.rb +213 -0
  148. metadata +43 -6
  149. data/app/views/ruby_llm/agents/dashboard/_execution_item.html.erb +0 -66
  150. data/lib/ruby_llm/agents/workflow/parallel.rb +0 -299
  151. data/lib/ruby_llm/agents/workflow/pipeline.rb +0 -306
  152. data/lib/ruby_llm/agents/workflow/router.rb +0 -429
@@ -0,0 +1,179 @@
1
+ <div class="mb-6">
2
+ <div class="flex items-center gap-2">
3
+ <h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">Workflows</h1>
4
+ <%= render "ruby_llm/agents/shared/doc_link" %>
5
+ </div>
6
+ <p class="text-gray-500 dark:text-gray-400 mt-1">Orchestrated multi-agent workflows with execution statistics</p>
7
+ </div>
8
+
9
+ <% if @workflows.empty? %>
10
+ <%= render "ruby_llm/agents/agents/empty_state", type: "workflows" %>
11
+ <% else %>
12
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
13
+ <div class="overflow-x-auto">
14
+ <table class="w-full divide-y divide-gray-200 dark:divide-gray-700">
15
+ <thead class="bg-gray-50 dark:bg-gray-900">
16
+ <tr>
17
+ <%= render "ruby_llm/agents/agents/sortable_header",
18
+ column: "name", label: "Name",
19
+ current_sort: @sort_params[:column], current_direction: @sort_params[:direction] %>
20
+
21
+ <th scope="col" class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
22
+ Status
23
+ </th>
24
+
25
+ <th scope="col" class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 hidden md:table-cell">
26
+ Steps
27
+ </th>
28
+
29
+ <%= render "ruby_llm/agents/agents/sortable_header",
30
+ column: "execution_count", label: "Executions",
31
+ current_sort: @sort_params[:column], current_direction: @sort_params[:direction] %>
32
+
33
+ <%= render "ruby_llm/agents/agents/sortable_header",
34
+ column: "total_cost", label: "Cost",
35
+ current_sort: @sort_params[:column], current_direction: @sort_params[:direction],
36
+ th_class: "hidden md:table-cell" %>
37
+
38
+ <%= render "ruby_llm/agents/agents/sortable_header",
39
+ column: "success_rate", label: "Success",
40
+ current_sort: @sort_params[:column], current_direction: @sort_params[:direction] %>
41
+
42
+ <%= render "ruby_llm/agents/agents/sortable_header",
43
+ column: "last_executed", label: "Last Run",
44
+ current_sort: @sort_params[:column], current_direction: @sort_params[:direction] %>
45
+ </tr>
46
+ </thead>
47
+
48
+ <% @workflows.each_with_index do |workflow, index| %>
49
+ <%
50
+ row_bg = index.even? ? '' : 'bg-gray-50/50 dark:bg-gray-900/30'
51
+ success_rate = workflow[:success_rate] || 0
52
+ steps_count = workflow[:workflow_children]&.size || 0
53
+ has_steps = workflow[:workflow_children].present? && workflow[:workflow_children].any?
54
+ %>
55
+ <tbody x-data="{ expanded: false }" class="<%= index > 0 ? 'border-t border-gray-100/50 dark:border-gray-700/50' : '' %>">
56
+ <tr class="<%= row_bg %> hover:bg-emerald-50/50 dark:hover:bg-emerald-900/20 transition-colors">
57
+ <!-- Name -->
58
+ <td class="px-4 py-3">
59
+ <a href="<%= ruby_llm_agents.workflow_path(ERB::Util.url_encode(workflow[:name])) %>" class="block">
60
+ <div class="flex items-center gap-2">
61
+ <span class="text-sm font-medium text-gray-900 dark:text-gray-100">
62
+ <% name_parts = workflow[:name].split('::') %>
63
+ <% if name_parts.length > 1 %>
64
+ <span class="text-gray-400 dark:text-gray-500 font-normal"><%= name_parts[0..-2].join('::') %>::</span>
65
+ <% end %>
66
+ <%= name_parts.last %>
67
+ </span>
68
+ <span class="hidden lg:inline text-xs text-gray-400 dark:text-gray-500">v<%= workflow[:version] %></span>
69
+ </div>
70
+ <% if workflow[:description].present? %>
71
+ <p class="text-xs text-gray-500 dark:text-gray-400 truncate max-w-64 mt-0.5" title="<%= workflow[:description] %>">
72
+ <%= workflow[:description] %>
73
+ </p>
74
+ <% end %>
75
+ </a>
76
+ </td>
77
+
78
+ <!-- Status -->
79
+ <td class="px-4 py-3 whitespace-nowrap">
80
+ <% if workflow[:active] %>
81
+ <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-800 dark:text-green-300">
82
+ Active
83
+ </span>
84
+ <% else %>
85
+ <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">
86
+ Deleted
87
+ </span>
88
+ <% end %>
89
+ </td>
90
+
91
+ <!-- Steps -->
92
+ <td class="px-4 py-3 whitespace-nowrap hidden md:table-cell">
93
+ <% if steps_count > 0 %>
94
+ <button
95
+ type="button"
96
+ @click.stop="expanded = !expanded"
97
+ class="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium bg-emerald-100 dark:bg-emerald-900/50 text-emerald-700 dark:text-emerald-300 hover:bg-emerald-200 dark:hover:bg-emerald-800 transition-colors"
98
+ >
99
+ <%= steps_count %> steps
100
+ <svg class="w-3 h-3 transition-transform duration-200" :class="{ 'rotate-180': expanded }" fill="none" stroke="currentColor" viewBox="0 0 24 24">
101
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
102
+ </svg>
103
+ </button>
104
+ <% else %>
105
+ <span class="text-sm text-gray-400 dark:text-gray-500">-</span>
106
+ <% end %>
107
+ </td>
108
+
109
+ <!-- Executions -->
110
+ <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300 font-mono">
111
+ <%= number_with_delimiter(workflow[:execution_count] || 0) %>
112
+ </td>
113
+
114
+ <!-- Cost -->
115
+ <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300 font-mono hidden md:table-cell">
116
+ $<%= number_with_precision(workflow[:total_cost] || 0, precision: 4) %>
117
+ </td>
118
+
119
+ <!-- Success Rate -->
120
+ <td class="px-4 py-3 whitespace-nowrap">
121
+ <div class="flex items-center gap-1.5">
122
+ <% if success_rate >= 95 %>
123
+ <span class="w-2 h-2 rounded-full bg-green-500"></span>
124
+ <span class="text-sm text-green-600 dark:text-green-400"><%= success_rate %>%</span>
125
+ <% elsif success_rate >= 80 %>
126
+ <span class="w-2 h-2 rounded-full bg-yellow-500"></span>
127
+ <span class="text-sm text-yellow-600 dark:text-yellow-400"><%= success_rate %>%</span>
128
+ <% else %>
129
+ <span class="w-2 h-2 rounded-full bg-red-500"></span>
130
+ <span class="text-sm text-red-600 dark:text-red-400"><%= success_rate %>%</span>
131
+ <% end %>
132
+ </div>
133
+ </td>
134
+
135
+ <!-- Last Executed -->
136
+ <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
137
+ <% if workflow[:last_executed] %>
138
+ <%= time_ago_in_words(workflow[:last_executed]) %> ago
139
+ <% else %>
140
+ <span class="text-gray-400 dark:text-gray-500">Never</span>
141
+ <% end %>
142
+ </td>
143
+ </tr>
144
+
145
+ <!-- Expandable Steps Row -->
146
+ <% if has_steps %>
147
+ <tr
148
+ x-show="expanded"
149
+ x-cloak
150
+ x-transition:enter="transition ease-out duration-200"
151
+ x-transition:enter-start="opacity-0"
152
+ x-transition:enter-end="opacity-100"
153
+ class="bg-gray-50/80 dark:bg-gray-900/50"
154
+ >
155
+ <td colspan="7" class="px-4 py-3">
156
+ <div class="flex flex-wrap gap-2">
157
+ <% workflow[:workflow_children].each_with_index do |child, step_index| %>
158
+ <div class="inline-flex items-center gap-1.5 px-2 py-1 rounded bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-xs">
159
+ <span class="w-4 h-4 flex items-center justify-center rounded-full bg-emerald-100 dark:bg-emerald-900/50 text-emerald-600 dark:text-emerald-300 text-[10px] font-medium">
160
+ <%= step_index + 1 %>
161
+ </span>
162
+ <span class="font-medium text-gray-700 dark:text-gray-300"><%= child[:name] %></span>
163
+ <span class="text-gray-400 dark:text-gray-500">-></span>
164
+ <span class="text-gray-500 dark:text-gray-400 font-mono"><%= child[:agent] %></span>
165
+ <% if child[:optional] %>
166
+ <span class="text-gray-400 dark:text-gray-500 italic">(optional)</span>
167
+ <% end %>
168
+ </div>
169
+ <% end %>
170
+ </div>
171
+ </td>
172
+ </tr>
173
+ <% end %>
174
+ </tbody>
175
+ <% end %>
176
+ </table>
177
+ </div>
178
+ </div>
179
+ <% end %>
@@ -1,7 +1,7 @@
1
1
  <%= render "ruby_llm/agents/shared/breadcrumbs", items: [
2
2
  { label: "Dashboard", path: ruby_llm_agents.root_path },
3
3
  { label: "Workflows", path: ruby_llm_agents.agents_path(tab: "workflows") },
4
- { label: @workflow_type.gsub(/Workflow$|Pipeline$|Parallel$|Router$/, '') }
4
+ { label: @workflow_type.split('::').last }
5
5
  ] %>
6
6
 
7
7
  <!-- Header -->
@@ -11,12 +11,13 @@
11
11
  <div class="flex items-center space-x-3">
12
12
  <%= render "ruby_llm/agents/shared/workflow_type_badge", workflow_type: @workflow_type_kind, size: :md %>
13
13
  <h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">
14
- <% name_parts = @workflow_type.gsub(/Workflow$|Pipeline$|Parallel$|Router$/, '').split('::') %>
14
+ <% name_parts = @workflow_type.split('::') %>
15
15
  <% if name_parts.length > 1 %>
16
16
  <span class="text-gray-400 dark:text-gray-500 font-normal"><%= name_parts[0..-2].join('::') %>::</span>
17
17
  <% end %>
18
18
  <%= name_parts.last %>
19
19
  </h1>
20
+ <%= render "ruby_llm/agents/shared/doc_link" %>
20
21
 
21
22
  <% if @workflow_active %>
22
23
  <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-300">
@@ -40,32 +41,6 @@
40
41
  <%= @config[:description] %>
41
42
  </p>
42
43
  <% end %>
43
-
44
- <!-- Workflow Summary Line -->
45
- <div class="flex items-center gap-4 mt-3 text-sm text-gray-500 dark:text-gray-400">
46
- <% if @workflow_type_kind == "pipeline" && @steps.present? %>
47
- <span><%= @steps.size %> steps</span>
48
- <% elsif @workflow_type_kind == "parallel" && @branches.present? %>
49
- <span><%= @branches.size %> branches</span>
50
- <% elsif @workflow_type_kind == "router" && @routes.present? %>
51
- <span><%= @routes.size %> routes</span>
52
- <% end %>
53
-
54
- <% if @config && @config[:timeout] %>
55
- <span class="text-gray-300 dark:text-gray-600">|</span>
56
- <span>timeout <%= @config[:timeout] %>s</span>
57
- <% end %>
58
-
59
- <% if @config && @config[:max_cost] %>
60
- <span class="text-gray-300 dark:text-gray-600">|</span>
61
- <span>max cost $<%= @config[:max_cost] %></span>
62
- <% end %>
63
-
64
- <% if @workflow_type_kind == "router" && @config && @config[:classifier_model] %>
65
- <span class="text-gray-300 dark:text-gray-600">|</span>
66
- <span>classifier: <%= @config[:classifier_model] %></span>
67
- <% end %>
68
- </div>
69
44
  </div>
70
45
 
71
46
  <div class="text-right">
@@ -94,43 +69,160 @@
94
69
  </div>
95
70
  </div>
96
71
 
72
+ <!-- Workflow Details Panel -->
73
+ <% if @config %>
74
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4 mb-6">
75
+ <div class="flex flex-wrap items-center gap-x-6 gap-y-2">
76
+ <!-- Structure Summary -->
77
+ <div class="flex items-center gap-4 text-sm">
78
+ <% if @steps.present? %>
79
+ <span class="text-gray-700 dark:text-gray-300">
80
+ <span class="font-medium"><%= @steps.size %></span>
81
+ <span class="text-gray-500 dark:text-gray-400">steps</span>
82
+ </span>
83
+ <% end %>
84
+ <% if @parallel_groups.present? && @parallel_groups.any? %>
85
+ <span class="text-purple-600 dark:text-purple-400">
86
+ <span class="font-medium"><%= @parallel_groups.size %></span> parallel
87
+ </span>
88
+ <% end %>
89
+ <% if @config[:has_routing] %>
90
+ <span class="text-amber-600 dark:text-amber-400">routing</span>
91
+ <% end %>
92
+ </div>
93
+
94
+ <!-- Divider -->
95
+ <span class="hidden sm:block w-px h-5 bg-gray-200 dark:bg-gray-700"></span>
96
+
97
+ <!-- Constraints -->
98
+ <div class="flex items-center gap-4 text-sm text-gray-600 dark:text-gray-400">
99
+ <% if @config[:timeout] %>
100
+ <span class="inline-flex items-center gap-1">
101
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
102
+ <%= @config[:timeout] %>s timeout
103
+ </span>
104
+ <% end %>
105
+ <% if @config[:max_cost] %>
106
+ <span class="inline-flex items-center gap-1">
107
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1"/></svg>
108
+ $<%= @config[:max_cost] %> max
109
+ </span>
110
+ <% end %>
111
+ </div>
112
+
113
+ <!-- Divider -->
114
+ <% if @config[:has_lifecycle_hooks] || @config[:has_conditions] || @config[:has_retries] || @config[:has_fallbacks] || @config[:has_input_schema] %>
115
+ <span class="hidden sm:block w-px h-5 bg-gray-200 dark:bg-gray-700"></span>
116
+ <% end %>
117
+
118
+ <!-- Features -->
119
+ <div class="flex flex-wrap items-center gap-2">
120
+ <% if @config[:has_input_schema] %>
121
+ <span class="text-xs px-2 py-1 rounded-full bg-green-100 dark:bg-green-900/50 text-green-700 dark:text-green-300">
122
+ Input Schema
123
+ </span>
124
+ <% end %>
125
+ <% if @config[:has_lifecycle_hooks] %>
126
+ <span class="text-xs px-2 py-1 rounded-full bg-emerald-100 dark:bg-emerald-900/50 text-emerald-700 dark:text-emerald-300">
127
+ Lifecycle Hooks
128
+ </span>
129
+ <% end %>
130
+ <% if @config[:has_conditions] %>
131
+ <span class="text-xs px-2 py-1 rounded-full bg-cyan-100 dark:bg-cyan-900/50 text-cyan-700 dark:text-cyan-300">
132
+ Conditional
133
+ </span>
134
+ <% end %>
135
+ <% if @config[:has_retries] %>
136
+ <span class="text-xs px-2 py-1 rounded-full bg-orange-100 dark:bg-orange-900/50 text-orange-700 dark:text-orange-300">
137
+ Retries
138
+ </span>
139
+ <% end %>
140
+ <% if @config[:has_fallbacks] %>
141
+ <span class="text-xs px-2 py-1 rounded-full bg-yellow-100 dark:bg-yellow-900/50 text-yellow-700 dark:text-yellow-300">
142
+ Fallbacks
143
+ </span>
144
+ <% end %>
145
+ </div>
146
+ </div>
147
+ </div>
148
+ <% end %>
149
+
97
150
  <!-- Workflow Structure -->
98
- <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
151
+ <div id="structure" class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6 scroll-mt-16" x-data="{ expanded: false }">
99
152
  <div class="flex items-center justify-between mb-4">
100
153
  <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">
101
154
  Workflow Structure
102
155
  </h3>
103
156
 
104
- <% if @workflow_type_kind == "parallel" && @config %>
105
- <span class="text-sm text-gray-500 dark:text-gray-400">
106
- fail_fast: <%= @config[:fail_fast] ? 'on' : 'off' %>
157
+ <button @click="expanded = !expanded" class="inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors cursor-pointer">
158
+ <span x-text="expanded ? 'Collapse' : 'Expand'">Collapse</span>
159
+ <svg class="w-4 h-4 transition-transform duration-200" :class="expanded ? 'rotate-180' : ''" fill="none" stroke="currentColor" viewBox="0 0 24 24">
160
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
161
+ </svg>
162
+ </button>
163
+ </div>
164
+
165
+ <!-- Collapsed: Compact Flow Summary -->
166
+ <div x-show="!expanded" x-collapse class="py-4">
167
+ <div class="flex items-center justify-center gap-2 flex-wrap">
168
+ <!-- Start -->
169
+ <span class="px-3 py-1 rounded-full bg-green-100 dark:bg-green-900/50 text-green-700 dark:text-green-300 text-sm font-medium">
170
+ Start
107
171
  </span>
108
- <% elsif @workflow_type_kind == "router" && @config && @config[:classifier_model] %>
109
- <span class="text-sm text-gray-500 dark:text-gray-400">
110
- classifier: <%= @config[:classifier_model] %> @ <%= @config[:classifier_temperature] || 0.0 %>
172
+
173
+ <% (@steps || []).each_with_index do |step, idx| %>
174
+ <svg class="w-4 h-4 text-gray-400 dark:text-gray-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
175
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
176
+ </svg>
177
+
178
+ <%
179
+ step_bg = if step[:workflow]
180
+ "bg-emerald-100 dark:bg-emerald-900/50 text-emerald-700 dark:text-emerald-300"
181
+ elsif step[:iteration]
182
+ "bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300"
183
+ elsif step[:routing]
184
+ "bg-amber-100 dark:bg-amber-900/50 text-amber-700 dark:text-amber-300"
185
+ elsif step[:parallel_group]
186
+ "bg-purple-100 dark:bg-purple-900/50 text-purple-700 dark:text-purple-300"
187
+ else
188
+ "bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300"
189
+ end
190
+ %>
191
+ <span class="px-3 py-1 rounded-full <%= step_bg %> text-sm font-medium">
192
+ <%= step[:name].to_s.titleize.truncate(20) %>
193
+ </span>
194
+ <% end %>
195
+
196
+ <svg class="w-4 h-4 text-gray-400 dark:text-gray-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
197
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
198
+ </svg>
199
+
200
+ <!-- End -->
201
+ <span class="px-3 py-1 rounded-full bg-red-100 dark:bg-red-900/50 text-red-700 dark:text-red-300 text-sm font-medium">
202
+ End
111
203
  </span>
112
- <% end %>
113
- </div>
204
+ </div>
114
205
 
115
- <% case @workflow_type_kind %>
116
- <% when "pipeline" %>
117
- <%= render "ruby_llm/agents/workflows/structure_pipeline", steps: @steps || [] %>
118
- <% when "parallel" %>
119
- <%= render "ruby_llm/agents/workflows/structure_parallel", branches: @branches || [] %>
120
- <% when "router" %>
121
- <%= render "ruby_llm/agents/workflows/structure_router", routes: @routes || [] %>
122
- <% else %>
123
- <p class="text-gray-500 dark:text-gray-400 italic">
124
- Workflow structure unavailable
206
+ <p class="text-center text-xs text-gray-500 dark:text-gray-400 mt-3">
207
+ Click "Expand" to see full diagram with details
125
208
  </p>
126
- <% end %>
209
+ </div>
210
+
211
+ <!-- Expanded: Full Diagram -->
212
+ <div x-show="expanded" x-collapse>
213
+ <%= render "ruby_llm/agents/workflows/workflow_diagram",
214
+ steps: @steps || [],
215
+ parallel_groups: @parallel_groups || [],
216
+ input_schema_fields: @input_schema_fields || {},
217
+ lifecycle_hooks: @lifecycle_hooks || {} %>
218
+ </div>
127
219
  </div>
128
220
 
129
221
  <!-- Stats Grid -->
130
222
  <% success_rate = @stats[:success_rate] || 0 %>
131
223
  <% success_rate_color = success_rate >= 95 ? 'text-green-600' : success_rate >= 80 ? 'text-yellow-600' : 'text-red-600' %>
132
224
 
133
- <div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4 mb-6">
225
+ <div id="stats" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4 mb-6 scroll-mt-16">
134
226
  <%= render "ruby_llm/agents/shared/stat_card",
135
227
  title: "Executions",
136
228
  value: number_with_delimiter(@stats[:count]),
@@ -167,30 +259,33 @@
167
259
  icon_color: "text-purple-500" %>
168
260
  </div>
169
261
 
170
- <!-- Step/Branch/Route Performance -->
262
+ <!-- Step Performance -->
171
263
  <% if @step_stats.present? %>
172
- <%= render "ruby_llm/agents/workflows/step_performance",
173
- workflow_type: @workflow_type_kind,
174
- step_stats: @step_stats,
175
- route_distribution: @route_distribution %>
264
+ <div id="performance" class="scroll-mt-16">
265
+ <%= render "ruby_llm/agents/workflows/step_performance",
266
+ workflow_type: @workflow_type_kind,
267
+ step_stats: @step_stats %>
268
+ </div>
176
269
  <% end %>
177
270
 
178
271
  <!-- Charts Section -->
179
- <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
180
- <!-- Executions Over Time -->
181
- <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 overflow-hidden">
182
- <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
183
- Executions (30 days)
184
- </h3>
185
- <div id="executions-chart" style="height: 220px;"></div>
186
- </div>
272
+ <div id="trends" class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6 scroll-mt-16">
273
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
274
+ Trends (30 days)
275
+ </h3>
187
276
 
188
- <!-- Cost Over Time -->
189
- <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 overflow-hidden">
190
- <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
191
- Cost (30 days)
192
- </h3>
193
- <div id="cost-chart" style="height: 220px;"></div>
277
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
278
+ <!-- Executions Over Time -->
279
+ <div>
280
+ <h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Executions</h4>
281
+ <div id="executions-chart" style="height: 200px;"></div>
282
+ </div>
283
+
284
+ <!-- Cost Over Time -->
285
+ <div>
286
+ <h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Cost</h4>
287
+ <div id="cost-chart" style="height: 200px;"></div>
288
+ </div>
194
289
  </div>
195
290
  </div>
196
291
 
@@ -258,79 +353,9 @@
258
353
  </div>
259
354
  <% end %>
260
355
 
261
- <% if @config %>
262
- <!-- Configuration -->
263
- <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
264
- <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
265
- Configuration
266
- </h3>
267
-
268
- <!-- Basic Configuration -->
269
- <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-3">
270
- Basic
271
- </p>
272
-
273
- <div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
274
- <div>
275
- <p class="text-sm text-gray-500 dark:text-gray-400">Version</p>
276
- <p class="font-medium text-gray-900 dark:text-gray-100">
277
- <%= @config[:version] || 'N/A' %>
278
- </p>
279
- </div>
280
-
281
- <div>
282
- <p class="text-sm text-gray-500 dark:text-gray-400">Timeout</p>
283
- <p class="font-medium text-gray-900 dark:text-gray-100">
284
- <%= @config[:timeout] ? "#{@config[:timeout]}s" : 'N/A' %>
285
- </p>
286
- </div>
287
-
288
- <div>
289
- <p class="text-sm text-gray-500 dark:text-gray-400">Max Cost</p>
290
- <p class="font-medium text-gray-900 dark:text-gray-100">
291
- <%= @config[:max_cost] ? "$#{@config[:max_cost]}" : 'N/A' %>
292
- </p>
293
- </div>
294
-
295
- <% if @workflow_type_kind == "parallel" %>
296
- <div>
297
- <p class="text-sm text-gray-500 dark:text-gray-400">Fail Fast</p>
298
- <p class="font-medium text-gray-900 dark:text-gray-100">
299
- <%= @config[:fail_fast] ? 'Yes' : 'No' %>
300
- </p>
301
- </div>
302
- <% end %>
303
- </div>
304
-
305
- <% if @workflow_type_kind == "router" && @config[:classifier_model] %>
306
- <!-- Router Settings -->
307
- <div class="border-t border-gray-100 dark:border-gray-700 pt-4">
308
- <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-3">
309
- Router Settings
310
- </p>
311
-
312
- <div class="grid grid-cols-2 md:grid-cols-4 gap-4">
313
- <div>
314
- <p class="text-sm text-gray-500 dark:text-gray-400">Classifier Model</p>
315
- <p class="font-medium text-gray-900 dark:text-gray-100">
316
- <%= @config[:classifier_model] %>
317
- </p>
318
- </div>
319
-
320
- <div>
321
- <p class="text-sm text-gray-500 dark:text-gray-400">Temperature</p>
322
- <p class="font-medium text-gray-900 dark:text-gray-100">
323
- <%= @config[:classifier_temperature] || 0.0 %>
324
- </p>
325
- </div>
326
- </div>
327
- </div>
328
- <% end %>
329
- </div>
330
- <% end %>
331
356
 
332
357
  <!-- Executions -->
333
- <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
358
+ <div id="executions" class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 scroll-mt-16">
334
359
  <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
335
360
  Executions
336
361
  </h3>
data/config/routes.rb CHANGED
@@ -5,7 +5,7 @@ RubyLLM::Agents::Engine.routes.draw do
5
5
  get "chart_data", to: "dashboard#chart_data"
6
6
 
7
7
  resources :agents, only: [:index, :show]
8
- resources :workflows, only: [:show]
8
+ resources :workflows, only: [:index, :show]
9
9
 
10
10
  resources :executions, only: [:index, :show] do
11
11
  collection do
@@ -7,10 +7,9 @@ module RubyLlmAgents
7
7
  #
8
8
  # Usage:
9
9
  # rails generate ruby_llm_agents:agent SearchIntent query:required limit:10
10
- # rails generate ruby_llm_agents:agent SearchIntent query:required --root=ai
11
10
  #
12
11
  # This will create:
13
- # - app/{root}/agents/search_intent_agent.rb
12
+ # - app/agents/search_intent_agent.rb
14
13
  #
15
14
  # Parameter syntax:
16
15
  # name - Optional parameter
@@ -28,18 +27,9 @@ module RubyLlmAgents
28
27
  desc: "The temperature setting (0.0-1.0)"
29
28
  class_option :cache, type: :string, default: nil,
30
29
  desc: "Cache TTL (e.g., '1.hour', '30.minutes')"
31
- class_option :root,
32
- type: :string,
33
- default: nil,
34
- desc: "Root directory name (default: uses config or 'llm')"
35
- class_option :namespace,
36
- type: :string,
37
- default: nil,
38
- desc: "Root namespace (default: camelized root or config)"
39
30
 
40
31
  def ensure_base_class_and_skill_file
41
- @root_namespace = root_namespace
42
- agents_dir = "app/#{root_directory}/agents"
32
+ agents_dir = "app/agents"
43
33
 
44
34
  # Create directory if needed
45
35
  empty_directory agents_dir
@@ -58,17 +48,15 @@ module RubyLlmAgents
58
48
  end
59
49
 
60
50
  def create_agent_file
61
- # Support nested paths: "chat/support" -> "app/{root}/agents/chat/support_agent.rb"
62
- # Rails' class_name handles namespacing: "chat/support" -> "Chat::Support"
63
- @root_namespace = root_namespace
51
+ # Support nested paths: "chat/support" -> "app/agents/chat/support_agent.rb"
64
52
  agent_path = name.underscore
65
- template "agent.rb.tt", "app/#{root_directory}/agents/#{agent_path}_agent.rb"
53
+ template "agent.rb.tt", "app/agents/#{agent_path}_agent.rb"
66
54
  end
67
55
 
68
56
  def show_usage
69
- # Build full class name from path (e.g., "chat/support" -> "Chat::Support")
57
+ # Build full class name from path (e.g., "chat/support" -> "Chat::SupportAgent")
70
58
  agent_class_name = name.split("/").map(&:camelize).join("::")
71
- full_class_name = "#{root_namespace}::#{agent_class_name}Agent"
59
+ full_class_name = "#{agent_class_name}Agent"
72
60
  say ""
73
61
  say "Agent #{full_class_name} created!", :green
74
62
  say ""
@@ -80,24 +68,6 @@ module RubyLlmAgents
80
68
 
81
69
  private
82
70
 
83
- def root_directory
84
- @root_directory ||= options[:root] || RubyLLM::Agents.configuration.root_directory
85
- end
86
-
87
- def root_namespace
88
- @root_namespace ||= options[:namespace] || camelize(root_directory)
89
- end
90
-
91
- def camelize(str)
92
- # Handle special cases for common abbreviations
93
- return "AI" if str.downcase == "ai"
94
- return "ML" if str.downcase == "ml"
95
- return "LLM" if str.downcase == "llm"
96
-
97
- # Standard camelization
98
- str.split(/[-_]/).map(&:capitalize).join
99
- end
100
-
101
71
  def parsed_params
102
72
  @parsed_params ||= params.map do |param|
103
73
  name, modifier = param.split(":")