ruby_llm-agents 3.0.0 → 3.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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -0
  3. data/app/controllers/ruby_llm/agents/agents_controller.rb +16 -14
  4. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +20 -20
  5. data/app/controllers/ruby_llm/agents/executions_controller.rb +5 -7
  6. data/app/helpers/ruby_llm/agents/application_helper.rb +57 -58
  7. data/app/models/ruby_llm/agents/execution/analytics.rb +27 -27
  8. data/app/models/ruby_llm/agents/execution/scopes.rb +4 -6
  9. data/app/models/ruby_llm/agents/execution.rb +26 -26
  10. data/app/models/ruby_llm/agents/tenant/budgetable.rb +16 -10
  11. data/app/models/ruby_llm/agents/tenant/resettable.rb +12 -12
  12. data/app/models/ruby_llm/agents/tenant/trackable.rb +7 -7
  13. data/app/services/ruby_llm/agents/agent_registry.rb +6 -6
  14. data/app/views/layouts/ruby_llm/agents/application.html.erb +142 -11
  15. data/app/views/ruby_llm/agents/agents/show.html.erb +10 -10
  16. data/app/views/ruby_llm/agents/dashboard/index.html.erb +10 -10
  17. data/app/views/ruby_llm/agents/executions/show.html.erb +13 -0
  18. data/lib/generators/ruby_llm_agents/agent_generator.rb +4 -4
  19. data/lib/generators/ruby_llm_agents/background_remover_generator.rb +6 -6
  20. data/lib/generators/ruby_llm_agents/embedder_generator.rb +4 -4
  21. data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +7 -7
  22. data/lib/generators/ruby_llm_agents/image_editor_generator.rb +4 -4
  23. data/lib/generators/ruby_llm_agents/image_generator_generator.rb +6 -6
  24. data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +9 -9
  25. data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +6 -6
  26. data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +4 -4
  27. data/lib/generators/ruby_llm_agents/image_variator_generator.rb +4 -4
  28. data/lib/generators/ruby_llm_agents/install_generator.rb +3 -3
  29. data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +4 -4
  30. data/lib/generators/ruby_llm_agents/multi_tenancy_generator.rb +2 -2
  31. data/lib/generators/ruby_llm_agents/restructure_generator.rb +13 -13
  32. data/lib/generators/ruby_llm_agents/speaker_generator.rb +6 -6
  33. data/lib/generators/ruby_llm_agents/templates/add_assistant_prompt_migration.rb.tt +9 -0
  34. data/lib/generators/ruby_llm_agents/templates/split_execution_details_migration.rb.tt +2 -1
  35. data/lib/generators/ruby_llm_agents/transcriber_generator.rb +4 -4
  36. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +22 -3
  37. data/lib/ruby_llm/agents/audio/speaker.rb +40 -31
  38. data/lib/ruby_llm/agents/audio/speech_client.rb +328 -0
  39. data/lib/ruby_llm/agents/audio/speech_pricing.rb +273 -0
  40. data/lib/ruby_llm/agents/audio/transcriber.rb +33 -33
  41. data/lib/ruby_llm/agents/base_agent.rb +16 -15
  42. data/lib/ruby_llm/agents/core/base/callbacks.rb +3 -3
  43. data/lib/ruby_llm/agents/core/configuration.rb +86 -73
  44. data/lib/ruby_llm/agents/core/errors.rb +27 -2
  45. data/lib/ruby_llm/agents/core/instrumentation.rb +101 -65
  46. data/lib/ruby_llm/agents/core/llm_tenant.rb +7 -7
  47. data/lib/ruby_llm/agents/core/version.rb +1 -1
  48. data/lib/ruby_llm/agents/dsl/base.rb +3 -3
  49. data/lib/ruby_llm/agents/dsl/reliability.rb +9 -9
  50. data/lib/ruby_llm/agents/image/analyzer/dsl.rb +1 -1
  51. data/lib/ruby_llm/agents/image/analyzer/execution.rb +4 -4
  52. data/lib/ruby_llm/agents/image/background_remover/dsl.rb +1 -1
  53. data/lib/ruby_llm/agents/image/background_remover/execution.rb +3 -3
  54. data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +8 -8
  55. data/lib/ruby_llm/agents/image/editor/execution.rb +1 -1
  56. data/lib/ruby_llm/agents/image/generator/pricing.rb +9 -10
  57. data/lib/ruby_llm/agents/image/generator.rb +6 -6
  58. data/lib/ruby_llm/agents/image/pipeline/dsl.rb +6 -6
  59. data/lib/ruby_llm/agents/image/pipeline/execution.rb +9 -9
  60. data/lib/ruby_llm/agents/image/pipeline.rb +1 -1
  61. data/lib/ruby_llm/agents/image/transformer/execution.rb +1 -1
  62. data/lib/ruby_llm/agents/image/upscaler/dsl.rb +1 -1
  63. data/lib/ruby_llm/agents/image/upscaler/execution.rb +3 -5
  64. data/lib/ruby_llm/agents/image/variator/execution.rb +1 -1
  65. data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +4 -4
  66. data/lib/ruby_llm/agents/infrastructure/attempt_tracker.rb +4 -4
  67. data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +9 -9
  68. data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +3 -3
  69. data/lib/ruby_llm/agents/infrastructure/budget/forecaster.rb +1 -1
  70. data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +17 -17
  71. data/lib/ruby_llm/agents/infrastructure/circuit_breaker.rb +1 -0
  72. data/lib/ruby_llm/agents/infrastructure/execution_logger_job.rb +1 -1
  73. data/lib/ruby_llm/agents/infrastructure/reliability.rb +6 -6
  74. data/lib/ruby_llm/agents/pipeline/builder.rb +11 -11
  75. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +3 -3
  76. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +4 -4
  77. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +62 -21
  78. data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +2 -3
  79. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +82 -4
  80. data/lib/ruby_llm/agents/results/background_removal_result.rb +6 -6
  81. data/lib/ruby_llm/agents/results/embedding_result.rb +15 -15
  82. data/lib/ruby_llm/agents/results/image_analysis_result.rb +7 -7
  83. data/lib/ruby_llm/agents/results/image_edit_result.rb +4 -4
  84. data/lib/ruby_llm/agents/results/image_generation_result.rb +5 -5
  85. data/lib/ruby_llm/agents/results/image_pipeline_result.rb +4 -4
  86. data/lib/ruby_llm/agents/results/image_transform_result.rb +4 -4
  87. data/lib/ruby_llm/agents/results/image_upscale_result.rb +5 -5
  88. data/lib/ruby_llm/agents/results/image_variation_result.rb +4 -4
  89. data/lib/ruby_llm/agents/results/transcription_result.rb +1 -1
  90. data/lib/ruby_llm/agents/text/embedder.rb +13 -13
  91. metadata +4 -1
