ruby_llm-agents 0.3.4 → 0.3.5
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.
- checksums.yaml +4 -4
- data/README.md +132 -1263
- data/app/controllers/concerns/ruby_llm/agents/filterable.rb +5 -1
- data/app/controllers/concerns/ruby_llm/agents/paginatable.rb +2 -1
- data/app/controllers/ruby_llm/agents/agents_controller.rb +21 -2
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +80 -16
- data/app/controllers/ruby_llm/agents/executions_controller.rb +83 -5
- data/app/models/ruby_llm/agents/execution/analytics.rb +17 -27
- data/app/models/ruby_llm/agents/execution/scopes.rb +25 -0
- data/app/models/ruby_llm/agents/execution/workflow.rb +299 -0
- data/app/models/ruby_llm/agents/execution.rb +9 -1
- data/app/models/ruby_llm/agents/tenant_budget.rb +165 -0
- data/app/services/ruby_llm/agents/agent_registry.rb +118 -7
- data/app/views/layouts/{rubyllm → ruby_llm}/agents/application.html.erb +91 -29
- data/app/views/ruby_llm/agents/agents/_empty_state.html.erb +23 -0
- data/app/views/ruby_llm/agents/agents/_workflow.html.erb +125 -0
- data/app/views/ruby_llm/agents/agents/index.html.erb +93 -0
- data/app/views/{rubyllm → ruby_llm}/agents/agents/show.html.erb +77 -20
- data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +112 -0
- data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_execution_item.html.erb +7 -4
- data/app/views/ruby_llm/agents/dashboard/_tenant_budget.html.erb +115 -0
- data/app/views/{rubyllm → ruby_llm}/agents/dashboard/index.html.erb +9 -6
- data/app/views/{rubyllm → ruby_llm}/agents/executions/_execution.html.erb +1 -1
- data/app/views/{rubyllm → ruby_llm}/agents/executions/_filters.html.erb +39 -11
- data/app/views/{rubyllm → ruby_llm}/agents/executions/_list.html.erb +19 -9
- data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +101 -0
- data/app/views/ruby_llm/agents/executions/index.html.erb +88 -0
- data/app/views/{rubyllm → ruby_llm}/agents/executions/show.html.erb +137 -126
- data/app/views/{rubyllm → ruby_llm}/agents/shared/_breadcrumbs.html.erb +2 -2
- data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +251 -0
- data/app/views/{rubyllm → ruby_llm}/agents/shared/_filter_dropdown.html.erb +1 -1
- data/app/views/{rubyllm → ruby_llm}/agents/shared/_select_dropdown.html.erb +1 -1
- data/app/views/{rubyllm → ruby_llm}/agents/shared/_status_badge.html.erb +1 -1
- data/app/views/{rubyllm → ruby_llm}/agents/shared/_status_dot.html.erb +1 -1
- data/app/views/ruby_llm/agents/shared/_tenant_filter.html.erb +26 -0
- data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +61 -0
- data/lib/generators/ruby_llm_agents/multi_tenancy_generator.rb +97 -0
- data/lib/generators/ruby_llm_agents/templates/add_attempts_migration.rb.tt +3 -3
- data/lib/generators/ruby_llm_agents/templates/add_tenant_to_executions_migration.rb.tt +23 -0
- data/lib/generators/ruby_llm_agents/templates/add_tool_calls_migration.rb.tt +2 -2
- data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +38 -0
- data/lib/generators/ruby_llm_agents/templates/create_tenant_budgets_migration.rb.tt +45 -0
- data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +17 -5
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +13 -0
- data/lib/ruby_llm/agents/alert_manager.rb +20 -16
- data/lib/ruby_llm/agents/base/caching.rb +4 -7
- data/lib/ruby_llm/agents/base/cost_calculation.rb +5 -3
- data/lib/ruby_llm/agents/base/execution.rb +61 -9
- data/lib/ruby_llm/agents/base/reliability_execution.rb +14 -9
- data/lib/ruby_llm/agents/base.rb +26 -0
- data/lib/ruby_llm/agents/budget_tracker.rb +250 -139
- data/lib/ruby_llm/agents/cache_helper.rb +98 -0
- data/lib/ruby_llm/agents/circuit_breaker.rb +48 -30
- data/lib/ruby_llm/agents/configuration.rb +40 -1
- data/lib/ruby_llm/agents/engine.rb +65 -1
- data/lib/ruby_llm/agents/inflections.rb +14 -0
- data/lib/ruby_llm/agents/instrumentation.rb +66 -0
- data/lib/ruby_llm/agents/reliability.rb +8 -2
- data/lib/ruby_llm/agents/version.rb +1 -1
- data/lib/ruby_llm/agents/workflow/instrumentation.rb +254 -0
- data/lib/ruby_llm/agents/workflow/parallel.rb +282 -0
- data/lib/ruby_llm/agents/workflow/pipeline.rb +306 -0
- data/lib/ruby_llm/agents/workflow/result.rb +390 -0
- data/lib/ruby_llm/agents/workflow/router.rb +429 -0
- data/lib/ruby_llm/agents/workflow.rb +232 -0
- data/lib/ruby_llm/agents.rb +1 -0
- metadata +50 -60
- data/app/views/rubyllm/agents/agents/index.html.erb +0 -20
- data/app/views/rubyllm/agents/dashboard/_agent_comparison.html.erb +0 -46
- data/app/views/rubyllm/agents/executions/index.html.erb +0 -28
- data/app/views/rubyllm/agents/executions/index.turbo_stream.erb +0 -18
- data/app/views/rubyllm/agents/shared/_executions_table.html.erb +0 -193
- /data/app/views/{rubyllm → ruby_llm}/agents/agents/_agent.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/agents/_version_comparison.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_action_center.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_alerts_feed.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_breaker_strip.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_budgets_bar.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_now_strip.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_top_errors.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/executions/dry_run.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/settings/show.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/shared/_nav_link.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/shared/_stat_card.html.erb +0 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
<%# Show tenant column when multi-tenancy is enabled and no specific tenant is selected %>
|
|
2
|
+
<% show_tenant_column = tenant_filter_enabled? && current_tenant_id.blank? %>
|
|
3
|
+
|
|
4
|
+
<% if executions.empty? %>
|
|
5
|
+
<p class="text-gray-500 dark:text-gray-400 text-center py-8">No executions found.</p>
|
|
6
|
+
<% else %>
|
|
7
|
+
<div class="overflow-x-auto">
|
|
8
|
+
<table class="min-w-full text-sm">
|
|
9
|
+
<thead class="bg-gray-50 dark:bg-gray-900/50">
|
|
10
|
+
<tr>
|
|
11
|
+
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Status</th>
|
|
12
|
+
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Agent</th>
|
|
13
|
+
<% if show_tenant_column %>
|
|
14
|
+
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Tenant</th>
|
|
15
|
+
<% end %>
|
|
16
|
+
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Model</th>
|
|
17
|
+
<th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Duration</th>
|
|
18
|
+
<th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Tokens</th>
|
|
19
|
+
<th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Cost</th>
|
|
20
|
+
<th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Time</th>
|
|
21
|
+
</tr>
|
|
22
|
+
</thead>
|
|
23
|
+
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-100 dark:divide-gray-700">
|
|
24
|
+
<% executions.each do |execution| %>
|
|
25
|
+
<%
|
|
26
|
+
is_workflow = execution.workflow_type.present?
|
|
27
|
+
children = execution.child_executions.sort_by(&:created_at)
|
|
28
|
+
has_children = children.any?
|
|
29
|
+
%>
|
|
30
|
+
|
|
31
|
+
<%# Parent/Main Row %>
|
|
32
|
+
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
|
|
33
|
+
<%# Status %>
|
|
34
|
+
<td class="px-4 py-3 whitespace-nowrap">
|
|
35
|
+
<%= render "ruby_llm/agents/shared/status_badge", status: execution.status, size: :sm %>
|
|
36
|
+
</td>
|
|
37
|
+
|
|
38
|
+
<%# Agent Name with Workflow Badge %>
|
|
39
|
+
<td class="px-4 py-3 whitespace-nowrap">
|
|
40
|
+
<div class="flex items-center gap-2">
|
|
41
|
+
<%= link_to ruby_llm_agents.execution_path(execution), class: "font-medium text-gray-900 dark:text-gray-100 hover:text-blue-600 dark:hover:text-blue-400" do %>
|
|
42
|
+
<%= execution.agent_type.gsub(/Agent$/, "") %>
|
|
43
|
+
<% end %>
|
|
44
|
+
<% if is_workflow %>
|
|
45
|
+
<% badge_style = case execution.workflow_type
|
|
46
|
+
when "pipeline" then "text-indigo-600 dark:text-indigo-400"
|
|
47
|
+
when "parallel" then "text-cyan-600 dark:text-cyan-400"
|
|
48
|
+
when "router" then "text-amber-600 dark:text-amber-400"
|
|
49
|
+
end %>
|
|
50
|
+
<% badge_icon = case execution.workflow_type
|
|
51
|
+
when "pipeline" then "→"
|
|
52
|
+
when "parallel" then "⫴"
|
|
53
|
+
when "router" then "⑂"
|
|
54
|
+
end %>
|
|
55
|
+
<span class="text-xs <%= badge_style %>"><%= badge_icon %></span>
|
|
56
|
+
<% end %>
|
|
57
|
+
</div>
|
|
58
|
+
</td>
|
|
59
|
+
|
|
60
|
+
<%# Tenant (only when viewing all tenants) %>
|
|
61
|
+
<% if show_tenant_column %>
|
|
62
|
+
<td class="px-4 py-3 whitespace-nowrap">
|
|
63
|
+
<% if execution.tenant_id.present? %>
|
|
64
|
+
<a href="<%= url_for(request.query_parameters.merge(tenant_id: execution.tenant_id)) %>"
|
|
65
|
+
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 hover:bg-blue-100 dark:hover:bg-blue-900/50">
|
|
66
|
+
<%= truncate(execution.tenant_id, length: 15) %>
|
|
67
|
+
</a>
|
|
68
|
+
<% else %>
|
|
69
|
+
<span class="text-gray-400 dark:text-gray-500 text-xs">—</span>
|
|
70
|
+
<% end %>
|
|
71
|
+
</td>
|
|
72
|
+
<% end %>
|
|
73
|
+
|
|
74
|
+
<%# Model %>
|
|
75
|
+
<td class="px-4 py-3 whitespace-nowrap text-gray-500 dark:text-gray-400 font-mono text-xs">
|
|
76
|
+
<%= execution.model_id %>
|
|
77
|
+
</td>
|
|
78
|
+
|
|
79
|
+
<%# Duration %>
|
|
80
|
+
<td class="px-4 py-3 whitespace-nowrap text-right text-gray-900 dark:text-gray-100 tabular-nums">
|
|
81
|
+
<%= execution.duration_ms ? "#{number_with_delimiter(execution.duration_ms)}ms" : "-" %>
|
|
82
|
+
</td>
|
|
83
|
+
|
|
84
|
+
<%# Tokens %>
|
|
85
|
+
<td class="px-4 py-3 whitespace-nowrap text-right text-gray-900 dark:text-gray-100 tabular-nums">
|
|
86
|
+
<%= number_with_delimiter(execution.total_tokens || 0) %>
|
|
87
|
+
</td>
|
|
88
|
+
|
|
89
|
+
<%# Cost %>
|
|
90
|
+
<td class="px-4 py-3 whitespace-nowrap text-right text-gray-900 dark:text-gray-100 tabular-nums">
|
|
91
|
+
$<%= number_with_precision(execution.total_cost || 0, precision: 4) %>
|
|
92
|
+
</td>
|
|
93
|
+
|
|
94
|
+
<%# Time %>
|
|
95
|
+
<td class="px-4 py-3 whitespace-nowrap text-right text-gray-500 dark:text-gray-400">
|
|
96
|
+
<%= time_ago_in_words(execution.created_at) %> ago
|
|
97
|
+
</td>
|
|
98
|
+
</tr>
|
|
99
|
+
|
|
100
|
+
<%# Error Row (if applicable) %>
|
|
101
|
+
<% if execution.status_error? && execution.error_message.present? %>
|
|
102
|
+
<tr class="bg-red-50/50 dark:bg-red-900/20">
|
|
103
|
+
<td></td>
|
|
104
|
+
<td colspan="<%= show_tenant_column ? 7 : 6 %>" class="px-4 py-2">
|
|
105
|
+
<p class="text-xs text-red-600 dark:text-red-400">
|
|
106
|
+
<span class="font-medium"><%= execution.error_class %>:</span>
|
|
107
|
+
<%= truncate(execution.error_message, length: 120) %>
|
|
108
|
+
</p>
|
|
109
|
+
</td>
|
|
110
|
+
</tr>
|
|
111
|
+
<% end %>
|
|
112
|
+
|
|
113
|
+
<%# Child Rows (for workflows) %>
|
|
114
|
+
<% if has_children %>
|
|
115
|
+
<% children.each_with_index do |child, index| %>
|
|
116
|
+
<% is_last = index == children.size - 1 %>
|
|
117
|
+
<tr class="bg-gray-50/50 dark:bg-gray-900/30 hover:bg-gray-100/50 dark:hover:bg-gray-800/50 transition-colors">
|
|
118
|
+
<%# Status with tree line %>
|
|
119
|
+
<td class="px-4 py-2 whitespace-nowrap">
|
|
120
|
+
<div class="flex items-center">
|
|
121
|
+
<span class="text-gray-300 dark:text-gray-600 mr-2 font-mono text-xs"><%= is_last ? "└─" : "├─" %></span>
|
|
122
|
+
<% case child.status
|
|
123
|
+
when "success" %>
|
|
124
|
+
<span class="text-green-600 dark:text-green-400">✓</span>
|
|
125
|
+
<% when "error" %>
|
|
126
|
+
<span class="text-red-600 dark:text-red-400">✗</span>
|
|
127
|
+
<% when "timeout" %>
|
|
128
|
+
<span class="text-orange-600 dark:text-orange-400">⏱</span>
|
|
129
|
+
<% when "running" %>
|
|
130
|
+
<span class="text-blue-600 dark:text-blue-400 animate-pulse">●</span>
|
|
131
|
+
<% else %>
|
|
132
|
+
<span class="text-gray-400">○</span>
|
|
133
|
+
<% end %>
|
|
134
|
+
</div>
|
|
135
|
+
</td>
|
|
136
|
+
|
|
137
|
+
<%# Step/Branch Name %>
|
|
138
|
+
<td class="px-4 py-2 whitespace-nowrap">
|
|
139
|
+
<%= link_to ruby_llm_agents.execution_path(child), class: "text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400" do %>
|
|
140
|
+
<% if execution.pipeline_workflow? %>
|
|
141
|
+
<span class="text-gray-400 dark:text-gray-500 text-xs"><%= index + 1 %>.</span>
|
|
142
|
+
<% end %>
|
|
143
|
+
<%= child.workflow_step || child.agent_type.gsub(/Agent$/, "") %>
|
|
144
|
+
<% end %>
|
|
145
|
+
</td>
|
|
146
|
+
|
|
147
|
+
<%# Tenant - empty for child rows %>
|
|
148
|
+
<% if show_tenant_column %>
|
|
149
|
+
<td class="px-4 py-2"></td>
|
|
150
|
+
<% end %>
|
|
151
|
+
|
|
152
|
+
<%# Model %>
|
|
153
|
+
<td class="px-4 py-2 whitespace-nowrap text-gray-400 dark:text-gray-500 font-mono text-xs">
|
|
154
|
+
<%= child.model_id %>
|
|
155
|
+
</td>
|
|
156
|
+
|
|
157
|
+
<%# Duration %>
|
|
158
|
+
<td class="px-4 py-2 whitespace-nowrap text-right text-gray-600 dark:text-gray-300 tabular-nums text-xs">
|
|
159
|
+
<%= child.duration_ms ? "#{number_with_delimiter(child.duration_ms)}ms" : "-" %>
|
|
160
|
+
</td>
|
|
161
|
+
|
|
162
|
+
<%# Tokens %>
|
|
163
|
+
<td class="px-4 py-2 whitespace-nowrap text-right text-gray-600 dark:text-gray-300 tabular-nums text-xs">
|
|
164
|
+
<%= number_with_delimiter(child.total_tokens || 0) %>
|
|
165
|
+
</td>
|
|
166
|
+
|
|
167
|
+
<%# Cost %>
|
|
168
|
+
<td class="px-4 py-2 whitespace-nowrap text-right text-gray-600 dark:text-gray-300 tabular-nums text-xs">
|
|
169
|
+
$<%= number_with_precision(child.total_cost || 0, precision: 4) %>
|
|
170
|
+
</td>
|
|
171
|
+
|
|
172
|
+
<%# Time - empty for children %>
|
|
173
|
+
<td class="px-4 py-2"></td>
|
|
174
|
+
</tr>
|
|
175
|
+
|
|
176
|
+
<%# Child Error Row %>
|
|
177
|
+
<% if child.status_error? && child.error_message.present? %>
|
|
178
|
+
<tr class="bg-red-50/30 dark:bg-red-900/10">
|
|
179
|
+
<td></td>
|
|
180
|
+
<td colspan="<%= show_tenant_column ? 7 : 6 %>" class="px-4 py-1.5 pl-8">
|
|
181
|
+
<p class="text-xs text-red-500 dark:text-red-400">
|
|
182
|
+
<%= truncate(child.error_message, length: 100) %>
|
|
183
|
+
</p>
|
|
184
|
+
</td>
|
|
185
|
+
</tr>
|
|
186
|
+
<% end %>
|
|
187
|
+
<% end %>
|
|
188
|
+
<% end %>
|
|
189
|
+
<% end %>
|
|
190
|
+
</tbody>
|
|
191
|
+
</table>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<%# Pagination %>
|
|
195
|
+
<% if pagination[:total_pages] > 1 %>
|
|
196
|
+
<%
|
|
197
|
+
current_page = pagination[:current_page]
|
|
198
|
+
total_pages = pagination[:total_pages]
|
|
199
|
+
total_count = pagination[:total_count]
|
|
200
|
+
per_page = pagination[:per_page]
|
|
201
|
+
|
|
202
|
+
from_record = ((current_page - 1) * per_page) + 1
|
|
203
|
+
to_record = [current_page * per_page, total_count].min
|
|
204
|
+
%>
|
|
205
|
+
<div class="mt-4 flex items-center justify-between border-t border-gray-100 dark:border-gray-700 pt-4">
|
|
206
|
+
<p class="text-sm text-gray-500 dark:text-gray-400">
|
|
207
|
+
Showing <%= from_record %>-<%= to_record %> of <%= number_with_delimiter(total_count) %> executions
|
|
208
|
+
</p>
|
|
209
|
+
<nav class="flex items-center space-x-1">
|
|
210
|
+
<% if current_page > 1 %>
|
|
211
|
+
<%= link_to "Previous", url_for(request.query_parameters.merge(page: current_page - 1)), class: "px-3 py-1.5 text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700" %>
|
|
212
|
+
<% else %>
|
|
213
|
+
<span class="px-3 py-1.5 text-sm font-medium text-gray-400 dark:text-gray-500 bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md cursor-not-allowed">Previous</span>
|
|
214
|
+
<% end %>
|
|
215
|
+
|
|
216
|
+
<%
|
|
217
|
+
window = 2
|
|
218
|
+
left_edge = 1
|
|
219
|
+
right_edge = 1
|
|
220
|
+
|
|
221
|
+
pages_to_show = []
|
|
222
|
+
(1..total_pages).each do |page|
|
|
223
|
+
if page <= left_edge ||
|
|
224
|
+
page > total_pages - right_edge ||
|
|
225
|
+
(page >= current_page - window && page <= current_page + window)
|
|
226
|
+
pages_to_show << page
|
|
227
|
+
elsif pages_to_show.last != :gap
|
|
228
|
+
pages_to_show << :gap
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
%>
|
|
232
|
+
|
|
233
|
+
<% pages_to_show.each do |page| %>
|
|
234
|
+
<% if page == :gap %>
|
|
235
|
+
<span class="px-2 py-1.5 text-sm text-gray-500 dark:text-gray-400">...</span>
|
|
236
|
+
<% elsif page == current_page %>
|
|
237
|
+
<span class="px-3 py-1.5 text-sm font-medium text-white bg-blue-600 border border-blue-600 rounded-md"><%= page %></span>
|
|
238
|
+
<% else %>
|
|
239
|
+
<%= link_to page, url_for(request.query_parameters.merge(page: page)), class: "px-3 py-1.5 text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700" %>
|
|
240
|
+
<% end %>
|
|
241
|
+
<% end %>
|
|
242
|
+
|
|
243
|
+
<% if current_page < total_pages %>
|
|
244
|
+
<%= link_to "Next", url_for(request.query_parameters.merge(page: current_page + 1)), class: "px-3 py-1.5 text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700" %>
|
|
245
|
+
<% else %>
|
|
246
|
+
<span class="px-3 py-1.5 text-sm font-medium text-gray-400 dark:text-gray-500 bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md cursor-not-allowed">Next</span>
|
|
247
|
+
<% end %>
|
|
248
|
+
</nav>
|
|
249
|
+
</div>
|
|
250
|
+
<% end %>
|
|
251
|
+
<% end %>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<%
|
|
2
2
|
# Status badge with icon and color coding
|
|
3
|
-
# Usage: render "
|
|
3
|
+
# Usage: render "ruby_llm/agents/shared/status_badge", status: execution.status
|
|
4
4
|
# Options: size: :sm (default), :md, :lg
|
|
5
5
|
|
|
6
6
|
size = local_assigns[:size] || :sm
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<% if tenant_filter_enabled? && available_tenants.any? %>
|
|
2
|
+
<div class="tenant-filter">
|
|
3
|
+
<%= form_with url: request.path, method: :get, local: true, class: "tenant-filter-form" do |f| %>
|
|
4
|
+
<label for="tenant_id">Tenant:</label>
|
|
5
|
+
<select name="tenant_id" id="tenant_id" onchange="this.form.submit()">
|
|
6
|
+
<option value="">All Tenants</option>
|
|
7
|
+
<% available_tenants.each do |tenant| %>
|
|
8
|
+
<option value="<%= tenant %>" <%= 'selected' if tenant == current_tenant_id %>>
|
|
9
|
+
<%= tenant %>
|
|
10
|
+
</option>
|
|
11
|
+
<% end %>
|
|
12
|
+
</select>
|
|
13
|
+
|
|
14
|
+
<%# Preserve other filter params %>
|
|
15
|
+
<% params.except(:tenant_id, :controller, :action).each do |key, value| %>
|
|
16
|
+
<% if value.is_a?(Array) %>
|
|
17
|
+
<% value.each do |v| %>
|
|
18
|
+
<input type="hidden" name="<%= key %>[]" value="<%= v %>">
|
|
19
|
+
<% end %>
|
|
20
|
+
<% else %>
|
|
21
|
+
<input type="hidden" name="<%= key %>" value="<%= value %>">
|
|
22
|
+
<% end %>
|
|
23
|
+
<% end %>
|
|
24
|
+
<% end %>
|
|
25
|
+
</div>
|
|
26
|
+
<% end %>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<%
|
|
2
|
+
# Workflow type badge with icon and color coding
|
|
3
|
+
# Usage: render "ruby_llm/agents/shared/workflow_type_badge", workflow_type: "pipeline"
|
|
4
|
+
# Options:
|
|
5
|
+
# size: :xs, :sm (default), :md
|
|
6
|
+
# show_label: true (default) or false for icon-only mode
|
|
7
|
+
|
|
8
|
+
workflow_type = local_assigns[:workflow_type]
|
|
9
|
+
size = local_assigns[:size] || :sm
|
|
10
|
+
show_label = local_assigns.fetch(:show_label, true)
|
|
11
|
+
|
|
12
|
+
config = case workflow_type.to_s
|
|
13
|
+
when "pipeline"
|
|
14
|
+
{
|
|
15
|
+
icon: "arrow-right",
|
|
16
|
+
label: "Pipeline",
|
|
17
|
+
bg: "bg-indigo-100 dark:bg-indigo-900/50",
|
|
18
|
+
text: "text-indigo-700 dark:text-indigo-300",
|
|
19
|
+
icon_char: "→"
|
|
20
|
+
}
|
|
21
|
+
when "parallel"
|
|
22
|
+
{
|
|
23
|
+
icon: "parallel",
|
|
24
|
+
label: "Parallel",
|
|
25
|
+
bg: "bg-cyan-100 dark:bg-cyan-900/50",
|
|
26
|
+
text: "text-cyan-700 dark:text-cyan-300",
|
|
27
|
+
icon_char: "⫿"
|
|
28
|
+
}
|
|
29
|
+
when "router"
|
|
30
|
+
{
|
|
31
|
+
icon: "router",
|
|
32
|
+
label: "Router",
|
|
33
|
+
bg: "bg-amber-100 dark:bg-amber-900/50",
|
|
34
|
+
text: "text-amber-700 dark:text-amber-300",
|
|
35
|
+
icon_char: "⌂"
|
|
36
|
+
}
|
|
37
|
+
else
|
|
38
|
+
{
|
|
39
|
+
icon: "workflow",
|
|
40
|
+
label: "Workflow",
|
|
41
|
+
bg: "bg-gray-100 dark:bg-gray-700",
|
|
42
|
+
text: "text-gray-700 dark:text-gray-300",
|
|
43
|
+
icon_char: "⚙"
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
size_classes = case size
|
|
48
|
+
when :xs
|
|
49
|
+
{ badge: "px-1.5 py-0.5", icon: "text-[10px]", text: "text-[10px]" }
|
|
50
|
+
when :md
|
|
51
|
+
{ badge: "px-2.5 py-1", icon: "text-sm", text: "text-sm" }
|
|
52
|
+
else # :sm
|
|
53
|
+
{ badge: "px-2 py-0.5", icon: "text-xs", text: "text-xs" }
|
|
54
|
+
end
|
|
55
|
+
%>
|
|
56
|
+
<span class="inline-flex items-center gap-1 rounded-md font-medium <%= config[:bg] %> <%= config[:text] %> <%= size_classes[:badge] %>">
|
|
57
|
+
<span class="<%= size_classes[:icon] %>" aria-hidden="true"><%= config[:icon_char] %></span>
|
|
58
|
+
<% if show_label %>
|
|
59
|
+
<span class="<%= size_classes[:text] %>"><%= config[:label] %></span>
|
|
60
|
+
<% end %>
|
|
61
|
+
</span>
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
|
+
|
|
6
|
+
module RubyLlmAgents
|
|
7
|
+
# Multi-tenancy generator for ruby_llm-agents
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# rails generate ruby_llm_agents:multi_tenancy
|
|
11
|
+
#
|
|
12
|
+
# This will create migrations for:
|
|
13
|
+
# - ruby_llm_agents_tenant_budgets table for per-tenant budget configuration
|
|
14
|
+
# - Adding tenant_id column to ruby_llm_agents_executions
|
|
15
|
+
#
|
|
16
|
+
class MultiTenancyGenerator < ::Rails::Generators::Base
|
|
17
|
+
include ::ActiveRecord::Generators::Migration
|
|
18
|
+
|
|
19
|
+
source_root File.expand_path("templates", __dir__)
|
|
20
|
+
|
|
21
|
+
desc "Adds multi-tenancy support to RubyLLM::Agents"
|
|
22
|
+
|
|
23
|
+
def create_tenant_budgets_migration
|
|
24
|
+
if table_exists?(:ruby_llm_agents_tenant_budgets)
|
|
25
|
+
say_status :skip, "ruby_llm_agents_tenant_budgets table already exists", :yellow
|
|
26
|
+
return
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
migration_template(
|
|
30
|
+
"create_tenant_budgets_migration.rb.tt",
|
|
31
|
+
File.join(db_migrate_path, "create_ruby_llm_agents_tenant_budgets.rb")
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def create_add_tenant_to_executions_migration
|
|
36
|
+
if column_exists?(:ruby_llm_agents_executions, :tenant_id)
|
|
37
|
+
say_status :skip, "tenant_id column already exists", :yellow
|
|
38
|
+
return
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
migration_template(
|
|
42
|
+
"add_tenant_to_executions_migration.rb.tt",
|
|
43
|
+
File.join(db_migrate_path, "add_tenant_id_to_ruby_llm_agents_executions.rb")
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def show_post_install_message
|
|
48
|
+
say ""
|
|
49
|
+
say "Multi-tenancy migrations created!", :green
|
|
50
|
+
say ""
|
|
51
|
+
say "Next steps:"
|
|
52
|
+
say " 1. Run: rails db:migrate"
|
|
53
|
+
say " 2. Configure multi-tenancy in your initializer:"
|
|
54
|
+
say ""
|
|
55
|
+
say " RubyLLM::Agents.configure do |config|"
|
|
56
|
+
say " config.multi_tenancy_enabled = true"
|
|
57
|
+
say " config.tenant_resolver = -> { Current.tenant&.id }"
|
|
58
|
+
say " end"
|
|
59
|
+
say ""
|
|
60
|
+
say " 3. Set Current.tenant in your ApplicationController"
|
|
61
|
+
say ""
|
|
62
|
+
say " 4. Create tenant budgets:"
|
|
63
|
+
say ""
|
|
64
|
+
say " RubyLLM::Agents::TenantBudget.create!("
|
|
65
|
+
say " tenant_id: 'acme_corp',"
|
|
66
|
+
say " daily_limit: 50.0,"
|
|
67
|
+
say " monthly_limit: 500.0,"
|
|
68
|
+
say " enforcement: 'hard'"
|
|
69
|
+
say " )"
|
|
70
|
+
say ""
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def migration_version
|
|
76
|
+
"[#{::ActiveRecord::VERSION::STRING.to_f}]"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def db_migrate_path
|
|
80
|
+
"db/migrate"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def table_exists?(table)
|
|
84
|
+
ActiveRecord::Base.connection.table_exists?(table)
|
|
85
|
+
rescue StandardError
|
|
86
|
+
false
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def column_exists?(table, column)
|
|
90
|
+
return false unless ActiveRecord::Base.connection.table_exists?(table)
|
|
91
|
+
|
|
92
|
+
ActiveRecord::Base.connection.column_exists?(table, column)
|
|
93
|
+
rescue StandardError
|
|
94
|
+
false
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
# Run with: rails db:migrate
|
|
9
9
|
class AddAttemptsToRubyLLMAgentsExecutions < ActiveRecord::Migration<%= migration_version %>
|
|
10
10
|
def change
|
|
11
|
-
# Add attempts
|
|
12
|
-
add_column :ruby_llm_agents_executions, :attempts, :
|
|
11
|
+
# Add attempts JSON array for storing per-attempt details
|
|
12
|
+
add_column :ruby_llm_agents_executions, :attempts, :json, null: false, default: []
|
|
13
13
|
|
|
14
14
|
# Add counter for quick access to attempt count
|
|
15
15
|
add_column :ruby_llm_agents_executions, :attempts_count, :integer, null: false, default: 0
|
|
@@ -18,7 +18,7 @@ class AddAttemptsToRubyLLMAgentsExecutions < ActiveRecord::Migration<%= migratio
|
|
|
18
18
|
add_column :ruby_llm_agents_executions, :chosen_model_id, :string
|
|
19
19
|
|
|
20
20
|
# Add fallback chain (list of models that were configured to try)
|
|
21
|
-
add_column :ruby_llm_agents_executions, :fallback_chain, :
|
|
21
|
+
add_column :ruby_llm_agents_executions, :fallback_chain, :json, null: false, default: []
|
|
22
22
|
|
|
23
23
|
# Add indexes for common queries
|
|
24
24
|
add_index :ruby_llm_agents_executions, :attempts_count
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Migration to add tenant_id column to executions for multi-tenancy support
|
|
4
|
+
#
|
|
5
|
+
# This migration adds a tenant_id column to track which tenant each execution
|
|
6
|
+
# belongs to, enabling:
|
|
7
|
+
# - Filtering executions by tenant
|
|
8
|
+
# - Tenant-scoped analytics and reporting
|
|
9
|
+
# - Per-tenant budget tracking and circuit breakers
|
|
10
|
+
#
|
|
11
|
+
# Run with: rails db:migrate
|
|
12
|
+
class AddTenantIdToRubyLLMAgentsExecutions < ActiveRecord::Migration<%= migration_version %>
|
|
13
|
+
def change
|
|
14
|
+
# Add tenant_id column (nullable for backward compatibility)
|
|
15
|
+
add_column :ruby_llm_agents_executions, :tenant_id, :string
|
|
16
|
+
|
|
17
|
+
# Add indexes for efficient tenant-scoped queries
|
|
18
|
+
add_index :ruby_llm_agents_executions, :tenant_id
|
|
19
|
+
add_index :ruby_llm_agents_executions, [:tenant_id, :created_at]
|
|
20
|
+
add_index :ruby_llm_agents_executions, [:tenant_id, :agent_type]
|
|
21
|
+
add_index :ruby_llm_agents_executions, [:tenant_id, :status]
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
# Run with: rails db:migrate
|
|
16
16
|
class AddToolCallsToRubyLLMAgentsExecutions < ActiveRecord::Migration<%= migration_version %>
|
|
17
17
|
def change
|
|
18
|
-
# Add tool_calls
|
|
18
|
+
# Add tool_calls JSON array for storing tool call details
|
|
19
19
|
# Each tool call contains: id, name, arguments
|
|
20
|
-
add_column :ruby_llm_agents_executions, :tool_calls, :
|
|
20
|
+
add_column :ruby_llm_agents_executions, :tool_calls, :json, null: false, default: []
|
|
21
21
|
|
|
22
22
|
# Add counter for quick access to tool call count
|
|
23
23
|
add_column :ruby_llm_agents_executions, :tool_calls_count, :integer, null: false, default: 0
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Migration to add workflow orchestration columns to executions
|
|
4
|
+
#
|
|
5
|
+
# This migration adds columns for tracking workflow executions (Pipeline,
|
|
6
|
+
# Parallel, Router patterns) and linking child executions to their
|
|
7
|
+
# parent workflow.
|
|
8
|
+
#
|
|
9
|
+
# Workflow patterns supported:
|
|
10
|
+
# - Pipeline: Sequential execution with data flowing between steps
|
|
11
|
+
# - Parallel: Concurrent execution with result aggregation
|
|
12
|
+
# - Router: Conditional dispatch based on classification
|
|
13
|
+
#
|
|
14
|
+
# Run with: rails db:migrate
|
|
15
|
+
class AddWorkflowToRubyLLMAgentsExecutions < ActiveRecord::Migration<%= migration_version %>
|
|
16
|
+
def change
|
|
17
|
+
# Unique identifier for the workflow execution
|
|
18
|
+
# All steps/branches share the same workflow_id
|
|
19
|
+
add_column :ruby_llm_agents_executions, :workflow_id, :string
|
|
20
|
+
|
|
21
|
+
# Type of workflow: "pipeline", "parallel", "router", or nil for regular agents
|
|
22
|
+
add_column :ruby_llm_agents_executions, :workflow_type, :string
|
|
23
|
+
|
|
24
|
+
# Name of the step/branch within the workflow
|
|
25
|
+
add_column :ruby_llm_agents_executions, :workflow_step, :string
|
|
26
|
+
|
|
27
|
+
# For routers: the route that was selected
|
|
28
|
+
add_column :ruby_llm_agents_executions, :routed_to, :string
|
|
29
|
+
|
|
30
|
+
# For routers: classification details (route, method, time)
|
|
31
|
+
add_column :ruby_llm_agents_executions, :classification_result, :json
|
|
32
|
+
|
|
33
|
+
# Add indexes for efficient querying
|
|
34
|
+
add_index :ruby_llm_agents_executions, :workflow_id
|
|
35
|
+
add_index :ruby_llm_agents_executions, :workflow_type
|
|
36
|
+
add_index :ruby_llm_agents_executions, [:workflow_id, :workflow_step]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Migration to create the tenant_budgets table for multi-tenancy support
|
|
4
|
+
#
|
|
5
|
+
# This table stores per-tenant budget configuration, allowing different
|
|
6
|
+
# tenants to have their own budget limits and enforcement modes.
|
|
7
|
+
#
|
|
8
|
+
# Features:
|
|
9
|
+
# - Per-tenant daily and monthly budget limits
|
|
10
|
+
# - Per-agent budget limits within a tenant
|
|
11
|
+
# - Configurable enforcement mode (none, soft, hard)
|
|
12
|
+
# - Option to inherit global defaults for unset limits
|
|
13
|
+
#
|
|
14
|
+
# Run with: rails db:migrate
|
|
15
|
+
class CreateRubyLLMAgentsTenantBudgets < ActiveRecord::Migration<%= migration_version %>
|
|
16
|
+
def change
|
|
17
|
+
create_table :ruby_llm_agents_tenant_budgets do |t|
|
|
18
|
+
# Unique identifier for the tenant (e.g., organization ID, workspace ID)
|
|
19
|
+
t.string :tenant_id, null: false
|
|
20
|
+
|
|
21
|
+
# Global budget limits for this tenant
|
|
22
|
+
t.decimal :daily_limit, precision: 12, scale: 6
|
|
23
|
+
t.decimal :monthly_limit, precision: 12, scale: 6
|
|
24
|
+
|
|
25
|
+
# Per-agent budget limits (JSON hash)
|
|
26
|
+
# Format: { "AgentName" => limit_value }
|
|
27
|
+
t.json :per_agent_daily, null: false, default: {}
|
|
28
|
+
t.json :per_agent_monthly, null: false, default: {}
|
|
29
|
+
|
|
30
|
+
# Enforcement mode for this tenant: "none", "soft", or "hard"
|
|
31
|
+
# - none: no enforcement, only tracking
|
|
32
|
+
# - soft: log warnings when limits exceeded
|
|
33
|
+
# - hard: block execution when limits exceeded
|
|
34
|
+
t.string :enforcement, default: "soft"
|
|
35
|
+
|
|
36
|
+
# Whether to inherit from global config for unset limits
|
|
37
|
+
t.boolean :inherit_global_defaults, default: true
|
|
38
|
+
|
|
39
|
+
t.timestamps
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Ensure unique tenant IDs
|
|
43
|
+
add_index :ruby_llm_agents_tenant_budgets, :tenant_id, unique: true
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -54,10 +54,10 @@ class CreateRubyLLMAgentsExecutions < ActiveRecord::Migration<%= migration_versi
|
|
|
54
54
|
t.decimal :output_cost, precision: 12, scale: 6
|
|
55
55
|
t.decimal :total_cost, precision: 12, scale: 6
|
|
56
56
|
|
|
57
|
-
# Data (
|
|
58
|
-
t.
|
|
59
|
-
t.
|
|
60
|
-
t.
|
|
57
|
+
# Data (JSON - works with PostgreSQL, MySQL, SQLite3)
|
|
58
|
+
t.json :parameters, null: false, default: {}
|
|
59
|
+
t.json :response, default: {}
|
|
60
|
+
t.json :metadata, null: false, default: {}
|
|
61
61
|
|
|
62
62
|
# Error tracking
|
|
63
63
|
t.string :error_class
|
|
@@ -68,9 +68,16 @@ class CreateRubyLLMAgentsExecutions < ActiveRecord::Migration<%= migration_versi
|
|
|
68
68
|
t.text :user_prompt
|
|
69
69
|
|
|
70
70
|
# Tool calls tracking
|
|
71
|
-
t.
|
|
71
|
+
t.json :tool_calls, null: false, default: []
|
|
72
72
|
t.integer :tool_calls_count, null: false, default: 0
|
|
73
73
|
|
|
74
|
+
# Workflow orchestration
|
|
75
|
+
t.string :workflow_id
|
|
76
|
+
t.string :workflow_type
|
|
77
|
+
t.string :workflow_step
|
|
78
|
+
t.string :routed_to
|
|
79
|
+
t.json :classification_result
|
|
80
|
+
|
|
74
81
|
t.timestamps
|
|
75
82
|
end
|
|
76
83
|
|
|
@@ -96,6 +103,11 @@ class CreateRubyLLMAgentsExecutions < ActiveRecord::Migration<%= migration_versi
|
|
|
96
103
|
# Tool calls index
|
|
97
104
|
add_index :ruby_llm_agents_executions, :tool_calls_count
|
|
98
105
|
|
|
106
|
+
# Workflow indexes
|
|
107
|
+
add_index :ruby_llm_agents_executions, :workflow_id
|
|
108
|
+
add_index :ruby_llm_agents_executions, :workflow_type
|
|
109
|
+
add_index :ruby_llm_agents_executions, [:workflow_id, :workflow_step]
|
|
110
|
+
|
|
99
111
|
# Foreign keys for execution hierarchy
|
|
100
112
|
add_foreign_key :ruby_llm_agents_executions, :ruby_llm_agents_executions,
|
|
101
113
|
column: :parent_execution_id, on_delete: :nullify
|