roast-ai 0.2.0 → 0.2.1

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.
@@ -0,0 +1,44 @@
1
+ name: Smart Coercion Defaults Demo
2
+ description: Demonstrates how different step types get smart boolean coercion defaults
3
+
4
+ steps:
5
+ # Example 1: Ruby expressions default to regular boolean coercion
6
+ - set_counter:
7
+ value: 3
8
+
9
+ - repeat:
10
+ until: "{{counter >= 5}}" # Ruby expression defaults to boolean
11
+ steps:
12
+ - increment_counter:
13
+ value: "{{counter + 1}}"
14
+ - log: "Counter is now {{counter}}"
15
+
16
+ # Example 2: Bash commands default to exit code interpretation
17
+ - check_file_exists:
18
+ repeat:
19
+ until: "$(ls /tmp/important_file 2>/dev/null)" # Bash command defaults to exit code
20
+ max_iterations: 3
21
+ steps:
22
+ - create_file: "$(mkdir -p /tmp && touch /tmp/important_file)"
23
+ - log: "Waiting for file to exist..."
24
+
25
+ # Example 3: Prompt/step names default to LLM boolean interpretation
26
+ - ask_user_ready:
27
+ prompt: "Are you ready to continue? Please respond yes or no."
28
+
29
+ - conditional:
30
+ if: "ask_user_ready" # Step name defaults to llm_boolean
31
+ then:
32
+ - proceed: "Great! Let's continue..."
33
+ else:
34
+ - wait: "Okay, take your time."
35
+
36
+ # Example 4: Explicit coerce_to overrides smart defaults
37
+ - get_items:
38
+ prompt: "List three fruits, one per line"
39
+
40
+ - each: "get_items"
41
+ as: "fruit"
42
+ coerce_to: iterable # Override default llm_boolean to iterable
43
+ steps:
44
+ - process_fruit: "Processing {{fruit}}"
@@ -0,0 +1,87 @@
1
+ # Step Configuration Example
2
+
3
+ This example demonstrates how to configure various step types in Roast workflows, including:
4
+ - Inline prompts
5
+ - Iterator steps (each/repeat)
6
+ - Regular steps
7
+
8
+ ## Configuration Options
9
+
10
+ All step types support the following configuration options:
11
+
12
+ - `model`: The AI model to use (e.g., "gpt-4o", "claude-3-opus")
13
+ - `loop`: Whether to enable auto-looping (true/false)
14
+ - `print_response`: Whether to print the response to stdout (true/false)
15
+ - `json`: Whether to expect a JSON response (true/false)
16
+ - `params`: Additional parameters to pass to the model (e.g., temperature, max_tokens)
17
+ - `coerce_to`: How to convert the step result (options: "boolean", "llm_boolean", "iterable")
18
+
19
+ ## Configuration Precedence
20
+
21
+ 1. **Step-specific configuration** takes highest precedence
22
+ 2. **Global configuration** (defined at the workflow level) applies to all steps without specific configuration
23
+ 3. **Default values** are used when no configuration is provided
24
+
25
+ ## Inline Prompt Configuration
26
+
27
+ Inline prompts can be configured in two ways:
28
+
29
+ ### As a top-level key:
30
+ ```yaml
31
+ analyze the code:
32
+ model: gpt-4o
33
+ loop: false
34
+ print_response: true
35
+ ```
36
+
37
+ ### Inline in the steps array:
38
+ ```yaml
39
+ steps:
40
+ - suggest improvements:
41
+ model: claude-3-opus
42
+ params:
43
+ temperature: 0.9
44
+ ```
45
+
46
+ ## Iterator Configuration
47
+
48
+ Both `each` and `repeat` steps support configuration:
49
+
50
+ ```yaml
51
+ each:
52
+ each: "{{files}}"
53
+ as: file
54
+ model: gpt-3.5-turbo
55
+ loop: false
56
+ steps:
57
+ - process {{file}}
58
+
59
+ repeat:
60
+ repeat: true
61
+ until: "{{done}}"
62
+ model: gpt-4o
63
+ print_response: true
64
+ steps:
65
+ - check status
66
+ ```
67
+
68
+ ## Coercion Types
69
+
70
+ The `coerce_to` option allows you to convert step results to specific types:
71
+
72
+ - **`boolean`**: Converts any value to true/false (nil, false, empty string → false; everything else → true)
73
+ - **`llm_boolean`**: Interprets natural language responses as boolean (e.g., "Yes", "Definitely!" → true; "No", "Not yet" → false)
74
+ - **`iterable`**: Ensures the result can be iterated over (splits strings by newlines if needed)
75
+
76
+ This is particularly useful when:
77
+ - Using steps in conditional contexts (if/unless)
78
+ - Using steps as conditions in repeat loops
79
+ - Processing step results in each loops
80
+
81
+ ## Running the Example
82
+
83
+ ```bash
84
+ bin/roast examples/step_configuration/workflow.yml
85
+ ```
86
+
87
+ Note: This example is for demonstration purposes and shows the configuration syntax. You'll need to adapt it to your specific use case with appropriate prompts and logic.
@@ -0,0 +1,60 @@
1
+ name: Step Configuration Example
2
+ description: Demonstrates how to configure various step types including inline prompts, iterators, and regular steps
3
+
4
+ # Global configuration that applies to all steps
5
+ model: openai/gpt-4o-mini
6
+ loop: true
7
+
8
+ # Configuration for specific steps
9
+ summarize the code:
10
+ model: claude-3-opus # Override global model
11
+ loop: false # Disable auto-loop for this step
12
+ print_response: true # Print the response
13
+ json: false # Don't expect JSON response
14
+ params:
15
+ temperature: 0.7 # Custom temperature
16
+
17
+ analyze complexity:
18
+ model: gpt-4o
19
+ json: true # Expect JSON response
20
+ params:
21
+ temperature: 0.2 # Lower temperature for more consistent analysis
22
+
23
+ # Iterator step configuration
24
+ each:
25
+ each: "{{files}}"
26
+ as: file
27
+ model: gpt-3.5-turbo # Use a faster model for iteration
28
+ loop: false # Don't loop on each iteration
29
+ steps:
30
+ - process {{file}}
31
+
32
+ repeat:
33
+ repeat: true
34
+ until: "are all tests passing" # This will be evaluated as a step
35
+ max_iterations: 5
36
+ model: gpt-4o
37
+ print_response: true
38
+ coerce_to: llm_boolean # Interpret natural language response as boolean
39
+ steps:
40
+ - run tests
41
+ - fix failing tests
42
+
43
+ # Step used in boolean context
44
+ are all tests passing:
45
+ model: gpt-4o
46
+ coerce_to: llm_boolean # Convert "Yes, all tests are passing!" to true
47
+ params:
48
+ temperature: 0.2
49
+
50
+ # Steps can mix configured and unconfigured inline prompts
51
+ steps:
52
+ - list all source files # Uses global configuration
53
+ - summarize the code # Uses step-specific configuration
54
+ - analyze complexity # Uses step-specific configuration
55
+ - suggest improvements: # Step-specific configuration inline
56
+ model: claude-3-opus
57
+ params:
58
+ temperature: 0.9
59
+ - each # Iterator with configuration
60
+ - repeat # Iterator with configuration
data/lib/roast/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Roast
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.1"
5
5
  end
