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.
Files changed (191) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +112 -336
  3. data/app/controllers/concerns/ruby_llm/agents/sortable.rb +0 -1
  4. data/app/controllers/ruby_llm/agents/agents_controller.rb +5 -56
  5. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +22 -106
  6. data/app/controllers/ruby_llm/agents/executions_controller.rb +4 -114
  7. data/app/controllers/ruby_llm/agents/tenants_controller.rb +30 -2
  8. data/app/helpers/ruby_llm/agents/application_helper.rb +19 -53
  9. data/app/models/ruby_llm/agents/execution/analytics.rb +13 -54
  10. data/app/models/ruby_llm/agents/execution/scopes.rb +61 -14
  11. data/app/models/ruby_llm/agents/execution.rb +52 -12
  12. data/app/models/ruby_llm/agents/execution_detail.rb +18 -0
  13. data/app/models/ruby_llm/agents/tenant/budgetable.rb +132 -24
  14. data/app/models/ruby_llm/agents/tenant/incrementable.rb +117 -0
  15. data/app/models/ruby_llm/agents/tenant/resettable.rb +128 -0
  16. data/app/models/ruby_llm/agents/tenant/trackable.rb +46 -12
  17. data/app/models/ruby_llm/agents/tenant.rb +2 -3
  18. data/app/models/ruby_llm/agents/tenant_budget.rb +6 -3
  19. data/app/services/ruby_llm/agents/agent_registry.rb +6 -112
  20. data/app/views/layouts/ruby_llm/agents/application.html.erb +89 -252
  21. data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +71 -218
  22. data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +20 -63
  23. data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +44 -131
  24. data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +16 -57
  25. data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +39 -104
  26. data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +29 -82
  27. data/app/views/ruby_llm/agents/agents/_empty_state.html.erb +4 -14
  28. data/app/views/ruby_llm/agents/agents/index.html.erb +105 -274
  29. data/app/views/ruby_llm/agents/agents/show.html.erb +248 -378
  30. data/app/views/ruby_llm/agents/dashboard/_action_center.html.erb +29 -52
  31. data/app/views/ruby_llm/agents/dashboard/_tenant_budget.html.erb +73 -99
  32. data/app/views/ruby_llm/agents/dashboard/index.html.erb +228 -433
  33. data/app/views/ruby_llm/agents/executions/_execution.html.erb +1 -1
  34. data/app/views/ruby_llm/agents/executions/_filters.html.erb +4 -25
  35. data/app/views/ruby_llm/agents/executions/_list.html.erb +111 -152
  36. data/app/views/ruby_llm/agents/executions/index.html.erb +5 -7
  37. data/app/views/ruby_llm/agents/executions/show.html.erb +526 -1037
  38. data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +5 -21
  39. data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +70 -191
  40. data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +16 -44
  41. data/app/views/ruby_llm/agents/shared/_select_dropdown.html.erb +12 -41
  42. data/app/views/ruby_llm/agents/shared/_status_badge.html.erb +11 -65
  43. data/app/views/ruby_llm/agents/shared/_tenant_filter.html.erb +6 -5
  44. data/app/views/ruby_llm/agents/system_config/show.html.erb +240 -351
  45. data/app/views/ruby_llm/agents/tenants/_form.html.erb +67 -77
  46. data/app/views/ruby_llm/agents/tenants/edit.html.erb +7 -9
  47. data/app/views/ruby_llm/agents/tenants/index.html.erb +100 -122
  48. data/app/views/ruby_llm/agents/tenants/show.html.erb +146 -336
  49. data/config/routes.rb +0 -13
  50. data/lib/generators/ruby_llm_agents/install_generator.rb +13 -17
  51. data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +2 -12
  52. data/lib/generators/ruby_llm_agents/restructure_generator.rb +0 -2
  53. data/lib/generators/ruby_llm_agents/templates/add_usage_counters_to_tenants_migration.rb.tt +37 -0
  54. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +1 -2
  55. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +1 -1
  56. data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +0 -1
  57. data/lib/generators/ruby_llm_agents/templates/create_execution_details_migration.rb.tt +27 -0
  58. data/lib/generators/ruby_llm_agents/templates/create_tenants_migration.rb.tt +25 -0
  59. data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +0 -1
  60. data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +33 -12
  61. data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +40 -71
  62. data/lib/generators/ruby_llm_agents/templates/remove_agent_version_migration.rb.tt +13 -0
  63. data/lib/generators/ruby_llm_agents/templates/remove_workflow_columns_migration.rb.tt +19 -0
  64. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +2 -4
  65. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +0 -1
  66. data/lib/generators/ruby_llm_agents/templates/split_execution_details_migration.rb.tt +232 -0
  67. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +77 -259
  68. data/lib/ruby_llm/agents/audio/speaker.rb +0 -1
  69. data/lib/ruby_llm/agents/audio/transcriber.rb +0 -1
  70. data/lib/ruby_llm/agents/base_agent.rb +54 -23
  71. data/lib/ruby_llm/agents/core/base/callbacks.rb +142 -0
  72. data/lib/ruby_llm/agents/core/base.rb +23 -55
  73. data/lib/ruby_llm/agents/core/configuration.rb +97 -117
  74. data/lib/ruby_llm/agents/core/errors.rb +0 -58
  75. data/lib/ruby_llm/agents/core/instrumentation.rb +157 -110
  76. data/lib/ruby_llm/agents/core/llm_tenant.rb +8 -7
  77. data/lib/ruby_llm/agents/core/version.rb +1 -1
  78. data/lib/ruby_llm/agents/dsl/base.rb +157 -17
  79. data/lib/ruby_llm/agents/dsl/caching.rb +33 -2
  80. data/lib/ruby_llm/agents/dsl/reliability.rb +148 -0
  81. data/lib/ruby_llm/agents/dsl.rb +1 -2
  82. data/lib/ruby_llm/agents/image/analyzer/execution.rb +1 -2
  83. data/lib/ruby_llm/agents/image/background_remover/execution.rb +1 -2
  84. data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +1 -13
  85. data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +2 -2
  86. data/lib/ruby_llm/agents/image/editor/dsl.rb +0 -14
  87. data/lib/ruby_llm/agents/image/editor/execution.rb +1 -10
  88. data/lib/ruby_llm/agents/image/editor.rb +0 -1
  89. data/lib/ruby_llm/agents/image/generator.rb +0 -21
  90. data/lib/ruby_llm/agents/image/pipeline/dsl.rb +0 -13
  91. data/lib/ruby_llm/agents/image/pipeline/execution.rb +0 -1
  92. data/lib/ruby_llm/agents/image/transformer/dsl.rb +0 -13
  93. data/lib/ruby_llm/agents/image/transformer/execution.rb +1 -10
  94. data/lib/ruby_llm/agents/image/transformer.rb +0 -1
  95. data/lib/ruby_llm/agents/image/upscaler/execution.rb +1 -2
  96. data/lib/ruby_llm/agents/image/variator/execution.rb +1 -2
  97. data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +78 -173
  98. data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +66 -2
  99. data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +0 -12
  100. data/lib/ruby_llm/agents/infrastructure/circuit_breaker.rb +10 -13
  101. data/lib/ruby_llm/agents/infrastructure/execution_logger_job.rb +8 -0
  102. data/lib/ruby_llm/agents/pipeline/context.rb +0 -1
  103. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +28 -4
  104. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +3 -10
  105. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +88 -55
  106. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +5 -41
  107. data/lib/ruby_llm/agents/rails/engine.rb +6 -6
  108. data/lib/ruby_llm/agents/results/base.rb +1 -49
  109. data/lib/ruby_llm/agents/text/embedder.rb +0 -1
  110. data/lib/ruby_llm/agents.rb +1 -9
  111. data/lib/tasks/ruby_llm_agents.rake +34 -0
  112. metadata +14 -83
  113. data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +0 -214
  114. data/app/controllers/ruby_llm/agents/workflows_controller.rb +0 -544
  115. data/app/mailers/ruby_llm/agents/alert_mailer.rb +0 -84
  116. data/app/mailers/ruby_llm/agents/application_mailer.rb +0 -28
  117. data/app/models/ruby_llm/agents/api_configuration.rb +0 -386
  118. data/app/models/ruby_llm/agents/execution/workflow.rb +0 -170
  119. data/app/models/ruby_llm/agents/tenant/configurable.rb +0 -135
  120. data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -98
  121. data/app/views/ruby_llm/agents/agents/_version_comparison.html.erb +0 -186
  122. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +0 -126
  123. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +0 -107
  124. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +0 -18
  125. data/app/views/ruby_llm/agents/api_configurations/_api_key_field.html.erb +0 -34
  126. data/app/views/ruby_llm/agents/api_configurations/_form.html.erb +0 -288
  127. data/app/views/ruby_llm/agents/api_configurations/edit.html.erb +0 -95
  128. data/app/views/ruby_llm/agents/api_configurations/edit_tenant.html.erb +0 -97
  129. data/app/views/ruby_llm/agents/api_configurations/show.html.erb +0 -214
  130. data/app/views/ruby_llm/agents/api_configurations/tenant.html.erb +0 -179
  131. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +0 -73
  132. data/app/views/ruby_llm/agents/dashboard/_alerts_feed.html.erb +0 -62
  133. data/app/views/ruby_llm/agents/dashboard/_breaker_strip.html.erb +0 -47
  134. data/app/views/ruby_llm/agents/dashboard/_budgets_bar.html.erb +0 -75
  135. data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +0 -56
  136. data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +0 -115
  137. data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +0 -59
  138. data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +0 -60
  139. data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +0 -86
  140. data/app/views/ruby_llm/agents/executions/dry_run.html.erb +0 -149
  141. data/app/views/ruby_llm/agents/shared/_breadcrumbs.html.erb +0 -48
  142. data/app/views/ruby_llm/agents/shared/_nav_link.html.erb +0 -27
  143. data/app/views/ruby_llm/agents/shared/_stat_card.html.erb +0 -14
  144. data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +0 -35
  145. data/app/views/ruby_llm/agents/workflows/_empty_state.html.erb +0 -22
  146. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +0 -228
  147. data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +0 -539
  148. data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +0 -76
  149. data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +0 -74
  150. data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +0 -108
  151. data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +0 -920
  152. data/app/views/ruby_llm/agents/workflows/index.html.erb +0 -179
  153. data/app/views/ruby_llm/agents/workflows/show.html.erb +0 -467
  154. data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +0 -100
  155. data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +0 -38
  156. data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +0 -48
  157. data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +0 -90
  158. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +0 -551
  159. data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +0 -181
  160. data/lib/ruby_llm/agents/core/base/moderation_execution.rb +0 -274
  161. data/lib/ruby_llm/agents/core/resolved_config.rb +0 -348
  162. data/lib/ruby_llm/agents/image/generator/content_policy.rb +0 -95
  163. data/lib/ruby_llm/agents/infrastructure/redactor.rb +0 -130
  164. data/lib/ruby_llm/agents/results/moderation_result.rb +0 -158
  165. data/lib/ruby_llm/agents/text/moderator.rb +0 -237
  166. data/lib/ruby_llm/agents/workflow/approval.rb +0 -205
  167. data/lib/ruby_llm/agents/workflow/approval_store.rb +0 -179
  168. data/lib/ruby_llm/agents/workflow/async.rb +0 -220
  169. data/lib/ruby_llm/agents/workflow/async_executor.rb +0 -156
  170. data/lib/ruby_llm/agents/workflow/dsl/executor.rb +0 -467
  171. data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +0 -244
  172. data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +0 -289
  173. data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +0 -107
  174. data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +0 -150
  175. data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +0 -187
  176. data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +0 -352
  177. data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +0 -415
  178. data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +0 -257
  179. data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +0 -317
  180. data/lib/ruby_llm/agents/workflow/dsl.rb +0 -576
  181. data/lib/ruby_llm/agents/workflow/instrumentation.rb +0 -249
  182. data/lib/ruby_llm/agents/workflow/notifiers/base.rb +0 -117
  183. data/lib/ruby_llm/agents/workflow/notifiers/email.rb +0 -117
  184. data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +0 -180
  185. data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +0 -121
  186. data/lib/ruby_llm/agents/workflow/notifiers.rb +0 -70
  187. data/lib/ruby_llm/agents/workflow/orchestrator.rb +0 -416
  188. data/lib/ruby_llm/agents/workflow/result.rb +0 -592
  189. data/lib/ruby_llm/agents/workflow/thread_pool.rb +0 -185
  190. data/lib/ruby_llm/agents/workflow/throttle_manager.rb +0 -206
  191. data/lib/ruby_llm/agents/workflow/wait_result.rb +0 -213
