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,14 +1,34 @@
1
1
  <div class="mb-6">
2
- <h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">Agents & Workflows</h1>
3
- <p class="text-gray-500 dark:text-gray-400 mt-1">All available agents and workflows with execution statistics</p>
2
+ <div class="flex items-center gap-2">
3
+ <h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">Agents</h1>
4
+ <%= render "ruby_llm/agents/shared/doc_link" %>
5
+ </div>
6
+ <p class="text-gray-500 dark:text-gray-400 mt-1">All available agents with execution statistics</p>
4
7
  </div>
5
8
 
9
+ <%
10
+ audio_count = @agents_by_type[:speaker].size + @agents_by_type[:transcriber].size
11
+ %>
6
12
  <div x-data="{
7
- activeTab: '<%= params[:tab].presence || 'agents' %>',
8
13
  activeSubTab: <%= params[:subtab].present? ? "'#{params[:subtab]}'" : 'null' %>,
14
+ counts: {
15
+ all: <%= @agent_count %>,
16
+ agent: <%= @agents_by_type[:agent].size %>,
17
+ embedder: <%= @agents_by_type[:embedder].size %>,
18
+ moderator: <%= @agents_by_type[:moderator].size %>,
19
+ speaker: <%= @agents_by_type[:speaker].size %>,
20
+ transcriber: <%= @agents_by_type[:transcriber].size %>,
21
+ audio: <%= audio_count %>,
22
+ image_generator: <%= @agents_by_type[:image_generator].size %>,
23
+ deleted: <%= @deleted_count %>
24
+ },
25
+ get currentCount() {
26
+ if (this.activeSubTab === null) return this.counts.all;
27
+ if (this.activeSubTab === 'audio') return this.counts.audio;
28
+ return this.counts[this.activeSubTab] || 0;
29
+ },
9
30
  updateUrl() {
10
31
  const url = new URL(window.location);
11
- url.searchParams.set('tab', this.activeTab);
12
32
  if (this.activeSubTab) {
13
33
  url.searchParams.set('subtab', this.activeSubTab);
14
34
  } else {
@@ -17,41 +37,8 @@
17
37
  history.replaceState({}, '', url);
18
38
  }
19
39
  }">
20
- <!-- Tab Navigation -->
21
- <div class="border-b border-gray-200 dark:border-gray-700 mb-6">
22
- <nav class="-mb-px flex space-x-6" aria-label="Tabs">
23
- <!-- Agents Tab -->
24
- <button type="button" @click="activeTab = 'agents'; activeSubTab = null; updateUrl()"
25
- :class="activeTab === 'agents' ? 'border-blue-500 text-blue-600 dark:text-blue-400' : 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300'"
26
- class="whitespace-nowrap py-3 px-1 border-b-2 font-medium text-sm flex items-center gap-2 transition-colors">
27
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
28
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
29
- </svg>
30
- Agents
31
- <span class="inline-flex items-center justify-center px-2 py-0.5 rounded-full text-xs font-medium"
32
- :class="activeTab === 'agents' ? 'bg-blue-100 dark:bg-blue-900/50 text-blue-600 dark:text-blue-300' : 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400'">
33
- <%= @agent_count %>
34
- </span>
35
- </button>
36
-
37
- <!-- Workflows Tab -->
38
- <button type="button" @click="activeTab = 'workflows'; activeSubTab = null; updateUrl()"
39
- :class="activeTab === 'workflows' ? 'border-blue-500 text-blue-600 dark:text-blue-400' : 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300'"
40
- class="whitespace-nowrap py-3 px-1 border-b-2 font-medium text-sm flex items-center gap-2 transition-colors">
41
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
42
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zm0 8a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zm12 0a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z"/>
43
- </svg>
44
- Workflows
45
- <span class="inline-flex items-center justify-center px-2 py-0.5 rounded-full text-xs font-medium"
46
- :class="activeTab === 'workflows' ? 'bg-blue-100 dark:bg-blue-900/50 text-blue-600 dark:text-blue-300' : 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400'">
47
- <%= @workflow_count %>
48
- </span>
49
- </button>
50
- </nav>
51
- </div>
52
-
53
40
  <!-- Agent Sub-tabs -->
54
- <div x-show="activeTab === 'agents'" x-cloak class="mb-4">
41
+ <div class="mb-4">
55
42
  <div class="flex flex-wrap gap-2">
56
43
  <button type="button" @click="activeSubTab = null; updateUrl()"