@@ -26,14 +26,21 @@ module Roast
26
26
  def process_iteration_input(input, context, coerce_to: nil)
27
27
  if input.is_a?(String)
28
28
  if ruby_expression?(input)
29
+ # Default to regular boolean for ruby expressions
30
+ coerce_to ||= :boolean
29
31
  process_ruby_expression(input, context, coerce_to)
30
32
  elsif bash_command?(input)
33
+ # Default to boolean (which will interpret exit code) for bash commands
34
+ coerce_to ||= :boolean
31
35
  process_bash_command(input, coerce_to)
32
36
  else
37
+ # For prompts/steps, default to llm_boolean
38
+ coerce_to ||= :llm_boolean
33
39
  process_step_or_prompt(input, context, coerce_to)
34
40
  end
35
41
  else
36
- # Non-string inputs are coerced as-is
42
+ # Non-string inputs default to regular boolean
43
+ coerce_to ||= :boolean
37
44
  coerce_result(input, coerce_to)
38
45
  end
39
46
  end
@@ -109,8 +116,20 @@ module Roast
109
116
  result = Roast::Tools::Cmd.call(command)
110
117
 
111
118
  if coerce_to == :boolean
112
- # For boolean coercion, use exit status (assume success unless error message)
113
- !result.to_s.start_with?("Error")
119
+ # For boolean coercion, check if command was allowed and exit status was 0
120
+ if result.to_s.start_with?("Error: Command not allowed")
121
+ return false
122
+ end
123
+
124
+ # Parse exit status from the output
125
+ # The Cmd tool returns output in format: "Command: X\nExit status: Y\nOutput:\nZ"
126
+ if result =~ /Exit status: (\d+)/
127
+ exit_status = ::Regexp.last_match(1).to_i
128
+ exit_status == 0
129
+ else
130
+ # If we can't parse exit status, assume success if no error
131
+ !result.to_s.start_with?("Error")
132
+ end
114
133
  else
115
134
  # For other uses, return the output
116
135
  result
@@ -9,7 +9,7 @@ module Roast
9
9
  class BaseStep
10
10
  extend Forwardable
11
11
 
12
- attr_accessor :model, :print_response, :auto_loop, :json, :params, :resource
12
+ attr_accessor :model, :print_response, :auto_loop, :json, :params, :resource, :coerce_to
13
13
  attr_reader :workflow, :name, :context_path
14
14
 
15
15
  def_delegator :workflow, :append_to_final_output
@@ -25,20 +25,33 @@ module Roast
25
25
  @auto_loop = auto_loop
26
26
  @json = false
27
27
  @params = {}
28
+ @coerce_to = nil
28
29
  @resource = workflow.resource if workflow.respond_to?(:resource)
29
30
  end
30
31
 
31
32
  def call
