ruby_llm-agents 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/ruby_llm/agents/paginatable.rb +9 -3
  3. data/app/controllers/concerns/ruby_llm/agents/sortable.rb +58 -0
  4. data/app/controllers/ruby_llm/agents/agents_controller.rb +59 -16
  5. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +144 -20
  6. data/app/controllers/ruby_llm/agents/executions_controller.rb +13 -16
  7. data/app/controllers/ruby_llm/agents/workflows_controller.rb +279 -90
  8. data/app/helpers/ruby_llm/agents/application_helper.rb +100 -0
  9. data/app/mailers/ruby_llm/agents/alert_mailer.rb +84 -0
  10. data/app/mailers/ruby_llm/agents/application_mailer.rb +28 -0
  11. data/app/models/ruby_llm/agents/execution/analytics.rb +170 -20
  12. data/app/models/ruby_llm/agents/execution/scopes.rb +0 -31
  13. data/app/models/ruby_llm/agents/execution/workflow.rb +0 -129
  14. data/app/models/ruby_llm/agents/execution.rb +50 -14
  15. data/app/models/ruby_llm/agents/tenant/budgetable.rb +277 -0
  16. data/app/models/ruby_llm/agents/tenant/configurable.rb +135 -0
  17. data/app/models/ruby_llm/agents/tenant/trackable.rb +310 -0
  18. data/app/models/ruby_llm/agents/tenant.rb +146 -0
  19. data/app/models/ruby_llm/agents/tenant_budget.rb +12 -253
  20. data/app/services/ruby_llm/agents/agent_registry.rb +18 -12
  21. data/app/views/layouts/ruby_llm/agents/application.html.erb +72 -76
  22. data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -12
  23. data/app/views/ruby_llm/agents/agents/_sortable_header.html.erb +56 -0
  24. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +5 -15
  25. data/app/views/ruby_llm/agents/agents/index.html.erb +271 -100
  26. data/app/views/ruby_llm/agents/agents/show.html.erb +1 -0
  27. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +107 -0
  28. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +18 -0
  29. data/app/views/ruby_llm/agents/api_configurations/show.html.erb +4 -1
  30. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +66 -359
  31. data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +56 -0
  32. data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +115 -0
  33. data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +35 -60
  34. data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +17 -6
  35. data/app/views/ruby_llm/agents/dashboard/index.html.erb +373 -72
  36. data/app/views/ruby_llm/agents/executions/_execution.html.erb +0 -1
  37. data/app/views/ruby_llm/agents/executions/_filters.html.erb +51 -39
  38. data/app/views/ruby_llm/agents/executions/_list.html.erb +53 -195
  39. data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +5 -20
  40. data/app/views/ruby_llm/agents/executions/index.html.erb +7 -83
  41. data/app/views/ruby_llm/agents/executions/show.html.erb +10 -20
  42. data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +2 -1
  43. data/app/views/ruby_llm/agents/shared/_doc_link.html.erb +12 -0
  44. data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +3 -15
  45. data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +1 -1
  46. data/app/views/ruby_llm/agents/shared/_select_dropdown.html.erb +1 -1
  47. data/app/views/ruby_llm/agents/shared/_sortable_header.html.erb +53 -0
  48. data/app/views/ruby_llm/agents/shared/_status_badge.html.erb +7 -0
  49. data/app/views/ruby_llm/agents/shared/_status_dot.html.erb +1 -1
  50. data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +9 -35
  51. data/app/views/ruby_llm/agents/system_config/show.html.erb +4 -1
  52. data/app/views/ruby_llm/agents/tenants/index.html.erb +4 -1
  53. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +7 -15
  54. data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +539 -0
  55. data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +920 -0
  56. data/app/views/ruby_llm/agents/workflows/index.html.erb +179 -0
  57. data/app/views/ruby_llm/agents/workflows/show.html.erb +164 -139
  58. data/config/routes.rb +1 -1
  59. data/lib/generators/ruby_llm_agents/agent_generator.rb +6 -36
  60. data/lib/generators/ruby_llm_agents/background_remover_generator.rb +7 -37
  61. data/lib/generators/ruby_llm_agents/embedder_generator.rb +5 -38
  62. data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +7 -37
  63. data/lib/generators/ruby_llm_agents/image_editor_generator.rb +7 -37
  64. data/lib/generators/ruby_llm_agents/image_generator_generator.rb +8 -41
  65. data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +18 -46
  66. data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +7 -37
  67. data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +7 -37
  68. data/lib/generators/ruby_llm_agents/image_variator_generator.rb +7 -37
  69. data/lib/generators/ruby_llm_agents/install_generator.rb +33 -56
  70. data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +480 -0
  71. data/lib/generators/ruby_llm_agents/multi_tenancy_generator.rb +42 -22
  72. data/lib/generators/ruby_llm_agents/restructure_generator.rb +2 -2
  73. data/lib/generators/ruby_llm_agents/speaker_generator.rb +8 -39
  74. data/lib/generators/ruby_llm_agents/templates/add_tenant_to_executions_migration.rb.tt +13 -2
  75. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +5 -8
  76. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +40 -42
  77. data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +20 -22
  78. data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +24 -26
  79. data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +20 -22
  80. data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +19 -17
  81. data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +31 -33
  82. data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +125 -127
  83. data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +20 -18
  84. data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +19 -17
  85. data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +19 -17
  86. data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +38 -40
  87. data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +42 -44
  88. data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +48 -0
  89. data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +19 -21
  90. data/lib/generators/ruby_llm_agents/templates/create_tenant_budgets_migration.rb.tt +11 -0
  91. data/lib/generators/ruby_llm_agents/templates/create_tenants_migration.rb.tt +72 -0
  92. data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +19 -21
  93. data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +20 -22
  94. data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +15 -17
  95. data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +25 -27
  96. data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +19 -21
  97. data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +20 -22
  98. data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +17 -19
  99. data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +15 -17
  100. data/lib/generators/ruby_llm_agents/templates/rename_tenant_budgets_to_tenants_migration.rb.tt +34 -0
  101. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +87 -24
  102. data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +21 -27
  103. data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +46 -54
  104. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +31 -39
  105. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +22 -28
  106. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +53 -63
  107. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +46 -56
  108. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +23 -31
  109. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +22 -30
  110. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +23 -31
  111. data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +38 -46
  112. data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +7 -7
  113. data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +59 -71
  114. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +274 -23
  115. data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +29 -31
  116. data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +28 -30
  117. data/lib/generators/ruby_llm_agents/transcriber_generator.rb +10 -43
  118. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +26 -0
  119. data/lib/ruby_llm/agents/core/configuration.rb +55 -43
  120. data/lib/ruby_llm/agents/core/llm_tenant.rb +60 -60
  121. data/lib/ruby_llm/agents/core/version.rb +1 -1
  122. data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +26 -0
  123. data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +4 -2
  124. data/lib/ruby_llm/agents/pipeline.rb +69 -0
  125. data/lib/ruby_llm/agents/workflow/approval.rb +205 -0
  126. data/lib/ruby_llm/agents/workflow/approval_store.rb +179 -0
  127. data/lib/ruby_llm/agents/workflow/dsl/executor.rb +467 -0
  128. data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +244 -0
  129. data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +289 -0
  130. data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +107 -0
  131. data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +150 -0
  132. data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +187 -0
  133. data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +352 -0
  134. data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +415 -0
  135. data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +257 -0
  136. data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +317 -0
  137. data/lib/ruby_llm/agents/workflow/dsl.rb +576 -0
  138. data/lib/ruby_llm/agents/workflow/instrumentation.rb +2 -7
  139. data/lib/ruby_llm/agents/workflow/notifiers/base.rb +117 -0
  140. data/lib/ruby_llm/agents/workflow/notifiers/email.rb +117 -0
  141. data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +180 -0
  142. data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +121 -0
  143. data/lib/ruby_llm/agents/workflow/notifiers.rb +70 -0
  144. data/lib/ruby_llm/agents/workflow/orchestrator.rb +190 -23
  145. data/lib/ruby_llm/agents/workflow/result.rb +202 -0
  146. data/lib/ruby_llm/agents/workflow/throttle_manager.rb +206 -0
  147. data/lib/ruby_llm/agents/workflow/wait_result.rb +213 -0
  148. metadata +43 -6
  149. data/app/views/ruby_llm/agents/dashboard/_execution_item.html.erb +0 -66
  150. data/lib/ruby_llm/agents/workflow/parallel.rb +0 -299
  151. data/lib/ruby_llm/agents/workflow/pipeline.rb +0 -306
  152. data/lib/ruby_llm/agents/workflow/router.rb +0 -429