57
44
  :class="activeSubTab === null ? 'bg-gray-900 dark:bg-gray-100 text-white dark:text-gray-900' : 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'"
@@ -73,7 +60,6 @@
73
60
  class="px-3 py-1.5 rounded-md text-sm font-medium transition-colors">
74
61
  Moderators (<%= @agents_by_type[:moderator].size %>)
75
62
  </button>
76
- <% audio_count = @agents_by_type[:speaker].size + @agents_by_type[:transcriber].size %>
77
63
  <% if audio_count > 0 %>
78
64
  <button type="button" @click="activeSubTab = 'audio'; updateUrl()"
79
65
  :class="activeSubTab === 'audio' ? 'bg-pink-600 text-white' : 'bg-pink-100 dark:bg-pink-900/50 text-pink-700 dark:text-pink-300 hover:bg-pink-200 dark:hover:bg-pink-800'"
@@ -88,71 +74,256 @@
88
74
  Image Generators (<%= @agents_by_type[:image_generator].size %>)
89
75
  </button>
90
76
  <% end %>
77
+ <% if @deleted_count > 0 %>
78
+ <button type="button" @click="activeSubTab = 'deleted'; updateUrl()"
79
+ :class="activeSubTab === 'deleted' ? 'bg-gray-600 text-white' : 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600'"
80
+ class="px-3 py-1.5 rounded-md text-sm font-medium transition-colors">
81
+ Deleted (<%= @deleted_count %>)
82
+ </button>
83
+ <% end %>
91
84
  </div>
92
85
  </div>
93
86
 
94
- <!-- Workflow Sub-tabs -->
95
- <div x-show="activeTab === 'workflows'" x-cloak class="mb-4">
96
- <div class="flex flex-wrap gap-2">
97
- <button type="button" @click="activeSubTab = null; updateUrl()"
98
- :class="activeSubTab === null ? 'bg-gray-900 dark:bg-gray-100 text-white dark:text-gray-900' : 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'"
99
- class="px-3 py-1.5 rounded-md text-sm font-medium transition-colors">
100
- All (<%= @workflow_count %>)
101
- </button>
102
- <button type="button" @click="activeSubTab = 'pipeline'; updateUrl()"
103
- :class="activeSubTab === 'pipeline' ? 'bg-indigo-600 text-white' : 'bg-indigo-100 dark:bg-indigo-900/50 text-indigo-700 dark:text-indigo-300 hover:bg-indigo-200 dark:hover:bg-indigo-800'"
104
- class="px-3 py-1.5 rounded-md text-sm font-medium transition-colors inline-flex items-center gap-1.5">
105
- <span aria-hidden="true">-></span> Pipeline (<%= @workflows_by_type[:pipeline].size %>)
106
- </button>
107
- <button type="button" @click="activeSubTab = 'parallel'; updateUrl()"
108
- :class="activeSubTab === 'parallel' ? 'bg-cyan-600 text-white' : 'bg-cyan-100 dark:bg-cyan-900/50 text-cyan-700 dark:text-cyan-300 hover:bg-cyan-200 dark:hover:bg-cyan-800'"
109
- class="px-3 py-1.5 rounded-md text-sm font-medium transition-colors inline-flex items-center gap-1.5">
110
- <span aria-hidden="true">//</span> Parallel (<%= @workflows_by_type[:parallel].size %>)
111
- </button>
112
- <button type="button" @click="activeSubTab = 'router'; updateUrl()"
113
- :class="activeSubTab === 'router' ? 'bg-amber-600 text-white' : 'bg-amber-100 dark:bg-amber-900/50 text-amber-700 dark:text-amber-300 hover:bg-amber-200 dark:hover:bg-amber-800'"
114
- class="px-3 py-1.5 rounded-md text-sm font-medium transition-colors inline-flex items-center gap-1.5">
115
- <span aria-hidden="true">&lt;&gt;</span> Router (<%= @workflows_by_type[:router].size %>)
116
- </button>
117
- </div>
118
- </div>
87
+ <!-- Agents Table -->
88
+ <% if @agents.empty? && @deleted_agents.empty? %>
89
+ <%= render "ruby_llm/agents/agents/empty_state", type: "agents" %>
90
+ <% else %>
91
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
92
+ <div class="overflow-x-auto">
93
+ <table class="w-full divide-y divide-gray-200 dark:divide-gray-700">
94
+ <thead class="bg-gray-50 dark:bg-gray-900">
95
+ <tr>
96
+ <%= render "ruby_llm/agents/agents/sortable_header",
97
+ column: "name", label: "Name",
98
+ current_sort: @sort_params[:column], current_direction: @sort_params[:direction] %>
119
99
 
