ruby_llm-agents 0.5.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +189 -31
- data/app/controllers/ruby_llm/agents/agents_controller.rb +136 -16
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +29 -9
- data/app/controllers/ruby_llm/agents/workflows_controller.rb +355 -0
- data/app/helpers/ruby_llm/agents/application_helper.rb +25 -0
- data/app/models/ruby_llm/agents/execution.rb +3 -0
- data/app/models/ruby_llm/agents/tenant_budget.rb +58 -15
- data/app/services/ruby_llm/agents/agent_registry.rb +51 -12
- data/app/views/layouts/ruby_llm/agents/application.html.erb +2 -29
- data/app/views/ruby_llm/agents/agents/_agent.html.erb +13 -1
- data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +235 -0
- data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +70 -0
- data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +152 -0
- data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +63 -0
- data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +108 -0
- data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +91 -0
- data/app/views/ruby_llm/agents/agents/_workflow.html.erb +1 -1
- data/app/views/ruby_llm/agents/agents/index.html.erb +74 -9
- data/app/views/ruby_llm/agents/agents/show.html.erb +18 -378
- data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +269 -15
- data/app/views/ruby_llm/agents/executions/show.html.erb +16 -0
- data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +93 -0
- data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +236 -0
- data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +76 -0
- data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +74 -0
- data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +108 -0
- data/app/views/ruby_llm/agents/workflows/show.html.erb +442 -0
- data/config/routes.rb +1 -0
- data/lib/generators/ruby_llm_agents/agent_generator.rb +56 -7
- data/lib/generators/ruby_llm_agents/background_remover_generator.rb +110 -0
- data/lib/generators/ruby_llm_agents/embedder_generator.rb +107 -0
- data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +115 -0
- data/lib/generators/ruby_llm_agents/image_editor_generator.rb +108 -0
- data/lib/generators/ruby_llm_agents/image_generator_generator.rb +116 -0
- data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +178 -0
- data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +109 -0
- data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +103 -0
- data/lib/generators/ruby_llm_agents/image_variator_generator.rb +102 -0
- data/lib/generators/ruby_llm_agents/install_generator.rb +76 -4
- data/lib/generators/ruby_llm_agents/restructure_generator.rb +292 -0
- data/lib/generators/ruby_llm_agents/speaker_generator.rb +121 -0
- data/lib/generators/ruby_llm_agents/templates/add_execution_type_migration.rb.tt +8 -0
- data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +99 -84
- data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +42 -40
- data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +26 -0
- data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +50 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +26 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +20 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +38 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +139 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +21 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +20 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +20 -0
- data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +49 -0
- data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +53 -0
- data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +44 -0
- data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +41 -0
- data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +45 -0
- data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +35 -0
- data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +47 -0
- data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +50 -0
- data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +44 -0
- data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +38 -0
- data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +33 -0
- data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +228 -0
- data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +131 -0
- data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +255 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +120 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +102 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +282 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +228 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +120 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +110 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +120 -0
- data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +212 -0
- data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +227 -0
- data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +251 -0
- data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +300 -0
- data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +56 -0
- data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +51 -0
- data/lib/generators/ruby_llm_agents/transcriber_generator.rb +107 -0
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +152 -1
- data/lib/ruby_llm/agents/audio/speaker.rb +553 -0
- data/lib/ruby_llm/agents/audio/transcriber.rb +669 -0
- data/lib/ruby_llm/agents/base_agent.rb +675 -0
- data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +181 -0
- data/lib/ruby_llm/agents/core/base/moderation_execution.rb +274 -0
- data/lib/ruby_llm/agents/core/base.rb +135 -0
- data/lib/ruby_llm/agents/core/configuration.rb +981 -0
- data/lib/ruby_llm/agents/core/errors.rb +150 -0
- data/lib/ruby_llm/agents/{instrumentation.rb → core/instrumentation.rb} +22 -1
- data/lib/ruby_llm/agents/core/llm_tenant.rb +358 -0
- data/lib/ruby_llm/agents/{version.rb → core/version.rb} +1 -1
- data/lib/ruby_llm/agents/dsl/base.rb +110 -0
- data/lib/ruby_llm/agents/dsl/caching.rb +142 -0
- data/lib/ruby_llm/agents/dsl/reliability.rb +307 -0
- data/lib/ruby_llm/agents/dsl.rb +41 -0
- data/lib/ruby_llm/agents/image/analyzer/dsl.rb +130 -0
- data/lib/ruby_llm/agents/image/analyzer/execution.rb +402 -0
- data/lib/ruby_llm/agents/image/analyzer.rb +90 -0
- data/lib/ruby_llm/agents/image/background_remover/dsl.rb +154 -0
- data/lib/ruby_llm/agents/image/background_remover/execution.rb +240 -0
- data/lib/ruby_llm/agents/image/background_remover.rb +89 -0
- data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +91 -0
- data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +165 -0
- data/lib/ruby_llm/agents/image/editor/dsl.rb +56 -0
- data/lib/ruby_llm/agents/image/editor/execution.rb +207 -0
- data/lib/ruby_llm/agents/image/editor.rb +92 -0
- data/lib/ruby_llm/agents/image/generator/active_storage_support.rb +127 -0
- data/lib/ruby_llm/agents/image/generator/content_policy.rb +95 -0
- data/lib/ruby_llm/agents/image/generator/pricing.rb +353 -0
- data/lib/ruby_llm/agents/image/generator/templates.rb +124 -0
- data/lib/ruby_llm/agents/image/generator.rb +455 -0
- data/lib/ruby_llm/agents/image/pipeline/dsl.rb +213 -0
- data/lib/ruby_llm/agents/image/pipeline/execution.rb +382 -0
- data/lib/ruby_llm/agents/image/pipeline.rb +97 -0
- data/lib/ruby_llm/agents/image/transformer/dsl.rb +148 -0
- data/lib/ruby_llm/agents/image/transformer/execution.rb +223 -0
- data/lib/ruby_llm/agents/image/transformer.rb +95 -0
- data/lib/ruby_llm/agents/image/upscaler/dsl.rb +83 -0
- data/lib/ruby_llm/agents/image/upscaler/execution.rb +219 -0
- data/lib/ruby_llm/agents/image/upscaler.rb +81 -0
- data/lib/ruby_llm/agents/image/variator/dsl.rb +62 -0
- data/lib/ruby_llm/agents/image/variator/execution.rb +189 -0
- data/lib/ruby_llm/agents/image/variator.rb +80 -0
- data/lib/ruby_llm/agents/{alert_manager.rb → infrastructure/alert_manager.rb} +17 -22
- data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +145 -0
- data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +149 -0
- data/lib/ruby_llm/agents/infrastructure/budget/forecaster.rb +68 -0
- data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +279 -0
- data/lib/ruby_llm/agents/infrastructure/budget_tracker.rb +275 -0
- data/lib/ruby_llm/agents/{execution_logger_job.rb → infrastructure/execution_logger_job.rb} +17 -1
- data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/executor.rb +2 -1
- data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/retry_strategy.rb +9 -3
- data/lib/ruby_llm/agents/{reliability.rb → infrastructure/reliability.rb} +11 -21
- data/lib/ruby_llm/agents/pipeline/builder.rb +215 -0
- data/lib/ruby_llm/agents/pipeline/context.rb +255 -0
- data/lib/ruby_llm/agents/pipeline/executor.rb +86 -0
- data/lib/ruby_llm/agents/pipeline/middleware/base.rb +124 -0
- data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +95 -0
- data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +171 -0
- data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +415 -0
- data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +276 -0
- data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +196 -0
- data/lib/ruby_llm/agents/pipeline.rb +68 -0
- data/lib/ruby_llm/agents/{engine.rb → rails/engine.rb} +79 -11
- data/lib/ruby_llm/agents/results/background_removal_result.rb +286 -0
- data/lib/ruby_llm/agents/{result.rb → results/base.rb} +73 -1
- data/lib/ruby_llm/agents/results/embedding_result.rb +243 -0
- data/lib/ruby_llm/agents/results/image_analysis_result.rb +314 -0
- data/lib/ruby_llm/agents/results/image_edit_result.rb +250 -0
- data/lib/ruby_llm/agents/results/image_generation_result.rb +346 -0
- data/lib/ruby_llm/agents/results/image_pipeline_result.rb +399 -0
- data/lib/ruby_llm/agents/results/image_transform_result.rb +251 -0
- data/lib/ruby_llm/agents/results/image_upscale_result.rb +255 -0
- data/lib/ruby_llm/agents/results/image_variation_result.rb +237 -0
- data/lib/ruby_llm/agents/results/moderation_result.rb +158 -0
- data/lib/ruby_llm/agents/results/speech_result.rb +338 -0
- data/lib/ruby_llm/agents/results/transcription_result.rb +408 -0
- data/lib/ruby_llm/agents/text/embedder.rb +444 -0
- data/lib/ruby_llm/agents/text/moderator.rb +237 -0
- data/lib/ruby_llm/agents/workflow/async.rb +220 -0
- data/lib/ruby_llm/agents/workflow/async_executor.rb +156 -0
- data/lib/ruby_llm/agents/{workflow.rb → workflow/orchestrator.rb} +6 -5
- data/lib/ruby_llm/agents/workflow/parallel.rb +34 -17
- data/lib/ruby_llm/agents/workflow/thread_pool.rb +185 -0
- data/lib/ruby_llm/agents.rb +86 -20
- metadata +172 -34
- data/lib/ruby_llm/agents/base/caching.rb +0 -40
- data/lib/ruby_llm/agents/base/cost_calculation.rb +0 -105
- data/lib/ruby_llm/agents/base/dsl.rb +0 -324
- data/lib/ruby_llm/agents/base/execution.rb +0 -366
- data/lib/ruby_llm/agents/base/reliability_dsl.rb +0 -82
- data/lib/ruby_llm/agents/base/reliability_execution.rb +0 -136
- data/lib/ruby_llm/agents/base/response_building.rb +0 -86
- data/lib/ruby_llm/agents/base/tool_tracking.rb +0 -57
- data/lib/ruby_llm/agents/base.rb +0 -210
- data/lib/ruby_llm/agents/budget_tracker.rb +0 -733
- data/lib/ruby_llm/agents/configuration.rb +0 -394
- /data/lib/ruby_llm/agents/{deprecations.rb → core/deprecations.rb} +0 -0
- /data/lib/ruby_llm/agents/{inflections.rb → core/inflections.rb} +0 -0
- /data/lib/ruby_llm/agents/{resolved_config.rb → core/resolved_config.rb} +0 -0
- /data/lib/ruby_llm/agents/{attempt_tracker.rb → infrastructure/attempt_tracker.rb} +0 -0
- /data/lib/ruby_llm/agents/{cache_helper.rb → infrastructure/cache_helper.rb} +0 -0
- /data/lib/ruby_llm/agents/{circuit_breaker.rb → infrastructure/circuit_breaker.rb} +0 -0
- /data/lib/ruby_llm/agents/{redactor.rb → infrastructure/redactor.rb} +0 -0
- /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/breaker_manager.rb +0 -0
- /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/execution_constraints.rb +0 -0
- /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/fallback_routing.rb +0 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module RubyLlmAgents
|
|
7
|
+
# Restructure generator for migrating existing apps to new directory structure
|
|
8
|
+
#
|
|
9
|
+
# Migrates ruby_llm-agents directories from the flat structure:
|
|
10
|
+
# app/agents/, app/speakers/, app/embedders/, etc.
|
|
11
|
+
#
|
|
12
|
+
# To the new grouped structure under app/llm/:
|
|
13
|
+
# app/llm/agents/, app/llm/audio/speakers/, app/llm/image/generators/, etc.
|
|
14
|
+
#
|
|
15
|
+
# Usage:
|
|
16
|
+
# rails generate ruby_llm_agents:restructure
|
|
17
|
+
# rails generate ruby_llm_agents:restructure --root=ai
|
|
18
|
+
#
|
|
19
|
+
class RestructureGenerator < ::Rails::Generators::Base
|
|
20
|
+
source_root File.expand_path("templates", __dir__)
|
|
21
|
+
|
|
22
|
+
class_option :root,
|
|
23
|
+
type: :string,
|
|
24
|
+
default: nil,
|
|
25
|
+
desc: "Root directory name (default: uses config or 'llm')"
|
|
26
|
+
|
|
27
|
+
class_option :namespace,
|
|
28
|
+
type: :string,
|
|
29
|
+
default: nil,
|
|
30
|
+
desc: "Root namespace (default: camelized root or config)"
|
|
31
|
+
|
|
32
|
+
class_option :dry_run,
|
|
33
|
+
type: :boolean,
|
|
34
|
+
default: false,
|
|
35
|
+
desc: "Show what would be done without making changes"
|
|
36
|
+
|
|
37
|
+
# Maps old directory -> { category:, type: }
|
|
38
|
+
DIRECTORY_MAPPING = {
|
|
39
|
+
# Top-level under llm/
|
|
40
|
+
"agents" => { category: nil, type: "agents" },
|
|
41
|
+
"workflows" => { category: nil, type: "workflows" },
|
|
42
|
+
"tools" => { category: nil, type: "tools" },
|
|
43
|
+
|
|
44
|
+
# Audio group
|
|
45
|
+
"speakers" => { category: :audio, type: "speakers" },
|
|
46
|
+
"transcribers" => { category: :audio, type: "transcribers" },
|
|
47
|
+
|
|
48
|
+
# Image group
|
|
49
|
+
"image_generators" => { category: :image, type: "generators" },
|
|
50
|
+
"image_editors" => { category: :image, type: "editors" },
|
|
51
|
+
"image_analyzers" => { category: :image, type: "analyzers" },
|
|
52
|
+
"image_transformers" => { category: :image, type: "transformers" },
|
|
53
|
+
"image_upscalers" => { category: :image, type: "upscalers" },
|
|
54
|
+
"image_variators" => { category: :image, type: "variators" },
|
|
55
|
+
"background_removers" => { category: :image, type: "background_removers" },
|
|
56
|
+
|
|
57
|
+
# Text group
|
|
58
|
+
"embedders" => { category: :text, type: "embedders" },
|
|
59
|
+
"moderators" => { category: :text, type: "moderators" }
|
|
60
|
+
}.freeze
|
|
61
|
+
|
|
62
|
+
def validate_root_directory
|
|
63
|
+
unless root_directory.match?(/\A[a-z][a-z0-9_-]*\z/i)
|
|
64
|
+
raise ArgumentError, "Invalid root directory name: #{root_directory}. " \
|
|
65
|
+
"Must start with a letter and contain only letters, numbers, underscores, or hyphens."
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def create_directory_structure
|
|
70
|
+
say_status :create, "#{root_directory}/ directory structure", :green
|
|
71
|
+
|
|
72
|
+
if options[:dry_run]
|
|
73
|
+
say_status :dry_run, "Would create directory structure under app/#{root_directory}/", :yellow
|
|
74
|
+
return
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Create root directory
|
|
78
|
+
empty_directory "app/#{root_directory}"
|
|
79
|
+
|
|
80
|
+
# Create all subdirectories
|
|
81
|
+
config.all_autoload_paths.each do |path|
|
|
82
|
+
empty_directory path
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def move_directories
|
|
87
|
+
say ""
|
|
88
|
+
say_status :migrate, "Moving directories to new structure", :green
|
|
89
|
+
|
|
90
|
+
directories_moved = 0
|
|
91
|
+
|
|
92
|
+
DIRECTORY_MAPPING.each do |old_dir, mapping|
|
|
93
|
+
source = Rails.root.join("app", old_dir)
|
|
94
|
+
next unless File.directory?(source)
|
|
95
|
+
|
|
96
|
+
destination = Rails.root.join(config.path_for(mapping[:category], mapping[:type]))
|
|
97
|
+
|
|
98
|
+
if options[:dry_run]
|
|
99
|
+
say_status :dry_run, "Would move app/#{old_dir}/* -> #{destination}", :yellow
|
|
100
|
+
directories_moved += 1
|
|
101
|
+
next
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
move_directory_contents(source, destination, old_dir)
|
|
105
|
+
directories_moved += 1
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
if directories_moved == 0
|
|
109
|
+
say_status :skip, "No directories found to migrate", :yellow
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def update_namespaces
|
|
114
|
+
say ""
|
|
115
|
+
say_status :update, "Adding namespaces to Ruby files", :green
|
|
116
|
+
|
|
117
|
+
DIRECTORY_MAPPING.each do |_old_dir, mapping|
|
|
118
|
+
directory_path = Rails.root.join(config.path_for(mapping[:category], mapping[:type]))
|
|
119
|
+
next unless File.directory?(directory_path)
|
|
120
|
+
|
|
121
|
+
namespace = config.namespace_for(mapping[:category])
|
|
122
|
+
|
|
123
|
+
Dir.glob("#{directory_path}/**/*.rb").each do |file|
|
|
124
|
+
update_file_namespace(file, namespace)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def cleanup_empty_directories
|
|
130
|
+
say ""
|
|
131
|
+
say_status :cleanup, "Removing empty old directories", :green
|
|
132
|
+
|
|
133
|
+
DIRECTORY_MAPPING.keys.each do |old_dir|
|
|
134
|
+
path = Rails.root.join("app", old_dir)
|
|
135
|
+
next unless File.directory?(path)
|
|
136
|
+
|
|
137
|
+
if Dir.empty?(path)
|
|
138
|
+
if options[:dry_run]
|
|
139
|
+
say_status :dry_run, "Would remove empty directory app/#{old_dir}", :yellow
|
|
140
|
+
else
|
|
141
|
+
FileUtils.rmdir(path)
|
|
142
|
+
say_status :removed, "app/#{old_dir}", :red
|
|
143
|
+
end
|
|
144
|
+
else
|
|
145
|
+
say_status :warning, "app/#{old_dir} is not empty, skipping removal", :yellow
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def show_completion_message
|
|
151
|
+
say ""
|
|
152
|
+
say "=" * 60
|
|
153
|
+
say ""
|
|
154
|
+
if options[:dry_run]
|
|
155
|
+
say "Dry run complete! No changes were made.", :yellow
|
|
156
|
+
say ""
|
|
157
|
+
say "To perform the actual migration, run:"
|
|
158
|
+
say " rails generate ruby_llm_agents:restructure"
|
|
159
|
+
else
|
|
160
|
+
say "Migration complete!", :green
|
|
161
|
+
say ""
|
|
162
|
+
say "Your app now uses the new directory structure:"
|
|
163
|
+
say ""
|
|
164
|
+
say " app/#{root_directory}/"
|
|
165
|
+
say " ├── agents/"
|
|
166
|
+
say " ├── audio/"
|
|
167
|
+
say " │ ├── speakers/"
|
|
168
|
+
say " │ └── transcribers/"
|
|
169
|
+
say " ├── image/"
|
|
170
|
+
say " │ ├── analyzers/"
|
|
171
|
+
say " │ ├── generators/"
|
|
172
|
+
say " │ └── ..."
|
|
173
|
+
say " ├── text/"
|
|
174
|
+
say " │ ├── embedders/"
|
|
175
|
+
say " │ └── moderators/"
|
|
176
|
+
say " ├── workflows/"
|
|
177
|
+
say " └── tools/"
|
|
178
|
+
say ""
|
|
179
|
+
say "Namespaces have been updated to use #{root_namespace}::"
|
|
180
|
+
say ""
|
|
181
|
+
say "Next steps:"
|
|
182
|
+
say " 1. Update any explicit class references in your code"
|
|
183
|
+
say " 2. Run your test suite to verify everything works"
|
|
184
|
+
say " 3. Commit the changes"
|
|
185
|
+
end
|
|
186
|
+
say ""
|
|
187
|
+
say "=" * 60
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
private
|
|
191
|
+
|
|
192
|
+
def root_directory
|
|
193
|
+
@root_directory ||= options[:root] || RubyLLM::Agents.configuration.root_directory
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def root_namespace
|
|
197
|
+
@root_namespace ||= options[:namespace] || camelize(root_directory)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def config
|
|
201
|
+
@config ||= begin
|
|
202
|
+
c = RubyLLM::Agents.configuration.dup
|
|
203
|
+
c.root_directory = root_directory
|
|
204
|
+
c.root_namespace = root_namespace
|
|
205
|
+
c
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def camelize(str)
|
|
210
|
+
# Handle special cases for common abbreviations
|
|
211
|
+
return "AI" if str.downcase == "ai"
|
|
212
|
+
return "ML" if str.downcase == "ml"
|
|
213
|
+
return "LLM" if str.downcase == "llm"
|
|
214
|
+
|
|
215
|
+
# Standard camelization
|
|
216
|
+
str.split(/[-_]/).map(&:capitalize).join
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def move_directory_contents(source, destination, old_dir_name)
|
|
220
|
+
# Ensure destination exists
|
|
221
|
+
FileUtils.mkdir_p(destination) unless File.directory?(destination)
|
|
222
|
+
|
|
223
|
+
# Move all contents
|
|
224
|
+
Dir.glob("#{source}/**/*", File::FNM_DOTMATCH).each do |item|
|
|
225
|
+
next if item.end_with?(".", "..")
|
|
226
|
+
next if File.directory?(item)
|
|
227
|
+
|
|
228
|
+
relative_path = item.sub("#{source}/", "")
|
|
229
|
+
dest_item = File.join(destination, relative_path)
|
|
230
|
+
|
|
231
|
+
FileUtils.mkdir_p(File.dirname(dest_item))
|
|
232
|
+
FileUtils.mv(item, dest_item)
|
|
233
|
+
|
|
234
|
+
say_status :moved, "app/#{old_dir_name}/#{relative_path}", :green
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Remove old directory if empty
|
|
238
|
+
cleanup_empty_subdirs(source)
|
|
239
|
+
FileUtils.rmdir(source) if File.directory?(source) && Dir.empty?(source)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def cleanup_empty_subdirs(dir)
|
|
243
|
+
return unless File.directory?(dir)
|
|
244
|
+
|
|
245
|
+
Dir.glob("#{dir}/**/").reverse_each do |subdir|
|
|
246
|
+
FileUtils.rmdir(subdir) if Dir.empty?(subdir)
|
|
247
|
+
rescue SystemCallError
|
|
248
|
+
# Ignore errors if directory is not empty or already removed
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def update_file_namespace(file, namespace)
|
|
253
|
+
content = File.read(file)
|
|
254
|
+
|
|
255
|
+
# Skip if already has the namespace
|
|
256
|
+
first_module = namespace.split("::").first
|
|
257
|
+
return if content.include?("module #{first_module}")
|
|
258
|
+
|
|
259
|
+
# Add namespace
|
|
260
|
+
updated = add_namespace(content, namespace)
|
|
261
|
+
File.write(file, updated)
|
|
262
|
+
|
|
263
|
+
say_status :namespaced, file.sub(Rails.root.to_s + "/", ""), :blue
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def add_namespace(content, namespace)
|
|
267
|
+
modules = namespace.split("::")
|
|
268
|
+
indent = ""
|
|
269
|
+
|
|
270
|
+
# Build opening modules
|
|
271
|
+
opening = modules.map do |mod|
|
|
272
|
+
line = "#{indent}module #{mod}"
|
|
273
|
+
indent += " "
|
|
274
|
+
line
|
|
275
|
+
end.join("\n")
|
|
276
|
+
|
|
277
|
+
# Build closing modules
|
|
278
|
+
closing = modules.map { "end" }.join("\n")
|
|
279
|
+
|
|
280
|
+
# Indent original content
|
|
281
|
+
indented_content = content.lines.map do |line|
|
|
282
|
+
if line.strip.empty?
|
|
283
|
+
line
|
|
284
|
+
else
|
|
285
|
+
(" " * modules.size) + line
|
|
286
|
+
end
|
|
287
|
+
end.join
|
|
288
|
+
|
|
289
|
+
"#{opening}\n#{indented_content}#{closing}\n"
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module RubyLlmAgents
|
|
6
|
+
# Speaker generator for creating new text-to-speech speakers
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# rails generate ruby_llm_agents:speaker Narrator
|
|
10
|
+
# rails generate ruby_llm_agents:speaker Narrator --provider elevenlabs
|
|
11
|
+
# rails generate ruby_llm_agents:speaker Narrator --voice alloy --speed 1.25
|
|
12
|
+
# rails generate ruby_llm_agents:speaker Narrator --root=ai
|
|
13
|
+
#
|
|
14
|
+
# This will create:
|
|
15
|
+
# - app/{root}/audio/speakers/narrator_speaker.rb
|
|
16
|
+
#
|
|
17
|
+
class SpeakerGenerator < ::Rails::Generators::NamedBase
|
|
18
|
+
source_root File.expand_path("templates", __dir__)
|
|
19
|
+
|
|
20
|
+
class_option :provider, type: :string, default: "openai",
|
|
21
|
+
desc: "The TTS provider to use (openai, elevenlabs)"
|
|
22
|
+
class_option :model, type: :string, default: nil,
|
|
23
|
+
desc: "The TTS model to use"
|
|
24
|
+
class_option :voice, type: :string, default: "nova",
|
|
25
|
+
desc: "The voice to use"
|
|
26
|
+
class_option :speed, type: :numeric, default: 1.0,
|
|
27
|
+
desc: "Speech speed (0.25-4.0 for OpenAI)"
|
|
28
|
+
class_option :format, type: :string, default: "mp3",
|
|
29
|
+
desc: "Output format (mp3, wav, ogg, flac)"
|
|
30
|
+
class_option :cache, type: :string, default: nil,
|
|
31
|
+
desc: "Cache TTL (e.g., '7.days')"
|
|
32
|
+
class_option :root,
|
|
33
|
+
type: :string,
|
|
34
|
+
default: nil,
|
|
35
|
+
desc: "Root directory name (default: uses config or 'llm')"
|
|
36
|
+
class_option :namespace,
|
|
37
|
+
type: :string,
|
|
38
|
+
default: nil,
|
|
39
|
+
desc: "Root namespace (default: camelized root or config)"
|
|
40
|
+
|
|
41
|
+
def ensure_base_class_and_skill_file
|
|
42
|
+
@root_namespace = root_namespace
|
|
43
|
+
@audio_namespace = "#{root_namespace}::Audio"
|
|
44
|
+
speakers_dir = "app/#{root_directory}/audio/speakers"
|
|
45
|
+
|
|
46
|
+
# Create directory if needed
|
|
47
|
+
empty_directory speakers_dir
|
|
48
|
+
|
|
49
|
+
# Create base class if it doesn't exist
|
|
50
|
+
base_class_path = "#{speakers_dir}/application_speaker.rb"
|
|
51
|
+
unless File.exist?(File.join(destination_root, base_class_path))
|
|
52
|
+
template "application_speaker.rb.tt", base_class_path
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Create skill file if it doesn't exist
|
|
56
|
+
skill_file_path = "#{speakers_dir}/SPEAKERS.md"
|
|
57
|
+
unless File.exist?(File.join(destination_root, skill_file_path))
|
|
58
|
+
template "skills/SPEAKERS.md.tt", skill_file_path
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def create_speaker_file
|
|
63
|
+
# Support nested paths: "article/narrator" -> "app/{root}/audio/speakers/article/narrator_speaker.rb"
|
|
64
|
+
@root_namespace = root_namespace
|
|
65
|
+
@audio_namespace = "#{root_namespace}::Audio"
|
|
66
|
+
speaker_path = name.underscore
|
|
67
|
+
template "speaker.rb.tt", "app/#{root_directory}/audio/speakers/#{speaker_path}_speaker.rb"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def show_usage
|
|
71
|
+
# Build full class name from path
|
|
72
|
+
speaker_class_name = name.split("/").map(&:camelize).join("::")
|
|
73
|
+
full_class_name = "#{root_namespace}::Audio::#{speaker_class_name}Speaker"
|
|
74
|
+
say ""
|
|
75
|
+
say "Speaker #{full_class_name} created!", :green
|
|
76
|
+
say ""
|
|
77
|
+
say "Usage:"
|
|
78
|
+
say " # Generate speech"
|
|
79
|
+
say " result = #{full_class_name}.call(text: \"Hello world\")"
|
|
80
|
+
say " result.audio # => Binary audio data"
|
|
81
|
+
say ""
|
|
82
|
+
say " # Save to file"
|
|
83
|
+
say " result.save_to(\"output.mp3\")"
|
|
84
|
+
say ""
|
|
85
|
+
say " # Stream audio"
|
|
86
|
+
say " #{full_class_name}.stream(text: \"Long article...\") do |chunk|"
|
|
87
|
+
say " audio_player.play(chunk.audio)"
|
|
88
|
+
say " end"
|
|
89
|
+
say ""
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def root_directory
|
|
95
|
+
@root_directory ||= options[:root] || RubyLLM::Agents.configuration.root_directory
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def root_namespace
|
|
99
|
+
@root_namespace ||= options[:namespace] || camelize(root_directory)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def camelize(str)
|
|
103
|
+
# Handle special cases for common abbreviations
|
|
104
|
+
return "AI" if str.downcase == "ai"
|
|
105
|
+
return "ML" if str.downcase == "ml"
|
|
106
|
+
return "LLM" if str.downcase == "llm"
|
|
107
|
+
|
|
108
|
+
# Standard camelization
|
|
109
|
+
str.split(/[-_]/).map(&:capitalize).join
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def default_model
|
|
113
|
+
case options[:provider].to_s
|
|
114
|
+
when "elevenlabs"
|
|
115
|
+
"eleven_monolingual_v1"
|
|
116
|
+
else
|
|
117
|
+
"tts-1"
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddExecutionTypeToRubyLLMAgentsExecutions < ActiveRecord::Migration<%= migration_version %>
|
|
4
|
+
def change
|
|
5
|
+
add_column :ruby_llm_agents_executions, :execution_type, :string, default: "chat"
|
|
6
|
+
add_index :ruby_llm_agents_executions, :execution_type
|
|
7
|
+
end
|
|
8
|
+
end
|
|
@@ -1,108 +1,123 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
3
|
+
module <%= @root_namespace %>
|
|
4
|
+
<%- if class_name.include?("::") -%>
|
|
5
|
+
<%- class_name.split("::")[0..-2].each_with_index do |mod, i| -%>
|
|
6
|
+
<%= " " * (i + 1) %>module <%= mod %>
|
|
7
|
+
<%- end -%>
|
|
8
|
+
<%= " " * class_name.split("::").length %>class <%= class_name.split("::").last %>Agent < ApplicationAgent
|
|
9
|
+
<%- else -%>
|
|
10
|
+
class <%= class_name %>Agent < ApplicationAgent
|
|
11
|
+
<%- end -%>
|
|
12
|
+
# ============================================
|
|
13
|
+
# Model Configuration
|
|
14
|
+
# ============================================
|
|
15
|
+
|
|
16
|
+
model "<%= options[:model] %>"
|
|
17
|
+
temperature <%= options[:temperature] %>
|
|
18
|
+
version "1.0"
|
|
19
|
+
# timeout 30 # Per-request timeout in seconds (default: 60)
|
|
20
|
+
|
|
21
|
+
# ============================================
|
|
22
|
+
# Caching
|
|
23
|
+
# ============================================
|
|
16
24
|
|
|
17
25
|
<% if options[:cache] -%>
|
|
18
|
-
|
|
26
|
+
cache <%= options[:cache] %>
|
|
19
27
|
<% else -%>
|
|
20
|
-
|
|
28
|
+
# cache 1.hour # Enable response caching with TTL
|
|
21
29
|
<% end -%>
|
|
22
30
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
31
|
+
# ============================================
|
|
32
|
+
# Reliability (Retries & Fallbacks)
|
|
33
|
+
# ============================================
|
|
26
34
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
# Automatic retries with exponential backoff
|
|
36
|
+
# - max: Number of retry attempts
|
|
37
|
+
# - backoff: :constant or :exponential
|
|
38
|
+
# - base: Base delay in seconds
|
|
39
|
+
# - max_delay: Maximum delay between retries
|
|
40
|
+
# - on: Additional error classes to retry on
|
|
41
|
+
# retries max: 2, backoff: :exponential, base: 0.4, max_delay: 3.0
|
|
34
42
|
|
|
35
|
-
|
|
36
|
-
|
|
43
|
+
# Fallback models (tried in order when primary model fails)
|
|
44
|
+
# fallback_models ["gpt-4o-mini", "claude-3-haiku"]
|
|
37
45
|
|
|
38
|
-
|
|
39
|
-
|
|
46
|
+
# Total timeout across all retry/fallback attempts
|
|
47
|
+
# total_timeout 30
|
|
40
48
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
# Circuit breaker (prevents repeated calls to failing models)
|
|
50
|
+
# - errors: Number of errors to trigger open state
|
|
51
|
+
# - within: Rolling window in seconds
|
|
52
|
+
# - cooldown: Time to wait before allowing requests again
|
|
53
|
+
# circuit_breaker errors: 5, within: 60, cooldown: 300
|
|
46
54
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
55
|
+
# ============================================
|
|
56
|
+
# Parameters
|
|
57
|
+
# ============================================
|
|
50
58
|
|
|
51
59
|
<% parsed_params.each do |param| -%>
|
|
52
|
-
|
|
60
|
+
param :<%= param.name %><%= ", required: true" if param.required? %><%= ", default: #{param.default.inspect}" if param.default && !param.required? %>
|
|
53
61
|
<% end -%>
|
|
54
62
|
|
|
55
|
-
|
|
63
|
+
private
|
|
56
64
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
65
|
+
# ============================================
|
|
66
|
+
# Prompts (required)
|
|
67
|
+
# ============================================
|
|
60
68
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
69
|
+
def system_prompt
|
|
70
|
+
<<~PROMPT
|
|
71
|
+
You are a helpful assistant.
|
|
72
|
+
# Define your system instructions here
|
|
73
|
+
PROMPT
|
|
74
|
+
end
|
|
67
75
|
|
|
68
|
-
|
|
69
|
-
|
|
76
|
+
def user_prompt
|
|
77
|
+
# Build the prompt from parameters
|
|
70
78
|
<% if parsed_params.any? -%>
|
|
71
|
-
|
|
79
|
+
<%= parsed_params.first.name %>
|
|
72
80
|
<% else -%>
|
|
73
|
-
|
|
81
|
+
"Your prompt here"
|
|
74
82
|
<% end -%>
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# ============================================
|
|
86
|
+
# Optional Overrides
|
|
87
|
+
# ============================================
|
|
88
|
+
|
|
89
|
+
# Structured output schema (returns parsed hash instead of raw text)
|
|
90
|
+
# def schema
|
|
91
|
+
# @schema ||= RubyLLM::Schema.create do
|
|
92
|
+
# string :result, description: "The result"
|
|
93
|
+
# integer :confidence, description: "Confidence score 1-100"
|
|
94
|
+
# array :tags, description: "Relevant tags" do
|
|
95
|
+
# string
|
|
96
|
+
# end
|
|
97
|
+
# end
|
|
98
|
+
# end
|
|
99
|
+
|
|
100
|
+
# Custom response processing (default: symbolize hash keys)
|
|
101
|
+
# def process_response(response)
|
|
102
|
+
# content = response.content
|
|
103
|
+
# # Transform or validate the response
|
|
104
|
+
# content
|
|
105
|
+
# end
|
|
106
|
+
|
|
107
|
+
# Custom metadata to include in execution logs
|
|
108
|
+
# def execution_metadata
|
|
109
|
+
# { custom_field: "value", request_id: params[:request_id] }
|
|
110
|
+
# end
|
|
111
|
+
|
|
112
|
+
# Custom cache key data (default: all params except skip_cache, dry_run)
|
|
113
|
+
# def cache_key_data
|
|
114
|
+
# { query: params[:query], locale: I18n.locale }
|
|
115
|
+
# end
|
|
116
|
+
<%- if class_name.include?("::") -%>
|
|
117
|
+
<%- (class_name.split("::").length).times do |i| -%>
|
|
118
|
+
<%= " " * (class_name.split("::").length - i) %>end
|
|
119
|
+
<%- end -%>
|
|
120
|
+
<%- else -%>
|
|
75
121
|
end
|
|
76
|
-
|
|
77
|
-
# ============================================
|
|
78
|
-
# Optional Overrides
|
|
79
|
-
# ============================================
|
|
80
|
-
|
|
81
|
-
# Structured output schema (returns parsed hash instead of raw text)
|
|
82
|
-
# def schema
|
|
83
|
-
# @schema ||= RubyLLM::Schema.create do
|
|
84
|
-
# string :result, description: "The result"
|
|
85
|
-
# integer :confidence, description: "Confidence score 1-100"
|
|
86
|
-
# array :tags, description: "Relevant tags" do
|
|
87
|
-
# string
|
|
88
|
-
# end
|
|
89
|
-
# end
|
|
90
|
-
# end
|
|
91
|
-
|
|
92
|
-
# Custom response processing (default: symbolize hash keys)
|
|
93
|
-
# def process_response(response)
|
|
94
|
-
# content = response.content
|
|
95
|
-
# # Transform or validate the response
|
|
96
|
-
# content
|
|
97
|
-
# end
|
|
98
|
-
|
|
99
|
-
# Custom metadata to include in execution logs
|
|
100
|
-
# def execution_metadata
|
|
101
|
-
# { custom_field: "value", request_id: params[:request_id] }
|
|
102
|
-
# end
|
|
103
|
-
|
|
104
|
-
# Custom cache key data (default: all params except skip_cache, dry_run)
|
|
105
|
-
# def cache_key_data
|
|
106
|
-
# { query: params[:query], locale: I18n.locale }
|
|
107
|
-
# end
|
|
122
|
+
<%- end -%>
|
|
108
123
|
end
|