@@ -147,6 +147,26 @@ module RubyLlmAgents
147
147
  )
148
148
  end
149
149
 
150
+ def create_rename_tenant_budgets_migration
151
+ # Skip if already using new table name
152
+ if table_exists?(:ruby_llm_agents_tenants)
153
+ say_status :skip, "ruby_llm_agents_tenants table already exists", :yellow
154
+ return
155
+ end
156
+
157
+ # Only run if old table exists (needs upgrade)
158
+ unless table_exists?(:ruby_llm_agents_tenant_budgets)
159
+ say_status :skip, "No tenant_budgets table to upgrade", :yellow
160
+ return
161
+ end
162
+
163
+ say_status :upgrade, "Renaming tenant_budgets to tenants", :blue
164
+ migration_template(
165
+ "rename_tenant_budgets_to_tenants_migration.rb.tt",
166
+ File.join(db_migrate_path, "rename_tenant_budgets_to_tenants.rb")
167
+ )
168
+ end
169
+
150
170
  def migrate_agents_directory
151
171
  root_dir = RubyLLM::Agents.configuration.root_directory
152
172
  namespace = RubyLLM::Agents.configuration.root_namespace
@@ -211,6 +231,12 @@ module RubyLlmAgents
211
231
  false
212
232
  end
213
233
 
