ruby_llm-agents 0.3.3 → 0.3.4
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/ruby_llm/agents/dashboard_controller.rb +68 -4
- data/app/models/ruby_llm/agents/execution/analytics.rb +114 -13
- data/app/models/ruby_llm/agents/execution.rb +19 -58
- data/app/views/layouts/rubyllm/agents/application.html.erb +92 -350
- data/app/views/rubyllm/agents/agents/show.html.erb +331 -385
- data/app/views/rubyllm/agents/dashboard/_agent_comparison.html.erb +46 -0
- data/app/views/rubyllm/agents/dashboard/_budgets_bar.html.erb +0 -90
- data/app/views/rubyllm/agents/dashboard/_now_strip.html.erb +79 -5
- data/app/views/rubyllm/agents/dashboard/_top_errors.html.erb +49 -0
- data/app/views/rubyllm/agents/dashboard/index.html.erb +76 -121
- data/app/views/rubyllm/agents/executions/show.html.erb +134 -85
- data/app/views/rubyllm/agents/settings/show.html.erb +1 -1
- data/app/views/rubyllm/agents/shared/_breadcrumbs.html.erb +48 -0
- data/app/views/rubyllm/agents/shared/_nav_link.html.erb +27 -0
- data/config/routes.rb +2 -0
- data/lib/ruby_llm/agents/base/caching.rb +43 -0
- data/lib/ruby_llm/agents/base/cost_calculation.rb +103 -0
- data/lib/ruby_llm/agents/base/dsl.rb +261 -0
- data/lib/ruby_llm/agents/base/execution.rb +206 -0
- data/lib/ruby_llm/agents/base/reliability_execution.rb +131 -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 +15 -805
- data/lib/ruby_llm/agents/version.rb +1 -1
- metadata +12 -20
- 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/rubyllm/agents/dashboard/_now_strip_values.html.erb +0 -71
|
@@ -1,24 +1,28 @@
|
|
|
1
1
|
<div id="execution-detail" data-execution-id="<%= @execution.id %>" data-status="<%= @execution.status %>">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
stroke="currentColor"
|
|
8
|
-
viewBox="0 0 24 24"
|
|
9
|
-
>
|
|
10
|
-
<path
|
|
11
|
-
stroke-linecap="round"
|
|
12
|
-
stroke-linejoin="round"
|
|
13
|
-
stroke-width="2"
|
|
14
|
-
d="M10 19l-7-7m0 0l7-7m-7 7h18"
|
|
15
|
-
/>
|
|
16
|
-
</svg>
|
|
17
|
-
Back to Executions
|
|
18
|
-
<% end %>
|
|
19
|
-
</div>
|
|
2
|
+
<%= render "rubyllm/agents/shared/breadcrumbs", items: [
|
|
3
|
+
{ label: "Dashboard", path: ruby_llm_agents.root_path },
|
|
4
|
+
{ label: "Executions", path: ruby_llm_agents.executions_path },
|
|
5
|
+
{ label: "##{@execution.id}" }
|
|
6
|
+
] %>
|
|
20
7
|
|
|
21
8
|
<!-- Header -->
|
|
9
|
+
<%
|
|
10
|
+
# Collect secondary badges
|
|
11
|
+
secondary_badges = []
|
|
12
|
+
secondary_badges << { label: "Stream", color: "cyan" } if @execution.streaming?
|
|
13
|
+
secondary_badges << { label: "Cached", color: "purple" } if @execution.cache_hit
|
|
14
|
+
if @execution.finish_reason.present?
|
|
15
|
+
finish_color = case @execution.finish_reason
|
|
16
|
+
when 'stop' then 'green'
|
|
17
|
+
when 'length' then 'yellow'
|
|
18
|
+
when 'content_filter' then 'red'
|
|
19
|
+
when 'tool_calls' then 'blue'
|
|
20
|
+
else 'gray'
|
|
21
|
+
end
|
|
22
|
+
secondary_badges << { label: @execution.finish_reason, color: finish_color }
|
|
23
|
+
end
|
|
24
|
+
secondary_badges << { label: "Rate Limited", color: "orange" } if @execution.respond_to?(:rate_limited?) && @execution.rate_limited?
|
|
25
|
+
%>
|
|
22
26
|
<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-4 sm:p-5 mb-6">
|
|
23
27
|
<!-- Desktop: Row 1 - Agent name + badges + buttons + date -->
|
|
24
28
|
<div class="hidden sm:flex sm:items-center sm:justify-between gap-4">
|
|
@@ -27,20 +31,44 @@
|
|
|
27
31
|
<%= @execution.agent_type.gsub(/Agent$/, '') %>
|
|
28
32
|
</h2>
|
|
29
33
|
<%= render "rubyllm/agents/shared/status_badge", status: @execution.status, size: :md %>
|
|
30
|
-
<% if
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
34
|
+
<% if secondary_badges.any? %>
|
|
35
|
+
<div class="relative" x-data="{ showDetails: false }">
|
|
36
|
+
<button
|
|
37
|
+
type="button"
|
|
38
|
+
@mouseenter="showDetails = true"
|
|
39
|
+
@mouseleave="showDetails = false"
|
|
40
|
+
class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
|
|
41
|
+
>
|
|
42
|
+
+<%= secondary_badges.size %>
|
|
43
|
+
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
44
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
45
|
+
</svg>
|
|
46
|
+
</button>
|
|
47
|
+
<div
|
|
48
|
+
x-show="showDetails"
|
|
49
|
+
x-cloak
|
|
50
|
+
x-transition:enter="transition ease-out duration-100"
|
|
51
|
+
x-transition:enter-start="opacity-0 scale-95"
|
|
52
|
+
x-transition:enter-end="opacity-100 scale-100"
|
|
53
|
+
class="absolute left-0 mt-1 z-10 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 p-2 min-w-max"
|
|
54
|
+
>
|
|
55
|
+
<div class="flex flex-wrap gap-1.5">
|
|
56
|
+
<% secondary_badges.each do |badge| %>
|
|
57
|
+
<% badge_classes = case badge[:color]
|
|
58
|
+
when 'cyan' then 'bg-cyan-100 dark:bg-cyan-900/50 text-cyan-800 dark:text-cyan-300'
|
|
59
|
+
when 'purple' then 'bg-purple-100 dark:bg-purple-900/50 text-purple-800 dark:text-purple-300'
|
|
60
|
+
when 'green' then 'bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-300'
|
|
61
|
+
when 'yellow' then 'bg-yellow-100 dark:bg-yellow-900/50 text-yellow-800 dark:text-yellow-300'
|
|
62
|
+
when 'red' then 'bg-red-100 dark:bg-red-900/50 text-red-800 dark:text-red-300'
|
|
63
|
+
when 'blue' then 'bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-300'
|
|
64
|
+
when 'orange' then 'bg-orange-100 dark:bg-orange-900/50 text-orange-800 dark:text-orange-300'
|
|
65
|
+
else 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300'
|
|
66
|
+
end %>
|
|
67
|
+
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium <%= badge_classes %>"><%= badge[:label] %></span>
|
|
68
|
+
<% end %>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
44
72
|
<% end %>
|
|
45
73
|
</div>
|
|
46
74
|
<div class="flex items-center gap-3 flex-shrink-0">
|
|
@@ -88,20 +116,18 @@
|
|
|
88
116
|
</h2>
|
|
89
117
|
<div class="flex flex-wrap items-center gap-2 mt-2">
|
|
90
118
|
<%= render "rubyllm/agents/shared/status_badge", status: @execution.status, size: :md %>
|
|
91
|
-
<%
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
'
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
} %>
|
|
104
|
-
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-medium <%= finish_colors[@execution.finish_reason] || 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300' %>"><%= @execution.finish_reason %></span>
|
|
119
|
+
<% secondary_badges.each do |badge| %>
|
|
120
|
+
<% badge_classes = case badge[:color]
|
|
121
|
+
when 'cyan' then 'bg-cyan-100 dark:bg-cyan-900/50 text-cyan-800 dark:text-cyan-300'
|
|
122
|
+
when 'purple' then 'bg-purple-100 dark:bg-purple-900/50 text-purple-800 dark:text-purple-300'
|
|
123
|
+
when 'green' then 'bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-300'
|
|
124
|
+
when 'yellow' then 'bg-yellow-100 dark:bg-yellow-900/50 text-yellow-800 dark:text-yellow-300'
|
|
125
|
+
when 'red' then 'bg-red-100 dark:bg-red-900/50 text-red-800 dark:text-red-300'
|
|
126
|
+
when 'blue' then 'bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-300'
|
|
127
|
+
when 'orange' then 'bg-orange-100 dark:bg-orange-900/50 text-orange-800 dark:text-orange-300'
|
|
128
|
+
else 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300'
|
|
129
|
+
end %>
|
|
130
|
+
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-medium <%= badge_classes %>"><%= badge[:label] %></span>
|
|
105
131
|
<% end %>
|
|
106
132
|
</div>
|
|
107
133
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-2">
|
|
@@ -484,7 +510,8 @@
|
|
|
484
510
|
|
|
485
511
|
<!-- Tool Calls -->
|
|
486
512
|
<% if @execution.tool_calls.present? && @execution.tool_calls.any? %>
|
|
487
|
-
|
|
513
|
+
<% tool_call_count = @execution.tool_calls.size %>
|
|
514
|
+
<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6" x-data="{ expanded: <%= tool_call_count <= 3 %> }">
|
|
488
515
|
<div class="flex items-center justify-between mb-4">
|
|
489
516
|
<div class="flex items-center gap-2">
|
|
490
517
|
<svg class="w-5 h-5 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
@@ -493,25 +520,32 @@
|
|
|
493
520
|
</svg>
|
|
494
521
|
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Tool Calls</h3>
|
|
495
522
|
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-300">
|
|
496
|
-
<%=
|
|
523
|
+
<%= tool_call_count %>
|
|
497
524
|
</span>
|
|
498
525
|
</div>
|
|
499
|
-
<
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
<
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
<
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
526
|
+
<div class="flex items-center gap-2">
|
|
527
|
+
<button
|
|
528
|
+
type="button"
|
|
529
|
+
data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(@execution.tool_calls)) %>"
|
|
530
|
+
class="copy-json-btn inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium 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 rounded-md transition-colors"
|
|
531
|
+
>
|
|
532
|
+
<svg class="w-4 h-4 copy-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
533
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
|
|
534
|
+
</svg>
|
|
535
|
+
<svg class="w-4 h-4 check-icon hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
536
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
|
537
|
+
</svg>
|
|
538
|
+
<span>Copy</span>
|
|
539
|
+
</button>
|
|
540
|
+
<% if tool_call_count > 3 %>
|
|
541
|
+
<button type="button" @click="expanded = !expanded" class="text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
|
542
|
+
<span x-text="expanded ? 'Collapse' : 'Expand'">Expand</span>
|
|
543
|
+
</button>
|
|
544
|
+
<% end %>
|
|
545
|
+
</div>
|
|
512
546
|
</div>
|
|
513
547
|
|
|
514
|
-
<div class="space-y-4">
|
|
548
|
+
<div class="space-y-4" x-show="expanded" <%= tool_call_count > 3 ? 'x-cloak' : '' %>>
|
|
515
549
|
<% @execution.tool_calls.each_with_index do |tool_call, index| %>
|
|
516
550
|
<%
|
|
517
551
|
# Handle both symbol and string keys
|
|
@@ -554,26 +588,36 @@
|
|
|
554
588
|
|
|
555
589
|
<!-- Metadata -->
|
|
556
590
|
<% if @execution.metadata.present? && @execution.metadata.any? %>
|
|
557
|
-
<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6">
|
|
558
|
-
<div class="flex items-center justify-between
|
|
559
|
-
<
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
<
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
591
|
+
<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6" x-data="{ expanded: false }">
|
|
592
|
+
<div class="flex items-center justify-between">
|
|
593
|
+
<div class="flex items-center gap-2">
|
|
594
|
+
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Metadata</h3>
|
|
595
|
+
<span class="text-xs text-gray-400 dark:text-gray-500">(<%= @execution.metadata.keys.count %> keys)</span>
|
|
596
|
+
</div>
|
|
597
|
+
<div class="flex items-center gap-2">
|
|
598
|
+
<button
|
|
599
|
+
type="button"
|
|
600
|
+
data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(redact_for_display(@execution.metadata))) %>"
|
|
601
|
+
data-copy-json-original="<%= Base64.strict_encode64(JSON.pretty_generate(@execution.metadata)) %>"
|
|
602
|
+
class="copy-json-btn inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium 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 rounded-md transition-colors"
|
|
603
|
+
>
|
|
604
|
+
<svg class="w-4 h-4 copy-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
605
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
|
|
606
|
+
</svg>
|
|
607
|
+
<svg class="w-4 h-4 check-icon hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
608
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
|
609
|
+
</svg>
|
|
610
|
+
<span>Copy</span>
|
|
611
|
+
</button>
|
|
612
|
+
<button type="button" @click="expanded = !expanded" class="text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
|
613
|
+
<span x-text="expanded ? 'Collapse' : 'Expand'">Expand</span>
|
|
614
|
+
</button>
|
|
615
|
+
</div>
|
|
616
|
+
</div>
|
|
617
|
+
<div x-show="expanded" x-cloak class="mt-4">
|
|
618
|
+
<pre id="metadata-masked" class="maskable-content bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto font-mono"><%= highlight_json_redacted(@execution.metadata) %></pre>
|
|
619
|
+
<pre id="metadata-original" class="maskable-content hidden bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto font-mono"><%= highlight_json(@execution.metadata) %></pre>
|
|
574
620
|
</div>
|
|
575
|
-
<pre id="metadata-masked" class="maskable-content bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto font-mono"><%= highlight_json_redacted(@execution.metadata) %></pre>
|
|
576
|
-
<pre id="metadata-original" class="maskable-content hidden bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto font-mono"><%= highlight_json(@execution.metadata) %></pre>
|
|
577
621
|
</div>
|
|
578
622
|
<% end %>
|
|
579
623
|
|
|
@@ -618,6 +662,7 @@
|
|
|
618
662
|
<span id="system-prompt-toggle">Expand</span>
|
|
619
663
|
</button>
|
|
620
664
|
</div>
|
|
665
|
+
<p id="system-prompt-preview" class="text-sm text-gray-600 dark:text-gray-300 font-mono bg-gray-50 dark:bg-gray-900 rounded-lg p-3 truncate"><%= @execution.system_prompt.truncate(150) %></p>
|
|
621
666
|
<pre id="system-prompt-content" class="hidden bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto max-h-96 font-mono whitespace-pre-wrap"><%= @execution.system_prompt %></pre>
|
|
622
667
|
</div>
|
|
623
668
|
<% end %>
|
|
@@ -631,6 +676,7 @@
|
|
|
631
676
|
<span id="user-prompt-toggle">Expand</span>
|
|
632
677
|
</button>
|
|
633
678
|
</div>
|
|
679
|
+
<p id="user-prompt-preview" class="text-sm text-gray-600 dark:text-gray-300 font-mono bg-gray-50 dark:bg-gray-900 rounded-lg p-3 truncate"><%= @execution.user_prompt.truncate(150) %></p>
|
|
634
680
|
<pre id="user-prompt-content" class="hidden bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto max-h-96 font-mono whitespace-pre-wrap"><%= @execution.user_prompt %></pre>
|
|
635
681
|
</div>
|
|
636
682
|
<% end %>
|
|
@@ -867,9 +913,12 @@
|
|
|
867
913
|
// Prompt toggle function
|
|
868
914
|
function togglePrompt(type) {
|
|
869
915
|
const content = document.getElementById(type + '-prompt-content');
|
|
916
|
+
const preview = document.getElementById(type + '-prompt-preview');
|
|
870
917
|
const toggle = document.getElementById(type + '-prompt-toggle');
|
|
871
|
-
content.classList.
|
|
872
|
-
|
|
918
|
+
const isHidden = content.classList.contains('hidden');
|
|
919
|
+
content.classList.toggle('hidden', !isHidden);
|
|
920
|
+
if (preview) preview.classList.toggle('hidden', isHidden);
|
|
921
|
+
toggle.textContent = isHidden ? 'Collapse' : 'Expand';
|
|
873
922
|
}
|
|
874
923
|
|
|
875
924
|
// Masking state (persisted in localStorage)
|
|
@@ -972,8 +1021,8 @@
|
|
|
972
1021
|
}
|
|
973
1022
|
});
|
|
974
1023
|
|
|
975
|
-
// Diagnostics panel toggle
|
|
976
|
-
let diagnosticsExpanded = localStorage.getItem('ruby_llm_agents_diagnostics_expanded')
|
|
1024
|
+
// Diagnostics panel toggle (default to expanded)
|
|
1025
|
+
let diagnosticsExpanded = localStorage.getItem('ruby_llm_agents_diagnostics_expanded') !== 'false';
|
|
977
1026
|
|
|
978
1027
|
function toggleDiagnostics() {
|
|
979
1028
|
diagnosticsExpanded = !diagnosticsExpanded;
|
|
@@ -128,7 +128,7 @@
|
|
|
128
128
|
<p class="text-sm font-medium text-gray-900 dark:text-gray-100">Parent Controller</p>
|
|
129
129
|
<p class="text-xs text-gray-500 dark:text-gray-400">Dashboard inherits from this</p>
|
|
130
130
|
</div>
|
|
131
|
-
<code class="bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 px-2 py-1 rounded text-
|
|
131
|
+
<code class="bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 px-2 py-1 rounded text-xs"><%= @config.dashboard_parent_controller %></code>
|
|
132
132
|
</div>
|
|
133
133
|
<div class="flex justify-between items-center">
|
|
134
134
|
<div>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<%
|
|
2
|
+
# Breadcrumb navigation partial
|
|
3
|
+
# Usage:
|
|
4
|
+
# render "rubyllm/agents/shared/breadcrumbs", items: [
|
|
5
|
+
# { label: "Dashboard", path: ruby_llm_agents.root_path },
|
|
6
|
+
# { label: "Executions", path: ruby_llm_agents.executions_path },
|
|
7
|
+
# { label: "#123" } # Current page (no path)
|
|
8
|
+
# ]
|
|
9
|
+
#
|
|
10
|
+
# Or with simple array:
|
|
11
|
+
# render "rubyllm/agents/shared/breadcrumbs", items: [
|
|
12
|
+
# ["Dashboard", ruby_llm_agents.root_path],
|
|
13
|
+
# ["Executions", ruby_llm_agents.executions_path],
|
|
14
|
+
# ["#123"]
|
|
15
|
+
# ]
|
|
16
|
+
|
|
17
|
+
items = local_assigns[:items] || []
|
|
18
|
+
%>
|
|
19
|
+
<nav class="flex items-center text-sm mb-4" aria-label="Breadcrumb">
|
|
20
|
+
<ol class="flex items-center space-x-1">
|
|
21
|
+
<% items.each_with_index do |item, index| %>
|
|
22
|
+
<%
|
|
23
|
+
# Normalize item format
|
|
24
|
+
if item.is_a?(Array)
|
|
25
|
+
label = item[0]
|
|
26
|
+
path = item[1]
|
|
27
|
+
else
|
|
28
|
+
label = item[:label]
|
|
29
|
+
path = item[:path]
|
|
30
|
+
end
|
|
31
|
+
is_last = index == items.length - 1
|
|
32
|
+
%>
|
|
33
|
+
<li class="flex items-center">
|
|
34
|
+
<% if index > 0 %>
|
|
35
|
+
<svg class="w-4 h-4 text-gray-400 dark:text-gray-500 mx-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
36
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
|
37
|
+
</svg>
|
|
38
|
+
<% end %>
|
|
39
|
+
|
|
40
|
+
<% if path.present? && !is_last %>
|
|
41
|
+
<%= link_to label, path, class: "text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 transition-colors" %>
|
|
42
|
+
<% else %>
|
|
43
|
+
<span class="text-gray-900 dark:text-gray-100 font-medium"><%= label %></span>
|
|
44
|
+
<% end %>
|
|
45
|
+
</li>
|
|
46
|
+
<% end %>
|
|
47
|
+
</ol>
|
|
48
|
+
</nav>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<%# Reusable navigation link for desktop and mobile %>
|
|
2
|
+
<%
|
|
3
|
+
# Determine if link is active
|
|
4
|
+
is_active = if path == ruby_llm_agents.root_path
|
|
5
|
+
current_page?(path)
|
|
6
|
+
elsif path == ruby_llm_agents.agents_path
|
|
7
|
+
request.path.start_with?(path)
|
|
8
|
+
else
|
|
9
|
+
current_page?(path)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Style classes
|
|
13
|
+
base_classes = mobile ? "flex items-center px-3 py-2 text-base font-medium rounded-md" : "inline-flex items-center px-3 py-1.5 text-sm font-medium rounded-md"
|
|
14
|
+
active_classes = "bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-gray-100"
|
|
15
|
+
inactive_classes = "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"
|
|
16
|
+
icon_classes = mobile ? "w-5 h-5 mr-3" : "w-4 h-4 mr-1.5"
|
|
17
|
+
|
|
18
|
+
link_options = { class: "#{base_classes} #{is_active ? active_classes : inactive_classes}" }
|
|
19
|
+
link_options["x-on:click"] = "mobileMenuOpen = false" if mobile
|
|
20
|
+
%>
|
|
21
|
+
|
|
22
|
+
<%= link_to path, **link_options do %>
|
|
23
|
+
<svg class="<%= icon_classes %>" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
24
|
+
<%= icon.html_safe %>
|
|
25
|
+
</svg>
|
|
26
|
+
<%= label %>
|
|
27
|
+
<% end %>
|
data/config/routes.rb
CHANGED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
class Base
|
|
6
|
+
# Cache management for agent responses
|
|
7
|
+
#
|
|
8
|
+
# Handles cache key generation and store access for
|
|
9
|
+
# caching agent execution results.
|
|
10
|
+
module Caching
|
|
11
|
+
# Returns the configured cache store
|
|
12
|
+
#
|
|
13
|
+
# @return [ActiveSupport::Cache::Store] The cache store
|
|
14
|
+
def cache_store
|
|
15
|
+
RubyLLM::Agents.configuration.cache_store
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Generates the full cache key for this agent invocation
|
|
19
|
+
#
|
|
20
|
+
# @return [String] Cache key in format "ruby_llm_agent/ClassName/version/hash"
|
|
21
|
+
def cache_key
|
|
22
|
+
["ruby_llm_agent", self.class.name, self.class.version, cache_key_hash].join("/")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Generates a hash of the cache key data
|
|
26
|
+
#
|
|
27
|
+
# @return [String] SHA256 hex digest of the cache key data
|
|
28
|
+
def cache_key_hash
|
|
29
|
+
Digest::SHA256.hexdigest(cache_key_data.to_json)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Returns data to include in cache key generation
|
|
33
|
+
#
|
|
34
|
+
# Override to customize what parameters affect cache invalidation.
|
|
35
|
+
#
|
|
36
|
+
# @return [Hash] Data to hash for cache key
|
|
37
|
+
def cache_key_data
|
|
38
|
+
@options.except(:skip_cache, :dry_run, :with)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
class Base
|
|
6
|
+
# Cost calculation methods for token and pricing calculations
|
|
7
|
+
#
|
|
8
|
+
# Handles input/output cost calculations, model info resolution,
|
|
9
|
+
# and budget tracking for agent executions.
|
|
10
|
+
module CostCalculation
|
|
11
|
+
# Calculates input cost from tokens
|
|
12
|
+
#
|
|
13
|
+
# @param input_tokens [Integer, nil] Number of input tokens
|
|
14
|
+
# @param response_model_id [String, nil] Model that responded
|
|
15
|
+
# @return [Float, nil] Input cost in USD
|
|
16
|
+
def result_input_cost(input_tokens, response_model_id)
|
|
17
|
+
return nil unless input_tokens
|
|
18
|
+
model_info = result_model_info(response_model_id)
|
|
19
|
+
return nil unless model_info&.pricing
|
|
20
|
+
price = model_info.pricing.text_tokens&.input || 0
|
|
21
|
+
(input_tokens / 1_000_000.0 * price).round(6)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Calculates output cost from tokens
|
|
25
|
+
#
|
|
26
|
+
# @param output_tokens [Integer, nil] Number of output tokens
|
|
27
|
+
# @param response_model_id [String, nil] Model that responded
|
|
28
|
+
# @return [Float, nil] Output cost in USD
|
|
29
|
+
def result_output_cost(output_tokens, response_model_id)
|
|
30
|
+
return nil unless output_tokens
|
|
31
|
+
model_info = result_model_info(response_model_id)
|
|
32
|
+
return nil unless model_info&.pricing
|
|
33
|
+
price = model_info.pricing.text_tokens&.output || 0
|
|
34
|
+
(output_tokens / 1_000_000.0 * price).round(6)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Calculates total cost from tokens
|
|
38
|
+
#
|
|
39
|
+
# @param input_tokens [Integer, nil] Number of input tokens
|
|
40
|
+
# @param output_tokens [Integer, nil] Number of output tokens
|
|
41
|
+
# @param response_model_id [String, nil] Model that responded
|
|
42
|
+
# @return [Float, nil] Total cost in USD
|
|
43
|
+
def result_total_cost(input_tokens, output_tokens, response_model_id)
|
|
44
|
+
input_cost = result_input_cost(input_tokens, response_model_id)
|
|
45
|
+
output_cost = result_output_cost(output_tokens, response_model_id)
|
|
46
|
+
return nil unless input_cost || output_cost
|
|
47
|
+
((input_cost || 0) + (output_cost || 0)).round(6)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Resolves model info for cost calculation
|
|
51
|
+
#
|
|
52
|
+
# @param response_model_id [String, nil] Model ID from response
|
|
53
|
+
# @return [Object, nil] Model info or nil
|
|
54
|
+
def result_model_info(response_model_id)
|
|
55
|
+
lookup_id = response_model_id || model
|
|
56
|
+
return nil unless lookup_id
|
|
57
|
+
model_obj, _provider = RubyLLM::Models.resolve(lookup_id)
|
|
58
|
+
model_obj
|
|
59
|
+
rescue StandardError
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Resolves model info for cost calculation (alternate method)
|
|
64
|
+
#
|
|
65
|
+
# @param model_id [String] The model identifier
|
|
66
|
+
# @return [Object, nil] Model info or nil
|
|
67
|
+
def resolve_model_info(model_id)
|
|
68
|
+
RubyLLM::Models.resolve(model_id)
|
|
69
|
+
rescue StandardError
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Records cost from an attempt to the budget tracker
|
|
74
|
+
#
|
|
75
|
+
# @param attempt_tracker [AttemptTracker] The attempt tracker
|
|
76
|
+
# @return [void]
|
|
77
|
+
def record_attempt_cost(attempt_tracker)
|
|
78
|
+
successful = attempt_tracker.successful_attempt
|
|
79
|
+
return unless successful
|
|
80
|
+
|
|
81
|
+
# Calculate cost for this execution
|
|
82
|
+
# Note: Full cost calculation happens in instrumentation, but we
|
|
83
|
+
# record the spend here for budget tracking
|
|
84
|
+
model_info = resolve_model_info(successful[:model_id])
|
|
85
|
+
return unless model_info&.pricing
|
|
86
|
+
|
|
87
|
+
input_tokens = successful[:input_tokens] || 0
|
|
88
|
+
output_tokens = successful[:output_tokens] || 0
|
|
89
|
+
|
|
90
|
+
input_price = model_info.pricing.text_tokens&.input || 0
|
|
91
|
+
output_price = model_info.pricing.text_tokens&.output || 0
|
|
92
|
+
|
|
93
|
+
total_cost = (input_tokens / 1_000_000.0 * input_price) +
|
|
94
|
+
(output_tokens / 1_000_000.0 * output_price)
|
|
95
|
+
|
|
96
|
+
BudgetTracker.record_spend!(self.class.name, total_cost)
|
|
97
|
+
rescue StandardError => e
|
|
98
|
+
Rails.logger.warn("[RubyLLM::Agents] Failed to record budget spend: #{e.message}")
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|