roast-ai 0.2.0 → 0.2.2

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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yaml +5 -1
  3. data/.gitignore +29 -1
  4. data/CHANGELOG.md +36 -0
  5. data/CLAUDE.md +5 -0
  6. data/CLAUDE_NOTES.md +68 -0
  7. data/Gemfile.lock +3 -4
  8. data/README.md +235 -10
  9. data/docs/ITERATION_SYNTAX.md +31 -3
  10. data/examples/case_when/README.md +58 -0
  11. data/examples/case_when/detect_language/prompt.md +16 -0
  12. data/examples/case_when/workflow.yml +58 -0
  13. data/examples/direct_coerce_syntax/README.md +32 -0
  14. data/examples/direct_coerce_syntax/workflow.yml +36 -0
  15. data/examples/grading/generate_recommendations/output.txt +6 -6
  16. data/examples/grading/workflow.yml +6 -4
  17. data/examples/json_handling/README.md +32 -0
  18. data/examples/json_handling/workflow.yml +52 -0
  19. data/examples/pre_post_processing/README.md +111 -0
  20. data/examples/pre_post_processing/analyze_test_file/prompt.md +23 -0
  21. data/examples/pre_post_processing/improve_test_coverage/prompt.md +17 -0
  22. data/examples/pre_post_processing/optimize_test_performance/prompt.md +25 -0
  23. data/examples/pre_post_processing/post_processing/aggregate_metrics/prompt.md +31 -0
  24. data/examples/pre_post_processing/post_processing/cleanup_environment/prompt.md +28 -0
  25. data/examples/pre_post_processing/post_processing/generate_summary_report/prompt.md +32 -0
  26. data/examples/pre_post_processing/post_processing/output.txt +24 -0
  27. data/examples/pre_post_processing/pre_processing/gather_baseline_metrics/prompt.md +26 -0
  28. data/examples/pre_post_processing/pre_processing/setup_test_environment/prompt.md +11 -0
  29. data/examples/pre_post_processing/validate_changes/prompt.md +24 -0
  30. data/examples/pre_post_processing/workflow.yml +21 -0
  31. data/examples/single_target_prepost/README.md +36 -0
  32. data/examples/single_target_prepost/post_processing/output.txt +27 -0
  33. data/examples/single_target_prepost/pre_processing/gather_dependencies/prompt.md +11 -0
  34. data/examples/single_target_prepost/workflow.yml +20 -0
  35. data/examples/smart_coercion_defaults/README.md +65 -0
  36. data/examples/smart_coercion_defaults/workflow.yml +44 -0
  37. data/examples/step_configuration/README.md +87 -0
  38. data/examples/step_configuration/workflow.yml +60 -0
  39. data/gemfiles/activesupport7.gemfile +4 -0
  40. data/gemfiles/activesupport8.gemfile +4 -0
  41. data/lib/roast/tools/grep.rb +13 -4
  42. data/lib/roast/tools/search_file.rb +2 -2
  43. data/lib/roast/tools.rb +16 -1
  44. data/lib/roast/value_objects/uri_base.rb +49 -0
  45. data/lib/roast/value_objects.rb +1 -0
  46. data/lib/roast/version.rb +1 -1
  47. data/lib/roast/workflow/api_configuration.rb +9 -1
  48. data/lib/roast/workflow/base_iteration_step.rb +22 -3
  49. data/lib/roast/workflow/base_step.rb +40 -3
  50. data/lib/roast/workflow/base_workflow.rb +4 -1
  51. data/lib/roast/workflow/case_executor.rb +49 -0
  52. data/lib/roast/workflow/case_step.rb +82 -0
  53. data/lib/roast/workflow/command_executor.rb +5 -2
  54. data/lib/roast/workflow/conditional_step.rb +6 -43
  55. data/lib/roast/workflow/configuration.rb +4 -2
  56. data/lib/roast/workflow/configuration_loader.rb +14 -0
  57. data/lib/roast/workflow/error_handler.rb +18 -0
  58. data/lib/roast/workflow/expression_evaluator.rb +86 -0
  59. data/lib/roast/workflow/iteration_executor.rb +18 -0
  60. data/lib/roast/workflow/prompt_step.rb +4 -1
  61. data/lib/roast/workflow/repeat_step.rb +2 -2
  62. data/lib/roast/workflow/step_executor_coordinator.rb +50 -8
  63. data/lib/roast/workflow/step_loader.rb +21 -7
  64. data/lib/roast/workflow/step_type_resolver.rb +16 -0
  65. data/lib/roast/workflow/workflow_execution_context.rb +39 -0
  66. data/lib/roast/workflow/workflow_executor.rb +22 -2
  67. data/lib/roast/workflow/workflow_initializer.rb +11 -2
  68. data/lib/roast/workflow/workflow_runner.rb +127 -5
  69. data/lib/roast/workflow.rb +1 -0
  70. data/lib/roast.rb +7 -1
  71. data/roast.gemspec +1 -1
  72. data/schema/workflow.json +41 -0
  73. metadata +40 -5
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "roast/workflow/base_step"
4
+ require "roast/workflow/expression_evaluator"
5
+ require "roast/workflow/interpolator"
6
+
7
+ module Roast
8
+ module Workflow
9
+ class CaseStep < BaseStep
10
+ include ExpressionEvaluator
11
+
12
+ def initialize(workflow, config:, name:, context_path:, workflow_executor:, **kwargs)
13
+ super(workflow, name: name, context_path: context_path, **kwargs)
14
+
15
+ @config = config
16
+ @case_expression = config["case"]
17
+ @when_clauses = config["when"] || {}
18
+ @else_steps = config["else"] || []
19
+ @workflow_executor = workflow_executor
20
+ end
21
+
22
+ def call
23
+ # Evaluate the case expression to get the value to match against
24
+ case_value = evaluate_case_expression(@case_expression)
25
+
26
+ # Find the matching when clause
27
+ matched_key = find_matching_when_clause(case_value)
28
+
29
+ # Determine which steps to execute
30
+ steps_to_execute = if matched_key
31
+ @when_clauses[matched_key]
32
+ else
33
+ @else_steps
34
+ end
35
+
36
+ # Execute the selected steps
37
+ unless steps_to_execute.nil? || steps_to_execute.empty?
38
+ @workflow_executor.execute_steps(steps_to_execute)
39
+ end
40
+
41
+ # Return a result indicating which branch was taken
42
+ {
43
+ case_value: case_value,
44
+ matched_when: matched_key,
45
+ branch_executed: matched_key || (steps_to_execute.empty? ? "none" : "else"),
46
+ }
47
+ end
48
+
49
+ private
50
+
51
+ def evaluate_case_expression(expression)
52
+ return unless expression
53
+
54
+ # Handle interpolated expressions
55
+ if expression.is_a?(String)
56
+ interpolated = Interpolator.new(@workflow).interpolate(expression)
57
+
58
+ if ruby_expression?(interpolated)
59
+ evaluate_ruby_expression(interpolated)
60
+ elsif bash_command?(interpolated)
61
+ evaluate_bash_command(interpolated, for_condition: false)
62
+ else
63
+ # Return the interpolated value as-is
64
+ interpolated
65
+ end
66
+ else
67
+ expression
68
+ end
69
+ end
70
+
71
+ def find_matching_when_clause(case_value)
72
+ # Convert case_value to string for comparison
73
+ case_value_str = case_value.to_s
74
+
75
+ @when_clauses.keys.find do |when_key|
76
+ # Direct string comparison
77
+ when_key.to_s == case_value_str
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -6,7 +6,7 @@ module Roast
6
6
  module Workflow
