roast-ai 0.1.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 +7 -0
- data/.claude/settings.json +12 -0
- data/.github/workflows/ci.yaml +29 -0
- data/.github/workflows/cla.yml +22 -0
- data/.gitignore +13 -0
- data/.rspec +1 -0
- data/.rubocop.yml +12 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +0 -0
- data/CLAUDE.md +31 -0
- data/CODE_OF_CONDUCT.md +133 -0
- data/CONTRIBUTING.md +35 -0
- data/Gemfile +19 -0
- data/Gemfile.lock +194 -0
- data/LICENSE.md +21 -0
- data/README.md +27 -0
- data/Rakefile +24 -0
- data/bin/console +11 -0
- data/examples/grading/analyze_coverage/prompt.md +52 -0
- data/examples/grading/calculate_final_grade.rb +67 -0
- data/examples/grading/format_result.rb +48 -0
- data/examples/grading/generate_grades/prompt.md +105 -0
- data/examples/grading/generate_recommendations/output.txt +17 -0
- data/examples/grading/generate_recommendations/prompt.md +60 -0
- data/examples/grading/run_coverage.rb +47 -0
- data/examples/grading/verify_mocks_and_stubs/prompt.md +12 -0
- data/examples/grading/verify_test_helpers/prompt.md +53 -0
- data/examples/grading/workflow.md +8 -0
- data/examples/grading/workflow.rb.md +6 -0
- data/examples/grading/workflow.ts+tsx.md +6 -0
- data/examples/grading/workflow.yml +46 -0
- data/exe/roast +17 -0
- data/lib/roast/helpers/function_caching_interceptor.rb +27 -0
- data/lib/roast/helpers/logger.rb +104 -0
- data/lib/roast/helpers/minitest_coverage_runner.rb +244 -0
- data/lib/roast/helpers/path_resolver.rb +148 -0
- data/lib/roast/helpers/prompt_loader.rb +97 -0
- data/lib/roast/helpers.rb +12 -0
- data/lib/roast/tools/cmd.rb +72 -0
- data/lib/roast/tools/grep.rb +43 -0
- data/lib/roast/tools/read_file.rb +49 -0
- data/lib/roast/tools/search_file.rb +51 -0
- data/lib/roast/tools/write_file.rb +60 -0
- data/lib/roast/tools.rb +50 -0
- data/lib/roast/version.rb +5 -0
- data/lib/roast/workflow/base_step.rb +94 -0
- data/lib/roast/workflow/base_workflow.rb +79 -0
- data/lib/roast/workflow/configuration.rb +117 -0
- data/lib/roast/workflow/configuration_parser.rb +92 -0
- data/lib/roast/workflow/validator.rb +37 -0
- data/lib/roast/workflow/workflow_executor.rb +119 -0
- data/lib/roast/workflow.rb +13 -0
- data/lib/roast.rb +40 -0
- data/roast.gemspec +44 -0
- data/schema/workflow.json +92 -0
- data/shipit.rubygems.yml +0 -0
- metadata +171 -0
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "roast/helpers/logger"
|
4
|
+
|
5
|
+
module Roast
|
6
|
+
module Tools
|
7
|
+
module SearchFile
|
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
|
+
:search_for_file,
|
16
|
+
'Search for a file in the project using `find . -type f -path "*#{@file_name}*"` in the project root',
|
17
|
+
name: { type: "string", description: "filename with as much of the path as you can deduce" },
|
18
|
+
) do |params|
|
19
|
+
Roast::Tools::SearchFile.call(params[:name]).tap do |result|
|
20
|
+
Roast::Helpers::Logger.debug(result) if ENV["DEBUG"]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def call(filename)
|
28
|
+
Roast::Helpers::Logger.info("🔍 Searching for file: #{filename}\n")
|
29
|
+
search_for(filename).then do |results|
|
30
|
+
return "No results found for #{filename}" if results.empty?
|
31
|
+
return Roast::Tools::ReadFile.call(results.first) if results.size == 1
|
32
|
+
|
33
|
+
results.inspect # purposely give the AI list of actual paths so that it can read without searching first
|
34
|
+
end
|
35
|
+
rescue StandardError => e
|
36
|
+
"Error searching for file: #{e.message}".tap do |error_message|
|
37
|
+
Roast::Helpers::Logger.error(error_message + "\n")
|
38
|
+
Roast::Helpers::Logger.debug(e.backtrace.join("\n") + "\n") if ENV["DEBUG"]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def search_for(filename)
|
43
|
+
# Execute find command and get the output using -path to match against full paths
|
44
|
+
result = %x(find . -type f -path "*#{filename}*").strip
|
45
|
+
|
46
|
+
# Split by newlines and get the first result
|
47
|
+
result.split("\n").map(&:strip).reject(&:empty?).map { |path| path.sub(%r{^\./}, "") }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fileutils"
|
4
|
+
require "roast/helpers/logger"
|
5
|
+
|
6
|
+
module Roast
|
7
|
+
module Tools
|
8
|
+
module WriteFile
|
9
|
+
extend self
|
10
|
+
|
11
|
+
class << self
|
12
|
+
# Add this method to be included in other classes
|
13
|
+
def included(base)
|
14
|
+
base.class_eval do
|
15
|
+
function(
|
16
|
+
:write_file,
|
17
|
+
"Write content to a file. Creates the file if it does not exist, or overwrites it if it does.",
|
18
|
+
path: {
|
19
|
+
type: "string",
|
20
|
+
description: "The path to the file to write, relative to the current working directory: #{Dir.pwd}",
|
21
|
+
},
|
22
|
+
content: { type: "string", description: "The content to write to the file" },
|
23
|
+
) do |params|
|
24
|
+
Roast::Tools::WriteFile.call(params[:path], params[:content]).tap do |_result|
|
25
|
+
Roast::Helpers::Logger.info(params[:content])
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def call(path, content)
|
33
|
+
if path.start_with?("test/")
|
34
|
+
|
35
|
+
Roast::Helpers::Logger.info("📝 Writing to file: #{path}\n")
|
36
|
+
|
37
|
+
# Ensure the directory exists
|
38
|
+
dir = File.dirname(path)
|
39
|
+
FileUtils.mkdir_p(dir) unless File.directory?(dir)
|
40
|
+
|
41
|
+
# Write the content to the file
|
42
|
+
# Check if path is absolute or relative
|
43
|
+
absolute_path = path.start_with?("/") ? path : File.join(Dir.pwd, path)
|
44
|
+
|
45
|
+
File.write(absolute_path, content)
|
46
|
+
|
47
|
+
"Successfully wrote #{content.lines.count} lines to #{path}"
|
48
|
+
else
|
49
|
+
Roast::Helpers::Logger.error("😳 Path must start with 'test/' to use the write_file tool\n")
|
50
|
+
"Error: Path must start with 'test/' to use the write_file tool, try again."
|
51
|
+
end
|
52
|
+
rescue StandardError => e
|
53
|
+
"Error writing file: #{e.message}".tap do |error_message|
|
54
|
+
Roast::Helpers::Logger.error(error_message + "\n")
|
55
|
+
Roast::Helpers::Logger.debug(e.backtrace.join("\n") + "\n") if ENV["DEBUG"]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/lib/roast/tools.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/cache"
|
4
|
+
require "English"
|
5
|
+
|
6
|
+
require "roast/tools/grep"
|
7
|
+
require "roast/tools/read_file"
|
8
|
+
require "roast/tools/search_file"
|
9
|
+
require "roast/tools/write_file"
|
10
|
+
require "roast/tools/cmd"
|
11
|
+
|
12
|
+
module Roast
|
13
|
+
module Tools
|
14
|
+
extend self
|
15
|
+
|
16
|
+
CACHE = ActiveSupport::Cache::FileStore.new(File.join(Dir.pwd, ".roast", "cache"))
|
17
|
+
|
18
|
+
def file_to_prompt(file)
|
19
|
+
<<~PROMPT
|
20
|
+
# #{file}
|
21
|
+
|
22
|
+
#{File.read(file)}
|
23
|
+
PROMPT
|
24
|
+
rescue StandardError => e
|
25
|
+
Roast::Helpers::Logger.error("In current directory: #{Dir.pwd}\n")
|
26
|
+
Roast::Helpers::Logger.error("Error reading file #{file}\n")
|
27
|
+
|
28
|
+
raise e # unable to continue without required file
|
29
|
+
end
|
30
|
+
|
31
|
+
def setup_interrupt_handler(object_to_inspect)
|
32
|
+
Signal.trap("INT") do
|
33
|
+
puts "\n\nCaught CTRL-C! Printing before exiting:\n"
|
34
|
+
puts JSON.pretty_generate(object_to_inspect)
|
35
|
+
exit(1)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def setup_exit_handler(context)
|
40
|
+
# Hook that runs on any exit (including crashes and unhandled exceptions)
|
41
|
+
at_exit do
|
42
|
+
if $ERROR_INFO && !$ERROR_INFO.is_a?(SystemExit) # If exiting due to unhandled exception
|
43
|
+
puts "\n\nExiting due to error: #{$ERROR_INFO.class}: #{$ERROR_INFO.message}\n"
|
44
|
+
# Temporary disable the debugger to fix directory issues
|
45
|
+
# context.instance_eval { binding.irb }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "erb"
|
4
|
+
require "forwardable"
|
5
|
+
|
6
|
+
module Roast
|
7
|
+
module Workflow
|
8
|
+
class BaseStep
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
attr_accessor :model, :print_response, :loop, :json, :params
|
12
|
+
attr_reader :workflow, :name, :context_path
|
13
|
+
|
14
|
+
def_delegator :workflow, :append_to_final_output
|
15
|
+
def_delegator :workflow, :chat_completion
|
16
|
+
def_delegator :workflow, :transcript
|
17
|
+
|
18
|
+
def initialize(workflow, model: "anthropic:claude-3-7-sonnet", name: nil, context_path: nil)
|
19
|
+
@workflow = workflow
|
20
|
+
@model = model
|
21
|
+
@name = name || self.class.name.underscore.split("/").last
|
22
|
+
@context_path = context_path || determine_context_path
|
23
|
+
@print_response = false
|
24
|
+
@loop = true
|
25
|
+
@json = false
|
26
|
+
@params = {}
|
27
|
+
end
|
28
|
+
|
29
|
+
def call
|
30
|
+
prompt(read_sidecar_prompt)
|
31
|
+
chat_completion(print_response:, loop:, json:, params:)
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
def chat_completion(print_response: false, loop: true, json: false, params: {})
|
37
|
+
workflow.chat_completion(openai: model, loop:, json:, params:).tap do |response|
|
38
|
+
append_to_final_output(response) if print_response
|
39
|
+
end.then do |response|
|
40
|
+
case response
|
41
|
+
in Array
|
42
|
+
response.map(&:presence).compact.join("\n")
|
43
|
+
else
|
44
|
+
response
|
45
|
+
end
|
46
|
+
end.tap do |response|
|
47
|
+
process_sidecar_output(response)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Determine the directory where the actual class is defined, not BaseWorkflow
|
52
|
+
def determine_context_path
|
53
|
+
# Get the actual class's source file
|
54
|
+
klass = self.class
|
55
|
+
|
56
|
+
# Try to get the file path where the class is defined
|
57
|
+
path = if klass.name.include?("::")
|
58
|
+
# For namespaced classes like Roast::Workflow::Grading::Workflow
|
59
|
+
# Convert the class name to a relative path
|
60
|
+
class_path = klass.name.underscore + ".rb"
|
61
|
+
# Look through load path to find the actual file
|
62
|
+
$LOAD_PATH.map { |p| File.join(p, class_path) }.find { |f| File.exist?(f) }
|
63
|
+
else
|
64
|
+
# Fall back to the current file if we can't find it
|
65
|
+
__FILE__
|
66
|
+
end
|
67
|
+
|
68
|
+
# Return directory containing the class definition
|
69
|
+
File.dirname(path || __FILE__)
|
70
|
+
end
|
71
|
+
|
72
|
+
def prompt(text)
|
73
|
+
transcript << { user: text }
|
74
|
+
end
|
75
|
+
|
76
|
+
def read_sidecar_prompt
|
77
|
+
Roast::Helpers::PromptLoader.load_prompt(self, workflow.file)
|
78
|
+
end
|
79
|
+
|
80
|
+
def process_sidecar_output(response)
|
81
|
+
# look for a file named output.txt.erb in the context path
|
82
|
+
# if found, render it with the response
|
83
|
+
# if not found, just return the response
|
84
|
+
# TODO: this can be a lot more sophisticated
|
85
|
+
# incorporating different file types, etc.
|
86
|
+
output_path = File.join(context_path, "output.txt")
|
87
|
+
if File.exist?(output_path)
|
88
|
+
# TODO: use the workflow binding or the step?
|
89
|
+
append_to_final_output(ERB.new(File.read(output_path), trim_mode: "-").result(binding))
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "raix/chat_completion"
|
4
|
+
require "raix/function_dispatch"
|
5
|
+
|
6
|
+
module Roast
|
7
|
+
module Workflow
|
8
|
+
class BaseWorkflow
|
9
|
+
include Raix::ChatCompletion
|
10
|
+
|
11
|
+
attr_accessor :file,
|
12
|
+
:concise,
|
13
|
+
:output_file,
|
14
|
+
:subject_file,
|
15
|
+
:verbose,
|
16
|
+
:name,
|
17
|
+
:context_path,
|
18
|
+
:output
|
19
|
+
|
20
|
+
def initialize(file, subject_file = nil, name: nil, context_path: nil)
|
21
|
+
@file = file
|
22
|
+
@subject_file = subject_file
|
23
|
+
@name = name || self.class.name.underscore.split("/").last
|
24
|
+
@context_path = context_path || determine_context_path
|
25
|
+
@final_output = []
|
26
|
+
@output = {}
|
27
|
+
transcript << { system: read_sidecar_prompt }
|
28
|
+
unless subject_file.blank?
|
29
|
+
transcript << { user: read_subject_file }
|
30
|
+
end
|
31
|
+
Roast::Tools.setup_interrupt_handler(transcript)
|
32
|
+
Roast::Tools.setup_exit_handler(self)
|
33
|
+
end
|
34
|
+
|
35
|
+
def append_to_final_output(message)
|
36
|
+
@final_output << message
|
37
|
+
end
|
38
|
+
|
39
|
+
def final_output
|
40
|
+
@final_output.join("\n")
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
# Determine the directory where the actual class is defined, not BaseWorkflow
|
46
|
+
def determine_context_path
|
47
|
+
# Get the actual class's source file
|
48
|
+
klass = self.class
|
49
|
+
|
50
|
+
# Try to get the file path where the class is defined
|
51
|
+
path = if klass.name.include?("::")
|
52
|
+
# For namespaced classes like Roast::Workflow::Grading::Workflow
|
53
|
+
# Convert the class name to a relative path
|
54
|
+
class_path = klass.name.underscore + ".rb"
|
55
|
+
# Look through load path to find the actual file
|
56
|
+
$LOAD_PATH.map { |p| File.join(p, class_path) }.find { |f| File.exist?(f) }
|
57
|
+
else
|
58
|
+
# Fall back to the current file if we can't find it
|
59
|
+
__FILE__
|
60
|
+
end
|
61
|
+
|
62
|
+
# Return directory containing the class definition
|
63
|
+
File.dirname(path || __FILE__)
|
64
|
+
end
|
65
|
+
|
66
|
+
def read_sidecar_prompt
|
67
|
+
Roast::Helpers::PromptLoader.load_prompt(self, file)
|
68
|
+
end
|
69
|
+
|
70
|
+
def read_subject_file
|
71
|
+
[
|
72
|
+
"# SUT (Subject Under Test)",
|
73
|
+
"# #{subject_file}",
|
74
|
+
File.read(subject_file),
|
75
|
+
].join("\n")
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "open3"
|
4
|
+
require "yaml"
|
5
|
+
|
6
|
+
module Roast
|
7
|
+
module Workflow
|
8
|
+
# Encapsulates workflow configuration data and provides structured access
|
9
|
+
# to the configuration settings
|
10
|
+
class Configuration
|
11
|
+
attr_reader :config_hash, :workflow_path, :name, :steps, :tools, :function_configs
|
12
|
+
attr_accessor :target
|
13
|
+
|
14
|
+
def initialize(workflow_path, options = {})
|
15
|
+
@workflow_path = workflow_path
|
16
|
+
@config_hash = YAML.load_file(workflow_path)
|
17
|
+
|
18
|
+
# Extract key configuration values
|
19
|
+
@name = @config_hash["name"] || File.basename(workflow_path, ".yml")
|
20
|
+
@steps = @config_hash["steps"] || []
|
21
|
+
|
22
|
+
# Process tools configuration
|
23
|
+
parse_tools
|
24
|
+
|
25
|
+
# Process function-specific configurations
|
26
|
+
parse_functions
|
27
|
+
|
28
|
+
# Read the target parameter
|
29
|
+
@target = options[:target] || @config_hash["target"]
|
30
|
+
|
31
|
+
# Process the target command if it's a shell command
|
32
|
+
@target = process_target(@target) if has_target?
|
33
|
+
end
|
34
|
+
|
35
|
+
def context_path
|
36
|
+
@context_path ||= File.dirname(workflow_path)
|
37
|
+
end
|
38
|
+
|
39
|
+
def basename
|
40
|
+
@basename ||= File.basename(workflow_path, ".yml")
|
41
|
+
end
|
42
|
+
|
43
|
+
def has_target?
|
44
|
+
!target.nil? && !target.empty?
|
45
|
+
end
|
46
|
+
|
47
|
+
def get_step_config(step_name)
|
48
|
+
@config_hash[step_name] || {}
|
49
|
+
end
|
50
|
+
|
51
|
+
def find_step_index(steps, target_step)
|
52
|
+
steps.each_with_index do |step, index|
|
53
|
+
step_name = extract_step_name(step)
|
54
|
+
if step_name.is_a?(Array)
|
55
|
+
# For arrays (parallel steps), check if target is in the array
|
56
|
+
return index if step_name.flatten.include?(target_step)
|
57
|
+
elsif step_name == target_step
|
58
|
+
return index
|
59
|
+
end
|
60
|
+
end
|
61
|
+
nil
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns an array of all tool class names
|
65
|
+
def parse_tools
|
66
|
+
# Only support array format: ["Roast::Tools::Grep", "Roast::Tools::ReadFile"]
|
67
|
+
@tools = @config_hash["tools"] || []
|
68
|
+
end
|
69
|
+
|
70
|
+
# Parse function-specific configurations
|
71
|
+
def parse_functions
|
72
|
+
@function_configs = @config_hash["functions"] || {}
|
73
|
+
end
|
74
|
+
|
75
|
+
# Get configuration for a specific function
|
76
|
+
# @param function_name [String, Symbol] The name of the function (e.g., 'grep', 'search_file')
|
77
|
+
# @return [Hash] The configuration for the function or empty hash if not found
|
78
|
+
def function_config(function_name)
|
79
|
+
@function_configs[function_name.to_s] || {}
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def process_target(command)
|
85
|
+
# If it's a bash command with the new $(command) syntax
|
86
|
+
if command =~ /^\$\((.*)\)$/
|
87
|
+
return Open3.capture2e({}, ::Regexp.last_match(1)).first.strip
|
88
|
+
end
|
89
|
+
|
90
|
+
# Legacy % prefix for backward compatibility
|
91
|
+
if command.start_with?("% ")
|
92
|
+
return Open3.capture2e({}, *command.split(" ")[1..-1]).first.strip
|
93
|
+
end
|
94
|
+
|
95
|
+
# If it's a glob pattern, return the full paths of the files it matches
|
96
|
+
if command.include?("*")
|
97
|
+
return Dir.glob(command).map { |file| File.expand_path(file) }.join("\n")
|
98
|
+
end
|
99
|
+
|
100
|
+
# assumed to be a direct file path(s)
|
101
|
+
File.expand_path(command)
|
102
|
+
end
|
103
|
+
|
104
|
+
def extract_step_name(step)
|
105
|
+
case step
|
106
|
+
when String
|
107
|
+
step
|
108
|
+
when Hash
|
109
|
+
step.keys.first
|
110
|
+
when Array
|
111
|
+
# For arrays, we'll need special handling as they contain multiple steps
|
112
|
+
step.map { |s| extract_step_name(s) }
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "workflow_executor"
|
4
|
+
require_relative "configuration"
|
5
|
+
require_relative "../helpers/function_caching_interceptor"
|
6
|
+
|
7
|
+
module Roast
|
8
|
+
module Workflow
|
9
|
+
class ConfigurationParser
|
10
|
+
extend Forwardable
|
11
|
+
|
12
|
+
attr_reader :configuration, :options, :files, :current_workflow
|
13
|
+
|
14
|
+
def_delegator :current_workflow, :output
|
15
|
+
|
16
|
+
def initialize(workflow_path, files = [], options = {})
|
17
|
+
@configuration = Configuration.new(workflow_path, options)
|
18
|
+
@options = options
|
19
|
+
@files = files
|
20
|
+
include_tools
|
21
|
+
end
|
22
|
+
|
23
|
+
def begin!
|
24
|
+
$stderr.puts "Starting workflow..."
|
25
|
+
$stderr.puts "Workflow: #{configuration.workflow_path}"
|
26
|
+
$stderr.puts "Options: #{options}"
|
27
|
+
|
28
|
+
name = configuration.basename
|
29
|
+
context_path = configuration.context_path
|
30
|
+
|
31
|
+
if files.any?
|
32
|
+
$stderr.puts "WARNING: Ignoring target parameter because files were provided: #{configuration.target}" if configuration.has_target?
|
33
|
+
files.each do |file|
|
34
|
+
$stderr.puts "Running workflow for file: #{file}"
|
35
|
+
setup_workflow(file.strip, name:, context_path:)
|
36
|
+
parse(configuration.steps)
|
37
|
+
end
|
38
|
+
elsif configuration.has_target?
|
39
|
+
configuration.target.lines.each do |file|
|
40
|
+
$stderr.puts "Running workflow for file: #{file.strip}"
|
41
|
+
setup_workflow(file.strip, name:, context_path:)
|
42
|
+
parse(configuration.steps)
|
43
|
+
end
|
44
|
+
else
|
45
|
+
$stdout.puts "🚫 ERROR: No files or target provided! 🚫"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def setup_workflow(file, name:, context_path:)
|
52
|
+
@current_workflow = BaseWorkflow.new(file, name:, context_path:).tap do |workflow|
|
53
|
+
workflow.output_file = options[:output] if options[:output].present?
|
54
|
+
workflow.subject_file = options[:subject] if options[:subject].present?
|
55
|
+
workflow.verbose = options[:verbose] if options[:verbose].present?
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def include_tools
|
60
|
+
return unless configuration.tools.present?
|
61
|
+
|
62
|
+
BaseWorkflow.include(Raix::FunctionDispatch)
|
63
|
+
BaseWorkflow.include(Roast::Helpers::FunctionCachingInterceptor) # Add caching support
|
64
|
+
BaseWorkflow.include(*configuration.tools.map(&:constantize))
|
65
|
+
end
|
66
|
+
|
67
|
+
def parse(steps)
|
68
|
+
return run(steps) if steps.is_a?(String)
|
69
|
+
|
70
|
+
# Use the WorkflowExecutor to execute the steps
|
71
|
+
executor = WorkflowExecutor.new(current_workflow, configuration.config_hash, configuration.context_path)
|
72
|
+
executor.execute_steps(steps)
|
73
|
+
|
74
|
+
$stderr.puts "🔥🔥🔥 ROAST COMPLETE! 🔥🔥🔥"
|
75
|
+
|
76
|
+
# Save results to file if specified
|
77
|
+
if current_workflow.output_file
|
78
|
+
File.write(current_workflow.output_file, current_workflow.final_output)
|
79
|
+
$stdout.puts "Results saved to #{current_workflow.output_file}"
|
80
|
+
else
|
81
|
+
$stdout.puts current_workflow.final_output
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Delegates to WorkflowExecutor
|
86
|
+
def run(name)
|
87
|
+
executor = WorkflowExecutor.new(current_workflow, configuration.config_hash, configuration.context_path)
|
88
|
+
executor.execute_step(name)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json-schema"
|
4
|
+
require "json"
|
5
|
+
require "yaml"
|
6
|
+
|
7
|
+
module Roast
|
8
|
+
module Workflow
|
9
|
+
class Validator
|
10
|
+
attr_reader :errors
|
11
|
+
|
12
|
+
def initialize(yaml_content)
|
13
|
+
@yaml_content = yaml_content&.strip || ""
|
14
|
+
@errors = []
|
15
|
+
|
16
|
+
@parsed_yaml = @yaml_content.empty? ? {} : YAML.safe_load(@yaml_content)
|
17
|
+
end
|
18
|
+
|
19
|
+
def valid?
|
20
|
+
return false if @parsed_yaml.empty?
|
21
|
+
|
22
|
+
schema_path = File.join(Roast::ROOT, "schema/workflow.json")
|
23
|
+
schema = JSON.parse(File.read(schema_path))
|
24
|
+
|
25
|
+
begin
|
26
|
+
@errors = JSON::Validator.fully_validate(
|
27
|
+
schema,
|
28
|
+
@parsed_yaml,
|
29
|
+
validate_schema: false,
|
30
|
+
)
|
31
|
+
|
32
|
+
@errors.empty?
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|