120
- <!-- Agents Content -->
121
- <div x-show="activeTab === 'agents'">
122
- <% if @agents.empty? %>
123
- <%= render "ruby_llm/agents/agents/empty_state", type: "agents" %>
124
- <% else %>
125
- <div class="space-y-3 sm:space-y-4">
126
- <% @agents.each do |agent| %>
127
- <%
128
- # Determine if this agent should be shown based on sub-tab
129
- # 'audio' sub-tab shows both speakers and transcribers
130
- show_condition = if agent[:agent_type] == 'speaker' || agent[:agent_type] == 'transcriber'
131
- "activeSubTab === null || activeSubTab === 'audio' || activeSubTab === '#{agent[:agent_type]}'"
132
- else
133
- "activeSubTab === null || activeSubTab === '#{agent[:agent_type]}'"
134
- end
135
- %>
136
- <div x-show="<%= show_condition %>">
137
- <%= render partial: "ruby_llm/agents/agents/agent", locals: { agent: agent, current_tab: params[:tab] || 'agents', current_subtab: params[:subtab] } %>
138
- </div>
139
- <% end %>
140
- </div>
141
- <% end %>
142
- </div>
100
+ <%= render "ruby_llm/agents/agents/sortable_header",
101
+ column: "agent_type", label: "Type",
102
+ current_sort: @sort_params[:column], current_direction: @sort_params[:direction] %>
103
+
104
+ <th scope="col" class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
105
+ Status
106
+ </th>
107
+
108
+ <%= render "ruby_llm/agents/agents/sortable_header",
109
+ column: "model", label: "Model",
110
+ current_sort: @sort_params[:column], current_direction: @sort_params[:direction],
111
+ th_class: "hidden lg:table-cell" %>
143
112
 