7
7
  class CommandExecutor
8
8
  class CommandExecutionError < StandardError
9
- attr_reader :command, :exit_status, :original_error
9
+ attr_reader :command, :exit_status, :original_error, :output
10
10
 
11
11
  def initialize(message, command:, exit_status: nil, original_error: nil)
12
12
  @command = command
@@ -56,11 +56,14 @@ module Roast
56
56
  return output if success
57
57
 
58
58
  if exit_on_error
59
- raise CommandExecutionError.new(
59
+ error = CommandExecutionError.new(
60
60
  "Command exited with non-zero status (#{exit_status})",
61
61
  command: command,
62
62
  exit_status: exit_status,
63
63
  )
64
+ # Store the output in the error
65
+ error.instance_variable_set(:@output, output)
66
+ raise error
64
67
  else
65
68
  @logger.warn("Command '#{command}' exited with non-zero status (#{exit_status}), continuing execution")
66
69
  output + "\n[Exit status: #{exit_status}]"
@@ -1,14 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "roast/workflow/base_step"
4
- require "roast/workflow/command_executor"
5
- require "roast/workflow/expression_utils"
4
+ require "roast/workflow/expression_evaluator"
6
5
  require "roast/workflow/interpolator"
7
6
 
8
7
  module Roast
9
8
  module Workflow
10
9
  class ConditionalStep < BaseStep
11
- include ExpressionUtils
10
+ include ExpressionEvaluator
12
11
 
13
12
  def initialize(workflow, config:, name:, context_path:, workflow_executor:, **kwargs)
14
13
  super(workflow, name: name, context_path: context_path, **kwargs)
@@ -46,51 +45,15 @@ module Roast
46
45
  return false unless condition.is_a?(String)
47
46
 
48
47
  if ruby_expression?(condition)
49
- evaluate_ruby_expression(condition)
48
+ # For conditionals, coerce result to boolean
49
+ !!evaluate_ruby_expression(condition)
50
50
  elsif bash_command?(condition)
51
- evaluate_bash_command(condition)
51
+ evaluate_bash_command(condition, for_condition: true)
52
52
  else
53
53
  # Treat as a step name or direct boolean
54
- evaluate_step_or_value(condition)
54
+ evaluate_step_or_value(condition, for_condition: true)
55
55
  end
56
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
57
  end
95
58
  end
96
59
  end
@@ -11,10 +11,10 @@ module Roast
11
11
  # Encapsulates workflow configuration data and provides structured access
12
12
  # to the configuration settings
13
13
  class Configuration
14
- attr_reader :config_hash, :workflow_path, :name, :steps, :tools, :function_configs, :model, :resource
14
+ attr_reader :config_hash, :workflow_path, :name, :steps, :pre_processing, :post_processing, :tools, :function_configs, :model, :resource
15
15
  attr_accessor :target
16
16
 
17
- delegate :api_provider, :openrouter?, :openai?, to: :api_configuration
17
+ delegate :api_provider, :openrouter?, :openai?, :uri_base, to: :api_configuration
18
18
 
19
19
  # Delegate api_token to effective_token for backward compatibility
20
20
  def api_token
@@ -30,6 +30,8 @@ module Roast
30
30
  # Extract basic configuration values
31
31
  @name = ConfigurationLoader.extract_name(@config_hash, workflow_path)
32
32
  @steps = ConfigurationLoader.extract_steps(@config_hash)
33
+ @pre_processing = ConfigurationLoader.extract_pre_processing(@config_hash)
34
+ @post_processing = ConfigurationLoader.extract_post_processing(@config_hash)
33
35
  @tools = ConfigurationLoader.extract_tools(@config_hash)
34
36
  @function_configs = ConfigurationLoader.extract_functions(@config_hash)
35
37
  @model = ConfigurationLoader.extract_model(@config_hash)
@@ -32,6 +32,20 @@ module Roast
32
32
  config_hash["steps"] || []
33
33
  end
34
34
 
35
+ # Extract pre-processing steps from the configuration
36
+ # @param config_hash [Hash] The configuration hash
37
+ # @return [Array] The pre_processing array or empty array
38
+ def extract_pre_processing(config_hash)
39
+ config_hash["pre_processing"] || []
40
+ end
41
+
42
+ # Extract post-processing steps from the configuration
43
+ # @param config_hash [Hash] The configuration hash
44
+ # @return [Array] The post_processing array or empty array
45
+ def extract_post_processing(config_hash)
46
+ config_hash["post_processing"] || []
47
+ end
48
+
35
49
  # Extract tools from the configuration
36
50
  # @param config_hash [Hash] The configuration hash
37
51
  # @return [Array] The tools array or empty array
@@ -85,6 +85,24 @@ module Roast
85
85
  execution_time: execution_time,
86
86
  })
