ruby_llm-agents 1.3.3 → 2.0.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 (192) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +101 -334
  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 +46 -10
  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 +87 -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 +528 -989
  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 +9 -14
  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 +9 -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 +58 -262
  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 +52 -6
  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 +58 -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/attempt_tracker.rb +1 -0
  99. data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +66 -2
  100. data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +0 -12
  101. data/lib/ruby_llm/agents/infrastructure/circuit_breaker.rb +10 -13
  102. data/lib/ruby_llm/agents/infrastructure/reliability.rb +37 -2
  103. data/lib/ruby_llm/agents/pipeline/context.rb +0 -1
  104. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +28 -4
  105. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +3 -10
  106. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +88 -55
  107. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +5 -41
  108. data/lib/ruby_llm/agents/rails/engine.rb +6 -6
  109. data/lib/ruby_llm/agents/results/base.rb +1 -49
  110. data/lib/ruby_llm/agents/text/embedder.rb +0 -1
  111. data/lib/ruby_llm/agents.rb +1 -9
  112. data/lib/tasks/ruby_llm_agents.rake +34 -0
  113. metadata +12 -81
  114. data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +0 -214
  115. data/app/controllers/ruby_llm/agents/workflows_controller.rb +0 -544
  116. data/app/mailers/ruby_llm/agents/alert_mailer.rb +0 -84
  117. data/app/mailers/ruby_llm/agents/application_mailer.rb +0 -28
  118. data/app/models/ruby_llm/agents/api_configuration.rb +0 -386
  119. data/app/models/ruby_llm/agents/execution/workflow.rb +0 -170
  120. data/app/models/ruby_llm/agents/tenant/configurable.rb +0 -135
  121. data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -98
  122. data/app/views/ruby_llm/agents/agents/_version_comparison.html.erb +0 -186
  123. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +0 -126
  124. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +0 -107
  125. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +0 -18
  126. data/app/views/ruby_llm/agents/api_configurations/_api_key_field.html.erb +0 -34
  127. data/app/views/ruby_llm/agents/api_configurations/_form.html.erb +0 -288
  128. data/app/views/ruby_llm/agents/api_configurations/edit.html.erb +0 -95
  129. data/app/views/ruby_llm/agents/api_configurations/edit_tenant.html.erb +0 -97
  130. data/app/views/ruby_llm/agents/api_configurations/show.html.erb +0 -214
  131. data/app/views/ruby_llm/agents/api_configurations/tenant.html.erb +0 -179
  132. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +0 -73
  133. data/app/views/ruby_llm/agents/dashboard/_alerts_feed.html.erb +0 -62
  134. data/app/views/ruby_llm/agents/dashboard/_breaker_strip.html.erb +0 -47
  135. data/app/views/ruby_llm/agents/dashboard/_budgets_bar.html.erb +0 -75
  136. data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +0 -56
  137. data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +0 -115
  138. data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +0 -59
  139. data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +0 -60
  140. data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +0 -86
  141. data/app/views/ruby_llm/agents/executions/dry_run.html.erb +0 -149
  142. data/app/views/ruby_llm/agents/shared/_breadcrumbs.html.erb +0 -48
  143. data/app/views/ruby_llm/agents/shared/_nav_link.html.erb +0 -27
  144. data/app/views/ruby_llm/agents/shared/_stat_card.html.erb +0 -14
  145. data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +0 -35
  146. data/app/views/ruby_llm/agents/workflows/_empty_state.html.erb +0 -22
  147. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +0 -228
  148. data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +0 -539
  149. data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +0 -76
  150. data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +0 -74
  151. data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +0 -108
  152. data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +0 -920
  153. data/app/views/ruby_llm/agents/workflows/index.html.erb +0 -179
  154. data/app/views/ruby_llm/agents/workflows/show.html.erb +0 -467
  155. data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +0 -100
  156. data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +0 -38
  157. data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +0 -48
  158. data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +0 -90
  159. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +0 -551
  160. data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +0 -181
  161. data/lib/ruby_llm/agents/core/base/moderation_execution.rb +0 -274
  162. data/lib/ruby_llm/agents/core/resolved_config.rb +0 -348
  163. data/lib/ruby_llm/agents/image/generator/content_policy.rb +0 -95
  164. data/lib/ruby_llm/agents/infrastructure/redactor.rb +0 -130
  165. data/lib/ruby_llm/agents/results/moderation_result.rb +0 -158
  166. data/lib/ruby_llm/agents/text/moderator.rb +0 -237
  167. data/lib/ruby_llm/agents/workflow/approval.rb +0 -205
  168. data/lib/ruby_llm/agents/workflow/approval_store.rb +0 -179
  169. data/lib/ruby_llm/agents/workflow/async.rb +0 -220
  170. data/lib/ruby_llm/agents/workflow/async_executor.rb +0 -156
  171. data/lib/ruby_llm/agents/workflow/dsl/executor.rb +0 -467
  172. data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +0 -244
  173. data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +0 -289
  174. data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +0 -107
  175. data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +0 -150
  176. data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +0 -187
  177. data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +0 -352
  178. data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +0 -415
  179. data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +0 -257
  180. data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +0 -317
  181. data/lib/ruby_llm/agents/workflow/dsl.rb +0 -576
  182. data/lib/ruby_llm/agents/workflow/instrumentation.rb +0 -249
  183. data/lib/ruby_llm/agents/workflow/notifiers/base.rb +0 -117
  184. data/lib/ruby_llm/agents/workflow/notifiers/email.rb +0 -117
  185. data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +0 -180
  186. data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +0 -121
  187. data/lib/ruby_llm/agents/workflow/notifiers.rb +0 -70
  188. data/lib/ruby_llm/agents/workflow/orchestrator.rb +0 -416
  189. data/lib/ruby_llm/agents/workflow/result.rb +0 -592
  190. data/lib/ruby_llm/agents/workflow/thread_pool.rb +0 -185
  191. data/lib/ruby_llm/agents/workflow/throttle_manager.rb +0 -206
  192. data/lib/ruby_llm/agents/workflow/wait_result.rb +0 -213
