ruby_llm-agents 1.2.3 → 1.3.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/views/ruby_llm/agents/executions/show.html.erb +60 -15
- data/lib/ruby_llm/agents/base_agent.rb +125 -1
- data/lib/ruby_llm/agents/core/configuration.rb +12 -1
- data/lib/ruby_llm/agents/core/version.rb +1 -1
- data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +12 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fa47482df367b7b0cb59f616a7aecebeed440a97aa95df73f185c988cbcc8220
|
|
4
|
+
data.tar.gz: 005d587ed60cfc76e6e14baaca2dbf6dcf166c9e8e1a9e2291732cf2054ed8d2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5e06dfba78ef9f8d87f100addc95b3fce4f7ca41af017e00c3bc4bae27c1b82a0ba55365279768d1b85eb06f34447fbdf3e3872d54a9b00f538f7cf44655097e
|
|
7
|
+
data.tar.gz: 7f6da9cdc2cd776e08ca827a8575591789e814f01bd9e66e0ffd75ff1c2f1f8a1aded511bb2728b729322a805ef425655904f1679c501ea71b34dca937ba7f8d
|
|
@@ -613,41 +613,86 @@
|
|
|
613
613
|
</div>
|
|
614
614
|
|
|
615
615
|
<% if tool_call_count > 0 %>
|
|
616
|
-
<div class="space-y-4" x-show="expanded"
|
|
616
|
+
<div class="space-y-4" x-show="expanded" x-cloak>
|
|
617
617
|
<% tool_calls.each_with_index do |tool_call, index| %>
|
|
618
618
|
<%
|
|
619
|
-
# Handle both symbol and string keys
|
|
619
|
+
# Handle both symbol and string keys for backward compatibility
|
|
620
620
|
tool_id = tool_call['id'] || tool_call[:id]
|
|
621
621
|
tool_name = tool_call['name'] || tool_call[:name]
|
|
622
622
|
tool_args = tool_call['arguments'] || tool_call[:arguments] || {}
|
|
623
|
+
tool_result = tool_call['result'] || tool_call[:result]
|
|
624
|
+
tool_status = tool_call['status'] || tool_call[:status] || 'unknown'
|
|
625
|
+
tool_error = tool_call['error_message'] || tool_call[:error_message]
|
|
626
|
+
tool_duration = tool_call['duration_ms'] || tool_call[:duration_ms]
|
|
627
|
+
tool_called_at = tool_call['called_at'] || tool_call[:called_at]
|
|
628
|
+
|
|
629
|
+
# Status badge styling
|
|
630
|
+
status_badge_class = case tool_status
|
|
631
|
+
when 'success' then 'bg-green-100 dark:bg-green-900/50 text-green-700 dark:text-green-300'
|
|
632
|
+
when 'error' then 'bg-red-100 dark:bg-red-900/50 text-red-700 dark:text-red-300'
|
|
633
|
+
else 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300'
|
|
634
|
+
end
|
|
623
635
|
%>
|
|
624
636
|
<div class="border border-gray-100 dark:border-gray-700 rounded-lg overflow-hidden">
|
|
625
637
|
<!-- Tool Call Header -->
|
|
626
|
-
<div class="bg-gray-50 dark:bg-gray-900/50 px-4 py-3
|
|
627
|
-
<div class="flex items-center
|
|
628
|
-
<
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
638
|
+
<div class="bg-gray-50 dark:bg-gray-900/50 px-4 py-3">
|
|
639
|
+
<div class="flex items-center justify-between">
|
|
640
|
+
<div class="flex items-center gap-3">
|
|
641
|
+
<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">
|
|
642
|
+
<%= index + 1 %>
|
|
643
|
+
</span>
|
|
644
|
+
<code class="text-sm font-semibold text-gray-900 dark:text-gray-100"><%= tool_name %></code>
|
|
645
|
+
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium <%= status_badge_class %>">
|
|
646
|
+
<%= tool_status %>
|
|
647
|
+
</span>
|
|
648
|
+
<% if tool_duration.present? %>
|
|
649
|
+
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-100 dark:bg-purple-900/50 text-purple-700 dark:text-purple-300">
|
|
650
|
+
<%= tool_duration %>ms
|
|
651
|
+
</span>
|
|
652
|
+
<% end %>
|
|
653
|
+
</div>
|
|
654
|
+
<div class="flex items-center gap-3 text-xs text-gray-400 dark:text-gray-500">
|
|
655
|
+
<% if tool_called_at.present? %>
|
|
656
|
+
<span class="font-mono" title="Called at"><%= Time.parse(tool_called_at).strftime("%H:%M:%S.%L") rescue tool_called_at %></span>
|
|
657
|
+
<% end %>
|
|
658
|
+
<% if tool_id.present? %>
|
|
659
|
+
<span class="font-mono truncate max-w-[120px]" title="<%= tool_id %>">
|
|
660
|
+
<%= tool_id.to_s.truncate(16) %>
|
|
661
|
+
</span>
|
|
662
|
+
<% end %>
|
|
663
|
+
</div>
|
|
632
664
|
</div>
|
|
633
|
-
<% if tool_id.present? %>
|
|
634
|
-
<span class="text-xs text-gray-400 dark:text-gray-500 font-mono truncate max-w-xs" title="<%= tool_id %>">
|
|
635
|
-
<%= tool_id.to_s.truncate(24) %>
|
|
636
|
-
</span>
|
|
637
|
-
<% end %>
|
|
638
665
|
</div>
|
|
639
666
|
|
|
640
667
|
<!-- Tool Call Arguments -->
|
|
641
668
|
<% if tool_args.present? && tool_args.any? %>
|
|
642
|
-
<div class="px-4 py-3">
|
|
669
|
+
<div class="px-4 py-3 border-t border-gray-100 dark:border-gray-700">
|
|
643
670
|
<p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-2">Arguments</p>
|
|
644
671
|
<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>
|
|
645
672
|
</div>
|
|
646
673
|
<% else %>
|
|
647
|
-
<div class="px-4 py-3">
|
|
674
|
+
<div class="px-4 py-3 border-t border-gray-100 dark:border-gray-700">
|
|
648
675
|
<p class="text-xs text-gray-400 dark:text-gray-500 italic">No arguments</p>
|
|
649
676
|
</div>
|
|
650
677
|
<% end %>
|
|
678
|
+
|
|
679
|
+
<!-- Tool Call Result (NEW) -->
|
|
680
|
+
<% if tool_result.present? %>
|
|
681
|
+
<div class="px-4 py-3 border-t border-gray-100 dark:border-gray-700">
|
|
682
|
+
<p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-2">Result</p>
|
|
683
|
+
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-3 max-h-48 overflow-y-auto">
|
|
684
|
+
<pre class="text-sm text-gray-900 dark:text-gray-100 font-mono whitespace-pre-wrap break-words"><%= tool_result.is_a?(String) ? tool_result : JSON.pretty_generate(tool_result) rescue tool_result.to_s %></pre>
|
|
685
|
+
</div>
|
|
686
|
+
</div>
|
|
687
|
+
<% end %>
|
|
688
|
+
|
|
689
|
+
<!-- Tool Call Error (NEW) -->
|
|
690
|
+
<% if tool_status == 'error' && tool_error.present? %>
|
|
691
|
+
<div class="px-4 py-3 border-t border-red-100 dark:border-red-900/50 bg-red-50 dark:bg-red-900/20">
|
|
692
|
+
<p class="text-xs text-red-600 dark:text-red-400 uppercase tracking-wide mb-2">Error</p>
|
|
693
|
+
<pre class="text-sm text-red-700 dark:text-red-300 font-mono whitespace-pre-wrap break-words"><%= tool_error %></pre>
|
|
694
|
+
</div>
|
|
695
|
+
<% end %>
|
|
651
696
|
</div>
|
|
652
697
|
<% end %>
|
|
653
698
|
</div>
|
|
@@ -212,7 +212,9 @@ module RubyLLM
|
|
|
212
212
|
# @return [Float] The temperature setting
|
|
213
213
|
# @!attribute [r] client
|
|
214
214
|
# @return [RubyLLM::Chat] The configured RubyLLM client
|
|
215
|
-
|
|
215
|
+
# @!attribute [r] tracked_tool_calls
|
|
216
|
+
# @return [Array<Hash>] Tool calls tracked during execution with results, timing, and status
|
|
217
|
+
attr_reader :model, :temperature, :client, :tracked_tool_calls
|
|
216
218
|
|
|
217
219
|
# Creates a new agent instance
|
|
218
220
|
#
|
|
@@ -223,6 +225,8 @@ module RubyLLM
|
|
|
223
225
|
@model = model
|
|
224
226
|
@temperature = temperature
|
|
225
227
|
@options = options
|
|
228
|
+
@tracked_tool_calls = []
|
|
229
|
+
@pending_tool_call = nil
|
|
226
230
|
validate_required_params!
|
|
227
231
|
end
|
|
228
232
|
|
|
@@ -485,6 +489,7 @@ module RubyLLM
|
|
|
485
489
|
client = client.with_instructions(system_prompt) if system_prompt
|
|
486
490
|
client = client.with_schema(schema) if schema
|
|
487
491
|
client = client.with_tools(*resolved_tools) if resolved_tools.any?
|
|
492
|
+
client = setup_tool_tracking(client) if resolved_tools.any?
|
|
488
493
|
client = apply_messages(client, resolved_messages) if resolved_messages.any?
|
|
489
494
|
client = client.with_thinking(**resolved_thinking) if resolved_thinking
|
|
490
495
|
|
|
@@ -543,6 +548,9 @@ module RubyLLM
|
|
|
543
548
|
# finish_reason may not be available on all RubyLLM::Message versions
|
|
544
549
|
context.finish_reason = response.respond_to?(:finish_reason) ? response.finish_reason : nil
|
|
545
550
|
|
|
551
|
+
# Store tracked tool calls in context for instrumentation
|
|
552
|
+
context[:tool_calls] = @tracked_tool_calls if @tracked_tool_calls.any?
|
|
553
|
+
|
|
546
554
|
calculate_costs(response, context) if context.input_tokens
|
|
547
555
|
end
|
|
548
556
|
|
|
@@ -670,6 +678,122 @@ module RubyLLM
|
|
|
670
678
|
end
|
|
671
679
|
client
|
|
672
680
|
end
|
|
681
|
+
|
|
682
|
+
# Sets up tool call tracking callbacks on the client
|
|
683
|
+
#
|
|
684
|
+
# @param client [RubyLLM::Chat] The chat client
|
|
685
|
+
# @return [RubyLLM::Chat] Client with tracking callbacks
|
|
686
|
+
def setup_tool_tracking(client)
|
|
687
|
+
client
|
|
688
|
+
.on_tool_call { |tool_call| start_tracking_tool_call(tool_call) }
|
|
689
|
+
.on_tool_result { |result| complete_tool_call_tracking(result) }
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
# Starts tracking a tool call
|
|
693
|
+
#
|
|
694
|
+
# @param tool_call [Object] The tool call object from RubyLLM
|
|
695
|
+
def start_tracking_tool_call(tool_call)
|
|
696
|
+
@pending_tool_call = {
|
|
697
|
+
id: extract_tool_call_value(tool_call, :id),
|
|
698
|
+
name: extract_tool_call_value(tool_call, :name),
|
|
699
|
+
arguments: extract_tool_call_value(tool_call, :arguments) || {},
|
|
700
|
+
called_at: Time.current.iso8601(3),
|
|
701
|
+
started_at: Time.current
|
|
702
|
+
}
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
# Completes tracking for the pending tool call with result
|
|
706
|
+
#
|
|
707
|
+
# @param result [Object] The tool result (string, hash, or object)
|
|
708
|
+
def complete_tool_call_tracking(result)
|
|
709
|
+
return unless @pending_tool_call
|
|
710
|
+
|
|
711
|
+
completed_at = Time.current
|
|
712
|
+
started_at = @pending_tool_call.delete(:started_at)
|
|
713
|
+
duration_ms = started_at ? ((completed_at - started_at) * 1000).to_i : nil
|
|
714
|
+
|
|
715
|
+
result_data = extract_tool_result(result)
|
|
716
|
+
|
|
717
|
+
tracked_call = @pending_tool_call.merge(
|
|
718
|
+
result: truncate_tool_result(result_data[:content]),
|
|
719
|
+
status: result_data[:status],
|
|
720
|
+
error_message: result_data[:error_message],
|
|
721
|
+
duration_ms: duration_ms,
|
|
722
|
+
completed_at: completed_at.iso8601(3)
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
@tracked_tool_calls << tracked_call
|
|
726
|
+
@pending_tool_call = nil
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
# Extracts result data from various tool result formats
|
|
730
|
+
#
|
|
731
|
+
# @param result [Object] The tool result
|
|
732
|
+
# @return [Hash] Hash with :content, :status, :error_message keys
|
|
733
|
+
def extract_tool_result(result)
|
|
734
|
+
content = nil
|
|
735
|
+
status = "success"
|
|
736
|
+
error_message = nil
|
|
737
|
+
|
|
738
|
+
if result.is_a?(Exception)
|
|
739
|
+
content = result.message
|
|
740
|
+
status = "error"
|
|
741
|
+
error_message = "#{result.class}: #{result.message}"
|
|
742
|
+
elsif result.respond_to?(:error?) && result.error?
|
|
743
|
+
content = result.respond_to?(:content) ? result.content : result.to_s
|
|
744
|
+
status = "error"
|
|
745
|
+
error_message = result.respond_to?(:error_message) ? result.error_message : content
|
|
746
|
+
elsif result.respond_to?(:content)
|
|
747
|
+
content = result.content
|
|
748
|
+
elsif result.is_a?(Hash)
|
|
749
|
+
content = result[:content] || result["content"] || result.to_json
|
|
750
|
+
if result[:error] || result["error"]
|
|
751
|
+
status = "error"
|
|
752
|
+
error_message = result[:error] || result["error"]
|
|
753
|
+
end
|
|
754
|
+
else
|
|
755
|
+
content = result.to_s
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
{ content: content, status: status, error_message: error_message }
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
# Truncates tool result if it exceeds the configured max length
|
|
762
|
+
#
|
|
763
|
+
# @param result [String, Object] The result to truncate
|
|
764
|
+
# @return [String] The truncated result
|
|
765
|
+
def truncate_tool_result(result)
|
|
766
|
+
return nil if result.nil?
|
|
767
|
+
|
|
768
|
+
result_str = result.is_a?(String) ? result : result.to_json
|
|
769
|
+
max_length = tool_result_max_length
|
|
770
|
+
|
|
771
|
+
return result_str if result_str.length <= max_length
|
|
772
|
+
|
|
773
|
+
result_str[0, max_length - 15] + "... [truncated]"
|
|
774
|
+
end
|
|
775
|
+
|
|
776
|
+
# Returns the configured max length for tool results
|
|
777
|
+
#
|
|
778
|
+
# @return [Integer] Max length
|
|
779
|
+
def tool_result_max_length
|
|
780
|
+
RubyLLM::Agents.configuration.tool_result_max_length || 10_000
|
|
781
|
+
rescue StandardError
|
|
782
|
+
10_000
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
# Extracts a value from a tool call object (supports both hash and object access)
|
|
786
|
+
#
|
|
787
|
+
# @param tool_call [Hash, Object] The tool call
|
|
788
|
+
# @param key [Symbol] The key to extract
|
|
789
|
+
# @return [Object, nil] The value or nil
|
|
790
|
+
def extract_tool_call_value(tool_call, key)
|
|
791
|
+
if tool_call.respond_to?(key)
|
|
792
|
+
tool_call.send(key)
|
|
793
|
+
elsif tool_call.respond_to?(:[])
|
|
794
|
+
tool_call[key] || tool_call[key.to_s]
|
|
795
|
+
end
|
|
796
|
+
end
|
|
673
797
|
end
|
|
674
798
|
end
|
|
675
799
|
end
|
|
@@ -369,6 +369,13 @@ module RubyLLM
|
|
|
369
369
|
# @example No namespace (default)
|
|
370
370
|
# config.root_namespace = nil # app/agents/embedders -> Embedders
|
|
371
371
|
|
|
372
|
+
# @!attribute [rw] tool_result_max_length
|
|
373
|
+
# Maximum character length for tool call results stored in execution records.
|
|
374
|
+
# Results exceeding this length will be truncated with "... [truncated]".
|
|
375
|
+
# @return [Integer] Max length for tool results (default: 10000)
|
|
376
|
+
# @example
|
|
377
|
+
# config.tool_result_max_length = 5000
|
|
378
|
+
|
|
372
379
|
# Attributes without validation (simple accessors)
|
|
373
380
|
attr_accessor :default_model,
|
|
374
381
|
:async_logging,
|
|
@@ -429,7 +436,8 @@ module RubyLLM
|
|
|
429
436
|
:default_background_remover_model,
|
|
430
437
|
:default_background_output_format,
|
|
431
438
|
:root_directory,
|
|
432
|
-
:root_namespace
|
|
439
|
+
:root_namespace,
|
|
440
|
+
:tool_result_max_length
|
|
433
441
|
|
|
434
442
|
# Attributes with validation (readers only, custom setters below)
|
|
435
443
|
attr_reader :default_temperature,
|
|
@@ -704,6 +712,9 @@ module RubyLLM
|
|
|
704
712
|
# Directory structure defaults
|
|
705
713
|
@root_directory = "agents" # Root directory under app/
|
|
706
714
|
@root_namespace = nil # No namespace (top-level classes)
|
|
715
|
+
|
|
716
|
+
# Tool tracking defaults
|
|
717
|
+
@tool_result_max_length = 10_000
|
|
707
718
|
end
|
|
708
719
|
|
|
709
720
|
# Returns the configured cache store, falling back to Rails.cache
|
|
@@ -226,6 +226,12 @@ module RubyLLM
|
|
|
226
226
|
# Add custom metadata
|
|
227
227
|
data[:metadata] = context.metadata if context.metadata.any?
|
|
228
228
|
|
|
229
|
+
# Add enhanced tool calls if present
|
|
230
|
+
if context[:tool_calls].present?
|
|
231
|
+
data[:tool_calls] = context[:tool_calls]
|
|
232
|
+
data[:tool_calls_count] = context[:tool_calls].size
|
|
233
|
+
end
|
|
234
|
+
|
|
229
235
|
data
|
|
230
236
|
end
|
|
231
237
|
|
|
@@ -292,6 +298,12 @@ module RubyLLM
|
|
|
292
298
|
# Add sanitized parameters
|
|
293
299
|
data[:parameters] = sanitize_parameters(context)
|
|
294
300
|
|
|
301
|
+
# Add enhanced tool calls if present
|
|
302
|
+
if context[:tool_calls].present?
|
|
303
|
+
data[:tool_calls] = context[:tool_calls]
|
|
304
|
+
data[:tool_calls_count] = context[:tool_calls].size
|
|
305
|
+
end
|
|
306
|
+
|
|
295
307
|
data
|
|
296
308
|
end
|
|
297
309
|
|