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
@@ -67,27 +67,27 @@ module RubyLLM
67
67
  belongs_to :parent_execution, class_name: "RubyLLM::Agents::Execution", optional: true
68
68
  belongs_to :root_execution, class_name: "RubyLLM::Agents::Execution", optional: true
69
69
  has_many :child_executions, class_name: "RubyLLM::Agents::Execution",
70
- foreign_key: :parent_execution_id, dependent: :nullify, inverse_of: :parent_execution
70
+ foreign_key: :parent_execution_id, dependent: :nullify, inverse_of: :parent_execution
71
71
 
72
72
  # Detail record for large payloads (prompts, responses, tool calls, etc.)
73
73
  has_one :detail, class_name: "RubyLLM::Agents::ExecutionDetail",
74
- foreign_key: :execution_id, dependent: :destroy
74
+ foreign_key: :execution_id, dependent: :destroy
75
75
 
76
76
  # Delegations so existing code keeps working transparently
77
- delegate :system_prompt, :user_prompt, :response, :error_message,
78
- :messages_summary, :tool_calls, :attempts, :fallback_chain,
79
- :parameters, :routed_to, :classification_result,
80
- :cached_at, :cache_creation_tokens,
81
- to: :detail, prefix: false, allow_nil: true
77
+ delegate :system_prompt, :user_prompt, :assistant_prompt, :response, :error_message,
78
+ :messages_summary, :tool_calls, :attempts, :fallback_chain,
79
+ :parameters, :routed_to, :classification_result,
80
+ :cached_at, :cache_creation_tokens,
81
+ to: :detail, prefix: false, allow_nil: true
82
82
 
83
83
  # Validations
84
84
  validates :agent_type, :model_id, :started_at, presence: true
85
- validates :status, inclusion: { in: statuses.keys }
86
- validates :temperature, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 2 }, allow_nil: true
87
- validates :input_tokens, :output_tokens, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
88
- validates :duration_ms, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
89
- validates :input_cost, :output_cost, :total_cost, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
90
- validates :finish_reason, inclusion: { in: FINISH_REASONS }, allow_nil: true
85
+ validates :status, inclusion: {in: statuses.keys}
86
+ validates :temperature, numericality: {greater_than_or_equal_to: 0, less_than_or_equal_to: 2}, allow_nil: true
87
+ validates :input_tokens, :output_tokens, numericality: {greater_than_or_equal_to: 0}, allow_nil: true
88
+ validates :duration_ms, numericality: {greater_than_or_equal_to: 0}, allow_nil: true
89
+ validates :input_cost, :output_cost, :total_cost, numericality: {greater_than_or_equal_to: 0}, allow_nil: true
90
+ validates :finish_reason, inclusion: {in: FINISH_REASONS}, allow_nil: true
91
91
 
92
92
  before_save :calculate_total_tokens, if: -> { input_tokens_changed? || output_tokens_changed? }
93
93
  before_save :calculate_total_cost, if: -> { input_cost_changed? || output_cost_changed? }
@@ -130,10 +130,10 @@ module RubyLLM
130
130
  # @return [Boolean] true if more than one attempt was made
131
131
  def has_retries?
132
132
  count = if self.class.column_names.include?("attempts_count")
133
- attempts_count
134
- elsif self.class.column_names.include?("attempts")
135
- attempts&.size
136
- end
133
+ attempts_count
134
+ elsif self.class.column_names.include?("attempts")
135
+ attempts&.size
136
+ end
137
137
  (count || 0) > 1
138
138
  end
139
139
 
@@ -278,16 +278,16 @@ module RubyLLM
278
278
  # @return [Hash] Now strip metrics with period-over-period comparisons
279
279
  def self.now_strip_data(range: "today")
280
280
  current_scope = case range
281
- when "7d" then last_n_days(7)
282
- when "30d" then last_n_days(30)
283
- else today
284
- end
281
+ when "7d" then last_n_days(7)
282
+ when "30d" then last_n_days(30)
283
+ else today
284
+ end
285
285
 
286
286
  previous_scope = case range
287
- when "7d" then where(created_at: 14.days.ago.beginning_of_day..7.days.ago.beginning_of_day)
288
- when "30d" then where(created_at: 60.days.ago.beginning_of_day..30.days.ago.beginning_of_day)
289
- else yesterday
290
- end
287
+ when "7d" then where(created_at: 14.days.ago.beginning_of_day..7.days.ago.beginning_of_day)
288
+ when "30d" then where(created_at: 60.days.ago.beginning_of_day..30.days.ago.beginning_of_day)
289
+ else yesterday
290
+ end
291
291
 