@@ -57,25 +57,51 @@ module RubyLLM
57
57
 
58
58
  # Checks budget before execution
59
59
  #
60
+ # For tenants, checks budget via counter columns on the tenant model.
61
+ # For non-tenant usage, falls back to BudgetTracker (cache-based).
62
+ #
60
63
  # @param context [Context] The execution context
61
64
  # @raise [BudgetExceededError] If budget exceeded with hard enforcement
62
65
  def check_budget!(context)
66
+ if context.tenant_id.present?
67
+ tenant = RubyLLM::Agents::Tenant.find_by(tenant_id: context.tenant_id)
68
+ if tenant
69
+ tenant.check_budget!(context.agent_class&.name)
70
+ return
71
+ end
72
+ end
73
+
74
+ # Fallback to cache-based checking (non-tenant or no tenant record)
63
75
  BudgetTracker.check_budget!(
64
76
  context.agent_class&.name,
65
77
  tenant_id: context.tenant_id
66
78
  )
67
79
  rescue RubyLLM::Agents::Reliability::BudgetExceededError
68
- # Re-raise budget errors
69
80
  raise
70
81
  rescue StandardError => e
71
- # Log but don't fail on budget check errors
72
82
  error("Budget check failed: #{e.message}")
73
83
  end
74
84
 
75
85
  # Records spend after execution
76
86
  #
87
+ # For tenants, uses atomic SQL increment via tenant.record_execution!.
88
+ # For non-tenant usage, falls back to BudgetTracker (cache-based).
89
+ #
77
90
  # @param context [Context] The execution context
78
91
  def record_spend!(context)
92
+ if context.tenant_id.present?
93
+ tenant = RubyLLM::Agents::Tenant.find_by(tenant_id: context.tenant_id)
94
+ if tenant
95
+ tenant.record_execution!(
96
+ cost: context.total_cost || 0,
97
+ tokens: context.total_tokens || 0,
98
+ error: context.error?
99
+ )
100
+ return
101
+ end
102
+ end
103
+
104
+ # Fallback for non-tenant usage
79
105
  return unless context.total_cost&.positive?
80
106
 
81
107
  BudgetTracker.record_spend!(
@@ -84,7 +110,6 @@ module RubyLLM
84
110
  tenant_id: context.tenant_id
85
111
  )
