roast-ai 0.4.1 → 0.4.3

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +43 -0
  4. data/Gemfile +0 -1
  5. data/Gemfile.lock +48 -23
  6. data/README.md +228 -29
  7. data/examples/coding_agent_with_model.yml +20 -0
  8. data/examples/coding_agent_with_retries.yml +30 -0
  9. data/examples/grading/rb_test_runner +1 -1
  10. data/lib/roast/errors.rb +3 -0
  11. data/lib/roast/helpers/metadata_access.rb +39 -0
  12. data/lib/roast/helpers/timeout_handler.rb +1 -1
  13. data/lib/roast/tools/coding_agent.rb +99 -27
  14. data/lib/roast/tools/grep.rb +4 -0
  15. data/lib/roast/version.rb +1 -1
  16. data/lib/roast/workflow/agent_step.rb +57 -4
  17. data/lib/roast/workflow/base_workflow.rb +4 -2
  18. data/lib/roast/workflow/command_executor.rb +3 -1
  19. data/lib/roast/workflow/configuration_parser.rb +2 -0
  20. data/lib/roast/workflow/each_step.rb +5 -3
  21. data/lib/roast/workflow/input_step.rb +2 -0
  22. data/lib/roast/workflow/interpolator.rb +23 -1
  23. data/lib/roast/workflow/metadata_manager.rb +47 -0
  24. data/lib/roast/workflow/output_handler.rb +1 -0
  25. data/lib/roast/workflow/replay_handler.rb +8 -0
  26. data/lib/roast/workflow/shell_script_step.rb +115 -0
  27. data/lib/roast/workflow/sqlite_state_repository.rb +17 -17
  28. data/lib/roast/workflow/state_manager.rb +8 -0
  29. data/lib/roast/workflow/step_executor_coordinator.rb +43 -8
  30. data/lib/roast/workflow/step_executor_with_reporting.rb +2 -2
  31. data/lib/roast/workflow/step_loader.rb +55 -9
  32. data/lib/roast/workflow/workflow_executor.rb +3 -4
  33. data/lib/roast/workflow/workflow_initializer.rb +95 -4
  34. data/lib/roast/workflow/workflow_runner.rb +2 -2
  35. data/lib/roast.rb +2 -0
  36. data/roast.gemspec +3 -2
  37. metadata +36 -18
  38. data/lib/roast/workflow/step_orchestrator.rb +0 -48
@@ -29,13 +29,13 @@ module Roast
29
29
 
30
30
  session_id = ensure_session(workflow)
31
31
 
32
- @db.execute(<<~SQL, session_id, state_data[:order], step_name, state_data.to_json)
32
+ @db.execute(<<~SQL, [session_id, state_data[:order], step_name, state_data.to_json])
33
33
  INSERT INTO session_states (session_id, step_index, step_name, state_data)
34
34
  VALUES (?, ?, ?, ?)
35
35
  SQL
36
36
 
37
37
  # Update session's current step
38
- @db.execute(<<~SQL, state_data[:order], session_id)
38
+ @db.execute(<<~SQL, [state_data[:order], session_id])
39
39
  UPDATE sessions#{" "}
40
40
  SET current_step_index = ?, updated_at = CURRENT_TIMESTAMP
41
41
  WHERE id = ?
@@ -49,7 +49,7 @@ module Roast
49
49
  return false unless session_id
50
50
 
51
51
  # Find the state before the target step
52
- result = @db.execute(<<~SQL, session_id, step_name)
52
+ result = @db.execute(<<~SQL, [session_id, step_name])
53
53
  SELECT state_data, step_name
54
54
  FROM session_states
55
55
  WHERE session_id = ?
@@ -64,7 +64,7 @@ module Roast
64
64
 
65
65
  if result.empty?
66
66
  # Try to find the latest state if target step doesn't exist
67
- result = @db.execute(<<~SQL, session_id)
67
+ result = @db.execute(<<~SQL, [session_id])
68
68
  SELECT state_data, step_name
