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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yaml +5 -1
- data/.gitignore +29 -1
- data/CHANGELOG.md +36 -0
- data/CLAUDE.md +5 -0
- data/CLAUDE_NOTES.md +68 -0
- data/Gemfile.lock +3 -4
- data/README.md +235 -10
- data/docs/ITERATION_SYNTAX.md +31 -3
- data/examples/case_when/README.md +58 -0
- data/examples/case_when/detect_language/prompt.md +16 -0
- data/examples/case_when/workflow.yml +58 -0
- data/examples/direct_coerce_syntax/README.md +32 -0
- data/examples/direct_coerce_syntax/workflow.yml +36 -0
- data/examples/grading/generate_recommendations/output.txt +6 -6
- data/examples/grading/workflow.yml +6 -4
- data/examples/json_handling/README.md +32 -0
- data/examples/json_handling/workflow.yml +52 -0
- data/examples/pre_post_processing/README.md +111 -0
- data/examples/pre_post_processing/analyze_test_file/prompt.md +23 -0
- data/examples/pre_post_processing/improve_test_coverage/prompt.md +17 -0
- data/examples/pre_post_processing/optimize_test_performance/prompt.md +25 -0
- data/examples/pre_post_processing/post_processing/aggregate_metrics/prompt.md +31 -0
- data/examples/pre_post_processing/post_processing/cleanup_environment/prompt.md +28 -0
- data/examples/pre_post_processing/post_processing/generate_summary_report/prompt.md +32 -0
- data/examples/pre_post_processing/post_processing/output.txt +24 -0
- data/examples/pre_post_processing/pre_processing/gather_baseline_metrics/prompt.md +26 -0
- data/examples/pre_post_processing/pre_processing/setup_test_environment/prompt.md +11 -0
- data/examples/pre_post_processing/validate_changes/prompt.md +24 -0
- data/examples/pre_post_processing/workflow.yml +21 -0
- data/examples/single_target_prepost/README.md +36 -0
- data/examples/single_target_prepost/post_processing/output.txt +27 -0
- data/examples/single_target_prepost/pre_processing/gather_dependencies/prompt.md +11 -0
- data/examples/single_target_prepost/workflow.yml +20 -0
- data/examples/smart_coercion_defaults/README.md +65 -0
- data/examples/smart_coercion_defaults/workflow.yml +44 -0
- data/examples/step_configuration/README.md +87 -0
- data/examples/step_configuration/workflow.yml +60 -0
- data/gemfiles/activesupport7.gemfile +4 -0
- data/gemfiles/activesupport8.gemfile +4 -0
- data/lib/roast/tools/grep.rb +13 -4
- data/lib/roast/tools/search_file.rb +2 -2
- data/lib/roast/tools.rb +16 -1
- data/lib/roast/value_objects/uri_base.rb +49 -0
- data/lib/roast/value_objects.rb +1 -0
- data/lib/roast/version.rb +1 -1
- data/lib/roast/workflow/api_configuration.rb +9 -1
- data/lib/roast/workflow/base_iteration_step.rb +22 -3
- data/lib/roast/workflow/base_step.rb +40 -3
- data/lib/roast/workflow/base_workflow.rb +4 -1
- data/lib/roast/workflow/case_executor.rb +49 -0
- data/lib/roast/workflow/case_step.rb +82 -0
- data/lib/roast/workflow/command_executor.rb +5 -2
- data/lib/roast/workflow/conditional_step.rb +6 -43
- data/lib/roast/workflow/configuration.rb +4 -2
- data/lib/roast/workflow/configuration_loader.rb +14 -0
- data/lib/roast/workflow/error_handler.rb +18 -0
- data/lib/roast/workflow/expression_evaluator.rb +86 -0
- data/lib/roast/workflow/iteration_executor.rb +18 -0
- data/lib/roast/workflow/prompt_step.rb +4 -1
- data/lib/roast/workflow/repeat_step.rb +2 -2
- data/lib/roast/workflow/step_executor_coordinator.rb +50 -8
- data/lib/roast/workflow/step_loader.rb +21 -7
- data/lib/roast/workflow/step_type_resolver.rb +16 -0
- data/lib/roast/workflow/workflow_execution_context.rb +39 -0
- data/lib/roast/workflow/workflow_executor.rb +22 -2
- data/lib/roast/workflow/workflow_initializer.rb +11 -2
- data/lib/roast/workflow/workflow_runner.rb +127 -5
- data/lib/roast/workflow.rb +1 -0
- data/lib/roast.rb +7 -1
- data/roast.gemspec +1 -1
- data/schema/workflow.json +41 -0
- 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
|
-
|
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/
|
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
|
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
|
-
|
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
|
@@ -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
|
27
|
-
until process_iteration_input(@until_condition, workflow, coerce_to:
|
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
|
-
|
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
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
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
|
-
|
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:
|
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
|
148
|
-
step.auto_loop = step_config["loop"] if step_config
|
149
|
-
step.json = step_config["json"] if step_config
|
150
|
-
step.params = step_config["params"] if step_config
|
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
|