32
33
  prompt(read_sidecar_prompt)
33
- chat_completion(print_response:, auto_loop:, json:, params:)
34
+ result = chat_completion(print_response:, auto_loop:, json:, params:)
35
+
36
+ # Apply coercion if configured
37
+ apply_coercion(result)
34
38
  end
35
39
 
36
40
  protected
37
41
 
38
- def chat_completion(print_response: false, auto_loop: true, json: false, params: {})
42
+ def chat_completion(print_response: nil, auto_loop: nil, json: nil, params: nil)
43
+ # Use instance variables as defaults if parameters are not provided
44
+ print_response = @print_response if print_response.nil?
45
+ auto_loop = @auto_loop if auto_loop.nil?
46
+ json = @json if json.nil?
47
+ params = @params if params.nil?
48
+
39
49
  workflow.chat_completion(openai: workflow.openai? && model, loop: auto_loop, model: model, json:, params:).then do |response|
40
50
  case response
51
+ in Array if json
52
+ response.flatten.first
41
53
  in Array
54
+ # For non-JSON responses, join array elements
42
55
  response.map(&:presence).compact.join("\n")
43
56
  else
44
57
  response
@@ -73,6 +86,30 @@ module Roast
73
86
  append_to_final_output(response)
74
87
  end
75
88
  end
89
+
90
+ private
91
+
92
+ def apply_coercion(result)
93
+ return result unless @coerce_to
94
+
95
+ case @coerce_to
96
+ when :boolean
97
+ # Simple boolean coercion
98
+ !!result
99
+ when :llm_boolean
100
+ # Use LLM boolean coercer for natural language responses
101
+ require "roast/workflow/llm_boolean_coercer"
102
+ LlmBooleanCoercer.coerce(result)
103
+ when :iterable
104
+ # Ensure result is iterable
105
+ return result if result.respond_to?(:each)
106
+
107
+ result.to_s.split("\n")
108
+ else
109
+ # Unknown coercion type, return as-is
110
+ result
111
+ end
112
+ end
76
113
  end
77
114
  end
78
115
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roast
4
+ module Workflow
5
+ # Handles execution of case/when/else steps
6
+ class CaseExecutor
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_case(case_config)
15
+ $stderr.puts "Executing case step: #{case_config.inspect}"
16
+
17
+ # Extract case expression
18
+ case_expr = case_config["case"]
19
+ when_clauses = case_config["when"]
20
+ case_config["else"]
21
+
22
+ # Verify required parameters
23
+ raise WorkflowExecutor::ConfigurationError, "Missing 'case' expression in case configuration" unless case_expr
24
+ raise WorkflowExecutor::ConfigurationError, "Missing 'when' clauses in case configuration" unless when_clauses
25
+
26
+ # Create and execute a CaseStep
27
+ require "roast/workflow/case_step" unless defined?(Roast::Workflow::CaseStep)
28
+ case_step = CaseStep.new(
29
+ @workflow,
30
+ config: case_config,
31
+ name: "case_#{case_expr.to_s.gsub(/[^a-zA-Z0-9_]/, "_")[0..30]}",
32
+ context_path: @context_path,
33
+ workflow_executor: @workflow_executor,
34
+ )
35
+
36
+ result = case_step.call
37
+
38
+ # Store the result in workflow output
39
+ step_name = "case_#{case_expr.to_s.gsub(/[^a-zA-Z0-9_]/, "_")[0..30]}"
40
+ @workflow.output[step_name] = result
41
+
42
+ # Save state
43
+ @state_manager.save_state(step_name, @workflow.output[step_name])
44
+
45
+ result
46
+ end
47
+ end
48
+ end
49
+ end
@@ -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
@@ -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
@@ -0,0 +1,78 @@
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
+ if for_condition
37
+ # For conditions, we care about the exit status (success = true)
38
+ # Check if output contains exit status marker
39
+ !output.include?("[Exit status:")
40
+ else
41
+ # For case expressions, we want the actual output
42
+ output.strip
43
+ end
44
+ rescue => e
45
+ $stderr.puts "Warning: Error executing command '#{cmd}': #{e.message}"
46
+ for_condition ? false : nil
47
+ end
48
+ end
49
+
50
+ # Evaluate a step reference or direct value
51
+ # @param input [String] The input to evaluate
52
+ # @return [Boolean, Object] The result for conditions, or the value itself
53
+ def evaluate_step_or_value(input, for_condition: false)
54
+ # Check if it's a reference to a previous step output
55
+ if @workflow.output.key?(input)
56
+ result = @workflow.output[input]
57
+
58
+ if for_condition
59
+ # Coerce to boolean for conditions
60
+ return false if result.nil? || result == false || result == "" || result == "false"
61
+
62
+ return true
63
+ else
64
+ # Return the actual value for case expressions
65
+ return result
66
+ end
67
+ end
68
+
69
+ # Otherwise treat as a direct value
70
+ if for_condition
71
+ input.to_s.downcase == "true"
72
+ else
73
+ input
74
+ end
75
+ end
76
+ end
77
+ end
78
+ 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