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.
@@ -385,6 +385,12 @@ module RubyLLM
385
385
  update_data[:response] = redacted_response(@last_response)
386
386
  end
387
387
 
388
+ # Add tool calls from accumulated_tool_calls (captured from all responses)
389
+ if respond_to?(:accumulated_tool_calls) && accumulated_tool_calls.present?
390
+ update_data[:tool_calls] = accumulated_tool_calls
391
+ update_data[:tool_calls_count] = accumulated_tool_calls.size
392
+ end
393
+
388
394
  # Add error data if failed
389
395
  if error
390
396
  update_data.merge!(
@@ -566,7 +572,11 @@ module RubyLLM
566
572
  # @param response [RubyLLM::Message, nil] The LLM response
567
573
  # @return [Hash] Extracted response data (empty if response invalid)
568
574
  def safe_extract_response_data(response)
569
- return {} unless response.is_a?(RubyLLM::Message)
575
+ return {} unless response.respond_to?(:input_tokens)
576
+
577
+ # Use accumulated_tool_calls which captures tool calls from ALL responses
578
+ # during multi-turn conversations (when tools are used)
579
+ tool_calls_data = respond_to?(:accumulated_tool_calls) ? accumulated_tool_calls : []
570
580
 
571
581
  {
572
582
  input_tokens: safe_response_value(response, :input_tokens),
@@ -575,7 +585,9 @@ module RubyLLM
575
585
  cache_creation_tokens: safe_response_value(response, :cache_creation_tokens, 0),
576
586
  model_id: safe_response_value(response, :model_id),
577
587
  finish_reason: safe_extract_finish_reason(response),
578
- response: safe_serialize_response(response)
588
+ response: safe_serialize_response(response),
589
+ tool_calls: tool_calls_data || [],
590
+ tool_calls_count: tool_calls_data&.size || 0
579
591
  }.compact
580
592
  end
581
593
 
@@ -689,16 +701,37 @@ module RubyLLM
689
701
  # @param response [RubyLLM::Message] The LLM response
690
702
  # @return [Hash] Serialized response data
691
703
  def safe_serialize_response(response)
704
+ # Use accumulated_tool_calls which captures tool calls from ALL responses
705
+ tool_calls_data = respond_to?(:accumulated_tool_calls) ? accumulated_tool_calls : nil
706
+
692
707
  {
693
708
  content: safe_response_value(response, :content),
694
709
  model_id: safe_response_value(response, :model_id),
695
710
  input_tokens: safe_response_value(response, :input_tokens),
696
711
  output_tokens: safe_response_value(response, :output_tokens),
697
712
  cached_tokens: safe_response_value(response, :cached_tokens, 0),
698
- cache_creation_tokens: safe_response_value(response, :cache_creation_tokens, 0)
713
+ cache_creation_tokens: safe_response_value(response, :cache_creation_tokens, 0),
714
+ tool_calls: tool_calls_data.presence
699
715
  }.compact
700
716
  end
701
717
 
718
+ # Serializes tool calls to an array of hashes for storage
719
+ #
720
+ # @param response [RubyLLM::Message] The LLM response
721
+ # @return [Array<Hash>, nil] Serialized tool calls or nil if none
722
+ def serialize_tool_calls(response)
723
+ tool_calls = safe_response_value(response, :tool_calls)
724
+ return nil if tool_calls.nil? || tool_calls.empty?
725
+
726
+ tool_calls.map do |id, tool_call|
727
+ if tool_call.respond_to?(:to_h)
728
+ tool_call.to_h
729
+ else
730
+ { id: id, name: tool_call[:name], arguments: tool_call[:arguments] }
731
+ end
732
+ end
733
+ end
734
+
702
735
  # Emergency fallback to mark execution as failed
703
736
  #
704
737
  # Uses update_all to bypass ActiveRecord callbacks and validations,
@@ -0,0 +1,235 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ # Wrapper for agent execution results with full metadata
6
+ #
7
+ # Provides access to the response content along with execution details
8
+ # like token usage, cost, timing, and model information.
9
+ #
10
+ # @example Basic usage
11
+ # result = MyAgent.call(query: "test")
12
+ # result.content # => processed response
13
+ # result.input_tokens # => 150
14
+ # result.total_cost # => 0.00025
15
+ #
16
+ # @example Backward compatible hash access
17
+ # result[:key] # delegates to result.content[:key]
18
+ # result.dig(:nested, :key)
19
+ #
20
+ # @api public
21
+ class Result
22
+ extend ActiveSupport::Delegation
23
+
24
+ # @!attribute [r] content
25
+ # @return [Hash, String] The processed response content
26
+ attr_reader :content
27
+
28
+ # @!group Token Usage
29
+ # @!attribute [r] input_tokens
30
+ # @return [Integer, nil] Number of input tokens consumed
31
+ # @!attribute [r] output_tokens
32
+ # @return [Integer, nil] Number of output tokens generated
33
+ # @!attribute [r] cached_tokens
34
+ # @return [Integer] Number of tokens served from cache
35
+ # @!attribute [r] cache_creation_tokens
36
+ # @return [Integer] Number of tokens used to create cache
37
+ attr_reader :input_tokens, :output_tokens, :cached_tokens, :cache_creation_tokens
38
+
39
+ # @!group Cost
40
+ # @!attribute [r] input_cost
41
+ # @return [Float, nil] Cost of input tokens in USD
42
+ # @!attribute [r] output_cost
43
+ # @return [Float, nil] Cost of output tokens in USD
44
+ # @!attribute [r] total_cost
45
+ # @return [Float, nil] Total cost in USD
46
+ attr_reader :input_cost, :output_cost, :total_cost
47
+
48
+ # @!group Model Info
49
+ # @!attribute [r] model_id
50
+ # @return [String, nil] The model that was requested
51
+ # @!attribute [r] chosen_model_id
52
+ # @return [String, nil] The model that actually responded (may differ if fallback used)
53
+ # @!attribute [r] temperature
54
+ # @return [Float, nil] Temperature setting used
55
+ attr_reader :model_id, :chosen_model_id, :temperature
56
+
57
+ # @!group Timing
58
+ # @!attribute [r] started_at
59
+ # @return [Time, nil] When execution started
60
+ # @!attribute [r] completed_at
61
+ # @return [Time, nil] When execution completed
62
+ # @!attribute [r] duration_ms
63
+ # @return [Integer, nil] Execution duration in milliseconds
64
+ # @!attribute [r] time_to_first_token_ms
65
+ # @return [Integer, nil] Time to first token (streaming only)
66
+ attr_reader :started_at, :completed_at, :duration_ms, :time_to_first_token_ms
67
+
68
+ # @!group Status
69
+ # @!attribute [r] finish_reason
70
+ # @return [String, nil] Why generation stopped (stop, length, tool_calls, etc.)
71
+ # @!attribute [r] streaming
72
+ # @return [Boolean] Whether streaming was enabled
73
+ attr_reader :finish_reason, :streaming
74
+
75
+ # @!group Error Info
76
+ # @!attribute [r] error_class
77
+ # @return [String, nil] Exception class name if failed
78
+ # @!attribute [r] error_message
79
+ # @return [String, nil] Exception message if failed
80
+ attr_reader :error_class, :error_message
81
+
82
+ # @!group Reliability
83
+ # @!attribute [r] attempts
84
+ # @return [Array<Hash>] Details of each attempt (for retries/fallbacks)
85
+ # @!attribute [r] attempts_count
86
+ # @return [Integer] Number of attempts made
87
+ attr_reader :attempts, :attempts_count
88
+
89
+ # @!group Tool Calls
90
+ # @!attribute [r] tool_calls
91
+ # @return [Array<Hash>] Tool calls made during execution
92
+ # @!attribute [r] tool_calls_count
93
+ # @return [Integer] Number of tool calls made
94
+ attr_reader :tool_calls, :tool_calls_count
95
+
96
+ # Creates a new Result instance
97
+ #
98
+ # @param content [Hash, String] The processed response content
99
+ # @param options [Hash] Execution metadata
100
+ def initialize(content:, **options)
101
+ @content = content
102
+
103
+ # Token usage
104
+ @input_tokens = options[:input_tokens]
105
+ @output_tokens = options[:output_tokens]
106
+ @cached_tokens = options[:cached_tokens] || 0
107
+ @cache_creation_tokens = options[:cache_creation_tokens] || 0
108
+
109
+ # Cost
110
+ @input_cost = options[:input_cost]
111
+ @output_cost = options[:output_cost]
112
+ @total_cost = options[:total_cost]
113
+
114
+ # Model info
115
+ @model_id = options[:model_id]
116
+ @chosen_model_id = options[:chosen_model_id] || options[:model_id]
117
+ @temperature = options[:temperature]
118
+
119
+ # Timing
120
+ @started_at = options[:started_at]
121
+ @completed_at = options[:completed_at]
122
+ @duration_ms = options[:duration_ms]
123
+ @time_to_first_token_ms = options[:time_to_first_token_ms]
124
+
125
+ # Status
126
+ @finish_reason = options[:finish_reason]
127
+ @streaming = options[:streaming] || false
128
+
129
+ # Error
130
+ @error_class = options[:error_class]
131
+ @error_message = options[:error_message]
132
+
133
+ # Reliability
134
+ @attempts = options[:attempts] || []
135
+ @attempts_count = options[:attempts_count] || 1
136
+
137
+ # Tool calls
138
+ @tool_calls = options[:tool_calls] || []
139
+ @tool_calls_count = options[:tool_calls_count] || 0
140
+ end
141
+
142
+ # Returns total tokens (input + output)
143
+ #
144
+ # @return [Integer] Total token count
145
+ def total_tokens
146
+ (input_tokens || 0) + (output_tokens || 0)
147
+ end
148
+
149
+ # Returns whether streaming was enabled
150
+ #
151
+ # @return [Boolean] true if streaming was used
152
+ def streaming?
153
+ streaming == true
154
+ end
155
+
156
+ # Returns whether the execution succeeded
157
+ #
158
+ # @return [Boolean] true if no error occurred
159
+ def success?
160
+ error_class.nil?
161
+ end
162
+
163
+ # Returns whether the execution failed
164
+ #
165
+ # @return [Boolean] true if an error occurred
166
+ def error?
167
+ !success?
168
+ end
169
+
170
+ # Returns whether a fallback model was used
171
+ #
172
+ # @return [Boolean] true if chosen_model_id differs from model_id
173
+ def used_fallback?
174
+ chosen_model_id.present? && chosen_model_id != model_id
175
+ end
176
+
177
+ # Returns whether the response was truncated due to max tokens
178
+ #
179
+ # @return [Boolean] true if finish_reason is "length"
180
+ def truncated?
181
+ finish_reason == "length"
182
+ end
183
+
184
+ # Returns whether tool calls were made during execution
185
+ #
186
+ # @return [Boolean] true if tool_calls_count > 0
187
+ def has_tool_calls?
188
+ tool_calls_count.to_i > 0
189
+ end
190
+
191
+ # Converts the result to a hash
192
+ #
193
+ # @return [Hash] All result data as a hash
194
+ def to_h
195
+ {
196
+ content: content,
197
+ input_tokens: input_tokens,
198
+ output_tokens: output_tokens,
199
+ total_tokens: total_tokens,
200
+ cached_tokens: cached_tokens,
201
+ cache_creation_tokens: cache_creation_tokens,
202
+ input_cost: input_cost,
203
+ output_cost: output_cost,
204
+ total_cost: total_cost,
205
+ model_id: model_id,
206
+ chosen_model_id: chosen_model_id,
207
+ temperature: temperature,
208
+ started_at: started_at,
209
+ completed_at: completed_at,
210
+ duration_ms: duration_ms,
211
+ time_to_first_token_ms: time_to_first_token_ms,
212
+ finish_reason: finish_reason,
213
+ streaming: streaming,
214
+ error_class: error_class,
215
+ error_message: error_message,
216
+ attempts_count: attempts_count,
217
+ attempts: attempts,
218
+ tool_calls: tool_calls,
219
+ tool_calls_count: tool_calls_count
220
+ }
221
+ end
222
+
223
+ # Delegate hash methods to content for backward compatibility
224
+ delegate :[], :dig, :keys, :values, :each, :map, to: :content, allow_nil: true
225
+
226
+ # Custom to_json that returns content as JSON for backward compatibility
227
+ #
228
+ # @param args [Array] Arguments passed to to_json
229
+ # @return [String] JSON representation
230
+ def to_json(*args)
231
+ content.to_json(*args)
232
+ end
233
+ end
234
+ end
235
+ end
@@ -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 = "0.3.1"
7
+ VERSION = "0.3.3"
8
8
  end
9
9
  end
@@ -10,6 +10,7 @@ require_relative "agents/circuit_breaker"
10
10
  require_relative "agents/budget_tracker"
11
11
  require_relative "agents/alert_manager"
12
12
  require_relative "agents/attempt_tracker"
13
+ require_relative "agents/result"
13
14
  require_relative "agents/inflections" if defined?(Rails)
14
15
  require_relative "agents/engine" if defined?(Rails::Engine)
15
16
 
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: 0.3.1
4
+ version: 0.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - adham90
@@ -121,6 +121,7 @@ files:
121
121
  - app/models/ruby_llm/agents/execution/scopes.rb
122
122
  - app/services/ruby_llm/agents/agent_registry.rb
123
123
  - app/views/layouts/rubyllm/agents/application.html.erb
124
+ - app/views/rubyllm/agents/agents/_agent.html.erb
124
125
  - app/views/rubyllm/agents/agents/_version_comparison.html.erb
125
126
  - app/views/rubyllm/agents/agents/index.html.erb
126
127
  - app/views/rubyllm/agents/agents/show.html.erb
@@ -155,6 +156,7 @@ files:
155
156
  - lib/generators/ruby_llm_agents/templates/add_prompts_migration.rb.tt
156
157
  - lib/generators/ruby_llm_agents/templates/add_routing_migration.rb.tt
157
158
  - lib/generators/ruby_llm_agents/templates/add_streaming_migration.rb.tt
159
+ - lib/generators/ruby_llm_agents/templates/add_tool_calls_migration.rb.tt
158
160
  - lib/generators/ruby_llm_agents/templates/add_tracing_migration.rb.tt
159
161
  - lib/generators/ruby_llm_agents/templates/agent.rb.tt
160
162
  - lib/generators/ruby_llm_agents/templates/application_agent.rb.tt
@@ -175,6 +177,7 @@ files:
175
177
  - lib/ruby_llm/agents/instrumentation.rb
176
178
  - lib/ruby_llm/agents/redactor.rb
177
179
  - lib/ruby_llm/agents/reliability.rb
180
+ - lib/ruby_llm/agents/result.rb
178
181
  - lib/ruby_llm/agents/version.rb
179
182
  homepage: https://github.com/adham90/ruby_llm-agents
180
183
  licenses: