ruby_llm-agents 3.12.0 → 3.14.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/README.md +1 -1
- data/app/controllers/ruby_llm/agents/analytics_controller.rb +8 -0
- data/app/controllers/ruby_llm/agents/executions_controller.rb +8 -2
- data/app/controllers/ruby_llm/agents/tenants_controller.rb +8 -2
- data/app/models/ruby_llm/agents/execution.rb +63 -3
- data/app/models/ruby_llm/agents/tenant.rb +30 -2
- data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +10 -6
- data/app/views/ruby_llm/agents/agents/show.html.erb +5 -4
- data/app/views/ruby_llm/agents/executions/_audio_player.html.erb +1 -1
- data/app/views/ruby_llm/agents/executions/_filters.html.erb +12 -8
- data/app/views/ruby_llm/agents/executions/show.html.erb +26 -12
- data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +46 -7
- data/app/views/ruby_llm/agents/shared/_tenant_filter.html.erb +2 -2
- data/app/views/ruby_llm/agents/system_config/show.html.erb +6 -2
- data/app/views/ruby_llm/agents/tenants/_form.html.erb +16 -7
- data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +27 -1
- data/lib/ruby_llm/agents/base_agent.rb +189 -21
- data/lib/ruby_llm/agents/core/configuration.rb +96 -6
- data/lib/ruby_llm/agents/core/llm_tenant.rb +40 -0
- data/lib/ruby_llm/agents/core/version.rb +1 -1
- data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +9 -5
- data/lib/ruby_llm/agents/infrastructure/execution_logger_job.rb +4 -2
- data/lib/ruby_llm/agents/infrastructure/retention_job.rb +118 -0
- data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +52 -1
- data/lib/ruby_llm/agents/rails/engine.rb +20 -4
- data/lib/ruby_llm/agents/routing.rb +28 -5
- data/lib/ruby_llm/agents.rb +1 -0
- data/lib/tasks/ruby_llm_agents.rake +7 -0
- metadata +4 -3
|
@@ -48,7 +48,7 @@ module RubyLLM
|
|
|
48
48
|
raised_exception = nil
|
|
49
49
|
|
|
50
50
|
begin
|
|
51
|
-
@app.call(context)
|
|
51
|
+
capture_llm_requests(context) { @app.call(context) }
|
|
52
52
|
context.completed_at = Time.current
|
|
53
53
|
|
|
54
54
|
begin
|
|
@@ -84,6 +84,55 @@ module RubyLLM
|
|
|
84
84
|
|
|
85
85
|
private
|
|
86
86
|
|
|
87
|
+
# Fiber-local stack of in-flight request accumulators, innermost last.
|
|
88
|
+
REQUEST_CAPTURE_STACK = :ruby_llm_agents_request_capture
|
|
89
|
+
|
|
90
|
+
# Captures real HTTP-level provider latency for the LLM call(s) made
|
|
91
|
+
# while running the rest of the pipeline.
|
|
92
|
+
#
|
|
93
|
+
# ruby_llm 1.16 emits a "request.ruby_llm" event per HTTP request and
|
|
94
|
+
# its Railtie wires ActiveSupport::Notifications as the instrumenter
|
|
95
|
+
# in Rails, so we subscribe for the duration of the downstream call
|
|
96
|
+
# and accumulate provider time and request count (retries/fallbacks
|
|
97
|
+
# add up). This is distinct from the total pipeline duration, which
|
|
98
|
+
# also includes middleware and tool execution. The values are stored
|
|
99
|
+
# in context metadata and persisted with the execution.
|
|
100
|
+
#
|
|
101
|
+
# AS::Notifications subscriptions are process-global, so a naive
|
|
102
|
+
# subscriber would also see events from other executions running
|
|
103
|
+
# concurrently (other threads) or nested inside this one (agent-as-
|
|
104
|
+
# tool). To attribute each request to exactly one execution, we keep
|
|
105
|
+
# a fiber-local stack of accumulators and only credit the innermost
|
|
106
|
+
# one on the thread that actually emitted the event — the callback
|
|
107
|
+
# runs synchronously on the emitting thread, so its top-of-stack is
|
|
108
|
+
# the execution whose LLM call fired.
|
|
109
|
+
#
|
|
110
|
+
# @param context [Context] The execution context
|
|
111
|
+
# @return [Object] The downstream call's return value
|
|
112
|
+
def capture_llm_requests(context)
|
|
113
|
+
return yield unless defined?(ActiveSupport::Notifications)
|
|
114
|
+
|
|
115
|
+
accumulator = {ms: 0.0, count: 0}
|
|
116
|
+
stack = (Thread.current[REQUEST_CAPTURE_STACK] ||= [])
|
|
117
|
+
stack.push(accumulator)
|
|
118
|
+
|
|
119
|
+
callback = lambda do |_name, started, finished, _id, _payload|
|
|
120
|
+
top = Thread.current[REQUEST_CAPTURE_STACK]&.last
|
|
121
|
+
next unless top.equal?(accumulator)
|
|
122
|
+
|
|
123
|
+
accumulator[:ms] += (finished - started) * 1000.0
|
|
124
|
+
accumulator[:count] += 1
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
ActiveSupport::Notifications.subscribed(callback, "request.ruby_llm") { yield }
|
|
128
|
+
ensure
|
|
129
|
+
stack&.pop
|
|
130
|
+
if accumulator && accumulator[:count].positive?
|
|
131
|
+
context[:llm_request_ms] = accumulator[:ms].round
|
|
132
|
+
context[:llm_request_count] = accumulator[:count]
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
87
136
|
# Creates initial execution record with 'running' status
|
|
88
137
|
#
|
|
89
138
|
# Creates the record synchronously so it appears on the dashboard immediately.
|
|
@@ -339,6 +388,8 @@ module RubyLLM
|
|
|
339
388
|
cache_hit: context.cached?,
|
|
340
389
|
input_tokens: context.input_tokens || 0,
|
|
341
390
|
output_tokens: context.output_tokens || 0,
|
|
391
|
+
input_cost: context.input_cost,
|
|
392
|
+
output_cost: context.output_cost,
|
|
342
393
|
total_cost: context.total_cost || 0,
|
|
343
394
|
attempts_count: context.attempts_made,
|
|
344
395
|
chosen_model_id: context.model_used,
|
|
@@ -33,6 +33,7 @@ module RubyLLM
|
|
|
33
33
|
# @api private
|
|
34
34
|
config.to_prepare do
|
|
35
35
|
require_relative "../infrastructure/execution_logger_job"
|
|
36
|
+
require_relative "../infrastructure/retention_job"
|
|
36
37
|
require_relative "../core/instrumentation"
|
|
37
38
|
require_relative "../core/base"
|
|
38
39
|
|
|
@@ -153,18 +154,33 @@ module RubyLLM
|
|
|
153
154
|
end
|
|
154
155
|
helper_method :tenant_scoped_executions
|
|
155
156
|
|
|
156
|
-
# Returns list of
|
|
157
|
+
# Returns the list of tenants for the dropdown as label/value pairs.
|
|
157
158
|
#
|
|
158
|
-
#
|
|
159
|
+
# Tenants that have a matching row in ruby_llm_agents_tenants get their
|
|
160
|
+
# configured name; legacy or string-only tenants fall back to the raw
|
|
161
|
+
# tenant_id so nothing disappears from the filter.
|
|
162
|
+
#
|
|
163
|
+
# Two queries total — one DISTINCT pluck on executions, one pluck on
|
|
164
|
+
# tenants — regardless of how many tenant_ids exist.
|
|
165
|
+
#
|
|
166
|
+
# @return [Array<Hash>] Entries shaped as { value:, label: }
|
|
159
167
|
# @api public
|
|
160
168
|
def available_tenants
|
|
161
169
|
return @available_tenants if defined?(@available_tenants)
|
|
162
170
|
|
|
163
|
-
|
|
171
|
+
tenant_ids = RubyLLM::Agents::Execution
|
|
164
172
|
.where.not(tenant_id: nil)
|
|
165
173
|
.distinct
|
|
166
174
|
.pluck(:tenant_id)
|
|
167
|
-
|
|
175
|
+
|
|
176
|
+
names_by_id = RubyLLM::Agents::Tenant
|
|
177
|
+
.where(tenant_id: tenant_ids)
|
|
178
|
+
.pluck(:tenant_id, :name)
|
|
179
|
+
.to_h
|
|
180
|
+
|
|
181
|
+
@available_tenants = tenant_ids
|
|
182
|
+
.map { |id| {value: id, label: (names_by_id[id].presence || id).to_s} }
|
|
183
|
+
.sort_by { |t| t[:label].downcase }
|
|
168
184
|
end
|
|
169
185
|
helper_method :available_tenants
|
|
170
186
|
end)
|
|
@@ -115,6 +115,15 @@ module RubyLLM
|
|
|
115
115
|
@ask_message || options[:message] || super
|
|
116
116
|
end
|
|
117
117
|
|
|
118
|
+
# Override call to capture the caller's stream block so it can be
|
|
119
|
+
# forwarded to the delegated agent. Without this, chunks from the
|
|
120
|
+
# delegated agent are swallowed because build_result has no access
|
|
121
|
+
# to the original block.
|
|
122
|
+
def call(&block)
|
|
123
|
+
@delegation_stream_block = block
|
|
124
|
+
super
|
|
125
|
+
end
|
|
126
|
+
|
|
118
127
|
# Override process_response to parse the route from LLM output.
|
|
119
128
|
def process_response(response)
|
|
120
129
|
raw = response.content.to_s.strip.downcase.gsub(/[^a-z0-9_]/, "")
|
|
@@ -131,25 +140,39 @@ module RubyLLM
|
|
|
131
140
|
end
|
|
132
141
|
|
|
133
142
|
# Override build_result to return a RoutingResult.
|
|
134
|
-
# Auto-delegates to the mapped agent when the route has an `agent:` mapping
|
|
143
|
+
# Auto-delegates to the mapped agent when the route has an `agent:` mapping,
|
|
144
|
+
# unless the caller opts out with `auto_delegate: false`.
|
|
135
145
|
def build_result(content, response, context)
|
|
136
146
|
base = super
|
|
137
147
|
|
|
138
|
-
# Auto-delegate to the mapped agent
|
|
139
148
|
agent_class = content[:agent_class]
|
|
140
|
-
if agent_class
|
|
141
|
-
content[:delegated_result] =
|
|
149
|
+
if agent_class && auto_delegate?
|
|
150
|
+
content[:delegated_result] = if @delegation_stream_block
|
|
151
|
+
agent_class.call(**delegation_params, &@delegation_stream_block)
|
|
152
|
+
else
|
|
153
|
+
agent_class.call(**delegation_params)
|
|
154
|
+
end
|
|
142
155
|
end
|
|
143
156
|
|
|
144
157
|
RoutingResult.new(base_result: base, route_data: content)
|
|
145
158
|
end
|
|
146
159
|
|
|
160
|
+
# Whether auto-delegation to the mapped agent is enabled for this call.
|
|
161
|
+
# Defaults to true. Pass `auto_delegate: false` to receive a
|
|
162
|
+
# classification-only RoutingResult with `delegated? == false` and
|
|
163
|
+
# `agent_class` set so the caller can invoke it manually.
|
|
164
|
+
#
|
|
165
|
+
# @return [Boolean]
|
|
166
|
+
def auto_delegate?
|
|
167
|
+
@options.fetch(:auto_delegate, true)
|
|
168
|
+
end
|
|
169
|
+
|
|
147
170
|
# Builds params to forward to the delegated agent.
|
|
148
171
|
# Forwards original message and custom params, excludes routing internals.
|
|
149
172
|
#
|
|
150
173
|
# @return [Hash] Params for the delegated agent
|
|
151
174
|
def delegation_params
|
|
152
|
-
forward = @options.except(:dry_run, :skip_cache, :debug, :stream_events)
|
|
175
|
+
forward = @options.except(:dry_run, :skip_cache, :debug, :stream_events, :auto_delegate)
|
|
153
176
|
forward[:_parent_execution_id] = @parent_execution_id if @parent_execution_id
|
|
154
177
|
forward[:_root_execution_id] = @root_execution_id if @root_execution_id
|
|
155
178
|
forward
|
data/lib/ruby_llm/agents.rb
CHANGED
|
@@ -96,6 +96,7 @@ if defined?(Rails)
|
|
|
96
96
|
require_relative "agents/core/inflections"
|
|
97
97
|
require_relative "agents/core/instrumentation"
|
|
98
98
|
require_relative "agents/infrastructure/execution_logger_job"
|
|
99
|
+
require_relative "agents/infrastructure/retention_job"
|
|
99
100
|
end
|
|
100
101
|
require_relative "agents/rails/engine" if defined?(Rails::Engine)
|
|
101
102
|
|
|
@@ -7,6 +7,13 @@ namespace :ruby_llm_agents do
|
|
|
7
7
|
RubyLlmAgents::DoctorGenerator.start([])
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
+
desc "Run the retention job synchronously (soft + hard purges per configuration)"
|
|
11
|
+
task purge: :environment do
|
|
12
|
+
result = RubyLLM::Agents::RetentionJob.new.perform
|
|
13
|
+
puts "Soft purged: #{result[:soft_purged]} executions (details destroyed)"
|
|
14
|
+
puts "Hard purged: #{result[:hard_purged]} executions (rows destroyed)"
|
|
15
|
+
end
|
|
16
|
+
|
|
10
17
|
desc "Rename an agent type in execution records. Usage: rake ruby_llm_agents:rename_agent FROM=OldName TO=NewName [DRY_RUN=1]"
|
|
11
18
|
task rename_agent: :environment do
|
|
12
19
|
from = ENV["FROM"]
|
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: 3.
|
|
4
|
+
version: 3.14.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- adham90
|
|
@@ -29,14 +29,14 @@ dependencies:
|
|
|
29
29
|
requirements:
|
|
30
30
|
- - ">="
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: 1.
|
|
32
|
+
version: 1.16.0
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: 1.
|
|
39
|
+
version: 1.16.0
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
41
|
name: csv
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -290,6 +290,7 @@ files:
|
|
|
290
290
|
- lib/ruby_llm/agents/infrastructure/circuit_breaker.rb
|
|
291
291
|
- lib/ruby_llm/agents/infrastructure/execution_logger_job.rb
|
|
292
292
|
- lib/ruby_llm/agents/infrastructure/reliability.rb
|
|
293
|
+
- lib/ruby_llm/agents/infrastructure/retention_job.rb
|
|
293
294
|
- lib/ruby_llm/agents/pipeline.rb
|
|
294
295
|
- lib/ruby_llm/agents/pipeline/builder.rb
|
|
295
296
|
- lib/ruby_llm/agents/pipeline/context.rb
|