292
292
  current = {
293
293
  running: running.count,
@@ -371,7 +371,7 @@ module RubyLLM
371
371
  return nil unless lookup_model_id
372
372
 
373
373
  RubyLLM::Models.find(lookup_model_id)
374
- rescue StandardError
374
+ rescue
375
375
  nil
376
376
  end
377
377
  end
@@ -35,13 +35,13 @@ module RubyLLM
35
35
 
36
36
  included do
37
37
  # Validations
38
- validates :enforcement, inclusion: { in: ENFORCEMENT_MODES }, allow_nil: true
38
+ validates :enforcement, inclusion: {in: ENFORCEMENT_MODES}, allow_nil: true
39
39
  validates :daily_limit, :monthly_limit,
40
- numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
40
+ numericality: {greater_than_or_equal_to: 0}, allow_nil: true
41
41
  validates :daily_token_limit, :monthly_token_limit,
42
- numericality: { greater_than_or_equal_to: 0, only_integer: true }, allow_nil: true
42
+ numericality: {greater_than_or_equal_to: 0, only_integer: true}, allow_nil: true
43
43
  validates :daily_execution_limit, :monthly_execution_limit,
44
- numericality: { greater_than_or_equal_to: 0, only_integer: true }, allow_nil: true
44
+ numericality: {greater_than_or_equal_to: 0, only_integer: true}, allow_nil: true
45
45
 
46
46
  # Scopes
47
47
  scope :with_budgets, -> { where.not(daily_limit: nil).or(where.not(monthly_limit: nil)) }
@@ -203,17 +203,23 @@ module RubyLLM
203
203
  def remaining_budget(type: :daily_cost)
204
204
  case type
205
205
  when :daily_cost
206
- effective_daily_limit && (ensure_daily_reset!; effective_daily_limit - daily_cost_spent)
206
+ effective_daily_limit && (ensure_daily_reset!
207
+ effective_daily_limit - daily_cost_spent)
207
208
  when :monthly_cost
208
- effective_monthly_limit && (ensure_monthly_reset!; effective_monthly_limit - monthly_cost_spent)
209
+ effective_monthly_limit && (ensure_monthly_reset!
210
+ effective_monthly_limit - monthly_cost_spent)
209
211
  when :daily_tokens
210
- effective_daily_token_limit && (ensure_daily_reset!; effective_daily_token_limit - daily_tokens_used)
212
+ effective_daily_token_limit && (ensure_daily_reset!
213
+ effective_daily_token_limit - daily_tokens_used)
211
214
  when :monthly_tokens
212
- effective_monthly_token_limit && (ensure_monthly_reset!; effective_monthly_token_limit - monthly_tokens_used)
215
+ effective_monthly_token_limit && (ensure_monthly_reset!
216
+ effective_monthly_token_limit - monthly_tokens_used)
213
217
  when :daily_executions
214
- effective_daily_execution_limit && (ensure_daily_reset!; effective_daily_execution_limit - daily_executions_count)
218
+ effective_daily_execution_limit && (ensure_daily_reset!
219
+ effective_daily_execution_limit - daily_executions_count)
215
220
  when :monthly_executions
216
- effective_monthly_execution_limit && (ensure_monthly_reset!; effective_monthly_execution_limit - monthly_executions_count)
221
+ effective_monthly_execution_limit && (ensure_monthly_reset!
222
+ effective_monthly_execution_limit - monthly_executions_count)
217
223
  end
218
224
  end
219
225
 
@@ -71,19 +71,19 @@ module RubyLLM
71
71
  last_exec = executions.order(created_at: :desc).pick(:created_at, :status)
72
72
 
73
73
  update_columns(
74
- daily_cost_spent: daily_stats[:cost],
75
- daily_tokens_used: daily_stats[:tokens],
76
- daily_executions_count: daily_stats[:count],
77
- daily_error_count: daily_stats[:errors],
78
- daily_reset_date: today,
79
-
80
- monthly_cost_spent: monthly_stats[:cost],
81
- monthly_tokens_used: monthly_stats[:tokens],
74
+ daily_cost_spent: daily_stats[:cost],
75
+ daily_tokens_used: daily_stats[:tokens],
76
+ daily_executions_count: daily_stats[:count],
77
+ daily_error_count: daily_stats[:errors],
78
+ daily_reset_date: today,
79
+
80
+ monthly_cost_spent: monthly_stats[:cost],
81
+ monthly_tokens_used: monthly_stats[:tokens],
82
82
  monthly_executions_count: monthly_stats[:count],
83
- monthly_error_count: monthly_stats[:errors],
84
- monthly_reset_date: bom,
83
+ monthly_error_count: monthly_stats[:errors],
84
+ monthly_reset_date: bom,
85
85
 
86
- last_execution_at: last_exec&.first,
86
+ last_execution_at: last_exec&.first,
87
87
  last_execution_status: last_exec&.last
88
88
  )
89
89
 
@@ -120,7 +120,7 @@ module RubyLLM
120
120
  Arel.sql("COALESCE(SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END), 0)")
121
121
  )
122
122
 
123
- { cost: agg[0], tokens: agg[1], count: agg[2], errors: agg[3] }
123
+ {cost: agg[0], tokens: agg[1], count: agg[2], errors: agg[3]}
124
124
  end
125
125
  end
126
126
  end
@@ -32,10 +32,10 @@ module RubyLLM
32
32
  included do