234
+ def table_exists?(table)
235
+ ActiveRecord::Base.connection.table_exists?(table)
236
+ rescue StandardError
237
+ false
238
+ end
239
+
214
240
  def migrate_directory(old_dir, new_dir, namespace)
215
241
  source = Rails.root.join("app", old_dir)
216
242
  destination = Rails.root.join("app", new_dir)
@@ -702,8 +702,8 @@ module RubyLLM
702
702
  @default_background_output_format = :png # Output format for transparency
703
703
 
704
704
  # Directory structure defaults
705
- @root_directory = "llm" # Root directory under app/
706
- @root_namespace = "Llm" # Root namespace for classes
705
+ @root_directory = "agents" # Root directory under app/
706
+ @root_namespace = nil # No namespace (top-level classes)
707
707
  end
708
708
 
709
709
  # Returns the configured cache store, falling back to Rails.cache
@@ -744,7 +744,8 @@ module RubyLLM
744
744
 
745
745
  alerts[:slack_webhook_url].present? ||
746
746
  alerts[:webhook_url].present? ||
747
- alerts[:custom].present?
747
+ alerts[:custom].present? ||
748
+ alerts[:email_recipients].present?
748
749
  end
749
750
 
750
751
  # Returns the list of events to alert on
@@ -822,28 +823,27 @@ module RubyLLM
822
823
  #
823
824
  # @param category [Symbol, nil] Category (:audio, :image, :text, or nil for root)
824
825
  # @return [String, nil] Full namespace string, or nil if no namespace configured
825
- # @example With default namespace
826
+ # @example With root_namespace = "Llm"
826
827
  # namespace_for(:image) #=> "Llm::Image"
827
828
  # namespace_for(nil) #=> "Llm"
828
829
  # @example With no namespace (root_namespace = nil)
829
830
  # namespace_for(:image) #=> "Image"
830
831
  # namespace_for(nil) #=> nil
