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,20 @@
1
+ name: analyze_codebase
2
+ description: Analyze a single codebase with pre/post processing support
3
+ model: gpt-4o
4
+ target: "src/main.rb"
5
+
6
+ # Pre-processing: Gather context before analyzing the main file
7
+ pre_processing:
8
+ - gather_dependencies
9
+ - setup_analysis_tools
10
+
11
+ # Main workflow: Analyze the target file
12
+ steps:
13
+ - analyze_code_quality
14
+ - identify_improvements
15
+ - generate_recommendations
16
+
17
+ # Post-processing: Generate final report
18
+ post_processing:
19
+ - summarize_findings
20
+ - create_action_items
@@ -0,0 +1,65 @@
1
+ # Smart Coercion Defaults
2
+
3
+ This example demonstrates how Roast applies intelligent defaults for boolean coercion based on the type of expression being evaluated.
4
+
5
+ ## Default Coercion Rules
6
+
7
+ When a step is used in a boolean context (like `if`, `unless`, or `until` conditions) and no explicit `coerce_to` is specified, Roast applies these smart defaults:
8
+
9
+ 1. **Ruby Expressions** (`{{expression}}`) → Regular boolean coercion (`!!value`)
10
+ - `nil` and `false` are falsy
11
+ - Everything else is truthy (including 0, empty arrays, etc.)
12
+
13
+ 2. **Bash Commands** (`$(command)`) → Exit code interpretation
14
+ - Exit code 0 = true (success)
15
+ - Non-zero exit code = false (failure)
16
+
17
+ 3. **Prompt/Step Names** → LLM boolean interpretation
18
+ - Analyzes natural language responses for yes/no intent
19
+ - "Yes", "True", "Affirmative" → true
20
+ - "No", "False", "Negative" → false
21
+
22
+ 4. **Non-string Values** → Regular boolean coercion
23
+
24
+ ## Examples
25
+
26
+ ### Ruby Expression (Regular Boolean)
27
+ ```yaml
28
+ - repeat:
29
+ until: "{{counter >= 5}}" # Uses !! coercion
30
+ steps:
31
+ - increment: counter
32
+ ```
33
+
34
+ ### Bash Command (Exit Code)
35
+ ```yaml
36
+ - repeat:
37
+ until: "$(test -f /tmp/done)" # True when file exists (exit 0)
38
+ steps:
39
+ - wait: 1
40
+ ```
41
+
42
+ ### Prompt Response (LLM Boolean)
43
+ ```yaml
44
+ - if: "Should we continue?" # Interprets "Yes, let's continue" as true
45
+ then:
46
+ - proceed: "Continuing..."
47
+ ```
48
+
49
+ ## Overriding Defaults
50
+
51
+ You can always override the default coercion by specifying `coerce_to` directly in the step:
52
+
53
+ ```yaml
54
+ - each: "get_items"
55
+ as: "item"
56
+ coerce_to: iterable # Override default to split into array
57
+ steps:
58
+ - process: "{{item}}"
59
+ ```
60
+
61
+ ## Supported Coercion Types
62
+
63
+ - `boolean` - Standard Ruby truthiness (!! operator)
64
+ - `llm_boolean` - Natural language yes/no interpretation
65
+ - `iterable` - Convert to array (splits strings on newlines)
@@ -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
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ eval_gemfile "../Gemfile"
4
+ gem "activesupport", "~> 7.0"
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ eval_gemfile "../Gemfile"
4
+ gem "activesupport", "~> 8.0"
@@ -28,10 +28,19 @@ module Roast
28
28
 
29
29
  def call(string)
30
30
  Roast::Helpers::Logger.info("🔍 Grepping for string: #{string}\n")