86
112
 
87
- # Also record tokens if available
88
113
  if context.total_tokens&.positive?
89
114
  BudgetTracker.record_tokens!(
90
115
  context.agent_class&.name,
@@ -93,7 +118,6 @@ module RubyLLM
93
118
  )
94
119
  end
95
120
  rescue StandardError => e
96
- # Log but don't fail on spend recording errors
97
121
  error("Failed to record spend: #{e.message}")
98
122
  end
99
123
  end
@@ -23,13 +23,6 @@ module RubyLLM
23
23
  # cache_for 1.hour
24
24
  # end
25
25
  #
26
- # @example Cache versioning
27
- # class MyEmbedder < RubyLLM::Agents::Embedder
28
- # model "text-embedding-3-small"
29
- # version "2.0" # Change to invalidate cache
30
- # cache_for 1.hour
31
- # end
32
- #
33
26
  class Cache < Base
34
27
  # Process caching
35
28
  #
@@ -91,14 +84,15 @@ module RubyLLM
91
84
 
92
85
  # Generates a cache key for the context
93
86
  #
94
- # The cache key includes:
87
+ # Cache keys are content-based, including:
95
88
  # - Namespace prefix
96
89
  # - Agent type
97
90
  # - Agent class name
98
- # - Version (for cache invalidation)
99
91
  # - Model
100
92
  # - SHA256 hash of input
101
93
  #
94
+ # This means caches automatically invalidate when inputs change.
95
+ #
102
96
  # @param context [Context] The execution context
103
97
  # @return [String] The cache key
104
98
  def generate_cache_key(context)
@@ -106,7 +100,6 @@ module RubyLLM
106
100
  "ruby_llm_agents",
107
101
  context.agent_type,
108
102
  context.agent_class&.name,
109
- config(:version, "1.0"),
110
103
  context.model,
111
104
  hash_input(context.input)
112
105
  ].compact
@@ -18,7 +18,6 @@ module RubyLLM
18
18
  # Tracking is enabled/disabled per agent type via configuration:
19
19
  # - track_executions (conversation agents)
20
20
  # - track_embeddings
21
- # - track_moderations
22
21
  # - track_image_generations
23
22
  # - track_audio
24
23
  #
@@ -92,7 +91,15 @@ module RubyLLM
92
91
  return nil if context.cached? && !track_cache_hits?
93
92
 
94
93
  data = build_running_execution_data(context)
95
- Execution.create!(data)
94
+ execution = Execution.create!(data)
95
+
96
+ # Create detail record with parameters
97
+ params = sanitize_parameters(context)
98
+ if params.present? && params != {}
99
+ execution.create_detail!(parameters: params)
100
+ end
101
+
102
+ execution
96
103
  rescue StandardError => e
97
104
  error("Failed to create running execution record: #{e.message}")
98
105
  nil
@@ -120,14 +127,10 @@ module RubyLLM
120
127
  end
121
128
 
122
129
  update_data = build_completion_data(context, status)
130
+ execution.update!(update_data)
123
131
 
124
- if async_logging?
125
- # For async updates, use a job (if update support exists)
126
- # For now, update synchronously to ensure dashboard shows correct status
127
- execution.update!(update_data)
128
- else
129
- execution.update!(update_data)
130
- end
132
+ # Save detail data (prompts, responses, tool calls, etc.)
133
+ save_execution_details(execution, context, status)
131
134
  rescue StandardError => e
132
135
  error("Failed to complete execution record: #{e.message}")
133
136
  raise # Re-raise for ensure block to handle via mark_execution_failed!
@@ -150,11 +153,22 @@ module RubyLLM
150
153
  update_data = {
151
154
  status: "error",
152
155
  completed_at: Time.current,
153
- error_class: error&.class&.name || "UnknownError",
154
- error_message: error_message
156
+ error_class: error&.class&.name || "UnknownError"
155
157
  }
156
158
 
157
159
  execution.class.where(id: execution.id, status: "running").update_all(update_data)