831
832
  def namespace_for(category = nil)
832
- # Handle no namespace configuration
833
- if root_namespace.blank?
834
- case category
835
- when :audio then "Audio"
836
- when :image then "Image"
837
- when :text then "Text"
838
- else nil
839
- end
833
+ category_namespace = case category
834
+ when :images then "Images"
835
+ when :audio then "Audio"
836
+ when :embedders then "Embedders"
837
+ when :moderators then "Moderators"
838
+ when :workflows then "Workflows"
839
+ when :text then "Text"
840
+ when :image then "Image"
841
+ end
842
+
843
+ if root_namespace
844
+ category_namespace ? "#{root_namespace}::#{category_namespace}" : root_namespace
840
845
  else
841
- case category
842
- when :audio then "#{root_namespace}::Audio"
843
- when :image then "#{root_namespace}::Image"
844
- when :text then "#{root_namespace}::Text"
845
- else root_namespace
846
- end
846
+ category_namespace
847
847
  end
848
848
  end
849
849
 
@@ -852,15 +852,34 @@ module RubyLLM
852
852
  # @param category [Symbol, nil] Category (:audio, :image, :text, or nil)
853
853
  # @param type [String] Component type (e.g., "agents", "speakers", "generators")
854
854
  # @return [String] Full path relative to Rails.root
855
- # @example
855
+ # @example With root_directory = "llm"
856
856
  # path_for(:image, "generators") #=> "app/llm/image/generators"
857
857
  # path_for(nil, "agents") #=> "app/llm/agents"
858
- def path_for(category, type)
859
- case category
860
- when :audio then "app/#{root_directory}/audio/#{type}"
861
- when :image then "app/#{root_directory}/image/#{type}"
862
- when :text then "app/#{root_directory}/text/#{type}"
863
- else "app/#{root_directory}/#{type}"
858
+ # @example With root_directory = "agents" (default)
859
+ # path_for(:images) #=> "app/agents/images"
860
+ # path_for(nil, "tools") #=> "app/agents/tools"
861
+ def path_for(category, type = nil)
862
+ root = root_directory || "agents"
863
+ base = "app/#{root}"
864
+
865
+ category_path = case category
866
+ when :images then "images"
867
+ when :audio then "audio"
868
+ when :embedders then "embedders"
869
+ when :moderators then "moderators"
870
+ when :workflows then "workflows"
871
+ when :text then "text"
872
+ when :image then "image"
873
+ end
874
+
875
+ if category_path && type
876
+ "#{base}/#{category_path}/#{type}"
877
+ elsif category_path
878
+ "#{base}/#{category_path}"
879
+ elsif type
880
+ "#{base}/#{type}"
881
+ else
882
+ base
864
883
  end
865
884
  end
866
885
 
@@ -868,25 +887,18 @@ module RubyLLM
868
887
  #
869
888
  # @return [Array<String>] List of paths relative to Rails.root
870
889
  def all_autoload_paths
890
+ root = root_directory || "agents"
891
+ base = "app/#{root}"
892
+
871
893
  [
872
- # Top-level
873
- path_for(nil, "agents"),
874
- path_for(nil, "workflows"),
875
- path_for(nil, "tools"),
876
- # Audio
877
- path_for(:audio, "speakers"),
878
- path_for(:audio, "transcribers"),
879
- # Image
880
- path_for(:image, "analyzers"),
881
- path_for(:image, "background_removers"),
882
- path_for(:image, "editors"),
883
- path_for(:image, "generators"),
884
- path_for(:image, "transformers"),
885
- path_for(:image, "upscalers"),
886
- path_for(:image, "variators"),
887
- # Text
888
- path_for(:text, "embedders"),
889
- path_for(:text, "moderators")
894
+ base,
895
+ "app/workflows", # Top-level workflows directory
896
+ "#{base}/images",
897
+ "#{base}/audio",
898
+ "#{base}/embedders",
899
+ "#{base}/moderators",
900
+ "#{base}/workflows",
901
+ "#{base}/tools"
890
902
  ]
