roast-ai 0.1.7 → 0.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 (109) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yaml +1 -1
  3. data/CHANGELOG.md +40 -1
  4. data/CLAUDE.md +20 -0
  5. data/Gemfile +1 -0
  6. data/Gemfile.lock +9 -6
  7. data/README.md +81 -14
  8. data/bin/roast +27 -0
  9. data/docs/ITERATION_SYNTAX.md +119 -0
  10. data/examples/conditional/README.md +161 -0
  11. data/examples/conditional/check_condition/prompt.md +1 -0
  12. data/examples/conditional/simple_workflow.yml +15 -0
  13. data/examples/conditional/workflow.yml +23 -0
  14. data/examples/dot_notation/README.md +37 -0
  15. data/examples/dot_notation/workflow.yml +44 -0
  16. data/examples/exit_on_error/README.md +50 -0
  17. data/examples/exit_on_error/analyze_lint_output/prompt.md +9 -0
  18. data/examples/exit_on_error/apply_fixes/prompt.md +2 -0
  19. data/examples/exit_on_error/workflow.yml +19 -0
  20. data/examples/grading/workflow.yml +5 -1
  21. data/examples/iteration/IMPLEMENTATION.md +88 -0
  22. data/examples/iteration/README.md +68 -0
  23. data/examples/iteration/analyze_complexity/prompt.md +22 -0
  24. data/examples/iteration/generate_recommendations/prompt.md +21 -0
  25. data/examples/iteration/generate_report/prompt.md +129 -0
  26. data/examples/iteration/implement_fix/prompt.md +25 -0
  27. data/examples/iteration/prioritize_issues/prompt.md +24 -0
  28. data/examples/iteration/prompts/analyze_file.md +28 -0
  29. data/examples/iteration/prompts/generate_summary.md +24 -0
  30. data/examples/iteration/prompts/update_report.md +29 -0
  31. data/examples/iteration/prompts/write_report.md +22 -0
  32. data/examples/iteration/read_file/prompt.md +9 -0
  33. data/examples/iteration/select_next_issue/prompt.md +25 -0
  34. data/examples/iteration/simple_workflow.md +39 -0
  35. data/examples/iteration/simple_workflow.yml +58 -0
  36. data/examples/iteration/update_fix_count/prompt.md +26 -0
  37. data/examples/iteration/verify_fix/prompt.md +29 -0
  38. data/examples/iteration/workflow.yml +42 -0
  39. data/examples/openrouter_example/workflow.yml +2 -2
  40. data/examples/workflow_generator/README.md +27 -0
  41. data/examples/workflow_generator/analyze_user_request/prompt.md +34 -0
  42. data/examples/workflow_generator/create_workflow_files/prompt.md +32 -0
  43. data/examples/workflow_generator/get_user_input/prompt.md +14 -0
  44. data/examples/workflow_generator/info_from_roast.rb +22 -0
  45. data/examples/workflow_generator/workflow.yml +35 -0
  46. data/lib/roast/errors.rb +9 -0
  47. data/lib/roast/factories/api_provider_factory.rb +61 -0
  48. data/lib/roast/helpers/function_caching_interceptor.rb +1 -1
  49. data/lib/roast/helpers/minitest_coverage_runner.rb +1 -1
  50. data/lib/roast/helpers/prompt_loader.rb +50 -1
  51. data/lib/roast/resources/base_resource.rb +7 -0
  52. data/lib/roast/resources.rb +6 -6
  53. data/lib/roast/tools/ask_user.rb +40 -0
  54. data/lib/roast/tools/cmd.rb +1 -1
  55. data/lib/roast/tools/search_file.rb +1 -1
  56. data/lib/roast/tools.rb +11 -1
  57. data/lib/roast/value_objects/api_token.rb +49 -0
  58. data/lib/roast/value_objects/step_name.rb +39 -0
  59. data/lib/roast/value_objects/workflow_path.rb +77 -0
  60. data/lib/roast/value_objects.rb +5 -0
  61. data/lib/roast/version.rb +1 -1
  62. data/lib/roast/workflow/api_configuration.rb +61 -0
  63. data/lib/roast/workflow/base_iteration_step.rb +165 -0
  64. data/lib/roast/workflow/base_step.rb +4 -24
  65. data/lib/roast/workflow/base_workflow.rb +76 -73
  66. data/lib/roast/workflow/command_executor.rb +88 -0
  67. data/lib/roast/workflow/conditional_executor.rb +50 -0
  68. data/lib/roast/workflow/conditional_step.rb +96 -0
  69. data/lib/roast/workflow/configuration.rb +35 -158
  70. data/lib/roast/workflow/configuration_loader.rb +78 -0
  71. data/lib/roast/workflow/configuration_parser.rb +13 -248
  72. data/lib/roast/workflow/context_path_resolver.rb +43 -0
  73. data/lib/roast/workflow/dot_access_hash.rb +198 -0
  74. data/lib/roast/workflow/each_step.rb +86 -0
  75. data/lib/roast/workflow/error_handler.rb +97 -0
  76. data/lib/roast/workflow/expression_utils.rb +36 -0
  77. data/lib/roast/workflow/file_state_repository.rb +3 -2
  78. data/lib/roast/workflow/interpolator.rb +34 -0
  79. data/lib/roast/workflow/iteration_executor.rb +85 -0
  80. data/lib/roast/workflow/llm_boolean_coercer.rb +55 -0
  81. data/lib/roast/workflow/output_handler.rb +35 -0
  82. data/lib/roast/workflow/output_manager.rb +77 -0
  83. data/lib/roast/workflow/parallel_executor.rb +49 -0
  84. data/lib/roast/workflow/repeat_step.rb +75 -0
  85. data/lib/roast/workflow/replay_handler.rb +123 -0
  86. data/lib/roast/workflow/resource_resolver.rb +77 -0
  87. data/lib/roast/workflow/session_manager.rb +6 -2
  88. data/lib/roast/workflow/state_manager.rb +97 -0
  89. data/lib/roast/workflow/step_executor_coordinator.rb +205 -0
  90. data/lib/roast/workflow/step_executor_factory.rb +47 -0
  91. data/lib/roast/workflow/step_executor_registry.rb +79 -0
  92. data/lib/roast/workflow/step_executors/base_step_executor.rb +23 -0
  93. data/lib/roast/workflow/step_executors/hash_step_executor.rb +43 -0
  94. data/lib/roast/workflow/step_executors/parallel_step_executor.rb +54 -0
  95. data/lib/roast/workflow/step_executors/string_step_executor.rb +29 -0
  96. data/lib/roast/workflow/step_finder.rb +97 -0
  97. data/lib/roast/workflow/step_loader.rb +154 -0
  98. data/lib/roast/workflow/step_orchestrator.rb +45 -0
  99. data/lib/roast/workflow/step_runner.rb +23 -0
  100. data/lib/roast/workflow/step_type_resolver.rb +117 -0
  101. data/lib/roast/workflow/workflow_context.rb +60 -0
  102. data/lib/roast/workflow/workflow_executor.rb +90 -209
  103. data/lib/roast/workflow/workflow_initializer.rb +112 -0
  104. data/lib/roast/workflow/workflow_runner.rb +87 -0
  105. data/lib/roast/workflow.rb +3 -0
  106. data/lib/roast.rb +96 -3
  107. data/roast.gemspec +2 -1
  108. data/schema/workflow.json +85 -0
  109. metadata +97 -4