160
+
161
+ # Store error_message in detail table (best-effort)
162
+ begin
163
+ detail_attrs = { error_message: error_message }
164
+ if execution.detail
165
+ execution.detail.update_columns(detail_attrs)
166
+ else
167
+ RubyLLM::Agents::ExecutionDetail.create!(detail_attrs.merge(execution_id: execution.id))
168
+ end
169
+ rescue StandardError
170
+ # Non-critical
171
+ end
158
172
  rescue StandardError => e
159
173
  error("CRITICAL: Failed emergency status update for execution #{execution&.id}: #{e.message}")
160
174
  end
@@ -174,7 +188,6 @@ module RubyLLM
174
188
  def build_running_execution_data(context)
175
189
  data = {
176
190
  agent_type: context.agent_class&.name,
177
- agent_version: config(:version, "1.0"),
178
191
  model_id: context.model,
179
192
  status: "running",
180
193
  started_at: context.started_at,
@@ -189,9 +202,6 @@ module RubyLLM
189
202
  data[:tenant_id] = context.tenant_id
190
203
  end
191
204
 
192
- # Add sanitized parameters
193
- data[:parameters] = sanitize_parameters(context)
194
-
195
205
  data
196
206
  end
197
207
 
@@ -212,38 +222,63 @@ module RubyLLM
212
222
  attempts_count: context.attempts_made
213
223
  }
214
224
 
215
- # Add cache key for cache hit executions
225
+ # Store niche cache key in metadata
226
+ merged_metadata = context.metadata.dup rescue {}
216
227
  if context.cached? && context[:cache_key]
217
- data[:response_cache_key] = context[:cache_key]
228
+ merged_metadata["response_cache_key"] = context[:cache_key]
218
229
  end
230
+ data[:metadata] = merged_metadata if merged_metadata.any?
219
231
 
220
- # Add error details if present
232
+ # Error class on execution (error_message goes to detail)
221
233
  if context.error
222
234
  data[:error_class] = context.error.class.name
223
- data[:error_message] = truncate_error_message(context.error.message)
224
235
  end
225
236
 
226
- # Add custom metadata
227
- data[:metadata] = context.metadata if context.metadata.any?
228
-
229
- # Add enhanced tool calls if present
237
+ # Tool calls count on execution
230
238
  if context[:tool_calls].present?
231
- data[:tool_calls] = context[:tool_calls]
232
239
  data[:tool_calls_count] = context[:tool_calls].size
233
240
  end
234
241
 
235
- # Add reliability attempts if present
242
+ # Attempts count on execution
236
243
  if context[:reliability_attempts].present?
237
- data[:attempts] = context[:reliability_attempts]
238
244
  data[:attempts_count] = context[:reliability_attempts].size
239
245
  end
240
246
 
241
- # Add response if persist_responses is enabled
247
+ data
248
+ end
249
+
250
+ # Saves detail data to the execution_details table after completion
251
+ def save_execution_details(execution, context, status)
252
+ return unless execution
253
+
254
+ detail_data = {}
255
+
256
+ if context.error
257
+ detail_data[:error_message] = truncate_error_message(context.error.message)
258
+ end
259
+
260
+ if context[:tool_calls].present?
261
+ detail_data[:tool_calls] = context[:tool_calls]
262
+ end
263
+
264
+ if context[:reliability_attempts].present?
265
+ detail_data[:attempts] = context[:reliability_attempts]
266
+ end
267
+
242
268
  if global_config.persist_responses && context.output.respond_to?(:content)
243
- data[:response] = serialize_response(context)
269
+ detail_data[:response] = serialize_response(context)
244
270
  end
245
271
 
246
- data
272
+ has_data = detail_data.values.any? { |v| v.present? && v != {} && v != [] }
273
+ return unless has_data
274
+
275
+ if execution.detail
276
+ execution.detail.update!(detail_data)
277
+ else
278
+ execution.create_detail!(detail_data)
279
+ end
280
+ rescue StandardError => e
281
+ error("Failed to save execution details: #{e.message}")
247
282
  end
248
283
 
249
284
  # Persists execution data to database (legacy fallback)
@@ -272,9 +307,13 @@ module RubyLLM
272
307
  # @param status [String] "success" or "error"
273
308
  # @return [Hash] Execution data
274
309
  def build_execution_data(context, status)
