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,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
|
data/lib/roast/tools/grep.rb
CHANGED
@@ -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
|
-
|
32
|
-
#
|
33
|
-
|
34
|
-
|
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
|
-
|
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
|
data/lib/roast/value_objects.rb
CHANGED
data/lib/roast/version.rb
CHANGED
@@ -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
|
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,
|
113
|
-
|
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:
|
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
|