144
- <!-- Workflows Content -->
145
- <div x-show="activeTab === 'workflows'" x-cloak>
146
- <% if @workflows.empty? %>
147
- <%= render "ruby_llm/agents/agents/empty_state", type: "workflows" %>
148
- <% else %>
149
- <div class="space-y-3 sm:space-y-4">
150
- <% @workflows.each do |workflow| %>
151
- <div x-show="activeSubTab === null || activeSubTab === '<%= workflow[:workflow_type] %>'">
152
- <%= render partial: "ruby_llm/agents/agents/workflow", locals: { workflow: workflow, current_tab: params[:tab] || 'workflows', current_subtab: params[:subtab] } %>
153
- </div>
154
- <% end %>
113
+ <%= render "ruby_llm/agents/agents/sortable_header",
114
+ column: "execution_count", label: "Executions",
115
+ current_sort: @sort_params[:column], current_direction: @sort_params[:direction] %>
116
+
117
+ <%= render "ruby_llm/agents/agents/sortable_header",
118
+ column: "total_cost", label: "Cost",
119
+ current_sort: @sort_params[:column], current_direction: @sort_params[:direction],
120
+ th_class: "hidden md:table-cell" %>
121
+
122
+ <%= render "ruby_llm/agents/agents/sortable_header",
123
+ column: "success_rate", label: "Success",
124
+ current_sort: @sort_params[:column], current_direction: @sort_params[:direction] %>
125
+
126
+ <%= render "ruby_llm/agents/agents/sortable_header",
127
+ column: "last_executed", label: "Last Run",
128
+ current_sort: @sort_params[:column], current_direction: @sort_params[:direction] %>
129
+ </tr>
130
+ </thead>
131
+
132
+ <tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-100/50 dark:divide-gray-700/50">
133
+ <% @agents.each_with_index do |agent, index| %>
134
+ <%
135
+ # Determine if this agent should be shown based on sub-tab (exclude deleted tab)
136
+ show_condition = if agent[:agent_type] == 'speaker' || agent[:agent_type] == 'transcriber'
137
+ "activeSubTab !== 'deleted' && (activeSubTab === null || activeSubTab === 'audio' || activeSubTab === '#{agent[:agent_type]}')"
138
+ else
139
+ "activeSubTab !== 'deleted' && (activeSubTab === null || activeSubTab === '#{agent[:agent_type]}')"
140
+ end
141
+ row_bg = index.even? ? '' : 'bg-gray-50/50 dark:bg-gray-900/30'
142
+ success_rate = agent[:success_rate] || 0
143
+ %>
144
+ <tr
145
+ x-show="<%= show_condition %>"
146
+ class="<%= row_bg %> hover:bg-blue-50/50 dark:hover:bg-blue-900/20 transition-colors cursor-pointer"
147
+ onclick="window.location='<%= ruby_llm_agents.agent_path(ERB::Util.url_encode(agent[:name]), subtab: params[:subtab]) %>'"
148
+ >
149
+ <!-- Name -->
150
+ <td class="px-4 py-3">
151
+ <div class="flex items-center gap-2">
152
+ <span class="text-sm font-medium text-gray-900 dark:text-gray-100">
153
+ <%= agent[:name].split('::').last.gsub(/Agent$/, '') %>
154
+ </span>
155
+ <span class="hidden lg:inline text-xs text-gray-400 dark:text-gray-500">v<%= agent[:version] %></span>
156
+ </div>
157
+ <% if agent[:description].present? %>
158
+ <p class="text-xs text-gray-500 dark:text-gray-400 truncate max-w-64 mt-0.5" title="<%= agent[:description] %>">
159
+ <%= agent[:description] %>
160
+ </p>
161
+ <% end %>
162
+ </td>
163
+
164
+ <!-- Type -->
165
+ <td class="px-4 py-3 whitespace-nowrap">
166
+ <span class="text-sm text-gray-600 dark:text-gray-400">
167
+ <%= agent[:agent_type]&.titleize || 'Agent' %>
168
+ </span>
169
+ </td>
170
+
171
+ <!-- Status -->
172
+ <td class="px-4 py-3 whitespace-nowrap">
173
+ <% if agent[:active] %>
174
+ <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">
175
+ Active
176
+ </span>
177
+ <% else %>
178
+ <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">
179
+ Deleted
180
+ </span>
181
+ <% end %>
182
+ </td>
183
+
184
+ <!-- Model -->
185
+ <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400 hidden lg:table-cell">
186
+ <span title="<%= agent[:model] %>">
187
+ <%= agent[:model]&.split("/")&.last || agent[:model] || "-" %>
188
+ </span>
189
+ </td>
190
+
191
+ <!-- Executions -->
192
+ <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300 font-mono">
193
+ <%= number_with_delimiter(agent[:execution_count] || 0) %>
194
+ </td>
195
+
196
+ <!-- Cost -->
197
+ <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300 font-mono hidden md:table-cell">
198
+ $<%= number_with_precision(agent[:total_cost] || 0, precision: 4) %>
199
+ </td>
200
+
201
+ <!-- Success Rate -->
202
+ <td class="px-4 py-3 whitespace-nowrap">
203
+ <div class="flex items-center gap-1.5">
204
+ <% if success_rate >= 95 %>
205
+ <span class="w-2 h-2 rounded-full bg-green-500"></span>
206
+ <span class="text-sm text-green-600 dark:text-green-400"><%= success_rate %>%</span>
207
+ <% elsif success_rate >= 80 %>
208
+ <span class="w-2 h-2 rounded-full bg-yellow-500"></span>
209
+ <span class="text-sm text-yellow-600 dark:text-yellow-400"><%= success_rate %>%</span>
210
+ <% else %>
211
+ <span class="w-2 h-2 rounded-full bg-red-500"></span>
212
+ <span class="text-sm text-red-600 dark:text-red-400"><%= success_rate %>%</span>
213
+ <% end %>
214
+ </div>
215
+ </td>
216
+
217
+ <!-- Last Executed -->
218
+ <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
219
+ <% if agent[:last_executed] %>
220
+ <%= time_ago_in_words(agent[:last_executed]) %> ago
221
+ <% else %>
222
+ <span class="text-gray-400 dark:text-gray-500">Never</span>
223
+ <% end %>
224
+ </td>
225
+ </tr>
226
+ <% end %>
227
+
228
+ <!-- Deleted Agents -->
229
+ <% @deleted_agents.each_with_index do |agent, index| %>
230
+ <%
231
+ row_bg = index.even? ? '' : 'bg-gray-50/50 dark:bg-gray-900/30'
232
+ success_rate = agent[:success_rate] || 0
233
+ %>
234
+ <tr
235
+ x-show="activeSubTab === 'deleted'"
236
+ class="<%= row_bg %> hover:bg-blue-50/50 dark:hover:bg-blue-900/20 transition-colors cursor-pointer"
237
+ onclick="window.location='<%= ruby_llm_agents.agent_path(ERB::Util.url_encode(agent[:name]), subtab: params[:subtab]) %>'"
238
+ >
239
+ <!-- Name -->
240
+ <td class="px-4 py-3">
241
+ <div class="flex items-center gap-2">
242
+ <span class="text-sm font-medium text-gray-900 dark:text-gray-100">
243
+ <%= agent[:name].split('::').last.gsub(/Agent$/, '') %>
244
+ </span>
245
+ <span class="hidden lg:inline text-xs text-gray-400 dark:text-gray-500">v<%= agent[:version] %></span>
246
+ </div>
247
+ <% if agent[:description].present? %>
248
+ <p class="text-xs text-gray-500 dark:text-gray-400 truncate max-w-48 mt-0.5" title="<%= agent[:description] %>">
249
+ <%= agent[:description] %>
250
+ </p>
251
+ <% end %>
252
+ </td>
253
+
254
+ <!-- Type -->
255
+ <td class="px-4 py-3 whitespace-nowrap">
256
+ <span class="text-sm text-gray-600 dark:text-gray-400">
257
+ <%= agent[:agent_type]&.titleize || 'Agent' %>
258
+ </span>
259
+ </td>
260
+
261
+ <!-- Status -->
262
+ <td class="px-4 py-3 whitespace-nowrap">
263
+ <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">
264
+ Deleted
265
+ </span>
266
+ </td>
267
+
268
+ <!-- Model -->
269
+ <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400 hidden lg:table-cell">
270
+ <span title="<%= agent[:model] %>">
271
+ <%= agent[:model]&.split("/")&.last || agent[:model] || "-" %>
272
+ </span>
273
+ </td>
274
+
275
+ <!-- Executions -->
276
+ <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300 font-mono">
277
+ <%= number_with_delimiter(agent[:execution_count] || 0) %>
278
+ </td>
279
+
280
+ <!-- Cost -->
281
+ <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300 font-mono hidden md:table-cell">
282
+ $<%= number_with_precision(agent[:total_cost] || 0, precision: 4) %>
283
+ </td>
284
+
285
+ <!-- Success Rate -->
286
+ <td class="px-4 py-3 whitespace-nowrap">
287
+ <div class="flex items-center gap-1.5">
288
+ <% if success_rate >= 95 %>
289
+ <span class="w-2 h-2 rounded-full bg-green-500"></span>
290
+ <span class="text-sm text-green-600 dark:text-green-400"><%= success_rate %>%</span>
291
+ <% elsif success_rate >= 80 %>
292
+ <span class="w-2 h-2 rounded-full bg-yellow-500"></span>
293
+ <span class="text-sm text-yellow-600 dark:text-yellow-400"><%= success_rate %>%</span>
294
+ <% else %>
295
+ <span class="w-2 h-2 rounded-full bg-red-500"></span>
296
+ <span class="text-sm text-red-600 dark:text-red-400"><%= success_rate %>%</span>
297
+ <% end %>
298
+ </div>
299
+ </td>
300
+
301
+ <!-- Last Executed -->
302
+ <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
303
+ <% if agent[:last_executed] %>
304
+ <%= time_ago_in_words(agent[:last_executed]) %> ago
305
+ <% else %>
306
+ <span class="text-gray-400 dark:text-gray-500">Never</span>
307
+ <% end %>
308
+ </td>
309
+ </tr>
310
+ <% end %>
311
+
312
+ <!-- Empty State -->
313
+ <tr x-show="currentCount === 0" x-cloak>
314
+ <td colspan="8" class="px-6 py-12 text-center">
315
+ <div class="text-gray-400 dark:text-gray-500">
316
+ <svg class="mx-auto h-12 w-12 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
317
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
318
+ </svg>
319
+ <p class="text-lg font-medium text-gray-500 dark:text-gray-400">No agents in this category</p>
320
+ <p class="text-sm mt-1">Try selecting a different filter</p>
321
+ </div>
322
+ </td>
323
+ </tr>
324
+ </tbody>
325
+ </table>
155
326
  </div>