310
+ merged_metadata = context.metadata.dup rescue {}
311
+ if context.cached? && context[:cache_key]
312
+ merged_metadata["response_cache_key"] = context[:cache_key]
313
+ end
314
+
275
315
  data = {
276
316
  agent_type: context.agent_class&.name,
277
- agent_version: config(:version, "1.0"),
278
317
  model_id: context.model,
279
318
  status: determine_status(context, status),
280
319
  duration_ms: context.duration_ms,
@@ -284,7 +323,8 @@ module RubyLLM
284
323
  input_tokens: context.input_tokens || 0,
285
324
  output_tokens: context.output_tokens || 0,
286
325
  total_cost: context.total_cost || 0,
287
- attempts_count: context.attempts_made
326
+ attempts_count: context.attempts_made,
327
+ metadata: merged_metadata
288
328
  }
289
329
 
290
330
  # Add tenant_id only if multi-tenancy is enabled and tenant is set
@@ -292,39 +332,30 @@ module RubyLLM
292
332
  data[:tenant_id] = context.tenant_id
293
333
  end
294
334
 
295
- # Add cache key for cache hit executions
296
- if context.cached? && context[:cache_key]
297
- data[:response_cache_key] = context[:cache_key]
298
- end
299
-
300
- # Add error details if present
335
+ # Error class on execution
301
336
  if context.error
302
337
  data[:error_class] = context.error.class.name
303
- data[:error_message] = truncate_error_message(context.error.message)
304
338
  end
305
339
 
306
- # Add custom metadata
307
- data[:metadata] = context.metadata if context.metadata.any?
308
-
309
- # Add sanitized parameters
310
- data[:parameters] = sanitize_parameters(context)
311
-
312
- # Add enhanced tool calls if present
340
+ # Tool calls count on execution
313
341
  if context[:tool_calls].present?
314
- data[:tool_calls] = context[:tool_calls]
315
342
  data[:tool_calls_count] = context[:tool_calls].size
316
343
  end
317
344
 
318
- # Add reliability attempts if present
345
+ # Attempts count on execution
319
346
  if context[:reliability_attempts].present?
320
- data[:attempts] = context[:reliability_attempts]
321
347
  data[:attempts_count] = context[:reliability_attempts].size
322
348
  end
323
349
 
324
- # Add response if persist_responses is enabled
350
+ # Store detail data for separate creation
351
+ detail_data = { parameters: sanitize_parameters(context) }
352
+ detail_data[:error_message] = truncate_error_message(context.error.message) if context.error
353
+ detail_data[:tool_calls] = context[:tool_calls] if context[:tool_calls].present?
354
+ detail_data[:attempts] = context[:reliability_attempts] if context[:reliability_attempts].present?
325
355
  if global_config.persist_responses && context.output.respond_to?(:content)
326
- data[:response] = serialize_response(context)
356
+ detail_data[:response] = serialize_response(context)
327
357
  end
358
+ data[:_detail_data] = detail_data
328
359
 
329
360
  data
330
361
  end
@@ -401,8 +432,7 @@ module RubyLLM
401
432
  response_data[:input_tokens] = context.input_tokens if context.input_tokens
402
433
  response_data[:output_tokens] = context.output_tokens if context.output_tokens
403
434
 
404
- # Apply redaction for sensitive data
405
- Redactor.redact(response_data)
435
+ response_data
406
436
  rescue StandardError => e
407
437
  error("Failed to serialize response: #{e.message}")
408
438
  nil
@@ -419,7 +449,12 @@ module RubyLLM
419
449
  #
420
450
  # @param data [Hash] Execution data
421
451
  def create_execution_record(data)
422
- Execution.create!(data)
452
+ detail_data = data.delete(:_detail_data)
453
+ execution = Execution.create!(data)
454
+ if detail_data && detail_data.values.any? { |v| v.present? && v != {} && v != [] }
455
+ execution.create_detail!(detail_data)
456
+ end
457
+ execution
423
458
  end
424
459
 
425
460
  # Returns whether tracking is enabled for this agent type
@@ -432,8 +467,6 @@ module RubyLLM
432
467
  case context.agent_type
433
468
  when :embedding
434
469
  cfg.track_embeddings
435
- when :moderation
436
- cfg.track_moderation
437
470
  when :image
