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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/ruby_llm/agents/agents_controller.rb +74 -0
  3. data/app/controllers/ruby_llm/agents/analytics_controller.rb +304 -0
  4. data/app/controllers/ruby_llm/agents/tenants_controller.rb +74 -2
  5. data/app/models/ruby_llm/agents/agent_override.rb +47 -0
  6. data/app/models/ruby_llm/agents/execution/analytics.rb +37 -16
  7. data/app/services/ruby_llm/agents/agent_registry.rb +8 -1
  8. data/app/views/layouts/ruby_llm/agents/application.html.erb +4 -2
  9. data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +89 -4
  10. data/app/views/ruby_llm/agents/agents/show.html.erb +14 -0
  11. data/app/views/ruby_llm/agents/analytics/index.html.erb +398 -0
  12. data/app/views/ruby_llm/agents/tenants/index.html.erb +3 -2
  13. data/app/views/ruby_llm/agents/tenants/show.html.erb +225 -0
  14. data/config/routes.rb +12 -4
  15. data/lib/generators/ruby_llm_agents/templates/create_overrides_migration.rb.tt +28 -0
  16. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +1 -1
  17. data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +1 -1
  18. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +14 -0
  19. data/lib/ruby_llm/agents/base_agent.rb +90 -133
  20. data/lib/ruby_llm/agents/core/base.rb +9 -0
  21. data/lib/ruby_llm/agents/core/configuration.rb +5 -1
  22. data/lib/ruby_llm/agents/core/version.rb +1 -1
  23. data/lib/ruby_llm/agents/dsl/base.rb +131 -4
  24. data/lib/ruby_llm/agents/dsl/knowledge.rb +157 -0
  25. data/lib/ruby_llm/agents/dsl.rb +1 -1
  26. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +32 -20
  27. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +22 -1
  28. data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +1 -1
  29. data/lib/ruby_llm/agents/stream_event.rb +2 -10
  30. data/lib/ruby_llm/agents/tool.rb +1 -1
  31. data/lib/ruby_llm/agents.rb +0 -3
  32. metadata +6 -3
  33. data/lib/ruby_llm/agents/agent_tool.rb +0 -143
  34. 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
@@ -4,7 +4,7 @@ require_relative "dsl/base"
4
4
  require_relative "dsl/reliability"
5
5
  require_relative "dsl/caching"
6
6
  require_relative "dsl/queryable"
7
- require_relative "dsl/agents"
7
+ require_relative "dsl/knowledge"
8
8
 
9
9
  module RubyLLM
10
10
  module Agents
@@ -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 context.tenant_id.present?
94
- tenant = RubyLLM::Agents::Tenant.find_by(tenant_id: context.tenant_id)
95
- if tenant
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
- def record_spend!(context)
121
- if context.tenant_id.present?
122
- tenant = RubyLLM::Agents::Tenant.find_by(tenant_id: context.tenant_id)
123
- if tenant
124
- tenant.record_execution!(
125
- cost: context.total_cost || 0,
126
- tokens: context.total_tokens || 0,
127
- error: context.failed?
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 + elapsed
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, agent delegations, and errors.
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
@@ -31,7 +31,7 @@ module RubyLLM
31
31
  # @example Using with an agent
32
32
  # class CodingAgent < ApplicationAgent
33
33
  # param :container_id, required: true
34
- # tools [BashTool]
34
+ # tools BashTool
35
35
  # end
36
36
  #
37
37
  # CodingAgent.call(query: "list files", container_id: "abc123")
@@ -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.11.0
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