33
33
  # Association to executions via tenant_id
34
34
  has_many :executions,
35
- class_name: "RubyLLM::Agents::Execution",
36
- primary_key: :tenant_id,
37
- foreign_key: :tenant_id,
38
- inverse_of: false
35
+ class_name: "RubyLLM::Agents::Execution",
36
+ primary_key: :tenant_id,
37
+ foreign_key: :tenant_id,
38
+ inverse_of: false
39
39
  end
40
40
 
41
41
  # Cost queries
@@ -251,7 +251,7 @@ module RubyLLM
251
251
  Arel.sql("SUM(total_tokens)"),
252
252
  Arel.sql("COUNT(*)")
253
253
  ).to_h do |agent_type, total_cost, total_tokens, count|
254
- [agent_type, { cost: total_cost || 0, tokens: total_tokens || 0, count: count }]
254
+ [agent_type, {cost: total_cost || 0, tokens: total_tokens || 0, count: count}]
255
255
  end
256
256
  end
257
257
 
@@ -269,7 +269,7 @@ module RubyLLM
269
269
  Arel.sql("SUM(total_tokens)"),
270
270
  Arel.sql("COUNT(*)")
271
271
  ).to_h do |model_id, total_cost, total_tokens, count|
272
- [model_id, { cost: total_cost || 0, tokens: total_tokens || 0, count: count }]
272
+ [model_id, {cost: total_cost || 0, tokens: total_tokens || 0, count: count}]
273
273
  end
274
274
  end
275
275
 
@@ -287,7 +287,7 @@ module RubyLLM
287
287
  Arel.sql("SUM(total_tokens)"),
288
288
  Arel.sql("COUNT(*)")
289
289
  ).to_h do |date, total_cost, total_tokens, count|
290
- [date.to_date, { cost: total_cost || 0, tokens: total_tokens || 0, count: count }]
290
+ [date.to_date, {cost: total_cost || 0, tokens: total_tokens || 0, count: count}]
291
291
  end
292
292
  end
293
293
 
@@ -71,7 +71,7 @@ module RubyLLM
71
71
  image_generators = RubyLLM::Agents::ImageGenerator.descendants.map(&:name).compact
72
72
 
73
73
  (agents + embedders + speakers + transcribers + image_generators).uniq
74
- rescue StandardError => e
74
+ rescue => e
75
75
  Rails.logger.error("[RubyLLM::Agents] Error loading agents from file system: #{e.message}")
76
76
  []
77
77
  end
@@ -81,7 +81,7 @@ module RubyLLM
81
81
  # @return [Array<String>] Agent class names with execution records
82
82
  def execution_agents
83
83
  Execution.distinct.pluck(:agent_type).compact
84
- rescue StandardError => e
84
+ rescue => e
85
85
  Rails.logger.error("[RubyLLM::Agents] Error loading agents from executions: #{e.message}")
86
86
  []
87
87
  end
@@ -149,7 +149,7 @@ module RubyLLM
149
149
  return nil unless klass.respond_to?(method_name)
150
150
 
151
151
  klass.public_send(method_name)
152
- rescue StandardError
152
+ rescue
153
153
  nil
154
154
  end
155
155
 
@@ -159,8 +159,8 @@ module RubyLLM
159
159
  # @return [Hash] Statistics hash
160
160
  def fetch_stats(agent_type)
161
161
  Execution.stats_for(agent_type, period: :all_time)
162
- rescue StandardError
163
- { count: 0, total_cost: 0, total_tokens: 0, avg_duration_ms: 0, success_rate: 0, error_rate: 0 }
162
+ rescue
163
+ {count: 0, total_cost: 0, total_tokens: 0, avg_duration_ms: 0, success_rate: 0, error_rate: 0}
164
164
  end
165
165
 
166
166
  # Gets the timestamp of the last execution for an agent
@@ -169,7 +169,7 @@ module RubyLLM
169
169
  # @return [Time, nil] Last execution time or nil
170
170
  def last_execution_time(agent_type)
171
171
  Execution.by_agent(agent_type).order(created_at: :desc).first&.created_at
172
- rescue StandardError
172
+ rescue
173
173
  nil
174
174
  end
175
175
 
@@ -19,20 +19,151 @@
19
19
  })();
20
20
  </script>
21
21
 