69
69
  FROM session_states
70
70
  WHERE session_id = ?
@@ -95,7 +95,7 @@ module Roast
95
95
 
96
96
  session_id = ensure_session(workflow)
97
97
 
98
- @db.execute(<<~SQL, output_content, session_id)
98
+ @db.execute(<<~SQL, [output_content, session_id])
99
99
  UPDATE sessions#{" "}
100
100
  SET final_output = ?, status = 'completed', updated_at = CURRENT_TIMESTAMP
101
101
  WHERE id = ?
@@ -130,7 +130,7 @@ module Roast
130
130
 
131
131
  where_clause = conditions.empty? ? "" : "WHERE #{conditions.join(" AND ")}"
132
132
 
133
- @db.execute(<<~SQL, *params)
133
+ @db.execute(<<~SQL, params)
134
134
  SELECT id, workflow_name, workflow_path, status, current_step_index,#{" "}
135
135
  created_at, updated_at
136
136
  FROM sessions
@@ -141,20 +141,20 @@ module Roast
141
141
  end
142
142
 
143
143
  def get_session_details(session_id)
144
- session = @db.execute(<<~SQL, session_id).first
144
+ session = @db.execute(<<~SQL, [session_id]).first
145
145
  SELECT * FROM sessions WHERE id = ?
146
146
  SQL
147
147
 
148
148
  return unless session
149
149
 
150
- states = @db.execute(<<~SQL, session_id)
150
+ states = @db.execute(<<~SQL, [session_id])
151
151
  SELECT step_index, step_name, created_at
152
152
  FROM session_states
153
153
  WHERE session_id = ?
154
154
  ORDER BY step_index
155
155
  SQL
156
156
 
157
- events = @db.execute(<<~SQL, session_id)
157
+ events = @db.execute(<<~SQL, [session_id])
158
158
  SELECT event_name, event_data, received_at
159
159
  FROM session_events
160
160
  WHERE session_id = ?
@@ -170,7 +170,7 @@ module Roast
170
170
 
171
171
  def cleanup_old_sessions(older_than)
172
172
  count = @db.changes
173
- @db.execute(<<~SQL, "-#{older_than}")
173
+ @db.execute(<<~SQL, ["-#{older_than}"])
174
174
  DELETE FROM sessions
175
175
  WHERE created_at < datetime('now', ?)
176
176
  SQL
@@ -181,7 +181,7 @@ module Roast
181
181
  # Find the session if session_id not provided
182
182
  unless session_id
183
183
  workflow_name = File.basename(File.dirname(workflow_path))
184
- result = @db.execute(<<~SQL, workflow_name, "waiting")
184
+ result = @db.execute(<<~SQL, [workflow_name, "waiting"])
185
185
  SELECT id FROM sessions
186
186
  WHERE workflow_name = ? AND status = ?
187
187
  ORDER BY created_at DESC
@@ -194,13 +194,13 @@ module Roast
194
194
  end
195
195
 
196
196
  # Add the event
197
- @db.execute(<<~SQL, session_id, event_name, event_data&.to_json)
197
+ @db.execute(<<~SQL, [session_id, event_name, event_data&.to_json])
198
198
  INSERT INTO session_events (session_id, event_name, event_data)
199
199
  VALUES (?, ?, ?)
200
200
  SQL
201
201
 
202
202
  # Update session status
203
- @db.execute(<<~SQL, session_id)
203
+ @db.execute(<<~SQL, [session_id])
204
204
  UPDATE sessions#{" "}
205
205
  SET status = 'running', updated_at = CURRENT_TIMESTAMP
206
206
  WHERE id = ?
@@ -272,14 +272,14 @@ module Roast
272
272
  session_id = generate_session_id(workflow)
273
273
 
274
274
  # Check if session exists
275
- existing = @db.execute("SELECT id FROM sessions WHERE id = ?", session_id).first
275
+ existing = @db.execute("SELECT id FROM sessions WHERE id = ?", [session_id]).first
276
276
  return session_id if existing
