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
@@ -1,239 +1,120 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "English"
|
3
4
|
require "active_support"
|
4
5
|
require "active_support/isolated_execution_state"
|
5
6
|
require "active_support/notifications"
|
7
|
+
require "roast/workflow/command_executor"
|
8
|
+
require "roast/workflow/conditional_executor"
|
9
|
+
require "roast/workflow/error_handler"
|
10
|
+
require "roast/workflow/interpolator"
|
11
|
+
require "roast/workflow/iteration_executor"
|
12
|
+
require "roast/workflow/parallel_executor"
|
13
|
+
require "roast/workflow/state_manager"
|
14
|
+
require "roast/workflow/step_executor_factory"
|
15
|
+
require "roast/workflow/step_executor_coordinator"
|
16
|
+
require "roast/workflow/step_loader"
|
17
|
+
require "roast/workflow/step_orchestrator"
|
18
|
+
require "roast/workflow/step_type_resolver"
|
19
|
+
require "roast/workflow/workflow_context"
|
6
20
|
|
7
21
|
module Roast
|
8
22
|
module Workflow
|
9
23
|
# Handles the execution of workflow steps, including orchestration and threading
|
24
|
+
#
|
25
|
+
# This class now delegates all step execution to StepExecutorCoordinator,
|
26
|
+
# which handles type resolution and execution for all step types.
|
27
|
+
# The circular dependency between executors and workflow has been broken
|
28
|
+
# by introducing the StepRunner interface.
|
10
29
|
class WorkflowExecutor
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
end
|
20
|
-
|
21
|
-
def execute_steps(steps)
|
22
|
-
steps.each do |step|
|
23
|
-
case step
|
24
|
-
when Hash
|
25
|
-
execute_hash_step(step)
|
26
|
-
when Array
|
27
|
-
execute_parallel_steps(step)
|
28
|
-
when String
|
29
|
-
execute_string_step(step)
|
30
|
-
else
|
31
|
-
raise "Unknown step type: #{step.inspect}"
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
# Interpolates {{expression}} in a string with values from the workflow context
|
37
|
-
def interpolate(text)
|
38
|
-
return text unless text.is_a?(String) && text.include?("{{") && text.include?("}}")
|
39
|
-
|
40
|
-
# Replace all {{expression}} with their evaluated values
|
41
|
-
text.gsub(/\{\{([^}]+)\}\}/) do |match|
|
42
|
-
expression = Regexp.last_match(1).strip
|
43
|
-
begin
|
44
|
-
# Evaluate the expression in the workflow's context
|
45
|
-
workflow.instance_eval(expression).to_s
|
46
|
-
rescue => e
|
47
|
-
# If evaluation fails, provide a more detailed error message but preserve the original expression
|
48
|
-
error_msg = "Error interpolating {{#{expression}}}: #{e.message}. This variable is not defined in the workflow context. Please define it before using it in a step name."
|
49
|
-
$stderr.puts "ERROR: #{error_msg}"
|
50
|
-
match # Return the original match to preserve it in the string
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
def execute_step(name)
|
56
|
-
start_time = Time.now
|
57
|
-
# For tests, make sure that we handle this gracefully
|
58
|
-
resource_type = workflow.respond_to?(:resource) ? workflow.resource&.type : nil
|
59
|
-
|
60
|
-
ActiveSupport::Notifications.instrument("roast.step.start", {
|
61
|
-
step_name: name,
|
62
|
-
resource_type: resource_type,
|
63
|
-
})
|
64
|
-
|
65
|
-
$stderr.puts "Executing: #{name} (Resource type: #{resource_type || "unknown"})"
|
66
|
-
|
67
|
-
result = if name.starts_with?("$(")
|
68
|
-
strip_and_execute(name).tap do |output|
|
69
|
-
# Add the command and output to the transcript for reference in following steps
|
70
|
-
workflow.transcript << { user: "I just executed the following command: ```\n#{name}\n```\n\nHere is the output:\n\n```\n#{output}\n```" }
|
71
|
-
workflow.transcript << { assistant: "Noted, thank you." }
|
72
|
-
end
|
73
|
-
elsif name.include?("*") && (!workflow.respond_to?(:resource) || !workflow.resource)
|
74
|
-
# Only use the glob method if we don't have a resource object yet
|
75
|
-
# This is for backward compatibility
|
76
|
-
glob(name)
|
77
|
-
else
|
78
|
-
step_object = find_and_load_step(name)
|
79
|
-
step_result = step_object.call
|
80
|
-
workflow.output[name] = step_result
|
81
|
-
|
82
|
-
# Save state after each step if the workflow supports it
|
83
|
-
save_state(name, step_result) if workflow.respond_to?(:session_name) && workflow.session_name
|
84
|
-
|
85
|
-
step_result
|
86
|
-
end
|
87
|
-
|
88
|
-
execution_time = Time.now - start_time
|
89
|
-
|
90
|
-
ActiveSupport::Notifications.instrument("roast.step.complete", {
|
91
|
-
step_name: name,
|
92
|
-
resource_type: resource_type,
|
93
|
-
success: true,
|
94
|
-
execution_time: execution_time,
|
95
|
-
result_size: result.to_s.length,
|
96
|
-
})
|
97
|
-
|
98
|
-
result
|
99
|
-
rescue => e
|
100
|
-
execution_time = Time.now - start_time
|
101
|
-
|
102
|
-
ActiveSupport::Notifications.instrument("roast.step.error", {
|
103
|
-
step_name: name,
|
104
|
-
resource_type: resource_type,
|
105
|
-
error: e.class.name,
|
106
|
-
message: e.message,
|
107
|
-
execution_time: execution_time,
|
108
|
-
})
|
109
|
-
raise
|
110
|
-
end
|
111
|
-
|
112
|
-
private
|
113
|
-
|
114
|
-
def execute_hash_step(step)
|
115
|
-
# execute a command and store the output in a variable
|
116
|
-
name, command = step.to_a.flatten
|
117
|
-
|
118
|
-
# Interpolate variable name if it contains {{}}
|
119
|
-
interpolated_name = interpolate(name)
|
120
|
-
|
121
|
-
if command.is_a?(Hash)
|
122
|
-
execute_steps([command])
|
123
|
-
else
|
124
|
-
# Interpolate command value
|
125
|
-
interpolated_command = interpolate(command)
|
126
|
-
workflow.output[interpolated_name] = execute_step(interpolated_command)
|
30
|
+
# Define custom exception classes for specific error scenarios
|
31
|
+
class WorkflowExecutorError < StandardError
|
32
|
+
attr_reader :step_name, :original_error
|
33
|
+
|
34
|
+
def initialize(message, step_name: nil, original_error: nil)
|
35
|
+
@step_name = step_name
|
36
|
+
@original_error = original_error
|
37
|
+
super(message)
|
127
38
|
end
|
128
39
|
end
|
129
40
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
41
|
+
class StepExecutionError < WorkflowExecutorError; end
|
42
|
+
class StepNotFoundError < WorkflowExecutorError; end
|
43
|
+
class InterpolationError < WorkflowExecutorError; end
|
44
|
+
class StateError < WorkflowExecutorError; end
|
45
|
+
class ConfigurationError < WorkflowExecutorError; end
|
46
|
+
|
47
|
+
attr_reader :context, :step_loader, :state_manager, :step_executor_coordinator
|
48
|
+
|
49
|
+
delegate :workflow, :config_hash, :context_path, to: :context
|
50
|
+
|
51
|
+
def initialize(workflow, config_hash, context_path,
|
52
|
+
error_handler: nil, step_loader: nil, command_executor: nil,
|
53
|
+
interpolator: nil, state_manager: nil, iteration_executor: nil,
|
54
|
+
conditional_executor: nil, step_orchestrator: nil, step_executor_coordinator: nil)
|
55
|
+
# Create context object to reduce data clump
|
56
|
+
@context = WorkflowContext.new(
|
57
|
+
workflow: workflow,
|
58
|
+
config_hash: config_hash,
|
59
|
+
context_path: context_path,
|
60
|
+
)
|
61
|
+
|
62
|
+
# Dependencies with defaults
|
63
|
+
@error_handler = error_handler || ErrorHandler.new
|
64
|
+
@step_loader = step_loader || StepLoader.new(workflow, config_hash, context_path)
|
65
|
+
@command_executor = command_executor || CommandExecutor.new(logger: @error_handler)
|
66
|
+
@interpolator = interpolator || Interpolator.new(workflow, logger: @error_handler)
|
67
|
+
@state_manager = state_manager || StateManager.new(workflow, logger: @error_handler)
|
68
|
+
@iteration_executor = iteration_executor || IterationExecutor.new(workflow, context_path, @state_manager)
|
69
|
+
@conditional_executor = conditional_executor || ConditionalExecutor.new(workflow, context_path, @state_manager, self)
|
70
|
+
@step_orchestrator = step_orchestrator || StepOrchestrator.new(workflow, @step_loader, @state_manager, @error_handler, self)
|
71
|
+
|
72
|
+
# Initialize coordinator with dependencies
|
73
|
+
@step_executor_coordinator = step_executor_coordinator || StepExecutorCoordinator.new(
|
74
|
+
context: @context,
|
75
|
+
dependencies: {
|
76
|
+
workflow_executor: self,
|
77
|
+
interpolator: @interpolator,
|
78
|
+
command_executor: @command_executor,
|
79
|
+
iteration_executor: @iteration_executor,
|
80
|
+
conditional_executor: @conditional_executor,
|
81
|
+
step_orchestrator: @step_orchestrator,
|
82
|
+
error_handler: @error_handler,
|
83
|
+
},
|
84
|
+
)
|
135
85
|
end
|
136
86
|
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
execute_step(interpolated_step)
|
87
|
+
# Logger interface methods for backward compatibility
|
88
|
+
def log_error(message)
|
89
|
+
@error_handler.log_error(message)
|
141
90
|
end
|
142
91
|
|
143
|
-
def
|
144
|
-
|
145
|
-
if step_name.strip.include?(" ")
|
146
|
-
return Roast::Workflow::PromptStep.new(workflow, name: step_name, auto_loop: false)
|
147
|
-
end
|
148
|
-
|
149
|
-
# First check for a ruby file with the step name
|
150
|
-
rb_file_path = File.join(context_path, "#{step_name}.rb")
|
151
|
-
if File.file?(rb_file_path)
|
152
|
-
return load_ruby_step(rb_file_path, step_name)
|
153
|
-
end
|
154
|
-
|
155
|
-
# Check in shared directory for ruby file
|
156
|
-
shared_rb_path = File.expand_path(File.join(context_path, "..", "shared", "#{step_name}.rb"))
|
157
|
-
if File.file?(shared_rb_path)
|
158
|
-
return load_ruby_step(shared_rb_path, step_name, File.dirname(shared_rb_path))
|
159
|
-
end
|
160
|
-
|
161
|
-
# Continue with existing directory check logic
|
162
|
-
step_path = File.join(context_path, step_name)
|
163
|
-
step_path = File.expand_path(File.join(context_path, "..", "shared", step_name)) unless File.directory?(step_path)
|
164
|
-
raise "Step directory or file not found: #{step_path}" unless File.directory?(step_path)
|
165
|
-
|
166
|
-
setup_step(Roast::Workflow::BaseStep, step_name, step_path)
|
92
|
+
def log_warning(message)
|
93
|
+
@error_handler.log_warning(message)
|
167
94
|
end
|
168
95
|
|
169
|
-
def
|
170
|
-
|
96
|
+
def warn(message)
|
97
|
+
@error_handler.log_warning(message)
|
171
98
|
end
|
172
99
|
|
173
|
-
def
|
174
|
-
|
175
|
-
require file_path
|
176
|
-
step_class = step_name.classify.constantize
|
177
|
-
setup_step(step_class, step_name, context_path)
|
100
|
+
def error(message)
|
101
|
+
@error_handler.log_error(message)
|
178
102
|
end
|
179
103
|
|
180
|
-
def
|
181
|
-
|
182
|
-
step_config = config_hash[step_name]
|
183
|
-
|
184
|
-
# Always set the model, even if there's no step_config
|
185
|
-
# Use step-specific model if defined, otherwise use workflow default model, or fallback to DEFAULT_MODEL
|
186
|
-
step.model = step_config&.dig("model") || config_hash["model"] || DEFAULT_MODEL
|
187
|
-
|
188
|
-
# Pass resource to step if supported
|
189
|
-
step.resource = workflow.resource if step.respond_to?(:resource=)
|
190
|
-
|
191
|
-
if step_config.present?
|
192
|
-
step.print_response = step_config["print_response"] if step_config["print_response"].present?
|
193
|
-
step.loop = step_config["loop"] if step_config["loop"].present?
|
194
|
-
step.json = step_config["json"] if step_config["json"].present?
|
195
|
-
step.params = step_config["params"] if step_config["params"].present?
|
196
|
-
end
|
197
|
-
end
|
104
|
+
def execute_steps(workflow_steps)
|
105
|
+
@step_executor_coordinator.execute_steps(workflow_steps)
|
198
106
|
end
|
199
107
|
|
200
|
-
def
|
201
|
-
|
202
|
-
# Extract the command from the $(command) syntax
|
203
|
-
command = step.strip.match(/^\$\((.*)\)$/)[1]
|
204
|
-
|
205
|
-
# NOTE: We don't need to call interpolate here as it's already been done
|
206
|
-
# in execute_string_step before this method is called
|
207
|
-
%x(#{command})
|
208
|
-
else
|
209
|
-
raise "Missing closing parentheses: #{step}"
|
210
|
-
end
|
108
|
+
def interpolate(text)
|
109
|
+
@interpolator.interpolate(text)
|
211
110
|
end
|
212
111
|
|
213
|
-
def
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
# Get output and final_output if available
|
220
|
-
output = workflow.respond_to?(:output) ? workflow.output.clone : {}
|
221
|
-
final_output = workflow.respond_to?(:final_output) ? workflow.final_output.clone : []
|
222
|
-
|
223
|
-
state_data = {
|
224
|
-
step_name: step_name,
|
225
|
-
order: output.keys.index(step_name) || output.size,
|
226
|
-
transcript: static_data,
|
227
|
-
output: output,
|
228
|
-
final_output: final_output,
|
229
|
-
execution_order: output.keys,
|
230
|
-
}
|
231
|
-
|
232
|
-
# Save the state
|
233
|
-
state_repository.save_state(workflow, step_name, state_data)
|
234
|
-
rescue => e
|
235
|
-
# Don't fail the workflow if state saving fails
|
236
|
-
$stderr.puts "Warning: Failed to save workflow state: #{e.message}"
|
112
|
+
def execute_step(name, exit_on_error: true)
|
113
|
+
@step_executor_coordinator.execute(name, exit_on_error: exit_on_error)
|
114
|
+
rescue StepLoader::StepNotFoundError => e
|
115
|
+
raise StepNotFoundError.new(e.message, step_name: e.step_name, original_error: e.original_error)
|
116
|
+
rescue StepLoader::StepExecutionError => e
|
117
|
+
raise StepExecutionError.new(e.message, step_name: e.step_name, original_error: e.original_error)
|
237
118
|
end
|
238
119
|
end
|
239
120
|
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "raix"
|
4
|
+
require "roast/initializers"
|
5
|
+
require "roast/helpers/function_caching_interceptor"
|
6
|
+
require "roast/helpers/logger"
|
7
|
+
require "roast/workflow/base_workflow"
|
8
|
+
|
9
|
+
module Roast
|
10
|
+
module Workflow
|
11
|
+
# Handles initialization of workflow dependencies: initializers, tools, and API clients
|
12
|
+
class WorkflowInitializer
|
13
|
+
def initialize(configuration)
|
14
|
+
@configuration = configuration
|
15
|
+
end
|
16
|
+
|
17
|
+
def setup
|
18
|
+
load_roast_initializers
|
19
|
+
include_tools
|
20
|
+
configure_api_client
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def load_roast_initializers
|
26
|
+
Roast::Initializers.load_all
|
27
|
+
end
|
28
|
+
|
29
|
+
def include_tools
|
30
|
+
return unless @configuration.tools.present?
|
31
|
+
|
32
|
+
BaseWorkflow.include(Raix::FunctionDispatch)
|
33
|
+
BaseWorkflow.include(Roast::Helpers::FunctionCachingInterceptor) # Add caching support
|
34
|
+
BaseWorkflow.include(*@configuration.tools.map(&:constantize))
|
35
|
+
end
|
36
|
+
|
37
|
+
def configure_api_client
|
38
|
+
# Skip if api client is already configured (e.g., by initializers)
|
39
|
+
return if api_client_already_configured?
|
40
|
+
|
41
|
+
# Skip if no api_token is provided in the workflow
|
42
|
+
return if @configuration.api_token.blank?
|
43
|
+
|
44
|
+
client = case @configuration.api_provider
|
45
|
+
when :openrouter
|
46
|
+
configure_openrouter_client
|
47
|
+
when :openai
|
48
|
+
configure_openai_client
|
49
|
+
when nil
|
50
|
+
# Skip configuration if no api_provider is set
|
51
|
+
return
|
52
|
+
else
|
53
|
+
raise "Unsupported api_provider in workflow configuration: #{@configuration.api_provider}"
|
54
|
+
end
|
55
|
+
|
56
|
+
# Validate the client configuration by making a test API call
|
57
|
+
validate_api_client(client) if client
|
58
|
+
rescue OpenRouter::ConfigurationError, Faraday::UnauthorizedError => e
|
59
|
+
error = Roast::AuthenticationError.new("API authentication failed: No API token provided or token is invalid")
|
60
|
+
error.set_backtrace(e.backtrace)
|
61
|
+
|
62
|
+
ActiveSupport::Notifications.instrument("roast.workflow.start.error", {
|
63
|
+
error: error.class.name,
|
64
|
+
message: error.message,
|
65
|
+
})
|
66
|
+
|
67
|
+
raise error
|
68
|
+
rescue => e
|
69
|
+
Roast::Helpers::Logger.error("Error configuring API client: #{e.message}")
|
70
|
+
raise e
|
71
|
+
end
|
72
|
+
|
73
|
+
def api_client_already_configured?
|
74
|
+
case @configuration.api_provider
|
75
|
+
when :openrouter
|
76
|
+
Raix.configuration.openrouter_client.present?
|
77
|
+
when :openai
|
78
|
+
Raix.configuration.openai_client.present?
|
79
|
+
else
|
80
|
+
false
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def configure_openrouter_client
|
85
|
+
$stderr.puts "Configuring OpenRouter client with token from workflow"
|
86
|
+
require "open_router"
|
87
|
+
|
88
|
+
client = OpenRouter::Client.new(access_token: @configuration.api_token)
|
89
|
+
Raix.configure do |config|
|
90
|
+
config.openrouter_client = client
|
91
|
+
end
|
92
|
+
client
|
93
|
+
end
|
94
|
+
|
95
|
+
def configure_openai_client
|
96
|
+
$stderr.puts "Configuring OpenAI client with token from workflow"
|
97
|
+
require "openai"
|
98
|
+
|
99
|
+
client = OpenAI::Client.new(access_token: @configuration.api_token)
|
100
|
+
Raix.configure do |config|
|
101
|
+
config.openai_client = client
|
102
|
+
end
|
103
|
+
client
|
104
|
+
end
|
105
|
+
|
106
|
+
def validate_api_client(client)
|
107
|
+
# Make a lightweight API call to validate the token
|
108
|
+
client.models.list if client.respond_to?(:models)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/notifications"
|
4
|
+
require "roast/workflow/replay_handler"
|
5
|
+
require "roast/workflow/workflow_executor"
|
6
|
+
require "roast/workflow/output_handler"
|
7
|
+
require "roast/workflow/base_workflow"
|
8
|
+
|
9
|
+
module Roast
|
10
|
+
module Workflow
|
11
|
+
# Handles running workflows for files/targets and orchestrating execution
|
12
|
+
class WorkflowRunner
|
13
|
+
def initialize(configuration, options = {})
|
14
|
+
@configuration = configuration
|
15
|
+
@options = options
|
16
|
+
@output_handler = OutputHandler.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def run_for_files(files)
|
20
|
+
if @configuration.has_target?
|
21
|
+
$stderr.puts "WARNING: Ignoring target parameter because files were provided: #{@configuration.target}"
|
22
|
+
end
|
23
|
+
|
24
|
+
files.each do |file|
|
25
|
+
$stderr.puts "Running workflow for file: #{file}"
|
26
|
+
run_single_workflow(file.strip)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def run_for_targets
|
31
|
+
@configuration.target.lines.each do |file|
|
32
|
+
$stderr.puts "Running workflow for file: #{file.strip}"
|
33
|
+
run_single_workflow(file.strip)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def run_targetless
|
38
|
+
$stderr.puts "Running targetless workflow"
|
39
|
+
run_single_workflow(nil)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Public for backward compatibility with tests
|
43
|
+
def execute_workflow(workflow)
|
44
|
+
steps = @configuration.steps
|
45
|
+
|
46
|
+
# Handle replay option
|
47
|
+
if @options[:replay]
|
48
|
+
replay_handler = ReplayHandler.new(workflow)
|
49
|
+
steps = replay_handler.process_replay(steps, @options[:replay])
|
50
|
+
end
|
51
|
+
|
52
|
+
# Execute the steps
|
53
|
+
executor = WorkflowExecutor.new(workflow, @configuration.config_hash, @configuration.context_path)
|
54
|
+
executor.execute_steps(steps)
|
55
|
+
|
56
|
+
$stderr.puts "🔥🔥🔥 ROAST COMPLETE! 🔥🔥🔥"
|
57
|
+
|
58
|
+
# Save outputs
|
59
|
+
@output_handler.save_final_output(workflow)
|
60
|
+
@output_handler.write_results(workflow)
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def run_single_workflow(file)
|
66
|
+
workflow = create_workflow(file)
|
67
|
+
execute_workflow(workflow)
|
68
|
+
end
|
69
|
+
|
70
|
+
def create_workflow(file)
|
71
|
+
BaseWorkflow.new(
|
72
|
+
file,
|
73
|
+
name: @configuration.basename,
|
74
|
+
context_path: @configuration.context_path,
|
75
|
+
resource: @configuration.resource,
|
76
|
+
session_name: @configuration.name,
|
77
|
+
configuration: @configuration,
|
78
|
+
).tap do |workflow|
|
79
|
+
workflow.output_file = @options[:output] if @options[:output].present?
|
80
|
+
workflow.verbose = @options[:verbose] if @options[:verbose].present?
|
81
|
+
workflow.concise = @options[:concise] if @options[:concise].present?
|
82
|
+
workflow.pause_step_name = @options[:pause] if @options[:pause].present?
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
data/lib/roast/workflow.rb
CHANGED
@@ -2,6 +2,9 @@
|
|
2
2
|
|
3
3
|
require "roast/workflow/base_step"
|
4
4
|
require "roast/workflow/prompt_step"
|
5
|
+
require "roast/workflow/base_iteration_step"
|
6
|
+
require "roast/workflow/repeat_step"
|
7
|
+
require "roast/workflow/each_step"
|
5
8
|
require "roast/workflow/base_workflow"
|
6
9
|
require "roast/workflow/configuration"
|
7
10
|
require "roast/workflow/workflow_executor"
|
data/lib/roast.rb
CHANGED
@@ -1,13 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "fileutils"
|
4
|
+
require "cli/ui"
|
3
5
|
require "raix"
|
4
6
|
require "thor"
|
5
|
-
require "roast/
|
6
|
-
require "roast/tools"
|
7
|
+
require "roast/errors"
|
7
8
|
require "roast/helpers"
|
9
|
+
require "roast/initializers"
|
8
10
|
require "roast/resources"
|
11
|
+
require "roast/tools"
|
12
|
+
require "roast/version"
|
9
13
|
require "roast/workflow"
|
10
|
-
require "roast/initializers"
|
11
14
|
|
12
15
|
module Roast
|
13
16
|
ROOT = File.expand_path("../..", __FILE__)
|
@@ -19,6 +22,8 @@ module Roast
|
|
19
22
|
option :verbose, type: :boolean, aliases: "-v", desc: "Show output from all steps as they are executed"
|
20
23
|
option :target, type: :string, aliases: "-t", desc: "Override target files. Can be file path, glob pattern, or $(shell command)"
|
21
24
|
option :replay, type: :string, aliases: "-r", desc: "Resume workflow from a specific step. Format: step_name or session_timestamp:step_name"
|
25
|
+
option :pause, type: :string, aliases: "-p", desc: "Pause workflow after a specific step. Format: step_name"
|
26
|
+
|
22
27
|
def execute(*paths)
|
23
28
|
raise Thor::Error, "Workflow configuration file is required" if paths.empty?
|
24
29
|
|
@@ -34,6 +39,94 @@ module Roast
|
|
34
39
|
puts "Roast version #{Roast::VERSION}"
|
35
40
|
end
|
36
41
|
|
42
|
+
desc "init", "Initialize a new Roast workflow from an example"
|
43
|
+
option :example, type: :string, aliases: "-e", desc: "Name of the example to use directly (skips picker)"
|
44
|
+
def init
|
45
|
+
if options[:example]
|
46
|
+
copy_example(options[:example])
|
47
|
+
else
|
48
|
+
show_example_picker
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def show_example_picker
|
55
|
+
examples = available_examples
|
56
|
+
|
57
|
+
if examples.empty?
|
58
|
+
puts "No examples found!"
|
59
|
+
return
|
60
|
+
end
|
61
|
+
|
62
|
+
puts "Select an option:"
|
63
|
+
choices = ["Pick from examples", "New from prompt (beta)"]
|
64
|
+
|
65
|
+
selected = run_picker(choices, "Select initialization method:")
|
66
|
+
|
67
|
+
case selected
|
68
|
+
when "Pick from examples"
|
69
|
+
example_choice = run_picker(examples, "Select an example:")
|
70
|
+
copy_example(example_choice) if example_choice
|
71
|
+
when "New from prompt (beta)"
|
72
|
+
create_from_prompt
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def available_examples
|
77
|
+
examples_dir = File.join(Roast::ROOT, "examples")
|
78
|
+
return [] unless File.directory?(examples_dir)
|
79
|
+
|
80
|
+
Dir.entries(examples_dir)
|
81
|
+
.select { |entry| File.directory?(File.join(examples_dir, entry)) && entry != "." && entry != ".." }
|
82
|
+
.sort
|
83
|
+
end
|
84
|
+
|
85
|
+
def run_picker(options, prompt)
|
86
|
+
return if options.empty?
|
87
|
+
|
88
|
+
::CLI::UI::Prompt.ask(prompt) do |handler|
|
89
|
+
options.each { |option| handler.option(option) { |selection| selection } }
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def copy_example(example_name)
|
94
|
+
examples_dir = File.join(Roast::ROOT, "examples")
|
95
|
+
source_path = File.join(examples_dir, example_name)
|
96
|
+
target_path = File.join(Dir.pwd, example_name)
|
97
|
+
|
98
|
+
unless File.directory?(source_path)
|
99
|
+
puts "Example '#{example_name}' not found!"
|
100
|
+
return
|
101
|
+
end
|
102
|
+
|
103
|
+
if File.exist?(target_path)
|
104
|
+
puts "Directory '#{example_name}' already exists in current directory!"
|
105
|
+
return
|
106
|
+
end
|
107
|
+
|
108
|
+
FileUtils.cp_r(source_path, target_path)
|
109
|
+
puts "Successfully copied example '#{example_name}' to current directory."
|
110
|
+
end
|
111
|
+
|
112
|
+
def create_from_prompt
|
113
|
+
puts("Create a new workflow from a description")
|
114
|
+
puts
|
115
|
+
|
116
|
+
# Execute the workflow generator
|
117
|
+
generator_path = File.join(Roast::ROOT, "examples", "workflow_generator", "workflow.yml")
|
118
|
+
|
119
|
+
begin
|
120
|
+
# Execute the workflow generator (it will handle user input)
|
121
|
+
Roast::Workflow::ConfigurationParser.new(generator_path, [], {}).begin!
|
122
|
+
|
123
|
+
puts
|
124
|
+
puts("Workflow generation complete!")
|
125
|
+
rescue => e
|
126
|
+
puts("Error generating workflow: #{e.message}")
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
37
130
|
class << self
|
38
131
|
def exit_on_failure?
|
39
132
|
true
|
data/roast.gemspec
CHANGED
@@ -37,9 +37,10 @@ Gem::Specification.new do |spec|
|
|
37
37
|
spec.require_paths = ["lib"]
|
38
38
|
|
39
39
|
spec.add_dependency("activesupport", "~> 8.0")
|
40
|
+
spec.add_dependency("cli-ui")
|
40
41
|
spec.add_dependency("diff-lcs", "~> 1.5")
|
41
42
|
spec.add_dependency("faraday-retry")
|
42
43
|
spec.add_dependency("json-schema")
|
43
|
-
spec.add_dependency("raix", "~> 0.8.
|
44
|
+
spec.add_dependency("raix", "~> 0.8.6")
|
44
45
|
spec.add_dependency("thor", "~> 1.3")
|
45
46
|
end
|