891
903
  end
892
904
 
@@ -12,21 +12,25 @@ module RubyLLM
12
12
  #
13
13
  # @example Basic usage
14
14
  # class Organization < ApplicationRecord
15
+ # include RubyLLM::Agents::LLMTenant
15
16
  # llm_tenant
16
17
  # end
17
18
  #
18
19
  # @example With custom ID method
19
20
  # class Organization < ApplicationRecord
21
+ # include RubyLLM::Agents::LLMTenant
20
22
  # llm_tenant id: :slug
21
23
  # end
22
24
  #
23
25
  # @example With auto-created budget
24
26
  # class Organization < ApplicationRecord
27
+ # include RubyLLM::Agents::LLMTenant
25
28
  # llm_tenant id: :slug, budget: true
26
29
  # end
27
30
  #
28
31
  # @example With limits (auto-creates budget)
29
32
  # class Organization < ApplicationRecord
33
+ # include RubyLLM::Agents::LLMTenant
30
34
  # llm_tenant(
31
35
  # id: :slug,
32
36
  # name: :company_name,
@@ -41,6 +45,7 @@ module RubyLLM
41
45
  #
42
46
  # @example With API keys from model columns/methods
43
47
  # class Organization < ApplicationRecord
48
+ # include RubyLLM::Agents::LLMTenant
44
49
  # encrypts :openai_api_key, :anthropic_api_key # Rails 7+ encryption
45
50
  #
46
51
  # llm_tenant(
@@ -57,7 +62,7 @@ module RubyLLM
57
62
  # end
58
63
  # end
59
64
  #
60
- # @see RubyLLM::Agents::TenantBudget
65
+ # @see RubyLLM::Agents::Tenant
61
66
  # @api public
62
67
  module LLMTenant
63
68
  extend ActiveSupport::Concern
@@ -69,12 +74,16 @@ module RubyLLM
69
74
  as: :tenant_record,
70
75
  dependent: :nullify
71
76
 
72
- # Budget association (optional)
73
- has_one :llm_budget,
74
- class_name: "RubyLLM::Agents::TenantBudget",
77
+ # Link to gem's Tenant model (new name)
78
+ has_one :llm_tenant_record,
79
+ class_name: "RubyLLM::Agents::Tenant",
75
80
  as: :tenant_record,
76
81
  dependent: :destroy
77
82
 
83
+ # Backward compatible alias (llm_budget points to same Tenant record)
84
+ # @deprecated Use llm_tenant_record instead
85
+ alias_method :llm_budget_association, :llm_tenant_record
86
+
78
87
  # Store options at class level
79
88
  class_attribute :llm_tenant_options, default: {}
80
89
  end
@@ -84,7 +93,7 @@ module RubyLLM
84
93
  #
85
94
  # @param id [Symbol] Method to call for tenant_id string (default: :id)
86
95
  # @param name [Symbol] Method for budget display name (default: :to_s)
87
- # @param budget [Boolean] Auto-create TenantBudget on model creation (default: false)
96
+ # @param budget [Boolean] Auto-create Tenant record on model creation (default: false)
88
97
  # @param limits [Hash] Default budget limits (implies budget: true)
89
98
  # @param enforcement [Symbol] Budget enforcement mode (:none, :soft, :hard)
90
99
  # @param inherit_global [Boolean] Inherit from global config (default: true)
@@ -101,8 +110,8 @@ module RubyLLM
101
110
  api_keys: api_keys
102
111
  }
103
112
 
104
- # Auto-create budget callback
105
- after_create :create_default_llm_budget if llm_tenant_options[:budget]
113
+ # Auto-create tenant record callback
114
+ after_create :create_default_llm_tenant if llm_tenant_options[:budget]
106
115
  end
107
116
 
108
117
  private
@@ -152,13 +161,42 @@ module RubyLLM
152
161
  end.compact
153
162
  end
154
163
 
