ruby_llm-agents 0.4.0 → 1.0.0.beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +225 -34
  3. data/app/controllers/ruby_llm/agents/agents_controller.rb +136 -16
  4. data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +214 -0
  5. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +29 -9
  6. data/app/controllers/ruby_llm/agents/{settings_controller.rb → system_config_controller.rb} +3 -3
  7. data/app/controllers/ruby_llm/agents/tenants_controller.rb +109 -0
  8. data/app/controllers/ruby_llm/agents/workflows_controller.rb +355 -0
  9. data/app/helpers/ruby_llm/agents/application_helper.rb +25 -0
  10. data/app/models/ruby_llm/agents/api_configuration.rb +386 -0
  11. data/app/models/ruby_llm/agents/execution.rb +3 -0
  12. data/app/models/ruby_llm/agents/tenant_budget.rb +112 -14
  13. data/app/services/ruby_llm/agents/agent_registry.rb +51 -12
  14. data/app/views/layouts/ruby_llm/agents/application.html.erb +5 -30
  15. data/app/views/ruby_llm/agents/agents/_agent.html.erb +13 -1
  16. data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +235 -0
  17. data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +70 -0
  18. data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +152 -0
  19. data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +63 -0
  20. data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +108 -0
  21. data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +91 -0
  22. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +1 -1
  23. data/app/views/ruby_llm/agents/agents/index.html.erb +74 -9
  24. data/app/views/ruby_llm/agents/agents/show.html.erb +18 -378
  25. data/app/views/ruby_llm/agents/api_configurations/_api_key_field.html.erb +34 -0
  26. data/app/views/ruby_llm/agents/api_configurations/_form.html.erb +288 -0
  27. data/app/views/ruby_llm/agents/api_configurations/edit.html.erb +95 -0
  28. data/app/views/ruby_llm/agents/api_configurations/edit_tenant.html.erb +97 -0
  29. data/app/views/ruby_llm/agents/api_configurations/show.html.erb +211 -0
  30. data/app/views/ruby_llm/agents/api_configurations/tenant.html.erb +179 -0
  31. data/app/views/ruby_llm/agents/dashboard/_action_center.html.erb +1 -1
  32. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +269 -15
  33. data/app/views/ruby_llm/agents/executions/show.html.erb +98 -0
  34. data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +93 -0
  35. data/app/views/ruby_llm/agents/{settings → system_config}/show.html.erb +1 -1
  36. data/app/views/ruby_llm/agents/tenants/_form.html.erb +150 -0
  37. data/app/views/ruby_llm/agents/tenants/edit.html.erb +13 -0
  38. data/app/views/ruby_llm/agents/tenants/index.html.erb +129 -0
  39. data/app/views/ruby_llm/agents/tenants/show.html.erb +374 -0
  40. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +236 -0
  41. data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +76 -0
  42. data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +74 -0
  43. data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +108 -0
  44. data/app/views/ruby_llm/agents/workflows/show.html.erb +442 -0
  45. data/config/routes.rb +13 -1
  46. data/lib/generators/ruby_llm_agents/agent_generator.rb +56 -7
  47. data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +100 -0
  48. data/lib/generators/ruby_llm_agents/background_remover_generator.rb +110 -0
  49. data/lib/generators/ruby_llm_agents/embedder_generator.rb +107 -0
  50. data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +115 -0
  51. data/lib/generators/ruby_llm_agents/image_editor_generator.rb +108 -0
  52. data/lib/generators/ruby_llm_agents/image_generator_generator.rb +116 -0
  53. data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +178 -0
  54. data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +109 -0
  55. data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +103 -0
  56. data/lib/generators/ruby_llm_agents/image_variator_generator.rb +102 -0
  57. data/lib/generators/ruby_llm_agents/install_generator.rb +76 -4
  58. data/lib/generators/ruby_llm_agents/restructure_generator.rb +292 -0
  59. data/lib/generators/ruby_llm_agents/speaker_generator.rb +121 -0
  60. data/lib/generators/ruby_llm_agents/templates/add_execution_type_migration.rb.tt +8 -0
  61. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +99 -84
  62. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +42 -40
  63. data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +26 -0
  64. data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +50 -0
  65. data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +26 -0
  66. data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +20 -0
  67. data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +38 -0
  68. data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +139 -0
  69. data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +21 -0
  70. data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +20 -0
  71. data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +20 -0
  72. data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +49 -0
  73. data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +53 -0
  74. data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +44 -0
  75. data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +90 -0
  76. data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +41 -0
  77. data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +45 -0
  78. data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +35 -0
  79. data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +47 -0
  80. data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +50 -0
  81. data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +44 -0
  82. data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +38 -0
  83. data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +33 -0
  84. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +228 -0
  85. data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +131 -0
  86. data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +255 -0
  87. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +120 -0
  88. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +102 -0
  89. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +282 -0
  90. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +228 -0
  91. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +120 -0
  92. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +110 -0
  93. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +120 -0
  94. data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +212 -0
  95. data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +227 -0
  96. data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +251 -0
  97. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +300 -0
  98. data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +56 -0
  99. data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +51 -0
  100. data/lib/generators/ruby_llm_agents/transcriber_generator.rb +107 -0
  101. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +152 -1
  102. data/lib/ruby_llm/agents/audio/speaker.rb +553 -0
  103. data/lib/ruby_llm/agents/audio/transcriber.rb +669 -0
  104. data/lib/ruby_llm/agents/base_agent.rb +675 -0
  105. data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +181 -0
  106. data/lib/ruby_llm/agents/core/base/moderation_execution.rb +274 -0
  107. data/lib/ruby_llm/agents/core/base.rb +135 -0
  108. data/lib/ruby_llm/agents/core/configuration.rb +981 -0
  109. data/lib/ruby_llm/agents/core/errors.rb +150 -0
  110. data/lib/ruby_llm/agents/{instrumentation.rb → core/instrumentation.rb} +93 -4
  111. data/lib/ruby_llm/agents/core/llm_tenant.rb +358 -0
  112. data/lib/ruby_llm/agents/core/resolved_config.rb +348 -0
  113. data/lib/ruby_llm/agents/{version.rb → core/version.rb} +1 -1
  114. data/lib/ruby_llm/agents/dsl/base.rb +110 -0
  115. data/lib/ruby_llm/agents/dsl/caching.rb +142 -0
  116. data/lib/ruby_llm/agents/dsl/reliability.rb +307 -0
  117. data/lib/ruby_llm/agents/dsl.rb +41 -0
  118. data/lib/ruby_llm/agents/image/analyzer/dsl.rb +130 -0
  119. data/lib/ruby_llm/agents/image/analyzer/execution.rb +402 -0
  120. data/lib/ruby_llm/agents/image/analyzer.rb +90 -0
  121. data/lib/ruby_llm/agents/image/background_remover/dsl.rb +154 -0
  122. data/lib/ruby_llm/agents/image/background_remover/execution.rb +240 -0
  123. data/lib/ruby_llm/agents/image/background_remover.rb +89 -0
  124. data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +91 -0
  125. data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +165 -0
  126. data/lib/ruby_llm/agents/image/editor/dsl.rb +56 -0
  127. data/lib/ruby_llm/agents/image/editor/execution.rb +207 -0
  128. data/lib/ruby_llm/agents/image/editor.rb +92 -0
  129. data/lib/ruby_llm/agents/image/generator/active_storage_support.rb +127 -0
  130. data/lib/ruby_llm/agents/image/generator/content_policy.rb +95 -0
  131. data/lib/ruby_llm/agents/image/generator/pricing.rb +353 -0
  132. data/lib/ruby_llm/agents/image/generator/templates.rb +124 -0
  133. data/lib/ruby_llm/agents/image/generator.rb +455 -0
  134. data/lib/ruby_llm/agents/image/pipeline/dsl.rb +213 -0
  135. data/lib/ruby_llm/agents/image/pipeline/execution.rb +382 -0
  136. data/lib/ruby_llm/agents/image/pipeline.rb +97 -0
  137. data/lib/ruby_llm/agents/image/transformer/dsl.rb +148 -0
  138. data/lib/ruby_llm/agents/image/transformer/execution.rb +223 -0
  139. data/lib/ruby_llm/agents/image/transformer.rb +95 -0
  140. data/lib/ruby_llm/agents/image/upscaler/dsl.rb +83 -0
  141. data/lib/ruby_llm/agents/image/upscaler/execution.rb +219 -0
  142. data/lib/ruby_llm/agents/image/upscaler.rb +81 -0
  143. data/lib/ruby_llm/agents/image/variator/dsl.rb +62 -0
  144. data/lib/ruby_llm/agents/image/variator/execution.rb +189 -0
  145. data/lib/ruby_llm/agents/image/variator.rb +80 -0
  146. data/lib/ruby_llm/agents/{alert_manager.rb → infrastructure/alert_manager.rb} +17 -22
  147. data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +145 -0
  148. data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +149 -0
  149. data/lib/ruby_llm/agents/infrastructure/budget/forecaster.rb +68 -0
  150. data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +279 -0
  151. data/lib/ruby_llm/agents/infrastructure/budget_tracker.rb +275 -0
  152. data/lib/ruby_llm/agents/{execution_logger_job.rb → infrastructure/execution_logger_job.rb} +17 -1
  153. data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/executor.rb +2 -1
  154. data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/retry_strategy.rb +9 -3
  155. data/lib/ruby_llm/agents/{reliability.rb → infrastructure/reliability.rb} +11 -21
  156. data/lib/ruby_llm/agents/pipeline/builder.rb +215 -0
  157. data/lib/ruby_llm/agents/pipeline/context.rb +255 -0
  158. data/lib/ruby_llm/agents/pipeline/executor.rb +86 -0
  159. data/lib/ruby_llm/agents/pipeline/middleware/base.rb +124 -0
  160. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +95 -0
  161. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +171 -0
  162. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +415 -0
  163. data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +276 -0
  164. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +196 -0
  165. data/lib/ruby_llm/agents/pipeline.rb +68 -0
  166. data/lib/ruby_llm/agents/{engine.rb → rails/engine.rb} +79 -10
  167. data/lib/ruby_llm/agents/results/background_removal_result.rb +286 -0
  168. data/lib/ruby_llm/agents/{result.rb → results/base.rb} +73 -1
  169. data/lib/ruby_llm/agents/results/embedding_result.rb +243 -0
  170. data/lib/ruby_llm/agents/results/image_analysis_result.rb +314 -0
  171. data/lib/ruby_llm/agents/results/image_edit_result.rb +250 -0
  172. data/lib/ruby_llm/agents/results/image_generation_result.rb +346 -0
  173. data/lib/ruby_llm/agents/results/image_pipeline_result.rb +399 -0
  174. data/lib/ruby_llm/agents/results/image_transform_result.rb +251 -0
  175. data/lib/ruby_llm/agents/results/image_upscale_result.rb +255 -0
  176. data/lib/ruby_llm/agents/results/image_variation_result.rb +237 -0
  177. data/lib/ruby_llm/agents/results/moderation_result.rb +158 -0
  178. data/lib/ruby_llm/agents/results/speech_result.rb +338 -0
  179. data/lib/ruby_llm/agents/results/transcription_result.rb +408 -0
  180. data/lib/ruby_llm/agents/text/embedder.rb +444 -0
  181. data/lib/ruby_llm/agents/text/moderator.rb +237 -0
  182. data/lib/ruby_llm/agents/workflow/async.rb +220 -0
  183. data/lib/ruby_llm/agents/workflow/async_executor.rb +156 -0
  184. data/lib/ruby_llm/agents/{workflow.rb → workflow/orchestrator.rb} +6 -5
  185. data/lib/ruby_llm/agents/workflow/parallel.rb +34 -17
  186. data/lib/ruby_llm/agents/workflow/thread_pool.rb +185 -0
  187. data/lib/ruby_llm/agents.rb +86 -20
  188. metadata +189 -35
  189. data/lib/ruby_llm/agents/base/caching.rb +0 -40
  190. data/lib/ruby_llm/agents/base/cost_calculation.rb +0 -105
  191. data/lib/ruby_llm/agents/base/dsl.rb +0 -324
  192. data/lib/ruby_llm/agents/base/execution.rb +0 -283
  193. data/lib/ruby_llm/agents/base/reliability_dsl.rb +0 -82
  194. data/lib/ruby_llm/agents/base/reliability_execution.rb +0 -136
  195. data/lib/ruby_llm/agents/base/response_building.rb +0 -86
  196. data/lib/ruby_llm/agents/base/tool_tracking.rb +0 -57
  197. data/lib/ruby_llm/agents/base.rb +0 -209
  198. data/lib/ruby_llm/agents/budget_tracker.rb +0 -471
  199. data/lib/ruby_llm/agents/configuration.rb +0 -357
  200. /data/lib/ruby_llm/agents/{deprecations.rb → core/deprecations.rb} +0 -0
  201. /data/lib/ruby_llm/agents/{inflections.rb → core/inflections.rb} +0 -0
  202. /data/lib/ruby_llm/agents/{attempt_tracker.rb → infrastructure/attempt_tracker.rb} +0 -0
  203. /data/lib/ruby_llm/agents/{cache_helper.rb → infrastructure/cache_helper.rb} +0 -0
  204. /data/lib/ruby_llm/agents/{circuit_breaker.rb → infrastructure/circuit_breaker.rb} +0 -0
  205. /data/lib/ruby_llm/agents/{redactor.rb → infrastructure/redactor.rb} +0 -0
  206. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/breaker_manager.rb +0 -0
  207. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/execution_constraints.rb +0 -0
  208. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/fallback_routing.rb +0 -0
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ module Budget
6
+ # Budget forecasting based on current spending trends
7
+ #
8
+ # @api private
9
+ module Forecaster
10
+ class << self
11
+ # Calculates budget forecasts based on current spending trends
12
+ #
13
+ # @param tenant_id [String, nil] The tenant identifier
14
+ # @param budget_config [Hash] Budget configuration
15
+ # @return [Hash, nil] Forecast information
16
+ def calculate_forecast(tenant_id: nil, budget_config:)
17
+ return nil unless budget_config[:enabled]
18
+ return nil unless budget_config[:global_daily] || budget_config[:global_monthly]
19
+
20
+ daily_current = BudgetQuery.current_spend(:global, :daily, tenant_id: tenant_id)
21
+ monthly_current = BudgetQuery.current_spend(:global, :monthly, tenant_id: tenant_id)
22
+
23
+ # Calculate hours elapsed today and days elapsed this month
24
+ hours_elapsed = Time.current.hour + (Time.current.min / 60.0)
25
+ hours_elapsed = [hours_elapsed, 1].max # Avoid division by zero
26
+ days_in_month = Time.current.end_of_month.day
27
+ day_of_month = Time.current.day
28
+ days_elapsed = day_of_month - 1 + (hours_elapsed / 24.0)
29
+ days_elapsed = [days_elapsed, 1].max
30
+
31
+ forecast = {}
32
+
33
+ # Daily forecast
34
+ if budget_config[:global_daily]
35
+ daily_rate = daily_current / hours_elapsed
36
+ projected_daily = daily_rate * 24
37
+ forecast[:daily] = {
38
+ current: daily_current.round(4),
39
+ projected: projected_daily.round(4),
40
+ limit: budget_config[:global_daily],
41
+ on_track: projected_daily <= budget_config[:global_daily],
42
+ hours_remaining: (24 - hours_elapsed).round(1),
43
+ rate_per_hour: daily_rate.round(6)
44
+ }
45
+ end
46
+
47
+ # Monthly forecast
48
+ if budget_config[:global_monthly]
49
+ monthly_rate = monthly_current / days_elapsed
50
+ projected_monthly = monthly_rate * days_in_month
51
+ days_remaining = days_in_month - day_of_month
52
+ forecast[:monthly] = {
53
+ current: monthly_current.round(4),
54
+ projected: projected_monthly.round(4),
55
+ limit: budget_config[:global_monthly],
56
+ on_track: projected_monthly <= budget_config[:global_monthly],
57
+ days_remaining: days_remaining,
58
+ rate_per_day: monthly_rate.round(4)
59
+ }
60
+ end
61
+
62
+ forecast.presence
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,279 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../cache_helper"
4
+
5
+ module RubyLLM
6
+ module Agents
7
+ module Budget
8
+ # Records spend and token usage, and handles soft cap alerting
9
+ #
10
+ # @api private
11
+ module SpendRecorder
12
+ extend CacheHelper
13
+
14
+ class << self
15
+ # Records spend and checks for soft cap alerts
16
+ #
17
+ # @param agent_type [String] The agent class name
18
+ # @param amount [Float] The amount spent in USD
19
+ # @param tenant_id [String, nil] The tenant identifier
20
+ # @param budget_config [Hash] Budget configuration
21
+ # @return [void]
22
+ def record_spend!(agent_type, amount, tenant_id:, budget_config:)
23
+ return if amount.nil? || amount <= 0
24
+
25
+ # Increment all relevant counters
26
+ increment_spend(:global, :daily, amount, tenant_id: tenant_id)
27
+ increment_spend(:global, :monthly, amount, tenant_id: tenant_id)
28
+ increment_spend(:agent, :daily, amount, agent_type: agent_type, tenant_id: tenant_id)
29
+ increment_spend(:agent, :monthly, amount, agent_type: agent_type, tenant_id: tenant_id)
30
+
31
+ # Check for soft cap alerts
32
+ check_soft_cap_alerts(agent_type, tenant_id, budget_config) if budget_config[:enabled]
33
+ end
34
+
35
+ # Records token usage and checks for soft cap alerts
36
+ #
37
+ # @param agent_type [String] The agent class name
38
+ # @param tokens [Integer] The number of tokens used
39
+ # @param tenant_id [String, nil] The tenant identifier
40
+ # @param budget_config [Hash] Budget configuration
41
+ # @return [void]
42
+ def record_tokens!(agent_type, tokens, tenant_id:, budget_config:)
43
+ return if tokens.nil? || tokens <= 0
44
+
45
+ # Increment global token counters (daily and monthly)
46
+ # Note: We only track global token usage, not per-agent (scope is ignored in increment_tokens)
47
+ increment_tokens(:global, :daily, tokens, tenant_id: tenant_id)
48
+ increment_tokens(:global, :monthly, tokens, tenant_id: tenant_id)
49
+
50
+ # Check for soft cap alerts
51
+ check_soft_token_alerts(agent_type, tenant_id, budget_config) if budget_config[:enabled]
52
+ end
53
+
54
+ # Increments the spend counter for a scope and period
55
+ #
56
+ # @param scope [Symbol] :global or :agent
57
+ # @param period [Symbol] :daily or :monthly
58
+ # @param amount [Float] Amount to add
59
+ # @param agent_type [String, nil] Required when scope is :agent
60
+ # @param tenant_id [String, nil] The tenant identifier
61
+ # @return [Float] New total
62
+ def increment_spend(scope, period, amount, agent_type: nil, tenant_id: nil)
63
+ key = budget_cache_key(scope, period, agent_type: agent_type, tenant_id: tenant_id)
64
+ ttl = period == :daily ? 1.day : 31.days
65
+
66
+ # Read-modify-write for float values (cache increment is for integers)
67
+ current = (SpendRecorder.cache_read(key) || 0).to_f
68
+ new_total = current + amount
69
+ SpendRecorder.cache_write(key, new_total, expires_in: ttl)
70
+ new_total
71
+ end
72
+
73
+ # Increments the token counter for a period
74
+ #
75
+ # @param scope [Symbol] :global (only global supported for tokens)
76
+ # @param period [Symbol] :daily or :monthly
77
+ # @param tokens [Integer] Tokens to add
78
+ # @param agent_type [String, nil] Not used for tokens
79
+ # @param tenant_id [String, nil] The tenant identifier
80
+ # @return [Integer] New total
81
+ def increment_tokens(scope, period, tokens, agent_type: nil, tenant_id: nil)
82
+ # For now, we only track global token usage (not per-agent)
83
+ key = token_cache_key(period, tenant_id: tenant_id)
84
+ ttl = period == :daily ? 1.day : 31.days
85
+
86
+ current = (SpendRecorder.cache_read(key) || 0).to_i
87
+ new_total = current + tokens
88
+ SpendRecorder.cache_write(key, new_total, expires_in: ttl)
89
+ new_total
90
+ end
91
+
92
+ # Returns the tenant key part for cache keys
93
+ #
94
+ # @param tenant_id [String, nil] The tenant identifier
95
+ # @return [String] "tenant:{id}" or "global"
96
+ def tenant_key_part(tenant_id)
97
+ tenant_id.present? ? "tenant:#{tenant_id}" : "global"
98
+ end
99
+
100
+ # Returns the date key part for cache keys based on period
101
+ #
102
+ # @param period [Symbol] :daily or :monthly
103
+ # @return [String] Date string
104
+ def date_key_part(period)
105
+ period == :daily ? Date.current.to_s : Date.current.strftime("%Y-%m")
106
+ end
107
+
108
+ # Generates an alert cache key
109
+ #
110
+ # @param alert_type [String] Type of alert (e.g., "budget_alert", "token_alert")
111
+ # @param scope [Symbol] Alert scope
112
+ # @param tenant_id [String, nil] The tenant identifier
113
+ # @return [String] Cache key
114
+ def alert_cache_key(alert_type, scope, tenant_id)
115
+ SpendRecorder.cache_key(alert_type, tenant_key_part(tenant_id), scope, Date.current.to_s)
116
+ end
117
+
118
+ # Generates a cache key for budget tracking
119
+ #
120
+ # @param scope [Symbol] :global or :agent
121
+ # @param period [Symbol] :daily or :monthly
122
+ # @param agent_type [String, nil] Required when scope is :agent
123
+ # @param tenant_id [String, nil] The tenant identifier
124
+ # @return [String] Cache key
125
+ def budget_cache_key(scope, period, agent_type: nil, tenant_id: nil)
126
+ date_part = date_key_part(period)
127
+ tenant_part = tenant_key_part(tenant_id)
128
+
129
+ case scope
130
+ when :global
131
+ SpendRecorder.cache_key("budget", tenant_part, date_part)
132
+ when :agent
133
+ SpendRecorder.cache_key("budget", tenant_part, "agent", agent_type, date_part)
134
+ else
135
+ raise ArgumentError, "Unknown scope: #{scope}"
136
+ end
137
+ end
138
+
139
+ # Generates a cache key for token tracking
140
+ #
141
+ # @param period [Symbol] :daily or :monthly
142
+ # @param tenant_id [String, nil] The tenant identifier
143
+ # @return [String] Cache key
144
+ def token_cache_key(period, tenant_id: nil)
145
+ SpendRecorder.cache_key("tokens", tenant_key_part(tenant_id), date_key_part(period))
146
+ end
147
+
148
+ private
149
+
150
+ # Checks for soft cap alerts after recording spend
151
+ #
152
+ # @param agent_type [String] The agent class name
153
+ # @param tenant_id [String, nil] The tenant identifier
154
+ # @param budget_config [Hash] Budget configuration
155
+ # @return [void]
156
+ def check_soft_cap_alerts(agent_type, tenant_id, budget_config)
157
+ config = RubyLLM::Agents.configuration
158
+ return unless config.alerts_enabled?
159
+ return unless config.alert_events.include?(:budget_soft_cap) || config.alert_events.include?(:budget_hard_cap)
160
+
161
+ # Check global daily
162
+ check_budget_alert(:global_daily, budget_config[:global_daily],
163
+ BudgetQuery.current_spend(:global, :daily, tenant_id: tenant_id),
164
+ agent_type, tenant_id, budget_config)
165
+
166
+ # Check global monthly
167
+ check_budget_alert(:global_monthly, budget_config[:global_monthly],
168
+ BudgetQuery.current_spend(:global, :monthly, tenant_id: tenant_id),
169
+ agent_type, tenant_id, budget_config)
170
+
171
+ # Check per-agent daily
172
+ agent_daily_limit = budget_config[:per_agent_daily]&.dig(agent_type)
173
+ if agent_daily_limit
174
+ check_budget_alert(:per_agent_daily, agent_daily_limit,
175
+ BudgetQuery.current_spend(:agent, :daily, agent_type: agent_type, tenant_id: tenant_id),
176
+ agent_type, tenant_id, budget_config)
177
+ end
178
+
179
+ # Check per-agent monthly
180
+ agent_monthly_limit = budget_config[:per_agent_monthly]&.dig(agent_type)
181
+ if agent_monthly_limit
182
+ check_budget_alert(:per_agent_monthly, agent_monthly_limit,
183
+ BudgetQuery.current_spend(:agent, :monthly, agent_type: agent_type, tenant_id: tenant_id),
184
+ agent_type, tenant_id, budget_config)
185
+ end
186
+ end
187
+
188
+ # Checks if an alert should be fired for a budget
189
+ #
190
+ # @param scope [Symbol] Budget scope
191
+ # @param limit [Float, nil] Budget limit
192
+ # @param current [Float] Current spend
193
+ # @param agent_type [String] Agent type
194
+ # @param tenant_id [String, nil] The tenant identifier
195
+ # @param budget_config [Hash] Budget configuration
196
+ # @return [void]
197
+ def check_budget_alert(scope, limit, current, agent_type, tenant_id, budget_config)
198
+ return unless limit
199
+ return if current <= limit
200
+
201
+ event = budget_config[:enforcement] == :hard ? :budget_hard_cap : :budget_soft_cap
202
+ config = RubyLLM::Agents.configuration
203
+ return unless config.alert_events.include?(event)
204
+
205
+ # Prevent duplicate alerts by using a cache key (include tenant for isolation)
206
+ key = alert_cache_key("budget_alert", scope, tenant_id)
207
+ return if SpendRecorder.cache_exist?(key)
208
+
209
+ SpendRecorder.cache_write(key, true, expires_in: 1.hour)
210
+
211
+ AlertManager.notify(event, {
212
+ scope: scope,
213
+ limit: limit,
214
+ total: current.round(6),
215
+ agent_type: agent_type,
216
+ tenant_id: tenant_id,
217
+ timestamp: Date.current.to_s
218
+ })
219
+ end
220
+
221
+ # Checks for soft cap token alerts after recording usage
222
+ #
223
+ # @param agent_type [String] The agent class name
224
+ # @param tenant_id [String, nil] The tenant identifier
225
+ # @param budget_config [Hash] Budget configuration
226
+ # @return [void]
227
+ def check_soft_token_alerts(agent_type, tenant_id, budget_config)
228
+ config = RubyLLM::Agents.configuration
229
+ return unless config.alerts_enabled?
230
+ return unless config.alert_events.include?(:token_soft_cap) || config.alert_events.include?(:token_hard_cap)
231
+
232
+ # Check global daily tokens
233
+ check_token_alert(:global_daily_tokens, budget_config[:global_daily_tokens],
234
+ BudgetQuery.current_tokens(:daily, tenant_id: tenant_id),
235
+ agent_type, tenant_id, budget_config)
236
+
237
+ # Check global monthly tokens
238
+ check_token_alert(:global_monthly_tokens, budget_config[:global_monthly_tokens],
239
+ BudgetQuery.current_tokens(:monthly, tenant_id: tenant_id),
240
+ agent_type, tenant_id, budget_config)
241
+ end
242
+
243
+ # Checks if a token alert should be fired
244
+ #
245
+ # @param scope [Symbol] Token scope
246
+ # @param limit [Integer, nil] Token limit
247
+ # @param current [Integer] Current token usage
248
+ # @param agent_type [String] Agent type
249
+ # @param tenant_id [String, nil] The tenant identifier
250
+ # @param budget_config [Hash] Budget configuration
251
+ # @return [void]
252
+ def check_token_alert(scope, limit, current, agent_type, tenant_id, budget_config)
253
+ return unless limit
254
+ return if current <= limit
255
+
256
+ event = budget_config[:enforcement] == :hard ? :token_hard_cap : :token_soft_cap
257
+ config = RubyLLM::Agents.configuration
258
+ return unless config.alert_events.include?(event)
259
+
260
+ # Prevent duplicate alerts
261
+ key = alert_cache_key("token_alert", scope, tenant_id)
262
+ return if SpendRecorder.cache_exist?(key)
263
+
264
+ SpendRecorder.cache_write(key, true, expires_in: 1.hour)
265
+
266
+ AlertManager.notify(event, {
267
+ scope: scope,
268
+ limit: limit,
269
+ total: current,
270
+ agent_type: agent_type,
271
+ tenant_id: tenant_id,
272
+ timestamp: Date.current.to_s
273
+ })
274
+ end
275
+ end
276
+ end
277
+ end
278
+ end
279
+ end
@@ -0,0 +1,275 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "cache_helper"
4
+ require_relative "budget/config_resolver"
5
+ require_relative "budget/spend_recorder"
6
+ require_relative "budget/budget_query"
7
+ require_relative "budget/forecaster"
8
+
9
+ module RubyLLM
10
+ module Agents
11
+ # Cache-based budget tracking for cost governance
12
+ #
13
+ # Tracks spending against configured budget limits using cache counters.
14
+ # Supports daily and monthly budgets at both global and per-agent levels.
15
+ # In multi-tenant mode, budgets are tracked separately per tenant.
16
+ #
17
+ # Note: Uses best-effort enforcement with cache counters. In high-concurrency
18
+ # scenarios, slight overruns may occur due to race conditions. This is an
19
+ # acceptable trade-off for performance.
20
+ #
21
+ # @example Checking budget before execution
22
+ # BudgetTracker.check_budget!("MyAgent") # raises BudgetExceededError if over limit
23
+ #
24
+ # @example Recording spend after execution
25
+ # BudgetTracker.record_spend!("MyAgent", 0.05)
26
+ #
27
+ # @example Multi-tenant usage
28
+ # BudgetTracker.check_budget!("MyAgent", tenant_id: "acme_corp")
29
+ # BudgetTracker.record_spend!("MyAgent", 0.05, tenant_id: "acme_corp")
30
+ #
31
+ # @see RubyLLM::Agents::Configuration
32
+ # @see RubyLLM::Agents::Reliability::BudgetExceededError
33
+ # @see RubyLLM::Agents::TenantBudget
34
+ # @api public
35
+ module BudgetTracker
36
+ extend CacheHelper
37
+
38
+ class << self
39
+ # Checks if the current spend exceeds budget limits
40
+ #
41
+ # @param agent_type [String] The agent class name
42
+ # @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
43
+ # @param tenant_config [Hash, nil] Optional runtime tenant config (takes priority over resolver/DB)
44
+ # @raise [Reliability::BudgetExceededError] If hard cap is exceeded
45
+ # @return [void]
46
+ def check_budget!(agent_type, tenant_id: nil, tenant_config: nil)
47
+ tenant_id = Budget::ConfigResolver.resolve_tenant_id(tenant_id)
48
+ budget_config = Budget::ConfigResolver.resolve_budget_config(tenant_id, runtime_config: tenant_config)
49
+
50
+ return unless budget_config[:enabled]
51
+ return unless budget_config[:enforcement] == :hard
52
+
53
+ check_budget_limits!(agent_type, tenant_id, budget_config)
54
+ end
55
+
56
+ # Checks if the current token usage exceeds budget limits
57
+ #
58
+ # @param agent_type [String] The agent class name
59
+ # @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
60
+ # @param tenant_config [Hash, nil] Optional runtime tenant config (takes priority over resolver/DB)
61
+ # @raise [Reliability::BudgetExceededError] If hard cap is exceeded
62
+ # @return [void]
63
+ def check_token_budget!(agent_type, tenant_id: nil, tenant_config: nil)
64
+ tenant_id = Budget::ConfigResolver.resolve_tenant_id(tenant_id)
65
+ budget_config = Budget::ConfigResolver.resolve_budget_config(tenant_id, runtime_config: tenant_config)
66
+
67
+ return unless budget_config[:enabled]
68
+ return unless budget_config[:enforcement] == :hard
69
+
70
+ check_token_limits!(agent_type, tenant_id, budget_config)
71
+ end
72
+
73
+ # Records spend and checks for soft cap alerts
74
+ #
75
+ # @param agent_type [String] The agent class name
76
+ # @param amount [Float] The amount spent in USD
77
+ # @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
78
+ # @param tenant_config [Hash, nil] Optional runtime tenant config (takes priority over resolver/DB)
79
+ # @return [void]
80
+ def record_spend!(agent_type, amount, tenant_id: nil, tenant_config: nil)
81
+ return if amount.nil? || amount <= 0
82
+
83
+ tenant_id = Budget::ConfigResolver.resolve_tenant_id(tenant_id)
84
+ budget_config = Budget::ConfigResolver.resolve_budget_config(tenant_id, runtime_config: tenant_config)
85
+
86
+ Budget::SpendRecorder.record_spend!(agent_type, amount, tenant_id: tenant_id, budget_config: budget_config)
87
+ end
88
+
89
+ # Records token usage and checks for soft cap alerts
90
+ #
91
+ # @param agent_type [String] The agent class name
92
+ # @param tokens [Integer] The number of tokens used
93
+ # @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
94
+ # @param tenant_config [Hash, nil] Optional runtime tenant config (takes priority over resolver/DB)
95
+ # @return [void]
96
+ def record_tokens!(agent_type, tokens, tenant_id: nil, tenant_config: nil)
97
+ return if tokens.nil? || tokens <= 0
98
+
99
+ tenant_id = Budget::ConfigResolver.resolve_tenant_id(tenant_id)
100
+ budget_config = Budget::ConfigResolver.resolve_budget_config(tenant_id, runtime_config: tenant_config)
101
+
102
+ Budget::SpendRecorder.record_tokens!(agent_type, tokens, tenant_id: tenant_id, budget_config: budget_config)
103
+ end
104
+
105
+ # Returns the current spend for a scope and period
106
+ #
107
+ # @param scope [Symbol] :global or :agent
108
+ # @param period [Symbol] :daily or :monthly
109
+ # @param agent_type [String, nil] Required when scope is :agent
110
+ # @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
111
+ # @return [Float] Current spend in USD
112
+ def current_spend(scope, period, agent_type: nil, tenant_id: nil)
113
+ tenant_id = Budget::ConfigResolver.resolve_tenant_id(tenant_id)
114
+ Budget::BudgetQuery.current_spend(scope, period, agent_type: agent_type, tenant_id: tenant_id)
115
+ end
116
+
117
+ # Returns the current token usage for a period (global only)
118
+ #
119
+ # @param period [Symbol] :daily or :monthly
120
+ # @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
121
+ # @return [Integer] Current token usage
122
+ def current_tokens(period, tenant_id: nil)
123
+ tenant_id = Budget::ConfigResolver.resolve_tenant_id(tenant_id)
124
+ Budget::BudgetQuery.current_tokens(period, tenant_id: tenant_id)
125
+ end
126
+
127
+ # Returns the remaining budget for a scope and period
128
+ #
129
+ # @param scope [Symbol] :global or :agent
130
+ # @param period [Symbol] :daily or :monthly
131
+ # @param agent_type [String, nil] Required when scope is :agent
132
+ # @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
133
+ # @return [Float, nil] Remaining budget in USD, or nil if no limit configured
134
+ def remaining_budget(scope, period, agent_type: nil, tenant_id: nil)
135
+ tenant_id = Budget::ConfigResolver.resolve_tenant_id(tenant_id)
136
+ budget_config = Budget::ConfigResolver.resolve_budget_config(tenant_id)
137
+
138
+ Budget::BudgetQuery.remaining_budget(scope, period, agent_type: agent_type, tenant_id: tenant_id, budget_config: budget_config)
139
+ end
140
+
141
+ # Returns the remaining token budget for a period (global only)
142
+ #
143
+ # @param period [Symbol] :daily or :monthly
144
+ # @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
145
+ # @return [Integer, nil] Remaining token budget, or nil if no limit configured
146
+ def remaining_token_budget(period, tenant_id: nil)
147
+ tenant_id = Budget::ConfigResolver.resolve_tenant_id(tenant_id)
148
+ budget_config = Budget::ConfigResolver.resolve_budget_config(tenant_id)
149
+
150
+ Budget::BudgetQuery.remaining_token_budget(period, tenant_id: tenant_id, budget_config: budget_config)
151
+ end
152
+
153
+ # Returns a summary of all budget statuses
154
+ #
155
+ # @param agent_type [String, nil] Optional agent type for per-agent budgets
156
+ # @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
157
+ # @return [Hash] Budget status information
158
+ def status(agent_type: nil, tenant_id: nil)
159
+ tenant_id = Budget::ConfigResolver.resolve_tenant_id(tenant_id)
160
+ budget_config = Budget::ConfigResolver.resolve_budget_config(tenant_id)
161
+
162
+ Budget::BudgetQuery.status(agent_type: agent_type, tenant_id: tenant_id, budget_config: budget_config)
163
+ end
164
+
165
+ # Calculates budget forecasts based on current spending trends
166
+ #
167
+ # @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
168
+ # @return [Hash, nil] Forecast information
169
+ def calculate_forecast(tenant_id: nil)
170
+ tenant_id = Budget::ConfigResolver.resolve_tenant_id(tenant_id)
171
+ budget_config = Budget::ConfigResolver.resolve_budget_config(tenant_id)
172
+
173
+ Budget::Forecaster.calculate_forecast(tenant_id: tenant_id, budget_config: budget_config)
174
+ end
175
+
176
+ # Resets all budget counters (useful for testing)
177
+ #
178
+ # @param tenant_id [String, nil] Optional tenant identifier to reset only that tenant's counters
179
+ # @return [void]
180
+ def reset!(tenant_id: nil)
181
+ tenant_id = Budget::ConfigResolver.resolve_tenant_id(tenant_id)
182
+ tenant_part = Budget::SpendRecorder.tenant_key_part(tenant_id)
183
+ today = Budget::SpendRecorder.date_key_part(:daily)
184
+ month = Budget::SpendRecorder.date_key_part(:monthly)
185
+
186
+ BudgetTracker.cache_delete(BudgetTracker.cache_key("budget", tenant_part, today))
187
+ BudgetTracker.cache_delete(BudgetTracker.cache_key("budget", tenant_part, month))
188
+
189
+ # Reset memoized table existence check (useful for testing)
190
+ Budget::ConfigResolver.reset_tenant_budget_table_check!
191
+ end
192
+
193
+ private
194
+
195
+ # Checks budget limits and raises error if exceeded
196
+ #
197
+ # @param agent_type [String] The agent class name
198
+ # @param tenant_id [String, nil] The tenant identifier
199
+ # @param budget_config [Hash] The budget configuration
200
+ # @raise [Reliability::BudgetExceededError] If limit exceeded
201
+ # @return [void]
202
+ def check_budget_limits!(agent_type, tenant_id, budget_config)
203
+ # Check global daily budget
204
+ if budget_config[:global_daily]
205
+ current = Budget::BudgetQuery.current_spend(:global, :daily, tenant_id: tenant_id)
206
+ if current >= budget_config[:global_daily]
207
+ raise Reliability::BudgetExceededError.new(:global_daily, budget_config[:global_daily], current, tenant_id: tenant_id)
208
+ end
209
+ end
210
+
211
+ # Check global monthly budget
212
+ if budget_config[:global_monthly]
213
+ current = Budget::BudgetQuery.current_spend(:global, :monthly, tenant_id: tenant_id)
214
+ if current >= budget_config[:global_monthly]
215
+ raise Reliability::BudgetExceededError.new(:global_monthly, budget_config[:global_monthly], current, tenant_id: tenant_id)
216
+ end
217
+ end
218
+
219
+ # Check per-agent daily budget
220
+ agent_daily_limit = budget_config[:per_agent_daily]&.dig(agent_type)
221
+ if agent_daily_limit
222
+ current = Budget::BudgetQuery.current_spend(:agent, :daily, agent_type: agent_type, tenant_id: tenant_id)
223
+ if current >= agent_daily_limit
224
+ raise Reliability::BudgetExceededError.new(:per_agent_daily, agent_daily_limit, current, agent_type: agent_type, tenant_id: tenant_id)
225
+ end
226
+ end
227
+
228
+ # Check per-agent monthly budget
229
+ agent_monthly_limit = budget_config[:per_agent_monthly]&.dig(agent_type)
230
+ if agent_monthly_limit
231
+ current = Budget::BudgetQuery.current_spend(:agent, :monthly, agent_type: agent_type, tenant_id: tenant_id)
232
+ if current >= agent_monthly_limit
233
+ raise Reliability::BudgetExceededError.new(:per_agent_monthly, agent_monthly_limit, current, agent_type: agent_type, tenant_id: tenant_id)
234
+ end
235
+ end
236
+ end
237
+
238
+ # Checks token limits and raises error if exceeded
239
+ #
240
+ # @param agent_type [String] The agent class name
241
+ # @param tenant_id [String, nil] The tenant identifier
242
+ # @param budget_config [Hash] The budget configuration
243
+ # @raise [Reliability::BudgetExceededError] If limit exceeded
244
+ # @return [void]
245
+ def check_token_limits!(agent_type, tenant_id, budget_config)
246
+ # Check global daily token budget
247
+ if budget_config[:global_daily_tokens]
248
+ current = Budget::BudgetQuery.current_tokens(:daily, tenant_id: tenant_id)
249
+ if current >= budget_config[:global_daily_tokens]
250
+ raise Reliability::BudgetExceededError.new(
251
+ :global_daily_tokens,
252
+ budget_config[:global_daily_tokens],
253
+ current,
254
+ tenant_id: tenant_id
255
+ )
256
+ end
257
+ end
258
+
259
+ # Check global monthly token budget
260
+ if budget_config[:global_monthly_tokens]
261
+ current = Budget::BudgetQuery.current_tokens(:monthly, tenant_id: tenant_id)
262
+ if current >= budget_config[:global_monthly_tokens]
263
+ raise Reliability::BudgetExceededError.new(
264
+ :global_monthly_tokens,
265
+ budget_config[:global_monthly_tokens],
266
+ current,
267
+ tenant_id: tenant_id
268
+ )
269
+ end
270
+ end
271
+ end
272
+ end
273
+ end
274
+ end
275
+ end
@@ -25,7 +25,9 @@ module RubyLLM
25
25
  # @param execution_data [Hash] Execution attributes from instrumentation