277
277
 
278
278
  # Create new session
279
279
  workflow_name = workflow.session_name || "unnamed"
280
280
  workflow_path = workflow.file || "notarget"
281
281
 
282
- @db.execute(<<~SQL, session_id, workflow_name, workflow_path)
282
+ @db.execute(<<~SQL, [session_id, workflow_name, workflow_path])
283
283
  INSERT INTO sessions (id, workflow_name, workflow_path)
284
284
  VALUES (?, ?, ?)
285
285
  SQL
@@ -296,7 +296,7 @@ module Roast
296
296
  workflow_name = workflow.session_name || "unnamed"
297
297
  workflow_path = workflow.file || "notarget"
298
298
 
299
- result = @db.execute(<<~SQL, workflow_name, workflow_path)
299
+ result = @db.execute(<<~SQL, [workflow_name, workflow_path])
300
300
  SELECT id FROM sessions
301
301
  WHERE workflow_name = ? AND workflow_path = ?
302
302
  ORDER BY created_at DESC
@@ -324,7 +324,7 @@ module Roast
324
324
  new_session_id = ensure_session(workflow)
325
325
 
326
326
  # Copy states up to the target step
327
- @db.execute(<<~SQL, new_session_id, source_session_id, target_step_name, source_session_id)
327
+ @db.execute(<<~SQL, [new_session_id, source_session_id, target_step_name, source_session_id])
328
328
  INSERT INTO session_states (session_id, step_index, step_name, state_data)
329
329
  SELECT ?, step_index, step_name, state_data
330
330
  FROM session_states
@@ -42,6 +42,7 @@ module Roast
42
42
  order: determine_step_order(step_name),
43
43
  transcript: extract_transcript,
44
44
  output: extract_output,
45
+ metadata: extract_metadata,
45
46
  final_output: extract_final_output,
46
47
  execution_order: extract_execution_order,
47
48
  }
@@ -68,6 +69,13 @@ module Roast
68
69
  workflow.output.clone
69
70
  end
70
71
 
72
+ # Extract metadata if available
73
+ def extract_metadata
74
+ return {} unless workflow.respond_to?(:metadata)
75
+
76
+ workflow.metadata.clone
77
+ end
78
+
71
79
  # Extract final output data if available
72
80
  def extract_final_output
73
81
  return [] unless workflow.respond_to?(:final_output)
@@ -38,7 +38,7 @@ module Roast
38
38
  Kernel.binding.irb # rubocop:disable Lint/Debugger
39
39
  end
40
40
  else
41
- step_orchestrator.execute_step(step, is_last_step: is_last_step)
41
+ execute_custom_step(step, is_last_step: is_last_step)
42
42
  end
43
43
  end
44
44
  end
@@ -54,6 +54,10 @@ module Roast
54
54
  # @return [Object] The result of the step execution
55
55
  def execute(step, options = {})
56
56
  step_type = StepTypeResolver.resolve(step, @context)
57
+ step_name = StepTypeResolver.extract_name(step)
58
+
59
+ Thread.current[:current_step_name] = step_name if step_name
60
+ Thread.current[:workflow_metadata] = @context.workflow.metadata
57
61
 
58
62
  case step_type
59
63
  when StepTypeResolver::COMMAND_STEP
@@ -126,14 +130,18 @@ module Roast
126
130
  )
127
131
  end
128
132
 
129
- def step_orchestrator
130
- dependencies[:step_orchestrator]
131
- end
132
-
133
133
  def error_handler
134
134
  dependencies[:error_handler]
135
135
  end
136
136
 
137
+ def step_loader
138
+ dependencies[:step_loader]
139
+ end
140
+
141
+ def state_manager
142
+ dependencies[:state_manager]
143
+ end
144
+
137
145
  def execute_command_step(step, options)
138
146
  exit_on_error = options.fetch(:exit_on_error, true)
139
147
  resource_type = @context.resource_type
@@ -185,8 +193,11 @@ module Roast
185
193
  step_name = StepTypeResolver.extract_name(step)
