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.
- checksums.yaml +4 -4
- data/app/controllers/concerns/ruby_llm/agents/paginatable.rb +9 -3
- data/app/controllers/concerns/ruby_llm/agents/sortable.rb +58 -0
- data/app/controllers/ruby_llm/agents/agents_controller.rb +59 -16
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +144 -20
- data/app/controllers/ruby_llm/agents/executions_controller.rb +13 -16
- data/app/controllers/ruby_llm/agents/workflows_controller.rb +279 -90
- data/app/helpers/ruby_llm/agents/application_helper.rb +100 -0
- data/app/mailers/ruby_llm/agents/alert_mailer.rb +84 -0
- data/app/mailers/ruby_llm/agents/application_mailer.rb +28 -0
- data/app/models/ruby_llm/agents/execution/analytics.rb +170 -20
- data/app/models/ruby_llm/agents/execution/scopes.rb +0 -31
- data/app/models/ruby_llm/agents/execution/workflow.rb +0 -129
- data/app/models/ruby_llm/agents/execution.rb +50 -14
- data/app/models/ruby_llm/agents/tenant/budgetable.rb +277 -0
- data/app/models/ruby_llm/agents/tenant/configurable.rb +135 -0
- data/app/models/ruby_llm/agents/tenant/trackable.rb +310 -0
- data/app/models/ruby_llm/agents/tenant.rb +146 -0
- data/app/models/ruby_llm/agents/tenant_budget.rb +12 -253
- data/app/services/ruby_llm/agents/agent_registry.rb +18 -12
- data/app/views/layouts/ruby_llm/agents/application.html.erb +72 -76
- data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -12
- data/app/views/ruby_llm/agents/agents/_sortable_header.html.erb +56 -0
- data/app/views/ruby_llm/agents/agents/_workflow.html.erb +5 -15
- data/app/views/ruby_llm/agents/agents/index.html.erb +271 -100
- data/app/views/ruby_llm/agents/agents/show.html.erb +1 -0
- data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +107 -0
- data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +18 -0
- data/app/views/ruby_llm/agents/api_configurations/show.html.erb +4 -1
- data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +66 -359
- data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +56 -0
- data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +115 -0
- data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +35 -60
- data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +17 -6
- data/app/views/ruby_llm/agents/dashboard/index.html.erb +373 -72
- data/app/views/ruby_llm/agents/executions/_execution.html.erb +0 -1
- data/app/views/ruby_llm/agents/executions/_filters.html.erb +51 -39
- data/app/views/ruby_llm/agents/executions/_list.html.erb +53 -195
- data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +5 -20
- data/app/views/ruby_llm/agents/executions/index.html.erb +7 -83
- data/app/views/ruby_llm/agents/executions/show.html.erb +10 -20
- data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +2 -1
- data/app/views/ruby_llm/agents/shared/_doc_link.html.erb +12 -0
- data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +3 -15
- data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +1 -1
- data/app/views/ruby_llm/agents/shared/_select_dropdown.html.erb +1 -1
- data/app/views/ruby_llm/agents/shared/_sortable_header.html.erb +53 -0
- data/app/views/ruby_llm/agents/shared/_status_badge.html.erb +7 -0
- data/app/views/ruby_llm/agents/shared/_status_dot.html.erb +1 -1
- data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +9 -35
- data/app/views/ruby_llm/agents/system_config/show.html.erb +4 -1
- data/app/views/ruby_llm/agents/tenants/index.html.erb +4 -1
- data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +7 -15
- data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +539 -0
- data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +920 -0
- data/app/views/ruby_llm/agents/workflows/index.html.erb +179 -0
- data/app/views/ruby_llm/agents/workflows/show.html.erb +164 -139
- data/config/routes.rb +1 -1
- data/lib/generators/ruby_llm_agents/agent_generator.rb +6 -36
- data/lib/generators/ruby_llm_agents/background_remover_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/embedder_generator.rb +5 -38
- data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/image_editor_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/image_generator_generator.rb +8 -41
- data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +18 -46
- data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/image_variator_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/install_generator.rb +33 -56
- data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +480 -0
- data/lib/generators/ruby_llm_agents/multi_tenancy_generator.rb +42 -22
- data/lib/generators/ruby_llm_agents/restructure_generator.rb +2 -2
- data/lib/generators/ruby_llm_agents/speaker_generator.rb +8 -39
- data/lib/generators/ruby_llm_agents/templates/add_tenant_to_executions_migration.rb.tt +13 -2
- data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +5 -8
- data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +40 -42
- data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +20 -22
- data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +24 -26
- data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +20 -22
- data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +19 -17
- data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +31 -33
- data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +125 -127
- data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +20 -18
- data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +19 -17
- data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +19 -17
- data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +38 -40
- data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +42 -44
- data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +48 -0
- data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +19 -21
- data/lib/generators/ruby_llm_agents/templates/create_tenant_budgets_migration.rb.tt +11 -0
- data/lib/generators/ruby_llm_agents/templates/create_tenants_migration.rb.tt +72 -0
- data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +19 -21
- data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +20 -22
- data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +15 -17
- data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +25 -27
- data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +19 -21
- data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +20 -22
- data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +17 -19
- data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +15 -17
- data/lib/generators/ruby_llm_agents/templates/rename_tenant_budgets_to_tenants_migration.rb.tt +34 -0
- data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +87 -24
- data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +21 -27
- data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +46 -54
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +31 -39
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +22 -28
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +53 -63
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +46 -56
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +23 -31
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +22 -30
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +23 -31
- data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +38 -46
- data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +7 -7
- data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +59 -71
- data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +274 -23
- data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +29 -31
- data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +28 -30
- data/lib/generators/ruby_llm_agents/transcriber_generator.rb +10 -43
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +26 -0
- data/lib/ruby_llm/agents/core/configuration.rb +55 -43
- data/lib/ruby_llm/agents/core/llm_tenant.rb +60 -60
- data/lib/ruby_llm/agents/core/version.rb +1 -1
- data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +26 -0
- data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +4 -2
- data/lib/ruby_llm/agents/pipeline.rb +69 -0
- data/lib/ruby_llm/agents/workflow/approval.rb +205 -0
- data/lib/ruby_llm/agents/workflow/approval_store.rb +179 -0
- data/lib/ruby_llm/agents/workflow/dsl/executor.rb +467 -0
- data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +244 -0
- data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +289 -0
- data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +107 -0
- data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +150 -0
- data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +187 -0
- data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +352 -0
- data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +415 -0
- data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +257 -0
- data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +317 -0
- data/lib/ruby_llm/agents/workflow/dsl.rb +576 -0
- data/lib/ruby_llm/agents/workflow/instrumentation.rb +2 -7
- data/lib/ruby_llm/agents/workflow/notifiers/base.rb +117 -0
- data/lib/ruby_llm/agents/workflow/notifiers/email.rb +117 -0
- data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +180 -0
- data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +121 -0
- data/lib/ruby_llm/agents/workflow/notifiers.rb +70 -0
- data/lib/ruby_llm/agents/workflow/orchestrator.rb +190 -23
- data/lib/ruby_llm/agents/workflow/result.rb +202 -0
- data/lib/ruby_llm/agents/workflow/throttle_manager.rb +206 -0
- data/lib/ruby_llm/agents/workflow/wait_result.rb +213 -0
- metadata +43 -6
- data/app/views/ruby_llm/agents/dashboard/_execution_item.html.erb +0 -66
- data/lib/ruby_llm/agents/workflow/parallel.rb +0 -299
- data/lib/ruby_llm/agents/workflow/pipeline.rb +0 -306
- 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 = "
|
|
706
|
-
@root_namespace =
|
|
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
|
|
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
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
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
|
-
|
|
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
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
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
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
#
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
#
|
|
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::
|
|
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
|
-
#
|
|
73
|
-
has_one :
|
|
74
|
-
class_name: "RubyLLM::Agents::
|
|
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
|
|
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
|
|
105
|
-
after_create :
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
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
|
|
332
|
+
def create_default_llm_tenant
|
|
333
333
|
return if self.class.llm_tenant_options.blank?
|
|
334
|
-
return if
|
|
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
|
-
|
|
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
|
-
|
|
354
|
-
|
|
353
|
+
tenant.tenant_record = self
|
|
354
|
+
tenant.save!
|
|
355
355
|
end
|
|
356
356
|
end
|
|
357
357
|
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
|
|
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
|
-
|
|
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
|
#
|