@@ -17,13 +17,13 @@ module RubyLlmAgents
17
17
  source_root File.expand_path("templates", __dir__)
18
18
 
19
19
  class_option :model, type: :string, default: "text-embedding-3-small",
20
- desc: "The embedding model to use"
20
+ desc: "The embedding model to use"
21
21
  class_option :dimensions, type: :numeric, default: nil,
22
- desc: "Vector dimensions (nil for model default)"
22
+ desc: "Vector dimensions (nil for model default)"
23
23
  class_option :batch_size, type: :numeric, default: 100,
24
- desc: "Texts per API call for batch processing"
24
+ desc: "Texts per API call for batch processing"
25
25
  class_option :cache, type: :string, default: nil,
26
- desc: "Cache TTL (e.g., '1.week', '1.day')"
26
+ desc: "Cache TTL (e.g., '1.week', '1.day')"
27
27
 
28
28
  def ensure_base_class_and_skill_file
29
29
  embedders_dir = "app/agents/embedders"
@@ -17,19 +17,19 @@ module RubyLlmAgents
17
17
  source_root File.expand_path("templates", __dir__)
18
18
 
19
19
  class_option :model, type: :string, default: "gpt-4o",
20
- desc: "The vision model to use"
20
+ desc: "The vision model to use"
21
21
  class_option :analysis_type, type: :string, default: "detailed",
22
- desc: "Analysis type (caption, detailed, tags, objects, colors, all)"
22
+ desc: "Analysis type (caption, detailed, tags, objects, colors, all)"
23
23
  class_option :extract_colors, type: :boolean, default: false,
24
- desc: "Enable color extraction"
24
+ desc: "Enable color extraction"
25
25
  class_option :detect_objects, type: :boolean, default: false,