26
26
  # @return [void]
27
27
  def perform(execution_data)
28
- execution = Execution.create!(execution_data)
28
+ # Filter to only known attributes to prevent schema mismatches
29
+ filtered_data = filter_known_attributes(execution_data)
30
+ execution = Execution.create!(filtered_data)
29
31
 
30
32
  # Calculate costs if token data is available
31
33
  if execution.input_tokens && execution.output_tokens
@@ -39,6 +41,20 @@ module RubyLLM
39
41
 
40
42
  private
41
43
 
44
+ # Filters data to only include attributes that exist on the Execution model
45
+ #
46
+ # This provides a safety net against schema mismatches, such as when
47
+ # tenant_id is passed but the column doesn't exist in the database.
48
+ #
49
+ # @param data [Hash] The raw execution data
50
+ # @return [Hash] Filtered data with only known attributes
51
+ def filter_known_attributes(data)
52
+ return data unless defined?(Execution) && Execution.respond_to?(:column_names)
53
+
54
+ known_columns = Execution.column_names
55
+ data.select { |key, _| known_columns.include?(key.to_s) }
56
+ end
57
+
42
58
  # Checks if execution should be flagged as anomalous
43
59
  #
44
60
  # @param execution [Execution] The execution to check
@@ -32,7 +32,8 @@ module RubyLLM
32
32
  backoff: retries_config[:backoff] || :exponential,
33
33
  base: retries_config[:base] || 0.4,
34
34
  max_delay: retries_config[:max_delay] || 3.0,
35
- on: retries_config[:on] || []
35
+ on: retries_config[:on] || [],
36
+ patterns: config[:retryable_patterns]
36
37
  )
37
38
 
38
39
  @fallback_routing = FallbackRouting.new(