22
+ <!-- Gruvbox-aware color palette: CSS custom properties swapped in .dark -->
23
+ <style>
24
+ :root {
25
+ /* Gray (Tailwind defaults) */
26
+ --c-gray-50: 249 250 251; --c-gray-100: 243 244 246; --c-gray-200: 229 231 235;
27
+ --c-gray-300: 209 213 219; --c-gray-400: 156 163 175; --c-gray-500: 107 114 128;
28
+ --c-gray-600: 75 85 99; --c-gray-700: 55 65 81; --c-gray-800: 31 41 55;
29
+ --c-gray-900: 17 24 39; --c-gray-950: 3 7 18;
30
+ /* Blue */
31
+ --c-blue-50: 239 246 255; --c-blue-100: 219 234 254; --c-blue-200: 191 219 254;
32
+ --c-blue-300: 147 197 253; --c-blue-400: 96 165 250; --c-blue-500: 59 130 246;
33
+ --c-blue-600: 37 99 235; --c-blue-700: 29 78 216; --c-blue-800: 30 64 175;
34
+ --c-blue-900: 30 58 138; --c-blue-950: 23 37 84;
35
+ /* Red */
36
+ --c-red-50: 254 242 242; --c-red-100: 254 226 226; --c-red-200: 254 202 202;
37
+ --c-red-300: 252 165 165; --c-red-400: 248 113 113; --c-red-500: 239 68 68;
38
+ --c-red-600: 220 38 38; --c-red-700: 185 28 28; --c-red-800: 153 27 27;
39
+ --c-red-900: 127 29 29; --c-red-950: 69 10 10;
40
+ /* Green */
41
+ --c-green-50: 240 253 244; --c-green-100: 220 252 231; --c-green-200: 187 247 208;
42
+ --c-green-300: 134 239 172; --c-green-400: 74 222 128; --c-green-500: 34 197 94;
43
+ --c-green-600: 22 163 74; --c-green-700: 21 128 61; --c-green-800: 22 101 52;
44
+ --c-green-900: 20 83 45; --c-green-950: 5 46 22;
45
+ /* Yellow */
46
+ --c-yellow-50: 254 252 232; --c-yellow-100: 254 249 195; --c-yellow-200: 254 240 138;
47
+ --c-yellow-300: 253 224 71; --c-yellow-400: 250 204 21; --c-yellow-500: 234 179 8;
48
+ --c-yellow-600: 202 138 4; --c-yellow-700: 161 98 7; --c-yellow-800: 133 77 14;
49
+ --c-yellow-900: 113 63 18; --c-yellow-950: 66 32 6;
50
+ /* Orange */
51
+ --c-orange-50: 255 247 237; --c-orange-100: 255 237 213; --c-orange-200: 254 215 170;
52
+ --c-orange-300: 253 186 116; --c-orange-400: 251 146 60; --c-orange-500: 249 115 22;
53
+ --c-orange-600: 234 88 12; --c-orange-700: 194 65 12; --c-orange-800: 154 52 18;
54
+ --c-orange-900: 124 45 18; --c-orange-950: 67 20 7;
55
+ /* Purple */
56
+ --c-purple-50: 250 245 255; --c-purple-100: 243 232 255; --c-purple-200: 233 213 255;
57
+ --c-purple-300: 216 180 254; --c-purple-400: 192 132 252; --c-purple-500: 168 85 247;
58
+ --c-purple-600: 147 51 234; --c-purple-700: 126 34 206; --c-purple-800: 107 33 168;
59
+ --c-purple-900: 88 28 135; --c-purple-950: 59 7 100;
60
+ /* Cyan */
61
+ --c-cyan-50: 236 254 255; --c-cyan-100: 207 250 254; --c-cyan-200: 165 243 252;
62
+ --c-cyan-300: 103 232 249; --c-cyan-400: 34 211 238; --c-cyan-500: 6 182 212;
63
+ --c-cyan-600: 8 145 178; --c-cyan-700: 14 116 144; --c-cyan-800: 21 94 117;
64
+ --c-cyan-900: 22 78 99; --c-cyan-950: 8 51 68;
65
+ /* Pink */
66
+ --c-pink-50: 253 242 248; --c-pink-100: 252 231 243; --c-pink-200: 251 207 232;
67
+ --c-pink-300: 249 168 212; --c-pink-400: 244 114 182; --c-pink-500: 236 72 153;
68
+ --c-pink-600: 219 39 119; --c-pink-700: 190 24 93; --c-pink-800: 157 23 77;
69
+ --c-pink-900: 131 24 67; --c-pink-950: 80 7 36;
70
+ /* Amber */
71
+ --c-amber-50: 255 251 235; --c-amber-100: 254 243 199; --c-amber-200: 253 230 138;
72
+ --c-amber-300: 252 211 77; --c-amber-400: 251 191 36; --c-amber-500: 245 158 11;
73
+ --c-amber-600: 217 119 6; --c-amber-700: 180 83 9; --c-amber-800: 146 64 14;
74
+ --c-amber-900: 120 53 15; --c-amber-950: 69 26 3;
75
+ }
76
+ .dark {
77
+ /* Gray → Gruvbox bg/fg */
78
+ --c-gray-50: 251 241 199; --c-gray-100: 235 219 178; --c-gray-200: 213 196 161;
79
+ --c-gray-300: 189 174 147; --c-gray-400: 189 174 147; --c-gray-500: 168 153 132;
80
+ --c-gray-600: 146 131 116; --c-gray-700: 102 92 84; --c-gray-800: 80 73 69;
81
+ --c-gray-900: 60 56 54; --c-gray-950: 40 40 40;
82
+ /* Blue → Gruvbox aqua (#83a598 / #458588) */
83
+ --c-blue-50: 195 215 205; --c-blue-100: 174 199 188; --c-blue-200: 153 182 170;
84
+ --c-blue-300: 131 165 152; --c-blue-400: 100 149 144; --c-blue-500: 69 133 136;
85
+ --c-blue-600: 58 112 115; --c-blue-700: 48 93 95; --c-blue-800: 38 73 75;
86
+ --c-blue-900: 28 55 57; --c-blue-950: 20 40 42;
87
+ /* Red → Gruvbox red (#fb4934 / #cc241d) */
88
+ --c-red-50: 253 165 155; --c-red-100: 253 135 122; --c-red-200: 252 104 87;
89
+ --c-red-300: 251 73 52; --c-red-400: 228 55 41; --c-red-500: 251 89 66;
90
+ --c-red-600: 204 36 29; --c-red-700: 136 24 19; --c-red-800: 102 18 14;
91
+ --c-red-900: 72 12 10; --c-red-950: 50 8 7;
92
+ /* Green → Gruvbox green (#b8bb26 / #98971a) */
93
+ --c-green-50: 225 227 145; --c-green-100: 213 215 112; --c-green-200: 199 201 75;
94
+ --c-green-300: 184 187 38; --c-green-400: 168 169 32; --c-green-500: 152 151 26;
95
+ --c-green-600: 126 125 22; --c-green-700: 101 100 17; --c-green-800: 77 77 13;
96
+ --c-green-900: 55 55 9; --c-green-950: 38 38 6;
97
+ /* Yellow → Gruvbox yellow (#fabd2f / #d79921) */
98
+ --c-yellow-50: 253 225 155; --c-yellow-100: 252 215 121; --c-yellow-200: 251 202 84;
99
+ --c-yellow-300: 250 189 47; --c-yellow-400: 233 171 40; --c-yellow-500: 215 153 33;
100
+ --c-yellow-600: 179 127 27; --c-yellow-700: 143 102 22; --c-yellow-800: 109 78 17;
101
+ --c-yellow-900: 77 55 12; --c-yellow-950: 53 38 8;
102
+ /* Orange → Gruvbox orange (#fe8019 / #d65d0e) */
103
+ --c-orange-50: 255 197 140; --c-orange-100: 255 172 102; --c-orange-200: 254 150 64;
104
+ --c-orange-300: 254 128 25; --c-orange-400: 234 111 20; --c-orange-500: 214 93 14;
105
+ --c-orange-600: 178 77 12; --c-orange-700: 142 62 9; --c-orange-800: 108 47 7;
106
+ --c-orange-900: 76 33 5; --c-orange-950: 52 23 3;
107
+ /* Purple → Gruvbox purple (#d3869b / #b16286) */
108
+ --c-purple-50: 235 186 200; --c-purple-100: 227 168 184; --c-purple-200: 219 151 170;
109
+ --c-purple-300: 211 134 155; --c-purple-400: 194 116 145; --c-purple-500: 177 98 134;
110
+ --c-purple-600: 147 81 111; --c-purple-700: 118 65 89; --c-purple-800: 90 50 68;
111
+ --c-purple-900: 63 35 48; --c-purple-950: 43 24 33;
112
+ /* Cyan → Gruvbox green-cyan (#8ec07c / #689d6a) */
113
+ --c-cyan-50: 194 228 184; --c-cyan-100: 177 216 164; --c-cyan-200: 160 204 144;
114
+ --c-cyan-300: 142 192 124; --c-cyan-400: 123 175 115; --c-cyan-500: 104 157 106;
115
+ --c-cyan-600: 86 131 88; --c-cyan-700: 69 105 70; --c-cyan-800: 53 80 54;
116
+ --c-cyan-900: 37 57 38; --c-cyan-950: 26 40 27;
117
+ /* Pink → Gruvbox purple (no pink in Gruvbox) */
118
+ --c-pink-50: 235 186 200; --c-pink-100: 227 168 184; --c-pink-200: 219 151 170;
119
+ --c-pink-300: 211 134 155; --c-pink-400: 194 116 145; --c-pink-500: 177 98 134;
120
+ --c-pink-600: 147 81 111; --c-pink-700: 118 65 89; --c-pink-800: 90 50 68;
121
+ --c-pink-900: 63 35 48; --c-pink-950: 43 24 33;
122
+ /* Amber → Gruvbox yellow-orange */
123
+ --c-amber-50: 254 224 155; --c-amber-100: 253 213 121; --c-amber-200: 252 198 84;
124
+ --c-amber-300: 251 184 47; --c-amber-400: 235 162 37; --c-amber-500: 215 140 26;
125
+ --c-amber-600: 179 116 22; --c-amber-700: 143 93 17; --c-amber-800: 109 71 13;
126
+ --c-amber-900: 77 50 9; --c-amber-950: 53 35 6;
127
+ }
128
+ </style>
129
+
22
130
  <!-- Tailwind CSS via CDN -->