@@ -6,125 +6,128 @@ require "active_support"
6
6
  require "active_support/isolated_execution_state"
7
7
  require "active_support/notifications"
8
8
  require "active_support/core_ext/hash/indifferent_access"
9
+ require "roast/workflow/output_manager"
10
+ require "roast/workflow/context_path_resolver"
9
11
 
10
12
  module Roast
11
13
  module Workflow
12
14
  class BaseWorkflow
13
15
  include Raix::ChatCompletion
14
16
 
15
- attr_reader :output
16
17
  attr_accessor :file,
17
18
  :concise,
18
19
  :output_file,
20
+ :pause_step_name,
19
21
  :verbose,
20
22
  :name,
21
23
  :context_path,
22
24
  :resource,
23
25
  :session_name,
24
26
  :session_timestamp,
25
- :configuration
27
+ :configuration,
28
+ :model
29
+
30
+ delegate :api_provider, :openai?, to: :configuration
31
+ delegate :output, :output=, :append_to_final_output, :final_output, to: :output_manager
26
32
 
27
33
  def initialize(file = nil, name: nil, context_path: nil, resource: nil, session_name: nil, configuration: nil)
28
34
  @file = file
29
35
  @name = name || self.class.name.underscore.split("/").last
30
- @context_path = context_path || determine_context_path
31
- @final_output = []
32
- @output = ActiveSupport::HashWithIndifferentAccess.new
36
+ @context_path = context_path || ContextPathResolver.resolve(self.class)
33
37
  @resource = resource || Roast::Resources.for(file)
