ruby_llm-agents 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/ruby_llm/agents/paginatable.rb +9 -3
  3. data/app/controllers/concerns/ruby_llm/agents/sortable.rb +58 -0
  4. data/app/controllers/ruby_llm/agents/agents_controller.rb +59 -16
  5. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +144 -20
  6. data/app/controllers/ruby_llm/agents/executions_controller.rb +13 -16
  7. data/app/controllers/ruby_llm/agents/workflows_controller.rb +279 -90
  8. data/app/helpers/ruby_llm/agents/application_helper.rb +100 -0
  9. data/app/mailers/ruby_llm/agents/alert_mailer.rb +84 -0
  10. data/app/mailers/ruby_llm/agents/application_mailer.rb +28 -0
  11. data/app/models/ruby_llm/agents/execution/analytics.rb +170 -20
  12. data/app/models/ruby_llm/agents/execution/scopes.rb +0 -31
  13. data/app/models/ruby_llm/agents/execution/workflow.rb +0 -129
  14. data/app/models/ruby_llm/agents/execution.rb +50 -14
  15. data/app/models/ruby_llm/agents/tenant/budgetable.rb +277 -0
  16. data/app/models/ruby_llm/agents/tenant/configurable.rb +135 -0
  17. data/app/models/ruby_llm/agents/tenant/trackable.rb +310 -0
  18. data/app/models/ruby_llm/agents/tenant.rb +146 -0
  19. data/app/models/ruby_llm/agents/tenant_budget.rb +12 -253
  20. data/app/services/ruby_llm/agents/agent_registry.rb +18 -12
  21. data/app/views/layouts/ruby_llm/agents/application.html.erb +72 -76
  22. data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -12
  23. data/app/views/ruby_llm/agents/agents/_sortable_header.html.erb +56 -0
  24. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +5 -15
  25. data/app/views/ruby_llm/agents/agents/index.html.erb +271 -100
  26. data/app/views/ruby_llm/agents/agents/show.html.erb +1 -0
  27. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +107 -0
  28. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +18 -0
  29. data/app/views/ruby_llm/agents/api_configurations/show.html.erb +4 -1
  30. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +66 -359
  31. data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +56 -0
  32. data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +115 -0
  33. data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +35 -60
  34. data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +17 -6
  35. data/app/views/ruby_llm/agents/dashboard/index.html.erb +373 -72
  36. data/app/views/ruby_llm/agents/executions/_execution.html.erb +0 -1
  37. data/app/views/ruby_llm/agents/executions/_filters.html.erb +51 -39
  38. data/app/views/ruby_llm/agents/executions/_list.html.erb +53 -195
  39. data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +5 -20
  40. data/app/views/ruby_llm/agents/executions/index.html.erb +7 -83
  41. data/app/views/ruby_llm/agents/executions/show.html.erb +10 -20
  42. data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +2 -1
  43. data/app/views/ruby_llm/agents/shared/_doc_link.html.erb +12 -0
  44. data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +3 -15
  45. data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +1 -1
  46. data/app/views/ruby_llm/agents/shared/_select_dropdown.html.erb +1 -1
  47. data/app/views/ruby_llm/agents/shared/_sortable_header.html.erb +53 -0
  48. data/app/views/ruby_llm/agents/shared/_status_badge.html.erb +7 -0
  49. data/app/views/ruby_llm/agents/shared/_status_dot.html.erb +1 -1
  50. data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +9 -35
  51. data/app/views/ruby_llm/agents/system_config/show.html.erb +4 -1
  52. data/app/views/ruby_llm/agents/tenants/index.html.erb +4 -1
  53. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +7 -15
  54. data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +539 -0
  55. data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +920 -0
  56. data/app/views/ruby_llm/agents/workflows/index.html.erb +179 -0
  57. data/app/views/ruby_llm/agents/workflows/show.html.erb +164 -139
  58. data/config/routes.rb +1 -1
  59. data/lib/generators/ruby_llm_agents/agent_generator.rb +6 -36
  60. data/lib/generators/ruby_llm_agents/background_remover_generator.rb +7 -37
  61. data/lib/generators/ruby_llm_agents/embedder_generator.rb +5 -38
  62. data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +7 -37
  63. data/lib/generators/ruby_llm_agents/image_editor_generator.rb +7 -37
  64. data/lib/generators/ruby_llm_agents/image_generator_generator.rb +8 -41
  65. data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +18 -46
  66. data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +7 -37
  67. data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +7 -37
  68. data/lib/generators/ruby_llm_agents/image_variator_generator.rb +7 -37
  69. data/lib/generators/ruby_llm_agents/install_generator.rb +33 -56
  70. data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +480 -0
  71. data/lib/generators/ruby_llm_agents/multi_tenancy_generator.rb +42 -22
  72. data/lib/generators/ruby_llm_agents/restructure_generator.rb +2 -2
  73. data/lib/generators/ruby_llm_agents/speaker_generator.rb +8 -39
  74. data/lib/generators/ruby_llm_agents/templates/add_tenant_to_executions_migration.rb.tt +13 -2
  75. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +5 -8
  76. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +40 -42
  77. data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +20 -22
  78. data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +24 -26
  79. data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +20 -22
  80. data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +19 -17
  81. data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +31 -33
  82. data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +125 -127
  83. data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +20 -18
  84. data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +19 -17
  85. data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +19 -17
  86. data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +38 -40
  87. data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +42 -44
  88. data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +48 -0
  89. data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +19 -21
  90. data/lib/generators/ruby_llm_agents/templates/create_tenant_budgets_migration.rb.tt +11 -0
  91. data/lib/generators/ruby_llm_agents/templates/create_tenants_migration.rb.tt +72 -0
  92. data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +19 -21
  93. data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +20 -22
  94. data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +15 -17
  95. data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +25 -27
  96. data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +19 -21
  97. data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +20 -22
  98. data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +17 -19
  99. data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +15 -17
  100. data/lib/generators/ruby_llm_agents/templates/rename_tenant_budgets_to_tenants_migration.rb.tt +34 -0
  101. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +87 -24
  102. data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +21 -27
  103. data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +46 -54
  104. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +31 -39
  105. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +22 -28
  106. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +53 -63
  107. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +46 -56
  108. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +23 -31
  109. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +22 -30
  110. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +23 -31
  111. data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +38 -46
  112. data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +7 -7
  113. data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +59 -71
  114. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +274 -23
  115. data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +29 -31
  116. data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +28 -30
  117. data/lib/generators/ruby_llm_agents/transcriber_generator.rb +10 -43
  118. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +26 -0
  119. data/lib/ruby_llm/agents/core/configuration.rb +55 -43
  120. data/lib/ruby_llm/agents/core/llm_tenant.rb +60 -60
  121. data/lib/ruby_llm/agents/core/version.rb +1 -1
  122. data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +26 -0
  123. data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +4 -2
  124. data/lib/ruby_llm/agents/pipeline.rb +69 -0
  125. data/lib/ruby_llm/agents/workflow/approval.rb +205 -0
  126. data/lib/ruby_llm/agents/workflow/approval_store.rb +179 -0
  127. data/lib/ruby_llm/agents/workflow/dsl/executor.rb +467 -0
  128. data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +244 -0
  129. data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +289 -0
  130. data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +107 -0
  131. data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +150 -0
  132. data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +187 -0
  133. data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +352 -0
  134. data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +415 -0
  135. data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +257 -0
  136. data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +317 -0
  137. data/lib/ruby_llm/agents/workflow/dsl.rb +576 -0
  138. data/lib/ruby_llm/agents/workflow/instrumentation.rb +2 -7
  139. data/lib/ruby_llm/agents/workflow/notifiers/base.rb +117 -0
  140. data/lib/ruby_llm/agents/workflow/notifiers/email.rb +117 -0
  141. data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +180 -0
  142. data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +121 -0
  143. data/lib/ruby_llm/agents/workflow/notifiers.rb +70 -0
  144. data/lib/ruby_llm/agents/workflow/orchestrator.rb +190 -23
  145. data/lib/ruby_llm/agents/workflow/result.rb +202 -0
  146. data/lib/ruby_llm/agents/workflow/throttle_manager.rb +206 -0
  147. data/lib/ruby_llm/agents/workflow/wait_result.rb +213 -0
  148. metadata +43 -6
  149. data/app/views/ruby_llm/agents/dashboard/_execution_item.html.erb +0 -66
  150. data/lib/ruby_llm/agents/workflow/parallel.rb +0 -299
  151. data/lib/ruby_llm/agents/workflow/pipeline.rb +0 -306
  152. data/lib/ruby_llm/agents/workflow/router.rb +0 -429