87
87
 
88
+ # Print user-friendly error message based on error type
89
+ case error
90
+ when StepLoader::StepNotFoundError
91
+ $stderr.puts "\n❌ Step not found: '#{step_name}'"
92
+ $stderr.puts " Please check that the step exists in your workflow's steps directory."
93
+ $stderr.puts " Looking for: steps/#{step_name}.rb or steps/#{step_name}/prompt.md"
94
+ when NoMethodError
95
+ if error.message.include?("undefined method")
96
+ $stderr.puts "\n❌ Step error: '#{step_name}'"
97
+ $stderr.puts " The step file exists but may be missing the 'call' method."
98
+ $stderr.puts " Error: #{error.message}"
99
+ end
100
+ else
101
+ $stderr.puts "\n❌ Step failed: '#{step_name}'"
102
+ $stderr.puts " Error: #{error.message}"
103
+ $stderr.puts " This may be an issue with the step's implementation."
104
+ end
105
+
88
106
  # Wrap the original error with context about which step failed
89
107
  raise WorkflowExecutor::StepExecutionError.new(
90
108
  "Failed to execute step '#{step_name}': #{error.message}",
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "roast/workflow/command_executor"
4
+ require "roast/workflow/expression_utils"
5
+
6
+ module Roast
7
+ module Workflow
8
+ # Shared module for evaluating expressions in workflow steps
9
+ module ExpressionEvaluator
10
+ include ExpressionUtils
11
+
12
+ # Evaluate a Ruby expression in the workflow context
13
+ # @param expression [String] The expression to evaluate
14
+ # @return [Object] The result of the expression
15
+ def evaluate_ruby_expression(expression)
16
+ expr = extract_expression(expression)
17
+ begin
18
+ @workflow.instance_eval(expr)
19
+ rescue => e
20
+ $stderr.puts "Warning: Error evaluating expression '#{expr}': #{e.message}"
21
+ nil
22
+ end
23
+ end
24
+
25
+ # Evaluate a bash command and return its output
26
+ # @param command [String] The command to execute
27
+ # @param for_condition [Boolean] If true, returns success status; if false, returns output
28
+ # @return [Boolean, String, nil] Command result based on for_condition flag
29
+ def evaluate_bash_command(command, for_condition: false)
30
+ cmd = command.start_with?("$(") ? command : extract_command(command)
31
+ executor = CommandExecutor.new(logger: Roast::Helpers::Logger)
32
+
33
+ begin
34
+ output = executor.execute(cmd, exit_on_error: false)
35
+
36
+ # Print command output in verbose mode
37
+ if @workflow.verbose
38
+ $stderr.puts "Evaluating command: #{cmd}"
39
+ $stderr.puts "Command output:"
40
+ $stderr.puts output
41
+ $stderr.puts
42
+ end
43
+
44
+ if for_condition
45
+ # For conditions, we care about the exit status (success = true)
46
+ # Check if output contains exit status marker
47
+ !output.include?("[Exit status:")
48
+ else
49
+ # For case expressions, we want the actual output
50
+ output.strip
51
+ end
52
+ rescue => e
53
+ $stderr.puts "Warning: Error executing command '#{cmd}': #{e.message}"
54
+ for_condition ? false : nil
55
+ end
56
+ end
57
+
58
+ # Evaluate a step reference or direct value
59
+ # @param input [String] The input to evaluate
60
+ # @return [Boolean, Object] The result for conditions, or the value itself
61
+ def evaluate_step_or_value(input, for_condition: false)
62
+ # Check if it's a reference to a previous step output
63
+ if @workflow.output.key?(input)
64
+ result = @workflow.output[input]
65
+
66
+ if for_condition
67
+ # Coerce to boolean for conditions
68
+ return false if result.nil? || result == false || result == "" || result == "false"
69
+
70
+ return true
71
+ else
72
+ # Return the actual value for case expressions
73
+ return result
74
+ end
75
+ end
76
+
77
+ # Otherwise treat as a direct value
78
+ if for_condition
79
+ input.to_s.downcase == "true"
80
+ else
81
+ input
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -33,6 +33,9 @@ module Roast
33
33
  context_path: @context_path,
34
34
  )