34
38
  @session_name = session_name || @name
35
39
  @session_timestamp = nil
36
40
  @configuration = configuration
37
- transcript << { system: read_sidecar_prompt }
38
- Roast::Tools.setup_interrupt_handler(transcript)
39
- Roast::Tools.setup_exit_handler(self)
40
- end
41
-
42
- # Custom writer for output to ensure it's always a HashWithIndifferentAccess
43
- def output=(value)
44
- @output = if value.is_a?(ActiveSupport::HashWithIndifferentAccess)
45
- value
46
- else
47
- ActiveSupport::HashWithIndifferentAccess.new(value)
48
- end
49
- end
50
41
 
51
- def append_to_final_output(message)
52
- @final_output << message
53
- end
42
+ # Initialize managers
43
+ @output_manager = OutputManager.new
54
44
 
55
- def final_output
56
- return @final_output if @final_output.is_a?(String)
57
- return "" if @final_output.nil?
45
+ # Setup prompt and handlers
46
+ read_sidecar_prompt.then do |prompt|
47
+ next unless prompt
58
48
 
59
- # Handle array case (expected normal case)
60
- if @final_output.respond_to?(:join)
61
- @final_output.join("\n\n")
62
- else
63
- # Handle any other unexpected type by converting to string
64
- @final_output.to_s
49
+ transcript << { system: prompt }
65
50
  end
51
+ Roast::Tools.setup_interrupt_handler(transcript)
52
+ Roast::Tools.setup_exit_handler(self)
66
53
  end
67
54
 
68
55
  # Override chat_completion to add instrumentation
69
56
  def chat_completion(**kwargs)
70
57
  start_time = Time.now
71
- model = kwargs[:openai] || "default"
72
-
73
- ActiveSupport::Notifications.instrument("roast.chat_completion.start", {
74
- model: model,
75
- parameters: kwargs.except(:openai),
76
- })
77
-
78
- result = super(**kwargs)
58
+ step_model = kwargs[:model]
59
+
60
+ with_model(step_model) do
61
+ ActiveSupport::Notifications.instrument("roast.chat_completion.start", {
62
+ model: model,
63
+ parameters: kwargs.except(:openai, :model),
64
+ })
65
+
66
+ # Call the parent module's chat_completion
67
+ # skip model because it is read directly from the model method
68
+ result = super(**kwargs.except(:model))
69
+ execution_time = Time.now - start_time
70
+
71
+ ActiveSupport::Notifications.instrument("roast.chat_completion.complete", {
72
+ success: true,
73
+ model: model,
74
+ parameters: kwargs.except(:openai, :model),
75
+ execution_time: execution_time,
76
+ response_size: result.to_s.length,
77
+ })
78
+ result
79
+ end
80
+ rescue Faraday::ResourceNotFound => e
79
81
  execution_time = Time.now - start_time