438
471
  cfg.track_image_generation
439
472
  when :audio
@@ -8,18 +8,16 @@ module RubyLLM
8
8
  #
9
9
  # This middleware extracts tenant information from the context options,
10
10
  # sets the tenant_id, tenant_object, and tenant_config on the context,
11
- # and applies the resolved API configuration to RubyLLM.
11
+ # and applies any tenant-specific API keys to RubyLLM.
12
12
  #
13
13
  # Supports three formats:
14
14
  # - Object with llm_tenant_id method (recommended for ActiveRecord models)
15
15
  # - Hash with :id key (simple/legacy format)
16
16
  # - nil (no tenant - single-tenant mode)
17
17
  #
18
- # API key resolution priority:
19
- # 1. Tenant object's llm_api_keys method (if present)
20
- # 2. Tenant-specific database config (ApiConfiguration)
21
- # 3. Global database config
22
- # 4. RubyLLM.configuration (set via initializer or environment)
18
+ # API keys are configured via:
19
+ # - RubyLLM.configuration (set via initializer or environment variables)
20
+ # - Tenant object's llm_api_keys method (for per-tenant overrides)
23
21
  #
24
22
  # @example With ActiveRecord model
25
23
  # # Model uses llm_tenant DSL
@@ -88,16 +86,10 @@ module RubyLLM
88
86
 
89
87
  # Applies API configuration to RubyLLM based on resolved tenant
90
88
  #
91
- # This method resolves API keys from multiple sources and applies
92
- # them to RubyLLM.config before the agent executes.
93
- #
94
89
  # @param context [Context] The execution context
95
90
  def apply_api_configuration!(context)
96
- # First, try to apply keys from tenant object's llm_api_keys method
91
+ # Apply keys from tenant object's llm_api_keys method if present
97
92
  apply_tenant_object_api_keys!(context)
98
-
99
- # Then, apply database configuration (tenant > global > ruby_llm_config)
100
- apply_database_api_configuration!(context)
101
93
  end
102
94
 
103
95
  # Applies API keys from tenant object's llm_api_keys method
@@ -116,22 +108,6 @@ module RubyLLM
116
108
  warn_api_key_error("tenant object", e)
117
109
  end
118
110
 
119
- # Applies API configuration from the database
120
- #
121
- # @param context [Context] The execution context
122
- def apply_database_api_configuration!(context)
123
- return unless api_configuration_available?
124
-
125
- resolved = ApiConfiguration.resolve(tenant_id: context.tenant_id)
126
- resolved.apply_to_ruby_llm!
127
-
128
- # Store resolved config on context for observability
129
- context[:resolved_api_config] = resolved
130
- rescue StandardError => e
131
- # Log but don't fail if DB lookup fails
132
- warn_api_key_error("database", e)
133
- end
134
-
135
111
  # Applies a hash of API keys to RubyLLM configuration
136
112
  #
137
113
  # @param api_keys [Hash] Hash of provider => key mappings
@@ -154,18 +130,6 @@ module RubyLLM
154
130
  "#{provider}_api_key="
155
131
  end
156
132
 
157
- # Checks if ApiConfiguration model is available
158
- #
159
- # @return [Boolean]
160
- def api_configuration_available?
161
- return false unless defined?(RubyLLM::Agents::ApiConfiguration)
162
-
163
- # Check if table exists
164
- ApiConfiguration.table_exists?
165
- rescue StandardError
166
- false
167
- end
168
-
169
133
  # Logs a warning about API key resolution failure
170
134
  #
171
135
  # @param source [String] Source that failed
@@ -34,9 +34,7 @@ module RubyLLM
34
34
  config.to_prepare do
35
35
  require_relative "../infrastructure/execution_logger_job"
36
36
  require_relative "../core/instrumentation"
37
- require_relative "../core/resolved_config"
38
37
  require_relative "../core/base"
39
- require_relative "../workflow/orchestrator"
40
38
 
41
39
  # Resolve the parent controller class from configuration
42
40
  # Default is ActionController::Base, but can be set to inherit from app controllers
@@ -161,6 +159,12 @@ module RubyLLM
161
159
  end)
162
160
  end
163
161
 