186
194
 
187
195
  # Load and execute the agent step
188
- exit_on_error = options.fetch(:exit_on_error, context.exit_on_error?(step))
189
- step_orchestrator.execute_step(step_name, exit_on_error:, step_key: options[:step_key], agent_type: :coding_agent)
196
+ merged_options = options.merge(
197
+ exit_on_error: options.fetch(:exit_on_error) { context.exit_on_error?(step) },
198
+ agent_type: :coding_agent,
199
+ )
200
+ execute_custom_step(step_name, **merged_options)
190
201
  end
191
202
 
192
203
  def execute_glob_step(step, options = {})
@@ -259,7 +270,7 @@ module Roast
259
270
  exit_on_error = options.fetch(:exit_on_error, true)
260
271
  step_key = options[:step_key]
261
272
  is_last_step = options[:is_last_step]
262
- step_orchestrator.execute_step(step, exit_on_error:, step_key:, is_last_step:)
273
+ execute_custom_step(step, exit_on_error:, step_key:, is_last_step:)
263
274
  end
264
275
 
265
276
  def validate_each_step!(step)
@@ -268,6 +279,30 @@ module Roast
268
279
  "Invalid 'each' step format. 'as' and 'steps' must be at the same level as 'each'"
269
280
  end
270
281
  end
282
+
283
+ def execute_custom_step(name, step_key: nil, **options)
284
+ resource_type = @context.workflow.respond_to?(:resource) ? @context.workflow.resource&.type : nil
285
+
286
+ error_handler.with_error_handling(name, resource_type: resource_type) do
287
+ $stderr.puts "Executing: #{name} (Resource type: #{resource_type || "unknown"})"
288
+
289
+ # Use step_key for loading if provided, otherwise use name
290
+ load_key = step_key || name
291
+ is_last_step = options[:is_last_step]
292
+ step_object = step_loader.load(name, exit_on_error: false, step_key: load_key, is_last_step:, **options)
293
+ step_result = step_object.call
294
+
295
+ # Store result in workflow output
296
+ # Use step_key for output storage if provided (for hash steps)
297
+ output_key = step_key || name
298
+ @context.workflow.output[output_key] = step_result
299
+
300
+ # Save state after each step
301
+ state_manager.save_state(name, step_result)
302
+
303
+ step_result
304
+ end
305
+ end
271
306
  end
272
307
  end
273
308
  end
@@ -11,12 +11,12 @@ module Roast
11
11
  @name_extractor = StepNameExtractor.new
12
12
  end
13
13
 
14
- def execute(step, options = {})
14
+ def execute(step, **options)
15
15
  # Track tokens before execution
16
16
  tokens_before = @context.workflow.context_manager&.total_tokens || 0
17
17
 
18
18
  # Execute the step
19
- result = @base_executor.execute(step, options)
19
+ result = @base_executor.execute(step, **options)
20
20
 
21
21
  # Report token consumption after successful execution
22
22
  tokens_after = @context.workflow.context_manager&.total_tokens || 0
@@ -60,10 +60,15 @@ module Roast
60
60
  return step
61
61
  end
62
62
 
63
- # Look for Ruby file in various locations
64
- step_file_path = find_step_file(name.to_s, per_step_path)
65
- if step_file_path
66
- return load_ruby_step(step_file_path, name.to_s, is_last_step:)
63
+ # Look for Ruby or shell script file in various locations
64
+ step_file_info = find_step_file(name.to_s, per_step_path)
65
+ if step_file_info
66
+ case step_file_info[:type]
67
+ when :ruby
68
+ return load_ruby_step(step_file_info[:path], name.to_s, is_last_step:)
69
+ when :shell
70
+ return load_shell_script_step(step_file_info[:path], name.to_s, step_key, is_last_step:)
71
+ end
67
72
  end
68
73
 
69
74
  # Look for step directory
@@ -87,28 +92,40 @@ module Roast
87
92
  File.expand_path(path, context_path)
88
93
  end