26
- desc: "Enable object detection"
26
+ desc: "Enable object detection"
27
27
  class_option :extract_text, type: :boolean, default: false,
28
- desc: "Enable text extraction (OCR)"
28
+ desc: "Enable text extraction (OCR)"
29
29
  class_option :max_tags, type: :string, default: "10",
30
- desc: "Maximum number of tags to return"
30
+ desc: "Maximum number of tags to return"
31
31
  class_option :cache, type: :string, default: nil,
32
- desc: "Cache TTL (e.g., '1.hour', '1.day')"
32
+ desc: "Cache TTL (e.g., '1.hour', '1.day')"
33
33
 
34
34
  def ensure_base_class_and_skill_file
35
35
  images_dir = "app/agents/images"
@@ -17,13 +17,13 @@ module RubyLlmAgents
17
17
  source_root File.expand_path("templates", __dir__)
18
18
 
19
19
  class_option :model, type: :string, default: "gpt-image-1",
20
- desc: "The image model to use"
20
+ desc: "The image model to use"
21
21
  class_option :size, type: :string, default: "1024x1024",
22
- desc: "Output image size (e.g., 1024x1024)"
22
+ desc: "Output image size (e.g., 1024x1024)"
23
23
  class_option :content_policy, type: :string, default: "standard",
24
- desc: "Content policy level (none, standard, moderate, strict)"
24
+ desc: "Content policy level (none, standard, moderate, strict)"
25
25
  class_option :cache, type: :string, default: nil,
26
- desc: "Cache TTL (e.g., '1.hour', '1.day')"
26
+ desc: "Cache TTL (e.g., '1.hour', '1.day')"
27
27
 
28
28
  def ensure_base_class_and_skill_file
29
29
  images_dir = "app/agents/images"
@@ -17,17 +17,17 @@ module RubyLlmAgents
17
17
  source_root File.expand_path("templates", __dir__)
18
18
 
19
19
  class_option :model, type: :string, default: "gpt-image-1",
20
- desc: "The image generation model to use"
20
+ desc: "The image generation model to use"
21
21
  class_option :size, type: :string, default: "1024x1024",
22
- desc: "Image size (e.g., 1024x1024, 1792x1024)"
22
+ desc: "Image size (e.g., 1024x1024, 1792x1024)"
23
23
  class_option :quality, type: :string, default: "standard",
24
- desc: "Image quality (standard, hd)"
24
+ desc: "Image quality (standard, hd)"
25
25
  class_option :style, type: :string, default: "vivid",
26
- desc: "Image style (vivid, natural)"
26
+ desc: "Image style (vivid, natural)"
27
27
  class_option :content_policy, type: :string, default: "standard",
28
- desc: "Content policy level (none, standard, moderate, strict)"
28
+ desc: "Content policy level (none, standard, moderate, strict)"
29
29
  class_option :cache, type: :string, default: nil,
30
- desc: "Cache TTL (e.g., '1.hour', '1.day')"
30
+ desc: "Cache TTL (e.g., '1.hour', '1.day')"
31
31
 
32
32
  def ensure_base_class_and_skill_file
33
33
  images_dir = "app/agents/images"
@@ -17,11 +17,11 @@ module RubyLlmAgents
17
17
  source_root File.expand_path("templates", __dir__)
18
18
 
19
19
  class_option :steps, type: :string, default: "generate,upscale",
20
- desc: "Pipeline steps (comma-separated: generate,upscale,transform,analyze,remove_background)"
20
+ desc: "Pipeline steps (comma-separated: generate,upscale,transform,analyze,remove_background)"
21
21
  class_option :stop_on_error, type: :boolean, default: true,
22
- desc: "Stop pipeline on first error"
22
+ desc: "Stop pipeline on first error"
23
23
  class_option :cache, type: :string, default: nil,
24
- desc: "Cache TTL (e.g., '1.hour', '1.day')"
24
+ desc: "Cache TTL (e.g., '1.hour', '1.day')"
25
25
 
26
26
  def ensure_base_class_and_skill_file
27
27
  images_dir = "app/agents/images"
@@ -132,17 +132,17 @@ module RubyLlmAgents
132
132
  class_base = name.split("/").map(&:camelize).join("::")
133
133
  case step
134
134
  when :generate