31
- # Escape regex special characters in strings with curly braces
32
- # Example: "import {render}" becomes "import \{render\}"
33
- escaped_string = string.gsub(/(\{|\})/, '\\\\\\1')
34
- %x(rg -C 4 --trim --color=never --heading -F -- "#{escaped_string}" . | head -n #{MAX_RESULT_LINES})
31
+
32
+ # Use Open3 to safely pass the string as an argument, avoiding shell injection
33
+ require "open3"
34
+ cmd = ["rg", "-C", "4", "--trim", "--color=never", "--heading", "-F", "--", string, "."]
35
+ stdout, _stderr, _status = Open3.capture3(*cmd)
36
+
37
+ # Limit output to MAX_RESULT_LINES
38
+ lines = stdout.lines
39
+ if lines.size > MAX_RESULT_LINES
40
+ lines.first(MAX_RESULT_LINES).join + "\n... (truncated to #{MAX_RESULT_LINES} lines)"
41
+ else
42
+ stdout
43
+ end
35
44
  rescue StandardError => e
36
45
  "Error grepping for string: #{e.message}".tap do |error_message|
37
46
  Roast::Helpers::Logger.error(error_message + "\n")
@@ -29,9 +29,9 @@ module Roast
29
29
  Roast::Helpers::Logger.info("🔍 Searching for: '#{glob_pattern}' in '#{File.expand_path(path)}'\n")
30
30
  search_for(glob_pattern, path).then do |results|
31
31
  return "No results found for #{glob_pattern} in #{path}" if results.empty?
32
- return read_contents(results.first) if results.size == 1
32
+ return read_contents(File.join(path, results.first)) if results.size == 1
33
33
 
34
- results.join("\n") # purposely give the AI list of actual paths so that it can read without searching first
34
+ results.map { |result| File.join(path, result) }.join("\n") # purposely give the AI list of actual paths so that it can read without searching first
35
35
  end
36
36
  rescue StandardError => e
37
37
  "Error searching for '#{glob_pattern}' in '#{path}': #{e.message}".tap do |error_message|
data/lib/roast/tools.rb CHANGED
@@ -52,7 +52,22 @@ module Roast
52
52
  # Hook that runs on any exit (including crashes and unhandled exceptions)
53
53
  at_exit do
54
54
  if $ERROR_INFO && !$ERROR_INFO.is_a?(SystemExit) # If exiting due to unhandled exception
55
- puts "\n\nExiting due to error: #{$ERROR_INFO.class}: #{$ERROR_INFO.message}\n"
55
+ # Print a more user-friendly message based on the error type
56
+ case $ERROR_INFO
57
+ when Roast::Workflow::CommandExecutor::CommandExecutionError
58
+ puts "\n\n🛑 Workflow stopped due to command failure."
59
+ puts " To continue execution despite command failures, you can:"
60
+ puts " - Fix the failing command"
61
+ puts " - Run with --verbose to see command output"
62
+ puts " - Modify your workflow to handle errors gracefully"
63
+ when Roast::Workflow::WorkflowExecutor::StepExecutionError
64
+ puts "\n\n🛑 Workflow stopped due to step failure."
65
+ puts " Check the error message above for details."
66
+ else
67
+ puts "\n\n🛑 Workflow stopped due to an unexpected error:"
68
+ puts " #{$ERROR_INFO.class}: #{$ERROR_INFO.message}"
69
+ end
70
+ puts "\nFor debugging, you can run with --verbose for more details."
56
71
  # Temporary disable the debugger to fix directory issues
57
72
  # context.instance_eval { binding.irb }
58
73
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roast
4
+ module ValueObjects
5
+ # Value object representing a URI base with validation
6
+ class UriBase
7
+ class InvalidUriBaseError < StandardError; end
8
+
9
+ attr_reader :value
10
+
11
+ def initialize(value)
12
+ @value = value&.to_s
13
+ validate!
14
+ freeze
15
+ end
16
+
17
+ def present?
18
+ !blank?
19
+ end
20
+
21
+ def blank?
22
+ @value.nil? || @value.strip.empty?
23
+ end
24
+
25
+ def to_s
26
+ @value
27
+ end
28
+
29
+ def ==(other)
30
+ return false unless other.is_a?(UriBase)
31
+
32
+ value == other.value
33
+ end
34
+ alias_method :eql?, :==
35
+
36
+ def hash
37
+ [self.class, @value].hash
38
+ end
39
+
40
+ private
41
+
42
+ def validate!
43
+ return if @value.nil? # Allow nil URI base, just not empty strings
44
+
45
+ raise InvalidUriBaseError, "URI base cannot be an empty string" if @value.strip.empty?
46
+ end
47
+ end
48
+ end
49
+ end
@@ -3,3 +3,4 @@
3
3
  require "roast/value_objects/api_token"
4
4
  require "roast/value_objects/step_name"
5
5
  require "roast/value_objects/workflow_path"
6
+ require "roast/value_objects/uri_base"
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.2"
5
5
  end
@@ -2,12 +2,13 @@
2
2
 
3
3
  require "roast/factories/api_provider_factory"
4
4
  require "roast/workflow/resource_resolver"
5
+ require "roast/value_objects/uri_base"
5
6
 
6
7
  module Roast
7
8
  module Workflow
8
9
  # Handles API-related configuration including tokens and providers
9
10
  class ApiConfiguration
10
- attr_reader :api_token, :api_provider
11
+ attr_reader :api_token, :api_provider, :uri_base
11
12
 
12
13
  def initialize(config_hash)
13
14
  @config_hash = config_hash
@@ -37,6 +38,7 @@ module Roast
37
38
  def process_api_configuration
38
39
  extract_api_token
39
40
  extract_api_provider
41
+ extract_uri_base
40
42
  end
41
43
 
42
44
  def extract_api_token
@@ -49,6 +51,12 @@ module Roast
49
51
  @api_provider = Roast::Factories::ApiProviderFactory.from_config(@config_hash)
50
52
  end
51
53
 
54
+ def extract_uri_base
55
+ if @config_hash["uri_base"]
56
+ @uri_base = ResourceResolver.process_shell_command(@config_hash["uri_base"])
57
+ end
58
+ end
59
+
52
60
  def environment_token
53
61
  if openai?
54
62
  ENV["OPENAI_API_KEY"]
@@ -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
@@ -27,10 +27,12 @@ module Roast
27
27
  :configuration,
28
28
  :model
29
29
 
30
+ attr_reader :pre_processing_data
31
+
30
32
  delegate :api_provider, :openai?, to: :configuration
31
33
  delegate :output, :output=, :append_to_final_output, :final_output, to: :output_manager
32
34
 
33
- def initialize(file = nil, name: nil, context_path: nil, resource: nil, session_name: nil, configuration: nil)
35
+ def initialize(file = nil, name: nil, context_path: nil, resource: nil, session_name: nil, configuration: nil, pre_processing_data: nil)
34
36
  @file = file
35
37
  @name = name || self.class.name.underscore.split("/").last
36
38
  @context_path = context_path || ContextPathResolver.resolve(self.class)
@@ -38,6 +40,7 @@ module Roast
38
40
  @session_name = session_name || @name
39
41
  @session_timestamp = nil
40
42
  @configuration = configuration
43
+ @pre_processing_data = pre_processing_data ? DotAccessHash.new(pre_processing_data).freeze : nil
41
44
 
42
45
  # Initialize managers
43
46
  @output_manager = OutputManager.new
@@ -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