23
131
  <script src="https://cdn.tailwindcss.com"></script>
24
132
 
25
133
  <script>
26
134
  tailwind.config = {
27
- darkMode: 'class'
135
+ darkMode: 'class',
136
+ theme: {
137
+ extend: {
138
+ colors: (function() {
139
+ var families = ['gray','blue','red','green','yellow','orange','purple','cyan','pink','amber'];
140
+ var shades = [50,100,200,300,400,500,600,700,800,900,950];
141
+ var c = {};
142
+ families.forEach(function(f) {
143
+ c[f] = {};
144
+ shades.forEach(function(s) {
145
+ c[f][s] = 'rgb(var(--c-' + f + '-' + s + ') / <alpha-value>)';
146
+ });
147
+ });
148
+ return c;
149
+ })()
150
+ }
151
+ }
28
152
  }
29
153
  </script>
30
154
 
31
155
  <!-- Highcharts for charts -->
32
156
  <script src="https://code.highcharts.com/highcharts.js"></script>
33
157
 
34
- <!-- Configure Highcharts defaults -->
158
+ <!-- Chart color helpers + Highcharts defaults -->
35
159
  <script>
160
+ function chartColor(lightHex, darkHex) {
161
+ return document.documentElement.classList.contains('dark') ? darkHex : lightHex;
162
+ }
163
+ function chartColorAlpha(lightRgba, darkR, darkG, darkB, alpha) {
164
+ return document.documentElement.classList.contains('dark')
165
+ ? 'rgba(' + darkR + ',' + darkG + ',' + darkB + ',' + alpha + ')' : lightRgba;
166
+ }
36
167
  Highcharts.setOptions({
37
168
  credits: { enabled: false },
38
169
  chart: {
@@ -41,22 +172,22 @@
41
172
  },
42
173
  title: { text: null },
43
174
  xAxis: {
44
- labels: { style: { color: '#6B7280' } },
45
- lineColor: 'rgba(107, 114, 128, 0.1)',
46
- tickColor: 'rgba(107, 114, 128, 0.1)'
175
+ labels: { style: { color: chartColor('#6B7280', '#928374') } },
176
+ lineColor: chartColorAlpha('rgba(107, 114, 128, 0.1)', 146, 131, 116, 0.1),
177
+ tickColor: chartColorAlpha('rgba(107, 114, 128, 0.1)', 146, 131, 116, 0.1)
47
178
  },
48
179
  yAxis: {
49
- labels: { style: { color: '#6B7280' } },
50
- gridLineColor: 'rgba(107, 114, 128, 0.1)'
180
+ labels: { style: { color: chartColor('#6B7280', '#928374') } },
181
+ gridLineColor: chartColorAlpha('rgba(107, 114, 128, 0.1)', 146, 131, 116, 0.1)
51
182
  },
52
183
  legend: {
53
- itemStyle: { color: '#6B7280' },
54
- itemHoverStyle: { color: '#9CA3AF' }
184
+ itemStyle: { color: chartColor('#6B7280', '#928374') },
185
+ itemHoverStyle: { color: chartColor('#9CA3AF', '#bdae93') }
55
186
  },
56
187
  tooltip: {
57
- backgroundColor: 'rgba(0, 0, 0, 0.85)',
188
+ backgroundColor: chartColorAlpha('rgba(0, 0, 0, 0.85)', 40, 40, 40, 0.95),
58
189
  borderColor: 'transparent',
59
- style: { color: '#E5E7EB', fontFamily: 'ui-monospace, monospace' }
190
+ style: { color: chartColor('#E5E7EB', '#ebdbb2'), fontFamily: 'ui-monospace, monospace' }
60
191
  }
61
192
  });
