roast-ai 0.4.1 → 0.4.3
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/.gitignore +1 -0
- data/CHANGELOG.md +43 -0
- data/Gemfile +0 -1
- data/Gemfile.lock +48 -23
- data/README.md +228 -29
- data/examples/coding_agent_with_model.yml +20 -0
- data/examples/coding_agent_with_retries.yml +30 -0
- data/examples/grading/rb_test_runner +1 -1
- data/lib/roast/errors.rb +3 -0
- data/lib/roast/helpers/metadata_access.rb +39 -0
- data/lib/roast/helpers/timeout_handler.rb +1 -1
- data/lib/roast/tools/coding_agent.rb +99 -27
- data/lib/roast/tools/grep.rb +4 -0
- data/lib/roast/version.rb +1 -1
- data/lib/roast/workflow/agent_step.rb +57 -4
- data/lib/roast/workflow/base_workflow.rb +4 -2
- data/lib/roast/workflow/command_executor.rb +3 -1
- data/lib/roast/workflow/configuration_parser.rb +2 -0
- data/lib/roast/workflow/each_step.rb +5 -3
- data/lib/roast/workflow/input_step.rb +2 -0
- data/lib/roast/workflow/interpolator.rb +23 -1
- data/lib/roast/workflow/metadata_manager.rb +47 -0
- data/lib/roast/workflow/output_handler.rb +1 -0
- data/lib/roast/workflow/replay_handler.rb +8 -0
- data/lib/roast/workflow/shell_script_step.rb +115 -0
- data/lib/roast/workflow/sqlite_state_repository.rb +17 -17
- data/lib/roast/workflow/state_manager.rb +8 -0
- data/lib/roast/workflow/step_executor_coordinator.rb +43 -8
- data/lib/roast/workflow/step_executor_with_reporting.rb +2 -2
- data/lib/roast/workflow/step_loader.rb +55 -9
- data/lib/roast/workflow/workflow_executor.rb +3 -4
- data/lib/roast/workflow/workflow_initializer.rb +95 -4
- data/lib/roast/workflow/workflow_runner.rb +2 -2
- data/lib/roast.rb +2 -0
- data/roast.gemspec +3 -2
- metadata +36 -18
- data/lib/roast/workflow/step_orchestrator.rb +0 -48
@@ -29,13 +29,13 @@ module Roast
|
|
29
29
|
|
30
30
|
session_id = ensure_session(workflow)
|
31
31
|
|
32
|
-
@db.execute(<<~SQL, session_id, state_data[:order], step_name, state_data.to_json)
|
32
|
+
@db.execute(<<~SQL, [session_id, state_data[:order], step_name, state_data.to_json])
|
33
33
|
INSERT INTO session_states (session_id, step_index, step_name, state_data)
|
34
34
|
VALUES (?, ?, ?, ?)
|
35
35
|
SQL
|
36
36
|
|
37
37
|
# Update session's current step
|
38
|
-
@db.execute(<<~SQL, state_data[:order], session_id)
|
38
|
+
@db.execute(<<~SQL, [state_data[:order], session_id])
|
39
39
|
UPDATE sessions#{" "}
|
40
40
|
SET current_step_index = ?, updated_at = CURRENT_TIMESTAMP
|
41
41
|
WHERE id = ?
|
@@ -49,7 +49,7 @@ module Roast
|
|
49
49
|
return false unless session_id
|
50
50
|
|
51
51
|
# Find the state before the target step
|
52
|
-
result = @db.execute(<<~SQL, session_id, step_name)
|
52
|
+
result = @db.execute(<<~SQL, [session_id, step_name])
|
53
53
|
SELECT state_data, step_name
|
54
54
|
FROM session_states
|
55
55
|
WHERE session_id = ?
|
@@ -64,7 +64,7 @@ module Roast
|
|
64
64
|
|
65
65
|
if result.empty?
|
66
66
|
# Try to find the latest state if target step doesn't exist
|
67
|
-
result = @db.execute(<<~SQL, session_id)
|
67
|
+
result = @db.execute(<<~SQL, [session_id])
|
68
68
|
SELECT state_data, step_name
|
69
69
|
FROM session_states
|
70
70
|
WHERE session_id = ?
|
@@ -95,7 +95,7 @@ module Roast
|
|
95
95
|
|
96
96
|
session_id = ensure_session(workflow)
|
97
97
|
|
98
|
-
@db.execute(<<~SQL, output_content, session_id)
|
98
|
+
@db.execute(<<~SQL, [output_content, session_id])
|
99
99
|
UPDATE sessions#{" "}
|
100
100
|
SET final_output = ?, status = 'completed', updated_at = CURRENT_TIMESTAMP
|
101
101
|
WHERE id = ?
|
@@ -130,7 +130,7 @@ module Roast
|
|
130
130
|
|
131
131
|
where_clause = conditions.empty? ? "" : "WHERE #{conditions.join(" AND ")}"
|
132
132
|
|
133
|
-
@db.execute(<<~SQL,
|
133
|
+
@db.execute(<<~SQL, params)
|
134
134
|
SELECT id, workflow_name, workflow_path, status, current_step_index,#{" "}
|
135
135
|
created_at, updated_at
|
136
136
|
FROM sessions
|
@@ -141,20 +141,20 @@ module Roast
|
|
141
141
|
end
|
142
142
|
|
143
143
|
def get_session_details(session_id)
|
144
|
-
session = @db.execute(<<~SQL, session_id).first
|
144
|
+
session = @db.execute(<<~SQL, [session_id]).first
|
145
145
|
SELECT * FROM sessions WHERE id = ?
|
146
146
|
SQL
|
147
147
|
|
148
148
|
return unless session
|
149
149
|
|
150
|
-
states = @db.execute(<<~SQL, session_id)
|
150
|
+
states = @db.execute(<<~SQL, [session_id])
|
151
151
|
SELECT step_index, step_name, created_at
|
152
152
|
FROM session_states
|
153
153
|
WHERE session_id = ?
|
154
154
|
ORDER BY step_index
|
155
155
|
SQL
|
156
156
|
|
157
|
-
events = @db.execute(<<~SQL, session_id)
|
157
|
+
events = @db.execute(<<~SQL, [session_id])
|
158
158
|
SELECT event_name, event_data, received_at
|
159
159
|
FROM session_events
|
160
160
|
WHERE session_id = ?
|
@@ -170,7 +170,7 @@ module Roast
|
|
170
170
|
|
171
171
|
def cleanup_old_sessions(older_than)
|
172
172
|
count = @db.changes
|
173
|
-
@db.execute(<<~SQL, "-#{older_than}")
|
173
|
+
@db.execute(<<~SQL, ["-#{older_than}"])
|
174
174
|
DELETE FROM sessions
|
175
175
|
WHERE created_at < datetime('now', ?)
|
176
176
|
SQL
|
@@ -181,7 +181,7 @@ module Roast
|
|
181
181
|
# Find the session if session_id not provided
|
182
182
|
unless session_id
|
183
183
|
workflow_name = File.basename(File.dirname(workflow_path))
|
184
|
-
result = @db.execute(<<~SQL, workflow_name, "waiting")
|
184
|
+
result = @db.execute(<<~SQL, [workflow_name, "waiting"])
|
185
185
|
SELECT id FROM sessions
|
186
186
|
WHERE workflow_name = ? AND status = ?
|
187
187
|
ORDER BY created_at DESC
|
@@ -194,13 +194,13 @@ module Roast
|
|
194
194
|
end
|
195
195
|
|
196
196
|
# Add the event
|
197
|
-
@db.execute(<<~SQL, session_id, event_name, event_data&.to_json)
|
197
|
+
@db.execute(<<~SQL, [session_id, event_name, event_data&.to_json])
|
198
198
|
INSERT INTO session_events (session_id, event_name, event_data)
|
199
199
|
VALUES (?, ?, ?)
|
200
200
|
SQL
|
201
201
|
|
202
202
|
# Update session status
|
203
|
-
@db.execute(<<~SQL, session_id)
|
203
|
+
@db.execute(<<~SQL, [session_id])
|
204
204
|
UPDATE sessions#{" "}
|
205
205
|
SET status = 'running', updated_at = CURRENT_TIMESTAMP
|
206
206
|
WHERE id = ?
|
@@ -272,14 +272,14 @@ module Roast
|
|
272
272
|
session_id = generate_session_id(workflow)
|
273
273
|
|
274
274
|
# Check if session exists
|
275
|
-
existing = @db.execute("SELECT id FROM sessions WHERE id = ?", session_id).first
|
275
|
+
existing = @db.execute("SELECT id FROM sessions WHERE id = ?", [session_id]).first
|
276
276
|
return session_id if existing
|
277
277
|
|
278
278
|
# Create new session
|
279
279
|
workflow_name = workflow.session_name || "unnamed"
|
280
280
|
workflow_path = workflow.file || "notarget"
|
281
281
|
|
282
|
-
@db.execute(<<~SQL, session_id, workflow_name, workflow_path)
|
282
|
+
@db.execute(<<~SQL, [session_id, workflow_name, workflow_path])
|
283
283
|
INSERT INTO sessions (id, workflow_name, workflow_path)
|
284
284
|
VALUES (?, ?, ?)
|
285
285
|
SQL
|
@@ -296,7 +296,7 @@ module Roast
|
|
296
296
|
workflow_name = workflow.session_name || "unnamed"
|
297
297
|
workflow_path = workflow.file || "notarget"
|
298
298
|
|
299
|
-
result = @db.execute(<<~SQL, workflow_name, workflow_path)
|
299
|
+
result = @db.execute(<<~SQL, [workflow_name, workflow_path])
|
300
300
|
SELECT id FROM sessions
|
301
301
|
WHERE workflow_name = ? AND workflow_path = ?
|
302
302
|
ORDER BY created_at DESC
|
@@ -324,7 +324,7 @@ module Roast
|
|
324
324
|
new_session_id = ensure_session(workflow)
|
325
325
|
|
326
326
|
# Copy states up to the target step
|
327
|
-
@db.execute(<<~SQL, new_session_id, source_session_id, target_step_name, source_session_id)
|
327
|
+
@db.execute(<<~SQL, [new_session_id, source_session_id, target_step_name, source_session_id])
|
328
328
|
INSERT INTO session_states (session_id, step_index, step_name, state_data)
|
329
329
|
SELECT ?, step_index, step_name, state_data
|
330
330
|
FROM session_states
|
@@ -42,6 +42,7 @@ module Roast
|
|
42
42
|
order: determine_step_order(step_name),
|
43
43
|
transcript: extract_transcript,
|
44
44
|
output: extract_output,
|
45
|
+
metadata: extract_metadata,
|
45
46
|
final_output: extract_final_output,
|
46
47
|
execution_order: extract_execution_order,
|
47
48
|
}
|
@@ -68,6 +69,13 @@ module Roast
|
|
68
69
|
workflow.output.clone
|
69
70
|
end
|
70
71
|
|
72
|
+
# Extract metadata if available
|
73
|
+
def extract_metadata
|
74
|
+
return {} unless workflow.respond_to?(:metadata)
|
75
|
+
|
76
|
+
workflow.metadata.clone
|
77
|
+
end
|
78
|
+
|
71
79
|
# Extract final output data if available
|
72
80
|
def extract_final_output
|
73
81
|
return [] unless workflow.respond_to?(:final_output)
|
@@ -38,7 +38,7 @@ module Roast
|
|
38
38
|
Kernel.binding.irb # rubocop:disable Lint/Debugger
|
39
39
|
end
|
40
40
|
else
|
41
|
-
|
41
|
+
execute_custom_step(step, is_last_step: is_last_step)
|
42
42
|
end
|
43
43
|
end
|
44
44
|
end
|
@@ -54,6 +54,10 @@ module Roast
|
|
54
54
|
# @return [Object] The result of the step execution
|
55
55
|
def execute(step, options = {})
|
56
56
|
step_type = StepTypeResolver.resolve(step, @context)
|
57
|
+
step_name = StepTypeResolver.extract_name(step)
|
58
|
+
|
59
|
+
Thread.current[:current_step_name] = step_name if step_name
|
60
|
+
Thread.current[:workflow_metadata] = @context.workflow.metadata
|
57
61
|
|
58
62
|
case step_type
|
59
63
|
when StepTypeResolver::COMMAND_STEP
|
@@ -126,14 +130,18 @@ module Roast
|
|
126
130
|
)
|
127
131
|
end
|
128
132
|
|
129
|
-
def step_orchestrator
|
130
|
-
dependencies[:step_orchestrator]
|
131
|
-
end
|
132
|
-
|
133
133
|
def error_handler
|
134
134
|
dependencies[:error_handler]
|
135
135
|
end
|
136
136
|
|
137
|
+
def step_loader
|
138
|
+
dependencies[:step_loader]
|
139
|
+
end
|
140
|
+
|
141
|
+
def state_manager
|
142
|
+
dependencies[:state_manager]
|
143
|
+
end
|
144
|
+
|
137
145
|
def execute_command_step(step, options)
|
138
146
|
exit_on_error = options.fetch(:exit_on_error, true)
|
139
147
|
resource_type = @context.resource_type
|
@@ -185,8 +193,11 @@ module Roast
|
|
185
193
|
step_name = StepTypeResolver.extract_name(step)
|
186
194
|
|
187
195
|
# Load and execute the agent step
|
188
|
-
|
189
|
-
|
196
|
+
merged_options = options.merge(
|
197
|
+
exit_on_error: options.fetch(:exit_on_error) { context.exit_on_error?(step) },
|
198
|
+
agent_type: :coding_agent,
|
199
|
+
)
|
200
|
+
execute_custom_step(step_name, **merged_options)
|
190
201
|
end
|
191
202
|
|
192
203
|
def execute_glob_step(step, options = {})
|
@@ -259,7 +270,7 @@ module Roast
|
|
259
270
|
exit_on_error = options.fetch(:exit_on_error, true)
|
260
271
|
step_key = options[:step_key]
|
261
272
|
is_last_step = options[:is_last_step]
|
262
|
-
|
273
|
+
execute_custom_step(step, exit_on_error:, step_key:, is_last_step:)
|
263
274
|
end
|
264
275
|
|
265
276
|
def validate_each_step!(step)
|
@@ -268,6 +279,30 @@ module Roast
|
|
268
279
|
"Invalid 'each' step format. 'as' and 'steps' must be at the same level as 'each'"
|
269
280
|
end
|
270
281
|
end
|
282
|
+
|
283
|
+
def execute_custom_step(name, step_key: nil, **options)
|
284
|
+
resource_type = @context.workflow.respond_to?(:resource) ? @context.workflow.resource&.type : nil
|
285
|
+
|
286
|
+
error_handler.with_error_handling(name, resource_type: resource_type) do
|
287
|
+
$stderr.puts "Executing: #{name} (Resource type: #{resource_type || "unknown"})"
|
288
|
+
|
289
|
+
# Use step_key for loading if provided, otherwise use name
|
290
|
+
load_key = step_key || name
|
291
|
+
is_last_step = options[:is_last_step]
|
292
|
+
step_object = step_loader.load(name, exit_on_error: false, step_key: load_key, is_last_step:, **options)
|
293
|
+
step_result = step_object.call
|
294
|
+
|
295
|
+
# Store result in workflow output
|
296
|
+
# Use step_key for output storage if provided (for hash steps)
|
297
|
+
output_key = step_key || name
|
298
|
+
@context.workflow.output[output_key] = step_result
|
299
|
+
|
300
|
+
# Save state after each step
|
301
|
+
state_manager.save_state(name, step_result)
|
302
|
+
|
303
|
+
step_result
|
304
|
+
end
|
305
|
+
end
|
271
306
|
end
|
272
307
|
end
|
273
308
|
end
|
@@ -11,12 +11,12 @@ module Roast
|
|
11
11
|
@name_extractor = StepNameExtractor.new
|
12
12
|
end
|
13
13
|
|
14
|
-
def execute(step, options
|
14
|
+
def execute(step, **options)
|
15
15
|
# Track tokens before execution
|
16
16
|
tokens_before = @context.workflow.context_manager&.total_tokens || 0
|
17
17
|
|
18
18
|
# Execute the step
|
19
|
-
result = @base_executor.execute(step, options)
|
19
|
+
result = @base_executor.execute(step, **options)
|
20
20
|
|
21
21
|
# Report token consumption after successful execution
|
22
22
|
tokens_after = @context.workflow.context_manager&.total_tokens || 0
|
@@ -60,10 +60,15 @@ module Roast
|
|
60
60
|
return step
|
61
61
|
end
|
62
62
|
|
63
|
-
# Look for Ruby file in various locations
|
64
|
-
|
65
|
-
if
|
66
|
-
|
63
|
+
# Look for Ruby or shell script file in various locations
|
64
|
+
step_file_info = find_step_file(name.to_s, per_step_path)
|
65
|
+
if step_file_info
|
66
|
+
case step_file_info[:type]
|
67
|
+
when :ruby
|
68
|
+
return load_ruby_step(step_file_info[:path], name.to_s, is_last_step:)
|
69
|
+
when :shell
|
70
|
+
return load_shell_script_step(step_file_info[:path], name.to_s, step_key, is_last_step:)
|
71
|
+
end
|
67
72
|
end
|
68
73
|
|
69
74
|
# Look for step directory
|
@@ -87,28 +92,40 @@ module Roast
|
|
87
92
|
File.expand_path(path, context_path)
|
88
93
|
end
|
89
94
|
|
90
|
-
# Find a Ruby step file in various locations
|
95
|
+
# Find a Ruby or shell script step file in various locations
|
91
96
|
def find_step_file(step_name, per_step_path = nil)
|
92
97
|
# Check in per-step path first
|
93
98
|
if per_step_path
|
94
99
|
resolved_per_step_path = resolve_path(per_step_path)
|
95
100
|
custom_rb_path = File.join(resolved_per_step_path, "#{step_name}.rb")
|
96
|
-
return custom_rb_path if File.file?(custom_rb_path)
|
101
|
+
return { path: custom_rb_path, type: :ruby } if File.file?(custom_rb_path)
|
102
|
+
|
103
|
+
custom_sh_path = File.join(resolved_per_step_path, "#{step_name}.sh")
|
104
|
+
return { path: custom_sh_path, type: :shell } if File.file?(custom_sh_path)
|
97
105
|
end
|
98
106
|
|
99
107
|
# Check in phase-specific directory first
|
100
108
|
if phase != :steps
|
101
109
|
phase_rb_path = File.join(context_path, phase.to_s, "#{step_name}.rb")
|
102
|
-
return phase_rb_path if File.file?(phase_rb_path)
|
110
|
+
return { path: phase_rb_path, type: :ruby } if File.file?(phase_rb_path)
|
111
|
+
|
112
|
+
phase_sh_path = File.join(context_path, phase.to_s, "#{step_name}.sh")
|
113
|
+
return { path: phase_sh_path, type: :shell } if File.file?(phase_sh_path)
|
103
114
|
end
|
104
115
|
|
105
116
|
# Check in context path
|
106
117
|
rb_file_path = File.join(context_path, "#{step_name}.rb")
|
107
|
-
return rb_file_path if File.file?(rb_file_path)
|
118
|
+
return { path: rb_file_path, type: :ruby } if File.file?(rb_file_path)
|
119
|
+
|
120
|
+
sh_file_path = File.join(context_path, "#{step_name}.sh")
|
121
|
+
return { path: sh_file_path, type: :shell } if File.file?(sh_file_path)
|
108
122
|
|
109
123
|
# Check in shared directory
|
110
124
|
shared_rb_path = File.expand_path(File.join(context_path, "..", "shared", "#{step_name}.rb"))
|
111
|
-
return shared_rb_path if File.file?(shared_rb_path)
|
125
|
+
return { path: shared_rb_path, type: :ruby } if File.file?(shared_rb_path)
|
126
|
+
|
127
|
+
shared_sh_path = File.expand_path(File.join(context_path, "..", "shared", "#{step_name}.sh"))
|
128
|
+
return { path: shared_sh_path, type: :shell } if File.file?(shared_sh_path)
|
112
129
|
|
113
130
|
nil
|
114
131
|
end
|
@@ -161,6 +178,23 @@ module Roast
|
|
161
178
|
step
|
162
179
|
end
|
163
180
|
|
181
|
+
# Load a shell script step from a file
|
182
|
+
def load_shell_script_step(file_path, step_name, step_key, is_last_step: nil)
|
183
|
+
$stderr.puts "Loading shell script step: #{file_path}"
|
184
|
+
|
185
|
+
step_name_obj = Roast::ValueObjects::StepName.new(step_name)
|
186
|
+
|
187
|
+
step = ShellScriptStep.new(
|
188
|
+
workflow,
|
189
|
+
script_path: file_path,
|
190
|
+
name: step_name_obj,
|
191
|
+
context_path: File.dirname(file_path),
|
192
|
+
)
|
193
|
+
|
194
|
+
configure_step(step, step_key || step_name, is_last_step:)
|
195
|
+
step
|
196
|
+
end
|
197
|
+
|
164
198
|
# Create and configure a step instance
|
165
199
|
def create_step_instance(step_class, step_name, context_path, options = {})
|
166
200
|
is_last_step = options[:is_last_step]
|
@@ -203,6 +237,18 @@ module Roast
|
|
203
237
|
if step_config.key?("available_tools")
|
204
238
|
step.available_tools = step_config["available_tools"]
|
205
239
|
end
|
240
|
+
|
241
|
+
# Apply any other configuration attributes that the step supports
|
242
|
+
step_config.each do |key, value|
|
243
|
+
# Skip keys we've already handled above
|
244
|
+
next if ["print_response", "json", "params", "coerce_to", "available_tools"].include?(key)
|
245
|
+
|
246
|
+
# Apply configuration if the step has a setter for this attribute
|
247
|
+
setter_method = "#{key}="
|
248
|
+
if step.respond_to?(setter_method)
|
249
|
+
step.public_send(setter_method, value)
|
250
|
+
end
|
251
|
+
end
|
206
252
|
end
|
207
253
|
end
|
208
254
|
end
|
@@ -42,7 +42,6 @@ module Roast
|
|
42
42
|
# @param state_manager [StateManager] Optional custom state manager
|
43
43
|
# @param iteration_executor [IterationExecutor] Optional custom iteration executor
|
44
44
|
# @param conditional_executor [ConditionalExecutor] Optional custom conditional executor
|
45
|
-
# @param step_orchestrator [StepOrchestrator] Optional custom step orchestrator
|
46
45
|
# @param step_executor_coordinator [StepExecutorCoordinator] Optional custom step executor coordinator
|
47
46
|
# @param phase [Symbol] The execution phase - determines where to load steps from
|
48
47
|
# Valid values:
|
@@ -52,7 +51,7 @@ module Roast
|
|
52
51
|
def initialize(workflow, config_hash, context_path,
|
53
52
|
error_handler: nil, step_loader: nil, command_executor: nil,
|
54
53
|
interpolator: nil, state_manager: nil, iteration_executor: nil,
|
55
|
-
conditional_executor: nil,
|
54
|
+
conditional_executor: nil, step_executor_coordinator: nil,
|
56
55
|
phase: :steps)
|
57
56
|
# Create context object to reduce data clump
|
58
57
|
@context = WorkflowContext.new(
|
@@ -69,7 +68,6 @@ module Roast
|
|
69
68
|
@state_manager = state_manager || StateManager.new(workflow, logger: @error_handler, storage_type: workflow.storage_type)
|
70
69
|
@iteration_executor = iteration_executor || IterationExecutor.new(workflow, context_path, @state_manager, config_hash)
|
71
70
|
@conditional_executor = conditional_executor || ConditionalExecutor.new(workflow, context_path, @state_manager, self)
|
72
|
-
@step_orchestrator = step_orchestrator || StepOrchestrator.new(workflow, @step_loader, @state_manager, @error_handler, self)
|
73
71
|
|
74
72
|
# Initialize coordinator with dependencies
|
75
73
|
base_coordinator = step_executor_coordinator || StepExecutorCoordinator.new(
|
@@ -80,7 +78,8 @@ module Roast
|
|
80
78
|
command_executor: @command_executor,
|
81
79
|
iteration_executor: @iteration_executor,
|
82
80
|
conditional_executor: @conditional_executor,
|
83
|
-
|
81
|
+
step_loader: @step_loader,
|
82
|
+
state_manager: @state_manager,
|
84
83
|
error_handler: @error_handler,
|
85
84
|
},
|
86
85
|
)
|
@@ -10,6 +10,7 @@ module Roast
|
|
10
10
|
|
11
11
|
def setup
|
12
12
|
load_roast_initializers
|
13
|
+
check_raix_configuration
|
13
14
|
include_tools
|
14
15
|
configure_api_client
|
15
16
|
end
|
@@ -20,18 +21,108 @@ module Roast
|
|
20
21
|
Roast::Initializers.load_all
|
21
22
|
end
|
22
23
|
|
24
|
+
def check_raix_configuration
|
25
|
+
# Skip check in test environment
|
26
|
+
return if ENV["RAILS_ENV"] == "test" || ENV["RACK_ENV"] == "test" || defined?(Minitest)
|
27
|
+
|
28
|
+
# Only check if the workflow has steps that would need API access
|
29
|
+
return if @configuration.steps.empty?
|
30
|
+
|
31
|
+
# Check if Raix has been configured with the appropriate client
|
32
|
+
case @configuration.api_provider
|
33
|
+
when :openai
|
34
|
+
if Raix.configuration.openai_client.nil?
|
35
|
+
warn_about_missing_raix_configuration(:openai)
|
36
|
+
end
|
37
|
+
when :openrouter
|
38
|
+
if Raix.configuration.openrouter_client.nil?
|
39
|
+
warn_about_missing_raix_configuration(:openrouter)
|
40
|
+
end
|
41
|
+
when nil
|
42
|
+
# If no api_provider is set but we have steps that might need API access,
|
43
|
+
# check if any client is configured
|
44
|
+
if Raix.configuration.openai_client.nil? && Raix.configuration.openrouter_client.nil?
|
45
|
+
warn_about_missing_raix_configuration(:any)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def warn_about_missing_raix_configuration(provider)
|
51
|
+
::CLI::UI.frame_style = :box
|
52
|
+
::CLI::UI::Frame.open("{{red:Raix Configuration Missing}}", color: :red) do
|
53
|
+
case provider
|
54
|
+
when :openai
|
55
|
+
puts ::CLI::UI.fmt("{{yellow:⚠️ Warning: Raix OpenAI client is not configured!}}")
|
56
|
+
when :openrouter
|
57
|
+
puts ::CLI::UI.fmt("{{yellow:⚠️ Warning: Raix OpenRouter client is not configured!}}")
|
58
|
+
else
|
59
|
+
puts ::CLI::UI.fmt("{{yellow:⚠️ Warning: Raix is not configured!}}")
|
60
|
+
end
|
61
|
+
puts
|
62
|
+
puts "Roast requires Raix to be properly initialized to make API calls."
|
63
|
+
puts ::CLI::UI.fmt("To fix this, create a file at {{cyan:.roast/initializers/raix.rb}} with:")
|
64
|
+
puts
|
65
|
+
puts ::CLI::UI.fmt("{{cyan:# frozen_string_literal: true}}")
|
66
|
+
puts
|
67
|
+
puts ::CLI::UI.fmt("{{cyan:require \"raix\"}}")
|
68
|
+
|
69
|
+
if provider == :openrouter
|
70
|
+
puts ::CLI::UI.fmt("{{cyan:require \"open_router\"}}")
|
71
|
+
puts
|
72
|
+
puts ::CLI::UI.fmt("{{cyan:Raix.configure do |config|}}")
|
73
|
+
puts ::CLI::UI.fmt("{{cyan: config.openrouter_client = OpenRouter::Client.new(}}")
|
74
|
+
puts ::CLI::UI.fmt("{{cyan: access_token: ENV.fetch(\"OPENROUTER_API_KEY\"),}}")
|
75
|
+
puts ::CLI::UI.fmt("{{cyan: uri_base: \"https://openrouter.ai/api/v1\",}}")
|
76
|
+
puts ::CLI::UI.fmt("{{cyan: )}}")
|
77
|
+
else
|
78
|
+
puts
|
79
|
+
puts ::CLI::UI.fmt("{{cyan:faraday_retry = false}}")
|
80
|
+
puts ::CLI::UI.fmt("{{cyan:begin}}")
|
81
|
+
puts ::CLI::UI.fmt("{{cyan: require \"faraday/retry\"}}")
|
82
|
+
puts ::CLI::UI.fmt("{{cyan: faraday_retry = true}}")
|
83
|
+
puts ::CLI::UI.fmt("{{cyan:rescue LoadError}}")
|
84
|
+
puts ::CLI::UI.fmt("{{cyan: # Do nothing}}")
|
85
|
+
puts ::CLI::UI.fmt("{{cyan:end}}")
|
86
|
+
puts
|
87
|
+
puts ::CLI::UI.fmt("{{cyan:Raix.configure do |config|}}")
|
88
|
+
puts ::CLI::UI.fmt("{{cyan: config.openai_client = OpenAI::Client.new(}}")
|
89
|
+
puts ::CLI::UI.fmt("{{cyan: access_token: ENV.fetch(\"OPENAI_API_KEY\"),}}")
|
90
|
+
puts ::CLI::UI.fmt("{{cyan: uri_base: \"https://api.openai.com/v1\",}}")
|
91
|
+
puts ::CLI::UI.fmt("{{cyan: ) do |f|}}")
|
92
|
+
puts ::CLI::UI.fmt("{{cyan: if faraday_retry}}")
|
93
|
+
puts ::CLI::UI.fmt("{{cyan: f.request(:retry, {}}")
|
94
|
+
puts ::CLI::UI.fmt("{{cyan: max: 2,}}")
|
95
|
+
puts ::CLI::UI.fmt("{{cyan: interval: 0.05,}}")
|
96
|
+
puts ::CLI::UI.fmt("{{cyan: interval_randomness: 0.5,}}")
|
97
|
+
puts ::CLI::UI.fmt("{{cyan: backoff_factor: 2,}}")
|
98
|
+
puts ::CLI::UI.fmt("{{cyan: })}}")
|
99
|
+
puts ::CLI::UI.fmt("{{cyan: end}}")
|
100
|
+
puts ::CLI::UI.fmt("{{cyan: end}}")
|
101
|
+
end
|
102
|
+
puts ::CLI::UI.fmt("{{cyan:end}}")
|
103
|
+
puts
|
104
|
+
puts "For Shopify users, you need to use the LLM gateway proxy instead."
|
105
|
+
puts "Check the #roast slack channel for more information."
|
106
|
+
puts
|
107
|
+
end
|
108
|
+
raise ::CLI::Kit::Abort, "Please configure Raix before running workflows."
|
109
|
+
end
|
110
|
+
|
23
111
|
def include_tools
|
24
112
|
return unless @configuration.tools.present? || @configuration.mcp_tools.present?
|
25
113
|
|
26
|
-
|
27
|
-
BaseWorkflow.include(
|
114
|
+
# Only include modules if they haven't been included already to avoid method redefinition warnings
|
115
|
+
BaseWorkflow.include(Raix::FunctionDispatch) unless BaseWorkflow.included_modules.include?(Raix::FunctionDispatch)
|
116
|
+
BaseWorkflow.include(Roast::Helpers::FunctionCachingInterceptor) unless BaseWorkflow.included_modules.include?(Roast::Helpers::FunctionCachingInterceptor)
|
28
117
|
|
29
118
|
if @configuration.tools.present?
|
30
|
-
|
119
|
+
@configuration.tools.map(&:constantize).each do |tool|
|
120
|
+
BaseWorkflow.include(tool) unless BaseWorkflow.included_modules.include?(tool)
|
121
|
+
end
|
31
122
|
end
|
32
123
|
|
33
124
|
if @configuration.mcp_tools.present?
|
34
|
-
BaseWorkflow.include(Raix::MCP)
|
125
|
+
BaseWorkflow.include(Raix::MCP) unless BaseWorkflow.included_modules.include?(Raix::MCP)
|
35
126
|
|
36
127
|
# Create an interpolator for MCP tool configuration
|
37
128
|
# We use Object.new as the context because this interpolation happens during
|
@@ -91,11 +91,11 @@ module Roast
|
|
91
91
|
executor = WorkflowExecutor.new(workflow, @configuration.config_hash, @configuration.context_path)
|
92
92
|
executor.execute_steps(steps)
|
93
93
|
|
94
|
-
$stderr.puts "🔥🔥🔥 ROAST COMPLETE! 🔥🔥🔥"
|
95
|
-
|
96
94
|
# Save outputs
|
97
95
|
@output_handler.save_final_output(workflow)
|
98
96
|
@output_handler.write_results(workflow)
|
97
|
+
|
98
|
+
$stderr.puts "🔥🔥🔥 ROAST COMPLETE! 🔥🔥🔥"
|
99
99
|
end
|
100
100
|
|
101
101
|
private
|
data/lib/roast.rb
CHANGED
@@ -11,6 +11,7 @@ require "net/http"
|
|
11
11
|
require "open3"
|
12
12
|
require "pathname"
|
13
13
|
require "securerandom"
|
14
|
+
require "shellwords"
|
14
15
|
require "tempfile"
|
15
16
|
require "uri"
|
16
17
|
require "yaml"
|
@@ -25,6 +26,7 @@ require "active_support/core_ext/string/inflections"
|
|
25
26
|
require "active_support/isolated_execution_state"
|
26
27
|
require "active_support/notifications"
|
27
28
|
require "cli/ui"
|
29
|
+
require "cli/kit"
|
28
30
|
require "diff/lcs"
|
29
31
|
require "json-schema"
|
30
32
|
require "raix"
|
data/roast.gemspec
CHANGED
@@ -37,13 +37,14 @@ Gem::Specification.new do |spec|
|
|
37
37
|
spec.require_paths = ["lib"]
|
38
38
|
|
39
39
|
spec.add_dependency("activesupport", ">= 7.0")
|
40
|
+
spec.add_dependency("cli-kit", "~> 5.0")
|
40
41
|
spec.add_dependency("cli-ui", "2.3.0")
|
41
42
|
spec.add_dependency("diff-lcs", "~> 1.5")
|
42
|
-
spec.add_dependency("faraday-retry")
|
43
43
|
spec.add_dependency("json-schema")
|
44
44
|
spec.add_dependency("open_router", "~> 0.3")
|
45
|
-
spec.add_dependency("raix", "~> 1.0")
|
45
|
+
spec.add_dependency("raix-openai-eight", "~> 1.0")
|
46
46
|
spec.add_dependency("ruby-graphviz", "~> 1.2")
|
47
|
+
spec.add_dependency("sqlite3", "~> 2.6")
|
47
48
|
spec.add_dependency("thor", "~> 1.3")
|
48
49
|
spec.add_dependency("zeitwerk", "~> 2.6")
|
49
50
|
end
|