135
- { step: step, type: :generator, class_name: "Images::#{class_base}Generator" }
135
+ {step: step, type: :generator, class_name: "Images::#{class_base}Generator"}
136
136
  when :upscale
137
- { step: step, type: :upscaler, class_name: "Images::#{class_base}Upscaler" }
137
+ {step: step, type: :upscaler, class_name: "Images::#{class_base}Upscaler"}
138
138
  when :transform
139
- { step: step, type: :transformer, class_name: "Images::#{class_base}Transformer" }
139
+ {step: step, type: :transformer, class_name: "Images::#{class_base}Transformer"}
140
140
  when :analyze
141
- { step: step, type: :analyzer, class_name: "Images::#{class_base}Analyzer" }
141
+ {step: step, type: :analyzer, class_name: "Images::#{class_base}Analyzer"}
142
142
  when :remove_background
143
- { step: step, type: :remover, class_name: "Images::#{class_base}BackgroundRemover" }
143
+ {step: step, type: :remover, class_name: "Images::#{class_base}BackgroundRemover"}
144
144
  else
145
- { step: step, type: step, class_name: "Images::#{class_base}#{step.to_s.camelize}" }
145
+ {step: step, type: step, class_name: "Images::#{class_base}#{step.to_s.camelize}"}
146
146
  end
147
147
  end
148
148
  end
@@ -17,17 +17,17 @@ module RubyLlmAgents
17
17
  source_root File.expand_path("templates", __dir__)
18
18
 
19
19
  class_option :model, type: :string, default: "sdxl",
20
- desc: "The image model to use"
20
+ desc: "The image model to use"
21
21
  class_option :size, type: :string, default: "1024x1024",
22
- desc: "Output image size (e.g., 1024x1024)"
22
+ desc: "Output image size (e.g., 1024x1024)"
23
23
  class_option :strength, type: :string, default: "0.75",
24
- desc: "Transformation strength (0.0-1.0)"
24
+ desc: "Transformation strength (0.0-1.0)"
25
25
  class_option :template, type: :string, default: nil,
26
- desc: "Prompt template (use {prompt} as placeholder)"
26
+ desc: "Prompt template (use {prompt} as placeholder)"
27
27
  class_option :content_policy, type: :string, default: "standard",
28
- desc: "Content policy level (none, standard, moderate, strict)"
28
+ desc: "Content policy level (none, standard, moderate, strict)"
29
29
  class_option :cache, type: :string, default: nil,
30
- desc: "Cache TTL (e.g., '1.hour', '1.day')"
30
+ desc: "Cache TTL (e.g., '1.hour', '1.day')"
31
31
 
32
32
  def ensure_base_class_and_skill_file
33
33
  images_dir = "app/agents/images"
@@ -17,13 +17,13 @@ module RubyLlmAgents
17
17
  source_root File.expand_path("templates", __dir__)
18
18
 
19
19
  class_option :model, type: :string, default: "real-esrgan",
20
- desc: "The upscaling model to use"
20
+ desc: "The upscaling model to use"
21
21
  class_option :scale, type: :string, default: "4",
22
- desc: "Upscale factor (2, 4, or 8)"
22
+ desc: "Upscale factor (2, 4, or 8)"
23
23
  class_option :face_enhance, type: :boolean, default: false,
24
- desc: "Enable face enhancement"
24
+ desc: "Enable face enhancement"
25
25
  class_option :cache, type: :string, default: nil,
26
- desc: "Cache TTL (e.g., '1.hour', '1.day')"
26
+ desc: "Cache TTL (e.g., '1.hour', '1.day')"
27
27
 
28
28
  def ensure_base_class_and_skill_file
29
29
  images_dir = "app/agents/images"
@@ -17,13 +17,13 @@ module RubyLlmAgents
17
17
  source_root File.expand_path("templates", __dir__)
18
18
 
19
19
  class_option :model, type: :string, default: "gpt-image-1",
20
- desc: "The image model to use"
20
+ desc: "The image model to use"
21
21
  class_option :size, type: :string, default: "1024x1024",
22
- desc: "Output image size (e.g., 1024x1024)"
22
+ desc: "Output image size (e.g., 1024x1024)"
23
23
  class_option :variation_strength, type: :string, default: "0.5",
