ruby_llm-agents 3.10.0 → 3.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) 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 +158 -37
  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 -0
  26. data/lib/ruby_llm/agents/pipeline/context.rb +11 -2
  27. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +32 -20
  28. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +22 -1
  29. data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +1 -1
  30. data/lib/ruby_llm/agents/routing/result.rb +60 -9
  31. data/lib/ruby_llm/agents/routing.rb +19 -0
  32. data/lib/ruby_llm/agents/stream_event.rb +58 -0
  33. data/lib/ruby_llm/agents/tool.rb +1 -1
  34. data/lib/ruby_llm/agents.rb +2 -2
  35. metadata +7 -2
  36. data/lib/ruby_llm/agents/agent_tool.rb +0 -125
@@ -5,15 +5,23 @@ module RubyLLM
5
5
  module Routing
6
6
  # Wraps a standard Result with routing-specific accessors.
7
7
  #
8
- # Delegates all standard Result methods (tokens, cost, timing, etc.)
9
- # to the underlying result, adding only the route-specific interface.
8
+ # When the route has an `agent:` mapping, the router auto-delegates
9
+ # to that agent. The delegated result is available via `delegated_result`,
10
+ # and `content` returns the delegated agent's content.
10
11
  #
11
- # @example
12
+ # @example Classification only (no agent mapping)
12
13
  # result = SupportRouter.call(message: "I was charged twice")
13
14
  # result.route # => :billing
14
- # result.agent_class # => BillingAgent (if mapped)
15
- # result.success? # => true
16
- # result.total_cost # => 0.0001
15
+ # result.delegated? # => false
16
+ #
17
+ # @example Auto-delegation (with agent mapping)
18
+ # result = SupportRouter.call(message: "I was charged twice")
19
+ # result.route # => :billing
20
+ # result.delegated? # => true
21
+ # result.delegated_to # => BillingAgent
22
+ # result.content # => BillingAgent's response content
23
+ # result.routing_cost # => cost of classification step
24
+ # result.total_cost # => classification + delegation
17
25
  #
18
26
  class RoutingResult < Result
19
27
  # @return [Symbol] The classified route name
@@ -25,6 +33,9 @@ module RubyLLM
25
33
  # @return [String] The raw text response from the LLM
26
34
  attr_reader :raw_response
27
35
 
36
+ # @return [Result, nil] The result from the delegated agent (if auto-delegated)
37
+ attr_reader :delegated_result
38
+
28
39
  # Creates a new RoutingResult by wrapping a base Result with route data.
29
40
  #
30
41
  # @param base_result [Result] The standard Result from BaseAgent execution
@@ -32,14 +43,32 @@ module RubyLLM
32
43
  # @option route_data [Symbol] :route The classified route name
33
44
  # @option route_data [Class, nil] :agent_class Mapped agent class
34
45
  # @option route_data [String] :raw_response Raw LLM text
46
+ # @option route_data [Result, nil] :delegated_result Result from auto-delegation
35
47
  def initialize(base_result:, route_data:)