89
94
 
90
- # Find a Ruby step file in various locations
95
+ # Find a Ruby or shell script step file in various locations
91
96
  def find_step_file(step_name, per_step_path = nil)
92
97
  # Check in per-step path first
93
98
  if per_step_path
94
99
  resolved_per_step_path = resolve_path(per_step_path)
95
100
  custom_rb_path = File.join(resolved_per_step_path, "#{step_name}.rb")
96
- return custom_rb_path if File.file?(custom_rb_path)
101
+ return { path: custom_rb_path, type: :ruby } if File.file?(custom_rb_path)
102
+
103
+ custom_sh_path = File.join(resolved_per_step_path, "#{step_name}.sh")
104
+ return { path: custom_sh_path, type: :shell } if File.file?(custom_sh_path)
97
105
  end
98
106
 
99
107
  # Check in phase-specific directory first
100
108
  if phase != :steps
101
109
  phase_rb_path = File.join(context_path, phase.to_s, "#{step_name}.rb")
102
- return phase_rb_path if File.file?(phase_rb_path)
110
+ return { path: phase_rb_path, type: :ruby } if File.file?(phase_rb_path)
111
+
112
+ phase_sh_path = File.join(context_path, phase.to_s, "#{step_name}.sh")
113
+ return { path: phase_sh_path, type: :shell } if File.file?(phase_sh_path)
103
114
  end
104
115
 
105
116
  # Check in context path
106
117
  rb_file_path = File.join(context_path, "#{step_name}.rb")
107
- return rb_file_path if File.file?(rb_file_path)
118
+ return { path: rb_file_path, type: :ruby } if File.file?(rb_file_path)
119
+
120
+ sh_file_path = File.join(context_path, "#{step_name}.sh")
121
+ return { path: sh_file_path, type: :shell } if File.file?(sh_file_path)
108
122
 
109
123
  # Check in shared directory
110
124
  shared_rb_path = File.expand_path(File.join(context_path, "..", "shared", "#{step_name}.rb"))
111
- return shared_rb_path if File.file?(shared_rb_path)
125
+ return { path: shared_rb_path, type: :ruby } if File.file?(shared_rb_path)
126
+
127
+ shared_sh_path = File.expand_path(File.join(context_path, "..", "shared", "#{step_name}.sh"))
128
+ return { path: shared_sh_path, type: :shell } if File.file?(shared_sh_path)
112
129
 
113
130
  nil
114
131
  end
@@ -161,6 +178,23 @@ module Roast
161
178
  step
162
179
  end
163
180
 
181
+ # Load a shell script step from a file
182
+ def load_shell_script_step(file_path, step_name, step_key, is_last_step: nil)
183
+ $stderr.puts "Loading shell script step: #{file_path}"
184
+
185
+ step_name_obj = Roast::ValueObjects::StepName.new(step_name)
186
+
187
+ step = ShellScriptStep.new(
188
+ workflow,
189
+ script_path: file_path,
190
+ name: step_name_obj,
191
+ context_path: File.dirname(file_path),
192
+ )
193
+
194
+ configure_step(step, step_key || step_name, is_last_step:)
195
+ step
196
+ end
197
+
164
198
  # Create and configure a step instance
165
199
  def create_step_instance(step_class, step_name, context_path, options = {})
166
200
  is_last_step = options[:is_last_step]
@@ -203,6 +237,18 @@ module Roast
203
237
  if step_config.key?("available_tools")
204
238
  step.available_tools = step_config["available_tools"]
205
239
  end
240
+
241
+ # Apply any other configuration attributes that the step supports
242
+ step_config.each do |key, value|
243
+ # Skip keys we've already handled above
244
+ next if ["print_response", "json", "params", "coerce_to", "available_tools"].include?(key)
245
+
246
+ # Apply configuration if the step has a setter for this attribute
247
+ setter_method = "#{key}="
248
+ if step.respond_to?(setter_method)
249
+ step.public_send(setter_method, value)
250
+ end
251
+ end
206
252
  end