24
- desc: "Variation strength (0.0-1.0)"
24
+ desc: "Variation strength (0.0-1.0)"
25
25
  class_option :cache, type: :string, default: nil,
26
- desc: "Cache TTL (e.g., '1.hour', '1.day')"
26
+ desc: "Cache TTL (e.g., '1.hour', '1.day')"
27
27
 
28
28
  def ensure_base_class_and_skill_file
29
29
  images_dir = "app/agents/images"
@@ -22,11 +22,11 @@ module RubyLlmAgents
22
22
  source_root File.expand_path("templates", __dir__)
23
23
 
24
24
  class_option :skip_migration, type: :boolean, default: false,
25
- desc: "Skip generating the migration file"
25
+ desc: "Skip generating the migration file"
26
26
  class_option :skip_initializer, type: :boolean, default: false,
27
- desc: "Skip generating the initializer file"
27
+ desc: "Skip generating the initializer file"
28
28
  class_option :mount_dashboard, type: :boolean, default: true,
29
- desc: "Mount the dashboard engine in routes"
29
+ desc: "Mount the dashboard engine in routes"
30
30
 
31
31
  def create_migration_file
32
32
  return if options[:skip_migration]
@@ -158,7 +158,7 @@ module RubyLlmAgents
158
158
 
159
159
  directories.each do |dir|
160
160
  if options[:dry_run]
161
- say_status :dry_run, "Would create #{dir.sub(Rails.root.to_s + '/', '')}", :yellow
161
+ say_status :dry_run, "Would create #{dir.sub(Rails.root.to_s + "/", "")}", :yellow
162
162
  else
163
163
  FileUtils.mkdir_p(dir)
164
164
  say_status :mkdir, dir.sub(Rails.root.to_s + "/", ""), :green
@@ -393,7 +393,7 @@ module RubyLlmAgents
393
393
 
394
394
  if content != original_content
395
395
  File.write(file_path, content)
396
- say_status :updated, "base classes in #{file_path.sub(Rails.root.to_s + '/', '')}", :blue
396
+ say_status :updated, "base classes in #{file_path.sub(Rails.root.to_s + "/", "")}", :blue
397
397
  end
398
398
  end
399
399
 
@@ -441,7 +441,7 @@ module RubyLlmAgents
441
441
  next unless File.directory?(subdir) && Dir.empty?(subdir)
442
442
 
443
443
  if options[:dry_run]
444
- say_status :dry_run, "Would remove empty #{subdir.sub(Rails.root.to_s + '/', '')}", :yellow
444
+ say_status :dry_run, "Would remove empty #{subdir.sub(Rails.root.to_s + "/", "")}", :yellow
445
445
  else
446
446
  FileUtils.rmdir(subdir)
447
447
  say_status :removed, subdir.sub(Rails.root.to_s + "/", ""), :red
@@ -452,7 +452,7 @@ module RubyLlmAgents
452
452
  return unless File.directory?(dir) && Dir.empty?(dir)
453
453
 
454
454
  if options[:dry_run]
455
- say_status :dry_run, "Would remove empty #{dir.sub(Rails.root.to_s + '/', '')}", :yellow
455
+ say_status :dry_run, "Would remove empty #{dir.sub(Rails.root.to_s + "/", "")}", :yellow
456
456
  else
457
457
  FileUtils.rmdir(dir)
458
458
  say_status :removed, dir.sub(Rails.root.to_s + "/", ""), :red
@@ -102,7 +102,7 @@ module RubyLlmAgents
102
102
 
103
103
  def table_exists?(table)
104
104
  ActiveRecord::Base.connection.table_exists?(table)
105
- rescue StandardError
105
+ rescue
106
106
  false
107
107
  end
108
108
 
@@ -110,7 +110,7 @@ module RubyLlmAgents
110
110
  return false unless ActiveRecord::Base.connection.table_exists?(table)
111
111
 
112
112
  ActiveRecord::Base.connection.column_exists?(table, column)
113
- rescue StandardError
113
+ rescue
114
114
  false
115
115
  end
116
116
  end
@@ -37,25 +37,25 @@ module RubyLlmAgents
37
37
  # Maps old directory -> { category:, type: }
