roast-ai 0.1.6 → 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 +48 -0
- data/CLAUDE.md +20 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +11 -6
- data/README.md +225 -13
- data/bin/roast +27 -0
- data/docs/INSTRUMENTATION.md +42 -1
- 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/update_files.rb +413 -0
- data/lib/roast/tools.rb +12 -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 +3 -1
- data/schema/workflow.json +85 -0
- metadata +112 -4
@@ -1,55 +1,46 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "
|
4
|
-
require "
|
3
|
+
require "active_support/core_ext/module/delegation"
|
4
|
+
require "roast/workflow/api_configuration"
|
5
|
+
require "roast/workflow/configuration_loader"
|
6
|
+
require "roast/workflow/resource_resolver"
|
7
|
+
require "roast/workflow/step_finder"
|
5
8
|
|
6
9
|
module Roast
|
7
10
|
module Workflow
|
8
11
|
# Encapsulates workflow configuration data and provides structured access
|
9
12
|
# to the configuration settings
|
10
13
|
class Configuration
|
11
|
-
attr_reader :config_hash, :workflow_path, :name, :steps, :tools, :function_configs, :
|
14
|
+
attr_reader :config_hash, :workflow_path, :name, :steps, :tools, :function_configs, :model, :resource
|
12
15
|
attr_accessor :target
|
13
16
|
|
14
|
-
|
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"] || []
|
17
|
+
delegate :api_provider, :openrouter?, :openai?, to: :api_configuration
|
21
18
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
parse_functions
|
27
|
-
|
28
|
-
# Read the target parameter
|
29
|
-
@target = options[:target] || @config_hash["target"]
|
19
|
+
# Delegate api_token to effective_token for backward compatibility
|
20
|
+
def api_token
|
21
|
+
@api_configuration.effective_token
|
22
|
+
end
|
30
23
|
|
31
|
-
|
32
|
-
@
|
24
|
+
def initialize(workflow_path, options = {})
|
25
|
+
@workflow_path = workflow_path
|
33
26
|
|
34
|
-
#
|
35
|
-
|
36
|
-
@resource = if has_target?
|
37
|
-
Roast::Resources.for(@target)
|
38
|
-
else
|
39
|
-
Roast::Resources::NoneResource.new(nil)
|
40
|
-
end
|
41
|
-
end
|
27
|
+
# Load configuration using ConfigurationLoader
|
28
|
+
@config_hash = ConfigurationLoader.load(workflow_path)
|
42
29
|
|
43
|
-
#
|
44
|
-
|
45
|
-
|
46
|
-
|
30
|
+
# Extract basic configuration values
|
31
|
+
@name = ConfigurationLoader.extract_name(@config_hash, workflow_path)
|
32
|
+
@steps = ConfigurationLoader.extract_steps(@config_hash)
|
33
|
+
@tools = ConfigurationLoader.extract_tools(@config_hash)
|
34
|
+
@function_configs = ConfigurationLoader.extract_functions(@config_hash)
|
35
|
+
@model = ConfigurationLoader.extract_model(@config_hash)
|
47
36
|
|
48
|
-
#
|
49
|
-
@
|
37
|
+
# Initialize components
|
38
|
+
@api_configuration = ApiConfiguration.new(@config_hash)
|
39
|
+
@step_finder = StepFinder.new(@steps)
|
50
40
|
|
51
|
-
#
|
52
|
-
@
|
41
|
+
# Process target and resource
|
42
|
+
@target = ConfigurationLoader.extract_target(@config_hash, options)
|
43
|
+
process_resource
|
53
44
|
end
|
54
45
|
|
55
46
|
def context_path
|
@@ -76,62 +67,10 @@ module Roast
|
|
76
67
|
# Handle different call patterns for backward compatibility
|
77
68
|
if steps_array.is_a?(String) && target_step.nil?
|
78
69
|
target_step = steps_array
|
79
|
-
steps_array =
|
80
|
-
elsif steps_array.is_a?(Array) && target_step.is_a?(String)
|
81
|
-
# This is the normal case - steps_array and target_step are provided
|
82
|
-
else
|
83
|
-
# Default to self.steps if just the target_step is provided
|
84
|
-
steps_array = steps
|
70
|
+
steps_array = nil
|
85
71
|
end
|
86
72
|
|
87
|
-
|
88
|
-
steps_array.each_with_index do |step, index|
|
89
|
-
case step
|
90
|
-
when Hash
|
91
|
-
# Could be {name: command} or {name: {substeps}}
|
92
|
-
step_key = step.keys.first
|
93
|
-
return index if step_key == target_step
|
94
|
-
when Array
|
95
|
-
# This is a parallel step container, search inside it
|
96
|
-
found = step.any? do |substep|
|
97
|
-
case substep
|
98
|
-
when Hash
|
99
|
-
substep.keys.first == target_step
|
100
|
-
when String
|
101
|
-
substep == target_step
|
102
|
-
else
|
103
|
-
false
|
104
|
-
end
|
105
|
-
end
|
106
|
-
return index if found
|
107
|
-
when String
|
108
|
-
return index if step == target_step
|
109
|
-
end
|
110
|
-
end
|
111
|
-
|
112
|
-
# Fall back to the original method using extract_step_name
|
113
|
-
steps_array.each_with_index do |step, index|
|
114
|
-
step_name = extract_step_name(step)
|
115
|
-
if step_name.is_a?(Array)
|
116
|
-
# For arrays (parallel steps), check if target is in the array
|
117
|
-
return index if step_name.flatten.include?(target_step)
|
118
|
-
elsif step_name == target_step
|
119
|
-
return index
|
120
|
-
end
|
121
|
-
end
|
122
|
-
|
123
|
-
nil
|
124
|
-
end
|
125
|
-
|
126
|
-
# Returns an array of all tool class names
|
127
|
-
def parse_tools
|
128
|
-
# Only support array format: ["Roast::Tools::Grep", "Roast::Tools::ReadFile"]
|
129
|
-
@tools = @config_hash["tools"] || []
|
130
|
-
end
|
131
|
-
|
132
|
-
# Parse function-specific configurations
|
133
|
-
def parse_functions
|
134
|
-
@function_configs = @config_hash["functions"] || {}
|
73
|
+
@step_finder.find_index(target_step, steps_array)
|
135
74
|
end
|
136
75
|
|
137
76
|
# Get configuration for a specific function
|
@@ -141,77 +80,15 @@ module Roast
|
|
141
80
|
@function_configs[function_name.to_s] || {}
|
142
81
|
end
|
143
82
|
|
144
|
-
def openrouter?
|
145
|
-
@api_provider == :openrouter
|
146
|
-
end
|
147
|
-
|
148
|
-
def openai?
|
149
|
-
@api_provider == :openai
|
150
|
-
end
|
151
|
-
|
152
83
|
private
|
153
84
|
|
154
|
-
|
155
|
-
return :openai unless @config_hash["api_provider"]
|
156
|
-
|
157
|
-
provider = @config_hash["api_provider"].to_s.downcase
|
158
|
-
|
159
|
-
case provider
|
160
|
-
when "openai"
|
161
|
-
:openai
|
162
|
-
when "openrouter"
|
163
|
-
:openrouter
|
164
|
-
else
|
165
|
-
Roast::Helpers::Logger.warn("Unknown API provider '#{provider}', defaulting to OpenAI")
|
166
|
-
:openai
|
167
|
-
end
|
168
|
-
end
|
169
|
-
|
170
|
-
def process_shell_command(command)
|
171
|
-
# If it's a bash command with the $(command) syntax
|
172
|
-
if command =~ /^\$\((.*)\)$/
|
173
|
-
return Open3.capture2e({}, ::Regexp.last_match(1)).first.strip
|
174
|
-
end
|
175
|
-
|
176
|
-
# Legacy % prefix for backward compatibility
|
177
|
-
if command.start_with?("% ")
|
178
|
-
return Open3.capture2e({}, *command.split(" ")[1..-1]).first.strip
|
179
|
-
end
|
180
|
-
|
181
|
-
# Not a shell command, return as is
|
182
|
-
command
|
183
|
-
end
|
184
|
-
|
185
|
-
def process_target(command)
|
186
|
-
# Process shell command first
|
187
|
-
processed = process_shell_command(command)
|
188
|
-
|
189
|
-
# If it's a glob pattern, return the full paths of the files it matches
|
190
|
-
if processed.include?("*")
|
191
|
-
matched_files = Dir.glob(processed)
|
192
|
-
# If no files match, return the pattern itself
|
193
|
-
return processed if matched_files.empty?
|
194
|
-
|
195
|
-
return matched_files.map { |file| File.expand_path(file) }.join("\n")
|
196
|
-
end
|
197
|
-
|
198
|
-
# For tests, if the command was already processed as a shell command and is simple,
|
199
|
-
# don't expand the path to avoid breaking existing tests
|
200
|
-
return processed if command != processed && !processed.include?("/")
|
85
|
+
attr_reader :api_configuration
|
201
86
|
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
case step
|
208
|
-
when String
|
209
|
-
step
|
210
|
-
when Hash
|
211
|
-
step.keys.first
|
212
|
-
when Array
|
213
|
-
# For arrays, we'll need special handling as they contain multiple steps
|
214
|
-
step.map { |s| extract_step_name(s) }
|
87
|
+
def process_resource
|
88
|
+
if defined?(Roast::Resources)
|
89
|
+
@resource = ResourceResolver.resolve(@target, context_path)
|
90
|
+
# Update target with processed value for backward compatibility
|
91
|
+
@target = @resource.value if has_target?
|
215
92
|
end
|
216
93
|
end
|
217
94
|
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
|
5
|
+
module Roast
|
6
|
+
module Workflow
|
7
|
+
# Handles loading and parsing of workflow configuration files
|
8
|
+
class ConfigurationLoader
|
9
|
+
class << self
|
10
|
+
# Load configuration from a YAML file
|
11
|
+
# @param workflow_path [String] Path to the workflow YAML file
|
12
|
+
# @return [Hash] The parsed configuration hash
|
13
|
+
def load(workflow_path)
|
14
|
+
validate_path!(workflow_path)
|
15
|
+
config_hash = YAML.load_file(workflow_path)
|
16
|
+
validate_config!(config_hash)
|
17
|
+
config_hash
|
18
|
+
end
|
19
|
+
|
20
|
+
# Extract the workflow name from config or path
|
21
|
+
# @param config_hash [Hash] The configuration hash
|
22
|
+
# @param workflow_path [String] Path to the workflow file
|
23
|
+
# @return [String] The workflow name
|
24
|
+
def extract_name(config_hash, workflow_path)
|
25
|
+
config_hash["name"] || File.basename(workflow_path, ".yml")
|
26
|
+
end
|
27
|
+
|
28
|
+
# Extract steps from the configuration
|
29
|
+
# @param config_hash [Hash] The configuration hash
|
30
|
+
# @return [Array] The steps array or empty array
|
31
|
+
def extract_steps(config_hash)
|
32
|
+
config_hash["steps"] || []
|
33
|
+
end
|
34
|
+
|
35
|
+
# Extract tools from the configuration
|
36
|
+
# @param config_hash [Hash] The configuration hash
|
37
|
+
# @return [Array] The tools array or empty array
|
38
|
+
def extract_tools(config_hash)
|
39
|
+
config_hash["tools"] || []
|
40
|
+
end
|
41
|
+
|
42
|
+
# Extract function configurations
|
43
|
+
# @param config_hash [Hash] The configuration hash
|
44
|
+
# @return [Hash] The functions configuration or empty hash
|
45
|
+
def extract_functions(config_hash)
|
46
|
+
config_hash["functions"] || {}
|
47
|
+
end
|
48
|
+
|
49
|
+
# Extract model from the configuration
|
50
|
+
# @param config_hash [Hash] The configuration hash
|
51
|
+
# @return [String, nil] The model name if specified
|
52
|
+
def extract_model(config_hash)
|
53
|
+
config_hash["model"]
|
54
|
+
end
|
55
|
+
|
56
|
+
# Extract target from config or options
|
57
|
+
# @param config_hash [Hash] The configuration hash
|
58
|
+
# @param options [Hash] Runtime options
|
59
|
+
# @return [String, nil] The target if specified
|
60
|
+
def extract_target(config_hash, options = {})
|
61
|
+
options[:target] || config_hash["target"]
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def validate_path!(workflow_path)
|
67
|
+
raise ArgumentError, "Workflow path cannot be nil" if workflow_path.nil?
|
68
|
+
raise ArgumentError, "Workflow file not found: #{workflow_path}" unless File.exist?(workflow_path)
|
69
|
+
raise ArgumentError, "Workflow path must be a YAML file" unless workflow_path.end_with?(".yml", ".yaml")
|
70
|
+
end
|
71
|
+
|
72
|
+
def validate_config!(config_hash)
|
73
|
+
raise ArgumentError, "Invalid workflow configuration" unless config_hash.is_a?(Hash)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -1,12 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
require "roast/workflow/configuration"
|
4
|
+
require "roast/workflow/workflow_initializer"
|
5
|
+
require "roast/workflow/workflow_runner"
|
6
6
|
require "active_support"
|
7
7
|
require "active_support/isolated_execution_state"
|
8
8
|
require "active_support/notifications"
|
9
|
-
require "raix"
|
10
9
|
|
11
10
|
module Roast
|
12
11
|
module Workflow
|
@@ -21,10 +20,12 @@ module Roast
|
|
21
20
|
@configuration = Configuration.new(workflow_path, options)
|
22
21
|
@options = options
|
23
22
|
@files = files
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
23
|
+
|
24
|
+
# Initialize workflow dependencies
|
25
|
+
initializer = WorkflowInitializer.new(@configuration)
|
26
|
+
initializer.setup
|
27
|
+
|
28
|
+
@workflow_runner = WorkflowRunner.new(@configuration, @options)
|
28
29
|
end
|
29
30
|
|
30
31
|
def begin!
|
@@ -33,33 +34,18 @@ module Roast
|
|
33
34
|
$stderr.puts "Workflow: #{configuration.workflow_path}"
|
34
35
|
$stderr.puts "Options: #{options}"
|
35
36
|
|
36
|
-
name = configuration.basename
|
37
|
-
context_path = configuration.context_path
|
38
|
-
|
39
37
|
ActiveSupport::Notifications.instrument("roast.workflow.start", {
|
40
38
|
workflow_path: configuration.workflow_path,
|
41
39
|
options: options,
|
42
|
-
name:
|
40
|
+
name: configuration.basename,
|
43
41
|
})
|
44
42
|
|
45
43
|
if files.any?
|
46
|
-
|
47
|
-
files.each do |file|
|
48
|
-
$stderr.puts "Running workflow for file: #{file}"
|
49
|
-
setup_workflow(file.strip, name:, context_path:)
|
50
|
-
parse(configuration.steps)
|
51
|
-
end
|
44
|
+
@workflow_runner.run_for_files(files)
|
52
45
|
elsif configuration.has_target?
|
53
|
-
|
54
|
-
$stderr.puts "Running workflow for file: #{file.strip}"
|
55
|
-
setup_workflow(file.strip, name:, context_path:)
|
56
|
-
parse(configuration.steps)
|
57
|
-
end
|
46
|
+
@workflow_runner.run_for_targets
|
58
47
|
else
|
59
|
-
|
60
|
-
$stderr.puts "Running targetless workflow"
|
61
|
-
setup_workflow(nil, name:, context_path:)
|
62
|
-
parse(configuration.steps)
|
48
|
+
@workflow_runner.run_targetless
|
63
49
|
end
|
64
50
|
ensure
|
65
51
|
execution_time = Time.now - start_time
|
@@ -70,227 +56,6 @@ module Roast
|
|
70
56
|
execution_time: execution_time,
|
71
57
|
})
|
72
58
|
end
|
73
|
-
|
74
|
-
private
|
75
|
-
|
76
|
-
def setup_workflow(file, name:, context_path:)
|
77
|
-
session_name = configuration.name
|
78
|
-
|
79
|
-
@current_workflow = BaseWorkflow.new(
|
80
|
-
file,
|
81
|
-
name: name,
|
82
|
-
context_path: context_path,
|
83
|
-
resource: configuration.resource,
|
84
|
-
session_name: session_name,
|
85
|
-
configuration: configuration,
|
86
|
-
).tap do |workflow|
|
87
|
-
workflow.output_file = options[:output] if options[:output].present?
|
88
|
-
workflow.verbose = options[:verbose] if options[:verbose].present?
|
89
|
-
workflow.concise = options[:concise] if options[:concise].present?
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
93
|
-
def include_tools
|
94
|
-
return unless configuration.tools.present?
|
95
|
-
|
96
|
-
BaseWorkflow.include(Raix::FunctionDispatch)
|
97
|
-
BaseWorkflow.include(Roast::Helpers::FunctionCachingInterceptor) # Add caching support
|
98
|
-
BaseWorkflow.include(*configuration.tools.map(&:constantize))
|
99
|
-
end
|
100
|
-
|
101
|
-
def load_roast_initializers
|
102
|
-
Roast::Initializers.load_all
|
103
|
-
end
|
104
|
-
|
105
|
-
def configure_api_client
|
106
|
-
return unless configuration.api_token
|
107
|
-
|
108
|
-
begin
|
109
|
-
case configuration.api_provider
|
110
|
-
when :openrouter
|
111
|
-
$stderr.puts "Configuring OpenRouter client with token from workflow"
|
112
|
-
require "open_router"
|
113
|
-
|
114
|
-
Raix.configure do |config|
|
115
|
-
config.openrouter_client = OpenRouter::Client.new(api_key: configuration.api_token)
|
116
|
-
end
|
117
|
-
else
|
118
|
-
$stderr.puts "Configuring OpenAI client with token from workflow"
|
119
|
-
require "openai"
|
120
|
-
|
121
|
-
Raix.configure do |config|
|
122
|
-
config.openai_client = OpenAI::Client.new(access_token: configuration.api_token)
|
123
|
-
end
|
124
|
-
end
|
125
|
-
rescue => e
|
126
|
-
Roast::Helpers::Logger.error("Error configuring API client: #{e.message}")
|
127
|
-
# Don't fail the workflow if client can't be configured
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
def load_state_and_update_steps(steps, skip_until, step_name, timestamp)
|
132
|
-
state_repository = FileStateRepository.new
|
133
|
-
state_data = nil
|
134
|
-
|
135
|
-
if timestamp
|
136
|
-
$stderr.puts "Looking for state before '#{step_name}' in session #{timestamp}..."
|
137
|
-
state_data = state_repository.load_state_before_step(current_workflow, step_name, timestamp: timestamp)
|
138
|
-
if state_data
|
139
|
-
$stderr.puts "Successfully loaded state with data from previous step"
|
140
|
-
restore_workflow_state(state_data)
|
141
|
-
else
|
142
|
-
$stderr.puts "Could not find suitable state data from a previous step to '#{step_name}' in session #{timestamp}."
|
143
|
-
$stderr.puts "Will run workflow from '#{step_name}' without prior context."
|
144
|
-
end
|
145
|
-
else
|
146
|
-
$stderr.puts "Looking for state before '#{step_name}' in most recent session..."
|
147
|
-
state_data = state_repository.load_state_before_step(current_workflow, step_name)
|
148
|
-
if state_data
|
149
|
-
$stderr.puts "Successfully loaded state with data from previous step"
|
150
|
-
restore_workflow_state(state_data)
|
151
|
-
else
|
152
|
-
$stderr.puts "Could not find suitable state data from a previous step to '#{step_name}'."
|
153
|
-
$stderr.puts "Will run workflow from '#{step_name}' without prior context."
|
154
|
-
end
|
155
|
-
end
|
156
|
-
|
157
|
-
# Always return steps from the requested index, regardless of state loading success
|
158
|
-
steps[skip_until..-1]
|
159
|
-
end
|
160
|
-
|
161
|
-
# Restore workflow state from loaded state data
|
162
|
-
def restore_workflow_state(state_data)
|
163
|
-
return unless state_data && current_workflow
|
164
|
-
|
165
|
-
# Restore output
|
166
|
-
if state_data[:output] && current_workflow.respond_to?(:output=)
|
167
|
-
# Use the setter which will ensure it's a HashWithIndifferentAccess
|
168
|
-
current_workflow.output = state_data[:output]
|
169
|
-
end
|
170
|
-
|
171
|
-
# Restore transcript if available
|
172
|
-
if state_data[:transcript] && current_workflow.respond_to?(:transcript=)
|
173
|
-
current_workflow.transcript = state_data[:transcript]
|
174
|
-
elsif state_data[:transcript] && current_workflow.respond_to?(:transcript) &&
|
175
|
-
current_workflow.transcript.respond_to?(:clear) &&
|
176
|
-
current_workflow.transcript.respond_to?(:<<)
|
177
|
-
current_workflow.transcript.clear
|
178
|
-
state_data[:transcript].each do |message|
|
179
|
-
current_workflow.transcript << message
|
180
|
-
end
|
181
|
-
end
|
182
|
-
|
183
|
-
# Restore final output if available
|
184
|
-
if state_data[:final_output]
|
185
|
-
# Make sure final_output is always handled as an array
|
186
|
-
final_output = state_data[:final_output]
|
187
|
-
final_output = [final_output] if final_output.is_a?(String)
|
188
|
-
|
189
|
-
if current_workflow.respond_to?(:final_output=)
|
190
|
-
current_workflow.final_output = final_output
|
191
|
-
elsif current_workflow.instance_variable_defined?(:@final_output)
|
192
|
-
current_workflow.instance_variable_set(:@final_output, final_output)
|
193
|
-
end
|
194
|
-
end
|
195
|
-
end
|
196
|
-
|
197
|
-
def parse(steps)
|
198
|
-
return run(steps) if steps.is_a?(String)
|
199
|
-
|
200
|
-
# Handle replay option - skip to the specified step
|
201
|
-
if @options[:replay] && !@replay_processed
|
202
|
-
replay_param = @options[:replay]
|
203
|
-
timestamp = nil
|
204
|
-
step_name = replay_param
|
205
|
-
|
206
|
-
# Check if timestamp is prepended (format: timestamp:step_name)
|
207
|
-
if replay_param.include?(":")
|
208
|
-
timestamp, step_name = replay_param.split(":", 2)
|
209
|
-
|
210
|
-
# Validate timestamp format (YYYYMMDD_HHMMSS_LLL)
|
211
|
-
unless timestamp.match?(/^\d{8}_\d{6}_\d{3}$/)
|
212
|
-
raise ArgumentError, "Invalid timestamp format: #{timestamp}. Expected YYYYMMDD_HHMMSS_LLL"
|
213
|
-
end
|
214
|
-
end
|
215
|
-
|
216
|
-
# Find step index by iterating through the steps
|
217
|
-
skip_until = find_step_index_in_array(steps, step_name)
|
218
|
-
|
219
|
-
if skip_until
|
220
|
-
$stderr.puts "Replaying from step: #{step_name}#{timestamp ? " (session: #{timestamp})" : ""}"
|
221
|
-
current_workflow.session_timestamp = timestamp if timestamp
|
222
|
-
steps = load_state_and_update_steps(steps, skip_until, step_name, timestamp)
|
223
|
-
else
|
224
|
-
$stderr.puts "Step #{step_name} not found in workflow, running from beginning"
|
225
|
-
end
|
226
|
-
@replay_processed = true # Mark that we've processed replay, so we don't do it again in recursive calls
|
227
|
-
end
|
228
|
-
|
229
|
-
# Use the WorkflowExecutor to execute the steps
|
230
|
-
executor = WorkflowExecutor.new(current_workflow, configuration.config_hash, configuration.context_path)
|
231
|
-
executor.execute_steps(steps)
|
232
|
-
|
233
|
-
$stderr.puts "🔥🔥🔥 ROAST COMPLETE! 🔥🔥🔥"
|
234
|
-
|
235
|
-
# Save the final output to the session directory
|
236
|
-
save_final_output(current_workflow)
|
237
|
-
|
238
|
-
# Save results to file if specified
|
239
|
-
if current_workflow.output_file
|
240
|
-
File.write(current_workflow.output_file, current_workflow.final_output)
|
241
|
-
$stdout.puts "Results saved to #{current_workflow.output_file}"
|
242
|
-
else
|
243
|
-
$stdout.puts current_workflow.final_output
|
244
|
-
end
|
245
|
-
end
|
246
|
-
|
247
|
-
# Delegates to WorkflowExecutor
|
248
|
-
def run(name)
|
249
|
-
executor = WorkflowExecutor.new(current_workflow, configuration.config_hash, configuration.context_path)
|
250
|
-
executor.execute_step(name)
|
251
|
-
end
|
252
|
-
|
253
|
-
def find_step_index_in_array(steps_array, step_name)
|
254
|
-
steps_array.each_with_index do |step, index|
|
255
|
-
case step
|
256
|
-
when Hash
|
257
|
-
# Could be {name: command} or {name: {substeps}}
|
258
|
-
step_key = step.keys.first
|
259
|
-
return index if step_key == step_name
|
260
|
-
when Array
|
261
|
-
# This is a parallel step container, search inside it
|
262
|
-
step.each_with_index do |substep, _substep_index|
|
263
|
-
case substep
|
264
|
-
when Hash
|
265
|
-
# Could be {name: command}
|
266
|
-
substep_key = substep.keys.first
|
267
|
-
return index if substep_key == step_name
|
268
|
-
when String
|
269
|
-
return index if substep == step_name
|
270
|
-
end
|
271
|
-
end
|
272
|
-
when String
|
273
|
-
return index if step == step_name
|
274
|
-
end
|
275
|
-
end
|
276
|
-
nil
|
277
|
-
end
|
278
|
-
|
279
|
-
def save_final_output(workflow)
|
280
|
-
return unless workflow.respond_to?(:session_name) && workflow.session_name && workflow.respond_to?(:final_output)
|
281
|
-
|
282
|
-
begin
|
283
|
-
final_output = workflow.final_output.to_s
|
284
|
-
return if final_output.empty?
|
285
|
-
|
286
|
-
state_repository = FileStateRepository.new
|
287
|
-
output_file = state_repository.save_final_output(workflow, final_output)
|
288
|
-
$stderr.puts "Final output saved to: #{output_file}" if output_file
|
289
|
-
rescue => e
|
290
|
-
# Don't fail if saving output fails
|
291
|
-
$stderr.puts "Warning: Failed to save final output to session: #{e.message}"
|
292
|
-
end
|
293
|
-
end
|
294
59
|
end
|
295
60
|
end
|
296
61
|
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Workflow
|
5
|
+
# Determines the context path for workflow and step classes
|
6
|
+
class ContextPathResolver
|
7
|
+
class << self
|
8
|
+
# Determine the directory where the actual class is defined
|
9
|
+
# @param klass [Class] The class to find the context path for
|
10
|
+
# @return [String] The directory path containing the class definition
|
11
|
+
def resolve(klass)
|
12
|
+
# Try to get the file path where the class is defined
|
13
|
+
path = if klass.name&.include?("::")
|
14
|
+
# For namespaced classes like Roast::Workflow::Grading::Workflow
|
15
|
+
# Convert the class name to a relative path
|
16
|
+
class_path = klass.name.underscore + ".rb"
|
17
|
+
# Look through load path to find the actual file
|
18
|
+
$LOAD_PATH.map { |p| File.join(p, class_path) }.find { |f| File.exist?(f) }
|
19
|
+
end
|
20
|
+
|
21
|
+
# Fall back to trying to get the source location
|
22
|
+
if path.nil? && klass.instance_methods(false).any?
|
23
|
+
# Try to get source location from any instance method
|
24
|
+
method = klass.instance_methods(false).first
|
25
|
+
source_location = klass.instance_method(method).source_location
|
26
|
+
path = source_location&.first
|
27
|
+
end
|
28
|
+
|
29
|
+
# Return directory containing the class definition
|
30
|
+
# or the current directory if we can't find it
|
31
|
+
File.dirname(path || Dir.pwd)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Resolve context path for an instance
|
35
|
+
# @param instance [Object] The instance to find the context path for
|
36
|
+
# @return [String] The directory path containing the class definition
|
37
|
+
def resolve_for_instance(instance)
|
38
|
+
resolve(instance.class)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|