roast-ai 0.1.7 → 0.2.1
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 +49 -1
- data/CLAUDE.md +20 -0
- data/CLAUDE_NOTES.md +68 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +9 -6
- data/README.md +159 -26
- data/bin/roast +27 -0
- data/docs/ITERATION_SYNTAX.md +147 -0
- data/examples/case_when/README.md +58 -0
- data/examples/case_when/detect_language/prompt.md +16 -0
- data/examples/case_when/workflow.yml +58 -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/direct_coerce_syntax/README.md +32 -0
- data/examples/direct_coerce_syntax/workflow.yml +36 -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 +10 -4
- 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/json_handling/README.md +32 -0
- data/examples/json_handling/workflow.yml +52 -0
- data/examples/openrouter_example/workflow.yml +2 -2
- data/examples/smart_coercion_defaults/README.md +65 -0
- data/examples/smart_coercion_defaults/workflow.yml +44 -0
- data/examples/step_configuration/README.md +87 -0
- data/examples/step_configuration/workflow.yml +60 -0
- 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 +184 -0
- data/lib/roast/workflow/base_step.rb +44 -27
- data/lib/roast/workflow/base_workflow.rb +76 -73
- data/lib/roast/workflow/case_executor.rb +49 -0
- data/lib/roast/workflow/case_step.rb +82 -0
- 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 +59 -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_evaluator.rb +78 -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 +103 -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/prompt_step.rb +4 -1
- 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 +221 -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 +155 -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 +133 -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 +112 -0
- metadata +112 -4
@@ -0,0 +1,184 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "roast/workflow/expression_utils"
|
4
|
+
require "roast/workflow/llm_boolean_coercer"
|
5
|
+
require "roast/workflow/workflow_executor"
|
6
|
+
|
7
|
+
module Roast
|
8
|
+
module Workflow
|
9
|
+
# Base class for iteration steps (RepeatStep and EachStep)
|
10
|
+
class BaseIterationStep < BaseStep
|
11
|
+
include ExpressionUtils
|
12
|
+
|
13
|
+
DEFAULT_MAX_ITERATIONS = 100
|
14
|
+
|
15
|
+
attr_reader :steps
|
16
|
+
|
17
|
+
def initialize(workflow, steps:, **kwargs)
|
18
|
+
super(workflow, **kwargs)
|
19
|
+
@steps = steps
|
20
|
+
# Don't initialize cmd_tool here - we'll do it lazily when needed
|
21
|
+
end
|
22
|
+
|
23
|
+
protected
|
24
|
+
|
25
|
+
# Process various types of inputs and convert to appropriate types for iteration
|
26
|
+
def process_iteration_input(input, context, coerce_to: nil)
|
27
|
+
if input.is_a?(String)
|
28
|
+
if ruby_expression?(input)
|
29
|
+
# Default to regular boolean for ruby expressions
|
30
|
+
coerce_to ||= :boolean
|
31
|
+
process_ruby_expression(input, context, coerce_to)
|
32
|
+
elsif bash_command?(input)
|
33
|
+
# Default to boolean (which will interpret exit code) for bash commands
|
34
|
+
coerce_to ||= :boolean
|
35
|
+
process_bash_command(input, coerce_to)
|
36
|
+
else
|
37
|
+
# For prompts/steps, default to llm_boolean
|
38
|
+
coerce_to ||= :llm_boolean
|
39
|
+
process_step_or_prompt(input, context, coerce_to)
|
40
|
+
end
|
41
|
+
else
|
42
|
+
# Non-string inputs default to regular boolean
|
43
|
+
coerce_to ||= :boolean
|
44
|
+
coerce_result(input, coerce_to)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Interpolates {{expression}} in a string with values from the workflow context
|
49
|
+
def interpolate_expression(text, context)
|
50
|
+
return text unless text.is_a?(String) && text.include?("{{") && text.include?("}}")
|
51
|
+
|
52
|
+
# Replace all {{expression}} with their evaluated values
|
53
|
+
text.gsub(/\{\{([^}]+)\}\}/) do |match|
|
54
|
+
expression = extract_expression(match)
|
55
|
+
begin
|
56
|
+
# Evaluate the expression in the workflow's context
|
57
|
+
result = context.instance_eval(expression)
|
58
|
+
result.inspect # Convert to string representation
|
59
|
+
rescue => e
|
60
|
+
warn_interpolation_error(expression, e)
|
61
|
+
match # Return the original match to preserve it in the string
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Execute nested steps
|
67
|
+
def execute_nested_steps(steps, context, executor = nil)
|
68
|
+
executor ||= WorkflowExecutor.new(context, {}, context_path)
|
69
|
+
results = []
|
70
|
+
|
71
|
+
steps.each do |step|
|
72
|
+
result = case step
|
73
|
+
when String
|
74
|
+
executor.execute_step(step)
|
75
|
+
when Hash, Array
|
76
|
+
executor.execute_steps([step])
|
77
|
+
end
|
78
|
+
results << result
|
79
|
+
end
|
80
|
+
|
81
|
+
results
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
# Process a Ruby expression
|
87
|
+
def process_ruby_expression(input, context, coerce_to)
|
88
|
+
expression = extract_expression(input)
|
89
|
+
result = evaluate_ruby_expression(expression, context)
|
90
|
+
coerce_result(result, coerce_to)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Process a Bash command
|
94
|
+
def process_bash_command(input, coerce_to)
|
95
|
+
command = extract_command(input)
|
96
|
+
execute_command(command, coerce_to)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Process a step name or prompt
|
100
|
+
def process_step_or_prompt(input, context, coerce_to)
|
101
|
+
step_result = execute_step_by_name(input, context)
|
102
|
+
coerce_result(step_result, coerce_to)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Execute a Ruby expression in the workflow context
|
106
|
+
def evaluate_ruby_expression(expression, context)
|
107
|
+
context.instance_eval(expression)
|
108
|
+
rescue => e
|
109
|
+
warn_expression_error(expression, e)
|
110
|
+
nil
|
111
|
+
end
|
112
|
+
|
113
|
+
# Execute a bash command and return its result
|
114
|
+
def execute_command(command, coerce_to)
|
115
|
+
# Use the Cmd module to execute the command
|
116
|
+
result = Roast::Tools::Cmd.call(command)
|
117
|
+
|
118
|
+
if coerce_to == :boolean
|
119
|
+
# For boolean coercion, check if command was allowed and exit status was 0
|
120
|
+
if result.to_s.start_with?("Error: Command not allowed")
|
121
|
+
return false
|
122
|
+
end
|
123
|
+
|
124
|
+
# Parse exit status from the output
|
125
|
+
# The Cmd tool returns output in format: "Command: X\nExit status: Y\nOutput:\nZ"
|
126
|
+
if result =~ /Exit status: (\d+)/
|
127
|
+
exit_status = ::Regexp.last_match(1).to_i
|
128
|
+
exit_status == 0
|
129
|
+
else
|
130
|
+
# If we can't parse exit status, assume success if no error
|
131
|
+
!result.to_s.start_with?("Error")
|
132
|
+
end
|
133
|
+
else
|
134
|
+
# For other uses, return the output
|
135
|
+
result
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Execute a step by name and return its result
|
140
|
+
def execute_step_by_name(step_name, context)
|
141
|
+
# Reuse existing step execution logic
|
142
|
+
executor = WorkflowExecutor.new(context, {}, context_path)
|
143
|
+
executor.execute_step(step_name)
|
144
|
+
end
|
145
|
+
|
146
|
+
# Coerce results to the appropriate type
|
147
|
+
def coerce_result(result, coerce_to)
|
148
|
+
return coerce_to_boolean(result) if coerce_to == :boolean
|
149
|
+
return coerce_to_iterable(result) if coerce_to == :iterable
|
150
|
+
return coerce_to_llm_boolean(result) if coerce_to == :llm_boolean
|
151
|
+
|
152
|
+
# Default - return as is
|
153
|
+
result
|
154
|
+
end
|
155
|
+
|
156
|
+
# Force a value to boolean
|
157
|
+
def coerce_to_boolean(result)
|
158
|
+
!!result
|
159
|
+
end
|
160
|
+
|
161
|
+
# Ensure a value is iterable
|
162
|
+
def coerce_to_iterable(result)
|
163
|
+
return result if result.respond_to?(:each)
|
164
|
+
|
165
|
+
result.to_s.split("\n")
|
166
|
+
end
|
167
|
+
|
168
|
+
# Convert LLM response to boolean
|
169
|
+
def coerce_to_llm_boolean(result)
|
170
|
+
LlmBooleanCoercer.coerce(result)
|
171
|
+
end
|
172
|
+
|
173
|
+
# Log a warning for expression evaluation errors
|
174
|
+
def warn_expression_error(expression, error)
|
175
|
+
$stderr.puts "Warning: Error evaluating expression '#{expression}': #{error.message}"
|
176
|
+
end
|
177
|
+
|
178
|
+
# Log a warning for interpolation errors
|
179
|
+
def warn_interpolation_error(expression, error)
|
180
|
+
$stderr.puts "Warning: Error interpolating {{#{expression}}}: #{error.message}"
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
@@ -2,42 +2,56 @@
|
|
2
2
|
|
3
3
|
require "erb"
|
4
4
|
require "forwardable"
|
5
|
+
require "roast/workflow/context_path_resolver"
|
5
6
|
|
6
7
|
module Roast
|
7
8
|
module Workflow
|
8
9
|
class BaseStep
|
9
10
|
extend Forwardable
|
10
11
|
|
11
|
-
attr_accessor :model, :print_response, :auto_loop, :json, :params, :resource
|
12
|
+
attr_accessor :model, :print_response, :auto_loop, :json, :params, :resource, :coerce_to
|
12
13
|
attr_reader :workflow, :name, :context_path
|
13
14
|
|
14
15
|
def_delegator :workflow, :append_to_final_output
|
15
16
|
def_delegator :workflow, :chat_completion
|
16
17
|
def_delegator :workflow, :transcript
|
17
18
|
|
18
|
-
def initialize(workflow, model: "anthropic:claude-
|
19
|
+
def initialize(workflow, model: "anthropic:claude-opus-4", name: nil, context_path: nil, auto_loop: true)
|
19
20
|
@workflow = workflow
|
20
21
|
@model = model
|
21
22
|
@name = name || self.class.name.underscore.split("/").last
|
22
|
-
@context_path = context_path ||
|
23
|
+
@context_path = context_path || ContextPathResolver.resolve(self.class)
|
23
24
|
@print_response = false
|
24
25
|
@auto_loop = auto_loop
|
25
26
|
@json = false
|
26
27
|
@params = {}
|
28
|
+
@coerce_to = nil
|
27
29
|
@resource = workflow.resource if workflow.respond_to?(:resource)
|
28
30
|
end
|
29
31
|
|
30
32
|
def call
|
31
33
|
prompt(read_sidecar_prompt)
|
32
|
-
chat_completion(print_response:, auto_loop:, json:, params:)
|
34
|
+
result = chat_completion(print_response:, auto_loop:, json:, params:)
|
35
|
+
|
36
|
+
# Apply coercion if configured
|
37
|
+
apply_coercion(result)
|
33
38
|
end
|
34
39
|
|
35
40
|
protected
|
36
41
|
|
37
|
-
def chat_completion(print_response:
|
38
|
-
|
42
|
+
def chat_completion(print_response: nil, auto_loop: nil, json: nil, params: nil)
|
43
|
+
# Use instance variables as defaults if parameters are not provided
|
44
|
+
print_response = @print_response if print_response.nil?
|
45
|
+
auto_loop = @auto_loop if auto_loop.nil?
|
46
|
+
json = @json if json.nil?
|
47
|
+
params = @params if params.nil?
|
48
|
+
|
49
|
+
workflow.chat_completion(openai: workflow.openai? && model, loop: auto_loop, model: model, json:, params:).then do |response|
|
39
50
|
case response
|
51
|
+
in Array if json
|
52
|
+
response.flatten.first
|
40
53
|
in Array
|
54
|
+
# For non-JSON responses, join array elements
|
41
55
|
response.map(&:presence).compact.join("\n")
|
42
56
|
else
|
43
57
|
response
|
@@ -47,27 +61,6 @@ module Roast
|
|
47
61
|
end
|
48
62
|
end
|
49
63
|
|
50
|
-
# Determine the directory where the actual class is defined, not BaseWorkflow
|
51
|
-
def determine_context_path
|
52
|
-
# Get the actual class's source file
|
53
|
-
klass = self.class
|
54
|
-
|
55
|
-
# Try to get the file path where the class is defined
|
56
|
-
path = if klass.name.include?("::")
|
57
|
-
# For namespaced classes like Roast::Workflow::Grading::Workflow
|
58
|
-
# Convert the class name to a relative path
|
59
|
-
class_path = klass.name.underscore + ".rb"
|
60
|
-
# Look through load path to find the actual file
|
61
|
-
$LOAD_PATH.map { |p| File.join(p, class_path) }.find { |f| File.exist?(f) }
|
62
|
-
else
|
63
|
-
# Fall back to the current file if we can't find it
|
64
|
-
__FILE__
|
65
|
-
end
|
66
|
-
|
67
|
-
# Return directory containing the class definition
|
68
|
-
File.dirname(path || __FILE__)
|
69
|
-
end
|
70
|
-
|
71
64
|
def prompt(text)
|
72
65
|
transcript << { user: text }
|
73
66
|
end
|
@@ -93,6 +86,30 @@ module Roast
|
|
93
86
|
append_to_final_output(response)
|
94
87
|
end
|
95
88
|
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def apply_coercion(result)
|
93
|
+
return result unless @coerce_to
|
94
|
+
|
95
|
+
case @coerce_to
|
96
|
+
when :boolean
|
97
|
+
# Simple boolean coercion
|
98
|
+
!!result
|
99
|
+
when :llm_boolean
|
100
|
+
# Use LLM boolean coercer for natural language responses
|
101
|
+
require "roast/workflow/llm_boolean_coercer"
|
102
|
+
LlmBooleanCoercer.coerce(result)
|
103
|
+
when :iterable
|
104
|
+
# Ensure result is iterable
|
105
|
+
return result if result.respond_to?(:each)
|
106
|
+
|
107
|
+
result.to_s.split("\n")
|
108
|
+
else
|
109
|
+
# Unknown coercion type, return as-is
|
110
|
+
result
|
111
|
+
end
|
112
|
+
end
|
96
113
|
end
|
97
114
|
end
|
98
115
|
end
|
@@ -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,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Workflow
|
5
|
+
# Handles execution of case/when/else steps
|
6
|
+
class CaseExecutor
|
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_case(case_config)
|
15
|
+
$stderr.puts "Executing case step: #{case_config.inspect}"
|
16
|
+
|
17
|
+
# Extract case expression
|
18
|
+
case_expr = case_config["case"]
|
19
|
+
when_clauses = case_config["when"]
|
20
|
+
case_config["else"]
|
21
|
+
|
22
|
+
# Verify required parameters
|
23
|
+
raise WorkflowExecutor::ConfigurationError, "Missing 'case' expression in case configuration" unless case_expr
|
24
|
+
raise WorkflowExecutor::ConfigurationError, "Missing 'when' clauses in case configuration" unless when_clauses
|
25
|
+
|
26
|
+
# Create and execute a CaseStep
|
27
|
+
require "roast/workflow/case_step" unless defined?(Roast::Workflow::CaseStep)
|
28
|
+
case_step = CaseStep.new(
|
29
|
+
@workflow,
|
30
|
+
config: case_config,
|
31
|
+
name: "case_#{case_expr.to_s.gsub(/[^a-zA-Z0-9_]/, "_")[0..30]}",
|
32
|
+
context_path: @context_path,
|
33
|
+
workflow_executor: @workflow_executor,
|
34
|
+
)
|
35
|
+
|
36
|
+
result = case_step.call
|
37
|
+
|
38
|
+
# Store the result in workflow output
|
39
|
+
step_name = "case_#{case_expr.to_s.gsub(/[^a-zA-Z0-9_]/, "_")[0..30]}"
|
40
|
+
@workflow.output[step_name] = result
|
41
|
+
|
42
|
+
# Save state
|
43
|
+
@state_manager.save_state(step_name, @workflow.output[step_name])
|
44
|
+
|
45
|
+
result
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "roast/workflow/base_step"
|
4
|
+
require "roast/workflow/expression_evaluator"
|
5
|
+
require "roast/workflow/interpolator"
|
6
|
+
|
7
|
+
module Roast
|
8
|
+
module Workflow
|
9
|
+
class CaseStep < BaseStep
|
10
|
+
include ExpressionEvaluator
|
11
|
+
|
12
|
+
def initialize(workflow, config:, name:, context_path:, workflow_executor:, **kwargs)
|
13
|
+
super(workflow, name: name, context_path: context_path, **kwargs)
|
14
|
+
|
15
|
+
@config = config
|
16
|
+
@case_expression = config["case"]
|
17
|
+
@when_clauses = config["when"] || {}
|
18
|
+
@else_steps = config["else"] || []
|
19
|
+
@workflow_executor = workflow_executor
|
20
|
+
end
|
21
|
+
|
22
|
+
def call
|
23
|
+
# Evaluate the case expression to get the value to match against
|
24
|
+
case_value = evaluate_case_expression(@case_expression)
|
25
|
+
|
26
|
+
# Find the matching when clause
|
27
|
+
matched_key = find_matching_when_clause(case_value)
|
28
|
+
|
29
|
+
# Determine which steps to execute
|
30
|
+
steps_to_execute = if matched_key
|
31
|
+
@when_clauses[matched_key]
|
32
|
+
else
|
33
|
+
@else_steps
|
34
|
+
end
|
35
|
+
|
36
|
+
# Execute the selected steps
|
37
|
+
unless steps_to_execute.nil? || steps_to_execute.empty?
|
38
|
+
@workflow_executor.execute_steps(steps_to_execute)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Return a result indicating which branch was taken
|
42
|
+
{
|
43
|
+
case_value: case_value,
|
44
|
+
matched_when: matched_key,
|
45
|
+
branch_executed: matched_key || (steps_to_execute.empty? ? "none" : "else"),
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def evaluate_case_expression(expression)
|
52
|
+
return unless expression
|
53
|
+
|
54
|
+
# Handle interpolated expressions
|
55
|
+
if expression.is_a?(String)
|
56
|
+
interpolated = Interpolator.new(@workflow).interpolate(expression)
|
57
|
+
|
58
|
+
if ruby_expression?(interpolated)
|
59
|
+
evaluate_ruby_expression(interpolated)
|
60
|
+
elsif bash_command?(interpolated)
|
61
|
+
evaluate_bash_command(interpolated, for_condition: false)
|
62
|
+
else
|
63
|
+
# Return the interpolated value as-is
|
64
|
+
interpolated
|
65
|
+
end
|
66
|
+
else
|
67
|
+
expression
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def find_matching_when_clause(case_value)
|
72
|
+
# Convert case_value to string for comparison
|
73
|
+
case_value_str = case_value.to_s
|
74
|
+
|
75
|
+
@when_clauses.keys.find do |when_key|
|
76
|
+
# Direct string comparison
|
77
|
+
when_key.to_s == case_value_str
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|