@@ -1,205 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "securerandom"
4
-
5
- module RubyLLM
6
- module Agents
7
- class Workflow
8
- # Represents an approval request for human-in-the-loop workflows
9
- #
10
- # Tracks the state of an approval including who created it, who can approve it,
11
- # and the final decision with timestamp and reason.
12
- #
13
- # @example Creating an approval
14
- # approval = Approval.new(
15
- # workflow_id: "order-123",
16
- # workflow_type: "OrderApprovalWorkflow",
17
- # name: :manager_approval,
18
- # metadata: { order_total: 5000 }
19
- # )
20
- #
21
- # @example Approving
22
- # approval.approve!("manager@example.com")
23
- #
24
- # @example Rejecting
25
- # approval.reject!("manager@example.com", reason: "Budget exceeded")
26
- #
27
- # @api public
28
- class Approval
29
- STATUSES = %i[pending approved rejected expired].freeze
30
-
31
- attr_reader :id, :workflow_id, :workflow_type, :name, :status,
32
- :created_at, :metadata, :approvers, :expires_at
33
- attr_accessor :approved_by, :approved_at, :rejected_by, :rejected_at,
34
- :reason, :reminded_at
35
-
36
- # @param workflow_id [String] The workflow instance ID
37
- # @param workflow_type [String] The workflow class name
38
- # @param name [Symbol] The approval point name
39
- # @param approvers [Array<String>] List of user IDs who can approve
40
- # @param expires_at [Time, nil] When the approval expires
41
- # @param metadata [Hash] Additional context for the approval
42
- def initialize(workflow_id:, workflow_type:, name:, approvers: [], expires_at: nil, metadata: {})
43
- @id = SecureRandom.uuid
44
- @workflow_id = workflow_id
45
- @workflow_type = workflow_type
46
- @name = name
47
- @status = :pending
48
- @approvers = approvers
49
- @expires_at = expires_at
50
- @metadata = metadata
51
- @created_at = Time.now
52
- end
53
-
54
- # Approve the request
55
- #
56
- # @param user_id [String] The user approving
57
- # @param comment [String, nil] Optional comment
58
- # @return [void]
59
- def approve!(user_id, comment: nil)
60
- raise InvalidStateError, "Cannot approve: status is #{status}" unless pending?
61
-
62
- @status = :approved
63
- @approved_by = user_id
64
- @approved_at = Time.now
65
- @metadata[:approval_comment] = comment if comment
66
- end
67
-
68
- # Reject the request
69
- #
70
- # @param user_id [String] The user rejecting
71
- # @param reason [String, nil] Reason for rejection
72
- # @return [void]
73
- def reject!(user_id, reason: nil)
74
- raise InvalidStateError, "Cannot reject: status is #{status}" unless pending?
75
-
76
- @status = :rejected
77
- @rejected_by = user_id
78
- @rejected_at = Time.now
79
- @reason = reason
80
- end
81
-
82
- # Expire the approval request
83
- #
84
- # @return [void]
85
- def expire!
86
- raise InvalidStateError, "Cannot expire: status is #{status}" unless pending?
87
-
88
- @status = :expired
89
- end
90
-
91
- # Check if approval is still pending
92
- #
93
- # @return [Boolean]
94
- def pending?
95
- status == :pending
96
- end
97
-
98
- # Check if approval was granted
99
- #
100
- # @return [Boolean]
101
- def approved?
102
- status == :approved
103
- end
104
-
105
- # Check if approval was rejected
106
- #
107
- # @return [Boolean]
108
- def rejected?
109
- status == :rejected
110
- end
111
-
112
- # Check if approval has expired
113
- #
114
- # @return [Boolean]
115
- def expired?
116
- status == :expired
117
- end
118
-
119
- # Check if the approval has timed out
120
- #
121
- # @return [Boolean]
122
- def timed_out?
123
- return false unless expires_at
124
-
125
- Time.now > expires_at && pending?
126
- end
127
-
128
- # Check if a user can approve this request
129
- #
130
- # @param user_id [String] The user to check
131
- # @return [Boolean]
132
- def can_approve?(user_id)
133
- return true if approvers.empty? # Anyone can approve if no restrictions
134
-
135
- approvers.include?(user_id)
136
- end
137
-
138
- # Duration since creation
139
- #
140
- # @return [Float] Seconds since creation
141
- def age
142
- Time.now - created_at
143
- end
144
-
145
- # Duration until expiry
146
- #
147
- # @return [Float, nil] Seconds until expiry, nil if no expiry
148
- def time_until_expiry
149
- return nil unless expires_at
150
-
151
- expires_at - Time.now
152
- end
153
-
154
- # Mark that a reminder was sent
155
- #
156
- # @return [void]
157
- def mark_reminded!
158
- @reminded_at = Time.now
159
- end
160
-
161
- # Check if a reminder should be sent
162
- #
163
- # @param reminder_after [Integer] Seconds after creation to send reminder
164
- # @param reminder_interval [Integer, nil] Interval between reminders
165
- # @return [Boolean]
166
- def should_remind?(reminder_after, reminder_interval: nil)
167
- return false unless pending?
168
- return false if age < reminder_after
169
-
170
- if reminded_at && reminder_interval
171
- Time.now - reminded_at >= reminder_interval
172
- else
173
- reminded_at.nil?
174
- end
175
- end
176
-
177
- # Convert to hash for serialization
178
- #
179
- # @return [Hash]
180
- def to_h
181
- {
182
- id: id,
183
- workflow_id: workflow_id,
184
- workflow_type: workflow_type,
185
- name: name,
186
- status: status,
187
- approvers: approvers,
188
- approved_by: approved_by,
189
- approved_at: approved_at,
190
- rejected_by: rejected_by,
191
- rejected_at: rejected_at,
192
- reason: reason,
193
- expires_at: expires_at,
194
- reminded_at: reminded_at,
195
- metadata: metadata,
196
- created_at: created_at
197
- }.compact
198
- end
199
-
200
- # Error for invalid state transitions
201
- class InvalidStateError < StandardError; end
202
- end
203
- end
204
- end
205
- end
@@ -1,179 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyLLM
4
- module Agents
5
- class Workflow
6
- # Abstract base class for approval storage
7
- #
8
- # Provides a common interface for storing and retrieving approval requests.
9
- # Implementations can use in-memory storage, databases, Redis, etc.
10
- #
11
- # @example Setting a custom store
12
- # RubyLLM::Agents::Workflow::ApprovalStore.store = MyRedisStore.new
13
- #
14
- # @example Using the default store
15
- # store = RubyLLM::Agents::Workflow::ApprovalStore.store
16
- # store.save(approval)
17
- #
18
- # @api public
19
- class ApprovalStore
20
- class << self
21
- # Returns the configured store instance
22
- #
23
- # @return [ApprovalStore]
24
- def store
25
- @store ||= default_store
26
- end
27
-
28
- # Sets the store instance
29
- #
30
- # @param store [ApprovalStore] The store to use
31
- # @return [void]
32
- def store=(store)
33
- @store = store
34
- end
35
-
36
- # Resets to the default store (useful for testing)
37
- #
38
- # @return [void]
39
- def reset!
40
- @store = nil
41
- end
42
-
43
- private
44
-
45
- def default_store
46
- MemoryApprovalStore.new
47
- end
48
- end
49
-
50
- # Save an approval
51
- #
52
- # @param approval [Approval] The approval to save
53
- # @return [Approval] The saved approval
54
- def save(approval)
55
- raise NotImplementedError, "#{self.class}#save must be implemented"
56
- end
57
-
58
- # Find an approval by ID
59
- #
60
- # @param id [String] The approval ID
61
- # @return [Approval, nil]
62
- def find(id)
63
- raise NotImplementedError, "#{self.class}#find must be implemented"
64
- end
65
-
66
- # Find all approvals for a workflow
67
- #
68
- # @param workflow_id [String] The workflow ID
69
- # @return [Array<Approval>]
70
- def find_by_workflow(workflow_id)
71
- raise NotImplementedError, "#{self.class}#find_by_workflow must be implemented"
72
- end
73
-
74
- # Find pending approvals for a user
75
- #
76
- # @param user_id [String] The user ID
77
- # @return [Array<Approval>]
78
- def pending_for_user(user_id)
79
- raise NotImplementedError, "#{self.class}#pending_for_user must be implemented"
80
- end
81
-
82
- # Find all pending approvals
83
- #
84
- # @return [Array<Approval>]
85
- def all_pending
86
- raise NotImplementedError, "#{self.class}#all_pending must be implemented"
87
- end
88
-
89
- # Delete an approval
90
- #
91
- # @param id [String] The approval ID
92
- # @return [Boolean] true if deleted
93
- def delete(id)
94
- raise NotImplementedError, "#{self.class}#delete must be implemented"
95
- end
96
-
97
- # Delete all approvals (useful for testing)
98
- #
99
- # @return [void]
100
- def clear!
101
- raise NotImplementedError, "#{self.class}#clear! must be implemented"
102
- end
103
- end
104
-
105
- # In-memory approval store for development and testing
106
- #
107
- # Thread-safe storage using a Mutex.
108
- #
109
- # @api public
110
- class MemoryApprovalStore < ApprovalStore
111
- def initialize
112
- @approvals = {}
113
- @mutex = Mutex.new
114
- end
115
-
116
- # @see ApprovalStore#save
117
- def save(approval)
118
- @mutex.synchronize do
119
- @approvals[approval.id] = approval
120
- end
121
- approval
122
- end
123
-
124
- # @see ApprovalStore#find
125
- def find(id)
126
- @mutex.synchronize do
127
- @approvals[id]
128
- end
129
- end
130
-
131
- # @see ApprovalStore#find_by_workflow
132
- def find_by_workflow(workflow_id)
133
- @mutex.synchronize do
134
- @approvals.values.select { |a| a.workflow_id == workflow_id }
135
- end
136
- end
137
-
138
- # @see ApprovalStore#pending_for_user
139
- def pending_for_user(user_id)
140
- @mutex.synchronize do
141
- @approvals.values.select do |a|
142
- a.pending? && a.can_approve?(user_id)
143
- end
144
- end
145
- end
146
-
147
- # @see ApprovalStore#all_pending
148
- def all_pending
149
- @mutex.synchronize do
150
- @approvals.values.select(&:pending?)
151
- end
152
- end
153
-
154
- # @see ApprovalStore#delete
155
- def delete(id)
156
- @mutex.synchronize do
157
- !!@approvals.delete(id)
158
- end
159
- end
160
-
161
- # @see ApprovalStore#clear!
162
- def clear!
163
- @mutex.synchronize do
164
- @approvals.clear
165
- end
166
- end
167
-
168
- # Returns the count of stored approvals
169
- #
170
- # @return [Integer]
171
- def count
172
- @mutex.synchronize do
173
- @approvals.size
174
- end
175
- end
176
- end
177
- end
178
- end
179
- end
@@ -1,220 +0,0 @@
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