207
253
  end
208
254
  end
@@ -42,7 +42,6 @@ module Roast
42
42
  # @param state_manager [StateManager] Optional custom state manager
43
43
  # @param iteration_executor [IterationExecutor] Optional custom iteration executor
44
44
  # @param conditional_executor [ConditionalExecutor] Optional custom conditional executor
45
- # @param step_orchestrator [StepOrchestrator] Optional custom step orchestrator
46
45
  # @param step_executor_coordinator [StepExecutorCoordinator] Optional custom step executor coordinator
47
46
  # @param phase [Symbol] The execution phase - determines where to load steps from
48
47
  # Valid values:
@@ -52,7 +51,7 @@ module Roast
52
51
  def initialize(workflow, config_hash, context_path,
53
52
  error_handler: nil, step_loader: nil, command_executor: nil,
54
53
  interpolator: nil, state_manager: nil, iteration_executor: nil,
55
- conditional_executor: nil, step_orchestrator: nil, step_executor_coordinator: nil,
54
+ conditional_executor: nil, step_executor_coordinator: nil,
56
55
  phase: :steps)
57
56
  # Create context object to reduce data clump
58
57
  @context = WorkflowContext.new(
@@ -69,7 +68,6 @@ module Roast
69
68
  @state_manager = state_manager || StateManager.new(workflow, logger: @error_handler, storage_type: workflow.storage_type)
70
69
  @iteration_executor = iteration_executor || IterationExecutor.new(workflow, context_path, @state_manager, config_hash)
71
70
  @conditional_executor = conditional_executor || ConditionalExecutor.new(workflow, context_path, @state_manager, self)
72
- @step_orchestrator = step_orchestrator || StepOrchestrator.new(workflow, @step_loader, @state_manager, @error_handler, self)
73
71
 
74
72
  # Initialize coordinator with dependencies
75
73
  base_coordinator = step_executor_coordinator || StepExecutorCoordinator.new(
@@ -80,7 +78,8 @@ module Roast
80
78
  command_executor: @command_executor,
81
79
  iteration_executor: @iteration_executor,
82
80
  conditional_executor: @conditional_executor,
83
- step_orchestrator: @step_orchestrator,
81
+ step_loader: @step_loader,
82
+ state_manager: @state_manager,
84
83
  error_handler: @error_handler,
85
84
  },
86
85
  )
@@ -10,6 +10,7 @@ module Roast
10
10
 
11
11
  def setup
12
12
  load_roast_initializers
13
+ check_raix_configuration
13
14
  include_tools
14
15
  configure_api_client
15
16
  end
@@ -20,18 +21,108 @@ module Roast
20
21
  Roast::Initializers.load_all
21
22
  end
22
23
 