156
- <% end %>
157
- </div>
327
+ </div>
328
+ <% end %>
158
329
  </div>
@@ -12,6 +12,7 @@
12
12
  <h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">
13
13
  <%= @agent_type.gsub(/Agent$/, '') %>
14
14
  </h1>
15
+ <%= render "ruby_llm/agents/shared/doc_link" %>
15
16
 
16
17
  <% if @agent_active %>
17
18
  <span
@@ -0,0 +1,107 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title><%= @title %></title>
7
+ <style>
8
+ body {
9
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
10
+ line-height: 1.6;
11
+ color: #333;
12
+ max-width: 600px;
13
+ margin: 0 auto;
14
+ padding: 20px;
15
+ }
16
+ .header {
17
+ border-left: 4px solid <%= @color %>;
18
+ padding-left: 16px;
19
+ margin-bottom: 24px;
20
+ }
21
+ .title {
22
+ font-size: 24px;
23
+ font-weight: 600;
24
+ margin: 0 0 8px 0;
25
+ }
26
+ .severity {
27
+ display: inline-block;
28
+ padding: 4px 12px;
29
+ border-radius: 4px;
30
+ font-size: 12px;
31
+ font-weight: 600;
32
+ text-transform: uppercase;
33
+ color: white;
34
+ background-color: <%= @color %>;
35
+ }
36
+ .timestamp {
37
+ color: #666;
38
+ font-size: 14px;
39
+ margin-top: 8px;
40
+ }
41
+ .details {
42
+ background-color: #f8f9fa;
43
+ border-radius: 8px;
44
+ padding: 16px;
45
+ margin-top: 24px;
46
+ }
47
+ .details-title {
48
+ font-size: 14px;
49
+ font-weight: 600;
50
+ text-transform: uppercase;
51
+ color: #666;
52
+ margin: 0 0 12px 0;
53
+ }
54
+ .detail-row {
55
+ display: flex;
56
+ padding: 8px 0;
57
+ border-bottom: 1px solid #e9ecef;
58
+ }
59
+ .detail-row:last-child {
60
+ border-bottom: none;
61
+ }
62
+ .detail-key {
63
+ font-weight: 500;
64
+ color: #495057;
65
+ min-width: 140px;
66
+ }
67
+ .detail-value {
68
+ color: #212529;
69
+ word-break: break-word;
70
+ }
71
+ .footer {
72
+ margin-top: 32px;
73
+ padding-top: 16px;
74
+ border-top: 1px solid #e9ecef;
75
+ font-size: 12px;
76
+ color: #666;
77
+ }
78
+ </style>
79
+ </head>
80
+ <body>
81
+ <div class="header">
82
+ <h1 class="title"><%= @title %></h1>
83
+ <span class="severity"><%= @severity %></span>
84
+ <div class="timestamp">
85
+ <%= @timestamp.strftime("%B %d, %Y at %I:%M %p %Z") %>
86
+ </div>
87
+ </div>
88
+
89
+ <% if @payload.present? %>
90
+ <div class="details">
91
+ <h2 class="details-title">Event Details</h2>
92
+ <% @payload.except(:event).each do |key, value| %>
93
+ <div class="detail-row">
94
+ <div class="detail-key"><%= key.to_s.titleize %></div>
95
+ <div class="detail-value"><%= value.is_a?(Hash) ? value.to_json : value %></div>
96
+ </div>
97
+ <% end %>
98
+ </div>
99
+ <% end %>
100
+
101
+ <div class="footer">
102
+ This alert was sent by RubyLLM::Agents.
103
+ <br>
104
+ Event type: <code><%= @event %></code>
105
+ </div>
106
+ </body>
107
+ </html>
@@ -0,0 +1,18 @@
1
+ RubyLLM::Agents Alert
2
+ =====================
3
+
4
+ <%= @title %>
5
+ Severity: <%= @severity %>
6
+ Time: <%= @timestamp.strftime("%B %d, %Y at %I:%M %p %Z") %>
7
+
8
+ <% if @payload.present? %>
9
+ Event Details
10
+ -------------
11
+ <% @payload.except(:event).each do |key, value| %>
12
+ <%= key.to_s.titleize %>: <%= value.is_a?(Hash) ? value.to_json : value %>
13
+ <% end %>
14
+ <% end %>
15
+
16
+ ---
17
+ This alert was sent by RubyLLM::Agents.
18
+ Event type: <%= @event %>
@@ -13,7 +13,10 @@
13
13
  </svg>
14
14
  </div>
15
15
  <div>
16
- <h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">API Configuration</h1>
16
+ <div class="flex items-center gap-2">
17
+ <h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">API Configuration</h1>
18
+ <%= render "ruby_llm/agents/shared/doc_link" %>
19
+ </div>
17
20
  <p class="text-sm text-gray-500 dark:text-gray-400">
18
21
  Manage API keys and connection settings for LLM providers
19
22
  </p>