@@ -0,0 +1,317 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ class Workflow
6
+ module DSL
7
+ # Executes wait steps within a workflow
8
+ #
9
+ # Handles the four types of waits:
10
+ # - delay: Simple time-based pause
11
+ # - until: Poll until a condition is met
12
+ # - schedule: Wait until a specific time
13
+ # - approval: Wait for human approval
14
+ #
15
+ # @example Executing a delay wait
16
+ # executor = WaitExecutor.new(wait_config, workflow)
17
+ # result = executor.execute
18
+ #
19
+ # @api private
20
+ class WaitExecutor
21
+ # @param config [WaitConfig] The wait configuration
22
+ # @param workflow [Workflow] The workflow instance
23
+ # @param approval_store [ApprovalStore, nil] Custom approval store
24
+ def initialize(config, workflow, approval_store: nil)
25
+ @config = config
26
+ @workflow = workflow
27
+ @approval_store = approval_store || ApprovalStore.store
28
+ end
29
+
30
+ # Execute the wait step
31
+ #
32
+ # @return [WaitResult] The result of the wait
33
+ def execute
34
+ # Check conditions first
35
+ unless @config.should_execute?(@workflow)
36
+ return WaitResult.skipped(@config.type, reason: "Condition not met")
37
+ end
38
+
39
+ case @config.type
40
+ when :delay
41
+ execute_delay
42
+ when :until
43
+ execute_until
44
+ when :schedule
45
+ execute_schedule
46
+ when :approval
47
+ execute_approval
48
+ else
49
+ raise ArgumentError, "Unknown wait type: #{@config.type}"
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ # Execute a simple delay
56
+ #
57
+ # @return [WaitResult]
58
+ def execute_delay
59
+ duration = resolve_duration(@config.duration)
60
+ sleep_with_interruption(duration)
61
+ WaitResult.success(:delay, duration)
62
+ end
63
+
64
+ # Execute a conditional wait (polling)
65
+ #
66
+ # @return [WaitResult]
67
+ def execute_until
68
+ started_at = Time.now
69
+ interval = normalize_duration(@config.poll_interval)
70
+ timeout = @config.timeout ? normalize_duration(@config.timeout) : nil
71
+ max_interval = @config.max_interval ? normalize_duration(@config.max_interval) : nil
72
+
73
+ loop do
74
+ # Check condition
75
+ if evaluate_condition(@config.condition)
76
+ waited = Time.now - started_at
77
+ return WaitResult.success(:until, waited)
78
+ end
79
+
80
+ # Check timeout
81
+ if timeout
82
+ elapsed = Time.now - started_at
83
+ if elapsed >= timeout
84
+ return handle_timeout(:until, elapsed)
85
+ end
86
+ end
87
+
88
+ # Wait before next poll
89
+ sleep_with_interruption(interval)
90
+
91
+ # Apply exponential backoff if configured
92
+ if @config.exponential_backoff?
93
+ interval = apply_backoff(interval, max_interval)
94
+ end
95
+ end
96
+ end
97
+
98
+ # Execute a scheduled wait
99
+ #
100
+ # @return [WaitResult]
101
+ def execute_schedule
102
+ target_time = evaluate_time(@config.condition)
103
+
104
+ unless target_time.is_a?(Time)
105
+ raise ArgumentError, "Schedule condition must return a Time, got #{target_time.class}"
106
+ end
107
+
108
+ wait_duration = target_time - Time.now
109
+
110
+ if wait_duration > 0
111
+ sleep_with_interruption(wait_duration)
112
+ end
113
+
114
+ WaitResult.success(:schedule, [wait_duration, 0].max, target_time: target_time)
115
+ end
116
+
117
+ # Execute a human approval wait
118
+ #
119
+ # @return [WaitResult]
120
+ def execute_approval
121
+ started_at = Time.now
122
+
123
+ # Create approval request
124
+ approval = create_approval_request
125
+
126
+ # Save to store
127
+ @approval_store.save(approval)
128
+
129
+ # Send notifications
130
+ send_notifications(approval)
131
+
132
+ # Set up reminder tracking
133
+ reminder_sent = false
134
+ reminder_after = @config.reminder_after ? normalize_duration(@config.reminder_after) : nil
135
+ reminder_interval = @config.reminder_interval ? normalize_duration(@config.reminder_interval) : nil
136
+
137
+ # Poll for approval or timeout
138
+ timeout = @config.timeout ? normalize_duration(@config.timeout) : nil
139
+ poll_interval = normalize_duration(@config.poll_interval)
140
+
141
+ loop do
142
+ # Refresh approval from store
143
+ approval = @approval_store.find(approval.id)
144
+
145
+ unless approval
146
+ waited = Time.now - started_at
147
+ return WaitResult.timeout(:approval, waited, :fail,
148
+ error: "Approval not found")
149
+ end
150
+
151
+ # Check if approved
152
+ if approval.approved?
153
+ waited = Time.now - started_at
154
+ return WaitResult.approved(approval.id, approval.approved_by, waited)
155
+ end
156
+
157
+ # Check if rejected
158
+ if approval.rejected?
159
+ waited = Time.now - started_at
160
+ return WaitResult.rejected(approval.id, approval.rejected_by, waited,
161
+ reason: approval.reason)
162
+ end
163
+
164
+ # Check if expired
165
+ if approval.expired? || approval.timed_out?
166
+ waited = Time.now - started_at
167
+ return handle_timeout(:approval, waited, approval_id: approval.id)
168
+ end
169
+
170
+ # Check timeout
171
+ if timeout
172
+ elapsed = Time.now - started_at
173
+ if elapsed >= timeout
174
+ approval.expire!
175
+ @approval_store.save(approval)
176
+ return handle_timeout(:approval, elapsed, approval_id: approval.id)
177
+ end
178
+ end
179
+
180
+ # Check if reminder should be sent
181
+ if reminder_after && approval.should_remind?(reminder_after,
182
+ reminder_interval: reminder_interval)
183
+ send_reminder(approval)
184
+ approval.mark_reminded!
185
+ @approval_store.save(approval)
186
+ end
187
+
188
+ # Wait before next poll
189
+ sleep_with_interruption(poll_interval)
190
+ end
191
+ end
192
+
193
+ def create_approval_request
194
+ Approval.new(
195
+ workflow_id: @workflow.object_id.to_s,
196
+ workflow_type: @workflow.class.name,
197
+ name: @config.name,
198
+ approvers: @config.approvers,
199
+ expires_at: @config.timeout ? Time.now + normalize_duration(@config.timeout) : nil,
200
+ metadata: {
201
+ workflow_input: @workflow.input.to_h,
202
+ created_by: "workflow"
203
+ }
204
+ )
205
+ end
206
+
207
+ def send_notifications(approval)
208
+ return if @config.notify_channels.empty?
209
+
210
+ message = resolve_message(@config.message, approval)
211
+ Notifiers.notify(approval, message, channels: @config.notify_channels)
212
+ end
213
+
214
+ def send_reminder(approval)
215
+ return if @config.notify_channels.empty?
216
+
217
+ message = resolve_message(@config.message, approval)
218
+ @config.notify_channels.each do |channel|
219
+ notifier = Notifiers[channel]
220
+ notifier&.remind(approval, message)
221
+ end
222
+ end
223
+
224
+ def resolve_message(message_config, approval)
225
+ case message_config
226
+ when String
227
+ message_config
228
+ when Proc
229
+ @workflow.instance_exec(approval, &message_config)
230
+ else
231
+ "Approval required: #{approval.name}"
232
+ end
233
+ end
234
+
235
+ def handle_timeout(type, elapsed, **metadata)
236
+ action = @config.on_timeout
237
+
238
+ case action
239
+ when :continue
240
+ WaitResult.timeout(type, elapsed, :continue, **metadata)
241
+ when :skip_next
242
+ WaitResult.timeout(type, elapsed, :skip_next, **metadata)
243
+ when :escalate
244
+ handle_escalation(type, elapsed, metadata)
245
+ else # :fail
246
+ WaitResult.timeout(type, elapsed, :fail, **metadata)
247
+ end
248
+ end
249
+
250
+ def handle_escalation(type, elapsed, metadata)
251
+ if @config.escalate_to
252
+ # Create escalated approval or notify escalation target
253
+ WaitResult.timeout(type, elapsed, :escalate,
254
+ escalated_to: @config.escalate_to,
255
+ **metadata)
256
+ else
257
+ WaitResult.timeout(type, elapsed, :fail, **metadata)
258
+ end
259
+ end
260
+
261
+ def resolve_duration(duration)
262
+ normalize_duration(duration)
263
+ end
264
+
265
+ def normalize_duration(duration)
266
+ if duration.respond_to?(:to_f)
267
+ duration.to_f
268
+ else
269
+ duration.to_i.to_f
270
+ end
271
+ end
272
+
273
+ def evaluate_condition(condition)
274
+ case condition
275
+ when Proc
276
+ @workflow.instance_exec(&condition)
277
+ when Symbol
278
+ @workflow.send(condition)
279
+ else
280
+ !!condition
281
+ end
282
+ end
283
+
284
+ def evaluate_time(time_config)
285
+ case time_config
286
+ when Proc
287
+ @workflow.instance_exec(&time_config)
288
+ when Time
289
+ time_config
290
+ else
291
+ raise ArgumentError, "Schedule time must be a Time or Proc, got #{time_config.class}"
292
+ end
293
+ end
294
+
295
+ def apply_backoff(current_interval, max_interval)
296
+ new_interval = current_interval * @config.backoff
297
+
298
+ if max_interval
299
+ [new_interval, max_interval].min
300
+ else
301
+ new_interval
302
+ end
303
+ end
304
+
305
+ def sleep_with_interruption(duration)
306
+ # Use Async.sleep if available, otherwise Kernel.sleep
307
+ if defined?(::Async::Task) && ::Async::Task.current?
308
+ ::Async::Task.current.sleep(duration)
309
+ else
310
+ Kernel.sleep(duration)
311
+ end
312
+ end
313
+ end
314
+ end
315
+ end
316
+ end
317
+ end