ruby_llm-agents 0.3.1 → 0.3.3
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 +88 -0
- data/app/models/ruby_llm/agents/execution/scopes.rb +10 -0
- data/app/models/ruby_llm/agents/execution.rb +7 -0
- data/app/views/layouts/rubyllm/agents/application.html.erb +11 -2
- data/app/views/rubyllm/agents/agents/_agent.html.erb +87 -0
- data/app/views/rubyllm/agents/agents/index.html.erb +2 -71
- data/app/views/rubyllm/agents/agents/show.html.erb +20 -33
- data/app/views/rubyllm/agents/dashboard/_action_center.html.erb +7 -7
- data/app/views/rubyllm/agents/dashboard/_execution_item.html.erb +54 -39
- data/app/views/rubyllm/agents/dashboard/index.html.erb +4 -34
- data/app/views/rubyllm/agents/executions/show.html.erb +166 -52
- data/lib/generators/ruby_llm_agents/templates/add_tool_calls_migration.rb.tt +28 -0
- data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +7 -0
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +13 -0
- data/lib/ruby_llm/agents/base.rb +208 -18
- data/lib/ruby_llm/agents/instrumentation.rb +36 -3
- data/lib/ruby_llm/agents/result.rb +235 -0
- data/lib/ruby_llm/agents/version.rb +1 -1
- data/lib/ruby_llm/agents.rb +1 -0
- metadata +4 -1
|
@@ -19,56 +19,109 @@
|
|
|
19
19
|
</div>
|
|
20
20
|
|
|
21
21
|
<!-- Header -->
|
|
22
|
-
<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
22
|
+
<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
|
+
<!-- Desktop: Row 1 - Agent name + badges + buttons + date -->
|
|
24
|
+
<div class="hidden sm:flex sm:items-center sm:justify-between gap-4">
|
|
25
|
+
<div class="flex items-center gap-3 min-w-0">
|
|
26
|
+
<h2 class="text-lg font-bold text-gray-900 dark:text-gray-100 truncate">
|
|
27
|
+
<%= @execution.agent_type.gsub(/Agent$/, '') %>
|
|
28
|
+
</h2>
|
|
29
|
+
<%= render "rubyllm/agents/shared/status_badge", status: @execution.status, size: :md %>
|
|
30
|
+
<% if @execution.streaming? %>
|
|
31
|
+
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-cyan-100 dark:bg-cyan-900/50 text-cyan-800 dark:text-cyan-300">Stream</span>
|
|
32
|
+
<% end %>
|
|
33
|
+
<% if @execution.cache_hit %>
|
|
34
|
+
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-purple-100 dark:bg-purple-900/50 text-purple-800 dark:text-purple-300">Cached</span>
|
|
35
|
+
<% end %>
|
|
36
|
+
<% if @execution.finish_reason.present? %>
|
|
37
|
+
<% finish_colors = {
|
|
38
|
+
'stop' => 'bg-green-100 text-green-800 dark:bg-green-900/50 dark:text-green-300',
|
|
39
|
+
'length' => 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/50 dark:text-yellow-300',
|
|
40
|
+
'content_filter' => 'bg-red-100 text-red-800 dark:bg-red-900/50 dark:text-red-300',
|
|
41
|
+
'tool_calls' => 'bg-blue-100 text-blue-800 dark:bg-blue-900/50 dark:text-blue-300'
|
|
42
|
+
} %>
|
|
43
|
+
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs 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>
|
|
44
|
+
<% end %>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="flex items-center gap-3 flex-shrink-0">
|
|
47
|
+
<%= button_to rerun_execution_path(@execution, dry_run: true),
|
|
48
|
+
method: :post,
|
|
49
|
+
data: { turbo: false },
|
|
50
|
+
class: "inline-flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors",
|
|
51
|
+
title: "Preview what would be sent without making an API call" do %>
|
|
52
|
+
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
53
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
|
54
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
|
55
|
+
</svg>
|
|
56
|
+
Dry Run
|
|
57
|
+
<% end %>
|
|
58
|
+
<button
|
|
59
|
+
type="button"
|
|
60
|
+
onclick="confirmRerun()"
|
|
61
|
+
class="inline-flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors"
|
|
62
|
+
title="Re-execute this agent with the same parameters"
|
|
63
|
+
>
|
|
64
|
+
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
65
|
+
<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"/>
|
|
66
|
+
</svg>
|
|
67
|
+
Rerun
|
|
68
|
+
</button>
|
|
69
|
+
<span class="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap"><%= @execution.created_at.strftime("%b %d, %H:%M") %></span>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
43
72
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
<% end %>
|
|
55
|
-
</div>
|
|
73
|
+
<!-- Desktop: Row 2 - Info line + relative time -->
|
|
74
|
+
<div class="hidden sm:flex sm:items-center sm:justify-between mt-1.5">
|
|
75
|
+
<p class="text-xs text-gray-500 dark:text-gray-400">
|
|
76
|
+
#<%= @execution.id %> · v<%= @execution.agent_version %>
|
|
77
|
+
<% if @execution.model_provider.present? %>
|
|
78
|
+
· <%= @execution.model_provider %>
|
|
79
|
+
<% end %>
|
|
80
|
+
</p>
|
|
81
|
+
<span class="text-xs text-gray-400 dark:text-gray-500"><%= time_ago_in_words(@execution.created_at) %> ago</span>
|
|
82
|
+
</div>
|
|
56
83
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
84
|
+
<!-- Mobile: Stacked layout -->
|
|
85
|
+
<div class="sm:hidden">
|
|
86
|
+
<h2 class="text-lg font-bold text-gray-900 dark:text-gray-100 truncate">
|
|
87
|
+
<%= @execution.agent_type.gsub(/Agent$/, '') %>
|
|
88
|
+
</h2>
|
|
89
|
+
<div class="flex flex-wrap items-center gap-2 mt-2">
|
|
90
|
+
<%= render "rubyllm/agents/shared/status_badge", status: @execution.status, size: :md %>
|
|
91
|
+
<% if @execution.streaming? %>
|
|
92
|
+
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-medium bg-cyan-100 dark:bg-cyan-900/50 text-cyan-800 dark:text-cyan-300">Stream</span>
|
|
93
|
+
<% end %>
|
|
94
|
+
<% if @execution.cache_hit %>
|
|
95
|
+
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-medium bg-purple-100 dark:bg-purple-900/50 text-purple-800 dark:text-purple-300">Cached</span>
|
|
96
|
+
<% end %>
|
|
97
|
+
<% if @execution.finish_reason.present? %>
|
|
98
|
+
<% finish_colors = {
|
|
99
|
+
'stop' => 'bg-green-100 text-green-800 dark:bg-green-900/50 dark:text-green-300',
|
|
100
|
+
'length' => 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/50 dark:text-yellow-300',
|
|
101
|
+
'content_filter' => 'bg-red-100 text-red-800 dark:bg-red-900/50 dark:text-red-300',
|
|
102
|
+
'tool_calls' => 'bg-blue-100 text-blue-800 dark:bg-blue-900/50 dark:text-blue-300'
|
|
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>
|
|
105
|
+
<% end %>
|
|
63
106
|
</div>
|
|
107
|
+
<p class="text-xs text-gray-500 dark:text-gray-400 mt-2">
|
|
108
|
+
#<%= @execution.id %> · v<%= @execution.agent_version %>
|
|
109
|
+
<% if @execution.model_provider.present? %>
|
|
110
|
+
· <%= @execution.model_provider %>
|
|
111
|
+
<% end %>
|
|
112
|
+
</p>
|
|
64
113
|
|
|
65
|
-
|
|
66
|
-
|
|
114
|
+
<!-- Mobile: Date + Buttons -->
|
|
115
|
+
<div class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-700">
|
|
116
|
+
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
|
117
|
+
<%= @execution.created_at.strftime("%b %d, %Y at %H:%M") %>
|
|
118
|
+
<span class="text-gray-400 dark:text-gray-500">· <%= time_ago_in_words(@execution.created_at) %> ago</span>
|
|
119
|
+
</p>
|
|
67
120
|
<div class="flex items-center gap-2">
|
|
68
121
|
<%= button_to rerun_execution_path(@execution, dry_run: true),
|
|
69
122
|
method: :post,
|
|
70
123
|
data: { turbo: false },
|
|
71
|
-
class: "inline-flex items-center gap-1.5 px-3 py-
|
|
124
|
+
class: "flex-1 inline-flex items-center justify-center gap-1.5 px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors",
|
|
72
125
|
title: "Preview what would be sent without making an API call" do %>
|
|
73
126
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
74
127
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
|
@@ -76,11 +129,10 @@
|
|
|
76
129
|
</svg>
|
|
77
130
|
Dry Run
|
|
78
131
|
<% end %>
|
|
79
|
-
|
|
80
132
|
<button
|
|
81
133
|
type="button"
|
|
82
134
|
onclick="confirmRerun()"
|
|
83
|
-
class="inline-flex items-center gap-1.5 px-3 py-
|
|
135
|
+
class="flex-1 inline-flex items-center justify-center gap-1.5 px-3 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors"
|
|
84
136
|
title="Re-execute this agent with the same parameters"
|
|
85
137
|
>
|
|
86
138
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
@@ -89,14 +141,6 @@
|
|
|
89
141
|
Rerun
|
|
90
142
|
</button>
|
|
91
143
|
</div>
|
|
92
|
-
|
|
93
|
-
<div class="text-right text-sm text-gray-500 dark:text-gray-400">
|
|
94
|
-
<p><%= @execution.created_at.strftime("%b %d, %Y at %H:%M") %></p>
|
|
95
|
-
|
|
96
|
-
<p class="text-xs text-gray-400 dark:text-gray-500">
|
|
97
|
-
<%= time_ago_in_words(@execution.created_at) %> ago
|
|
98
|
-
</p>
|
|
99
|
-
</div>
|
|
100
144
|
</div>
|
|
101
145
|
</div>
|
|
102
146
|
</div>
|
|
@@ -438,6 +482,76 @@
|
|
|
438
482
|
</div>
|
|
439
483
|
<% end %>
|
|
440
484
|
|
|
485
|
+
<!-- Tool Calls -->
|
|
486
|
+
<% if @execution.tool_calls.present? && @execution.tool_calls.any? %>
|
|
487
|
+
<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6">
|
|
488
|
+
<div class="flex items-center justify-between mb-4">
|
|
489
|
+
<div class="flex items-center gap-2">
|
|
490
|
+
<svg class="w-5 h-5 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
491
|
+
<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"/>
|
|
492
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
|
493
|
+
</svg>
|
|
494
|
+
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Tool Calls</h3>
|
|
495
|
+
<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
|
+
<%= @execution.tool_calls.size %>
|
|
497
|
+
</span>
|
|
498
|
+
</div>
|
|
499
|
+
<button
|
|
500
|
+
type="button"
|
|
501
|
+
data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(@execution.tool_calls)) %>"
|
|
502
|
+
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"
|
|
503
|
+
>
|
|
504
|
+
<svg class="w-4 h-4 copy-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
505
|
+
<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"/>
|
|
506
|
+
</svg>
|
|
507
|
+
<svg class="w-4 h-4 check-icon hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
508
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
|
509
|
+
</svg>
|
|
510
|
+
<span>Copy</span>
|
|
511
|
+
</button>
|
|
512
|
+
</div>
|
|
513
|
+
|
|
514
|
+
<div class="space-y-4">
|
|
515
|
+
<% @execution.tool_calls.each_with_index do |tool_call, index| %>
|
|
516
|
+
<%
|
|
517
|
+
# Handle both symbol and string keys
|
|
518
|
+
tool_id = tool_call['id'] || tool_call[:id]
|
|
519
|
+
tool_name = tool_call['name'] || tool_call[:name]
|
|
520
|
+
tool_args = tool_call['arguments'] || tool_call[:arguments] || {}
|
|
521
|
+
%>
|
|
522
|
+
<div class="border border-gray-100 dark:border-gray-700 rounded-lg overflow-hidden">
|
|
523
|
+
<!-- Tool Call Header -->
|
|
524
|
+
<div class="bg-gray-50 dark:bg-gray-900/50 px-4 py-3 flex items-center justify-between">
|
|
525
|
+
<div class="flex items-center gap-3">
|
|
526
|
+
<span class="inline-flex items-center justify-center w-6 h-6 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 text-xs font-medium">
|
|
527
|
+
<%= index + 1 %>
|
|
528
|
+
</span>
|
|
529
|
+
<code class="text-sm font-semibold text-gray-900 dark:text-gray-100"><%= tool_name %></code>
|
|
530
|
+
</div>
|
|
531
|
+
<% if tool_id.present? %>
|
|
532
|
+
<span class="text-xs text-gray-400 dark:text-gray-500 font-mono truncate max-w-xs" title="<%= tool_id %>">
|
|
533
|
+
<%= tool_id.to_s.truncate(24) %>
|
|
534
|
+
</span>
|
|
535
|
+
<% end %>
|
|
536
|
+
</div>
|
|
537
|
+
|
|
538
|
+
<!-- Tool Call Arguments -->
|
|
539
|
+
<% if tool_args.present? && tool_args.any? %>
|
|
540
|
+
<div class="px-4 py-3">
|
|
541
|
+
<p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-2">Arguments</p>
|
|
542
|
+
<pre class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-3 text-sm overflow-x-auto font-mono"><%= highlight_json(tool_args) %></pre>
|
|
543
|
+
</div>
|
|
544
|
+
<% else %>
|
|
545
|
+
<div class="px-4 py-3">
|
|
546
|
+
<p class="text-xs text-gray-400 dark:text-gray-500 italic">No arguments</p>
|
|
547
|
+
</div>
|
|
548
|
+
<% end %>
|
|
549
|
+
</div>
|
|
550
|
+
<% end %>
|
|
551
|
+
</div>
|
|
552
|
+
</div>
|
|
553
|
+
<% end %>
|
|
554
|
+
|
|
441
555
|
<!-- Metadata -->
|
|
442
556
|
<% if @execution.metadata.present? && @execution.metadata.any? %>
|
|
443
557
|
<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6">
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Migration to add dedicated tool_calls column to executions
|
|
4
|
+
#
|
|
5
|
+
# This migration adds a dedicated column for storing tool call data,
|
|
6
|
+
# enabling easier querying and display of tool/function calls made
|
|
7
|
+
# during agent execution.
|
|
8
|
+
#
|
|
9
|
+
# Tool call structure:
|
|
10
|
+
# [
|
|
11
|
+
# { "id": "call_abc123", "name": "search", "arguments": { "query": "..." } },
|
|
12
|
+
# { "id": "call_def456", "name": "calculate", "arguments": { ... } }
|
|
13
|
+
# ]
|
|
14
|
+
#
|
|
15
|
+
# Run with: rails db:migrate
|
|
16
|
+
class AddToolCallsToRubyLLMAgentsExecutions < ActiveRecord::Migration<%= migration_version %>
|
|
17
|
+
def change
|
|
18
|
+
# Add tool_calls JSONB array for storing tool call details
|
|
19
|
+
# Each tool call contains: id, name, arguments
|
|
20
|
+
add_column :ruby_llm_agents_executions, :tool_calls, :jsonb, null: false, default: []
|
|
21
|
+
|
|
22
|
+
# Add counter for quick access to tool call count
|
|
23
|
+
add_column :ruby_llm_agents_executions, :tool_calls_count, :integer, null: false, default: 0
|
|
24
|
+
|
|
25
|
+
# Add index for querying executions that have tool calls
|
|
26
|
+
add_index :ruby_llm_agents_executions, :tool_calls_count
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -67,6 +67,10 @@ class CreateRubyLLMAgentsExecutions < ActiveRecord::Migration<%= migration_versi
|
|
|
67
67
|
t.text :system_prompt
|
|
68
68
|
t.text :user_prompt
|
|
69
69
|
|
|
70
|
+
# Tool calls tracking
|
|
71
|
+
t.jsonb :tool_calls, null: false, default: []
|
|
72
|
+
t.integer :tool_calls_count, null: false, default: 0
|
|
73
|
+
|
|
70
74
|
t.timestamps
|
|
71
75
|
end
|
|
72
76
|
|
|
@@ -89,6 +93,9 @@ class CreateRubyLLMAgentsExecutions < ActiveRecord::Migration<%= migration_versi
|
|
|
89
93
|
# Caching index
|
|
90
94
|
add_index :ruby_llm_agents_executions, :response_cache_key
|
|
91
95
|
|
|
96
|
+
# Tool calls index
|
|
97
|
+
add_index :ruby_llm_agents_executions, :tool_calls_count
|
|
98
|
+
|
|
92
99
|
# Foreign keys for execution hierarchy
|
|
93
100
|
add_foreign_key :ruby_llm_agents_executions, :ruby_llm_agents_executions,
|
|
94
101
|
column: :parent_execution_id, on_delete: :nullify
|
|
@@ -107,6 +107,19 @@ module RubyLlmAgents
|
|
|
107
107
|
)
|
|
108
108
|
end
|
|
109
109
|
|
|
110
|
+
def create_add_tool_calls_migration
|
|
111
|
+
# Check if columns already exist
|
|
112
|
+
if column_exists?(:ruby_llm_agents_executions, :tool_calls)
|
|
113
|
+
say_status :skip, "tool_calls column already exists", :yellow
|
|
114
|
+
return
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
migration_template(
|
|
118
|
+
"add_tool_calls_migration.rb.tt",
|
|
119
|
+
File.join(db_migrate_path, "add_tool_calls_to_ruby_llm_agents_executions.rb")
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
|
|
110
123
|
def show_post_upgrade_message
|
|
111
124
|
say ""
|
|
112
125
|
say "RubyLLM::Agents upgrade migration created!", :green
|
data/lib/ruby_llm/agents/base.rb
CHANGED
|
@@ -325,7 +325,9 @@ module RubyLLM
|
|
|
325
325
|
# @return [RubyLLM::Chat] The configured RubyLLM client
|
|
326
326
|
# @!attribute [r] time_to_first_token_ms
|
|
327
327
|
# @return [Integer, nil] Time to first token in milliseconds (streaming only)
|
|
328
|
-
|
|
328
|
+
# @!attribute [r] accumulated_tool_calls
|
|
329
|
+
# @return [Array<Hash>] Tool calls accumulated during execution
|
|
330
|
+
attr_reader :model, :temperature, :client, :time_to_first_token_ms, :accumulated_tool_calls
|
|
329
331
|
|
|
330
332
|
# Creates a new agent instance
|
|
331
333
|
#
|
|
@@ -337,6 +339,7 @@ module RubyLLM
|
|
|
337
339
|
@model = model
|
|
338
340
|
@temperature = temperature
|
|
339
341
|
@options = options
|
|
342
|
+
@accumulated_tool_calls = []
|
|
340
343
|
validate_required_params!
|
|
341
344
|
@client = build_client
|
|
342
345
|
end
|
|
@@ -424,21 +427,26 @@ module RubyLLM
|
|
|
424
427
|
|
|
425
428
|
# Returns prompt info without making an API call (debug mode)
|
|
426
429
|
#
|
|
427
|
-
# @return [
|
|
430
|
+
# @return [Result] A Result with dry run configuration info
|
|
428
431
|
def dry_run_response
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
432
|
+
Result.new(
|
|
433
|
+
content: {
|
|
434
|
+
dry_run: true,
|
|
435
|
+
agent: self.class.name,
|
|
436
|
+
model: model,
|
|
437
|
+
temperature: temperature,
|
|
438
|
+
timeout: self.class.timeout,
|
|
439
|
+
system_prompt: system_prompt,
|
|
440
|
+
user_prompt: user_prompt,
|
|
441
|
+
attachments: @options[:with],
|
|
442
|
+
schema: schema&.class&.name,
|
|
443
|
+
streaming: self.class.streaming,
|
|
444
|
+
tools: self.class.tools.map { |t| t.respond_to?(:name) ? t.name : t.to_s }
|
|
445
|
+
},
|
|
446
|
+
model_id: model,
|
|
433
447
|
temperature: temperature,
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
user_prompt: user_prompt,
|
|
437
|
-
attachments: @options[:with],
|
|
438
|
-
schema: schema&.class&.name,
|
|
439
|
-
streaming: self.class.streaming,
|
|
440
|
-
tools: self.class.tools.map { |t| t.respond_to?(:name) ? t.name : t.to_s }
|
|
441
|
-
}
|
|
448
|
+
streaming: self.class.streaming
|
|
449
|
+
)
|
|
442
450
|
end
|
|
443
451
|
|
|
444
452
|
private
|
|
@@ -462,17 +470,20 @@ module RubyLLM
|
|
|
462
470
|
#
|
|
463
471
|
# @param model_override [String, nil] Optional model to use instead of default
|
|
464
472
|
# @yield [chunk] Yields chunks when streaming is enabled
|
|
465
|
-
# @return [
|
|
473
|
+
# @return [Result] A Result object with processed content and metadata
|
|
466
474
|
def execute_single_attempt(model_override: nil, &block)
|
|
467
475
|
current_client = model_override ? build_client_with_model(model_override) : client
|
|
468
476
|
@execution_started_at ||= Time.current
|
|
477
|
+
reset_accumulated_tool_calls!
|
|
469
478
|
|
|
470
479
|
Timeout.timeout(self.class.timeout) do
|
|
471
480
|
if self.class.streaming && block_given?
|
|
472
481
|
execute_with_streaming(current_client, &block)
|
|
473
482
|
else
|
|
474
483
|
response = current_client.ask(user_prompt, **ask_options)
|
|
475
|
-
|
|
484
|
+
extract_tool_calls_from_client(current_client)
|
|
485
|
+
capture_response(response)
|
|
486
|
+
build_result(process_response(response), response)
|
|
476
487
|
end
|
|
477
488
|
end
|
|
478
489
|
end
|
|
@@ -485,7 +496,7 @@ module RubyLLM
|
|
|
485
496
|
# @param current_client [RubyLLM::Chat] The configured client
|
|
486
497
|
# @yield [chunk] Yields each chunk as it arrives
|
|
487
498
|
# @yieldparam chunk [RubyLLM::Chunk] A streaming chunk
|
|
488
|
-
# @return [
|
|
499
|
+
# @return [Result] A Result object with processed content and metadata
|
|
489
500
|
def execute_with_streaming(current_client, &block)
|
|
490
501
|
first_chunk_at = nil
|
|
491
502
|
|
|
@@ -498,7 +509,9 @@ module RubyLLM
|
|
|
498
509
|
@time_to_first_token_ms = ((first_chunk_at - @execution_started_at) * 1000).to_i
|
|
499
510
|
end
|
|
500
511
|
|
|
501
|
-
|
|
512
|
+
extract_tool_calls_from_client(current_client)
|
|
513
|
+
capture_response(response)
|
|
514
|
+
build_result(process_response(response), response)
|
|
502
515
|
end
|
|
503
516
|
|
|
504
517
|
# Executes the agent with retry/fallback/circuit breaker support
|
|
@@ -751,6 +764,183 @@ module RubyLLM
|
|
|
751
764
|
client.with_message(message[:role], message[:content])
|
|
752
765
|
end
|
|
753
766
|
end
|
|
767
|
+
|
|
768
|
+
# @!group Result Building
|
|
769
|
+
|
|
770
|
+
# Builds a Result object from processed content and response metadata
|
|
771
|
+
#
|
|
772
|
+
# @param content [Hash, String] The processed response content
|
|
773
|
+
# @param response [RubyLLM::Message] The raw LLM response
|
|
774
|
+
# @return [Result] A Result object with full execution metadata
|
|
775
|
+
def build_result(content, response)
|
|
776
|
+
completed_at = Time.current
|
|
777
|
+
input_tokens = result_response_value(response, :input_tokens)
|
|
778
|
+
output_tokens = result_response_value(response, :output_tokens)
|
|
779
|
+
response_model_id = result_response_value(response, :model_id)
|
|
780
|
+
|
|
781
|
+
Result.new(
|
|
782
|
+
content: content,
|
|
783
|
+
input_tokens: input_tokens,
|
|
784
|
+
output_tokens: output_tokens,
|
|
785
|
+
cached_tokens: result_response_value(response, :cached_tokens, 0),
|
|
786
|
+
cache_creation_tokens: result_response_value(response, :cache_creation_tokens, 0),
|
|
787
|
+
model_id: model,
|
|
788
|
+
chosen_model_id: response_model_id || model,
|
|
789
|
+
temperature: temperature,
|
|
790
|
+
started_at: @execution_started_at,
|
|
791
|
+
completed_at: completed_at,
|
|
792
|
+
duration_ms: result_duration_ms(completed_at),
|
|
793
|
+
time_to_first_token_ms: @time_to_first_token_ms,
|
|
794
|
+
finish_reason: result_finish_reason(response),
|
|
795
|
+
streaming: self.class.streaming,
|
|
796
|
+
input_cost: result_input_cost(input_tokens, response_model_id),
|
|
797
|
+
output_cost: result_output_cost(output_tokens, response_model_id),
|
|
798
|
+
total_cost: result_total_cost(input_tokens, output_tokens, response_model_id),
|
|
799
|
+
tool_calls: @accumulated_tool_calls,
|
|
800
|
+
tool_calls_count: @accumulated_tool_calls.size
|
|
801
|
+
)
|
|
802
|
+
end
|
|
803
|
+
|
|
804
|
+
# Safely extracts a value from the response object
|
|
805
|
+
#
|
|
806
|
+
# @param response [Object] The response object
|
|
807
|
+
# @param method [Symbol] The method to call
|
|
808
|
+
# @param default [Object] Default value if method doesn't exist
|
|
809
|
+
# @return [Object] The extracted value or default
|
|
810
|
+
def result_response_value(response, method, default = nil)
|
|
811
|
+
return default unless response.respond_to?(method)
|
|
812
|
+
response.send(method) || default
|
|
813
|
+
end
|
|
814
|
+
|
|
815
|
+
# Calculates execution duration in milliseconds
|
|
816
|
+
#
|
|
817
|
+
# @param completed_at [Time] When execution completed
|
|
818
|
+
# @return [Integer, nil] Duration in ms or nil
|
|
819
|
+
def result_duration_ms(completed_at)
|
|
820
|
+
return nil unless @execution_started_at
|
|
821
|
+
((completed_at - @execution_started_at) * 1000).to_i
|
|
822
|
+
end
|
|
823
|
+
|
|
824
|
+
# Extracts finish reason from response
|
|
825
|
+
#
|
|
826
|
+
# @param response [Object] The response object
|
|
827
|
+
# @return [String, nil] Normalized finish reason
|
|
828
|
+
def result_finish_reason(response)
|
|
829
|
+
reason = result_response_value(response, :finish_reason) ||
|
|
830
|
+
result_response_value(response, :stop_reason)
|
|
831
|
+
return nil unless reason
|
|
832
|
+
|
|
833
|
+
# Normalize to standard values
|
|
834
|
+
case reason.to_s.downcase
|
|
835
|
+
when "stop", "end_turn" then "stop"
|
|
836
|
+
when "length", "max_tokens" then "length"
|
|
837
|
+
when "content_filter", "safety" then "content_filter"
|
|
838
|
+
when "tool_calls", "tool_use" then "tool_calls"
|
|
839
|
+
else "other"
|
|
840
|
+
end
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
# Calculates input cost from tokens
|
|
844
|
+
#
|
|
845
|
+
# @param input_tokens [Integer, nil] Number of input tokens
|
|
846
|
+
# @param response_model_id [String, nil] Model that responded
|
|
847
|
+
# @return [Float, nil] Input cost in USD
|
|
848
|
+
def result_input_cost(input_tokens, response_model_id)
|
|
849
|
+
return nil unless input_tokens
|
|
850
|
+
model_info = result_model_info(response_model_id)
|
|
851
|
+
return nil unless model_info&.pricing
|
|
852
|
+
price = model_info.pricing.text_tokens&.input || 0
|
|
853
|
+
(input_tokens / 1_000_000.0 * price).round(6)
|
|
854
|
+
end
|
|
855
|
+
|
|
856
|
+
# Calculates output cost from tokens
|
|
857
|
+
#
|
|
858
|
+
# @param output_tokens [Integer, nil] Number of output tokens
|
|
859
|
+
# @param response_model_id [String, nil] Model that responded
|
|
860
|
+
# @return [Float, nil] Output cost in USD
|
|
861
|
+
def result_output_cost(output_tokens, response_model_id)
|
|
862
|
+
return nil unless output_tokens
|
|
863
|
+
model_info = result_model_info(response_model_id)
|
|
864
|
+
return nil unless model_info&.pricing
|
|
865
|
+
price = model_info.pricing.text_tokens&.output || 0
|
|
866
|
+
(output_tokens / 1_000_000.0 * price).round(6)
|
|
867
|
+
end
|
|
868
|
+
|
|
869
|
+
# Calculates total cost from tokens
|
|
870
|
+
#
|
|
871
|
+
# @param input_tokens [Integer, nil] Number of input tokens
|
|
872
|
+
# @param output_tokens [Integer, nil] Number of output tokens
|
|
873
|
+
# @param response_model_id [String, nil] Model that responded
|
|
874
|
+
# @return [Float, nil] Total cost in USD
|
|
875
|
+
def result_total_cost(input_tokens, output_tokens, response_model_id)
|
|
876
|
+
input_cost = result_input_cost(input_tokens, response_model_id)
|
|
877
|
+
output_cost = result_output_cost(output_tokens, response_model_id)
|
|
878
|
+
return nil unless input_cost || output_cost
|
|
879
|
+
((input_cost || 0) + (output_cost || 0)).round(6)
|
|
880
|
+
end
|
|
881
|
+
|
|
882
|
+
# Resolves model info for cost calculation
|
|
883
|
+
#
|
|
884
|
+
# @param response_model_id [String, nil] Model ID from response
|
|
885
|
+
# @return [Object, nil] Model info or nil
|
|
886
|
+
def result_model_info(response_model_id)
|
|
887
|
+
lookup_id = response_model_id || model
|
|
888
|
+
return nil unless lookup_id
|
|
889
|
+
model_obj, _provider = RubyLLM::Models.resolve(lookup_id)
|
|
890
|
+
model_obj
|
|
891
|
+
rescue StandardError
|
|
892
|
+
nil
|
|
893
|
+
end
|
|
894
|
+
|
|
895
|
+
# @!endgroup
|
|
896
|
+
|
|
897
|
+
# @!group Tool Call Tracking
|
|
898
|
+
|
|
899
|
+
# Resets accumulated tool calls for a new execution
|
|
900
|
+
#
|
|
901
|
+
# @return [void]
|
|
902
|
+
def reset_accumulated_tool_calls!
|
|
903
|
+
@accumulated_tool_calls = []
|
|
904
|
+
end
|
|
905
|
+
|
|
906
|
+
# Extracts tool calls from all assistant messages in the conversation
|
|
907
|
+
#
|
|
908
|
+
# RubyLLM handles tool call loops internally. After ask() completes,
|
|
909
|
+
# the conversation history contains all intermediate assistant messages
|
|
910
|
+
# that had tool_calls. This method extracts those tool calls.
|
|
911
|
+
#
|
|
912
|
+
# @param client [RubyLLM::Chat] The chat client with conversation history
|
|
913
|
+
# @return [void]
|
|
914
|
+
def extract_tool_calls_from_client(client)
|
|
915
|
+
return unless client.respond_to?(:messages)
|
|
916
|
+
|
|
917
|
+
client.messages.each do |message|
|
|
918
|
+
next unless message.role == :assistant
|
|
919
|
+
next unless message.respond_to?(:tool_calls) && message.tool_calls.present?
|
|
920
|
+
|
|
921
|
+
message.tool_calls.each_value do |tool_call|
|
|
922
|
+
@accumulated_tool_calls << serialize_tool_call(tool_call)
|
|
923
|
+
end
|
|
924
|
+
end
|
|
925
|
+
end
|
|
926
|
+
|
|
927
|
+
# Serializes a single tool call to a hash
|
|
928
|
+
#
|
|
929
|
+
# @param tool_call [Object] The tool call object
|
|
930
|
+
# @return [Hash] Serialized tool call
|
|
931
|
+
def serialize_tool_call(tool_call)
|
|
932
|
+
if tool_call.respond_to?(:to_h)
|
|
933
|
+
tool_call.to_h.transform_keys(&:to_s)
|
|
934
|
+
else
|
|
935
|
+
{
|
|
936
|
+
"id" => tool_call.id,
|
|
937
|
+
"name" => tool_call.name,
|
|
938
|
+
"arguments" => tool_call.arguments
|
|
939
|
+
}
|
|
940
|
+
end
|
|
941
|
+
end
|
|
942
|
+
|
|
943
|
+
# @!endgroup
|
|
754
944
|
end
|
|
755
945
|
end
|
|
756
946
|
end
|