38
38
  DIRECTORY_MAPPING = {
39
39
  # Top-level under llm/
40
- "agents" => { category: nil, type: "agents" },
41
- "tools" => { category: nil, type: "tools" },
40
+ "agents" => {category: nil, type: "agents"},
41
+ "tools" => {category: nil, type: "tools"},
42
42
 
43
43
  # Audio group
44
- "speakers" => { category: :audio, type: "speakers" },
45
- "transcribers" => { category: :audio, type: "transcribers" },
44
+ "speakers" => {category: :audio, type: "speakers"},
45
+ "transcribers" => {category: :audio, type: "transcribers"},
46
46
 
47
47
  # Image group
48
- "image_generators" => { category: :image, type: "generators" },
49
- "image_editors" => { category: :image, type: "editors" },
50
- "image_analyzers" => { category: :image, type: "analyzers" },
51
- "image_transformers" => { category: :image, type: "transformers" },
52
- "image_upscalers" => { category: :image, type: "upscalers" },
53
- "image_variators" => { category: :image, type: "variators" },
54
- "background_removers" => { category: :image, type: "background_removers" },
48
+ "image_generators" => {category: :image, type: "generators"},
49
+ "image_editors" => {category: :image, type: "editors"},
50
+ "image_analyzers" => {category: :image, type: "analyzers"},
51
+ "image_transformers" => {category: :image, type: "transformers"},
52
+ "image_upscalers" => {category: :image, type: "upscalers"},
53
+ "image_variators" => {category: :image, type: "variators"},
54
+ "background_removers" => {category: :image, type: "background_removers"},
55
55
 
56
56
  # Text group
57
- "embedders" => { category: :text, type: "embedders" },
58
- "moderators" => { category: :text, type: "moderators" }
57
+ "embedders" => {category: :text, type: "embedders"},
58
+ "moderators" => {category: :text, type: "moderators"}
59
59
  }.freeze
60
60
 
61
61
  def validate_root_directory
@@ -17,17 +17,17 @@ module RubyLlmAgents
17
17
  source_root File.expand_path("templates", __dir__)
18
18
 
19
19
  class_option :provider, type: :string, default: "openai",
20
- desc: "The TTS provider to use (openai, elevenlabs)"
20
+ desc: "The TTS provider to use (openai, elevenlabs)"
21
21
  class_option :model, type: :string, default: nil,
22
- desc: "The TTS model to use"
22
+ desc: "The TTS model to use"
23
23
  class_option :voice, type: :string, default: "nova",
24
- desc: "The voice to use"
24
+ desc: "The voice to use"
25
25
  class_option :speed, type: :numeric, default: 1.0,
26
- desc: "Speech speed (0.25-4.0 for OpenAI)"
26
+ desc: "Speech speed (0.25-4.0 for OpenAI)"
27
27
  class_option :format, type: :string, default: "mp3",
28
- desc: "Output format (mp3, wav, ogg, flac)"
28
+ desc: "Output format (mp3, wav, ogg, flac)"
29
29
  class_option :cache, type: :string, default: nil,
30
- desc: "Cache TTL (e.g., '7.days')"
30
+ desc: "Cache TTL (e.g., '7.days')"
31
31
 
32
32
  def ensure_base_class_and_skill_file
33
33
  audio_dir = "app/agents/audio"
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddAssistantPromptToExecutionDetails < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ unless column_exists?(:ruby_llm_agents_execution_details, :assistant_prompt)
6
+ add_column :ruby_llm_agents_execution_details, :assistant_prompt, :text
7
+ end
8
+ end
9
+ end
@@ -9,7 +9,7 @@
9
9
  class SplitExecutionDetailsFromExecutions < ActiveRecord::Migration<%= migration_version %>
10
10
  # Columns that belong on execution_details, not executions
11
11
  DETAIL_COLUMNS = %i[
12
- error_message system_prompt user_prompt response messages_summary
12
+ error_message system_prompt user_prompt assistant_prompt response messages_summary
13
13
  tool_calls attempts fallback_chain parameters routed_to
14
14
  classification_result cached_at cache_creation_tokens
15
15
  ].freeze
@@ -50,6 +50,7 @@ class SplitExecutionDetailsFromExecutions < ActiveRecord::Migration<%= migration
50
50
  t.text :error_message
51
51
  t.text :system_prompt
52
52
  t.text :user_prompt
53
+ t.text :assistant_prompt
53
54
  t.json :response, default: {}
