roast-ai 0.1.0 → 0.1.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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/cla.yml +1 -1
  3. data/.gitignore +1 -0
  4. data/CHANGELOG.md +20 -0
  5. data/CLAUDE.md +3 -1
  6. data/Gemfile +0 -1
  7. data/Gemfile.lock +3 -4
  8. data/README.md +418 -4
  9. data/Rakefile +1 -6
  10. data/docs/INSTRUMENTATION.md +202 -0
  11. data/examples/api_workflow/README.md +85 -0
  12. data/examples/api_workflow/fetch_api_data/prompt.md +10 -0
  13. data/examples/api_workflow/generate_report/prompt.md +10 -0
  14. data/examples/api_workflow/prompt.md +10 -0
  15. data/examples/api_workflow/transform_data/prompt.md +10 -0
  16. data/examples/api_workflow/workflow.yml +30 -0
  17. data/examples/grading/workflow.yml +2 -2
  18. data/examples/instrumentation.rb +76 -0
  19. data/examples/rspec_to_minitest/README.md +68 -0
  20. data/examples/rspec_to_minitest/analyze_spec/prompt.md +30 -0
  21. data/examples/rspec_to_minitest/create_minitest/prompt.md +33 -0
  22. data/examples/rspec_to_minitest/run_and_improve/prompt.md +35 -0
  23. data/examples/rspec_to_minitest/workflow.md +10 -0
  24. data/examples/rspec_to_minitest/workflow.yml +40 -0
  25. data/lib/roast/helpers/function_caching_interceptor.rb +72 -8
  26. data/lib/roast/helpers/prompt_loader.rb +2 -0
  27. data/lib/roast/resources/api_resource.rb +137 -0
  28. data/lib/roast/resources/base_resource.rb +47 -0
  29. data/lib/roast/resources/directory_resource.rb +40 -0
  30. data/lib/roast/resources/file_resource.rb +33 -0
  31. data/lib/roast/resources/none_resource.rb +29 -0
  32. data/lib/roast/resources/url_resource.rb +63 -0
  33. data/lib/roast/resources.rb +100 -0
  34. data/lib/roast/tools/coding_agent.rb +69 -0
  35. data/lib/roast/tools.rb +1 -0
  36. data/lib/roast/version.rb +1 -1
  37. data/lib/roast/workflow/base_step.rb +21 -17
  38. data/lib/roast/workflow/base_workflow.rb +49 -16
  39. data/lib/roast/workflow/configuration.rb +83 -8
  40. data/lib/roast/workflow/configuration_parser.rb +171 -3
  41. data/lib/roast/workflow/file_state_repository.rb +126 -0
  42. data/lib/roast/workflow/prompt_step.rb +16 -0
  43. data/lib/roast/workflow/session_manager.rb +82 -0
  44. data/lib/roast/workflow/state_repository.rb +21 -0
  45. data/lib/roast/workflow/workflow_executor.rb +99 -9
  46. data/lib/roast/workflow.rb +4 -0
  47. data/lib/roast.rb +2 -5
  48. data/roast.gemspec +1 -1
  49. data/schema/workflow.json +12 -0
  50. metadata +31 -6
  51. data/.rspec +0 -1
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roast
4
+ module Workflow
5
+ class PromptStep < BaseStep
6
+ def initialize(workflow, **kwargs)
7
+ super(workflow, **kwargs)
8
+ end
9
+
10
+ def call
11
+ prompt(name)
12
+ chat_completion(auto_loop: false, print_response: true)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "digest"
5
+
6
+ module Roast
7
+ module Workflow
8
+ # Manages session creation, timestamping, and directory management
9
+ class SessionManager
10
+ def initialize
11
+ @session_mutex = Mutex.new
12
+ @session_timestamps = {}
13
+ end
14
+
15
+ # Get or create a session directory for the workflow
16
+ def ensure_session_directory(workflow_id, session_name, file_path, timestamp: nil)
17
+ @session_mutex.synchronize do
18
+ # Create or get the workflow directory
19
+ workflow_dir = workflow_directory(session_name, file_path)
20
+ FileUtils.mkdir_p(workflow_dir)
21
+
22
+ # Ensure .gitignore exists
23
+ gitignore_path = File.join(workflow_dir, ".gitignore")
24
+ File.write(gitignore_path, "*") unless File.exist?(gitignore_path)
25
+
26
+ # Get or create session timestamp
27
+ session_timestamp = timestamp || @session_timestamps[workflow_id] || create_new_session(workflow_id)
28
+
29
+ # Create session directory
30
+ session_dir = File.join(workflow_dir, session_timestamp)
31
+ FileUtils.mkdir_p(session_dir)
32
+ session_dir
33
+ end
34
+ end
35
+
36
+ # Find a session directory for the workflow
37
+ def find_session_directory(session_name, file_path, timestamp = nil)
38
+ workflow_dir = workflow_directory(session_name, file_path)
39
+ return unless File.directory?(workflow_dir)
40
+
41
+ if timestamp
42
+ session_dir = File.join(workflow_dir, timestamp)
43
+ File.directory?(session_dir) ? session_dir : nil
44
+ else
45
+ find_latest_session_directory(workflow_dir)
46
+ end
47
+ end
48
+
49
+ # Get the session timestamp for a workflow
50
+ def session_timestamp(workflow_id)
51
+ @session_timestamps[workflow_id]
52
+ end
53
+
54
+ # Set the session timestamp for a workflow
55
+ def set_session_timestamp(workflow_id, timestamp)
56
+ @session_timestamps[workflow_id] = timestamp
57
+ end
58
+
59
+ # Create a new session for a workflow
60
+ def create_new_session(workflow_id)
61
+ timestamp = Time.now.utc.strftime("%Y%m%d_%H%M%S_%L")
62
+ @session_timestamps[workflow_id] = timestamp
63
+ timestamp
64
+ end
65
+
66
+ private
67
+
68
+ def workflow_directory(session_name, file_path)
69
+ workflow_dir_name = session_name.parameterize.underscore
70
+ file_id = Digest::MD5.hexdigest(file_path)
71
+ file_basename = File.basename(file_path).parameterize.underscore
72
+ human_readable_id = "#{file_basename}_#{file_id[0..7]}"
73
+ File.join(Dir.pwd, ".roast", "sessions", workflow_dir_name, human_readable_id)
74
+ end
75
+
76
+ def find_latest_session_directory(workflow_dir)
77
+ sessions = Dir.children(workflow_dir).sort.reverse
78
+ sessions.empty? ? nil : File.join(workflow_dir, sessions.first)
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roast
4
+ module Workflow
5
+ # Interface for state persistence operations
6
+ # Handles saving and loading workflow state in a thread-safe manner
7
+ class StateRepository
8
+ def save_state(workflow, step_name, state_data)
9
+ raise NotImplementedError, "#{self.class} must implement save_state"
10
+ end
11
+
12
+ def load_state_before_step(workflow, step_name, timestamp: nil)
13
+ raise NotImplementedError, "#{self.class} must implement load_state_before_step"
14
+ end
15
+
16
+ def save_final_output(workflow, output_content)
17
+ raise NotImplementedError, "#{self.class} must implement save_final_output"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support"
4
+ require "active_support/isolated_execution_state"
5
+ require "active_support/notifications"
6
+
3
7
  module Roast