35
35
 
36
+ # Apply configuration if provided
37
+ apply_step_configuration(repeat_step, repeat_config)
38
+
36
39
  results = repeat_step.call
37
40
 
38
41
  # Store results in workflow output
@@ -69,6 +72,9 @@ module Roast
69
72
  context_path: @context_path,
70
73
  )
71
74
 
75
+ # Apply configuration if provided
76
+ apply_step_configuration(each_step, each_config)
77
+
72
78
  results = each_step.call
73
79
 
74
80
  # Store results in workflow output
@@ -80,6 +86,18 @@ module Roast
80
86
 
81
87
  results
82
88
  end
89
+
90
+ private
91
+
92
+ # Apply configuration settings to a step
93
+ def apply_step_configuration(step, step_config)
94
+ step.print_response = step_config["print_response"] if step_config.key?("print_response")
95
+ step.auto_loop = step_config["loop"] if step_config.key?("loop")
96
+ step.json = step_config["json"] if step_config.key?("json")
97
+ step.params = step_config["params"] if step_config.key?("params")
98
+ step.model = step_config["model"] if step_config.key?("model")
99
+ step.coerce_to = step_config["coerce_to"].to_sym if step_config.key?("coerce_to")
100
+ end
83
101
  end
84
102
  end
85
103
  end