162
+ # Load rake tasks from lib/tasks
163
+ rake_tasks do
164
+ tasks_path = File.expand_path("../../../tasks", __dir__)
165
+ Dir[File.join(tasks_path, "**", "*.rake")].each { |f| load f }
166
+ end
167
+
164
168
  # Configures default generators for the engine
165
169
  # Sets up RSpec and FactoryBot for generated specs
166
170
  # @api private
@@ -179,7 +183,6 @@ module RubyLLM
179
183
  # - app/agents/ (top-level, no namespace)
180
184
  # - app/agents/embedders/ -> Embedders namespace
181
185
  # - app/agents/images/ -> Images namespace
182
- # - app/workflows/ (top-level, no namespace)
183
186
  #
184
187
  # @api private
185
188
  initializer "ruby_llm_agents.autoload_agents", before: :set_autoload_paths do |app|
@@ -219,9 +222,6 @@ module RubyLLM
219
222
  def self.namespace_for_path(path, config)
220
223
  parts = path.split("/")
221
224
 
222
- # app/workflows -> no namespace (top-level workflows)
223
- return nil if parts == ["app", "workflows"]
224
-
225
225
  # Need at least app/{root_directory}
226
226
  return nil unless parts.length >= 2 && parts[0] == "app"
227
227
  return nil unless parts[1] == config.root_directory
@@ -102,15 +102,6 @@ module RubyLLM
102
102
  # @return [Integer, nil] Number of tokens used for thinking
103
103
  attr_reader :thinking_text, :thinking_signature, :thinking_tokens
104
104
 
105
- # @!group Moderation
106
- # @!attribute [r] status
107
- # @return [Symbol, nil] Result status (:success, :input_moderation_blocked, :output_moderation_blocked)
108
- # @!attribute [r] moderation_result
109
- # @return [Object, nil] The raw moderation result from RubyLLM
110
- # @!attribute [r] moderation_phase
111
- # @return [Symbol, nil] The phase where moderation blocked (:input or :output)
112
- attr_reader :status, :moderation_result, :moderation_phase
113
-
114
105
  # Creates a new Result instance
115
106
  #
116
107
  # @param content [Hash, String] The processed response content
@@ -160,12 +151,6 @@ module RubyLLM
160
151
  @thinking_text = options[:thinking_text]
161
152
  @thinking_signature = options[:thinking_signature]
162
153
  @thinking_tokens = options[:thinking_tokens]
163
-
164
- # Moderation
165
- @status = options[:status] || :success
166
- @moderation_flagged = options[:moderation_flagged] || false
167
- @moderation_result = options[:moderation_result]
168
- @moderation_phase = options[:moderation_phase]
169
154
  end
170
155
 
171
156
  # Returns total tokens (input + output)
@@ -224,34 +209,6 @@ module RubyLLM
224
209
  thinking_text.present?
225
210
  end
226
211
 
227
- # Returns whether content was flagged by moderation
228
- #
229
- # @return [Boolean] true if moderation flagged the content
230
- def moderation_flagged?
231
- @moderation_flagged == true
232
- end
233
-
234
- # Returns whether content passed moderation
235
- #
236
- # @return [Boolean] true if content was not flagged
237
- def moderation_passed?
238
- !moderation_flagged?
239
- end
240
-
241
- # Returns the categories flagged by moderation
242
- #
243
- # @return [Array<String, Symbol>] Flagged category names
244
- def moderation_categories
245
- @moderation_result&.flagged_categories || []
246
- end
247
-
248
- # Returns the moderation category scores
249
- #
250
- # @return [Hash{String, Symbol => Float}] Category to score mapping
251
- def moderation_scores
252
- @moderation_result&.category_scores || {}
253
- end
254
-
255
212
  # Converts the result to a hash
256
213
  #
257
214
  # @return [Hash] All result data as a hash
@@ -283,12 +240,7 @@ module RubyLLM
283
240
  tool_calls_count: tool_calls_count,
284
241
  thinking_text: thinking_text,
285
242
  thinking_signature: thinking_signature,