4
8
  module Workflow
5
9
  # Handles the execution of workflow steps, including orchestration and threading
@@ -30,16 +34,60 @@ module Roast
30
34
  end
31
35
 
32
36
  def execute_step(name)
33
- $stderr.puts "Executing: #{name}"
34
- return strip_and_execute(name) if name.starts_with?("%") || name.starts_with?("$(")
37
+ start_time = Time.now
38
+ # For tests, make sure that we handle this gracefully
39
+ resource_type = workflow.respond_to?(:resource) ? workflow.resource&.type : nil
40
+
41
+ ActiveSupport::Notifications.instrument("roast.step.start", {
42
+ step_name: name,
43
+ resource_type: resource_type,
44
+ })
45
+
46
+ $stderr.puts "Executing: #{name} (Resource type: #{resource_type || "unknown"})"
47
+
48
+ result = if name.starts_with?("$(")
49
+ strip_and_execute(name).tap do |output|
50
+ # Add the command and output to the transcript for reference in following steps
51
+ workflow.transcript << { user: "I just executed the following command: ```\n#{name}\n```\n\nHere is the output:\n\n```\n#{output}\n```" }
52
+ workflow.transcript << { assistant: "Noted, thank you." }
53
+ end
54
+ elsif name.include?("*") && (!workflow.respond_to?(:resource) || !workflow.resource)
55
+ # Only use the glob method if we don't have a resource object yet
56
+ # This is for backward compatibility
57
+ glob(name)
58
+ else
59
+ step_object = find_and_load_step(name)
60
+ step_result = step_object.call
61
+ workflow.output[name] = step_result
62
+
63
+ # Save state after each step if the workflow supports it
64
+ save_state(name, step_result) if workflow.respond_to?(:session_name) && workflow.session_name
35
65
 
