roast-ai 0.4.2 → 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.
@@ -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
  )
@@ -75,20 +75,28 @@ module Roast
75
75
  puts ::CLI::UI.fmt("{{cyan: uri_base: \"https://openrouter.ai/api/v1\",}}")
76
76
  puts ::CLI::UI.fmt("{{cyan: )}}")
77
77
  else
78
- puts ::CLI::UI.fmt("{{cyan:require \"faraday\"}}")
79
- puts ::CLI::UI.fmt("{{cyan:require \"faraday/retry\"}}")
80
78
  puts
81
- puts ::CLI::UI.fmt("{{cyan: Raix.configure do |config|}}")
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|}}")
82
88
  puts ::CLI::UI.fmt("{{cyan: config.openai_client = OpenAI::Client.new(}}")
83
89
  puts ::CLI::UI.fmt("{{cyan: access_token: ENV.fetch(\"OPENAI_API_KEY\"),}}")
84
90
  puts ::CLI::UI.fmt("{{cyan: uri_base: \"https://api.openai.com/v1\",}}")
85
91
  puts ::CLI::UI.fmt("{{cyan: ) do |f|}}")
86
- puts ::CLI::UI.fmt("{{cyan: f.request(:retry, {}}")
87
- puts ::CLI::UI.fmt("{{cyan: max: 2,}}")
88
- puts ::CLI::UI.fmt("{{cyan: interval: 0.05,}}")
89
- puts ::CLI::UI.fmt("{{cyan: interval_randomness: 0.5,}}")
90
- puts ::CLI::UI.fmt("{{cyan: backoff_factor: 2,}}")
91
- puts ::CLI::UI.fmt("{{cyan: })}}")
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}}")
92
100
  puts ::CLI::UI.fmt("{{cyan: end}}")
93
101
  end
94
102
  puts ::CLI::UI.fmt("{{cyan:end}}")
@@ -103,15 +111,18 @@ module Roast
103
111
  def include_tools
104
112
  return unless @configuration.tools.present? || @configuration.mcp_tools.present?
105
113
 
106
- BaseWorkflow.include(Raix::FunctionDispatch)
107
- 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)
108
117
 
109
118
  if @configuration.tools.present?
110
- 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
111
122
  end
112
123
 
113
124
  if @configuration.mcp_tools.present?
114
- BaseWorkflow.include(Raix::MCP)
125
+ BaseWorkflow.include(Raix::MCP) unless BaseWorkflow.included_modules.include?(Raix::MCP)
115
126
 
116
127
  # Create an interpolator for MCP tool configuration
117
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"
data/roast.gemspec CHANGED
@@ -40,10 +40,9 @@ Gem::Specification.new do |spec|
40
40
  spec.add_dependency("cli-kit", "~> 5.0")
41
41
  spec.add_dependency("cli-ui", "2.3.0")
42
42
  spec.add_dependency("diff-lcs", "~> 1.5")
43
- spec.add_dependency("faraday-retry")
44
43
  spec.add_dependency("json-schema")
45
44
  spec.add_dependency("open_router", "~> 0.3")
46
- spec.add_dependency("raix", "~> 1.0")
45
+ spec.add_dependency("raix-openai-eight", "~> 1.0")
47
46
  spec.add_dependency("ruby-graphviz", "~> 1.2")
48
47
  spec.add_dependency("sqlite3", "~> 2.6")
49
48
  spec.add_dependency("thor", "~> 1.3")
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: roast-ai
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.2
4
+ version: 0.4.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
@@ -65,20 +65,6 @@ dependencies:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
67
  version: '1.5'
68
- - !ruby/object:Gem::Dependency
69
- name: faraday-retry
70
- requirement: !ruby/object:Gem::Requirement
71
- requirements:
72
- - - ">="
73
- - !ruby/object:Gem::Version
74
- version: '0'
75
- type: :runtime
76
- prerelease: false
77
- version_requirements: !ruby/object:Gem::Requirement
78
- requirements:
79
- - - ">="
80
- - !ruby/object:Gem::Version
81
- version: '0'
82
68
  - !ruby/object:Gem::Dependency
83
69
  name: json-schema
84
70
  requirement: !ruby/object:Gem::Requirement
@@ -108,7 +94,7 @@ dependencies:
108
94
  - !ruby/object:Gem::Version
109
95
  version: '0.3'
110
96
  - !ruby/object:Gem::Dependency
111
- name: raix
97
+ name: raix-openai-eight
112
98
  requirement: !ruby/object:Gem::Requirement
113
99
  requirements:
114
100
  - - "~>"
