ruby_llm-agents 0.2.4 → 0.3.1
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 +413 -0
- data/app/channels/ruby_llm/agents/executions_channel.rb +24 -1
- data/app/controllers/concerns/ruby_llm/agents/filterable.rb +81 -0
- data/app/controllers/concerns/ruby_llm/agents/paginatable.rb +51 -0
- data/app/controllers/ruby_llm/agents/agents_controller.rb +228 -59
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +167 -12
- data/app/controllers/ruby_llm/agents/executions_controller.rb +189 -31
- data/app/controllers/ruby_llm/agents/settings_controller.rb +20 -0
- data/app/helpers/ruby_llm/agents/application_helper.rb +307 -7
- data/app/models/ruby_llm/agents/execution/analytics.rb +224 -20
- data/app/models/ruby_llm/agents/execution/metrics.rb +41 -25
- data/app/models/ruby_llm/agents/execution/scopes.rb +234 -14
- data/app/models/ruby_llm/agents/execution.rb +259 -16
- data/app/services/ruby_llm/agents/agent_registry.rb +49 -12
- data/app/views/layouts/rubyllm/agents/application.html.erb +351 -85
- data/app/views/rubyllm/agents/agents/_version_comparison.html.erb +186 -0
- data/app/views/rubyllm/agents/agents/show.html.erb +233 -10
- data/app/views/rubyllm/agents/dashboard/_action_center.html.erb +62 -0
- data/app/views/rubyllm/agents/dashboard/_alerts_feed.html.erb +62 -0
- data/app/views/rubyllm/agents/dashboard/_breaker_strip.html.erb +47 -0
- data/app/views/rubyllm/agents/dashboard/_budgets_bar.html.erb +165 -0
- data/app/views/rubyllm/agents/dashboard/_now_strip.html.erb +10 -0
- data/app/views/rubyllm/agents/dashboard/_now_strip_values.html.erb +71 -0
- data/app/views/rubyllm/agents/dashboard/index.html.erb +215 -109
- data/app/views/rubyllm/agents/executions/_filters.html.erb +152 -155
- data/app/views/rubyllm/agents/executions/_list.html.erb +103 -12
- data/app/views/rubyllm/agents/executions/dry_run.html.erb +149 -0
- data/app/views/rubyllm/agents/executions/index.html.erb +17 -72
- data/app/views/rubyllm/agents/executions/index.turbo_stream.erb +16 -2
- data/app/views/rubyllm/agents/executions/show.html.erb +693 -14
- data/app/views/rubyllm/agents/settings/show.html.erb +369 -0
- data/app/views/rubyllm/agents/shared/_filter_dropdown.html.erb +121 -0
- data/app/views/rubyllm/agents/shared/_select_dropdown.html.erb +85 -0
- data/config/routes.rb +7 -0
- data/lib/generators/ruby_llm_agents/templates/add_attempts_migration.rb.tt +27 -0
- data/lib/generators/ruby_llm_agents/templates/add_caching_migration.rb.tt +23 -0
- data/lib/generators/ruby_llm_agents/templates/add_finish_reason_migration.rb.tt +19 -0
- data/lib/generators/ruby_llm_agents/templates/add_routing_migration.rb.tt +19 -0
- data/lib/generators/ruby_llm_agents/templates/add_streaming_migration.rb.tt +8 -0
- data/lib/generators/ruby_llm_agents/templates/add_tracing_migration.rb.tt +34 -0
- data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +66 -4
- data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +53 -6
- data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +143 -8
- data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +38 -1
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +78 -0
- data/lib/ruby_llm/agents/alert_manager.rb +207 -0
- data/lib/ruby_llm/agents/attempt_tracker.rb +295 -0
- data/lib/ruby_llm/agents/base.rb +597 -112
- data/lib/ruby_llm/agents/budget_tracker.rb +360 -0
- data/lib/ruby_llm/agents/circuit_breaker.rb +197 -0
- data/lib/ruby_llm/agents/configuration.rb +279 -1
- data/lib/ruby_llm/agents/engine.rb +58 -6
- data/lib/ruby_llm/agents/execution_logger_job.rb +17 -6
- data/lib/ruby_llm/agents/inflections.rb +13 -2
- data/lib/ruby_llm/agents/instrumentation.rb +538 -87
- data/lib/ruby_llm/agents/redactor.rb +130 -0
- data/lib/ruby_llm/agents/reliability.rb +185 -0
- data/lib/ruby_llm/agents/version.rb +3 -1
- data/lib/ruby_llm/agents.rb +52 -0
- metadata +41 -2
- data/app/controllers/ruby_llm/agents/application_controller.rb +0 -37
data/lib/ruby_llm/agents/base.rb
CHANGED
|
@@ -1,109 +1,220 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# RubyLLM::Agents::Base - Base class for LLM-powered agents
|
|
4
|
-
#
|
|
5
|
-
# == Creating an Agent
|
|
6
|
-
#
|
|
7
|
-
# class SearchAgent < ApplicationAgent
|
|
8
|
-
# model "gemini-2.0-flash"
|
|
9
|
-
# temperature 0.0
|
|
10
|
-
# version "1.0"
|
|
11
|
-
# timeout 30
|
|
12
|
-
# cache 1.hour
|
|
13
|
-
#
|
|
14
|
-
# param :query, required: true
|
|
15
|
-
# param :limit, default: 10
|
|
16
|
-
#
|
|
17
|
-
# def system_prompt
|
|
18
|
-
# "You are a search assistant..."
|
|
19
|
-
# end
|
|
20
|
-
#
|
|
21
|
-
# def user_prompt
|
|
22
|
-
# query
|
|
23
|
-
# end
|
|
24
|
-
#
|
|
25
|
-
# def schema
|
|
26
|
-
# @schema ||= RubyLLM::Schema.create do
|
|
27
|
-
# string :result
|
|
28
|
-
# end
|
|
29
|
-
# end
|
|
30
|
-
# end
|
|
31
|
-
#
|
|
32
|
-
# == Calling an Agent
|
|
33
|
-
#
|
|
34
|
-
# SearchAgent.call(query: "red dress")
|
|
35
|
-
# SearchAgent.call(query: "red dress", dry_run: true) # Debug prompts
|
|
36
|
-
# SearchAgent.call(query: "red dress", skip_cache: true) # Bypass cache
|
|
37
|
-
#
|
|
38
|
-
# == Configuration DSL
|
|
39
|
-
#
|
|
40
|
-
# model "gemini-2.0-flash" # LLM model (default from config)
|
|
41
|
-
# temperature 0.0 # Randomness 0.0-1.0 (default from config)
|
|
42
|
-
# version "1.0" # Version for cache invalidation
|
|
43
|
-
# timeout 30 # Seconds before timeout (default from config)
|
|
44
|
-
# cache 1.hour # Enable caching with TTL (default: disabled)
|
|
45
|
-
#
|
|
46
|
-
# == Parameter DSL
|
|
47
|
-
#
|
|
48
|
-
# param :name # Optional parameter
|
|
49
|
-
# param :query, required: true # Required - raises ArgumentError if missing
|
|
50
|
-
# param :limit, default: 10 # Optional with default value
|
|
51
|
-
#
|
|
52
|
-
# == Template Methods (override in subclasses)
|
|
53
|
-
#
|
|
54
|
-
# user_prompt - Required. The prompt sent to the LLM.
|
|
55
|
-
# system_prompt - Optional. System instructions for the LLM.
|
|
56
|
-
# schema - Optional. RubyLLM::Schema for structured output.
|
|
57
|
-
# process_response(response) - Optional. Post-process the LLM response.
|
|
58
|
-
# cache_key_data - Optional. Override to customize cache key generation.
|
|
59
|
-
#
|
|
60
3
|
module RubyLLM
|
|
61
4
|
module Agents
|
|
5
|
+
# Base class for LLM-powered agents
|
|
6
|
+
#
|
|
7
|
+
# Provides a DSL for configuring and executing agents that interact with
|
|
8
|
+
# large language models. Includes built-in support for caching, timeouts,
|
|
9
|
+
# structured output, and execution tracking.
|
|
10
|
+
#
|
|
11
|
+
# @example Creating an agent
|
|
12
|
+
# class SearchAgent < ApplicationAgent
|
|
13
|
+
# model "gpt-4o"
|
|
14
|
+
# temperature 0.0
|
|
15
|
+
# version "1.0"
|
|
16
|
+
# timeout 30
|
|
17
|
+
# cache 1.hour
|
|
18
|
+
#
|
|
19
|
+
# param :query, required: true
|
|
20
|
+
# param :limit, default: 10
|
|
21
|
+
#
|
|
22
|
+
# def system_prompt
|
|
23
|
+
# "You are a search assistant..."
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# def user_prompt
|
|
27
|
+
# "Search for: #{query}"
|
|
28
|
+
# end
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
# @example Calling an agent
|
|
32
|
+
# SearchAgent.call(query: "red dress")
|
|
33
|
+
# SearchAgent.call(query: "red dress", dry_run: true) # Debug mode
|
|
34
|
+
# SearchAgent.call(query: "red dress", skip_cache: true) # Bypass cache
|
|
35
|
+
#
|
|
36
|
+
# @see RubyLLM::Agents::Instrumentation
|
|
37
|
+
# @api public
|
|
62
38
|
class Base
|
|
63
39
|
include Instrumentation
|
|
64
40
|
|
|
65
|
-
#
|
|
66
|
-
VERSION
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
# ==========================================================================
|
|
70
|
-
# Class Methods (DSL)
|
|
71
|
-
# ==========================================================================
|
|
41
|
+
# @!visibility private
|
|
42
|
+
VERSION = "1.0".freeze
|
|
43
|
+
# @!visibility private
|
|
44
|
+
CACHE_TTL = 1.hour
|
|
72
45
|
|
|
73
46
|
class << self
|
|
74
|
-
# Factory method
|
|
75
|
-
|
|
76
|
-
|
|
47
|
+
# Factory method to instantiate and execute an agent
|
|
48
|
+
#
|
|
49
|
+
# @param args [Array] Positional arguments (reserved for future use)
|
|
50
|
+
# @param kwargs [Hash] Named parameters for the agent
|
|
51
|
+
# @option kwargs [Boolean] :dry_run Return prompt info without API call
|
|
52
|
+
# @option kwargs [Boolean] :skip_cache Bypass caching even if enabled
|
|
53
|
+
# @option kwargs [String, Array<String>] :with Attachments (files, URLs) to send with the prompt
|
|
54
|
+
# @yield [chunk] Yields chunks when streaming is enabled
|
|
55
|
+
# @yieldparam chunk [RubyLLM::Chunk] A streaming chunk with content
|
|
56
|
+
# @return [Object] The processed response from the agent
|
|
57
|
+
#
|
|
58
|
+
# @example Basic usage
|
|
59
|
+
# SearchAgent.call(query: "red dress")
|
|
60
|
+
#
|
|
61
|
+
# @example Debug mode
|
|
62
|
+
# SearchAgent.call(query: "red dress", dry_run: true)
|
|
63
|
+
#
|
|
64
|
+
# @example Streaming mode
|
|
65
|
+
# ChatAgent.call(message: "Hello") do |chunk|
|
|
66
|
+
# print chunk.content
|
|
67
|
+
# end
|
|
68
|
+
#
|
|
69
|
+
# @example With attachments
|
|
70
|
+
# VisionAgent.call(query: "Describe this image", with: "photo.jpg")
|
|
71
|
+
# VisionAgent.call(query: "Compare these", with: ["a.png", "b.png"])
|
|
72
|
+
def call(*args, **kwargs, &block)
|
|
73
|
+
new(*args, **kwargs).call(&block)
|
|
77
74
|
end
|
|
78
75
|
|
|
79
|
-
#
|
|
80
|
-
# Configuration DSL
|
|
81
|
-
# ------------------------------------------------------------------------
|
|
76
|
+
# @!group Configuration DSL
|
|
82
77
|
|
|
78
|
+
# Sets or returns the LLM model for this agent class
|
|
79
|
+
#
|
|
80
|
+
# @param value [String, nil] The model identifier to set
|
|
81
|
+
# @return [String] The current model setting
|
|
82
|
+
# @example
|
|
83
|
+
# model "gpt-4o"
|
|
83
84
|
def model(value = nil)
|
|
84
85
|
@model = value if value
|
|
85
86
|
@model || inherited_or_default(:model, RubyLLM::Agents.configuration.default_model)
|
|
86
87
|
end
|
|
87
88
|
|
|
89
|
+
# Sets or returns the temperature for LLM responses
|
|
90
|
+
#
|
|
91
|
+
# @param value [Float, nil] Temperature value (0.0-2.0)
|
|
92
|
+
# @return [Float] The current temperature setting
|
|
93
|
+
# @example
|
|
94
|
+
# temperature 0.7
|
|
88
95
|
def temperature(value = nil)
|
|
89
96
|
@temperature = value if value
|
|
90
97
|
@temperature || inherited_or_default(:temperature, RubyLLM::Agents.configuration.default_temperature)
|
|
91
98
|
end
|
|
92
99
|
|
|
100
|
+
# Sets or returns the version string for cache invalidation
|
|
101
|
+
#
|
|
102
|
+
# @param value [String, nil] Version string
|
|
103
|
+
# @return [String] The current version
|
|
104
|
+
# @example
|
|
105
|
+
# version "2.0"
|
|
93
106
|
def version(value = nil)
|
|
94
107
|
@version = value if value
|
|
95
108
|
@version || inherited_or_default(:version, VERSION)
|
|
96
109
|
end
|
|
97
110
|
|
|
111
|
+
# Sets or returns the timeout in seconds for LLM requests
|
|
112
|
+
#
|
|
113
|
+
# @param value [Integer, nil] Timeout in seconds
|
|
114
|
+
# @return [Integer] The current timeout setting
|
|
115
|
+
# @example
|
|
116
|
+
# timeout 30
|
|
98
117
|
def timeout(value = nil)
|
|
99
118
|
@timeout = value if value
|
|
100
119
|
@timeout || inherited_or_default(:timeout, RubyLLM::Agents.configuration.default_timeout)
|
|
101
120
|
end
|
|
102
121
|
|
|
103
|
-
#
|
|
104
|
-
|
|
105
|
-
#
|
|
122
|
+
# @!endgroup
|
|
123
|
+
|
|
124
|
+
# @!group Reliability DSL
|
|
125
|
+
|
|
126
|
+
# Configures retry behavior for this agent
|
|
127
|
+
#
|
|
128
|
+
# @param max [Integer] Maximum number of retry attempts (default: 0)
|
|
129
|
+
# @param backoff [Symbol] Backoff strategy (:constant or :exponential)
|
|
130
|
+
# @param base [Float] Base delay in seconds
|
|
131
|
+
# @param max_delay [Float] Maximum delay between retries
|
|
132
|
+
# @param on [Array<Class>] Error classes to retry on (extends defaults)
|
|
133
|
+
# @return [Hash] The current retry configuration
|
|
134
|
+
# @example
|
|
135
|
+
# retries max: 2, backoff: :exponential, base: 0.4, max_delay: 3.0, on: [Timeout::Error]
|
|
136
|
+
def retries(max: nil, backoff: nil, base: nil, max_delay: nil, on: nil)
|
|
137
|
+
if max || backoff || base || max_delay || on
|
|
138
|
+
@retries_config ||= RubyLLM::Agents.configuration.default_retries.dup
|
|
139
|
+
@retries_config[:max] = max if max
|
|
140
|
+
@retries_config[:backoff] = backoff if backoff
|
|
141
|
+
@retries_config[:base] = base if base
|
|
142
|
+
@retries_config[:max_delay] = max_delay if max_delay
|
|
143
|
+
@retries_config[:on] = on if on
|
|
144
|
+
end
|
|
145
|
+
@retries_config || inherited_or_default(:retries_config, RubyLLM::Agents.configuration.default_retries)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Returns the retry configuration for this agent
|
|
149
|
+
#
|
|
150
|
+
# @return [Hash, nil] The retry configuration
|
|
151
|
+
def retries_config
|
|
152
|
+
@retries_config || (superclass.respond_to?(:retries_config) ? superclass.retries_config : nil)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Sets or returns fallback models to try when primary model fails
|
|
156
|
+
#
|
|
157
|
+
# @param models [Array<String>, nil] Model identifiers to use as fallbacks
|
|
158
|
+
# @return [Array<String>] The current fallback models
|
|
159
|
+
# @example
|
|
160
|
+
# fallback_models ["gpt-4o-mini", "gpt-4o"]
|
|
161
|
+
def fallback_models(models = nil)
|
|
162
|
+
@fallback_models = models if models
|
|
163
|
+
@fallback_models || inherited_or_default(:fallback_models, RubyLLM::Agents.configuration.default_fallback_models)
|
|
164
|
+
end
|
|
106
165
|
|
|
166
|
+
# Sets or returns the total timeout for all retry/fallback attempts
|
|
167
|
+
#
|
|
168
|
+
# @param seconds [Integer, nil] Total timeout in seconds
|
|
169
|
+
# @return [Integer, nil] The current total timeout
|
|
170
|
+
# @example
|
|
171
|
+
# total_timeout 20
|
|
172
|
+
def total_timeout(seconds = nil)
|
|
173
|
+
@total_timeout = seconds if seconds
|
|
174
|
+
@total_timeout || inherited_or_default(:total_timeout, RubyLLM::Agents.configuration.default_total_timeout)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Configures circuit breaker for this agent
|
|
178
|
+
#
|
|
179
|
+
# @param errors [Integer] Number of errors to trigger open state
|
|
180
|
+
# @param within [Integer] Rolling window in seconds
|
|
181
|
+
# @param cooldown [Integer] Cooldown period in seconds when open
|
|
182
|
+
# @return [Hash, nil] The current circuit breaker configuration
|
|
183
|
+
# @example
|
|
184
|
+
# circuit_breaker errors: 10, within: 60, cooldown: 300
|
|
185
|
+
def circuit_breaker(errors: nil, within: nil, cooldown: nil)
|
|
186
|
+
if errors || within || cooldown
|
|
187
|
+
@circuit_breaker_config ||= { errors: 10, within: 60, cooldown: 300 }
|
|
188
|
+
@circuit_breaker_config[:errors] = errors if errors
|
|
189
|
+
@circuit_breaker_config[:within] = within if within
|
|
190
|
+
@circuit_breaker_config[:cooldown] = cooldown if cooldown
|
|
191
|
+
end
|
|
192
|
+
@circuit_breaker_config || (superclass.respond_to?(:circuit_breaker_config) ? superclass.circuit_breaker_config : nil)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Returns the circuit breaker configuration for this agent
|
|
196
|
+
#
|
|
197
|
+
# @return [Hash, nil] The circuit breaker configuration
|
|
198
|
+
def circuit_breaker_config
|
|
199
|
+
@circuit_breaker_config || (superclass.respond_to?(:circuit_breaker_config) ? superclass.circuit_breaker_config : nil)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# @!endgroup
|
|
203
|
+
|
|
204
|
+
# @!group Parameter DSL
|
|
205
|
+
|
|
206
|
+
# Defines a parameter for the agent
|
|
207
|
+
#
|
|
208
|
+
# Creates an accessor method for the parameter that retrieves values
|
|
209
|
+
# from the options hash, falling back to the default value.
|
|
210
|
+
#
|
|
211
|
+
# @param name [Symbol] The parameter name
|
|
212
|
+
# @param required [Boolean] Whether the parameter is required
|
|
213
|
+
# @param default [Object, nil] Default value if not provided
|
|
214
|
+
# @return [void]
|
|
215
|
+
# @example
|
|
216
|
+
# param :query, required: true
|
|
217
|
+
# param :limit, default: 10
|
|
107
218
|
def param(name, required: false, default: nil)
|
|
108
219
|
@params ||= {}
|
|
109
220
|
@params[name] = { required: required, default: default }
|
|
@@ -112,41 +223,116 @@ module RubyLLM
|
|
|
112
223
|
end
|
|
113
224
|
end
|
|
114
225
|
|
|
226
|
+
# Returns all defined parameters including inherited ones
|
|
227
|
+
#
|
|
228
|
+
# @return [Hash{Symbol => Hash}] Parameter definitions
|
|
115
229
|
def params
|
|
116
230
|
parent = superclass.respond_to?(:params) ? superclass.params : {}
|
|
117
231
|
parent.merge(@params || {})
|
|
118
232
|
end
|
|
119
233
|
|
|
120
|
-
#
|
|
121
|
-
|
|
122
|
-
#
|
|
234
|
+
# @!endgroup
|
|
235
|
+
|
|
236
|
+
# @!group Caching DSL
|
|
123
237
|
|
|
238
|
+
# Enables caching for this agent with optional TTL
|
|
239
|
+
#
|
|
240
|
+
# @param ttl [ActiveSupport::Duration] Time-to-live for cached responses
|
|
241
|
+
# @return [void]
|
|
242
|
+
# @example
|
|
243
|
+
# cache 1.hour
|
|
124
244
|
def cache(ttl = CACHE_TTL)
|
|
125
245
|
@cache_enabled = true
|
|
126
246
|
@cache_ttl = ttl
|
|
127
247
|
end
|
|
128
248
|
|
|
249
|
+
# Returns whether caching is enabled for this agent
|
|
250
|
+
#
|
|
251
|
+
# @return [Boolean] true if caching is enabled
|
|
129
252
|
def cache_enabled?
|
|
130
253
|
@cache_enabled || false
|
|
131
254
|
end
|
|
132
255
|
|
|
256
|
+
# Returns the cache TTL for this agent
|
|
257
|
+
#
|
|
258
|
+
# @return [ActiveSupport::Duration] The cache TTL
|
|
133
259
|
def cache_ttl
|
|
134
260
|
@cache_ttl || CACHE_TTL
|
|
135
261
|
end
|
|
136
262
|
|
|
263
|
+
# @!endgroup
|
|
264
|
+
|
|
265
|
+
# @!group Streaming DSL
|
|
266
|
+
|
|
267
|
+
# Enables or returns streaming mode for this agent
|
|
268
|
+
#
|
|
269
|
+
# When streaming is enabled and a block is passed to call,
|
|
270
|
+
# chunks will be yielded to the block as they arrive.
|
|
271
|
+
#
|
|
272
|
+
# @param value [Boolean, nil] Whether to enable streaming
|
|
273
|
+
# @return [Boolean] The current streaming setting
|
|
274
|
+
# @example
|
|
275
|
+
# streaming true
|
|
276
|
+
def streaming(value = nil)
|
|
277
|
+
@streaming = value unless value.nil?
|
|
278
|
+
return @streaming unless @streaming.nil?
|
|
279
|
+
|
|
280
|
+
inherited_or_default(:streaming, RubyLLM::Agents.configuration.default_streaming)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# @!endgroup
|
|
284
|
+
|
|
285
|
+
# @!group Tools DSL
|
|
286
|
+
|
|
287
|
+
# Sets or returns the tools available to this agent
|
|
288
|
+
#
|
|
289
|
+
# Tools are RubyLLM::Tool classes that the model can invoke.
|
|
290
|
+
# The agent will automatically execute tool calls and continue
|
|
291
|
+
# until the model produces a final response.
|
|
292
|
+
#
|
|
293
|
+
# @param tool_classes [Array<Class>] Tool classes to make available
|
|
294
|
+
# @return [Array<Class>] The current tools
|
|
295
|
+
# @example Single tool
|
|
296
|
+
# tools WeatherTool
|
|
297
|
+
# @example Multiple tools
|
|
298
|
+
# tools WeatherTool, SearchTool, CalculatorTool
|
|
299
|
+
def tools(*tool_classes)
|
|
300
|
+
if tool_classes.any?
|
|
301
|
+
@tools = tool_classes.flatten
|
|
302
|
+
end
|
|
303
|
+
@tools || inherited_or_default(:tools, RubyLLM::Agents.configuration.default_tools)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# @!endgroup
|
|
307
|
+
|
|
137
308
|
private
|
|
138
309
|
|
|
310
|
+
# Looks up setting from superclass or uses default
|
|
311
|
+
#
|
|
312
|
+
# @param method [Symbol] The method to call on superclass
|
|
313
|
+
# @param default [Object] Default value if not found
|
|
314
|
+
# @return [Object] The resolved value
|
|
139
315
|
def inherited_or_default(method, default)
|
|
140
316
|
superclass.respond_to?(method) ? superclass.send(method) : default
|
|
141
317
|
end
|
|
142
318
|
end
|
|
143
319
|
|
|
144
|
-
#
|
|
145
|
-
#
|
|
146
|
-
#
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
320
|
+
# @!attribute [r] model
|
|
321
|
+
# @return [String] The LLM model being used
|
|
322
|
+
# @!attribute [r] temperature
|
|
323
|
+
# @return [Float] The temperature setting
|
|
324
|
+
# @!attribute [r] client
|
|
325
|
+
# @return [RubyLLM::Chat] The configured RubyLLM client
|
|
326
|
+
# @!attribute [r] time_to_first_token_ms
|
|
327
|
+
# @return [Integer, nil] Time to first token in milliseconds (streaming only)
|
|
328
|
+
attr_reader :model, :temperature, :client, :time_to_first_token_ms
|
|
329
|
+
|
|
330
|
+
# Creates a new agent instance
|
|
331
|
+
#
|
|
332
|
+
# @param model [String] Override the class-level model setting
|
|
333
|
+
# @param temperature [Float] Override the class-level temperature
|
|
334
|
+
# @param options [Hash] Agent parameters defined via the param DSL
|
|
335
|
+
# @raise [ArgumentError] If required parameters are missing
|
|
150
336
|
def initialize(model: self.class.model, temperature: self.class.temperature, **options)
|
|
151
337
|
@model = model
|
|
152
338
|
@temperature = temperature
|
|
@@ -155,39 +341,90 @@ module RubyLLM
|
|
|
155
341
|
@client = build_client
|
|
156
342
|
end
|
|
157
343
|
|
|
158
|
-
#
|
|
159
|
-
|
|
344
|
+
# Executes the agent and returns the processed response
|
|
345
|
+
#
|
|
346
|
+
# Handles caching, dry-run mode, and delegates to uncached_call
|
|
347
|
+
# for actual LLM execution.
|
|
348
|
+
#
|
|
349
|
+
# @yield [chunk] Yields chunks when streaming is enabled
|
|
350
|
+
# @yieldparam chunk [RubyLLM::Chunk] A streaming chunk with content
|
|
351
|
+
# @return [Object] The processed LLM response
|
|
352
|
+
def call(&block)
|
|
160
353
|
return dry_run_response if @options[:dry_run]
|
|
161
|
-
return uncached_call if @options[:skip_cache] || !self.class.cache_enabled?
|
|
354
|
+
return uncached_call(&block) if @options[:skip_cache] || !self.class.cache_enabled?
|
|
162
355
|
|
|
356
|
+
# Note: Cached responses don't stream (already complete)
|
|
163
357
|
cache_store.fetch(cache_key, expires_in: self.class.cache_ttl) do
|
|
164
|
-
uncached_call
|
|
358
|
+
uncached_call(&block)
|
|
165
359
|
end
|
|
166
360
|
end
|
|
167
361
|
|
|
168
|
-
#
|
|
169
|
-
# Template Methods (override in subclasses)
|
|
170
|
-
# --------------------------------------------------------------------------
|
|
362
|
+
# @!group Template Methods (override in subclasses)
|
|
171
363
|
|
|
364
|
+
# User prompt to send to the LLM
|
|
365
|
+
#
|
|
366
|
+
# @abstract Subclasses must implement this method
|
|
367
|
+
# @return [String] The user prompt
|
|
368
|
+
# @raise [NotImplementedError] If not overridden in subclass
|
|
172
369
|
def user_prompt
|
|
173
370
|
raise NotImplementedError, "#{self.class} must implement #user_prompt"
|
|
174
371
|
end
|
|
175
372
|
|
|
373
|
+
# System prompt for LLM instructions
|
|
374
|
+
#
|
|
375
|
+
# @return [String, nil] System instructions, or nil for none
|
|
176
376
|
def system_prompt
|
|
177
377
|
nil
|
|
178
378
|
end
|
|
179
379
|
|
|
380
|
+
# Response schema for structured output
|
|
381
|
+
#
|
|
382
|
+
# @return [RubyLLM::Schema, nil] Schema definition, or nil for free-form
|
|
180
383
|
def schema
|
|
181
384
|
nil
|
|
182
385
|
end
|
|
183
386
|
|
|
387
|
+
# Post-processes the LLM response
|
|
388
|
+
#
|
|
389
|
+
# Override to transform the response before returning to the caller.
|
|
390
|
+
# Default implementation symbolizes hash keys.
|
|
391
|
+
#
|
|
392
|
+
# @param response [RubyLLM::Message] The raw response from the LLM
|
|
393
|
+
# @return [Object] The processed result
|
|
184
394
|
def process_response(response)
|
|
185
395
|
content = response.content
|
|
186
396
|
return content unless content.is_a?(Hash)
|
|
187
397
|
content.transform_keys(&:to_sym)
|
|
188
398
|
end
|
|
189
399
|
|
|
190
|
-
#
|
|
400
|
+
# @!endgroup
|
|
401
|
+
|
|
402
|
+
# Returns the consolidated reliability configuration for this agent instance
|
|
403
|
+
#
|
|
404
|
+
# @return [Hash] Reliability config with :retries, :fallback_models, :total_timeout, :circuit_breaker
|
|
405
|
+
def reliability_config
|
|
406
|
+
default_retries = RubyLLM::Agents.configuration.default_retries
|
|
407
|
+
{
|
|
408
|
+
retries: self.class.retries || default_retries,
|
|
409
|
+
fallback_models: self.class.fallback_models,
|
|
410
|
+
total_timeout: self.class.total_timeout,
|
|
411
|
+
circuit_breaker: self.class.circuit_breaker_config
|
|
412
|
+
}
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# Returns whether any reliability features are enabled for this agent
|
|
416
|
+
#
|
|
417
|
+
# @return [Boolean] true if retries, fallbacks, or circuit breaker is configured
|
|
418
|
+
def reliability_enabled?
|
|
419
|
+
config = reliability_config
|
|
420
|
+
(config[:retries]&.dig(:max) || 0) > 0 ||
|
|
421
|
+
config[:fallback_models]&.any? ||
|
|
422
|
+
config[:circuit_breaker].present?
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# Returns prompt info without making an API call (debug mode)
|
|
426
|
+
#
|
|
427
|
+
# @return [Hash] Agent configuration and prompt info
|
|
191
428
|
def dry_run_response
|
|
192
429
|
{
|
|
193
430
|
dry_run: true,
|
|
@@ -197,70 +434,318 @@ module RubyLLM
|
|
|
197
434
|
timeout: self.class.timeout,
|
|
198
435
|
system_prompt: system_prompt,
|
|
199
436
|
user_prompt: user_prompt,
|
|
200
|
-
|
|
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 }
|
|
201
441
|
}
|
|
202
442
|
end
|
|
203
443
|
|
|
204
|
-
# ==========================================================================
|
|
205
|
-
# Private Methods
|
|
206
|
-
# ==========================================================================
|
|
207
|
-
|
|
208
444
|
private
|
|
209
445
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
446
|
+
# Executes the agent without caching
|
|
447
|
+
#
|
|
448
|
+
# Routes to reliability-enabled execution if configured, otherwise
|
|
449
|
+
# uses simple single-attempt execution.
|
|
450
|
+
#
|
|
451
|
+
# @yield [chunk] Yields chunks when streaming is enabled
|
|
452
|
+
# @return [Object] The processed response
|
|
453
|
+
def uncached_call(&block)
|
|
454
|
+
if reliability_enabled?
|
|
455
|
+
execute_with_reliability(&block)
|
|
456
|
+
else
|
|
457
|
+
instrument_execution { execute_single_attempt(&block) }
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
# Executes a single LLM attempt with timeout
|
|
462
|
+
#
|
|
463
|
+
# @param model_override [String, nil] Optional model to use instead of default
|
|
464
|
+
# @yield [chunk] Yields chunks when streaming is enabled
|
|
465
|
+
# @return [Object] The processed response
|
|
466
|
+
def execute_single_attempt(model_override: nil, &block)
|
|
467
|
+
current_client = model_override ? build_client_with_model(model_override) : client
|
|
468
|
+
@execution_started_at ||= Time.current
|
|
469
|
+
|
|
470
|
+
Timeout.timeout(self.class.timeout) do
|
|
471
|
+
if self.class.streaming && block_given?
|
|
472
|
+
execute_with_streaming(current_client, &block)
|
|
473
|
+
else
|
|
474
|
+
response = current_client.ask(user_prompt, **ask_options)
|
|
214
475
|
process_response(capture_response(response))
|
|
215
476
|
end
|
|
216
477
|
end
|
|
217
478
|
end
|
|
218
479
|
|
|
219
|
-
#
|
|
220
|
-
#
|
|
221
|
-
#
|
|
480
|
+
# Executes an LLM request with streaming enabled
|
|
481
|
+
#
|
|
482
|
+
# Yields chunks to the provided block as they arrive and tracks
|
|
483
|
+
# time to first token for latency analysis.
|
|
484
|
+
#
|
|
485
|
+
# @param current_client [RubyLLM::Chat] The configured client
|
|
486
|
+
# @yield [chunk] Yields each chunk as it arrives
|
|
487
|
+
# @yieldparam chunk [RubyLLM::Chunk] A streaming chunk
|
|
488
|
+
# @return [Object] The processed response
|
|
489
|
+
def execute_with_streaming(current_client, &block)
|
|
490
|
+
first_chunk_at = nil
|
|
491
|
+
|
|
492
|
+
response = current_client.ask(user_prompt, **ask_options) do |chunk|
|
|
493
|
+
first_chunk_at ||= Time.current
|
|
494
|
+
yield chunk if block_given?
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
if first_chunk_at && @execution_started_at
|
|
498
|
+
@time_to_first_token_ms = ((first_chunk_at - @execution_started_at) * 1000).to_i
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
process_response(capture_response(response))
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
# Executes the agent with retry/fallback/circuit breaker support
|
|
505
|
+
#
|
|
506
|
+
# @yield [chunk] Yields chunks when streaming is enabled
|
|
507
|
+
# @return [Object] The processed response
|
|
508
|
+
# @raise [Reliability::AllModelsExhaustedError] If all models fail
|
|
509
|
+
# @raise [Reliability::BudgetExceededError] If budget limits exceeded
|
|
510
|
+
# @raise [Reliability::TotalTimeoutError] If total timeout exceeded
|
|
511
|
+
def execute_with_reliability(&block)
|
|
512
|
+
config = reliability_config
|
|
513
|
+
models_to_try = [model, *config[:fallback_models]].uniq
|
|
514
|
+
total_deadline = config[:total_timeout] ? Time.current + config[:total_timeout] : nil
|
|
515
|
+
started_at = Time.current
|
|
516
|
+
|
|
517
|
+
# Pre-check budget
|
|
518
|
+
BudgetTracker.check_budget!(self.class.name) if RubyLLM::Agents.configuration.budgets_enabled?
|
|
519
|
+
|
|
520
|
+
instrument_execution_with_attempts(models_to_try: models_to_try) do |attempt_tracker|
|
|
521
|
+
last_error = nil
|
|
522
|
+
|
|
523
|
+
models_to_try.each do |current_model|
|
|
524
|
+
# Check circuit breaker
|
|
525
|
+
breaker = get_circuit_breaker(current_model)
|
|
526
|
+
if breaker&.open?
|
|
527
|
+
attempt_tracker.record_short_circuit(current_model)
|
|
528
|
+
next
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
retries_remaining = config[:retries]&.dig(:max) || 0
|
|
532
|
+
attempt_index = 0
|
|
533
|
+
|
|
534
|
+
loop do
|
|
535
|
+
# Check total timeout
|
|
536
|
+
if total_deadline && Time.current > total_deadline
|
|
537
|
+
elapsed = Time.current - started_at
|
|
538
|
+
raise Reliability::TotalTimeoutError.new(config[:total_timeout], elapsed)
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
attempt = attempt_tracker.start_attempt(current_model)
|
|
542
|
+
|
|
543
|
+
begin
|
|
544
|
+
result = execute_single_attempt(model_override: current_model, &block)
|
|
545
|
+
attempt_tracker.complete_attempt(attempt, success: true, response: @last_response)
|
|
546
|
+
|
|
547
|
+
# Record success in circuit breaker
|
|
548
|
+
breaker&.record_success!
|
|
549
|
+
|
|
550
|
+
# Record budget spend
|
|
551
|
+
if @last_response && RubyLLM::Agents.configuration.budgets_enabled?
|
|
552
|
+
record_attempt_cost(attempt_tracker)
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
# Use throw instead of return to allow instrument_execution_with_attempts
|
|
556
|
+
# to properly complete the execution record before returning
|
|
557
|
+
throw :execution_success, result
|
|
558
|
+
|
|
559
|
+
rescue *retryable_errors(config) => e
|
|
560
|
+
last_error = e
|
|
561
|
+
attempt_tracker.complete_attempt(attempt, success: false, error: e)
|
|
562
|
+
breaker&.record_failure!
|
|
563
|
+
|
|
564
|
+
if retries_remaining > 0 && !past_deadline?(total_deadline)
|
|
565
|
+
retries_remaining -= 1
|
|
566
|
+
attempt_index += 1
|
|
567
|
+
retries_config = config[:retries] || {}
|
|
568
|
+
delay = Reliability.calculate_backoff(
|
|
569
|
+
strategy: retries_config[:backoff] || :exponential,
|
|
570
|
+
base: retries_config[:base] || 0.4,
|
|
571
|
+
max_delay: retries_config[:max_delay] || 3.0,
|
|
572
|
+
attempt: attempt_index
|
|
573
|
+
)
|
|
574
|
+
sleep(delay)
|
|
575
|
+
else
|
|
576
|
+
break # Move to next model
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
rescue StandardError => e
|
|
580
|
+
# Non-retryable error - record and move to next model
|
|
581
|
+
last_error = e
|
|
582
|
+
attempt_tracker.complete_attempt(attempt, success: false, error: e)
|
|
583
|
+
breaker&.record_failure!
|
|
584
|
+
break
|
|
585
|
+
end
|
|
586
|
+
end
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
# All models exhausted
|
|
590
|
+
raise Reliability::AllModelsExhaustedError.new(models_to_try, last_error)
|
|
591
|
+
end
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
# Returns the list of retryable error classes
|
|
595
|
+
#
|
|
596
|
+
# @param config [Hash] Reliability configuration
|
|
597
|
+
# @return [Array<Class>] Error classes to retry on
|
|
598
|
+
def retryable_errors(config)
|
|
599
|
+
custom_errors = config[:retries]&.dig(:on) || []
|
|
600
|
+
Reliability.default_retryable_errors + custom_errors
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
# Checks if the total deadline has passed
|
|
604
|
+
#
|
|
605
|
+
# @param deadline [Time, nil] The deadline
|
|
606
|
+
# @return [Boolean] true if past deadline
|
|
607
|
+
def past_deadline?(deadline)
|
|
608
|
+
deadline && Time.current > deadline
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
# Gets or creates a circuit breaker for a model
|
|
612
|
+
#
|
|
613
|
+
# @param model_id [String] The model identifier
|
|
614
|
+
# @return [CircuitBreaker, nil] The circuit breaker or nil if not configured
|
|
615
|
+
def get_circuit_breaker(model_id)
|
|
616
|
+
config = reliability_config[:circuit_breaker]
|
|
617
|
+
return nil unless config
|
|
618
|
+
|
|
619
|
+
CircuitBreaker.from_config(self.class.name, model_id, config)
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
# Records cost from an attempt to the budget tracker
|
|
623
|
+
#
|
|
624
|
+
# @param attempt_tracker [AttemptTracker] The attempt tracker
|
|
625
|
+
# @return [void]
|
|
626
|
+
def record_attempt_cost(attempt_tracker)
|
|
627
|
+
successful = attempt_tracker.successful_attempt
|
|
628
|
+
return unless successful
|
|
222
629
|
|
|
630
|
+
# Calculate cost for this execution
|
|
631
|
+
# Note: Full cost calculation happens in instrumentation, but we
|
|
632
|
+
# record the spend here for budget tracking
|
|
633
|
+
model_info = resolve_model_info(successful[:model_id])
|
|
634
|
+
return unless model_info&.pricing
|
|
635
|
+
|
|
636
|
+
input_tokens = successful[:input_tokens] || 0
|
|
637
|
+
output_tokens = successful[:output_tokens] || 0
|
|
638
|
+
|
|
639
|
+
input_price = model_info.pricing.text_tokens&.input || 0
|
|
640
|
+
output_price = model_info.pricing.text_tokens&.output || 0
|
|
641
|
+
|
|
642
|
+
total_cost = (input_tokens / 1_000_000.0 * input_price) +
|
|
643
|
+
(output_tokens / 1_000_000.0 * output_price)
|
|
644
|
+
|
|
645
|
+
BudgetTracker.record_spend!(self.class.name, total_cost)
|
|
646
|
+
rescue StandardError => e
|
|
647
|
+
Rails.logger.warn("[RubyLLM::Agents] Failed to record budget spend: #{e.message}")
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
# Resolves model info for cost calculation
|
|
651
|
+
#
|
|
652
|
+
# @param model_id [String] The model identifier
|
|
653
|
+
# @return [Object, nil] Model info or nil
|
|
654
|
+
def resolve_model_info(model_id)
|
|
655
|
+
RubyLLM::Models.resolve(model_id)
|
|
656
|
+
rescue StandardError
|
|
657
|
+
nil
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
# Builds a client with a specific model
|
|
661
|
+
#
|
|
662
|
+
# @param model_id [String] The model identifier
|
|
663
|
+
# @return [RubyLLM::Chat] Configured chat client
|
|
664
|
+
def build_client_with_model(model_id)
|
|
665
|
+
client = RubyLLM.chat
|
|
666
|
+
.with_model(model_id)
|
|
667
|
+
.with_temperature(temperature)
|
|
668
|
+
client = client.with_instructions(system_prompt) if system_prompt
|
|
669
|
+
client = client.with_schema(schema) if schema
|
|
670
|
+
client = client.with_tools(*self.class.tools) if self.class.tools.any?
|
|
671
|
+
client
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
# Returns the configured cache store
|
|
675
|
+
#
|
|
676
|
+
# @return [ActiveSupport::Cache::Store] The cache store
|
|
223
677
|
def cache_store
|
|
224
678
|
RubyLLM::Agents.configuration.cache_store
|
|
225
679
|
end
|
|
226
680
|
|
|
681
|
+
# Generates the full cache key for this agent invocation
|
|
682
|
+
#
|
|
683
|
+
# @return [String] Cache key in format "ruby_llm_agent/ClassName/version/hash"
|
|
227
684
|
def cache_key
|
|
228
685
|
["ruby_llm_agent", self.class.name, self.class.version, cache_key_hash].join("/")
|
|
229
686
|
end
|
|
230
687
|
|
|
688
|
+
# Generates a hash of the cache key data
|
|
689
|
+
#
|
|
690
|
+
# @return [String] SHA256 hex digest of the cache key data
|
|
231
691
|
def cache_key_hash
|
|
232
692
|
Digest::SHA256.hexdigest(cache_key_data.to_json)
|
|
233
693
|
end
|
|
234
694
|
|
|
235
|
-
#
|
|
695
|
+
# Returns data to include in cache key generation
|
|
696
|
+
#
|
|
697
|
+
# Override to customize what parameters affect cache invalidation.
|
|
698
|
+
#
|
|
699
|
+
# @return [Hash] Data to hash for cache key
|
|
236
700
|
def cache_key_data
|
|
237
|
-
@options.except(:skip_cache, :dry_run)
|
|
701
|
+
@options.except(:skip_cache, :dry_run, :with)
|
|
238
702
|
end
|
|
239
703
|
|
|
240
|
-
#
|
|
241
|
-
#
|
|
242
|
-
#
|
|
704
|
+
# Returns options to pass to the ask method
|
|
705
|
+
#
|
|
706
|
+
# Currently supports :with for attachments (images, PDFs, etc.)
|
|
707
|
+
#
|
|
708
|
+
# @return [Hash] Options for the ask call
|
|
709
|
+
def ask_options
|
|
710
|
+
opts = {}
|
|
711
|
+
opts[:with] = @options[:with] if @options[:with]
|
|
712
|
+
opts
|
|
713
|
+
end
|
|
243
714
|
|
|
715
|
+
# Validates that all required parameters are present
|
|
716
|
+
#
|
|
717
|
+
# @raise [ArgumentError] If required parameters are missing
|
|
718
|
+
# @return [void]
|
|
244
719
|
def validate_required_params!
|
|
245
720
|
required = self.class.params.select { |_, v| v[:required] }.keys
|
|
246
721
|
missing = required.reject { |p| @options.key?(p) || @options.key?(p.to_s) }
|
|
247
722
|
raise ArgumentError, "#{self.class} missing required params: #{missing.join(', ')}" if missing.any?
|
|
248
723
|
end
|
|
249
724
|
|
|
250
|
-
#
|
|
251
|
-
#
|
|
252
|
-
#
|
|
253
|
-
|
|
725
|
+
# Builds and configures the RubyLLM client
|
|
726
|
+
#
|
|
727
|
+
# @return [RubyLLM::Chat] Configured chat client
|
|
254
728
|
def build_client
|
|
255
729
|
client = RubyLLM.chat
|
|
256
730
|
.with_model(model)
|
|
257
731
|
.with_temperature(temperature)
|
|
258
732
|
client = client.with_instructions(system_prompt) if system_prompt
|
|
259
733
|
client = client.with_schema(schema) if schema
|
|
734
|
+
client = client.with_tools(*self.class.tools) if self.class.tools.any?
|
|
260
735
|
client
|
|
261
736
|
end
|
|
262
737
|
|
|
263
|
-
#
|
|
738
|
+
# Builds a client with pre-populated conversation history
|
|
739
|
+
#
|
|
740
|
+
# Useful for multi-turn conversations or providing context.
|
|
741
|
+
#
|
|
742
|
+
# @param messages [Array<Hash>] Messages with :role and :content keys
|
|
743
|
+
# @return [RubyLLM::Chat] Client with messages added
|
|
744
|
+
# @example
|
|
745
|
+
# build_client_with_messages([
|
|
746
|
+
# { role: "user", content: "Hello" },
|
|
747
|
+
# { role: "assistant", content: "Hi there!" }
|
|
748
|
+
# ])
|
|
264
749
|
def build_client_with_messages(messages)
|
|
265
750
|
messages.reduce(build_client) do |client, message|
|
|
266
751
|
client.with_message(message[:role], message[:content])
|