ruby_llm-agents 3.10.0 → 3.12.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/controllers/ruby_llm/agents/agents_controller.rb +74 -0
- data/app/controllers/ruby_llm/agents/analytics_controller.rb +304 -0
- data/app/controllers/ruby_llm/agents/tenants_controller.rb +74 -2
- data/app/models/ruby_llm/agents/agent_override.rb +47 -0
- data/app/models/ruby_llm/agents/execution/analytics.rb +37 -16
- data/app/services/ruby_llm/agents/agent_registry.rb +8 -1
- data/app/views/layouts/ruby_llm/agents/application.html.erb +4 -2
- data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +89 -4
- data/app/views/ruby_llm/agents/agents/show.html.erb +14 -0
- data/app/views/ruby_llm/agents/analytics/index.html.erb +398 -0
- data/app/views/ruby_llm/agents/tenants/index.html.erb +3 -2
- data/app/views/ruby_llm/agents/tenants/show.html.erb +225 -0
- data/config/routes.rb +12 -4
- data/lib/generators/ruby_llm_agents/templates/create_overrides_migration.rb.tt +28 -0
- data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +1 -1
- data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +1 -1
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +14 -0
- data/lib/ruby_llm/agents/base_agent.rb +158 -37
- data/lib/ruby_llm/agents/core/base.rb +9 -0
- data/lib/ruby_llm/agents/core/configuration.rb +5 -1
- data/lib/ruby_llm/agents/core/version.rb +1 -1
- data/lib/ruby_llm/agents/dsl/base.rb +131 -4
- data/lib/ruby_llm/agents/dsl/knowledge.rb +157 -0
- data/lib/ruby_llm/agents/dsl.rb +1 -0
- data/lib/ruby_llm/agents/pipeline/context.rb +11 -2
- data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +32 -20
- data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +22 -1
- data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +1 -1
- data/lib/ruby_llm/agents/routing/result.rb +60 -9
- data/lib/ruby_llm/agents/routing.rb +19 -0
- data/lib/ruby_llm/agents/stream_event.rb +58 -0
- data/lib/ruby_llm/agents/tool.rb +1 -1
- data/lib/ruby_llm/agents.rb +2 -2
- metadata +7 -2
- data/lib/ruby_llm/agents/agent_tool.rb +0 -125
|
@@ -72,8 +72,13 @@ module RubyLLM
|
|
|
72
72
|
# @param context [Pipeline::Context] The execution context
|
|
73
73
|
# @return [void] Sets context.output with the result
|
|
74
74
|
def execute(context)
|
|
75
|
+
@context = context
|
|
75
76
|
@execution_started_at = context.started_at || Time.current
|
|
76
77
|
|
|
78
|
+
# Make context available to Tool instances during tool execution
|
|
79
|
+
previous_context = Thread.current[:ruby_llm_agents_caller_context]
|
|
80
|
+
Thread.current[:ruby_llm_agents_caller_context] = context
|
|
81
|
+
|
|
77
82
|
# Run before_call callbacks
|
|
78
83
|
run_callbacks(:before, context)
|
|
79
84
|
|
|
@@ -87,10 +92,14 @@ module RubyLLM
|
|
|
87
92
|
run_callbacks(:after, context, response)
|
|
88
93
|
|
|
89
94
|
context.output = build_result(processed_content, response, context)
|
|
95
|
+
rescue RubyLLM::Agents::CancelledError
|
|
96
|
+
context.output = Result.new(content: nil, cancelled: true)
|
|
90
97
|
rescue RubyLLM::UnauthorizedError, RubyLLM::ForbiddenError => e
|
|
91
98
|
raise_with_setup_hint(e, context)
|
|
92
99
|
rescue RubyLLM::ModelNotFoundError => e
|
|
93
100
|
raise_with_model_hint(e, context)
|
|
101
|
+
ensure
|
|
102
|
+
Thread.current[:ruby_llm_agents_caller_context] = previous_context
|
|
94
103
|
end
|
|
95
104
|
|
|
96
105
|
# Returns the resolved tenant ID for tracking
|
|
@@ -448,7 +448,8 @@ module RubyLLM
|
|
|
448
448
|
:helicone_pricing_enabled,
|
|
449
449
|
:helicone_pricing_url,
|
|
450
450
|
:llmpricing_enabled,
|
|
451
|
-
:llmpricing_url
|
|
451
|
+
:llmpricing_url,
|
|
452
|
+
:knowledge_path
|
|
452
453
|
|
|
453
454
|
# Attributes with validation (readers only, custom setters below)
|
|
454
455
|
attr_reader :default_temperature,
|
|
@@ -752,6 +753,9 @@ module RubyLLM
|
|
|
752
753
|
@elevenlabs_base_cost_per_1k = nil
|
|
753
754
|
# ElevenLabs models cache TTL in seconds (6 hours)
|
|
754
755
|
@elevenlabs_models_cache_ttl = 21_600
|
|
756
|
+
|
|
757
|
+
# Knowledge defaults
|
|
758
|
+
@knowledge_path = "app/agents/knowledge"
|
|
755
759
|
end
|
|
756
760
|
|
|
757
761
|
# Returns the configured cache store, falling back to Rails.cache
|
|
@@ -43,6 +43,13 @@ module RubyLLM
|
|
|
43
43
|
#
|
|
44
44
|
# RubyExpert.ask("What is metaprogramming?")
|
|
45
45
|
#
|
|
46
|
+
# @example Dashboard-overridable settings
|
|
47
|
+
# class SupportAgent < RubyLLM::Agents::BaseAgent
|
|
48
|
+
# model "gpt-4o", overridable: true # can be changed from the dashboard
|
|
49
|
+
# temperature 0.7, overridable: true # can be changed from the dashboard
|
|
50
|
+
# timeout 30 # locked to code value
|
|
51
|
+
# end
|
|
52
|
+
#
|
|
46
53
|
# @example Dynamic prompts with method overrides
|
|
47
54
|
# class SmartAgent < RubyLLM::Agents::BaseAgent
|
|
48
55
|
# def system_prompt
|
|
@@ -63,12 +70,18 @@ module RubyLLM
|
|
|
63
70
|
# Sets or returns the LLM model for this agent class
|
|
64
71
|
#
|
|
65
72
|
# @param value [String, nil] The model identifier to set
|
|
73
|
+
# @param overridable [Boolean, nil] When true, this field can be changed from the dashboard
|
|
66
74
|
# @return [String] The current model setting
|
|
67
75
|
# @example
|
|
68
76
|
# model "gpt-4o"
|
|
69
|
-
|
|
77
|
+
# @example Dashboard-overridable
|
|
78
|
+
# model "gpt-4o", overridable: true
|
|
79
|
+
def model(value = nil, overridable: nil)
|
|
70
80
|
@model = value if value
|
|
71
|
-
|
|
81
|
+
register_overridable(:model) if overridable
|
|
82
|
+
base = @model || inherited_or_default(:model, default_model)
|
|
83
|
+
|
|
84
|
+
apply_override(:model, base)
|
|
72
85
|
end
|
|
73
86
|
|
|
74
87
|
# Sets the user prompt template
|
|
@@ -206,12 +219,45 @@ module RubyLLM
|
|
|
206
219
|
# Sets or returns the timeout in seconds for LLM requests
|
|
207
220
|
#
|
|
208
221
|
# @param value [Integer, nil] Timeout in seconds
|
|
222
|
+
# @param overridable [Boolean, nil] When true, this field can be changed from the dashboard
|
|
209
223
|
# @return [Integer] The current timeout setting
|
|
210
224
|
# @example
|
|
211
225
|
# timeout 30
|
|
212
|
-
|
|
226
|
+
# @example Dashboard-overridable
|
|
227
|
+
# timeout 30, overridable: true
|
|
228
|
+
def timeout(value = nil, overridable: nil)
|
|
213
229
|
@timeout = value if value
|
|
214
|
-
|
|
230
|
+
register_overridable(:timeout) if overridable
|
|
231
|
+
base = @timeout || inherited_or_default(:timeout, default_timeout)
|
|
232
|
+
|
|
233
|
+
apply_override(:timeout, base)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Enables Anthropic prompt caching for this agent
|
|
237
|
+
#
|
|
238
|
+
# When enabled, adds cache_control breakpoints to the system prompt
|
|
239
|
+
# and the last tool definition so Anthropic caches them across
|
|
240
|
+
# multi-turn agent loops. This reduces input token costs by ~90%
|
|
241
|
+
# on subsequent calls within the same cache window (~5 minutes).
|
|
242
|
+
#
|
|
243
|
+
# Only takes effect when the resolved model is served by Anthropic.
|
|
244
|
+
# Non-Anthropic models silently ignore this setting.
|
|
245
|
+
#
|
|
246
|
+
# @param value [Boolean, nil] Whether to enable prompt caching
|
|
247
|
+
# @return [Boolean] The current setting
|
|
248
|
+
#
|
|
249
|
+
# @example
|
|
250
|
+
# class BuildAgent < ApplicationAgent
|
|
251
|
+
# cache_prompts true
|
|
252
|
+
# system "You are a build assistant."
|
|
253
|
+
# tools BuildTool, TestTool, DeployTool
|
|
254
|
+
# end
|
|
255
|
+
#
|
|
256
|
+
def cache_prompts(value = nil)
|
|
257
|
+
@cache_prompts = value unless value.nil?
|
|
258
|
+
return @cache_prompts if defined?(@cache_prompts) && !@cache_prompts.nil?
|
|
259
|
+
|
|
260
|
+
inherited_or_default(:cache_prompts, false)
|
|
215
261
|
end
|
|
216
262
|
|
|
217
263
|
# Sets or returns the response schema for structured output
|
|
@@ -259,6 +305,47 @@ module RubyLLM
|
|
|
259
305
|
|
|
260
306
|
# @!endgroup
|
|
261
307
|
|
|
308
|
+
# @!group Dashboard Override Support
|
|
309
|
+
|
|
310
|
+
# Returns which fields are overridable for this agent
|
|
311
|
+
#
|
|
312
|
+
# @return [Array<Symbol>] The list of overridable field names
|
|
313
|
+
def overridable_fields
|
|
314
|
+
own = @overridable_fields || []
|
|
315
|
+
inherited = superclass.respond_to?(:overridable_fields) ? superclass.overridable_fields : []
|
|
316
|
+
(own + inherited).uniq
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Returns true if any field is overridable from the dashboard
|
|
320
|
+
#
|
|
321
|
+
# @return [Boolean]
|
|
322
|
+
def overridable?
|
|
323
|
+
overridable_fields.any?
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Returns the currently active dashboard overrides for this agent
|
|
327
|
+
#
|
|
328
|
+
# Only returns overrides for fields that are declared overridable.
|
|
329
|
+
#
|
|
330
|
+
# @return [Hash{String => Object}] Active override values
|
|
331
|
+
def active_overrides
|
|
332
|
+
return {} unless overridable?
|
|
333
|
+
|
|
334
|
+
raw = load_overrides
|
|
335
|
+
raw.select { |field, _| overridable_fields.include?(field.to_sym) }
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Clears the in-memory override cache so the next access reloads from DB
|
|
339
|
+
#
|
|
340
|
+
# Called automatically by AgentOverride after_save/after_destroy callbacks.
|
|
341
|
+
#
|
|
342
|
+
# @return [void]
|
|
343
|
+
def clear_override_cache!
|
|
344
|
+
@_override_cache = nil
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# @!endgroup
|
|
348
|
+
|
|
262
349
|
private
|
|
263
350
|
|
|
264
351
|
# Auto-registers parameters found in prompt template placeholders
|
|
@@ -310,6 +397,46 @@ module RubyLLM
|
|
|
310
397
|
rescue
|
|
311
398
|
120
|
|
312
399
|
end
|
|
400
|
+
|
|
401
|
+
# Registers a field as overridable from the dashboard
|
|
402
|
+
#
|
|
403
|
+
# @param field [Symbol] The field name
|
|
404
|
+
# @return [void]
|
|
405
|
+
def register_overridable(field)
|
|
406
|
+
@overridable_fields = (@overridable_fields || []) | [field]
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# Applies a dashboard override if the field is overridable and an override exists
|
|
410
|
+
#
|
|
411
|
+
# @param field [Symbol] The field name
|
|
412
|
+
# @param base [Object] The code-defined value to use as fallback
|
|
413
|
+
# @return [Object] The override value, or the base value
|
|
414
|
+
def apply_override(field, base)
|
|
415
|
+
return base unless overridable_fields.include?(field)
|
|
416
|
+
|
|
417
|
+
override = resolve_override(field)
|
|
418
|
+
override.nil? ? base : override
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# Fetches the override value for a single field from the cached override hash
|
|
422
|
+
#
|
|
423
|
+
# @param field [Symbol] The field name
|
|
424
|
+
# @return [Object, nil] The override value, or nil
|
|
425
|
+
def resolve_override(field)
|
|
426
|
+
@_override_cache = load_overrides unless defined?(@_override_cache) && @_override_cache
|
|
427
|
+
@_override_cache[field.to_s]
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
# Loads all overrides for this agent from the database
|
|
431
|
+
#
|
|
432
|
+
# @return [Hash{String => Object}] The override settings hash
|
|
433
|
+
def load_overrides
|
|
434
|
+
return {} unless defined?(RubyLLM::Agents::AgentOverride)
|
|
435
|
+
|
|
436
|
+
RubyLLM::Agents::AgentOverride.find_by(agent_type: name)&.settings || {}
|
|
437
|
+
rescue
|
|
438
|
+
{}
|
|
439
|
+
end
|
|
313
440
|
end
|
|
314
441
|
end
|
|
315
442
|
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
module DSL
|
|
6
|
+
# Knowledge DSL for declaring domain knowledge to inject into system prompts.
|
|
7
|
+
#
|
|
8
|
+
# Supports two modes:
|
|
9
|
+
# - Static: `knows :name` loads from a file resolved via `knowledge_path`
|
|
10
|
+
# - Dynamic: `knows(:name) { ... }` evaluates a block at call time via instance_exec
|
|
11
|
+
#
|
|
12
|
+
# Optional `if:` condition gates inclusion without forcing static content into blocks.
|
|
13
|
+
#
|
|
14
|
+
# @example Static knowledge from files
|
|
15
|
+
# class MyAgent < RubyLLM::Agents::Base
|
|
16
|
+
# knowledge_path "knowledge"
|
|
17
|
+
# knows :refund_policy
|
|
18
|
+
# knows :pricing
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# @example Dynamic knowledge
|
|
22
|
+
# class MyAgent < RubyLLM::Agents::Base
|
|
23
|
+
# knows :recent_tickets do
|
|
24
|
+
# Ticket.recent.pluck(:summary)
|
|
25
|
+
# end
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# @example Conditional knowledge
|
|
29
|
+
# class MyAgent < RubyLLM::Agents::Base
|
|
30
|
+
# param :region, required: true
|
|
31
|
+
# knows :hipaa, if: -> { region == "us" }
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
module Knowledge
|
|
35
|
+
# Registers one or more knowledge entries.
|
|
36
|
+
#
|
|
37
|
+
# @overload knows(name, **options, &block)
|
|
38
|
+
# Register a single entry (static or dynamic)
|
|
39
|
+
# @param name [Symbol] Identifier for this knowledge entry
|
|
40
|
+
# @param options [Hash] Options including `if:` condition lambda
|
|
41
|
+
# @param block [Proc] Optional block for dynamic knowledge (evaluated via instance_exec)
|
|
42
|
+
#
|
|
43
|
+
# @overload knows(name, *more_names)
|
|
44
|
+
# Register multiple static entries at once
|
|
45
|
+
# @param name [Symbol] First entry name
|
|
46
|
+
# @param more_names [Array<Symbol>] Additional entry names
|
|
47
|
+
def knows(name, *more_names, **options, &block)
|
|
48
|
+
if more_names.any?
|
|
49
|
+
# Multi-arg form: knows :a, :b, :c — all static, no block/options
|
|
50
|
+
[name, *more_names].each do |n|
|
|
51
|
+
knowledge_entries.reject! { |e| e[:name] == n }
|
|
52
|
+
knowledge_entries << {name: n, loader: nil, options: {}}
|
|
53
|
+
end
|
|
54
|
+
else
|
|
55
|
+
knowledge_entries.reject! { |e| e[:name] == name }
|
|
56
|
+
knowledge_entries << {
|
|
57
|
+
name: name,
|
|
58
|
+
loader: block,
|
|
59
|
+
options: options
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Sets or returns the base path for static knowledge files.
|
|
65
|
+
#
|
|
66
|
+
# @param path [String, nil] Path to set, or nil to read
|
|
67
|
+
# @return [String, nil] The resolved knowledge path
|
|
68
|
+
def knowledge_path(path = nil)
|
|
69
|
+
if path
|
|
70
|
+
@knowledge_path = path
|
|
71
|
+
else
|
|
72
|
+
@knowledge_path ||
|
|
73
|
+
inherited_or_default(:knowledge_path, nil) ||
|
|
74
|
+
RubyLLM::Agents.configuration.knowledge_path
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Returns the list of registered knowledge entries, inheriting from superclass.
|
|
79
|
+
#
|
|
80
|
+
# @return [Array<Hash>] Knowledge entries
|
|
81
|
+
def knowledge_entries
|
|
82
|
+
@knowledge_entries ||= if superclass.respond_to?(:knowledge_entries)
|
|
83
|
+
superclass.knowledge_entries.dup
|
|
84
|
+
else
|
|
85
|
+
[]
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Instance methods mixed into agent instances via include.
|
|
90
|
+
module InstanceMethods
|
|
91
|
+
# Compiles all knowledge entries into a single string with headings and separators.
|
|
92
|
+
#
|
|
93
|
+
# @return [String] Compiled knowledge (empty string if no entries resolve)
|
|
94
|
+
def compiled_knowledge
|
|
95
|
+
self.class.knowledge_entries.filter_map { |entry|
|
|
96
|
+
content = resolve_knowledge(entry)
|
|
97
|
+
next if content.blank?
|
|
98
|
+
|
|
99
|
+
heading = entry[:name].to_s.tr("_", " ").gsub(/\b\w/, &:upcase)
|
|
100
|
+
"## #{heading}\n\n#{content}"
|
|
101
|
+
}.join("\n\n---\n\n")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def resolve_knowledge(entry)
|
|
107
|
+
if (condition = entry[:options][:if])
|
|
108
|
+
return nil unless instance_exec(&condition)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
if entry[:loader]
|
|
112
|
+
resolve_dynamic_knowledge(entry)
|
|
113
|
+
else
|
|
114
|
+
resolve_static_knowledge(entry)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def resolve_dynamic_knowledge(entry)
|
|
119
|
+
result = instance_exec(&entry[:loader])
|
|
120
|
+
case result
|
|
121
|
+
when Array
|
|
122
|
+
result.map { |r| "- #{r}" }.join("\n")
|
|
123
|
+
when String
|
|
124
|
+
result
|
|
125
|
+
when nil
|
|
126
|
+
nil
|
|
127
|
+
else
|
|
128
|
+
result.to_s
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def resolve_static_knowledge(entry)
|
|
133
|
+
path = find_knowledge_file(entry[:name])
|
|
134
|
+
return nil unless path && File.exist?(path)
|
|
135
|
+
File.read(path)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def find_knowledge_file(name)
|
|
139
|
+
base_path = self.class.knowledge_path
|
|
140
|
+
return nil unless base_path
|
|
141
|
+
|
|
142
|
+
candidates = [
|
|
143
|
+
File.join(base_path, "#{name}.md"),
|
|
144
|
+
File.join(base_path, name.to_s)
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
|
|
148
|
+
candidates.map! { |c| Rails.root.join(c).to_s }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
candidates.find { |c| File.exist?(c) }
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
data/lib/ruby_llm/agents/dsl.rb
CHANGED
|
@@ -50,7 +50,7 @@ module RubyLLM
|
|
|
50
50
|
attr_accessor :trace
|
|
51
51
|
|
|
52
52
|
# Streaming support
|
|
53
|
-
attr_accessor :stream_block, :skip_cache
|
|
53
|
+
attr_accessor :stream_block, :skip_cache, :stream_events
|
|
54
54
|
|
|
55
55
|
# Agent metadata
|
|
56
56
|
attr_reader :agent_class, :agent_type
|
|
@@ -65,7 +65,7 @@ module RubyLLM
|
|
|
65
65
|
# @param skip_cache [Boolean] Whether to skip caching
|
|
66
66
|
# @param stream_block [Proc, nil] Block for streaming
|
|
67
67
|
# @param options [Hash] Additional options passed to the agent
|
|
68
|
-
def initialize(input:, agent_class:, agent_instance: nil, model: nil, tenant: nil, skip_cache: false, stream_block: nil, parent_execution_id: nil, root_execution_id: nil, **options)
|
|
68
|
+
def initialize(input:, agent_class:, agent_instance: nil, model: nil, tenant: nil, skip_cache: false, stream_block: nil, stream_events: false, parent_execution_id: nil, root_execution_id: nil, **options)
|
|
69
69
|
@input = input
|
|
70
70
|
@agent_class = agent_class
|
|
71
71
|
@agent_instance = agent_instance
|
|
@@ -87,6 +87,7 @@ module RubyLLM
|
|
|
87
87
|
# Execution options
|
|
88
88
|
@skip_cache = skip_cache
|
|
89
89
|
@stream_block = stream_block
|
|
90
|
+
@stream_events = stream_events
|
|
90
91
|
|
|
91
92
|
# Debug trace
|
|
92
93
|
@trace = []
|
|
@@ -132,6 +133,13 @@ module RubyLLM
|
|
|
132
133
|
@trace << {middleware: middleware_name, started_at: started_at, duration_ms: duration_ms, action: action}.compact
|
|
133
134
|
end
|
|
134
135
|
|
|
136
|
+
# Are stream events enabled?
|
|
137
|
+
#
|
|
138
|
+
# @return [Boolean]
|
|
139
|
+
def stream_events?
|
|
140
|
+
@stream_events == true
|
|
141
|
+
end
|
|
142
|
+
|
|
135
143
|
# Was the result served from cache?
|
|
136
144
|
#
|
|
137
145
|
# @return [Boolean]
|
|
@@ -243,6 +251,7 @@ module RubyLLM
|
|
|
243
251
|
model: @model,
|
|
244
252
|
skip_cache: @skip_cache,
|
|
245
253
|
stream_block: @stream_block,
|
|
254
|
+
stream_events: @stream_events,
|
|
246
255
|
**opts_without_tenant
|
|
247
256
|
)
|
|
248
257
|
# Preserve resolved tenant state
|
|
@@ -33,15 +33,18 @@ module RubyLLM
|
|
|
33
33
|
return @app.call(context) unless budgets_enabled?
|
|
34
34
|
|
|
35
35
|
trace(context) do
|
|
36
|
+
# Resolve tenant once for both check and record
|
|
37
|
+
tenant = resolve_tenant(context)
|
|
38
|
+
|
|
36
39
|
# Check budget before execution
|
|
37
|
-
check_budget!(context)
|
|
40
|
+
check_budget!(context, tenant)
|
|
38
41
|
|
|
39
42
|
# Execute the chain
|
|
40
43
|
@app.call(context)
|
|
41
44
|
|
|
42
45
|
# Record spend after successful execution (if not cached)
|
|
43
46
|
if context.success? && !context.cached?
|
|
44
|
-
record_spend!(context)
|
|
47
|
+
record_spend!(context, tenant)
|
|
45
48
|
emit_budget_notification("ruby_llm_agents.budget.record", context,
|
|
46
49
|
total_cost: context.total_cost,
|
|
47
50
|
total_tokens: context.total_tokens)
|
|
@@ -80,22 +83,33 @@ module RubyLLM
|
|
|
80
83
|
false
|
|
81
84
|
end
|
|
82
85
|
|
|
86
|
+
# Resolves the tenant record once for reuse across check and record
|
|
87
|
+
#
|
|
88
|
+
# @param context [Context] The execution context
|
|
89
|
+
# @return [Tenant, nil] The tenant record or nil
|
|
90
|
+
def resolve_tenant(context)
|
|
91
|
+
return nil unless context.tenant_id.present?
|
|
92
|
+
|
|
93
|
+
RubyLLM::Agents::Tenant.find_by(tenant_id: context.tenant_id)
|
|
94
|
+
rescue => e
|
|
95
|
+
debug("Tenant lookup failed: #{e.message}", context)
|
|
96
|
+
nil
|
|
97
|
+
end
|
|
98
|
+
|
|
83
99
|
# Checks budget before execution
|
|
84
100
|
#
|
|
85
101
|
# For tenants, checks budget via counter columns on the tenant model.
|
|
86
102
|
# For non-tenant usage, falls back to BudgetTracker (cache-based).
|
|
87
103
|
#
|
|
88
104
|
# @param context [Context] The execution context
|
|
105
|
+
# @param tenant [Tenant, nil] Pre-resolved tenant record
|
|
89
106
|
# @raise [BudgetExceededError] If budget exceeded with hard enforcement
|
|
90
|
-
def check_budget!(context)
|
|
107
|
+
def check_budget!(context, tenant = nil)
|
|
91
108
|
emit_budget_notification("ruby_llm_agents.budget.check", context)
|
|
92
109
|
|
|
93
|
-
if
|
|
94
|
-
tenant
|
|
95
|
-
|
|
96
|
-
tenant.check_budget!(context.agent_class&.name)
|
|
97
|
-
return
|
|
98
|
-
end
|
|
110
|
+
if tenant
|
|
111
|
+
tenant.check_budget!(context.agent_class&.name)
|
|
112
|
+
return
|
|
99
113
|
end
|
|
100
114
|
|
|
101
115
|
# Fallback to cache-based checking (non-tenant or no tenant record)
|
|
@@ -117,17 +131,15 @@ module RubyLLM
|
|
|
117
131
|
# For non-tenant usage, falls back to BudgetTracker (cache-based).
|
|
118
132
|
#
|
|
119
133
|
# @param context [Context] The execution context
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
return
|
|
130
|
-
end
|
|
134
|
+
# @param tenant [Tenant, nil] Pre-resolved tenant record
|
|
135
|
+
def record_spend!(context, tenant = nil)
|
|
136
|
+
if tenant
|
|
137
|
+
tenant.record_execution!(
|
|
138
|
+
cost: context.total_cost || 0,
|
|
139
|
+
tokens: context.total_tokens || 0,
|
|
140
|
+
error: context.failed?
|
|
141
|
+
)
|
|
142
|
+
return
|
|
131
143
|
end
|
|
132
144
|
|
|
133
145
|
# Fallback for non-tenant usage
|
|
@@ -278,6 +278,7 @@ module RubyLLM
|
|
|
278
278
|
data = {
|
|
279
279
|
agent_type: context.agent_class&.name,
|
|
280
280
|
model_id: context.model,
|
|
281
|
+
model_provider: resolve_model_provider(context.model),
|
|
281
282
|
status: "running",
|
|
282
283
|
started_at: context.started_at,
|
|
283
284
|
input_tokens: 0,
|
|
@@ -339,7 +340,9 @@ module RubyLLM
|
|
|
339
340
|
input_tokens: context.input_tokens || 0,
|
|
340
341
|
output_tokens: context.output_tokens || 0,
|
|
341
342
|
total_cost: context.total_cost || 0,
|
|
342
|
-
attempts_count: context.attempts_made
|
|
343
|
+
attempts_count: context.attempts_made,
|
|
344
|
+
chosen_model_id: context.model_used,
|
|
345
|
+
finish_reason: context.finish_reason
|
|
343
346
|
}
|
|
344
347
|
|
|
345
348
|
# Merge metadata: agent metadata (base) < middleware metadata (overlay)
|
|
@@ -536,6 +539,24 @@ module RubyLLM
|
|
|
536
539
|
{}
|
|
537
540
|
end
|
|
538
541
|
|
|
542
|
+
# Resolves the provider name for a given model ID
|
|
543
|
+
#
|
|
544
|
+
# Uses RubyLLM::Models.find which is an in-process registry lookup
|
|
545
|
+
# (no API keys or network calls needed).
|
|
546
|
+
#
|
|
547
|
+
# @param model_id [String, nil] The model identifier
|
|
548
|
+
# @return [String, nil] Provider name (e.g., "openai", "anthropic") or nil
|
|
549
|
+
def resolve_model_provider(model_id)
|
|
550
|
+
return nil unless model_id
|
|
551
|
+
return nil unless defined?(RubyLLM::Models)
|
|
552
|
+
|
|
553
|
+
model_info = RubyLLM::Models.find(model_id)
|
|
554
|
+
provider = model_info&.provider
|
|
555
|
+
provider&.to_s.presence
|
|
556
|
+
rescue
|
|
557
|
+
nil
|
|
558
|
+
end
|
|
559
|
+
|
|
539
560
|
# Injects tracker request_id and tags into execution data
|
|
540
561
|
#
|
|
541
562
|
# Reads @_track_request_id and @_track_tags from the agent instance,
|
|
@@ -223,7 +223,7 @@ module RubyLLM
|
|
|
223
223
|
return unless deadline && Time.current > deadline
|
|
224
224
|
|
|
225
225
|
elapsed = Time.current - started_at
|
|
226
|
-
timeout_value = deadline - started_at
|
|
226
|
+
timeout_value = deadline - started_at
|
|
227
227
|
raise Agents::Reliability::TotalTimeoutError.new(timeout_value, elapsed)
|
|
228
228
|
end
|
|
229
229
|
|