@@ -9,7 +9,10 @@ module Roast
9
9
 
10
10
  def call
11
11
  prompt(name)
12
- chat_completion(auto_loop: false, print_response: true)
12
+ result = chat_completion
13
+
14
+ # Apply coercion if configured
15
+ apply_coercion(result)
13
16
  end
14
17
  end
15
18
  end
@@ -23,8 +23,8 @@ module Roast
23
23
 
24
24
  begin
25
25
  # Loop until condition is met or max_iterations is reached
26
- # Process the until_condition based on its type with Boolean coercion
27
- until process_iteration_input(@until_condition, workflow, coerce_to: :boolean) || (iteration >= @max_iterations)
26
+ # Process the until_condition based on its type with configured coercion
27
+ until process_iteration_input(@until_condition, workflow, coerce_to: @coerce_to) || (iteration >= @max_iterations)
28
28
  $stderr.puts "Repeat loop iteration #{iteration + 1}"
29
29
 
30
30
  # Execute the nested steps
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "roast/workflow/case_executor"
3
4
  require "roast/workflow/conditional_executor"
4
5
  require "roast/workflow/step_executor_factory"
5
6
  require "roast/workflow/step_type_resolver"
@@ -68,6 +69,8 @@ module Roast
68
69
  execute_iteration_step(step)
69
70
  when StepTypeResolver::CONDITIONAL_STEP
70
71
  execute_conditional_step(step)
72
+ when StepTypeResolver::CASE_STEP
73
+ execute_case_step(step)
71
74
  when StepTypeResolver::HASH_STEP
72
75
  execute_hash_step(step)
73
76
  when StepTypeResolver::PARALLEL_STEP
@@ -105,6 +108,15 @@ module Roast
105
108
  dependencies[:conditional_executor]
106
109
  end
107
110
 
111
+ def case_executor
112
+ @case_executor ||= dependencies[:case_executor] || CaseExecutor.new(
113
+ context.workflow,
114
+ context.context_path,
115
+ dependencies[:state_manager] || dependencies[:workflow_executor].state_manager,
116
+ workflow_executor,
117
+ )
118
+ end
119
+
108
120
  def step_orchestrator
109
121
  dependencies[:step_orchestrator]
110
122
  end
@@ -120,16 +132,42 @@ module Roast
120
132
  error_handler.with_error_handling(step, resource_type: resource_type) do
121
133
  $stderr.puts "Executing: #{step} (Resource type: #{resource_type || "unknown"})"
122
134
 
123
- output = command_executor.execute(step, exit_on_error: exit_on_error)
135
+ begin
136
+ output = command_executor.execute(step, exit_on_error: exit_on_error)
137
+
138
+ # Print command output in verbose mode
139
+ workflow = context.workflow
140
+ if workflow.verbose
141
+ $stderr.puts "Command output:"
142
+ $stderr.puts output
143
+ $stderr.puts
144
+ end
124
145
 