164
+ # Returns or builds the associated Tenant record
165
+ #
166
+ # @return [Tenant] The tenant record
167
+ def llm_tenant
168
+ llm_tenant_record || build_llm_tenant_record(tenant_id: llm_tenant_id)
169
+ end
170
+
171
+ # Backward compatible alias for llm_tenant
172
+ # @deprecated Use llm_tenant instead
173
+ alias_method :llm_budget, :llm_tenant
174
+
175
+ # Configure tenant with a block
176
+ #
177
+ # @yield [tenant] The tenant to configure
178
+ # @return [Tenant] The saved tenant
179
+ def llm_configure(&block)
180
+ tenant = llm_tenant
181
+ yield(tenant) if block_given?
182
+ tenant.save!
183
+ tenant
184
+ end
185
+
186
+ # Backward compatible alias
187
+ # @deprecated Use llm_configure instead
188
+ alias_method :llm_configure_budget, :llm_configure
189
+
190
+ # Tracking methods using llm_executions association
191
+ # These query executions via the polymorphic tenant_record association
192
+
155
193
  # Returns cost for a given period
156
194
  #
157
195
  # @param period [Symbol, Range, nil] Time period (:today, :this_month, etc.)
158
196
  # @return [BigDecimal] Total cost
159
197
  def llm_cost(period: nil)
160
198
  scope = llm_executions
161
- scope = apply_period_scope(scope, period) if period
199
+ scope = apply_llm_period_scope(scope, period) if period
162
200
  scope.sum(:total_cost) || 0
163
201
  end
164
202
 
@@ -182,7 +220,7 @@ module RubyLLM
182
220
  # @return [Integer] Total tokens
183
221
  def llm_tokens(period: nil)
184
222
  scope = llm_executions
185
- scope = apply_period_scope(scope, period) if period
223
+ scope = apply_llm_period_scope(scope, period) if period
186
224
  scope.sum(:total_tokens) || 0
187
225
  end
188
226
 
@@ -206,7 +244,7 @@ module RubyLLM
206
244
  # @return [Integer] Execution count
207
245
  def llm_execution_count(period: nil)
208
246
  scope = llm_executions
209
- scope = apply_period_scope(scope, period) if period
247
+ scope = apply_llm_period_scope(scope, period) if period
210
248
  scope.count
211
249
  end
212
250
 
@@ -237,29 +275,13 @@ module RubyLLM
237
275
  }
238
276
  end
239
277
 
240
- # Returns or builds the associated TenantBudget
241
- #
242
- # @return [TenantBudget] The budget record
243
- def llm_budget
244
- super || build_llm_budget(tenant_id: llm_tenant_id)
245
- end
246
-
247
- # Configure budget with a block
248
- #
249
- # @yield [budget] The budget to configure
250
- # @return [TenantBudget] The saved budget
251
- def llm_configure_budget
252
- budget = llm_budget
253
- yield(budget) if block_given?
254
- budget.save!
255
- budget
256
- end
278
+ # Delegate budget methods to the Tenant record
257
279
 
258
280
  # Returns the budget status from BudgetTracker
259
281
  #
260
282
  # @return [Hash] Budget status
261
283
  def llm_budget_status
262
- BudgetTracker.status(tenant_id: llm_tenant_id)
284
+ llm_tenant.budget_status
263
285
  end
264
286
 
265
287
  # Checks if within budget for a given limit type
@@ -267,11 +289,7 @@ module RubyLLM
267
289
  # @param type [Symbol] Limit type (:daily_cost, :monthly_cost, :daily_tokens, etc.)
268
290
  # @return [Boolean] true if within budget
269
291
  def llm_within_budget?(type: :daily_cost)
270
- status = llm_budget_status
271
- return true unless status[:enabled]
272
-
273
- key = budget_status_key(type)
274
- status.dig(key, :percentage_used).to_f < 100
292
+ llm_tenant.within_budget?(type: type)
275
293
  end
276
294
 
277
295
  # Returns remaining budget for a given limit type