286
- thinking_tokens: thinking_tokens,
287
- status: status,
288
- moderation_flagged: moderation_flagged?,
289
- moderation_phase: moderation_phase,
290
- moderation_categories: moderation_categories,
291
- moderation_scores: moderation_scores
243
+ thinking_tokens: thinking_tokens
292
244
  }
293
245
  end
294
246
 
@@ -257,7 +257,6 @@ module RubyLLM
257
257
  "ruby_llm_agents",
258
258
  "embedding",
259
259
  self.class.name,
260
- self.class.version,
261
260
  resolved_model,
262
261
  resolved_dimensions,
263
262
  Digest::SHA256.hexdigest(input_texts.map { |t| preprocess(t) }.join("\n"))
@@ -2,13 +2,13 @@
2
2
 
3
3
  require "csv"
4
4
  require "ruby_llm"
5
+ require "ruby_llm/schema"
5
6
 
6
7
  # Core
7
8
  require_relative "agents/core/version"
8
9
  require_relative "agents/core/configuration"
9
10
  require_relative "agents/core/deprecations"
10
11
  require_relative "agents/core/errors"
11
- require_relative "agents/core/resolved_config"
12
12
  require_relative "agents/core/llm_tenant"
13
13
 
14
14
  # Infrastructure - Reliability
@@ -29,7 +29,6 @@ require_relative "agents/dsl"
29
29
  require_relative "agents/base_agent"
30
30
 
31
31
  # Infrastructure - Budget & Utilities
32
- require_relative "agents/infrastructure/redactor"
33
32
  require_relative "agents/infrastructure/circuit_breaker"
34
33
  require_relative "agents/infrastructure/budget_tracker"
35
34
  require_relative "agents/infrastructure/alert_manager"
@@ -43,7 +42,6 @@ require_relative "agents/infrastructure/budget/spend_recorder"
43
42
  # Results
44
43
  require_relative "agents/results/base"
45
44
  require_relative "agents/results/embedding_result"
46
- require_relative "agents/results/moderation_result"
47
45
  require_relative "agents/results/transcription_result"
48
46
  require_relative "agents/results/speech_result"
49
47
  require_relative "agents/results/image_generation_result"
@@ -61,7 +59,6 @@ require_relative "agents/image/concerns/image_operation_execution"
61
59
 
62
60
  # Text agents
63
61
  require_relative "agents/text/embedder"
64
- require_relative "agents/text/moderator"
65
62
 
66
63
  # Audio agents
67
64
  require_relative "agents/audio/transcriber"
@@ -77,11 +74,6 @@ require_relative "agents/image/analyzer"
77
74
  require_relative "agents/image/background_remover"
78
75
  require_relative "agents/image/pipeline"
79
76
 
80
- # Workflow
81
- require_relative "agents/workflow/async"
82
- require_relative "agents/workflow/orchestrator"
83
- require_relative "agents/workflow/async_executor"
84
-
85
77
  # Rails integration
86
78
  if defined?(Rails)
87
79
  require_relative "agents/core/inflections"
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :ruby_llm_agents do
4
+ namespace :tenants do
5
+ desc "Refresh all tenant counters from executions table"
6
+ task refresh: :environment do
7
+ count = 0
8
+ RubyLLM::Agents::Tenant.find_each do |tenant|
9
+ tenant.refresh_counters!
10
+ count += 1
11
+ end
12
+ puts "Refreshed #{count} tenants"
13
+ end
14
+
15
+ desc "Refresh active tenant counters from executions table"
16
+ task refresh_active: :environment do
17
+ count = 0
18
+ RubyLLM::Agents::Tenant.active.find_each do |tenant|
19
+ tenant.refresh_counters!
20
+ count += 1
21
+ end
22
+ puts "Refreshed #{count} active tenants"
23
+ end
24
+
25
+ desc "Refresh a single tenant's counters"
26
+ task :refresh_one, [:tenant_id] => :environment do |_, args|
27
+ abort "Usage: rake ruby_llm_agents:tenants:refresh_one[tenant_id]" unless args[:tenant_id]
28
+
29
+ tenant = RubyLLM::Agents::Tenant.find_by!(tenant_id: args[:tenant_id])
30
+ tenant.refresh_counters!
31
+ puts "Refreshed tenant: #{tenant.tenant_id}"
32
+ end
33
+ end
34
+ end