125
- # Add to transcript
126
- workflow = context.workflow
127
- workflow.transcript << {
128
- user: "I just executed the following command: ```\n#{step}\n```\n\nHere is the output:\n\n```\n#{output}\n```",
129
- }
130
- workflow.transcript << { assistant: "Noted, thank you." }
146
+ # Add to transcript
147
+ workflow.transcript << {
148
+ user: "I just executed the following command: ```\n#{step}\n```\n\nHere is the output:\n\n```\n#{output}\n```",
149
+ }
150
+ workflow.transcript << { assistant: "Noted, thank you." }
151
+
152
+ output
153
+ rescue CommandExecutor::CommandExecutionError => e
154
+ # Print user-friendly error message
155
+ $stderr.puts "\n❌ Command failed: #{step}"
156
+ $stderr.puts " Exit status: #{e.exit_status}" if e.exit_status
157
+
158
+ # Show command output if available
159
+ if e.respond_to?(:output) && e.output && !e.output.strip.empty?
160
+ $stderr.puts " Command output:"
161
+ e.output.strip.split("\n").each do |line|
162
+ $stderr.puts " #{line}"
163
+ end
164
+ elsif workflow && !workflow.verbose
165
+ $stderr.puts " To see the command output, run with --verbose flag."
166
+ end
131
167
 
132
- output
168
+ $stderr.puts " This typically means the command returned an error.\n"
169
+ raise
170
+ end
133
171
  end
134
172
  end
135
173
 
@@ -154,6 +192,10 @@ module Roast
154
192
  conditional_executor.execute_conditional(step)
155
193
  end
156
194
 
195
+ def execute_case_step(step)
196
+ case_executor.execute_case(step)
197
+ end
198
+
157
199
  def execute_hash_step(step)
158
200
  name, command = step.to_a.flatten
159
201
  interpolated_name = interpolator.interpolate(name)
@@ -25,11 +25,11 @@ module Roast
25
25
  class StepNotFoundError < StepLoaderError; end
26
26
  class StepExecutionError < StepLoaderError; end
27
27
 
28
- attr_reader :context
28
+ attr_reader :context, :phase
29
29
 
30
30
  delegate :workflow, :config_hash, :context_path, to: :context
31
31
 
32
- def initialize(workflow, config_hash, context_path)
32
+ def initialize(workflow, config_hash, context_path, phase: :steps)
33
33
  # Support both old and new initialization patterns
34
34
  @context = if workflow.is_a?(WorkflowContext)
35
35
  workflow
@@ -40,6 +40,7 @@ module Roast
40
40
  context_path: context_path,
41
41
  )
42
42
  end
43
+ @phase = phase
43
44
  end
44
45
 
45
46
  # Finds and loads a step by name
@@ -51,7 +52,7 @@ module Roast
51
52
 
52
53
  # First check for a prompt step (contains spaces)
53
54
  if name.plain_text?
54
- step = Roast::Workflow::PromptStep.new(workflow, name: name.to_s, auto_loop: false)
55
+ step = Roast::Workflow::PromptStep.new(workflow, name: name.to_s, auto_loop: true)
55
56
  configure_step(step, name.to_s)
56
57
  return step
57
58
  end
@@ -75,6 +76,12 @@ module Roast
75
76
 
76
77
  # Find a Ruby step file in various locations
77
78
  def find_step_file(step_name)
79
+ # Check in phase-specific directory first
80
+ if phase != :steps
81
+ phase_rb_path = File.join(context_path, phase.to_s, "#{step_name}.rb")
82
+ return phase_rb_path if File.file?(phase_rb_path)
83
+ end
84
+
78
85
  # Check in context path
79
86
  rb_file_path = File.join(context_path, "#{step_name}.rb")
80
87
  return rb_file_path if File.file?(rb_file_path)
@@ -88,6 +95,12 @@ module Roast
88
95
 
89
96
  # Find a step directory
90
97
  def find_step_directory(step_name)