80
-
81
- ActiveSupport::Notifications.instrument("roast.chat_completion.complete", {
82
- success: true,
83
- model: model,
84
- parameters: kwargs.except(:openai),
85
- execution_time: execution_time,
86
- response_size: result.to_s.length,
87
- })
88
-
89
- result
82
+ message = e.response.dig(:body, "error", "message") || e.message
83
+ error = Roast::ResourceNotFoundError.new(message)
84
+ error.set_backtrace(e.backtrace)
85
+ log_and_raise_error(error, message, step_model || model, kwargs, execution_time)
90
86
  rescue => e
91
87
  execution_time = Time.now - start_time
88
+ log_and_raise_error(e, e.message, step_model || model, kwargs, execution_time)
89
+ end
92
90
 
93
- ActiveSupport::Notifications.instrument("roast.chat_completion.error", {
94
- error: e.class.name,
95
- message: e.message,
96
- model: model,
97
- parameters: kwargs.except(:openai),
98
- execution_time: execution_time,
99
- })
100
- raise
91
+ def with_model(model)
92
+ previous_model = @model
93
+ @model = model
94
+ yield
95
+ ensure
96
+ @model = previous_model
101
97
  end
102
98
 
103
99
  def workflow
104
100
  self
105
101
  end
106
102
 
107
- private
103
+ # Expose output manager for state management
104
+ attr_reader :output_manager
108
105
 
109
- # Determine the directory where the actual class is defined, not BaseWorkflow
110
- def determine_context_path
111
- # Get the actual class's source file
112
- klass = self.class
113
-
114
- # Try to get the file path where the class is defined
115
- path = if klass.name.include?("::")
116
- # For namespaced classes like Roast::Workflow::Grading::Workflow
117
- # Convert the class name to a relative path
118
- class_path = klass.name.underscore + ".rb"
119
- # Look through load path to find the actual file
120
- $LOAD_PATH.map { |p| File.join(p, class_path) }.find { |f| File.exist?(f) }
106
+ # Allow direct access to output values without 'output.' prefix
107
+ def method_missing(method_name, *args, &block)
108
+ if output.respond_to?(method_name)
109
+ output.send(method_name, *args, &block)
121
110
  else
122
- # Fall back to the current file if we can't find it
123
- __FILE__
111
+ super
124
112
  end
113
+ end
114
+
115
+ def respond_to_missing?(method_name, include_private = false)
116
+ output.respond_to?(method_name) || super
117
+ end
118
+
119
+ private
120
+
121
+ def log_and_raise_error(error, message, model, params, execution_time)
122
+ ActiveSupport::Notifications.instrument("roast.chat_completion.error", {
123
+ error: error.class.name,
124
+ message: message,
125
+ model: model,
126
+ parameters: params.except(:openai, :model),
127
+ execution_time: execution_time,
128
+ })
125
129
 
126
- # Return directory containing the class definition
127
- File.dirname(path || __FILE__)
130
+ raise error
128
131
  end
129
132
 