62
193
  </script>
@@ -109,7 +109,7 @@
109
109
  title: { text: null },
110
110
  xAxis: {
111
111
  type: 'datetime',
112
- labels: { style: { color: '#6B7280', fontSize: '9px', fontFamily: 'ui-monospace, monospace' }, format: '{value:%b %d}' },
112
+ labels: { style: { color: chartColor('#6B7280', '#928374'), fontSize: '9px', fontFamily: 'ui-monospace, monospace' }, format: '{value:%b %d}' },
113
113
  lineColor: 'transparent',
114
114
  tickLength: 0,
115
115
  gridLineWidth: 0
@@ -118,19 +118,19 @@
118
118
  title: { text: null },
119
119
  min: 0,
120
120
  allowDecimals: false,
121
- labels: { style: { color: '#6B7280', fontSize: '9px', fontFamily: 'ui-monospace, monospace' } },
122
- gridLineColor: 'rgba(107, 114, 128, 0.08)'
121
+ labels: { style: { color: chartColor('#6B7280', '#928374'), fontSize: '9px', fontFamily: 'ui-monospace, monospace' } },
122
+ gridLineColor: chartColorAlpha('rgba(107, 114, 128, 0.08)', 146, 131, 116, 0.08)
123
123
  },
124
124
  legend: { enabled: false },