24
+ def check_raix_configuration
25
+ # Skip check in test environment
26
+ return if ENV["RAILS_ENV"] == "test" || ENV["RACK_ENV"] == "test" || defined?(Minitest)
27
+
28
+ # Only check if the workflow has steps that would need API access
29
+ return if @configuration.steps.empty?
30
+
31
+ # Check if Raix has been configured with the appropriate client
32
+ case @configuration.api_provider
33
+ when :openai
34
+ if Raix.configuration.openai_client.nil?
35
+ warn_about_missing_raix_configuration(:openai)
36
+ end
37
+ when :openrouter
38
+ if Raix.configuration.openrouter_client.nil?
39
+ warn_about_missing_raix_configuration(:openrouter)
40
+ end
41
+ when nil
42
+ # If no api_provider is set but we have steps that might need API access,
43
+ # check if any client is configured
44
+ if Raix.configuration.openai_client.nil? && Raix.configuration.openrouter_client.nil?
45
+ warn_about_missing_raix_configuration(:any)
46
+ end
47
+ end
48
+ end
49
+
50
+ def warn_about_missing_raix_configuration(provider)
51
+ ::CLI::UI.frame_style = :box
52
+ ::CLI::UI::Frame.open("{{red:Raix Configuration Missing}}", color: :red) do
53
+ case provider
54
+ when :openai
55
+ puts ::CLI::UI.fmt("{{yellow:⚠️ Warning: Raix OpenAI client is not configured!}}")
56
+ when :openrouter
57
+ puts ::CLI::UI.fmt("{{yellow:⚠️ Warning: Raix OpenRouter client is not configured!}}")
58
+ else
59
+ puts ::CLI::UI.fmt("{{yellow:⚠️ Warning: Raix is not configured!}}")
60
+ end
61
+ puts
62
+ puts "Roast requires Raix to be properly initialized to make API calls."
63
+ puts ::CLI::UI.fmt("To fix this, create a file at {{cyan:.roast/initializers/raix.rb}} with:")
64
+ puts
65
+ puts ::CLI::UI.fmt("{{cyan:# frozen_string_literal: true}}")
66
+ puts
67
+ puts ::CLI::UI.fmt("{{cyan:require \"raix\"}}")
68
+
69
+ if provider == :openrouter
70
+ puts ::CLI::UI.fmt("{{cyan:require \"open_router\"}}")
71
+ puts
72
+ puts ::CLI::UI.fmt("{{cyan:Raix.configure do |config|}}")
73
+ puts ::CLI::UI.fmt("{{cyan: config.openrouter_client = OpenRouter::Client.new(}}")
74
+ puts ::CLI::UI.fmt("{{cyan: access_token: ENV.fetch(\"OPENROUTER_API_KEY\"),}}")
75
+ puts ::CLI::UI.fmt("{{cyan: uri_base: \"https://openrouter.ai/api/v1\",}}")
76
+ puts ::CLI::UI.fmt("{{cyan: )}}")
77
+ else
78
+ puts
79
+ puts ::CLI::UI.fmt("{{cyan:faraday_retry = false}}")
80
+ puts ::CLI::UI.fmt("{{cyan:begin}}")
81
+ puts ::CLI::UI.fmt("{{cyan: require \"faraday/retry\"}}")
82
+ puts ::CLI::UI.fmt("{{cyan: faraday_retry = true}}")
83
+ puts ::CLI::UI.fmt("{{cyan:rescue LoadError}}")
84
+ puts ::CLI::UI.fmt("{{cyan: # Do nothing}}")
85
+ puts ::CLI::UI.fmt("{{cyan:end}}")
86
+ puts
87
+ puts ::CLI::UI.fmt("{{cyan:Raix.configure do |config|}}")
88
+ puts ::CLI::UI.fmt("{{cyan: config.openai_client = OpenAI::Client.new(}}")
89
+ puts ::CLI::UI.fmt("{{cyan: access_token: ENV.fetch(\"OPENAI_API_KEY\"),}}")
90
+ puts ::CLI::UI.fmt("{{cyan: uri_base: \"https://api.openai.com/v1\",}}")
91
+ puts ::CLI::UI.fmt("{{cyan: ) do |f|}}")
92
+ puts ::CLI::UI.fmt("{{cyan: if faraday_retry}}")
93
+ puts ::CLI::UI.fmt("{{cyan: f.request(:retry, {}}")
94
+ puts ::CLI::UI.fmt("{{cyan: max: 2,}}")
95
+ puts ::CLI::UI.fmt("{{cyan: interval: 0.05,}}")
96
+ puts ::CLI::UI.fmt("{{cyan: interval_randomness: 0.5,}}")
97
+ puts ::CLI::UI.fmt("{{cyan: backoff_factor: 2,}}")
98
+ puts ::CLI::UI.fmt("{{cyan: })}}")
99
+ puts ::CLI::UI.fmt("{{cyan: end}}")
100
+ puts ::CLI::UI.fmt("{{cyan: end}}")
101
+ end
102
+ puts ::CLI::UI.fmt("{{cyan:end}}")
103
+ puts
104
+ puts "For Shopify users, you need to use the LLM gateway proxy instead."
105
+ puts "Check the #roast slack channel for more information."
106
+ puts
107
+ end
108
+ raise ::CLI::Kit::Abort, "Please configure Raix before running workflows."
109
+ end
110
+
23
111
  def include_tools