54
55
  t.json :messages_summary, default: {}, null: false
55
56
  t.json :tool_calls, default: [], null: false
@@ -17,13 +17,13 @@ module RubyLlmAgents
17
17
  source_root File.expand_path("templates", __dir__)
18
18
 
19
19
  class_option :model, type: :string, default: "whisper-1",
20
- desc: "The transcription model to use"
20
+ desc: "The transcription model to use"
21
21
  class_option :language, type: :string, default: nil,
22
- desc: "Language code (e.g., 'en', 'es')"
22
+ desc: "Language code (e.g., 'en', 'es')"
23
23
  class_option :output_format, type: :string, default: "text",
24
- desc: "Output format (text, srt, vtt, json)"
24
+ desc: "Output format (text, srt, vtt, json)"
25
25
  class_option :cache, type: :string, default: nil,
26
- desc: "Cache TTL (e.g., '30.days')"
26
+ desc: "Cache TTL (e.g., '30.days')"
27
27
 
28
28
  def ensure_base_class_and_skill_file
29
29
  audio_dir = "app/agents/audio"
@@ -60,6 +60,25 @@ module RubyLlmAgents
60
60
  )
61
61
  end
62
62
 
63
+ # Add assistant_prompt column to execution_details (v3.0 -> v3.1 upgrade)
64
+ def create_add_assistant_prompt_migration
65
+ if column_exists?(:ruby_llm_agents_execution_details, :assistant_prompt)
66
+ say_status :skip, "assistant_prompt column already exists on execution_details", :yellow
67
+ return
68
+ end
69
+
70
+ unless table_exists?(:ruby_llm_agents_execution_details)
71
+ say_status :skip, "execution_details table does not exist yet", :yellow
72
+ return
73
+ end
74
+
75
+ say_status :upgrade, "Adding assistant_prompt to execution_details", :blue
76
+ migration_template(
77
+ "add_assistant_prompt_migration.rb.tt",
78
+ File.join(db_migrate_path, "add_assistant_prompt_to_execution_details.rb")
79
+ )
80
+ end
81
+
63
82
  def suggest_config_consolidation
64
83
  ruby_llm_initializer = File.join(destination_root, "config/initializers/ruby_llm.rb")
65
84
  agents_initializer = File.join(destination_root, "config/initializers/ruby_llm_agents.rb")
@@ -116,7 +135,7 @@ module RubyLlmAgents
116
135
 
117
136
  # Detail columns that should only exist on execution_details, not executions
118
137
  DETAIL_COLUMNS = %i[
119
- error_message system_prompt user_prompt response messages_summary
138
+ error_message system_prompt user_prompt assistant_prompt response messages_summary
120
139
  tool_calls attempts fallback_chain parameters routed_to
121
140
  classification_result cached_at cache_creation_tokens
122
141
  ].freeze
@@ -145,13 +164,13 @@ module RubyLlmAgents
145
164
  return false unless ActiveRecord::Base.connection.table_exists?(table)
146
165
 
147
166
  ActiveRecord::Base.connection.column_exists?(table, column)
148
- rescue StandardError
167
+ rescue
149
168
  false
150
169
  end
151
170
 
152
171
  def table_exists?(table)
153
172
  ActiveRecord::Base.connection.table_exists?(table)
154
- rescue StandardError
173
+ rescue
155
174
  false
156
175
  end
157
176
  end
@@ -2,6 +2,8 @@
2
2
 
3
3
  require "digest"
4
4
  require_relative "../results/speech_result"
5
+ require_relative "speech_client"
6
+ require_relative "speech_pricing"
5
7
 
6
8
  module RubyLLM
7
9
  module Agents
@@ -194,19 +196,19 @@ module RubyLLM
194
196
 
195
197
  def default_tts_provider
196
198
  RubyLLM::Agents.configuration.default_tts_provider
197
- rescue StandardError
199
+ rescue
198
200
  :openai
199
201
  end
200
202
 
201
203
  def default_tts_model
202
204
  RubyLLM::Agents.configuration.default_tts_model
203
- rescue StandardError
205
+ rescue
204
206
  "tts-1"
205
207
  end
206
208
 
207
209
  def default_tts_voice
208
210
  RubyLLM::Agents.configuration.default_tts_voice
209
- rescue StandardError
211
+ rescue
210
212
  "nova"