125
125
  credits: { enabled: false },
126
126
  tooltip: {
127
- backgroundColor: 'rgba(0, 0, 0, 0.85)',
127
+ backgroundColor: chartColorAlpha('rgba(0, 0, 0, 0.85)', 40, 40, 40, 0.95),
128
128
  borderColor: 'transparent',
129
129
  borderRadius: 3,
130
- style: { color: '#E5E7EB', fontSize: '10px', fontFamily: 'ui-monospace, monospace' },
130
+ style: { color: chartColor('#E5E7EB', '#ebdbb2'), fontSize: '10px', fontFamily: 'ui-monospace, monospace' },
131
131
  shared: true,
132
132
  formatter: function() {
133
- let html = '<span style="color:#9CA3AF">' + Highcharts.dateFormat('%b %d', this.x) + '</span>';
133
+ let html = '<span style="color:' + chartColor('#9CA3AF', '#bdae93') + '">' + Highcharts.dateFormat('%b %d', this.x) + '</span>';
134
134
  let cost = trendData.find(d => new Date(d.date).getTime() === this.x);
135
135
  this.points.forEach(p => html += '<br/>' + p.series.name + ': <b>' + p.y + '</b>');
136
136
  if (cost) html += '<br/>cost: <b>$' + cost.cost.toFixed(4) + '</b>';
@@ -148,14 +148,14 @@
148
148
  {
149
149
  name: 'errors',
150
150
  data: errorData,
151
- color: '#EF4444',
152
- fillColor: { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, stops: [[0, 'rgba(239, 68, 68, 0.08)'], [1, 'rgba(239, 68, 68, 0)']] }
151
+ color: chartColor('#EF4444', '#fb4934'),
152
+ fillColor: { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, stops: [[0, chartColorAlpha('rgba(239, 68, 68, 0.08)', 251, 73, 52, 0.08)], [1, chartColorAlpha('rgba(239, 68, 68, 0)', 251, 73, 52, 0)]] }
153
153
  },
154
154
  {
155
155
  name: 'success',
156
156
  data: successData,
157
- color: '#10B981',
158
- fillColor: { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, stops: [[0, 'rgba(16, 185, 129, 0.08)'], [1, 'rgba(16, 185, 129, 0)']] }
157
+ color: chartColor('#10B981', '#b8bb26'),
158
+ fillColor: { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, stops: [[0, chartColorAlpha('rgba(16, 185, 129, 0.08)', 184, 187, 38, 0.08)], [1, chartColorAlpha('rgba(16, 185, 129, 0)', 184, 187, 38, 0)]] }
159
159
  }
160
160
  ]
161
161
  });
@@ -60,7 +60,7 @@
60
60
  type: 'datetime',
61
61
  min: now - getTimeRangeMs(range),
62
62
  max: now,
63
- labels: { style: { color: '#6B7280', fontSize: '9px', fontFamily: 'ui-monospace, monospace' }, format: '{value:' + fmt + '}' },
63
+ labels: { style: { color: chartColor('#6B7280', '#928374'), fontSize: '9px', fontFamily: 'ui-monospace, monospace' }, format: '{value:' + fmt + '}' },
64
64
  lineColor: 'transparent',
65
65
  tickLength: 0,
66
66
  gridLineWidth: 0
@@ -69,19 +69,19 @@
69
69
  title: { text: null },
70
70
  min: 0,
71
71
  allowDecimals: false,
72
- labels: { style: { color: '#6B7280', fontSize: '9px', fontFamily: 'ui-monospace, monospace' } },
73
- gridLineColor: 'rgba(107, 114, 128, 0.08)'
72
+ labels: { style: { color: chartColor('#6B7280', '#928374'), fontSize: '9px', fontFamily: 'ui-monospace, monospace' } },
73
+ gridLineColor: chartColorAlpha('rgba(107, 114, 128, 0.08)', 146, 131, 116, 0.08)
74
74
  },
75
75
  legend: { enabled: false },
76
76
  credits: { enabled: false },