24
112
  return unless @configuration.tools.present? || @configuration.mcp_tools.present?
25
113
 
26
- BaseWorkflow.include(Raix::FunctionDispatch)
27
- BaseWorkflow.include(Roast::Helpers::FunctionCachingInterceptor) # Add caching support
114
+ # Only include modules if they haven't been included already to avoid method redefinition warnings
115
+ BaseWorkflow.include(Raix::FunctionDispatch) unless BaseWorkflow.included_modules.include?(Raix::FunctionDispatch)
116
+ BaseWorkflow.include(Roast::Helpers::FunctionCachingInterceptor) unless BaseWorkflow.included_modules.include?(Roast::Helpers::FunctionCachingInterceptor)
28
117
 
29
118
  if @configuration.tools.present?
30
- BaseWorkflow.include(*@configuration.tools.map(&:constantize))
119
+ @configuration.tools.map(&:constantize).each do |tool|
120
+ BaseWorkflow.include(tool) unless BaseWorkflow.included_modules.include?(tool)
121
+ end
31
122
  end
32
123
 
33
124
  if @configuration.mcp_tools.present?
34
- BaseWorkflow.include(Raix::MCP)
125
+ BaseWorkflow.include(Raix::MCP) unless BaseWorkflow.included_modules.include?(Raix::MCP)
35
126
 
36
127
  # Create an interpolator for MCP tool configuration
37
128
  # We use Object.new as the context because this interpolation happens during
@@ -91,11 +91,11 @@ module Roast
91
91
  executor = WorkflowExecutor.new(workflow, @configuration.config_hash, @configuration.context_path)
92
92
  executor.execute_steps(steps)
93
93
 
94
- $stderr.puts "🔥🔥🔥 ROAST COMPLETE! 🔥🔥🔥"
95
-
96
94
  # Save outputs
97
95
  @output_handler.save_final_output(workflow)
98
96
  @output_handler.write_results(workflow)
97
+
98
+ $stderr.puts "🔥🔥🔥 ROAST COMPLETE! 🔥🔥🔥"
99
99
  end
100
100
 
101
101
  private
data/lib/roast.rb CHANGED
@@ -11,6 +11,7 @@ require "net/http"
11
11
  require "open3"
12
12
  require "pathname"
13
13
  require "securerandom"
14
+ require "shellwords"
14
15
  require "tempfile"
15
16
  require "uri"
16
17
  require "yaml"
@@ -25,6 +26,7 @@ require "active_support/core_ext/string/inflections"
25
26
  require "active_support/isolated_execution_state"
26
27
  require "active_support/notifications"
27
28
  require "cli/ui"
29
+ require "cli/kit"
28
30
  require "diff/lcs"
29
31
  require "json-schema"
30
32
  require "raix"
data/roast.gemspec CHANGED
@@ -37,13 +37,14 @@ Gem::Specification.new do |spec|
37
37
  spec.require_paths = ["lib"]
38
38
 
39
39
  spec.add_dependency("activesupport", ">= 7.0")
40
+ spec.add_dependency("cli-kit", "~> 5.0")
40
41
  spec.add_dependency("cli-ui", "2.3.0")
41
42
  spec.add_dependency("diff-lcs", "~> 1.5")
42
- spec.add_dependency("faraday-retry")
43
43
  spec.add_dependency("json-schema")
44
44
  spec.add_dependency("open_router", "~> 0.3")
45
- spec.add_dependency("raix", "~> 1.0")
45
+ spec.add_dependency("raix-openai-eight", "~> 1.0")
46
46
  spec.add_dependency("ruby-graphviz", "~> 1.2")
47
+ spec.add_dependency("sqlite3", "~> 2.6")
47
48
  spec.add_dependency("thor", "~> 1.3")
48
49
  spec.add_dependency("zeitwerk", "~> 2.6")
49
50
  end