36
- return glob(name) if name.include?("*")
66
+ step_result
67
+ end
68
+
69
+ execution_time = Time.now - start_time
37
70
 
38
- step_object = find_and_load_step(name)
39
- result = step_object.call
71
+ ActiveSupport::Notifications.instrument("roast.step.complete", {
72
+ step_name: name,
73
+ resource_type: resource_type,
74
+ success: true,
75
+ execution_time: execution_time,
76
+ result_size: result.to_s.length,
77
+ })
40
78
 
41
- workflow.output[name] = result
42
79
  result
80
+ rescue => e
81
+ execution_time = Time.now - start_time
82
+
83
+ ActiveSupport::Notifications.instrument("roast.step.error", {
84
+ step_name: name,
85
+ resource_type: resource_type,
86
+ error: e.class.name,
87
+ message: e.message,
88
+ execution_time: execution_time,
89
+ })
90
+ raise
43
91
  end
44
92
 
45
93
  private
@@ -66,6 +114,11 @@ module Roast
66
114
  end
67
115
 
68
116
  def find_and_load_step(step_name)
117
+ # First check for a prompt step
118
+ if step_name.strip.include?(" ")
119
+ return Roast::Workflow::PromptStep.new(workflow, name: step_name, auto_loop: false)
120
+ end
121
+
69
122
  # First check for a ruby file with the step name
70
123
  rb_file_path = File.join(context_path, "#{step_name}.rb")
71
124
  if File.file?(rb_file_path)
@@ -100,8 +153,15 @@ module Roast
100
153
  def setup_step(step_class, step_name, context_path)
101
154
  step_class.new(workflow, name: step_name, context_path: context_path).tap do |step|
102
155
  step_config = config_hash[step_name]
156
+
157
+ # Always set the model, even if there's no step_config
158
+ # Use step-specific model if defined, otherwise use workflow default model, or fallback to DEFAULT_MODEL
159
+ step.model = step_config&.dig("model") || config_hash["model"] || DEFAULT_MODEL
160
+
161
+ # Pass resource to step if supported
162
+ step.resource = workflow.resource if step.respond_to?(:resource=)
163
+
103
164
  if step_config.present?
104
- step.model = step_config["model"] || DEFAULT_MODEL
105
165
  step.print_response = step_config["print_response"] if step_config["print_response"].present?
106
166
  step.loop = step_config["loop"] if step_config["loop"].present?
107
167
  step.json = step_config["json"] if step_config["json"].present?
@@ -111,8 +171,38 @@ module Roast
111
171
  end
112
172
 
113
173
  def strip_and_execute(step)
