ruby_llm-agents 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/controllers/concerns/ruby_llm/agents/paginatable.rb +9 -3
- data/app/controllers/concerns/ruby_llm/agents/sortable.rb +58 -0
- data/app/controllers/ruby_llm/agents/agents_controller.rb +59 -16
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +144 -20
- data/app/controllers/ruby_llm/agents/executions_controller.rb +13 -16
- data/app/controllers/ruby_llm/agents/workflows_controller.rb +279 -90
- data/app/helpers/ruby_llm/agents/application_helper.rb +100 -0
- data/app/mailers/ruby_llm/agents/alert_mailer.rb +84 -0
- data/app/mailers/ruby_llm/agents/application_mailer.rb +28 -0
- data/app/models/ruby_llm/agents/execution/analytics.rb +170 -20
- data/app/models/ruby_llm/agents/execution/scopes.rb +0 -31
- data/app/models/ruby_llm/agents/execution/workflow.rb +0 -129
- data/app/models/ruby_llm/agents/execution.rb +50 -14
- data/app/models/ruby_llm/agents/tenant/budgetable.rb +277 -0
- data/app/models/ruby_llm/agents/tenant/configurable.rb +135 -0
- data/app/models/ruby_llm/agents/tenant/trackable.rb +310 -0
- data/app/models/ruby_llm/agents/tenant.rb +146 -0
- data/app/models/ruby_llm/agents/tenant_budget.rb +12 -253
- data/app/services/ruby_llm/agents/agent_registry.rb +18 -12
- data/app/views/layouts/ruby_llm/agents/application.html.erb +72 -76
- data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -12
- data/app/views/ruby_llm/agents/agents/_sortable_header.html.erb +56 -0
- data/app/views/ruby_llm/agents/agents/_workflow.html.erb +5 -15
- data/app/views/ruby_llm/agents/agents/index.html.erb +271 -100
- data/app/views/ruby_llm/agents/agents/show.html.erb +1 -0
- data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +107 -0
- data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +18 -0
- data/app/views/ruby_llm/agents/api_configurations/show.html.erb +4 -1
- data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +66 -359
- data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +56 -0
- data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +115 -0
- data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +35 -60
- data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +17 -6
- data/app/views/ruby_llm/agents/dashboard/index.html.erb +373 -72
- data/app/views/ruby_llm/agents/executions/_execution.html.erb +0 -1
- data/app/views/ruby_llm/agents/executions/_filters.html.erb +51 -39
- data/app/views/ruby_llm/agents/executions/_list.html.erb +53 -195
- data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +5 -20
- data/app/views/ruby_llm/agents/executions/index.html.erb +7 -83
- data/app/views/ruby_llm/agents/executions/show.html.erb +10 -20
- data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +2 -1
- data/app/views/ruby_llm/agents/shared/_doc_link.html.erb +12 -0
- data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +3 -15
- data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +1 -1
- data/app/views/ruby_llm/agents/shared/_select_dropdown.html.erb +1 -1
- data/app/views/ruby_llm/agents/shared/_sortable_header.html.erb +53 -0
- data/app/views/ruby_llm/agents/shared/_status_badge.html.erb +7 -0
- data/app/views/ruby_llm/agents/shared/_status_dot.html.erb +1 -1
- data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +9 -35
- data/app/views/ruby_llm/agents/system_config/show.html.erb +4 -1
- data/app/views/ruby_llm/agents/tenants/index.html.erb +4 -1
- data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +7 -15
- data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +539 -0
- data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +920 -0
- data/app/views/ruby_llm/agents/workflows/index.html.erb +179 -0
- data/app/views/ruby_llm/agents/workflows/show.html.erb +164 -139
- data/config/routes.rb +1 -1
- data/lib/generators/ruby_llm_agents/agent_generator.rb +6 -36
- data/lib/generators/ruby_llm_agents/background_remover_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/embedder_generator.rb +5 -38
- data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/image_editor_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/image_generator_generator.rb +8 -41
- data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +18 -46
- data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/image_variator_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/install_generator.rb +33 -56
- data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +480 -0
- data/lib/generators/ruby_llm_agents/multi_tenancy_generator.rb +42 -22
- data/lib/generators/ruby_llm_agents/restructure_generator.rb +2 -2
- data/lib/generators/ruby_llm_agents/speaker_generator.rb +8 -39
- data/lib/generators/ruby_llm_agents/templates/add_tenant_to_executions_migration.rb.tt +13 -2
- data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +5 -8
- data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +40 -42
- data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +20 -22
- data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +24 -26
- data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +20 -22
- data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +19 -17
- data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +31 -33
- data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +125 -127
- data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +20 -18
- data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +19 -17
- data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +19 -17
- data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +38 -40
- data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +42 -44
- data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +48 -0
- data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +19 -21
- data/lib/generators/ruby_llm_agents/templates/create_tenant_budgets_migration.rb.tt +11 -0
- data/lib/generators/ruby_llm_agents/templates/create_tenants_migration.rb.tt +72 -0
- data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +19 -21
- data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +20 -22
- data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +15 -17
- data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +25 -27
- data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +19 -21
- data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +20 -22
- data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +17 -19
- data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +15 -17
- data/lib/generators/ruby_llm_agents/templates/rename_tenant_budgets_to_tenants_migration.rb.tt +34 -0
- data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +87 -24
- data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +21 -27
- data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +46 -54
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +31 -39
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +22 -28
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +53 -63
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +46 -56
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +23 -31
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +22 -30
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +23 -31
- data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +38 -46
- data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +7 -7
- data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +59 -71
- data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +274 -23
- data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +29 -31
- data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +28 -30
- data/lib/generators/ruby_llm_agents/transcriber_generator.rb +10 -43
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +26 -0
- data/lib/ruby_llm/agents/core/configuration.rb +55 -43
- data/lib/ruby_llm/agents/core/llm_tenant.rb +60 -60
- data/lib/ruby_llm/agents/core/version.rb +1 -1
- data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +26 -0
- data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +4 -2
- data/lib/ruby_llm/agents/pipeline.rb +69 -0
- data/lib/ruby_llm/agents/workflow/approval.rb +205 -0
- data/lib/ruby_llm/agents/workflow/approval_store.rb +179 -0
- data/lib/ruby_llm/agents/workflow/dsl/executor.rb +467 -0
- data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +244 -0
- data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +289 -0
- data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +107 -0
- data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +150 -0
- data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +187 -0
- data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +352 -0
- data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +415 -0
- data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +257 -0
- data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +317 -0
- data/lib/ruby_llm/agents/workflow/dsl.rb +576 -0
- data/lib/ruby_llm/agents/workflow/instrumentation.rb +2 -7
- data/lib/ruby_llm/agents/workflow/notifiers/base.rb +117 -0
- data/lib/ruby_llm/agents/workflow/notifiers/email.rb +117 -0
- data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +180 -0
- data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +121 -0
- data/lib/ruby_llm/agents/workflow/notifiers.rb +70 -0
- data/lib/ruby_llm/agents/workflow/orchestrator.rb +190 -23
- data/lib/ruby_llm/agents/workflow/result.rb +202 -0
- data/lib/ruby_llm/agents/workflow/throttle_manager.rb +206 -0
- data/lib/ruby_llm/agents/workflow/wait_result.rb +213 -0
- metadata +43 -6
- data/app/views/ruby_llm/agents/dashboard/_execution_item.html.erb +0 -66
- data/lib/ruby_llm/agents/workflow/parallel.rb +0 -299
- data/lib/ruby_llm/agents/workflow/pipeline.rb +0 -306
- data/lib/ruby_llm/agents/workflow/router.rb +0 -429
|
@@ -1,23 +1,101 @@
|
|
|
1
|
+
<!-- Page Header -->
|
|
2
|
+
<div class="mb-6">
|
|
3
|
+
<div class="flex items-center gap-2">
|
|
4
|
+
<h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">Dashboard</h1>
|
|
5
|
+
<%= render "ruby_llm/agents/shared/doc_link" %>
|
|
6
|
+
</div>
|
|
7
|
+
<p class="text-gray-500 dark:text-gray-400 mt-1">Overview of agent executions and performance metrics</p>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
1
10
|
<!-- Action Center (only when critical alerts exist) -->
|
|
2
11
|
<%= render partial: "ruby_llm/agents/dashboard/action_center", locals: { critical_alerts: @critical_alerts } %>
|
|
3
12
|
|
|
4
|
-
<!--
|
|
5
|
-
<%= render partial: "ruby_llm/agents/dashboard/now_strip", locals: { now_strip: @now_strip } %>
|
|
6
|
-
|
|
7
|
-
<!-- Tenant Budget (when viewing specific tenant) -->
|
|
8
|
-
<%= render partial: "ruby_llm/agents/dashboard/tenant_budget", locals: { tenant_budget: @tenant_budget } %>
|
|
9
|
-
|
|
10
|
-
<!-- Activity Chart -->
|
|
13
|
+
<!-- Unified Metrics + Chart Component (Plausible-style) -->
|
|
11
14
|
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 mb-6">
|
|
12
|
-
|
|
15
|
+
<!-- Header with range picker -->
|
|
16
|
+
<div class="px-6 py-4 border-b border-gray-100 dark:border-gray-700">
|
|
13
17
|
<div class="flex items-center justify-between">
|
|
14
|
-
<h3 class="text-
|
|
15
|
-
<
|
|
18
|
+
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Activity</h3>
|
|
19
|
+
<div class="relative range-dropdown-container">
|
|
20
|
+
<button type="button"
|
|
21
|
+
data-range-dropdown
|
|
22
|
+
class="inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors">
|
|
23
|
+
<span><%= range_display_name(@selected_range) %></span>
|
|
24
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
25
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
|
26
|
+
</svg>
|
|
27
|
+
</button>
|
|
28
|
+
<div class="range-dropdown-menu hidden absolute right-0 mt-2 w-56 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 z-10">
|
|
29
|
+
<div class="py-1">
|
|
30
|
+
<%= link_to "Today", ruby_llm_agents.root_path(range: "today"), class: "block px-4 py-2 text-sm #{@selected_range == 'today' ? 'bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300' : 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}" %>
|
|
31
|
+
<%= link_to "Last 7 Days", ruby_llm_agents.root_path(range: "7d"), class: "block px-4 py-2 text-sm #{@selected_range == '7d' ? 'bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300' : 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}" %>
|
|
32
|
+
<%= link_to "Last 30 Days", ruby_llm_agents.root_path(range: "30d"), class: "block px-4 py-2 text-sm #{@selected_range == '30d' ? 'bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300' : 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}" %>
|
|
33
|
+
<%= link_to "Last 60 Days", ruby_llm_agents.root_path(range: "60d"), class: "block px-4 py-2 text-sm #{@selected_range == '60d' ? 'bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300' : 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}" %>
|
|
34
|
+
<%= link_to "Last 90 Days", ruby_llm_agents.root_path(range: "90d"), class: "block px-4 py-2 text-sm #{@selected_range == '90d' ? 'bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300' : 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}" %>
|
|
35
|
+
<hr class="border-gray-200 dark:border-gray-700 my-1">
|
|
36
|
+
<button type="button" data-custom-range-toggle class="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
|
|
37
|
+
Custom Range...
|
|
38
|
+
</button>
|
|
39
|
+
</div>
|
|
40
|
+
<div data-custom-range-form class="hidden border-t border-gray-200 dark:border-gray-700 p-3">
|
|
41
|
+
<div class="space-y-3">
|
|
42
|
+
<div>
|
|
43
|
+
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">From</label>
|
|
44
|
+
<input type="date"
|
|
45
|
+
name="range_from"
|
|
46
|
+
class="w-full px-2 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
|
|
47
|
+
</div>
|
|
48
|
+
<div>
|
|
49
|
+
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">To</label>
|
|
50
|
+
<input type="date"
|
|
51
|
+
name="range_to"
|
|
52
|
+
class="w-full px-2 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
|
|
53
|
+
</div>
|
|
54
|
+
<button type="button"
|
|
55
|
+
data-apply-custom-range
|
|
56
|
+
class="w-full px-3 py-1.5 text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 rounded-md transition-colors">
|
|
57
|
+
Apply
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<!-- Metric Row (Plausible-style: simple text, no cards) -->
|
|
67
|
+
<div class="px-6 py-4 border-b border-gray-100 dark:border-gray-700">
|
|
68
|
+
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-4">
|
|
69
|
+
<% metrics = [
|
|
70
|
+
{ key: "success", label: "SUCCESS", value: @now_strip[:success_today], change: @now_strip.dig(:comparisons, :success_change) },
|
|
71
|
+
{ key: "errors", label: "ERRORS", value: @now_strip[:errors_today], change: @now_strip.dig(:comparisons, :errors_change), is_error: @now_strip[:errors_today] > 0 },
|
|
72
|
+
{ key: "cost", label: "COST", value: "$#{number_with_precision(@now_strip[:cost_today], precision: 2)}", change: @now_strip.dig(:comparisons, :cost_change) },
|
|
73
|
+
{ key: "duration", label: "AVG TIME", value: format_duration_ms(@now_strip[:avg_duration_ms]), change: @now_strip.dig(:comparisons, :duration_change) },
|
|
74
|
+
{ key: "tokens", label: "TOKENS", value: number_to_human_short(@now_strip[:total_tokens]), change: @now_strip.dig(:comparisons, :tokens_change) }
|
|
75
|
+
] %>
|
|
76
|
+
|
|
77
|
+
<% metrics.each_with_index do |metric, i| %>
|
|
78
|
+
<button type="button"
|
|
79
|
+
data-metric="<%= metric[:key] %>"
|
|
80
|
+
data-index="<%= i %>"
|
|
81
|
+
class="metric-btn group text-left transition-all pb-2 <%= i == 0 ? 'border-b-2 border-indigo-500' : 'border-b-2 border-transparent hover:border-gray-300 dark:hover:border-gray-600' %>">
|
|
82
|
+
<span class="text-xs font-medium tracking-wide text-gray-500 dark:text-gray-400 uppercase"><%= metric[:label] %></span>
|
|
83
|
+
<div class="flex items-baseline gap-1">
|
|
84
|
+
<span class="text-2xl font-bold <%= metric[:is_error] ? 'text-red-600 dark:text-red-400' : 'text-gray-900 dark:text-gray-100' %>"><%= metric[:value] %></span>
|
|
85
|
+
<% if metric[:change] %>
|
|
86
|
+
<span class="text-xs font-medium <%= metric[:change] > 0 ? (metric[:key].in?(%w[success tokens]) ? 'text-green-600' : 'text-red-600') : (metric[:key].in?(%w[success tokens]) ? 'text-red-600' : 'text-green-600') %>">
|
|
87
|
+
<%= metric[:change] > 0 ? '+' : '' %><%= metric[:change] %>%
|
|
88
|
+
</span>
|
|
89
|
+
<% end %>
|
|
90
|
+
</div>
|
|
91
|
+
</button>
|
|
92
|
+
<% end %>
|
|
16
93
|
</div>
|
|
17
94
|
</div>
|
|
18
95
|
|
|
19
|
-
|
|
20
|
-
|
|
96
|
+
<!-- Chart -->
|
|
97
|
+
<div id="activity-chart-container" class="px-4 pt-4 pb-6">
|
|
98
|
+
<div id="activity-chart" style="width: 100%; height: 280px;"></div>
|
|
21
99
|
</div>
|
|
22
100
|
|
|
23
101
|
<script>
|
|
@@ -27,6 +105,35 @@
|
|
|
27
105
|
const hourMs = 3600000;
|
|
28
106
|
const dayMs = 24 * hourMs;
|
|
29
107
|
|
|
108
|
+
let chartData = null;
|
|
109
|
+
let chart = null;
|
|
110
|
+
let selectedMetric = 0;
|
|
111
|
+
|
|
112
|
+
const metricConfig = [
|
|
113
|
+
{ name: 'Success', color: '#6366F1' },
|
|
114
|
+
{ name: 'Errors', color: '#6366F1' },
|
|
115
|
+
{ name: 'Cost', color: '#6366F1', isCurrency: true },
|
|
116
|
+
{ name: 'Duration', color: '#6366F1', isDuration: true },
|
|
117
|
+
{ name: 'Tokens', color: '#6366F1' }
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
// Calculate time range in milliseconds based on range string
|
|
121
|
+
function getTimeRangeMs(range) {
|
|
122
|
+
if (range === 'today') return dayMs;
|
|
123
|
+
if (range === '7d') return 7 * dayMs;
|
|
124
|
+
if (range === '30d') return 30 * dayMs;
|
|
125
|
+
if (range === '60d') return 60 * dayMs;
|
|
126
|
+
if (range === '90d') return 90 * dayMs;
|
|
127
|
+
// Custom range: calculate from dates
|
|
128
|
+
if (range.includes('_')) {
|
|
129
|
+
const [from, to] = range.split('_');
|
|
130
|
+
const fromDate = new Date(from);
|
|
131
|
+
const toDate = new Date(to);
|
|
132
|
+
return toDate - fromDate + dayMs;
|
|
133
|
+
}
|
|
134
|
+
return dayMs;
|
|
135
|
+
}
|
|
136
|
+
|
|
30
137
|
function convertToDatetimePoints(data, range) {
|
|
31
138
|
const now = Date.now();
|
|
32
139
|
const numPoints = data.length;
|
|
@@ -41,93 +148,189 @@
|
|
|
41
148
|
return num.toLocaleString();
|
|
42
149
|
}
|
|
43
150
|
|
|
151
|
+
function formatDuration(ms) {
|
|
152
|
+
if (!ms || ms === 0) return '0ms';
|
|
153
|
+
if (ms < 1000) return ms.toFixed(0) + 'ms';
|
|
154
|
+
if (ms < 60000) return (ms / 1000).toFixed(1) + 's';
|
|
155
|
+
return (ms / 60000).toFixed(1) + 'm';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function updateChart(metricIndex) {
|
|
159
|
+
if (!chartData || !chart) return;
|
|
160
|
+
|
|
161
|
+
const config = metricConfig[metricIndex];
|
|
162
|
+
const series = chartData.series[metricIndex];
|
|
163
|
+
|
|
164
|
+
chart.series[0].update({
|
|
165
|
+
name: config.name,
|
|
166
|
+
data: convertToDatetimePoints(series.data, range),
|
|
167
|
+
color: config.color
|
|
168
|
+
}, false);
|
|
169
|
+
|
|
170
|
+
chart.yAxis[0].update({
|
|
171
|
+
labels: {
|
|
172
|
+
style: { color: '#9CA3AF' },
|
|
173
|
+
formatter: function() {
|
|
174
|
+
if (config.isCurrency) return '$' + this.value;
|
|
175
|
+
if (config.isDuration) return formatDuration(this.value);
|
|
176
|
+
return this.value;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}, false);
|
|
180
|
+
|
|
181
|
+
chart.tooltip.update({
|
|
182
|
+
formatter: function() {
|
|
183
|
+
const dateFormat = range === 'today' ? '%H:%M' : '%b %d';
|
|
184
|
+
const dateStr = Highcharts.dateFormat(dateFormat, this.x);
|
|
185
|
+
let valueStr;
|
|
186
|
+
if (config.isCurrency) valueStr = '$' + this.y.toFixed(4);
|
|
187
|
+
else if (config.isDuration) valueStr = formatDuration(this.y);
|
|
188
|
+
else valueStr = formatNumber(this.y);
|
|
189
|
+
return '<b>' + dateStr + '</b><br/>' + config.name + ': ' + valueStr;
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
chart.redraw();
|
|
194
|
+
|
|
195
|
+
// Update selection styles
|
|
196
|
+
document.querySelectorAll('.metric-btn').forEach((btn, i) => {
|
|
197
|
+
btn.classList.remove('border-indigo-500', 'border-transparent');
|
|
198
|
+
btn.classList.add(i === metricIndex ? 'border-indigo-500' : 'border-transparent');
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
44
202
|
function initChart() {
|
|
45
203
|
fetch(chartUrl)
|
|
46
204
|
.then(res => res.json())
|
|
47
205
|
.then(data => {
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
const costData = convertToDatetimePoints(data.series[2].data, range);
|
|
52
|
-
|
|
53
|
-
// Update totals in header
|
|
54
|
-
const totals = data.totals;
|
|
55
|
-
const totalExecs = totals.success + totals.failed;
|
|
56
|
-
document.getElementById('chart-totals').textContent =
|
|
57
|
-
formatNumber(totalExecs) + ' executions • $' + totals.cost.toFixed(2);
|
|
58
|
-
|
|
59
|
-
const timeRange = range === 'today' ? dayMs : (range === '7d' ? 7 * dayMs : 30 * dayMs);
|
|
206
|
+
chartData = data;
|
|
207
|
+
const config = metricConfig[0];
|
|
208
|
+
const timeRange = getTimeRangeMs(range);
|
|
60
209
|
const dateFormat = range === 'today' ? '%H:%M' : '%b %d';
|
|
210
|
+
const now = Date.now();
|
|
61
211
|
|
|
62
|
-
Highcharts.chart('activity-chart', {
|
|
63
|
-
chart: { type: 'areaspline', backgroundColor: 'transparent' },
|
|
212
|
+
chart = Highcharts.chart('activity-chart', {
|
|
213
|
+
chart: { type: 'areaspline', backgroundColor: 'transparent', spacing: [10, 0, 10, 0] },
|
|
64
214
|
title: { text: null },
|
|
65
215
|
xAxis: {
|
|
66
216
|
type: 'datetime',
|
|
67
217
|
min: now - timeRange,
|
|
68
218
|
max: now,
|
|
69
|
-
labels: { style: { color: '#9CA3AF' }, format: '{value:' + dateFormat + '}' },
|
|
70
|
-
lineColor: '
|
|
219
|
+
labels: { style: { color: '#9CA3AF', fontSize: '11px' }, format: '{value:' + dateFormat + '}' },
|
|
220
|
+
lineColor: 'transparent',
|
|
221
|
+
tickLength: 0
|
|
71
222
|
},
|
|
72
|
-
yAxis:
|
|
223
|
+
yAxis: {
|
|
73
224
|
title: { text: null },
|
|
74
225
|
min: 0,
|
|
75
226
|
allowDecimals: false,
|
|
76
|
-
labels: { style: { color: '#9CA3AF' } },
|
|
77
|
-
gridLineColor: 'rgba(156, 163, 175, 0.
|
|
78
|
-
}, {
|
|
79
|
-
title: { text: null },
|
|
80
|
-
min: 0,
|
|
81
|
-
labels: { style: { color: '#F59E0B' }, format: '${value}' },
|
|
82
|
-
opposite: true,
|
|
83
|
-
gridLineWidth: 0
|
|
84
|
-
}],
|
|
85
|
-
legend: {
|
|
86
|
-
enabled: true,
|
|
87
|
-
align: 'center',
|
|
88
|
-
verticalAlign: 'bottom',
|
|
89
|
-
itemStyle: { color: '#9CA3AF', fontWeight: 'normal' },
|
|
90
|
-
itemHoverStyle: { color: '#D1D5DB' }
|
|
227
|
+
labels: { style: { color: '#9CA3AF', fontSize: '11px' } },
|
|
228
|
+
gridLineColor: 'rgba(156, 163, 175, 0.15)'
|
|
91
229
|
},
|
|
230
|
+
legend: { enabled: false },
|
|
92
231
|
credits: { enabled: false },
|
|
93
232
|
tooltip: {
|
|
94
|
-
shared: true,
|
|
95
233
|
backgroundColor: 'rgba(17, 24, 39, 0.95)',
|
|
96
|
-
borderColor: '
|
|
97
|
-
|
|
98
|
-
|
|
234
|
+
borderColor: 'transparent',
|
|
235
|
+
borderRadius: 8,
|
|
236
|
+
style: { color: '#F3F4F6', fontSize: '12px' },
|
|
237
|
+
formatter: function() {
|
|
238
|
+
return '<b>' + Highcharts.dateFormat(dateFormat, this.x) + '</b><br/>' +
|
|
239
|
+
config.name + ': ' + formatNumber(this.y);
|
|
240
|
+
}
|
|
99
241
|
},
|
|
100
242
|
plotOptions: {
|
|
101
243
|
areaspline: {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
spline: {
|
|
244
|
+
fillColor: {
|
|
245
|
+
linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 },
|
|
246
|
+
stops: [[0, 'rgba(99, 102, 241, 0.3)'], [1, 'rgba(99, 102, 241, 0)']]
|
|
247
|
+
},
|
|
107
248
|
lineWidth: 2,
|
|
108
249
|
marker: { enabled: false, states: { hover: { enabled: true, radius: 4 } } }
|
|
109
250
|
}
|
|
110
251
|
},
|
|
111
|
-
series: [
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
]
|
|
252
|
+
series: [{
|
|
253
|
+
name: config.name,
|
|
254
|
+
data: convertToDatetimePoints(data.series[0].data, range),
|
|
255
|
+
color: config.color
|
|
256
|
+
}]
|
|
116
257
|
});
|
|
117
258
|
})
|
|
118
259
|
.catch(err => console.log('[RubyLLM::Agents] Chart error:', err));
|
|
260
|
+
|
|
261
|
+
document.querySelectorAll('.metric-btn').forEach(btn => {
|
|
262
|
+
btn.addEventListener('click', function() {
|
|
263
|
+
const index = parseInt(this.dataset.index);
|
|
264
|
+
selectedMetric = index;
|
|
265
|
+
updateChart(index);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Dropdown toggle functionality
|
|
271
|
+
function initDropdown() {
|
|
272
|
+
const dropdownBtn = document.querySelector('[data-range-dropdown]');
|
|
273
|
+
const dropdownMenu = document.querySelector('.range-dropdown-menu');
|
|
274
|
+
const customRangeToggle = document.querySelector('[data-custom-range-toggle]');
|
|
275
|
+
const customRangeForm = document.querySelector('[data-custom-range-form]');
|
|
276
|
+
const applyBtn = document.querySelector('[data-apply-custom-range]');
|
|
277
|
+
|
|
278
|
+
if (dropdownBtn && dropdownMenu) {
|
|
279
|
+
// Toggle dropdown on button click
|
|
280
|
+
dropdownBtn.addEventListener('click', function(e) {
|
|
281
|
+
e.stopPropagation();
|
|
282
|
+
dropdownMenu.classList.toggle('hidden');
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Close dropdown when clicking outside
|
|
286
|
+
document.addEventListener('click', function(e) {
|
|
287
|
+
if (!e.target.closest('.range-dropdown-container')) {
|
|
288
|
+
dropdownMenu.classList.add('hidden');
|
|
289
|
+
if (customRangeForm) customRangeForm.classList.add('hidden');
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Toggle custom range form
|
|
295
|
+
if (customRangeToggle && customRangeForm) {
|
|
296
|
+
customRangeToggle.addEventListener('click', function(e) {
|
|
297
|
+
e.stopPropagation();
|
|
298
|
+
customRangeForm.classList.toggle('hidden');
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Apply custom range
|
|
303
|
+
if (applyBtn) {
|
|
304
|
+
applyBtn.addEventListener('click', function() {
|
|
305
|
+
const fromInput = document.querySelector('[name="range_from"]');
|
|
306
|
+
const toInput = document.querySelector('[name="range_to"]');
|
|
307
|
+
const from = fromInput ? fromInput.value : '';
|
|
308
|
+
const to = toInput ? toInput.value : '';
|
|
309
|
+
|
|
310
|
+
if (from && to) {
|
|
311
|
+
window.location.href = '?range=' + from + '_' + to;
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
}
|
|
119
315
|
}
|
|
120
316
|
|
|
121
317
|
if (document.readyState === 'loading') {
|
|
122
|
-
document.addEventListener('DOMContentLoaded',
|
|
318
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
319
|
+
initChart();
|
|
320
|
+
initDropdown();
|
|
321
|
+
});
|
|
123
322
|
} else {
|
|
124
323
|
initChart();
|
|
324
|
+
initDropdown();
|
|
125
325
|
}
|
|
126
326
|
})();
|
|
127
327
|
</script>
|
|
128
328
|
</div>
|
|
129
329
|
|
|
130
|
-
<!--
|
|
330
|
+
<!-- Tenant Budget (when viewing specific tenant) -->
|
|
331
|
+
<%= render partial: "ruby_llm/agents/dashboard/tenant_budget", locals: { tenant_budget: @tenant_budget } %>
|
|
332
|
+
|
|
333
|
+
<!-- Recent Activity Table -->
|
|
131
334
|
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 mb-6">
|
|
132
335
|
<div class="px-6 py-3 border-b border-gray-100 dark:border-gray-700">
|
|
133
336
|
<div class="flex justify-between items-center">
|
|
@@ -135,21 +338,119 @@
|
|
|
135
338
|
<%= link_to "View All", ruby_llm_agents.executions_path, class: "text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 text-sm font-medium" %>
|
|
136
339
|
</div>
|
|
137
340
|
</div>
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
341
|
+
|
|
342
|
+
<% if @recent_executions.any? %>
|
|
343
|
+
<div class="overflow-x-auto">
|
|
344
|
+
<table class="w-full text-sm">
|
|
345
|
+
<thead>
|
|
346
|
+
<tr class="text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider border-b border-gray-100 dark:border-gray-700">
|
|
347
|
+
<th class="px-4 py-3 w-8"></th>
|
|
348
|
+
<th class="px-4 py-3">Agent</th>
|
|
349
|
+
<th class="px-4 py-3 hidden sm:table-cell">Model</th>
|
|
350
|
+
<th class="px-4 py-3 text-right">Tokens</th>
|
|
351
|
+
<th class="px-4 py-3 text-right hidden sm:table-cell">Cost</th>
|
|
352
|
+
<th class="px-4 py-3 text-right hidden md:table-cell">Time</th>
|
|
353
|
+
<th class="px-4 py-3 text-right">When</th>
|
|
354
|
+
</tr>
|
|
355
|
+
</thead>
|
|
356
|
+
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
|
357
|
+
<% @recent_executions.first(5).each do |execution| %>
|
|
358
|
+
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors cursor-pointer <%= execution.status_error? ? 'bg-red-50/50 dark:bg-red-900/10' : '' %>"
|
|
359
|
+
onclick="window.location='<%= ruby_llm_agents.execution_path(execution) %>'"
|
|
360
|
+
<% if execution.status_error? && execution.error_message.present? %>
|
|
361
|
+
title="<%= execution.error_class %>: <%= execution.error_message.truncate(100) %>"
|
|
362
|
+
<% end %>>
|
|
363
|
+
<!-- Status -->
|
|
364
|
+
<td class="px-4 py-3">
|
|
365
|
+
<%= render "ruby_llm/agents/shared/status_dot", status: execution.status %>
|
|
366
|
+
</td>
|
|
367
|
+
<!-- Agent + Badges -->
|
|
368
|
+
<td class="px-4 py-3">
|
|
369
|
+
<div class="flex items-center gap-1.5 min-w-0">
|
|
370
|
+
<% if execution.respond_to?(:workflow_type) && execution.workflow_type.present? %>
|
|
371
|
+
<%= render "ruby_llm/agents/shared/workflow_type_badge", workflow_type: execution.workflow_type, size: :xs, show_label: false %>
|
|
372
|
+
<% end %>
|
|
373
|
+
<span class="font-medium text-gray-900 dark:text-gray-100 truncate">
|
|
374
|
+
<%= execution.agent_type.gsub(/Agent$|Workflow$|Pipeline$|Parallel$|Router$/, '') %>
|
|
375
|
+
</span>
|
|
376
|
+
<% unless execution.status_running? %>
|
|
377
|
+
<% if execution.streaming? %>
|
|
378
|
+
<svg class="w-3.5 h-3.5 text-cyan-500 flex-shrink-0" title="Streaming" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
379
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
|
380
|
+
</svg>
|
|
381
|
+
<% end %>
|
|
382
|
+
<% if execution.cache_hit? %>
|
|
383
|
+
<svg class="w-3.5 h-3.5 text-purple-500 flex-shrink-0" title="Cached" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
384
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4"/>
|
|
385
|
+
</svg>
|
|
386
|
+
<% end %>
|
|
387
|
+
<% if execution.rate_limited? %>
|
|
388
|
+
<svg class="w-3.5 h-3.5 text-orange-500 flex-shrink-0" title="Rate Limited" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
389
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
|
390
|
+
</svg>
|
|
391
|
+
<% end %>
|
|
392
|
+
<% end %>
|
|
393
|
+
<% if execution.status_error? %>
|
|
394
|
+
<svg class="w-3.5 h-3.5 text-red-500 flex-shrink-0" title="Error" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
395
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
396
|
+
</svg>
|
|
397
|
+
<% end %>
|
|
398
|
+
</div>
|
|
399
|
+
</td>
|
|
400
|
+
<!-- Model -->
|
|
401
|
+
<td class="px-4 py-3 hidden sm:table-cell">
|
|
402
|
+
<span class="font-mono text-xs text-gray-500 dark:text-gray-400 truncate max-w-[120px] block">
|
|
403
|
+
<%= execution.model_id || '-' %>
|
|
404
|
+
</span>
|
|
405
|
+
</td>
|
|
406
|
+
<!-- Tokens -->
|
|
407
|
+
<td class="px-4 py-3 text-right text-gray-600 dark:text-gray-300">
|
|
408
|
+
<% if execution.status_running? %>
|
|
409
|
+
<span class="text-blue-500 dark:text-blue-400 animate-pulse">...</span>
|
|
410
|
+
<% else %>
|
|
411
|
+
<%= execution.total_tokens ? number_to_human_short(execution.total_tokens) : '-' %>
|
|
412
|
+
<% end %>
|
|
413
|
+
</td>
|
|
414
|
+
<!-- Cost -->
|
|
415
|
+
<td class="px-4 py-3 text-right text-gray-600 dark:text-gray-300 hidden sm:table-cell">
|
|
416
|
+
<% if execution.status_running? %>
|
|
417
|
+
<span class="text-blue-500 dark:text-blue-400 animate-pulse">...</span>
|
|
418
|
+
<% else %>
|
|
419
|
+
<%= execution.total_cost ? "$#{number_with_precision(execution.total_cost, precision: 2)}" : '-' %>
|
|
420
|
+
<% end %>
|
|
421
|
+
</td>
|
|
422
|
+
<!-- Duration -->
|
|
423
|
+
<td class="px-4 py-3 text-right text-gray-600 dark:text-gray-300 hidden md:table-cell">
|
|
424
|
+
<% if execution.status_running? %>
|
|
425
|
+
<span class="text-blue-500 dark:text-blue-400 animate-pulse">...</span>
|
|
426
|
+
<% else %>
|
|
427
|
+
<%= execution.duration_ms ? format_duration_ms(execution.duration_ms) : '-' %>
|
|
428
|
+
<% end %>
|
|
429
|
+
</td>
|
|
430
|
+
<!-- When -->
|
|
431
|
+
<td class="px-4 py-3 text-right text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
|
432
|
+
<%= time_ago_in_words(execution.created_at) %> ago
|
|
433
|
+
</td>
|
|
434
|
+
</tr>
|
|
435
|
+
<% end %>
|
|
436
|
+
</tbody>
|
|
437
|
+
</table>
|
|
438
|
+
</div>
|
|
439
|
+
<% else %>
|
|
440
|
+
<div class="py-8 text-center text-gray-500 dark:text-gray-400">
|
|
441
|
+
<p class="text-sm">No executions yet</p>
|
|
442
|
+
</div>
|
|
443
|
+
<% end %>
|
|
149
444
|
</div>
|
|
150
445
|
|
|
151
446
|
<!-- Agent Comparison + Top Errors -->
|
|
152
|
-
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
447
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
|
153
448
|
<%= render partial: "ruby_llm/agents/dashboard/agent_comparison", locals: { agent_stats: @agent_stats } %>
|
|
154
449
|
<%= render partial: "ruby_llm/agents/dashboard/top_errors", locals: { top_errors: @top_errors } %>
|
|
155
450
|
</div>
|
|
451
|
+
|
|
452
|
+
<!-- Model Performance + Cost Breakdown -->
|
|
453
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
454
|
+
<%= render partial: "ruby_llm/agents/dashboard/model_comparison", locals: { model_stats: @model_stats } %>
|
|
455
|
+
<%= render partial: "ruby_llm/agents/dashboard/model_cost_breakdown", locals: { model_stats: @model_stats } %>
|
|
456
|
+
</div>
|