roast-ai 0.1.7 → 0.2.0
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 +1 -1
- data/CHANGELOG.md +40 -1
- data/CLAUDE.md +20 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +9 -6
- data/README.md +81 -14
- data/bin/roast +27 -0
- data/docs/ITERATION_SYNTAX.md +119 -0
- data/examples/conditional/README.md +161 -0
- data/examples/conditional/check_condition/prompt.md +1 -0
- data/examples/conditional/simple_workflow.yml +15 -0
- data/examples/conditional/workflow.yml +23 -0
- data/examples/dot_notation/README.md +37 -0
- data/examples/dot_notation/workflow.yml +44 -0
- data/examples/exit_on_error/README.md +50 -0
- data/examples/exit_on_error/analyze_lint_output/prompt.md +9 -0
- data/examples/exit_on_error/apply_fixes/prompt.md +2 -0
- data/examples/exit_on_error/workflow.yml +19 -0
- data/examples/grading/workflow.yml +5 -1
- data/examples/iteration/IMPLEMENTATION.md +88 -0
- data/examples/iteration/README.md +68 -0
- data/examples/iteration/analyze_complexity/prompt.md +22 -0
- data/examples/iteration/generate_recommendations/prompt.md +21 -0
- data/examples/iteration/generate_report/prompt.md +129 -0
- data/examples/iteration/implement_fix/prompt.md +25 -0
- data/examples/iteration/prioritize_issues/prompt.md +24 -0
- data/examples/iteration/prompts/analyze_file.md +28 -0
- data/examples/iteration/prompts/generate_summary.md +24 -0
- data/examples/iteration/prompts/update_report.md +29 -0
- data/examples/iteration/prompts/write_report.md +22 -0
- data/examples/iteration/read_file/prompt.md +9 -0
- data/examples/iteration/select_next_issue/prompt.md +25 -0
- data/examples/iteration/simple_workflow.md +39 -0
- data/examples/iteration/simple_workflow.yml +58 -0
- data/examples/iteration/update_fix_count/prompt.md +26 -0
- data/examples/iteration/verify_fix/prompt.md +29 -0
- data/examples/iteration/workflow.yml +42 -0
- data/examples/openrouter_example/workflow.yml +2 -2
- data/examples/workflow_generator/README.md +27 -0
- data/examples/workflow_generator/analyze_user_request/prompt.md +34 -0
- data/examples/workflow_generator/create_workflow_files/prompt.md +32 -0
- data/examples/workflow_generator/get_user_input/prompt.md +14 -0
- data/examples/workflow_generator/info_from_roast.rb +22 -0
- data/examples/workflow_generator/workflow.yml +35 -0
- data/lib/roast/errors.rb +9 -0
- data/lib/roast/factories/api_provider_factory.rb +61 -0
- data/lib/roast/helpers/function_caching_interceptor.rb +1 -1
- data/lib/roast/helpers/minitest_coverage_runner.rb +1 -1
- data/lib/roast/helpers/prompt_loader.rb +50 -1
- data/lib/roast/resources/base_resource.rb +7 -0
- data/lib/roast/resources.rb +6 -6
- data/lib/roast/tools/ask_user.rb +40 -0
- data/lib/roast/tools/cmd.rb +1 -1
- data/lib/roast/tools/search_file.rb +1 -1
- data/lib/roast/tools.rb +11 -1
- data/lib/roast/value_objects/api_token.rb +49 -0
- data/lib/roast/value_objects/step_name.rb +39 -0
- data/lib/roast/value_objects/workflow_path.rb +77 -0
- data/lib/roast/value_objects.rb +5 -0
- data/lib/roast/version.rb +1 -1
- data/lib/roast/workflow/api_configuration.rb +61 -0
- data/lib/roast/workflow/base_iteration_step.rb +165 -0
- data/lib/roast/workflow/base_step.rb +4 -24
- data/lib/roast/workflow/base_workflow.rb +76 -73
- data/lib/roast/workflow/command_executor.rb +88 -0
- data/lib/roast/workflow/conditional_executor.rb +50 -0
- data/lib/roast/workflow/conditional_step.rb +96 -0
- data/lib/roast/workflow/configuration.rb +35 -158
- data/lib/roast/workflow/configuration_loader.rb +78 -0
- data/lib/roast/workflow/configuration_parser.rb +13 -248
- data/lib/roast/workflow/context_path_resolver.rb +43 -0
- data/lib/roast/workflow/dot_access_hash.rb +198 -0
- data/lib/roast/workflow/each_step.rb +86 -0
- data/lib/roast/workflow/error_handler.rb +97 -0
- data/lib/roast/workflow/expression_utils.rb +36 -0
- data/lib/roast/workflow/file_state_repository.rb +3 -2
- data/lib/roast/workflow/interpolator.rb +34 -0
- data/lib/roast/workflow/iteration_executor.rb +85 -0
- data/lib/roast/workflow/llm_boolean_coercer.rb +55 -0
- data/lib/roast/workflow/output_handler.rb +35 -0
- data/lib/roast/workflow/output_manager.rb +77 -0
- data/lib/roast/workflow/parallel_executor.rb +49 -0
- data/lib/roast/workflow/repeat_step.rb +75 -0
- data/lib/roast/workflow/replay_handler.rb +123 -0
- data/lib/roast/workflow/resource_resolver.rb +77 -0
- data/lib/roast/workflow/session_manager.rb +6 -2
- data/lib/roast/workflow/state_manager.rb +97 -0
- data/lib/roast/workflow/step_executor_coordinator.rb +205 -0
- data/lib/roast/workflow/step_executor_factory.rb +47 -0
- data/lib/roast/workflow/step_executor_registry.rb +79 -0
- data/lib/roast/workflow/step_executors/base_step_executor.rb +23 -0
- data/lib/roast/workflow/step_executors/hash_step_executor.rb +43 -0
- data/lib/roast/workflow/step_executors/parallel_step_executor.rb +54 -0
- data/lib/roast/workflow/step_executors/string_step_executor.rb +29 -0
- data/lib/roast/workflow/step_finder.rb +97 -0
- data/lib/roast/workflow/step_loader.rb +154 -0
- data/lib/roast/workflow/step_orchestrator.rb +45 -0
- data/lib/roast/workflow/step_runner.rb +23 -0
- data/lib/roast/workflow/step_type_resolver.rb +117 -0
- data/lib/roast/workflow/workflow_context.rb +60 -0
- data/lib/roast/workflow/workflow_executor.rb +90 -209
- data/lib/roast/workflow/workflow_initializer.rb +112 -0
- data/lib/roast/workflow/workflow_runner.rb +87 -0
- data/lib/roast/workflow.rb +3 -0
- data/lib/roast.rb +96 -3
- data/roast.gemspec +2 -1
- data/schema/workflow.json +85 -0
- metadata +97 -4
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "roast/helpers/logger"
|
4
|
+
|
5
|
+
module Roast
|
6
|
+
module Tools
|
7
|
+
module AskUser
|
8
|
+
extend self
|
9
|
+
|
10
|
+
class << self
|
11
|
+
# Add this method to be included in other classes
|
12
|
+
def included(base)
|
13
|
+
base.class_eval do
|
14
|
+
function(
|
15
|
+
:ask_user,
|
16
|
+
"Ask the user for input with a specific prompt. Returns the user's response.",
|
17
|
+
prompt: { type: "string", description: "The prompt to show the user" },
|
18
|
+
) do |params|
|
19
|
+
Roast::Tools::AskUser.call(params[:prompt])
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def call(prompt)
|
26
|
+
Roast::Helpers::Logger.info("💬 Asking user: #{prompt}\n")
|
27
|
+
|
28
|
+
response = ::CLI::UI::Prompt.ask(prompt)
|
29
|
+
|
30
|
+
Roast::Helpers::Logger.info("User responded: #{response}\n")
|
31
|
+
response
|
32
|
+
rescue StandardError => e
|
33
|
+
"Error getting user input: #{e.message}".tap do |error_message|
|
34
|
+
Roast::Helpers::Logger.error(error_message + "\n")
|
35
|
+
Roast::Helpers::Logger.debug(e.backtrace.join("\n") + "\n") if ENV["DEBUG"]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/roast/tools/cmd.rb
CHANGED
@@ -28,7 +28,7 @@ module Roast
|
|
28
28
|
Roast::Helpers::Logger.info("🔧 Running command: #{command}\n")
|
29
29
|
|
30
30
|
# Validate the command starts with one of the allowed prefixes
|
31
|
-
allowed_prefixes = ["pwd", "find", "ls", "rake", "ruby", "dev"]
|
31
|
+
allowed_prefixes = ["pwd", "find", "ls", "rake", "ruby", "dev", "mkdir"]
|
32
32
|
command_prefix = command.split(" ").first
|
33
33
|
|
34
34
|
err = "Error: Command not allowed. Only commands starting with #{allowed_prefixes.join(", ")} are permitted."
|
@@ -26,7 +26,7 @@ module Roast
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def call(glob_pattern, path = ".")
|
29
|
-
Roast::Helpers::Logger.info("🔍 Searching for: '#{glob_pattern}' in '#{path}'\n")
|
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
32
|
return read_contents(results.first) if results.size == 1
|
data/lib/roast/tools.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require "active_support/cache"
|
4
4
|
require "English"
|
5
|
+
require "fileutils"
|
5
6
|
|
6
7
|
require "roast/tools/grep"
|
7
8
|
require "roast/tools/read_file"
|
@@ -10,12 +11,21 @@ require "roast/tools/write_file"
|
|
10
11
|
require "roast/tools/update_files"
|
11
12
|
require "roast/tools/cmd"
|
12
13
|
require "roast/tools/coding_agent"
|
14
|
+
require "roast/tools/ask_user"
|
13
15
|
|
14
16
|
module Roast
|
15
17
|
module Tools
|
16
18
|
extend self
|
17
19
|
|
18
|
-
|
20
|
+
# Initialize cache and ensure .gitignore exists
|
21
|
+
cache_dir = File.join(Dir.pwd, ".roast", "cache")
|
22
|
+
FileUtils.mkdir_p(cache_dir) unless File.directory?(cache_dir)
|
23
|
+
|
24
|
+
# Add .gitignore to cache directory
|
25
|
+
gitignore_path = File.join(cache_dir, ".gitignore")
|
26
|
+
File.write(gitignore_path, "*") unless File.exist?(gitignore_path)
|
27
|
+
|
28
|
+
CACHE = ActiveSupport::Cache::FileStore.new(cache_dir)
|
19
29
|
|
20
30
|
def file_to_prompt(file)
|
21
31
|
<<~PROMPT
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module ValueObjects
|
5
|
+
# Value object representing an API token with validation
|
6
|
+
class ApiToken
|
7
|
+
class InvalidTokenError < 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?(ApiToken)
|
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 tokens, just not empty strings
|
44
|
+
|
45
|
+
raise InvalidTokenError, "API token cannot be an empty string" if @value.strip.empty?
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module ValueObjects
|
5
|
+
# Value object representing a step name, which can be either a plain text prompt
|
6
|
+
# or a reference to a step file
|
7
|
+
class StepName
|
8
|
+
attr_reader :value
|
9
|
+
|
10
|
+
def initialize(name)
|
11
|
+
@value = name.to_s.strip
|
12
|
+
freeze
|
13
|
+
end
|
14
|
+
|
15
|
+
def plain_text?
|
16
|
+
@value.include?(" ")
|
17
|
+
end
|
18
|
+
|
19
|
+
def file_reference?
|
20
|
+
!plain_text?
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_s
|
24
|
+
@value
|
25
|
+
end
|
26
|
+
|
27
|
+
def ==(other)
|
28
|
+
return false unless other.is_a?(StepName)
|
29
|
+
|
30
|
+
value == other.value
|
31
|
+
end
|
32
|
+
alias_method :eql?, :==
|
33
|
+
|
34
|
+
def hash
|
35
|
+
[self.class, @value].hash
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pathname"
|
4
|
+
|
5
|
+
module Roast
|
6
|
+
module ValueObjects
|
7
|
+
# Value object representing a workflow file path with validation and resolution
|
8
|
+
class WorkflowPath
|
9
|
+
class InvalidPathError < StandardError; end
|
10
|
+
|
11
|
+
attr_reader :value
|
12
|
+
|
13
|
+
def initialize(path)
|
14
|
+
@value = normalize_path(path)
|
15
|
+
@pathname = Pathname.new(@value)
|
16
|
+
validate!
|
17
|
+
freeze
|
18
|
+
end
|
19
|
+
|
20
|
+
def exist?
|
21
|
+
pathname.exist?
|
22
|
+
end
|
23
|
+
|
24
|
+
def absolute?
|
25
|
+
pathname.absolute?
|
26
|
+
end
|
27
|
+
|
28
|
+
def relative?
|
29
|
+
pathname.relative?
|
30
|
+
end
|
31
|
+
|
32
|
+
def dirname
|
33
|
+
pathname.dirname.to_s
|
34
|
+
end
|
35
|
+
|
36
|
+
def basename
|
37
|
+
pathname.basename.to_s
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_s
|
41
|
+
@value
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_path
|
45
|
+
@value
|
46
|
+
end
|
47
|
+
|
48
|
+
def ==(other)
|
49
|
+
return false unless other.is_a?(WorkflowPath)
|
50
|
+
|
51
|
+
value == other.value
|
52
|
+
end
|
53
|
+
alias_method :eql?, :==
|
54
|
+
|
55
|
+
def hash
|
56
|
+
[self.class, @value].hash
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
attr_reader :pathname
|
62
|
+
|
63
|
+
def normalize_path(path)
|
64
|
+
path.to_s.strip
|
65
|
+
end
|
66
|
+
|
67
|
+
def validate!
|
68
|
+
raise InvalidPathError, "Workflow path cannot be empty" if @value.empty?
|
69
|
+
raise InvalidPathError, "Workflow path must have .yml or .yaml extension" unless valid_extension?
|
70
|
+
end
|
71
|
+
|
72
|
+
def valid_extension?
|
73
|
+
@value.end_with?(".yml") || @value.end_with?(".yaml")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
data/lib/roast/version.rb
CHANGED
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "roast/factories/api_provider_factory"
|
4
|
+
require "roast/workflow/resource_resolver"
|
5
|
+
|
6
|
+
module Roast
|
7
|
+
module Workflow
|
8
|
+
# Handles API-related configuration including tokens and providers
|
9
|
+
class ApiConfiguration
|
10
|
+
attr_reader :api_token, :api_provider
|
11
|
+
|
12
|
+
def initialize(config_hash)
|
13
|
+
@config_hash = config_hash
|
14
|
+
process_api_configuration
|
15
|
+
end
|
16
|
+
|
17
|
+
# Check if using OpenRouter
|
18
|
+
# @return [Boolean] true if using OpenRouter
|
19
|
+
def openrouter?
|
20
|
+
Roast::Factories::ApiProviderFactory.openrouter?(@api_provider)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Check if using OpenAI
|
24
|
+
# @return [Boolean] true if using OpenAI
|
25
|
+
def openai?
|
26
|
+
Roast::Factories::ApiProviderFactory.openai?(@api_provider)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Get the effective API token including environment variables
|
30
|
+
# @return [String, nil] The API token
|
31
|
+
def effective_token
|
32
|
+
@api_token || environment_token
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def process_api_configuration
|
38
|
+
extract_api_token
|
39
|
+
extract_api_provider
|
40
|
+
end
|
41
|
+
|
42
|
+
def extract_api_token
|
43
|
+
if @config_hash["api_token"]
|
44
|
+
@api_token = ResourceResolver.process_shell_command(@config_hash["api_token"])
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def extract_api_provider
|
49
|
+
@api_provider = Roast::Factories::ApiProviderFactory.from_config(@config_hash)
|
50
|
+
end
|
51
|
+
|
52
|
+
def environment_token
|
53
|
+
if openai?
|
54
|
+
ENV["OPENAI_API_KEY"]
|
55
|
+
elsif openrouter?
|
56
|
+
ENV["OPENROUTER_API_KEY"]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "roast/workflow/expression_utils"
|
4
|
+
require "roast/workflow/llm_boolean_coercer"
|
5
|
+
require "roast/workflow/workflow_executor"
|
6
|
+
|
7
|
+
module Roast
|
8
|
+
module Workflow
|
9
|
+
# Base class for iteration steps (RepeatStep and EachStep)
|
10
|
+
class BaseIterationStep < BaseStep
|
11
|
+
include ExpressionUtils
|
12
|
+
|
13
|
+
DEFAULT_MAX_ITERATIONS = 100
|
14
|
+
|
15
|
+
attr_reader :steps
|
16
|
+
|
17
|
+
def initialize(workflow, steps:, **kwargs)
|
18
|
+
super(workflow, **kwargs)
|
19
|
+
@steps = steps
|
20
|
+
# Don't initialize cmd_tool here - we'll do it lazily when needed
|
21
|
+
end
|
22
|
+
|
23
|
+
protected
|
24
|
+
|
25
|
+
# Process various types of inputs and convert to appropriate types for iteration
|
26
|
+
def process_iteration_input(input, context, coerce_to: nil)
|
27
|
+
if input.is_a?(String)
|
28
|
+
if ruby_expression?(input)
|
29
|
+
process_ruby_expression(input, context, coerce_to)
|
30
|
+
elsif bash_command?(input)
|
31
|
+
process_bash_command(input, coerce_to)
|
32
|
+
else
|
33
|
+
process_step_or_prompt(input, context, coerce_to)
|
34
|
+
end
|
35
|
+
else
|
36
|
+
# Non-string inputs are coerced as-is
|
37
|
+
coerce_result(input, coerce_to)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Interpolates {{expression}} in a string with values from the workflow context
|
42
|
+
def interpolate_expression(text, context)
|
43
|
+
return text unless text.is_a?(String) && text.include?("{{") && text.include?("}}")
|
44
|
+
|
45
|
+
# Replace all {{expression}} with their evaluated values
|
46
|
+
text.gsub(/\{\{([^}]+)\}\}/) do |match|
|
47
|
+
expression = extract_expression(match)
|
48
|
+
begin
|
49
|
+
# Evaluate the expression in the workflow's context
|
50
|
+
result = context.instance_eval(expression)
|
51
|
+
result.inspect # Convert to string representation
|
52
|
+
rescue => e
|
53
|
+
warn_interpolation_error(expression, e)
|
54
|
+
match # Return the original match to preserve it in the string
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Execute nested steps
|
60
|
+
def execute_nested_steps(steps, context, executor = nil)
|
61
|
+
executor ||= WorkflowExecutor.new(context, {}, context_path)
|
62
|
+
results = []
|
63
|
+
|
64
|
+
steps.each do |step|
|
65
|
+
result = case step
|
66
|
+
when String
|
67
|
+
executor.execute_step(step)
|
68
|
+
when Hash, Array
|
69
|
+
executor.execute_steps([step])
|
70
|
+
end
|
71
|
+
results << result
|
72
|
+
end
|
73
|
+
|
74
|
+
results
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
# Process a Ruby expression
|
80
|
+
def process_ruby_expression(input, context, coerce_to)
|
81
|
+
expression = extract_expression(input)
|
82
|
+
result = evaluate_ruby_expression(expression, context)
|
83
|
+
coerce_result(result, coerce_to)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Process a Bash command
|
87
|
+
def process_bash_command(input, coerce_to)
|
88
|
+
command = extract_command(input)
|
89
|
+
execute_command(command, coerce_to)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Process a step name or prompt
|
93
|
+
def process_step_or_prompt(input, context, coerce_to)
|
94
|
+
step_result = execute_step_by_name(input, context)
|
95
|
+
coerce_result(step_result, coerce_to)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Execute a Ruby expression in the workflow context
|
99
|
+
def evaluate_ruby_expression(expression, context)
|
100
|
+
context.instance_eval(expression)
|
101
|
+
rescue => e
|
102
|
+
warn_expression_error(expression, e)
|
103
|
+
nil
|
104
|
+
end
|
105
|
+
|
106
|
+
# Execute a bash command and return its result
|
107
|
+
def execute_command(command, coerce_to)
|
108
|
+
# Use the Cmd module to execute the command
|
109
|
+
result = Roast::Tools::Cmd.call(command)
|
110
|
+
|
111
|
+
if coerce_to == :boolean
|
112
|
+
# For boolean coercion, use exit status (assume success unless error message)
|
113
|
+
!result.to_s.start_with?("Error")
|
114
|
+
else
|
115
|
+
# For other uses, return the output
|
116
|
+
result
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Execute a step by name and return its result
|
121
|
+
def execute_step_by_name(step_name, context)
|
122
|
+
# Reuse existing step execution logic
|
123
|
+
executor = WorkflowExecutor.new(context, {}, context_path)
|
124
|
+
executor.execute_step(step_name)
|
125
|
+
end
|
126
|
+
|
127
|
+
# Coerce results to the appropriate type
|
128
|
+
def coerce_result(result, coerce_to)
|
129
|
+
return coerce_to_boolean(result) if coerce_to == :boolean
|
130
|
+
return coerce_to_iterable(result) if coerce_to == :iterable
|
131
|
+
return coerce_to_llm_boolean(result) if coerce_to == :llm_boolean
|
132
|
+
|
133
|
+
# Default - return as is
|
134
|
+
result
|
135
|
+
end
|
136
|
+
|
137
|
+
# Force a value to boolean
|
138
|
+
def coerce_to_boolean(result)
|
139
|
+
!!result
|
140
|
+
end
|
141
|
+
|
142
|
+
# Ensure a value is iterable
|
143
|
+
def coerce_to_iterable(result)
|
144
|
+
return result if result.respond_to?(:each)
|
145
|
+
|
146
|
+
result.to_s.split("\n")
|
147
|
+
end
|
148
|
+
|
149
|
+
# Convert LLM response to boolean
|
150
|
+
def coerce_to_llm_boolean(result)
|
151
|
+
LlmBooleanCoercer.coerce(result)
|
152
|
+
end
|
153
|
+
|
154
|
+
# Log a warning for expression evaluation errors
|
155
|
+
def warn_expression_error(expression, error)
|
156
|
+
$stderr.puts "Warning: Error evaluating expression '#{expression}': #{error.message}"
|
157
|
+
end
|
158
|
+
|
159
|
+
# Log a warning for interpolation errors
|
160
|
+
def warn_interpolation_error(expression, error)
|
161
|
+
$stderr.puts "Warning: Error interpolating {{#{expression}}}: #{error.message}"
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require "erb"
|
4
4
|
require "forwardable"
|
5
|
+
require "roast/workflow/context_path_resolver"
|
5
6
|
|
6
7
|
module Roast
|
7
8
|
module Workflow
|
@@ -15,11 +16,11 @@ module Roast
|
|
15
16
|
def_delegator :workflow, :chat_completion
|
16
17
|
def_delegator :workflow, :transcript
|
17
18
|
|
18
|
-
def initialize(workflow, model: "anthropic:claude-
|
19
|
+
def initialize(workflow, model: "anthropic:claude-opus-4", name: nil, context_path: nil, auto_loop: true)
|
19
20
|
@workflow = workflow
|
20
21
|
@model = model
|
21
22
|
@name = name || self.class.name.underscore.split("/").last
|
22
|
-
@context_path = context_path ||
|
23
|
+
@context_path = context_path || ContextPathResolver.resolve(self.class)
|
23
24
|
@print_response = false
|
24
25
|
@auto_loop = auto_loop
|
25
26
|
@json = false
|
@@ -35,7 +36,7 @@ module Roast
|
|
35
36
|
protected
|
36
37
|
|
37
38
|
def chat_completion(print_response: false, auto_loop: true, json: false, params: {})
|
38
|
-
workflow.chat_completion(openai: model, loop: auto_loop, json:, params:).then do |response|
|
39
|
+
workflow.chat_completion(openai: workflow.openai? && model, loop: auto_loop, model: model, json:, params:).then do |response|
|
39
40
|
case response
|
40
41
|
in Array
|
41
42
|
response.map(&:presence).compact.join("\n")
|
@@ -47,27 +48,6 @@ module Roast
|
|
47
48
|
end
|
48
49
|
end
|
49
50
|
|
50
|
-
# Determine the directory where the actual class is defined, not BaseWorkflow
|
51
|
-
def determine_context_path
|
52
|
-
# Get the actual class's source file
|
53
|
-
klass = self.class
|
54
|
-
|
55
|
-
# Try to get the file path where the class is defined
|
56
|
-
path = if klass.name.include?("::")
|
57
|
-
# For namespaced classes like Roast::Workflow::Grading::Workflow
|
58
|
-
# Convert the class name to a relative path
|
59
|
-
class_path = klass.name.underscore + ".rb"
|
60
|
-
# Look through load path to find the actual file
|
61
|
-
$LOAD_PATH.map { |p| File.join(p, class_path) }.find { |f| File.exist?(f) }
|
62
|
-
else
|
63
|
-
# Fall back to the current file if we can't find it
|
64
|
-
__FILE__
|
65
|
-
end
|
66
|
-
|
67
|
-
# Return directory containing the class definition
|
68
|
-
File.dirname(path || __FILE__)
|
69
|
-
end
|
70
|
-
|
71
51
|
def prompt(text)
|
72
52
|
transcript << { user: text }
|
73
53
|
end
|