ruby_llm-agents 1.3.4 → 2.1.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/README.md +112 -336
- data/app/controllers/concerns/ruby_llm/agents/sortable.rb +0 -1
- data/app/controllers/ruby_llm/agents/agents_controller.rb +5 -56
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +22 -106
- data/app/controllers/ruby_llm/agents/executions_controller.rb +4 -114
- data/app/controllers/ruby_llm/agents/tenants_controller.rb +30 -2
- data/app/helpers/ruby_llm/agents/application_helper.rb +19 -53
- data/app/models/ruby_llm/agents/execution/analytics.rb +13 -54
- data/app/models/ruby_llm/agents/execution/scopes.rb +61 -14
- data/app/models/ruby_llm/agents/execution.rb +52 -12
- data/app/models/ruby_llm/agents/execution_detail.rb +18 -0
- data/app/models/ruby_llm/agents/tenant/budgetable.rb +132 -24
- data/app/models/ruby_llm/agents/tenant/incrementable.rb +117 -0
- data/app/models/ruby_llm/agents/tenant/resettable.rb +128 -0
- data/app/models/ruby_llm/agents/tenant/trackable.rb +46 -12
- data/app/models/ruby_llm/agents/tenant.rb +2 -3
- data/app/models/ruby_llm/agents/tenant_budget.rb +6 -3
- data/app/services/ruby_llm/agents/agent_registry.rb +6 -112
- data/app/views/layouts/ruby_llm/agents/application.html.erb +89 -252
- data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +71 -218
- data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +20 -63
- data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +44 -131
- data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +16 -57
- data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +39 -104
- data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +29 -82
- data/app/views/ruby_llm/agents/agents/_empty_state.html.erb +4 -14
- data/app/views/ruby_llm/agents/agents/index.html.erb +105 -274
- data/app/views/ruby_llm/agents/agents/show.html.erb +248 -378
- data/app/views/ruby_llm/agents/dashboard/_action_center.html.erb +29 -52
- data/app/views/ruby_llm/agents/dashboard/_tenant_budget.html.erb +73 -99
- data/app/views/ruby_llm/agents/dashboard/index.html.erb +228 -433
- data/app/views/ruby_llm/agents/executions/_execution.html.erb +1 -1
- data/app/views/ruby_llm/agents/executions/_filters.html.erb +4 -25
- data/app/views/ruby_llm/agents/executions/_list.html.erb +111 -152
- data/app/views/ruby_llm/agents/executions/index.html.erb +5 -7
- data/app/views/ruby_llm/agents/executions/show.html.erb +526 -1037
- data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +5 -21
- data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +70 -191
- data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +16 -44
- data/app/views/ruby_llm/agents/shared/_select_dropdown.html.erb +12 -41
- data/app/views/ruby_llm/agents/shared/_status_badge.html.erb +11 -65
- data/app/views/ruby_llm/agents/shared/_tenant_filter.html.erb +6 -5
- data/app/views/ruby_llm/agents/system_config/show.html.erb +240 -351
- data/app/views/ruby_llm/agents/tenants/_form.html.erb +67 -77
- data/app/views/ruby_llm/agents/tenants/edit.html.erb +7 -9
- data/app/views/ruby_llm/agents/tenants/index.html.erb +100 -122
- data/app/views/ruby_llm/agents/tenants/show.html.erb +146 -336
- data/config/routes.rb +0 -13
- data/lib/generators/ruby_llm_agents/install_generator.rb +13 -17
- data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +2 -12
- data/lib/generators/ruby_llm_agents/restructure_generator.rb +0 -2
- data/lib/generators/ruby_llm_agents/templates/add_usage_counters_to_tenants_migration.rb.tt +37 -0
- data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +1 -2
- data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +1 -1
- data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +0 -1
- data/lib/generators/ruby_llm_agents/templates/create_execution_details_migration.rb.tt +27 -0
- data/lib/generators/ruby_llm_agents/templates/create_tenants_migration.rb.tt +25 -0
- data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +0 -1
- data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +33 -12
- data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +40 -71
- data/lib/generators/ruby_llm_agents/templates/remove_agent_version_migration.rb.tt +13 -0
- data/lib/generators/ruby_llm_agents/templates/remove_workflow_columns_migration.rb.tt +19 -0
- data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +2 -4
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +0 -1
- data/lib/generators/ruby_llm_agents/templates/split_execution_details_migration.rb.tt +232 -0
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +77 -259
- data/lib/ruby_llm/agents/audio/speaker.rb +0 -1
- data/lib/ruby_llm/agents/audio/transcriber.rb +0 -1
- data/lib/ruby_llm/agents/base_agent.rb +54 -23
- data/lib/ruby_llm/agents/core/base/callbacks.rb +142 -0
- data/lib/ruby_llm/agents/core/base.rb +23 -55
- data/lib/ruby_llm/agents/core/configuration.rb +97 -117
- data/lib/ruby_llm/agents/core/errors.rb +0 -58
- data/lib/ruby_llm/agents/core/instrumentation.rb +157 -110
- data/lib/ruby_llm/agents/core/llm_tenant.rb +8 -7
- data/lib/ruby_llm/agents/core/version.rb +1 -1
- data/lib/ruby_llm/agents/dsl/base.rb +157 -17
- data/lib/ruby_llm/agents/dsl/caching.rb +33 -2
- data/lib/ruby_llm/agents/dsl/reliability.rb +148 -0
- data/lib/ruby_llm/agents/dsl.rb +1 -2
- data/lib/ruby_llm/agents/image/analyzer/execution.rb +1 -2
- data/lib/ruby_llm/agents/image/background_remover/execution.rb +1 -2
- data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +1 -13
- data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +2 -2
- data/lib/ruby_llm/agents/image/editor/dsl.rb +0 -14
- data/lib/ruby_llm/agents/image/editor/execution.rb +1 -10
- data/lib/ruby_llm/agents/image/editor.rb +0 -1
- data/lib/ruby_llm/agents/image/generator.rb +0 -21
- data/lib/ruby_llm/agents/image/pipeline/dsl.rb +0 -13
- data/lib/ruby_llm/agents/image/pipeline/execution.rb +0 -1
- data/lib/ruby_llm/agents/image/transformer/dsl.rb +0 -13
- data/lib/ruby_llm/agents/image/transformer/execution.rb +1 -10
- data/lib/ruby_llm/agents/image/transformer.rb +0 -1
- data/lib/ruby_llm/agents/image/upscaler/execution.rb +1 -2
- data/lib/ruby_llm/agents/image/variator/execution.rb +1 -2
- data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +78 -173
- data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +66 -2
- data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +0 -12
- data/lib/ruby_llm/agents/infrastructure/circuit_breaker.rb +10 -13
- data/lib/ruby_llm/agents/infrastructure/execution_logger_job.rb +8 -0
- data/lib/ruby_llm/agents/pipeline/context.rb +0 -1
- data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +28 -4
- data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +3 -10
- data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +88 -55
- data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +5 -41
- data/lib/ruby_llm/agents/rails/engine.rb +6 -6
- data/lib/ruby_llm/agents/results/base.rb +1 -49
- data/lib/ruby_llm/agents/text/embedder.rb +0 -1
- data/lib/ruby_llm/agents.rb +1 -9
- data/lib/tasks/ruby_llm_agents.rake +34 -0
- metadata +14 -83
- data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +0 -214
- data/app/controllers/ruby_llm/agents/workflows_controller.rb +0 -544
- data/app/mailers/ruby_llm/agents/alert_mailer.rb +0 -84
- data/app/mailers/ruby_llm/agents/application_mailer.rb +0 -28
- data/app/models/ruby_llm/agents/api_configuration.rb +0 -386
- data/app/models/ruby_llm/agents/execution/workflow.rb +0 -170
- data/app/models/ruby_llm/agents/tenant/configurable.rb +0 -135
- data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -98
- data/app/views/ruby_llm/agents/agents/_version_comparison.html.erb +0 -186
- data/app/views/ruby_llm/agents/agents/_workflow.html.erb +0 -126
- data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +0 -107
- data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +0 -18
- data/app/views/ruby_llm/agents/api_configurations/_api_key_field.html.erb +0 -34
- data/app/views/ruby_llm/agents/api_configurations/_form.html.erb +0 -288
- data/app/views/ruby_llm/agents/api_configurations/edit.html.erb +0 -95
- data/app/views/ruby_llm/agents/api_configurations/edit_tenant.html.erb +0 -97
- data/app/views/ruby_llm/agents/api_configurations/show.html.erb +0 -214
- data/app/views/ruby_llm/agents/api_configurations/tenant.html.erb +0 -179
- data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +0 -73
- data/app/views/ruby_llm/agents/dashboard/_alerts_feed.html.erb +0 -62
- data/app/views/ruby_llm/agents/dashboard/_breaker_strip.html.erb +0 -47
- data/app/views/ruby_llm/agents/dashboard/_budgets_bar.html.erb +0 -75
- data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +0 -56
- data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +0 -115
- data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +0 -59
- data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +0 -60
- data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +0 -86
- data/app/views/ruby_llm/agents/executions/dry_run.html.erb +0 -149
- data/app/views/ruby_llm/agents/shared/_breadcrumbs.html.erb +0 -48
- data/app/views/ruby_llm/agents/shared/_nav_link.html.erb +0 -27
- data/app/views/ruby_llm/agents/shared/_stat_card.html.erb +0 -14
- data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +0 -35
- data/app/views/ruby_llm/agents/workflows/_empty_state.html.erb +0 -22
- data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +0 -228
- data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +0 -539
- data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +0 -76
- data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +0 -74
- data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +0 -108
- data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +0 -920
- data/app/views/ruby_llm/agents/workflows/index.html.erb +0 -179
- data/app/views/ruby_llm/agents/workflows/show.html.erb +0 -467
- data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +0 -100
- data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +0 -38
- data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +0 -48
- data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +0 -90
- data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +0 -551
- data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +0 -181
- data/lib/ruby_llm/agents/core/base/moderation_execution.rb +0 -274
- data/lib/ruby_llm/agents/core/resolved_config.rb +0 -348
- data/lib/ruby_llm/agents/image/generator/content_policy.rb +0 -95
- data/lib/ruby_llm/agents/infrastructure/redactor.rb +0 -130
- data/lib/ruby_llm/agents/results/moderation_result.rb +0 -158
- data/lib/ruby_llm/agents/text/moderator.rb +0 -237
- data/lib/ruby_llm/agents/workflow/approval.rb +0 -205
- data/lib/ruby_llm/agents/workflow/approval_store.rb +0 -179
- data/lib/ruby_llm/agents/workflow/async.rb +0 -220
- data/lib/ruby_llm/agents/workflow/async_executor.rb +0 -156
- data/lib/ruby_llm/agents/workflow/dsl/executor.rb +0 -467
- data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +0 -244
- data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +0 -289
- data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +0 -107
- data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +0 -150
- data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +0 -187
- data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +0 -352
- data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +0 -415
- data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +0 -257
- data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +0 -317
- data/lib/ruby_llm/agents/workflow/dsl.rb +0 -576
- data/lib/ruby_llm/agents/workflow/instrumentation.rb +0 -249
- data/lib/ruby_llm/agents/workflow/notifiers/base.rb +0 -117
- data/lib/ruby_llm/agents/workflow/notifiers/email.rb +0 -117
- data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +0 -180
- data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +0 -121
- data/lib/ruby_llm/agents/workflow/notifiers.rb +0 -70
- data/lib/ruby_llm/agents/workflow/orchestrator.rb +0 -416
- data/lib/ruby_llm/agents/workflow/result.rb +0 -592
- data/lib/ruby_llm/agents/workflow/thread_pool.rb +0 -185
- data/lib/ruby_llm/agents/workflow/throttle_manager.rb +0 -206
- data/lib/ruby_llm/agents/workflow/wait_result.rb +0 -213
|
@@ -1,185 +0,0 @@
|
|
|
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
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RubyLLM
|
|
4
|
-
module Agents
|
|
5
|
-
class Workflow
|
|
6
|
-
# Manages rate limiting and throttling for workflow steps
|
|
7
|
-
#
|
|
8
|
-
# Provides two modes of rate limiting:
|
|
9
|
-
# 1. Throttle: Ensures minimum time between executions of the same step
|
|
10
|
-
# 2. Rate limit: Limits the number of calls within a time window (token bucket)
|
|
11
|
-
#
|
|
12
|
-
# Thread-safe using a Mutex for concurrent access.
|
|
13
|
-
#
|
|
14
|
-
# @example Using throttle
|
|
15
|
-
# manager = ThrottleManager.new
|
|
16
|
-
# manager.throttle("step:fetch", 1.0) # Wait at least 1 second between calls
|
|
17
|
-
#
|
|
18
|
-
# @example Using rate limit
|
|
19
|
-
# manager = ThrottleManager.new
|
|
20
|
-
# manager.rate_limit("api:external", calls: 10, per: 60) # 10 calls per minute
|
|
21
|
-
#
|
|
22
|
-
# @api private
|
|
23
|
-
class ThrottleManager
|
|
24
|
-
def initialize
|
|
25
|
-
@last_execution = {}
|
|
26
|
-
@rate_limiters = {}
|
|
27
|
-
@mutex = Mutex.new
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# Throttle execution to ensure minimum time between calls
|
|
31
|
-
#
|
|
32
|
-
# Blocks the current thread if necessary to maintain the minimum interval.
|
|
33
|
-
#
|
|
34
|
-
# @param key [String] Unique identifier for the throttle target
|
|
35
|
-
# @param duration [Float, Integer] Minimum seconds between executions
|
|
36
|
-
# @return [Float] Actual seconds waited (0 if no wait needed)
|
|
37
|
-
def throttle(key, duration)
|
|
38
|
-
duration_seconds = normalize_duration(duration)
|
|
39
|
-
|
|
40
|
-
@mutex.synchronize do
|
|
41
|
-
last = @last_execution[key]
|
|
42
|
-
waited = 0
|
|
43
|
-
|
|
44
|
-
if last
|
|
45
|
-
elapsed = Time.now - last
|
|
46
|
-
remaining = duration_seconds - elapsed
|
|
47
|
-
|
|
48
|
-
if remaining > 0
|
|
49
|
-
@mutex.sleep(remaining)
|
|
50
|
-
waited = remaining
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
@last_execution[key] = Time.now
|
|
55
|
-
waited
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# Check if a call would be throttled without actually waiting
|
|
60
|
-
#
|
|
61
|
-
# @param key [String] Unique identifier for the throttle target
|
|
62
|
-
# @param duration [Float, Integer] Minimum seconds between executions
|
|
63
|
-
# @return [Float] Seconds until next allowed execution (0 if ready)
|
|
64
|
-
def throttle_remaining(key, duration)
|
|
65
|
-
duration_seconds = normalize_duration(duration)
|
|
66
|
-
|
|
67
|
-
@mutex.synchronize do
|
|
68
|
-
last = @last_execution[key]
|
|
69
|
-
return 0 unless last
|
|
70
|
-
|
|
71
|
-
elapsed = Time.now - last
|
|
72
|
-
remaining = duration_seconds - elapsed
|
|
73
|
-
[remaining, 0].max
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
# Apply rate limiting using a token bucket algorithm
|
|
78
|
-
#
|
|
79
|
-
# Blocks until a token is available if the rate limit is exceeded.
|
|
80
|
-
#
|
|
81
|
-
# @param key [String] Unique identifier for the rate limit target
|
|
82
|
-
# @param calls [Integer] Number of calls allowed per window
|
|
83
|
-
# @param per [Float, Integer] Time window in seconds
|
|
84
|
-
# @return [Float] Seconds waited (0 if no wait needed)
|
|
85
|
-
def rate_limit(key, calls:, per:)
|
|
86
|
-
per_seconds = normalize_duration(per)
|
|
87
|
-
bucket = get_or_create_bucket(key, calls, per_seconds)
|
|
88
|
-
|
|
89
|
-
@mutex.synchronize do
|
|
90
|
-
waited = bucket.acquire
|
|
91
|
-
waited
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
# Check if a call would be rate limited without consuming a token
|
|
96
|
-
#
|
|
97
|
-
# @param key [String] Unique identifier for the rate limit target
|
|
98
|
-
# @param calls [Integer] Number of calls allowed per window
|
|
99
|
-
# @param per [Float, Integer] Time window in seconds
|
|
100
|
-
# @return [Boolean] true if a call would be allowed immediately
|
|
101
|
-
def rate_limit_available?(key, calls:, per:)
|
|
102
|
-
per_seconds = normalize_duration(per)
|
|
103
|
-
bucket = get_or_create_bucket(key, calls, per_seconds)
|
|
104
|
-
|
|
105
|
-
@mutex.synchronize do
|
|
106
|
-
bucket.available?
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
# Reset throttle state for a specific key
|
|
111
|
-
#
|
|
112
|
-
# @param key [String] The throttle key to reset
|
|
113
|
-
# @return [void]
|
|
114
|
-
def reset_throttle(key)
|
|
115
|
-
@mutex.synchronize do
|
|
116
|
-
@last_execution.delete(key)
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
# Reset rate limiter state for a specific key
|
|
121
|
-
#
|
|
122
|
-
# @param key [String] The rate limiter key to reset
|
|
123
|
-
# @return [void]
|
|
124
|
-
def reset_rate_limit(key)
|
|
125
|
-
@mutex.synchronize do
|
|
126
|
-
@rate_limiters.delete(key)
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
# Reset all throttle and rate limit state
|
|
131
|
-
#
|
|
132
|
-
# @return [void]
|
|
133
|
-
def reset_all!
|
|
134
|
-
@mutex.synchronize do
|
|
135
|
-
@last_execution.clear
|
|
136
|
-
@rate_limiters.clear
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
private
|
|
141
|
-
|
|
142
|
-
def normalize_duration(duration)
|
|
143
|
-
if duration.respond_to?(:to_f)
|
|
144
|
-
duration.to_f
|
|
145
|
-
else
|
|
146
|
-
duration.to_i.to_f
|
|
147
|
-
end
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def get_or_create_bucket(key, calls, per)
|
|
151
|
-
@rate_limiters[key] ||= TokenBucket.new(calls, per)
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
# Simple token bucket implementation for rate limiting
|
|
155
|
-
#
|
|
156
|
-
# @api private
|
|
157
|
-
class TokenBucket
|
|
158
|
-
def initialize(capacity, refill_time)
|
|
159
|
-
@capacity = capacity
|
|
160
|
-
@refill_time = refill_time
|
|
161
|
-
@tokens = capacity.to_f
|
|
162
|
-
@last_refill = Time.now
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
# Try to acquire a token, waiting if necessary
|
|
166
|
-
#
|
|
167
|
-
# @return [Float] Seconds waited
|
|
168
|
-
def acquire
|
|
169
|
-
refill
|
|
170
|
-
waited = 0
|
|
171
|
-
|
|
172
|
-
if @tokens < 1
|
|
173
|
-
# Calculate wait time for next token
|
|
174
|
-
tokens_needed = 1 - @tokens
|
|
175
|
-
wait_time = tokens_needed * @refill_time / @capacity
|
|
176
|
-
sleep(wait_time)
|
|
177
|
-
waited = wait_time
|
|
178
|
-
refill
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
@tokens -= 1
|
|
182
|
-
waited
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
# Check if a token is available without consuming it
|
|
186
|
-
#
|
|
187
|
-
# @return [Boolean]
|
|
188
|
-
def available?
|
|
189
|
-
refill
|
|
190
|
-
@tokens >= 1
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
private
|
|
194
|
-
|
|
195
|
-
def refill
|
|
196
|
-
now = Time.now
|
|
197
|
-
elapsed = now - @last_refill
|
|
198
|
-
refill_amount = elapsed * @capacity / @refill_time
|
|
199
|
-
@tokens = [@tokens + refill_amount, @capacity].min
|
|
200
|
-
@last_refill = now
|
|
201
|
-
end
|
|
202
|
-
end
|
|
203
|
-
end
|
|
204
|
-
end
|
|
205
|
-
end
|
|
206
|
-
end
|
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RubyLLM
|
|
4
|
-
module Agents
|
|
5
|
-
class Workflow
|
|
6
|
-
# Result object for wait step execution
|
|
7
|
-
#
|
|
8
|
-
# Encapsulates the outcome of a wait operation including success/failure status,
|
|
9
|
-
# duration waited, and any metadata like approval details.
|
|
10
|
-
#
|
|
11
|
-
# @example Success result
|
|
12
|
-
# WaitResult.success(:delay, 5.0)
|
|
13
|
-
#
|
|
14
|
-
# @example Timeout result
|
|
15
|
-
# WaitResult.timeout(:until, 60.0, :fail)
|
|
16
|
-
#
|
|
17
|
-
# @example Approval result
|
|
18
|
-
# WaitResult.approved("approval-123", "user@example.com", 3600.0)
|
|
19
|
-
#
|
|
20
|
-
# @api private
|
|
21
|
-
class WaitResult
|
|
22
|
-
STATUSES = %i[success timeout approved rejected skipped].freeze
|
|
23
|
-
|
|
24
|
-
attr_reader :type, :status, :waited_duration, :metadata
|
|
25
|
-
|
|
26
|
-
# @param type [Symbol] Wait type (:delay, :until, :schedule, :approval)
|
|
27
|
-
# @param status [Symbol] Result status (:success, :timeout, :approved, :rejected, :skipped)
|
|
28
|
-
# @param waited_duration [Float, nil] Duration waited in seconds
|
|
29
|
-
# @param metadata [Hash] Additional result metadata
|
|
30
|
-
def initialize(type:, status:, waited_duration: nil, metadata: {})
|
|
31
|
-
@type = type
|
|
32
|
-
@status = status
|
|
33
|
-
@waited_duration = waited_duration
|
|
34
|
-
@metadata = metadata
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# Creates a success result
|
|
38
|
-
#
|
|
39
|
-
# @param type [Symbol] Wait type
|
|
40
|
-
# @param waited_duration [Float] Duration waited
|
|
41
|
-
# @param metadata [Hash] Additional metadata
|
|
42
|
-
# @return [WaitResult]
|
|
43
|
-
def self.success(type, waited_duration, **metadata)
|
|
44
|
-
new(
|
|
45
|
-
type: type,
|
|
46
|
-
status: :success,
|
|
47
|
-
waited_duration: waited_duration,
|
|
48
|
-
metadata: metadata
|
|
49
|
-
)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# Creates a timeout result
|
|
53
|
-
#
|
|
54
|
-
# @param type [Symbol] Wait type
|
|
55
|
-
# @param waited_duration [Float] Duration waited before timeout
|
|
56
|
-
# @param action_taken [Symbol] Action taken on timeout (:fail, :continue, :skip_next)
|
|
57
|
-
# @param metadata [Hash] Additional metadata
|
|
58
|
-
# @return [WaitResult]
|
|
59
|
-
def self.timeout(type, waited_duration, action_taken, **metadata)
|
|
60
|
-
new(
|
|
61
|
-
type: type,
|
|
62
|
-
status: :timeout,
|
|
63
|
-
waited_duration: waited_duration,
|
|
64
|
-
metadata: metadata.merge(action_taken: action_taken)
|
|
65
|
-
)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# Creates a skipped result (when condition not met)
|
|
69
|
-
#
|
|
70
|
-
# @param type [Symbol] Wait type
|
|
71
|
-
# @param reason [String, nil] Reason for skipping
|
|
72
|
-
# @return [WaitResult]
|
|
73
|
-
def self.skipped(type, reason: nil)
|
|
74
|
-
new(
|
|
75
|
-
type: type,
|
|
76
|
-
status: :skipped,
|
|
77
|
-
waited_duration: 0,
|
|
78
|
-
metadata: { reason: reason }.compact
|
|
79
|
-
)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
# Creates an approved result for approval waits
|
|
83
|
-
#
|
|
84
|
-
# @param approval_id [String] Approval identifier
|
|
85
|
-
# @param approved_by [String] User who approved
|
|
86
|
-
# @param waited_duration [Float] Duration waited for approval
|
|
87
|
-
# @param metadata [Hash] Additional metadata
|
|
88
|
-
# @return [WaitResult]
|
|
89
|
-
def self.approved(approval_id, approved_by, waited_duration, **metadata)
|
|
90
|
-
new(
|
|
91
|
-
type: :approval,
|
|
92
|
-
status: :approved,
|
|
93
|
-
waited_duration: waited_duration,
|
|
94
|
-
metadata: metadata.merge(
|
|
95
|
-
approval_id: approval_id,
|
|
96
|
-
approved_by: approved_by
|
|
97
|
-
)
|
|
98
|
-
)
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
# Creates a rejected result for approval waits
|
|
102
|
-
#
|
|
103
|
-
# @param approval_id [String] Approval identifier
|
|
104
|
-
# @param rejected_by [String] User who rejected
|
|
105
|
-
# @param waited_duration [Float] Duration waited before rejection
|
|
106
|
-
# @param reason [String, nil] Rejection reason
|
|
107
|
-
# @param metadata [Hash] Additional metadata
|
|
108
|
-
# @return [WaitResult]
|
|
109
|
-
def self.rejected(approval_id, rejected_by, waited_duration, reason: nil, **metadata)
|
|
110
|
-
new(
|
|
111
|
-
type: :approval,
|
|
112
|
-
status: :rejected,
|
|
113
|
-
waited_duration: waited_duration,
|
|
114
|
-
metadata: metadata.merge(
|
|
115
|
-
approval_id: approval_id,
|
|
116
|
-
rejected_by: rejected_by,
|
|
117
|
-
reason: reason
|
|
118
|
-
).compact
|
|
119
|
-
)
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
# Returns whether the wait completed successfully
|
|
123
|
-
#
|
|
124
|
-
# @return [Boolean]
|
|
125
|
-
def success?
|
|
126
|
-
status == :success || status == :approved
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
# Returns whether the wait timed out
|
|
130
|
-
#
|
|
131
|
-
# @return [Boolean]
|
|
132
|
-
def timeout?
|
|
133
|
-
status == :timeout
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
# Returns whether the wait was skipped
|
|
137
|
-
#
|
|
138
|
-
# @return [Boolean]
|
|
139
|
-
def skipped?
|
|
140
|
-
status == :skipped
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
# Returns whether an approval was granted
|
|
144
|
-
#
|
|
145
|
-
# @return [Boolean]
|
|
146
|
-
def approved?
|
|
147
|
-
status == :approved
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
# Returns whether an approval was rejected
|
|
151
|
-
#
|
|
152
|
-
# @return [Boolean]
|
|
153
|
-
def rejected?
|
|
154
|
-
status == :rejected
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
# Returns whether the workflow should continue after this wait
|
|
158
|
-
#
|
|
159
|
-
# @return [Boolean]
|
|
160
|
-
def should_continue?
|
|
161
|
-
success? || skipped? || (timeout? && metadata[:action_taken] == :continue)
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
# Returns whether the next step should be skipped
|
|
165
|
-
#
|
|
166
|
-
# @return [Boolean]
|
|
167
|
-
def should_skip_next?
|
|
168
|
-
timeout? && metadata[:action_taken] == :skip_next
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
# Returns the action taken on timeout
|
|
172
|
-
#
|
|
173
|
-
# @return [Symbol, nil]
|
|
174
|
-
def timeout_action
|
|
175
|
-
metadata[:action_taken]
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
# Returns the approval ID for approval waits
|
|
179
|
-
#
|
|
180
|
-
# @return [String, nil]
|
|
181
|
-
def approval_id
|
|
182
|
-
metadata[:approval_id]
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
# Returns who approved/rejected for approval waits
|
|
186
|
-
#
|
|
187
|
-
# @return [String, nil]
|
|
188
|
-
def actor
|
|
189
|
-
metadata[:approved_by] || metadata[:rejected_by]
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
# Returns the rejection reason
|
|
193
|
-
#
|
|
194
|
-
# @return [String, nil]
|
|
195
|
-
def rejection_reason
|
|
196
|
-
metadata[:reason]
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
# Converts to hash for serialization
|
|
200
|
-
#
|
|
201
|
-
# @return [Hash]
|
|
202
|
-
def to_h
|
|
203
|
-
{
|
|
204
|
-
type: type,
|
|
205
|
-
status: status,
|
|
206
|
-
waited_duration: waited_duration,
|
|
207
|
-
metadata: metadata
|
|
208
|
-
}
|
|
209
|
-
end
|
|
210
|
-
end
|
|
211
|
-
end
|
|
212
|
-
end
|
|
213
|
-
end
|