@@ -279,9 +297,7 @@ module RubyLLM
279
297
  # @param type [Symbol] Limit type
280
298
  # @return [Numeric, nil] Remaining amount
281
299
  def llm_remaining_budget(type: :daily_cost)
282
- status = llm_budget_status
283
- key = budget_status_key(type)
284
- status.dig(key, :remaining)
300
+ llm_tenant.remaining_budget(type: type)
285
301
  end
286
302
 
287
303
  # Raises an error if over budget
@@ -289,7 +305,7 @@ module RubyLLM
289
305
  # @raise [BudgetExceededError] if budget is exceeded
290
306
  # @return [void]
291
307
  def llm_check_budget!
292
- BudgetTracker.check_budget!(self.class.name, tenant_id: llm_tenant_id)
308
+ llm_tenant.check_budget!(self.class.name)
293
309
  end
294
310
 
295
311
  private
@@ -299,7 +315,7 @@ module RubyLLM
299
315
  # @param scope [ActiveRecord::Relation] The query scope
300
316
  # @param period [Symbol, Range] The period to filter by
301
317
  # @return [ActiveRecord::Relation] Filtered scope
302
- def apply_period_scope(scope, period)
318
+ def apply_llm_period_scope(scope, period)
303
319
  case period
304
320
  when :today then scope.where(created_at: Time.current.all_day)
305
321
  when :yesterday then scope.where(created_at: 1.day.ago.all_day)
@@ -310,34 +326,18 @@ module RubyLLM
310
326
  end
311
327
  end
312
328
 
313
- # Maps user-friendly type to budget status key
314
- #
315
- # @param type [Symbol] User-friendly type
316
- # @return [Symbol] Status key
317
- def budget_status_key(type)
318
- case type
319
- when :daily_cost then :global_daily
320
- when :monthly_cost then :global_monthly
321
- when :daily_tokens then :global_daily_tokens
322
- when :monthly_tokens then :global_monthly_tokens
323
- when :daily_executions then :global_daily_executions
324
- when :monthly_executions then :global_monthly_executions
325
- else :global_daily
326
- end
327
- end
328
-
329
- # Creates the default budget on model creation
329
+ # Creates the default tenant record on model creation
330
330
  #
331
331
  # @return [void]
332
- def create_default_llm_budget
332
+ def create_default_llm_tenant
333
333
  return if self.class.llm_tenant_options.blank?
334
- return if llm_budget&.persisted?
334
+ return if llm_tenant_record&.persisted?
335
335
 
336
336
  options = self.class.llm_tenant_options
337
337
  limits = options[:limits] || {}
338
338
  name_method = options[:name] || :to_s
339
339
 
