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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5396493167b6da5bcc4fd878d502efdf7269246374ea30ef8d5ce25423f3868a
4
- data.tar.gz: 2e80ab75249560be31be4e1a9aa8c78efd6b0d6e2c692f391962a20b7ea98a5b
3
+ metadata.gz: fa47482df367b7b0cb59f616a7aecebeed440a97aa95df73f185c988cbcc8220
4
+ data.tar.gz: 005d587ed60cfc76e6e14baaca2dbf6dcf166c9e8e1a9e2291732cf2054ed8d2
5
5
  SHA512:
6
- metadata.gz: 2943372fc8709cef913eba67f491431b96e96b5ba4cc43338eabbfaee291fa53434e55fc1db7b7d0ab19b7d05e54c0b228c0799d73a5570f9dd9a18881fb9b7a
7
- data.tar.gz: b442321d5323899962a6fab0ec998fd5616339bd46428c4198291c611eb2741fc9c3f78080f90fdb055654b16ef491d903caddff5d95394c87c72f84e900fe06
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" <%= tool_call_count > 3 ? 'x-cloak' : '' %>>
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 flex items-center justify-between">
627
- <div class="flex items-center gap-3">
628
- <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">
629
- <%= index + 1 %>
630
- </span>
631
- <code class="text-sm font-semibold text-gray-900 dark:text-gray-100"><%= tool_name %></code>
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
- attr_reader :model, :temperature, :client
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
@@ -4,6 +4,6 @@ module RubyLLM
4
4
  module Agents
5
5
  # Current version of the RubyLLM::Agents gem
6
6
  # @return [String] Semantic version string
7
- VERSION = "1.2.3"
7
+ VERSION = "1.3.0"
8
8
  end
9
9
  end
@@ -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
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_llm-agents
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.3
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - adham90