130
133
  def read_sidecar_prompt
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "English"
4
+
5
+ module Roast
6
+ module Workflow
7
+ class CommandExecutor
8
+ class CommandExecutionError < StandardError
9
+ attr_reader :command, :exit_status, :original_error
10
+
11
+ def initialize(message, command:, exit_status: nil, original_error: nil)
12
+ @command = command
13
+ @exit_status = exit_status
14
+ @original_error = original_error
15
+ super(message)
16
+ end
17
+ end
18
+
19
+ def initialize(logger: nil)
20
+ @logger = logger || NullLogger.new
21
+ end
22
+
23
+ def execute(command_string, exit_on_error: true)
24
+ command = extract_command(command_string)
25
+
26
+ output = %x(#{command})
27
+ exit_status = $CHILD_STATUS.exitstatus
28
+
29
+ handle_execution_result(
30
+ command: command,
31
+ output: output,
32
+ exit_status: exit_status,
33
+ success: $CHILD_STATUS.success?,
34
+ exit_on_error: exit_on_error,
35
+ )
36
+ rescue ArgumentError, CommandExecutionError
37
+ raise
38
+ rescue => e
39
+ handle_execution_error(
40
+ command: command,
41
+ error: e,
42
+ exit_on_error: exit_on_error,
43
+ )
44
+ end
45
+
46
+ private
47
+
48
+ def extract_command(command_string)
49
+ match = command_string.strip.match(/^\$\((.*)\)$/)
50
+ raise ArgumentError, "Invalid command format. Expected $(command), got: #{command_string}" unless match
51
+
52
+ match[1]
53
+ end
54
+
55
+ def handle_execution_result(command:, output:, exit_status:, success:, exit_on_error:)
56
+ return output if success
57
+
58
+ if exit_on_error
59
+ raise CommandExecutionError.new(
60
+ "Command exited with non-zero status (#{exit_status})",
61
+ command: command,
62
+ exit_status: exit_status,
63
+ )
64
+ else
65
+ @logger.warn("Command '#{command}' exited with non-zero status (#{exit_status}), continuing execution")
66
+ output + "\n[Exit status: #{exit_status}]"
67
+ end
68
+ end
69
+
70
+ def handle_execution_error(command:, error:, exit_on_error:)
71
+ if exit_on_error
72
+ raise CommandExecutionError.new(
73
+ "Failed to execute command '#{command}': #{error.message}",
74
+ command: command,
75
+ original_error: error,
76
+ )
77
+ else
78
+ @logger.warn("Command '#{command}' failed with error: #{error.message}, continuing execution")
79
+ "Error executing command: #{error.message}\n[Exit status: error]"
80
+ end
81
+ end
82
+
83
+ class NullLogger
84
+ def warn(_message); end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roast
4
+ module Workflow
5
+ # Handles execution of conditional steps (if and unless)
6
+ class ConditionalExecutor
7
+ def initialize(workflow, context_path, state_manager, workflow_executor = nil)
8
+ @workflow = workflow
9
+ @context_path = context_path
10
+ @state_manager = state_manager
11
+ @workflow_executor = workflow_executor
12
+ end
13
+
14
+ def execute_conditional(conditional_config)
15
+ $stderr.puts "Executing conditional step: #{conditional_config.inspect}"
16
+
17
+ # Determine if this is an 'if' or 'unless' condition
18
+ condition_expr = conditional_config["if"] || conditional_config["unless"]
19
+ is_unless = conditional_config.key?("unless")
20
+ then_steps = conditional_config["then"]
21
+
22
+ # Verify required parameters
23
+ raise WorkflowExecutor::ConfigurationError, "Missing condition in conditional configuration" unless condition_expr
24
+ raise WorkflowExecutor::ConfigurationError, "Missing 'then' steps in conditional configuration" unless then_steps
25
+
26
+ # Create and execute a ConditionalStep
27
+ require "roast/workflow/conditional_step" unless defined?(Roast::Workflow::ConditionalStep)
28
+ conditional_step = ConditionalStep.new(
29
+ @workflow,
30
+ config: conditional_config,
31
+ name: "conditional_#{condition_expr.gsub(/[^a-zA-Z0-9_]/, "_")[0..20]}",
32
+ context_path: @context_path,
33
+ workflow_executor: @workflow_executor,
34
+ )
35
+
36
+ result = conditional_step.call
37
+
38
+ # Store a marker in workflow output to indicate which branch was taken
39
+ condition_key = is_unless ? "unless" : "if"
40
+ step_name = "#{condition_key}_#{condition_expr.gsub(/[^a-zA-Z0-9_]/, "_")[0..30]}"
41
+ @workflow.output[step_name] = result
42
+
43
+ # Save state
44
+ @state_manager.save_state(step_name, @workflow.output[step_name])
45
+
46
+ result
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "roast/workflow/base_step"
4
+ require "roast/workflow/command_executor"
5
+ require "roast/workflow/expression_utils"
6
+ require "roast/workflow/interpolator"
7
+
8
+ module Roast
9
+ module Workflow
10
+ class ConditionalStep < BaseStep
11
+ include ExpressionUtils
12
+
13
+ def initialize(workflow, config:, name:, context_path:, workflow_executor:, **kwargs)
14
+ super(workflow, name: name, context_path: context_path, **kwargs)
15
+
16
+ @config = config
17
+ @condition = config["if"] || config["unless"]
18
+ @is_unless = config.key?("unless")
19
+ @then_steps = config["then"] || []
20
+ @else_steps = config["else"] || []
21
+ @workflow_executor = workflow_executor
22
+ end
23
+
24
+ def call
25
+ # Evaluate the condition
26
+ condition_result = evaluate_condition(@condition)
27
+
28
+ # Invert the result if this is an 'unless' condition
29
+ condition_result = !condition_result if @is_unless
30
+
31
+ # Select which steps to execute based on the condition
32
+ steps_to_execute = condition_result ? @then_steps : @else_steps
33
+
34
+ # Execute the selected steps
35
+ unless steps_to_execute.empty?
36
+ @workflow_executor.execute_steps(steps_to_execute)
37
+ end
38
+
39
+ # Return a result indicating which branch was taken
40
+ { condition_result: condition_result, branch_executed: condition_result ? "then" : "else" }
41
+ end
42
+
43
+ private
44
+
45
+ def evaluate_condition(condition)
46
+ return false unless condition.is_a?(String)
47
+
48
+ if ruby_expression?(condition)
49
+ evaluate_ruby_expression(condition)
50
+ elsif bash_command?(condition)
51
+ evaluate_bash_command(condition)
52
+ else
53
+ # Treat as a step name or direct boolean
54
+ evaluate_step_or_value(condition)
55
+ end
56
+ end
57
+
58
+ def evaluate_ruby_expression(expression)
59
+ expr = extract_expression(expression)
60
+ begin
61
+ !!@workflow.instance_eval(expr)
62
+ rescue => e
63
+ $stderr.puts "Warning: Error evaluating expression '#{expr}': #{e.message}"
64
+ false
65
+ end
66
+ end
67
+
68
+ def evaluate_bash_command(command)
69
+ cmd = extract_command(command)
70
+ executor = CommandExecutor.new(logger: Roast::Helpers::Logger)
71
+ begin
72
+ result = executor.execute(cmd, exit_on_error: false)
73
+ # For conditionals, we care about the exit status
74
+ result[:success]
75
+ rescue => e
76
+ $stderr.puts "Warning: Error executing command '#{cmd}': #{e.message}"
77
+ false
78
+ end
79
+ end
80
+
81
+ def evaluate_step_or_value(input)
82
+ # Check if it's a reference to a previous step output
83
+ if @workflow.output.key?(input)
84
+ result = @workflow.output[input]
85
+ # Coerce to boolean
86
+ return false if result.nil? || result == false || result == "" || result == "false"
87
+
88
+ return true
89
+ end
90
+
91
+ # Otherwise treat as a direct value
92
+ input.to_s.downcase == "true"
93
+ end
94
+ end
95
+ end
96
+ end
@@ -1,55 +1,46 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "open3"
4
- require "yaml"
3
+ require "active_support/core_ext/module/delegation"
4
+ require "roast/workflow/api_configuration"
5
+ require "roast/workflow/configuration_loader"
6
+ require "roast/workflow/resource_resolver"
7
+ require "roast/workflow/step_finder"
5
8
 
6
9
  module Roast
7
10
  module Workflow
8
11
  # Encapsulates workflow configuration data and provides structured access
9
12
  # to the configuration settings
10
13
  class Configuration
11
- attr_reader :config_hash, :workflow_path, :name, :steps, :tools, :function_configs, :api_token, :api_provider, :model, :resource
14
+ attr_reader :config_hash, :workflow_path, :name, :steps, :tools, :function_configs, :model, :resource
12
15
  attr_accessor :target
13
16
 
14
- def initialize(workflow_path, options = {})
15
- @workflow_path = workflow_path
16
- @config_hash = YAML.load_file(workflow_path)
17
-
18
- # Extract key configuration values
19
- @name = @config_hash["name"] || File.basename(workflow_path, ".yml")
20
- @steps = @config_hash["steps"] || []
17
+ delegate :api_provider, :openrouter?, :openai?, to: :api_configuration
21
18
 
22
- # Process tools configuration
23
- parse_tools
24
-
25
- # Process function-specific configurations
26
- parse_functions
27
-
28
- # Read the target parameter
29
- @target = options[:target] || @config_hash["target"]
19
+ # Delegate api_token to effective_token for backward compatibility
20
+ def api_token
21
+ @api_configuration.effective_token
22
+ end
30
23
 
31
- # Process the target command if it's a shell command
32
- @target = process_target(@target) if has_target?
24
+ def initialize(workflow_path, options = {})
25
+ @workflow_path = workflow_path
33
26
 
34
- # Create the appropriate resource object for the target
35
- if defined?(Roast::Resources)
36
- @resource = if has_target?
37
- Roast::Resources.for(@target)
38
- else
39
- Roast::Resources::NoneResource.new(nil)
40
- end
41
- end
27
+ # Load configuration using ConfigurationLoader
28
+ @config_hash = ConfigurationLoader.load(workflow_path)
42
29
 
43
- # Process API token if provided
44
- if @config_hash["api_token"]
45
- @api_token = process_shell_command(@config_hash["api_token"])
46
- end
30
+ # Extract basic configuration values
31
+ @name = ConfigurationLoader.extract_name(@config_hash, workflow_path)
32
+ @steps = ConfigurationLoader.extract_steps(@config_hash)
33
+ @tools = ConfigurationLoader.extract_tools(@config_hash)
34
+ @function_configs = ConfigurationLoader.extract_functions(@config_hash)
35
+ @model = ConfigurationLoader.extract_model(@config_hash)
47
36
 
48
- # Determine API provider (defaults to OpenAI if not specified)
49
- @api_provider = determine_api_provider
37
+ # Initialize components
38
+ @api_configuration = ApiConfiguration.new(@config_hash)
39
+ @step_finder = StepFinder.new(@steps)
50
40
 
51
- # Extract default model if provided
52
- @model = @config_hash["model"]
41
+ # Process target and resource
42
+ @target = ConfigurationLoader.extract_target(@config_hash, options)
43
+ process_resource
53
44
  end
54
45
 
55
46
  def context_path
@@ -76,62 +67,10 @@ module Roast
76
67
  # Handle different call patterns for backward compatibility
77
68
  if steps_array.is_a?(String) && target_step.nil?
78
69
  target_step = steps_array
79
- steps_array = steps
80
- elsif steps_array.is_a?(Array) && target_step.is_a?(String)
81
- # This is the normal case - steps_array and target_step are provided
82
- else
83
- # Default to self.steps if just the target_step is provided
84
- steps_array = steps
70
+ steps_array = nil
85
71
  end
86
72
 
87
- # First, try using the new more detailed search
88
- steps_array.each_with_index do |step, index|
89
- case step
90
- when Hash
91
- # Could be {name: command} or {name: {substeps}}
92
- step_key = step.keys.first
93
- return index if step_key == target_step
94
- when Array
95
- # This is a parallel step container, search inside it
96
- found = step.any? do |substep|
97
- case substep
98
- when Hash
99
- substep.keys.first == target_step
100
- when String
101
- substep == target_step
102
- else
103
- false
104
- end
105
- end
106
- return index if found
107
- when String
108
- return index if step == target_step
109
- end
110
- end
111
-
112
- # Fall back to the original method using extract_step_name
113
- steps_array.each_with_index do |step, index|
114
- step_name = extract_step_name(step)
115
- if step_name.is_a?(Array)
116
- # For arrays (parallel steps), check if target is in the array
117
- return index if step_name.flatten.include?(target_step)
118
- elsif step_name == target_step
119
- return index
120
- end
121
- end
122
-
123
- nil
124
- end
125
-
126
- # Returns an array of all tool class names
127
- def parse_tools
128
- # Only support array format: ["Roast::Tools::Grep", "Roast::Tools::ReadFile"]
129
- @tools = @config_hash["tools"] || []
130
- end
131
-
132
- # Parse function-specific configurations
133
- def parse_functions
134
- @function_configs = @config_hash["functions"] || {}
73
+ @step_finder.find_index(target_step, steps_array)
135
74
  end
136
75
 
137
76
  # Get configuration for a specific function
@@ -141,77 +80,15 @@ module Roast
141
80
  @function_configs[function_name.to_s] || {}
142
81
  end
143
82
 
144
- def openrouter?
145
- @api_provider == :openrouter
146
- end
147
-
148
- def openai?
149
- @api_provider == :openai
150
- end
151
-
152
83
  private
153
84
 
154
- def determine_api_provider
155
- return :openai unless @config_hash["api_provider"]
156
-
157
- provider = @config_hash["api_provider"].to_s.downcase
158
-
159
- case provider
160
- when "openai"
161
- :openai
162
- when "openrouter"
163
- :openrouter
164
- else
165
- Roast::Helpers::Logger.warn("Unknown API provider '#{provider}', defaulting to OpenAI")
166
- :openai
167
- end
168
- end
169
-
170
- def process_shell_command(command)
171
- # If it's a bash command with the $(command) syntax
172
- if command =~ /^\$\((.*)\)$/
173
- return Open3.capture2e({}, ::Regexp.last_match(1)).first.strip
174
- end
175
-
176
- # Legacy % prefix for backward compatibility
177
- if command.start_with?("% ")
178
- return Open3.capture2e({}, *command.split(" ")[1..-1]).first.strip
179
- end
180
-
181
- # Not a shell command, return as is
182
- command
183
- end
184
-
185
- def process_target(command)
186
- # Process shell command first
187
- processed = process_shell_command(command)
188
-
189
- # If it's a glob pattern, return the full paths of the files it matches
190
- if processed.include?("*")
191
- matched_files = Dir.glob(processed)
192
- # If no files match, return the pattern itself
193
- return processed if matched_files.empty?
194
-
195
- return matched_files.map { |file| File.expand_path(file) }.join("\n")
196
- end
197
-
198
- # For tests, if the command was already processed as a shell command and is simple,
199
- # don't expand the path to avoid breaking existing tests
200
- return processed if command != processed && !processed.include?("/")
85
+ attr_reader :api_configuration
201
86
 
202
- # assumed to be a direct file path(s)
203
- File.expand_path(processed)
204
- end
205
-
206
- def extract_step_name(step)
207
- case step
208
- when String
209
- step
210
- when Hash
211
- step.keys.first
212
- when Array
213
- # For arrays, we'll need special handling as they contain multiple steps
214
- step.map { |s| extract_step_name(s) }
87
+ def process_resource
88
+ if defined?(Roast::Resources)
89
+ @resource = ResourceResolver.resolve(@target, context_path)
90
+ # Update target with processed value for backward compatibility
91
+ @target = @resource.value if has_target?
215
92
  end
216
93
  end
217
94
  end