114
- command = step.gsub("%", "")
115
- %x(#{command})
174
+ if step.match?(/^\$\((.*)\)$/)
175
+ command = step.strip.match(/^\$\((.*)\)$/)[1]
176
+ %x(#{command})
177
+ else
178
+ raise "Missing closing parentheses: #{step}"
179
+ end
180
+ end
181
+
182
+ def save_state(step_name, step_result)
183
+ state_repository = FileStateRepository.new
184
+
185
+ # Gather necessary data for state
186
+ static_data = workflow.respond_to?(:transcript) ? workflow.transcript.map(&:itself) : []
187
+
188
+ # Get output and final_output if available
189
+ output = workflow.respond_to?(:output) ? workflow.output.clone : {}
190
+ final_output = workflow.respond_to?(:final_output) ? workflow.final_output.clone : []
191
+
192
+ state_data = {
193
+ step_name: step_name,
194
+ order: output.keys.index(step_name) || output.size,
195
+ transcript: static_data,
196
+ output: output,
197
+ final_output: final_output,
198
+ execution_order: output.keys,
199
+ }
200
+
201
+ # Save the state
202
+ state_repository.save_state(workflow, step_name, state_data)
203
+ rescue => e
204
+ # Don't fail the workflow if state saving fails
205
+ $stderr.puts "Warning: Failed to save workflow state: #{e.message}"
116
206
  end
117
207
  end
118
208
  end
@@ -1,11 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "roast/workflow/base_step"
4
+ require "roast/workflow/prompt_step"
4
5
  require "roast/workflow/base_workflow"
5
6
  require "roast/workflow/configuration"
6
7
  require "roast/workflow/workflow_executor"
7
8
  require "roast/workflow/configuration_parser"
8
9
  require "roast/workflow/validator"
10
+ require "roast/workflow/state_repository"
11
+ require "roast/workflow/session_manager"
12
+ require "roast/workflow/file_state_repository"
9
13
 
10
14
  module Roast
11
15
  module Workflow
data/lib/roast.rb CHANGED
@@ -5,6 +5,7 @@ require "thor"
5
5
  require "roast/version"
6
6
  require "roast/tools"
7
7
  require "roast/helpers"
8
+ require "roast/resources"
8
9
  require "roast/workflow"
9
10
 
10
11
  module Roast
@@ -16,7 +17,7 @@ module Roast
16
17
  option :output, type: :string, aliases: "-o", desc: "Save results to a file"
17
18
  option :verbose, type: :boolean, aliases: "-v", desc: "Show output from all steps as they are executed"
18
19
  option :target, type: :string, aliases: "-t", desc: "Override target files. Can be file path, glob pattern, or $(shell command)"
19
- option :subject, type: :string, aliases: "-s", desc: "Subject file to analyze"
20
+ option :replay, type: :string, aliases: "-r", desc: "Resume workflow from a specific step. Format: step_name or session_timestamp:step_name"
20
21
  def execute(*paths)
21
22
  raise Thor::Error, "Workflow configuration file is required" if paths.empty?
22
23
 
@@ -24,10 +25,6 @@ module Roast
24
25
  expanded_workflow_path = File.expand_path(workflow_path)
25
26
  raise Thor::Error, "Expected a Roast workflow configuration file, got directory: #{expanded_workflow_path}" if File.directory?(expanded_workflow_path)
26
27
 
27
- if options[:subject] && !File.exist?(options[:subject])
28
- raise Thor::Error, "Subject file does not exist: #{options[:subject]}"
29
- end
30
-
31
28
  Roast::Workflow::ConfigurationParser.new(expanded_workflow_path, files, options.transform_keys(&:to_sym)).begin!
32
29
  end
33
30
 
data/roast.gemspec CHANGED
@@ -39,6 +39,6 @@ Gem::Specification.new do |spec|
39
39
  spec.add_dependency("activesupport", "~> 8.0")
40
40
  spec.add_dependency("faraday-retry")
41
41
  spec.add_dependency("json-schema")
42
- spec.add_dependency("raix", "0.8.3")
42
+ spec.add_dependency("raix", "~> 0.8.4")
43
43
  spec.add_dependency("thor", "~> 1.3")
44
44
  end
data/schema/workflow.json CHANGED
@@ -11,6 +11,18 @@
11
11
  "type": "string"
12
12
  }
13
13
  },
