ruby_llm-agents 3.10.0 → 3.11.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/lib/ruby_llm/agents/agent_tool.rb +21 -3
- data/lib/ruby_llm/agents/base_agent.rb +172 -8
- data/lib/ruby_llm/agents/core/version.rb +1 -1
- data/lib/ruby_llm/agents/dsl/agents.rb +141 -0
- data/lib/ruby_llm/agents/dsl.rb +1 -0
- data/lib/ruby_llm/agents/pipeline/context.rb +11 -2
- data/lib/ruby_llm/agents/routing/result.rb +60 -9
- data/lib/ruby_llm/agents/routing.rb +19 -0
- data/lib/ruby_llm/agents/stream_event.rb +66 -0
- data/lib/ruby_llm/agents.rb +3 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2db1aa75b826b63dae9aec7ee8bee4e3597ce9dab4509e49dcd5cc6bdd8b3aa8
|
|
4
|
+
data.tar.gz: aa5b53d9558b0724ba89a39575b2ac2c3ba2eda550c87d3388f3f66cbda6d0e3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c5537dd90aaceadb9b0948687d3a1ed6635e5c93d8b82a2eae23f805dcb3fc14c32c37aa3fa7bf56a581b48ea72e0d8b0c730f750fc866a6fc4461d784a3ac49
|
|
7
|
+
data.tar.gz: ebbaa8c6c7cf7d27163fbd33c1436cd3332ebbc7069eef6a6b24b50d12e75fd7b962dfaba21faf94d52e8a7ba67a37073c33dc82c91058beb32d486ef3565fc8
|
|
@@ -11,19 +11,25 @@ module RubyLLM
|
|
|
11
11
|
# Wraps an agent class as a RubyLLM::Tool subclass.
|
|
12
12
|
#
|
|
13
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)
|
|
14
17
|
# @return [Class] An anonymous RubyLLM::Tool subclass
|
|
15
|
-
def self.for(agent_class)
|
|
18
|
+
def self.for(agent_class, forwarded_params: [], description_override: nil, delegate: false)
|
|
16
19
|
tool_name = derive_tool_name(agent_class)
|
|
17
|
-
tool_desc = agent_class.respond_to?(:description) ? agent_class.description : nil
|
|
20
|
+
tool_desc = description_override || (agent_class.respond_to?(:description) ? agent_class.description : nil)
|
|
18
21
|
agent_params = agent_class.respond_to?(:params) ? agent_class.params : {}
|
|
19
22
|
captured_agent_class = agent_class
|
|
23
|
+
captured_forwarded = Array(forwarded_params).map(&:to_sym)
|
|
24
|
+
is_delegate = delegate
|
|
20
25
|
|
|
21
26
|
Class.new(RubyLLM::Tool) do
|
|
22
27
|
description tool_desc if tool_desc
|
|
23
28
|
|
|
24
|
-
# Map agent params to tool params
|
|
29
|
+
# Map agent params to tool params, excluding forwarded ones
|
|
25
30
|
agent_params.each do |name, config|
|
|
26
31
|
next if name.to_s.start_with?("_")
|
|
32
|
+
next if captured_forwarded.include?(name.to_sym)
|
|
27
33
|
|
|
28
34
|
param name,
|
|
29
35
|
desc: config[:desc] || "#{name} parameter",
|
|
@@ -34,6 +40,8 @@ module RubyLLM
|
|
|
34
40
|
# Store references on the class
|
|
35
41
|
define_singleton_method(:agent_class) { captured_agent_class }
|
|
36
42
|
define_singleton_method(:tool_name) { tool_name }
|
|
43
|
+
define_singleton_method(:agent_delegate?) { is_delegate }
|
|
44
|
+
define_singleton_method(:forwarded_params) { captured_forwarded }
|
|
37
45
|
|
|
38
46
|
# Instance #name returns the derived tool name
|
|
39
47
|
define_method(:name) { tool_name }
|
|
@@ -54,6 +62,16 @@ module RubyLLM
|
|
|
54
62
|
call_kwargs[:_parent_execution_id] = caller_ctx.execution_id
|
|
55
63
|
call_kwargs[:_root_execution_id] = caller_ctx.root_execution_id || caller_ctx.execution_id
|
|
56
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
|
|
57
75
|
end
|
|
58
76
|
|
|
59
77
|
result = captured_agent_class.call(**call_kwargs)
|
|
@@ -47,6 +47,7 @@ module RubyLLM
|
|
|
47
47
|
extend DSL::Reliability
|
|
48
48
|
extend DSL::Caching
|
|
49
49
|
extend DSL::Queryable
|
|
50
|
+
extend DSL::Agents
|
|
50
51
|
include CacheHelper
|
|
51
52
|
|
|
52
53
|
class << self
|
|
@@ -168,6 +169,7 @@ module RubyLLM
|
|
|
168
169
|
description: description,
|
|
169
170
|
schema: schema&.respond_to?(:name) ? schema.name : schema&.class&.name,
|
|
170
171
|
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 },
|
|
171
173
|
parameters: params.transform_values { |v| v.slice(:type, :required, :default, :desc) },
|
|
172
174
|
thinking: thinking_config,
|
|
173
175
|
caching: caching_config,
|
|
@@ -387,10 +389,19 @@ module RubyLLM
|
|
|
387
389
|
# System prompt for LLM instructions
|
|
388
390
|
#
|
|
389
391
|
# If a class-level `system` DSL is defined, it will be used.
|
|
390
|
-
#
|
|
392
|
+
# When `agents` are declared, auto-generated sections describing
|
|
393
|
+
# direct tools and agent delegates are appended.
|
|
391
394
|
#
|
|
392
395
|
# @return [String, nil] System instructions, or nil for none
|
|
393
396
|
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
|
|
394
405
|
system_config = self.class.system_config
|
|
395
406
|
return resolve_prompt_from_config(system_config) if system_config
|
|
396
407
|
|
|
@@ -512,6 +523,7 @@ module RubyLLM
|
|
|
512
523
|
tenant: resolve_tenant,
|
|
513
524
|
skip_cache: @options[:skip_cache],
|
|
514
525
|
stream_block: (block if streaming_enabled?),
|
|
526
|
+
stream_events: @options[:stream_events] == true,
|
|
515
527
|
parent_execution_id: @parent_execution_id,
|
|
516
528
|
root_execution_id: @root_execution_id,
|
|
517
529
|
debug: @options[:debug],
|
|
@@ -552,6 +564,55 @@ module RubyLLM
|
|
|
552
564
|
end
|
|
553
565
|
end
|
|
554
566
|
|
|
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
|
+
# Returns the description for a tool class
|
|
603
|
+
#
|
|
604
|
+
# @param tool [Class] A tool class
|
|
605
|
+
# @return [String] The tool's description
|
|
606
|
+
def tool_description_for(tool)
|
|
607
|
+
if tool.respond_to?(:description) && tool.description
|
|
608
|
+
tool.description
|
|
609
|
+
elsif tool.is_a?(Class) && tool < RubyLLM::Tool
|
|
610
|
+
tool.new.respond_to?(:description) ? tool.new.description : tool.name.to_s
|
|
611
|
+
else
|
|
612
|
+
tool.name.to_s
|
|
613
|
+
end
|
|
614
|
+
end
|
|
615
|
+
|
|
555
616
|
# Resolves tools for this execution
|
|
556
617
|
#
|
|
557
618
|
# Agent classes in the tools list are automatically wrapped as
|
|
@@ -567,9 +628,31 @@ module RubyLLM
|
|
|
567
628
|
self.class.tools
|
|
568
629
|
end
|
|
569
630
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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
|
+
detect_duplicate_tool_names!(all_tools)
|
|
636
|
+
all_tools
|
|
637
|
+
end
|
|
638
|
+
|
|
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
|
|
573
656
|
end
|
|
574
657
|
|
|
575
658
|
# Wraps an agent class as a tool, or returns the tool class as-is.
|
|
@@ -720,6 +803,7 @@ module RubyLLM
|
|
|
720
803
|
# @param context [Pipeline::Context] The execution context
|
|
721
804
|
# @return [void] Sets context.output with the result
|
|
722
805
|
def execute(context)
|
|
806
|
+
@context = context
|
|
723
807
|
client = build_client(context)
|
|
724
808
|
|
|
725
809
|
# Make context available to AgentTool instances during tool execution
|
|
@@ -817,7 +901,11 @@ module RubyLLM
|
|
|
817
901
|
|
|
818
902
|
response = client.complete do |chunk|
|
|
819
903
|
first_chunk_at ||= Time.current
|
|
820
|
-
context.
|
|
904
|
+
if context.stream_events?
|
|
905
|
+
context.stream_block.call(StreamEvent.new(:chunk, {content: chunk.content}))
|
|
906
|
+
else
|
|
907
|
+
context.stream_block.call(chunk)
|
|
908
|
+
end
|
|
821
909
|
end
|
|
822
910
|
|
|
823
911
|
if first_chunk_at
|
|
@@ -843,7 +931,11 @@ module RubyLLM
|
|
|
843
931
|
|
|
844
932
|
response = client.ask(user_prompt, **ask_opts) do |chunk|
|
|
845
933
|
first_chunk_at ||= Time.current
|
|
846
|
-
context.
|
|
934
|
+
if context.stream_events?
|
|
935
|
+
context.stream_block.call(StreamEvent.new(:chunk, {content: chunk.content}))
|
|
936
|
+
else
|
|
937
|
+
context.stream_block.call(chunk)
|
|
938
|
+
end
|
|
847
939
|
end
|
|
848
940
|
|
|
849
941
|
if first_chunk_at
|
|
@@ -993,8 +1085,80 @@ module RubyLLM
|
|
|
993
1085
|
# @return [RubyLLM::Chat] Client with tracking callbacks
|
|
994
1086
|
def setup_tool_tracking(client)
|
|
995
1087
|
client
|
|
996
|
-
.on_tool_call
|
|
997
|
-
|
|
1088
|
+
.on_tool_call do |tool_call|
|
|
1089
|
+
start_tracking_tool_call(tool_call)
|
|
1090
|
+
name = extract_tool_call_value(tool_call, :name)
|
|
1091
|
+
event_type = agent_delegate_name?(name) ? :agent_start : :tool_start
|
|
1092
|
+
emit_stream_event(event_type, tool_call_start_data(tool_call))
|
|
1093
|
+
end
|
|
1094
|
+
.on_tool_result do |result|
|
|
1095
|
+
name = @pending_tool_call&.dig(:name)
|
|
1096
|
+
event_type = agent_delegate_name?(name) ? :agent_end : :tool_end
|
|
1097
|
+
end_data = tool_call_end_data(result)
|
|
1098
|
+
complete_tool_call_tracking(result)
|
|
1099
|
+
emit_stream_event(event_type, end_data)
|
|
1100
|
+
end
|
|
1101
|
+
end
|
|
1102
|
+
|
|
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
|
+
# Emits a StreamEvent to the caller's stream block when stream_events is enabled
|
|
1117
|
+
#
|
|
1118
|
+
# @param type [Symbol] Event type (:chunk, :tool_start, :tool_end, :agent_start, :agent_end, :error)
|
|
1119
|
+
# @param data [Hash] Event-specific data
|
|
1120
|
+
def emit_stream_event(type, data)
|
|
1121
|
+
return unless @context&.stream_block && @context.stream_events?
|
|
1122
|
+
|
|
1123
|
+
@context.stream_block.call(StreamEvent.new(type, data))
|
|
1124
|
+
end
|
|
1125
|
+
|
|
1126
|
+
# Builds data hash for a tool_start/agent_start event
|
|
1127
|
+
#
|
|
1128
|
+
# @param tool_call [Object] The tool call object from RubyLLM
|
|
1129
|
+
# @return [Hash] Event data
|
|
1130
|
+
def tool_call_start_data(tool_call)
|
|
1131
|
+
name = extract_tool_call_value(tool_call, :name)
|
|
1132
|
+
data = {
|
|
1133
|
+
tool_name: name,
|
|
1134
|
+
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
|
|
1144
|
+
end
|
|
1145
|
+
|
|
1146
|
+
# Builds data hash for a tool_end/agent_end event from the pending tool call
|
|
1147
|
+
#
|
|
1148
|
+
# @param result [Object] The tool result
|
|
1149
|
+
# @return [Hash] Event data
|
|
1150
|
+
def tool_call_end_data(result)
|
|
1151
|
+
return {} unless @pending_tool_call
|
|
1152
|
+
|
|
1153
|
+
started_at = @pending_tool_call[:started_at]
|
|
1154
|
+
duration_ms = started_at ? ((Time.current - started_at) * 1000).to_i : nil
|
|
1155
|
+
result_data = extract_tool_result(result)
|
|
1156
|
+
|
|
1157
|
+
{
|
|
1158
|
+
tool_name: @pending_tool_call[:name],
|
|
1159
|
+
status: result_data[:status],
|
|
1160
|
+
duration_ms: duration_ms
|
|
1161
|
+
}.compact
|
|
998
1162
|
end
|
|
999
1163
|
|
|
1000
1164
|
# Starts tracking a tool call
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
module DSL
|
|
6
|
+
# DSL module for declaring sub-agents on an agent class.
|
|
7
|
+
#
|
|
8
|
+
# Provides two forms — simple list for common cases, block for
|
|
9
|
+
# per-agent configuration:
|
|
10
|
+
#
|
|
11
|
+
# @example Simple form
|
|
12
|
+
# agents [ModelsAgent, ViewsAgent], forward: [:workspace_path]
|
|
13
|
+
#
|
|
14
|
+
# @example Block form
|
|
15
|
+
# agents do
|
|
16
|
+
# use ModelsAgent, timeout: 180, description: "Build models"
|
|
17
|
+
# use ViewsAgent
|
|
18
|
+
# forward :workspace_path, :project_id
|
|
19
|
+
# parallel true
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
module Agents
|
|
23
|
+
# Declares sub-agents for this agent class.
|
|
24
|
+
#
|
|
25
|
+
# @param list [Array<Class>, nil] Agent classes (simple form)
|
|
26
|
+
# @param options [Hash] Global options (simple form)
|
|
27
|
+
# @yield Configuration block (block form)
|
|
28
|
+
# @return [Array<Hash>] Agent entries
|
|
29
|
+
def agents(list = nil, **options, &block)
|
|
30
|
+
if block
|
|
31
|
+
config = AgentsConfig.new
|
|
32
|
+
config.instance_eval(&block)
|
|
33
|
+
@agents_config = config
|
|
34
|
+
elsif list
|
|
35
|
+
config = AgentsConfig.new
|
|
36
|
+
Array(list).each { |a| config.use(a) }
|
|
37
|
+
options.each { |k, v| config.send(k, *Array(v)) }
|
|
38
|
+
@agents_config = config
|
|
39
|
+
end
|
|
40
|
+
@agents_config&.agent_entries || []
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Returns the agents configuration object.
|
|
44
|
+
#
|
|
45
|
+
# @return [AgentsConfig] Configuration (empty if no agents declared)
|
|
46
|
+
def agents_config
|
|
47
|
+
@agents_config ||
|
|
48
|
+
(superclass.respond_to?(:agents_config) ? superclass.agents_config : nil) ||
|
|
49
|
+
AgentsConfig.new
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Configuration object for the `agents` DSL.
|
|
55
|
+
#
|
|
56
|
+
# Holds the list of agent entries and global options like
|
|
57
|
+
# `parallel`, `forward`, `max_depth`, and `instructions`.
|
|
58
|
+
#
|
|
59
|
+
class AgentsConfig
|
|
60
|
+
attr_reader :agent_entries
|
|
61
|
+
|
|
62
|
+
def initialize
|
|
63
|
+
@agent_entries = []
|
|
64
|
+
@options = {
|
|
65
|
+
parallel: true,
|
|
66
|
+
timeout: nil,
|
|
67
|
+
max_depth: 5,
|
|
68
|
+
forward: [],
|
|
69
|
+
instructions: nil
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Registers an agent class with optional per-agent overrides.
|
|
74
|
+
#
|
|
75
|
+
# @param agent_class [Class] A BaseAgent subclass
|
|
76
|
+
# @param timeout [Integer, nil] Per-agent timeout override
|
|
77
|
+
# @param description [String, nil] Per-agent description override
|
|
78
|
+
def use(agent_class, timeout: nil, description: nil)
|
|
79
|
+
@agent_entries << {
|
|
80
|
+
agent_class: agent_class,
|
|
81
|
+
timeout: timeout,
|
|
82
|
+
description: description
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# @!group Global Options
|
|
87
|
+
|
|
88
|
+
def parallel(value = true)
|
|
89
|
+
@options[:parallel] = value
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def timeout(seconds)
|
|
93
|
+
@options[:timeout] = seconds
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def max_depth(depth)
|
|
97
|
+
@options[:max_depth] = depth
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def instructions(text)
|
|
101
|
+
@options[:instructions] = text
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def forward(*params)
|
|
105
|
+
@options[:forward] = params.flatten
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# @!endgroup
|
|
109
|
+
|
|
110
|
+
# @!group Query Methods
|
|
111
|
+
|
|
112
|
+
def parallel?
|
|
113
|
+
@options[:parallel]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def timeout_for(agent_class)
|
|
117
|
+
entry = @agent_entries.find { |e| e[:agent_class] == agent_class }
|
|
118
|
+
entry&.dig(:timeout) || @options[:timeout]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def forwarded_params
|
|
122
|
+
@options[:forward]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def max_depth_value
|
|
126
|
+
@options[:max_depth]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def instructions_text
|
|
130
|
+
@options[:instructions]
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def description_for(agent_class)
|
|
134
|
+
entry = @agent_entries.find { |e| e[:agent_class] == agent_class }
|
|
135
|
+
entry&.dig(:description)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# @!endgroup
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
data/lib/ruby_llm/agents/dsl.rb
CHANGED
|
@@ -50,7 +50,7 @@ module RubyLLM
|
|
|
50
50
|
attr_accessor :trace
|
|
51
51
|
|
|
52
52
|
# Streaming support
|
|
53
|
-
attr_accessor :stream_block, :skip_cache
|
|
53
|
+
attr_accessor :stream_block, :skip_cache, :stream_events
|
|
54
54
|
|
|
55
55
|
# Agent metadata
|
|
56
56
|
attr_reader :agent_class, :agent_type
|
|
@@ -65,7 +65,7 @@ module RubyLLM
|
|
|
65
65
|
# @param skip_cache [Boolean] Whether to skip caching
|
|
66
66
|
# @param stream_block [Proc, nil] Block for streaming
|
|
67
67
|
# @param options [Hash] Additional options passed to the agent
|
|
68
|
-
def initialize(input:, agent_class:, agent_instance: nil, model: nil, tenant: nil, skip_cache: false, stream_block: nil, parent_execution_id: nil, root_execution_id: nil, **options)
|
|
68
|
+
def initialize(input:, agent_class:, agent_instance: nil, model: nil, tenant: nil, skip_cache: false, stream_block: nil, stream_events: false, parent_execution_id: nil, root_execution_id: nil, **options)
|
|
69
69
|
@input = input
|
|
70
70
|
@agent_class = agent_class
|
|
71
71
|
@agent_instance = agent_instance
|
|
@@ -87,6 +87,7 @@ module RubyLLM
|
|
|
87
87
|
# Execution options
|
|
88
88
|
@skip_cache = skip_cache
|
|
89
89
|
@stream_block = stream_block
|
|
90
|
+
@stream_events = stream_events
|
|
90
91
|
|
|
91
92
|
# Debug trace
|
|
92
93
|
@trace = []
|
|
@@ -132,6 +133,13 @@ module RubyLLM
|
|
|
132
133
|
@trace << {middleware: middleware_name, started_at: started_at, duration_ms: duration_ms, action: action}.compact
|
|
133
134
|
end
|
|
134
135
|
|
|
136
|
+
# Are stream events enabled?
|
|
137
|
+
#
|
|
138
|
+
# @return [Boolean]
|
|
139
|
+
def stream_events?
|
|
140
|
+
@stream_events == true
|
|
141
|
+
end
|
|
142
|
+
|
|
135
143
|
# Was the result served from cache?
|
|
136
144
|
#
|
|
137
145
|
# @return [Boolean]
|
|
@@ -243,6 +251,7 @@ module RubyLLM
|
|
|
243
251
|
model: @model,
|
|
244
252
|
skip_cache: @skip_cache,
|
|
245
253
|
stream_block: @stream_block,
|
|
254
|
+
stream_events: @stream_events,
|
|
246
255
|
**opts_without_tenant
|
|
247
256
|
)
|
|
248
257
|
# Preserve resolved tenant state
|
|
@@ -5,15 +5,23 @@ module RubyLLM
|
|
|
5
5
|
module Routing
|
|
6
6
|
# Wraps a standard Result with routing-specific accessors.
|
|
7
7
|
#
|
|
8
|
-
#
|
|
9
|
-
# to
|
|
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.
|
|
15
|
-
#
|
|
16
|
-
#
|
|
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:
|
|
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:
|
|
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,66 @@
|
|
|
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, agent delegations, 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 :agent_start then puts "Delegated to #{event.data[:agent_name]}"
|
|
19
|
+
# when :agent_end then puts "Agent done (#{event.data[:duration_ms]}ms)"
|
|
20
|
+
# when :error then puts "Error: #{event.data[:message]}"
|
|
21
|
+
# end
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
class StreamEvent
|
|
25
|
+
# @return [Symbol] Event type (:chunk, :tool_start, :tool_end,
|
|
26
|
+
# :agent_start, :agent_end, :error)
|
|
27
|
+
attr_reader :type
|
|
28
|
+
|
|
29
|
+
# @return [Hash] Event-specific data
|
|
30
|
+
attr_reader :data
|
|
31
|
+
|
|
32
|
+
# Creates a new StreamEvent
|
|
33
|
+
#
|
|
34
|
+
# @param type [Symbol] The event type
|
|
35
|
+
# @param data [Hash] Event-specific data
|
|
36
|
+
def initialize(type, data = {})
|
|
37
|
+
@type = type
|
|
38
|
+
@data = data
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @return [Boolean] Whether this is a text chunk event
|
|
42
|
+
def chunk?
|
|
43
|
+
@type == :chunk
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @return [Boolean] Whether this is a tool lifecycle event
|
|
47
|
+
def tool_event?
|
|
48
|
+
@type == :tool_start || @type == :tool_end
|
|
49
|
+
end
|
|
50
|
+
|
|
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
|
+
# @return [Boolean] Whether this is an error event
|
|
57
|
+
def error?
|
|
58
|
+
@type == :error
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def to_h
|
|
62
|
+
{type: @type, data: @data}
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
data/lib/ruby_llm/agents.rb
CHANGED
|
@@ -26,6 +26,9 @@ require_relative "agents/base_agent"
|
|
|
26
26
|
# Agent-as-Tool adapter
|
|
27
27
|
require_relative "agents/agent_tool"
|
|
28
28
|
|
|
29
|
+
# Streaming events
|
|
30
|
+
require_relative "agents/stream_event"
|
|
31
|
+
|
|
29
32
|
# Tool base class and context for coding agents
|
|
30
33
|
require_relative "agents/tool_context"
|
|
31
34
|
require_relative "agents/tool"
|
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.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- adham90
|
|
@@ -240,6 +240,7 @@ files:
|
|
|
240
240
|
- lib/ruby_llm/agents/core/llm_tenant.rb
|
|
241
241
|
- lib/ruby_llm/agents/core/version.rb
|
|
242
242
|
- lib/ruby_llm/agents/dsl.rb
|
|
243
|
+
- lib/ruby_llm/agents/dsl/agents.rb
|
|
243
244
|
- lib/ruby_llm/agents/dsl/base.rb
|
|
244
245
|
- lib/ruby_llm/agents/dsl/caching.rb
|
|
245
246
|
- lib/ruby_llm/agents/dsl/queryable.rb
|
|
@@ -326,6 +327,7 @@ files:
|
|
|
326
327
|
- lib/ruby_llm/agents/routing.rb
|
|
327
328
|
- lib/ruby_llm/agents/routing/class_methods.rb
|
|
328
329
|
- lib/ruby_llm/agents/routing/result.rb
|
|
330
|
+
- lib/ruby_llm/agents/stream_event.rb
|
|
329
331
|
- lib/ruby_llm/agents/text/embedder.rb
|
|
330
332
|
- lib/ruby_llm/agents/tool.rb
|
|
331
333
|
- lib/ruby_llm/agents/tool_context.rb
|