ruby_llm-agents 0.5.0 → 1.0.0.beta.1
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/README.md +189 -31
- data/app/controllers/ruby_llm/agents/agents_controller.rb +136 -16
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +29 -9
- data/app/controllers/ruby_llm/agents/workflows_controller.rb +355 -0
- data/app/helpers/ruby_llm/agents/application_helper.rb +25 -0
- data/app/models/ruby_llm/agents/execution.rb +3 -0
- data/app/models/ruby_llm/agents/tenant_budget.rb +58 -15
- data/app/services/ruby_llm/agents/agent_registry.rb +51 -12
- data/app/views/layouts/ruby_llm/agents/application.html.erb +2 -29
- data/app/views/ruby_llm/agents/agents/_agent.html.erb +13 -1
- data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +235 -0
- data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +70 -0
- data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +152 -0
- data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +63 -0
- data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +108 -0
- data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +91 -0
- data/app/views/ruby_llm/agents/agents/_workflow.html.erb +1 -1
- data/app/views/ruby_llm/agents/agents/index.html.erb +74 -9
- data/app/views/ruby_llm/agents/agents/show.html.erb +18 -378
- data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +269 -15
- data/app/views/ruby_llm/agents/executions/show.html.erb +16 -0
- data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +93 -0
- data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +236 -0
- data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +76 -0
- data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +74 -0
- data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +108 -0
- data/app/views/ruby_llm/agents/workflows/show.html.erb +442 -0
- data/config/routes.rb +1 -0
- data/lib/generators/ruby_llm_agents/agent_generator.rb +56 -7
- data/lib/generators/ruby_llm_agents/background_remover_generator.rb +110 -0
- data/lib/generators/ruby_llm_agents/embedder_generator.rb +107 -0
- data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +115 -0
- data/lib/generators/ruby_llm_agents/image_editor_generator.rb +108 -0
- data/lib/generators/ruby_llm_agents/image_generator_generator.rb +116 -0
- data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +178 -0
- data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +109 -0
- data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +103 -0
- data/lib/generators/ruby_llm_agents/image_variator_generator.rb +102 -0
- data/lib/generators/ruby_llm_agents/install_generator.rb +76 -4
- data/lib/generators/ruby_llm_agents/restructure_generator.rb +292 -0
- data/lib/generators/ruby_llm_agents/speaker_generator.rb +121 -0
- data/lib/generators/ruby_llm_agents/templates/add_execution_type_migration.rb.tt +8 -0
- data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +99 -84
- data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +42 -40
- data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +26 -0
- data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +50 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +26 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +20 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +38 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +139 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +21 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +20 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +20 -0
- data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +49 -0
- data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +53 -0
- data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +44 -0
- data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +41 -0
- data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +45 -0
- data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +35 -0
- data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +47 -0
- data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +50 -0
- data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +44 -0
- data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +38 -0
- data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +33 -0
- data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +228 -0
- data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +131 -0
- data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +255 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +120 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +102 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +282 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +228 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +120 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +110 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +120 -0
- data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +212 -0
- data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +227 -0
- data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +251 -0
- data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +300 -0
- data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +56 -0
- data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +51 -0
- data/lib/generators/ruby_llm_agents/transcriber_generator.rb +107 -0
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +152 -1
- data/lib/ruby_llm/agents/audio/speaker.rb +553 -0
- data/lib/ruby_llm/agents/audio/transcriber.rb +669 -0
- data/lib/ruby_llm/agents/base_agent.rb +675 -0
- data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +181 -0
- data/lib/ruby_llm/agents/core/base/moderation_execution.rb +274 -0
- data/lib/ruby_llm/agents/core/base.rb +135 -0
- data/lib/ruby_llm/agents/core/configuration.rb +981 -0
- data/lib/ruby_llm/agents/core/errors.rb +150 -0
- data/lib/ruby_llm/agents/{instrumentation.rb → core/instrumentation.rb} +22 -1
- data/lib/ruby_llm/agents/core/llm_tenant.rb +358 -0
- data/lib/ruby_llm/agents/{version.rb → core/version.rb} +1 -1
- data/lib/ruby_llm/agents/dsl/base.rb +110 -0
- data/lib/ruby_llm/agents/dsl/caching.rb +142 -0
- data/lib/ruby_llm/agents/dsl/reliability.rb +307 -0
- data/lib/ruby_llm/agents/dsl.rb +41 -0
- data/lib/ruby_llm/agents/image/analyzer/dsl.rb +130 -0
- data/lib/ruby_llm/agents/image/analyzer/execution.rb +402 -0
- data/lib/ruby_llm/agents/image/analyzer.rb +90 -0
- data/lib/ruby_llm/agents/image/background_remover/dsl.rb +154 -0
- data/lib/ruby_llm/agents/image/background_remover/execution.rb +240 -0
- data/lib/ruby_llm/agents/image/background_remover.rb +89 -0
- data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +91 -0
- data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +165 -0
- data/lib/ruby_llm/agents/image/editor/dsl.rb +56 -0
- data/lib/ruby_llm/agents/image/editor/execution.rb +207 -0
- data/lib/ruby_llm/agents/image/editor.rb +92 -0
- data/lib/ruby_llm/agents/image/generator/active_storage_support.rb +127 -0
- data/lib/ruby_llm/agents/image/generator/content_policy.rb +95 -0
- data/lib/ruby_llm/agents/image/generator/pricing.rb +353 -0
- data/lib/ruby_llm/agents/image/generator/templates.rb +124 -0
- data/lib/ruby_llm/agents/image/generator.rb +455 -0
- data/lib/ruby_llm/agents/image/pipeline/dsl.rb +213 -0
- data/lib/ruby_llm/agents/image/pipeline/execution.rb +382 -0
- data/lib/ruby_llm/agents/image/pipeline.rb +97 -0
- data/lib/ruby_llm/agents/image/transformer/dsl.rb +148 -0
- data/lib/ruby_llm/agents/image/transformer/execution.rb +223 -0
- data/lib/ruby_llm/agents/image/transformer.rb +95 -0
- data/lib/ruby_llm/agents/image/upscaler/dsl.rb +83 -0
- data/lib/ruby_llm/agents/image/upscaler/execution.rb +219 -0
- data/lib/ruby_llm/agents/image/upscaler.rb +81 -0
- data/lib/ruby_llm/agents/image/variator/dsl.rb +62 -0
- data/lib/ruby_llm/agents/image/variator/execution.rb +189 -0
- data/lib/ruby_llm/agents/image/variator.rb +80 -0
- data/lib/ruby_llm/agents/{alert_manager.rb → infrastructure/alert_manager.rb} +17 -22
- data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +145 -0
- data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +149 -0
- data/lib/ruby_llm/agents/infrastructure/budget/forecaster.rb +68 -0
- data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +279 -0
- data/lib/ruby_llm/agents/infrastructure/budget_tracker.rb +275 -0
- data/lib/ruby_llm/agents/{execution_logger_job.rb → infrastructure/execution_logger_job.rb} +17 -1
- data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/executor.rb +2 -1
- data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/retry_strategy.rb +9 -3
- data/lib/ruby_llm/agents/{reliability.rb → infrastructure/reliability.rb} +11 -21
- data/lib/ruby_llm/agents/pipeline/builder.rb +215 -0
- data/lib/ruby_llm/agents/pipeline/context.rb +255 -0
- data/lib/ruby_llm/agents/pipeline/executor.rb +86 -0
- data/lib/ruby_llm/agents/pipeline/middleware/base.rb +124 -0
- data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +95 -0
- data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +171 -0
- data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +415 -0
- data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +276 -0
- data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +196 -0
- data/lib/ruby_llm/agents/pipeline.rb +68 -0
- data/lib/ruby_llm/agents/{engine.rb → rails/engine.rb} +79 -11
- data/lib/ruby_llm/agents/results/background_removal_result.rb +286 -0
- data/lib/ruby_llm/agents/{result.rb → results/base.rb} +73 -1
- data/lib/ruby_llm/agents/results/embedding_result.rb +243 -0
- data/lib/ruby_llm/agents/results/image_analysis_result.rb +314 -0
- data/lib/ruby_llm/agents/results/image_edit_result.rb +250 -0
- data/lib/ruby_llm/agents/results/image_generation_result.rb +346 -0
- data/lib/ruby_llm/agents/results/image_pipeline_result.rb +399 -0
- data/lib/ruby_llm/agents/results/image_transform_result.rb +251 -0
- data/lib/ruby_llm/agents/results/image_upscale_result.rb +255 -0
- data/lib/ruby_llm/agents/results/image_variation_result.rb +237 -0
- data/lib/ruby_llm/agents/results/moderation_result.rb +158 -0
- data/lib/ruby_llm/agents/results/speech_result.rb +338 -0
- data/lib/ruby_llm/agents/results/transcription_result.rb +408 -0
- data/lib/ruby_llm/agents/text/embedder.rb +444 -0
- data/lib/ruby_llm/agents/text/moderator.rb +237 -0
- data/lib/ruby_llm/agents/workflow/async.rb +220 -0
- data/lib/ruby_llm/agents/workflow/async_executor.rb +156 -0
- data/lib/ruby_llm/agents/{workflow.rb → workflow/orchestrator.rb} +6 -5
- data/lib/ruby_llm/agents/workflow/parallel.rb +34 -17
- data/lib/ruby_llm/agents/workflow/thread_pool.rb +185 -0
- data/lib/ruby_llm/agents.rb +86 -20
- metadata +172 -34
- data/lib/ruby_llm/agents/base/caching.rb +0 -40
- data/lib/ruby_llm/agents/base/cost_calculation.rb +0 -105
- data/lib/ruby_llm/agents/base/dsl.rb +0 -324
- data/lib/ruby_llm/agents/base/execution.rb +0 -366
- data/lib/ruby_llm/agents/base/reliability_dsl.rb +0 -82
- data/lib/ruby_llm/agents/base/reliability_execution.rb +0 -136
- data/lib/ruby_llm/agents/base/response_building.rb +0 -86
- data/lib/ruby_llm/agents/base/tool_tracking.rb +0 -57
- data/lib/ruby_llm/agents/base.rb +0 -210
- data/lib/ruby_llm/agents/budget_tracker.rb +0 -733
- data/lib/ruby_llm/agents/configuration.rb +0 -394
- /data/lib/ruby_llm/agents/{deprecations.rb → core/deprecations.rb} +0 -0
- /data/lib/ruby_llm/agents/{inflections.rb → core/inflections.rb} +0 -0
- /data/lib/ruby_llm/agents/{resolved_config.rb → core/resolved_config.rb} +0 -0
- /data/lib/ruby_llm/agents/{attempt_tracker.rb → infrastructure/attempt_tracker.rb} +0 -0
- /data/lib/ruby_llm/agents/{cache_helper.rb → infrastructure/cache_helper.rb} +0 -0
- /data/lib/ruby_llm/agents/{circuit_breaker.rb → infrastructure/circuit_breaker.rb} +0 -0
- /data/lib/ruby_llm/agents/{redactor.rb → infrastructure/redactor.rb} +0 -0
- /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/breaker_manager.rb +0 -0
- /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/execution_constraints.rb +0 -0
- /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/fallback_routing.rb +0 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
# Async/Fiber support for concurrent agent execution
|
|
6
|
+
#
|
|
7
|
+
# Provides utilities for running agents concurrently using Ruby's Fiber scheduler.
|
|
8
|
+
# When used inside an `Async` block, RubyLLM automatically becomes non-blocking
|
|
9
|
+
# because it uses `Net::HTTP` which cooperates with Ruby's fiber scheduler.
|
|
10
|
+
#
|
|
11
|
+
# @example Basic concurrent execution
|
|
12
|
+
# require 'async'
|
|
13
|
+
#
|
|
14
|
+
# Async do
|
|
15
|
+
# results = RubyLLM::Agents::Async.batch([
|
|
16
|
+
# [SentimentAgent, { input: "I love this!" }],
|
|
17
|
+
# [SummaryAgent, { input: "Long text..." }],
|
|
18
|
+
# [CategoryAgent, { input: "Product review" }]
|
|
19
|
+
# ])
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# @example With rate limiting
|
|
23
|
+
# Async do
|
|
24
|
+
# results = RubyLLM::Agents::Async.batch(
|
|
25
|
+
# items.map { |item| [ProcessorAgent, { input: item }] },
|
|
26
|
+
# max_concurrent: 5
|
|
27
|
+
# )
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# @example Streaming multiple agents
|
|
31
|
+
# Async do
|
|
32
|
+
# RubyLLM::Agents::Async.each([AgentA, AgentB]) do |agent|
|
|
33
|
+
# agent.call(input: data) { |chunk| stream_chunk(chunk) }
|
|
34
|
+
# end
|
|
35
|
+
# end
|
|
36
|
+
#
|
|
37
|
+
# @see https://rubyllm.com/async/ RubyLLM Async Documentation
|
|
38
|
+
# @api public
|
|
39
|
+
module Async
|
|
40
|
+
class << self
|
|
41
|
+
# Executes multiple agents concurrently with optional rate limiting
|
|
42
|
+
#
|
|
43
|
+
# @param agents_with_params [Array<Array(Class, Hash)>] Array of [AgentClass, params] pairs
|
|
44
|
+
# @param max_concurrent [Integer, nil] Maximum concurrent executions (nil = use config default)
|
|
45
|
+
# @yield [result, index] Optional block called for each completed result
|
|
46
|
+
# @return [Array<Object>] Results in the same order as input
|
|
47
|
+
#
|
|
48
|
+
# @example Basic batch
|
|
49
|
+
# results = RubyLLM::Agents::Async.batch([
|
|
50
|
+
# [AgentA, { input: "text1" }],
|
|
51
|
+
# [AgentB, { input: "text2" }]
|
|
52
|
+
# ])
|
|
53
|
+
#
|
|
54
|
+
# @example With progress callback
|
|
55
|
+
# RubyLLM::Agents::Async.batch(agents_with_params) do |result, index|
|
|
56
|
+
# puts "Completed #{index + 1}/#{agents_with_params.size}"
|
|
57
|
+
# end
|
|
58
|
+
def batch(agents_with_params, max_concurrent: nil, &block)
|
|
59
|
+
ensure_async_available!
|
|
60
|
+
|
|
61
|
+
max_concurrent ||= RubyLLM::Agents.configuration.async_max_concurrency
|
|
62
|
+
semaphore = ::Async::Semaphore.new(max_concurrent)
|
|
63
|
+
|
|
64
|
+
Kernel.send(:Async) do
|
|
65
|
+
agents_with_params.each_with_index.map do |(agent_class, params), index|
|
|
66
|
+
Kernel.send(:Async) do
|
|
67
|
+
result = semaphore.acquire do
|
|
68
|
+
agent_class.call(**(params || {}))
|
|
69
|
+
end
|
|
70
|
+
yield(result, index) if block
|
|
71
|
+
result
|
|
72
|
+
end
|
|
73
|
+
end.map(&:wait)
|
|
74
|
+
end.wait
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Executes a block for each item concurrently
|
|
78
|
+
#
|
|
79
|
+
# @param items [Array] Items to process
|
|
80
|
+
# @param max_concurrent [Integer, nil] Maximum concurrent executions
|
|
81
|
+
# @yield [item] Block to execute for each item
|
|
82
|
+
# @return [Array<Object>] Results in the same order as input
|
|
83
|
+
#
|
|
84
|
+
# @example Process items concurrently
|
|
85
|
+
# RubyLLM::Agents::Async.each(texts, max_concurrent: 10) do |text|
|
|
86
|
+
# SummaryAgent.call(input: text)
|
|
87
|
+
# end
|
|
88
|
+
def each(items, max_concurrent: nil, &block)
|
|
89
|
+
ensure_async_available!
|
|
90
|
+
raise ArgumentError, "Block required" unless block
|
|
91
|
+
|
|
92
|
+
max_concurrent ||= RubyLLM::Agents.configuration.async_max_concurrency
|
|
93
|
+
semaphore = ::Async::Semaphore.new(max_concurrent)
|
|
94
|
+
|
|
95
|
+
Kernel.send(:Async) do
|
|
96
|
+
items.map do |item|
|
|
97
|
+
Kernel.send(:Async) do
|
|
98
|
+
semaphore.acquire do
|
|
99
|
+
yield(item)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end.map(&:wait)
|
|
103
|
+
end.wait
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Executes multiple agents and returns results as they complete
|
|
107
|
+
#
|
|
108
|
+
# Unlike `batch`, this yields results as soon as they're ready,
|
|
109
|
+
# not in order. Useful for progress updates.
|
|
110
|
+
#
|
|
111
|
+
# @param agents_with_params [Array<Array(Class, Hash)>] Array of [AgentClass, params] pairs
|
|
112
|
+
# @param max_concurrent [Integer, nil] Maximum concurrent executions
|
|
113
|
+
# @yield [result, agent_class, index] Block called as each result completes
|
|
114
|
+
# @return [Hash<Integer, Object>] Results keyed by original index
|
|
115
|
+
#
|
|
116
|
+
# @example Stream results as they complete
|
|
117
|
+
# RubyLLM::Agents::Async.stream(agents) do |result, agent_class, index|
|
|
118
|
+
# puts "#{agent_class.name} finished: #{result.content}"
|
|
119
|
+
# end
|
|
120
|
+
def stream(agents_with_params, max_concurrent: nil, &block)
|
|
121
|
+
ensure_async_available!
|
|
122
|
+
|
|
123
|
+
max_concurrent ||= RubyLLM::Agents.configuration.async_max_concurrency
|
|
124
|
+
semaphore = ::Async::Semaphore.new(max_concurrent)
|
|
125
|
+
results = {}
|
|
126
|
+
mutex = Mutex.new
|
|
127
|
+
|
|
128
|
+
Kernel.send(:Async) do |task|
|
|
129
|
+
agents_with_params.each_with_index.map do |(agent_class, params), index|
|
|
130
|
+
Kernel.send(:Async) do
|
|
131
|
+
result = semaphore.acquire do
|
|
132
|
+
agent_class.call(**(params || {}))
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
mutex.synchronize { results[index] = result }
|
|
136
|
+
yield(result, agent_class, index) if block
|
|
137
|
+
end
|
|
138
|
+
end.map(&:wait)
|
|
139
|
+
end.wait
|
|
140
|
+
|
|
141
|
+
results
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Wraps a synchronous agent call in an async task
|
|
145
|
+
#
|
|
146
|
+
# @param agent_class [Class] The agent class to call
|
|
147
|
+
# @param params [Hash] Parameters to pass to the agent
|
|
148
|
+
# @yield [chunk] Optional streaming block
|
|
149
|
+
# @return [Async::Task] The async task (call .wait to get result)
|
|
150
|
+
#
|
|
151
|
+
# @example Fire and forget
|
|
152
|
+
# task = RubyLLM::Agents::Async.call_async(MyAgent, input: "Hello")
|
|
153
|
+
# # ... do other work ...
|
|
154
|
+
# result = task.wait
|
|
155
|
+
def call_async(agent_class, **params, &block)
|
|
156
|
+
ensure_async_available!
|
|
157
|
+
|
|
158
|
+
Kernel.send(:Async) do
|
|
159
|
+
agent_class.call(**params, &block)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Sleeps without blocking other fibers
|
|
164
|
+
#
|
|
165
|
+
# Automatically uses async sleep when in async context,
|
|
166
|
+
# falls back to regular sleep otherwise.
|
|
167
|
+
#
|
|
168
|
+
# @param seconds [Numeric] Duration to sleep
|
|
169
|
+
# @return [void]
|
|
170
|
+
def sleep(seconds)
|
|
171
|
+
if async_context?
|
|
172
|
+
::Async::Task.current.sleep(seconds)
|
|
173
|
+
else
|
|
174
|
+
Kernel.sleep(seconds)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Checks if async gem is available
|
|
179
|
+
#
|
|
180
|
+
# @return [Boolean] true if async gem is loaded
|
|
181
|
+
def available?
|
|
182
|
+
RubyLLM::Agents.configuration.async_available?
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Checks if currently in an async context
|
|
186
|
+
#
|
|
187
|
+
# @return [Boolean] true if inside an Async block
|
|
188
|
+
def async_context?
|
|
189
|
+
RubyLLM::Agents.configuration.async_context?
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
private
|
|
193
|
+
|
|
194
|
+
# Raises an error if async gem is not available
|
|
195
|
+
#
|
|
196
|
+
# @raise [RuntimeError] If async gem is not loaded
|
|
197
|
+
def ensure_async_available!
|
|
198
|
+
return if available?
|
|
199
|
+
|
|
200
|
+
raise <<~ERROR
|
|
201
|
+
Async gem is required for concurrent agent execution.
|
|
202
|
+
|
|
203
|
+
Add to your Gemfile:
|
|
204
|
+
gem 'async'
|
|
205
|
+
|
|
206
|
+
Then:
|
|
207
|
+
bundle install
|
|
208
|
+
|
|
209
|
+
Usage:
|
|
210
|
+
require 'async'
|
|
211
|
+
|
|
212
|
+
Async do
|
|
213
|
+
RubyLLM::Agents::Async.batch([...])
|
|
214
|
+
end
|
|
215
|
+
ERROR
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
class Workflow
|
|
6
|
+
# Fiber-based concurrent executor for parallel workflows
|
|
7
|
+
#
|
|
8
|
+
# Provides an alternative to ThreadPool that uses Ruby's Fiber scheduler
|
|
9
|
+
# for lightweight concurrency. Automatically used when the async gem is
|
|
10
|
+
# available and we're inside an async context.
|
|
11
|
+
#
|
|
12
|
+
# @example Basic usage
|
|
13
|
+
# executor = AsyncExecutor.new(max_concurrent: 4)
|
|
14
|
+
# executor.post { perform_task_1 }
|
|
15
|
+
# executor.post { perform_task_2 }
|
|
16
|
+
# executor.wait_for_completion
|
|
17
|
+
#
|
|
18
|
+
# @example With fail-fast
|
|
19
|
+
# executor = AsyncExecutor.new(max_concurrent: 4)
|
|
20
|
+
# executor.post { risky_task }
|
|
21
|
+
# executor.abort! if something_failed
|
|
22
|
+
# executor.wait_for_completion
|
|
23
|
+
#
|
|
24
|
+
# @api private
|
|
25
|
+
class AsyncExecutor
|
|
26
|
+
attr_reader :max_concurrent
|
|
27
|
+
|
|
28
|
+
# Creates a new async executor
|
|
29
|
+
#
|
|
30
|
+
# @param max_concurrent [Integer] Maximum concurrent fibers (default: 10)
|
|
31
|
+
def initialize(max_concurrent: 10)
|
|
32
|
+
@max_concurrent = max_concurrent
|
|
33
|
+
@tasks = []
|
|
34
|
+
@results = []
|
|
35
|
+
@mutex = Mutex.new
|
|
36
|
+
@aborted = false
|
|
37
|
+
@semaphore = nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Submits a task for execution
|
|
41
|
+
#
|
|
42
|
+
# @yield Block to execute
|
|
43
|
+
# @return [void]
|
|
44
|
+
def post(&block)
|
|
45
|
+
@mutex.synchronize do
|
|
46
|
+
@tasks << block
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Signals that remaining tasks should be skipped
|
|
51
|
+
#
|
|
52
|
+
# Currently running tasks will complete, but pending tasks will be skipped.
|
|
53
|
+
#
|
|
54
|
+
# @return [void]
|
|
55
|
+
def abort!
|
|
56
|
+
@mutex.synchronize do
|
|
57
|
+
@aborted = true
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Returns whether the executor has been aborted
|
|
62
|
+
#
|
|
63
|
+
# @return [Boolean] true if abort! was called
|
|
64
|
+
def aborted?
|
|
65
|
+
@mutex.synchronize { @aborted }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Executes all submitted tasks and waits for completion
|
|
69
|
+
#
|
|
70
|
+
# @param timeout [Integer, nil] Maximum seconds to wait (nil = indefinite)
|
|
71
|
+
# @return [Boolean] true if all tasks completed, false if timeout
|
|
72
|
+
def wait_for_completion(timeout: nil)
|
|
73
|
+
return true if @tasks.empty?
|
|
74
|
+
|
|
75
|
+
ensure_async_available!
|
|
76
|
+
|
|
77
|
+
@semaphore = ::Async::Semaphore.new(@max_concurrent)
|
|
78
|
+
|
|
79
|
+
if timeout
|
|
80
|
+
execute_with_timeout(timeout)
|
|
81
|
+
else
|
|
82
|
+
execute_all
|
|
83
|
+
true
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Shuts down the executor
|
|
88
|
+
#
|
|
89
|
+
# For AsyncExecutor this is a no-op since fibers are garbage collected.
|
|
90
|
+
#
|
|
91
|
+
# @param timeout [Integer] Ignored for async executor
|
|
92
|
+
# @return [void]
|
|
93
|
+
def shutdown(timeout: 5)
|
|
94
|
+
# No-op for fiber-based executor
|
|
95
|
+
# Fibers are lightweight and garbage collected
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Waits for termination (compatibility with ThreadPool)
|
|
99
|
+
#
|
|
100
|
+
# @param timeout [Integer] Ignored for async executor
|
|
101
|
+
# @return [void]
|
|
102
|
+
def wait_for_termination(timeout: 5)
|
|
103
|
+
# No-op for fiber-based executor
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
# Executes all tasks with async
|
|
109
|
+
#
|
|
110
|
+
# @return [void]
|
|
111
|
+
def execute_all
|
|
112
|
+
Kernel.send(:Async) do
|
|
113
|
+
@tasks.map do |task|
|
|
114
|
+
Kernel.send(:Async) do
|
|
115
|
+
next if aborted?
|
|
116
|
+
|
|
117
|
+
@semaphore.acquire do
|
|
118
|
+
next if aborted?
|
|
119
|
+
task.call
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end.map(&:wait)
|
|
123
|
+
end.wait
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Executes all tasks with a timeout
|
|
127
|
+
#
|
|
128
|
+
# @param timeout [Integer] Maximum seconds to wait
|
|
129
|
+
# @return [Boolean] true if completed, false if timeout
|
|
130
|
+
def execute_with_timeout(timeout)
|
|
131
|
+
completed = false
|
|
132
|
+
|
|
133
|
+
Kernel.send(:Async) do |task|
|
|
134
|
+
task.with_timeout(timeout) do
|
|
135
|
+
execute_all
|
|
136
|
+
completed = true
|
|
137
|
+
rescue ::Async::TimeoutError
|
|
138
|
+
completed = false
|
|
139
|
+
end
|
|
140
|
+
end.wait
|
|
141
|
+
|
|
142
|
+
completed
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Ensures async gem is available
|
|
146
|
+
#
|
|
147
|
+
# @raise [RuntimeError] If async gem is not loaded
|
|
148
|
+
def ensure_async_available!
|
|
149
|
+
return if defined?(::Async) && defined?(::Async::Semaphore)
|
|
150
|
+
|
|
151
|
+
raise "AsyncExecutor requires the 'async' gem. Add gem 'async' to your Gemfile."
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "
|
|
4
|
-
require_relative "
|
|
5
|
-
require_relative "
|
|
6
|
-
require_relative "
|
|
7
|
-
require_relative "
|
|
3
|
+
require_relative "result"
|
|
4
|
+
require_relative "instrumentation"
|
|
5
|
+
require_relative "thread_pool"
|
|
6
|
+
require_relative "pipeline"
|
|
7
|
+
require_relative "parallel"
|
|
8
|
+
require_relative "router"
|
|
8
9
|
|
|
9
10
|
module RubyLLM
|
|
10
11
|
module Agents
|
|
@@ -149,17 +149,22 @@ module RubyLLM
|
|
|
149
149
|
results = {}
|
|
150
150
|
errors = {}
|
|
151
151
|
mutex = Mutex.new
|
|
152
|
-
should_abort = false
|
|
153
152
|
|
|
154
|
-
#
|
|
155
|
-
|
|
156
|
-
|
|
153
|
+
# Determine pool size based on concurrency setting
|
|
154
|
+
pool_size = self.class.concurrency || self.class.branches.size
|
|
155
|
+
|
|
156
|
+
# Use async executor when in async context, otherwise use thread pool
|
|
157
|
+
pool = create_executor(pool_size)
|
|
158
|
+
|
|
159
|
+
# Submit all branches to the pool
|
|
160
|
+
self.class.branches.each do |name, config|
|
|
161
|
+
pool.post do
|
|
157
162
|
Thread.current.name = "parallel-#{name}"
|
|
158
163
|
Thread.current[:branch_name] = name
|
|
159
164
|
|
|
160
165
|
begin
|
|
161
|
-
# Check if
|
|
162
|
-
if
|
|
166
|
+
# Check if pool was aborted (fail-fast triggered)
|
|
167
|
+
if pool.aborted?
|
|
163
168
|
mutex.synchronize { results[name] = nil }
|
|
164
169
|
next
|
|
165
170
|
end
|
|
@@ -178,29 +183,24 @@ module RubyLLM
|
|
|
178
183
|
mutex.synchronize do
|
|
179
184
|
results[name] = result
|
|
180
185
|
|
|
181
|
-
# Check for failure
|
|
186
|
+
# Check for failure and trigger fail-fast if needed
|
|
182
187
|
if result.respond_to?(:error?) && result.error? && !config[:optional]
|
|
183
|
-
|
|
188
|
+
pool.abort! if self.class.fail_fast?
|
|
184
189
|
end
|
|
185
190
|
end
|
|
186
191
|
rescue StandardError => e
|
|
187
192
|
mutex.synchronize do
|
|
188
193
|
errors[name] = e
|
|
189
194
|
results[name] = nil
|
|
190
|
-
|
|
195
|
+
pool.abort! if self.class.fail_fast? && !config[:optional]
|
|
191
196
|
end
|
|
192
197
|
end
|
|
193
198
|
end
|
|
194
199
|
end
|
|
195
200
|
|
|
196
|
-
#
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
thread_batch.each(&:join)
|
|
200
|
-
end
|
|
201
|
-
else
|
|
202
|
-
threads.each(&:join)
|
|
203
|
-
end
|
|
201
|
+
# Wait for all branches to complete
|
|
202
|
+
pool.wait_for_completion
|
|
203
|
+
pool.shutdown
|
|
204
204
|
|
|
205
205
|
# Determine overall status
|
|
206
206
|
status = determine_parallel_status(results, errors)
|
|
@@ -276,6 +276,23 @@ module RubyLLM
|
|
|
276
276
|
duration_ms: (((Time.current - @workflow_started_at) * 1000).round if @workflow_started_at)
|
|
277
277
|
)
|
|
278
278
|
end
|
|
279
|
+
|
|
280
|
+
# Creates the appropriate executor based on context
|
|
281
|
+
#
|
|
282
|
+
# Uses AsyncExecutor when in async context for fiber-based concurrency,
|
|
283
|
+
# otherwise falls back to ThreadPool for traditional thread-based execution.
|
|
284
|
+
#
|
|
285
|
+
# @param size [Integer] Number of concurrent workers/fibers
|
|
286
|
+
# @return [ThreadPool, AsyncExecutor] The executor instance
|
|
287
|
+
def create_executor(size)
|
|
288
|
+
config = RubyLLM::Agents.configuration
|
|
289
|
+
|
|
290
|
+
if config.async_context?
|
|
291
|
+
AsyncExecutor.new(max_concurrent: size)
|
|
292
|
+
else
|
|
293
|
+
ThreadPool.new(size: size)
|
|
294
|
+
end
|
|
295
|
+
end
|
|
279
296
|
end
|
|
280
297
|
end
|
|
281
298
|
end
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
class Workflow
|
|
6
|
+
# Simple bounded thread pool for parallel workflow execution
|
|
7
|
+
#
|
|
8
|
+
# Provides a fixed-size pool of worker threads that process submitted tasks.
|
|
9
|
+
# Supports fail-fast abort and graceful shutdown.
|
|
10
|
+
#
|
|
11
|
+
# @example Basic usage
|
|
12
|
+
# pool = ThreadPool.new(size: 4)
|
|
13
|
+
# pool.post { perform_task_1 }
|
|
14
|
+
# pool.post { perform_task_2 }
|
|
15
|
+
# pool.wait_for_completion
|
|
16
|
+
# pool.shutdown
|
|
17
|
+
#
|
|
18
|
+
# @example With fail-fast
|
|
19
|
+
# pool = ThreadPool.new(size: 4)
|
|
20
|
+
# begin
|
|
21
|
+
# pool.post { risky_task }
|
|
22
|
+
# rescue => e
|
|
23
|
+
# pool.abort! # Signal workers to stop
|
|
24
|
+
# end
|
|
25
|
+
# pool.shutdown
|
|
26
|
+
#
|
|
27
|
+
# @api private
|
|
28
|
+
class ThreadPool
|
|
29
|
+
attr_reader :size
|
|
30
|
+
|
|
31
|
+
# Creates a new thread pool
|
|
32
|
+
#
|
|
33
|
+
# @param size [Integer] Number of worker threads (default: 4)
|
|
34
|
+
def initialize(size: 4)
|
|
35
|
+
@size = size
|
|
36
|
+
@queue = Queue.new
|
|
37
|
+
@workers = []
|
|
38
|
+
@mutex = Mutex.new
|
|
39
|
+
@completion_condition = ConditionVariable.new
|
|
40
|
+
@pending_count = 0
|
|
41
|
+
@completed_count = 0
|
|
42
|
+
@aborted = false
|
|
43
|
+
@shutdown = false
|
|
44
|
+
|
|
45
|
+
spawn_workers
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Submits a task to the pool
|
|
49
|
+
#
|
|
50
|
+
# @yield Block to execute in a worker thread
|
|
51
|
+
# @return [void]
|
|
52
|
+
# @raise [RuntimeError] If pool has been shutdown
|
|
53
|
+
def post(&block)
|
|
54
|
+
raise "ThreadPool has been shutdown" if @shutdown
|
|
55
|
+
|
|
56
|
+
@mutex.synchronize do
|
|
57
|
+
@pending_count += 1
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
@queue.push(block)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Signals workers to abort remaining tasks
|
|
64
|
+
#
|
|
65
|
+
# Currently running tasks will complete, but pending tasks will be skipped.
|
|
66
|
+
#
|
|
67
|
+
# @return [void]
|
|
68
|
+
def abort!
|
|
69
|
+
@mutex.synchronize do
|
|
70
|
+
@aborted = true
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Returns whether the pool has been aborted
|
|
75
|
+
#
|
|
76
|
+
# @return [Boolean] true if abort! was called
|
|
77
|
+
def aborted?
|
|
78
|
+
@mutex.synchronize { @aborted }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Waits for all submitted tasks to complete
|
|
82
|
+
#
|
|
83
|
+
# @param timeout [Integer, nil] Maximum seconds to wait (nil = indefinite)
|
|
84
|
+
# @return [Boolean] true if all tasks completed, false if timeout
|
|
85
|
+
def wait_for_completion(timeout: nil)
|
|
86
|
+
deadline = timeout ? Time.current + timeout : nil
|
|
87
|
+
|
|
88
|
+
@mutex.synchronize do
|
|
89
|
+
loop do
|
|
90
|
+
return true if @pending_count == @completed_count
|
|
91
|
+
|
|
92
|
+
if deadline
|
|
93
|
+
remaining = deadline - Time.current
|
|
94
|
+
return false if remaining <= 0
|
|
95
|
+
|
|
96
|
+
@completion_condition.wait(@mutex, remaining)
|
|
97
|
+
else
|
|
98
|
+
@completion_condition.wait(@mutex)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Shuts down the pool and waits for workers to terminate
|
|
105
|
+
#
|
|
106
|
+
# @param timeout [Integer] Maximum seconds to wait for termination
|
|
107
|
+
# @return [void]
|
|
108
|
+
def shutdown(timeout: 5)
|
|
109
|
+
@shutdown = true
|
|
110
|
+
|
|
111
|
+
# Send poison pills to stop workers
|
|
112
|
+
@size.times { @queue.push(nil) }
|
|
113
|
+
|
|
114
|
+
wait_for_termination(timeout: timeout)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Waits for all worker threads to terminate
|
|
118
|
+
#
|
|
119
|
+
# @param timeout [Integer] Maximum seconds to wait
|
|
120
|
+
# @return [void]
|
|
121
|
+
def wait_for_termination(timeout: 5)
|
|
122
|
+
deadline = Time.current + timeout
|
|
123
|
+
|
|
124
|
+
@workers.each do |worker|
|
|
125
|
+
remaining = deadline - Time.current
|
|
126
|
+
break if remaining <= 0
|
|
127
|
+
|
|
128
|
+
worker.join(remaining)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
# Spawns the worker threads
|
|
135
|
+
#
|
|
136
|
+
# @return [void]
|
|
137
|
+
def spawn_workers
|
|
138
|
+
@size.times do |i|
|
|
139
|
+
@workers << Thread.new do
|
|
140
|
+
Thread.current.name = "pool-worker-#{i}"
|
|
141
|
+
worker_loop
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Main worker loop - processes tasks from the queue
|
|
147
|
+
#
|
|
148
|
+
# @return [void]
|
|
149
|
+
def worker_loop
|
|
150
|
+
loop do
|
|
151
|
+
task = @queue.pop
|
|
152
|
+
|
|
153
|
+
# nil is the poison pill - time to exit
|
|
154
|
+
break if task.nil?
|
|
155
|
+
|
|
156
|
+
# Skip if aborted
|
|
157
|
+
if aborted?
|
|
158
|
+
mark_completed
|
|
159
|
+
next
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
begin
|
|
163
|
+
task.call
|
|
164
|
+
rescue StandardError
|
|
165
|
+
# Errors are handled by the task itself (via rescue in the block)
|
|
166
|
+
# We just need to ensure we mark completion
|
|
167
|
+
ensure
|
|
168
|
+
mark_completed
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Marks a task as completed and signals waiters
|
|
174
|
+
#
|
|
175
|
+
# @return [void]
|
|
176
|
+
def mark_completed
|
|
177
|
+
@mutex.synchronize do
|
|
178
|
+
@completed_count += 1
|
|
179
|
+
@completion_condition.broadcast
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|