340
- budget = build_llm_budget(
340
+ tenant = build_llm_tenant_record(
341
341
  tenant_id: llm_tenant_id,
342
342
  name: send(name_method).to_s,
343
343
  daily_limit: limits[:daily_cost],
@@ -350,8 +350,8 @@ module RubyLLM
350
350
  inherit_global_defaults: options.fetch(:inherit_global, true)
351
351
  )
352
352
 
353
- budget.tenant_record = self
354
- budget.save!
353
+ tenant.tenant_record = self
354
+ tenant.save!
355
355
  end
356
356
  end
357
357
  end
@@ -4,6 +4,6 @@ module RubyLLM
4
4
  module Agents
5
5
  # Current version of the RubyLLM::Agents gem
6
6
  # @return [String] Semantic version string
7
- VERSION = "1.0.0"
7
+ VERSION = "1.2.0"
8
8
  end
9
9
  end
@@ -46,6 +46,14 @@ module RubyLLM
46
46
  call_custom_alert(alerts[:custom], event, full_payload)
47
47
  end
48
48
 
49
+ # Send email alerts
50
+ if alerts[:email_recipients].present?
51
+ email_events = alerts[:email_events] || config.alert_events
52
+ if email_events.include?(event)
53
+ send_email_alerts(event, full_payload, alerts[:email_recipients])
54
+ end
55
+ end
56
+
49
57
  # Emit ActiveSupport::Notification for observability
50
58
  emit_notification(event, full_payload)
51
59
  rescue StandardError => e
@@ -92,6 +100,24 @@ module RubyLLM
92
100
  Rails.logger.warn("[RubyLLM::Agents::AlertManager] Custom alert failed: #{e.message}")
93
101
  end
94
102
 
103
+ # Sends email alerts to configured recipients
104
+ #
105
+ # @param event [Symbol] The event type
106
+ # @param payload [Hash] The payload
107
+ # @param recipients [Array<String>] Email addresses
108
+ # @return [void]
109
+ def send_email_alerts(event, payload, recipients)
110
+ Array(recipients).each do |recipient|
111
+ AlertMailer.alert_notification(
112
+ event: event,
113
+ payload: payload,
114
+ recipient: recipient
115
+ ).deliver_later
116
+ end
117
+ rescue StandardError => e
118
+ Rails.logger.warn("[RubyLLM::Agents::AlertManager] Email alert failed: #{e.message}")
119
+ end
120
+
95
121
  # Emits an ActiveSupport::Notification
96
122
  #
97
123
  # @param event [Symbol] The event type
@@ -125,13 +125,15 @@ module RubyLLM
125
125
  nil
126
126
  end
127
127
 
128
- # Checks if the tenant_budgets table exists
128
+ # Checks if the tenants table exists (supports old and new table names)
129
129
  #
130
130
  # @return [Boolean] true if table exists
131
131
  def tenant_budget_table_exists?
132
132
  return @tenant_budget_table_exists if defined?(@tenant_budget_table_exists)
133
133
 
134
- @tenant_budget_table_exists = ::ActiveRecord::Base.connection.table_exists?(:ruby_llm_agents_tenant_budgets)
134
+ # Check for new table name (tenants) or old table name (tenant_budgets) for backward compatibility
135
+ @tenant_budget_table_exists = ::ActiveRecord::Base.connection.table_exists?(:ruby_llm_agents_tenants) ||
136
+ ::ActiveRecord::Base.connection.table_exists?(:ruby_llm_agents_tenant_budgets)
135
137
  rescue StandardError
136
138
  @tenant_budget_table_exists = false
137
139
  end
@@ -40,6 +40,75 @@ require_relative "pipeline/middleware/reliability"
40
40
  module RubyLLM
41
41
  module Agents
42
42
  module Pipeline
43
+ # Represents an error result from a failed step
44
+ #
45
+ # Used to track errors that occurred during step execution while
46
+ # allowing the workflow to continue (for optional steps).
47
+ #
48
+ # @api public
49
+ class ErrorResult
50
+ attr_reader :step_name, :error_class, :error_message
51
+
52
+ def initialize(step_name:, error_class:, error_message:)
53
+ @step_name = step_name
54
+ @error_class = error_class
55
+ @error_message = error_message
56
+ end
57
+
58
+ def content
59
+ nil
60
+ end
61
+
62
+ def success?
63
+ false
64
+ end
65
+
66
+ def error?
67
+ true
68
+ end
69
+
70
+ def skipped?
71
+ false
72
+ end
73
+
74
+ def input_tokens
75
+ 0
76
+ end
77
+
78
+ def output_tokens
79
+ 0
80
+ end
81
+
82
+ def total_tokens
83
+ 0
84
+ end
85
+
86
+ def cached_tokens
87
+ 0
88
+ end
89
+
90
+ def input_cost
91
+ 0.0
92
+ end
93
+
94
+ def output_cost
95
+ 0.0
96
+ end
97
+
98
+ def total_cost
99
+ 0.0
100
+ end
101
+
102
+ def to_h
103
+ {
104
+ error: true,
105
+ step_name: step_name,
106
+ error_class: error_class,
107
+ error_message: error_message
108
+ }
109
+ end
110
+ end
111
+
43
112
  class << self
44
113
  # Build a pipeline for an agent class with default middleware
45
114
  #