ruby_llm-agents 0.3.3 → 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 +137 -9
- data/app/controllers/ruby_llm/agents/executions_controller.rb +83 -5
- data/app/models/ruby_llm/agents/execution/analytics.rb +103 -12
- 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 +28 -59
- 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/ruby_llm/agents/application.html.erb +430 -0
- 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/ruby_llm/agents/agents/show.html.erb +775 -0
- data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +112 -0
- data/app/views/ruby_llm/agents/dashboard/_budgets_bar.html.erb +75 -0
- data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_execution_item.html.erb +7 -4
- data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +84 -0
- data/app/views/ruby_llm/agents/dashboard/_tenant_budget.html.erb +115 -0
- data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +49 -0
- data/app/views/ruby_llm/agents/dashboard/index.html.erb +155 -0
- 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 +260 -200
- data/app/views/{rubyllm → ruby_llm}/agents/settings/show.html.erb +1 -1
- data/app/views/ruby_llm/agents/shared/_breadcrumbs.html.erb +48 -0
- 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/ruby_llm/agents/shared/_nav_link.html.erb +27 -0
- 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/config/routes.rb +2 -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 +40 -0
- data/lib/ruby_llm/agents/base/cost_calculation.rb +105 -0
- data/lib/ruby_llm/agents/base/dsl.rb +261 -0
- data/lib/ruby_llm/agents/base/execution.rb +258 -0
- data/lib/ruby_llm/agents/base/reliability_execution.rb +136 -0
- data/lib/ruby_llm/agents/base/response_building.rb +86 -0
- data/lib/ruby_llm/agents/base/tool_tracking.rb +57 -0
- data/lib/ruby_llm/agents/base.rb +37 -801
- 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 +57 -75
- data/app/channels/ruby_llm/agents/executions_channel.rb +0 -46
- data/app/javascript/ruby_llm/agents/controllers/filter_controller.js +0 -56
- data/app/javascript/ruby_llm/agents/controllers/index.js +0 -12
- data/app/javascript/ruby_llm/agents/controllers/refresh_controller.js +0 -83
- data/app/views/layouts/rubyllm/agents/application.html.erb +0 -626
- data/app/views/rubyllm/agents/agents/index.html.erb +0 -20
- data/app/views/rubyllm/agents/agents/show.html.erb +0 -772
- data/app/views/rubyllm/agents/dashboard/_budgets_bar.html.erb +0 -165
- data/app/views/rubyllm/agents/dashboard/_now_strip.html.erb +0 -10
- data/app/views/rubyllm/agents/dashboard/_now_strip_values.html.erb +0 -71
- data/app/views/rubyllm/agents/dashboard/index.html.erb +0 -197
- 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/executions/dry_run.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/shared/_stat_card.html.erb +0 -0
|
@@ -95,6 +95,28 @@ module RubyLLM
|
|
|
95
95
|
end
|
|
96
96
|
end
|
|
97
97
|
|
|
98
|
+
# Returns only regular agents (non-workflows)
|
|
99
|
+
#
|
|
100
|
+
# @return [Array<Hash>] Agent info hashes for non-workflow agents
|
|
101
|
+
def agents_only
|
|
102
|
+
all_with_details.reject { |a| a[:is_workflow] }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Returns only workflows
|
|
106
|
+
#
|
|
107
|
+
# @return [Array<Hash>] Agent info hashes for workflows only
|
|
108
|
+
def workflows_only
|
|
109
|
+
all_with_details.select { |a| a[:is_workflow] }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Returns workflows filtered by type
|
|
113
|
+
#
|
|
114
|
+
# @param type [String, Symbol] The workflow type (pipeline, parallel, router)
|
|
115
|
+
# @return [Array<Hash>] Filtered workflow info hashes
|
|
116
|
+
def workflows_by_type(type)
|
|
117
|
+
workflows_only.select { |w| w[:workflow_type] == type.to_s }
|
|
118
|
+
end
|
|
119
|
+
|
|
98
120
|
# Builds detailed info hash for an agent
|
|
99
121
|
#
|
|
100
122
|
# @param agent_type [String] The agent class name
|
|
@@ -103,17 +125,27 @@ module RubyLLM
|
|
|
103
125
|
agent_class = find(agent_type)
|
|
104
126
|
stats = fetch_stats(agent_type)
|
|
105
127
|
|
|
128
|
+
# Check if this is a workflow class vs a regular agent
|
|
129
|
+
is_workflow = agent_class&.ancestors&.any? { |a| a.name&.include?("Workflow") }
|
|
130
|
+
|
|
131
|
+
# Determine specific workflow type and children
|
|
132
|
+
workflow_type = is_workflow ? detect_workflow_type(agent_class) : nil
|
|
133
|
+
workflow_children = is_workflow ? extract_workflow_children(agent_class) : []
|
|
134
|
+
|
|
106
135
|
{
|
|
107
136
|
name: agent_type,
|
|
108
137
|
class: agent_class,
|
|
109
138
|
active: agent_class.present?,
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
139
|
+
is_workflow: is_workflow,
|
|
140
|
+
workflow_type: workflow_type,
|
|
141
|
+
workflow_children: workflow_children,
|
|
142
|
+
version: safe_call(agent_class, :version) || "N/A",
|
|
143
|
+
model: safe_call(agent_class, :model) || (is_workflow ? "workflow" : "N/A"),
|
|
144
|
+
temperature: safe_call(agent_class, :temperature),
|
|
145
|
+
timeout: safe_call(agent_class, :timeout),
|
|
146
|
+
cache_enabled: safe_call(agent_class, :cache_enabled?) || false,
|
|
147
|
+
cache_ttl: safe_call(agent_class, :cache_ttl),
|
|
148
|
+
params: safe_call(agent_class, :params) || {},
|
|
117
149
|
execution_count: stats[:count],
|
|
118
150
|
total_cost: stats[:total_cost],
|
|
119
151
|
total_tokens: stats[:total_tokens],
|
|
@@ -124,6 +156,20 @@ module RubyLLM
|
|
|
124
156
|
}
|
|
125
157
|
end
|
|
126
158
|
|
|
159
|
+
# Safely calls a method on a class, returning nil if method doesn't exist
|
|
160
|
+
#
|
|
161
|
+
# @param klass [Class, nil] The class to call the method on
|
|
162
|
+
# @param method_name [Symbol] The method to call
|
|
163
|
+
# @return [Object, nil] The result or nil
|
|
164
|
+
def safe_call(klass, method_name)
|
|
165
|
+
return nil unless klass
|
|
166
|
+
return nil unless klass.respond_to?(method_name)
|
|
167
|
+
|
|
168
|
+
klass.public_send(method_name)
|
|
169
|
+
rescue StandardError
|
|
170
|
+
nil
|
|
171
|
+
end
|
|
172
|
+
|
|
127
173
|
# Fetches statistics for an agent
|
|
128
174
|
#
|
|
129
175
|
# @param agent_type [String] The agent class name
|
|
@@ -143,6 +189,71 @@ module RubyLLM
|
|
|
143
189
|
rescue StandardError
|
|
144
190
|
nil
|
|
145
191
|
end
|
|
192
|
+
|
|
193
|
+
# Detects the specific workflow type from class hierarchy
|
|
194
|
+
#
|
|
195
|
+
# @param agent_class [Class, nil] The agent class
|
|
196
|
+
# @return [String, nil] "pipeline", "parallel", "router", or nil
|
|
197
|
+
def detect_workflow_type(agent_class)
|
|
198
|
+
return nil unless agent_class
|
|
199
|
+
|
|
200
|
+
ancestors = agent_class.ancestors.map { |a| a.name.to_s }
|
|
201
|
+
|
|
202
|
+
if ancestors.include?("RubyLLM::Agents::Workflow::Pipeline")
|
|
203
|
+
"pipeline"
|
|
204
|
+
elsif ancestors.include?("RubyLLM::Agents::Workflow::Parallel")
|
|
205
|
+
"parallel"
|
|
206
|
+
elsif ancestors.include?("RubyLLM::Agents::Workflow::Router")
|
|
207
|
+
"router"
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Extracts child agents from workflow DSL configuration
|
|
212
|
+
#
|
|
213
|
+
# @param agent_class [Class, nil] The workflow class
|
|
214
|
+
# @return [Array<Hash>] Array of child info hashes with :name, :agent, :type, :optional keys
|
|
215
|
+
def extract_workflow_children(agent_class)
|
|
216
|
+
return [] unless agent_class
|
|
217
|
+
|
|
218
|
+
children = []
|
|
219
|
+
|
|
220
|
+
if agent_class.respond_to?(:steps) && agent_class.steps.any?
|
|
221
|
+
# Pipeline workflow - extract steps
|
|
222
|
+
agent_class.steps.each do |name, config|
|
|
223
|
+
children << {
|
|
224
|
+
name: name,
|
|
225
|
+
agent: config[:agent]&.name,
|
|
226
|
+
type: "step",
|
|
227
|
+
optional: config[:continue_on_error] || false
|
|
228
|
+
}
|
|
229
|
+
end
|
|
230
|
+
elsif agent_class.respond_to?(:branches) && agent_class.branches.any?
|
|
231
|
+
# Parallel workflow - extract branches
|
|
232
|
+
agent_class.branches.each do |name, config|
|
|
233
|
+
children << {
|
|
234
|
+
name: name,
|
|
235
|
+
agent: config[:agent]&.name,
|
|
236
|
+
type: "branch",
|
|
237
|
+
optional: config[:optional] || false
|
|
238
|
+
}
|
|
239
|
+
end
|
|
240
|
+
elsif agent_class.respond_to?(:routes) && agent_class.routes.any?
|
|
241
|
+
# Router workflow - extract routes
|
|
242
|
+
agent_class.routes.each do |name, config|
|
|
243
|
+
children << {
|
|
244
|
+
name: name,
|
|
245
|
+
agent: config[:agent]&.name,
|
|
246
|
+
type: "route",
|
|
247
|
+
description: config[:description]
|
|
248
|
+
}
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
children
|
|
253
|
+
rescue StandardError => e
|
|
254
|
+
Rails.logger.error("[RubyLLM::Agents] Error extracting workflow children: #{e.message}")
|
|
255
|
+
[]
|
|
256
|
+
end
|
|
146
257
|
end
|
|
147
258
|
end
|
|
148
259
|
end
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
|
|
3
|
+
<html lang="en">
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
|
+
|
|
8
|
+
<title>RubyLLM Agents Dashboard</title>
|
|
9
|
+
|
|
10
|
+
<!-- Prevent flash of wrong theme - must run before any rendering -->
|
|
11
|
+
<script>
|
|
12
|
+
(function() {
|
|
13
|
+
const preference = localStorage.getItem('ruby_llm_agents_theme') || 'auto';
|
|
14
|
+
const isDark = preference === 'dark' ||
|
|
15
|
+
(preference === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
16
|
+
if (isDark) {
|
|
17
|
+
document.documentElement.classList.add('dark');
|
|
18
|
+
}
|
|
19
|
+
})();
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<!-- Tailwind CSS via CDN -->
|
|
23
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
24
|
+
|
|
25
|
+
<script>
|
|
26
|
+
tailwind.config = {
|
|
27
|
+
darkMode: 'class'
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<!-- Highcharts for charts -->
|
|
32
|
+
<script src="https://code.highcharts.com/highcharts.js"></script>
|
|
33
|
+
|
|
34
|
+
<!-- Configure Highcharts defaults -->
|
|
35
|
+
<script>
|
|
36
|
+
Highcharts.setOptions({
|
|
37
|
+
credits: { enabled: false },
|
|
38
|
+
chart: {
|
|
39
|
+
backgroundColor: 'transparent',
|
|
40
|
+
style: { fontFamily: 'inherit' }
|
|
41
|
+
},
|
|
42
|
+
title: { text: null },
|
|
43
|
+
xAxis: {
|
|
44
|
+
labels: { style: { color: '#9CA3AF' } },
|
|
45
|
+
lineColor: 'rgba(156, 163, 175, 0.2)',
|
|
46
|
+
tickColor: 'rgba(156, 163, 175, 0.2)'
|
|
47
|
+
},
|
|
48
|
+
yAxis: {
|
|
49
|
+
labels: { style: { color: '#9CA3AF' } },
|
|
50
|
+
gridLineColor: 'rgba(156, 163, 175, 0.2)'
|
|
51
|
+
},
|
|
52
|
+
legend: {
|
|
53
|
+
itemStyle: { color: '#9CA3AF' },
|
|
54
|
+
itemHoverStyle: { color: '#D1D5DB' }
|
|
55
|
+
},
|
|
56
|
+
tooltip: {
|
|
57
|
+
backgroundColor: 'rgba(17, 24, 39, 0.9)',
|
|
58
|
+
borderColor: 'rgba(75, 85, 99, 0.5)',
|
|
59
|
+
style: { color: '#F3F4F6' }
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
<!-- Alpine.js -->
|
|
65
|
+
<script
|
|
66
|
+
defer
|
|
67
|
+
src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.3/dist/cdn.min.js"
|
|
68
|
+
></script>
|
|
69
|
+
|
|
70
|
+
<style>[x-cloak] { display: none !important; }</style>
|
|
71
|
+
|
|
72
|
+
<!-- Clock and clickable rows -->
|
|
73
|
+
<script type="module">
|
|
74
|
+
function updateClock() {
|
|
75
|
+
const clock = document.getElementById('live-clock');
|
|
76
|
+
if (clock) {
|
|
77
|
+
const now = new Date();
|
|
78
|
+
clock.textContent = now.toLocaleTimeString('en-US', {
|
|
79
|
+
hour: '2-digit',
|
|
80
|
+
minute: '2-digit',
|
|
81
|
+
hour12: false
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Initialize on page load
|
|
87
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
88
|
+
updateClock();
|
|
89
|
+
|
|
90
|
+
// Handle data-href clickable rows (semantic alternative to onclick)
|
|
91
|
+
document.querySelectorAll('[data-href]').forEach(function(element) {
|
|
92
|
+
element.addEventListener('click', function(e) {
|
|
93
|
+
// Don't navigate if clicking on a link or button inside the row
|
|
94
|
+
if (e.target.closest('a, button')) return;
|
|
95
|
+
window.location = element.dataset.href;
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
</script>
|
|
100
|
+
|
|
101
|
+
<!-- Theme Switcher (Alpine.js) -->
|
|
102
|
+
<script>
|
|
103
|
+
function themeManager() {
|
|
104
|
+
return {
|
|
105
|
+
preference: localStorage.getItem('ruby_llm_agents_theme') || 'auto',
|
|
106
|
+
init() {
|
|
107
|
+
this.applyTheme();
|
|
108
|
+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
|
109
|
+
if (this.preference === 'auto') this.applyTheme();
|
|
110
|
+
});
|
|
111
|
+
},
|
|
112
|
+
setTheme(value) {
|
|
113
|
+
this.preference = value;
|
|
114
|
+
localStorage.setItem('ruby_llm_agents_theme', value);
|
|
115
|
+
this.applyTheme();
|
|
116
|
+
},
|
|
117
|
+
applyTheme() {
|
|
118
|
+
const isDark = this.preference === 'dark' ||
|
|
119
|
+
(this.preference === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
120
|
+
document.documentElement.classList.toggle('dark', isDark);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
</script>
|
|
125
|
+
|
|
126
|
+
<style>
|
|
127
|
+
/* Custom styles for the dashboard */
|
|
128
|
+
|
|
129
|
+
/* Standard card components */
|
|
130
|
+
.card {
|
|
131
|
+
@apply bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6;
|
|
132
|
+
}
|
|
133
|
+
.card-compact {
|
|
134
|
+
@apply bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-4 mb-4;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.stat-card {
|
|
138
|
+
@apply bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-4 transition-shadow;
|
|
139
|
+
}
|
|
140
|
+
.stat-card:hover {
|
|
141
|
+
@apply shadow-md;
|
|
142
|
+
}
|
|
143
|
+
.stat-value {
|
|
144
|
+
@apply text-2xl font-bold text-gray-900 dark:text-gray-100 mt-2;
|
|
145
|
+
}
|
|
146
|
+
@media (min-width: 1024px) {
|
|
147
|
+
.stat-value {
|
|
148
|
+
font-size: 1.5rem;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
.stat-label {
|
|
152
|
+
@apply text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide font-medium;
|
|
153
|
+
}
|
|
154
|
+
.chart-card {
|
|
155
|
+
@apply bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-6;
|
|
156
|
+
}
|
|
157
|
+
.nav-link {
|
|
158
|
+
@apply px-4 py-2 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors;
|
|
159
|
+
}
|
|
160
|
+
.nav-link.active {
|
|
161
|
+
@apply bg-blue-50 dark:bg-blue-900 text-blue-700 dark:text-blue-300;
|
|
162
|
+
}
|
|
163
|
+
.badge {
|
|
164
|
+
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
|
|
165
|
+
}
|
|
166
|
+
.badge-running {
|
|
167
|
+
@apply bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200;
|
|
168
|
+
}
|
|
169
|
+
.badge-success {
|
|
170
|
+
@apply bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200;
|
|
171
|
+
}
|
|
172
|
+
.badge-error {
|
|
173
|
+
@apply bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200;
|
|
174
|
+
}
|
|
175
|
+
.badge-timeout {
|
|
176
|
+
@apply bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200;
|
|
177
|
+
}
|
|
178
|
+
.badge-cyan {
|
|
179
|
+
@apply bg-cyan-100 dark:bg-cyan-900/50 text-cyan-700 dark:text-cyan-300;
|
|
180
|
+
}
|
|
181
|
+
.badge-purple {
|
|
182
|
+
@apply bg-purple-100 dark:bg-purple-900/50 text-purple-700 dark:text-purple-300;
|
|
183
|
+
}
|
|
184
|
+
.badge-orange {
|
|
185
|
+
@apply bg-orange-100 dark:bg-orange-900/50 text-orange-700 dark:text-orange-300;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* Workflow type badges */
|
|
189
|
+
.badge-pipeline {
|
|
190
|
+
@apply bg-indigo-100 dark:bg-indigo-900/50 text-indigo-700 dark:text-indigo-300;
|
|
191
|
+
}
|
|
192
|
+
.badge-parallel {
|
|
193
|
+
@apply bg-cyan-100 dark:bg-cyan-900/50 text-cyan-700 dark:text-cyan-300;
|
|
194
|
+
}
|
|
195
|
+
.badge-router {
|
|
196
|
+
@apply bg-amber-100 dark:bg-amber-900/50 text-amber-700 dark:text-amber-300;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* Alpine.js utilities */
|
|
200
|
+
[x-cloak] {
|
|
201
|
+
display: none !important;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
</style>
|
|
205
|
+
</head>
|
|
206
|
+
|
|
207
|
+
<body class="bg-gray-50 dark:bg-gray-900 min-h-screen flex flex-col">
|
|
208
|
+
<!-- Header -->
|
|
209
|
+
<header
|
|
210
|
+
class="
|
|
211
|
+
bg-white dark:bg-gray-800 border-b border-gray-200
|
|
212
|
+
dark:border-gray-700
|
|
213
|
+
"
|
|
214
|
+
x-data="{ mobileMenuOpen: false }"
|
|
215
|
+
>
|
|
216
|
+
<div class="max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center">
|
|
217
|
+
<div class="flex justify-between items-center w-full">
|
|
218
|
+
<div class="flex items-center space-x-8">
|
|
219
|
+
<%= link_to ruby_llm_agents.root_path, class: "flex items-center space-x-2" do %>
|
|
220
|
+
<span class="text-lg">🤖</span>
|
|
221
|
+
|
|
222
|
+
<span class="font-semibold text-gray-900 dark:text-gray-100">
|
|
223
|
+
RubyLLM Agents
|
|
224
|
+
</span>
|
|
225
|
+
<% end %>
|
|
226
|
+
|
|
227
|
+
<!-- Desktop Navigation -->
|
|
228
|
+
<%
|
|
229
|
+
nav_items = [
|
|
230
|
+
{ path: ruby_llm_agents.root_path, label: "Dashboard", icon: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />' },
|
|
231
|
+
{ path: ruby_llm_agents.agents_path, label: "Agents", icon: '<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" />' },
|
|
232
|
+
{ path: ruby_llm_agents.executions_path, label: "Executions", icon: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />' },
|
|
233
|
+
{ path: ruby_llm_agents.settings_path, label: "Settings", icon: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />' }
|
|
234
|
+
]
|
|
235
|
+
%>
|
|
236
|
+
<nav class="hidden md:flex items-center space-x-1">
|
|
237
|
+
<% nav_items.each do |item| %>
|
|
238
|
+
<%= render "ruby_llm/agents/shared/nav_link", path: item[:path], label: item[:label], icon: item[:icon], mobile: false %>
|
|
239
|
+
<% end %>
|
|
240
|
+
</nav>
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
<div
|
|
244
|
+
class="
|
|
245
|
+
flex items-center space-x-2 text-xs text-gray-500
|
|
246
|
+
dark:text-gray-400
|
|
247
|
+
"
|
|
248
|
+
>
|
|
249
|
+
<%# Tenant Selector Dropdown %>
|
|
250
|
+
<% if tenant_filter_enabled? && available_tenants.any? %>
|
|
251
|
+
<div x-data="{ open: false }" class="relative">
|
|
252
|
+
<button
|
|
253
|
+
@click="open = !open"
|
|
254
|
+
@click.outside="open = false"
|
|
255
|
+
type="button"
|
|
256
|
+
class="flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium rounded-md border transition-colors
|
|
257
|
+
<%= current_tenant_id.present? ?
|
|
258
|
+
'bg-blue-50 dark:bg-blue-900/30 border-blue-200 dark:border-blue-800 text-blue-700 dark:text-blue-300' :
|
|
259
|
+
'bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600' %>"
|
|
260
|
+
>
|
|
261
|
+
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
262
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
|
263
|
+
</svg>
|
|
264
|
+
<span class="hidden sm:inline"><%= current_tenant_id.present? ? current_tenant_id : 'All Tenants' %></span>
|
|
265
|
+
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
266
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
|
267
|
+
</svg>
|
|
268
|
+
</button>
|
|
269
|
+
|
|
270
|
+
<div
|
|
271
|
+
x-show="open"
|
|
272
|
+
x-cloak
|
|
273
|
+
x-transition:enter="transition ease-out duration-100"
|
|
274
|
+
x-transition:enter-start="opacity-0 scale-95"
|
|
275
|
+
x-transition:enter-end="opacity-100 scale-100"
|
|
276
|
+
x-transition:leave="transition ease-in duration-75"
|
|
277
|
+
x-transition:leave-start="opacity-100 scale-100"
|
|
278
|
+
x-transition:leave-end="opacity-0 scale-95"
|
|
279
|
+
class="absolute right-0 mt-1 w-48 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50 py-1"
|
|
280
|
+
>
|
|
281
|
+
<a
|
|
282
|
+
href="<%= url_for(request.query_parameters.except('tenant_id').merge(tenant_id: nil)) %>"
|
|
283
|
+
class="block px-3 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 <%= 'bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300' if current_tenant_id.blank? %>"
|
|
284
|
+
>
|
|
285
|
+
All Tenants
|
|
286
|
+
</a>
|
|
287
|
+
<div class="border-t border-gray-100 dark:border-gray-700 my-1"></div>
|
|
288
|
+
<% available_tenants.each do |tenant| %>
|
|
289
|
+
<a
|
|
290
|
+
href="<%= url_for(request.query_parameters.merge(tenant_id: tenant)) %>"
|
|
291
|
+
class="block px-3 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 <%= 'bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300' if tenant == current_tenant_id %>"
|
|
292
|
+
>
|
|
293
|
+
<%= tenant %>
|
|
294
|
+
</a>
|
|
295
|
+
<% end %>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
<% end %>
|
|
299
|
+
|
|
300
|
+
<span id="live-clock" class="tabular-nums"></span>
|
|
301
|
+
|
|
302
|
+
<button
|
|
303
|
+
id="refresh-button"
|
|
304
|
+
onclick="window.location.reload()"
|
|
305
|
+
title="Refresh page"
|
|
306
|
+
class="p-1.5 rounded-md text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
|
307
|
+
>
|
|
308
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
309
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
|
310
|
+
</svg>
|
|
311
|
+
</button>
|
|
312
|
+
|
|
313
|
+
<!-- Mobile menu button -->
|
|
314
|
+
<button
|
|
315
|
+
type="button"
|
|
316
|
+
@click="mobileMenuOpen = !mobileMenuOpen"
|
|
317
|
+
:aria-expanded="mobileMenuOpen"
|
|
318
|
+
aria-controls="mobile-menu"
|
|
319
|
+
class="
|
|
320
|
+
md:hidden inline-flex items-center justify-center p-2
|
|
321
|
+
rounded-md text-gray-500 dark:text-gray-400
|
|
322
|
+
hover:text-gray-900 dark:hover:text-gray-100
|
|
323
|
+
hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none
|
|
324
|
+
focus:ring-2 focus:ring-inset focus:ring-blue-500
|
|
325
|
+
"
|
|
326
|
+
>
|
|
327
|
+
<span class="sr-only">Open main menu</span>
|
|
328
|
+
|
|
329
|
+
<svg
|
|
330
|
+
class="w-5 h-5"
|
|
331
|
+
fill="none"
|
|
332
|
+
stroke="currentColor"
|
|
333
|
+
viewBox="0 0 24 24"
|
|
334
|
+
>
|
|
335
|
+
<path
|
|
336
|
+
stroke-linecap="round"
|
|
337
|
+
stroke-linejoin="round"
|
|
338
|
+
stroke-width="2"
|
|
339
|
+
d="M4 6h16M4 12h16M4 18h16"
|
|
340
|
+
/>
|
|
341
|
+
</svg>
|
|
342
|
+
</button>
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
|
|
347
|
+
<!-- Mobile Navigation Menu -->
|
|
348
|
+
<div
|
|
349
|
+
id="mobile-menu"
|
|
350
|
+
x-show="mobileMenuOpen"
|
|
351
|
+
x-cloak
|
|
352
|
+
x-transition:enter="transition ease-out duration-200"
|
|
353
|
+
x-transition:enter-start="opacity-0 -translate-y-1"
|
|
354
|
+
x-transition:enter-end="opacity-100 translate-y-0"
|
|
355
|
+
x-transition:leave="transition ease-in duration-150"
|
|
356
|
+
x-transition:leave-start="opacity-100 translate-y-0"
|
|
357
|
+
x-transition:leave-end="opacity-0 -translate-y-1"
|
|
358
|
+
@click.outside="mobileMenuOpen = false"
|
|
359
|
+
class="
|
|
360
|
+
md:hidden border-t border-gray-200 dark:border-gray-700 bg-white
|
|
361
|
+
dark:bg-gray-800
|
|
362
|
+
"
|
|
363
|
+
>
|
|
364
|
+
<nav class="max-w-7xl mx-auto px-4 py-3 space-y-1">
|
|
365
|
+
<% nav_items.each do |item| %>
|
|
366
|
+
<%= render "ruby_llm/agents/shared/nav_link", path: item[:path], label: item[:label], icon: item[:icon], mobile: true %>
|
|
367
|
+
<% end %>
|
|
368
|
+
</nav>
|
|
369
|
+
</div>
|
|
370
|
+
</header>
|
|
371
|
+
|
|
372
|
+
<%# Tenant Context Badge - shows when viewing specific tenant %>
|
|
373
|
+
<% if tenant_filter_enabled? && current_tenant_id.present? %>
|
|
374
|
+
<div class="bg-blue-50 dark:bg-blue-900/20 border-b border-blue-100 dark:border-blue-900/50">
|
|
375
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-2">
|
|
376
|
+
<div class="flex items-center justify-between">
|
|
377
|
+
<div class="flex items-center gap-2 text-sm text-blue-700 dark:text-blue-300">
|
|
378
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
379
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
|
380
|
+
</svg>
|
|
381
|
+
<span>Viewing tenant:</span>
|
|
382
|
+
<span class="font-semibold"><%= current_tenant_id %></span>
|
|
383
|
+
</div>
|
|
384
|
+
<a
|
|
385
|
+
href="<%= url_for(request.query_parameters.except('tenant_id')) %>"
|
|
386
|
+
class="flex items-center gap-1 text-sm text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-200"
|
|
387
|
+
>
|
|
388
|
+
<span>Clear filter</span>
|
|
389
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
390
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
391
|
+
</svg>
|
|
392
|
+
</a>
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
<% end %>
|
|
397
|
+
|
|
398
|
+
<!-- Main content -->
|
|
399
|
+
<main class="max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-8 h-full">
|
|
400
|
+
<%= yield %>
|
|
401
|
+
</main>
|
|
402
|
+
|
|
403
|
+
<!-- Footer -->
|
|
404
|
+
<footer
|
|
405
|
+
class="border-t bg-white dark:bg-gray-800 dark:border-gray-700 mt-auto"
|
|
406
|
+
x-data="themeManager()"
|
|
407
|
+
>
|
|
408
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
|
409
|
+
<div class="flex flex-col sm:flex-row items-center sm:justify-between gap-3 sm:gap-0">
|
|
410
|
+
<div class="flex items-center space-x-2">
|
|
411
|
+
<label for="theme-select" class="text-sm text-gray-500 dark:text-gray-400">Theme:</label>
|
|
412
|
+
<select
|
|
413
|
+
id="theme-select"
|
|
414
|
+
x-model="preference"
|
|
415
|
+
@change="setTheme($event.target.value)"
|
|
416
|
+
class="text-sm border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 py-1 px-2"
|
|
417
|
+
>
|
|
418
|
+
<option value="light">Light</option>
|
|
419
|
+
<option value="dark">Dark</option>
|
|
420
|
+
<option value="auto">Auto</option>
|
|
421
|
+
</select>
|
|
422
|
+
</div>
|
|
423
|
+
<p class="text-sm text-gray-500 dark:text-gray-400 text-center">
|
|
424
|
+
Powered by <a href="https://github.com/adham90/ruby_llm-agents" class="text-blue-600 dark:text-blue-400 hover:underline">ruby_llm-agents</a>
|
|
425
|
+
</p>
|
|
426
|
+
</div>
|
|
427
|
+
</div>
|
|
428
|
+
</footer>
|
|
429
|
+
</body>
|
|
430
|
+
</html>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<%
|
|
2
|
+
type = local_assigns[:type] || "agents"
|
|
3
|
+
is_workflow = type == "workflows"
|
|
4
|
+
%>
|
|
5
|
+
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-8 text-center">
|
|
6
|
+
<svg class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
7
|
+
<% if is_workflow %>
|
|
8
|
+
<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"/>
|
|
9
|
+
<% else %>
|
|
10
|
+
<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" />
|
|
11
|
+
<% end %>
|
|
12
|
+
</svg>
|
|
13
|
+
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
14
|
+
No <%= type %> found
|
|
15
|
+
</h3>
|
|
16
|
+
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
17
|
+
<% if is_workflow %>
|
|
18
|
+
Create a workflow by subclassing <code class="bg-gray-100 dark:bg-gray-700 dark:text-gray-200 px-1 rounded">Pipeline</code>, <code class="bg-gray-100 dark:bg-gray-700 dark:text-gray-200 px-1 rounded">Parallel</code>, or <code class="bg-gray-100 dark:bg-gray-700 dark:text-gray-200 px-1 rounded">Router</code>
|
|
19
|
+
<% else %>
|
|
20
|
+
Create an agent by running <code class="bg-gray-100 dark:bg-gray-700 dark:text-gray-200 px-1 rounded">rails g ruby_llm_agents:agent YourAgentName</code>
|
|
21
|
+
<% end %>
|
|
22
|
+
</p>
|
|
23
|
+
</div>
|