14
+ "target": {
15
+ "type": "string",
16
+ "description": "Optional target file, glob pattern, or shell command for the workflow to operate on"
17
+ },
18
+ "api_token": {
19
+ "type": "string",
20
+ "description": "Shell command to fetch an API token dynamically, e.g. $(cat ~/.my-token)"
21
+ },
22
+ "model": {
23
+ "type": "string",
24
+ "description": "Default AI model to use for all steps in the workflow"
25
+ },
14
26
  "inputs": {
15
27
  "type": "array",
16
28
  "items": {
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: roast-ai
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
@@ -55,16 +55,16 @@ dependencies:
55
55
  name: raix
56
56
  requirement: !ruby/object:Gem::Requirement
57
57
  requirements:
58
- - - '='
58
+ - - "~>"
59
59
  - !ruby/object:Gem::Version
60
- version: 0.8.3
60
+ version: 0.8.4
61
61
  type: :runtime
62
62
  prerelease: false
63
63
  version_requirements: !ruby/object:Gem::Requirement
64
64
  requirements:
65
- - - '='
65
+ - - "~>"
66
66
  - !ruby/object:Gem::Version
67
- version: 0.8.3
67
+ version: 0.8.4
68
68
  - !ruby/object:Gem::Dependency
69
69
  name: thor
70
70
  requirement: !ruby/object:Gem::Requirement
@@ -92,7 +92,6 @@ files:
92
92
  - ".github/workflows/ci.yaml"
93
93
  - ".github/workflows/cla.yml"
94
94
  - ".gitignore"
95
- - ".rspec"
96
95
  - ".rubocop.yml"
97
96
  - ".ruby-version"
98
97
  - CHANGELOG.md
@@ -105,6 +104,13 @@ files:
105
104
  - README.md
106
105
  - Rakefile
107
106
  - bin/console
107
+ - docs/INSTRUMENTATION.md
108
+ - examples/api_workflow/README.md
109
+ - examples/api_workflow/fetch_api_data/prompt.md
110
+ - examples/api_workflow/generate_report/prompt.md
111
+ - examples/api_workflow/prompt.md
112
+ - examples/api_workflow/transform_data/prompt.md
113
+ - examples/api_workflow/workflow.yml
108
114
  - examples/grading/analyze_coverage/prompt.md
109
115
  - examples/grading/calculate_final_grade.rb
110
116
  - examples/grading/format_result.rb
@@ -118,6 +124,13 @@ files:
118
124
  - examples/grading/workflow.rb.md
119
125
  - examples/grading/workflow.ts+tsx.md
120
126
  - examples/grading/workflow.yml
127
+ - examples/instrumentation.rb
128
+ - examples/rspec_to_minitest/README.md
129
+ - examples/rspec_to_minitest/analyze_spec/prompt.md
130
+ - examples/rspec_to_minitest/create_minitest/prompt.md
131
+ - examples/rspec_to_minitest/run_and_improve/prompt.md
132
+ - examples/rspec_to_minitest/workflow.md
133
+ - examples/rspec_to_minitest/workflow.yml
121
134
  - exe/roast
122
135
  - lib/roast.rb
123
136
  - lib/roast/helpers.rb
@@ -126,8 +139,16 @@ files:
126
139
  - lib/roast/helpers/minitest_coverage_runner.rb
127
140
  - lib/roast/helpers/path_resolver.rb
128
141
  - lib/roast/helpers/prompt_loader.rb
142
+ - lib/roast/resources.rb
143
+ - lib/roast/resources/api_resource.rb
144
+ - lib/roast/resources/base_resource.rb
145
+ - lib/roast/resources/directory_resource.rb
146
+ - lib/roast/resources/file_resource.rb
147
+ - lib/roast/resources/none_resource.rb
148
+ - lib/roast/resources/url_resource.rb
129
149
  - lib/roast/tools.rb
130
150
  - lib/roast/tools/cmd.rb
151
+ - lib/roast/tools/coding_agent.rb
131
152
  - lib/roast/tools/grep.rb
132
153
  - lib/roast/tools/read_file.rb
133
154
  - lib/roast/tools/search_file.rb
@@ -138,6 +159,10 @@ files:
138
159
  - lib/roast/workflow/base_workflow.rb
139
160
  - lib/roast/workflow/configuration.rb
140
161
  - lib/roast/workflow/configuration_parser.rb
162
+ - lib/roast/workflow/file_state_repository.rb
163
+ - lib/roast/workflow/prompt_step.rb
164
+ - lib/roast/workflow/session_manager.rb
165
+ - lib/roast/workflow/state_repository.rb
141
166
  - lib/roast/workflow/validator.rb
142
167
  - lib/roast/workflow/workflow_executor.rb
143
168
  - roast.gemspec
data/.rspec DELETED
@@ -1 +0,0 @@
1
- --require spec_helper