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
@@ -6,125 +6,128 @@ require "active_support"
|
|
6
6
|
require "active_support/isolated_execution_state"
|
7
7
|
require "active_support/notifications"
|
8
8
|
require "active_support/core_ext/hash/indifferent_access"
|
9
|
+
require "roast/workflow/output_manager"
|
10
|
+
require "roast/workflow/context_path_resolver"
|
9
11
|
|
10
12
|
module Roast
|
11
13
|
module Workflow
|
12
14
|
class BaseWorkflow
|
13
15
|
include Raix::ChatCompletion
|
14
16
|
|
15
|
-
attr_reader :output
|
16
17
|
attr_accessor :file,
|
17
18
|
:concise,
|
18
19
|
:output_file,
|
20
|
+
:pause_step_name,
|
19
21
|
:verbose,
|
20
22
|
:name,
|
21
23
|
:context_path,
|
22
24
|
:resource,
|
23
25
|
:session_name,
|
24
26
|
:session_timestamp,
|
25
|
-
:configuration
|
27
|
+
:configuration,
|
28
|
+
:model
|
29
|
+
|
30
|
+
delegate :api_provider, :openai?, to: :configuration
|
31
|
+
delegate :output, :output=, :append_to_final_output, :final_output, to: :output_manager
|
26
32
|
|
27
33
|
def initialize(file = nil, name: nil, context_path: nil, resource: nil, session_name: nil, configuration: nil)
|
28
34
|
@file = file
|
29
35
|
@name = name || self.class.name.underscore.split("/").last
|
30
|
-
@context_path = context_path ||
|
31
|
-
@final_output = []
|
32
|
-
@output = ActiveSupport::HashWithIndifferentAccess.new
|
36
|
+
@context_path = context_path || ContextPathResolver.resolve(self.class)
|
33
37
|
@resource = resource || Roast::Resources.for(file)
|
34
38
|
@session_name = session_name || @name
|
35
39
|
@session_timestamp = nil
|
36
40
|
@configuration = configuration
|
37
|
-
transcript << { system: read_sidecar_prompt }
|
38
|
-
Roast::Tools.setup_interrupt_handler(transcript)
|
39
|
-
Roast::Tools.setup_exit_handler(self)
|
40
|
-
end
|
41
|
-
|
42
|
-
# Custom writer for output to ensure it's always a HashWithIndifferentAccess
|
43
|
-
def output=(value)
|
44
|
-
@output = if value.is_a?(ActiveSupport::HashWithIndifferentAccess)
|
45
|
-
value
|
46
|
-
else
|
47
|
-
ActiveSupport::HashWithIndifferentAccess.new(value)
|
48
|
-
end
|
49
|
-
end
|
50
41
|
|
51
|
-
|
52
|
-
@
|
53
|
-
end
|
42
|
+
# Initialize managers
|
43
|
+
@output_manager = OutputManager.new
|
54
44
|
|
55
|
-
|
56
|
-
|
57
|
-
|
45
|
+
# Setup prompt and handlers
|
46
|
+
read_sidecar_prompt.then do |prompt|
|
47
|
+
next unless prompt
|
58
48
|
|
59
|
-
|
60
|
-
if @final_output.respond_to?(:join)
|
61
|
-
@final_output.join("\n\n")
|
62
|
-
else
|
63
|
-
# Handle any other unexpected type by converting to string
|
64
|
-
@final_output.to_s
|
49
|
+
transcript << { system: prompt }
|
65
50
|
end
|
51
|
+
Roast::Tools.setup_interrupt_handler(transcript)
|
52
|
+
Roast::Tools.setup_exit_handler(self)
|
66
53
|
end
|
67
54
|
|
68
55
|
# Override chat_completion to add instrumentation
|
69
56
|
def chat_completion(**kwargs)
|
70
57
|
start_time = Time.now
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
58
|
+
step_model = kwargs[:model]
|
59
|
+
|
60
|
+
with_model(step_model) do
|
61
|
+
ActiveSupport::Notifications.instrument("roast.chat_completion.start", {
|
62
|
+
model: model,
|
63
|
+
parameters: kwargs.except(:openai, :model),
|
64
|
+
})
|
65
|
+
|
66
|
+
# Call the parent module's chat_completion
|
67
|
+
# skip model because it is read directly from the model method
|
68
|
+
result = super(**kwargs.except(:model))
|
69
|
+
execution_time = Time.now - start_time
|
70
|
+
|
71
|
+
ActiveSupport::Notifications.instrument("roast.chat_completion.complete", {
|
72
|
+
success: true,
|
73
|
+
model: model,
|
74
|
+
parameters: kwargs.except(:openai, :model),
|
75
|
+
execution_time: execution_time,
|
76
|
+
response_size: result.to_s.length,
|
77
|
+
})
|
78
|
+
result
|
79
|
+
end
|
80
|
+
rescue Faraday::ResourceNotFound => e
|
79
81
|
execution_time = Time.now - start_time
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
parameters: kwargs.except(:openai),
|
85
|
-
execution_time: execution_time,
|
86
|
-
response_size: result.to_s.length,
|
87
|
-
})
|
88
|
-
|
89
|
-
result
|
82
|
+
message = e.response.dig(:body, "error", "message") || e.message
|
83
|
+
error = Roast::ResourceNotFoundError.new(message)
|
84
|
+
error.set_backtrace(e.backtrace)
|
85
|
+
log_and_raise_error(error, message, step_model || model, kwargs, execution_time)
|
90
86
|
rescue => e
|
91
87
|
execution_time = Time.now - start_time
|
88
|
+
log_and_raise_error(e, e.message, step_model || model, kwargs, execution_time)
|
89
|
+
end
|
92
90
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
})
|
100
|
-
raise
|
91
|
+
def with_model(model)
|
92
|
+
previous_model = @model
|
93
|
+
@model = model
|
94
|
+
yield
|
95
|
+
ensure
|
96
|
+
@model = previous_model
|
101
97
|
end
|
102
98
|
|
103
99
|
def workflow
|
104
100
|
self
|
105
101
|
end
|
106
102
|
|
107
|
-
|
103
|
+
# Expose output manager for state management
|
104
|
+
attr_reader :output_manager
|
108
105
|
|
109
|
-
#
|
110
|
-
def
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
# Try to get the file path where the class is defined
|
115
|
-
path = if klass.name.include?("::")
|
116
|
-
# For namespaced classes like Roast::Workflow::Grading::Workflow
|
117
|
-
# Convert the class name to a relative path
|
118
|
-
class_path = klass.name.underscore + ".rb"
|
119
|
-
# Look through load path to find the actual file
|
120
|
-
$LOAD_PATH.map { |p| File.join(p, class_path) }.find { |f| File.exist?(f) }
|
106
|
+
# Allow direct access to output values without 'output.' prefix
|
107
|
+
def method_missing(method_name, *args, &block)
|
108
|
+
if output.respond_to?(method_name)
|
109
|
+
output.send(method_name, *args, &block)
|
121
110
|
else
|
122
|
-
|
123
|
-
__FILE__
|
111
|
+
super
|
124
112
|
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def respond_to_missing?(method_name, include_private = false)
|
116
|
+
output.respond_to?(method_name) || super
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def log_and_raise_error(error, message, model, params, execution_time)
|
122
|
+
ActiveSupport::Notifications.instrument("roast.chat_completion.error", {
|
123
|
+
error: error.class.name,
|
124
|
+
message: message,
|
125
|
+
model: model,
|
126
|
+
parameters: params.except(:openai, :model),
|
127
|
+
execution_time: execution_time,
|
128
|
+
})
|
125
129
|
|
126
|
-
|
127
|
-
File.dirname(path || __FILE__)
|
130
|
+
raise error
|
128
131
|
end
|
129
132
|
|
130
133
|
def read_sidecar_prompt
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "English"
|
4
|
+
|
5
|
+
module Roast
|
6
|
+
module Workflow
|
7
|
+
class CommandExecutor
|
8
|
+
class CommandExecutionError < StandardError
|
9
|
+
attr_reader :command, :exit_status, :original_error
|
10
|
+
|
11
|
+
def initialize(message, command:, exit_status: nil, original_error: nil)
|
12
|
+
@command = command
|
13
|
+
@exit_status = exit_status
|
14
|
+
@original_error = original_error
|
15
|
+
super(message)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(logger: nil)
|
20
|
+
@logger = logger || NullLogger.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def execute(command_string, exit_on_error: true)
|
24
|
+
command = extract_command(command_string)
|
25
|
+
|
26
|
+
output = %x(#{command})
|
27
|
+
exit_status = $CHILD_STATUS.exitstatus
|
28
|
+
|
29
|
+
handle_execution_result(
|
30
|
+
command: command,
|
31
|
+
output: output,
|
32
|
+
exit_status: exit_status,
|
33
|
+
success: $CHILD_STATUS.success?,
|
34
|
+
exit_on_error: exit_on_error,
|
35
|
+
)
|
36
|
+
rescue ArgumentError, CommandExecutionError
|
37
|
+
raise
|
38
|
+
rescue => e
|
39
|
+
handle_execution_error(
|
40
|
+
command: command,
|
41
|
+
error: e,
|
42
|
+
exit_on_error: exit_on_error,
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def extract_command(command_string)
|
49
|
+
match = command_string.strip.match(/^\$\((.*)\)$/)
|
50
|
+
raise ArgumentError, "Invalid command format. Expected $(command), got: #{command_string}" unless match
|
51
|
+
|
52
|
+
match[1]
|
53
|
+
end
|
54
|
+
|
55
|
+
def handle_execution_result(command:, output:, exit_status:, success:, exit_on_error:)
|
56
|
+
return output if success
|
57
|
+
|
58
|
+
if exit_on_error
|
59
|
+
raise CommandExecutionError.new(
|
60
|
+
"Command exited with non-zero status (#{exit_status})",
|
61
|
+
command: command,
|
62
|
+
exit_status: exit_status,
|
63
|
+
)
|
64
|
+
else
|
65
|
+
@logger.warn("Command '#{command}' exited with non-zero status (#{exit_status}), continuing execution")
|
66
|
+
output + "\n[Exit status: #{exit_status}]"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def handle_execution_error(command:, error:, exit_on_error:)
|
71
|
+
if exit_on_error
|
72
|
+
raise CommandExecutionError.new(
|
73
|
+
"Failed to execute command '#{command}': #{error.message}",
|
74
|
+
command: command,
|
75
|
+
original_error: error,
|
76
|
+
)
|
77
|
+
else
|
78
|
+
@logger.warn("Command '#{command}' failed with error: #{error.message}, continuing execution")
|
79
|
+
"Error executing command: #{error.message}\n[Exit status: error]"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
class NullLogger
|
84
|
+
def warn(_message); end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Workflow
|
5
|
+
# Handles execution of conditional steps (if and unless)
|
6
|
+
class ConditionalExecutor
|
7
|
+
def initialize(workflow, context_path, state_manager, workflow_executor = nil)
|
8
|
+
@workflow = workflow
|
9
|
+
@context_path = context_path
|
10
|
+
@state_manager = state_manager
|
11
|
+
@workflow_executor = workflow_executor
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute_conditional(conditional_config)
|
15
|
+
$stderr.puts "Executing conditional step: #{conditional_config.inspect}"
|
16
|
+
|
17
|
+
# Determine if this is an 'if' or 'unless' condition
|
18
|
+
condition_expr = conditional_config["if"] || conditional_config["unless"]
|
19
|
+
is_unless = conditional_config.key?("unless")
|
20
|
+
then_steps = conditional_config["then"]
|
21
|
+
|
22
|
+
# Verify required parameters
|
23
|
+
raise WorkflowExecutor::ConfigurationError, "Missing condition in conditional configuration" unless condition_expr
|
24
|
+
raise WorkflowExecutor::ConfigurationError, "Missing 'then' steps in conditional configuration" unless then_steps
|
25
|
+
|
26
|
+
# Create and execute a ConditionalStep
|
27
|
+
require "roast/workflow/conditional_step" unless defined?(Roast::Workflow::ConditionalStep)
|
28
|
+
conditional_step = ConditionalStep.new(
|
29
|
+
@workflow,
|
30
|
+
config: conditional_config,
|
31
|
+
name: "conditional_#{condition_expr.gsub(/[^a-zA-Z0-9_]/, "_")[0..20]}",
|
32
|
+
context_path: @context_path,
|
33
|
+
workflow_executor: @workflow_executor,
|
34
|
+
)
|
35
|
+
|
36
|
+
result = conditional_step.call
|
37
|
+
|
38
|
+
# Store a marker in workflow output to indicate which branch was taken
|
39
|
+
condition_key = is_unless ? "unless" : "if"
|
40
|
+
step_name = "#{condition_key}_#{condition_expr.gsub(/[^a-zA-Z0-9_]/, "_")[0..30]}"
|
41
|
+
@workflow.output[step_name] = result
|
42
|
+
|
43
|
+
# Save state
|
44
|
+
@state_manager.save_state(step_name, @workflow.output[step_name])
|
45
|
+
|
46
|
+
result
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "roast/workflow/base_step"
|
4
|
+
require "roast/workflow/command_executor"
|
5
|
+
require "roast/workflow/expression_utils"
|
6
|
+
require "roast/workflow/interpolator"
|
7
|
+
|
8
|
+
module Roast
|
9
|
+
module Workflow
|
10
|
+
class ConditionalStep < BaseStep
|
11
|
+
include ExpressionUtils
|
12
|
+
|
13
|
+
def initialize(workflow, config:, name:, context_path:, workflow_executor:, **kwargs)
|
14
|
+
super(workflow, name: name, context_path: context_path, **kwargs)
|
15
|
+
|
16
|
+
@config = config
|
17
|
+
@condition = config["if"] || config["unless"]
|
18
|
+
@is_unless = config.key?("unless")
|
19
|
+
@then_steps = config["then"] || []
|
20
|
+
@else_steps = config["else"] || []
|
21
|
+
@workflow_executor = workflow_executor
|
22
|
+
end
|
23
|
+
|
24
|
+
def call
|
25
|
+
# Evaluate the condition
|
26
|
+
condition_result = evaluate_condition(@condition)
|
27
|
+
|
28
|
+
# Invert the result if this is an 'unless' condition
|
29
|
+
condition_result = !condition_result if @is_unless
|
30
|
+
|
31
|
+
# Select which steps to execute based on the condition
|
32
|
+
steps_to_execute = condition_result ? @then_steps : @else_steps
|
33
|
+
|
34
|
+
# Execute the selected steps
|
35
|
+
unless steps_to_execute.empty?
|
36
|
+
@workflow_executor.execute_steps(steps_to_execute)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Return a result indicating which branch was taken
|
40
|
+
{ condition_result: condition_result, branch_executed: condition_result ? "then" : "else" }
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def evaluate_condition(condition)
|
46
|
+
return false unless condition.is_a?(String)
|
47
|
+
|
48
|
+
if ruby_expression?(condition)
|
49
|
+
evaluate_ruby_expression(condition)
|
50
|
+
elsif bash_command?(condition)
|
51
|
+
evaluate_bash_command(condition)
|
52
|
+
else
|
53
|
+
# Treat as a step name or direct boolean
|
54
|
+
evaluate_step_or_value(condition)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def evaluate_ruby_expression(expression)
|
59
|
+
expr = extract_expression(expression)
|
60
|
+
begin
|
61
|
+
!!@workflow.instance_eval(expr)
|
62
|
+
rescue => e
|
63
|
+
$stderr.puts "Warning: Error evaluating expression '#{expr}': #{e.message}"
|
64
|
+
false
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def evaluate_bash_command(command)
|
69
|
+
cmd = extract_command(command)
|
70
|
+
executor = CommandExecutor.new(logger: Roast::Helpers::Logger)
|
71
|
+
begin
|
72
|
+
result = executor.execute(cmd, exit_on_error: false)
|
73
|
+
# For conditionals, we care about the exit status
|
74
|
+
result[:success]
|
75
|
+
rescue => e
|
76
|
+
$stderr.puts "Warning: Error executing command '#{cmd}': #{e.message}"
|
77
|
+
false
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def evaluate_step_or_value(input)
|
82
|
+
# Check if it's a reference to a previous step output
|
83
|
+
if @workflow.output.key?(input)
|
84
|
+
result = @workflow.output[input]
|
85
|
+
# Coerce to boolean
|
86
|
+
return false if result.nil? || result == false || result == "" || result == "false"
|
87
|
+
|
88
|
+
return true
|
89
|
+
end
|
90
|
+
|
91
|
+
# Otherwise treat as a direct value
|
92
|
+
input.to_s.downcase == "true"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -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
|