ruby_llm-agents 3.11.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 +90 -133
- 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 -1
- 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/stream_event.rb +2 -10
- data/lib/ruby_llm/agents/tool.rb +1 -1
- data/lib/ruby_llm/agents.rb +0 -3
- metadata +6 -3
- 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
|
|
@@ -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
|