77
77
  tooltip: {
78
- backgroundColor: 'rgba(0, 0, 0, 0.85)',
78
+ backgroundColor: chartColorAlpha('rgba(0, 0, 0, 0.85)', 40, 40, 40, 0.95),
79
79
  borderColor: 'transparent',
80
80
  borderRadius: 3,
81
- style: { color: '#E5E7EB', fontSize: '10px', fontFamily: 'ui-monospace, monospace' },
81
+ style: { color: chartColor('#E5E7EB', '#ebdbb2'), fontSize: '10px', fontFamily: 'ui-monospace, monospace' },
82
82
  shared: true,
83
83
  formatter: function() {
84
- let html = '<span style="color:#9CA3AF">' + Highcharts.dateFormat(fmt, this.x) + '</span>';
84
+ let html = '<span style="color:' + chartColor('#9CA3AF', '#bdae93') + '">' + Highcharts.dateFormat(fmt, this.x) + '</span>';
85
85
  this.points.forEach(p => html += '<br/>' + p.series.name + ': <b>' + p.y + '</b>');
86
86
  return html;
87
87
  }
@@ -97,14 +97,14 @@
97
97
  {
98
98
  name: 'errors',
99
99
  data: toDatetimePoints(data.series[1].data, range),
100
- color: '#EF4444',
101
- fillColor: { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, stops: [[0, 'rgba(239, 68, 68, 0.08)'], [1, 'rgba(239, 68, 68, 0)']] }
100
+ color: chartColor('#EF4444', '#fb4934'),
101
+ fillColor: { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, stops: [[0, chartColorAlpha('rgba(239, 68, 68, 0.08)', 251, 73, 52, 0.08)], [1, chartColorAlpha('rgba(239, 68, 68, 0)', 251, 73, 52, 0)]] }
102
102
  },
103
103
  {
104
104
  name: 'success',
105
105
  data: toDatetimePoints(data.series[0].data, range),
106
- color: '#10B981',
107
- fillColor: { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, stops: [[0, 'rgba(16, 185, 129, 0.08)'], [1, 'rgba(16, 185, 129, 0)']] }
106
+ color: chartColor('#10B981', '#b8bb26'),
107
+ fillColor: { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, stops: [[0, chartColorAlpha('rgba(16, 185, 129, 0.08)', 184, 187, 38, 0.08)], [1, chartColorAlpha('rgba(16, 185, 129, 0)', 184, 187, 38, 0)]] }
108
108
  }
109
109
  ]
110
110
  });
@@ -439,6 +439,19 @@
439
439
  <pre id="user-prompt-content" class="hidden bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded p-4 text-xs overflow-x-auto max-h-96 font-mono whitespace-pre-wrap"><%= @execution.user_prompt %></pre>
440
440
  <% end %>
441
441
 
442
+ <!-- ── assistant prompt ──────────────────── -->
443
+ <% if @execution.respond_to?(:assistant_prompt) && @execution.assistant_prompt.present? %>
444
+ <div class="flex items-center gap-3 mt-6 mb-3">
445
+ <span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">assistant prompt</span>
446
+ <div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
447
+ <button type="button" onclick="togglePrompt('assistant')" class="font-mono text-xs text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300">
448
+ <span id="assistant-prompt-toggle">expand</span>
449
+ </button>
450
+ </div>
451
+ <p id="assistant-prompt-preview" class="text-xs text-gray-600 dark:text-gray-300 font-mono bg-gray-50 dark:bg-gray-900 rounded p-3 truncate"><%= @execution.assistant_prompt.truncate(150) %></p>
452
+ <pre id="assistant-prompt-content" class="hidden bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded p-4 text-xs overflow-x-auto max-h-96 font-mono whitespace-pre-wrap"><%= @execution.assistant_prompt %></pre>
453
+ <% end %>
454
+
442
455
  <!-- ── conversation ──────────────────── -->
443
456
  <% if @execution.respond_to?(:messages_count) && @execution.messages_count.to_i > 0 %>
444
457
  <%
@@ -22,11 +22,11 @@ module RubyLlmAgents
22
22
  argument :params, type: :array, default: [], banner: "param[:required|:default] param[:required|:default]"
23
23
 
24
24
  class_option :model, type: :string, default: "gemini-2.0-flash",
25
- desc: "The LLM model to use"
25
+ desc: "The LLM model to use"
26
26
  class_option :temperature, type: :numeric, default: 0.0,
27
- desc: "The temperature setting (0.0-1.0)"
27
+ desc: "The temperature setting (0.0-1.0)"
28
28
  class_option :cache, type: :string, default: nil,
29
- desc: "Cache TTL (e.g., '1.hour', '30.minutes')"
29
+ desc: "Cache TTL (e.g., '1.hour', '30.minutes')"
30
30
 
31
31
  def ensure_base_class_and_skill_file
32
32
  agents_dir = "app/agents"
@@ -80,7 +80,7 @@ module RubyLlmAgents
80
80
  if p.required?
81
81
  "#{p.name}: value"
82
82
  else
83
- "#{p.name}: #{p.default || 'value'}"
83
+ "#{p.name}: #{p.default || "value"}"
84
84
  end
85
85
  end.join(", ")
86
86
  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: "rembg",
20
- desc: "The segmentation model to use"
20
+ desc: "The segmentation model to use"
21
21
  class_option :output_format, type: :string, default: "png",
22
- desc: "Output format (png, webp)"
22
+ desc: "Output format (png, webp)"
23
23
  class_option :refine_edges, type: :boolean, default: false,
24
- desc: "Enable edge refinement"
24
+ desc: "Enable edge refinement"
25
25
  class_option :alpha_matting, type: :boolean, default: false,
26
- desc: "Enable alpha matting for better edges"
26
+ desc: "Enable alpha matting for better edges"
27
27
  class_option :return_mask, type: :boolean, default: false,
28
- desc: "Also return the segmentation mask"
28
+ desc: "Also return the segmentation mask"
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"