98
+ # Check in phase-specific directory first
99
+ if phase != :steps
100
+ phase_step_path = File.join(context_path, phase.to_s, step_name)
101
+ return phase_step_path if File.directory?(phase_step_path)
102
+ end
103
+
91
104
  # Check in context path
92
105
  step_path = File.join(context_path, step_name)
93
106
  return step_path if File.directory?(step_path)
@@ -144,10 +157,11 @@ module Roast
144
157
 
145
158
  # Apply configuration settings to a step
146
159
  def apply_step_configuration(step, step_config)
147
- step.print_response = step_config["print_response"] if step_config["print_response"].present?
148
- step.auto_loop = step_config["loop"] if step_config["loop"].present?
149
- step.json = step_config["json"] if step_config["json"].present?
150
- step.params = step_config["params"] if step_config["params"].present?
160
+ step.print_response = step_config["print_response"] if step_config.key?("print_response")
161
+ step.auto_loop = step_config["loop"] if step_config.key?("loop")
162
+ step.json = step_config["json"] if step_config.key?("json")
163
+ step.params = step_config["params"] if step_config.key?("params")
164
+ step.coerce_to = step_config["coerce_to"].to_sym if step_config.key?("coerce_to")
151
165
  end
152
166
  end
153
167
  end
@@ -9,6 +9,7 @@ module Roast
9
9
  GLOB_STEP = :glob
10
10
  ITERATION_STEP = :iteration
11
11
  CONDITIONAL_STEP = :conditional
12
+ CASE_STEP = :case
12
13
  HASH_STEP = :hash
13
14
  PARALLEL_STEP = :parallel
14
15
  STRING_STEP = :string
@@ -20,6 +21,9 @@ module Roast
20
21
  # Special step names for conditionals
21
22
  CONDITIONAL_STEPS = ["if", "unless"].freeze
22
23
 
24
+ # Special step name for case statements
25
+ CASE_STEPS = ["case"].freeze
26
+
23
27
  class << self
24
28
  # Resolve the type of a step
25
29
  # @param step [String, Hash, Array] The step to analyze
@@ -76,6 +80,16 @@ module Roast
76
80
  CONDITIONAL_STEPS.include?(step_name)
77
81
  end
78
82
 
83
+ # Check if a step is a case step
84
+ # @param step [Hash] The step to check
85
+ # @return [Boolean] true if it's a case step
86
+ def case_step?(step)
87
+ return false unless step.is_a?(Hash)
88
+
89
+ step_name = step.keys.first
90
+ CASE_STEPS.include?(step_name)
91
+ end
92
+
79
93
  # Extract the step name from various step formats
80
94
  # @param step [String, Hash, Array] The step
81
95
  # @return [String, nil] The step name or nil
@@ -107,6 +121,8 @@ module Roast
107
121
  ITERATION_STEP
108
122
  elsif conditional_step?(step)
109
123
  CONDITIONAL_STEP
124
+ elsif case_step?(step)
125
+ CASE_STEP
110
126
  else
111
127
  HASH_STEP
112
128
  end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/string/inflections"
4
+
5
+ module Roast
6
+ module Workflow
7
+ # Manages execution context across pre-processing, target workflows, and post-processing phases
8
+ class WorkflowExecutionContext
9
+ attr_reader :pre_processing_output, :target_outputs
10
+
11
+ def initialize
12
+ @pre_processing_output = OutputManager.new
13
+ @target_outputs = {}
14
+ end
15
+
16
+ # Add output from a target workflow execution
17
+ def add_target_output(target, output_manager)
18
+ target_key = generate_target_key(target)
19
+ @target_outputs[target_key] = output_manager
20
+ end
21
+
22
+ # Get all data as a hash for post-processing
23
+ def to_h
24
+ {
25
+ pre_processing: @pre_processing_output.to_h,
26
+ targets: @target_outputs.transform_values(&:to_h),
27
+ }
28
+ end
29
+
30
+ private
31
+
32
+ def generate_target_key(target)
33
+ return "default" unless target
34
+
35
+ target.to_s.parameterize
36
+ end
37
+ end
38
+ end
39
+ end