211
213
  end
212
214
  end
@@ -410,7 +412,15 @@ module RubyLLM
410
412
 
411
413
  # Executes standard (non-streaming) speech synthesis
412
414
  def execute_standard_speech(text, options)
413
- response = RubyLLM.speak(text, **options)
415
+ response = speech_client.speak(
416
+ text,
417
+ model: options[:model],
418
+ voice: options[:voice],
419
+ voice_id: resolved_voice_id,
420
+ speed: options[:speed],
421
+ response_format: options[:response_format] || "mp3",
422
+ voice_settings: options[:voice_settings]
423
+ )
414
424
 
415
425
  {
416
426
  audio: response.audio,
@@ -428,9 +438,17 @@ module RubyLLM
428
438
  def execute_streaming_speech(text, options)
429
439
  audio_chunks = []
430
440
 
431
- RubyLLM.speak(text, **options.merge(stream: true)) do |chunk|
441
+ speech_client.speak_streaming(
442
+ text,
443
+ model: options[:model],
444
+ voice: options[:voice],
445
+ voice_id: resolved_voice_id,
446
+ speed: options[:speed],
447
+ response_format: options[:response_format] || "mp3",
448
+ voice_settings: options[:voice_settings]
449
+ ) do |chunk|
432
450
  audio_chunks << chunk.audio if chunk.respond_to?(:audio)
433
- @streaming_block.call(chunk) if @streaming_block
451
+ @streaming_block&.call(chunk)
434
452
  end
435
453
 
436
454
  {
@@ -445,7 +463,7 @@ module RubyLLM
445
463
  }
446
464
  end
447
465
 
448
- # Builds options for RubyLLM.speak
466
+ # Builds options for SpeechClient
449
467
  def build_speak_options
450
468
  options = {
451
469
  model: resolved_model,
@@ -453,13 +471,11 @@ module RubyLLM
453
471
  }
454
472
 
455
473
  speed = resolved_speed
456
- options[:speed] = speed if speed && speed != 1.0
474
+ options[:speed] = speed if speed && (speed - 1.0).abs > Float::EPSILON
457
475
  options[:response_format] = resolved_output_format.to_s
458
476
 
459
- if resolved_provider == :elevenlabs
460
- voice_settings = self.class.voice_settings_config
461
- options[:voice_settings] = voice_settings.to_h if voice_settings
462
- end
477
+ voice_settings = self.class.voice_settings_config
478
+ options[:voice_settings] = voice_settings.to_h if voice_settings
463
479
 
464
480
  options
465
481
  end
@@ -488,29 +504,17 @@ module RubyLLM
488
504
 
489
505
  # Calculates cost for speech synthesis
490
506
  def calculate_cost(raw_result)
491
- characters = raw_result[:characters] || 0
492
-
493
- if raw_result[:raw_response].respond_to?(:cost) && raw_result[:raw_response].cost
507
+ if raw_result[:raw_response].respond_to?(:cost) && raw_result[:raw_response]&.cost
494
508
  return raw_result[:raw_response].cost
495
509
  end
496
510
 
497
- provider = raw_result[:provider]
498
- model_name = raw_result[:model].to_s
499
-
500
- price_per_1k_chars = case provider
501
- when :openai
502
- model_name.include?("hd") ? 0.030 : 0.015
503
- when :elevenlabs
504
- 0.30
505
- when :google
506
- 0.016
507
- when :polly
508
- 0.016
509
- else
510
- 0.015
511
- end
511
+ characters = raw_result[:characters] || 0
512
512
 
513
- (characters / 1000.0) * price_per_1k_chars
513
+ Audio::SpeechPricing.calculate_cost(
514
+ provider: raw_result[:provider],
515
+ model_id: raw_result[:model].to_s,
516
+ characters: characters
517
+ )
514
518
  end
515
519
 
516
520
  # Resolves the provider to use
@@ -547,6 +551,11 @@ module RubyLLM
547
551
  def streaming_enabled?
548
552
  @runtime_streaming || self.class.streaming?
549
553
  end
554
+
555
+ # Returns a SpeechClient for the resolved provider
556
+ def speech_client
557
+ @speech_client ||= Audio::SpeechClient.new(provider: resolved_provider)
558
+ end
550
559
  end
551
560
  end
552
561
  end