ruby_llm-agents 3.11.0 → 3.13.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/executions_controller.rb +5 -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/models/ruby_llm/agents/execution.rb +51 -1
- 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 +93 -4
- data/app/views/ruby_llm/agents/agents/show.html.erb +17 -2
- data/app/views/ruby_llm/agents/analytics/index.html.erb +398 -0
- 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/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/initializer.rb.tt +27 -1
- 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 +90 -133
- data/lib/ruby_llm/agents/core/base.rb +9 -0
- data/lib/ruby_llm/agents/core/configuration.rb +93 -7
- 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 -1
- data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +9 -5
- data/lib/ruby_llm/agents/infrastructure/retention_job.rb +118 -0
- 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/rails/engine.rb +20 -4
- data/lib/ruby_llm/agents/routing.rb +28 -5
- data/lib/ruby_llm/agents/stream_event.rb +2 -10
- data/lib/ruby_llm/agents/tool.rb +1 -1
- data/lib/ruby_llm/agents.rb +1 -3
- data/lib/tasks/ruby_llm_agents.rake +7 -0
- metadata +9 -5
- data/lib/ruby_llm/agents/agent_tool.rb +0 -143
- data/lib/ruby_llm/agents/dsl/agents.rb +0 -141
|
@@ -47,7 +47,8 @@ module RubyLLM
|
|
|
47
47
|
extend DSL::Reliability
|
|
48
48
|
extend DSL::Caching
|
|
49
49
|
extend DSL::Queryable
|
|
50
|
-
extend DSL::
|
|
50
|
+
extend DSL::Knowledge
|
|
51
|
+
include DSL::Knowledge::InstanceMethods
|
|
51
52
|
include CacheHelper
|
|
52
53
|
|
|
53
54
|
class << self
|
|
@@ -169,9 +170,9 @@ module RubyLLM
|
|
|
169
170
|
description: description,
|
|
170
171
|
schema: schema&.respond_to?(:name) ? schema.name : schema&.class&.name,
|
|
171
172
|
tools: tools.map { |t| t.respond_to?(:name) ? t.name : t.to_s },
|
|
172
|
-
agents: agents_config.agent_entries.map { |e| e[:agent_class].name },
|
|
173
173
|
parameters: params.transform_values { |v| v.slice(:type, :required, :default, :desc) },
|
|
174
174
|
thinking: thinking_config,
|
|
175
|
+
cache_prompts: cache_prompts || nil,
|
|
175
176
|
caching: caching_config,
|
|
176
177
|
reliability: reliability_configured? ? reliability_config : nil
|
|
177
178
|
}.compact
|
|
@@ -234,12 +235,18 @@ module RubyLLM
|
|
|
234
235
|
# Enables or returns streaming mode for this agent
|
|
235
236
|
#
|
|
236
237
|
# @param value [Boolean, nil] Whether to enable streaming
|
|
238
|
+
# @param overridable [Boolean, nil] When true, this field can be changed from the dashboard
|
|
237
239
|
# @return [Boolean] The current streaming setting
|
|
238
|
-
def streaming(value = nil)
|
|
240
|
+
def streaming(value = nil, overridable: nil)
|
|
239
241
|
@streaming = value unless value.nil?
|
|
240
|
-
|
|
242
|
+
register_overridable(:streaming) if overridable
|
|
243
|
+
base = if @streaming.nil?
|
|
244
|
+
superclass.respond_to?(:streaming) ? superclass.streaming : default_streaming
|
|
245
|
+
else
|
|
246
|
+
@streaming
|
|
247
|
+
end
|
|
241
248
|
|
|
242
|
-
|
|
249
|
+
apply_override(:streaming, base)
|
|
243
250
|
end
|
|
244
251
|
|
|
245
252
|
# @!endgroup
|
|
@@ -248,10 +255,10 @@ module RubyLLM
|
|
|
248
255
|
|
|
249
256
|
# Sets or returns the tools available to this agent
|
|
250
257
|
#
|
|
251
|
-
# @param tool_classes [Array<Class>] Tool classes to make available
|
|
258
|
+
# @param tool_classes [Class, Array<Class>] Tool classes to make available
|
|
252
259
|
# @return [Array<Class>] The current tools
|
|
253
|
-
def tools(tool_classes
|
|
254
|
-
@tools =
|
|
260
|
+
def tools(*tool_classes)
|
|
261
|
+
@tools = tool_classes.flatten if tool_classes.any?
|
|
255
262
|
@tools || (superclass.respond_to?(:tools) ? superclass.tools : [])
|
|
256
263
|
end
|
|
257
264
|
|
|
@@ -262,10 +269,14 @@ module RubyLLM
|
|
|
262
269
|
# Sets or returns the temperature for LLM responses
|
|
263
270
|
#
|
|
264
271
|
# @param value [Float, nil] Temperature value (0.0-2.0)
|
|
272
|
+
# @param overridable [Boolean, nil] When true, this field can be changed from the dashboard
|
|
265
273
|
# @return [Float] The current temperature setting
|
|
266
|
-
def temperature(value = nil)
|
|
274
|
+
def temperature(value = nil, overridable: nil)
|
|
267
275
|
@temperature = value if value
|
|
268
|
-
|
|
276
|
+
register_overridable(:temperature) if overridable
|
|
277
|
+
base = @temperature || (superclass.respond_to?(:temperature) ? superclass.temperature : default_temperature)
|
|
278
|
+
|
|
279
|
+
apply_override(:temperature, base)
|
|
269
280
|
end
|
|
270
281
|
|
|
271
282
|
# @!endgroup
|
|
@@ -389,23 +400,19 @@ module RubyLLM
|
|
|
389
400
|
# System prompt for LLM instructions
|
|
390
401
|
#
|
|
391
402
|
# If a class-level `system` DSL is defined, it will be used.
|
|
392
|
-
#
|
|
393
|
-
# direct tools and agent delegates are appended.
|
|
403
|
+
# Knowledge entries declared via `knows` are auto-appended.
|
|
394
404
|
#
|
|
395
405
|
# @return [String, nil] System instructions, or nil for none
|
|
396
406
|
def system_prompt
|
|
397
|
-
base = base_system_prompt
|
|
398
|
-
append_agents_system_prompt(base)
|
|
399
|
-
end
|
|
400
|
-
|
|
401
|
-
# Returns the raw system prompt without agent/tool sections
|
|
402
|
-
#
|
|
403
|
-
# @return [String, nil]
|
|
404
|
-
def base_system_prompt
|
|
405
407
|
system_config = self.class.system_config
|
|
406
|
-
|
|
408
|
+
base = system_config ? resolve_prompt_from_config(system_config) : nil
|
|
407
409
|
|
|
408
|
-
|
|
410
|
+
knowledge = compiled_knowledge
|
|
411
|
+
if knowledge.present?
|
|
412
|
+
base ? "#{base}\n\n#{knowledge}" : knowledge
|
|
413
|
+
else
|
|
414
|
+
base
|
|
415
|
+
end
|
|
409
416
|
end
|
|
410
417
|
|
|
411
418
|
# Assistant prefill to prime the model's response
|
|
@@ -564,41 +571,6 @@ module RubyLLM
|
|
|
564
571
|
end
|
|
565
572
|
end
|
|
566
573
|
|
|
567
|
-
# Appends auto-generated agent/tool sections to the system prompt
|
|
568
|
-
# when agents are declared via the `agents` DSL.
|
|
569
|
-
#
|
|
570
|
-
# @param base [String, nil] The base system prompt
|
|
571
|
-
# @return [String, nil] System prompt with agent sections appended
|
|
572
|
-
def append_agents_system_prompt(base)
|
|
573
|
-
config = self.class.agents_config
|
|
574
|
-
return base if config.agent_entries.empty?
|
|
575
|
-
|
|
576
|
-
all_tools = resolved_tools
|
|
577
|
-
tools = all_tools.reject { |t| t.respond_to?(:agent_delegate?) && t.agent_delegate? }
|
|
578
|
-
agents = all_tools.select { |t| t.respond_to?(:agent_delegate?) && t.agent_delegate? }
|
|
579
|
-
|
|
580
|
-
sections = [base].compact
|
|
581
|
-
|
|
582
|
-
if tools.any?
|
|
583
|
-
sections << "\n## Direct Tools"
|
|
584
|
-
sections << "Fast, local operations:\n"
|
|
585
|
-
sections << tools.map { |t| "- #{tool_name_for(t)}: #{tool_description_for(t)}" }.join("\n")
|
|
586
|
-
end
|
|
587
|
-
|
|
588
|
-
if agents.any?
|
|
589
|
-
sections << "\n## Agents"
|
|
590
|
-
sections << "Specialized AI agents for substantial tasks."
|
|
591
|
-
sections << "Multiple agents can work simultaneously when called in the same turn.\n"
|
|
592
|
-
sections << agents.map { |t| "- #{tool_name_for(t)}: #{tool_description_for(t)}" }.join("\n")
|
|
593
|
-
end
|
|
594
|
-
|
|
595
|
-
if config.instructions_text
|
|
596
|
-
sections << "\n#{config.instructions_text}"
|
|
597
|
-
end
|
|
598
|
-
|
|
599
|
-
sections.join("\n")
|
|
600
|
-
end
|
|
601
|
-
|
|
602
574
|
# Returns the description for a tool class
|
|
603
575
|
#
|
|
604
576
|
# @param tool [Class] A tool class
|
|
@@ -615,58 +587,19 @@ module RubyLLM
|
|
|
615
587
|
|
|
616
588
|
# Resolves tools for this execution
|
|
617
589
|
#
|
|
618
|
-
# Agent classes in the tools list are automatically wrapped as
|
|
619
|
-
# RubyLLM::Tool subclasses via AgentTool.for. Regular tool classes
|
|
620
|
-
# pass through unchanged.
|
|
621
|
-
#
|
|
622
590
|
# @return [Array<Class>] Tool classes to use
|
|
623
591
|
# @raise [ArgumentError] If duplicate tool names are detected
|
|
624
592
|
def resolved_tools
|
|
625
|
-
|
|
593
|
+
all_tools = if self.class.method_defined?(:tools, false)
|
|
626
594
|
tools
|
|
627
595
|
else
|
|
628
596
|
self.class.tools
|
|
629
597
|
end
|
|
630
598
|
|
|
631
|
-
regular_tools = raw.map { |tool_class| wrap_if_agent(tool_class) }
|
|
632
|
-
agent_tools = resolve_agent_list
|
|
633
|
-
|
|
634
|
-
all_tools = regular_tools + agent_tools
|
|
635
599
|
detect_duplicate_tool_names!(all_tools)
|
|
636
600
|
all_tools
|
|
637
601
|
end
|
|
638
602
|
|
|
639
|
-
# Wraps agent classes from the `agents` DSL as AgentTool instances
|
|
640
|
-
# with `agent_delegate?` marker, forwarded params, and description overrides.
|
|
641
|
-
#
|
|
642
|
-
# @return [Array<Class>] Wrapped agent tool classes
|
|
643
|
-
def resolve_agent_list
|
|
644
|
-
config = self.class.agents_config
|
|
645
|
-
return [] if config.agent_entries.empty?
|
|
646
|
-
|
|
647
|
-
config.agent_entries.map do |entry|
|
|
648
|
-
agent_class = entry[:agent_class]
|
|
649
|
-
AgentTool.for(
|
|
650
|
-
agent_class,
|
|
651
|
-
forwarded_params: config.forwarded_params,
|
|
652
|
-
description_override: config.description_for(agent_class),
|
|
653
|
-
delegate: true
|
|
654
|
-
)
|
|
655
|
-
end
|
|
656
|
-
end
|
|
657
|
-
|
|
658
|
-
# Wraps an agent class as a tool, or returns the tool class as-is.
|
|
659
|
-
#
|
|
660
|
-
# @param tool_class [Class] A tool or agent class
|
|
661
|
-
# @return [Class] The original or wrapped class
|
|
662
|
-
def wrap_if_agent(tool_class)
|
|
663
|
-
if tool_class.respond_to?(:ancestors) && tool_class.ancestors.include?(RubyLLM::Agents::BaseAgent)
|
|
664
|
-
AgentTool.for(tool_class)
|
|
665
|
-
else
|
|
666
|
-
tool_class
|
|
667
|
-
end
|
|
668
|
-
end
|
|
669
|
-
|
|
670
603
|
# Raises if two tools resolve to the same name.
|
|
671
604
|
#
|
|
672
605
|
# @param tools [Array] Resolved tool classes or instances
|
|
@@ -806,7 +739,7 @@ module RubyLLM
|
|
|
806
739
|
@context = context
|
|
807
740
|
client = build_client(context)
|
|
808
741
|
|
|
809
|
-
# Make context available to
|
|
742
|
+
# Make context available to Tool instances during tool execution
|
|
810
743
|
previous_context = Thread.current[:ruby_llm_agents_caller_context]
|
|
811
744
|
Thread.current[:ruby_llm_agents_caller_context] = context
|
|
812
745
|
|
|
@@ -843,9 +776,20 @@ module RubyLLM
|
|
|
843
776
|
end
|
|
844
777
|
client = client.with_temperature(temperature)
|
|
845
778
|
|
|
846
|
-
|
|
779
|
+
use_prompt_caching = self.class.cache_prompts && anthropic_model?(effective_model)
|
|
780
|
+
|
|
781
|
+
if system_prompt
|
|
782
|
+
sys_content = if use_prompt_caching
|
|
783
|
+
RubyLLM::Providers::Anthropic::Content.new(system_prompt, cache: true)
|
|
784
|
+
else
|
|
785
|
+
system_prompt
|
|
786
|
+
end
|
|
787
|
+
client = client.with_instructions(sys_content)
|
|
788
|
+
end
|
|
789
|
+
|
|
847
790
|
client = client.with_schema(schema) if schema
|
|
848
791
|
client = client.with_tools(*resolved_tools) if resolved_tools.any?
|
|
792
|
+
apply_tool_prompt_caching(client) if use_prompt_caching && resolved_tools.any?
|
|
849
793
|
client = setup_tool_tracking(client) if resolved_tools.any?
|
|
850
794
|
client = apply_messages(client, resolved_messages) if resolved_messages.any?
|
|
851
795
|
client = client.with_thinking(**resolved_thinking) if resolved_thinking
|
|
@@ -959,6 +903,14 @@ module RubyLLM
|
|
|
959
903
|
# Store tracked tool calls in context for instrumentation
|
|
960
904
|
context[:tool_calls] = @tracked_tool_calls if @tracked_tool_calls.any?
|
|
961
905
|
|
|
906
|
+
# Capture Anthropic prompt caching metrics
|
|
907
|
+
if response.respond_to?(:cached_tokens) && response.cached_tokens&.positive?
|
|
908
|
+
context[:cached_tokens] = response.cached_tokens
|
|
909
|
+
end
|
|
910
|
+
if response.respond_to?(:cache_creation_tokens) && response.cache_creation_tokens&.positive?
|
|
911
|
+
context[:cache_creation_tokens] = response.cache_creation_tokens
|
|
912
|
+
end
|
|
913
|
+
|
|
962
914
|
calculate_costs(response, context) if context.input_tokens
|
|
963
915
|
end
|
|
964
916
|
|
|
@@ -993,6 +945,37 @@ module RubyLLM
|
|
|
993
945
|
nil
|
|
994
946
|
end
|
|
995
947
|
|
|
948
|
+
# Checks whether the given model is served by Anthropic
|
|
949
|
+
#
|
|
950
|
+
# Looks up the model's provider in the registry, falling back to
|
|
951
|
+
# model ID pattern matching when the registry is unavailable.
|
|
952
|
+
#
|
|
953
|
+
# @param model_id [String] The model ID
|
|
954
|
+
# @return [Boolean]
|
|
955
|
+
def anthropic_model?(model_id)
|
|
956
|
+
info = find_model_info(model_id)
|
|
957
|
+
return info.provider.to_s == "anthropic" if info&.provider
|
|
958
|
+
|
|
959
|
+
# Fallback: match common Anthropic model ID patterns
|
|
960
|
+
model_id.to_s.match?(/\Aclaude/i)
|
|
961
|
+
end
|
|
962
|
+
|
|
963
|
+
# Adds cache_control to the last tool so Anthropic caches all tool definitions
|
|
964
|
+
#
|
|
965
|
+
# Uses a singleton method override on the last tool instance so the
|
|
966
|
+
# cache_control is merged into the API payload by RubyLLM's
|
|
967
|
+
# Tools.function_for without mutating the tool class.
|
|
968
|
+
#
|
|
969
|
+
# @param client [RubyLLM::Chat] The chat client with tools already added
|
|
970
|
+
def apply_tool_prompt_caching(client)
|
|
971
|
+
last_tool = client.tools.values.last
|
|
972
|
+
return unless last_tool
|
|
973
|
+
|
|
974
|
+
last_tool.define_singleton_method(:provider_params) do
|
|
975
|
+
super().merge(cache_control: {type: "ephemeral"})
|
|
976
|
+
end
|
|
977
|
+
end
|
|
978
|
+
|
|
996
979
|
# Builds a Result object from the response
|
|
997
980
|
#
|
|
998
981
|
# @param content [Object] The processed content
|
|
@@ -1087,35 +1070,18 @@ module RubyLLM
|
|
|
1087
1070
|
client
|
|
1088
1071
|
.on_tool_call do |tool_call|
|
|
1089
1072
|
start_tracking_tool_call(tool_call)
|
|
1090
|
-
|
|
1091
|
-
event_type = agent_delegate_name?(name) ? :agent_start : :tool_start
|
|
1092
|
-
emit_stream_event(event_type, tool_call_start_data(tool_call))
|
|
1073
|
+
emit_stream_event(:tool_start, tool_call_start_data(tool_call))
|
|
1093
1074
|
end
|
|
1094
1075
|
.on_tool_result do |result|
|
|
1095
|
-
name = @pending_tool_call&.dig(:name)
|
|
1096
|
-
event_type = agent_delegate_name?(name) ? :agent_end : :tool_end
|
|
1097
1076
|
end_data = tool_call_end_data(result)
|
|
1098
1077
|
complete_tool_call_tracking(result)
|
|
1099
|
-
emit_stream_event(
|
|
1078
|
+
emit_stream_event(:tool_end, end_data)
|
|
1100
1079
|
end
|
|
1101
1080
|
end
|
|
1102
1081
|
|
|
1103
|
-
# Checks whether a tool name corresponds to an agent delegate
|
|
1104
|
-
#
|
|
1105
|
-
# @param tool_name [String] The tool name
|
|
1106
|
-
# @return [Boolean]
|
|
1107
|
-
def agent_delegate_name?(tool_name)
|
|
1108
|
-
return false unless tool_name
|
|
1109
|
-
|
|
1110
|
-
resolved_tools.any? do |t|
|
|
1111
|
-
t.respond_to?(:agent_delegate?) && t.agent_delegate? &&
|
|
1112
|
-
tool_name_for(t) == tool_name
|
|
1113
|
-
end
|
|
1114
|
-
end
|
|
1115
|
-
|
|
1116
1082
|
# Emits a StreamEvent to the caller's stream block when stream_events is enabled
|
|
1117
1083
|
#
|
|
1118
|
-
# @param type [Symbol] Event type (:chunk, :tool_start, :tool_end, :
|
|
1084
|
+
# @param type [Symbol] Event type (:chunk, :tool_start, :tool_end, :error)
|
|
1119
1085
|
# @param data [Hash] Event-specific data
|
|
1120
1086
|
def emit_stream_event(type, data)
|
|
1121
1087
|
return unless @context&.stream_block && @context.stream_events?
|
|
@@ -1123,27 +1089,18 @@ module RubyLLM
|
|
|
1123
1089
|
@context.stream_block.call(StreamEvent.new(type, data))
|
|
1124
1090
|
end
|
|
1125
1091
|
|
|
1126
|
-
# Builds data hash for a tool_start
|
|
1092
|
+
# Builds data hash for a tool_start event
|
|
1127
1093
|
#
|
|
1128
1094
|
# @param tool_call [Object] The tool call object from RubyLLM
|
|
1129
1095
|
# @return [Hash] Event data
|
|
1130
1096
|
def tool_call_start_data(tool_call)
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
tool_name: name,
|
|
1097
|
+
{
|
|
1098
|
+
tool_name: extract_tool_call_value(tool_call, :name),
|
|
1134
1099
|
input: extract_tool_call_value(tool_call, :arguments) || {}
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
if agent_delegate_name?(name)
|
|
1138
|
-
agent_tool = resolved_tools.find { |t| tool_name_for(t) == name && t.respond_to?(:agent_class) }
|
|
1139
|
-
data[:agent_name] = name
|
|
1140
|
-
data[:agent_class] = agent_tool&.agent_class&.name
|
|
1141
|
-
end
|
|
1142
|
-
|
|
1143
|
-
data.compact
|
|
1100
|
+
}.compact
|
|
1144
1101
|
end
|
|
1145
1102
|
|
|
1146
|
-
# Builds data hash for a tool_end
|
|
1103
|
+
# Builds data hash for a tool_end event from the pending tool call
|
|
1147
1104
|
#
|
|
1148
1105
|
# @param result [Object] The tool result
|
|
1149
1106
|
# @return [Hash] Event data
|
|
@@ -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
|
|
@@ -50,9 +50,26 @@ module RubyLLM
|
|
|
50
50
|
# When false, executions are logged synchronously.
|
|
51
51
|
# @return [Boolean] Enable async logging (default: true)
|
|
52
52
|
|
|
53
|
+
# @!attribute [rw] soft_purge_after
|
|
54
|
+
# How long to keep full execution details (prompts, responses, tool calls,
|
|
55
|
+
# attempts) before the retention job destroys them. The executions row is
|
|
56
|
+
# preserved so cost, token, and latency analytics remain intact. A
|
|
57
|
+
# truncated copy of the error message is stamped into metadata for
|
|
58
|
+
# long-term error-rate trend analysis.
|
|
59
|
+
# Set to nil to disable soft purging.
|
|
60
|
+
# @return [ActiveSupport::Duration, nil] Soft-purge window (default: 30.days)
|
|
61
|
+
|
|
62
|
+
# @!attribute [rw] hard_purge_after
|
|
63
|
+
# How long to keep the executions row itself before the retention job
|
|
64
|
+
# destroys it entirely. Must be greater than soft_purge_after when both
|
|
65
|
+
# are set. Set to nil to retain executions indefinitely.
|
|
66
|
+
# @return [ActiveSupport::Duration, nil] Hard-purge window (default: 365.days)
|
|
67
|
+
|
|
53
68
|
# @!attribute [rw] retention_period
|
|
54
|
-
#
|
|
55
|
-
#
|
|
69
|
+
# Deprecated. Alias for hard_purge_after, kept for backward compatibility.
|
|
70
|
+
# Prefer configuring soft_purge_after and hard_purge_after explicitly.
|
|
71
|
+
# @return [ActiveSupport::Duration, nil] Hard-purge window
|
|
72
|
+
# @deprecated Use {#hard_purge_after} instead.
|
|
56
73
|
|
|
57
74
|
# @!attribute [rw] anomaly_cost_threshold
|
|
58
75
|
# Cost threshold in dollars that triggers anomaly logging.
|
|
@@ -379,7 +396,6 @@ module RubyLLM
|
|
|
379
396
|
# Attributes without validation (simple accessors)
|
|
380
397
|
attr_accessor :default_model,
|
|
381
398
|
:async_logging,
|
|
382
|
-
:retention_period,
|
|
383
399
|
:dashboard_parent_controller,
|
|
384
400
|
:basic_auth_username,
|
|
385
401
|
:basic_auth_password,
|
|
@@ -448,7 +464,8 @@ module RubyLLM
|
|
|
448
464
|
:helicone_pricing_enabled,
|
|
449
465
|
:helicone_pricing_url,
|
|
450
466
|
:llmpricing_enabled,
|
|
451
|
-
:llmpricing_url
|
|
467
|
+
:llmpricing_url,
|
|
468
|
+
:knowledge_path
|
|
452
469
|
|
|
453
470
|
# Attributes with validation (readers only, custom setters below)
|
|
454
471
|
attr_reader :default_temperature,
|
|
@@ -463,7 +480,9 @@ module RubyLLM
|
|
|
463
480
|
:tenant_resolver,
|
|
464
481
|
:tenant_config_resolver,
|
|
465
482
|
:default_retries,
|
|
466
|
-
:budgets
|
|
483
|
+
:budgets,
|
|
484
|
+
:soft_purge_after,
|
|
485
|
+
:hard_purge_after
|
|
467
486
|
|
|
468
487
|
attr_writer :cache_store
|
|
469
488
|
|
|
@@ -593,6 +612,44 @@ module RubyLLM
|
|
|
593
612
|
@default_embedding_batch_size = value
|
|
594
613
|
end
|
|
595
614
|
|
|
615
|
+
# Sets soft_purge_after with validation
|
|
616
|
+
#
|
|
617
|
+
# @param value [ActiveSupport::Duration, Numeric, nil] Window or nil to disable
|
|
618
|
+
# @raise [ArgumentError] If value is not a Duration/Numeric or nil, or is negative
|
|
619
|
+
def soft_purge_after=(value)
|
|
620
|
+
validate_purge_window!(:soft_purge_after, value)
|
|
621
|
+
@soft_purge_after = value
|
|
622
|
+
validate_purge_ordering!
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
# Sets hard_purge_after with validation
|
|
626
|
+
#
|
|
627
|
+
# @param value [ActiveSupport::Duration, Numeric, nil] Window or nil to disable
|
|
628
|
+
# @raise [ArgumentError] If value is not a Duration/Numeric or nil, or is negative
|
|
629
|
+
def hard_purge_after=(value)
|
|
630
|
+
validate_purge_window!(:hard_purge_after, value)
|
|
631
|
+
@hard_purge_after = value
|
|
632
|
+
validate_purge_ordering!
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
# Deprecated alias for hard_purge_after.
|
|
636
|
+
#
|
|
637
|
+
# @return [ActiveSupport::Duration, nil]
|
|
638
|
+
# @deprecated Use {#hard_purge_after} instead.
|
|
639
|
+
def retention_period
|
|
640
|
+
hard_purge_after
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
# Deprecated setter for retention_period (maps to hard_purge_after).
|
|
644
|
+
#
|
|
645
|
+
# @param value [ActiveSupport::Duration, Numeric, nil]
|
|
646
|
+
# @deprecated Use {#hard_purge_after=} instead.
|
|
647
|
+
def retention_period=(value)
|
|
648
|
+
warn "[DEPRECATION] RubyLLM::Agents config.retention_period is deprecated. " \
|
|
649
|
+
"Use config.hard_purge_after instead (and set config.soft_purge_after for two-tier retention)."
|
|
650
|
+
self.hard_purge_after = value
|
|
651
|
+
end
|
|
652
|
+
|
|
596
653
|
# Sets default_embedding_dimensions with validation
|
|
597
654
|
#
|
|
598
655
|
# @param value [Integer, nil] Dimensions (must be nil or > 0)
|
|
@@ -615,7 +672,8 @@ module RubyLLM
|
|
|
615
672
|
@default_timeout = 60
|
|
616
673
|
@cache_store = nil
|
|
617
674
|
@async_logging = true
|
|
618
|
-
@
|
|
675
|
+
@soft_purge_after = 30.days
|
|
676
|
+
@hard_purge_after = 365.days
|
|
619
677
|
@anomaly_cost_threshold = 5.00
|
|
620
678
|
@anomaly_duration_threshold = 10_000
|
|
621
679
|
@dashboard_auth = ->(_controller) { true }
|
|
@@ -752,6 +810,9 @@ module RubyLLM
|
|
|
752
810
|
@elevenlabs_base_cost_per_1k = nil
|
|
753
811
|
# ElevenLabs models cache TTL in seconds (6 hours)
|
|
754
812
|
@elevenlabs_models_cache_ttl = 21_600
|
|
813
|
+
|
|
814
|
+
# Knowledge defaults
|
|
815
|
+
@knowledge_path = "app/agents/knowledge"
|
|
755
816
|
end
|
|
756
817
|
|
|
757
818
|
# Returns the configured cache store, falling back to Rails.cache
|
|
@@ -956,7 +1017,8 @@ module RubyLLM
|
|
|
956
1017
|
},
|
|
957
1018
|
logging: {
|
|
958
1019
|
async_logging: async_logging,
|
|
959
|
-
|
|
1020
|
+
soft_purge_after: soft_purge_after,
|
|
1021
|
+
hard_purge_after: hard_purge_after,
|
|
960
1022
|
job_retry_attempts: job_retry_attempts,
|
|
961
1023
|
track_executions: track_executions,
|
|
962
1024
|
track_cache_hits: track_cache_hits,
|
|
@@ -1157,6 +1219,30 @@ module RubyLLM
|
|
|
1157
1219
|
raise ArgumentError, "budgets[:enforcement] must be :none, :soft, or :hard"
|
|
1158
1220
|
end
|
|
1159
1221
|
end
|
|
1222
|
+
|
|
1223
|
+
# Validates a purge-window value (Duration, Numeric seconds, or nil).
|
|
1224
|
+
#
|
|
1225
|
+
# @param attr [Symbol] Attribute name for error messages
|
|
1226
|
+
# @param value [ActiveSupport::Duration, Numeric, nil] Value to validate
|
|
1227
|
+
# @raise [ArgumentError] If value is neither nil nor a non-negative duration/number
|
|
1228
|
+
def validate_purge_window!(attr, value)
|
|
1229
|
+
return if value.nil?
|
|
1230
|
+
return if value.is_a?(ActiveSupport::Duration) && value.to_i >= 0
|
|
1231
|
+
return if value.is_a?(Numeric) && value >= 0
|
|
1232
|
+
|
|
1233
|
+
raise ArgumentError, "#{attr} must be an ActiveSupport::Duration, non-negative Numeric, or nil"
|
|
1234
|
+
end
|
|
1235
|
+
|
|
1236
|
+
# Ensures soft_purge_after is strictly less than hard_purge_after when both are set.
|
|
1237
|
+
#
|
|
1238
|
+
# @raise [ArgumentError] If ordering is violated
|
|
1239
|
+
def validate_purge_ordering!
|
|
1240
|
+
return if @soft_purge_after.nil? || @hard_purge_after.nil?
|
|
1241
|
+
return if @soft_purge_after.to_i < @hard_purge_after.to_i
|
|
1242
|
+
|
|
1243
|
+
raise ArgumentError, "soft_purge_after (#{@soft_purge_after.inspect}) must be less than " \
|
|
1244
|
+
"hard_purge_after (#{@hard_purge_after.inspect})"
|
|
1245
|
+
end
|
|
1160
1246
|
end
|
|
1161
1247
|
end
|
|
1162
1248
|
end
|