48
+ @delegated_result = route_data[:delegated_result]
49
+ @routing_cost = base_result.total_cost
50
+
51
+ # When delegated, merge costs from both classification and delegation
52
+ total = if @delegated_result
53
+ (base_result.total_cost || 0) + (@delegated_result.respond_to?(:total_cost) ? @delegated_result.total_cost || 0 : 0)
54
+ else
55
+ base_result.total_cost
56
+ end
57
+
58
+ # Use delegated content when available
59
+ effective_content = if @delegated_result
60
+ @delegated_result.respond_to?(:content) ? @delegated_result.content : route_data
61
+ else
62
+ route_data
63
+ end
64
+
36
65
  super(
37
- content: route_data,
66
+ content: effective_content,
38
67
  input_tokens: base_result.input_tokens,
39
68
  output_tokens: base_result.output_tokens,
40
69
  input_cost: base_result.input_cost,
41
70
  output_cost: base_result.output_cost,
42
- total_cost: base_result.total_cost,
71
+ total_cost: total,
43
72
  model_id: base_result.model_id,
44
73
  chosen_model_id: base_result.chosen_model_id,
45
74
  temperature: base_result.temperature,
@@ -58,6 +87,27 @@ module RubyLLM
58
87
  @raw_response = route_data[:raw_response]
59
88
  end
60
89
 
90
+ # Whether the router auto-delegated to a mapped agent
91
+ #
92
+ # @return [Boolean]
93
+ def delegated?
94
+ !@delegated_result.nil?
95
+ end
96
+
97
+ # The agent class that was auto-invoked (alias for agent_class)
98
+ #
99
+ # @return [Class, nil]
100
+ def delegated_to
101
+ @agent_class if delegated?
102
+ end
103
+
104
+ # Cost of the classification step only (excluding delegation)
105
+ #
106
+ # @return [Float]
107
+ def routing_cost
108
+ @routing_cost || 0
109
+ end
110
+
61
111
  # Converts the result to a hash including routing fields.
62
112
  #
63
113
  # @return [Hash] All result data plus route, agent_class, raw_response
@@ -65,7 +115,8 @@ module RubyLLM
65
115
  super.merge(
66
116
  route: route,
67
117
  agent_class: agent_class&.name,
68
- raw_response: raw_response
118
+ raw_response: raw_response,
119
+ delegated: delegated?
69
120
  )
70
121
  end
71
122
  end
@@ -131,10 +131,29 @@ module RubyLLM
131
131
  end
132
132
 
133
133
  # Override build_result to return a RoutingResult.
134
+ # Auto-delegates to the mapped agent when the route has an `agent:` mapping.
134
135
  def build_result(content, response, context)
135
136
  base = super
137
+
138
+ # Auto-delegate to the mapped agent
139
+ agent_class = content[:agent_class]
140
+ if agent_class
141
+ content[:delegated_result] = agent_class.call(**delegation_params)
142
+ end
143
+
136
144
  RoutingResult.new(base_result: base, route_data: content)
137
145
  end
146
+
147
+ # Builds params to forward to the delegated agent.
148
+ # Forwards original message and custom params, excludes routing internals.
149
+ #
150
+ # @return [Hash] Params for the delegated agent
151
+ def delegation_params
152
+ forward = @options.except(:dry_run, :skip_cache, :debug, :stream_events)
153
+ forward[:_parent_execution_id] = @parent_execution_id if @parent_execution_id
154
+ forward[:_root_execution_id] = @root_execution_id if @root_execution_id
155
+ forward
156
+ end
138
157
  end
139
158
  end
140
159
  end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ # Typed event emitted during streaming execution.
6
+ #
7
+ # When `stream_events: true` is passed to an agent call, the stream
8
+ # block receives StreamEvent objects instead of raw RubyLLM chunks.
9
+ # This provides visibility into the full execution lifecycle —
10
+ # text chunks, tool invocations, and errors.
11
+ #
12
+ # @example Basic usage
13
+ # MyAgent.call(query: "test", stream_events: true) do |event|
14
+ # case event.type
15
+ # when :chunk then print event.data[:content]
16
+ # when :tool_start then puts "Running #{event.data[:tool_name]}..."
17
+ # when :tool_end then puts "Done (#{event.data[:duration_ms]}ms)"
18
+ # when :error then puts "Error: #{event.data[:message]}"
19
+ # end
20
+ # end
21
+ #
22
+ class StreamEvent
23
+ # @return [Symbol] Event type (:chunk, :tool_start, :tool_end, :error)
24
+ attr_reader :type
25
+
26
+ # @return [Hash] Event-specific data
27
+ attr_reader :data
28
+
29
+ # Creates a new StreamEvent
30
+ #
31
+ # @param type [Symbol] The event type
32
+ # @param data [Hash] Event-specific data
33
+ def initialize(type, data = {})
34
+ @type = type
35
+ @data = data
36
+ end
37
+
38
+ # @return [Boolean] Whether this is a text chunk event
39
+ def chunk?
40
+ @type == :chunk
41
+ end
42
+
43
+ # @return [Boolean] Whether this is a tool lifecycle event
44
+ def tool_event?
45
+ @type == :tool_start || @type == :tool_end
46
+ end
47
+
48
+ # @return [Boolean] Whether this is an error event
49
+ def error?
50
+ @type == :error
51
+ end
52
+
53
+ def to_h
54
+ {type: @type, data: @data}
55
+ end
56
+ end
57
+ end
58
+ end
@@ -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,8 +23,8 @@ 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"
26
+ # Streaming events
27
+ require_relative "agents/stream_event"
28
28
 
29
29
  # Tool base class and context for coding agents
30
30
  require_relative "agents/tool_context"
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.10.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
@@ -242,6 +245,7 @@ files:
242
245
  - lib/ruby_llm/agents/dsl.rb
243
246
  - lib/ruby_llm/agents/dsl/base.rb
244
247
  - lib/ruby_llm/agents/dsl/caching.rb
248
+ - lib/ruby_llm/agents/dsl/knowledge.rb
245
249
  - lib/ruby_llm/agents/dsl/queryable.rb
246
250
  - lib/ruby_llm/agents/dsl/reliability.rb
247
251
  - lib/ruby_llm/agents/eval.rb
@@ -326,6 +330,7 @@ files:
326
330
  - lib/ruby_llm/agents/routing.rb
327
331
  - lib/ruby_llm/agents/routing/class_methods.rb
328
332
  - lib/ruby_llm/agents/routing/result.rb
333
+ - lib/ruby_llm/agents/stream_event.rb
329
334
  - lib/ruby_llm/agents/text/embedder.rb
330
335
  - lib/ruby_llm/agents/tool.rb
331
336
  - lib/ruby_llm/agents/tool_context.rb
@@ -1,125 +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
- # @return [Class] An anonymous RubyLLM::Tool subclass
15
- def self.for(agent_class)
16
- tool_name = derive_tool_name(agent_class)
17
- tool_desc = agent_class.respond_to?(:description) ? agent_class.description : nil
18
- agent_params = agent_class.respond_to?(:params) ? agent_class.params : {}
19
- captured_agent_class = agent_class
20
-
21
- Class.new(RubyLLM::Tool) do
22
- description tool_desc if tool_desc
23
-
24
- # Map agent params to tool params
25
- agent_params.each do |name, config|
26
- next if name.to_s.start_with?("_")
27
-
28
- param name,
29
- desc: config[:desc] || "#{name} parameter",
30
- required: config[:required] == true,
31
- type: AgentTool.map_type(config[:type])
32
- end
33
-
34
- # Store references on the class
35
- define_singleton_method(:agent_class) { captured_agent_class }
36
- define_singleton_method(:tool_name) { tool_name }
37
-
38
- # Instance #name returns the derived tool name
39
- define_method(:name) { tool_name }
40
-
41
- define_method(:execute) do |**kwargs|
42
- depth = (Thread.current[:ruby_llm_agents_tool_depth] || 0) + 1
43
- if depth > MAX_AGENT_TOOL_DEPTH
44
- return "Error calling #{captured_agent_class.name}: Agent tool depth exceeded (max #{MAX_AGENT_TOOL_DEPTH})"
45
- end
46
-
47
- Thread.current[:ruby_llm_agents_tool_depth] = depth
48
-
49
- # Inject hierarchy context from thread-local (set by calling agent)
50
- caller_ctx = Thread.current[:ruby_llm_agents_caller_context]
51
-
52
- call_kwargs = kwargs.dup
53
- if caller_ctx
54
- call_kwargs[:_parent_execution_id] = caller_ctx.execution_id
55
- call_kwargs[:_root_execution_id] = caller_ctx.root_execution_id || caller_ctx.execution_id
56
- call_kwargs[:tenant] = caller_ctx.tenant_object if caller_ctx.tenant_id && !call_kwargs.key?(:tenant)
57
- end
58
-
59
- result = captured_agent_class.call(**call_kwargs)
60
- content = result.respond_to?(:content) ? result.content : result
61
- case content
62
- when String then content
63
- when Hash then content.to_json
64
- when nil then "(no response)"
65
- else content.to_s
66
- end
67
- rescue => e
68
- "Error calling #{captured_agent_class.name}: #{e.message}"
69
- ensure
70
- Thread.current[:ruby_llm_agents_tool_depth] = depth - 1
71
- end
72
- end
73
- end
74
-
75
- # Converts agent class name to tool name.
76
- #
77
- # @example
78
- # ResearchAgent -> "research"
79
- # CodeReviewAgent -> "code_review"
80
- #
81
- # @param agent_class [Class] The agent class
82
- # @return [String] Snake-cased tool name
83
- def self.derive_tool_name(agent_class)
84
- raw = agent_class.name.to_s.split("::").last
85
- raw.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
86
- .gsub(/([a-z\d])([A-Z])/, '\1_\2')
87
- .downcase
88
- .sub(/_agent$/, "")
89
- end
90
-
91
- # Maps Ruby types to JSON Schema types for tool parameters.
92
- #
93
- # @param type [Class, Symbol, nil] Ruby type
94
- # @return [Symbol] JSON Schema type
95
- def self.map_type(type)
96
- case type
97
- when :integer then :integer
98
- when :number, :float then :number
99
- when :boolean then :boolean
100
- when :array then :array
101
- when :object then :object
102
- else
103
- # Handle class objects (Integer, Float, Array, Hash, etc.)
104
- if type.is_a?(Class)
105
- if type <= Integer
106
- :integer
107
- elsif type <= Float
108
- :number
109
- elsif type <= Array
110
- :array
111
- elsif type <= Hash
112
- :object
113
- elsif type == TrueClass || type == FalseClass
114
- :boolean
115
- else
116
- :string
117
- end
118
- else
119
- :string
120
- end
121
- end
122
- end
123
- end
124
- end
125
- end