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
|
@@ -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
|
@@ -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
|
|
|
@@ -7,7 +7,7 @@ module RubyLLM
|
|
|
7
7
|
# When `stream_events: true` is passed to an agent call, the stream
|
|
8
8
|
# block receives StreamEvent objects instead of raw RubyLLM chunks.
|
|
9
9
|
# This provides visibility into the full execution lifecycle —
|
|
10
|
-
# text chunks, tool invocations,
|
|
10
|
+
# text chunks, tool invocations, and errors.
|
|
11
11
|
#
|
|
12
12
|
# @example Basic usage
|
|
13
13
|
# MyAgent.call(query: "test", stream_events: true) do |event|
|
|
@@ -15,15 +15,12 @@ module RubyLLM
|
|
|
15
15
|
# when :chunk then print event.data[:content]
|
|
16
16
|
# when :tool_start then puts "Running #{event.data[:tool_name]}..."
|
|
17
17
|
# when :tool_end then puts "Done (#{event.data[:duration_ms]}ms)"
|
|
18
|
-
# when :agent_start then puts "Delegated to #{event.data[:agent_name]}"
|
|
19
|
-
# when :agent_end then puts "Agent done (#{event.data[:duration_ms]}ms)"
|
|
20
18
|
# when :error then puts "Error: #{event.data[:message]}"
|
|
21
19
|
# end
|
|
22
20
|
# end
|
|
23
21
|
#
|
|
24
22
|
class StreamEvent
|
|
25
|
-
# @return [Symbol] Event type (:chunk, :tool_start, :tool_end,
|
|
26
|
-
# :agent_start, :agent_end, :error)
|
|
23
|
+
# @return [Symbol] Event type (:chunk, :tool_start, :tool_end, :error)
|
|
27
24
|
attr_reader :type
|
|
28
25
|
|
|
29
26
|
# @return [Hash] Event-specific data
|
|
@@ -48,11 +45,6 @@ module RubyLLM
|
|
|
48
45
|
@type == :tool_start || @type == :tool_end
|
|
49
46
|
end
|
|
50
47
|
|
|
51
|
-
# @return [Boolean] Whether this is an agent lifecycle event
|
|
52
|
-
def agent_event?
|
|
53
|
-
@type == :agent_start || @type == :agent_end
|
|
54
|
-
end
|
|
55
|
-
|
|
56
48
|
# @return [Boolean] Whether this is an error event
|
|
57
49
|
def error?
|
|
58
50
|
@type == :error
|
data/lib/ruby_llm/agents/tool.rb
CHANGED
data/lib/ruby_llm/agents.rb
CHANGED
|
@@ -23,9 +23,6 @@ require_relative "agents/dsl"
|
|
|
23
23
|
# BaseAgent - new middleware-based agent architecture
|
|
24
24
|
require_relative "agents/base_agent"
|
|
25
25
|
|
|
26
|
-
# Agent-as-Tool adapter
|
|
27
|
-
require_relative "agents/agent_tool"
|
|
28
|
-
|
|
29
26
|
# Streaming events
|
|
30
27
|
require_relative "agents/stream_event"
|
|
31
28
|
|
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.12.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- adham90
|
|
@@ -80,12 +80,14 @@ files:
|
|
|
80
80
|
- app/controllers/concerns/ruby_llm/agents/paginatable.rb
|
|
81
81
|
- app/controllers/concerns/ruby_llm/agents/sortable.rb
|
|
82
82
|
- app/controllers/ruby_llm/agents/agents_controller.rb
|
|
83
|
+
- app/controllers/ruby_llm/agents/analytics_controller.rb
|
|
83
84
|
- app/controllers/ruby_llm/agents/dashboard_controller.rb
|
|
84
85
|
- app/controllers/ruby_llm/agents/executions_controller.rb
|
|
85
86
|
- app/controllers/ruby_llm/agents/requests_controller.rb
|
|
86
87
|
- app/controllers/ruby_llm/agents/system_config_controller.rb
|
|
87
88
|
- app/controllers/ruby_llm/agents/tenants_controller.rb
|
|
88
89
|
- app/helpers/ruby_llm/agents/application_helper.rb
|
|
90
|
+
- app/models/ruby_llm/agents/agent_override.rb
|
|
89
91
|
- app/models/ruby_llm/agents/execution.rb
|
|
90
92
|
- app/models/ruby_llm/agents/execution/analytics.rb
|
|
91
93
|
- app/models/ruby_llm/agents/execution/metrics.rb
|
|
@@ -112,6 +114,7 @@ files:
|
|
|
112
114
|
- app/views/ruby_llm/agents/agents/_sortable_header.html.erb
|
|
113
115
|
- app/views/ruby_llm/agents/agents/index.html.erb
|
|
114
116
|
- app/views/ruby_llm/agents/agents/show.html.erb
|
|
117
|
+
- app/views/ruby_llm/agents/analytics/index.html.erb
|
|
115
118
|
- app/views/ruby_llm/agents/dashboard/_action_center.html.erb
|
|
116
119
|
- app/views/ruby_llm/agents/dashboard/_tenant_budget.html.erb
|
|
117
120
|
- app/views/ruby_llm/agents/dashboard/_top_tenants.html.erb
|
|
@@ -185,6 +188,7 @@ files:
|
|
|
185
188
|
- lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt
|
|
186
189
|
- lib/generators/ruby_llm_agents/templates/background_remover.rb.tt
|
|
187
190
|
- lib/generators/ruby_llm_agents/templates/create_execution_details_migration.rb.tt
|
|
191
|
+
- lib/generators/ruby_llm_agents/templates/create_overrides_migration.rb.tt
|
|
188
192
|
- lib/generators/ruby_llm_agents/templates/create_tenant_budgets_migration.rb.tt
|
|
189
193
|
- lib/generators/ruby_llm_agents/templates/create_tenants_migration.rb.tt
|
|
190
194
|
- lib/generators/ruby_llm_agents/templates/embedder.rb.tt
|
|
@@ -221,7 +225,6 @@ files:
|
|
|
221
225
|
- lib/generators/ruby_llm_agents/upgrade_generator.rb
|
|
222
226
|
- lib/ruby_llm-agents.rb
|
|
223
227
|
- lib/ruby_llm/agents.rb
|
|
224
|
-
- lib/ruby_llm/agents/agent_tool.rb
|
|
225
228
|
- lib/ruby_llm/agents/audio/elevenlabs/model_registry.rb
|
|
226
229
|
- lib/ruby_llm/agents/audio/speaker.rb
|
|
227
230
|
- lib/ruby_llm/agents/audio/speaker/active_storage_support.rb
|
|
@@ -240,9 +243,9 @@ files:
|
|
|
240
243
|
- lib/ruby_llm/agents/core/llm_tenant.rb
|
|
241
244
|
- lib/ruby_llm/agents/core/version.rb
|
|
242
245
|
- lib/ruby_llm/agents/dsl.rb
|
|
243
|
-
- lib/ruby_llm/agents/dsl/agents.rb
|
|
244
246
|
- lib/ruby_llm/agents/dsl/base.rb
|
|
245
247
|
- lib/ruby_llm/agents/dsl/caching.rb
|
|
248
|
+
- lib/ruby_llm/agents/dsl/knowledge.rb
|
|
246
249
|
- lib/ruby_llm/agents/dsl/queryable.rb
|
|
247
250
|
- lib/ruby_llm/agents/dsl/reliability.rb
|
|
248
251
|
- lib/ruby_llm/agents/eval.rb
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RubyLLM
|
|
4
|
-
module Agents
|
|
5
|
-
# Wraps an agent class as a RubyLLM::Tool so it can be used
|
|
6
|
-
# in another agent's `tools` list. The LLM sees the sub-agent
|
|
7
|
-
# as a callable tool and can invoke it with the agent's declared params.
|
|
8
|
-
module AgentTool
|
|
9
|
-
MAX_AGENT_TOOL_DEPTH = 5
|
|
10
|
-
|
|
11
|
-
# Wraps an agent class as a RubyLLM::Tool subclass.
|
|
12
|
-
#
|
|
13
|
-
# @param agent_class [Class] A BaseAgent subclass
|
|
14
|
-
# @param forwarded_params [Array<Symbol>] Params auto-injected from parent (excluded from LLM schema)
|
|
15
|
-
# @param description_override [String, nil] Custom description for the tool
|
|
16
|
-
# @param delegate [Boolean] Whether this tool represents an agent delegate (from `agents` DSL)
|
|
17
|
-
# @return [Class] An anonymous RubyLLM::Tool subclass
|
|
18
|
-
def self.for(agent_class, forwarded_params: [], description_override: nil, delegate: false)
|
|
19
|
-
tool_name = derive_tool_name(agent_class)
|
|
20
|
-
tool_desc = description_override || (agent_class.respond_to?(:description) ? agent_class.description : nil)
|
|
21
|
-
agent_params = agent_class.respond_to?(:params) ? agent_class.params : {}
|
|
22
|
-
captured_agent_class = agent_class
|
|
23
|
-
captured_forwarded = Array(forwarded_params).map(&:to_sym)
|
|
24
|
-
is_delegate = delegate
|
|
25
|
-
|
|
26
|
-
Class.new(RubyLLM::Tool) do
|
|
27
|
-
description tool_desc if tool_desc
|
|
28
|
-
|
|
29
|
-
# Map agent params to tool params, excluding forwarded ones
|
|
30
|
-
agent_params.each do |name, config|
|
|
31
|
-
next if name.to_s.start_with?("_")
|
|
32
|
-
next if captured_forwarded.include?(name.to_sym)
|
|
33
|
-
|
|
34
|
-
param name,
|
|
35
|
-
desc: config[:desc] || "#{name} parameter",
|
|
36
|
-
required: config[:required] == true,
|
|
37
|
-
type: AgentTool.map_type(config[:type])
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# Store references on the class
|
|
41
|
-
define_singleton_method(:agent_class) { captured_agent_class }
|
|
42
|
-
define_singleton_method(:tool_name) { tool_name }
|
|
43
|
-
define_singleton_method(:agent_delegate?) { is_delegate }
|
|
44
|
-
define_singleton_method(:forwarded_params) { captured_forwarded }
|
|
45
|
-
|
|
46
|
-
# Instance #name returns the derived tool name
|
|
47
|
-
define_method(:name) { tool_name }
|
|
48
|
-
|
|
49
|
-
define_method(:execute) do |**kwargs|
|
|
50
|
-
depth = (Thread.current[:ruby_llm_agents_tool_depth] || 0) + 1
|
|
51
|
-
if depth > MAX_AGENT_TOOL_DEPTH
|
|
52
|
-
return "Error calling #{captured_agent_class.name}: Agent tool depth exceeded (max #{MAX_AGENT_TOOL_DEPTH})"
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
Thread.current[:ruby_llm_agents_tool_depth] = depth
|
|
56
|
-
|
|
57
|
-
# Inject hierarchy context from thread-local (set by calling agent)
|
|
58
|
-
caller_ctx = Thread.current[:ruby_llm_agents_caller_context]
|
|
59
|
-
|
|
60
|
-
call_kwargs = kwargs.dup
|
|
61
|
-
if caller_ctx
|
|
62
|
-
call_kwargs[:_parent_execution_id] = caller_ctx.execution_id
|
|
63
|
-
call_kwargs[:_root_execution_id] = caller_ctx.root_execution_id || caller_ctx.execution_id
|
|
64
|
-
call_kwargs[:tenant] = caller_ctx.tenant_object if caller_ctx.tenant_id && !call_kwargs.key?(:tenant)
|
|
65
|
-
|
|
66
|
-
# Inject forwarded params from the parent agent instance
|
|
67
|
-
if captured_forwarded.any? && caller_ctx.agent_instance
|
|
68
|
-
captured_forwarded.each do |param_name|
|
|
69
|
-
next if call_kwargs.key?(param_name)
|
|
70
|
-
if caller_ctx.agent_instance.respond_to?(param_name)
|
|
71
|
-
call_kwargs[param_name] = caller_ctx.agent_instance.send(param_name)
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
result = captured_agent_class.call(**call_kwargs)
|
|
78
|
-
content = result.respond_to?(:content) ? result.content : result
|
|
79
|
-
case content
|
|
80
|
-
when String then content
|
|
81
|
-
when Hash then content.to_json
|
|
82
|
-
when nil then "(no response)"
|
|
83
|
-
else content.to_s
|
|
84
|
-
end
|
|
85
|
-
rescue => e
|
|
86
|
-
"Error calling #{captured_agent_class.name}: #{e.message}"
|
|
87
|
-
ensure
|
|
88
|
-
Thread.current[:ruby_llm_agents_tool_depth] = depth - 1
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
# Converts agent class name to tool name.
|
|
94
|
-
#
|
|
95
|
-
# @example
|
|
96
|
-
# ResearchAgent -> "research"
|
|
97
|
-
# CodeReviewAgent -> "code_review"
|
|
98
|
-
#
|
|
99
|
-
# @param agent_class [Class] The agent class
|
|
100
|
-
# @return [String] Snake-cased tool name
|
|
101
|
-
def self.derive_tool_name(agent_class)
|
|
102
|
-
raw = agent_class.name.to_s.split("::").last
|
|
103
|
-
raw.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
104
|
-
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
105
|
-
.downcase
|
|
106
|
-
.sub(/_agent$/, "")
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
# Maps Ruby types to JSON Schema types for tool parameters.
|
|
110
|
-
#
|
|
111
|
-
# @param type [Class, Symbol, nil] Ruby type
|
|
112
|
-
# @return [Symbol] JSON Schema type
|
|
113
|
-
def self.map_type(type)
|
|
114
|
-
case type
|
|
115
|
-
when :integer then :integer
|
|
116
|
-
when :number, :float then :number
|
|
117
|
-
when :boolean then :boolean
|
|
118
|
-
when :array then :array
|
|
119
|
-
when :object then :object
|
|
120
|
-
else
|
|
121
|
-
# Handle class objects (Integer, Float, Array, Hash, etc.)
|
|
122
|
-
if type.is_a?(Class)
|
|
123
|
-
if type <= Integer
|
|
124
|
-
:integer
|
|
125
|
-
elsif type <= Float
|
|
126
|
-
:number
|
|
127
|
-
elsif type <= Array
|
|
128
|
-
:array
|
|
129
|
-
elsif type <= Hash
|
|
130
|
-
:object
|
|
131
|
-
elsif type == TrueClass || type == FalseClass
|
|
132
|
-
:boolean
|
|
133
|
-
else
|
|
134
|
-
:string
|
|
135
|
-
end
|
|
136
|
-
else
|
|
137
|
-
:string
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
end
|
|
141
|
-
end
|
|
142
|
-
end
|
|
143
|
-
end
|