@@ -273,6 +259,8 @@ files:
273
259
  - examples/cmd/explorer_workflow.png
274
260
  - examples/cmd/explorer_workflow.yml
275
261
  - examples/cmd/smart_tool_selection/prompt.md
262
+ - examples/coding_agent_with_model.yml
263
+ - examples/coding_agent_with_retries.yml
276
264
  - examples/conditional/README.md
277
265
  - examples/conditional/check_condition/prompt.md
278
266
  - examples/conditional/simple_workflow.png
@@ -456,6 +444,7 @@ files:
456
444
  - lib/roast/factories/api_provider_factory.rb
457
445
  - lib/roast/helpers/function_caching_interceptor.rb
458
446
  - lib/roast/helpers/logger.rb
447
+ - lib/roast/helpers/metadata_access.rb
459
448
  - lib/roast/helpers/minitest_coverage_runner.rb
460
449
  - lib/roast/helpers/path_resolver.rb
461
450
  - lib/roast/helpers/prompt_loader.rb
@@ -517,6 +506,7 @@ files:
517
506
  - lib/roast/workflow/interpolator.rb
518
507
  - lib/roast/workflow/iteration_executor.rb
519
508
  - lib/roast/workflow/llm_boolean_coercer.rb
509
+ - lib/roast/workflow/metadata_manager.rb
520
510
  - lib/roast/workflow/output_handler.rb
521
511
  - lib/roast/workflow/output_manager.rb
522
512
  - lib/roast/workflow/parallel_executor.rb
@@ -525,6 +515,7 @@ files:
525
515
  - lib/roast/workflow/replay_handler.rb
526
516
  - lib/roast/workflow/resource_resolver.rb
527
517
  - lib/roast/workflow/session_manager.rb
518
+ - lib/roast/workflow/shell_script_step.rb
528
519
  - lib/roast/workflow/sqlite_state_repository.rb
529
520
  - lib/roast/workflow/state_manager.rb
530
521
  - lib/roast/workflow/state_repository.rb
@@ -542,7 +533,6 @@ files:
542
533
  - lib/roast/workflow/step_finder.rb
543
534
  - lib/roast/workflow/step_loader.rb
544
535
  - lib/roast/workflow/step_name_extractor.rb
545
- - lib/roast/workflow/step_orchestrator.rb
546
536
  - lib/roast/workflow/step_runner.rb
547
537
  - lib/roast/workflow/step_type_resolver.rb
548
538
  - lib/roast/workflow/validation_command.rb
@@ -585,7 +575,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
585
575
  - !ruby/object:Gem::Version
586
576
  version: '0'
587
577
  requirements: []
588
- rubygems_version: 3.6.9
578
+ rubygems_version: 3.7.1
589
579
  specification_version: 4
590
580
  summary: A framework for executing structured AI workflows in Ruby
591
581
  test_files: []
@@ -1,48 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Roast
4
- module Workflow
5
- # Handles the orchestration of step execution, managing the flow and control
6
- # of individual steps without knowing how to execute them
7
- #
8
- # This class is specifically for executing CUSTOM steps defined in the workflow's
9
- # step directory (e.g., steps/*.rb files). It loads and executes Ruby step files
10
- # that define a `call` method.
11
- #
12
- # The primary method execute_step is used by StepExecutorCoordinator for
13
- # executing custom Ruby steps.
14
- #
15
- # TODO: Consider renaming this class to CustomStepOrchestrator to clarify its purpose
16
- class StepOrchestrator
17
- def initialize(workflow, step_loader, state_manager, error_handler, workflow_executor)
18
- @workflow = workflow
19
- @step_loader = step_loader
20
- @state_manager = state_manager
21
- @error_handler = error_handler
22
- @workflow_executor = workflow_executor
23
- end
24
-
25
- def execute_step(name, exit_on_error: true, step_key: nil, **options)
26
- resource_type = @workflow.respond_to?(:resource) ? @workflow.resource&.type : nil
27
-
28
- @error_handler.with_error_handling(name, resource_type: resource_type) do
29
- $stderr.puts "Executing: #{name} (Resource type: #{resource_type || "unknown"})"
30
-
31
- # Use step_key for loading if provided, otherwise use name
32
- load_key = step_key || name
33
- is_last_step = options[:is_last_step]
34
- step_object = @step_loader.load(name, step_key: load_key, is_last_step:, **options)
35
- step_result = step_object.call
36
-
37
- # Store result in workflow output
38
- @workflow.output[name] = step_result
39
-
40
- # Save state after each step
41
- @state_manager.save_state(name, step_result)
42
-
43
- step_result
44
- end
45
- end
46
- end
47
- end
48
- end