roast-ai 0.1.3 → 0.1.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e40b7ab08d8d4c70d9d884c2c76027ad1426bcdacbae92121b162cc33aa45548
4
- data.tar.gz: 936ab6e40dfbfbcef29f68edbb1a7ea41a895d719782f2125c24f47e71005eeb
3
+ metadata.gz: 9bdaada020320af4c7fead86184a7cd0b1c06a89e88bc15ac424b31f4b449fb6
4
+ data.tar.gz: 3f83f410a1f21992de59b19b8e2f0ea402b9fe50b314b82514723cf20f9a599f
5
5
  SHA512:
6
- metadata.gz: 7c8de8b0ac1ee9496a869d056fb75de2101b4f1b674bde76c703a913ae749d22c650b44c783e8ebff4127862baa168e4ba1680f1e62bd60574068ba53c88f154
7
- data.tar.gz: f770140fee92dd287d171c52b61d437bbbb3ad7bbe0edc121c68cdb71d4b7628ebbbe1e56946cd8a251ecb991e803e79e62f7d084d59a0efc0efc2ce30b13769
6
+ metadata.gz: c6dfdd57104e363735a6e24bf5beb43a042340f582f94d8126c3f8a0d30dd92b031c5201a7d3d136bfcd3b7095a51c49329b08c07c5c19544240b18e20ad9948
7
+ data.tar.gz: 02aec8d620c3fe0c57d0d4e06790a7513a4a1aa44f68462721b6aa7da3f22f377598b4c00453e4ec825b0adcac3c335d470f945599d39571edb67e8a380597ac
@@ -16,7 +16,7 @@ jobs:
16
16
  fail-fast: false
17
17
  matrix:
18
18
  os: [ubuntu]
19
- ruby: ['3.4', 'ruby-head']
19
+ ruby: ['3.4']
20
20
  runs-on: ${{ matrix.os }}-latest
21
21
  continue-on-error: ${{ matrix.ruby == 'ruby-head' }}
22
22
  steps:
@@ -25,5 +25,5 @@ jobs:
25
25
  with:
26
26
  ruby-version: ${{ matrix.ruby }}
27
27
  bundler-cache: true
28
- - run: bundle exec rake
28
+ - run: bundle exec rake ci
29
29
 
data/CHANGELOG.md CHANGED
@@ -5,7 +5,47 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [0.1.3] - 2024-05-10
8
+ ## [0.1.6] - 2024-05-15
9
+
10
+ ### Added
11
+ - Support for OpenRouter as an API provider
12
+ - `api_provider` configuration option allowing choice between OpenAI and OpenRouter
13
+ - Added separate CI rake task for improved build pipeline
14
+ - Version command to check current Roast version
15
+ - Walking up to home folder for config root
16
+ - Improved initializer support for better project configuration
17
+
18
+ ### Changed
19
+ - Enhanced search tool to work with globs for more flexible searches
20
+ - Improved error handling in configuration and initializers
21
+ - Fixed and simplified interpolation examples
22
+
23
+ ### Fixed
24
+ - Better error messages for search file tool
25
+ - Improved initializer loading and error handling
26
+ - Fixed tests for nested .roast folders
27
+
28
+ [0.1.6]: https://github.com/Shopify/roast/compare/v0.1.5...v0.1.6
29
+
30
+ ## [0.1.5] - 2024-05-13
31
+
32
+ ### Added
33
+ - Interpolation feature for dynamic workflows using `{{}}` syntax
34
+ - Support for injecting values from workflow context into step names and commands
35
+ - Ability to access file metadata and step outputs using interpolation expressions
36
+ - Examples demonstrating interpolation usage with different file types
37
+
38
+ [0.1.5]: https://github.com/Shopify/roast/releases/tag/v0.1.5
39
+
40
+ ## [0.1.4] - 2024-05-13
41
+
42
+ ### Fixed
43
+ - Remove test directory restriction from WriteTool. (Thank you @endoze)
44
+
45
+ [0.1.4]: https://github.com/Shopify/roast/releases/tag/v0.1.4
46
+
47
+
48
+ ## [0.1.3] - 2024-05-12
9
49
 
10
50
  ### Fixed
11
51
  - ReadFile tool now handles absolute and relative paths correctly
data/CLAUDE.md CHANGED
@@ -7,16 +7,15 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
7
7
 
8
8
  ## Commands
9
9
 
10
- - Build: `bundle exec rake build`
11
- - Test all: `bundle exec rspec`
12
- - Run single test: `bundle exec rspec spec/path/to/test_file.rb`
13
- - Lint: `bundle exec rubocop -A`
14
10
  - Default (tests + lint): `bundle exec rake`
15
- - Run all tests plus rubocop: `bundle exec rake`
11
+ - Test all: `bundle exec test`
12
+ - Run single test: `bundle exec ruby -Itest test/path/to/test_file.rb`
13
+ - Lint: `bundle exec rubocop`
14
+ - Lint (with autocorrect, preferred): `bundle exec rubocop -A`
16
15
 
17
16
  ## Tech stack
18
- - `cli-kit` and `cli-ui` for the CLI tool
19
- - Testing: Use Rspec, VCR for HTTP mocking, test files named with `_spec.rb` suffix
17
+ - `thor` and `cli-ui` for the CLI tool
18
+ - Testing: Use Minitest, VCR for HTTP mocking, test files named with `_test.rb` suffix
20
19
 
21
20
  ## Code Style Guidelines
22
21
 
@@ -30,4 +29,115 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
30
29
  - Define class methods inside `class << self; end` declarations.
31
30
  - Add runtime dependencies to `roast.gemspec`.
32
31
  - Add development dependencies to `Gemfile`.
33
- - Don't ever test private methods directly. Specs should test behavior, not implementation.
32
+ - Don't ever test private methods directly. Specs should test behavior, not implementation.
33
+
34
+ ## Git Workflow Practices
35
+
36
+ 1. **Amending Commits**:
37
+ - Use `git commit --amend --no-edit` to add staged changes to the last commit without changing the commit message
38
+ - This is useful for incorporating small fixes or changes that belong with the previous commit
39
+ - Be careful when amending commits that have already been pushed, as it will require a force push
40
+
41
+ 2. **Force Pushing Safety**:
42
+ - Always use `git push --force-with-lease` rather than `git push --force` when pushing amended commits
43
+ - This prevents accidentally overwriting remote changes made by others that you haven't pulled yet
44
+ - It's a safer alternative that respects collaborative work environments
45
+
46
+ 4. **PR Management**:
47
+ - Pay attention to linting results before pushing to avoid CI failures
48
+
49
+ ## GitHub API Commands
50
+ To get comments from a Pull Request using the GitHub CLI:
51
+
52
+ ```bash
53
+ # Get review comments from a PR
54
+ gh api repos/Shopify/roast/pulls/{pr_number}/comments
55
+
56
+ # Get issue-style comments
57
+ gh api repos/Shopify/roast/issues/{pr_number}/comments
58
+
59
+ # Filter comments from a specific user using jq
60
+ gh api repos/Shopify/roast/pulls/{pr_number}/comments | jq '.[] | select(.user.login == "username")'
61
+
62
+ # Get only the comment content
63
+ gh api repos/Shopify/roast/pulls/{pr_number}/comments | jq '.[].body'
64
+ ```
65
+
66
+ ### Creating and Managing Issues via API
67
+
68
+ ```bash
69
+ # Create a new issue
70
+ gh api repos/Shopify/roast/issues -X POST -F title="Issue Title" -F body="Issue description"
71
+
72
+ # Update an existing issue
73
+ gh api repos/Shopify/roast/issues/{issue_number} -X PATCH -F body="Updated description"
74
+
75
+ # Add a comment to an issue
76
+ gh api repos/Shopify/roast/issues/{issue_number}/comments -X POST -F body="Comment text"
77
+ ```
78
+
79
+ ### Creating and Managing Pull Requests
80
+
81
+ ```bash
82
+ # Create a new PR with a detailed description using heredoc
83
+ gh pr create --title "PR Title" --body "$(cat <<'EOF'
84
+ ## Summary
85
+
86
+ Detailed description here...
87
+
88
+ ## Testing
89
+
90
+ Testing instructions here...
91
+ EOF
92
+ )"
93
+
94
+ # Update an existing PR description
95
+ gh pr edit {pr_number} --body "$(cat <<'EOF'
96
+ Updated PR description...
97
+ EOF
98
+ )"
99
+
100
+ # Check PR details
101
+ gh pr view {pr_number}
102
+
103
+ # View PR diff
104
+ gh pr diff {pr_number}
105
+ ```
106
+
107
+ #### Formatting Tips for GitHub API
108
+ 1. Use literal newlines in the body text instead of `\n` escape sequences
109
+ 2. When formatting is stripped (like backticks), use alternatives:
110
+ - **Bold text** instead of `code formatting`
111
+ - Add a follow-up comment with proper formatting
112
+ 3. For complex issues, create the basic issue first, then enhance with formatted comments
113
+ 4. Always verify the formatting in the created content
114
+ 5. Use raw JSON for complex formatting requirements:
115
+ ```bash
116
+ gh api repos/Shopify/roast/issues -X POST --raw-field '{"title":"Issue Title","body":"Complex **formatting** with `code` and lists:\n\n1. Item one\n2. Item two"}'
117
+ ```
118
+
119
+ ## PR Review Best Practices
120
+ 1. **Always provide your honest opinion about the PR** - be candid about both strengths and concerns
121
+ 2. Give a clear assessment of risks, architectural implications, and potential future issues
122
+ 3. Don't be afraid to point out potential problems even in otherwise good PRs
123
+ 4. When reviewing feature flag removal PRs, carefully inspect control flow changes, not just code branch removals
124
+ 5. Pay special attention to control flow modifiers like `next`, `return`, and `break` which affect iteration behavior
125
+ 6. Look for variable scope issues, especially for variables that persist across loop iterations
126
+ 7. Analyze how code behavior changes in all cases, not just how code structure changes
127
+ 8. Be skeptical of seemingly simple changes that simply remove conditional branches
128
+ 9. When CI checks fail, look for subtle logic inversions or control flow changes
129
+ 10. Examine every file changed in a PR with local code for context, focusing on both what's removed and what remains
130
+ 11. Verify variable initialization, modification, and usage patterns remain consistent after refactoring
131
+ 12. **Never try to directly check out PR branches** - instead, compare PR changes against the existing local codebase
132
+ 13. Understand the broader system architecture to identify potential impacts beyond the changed files
133
+ 14. Look at both the "before" and "after" state of the code when evaluating changes, not just the diff itself
134
+ 15. Consider how the changes will interact with other components that depend on the modified code
135
+ 16. Run searches or examine related files even if they're not directly modified by the PR
136
+ 17. Look for optimization opportunities, especially in frequently-called methods:
137
+ - Unnecessary object creation in loops
138
+ - Redundant collection transformations
139
+ - Inefficient filtering methods that create temporary collections
140
+ 18. Prioritize code readability while encouraging performance optimizations:
141
+ - Avoid premature optimization outside of hot paths
142
+ - Consider the tradeoff between readability and performance
143
+ - Suggest optimizations that improve both clarity and performance
data/Gemfile CHANGED
@@ -7,11 +7,11 @@ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
7
7
  # Specify your gem's dependencies in roast.gemspec
8
8
  gemspec
9
9
 
10
+ gem "cgi"
10
11
  gem "dotenv"
11
- gem "guard-rspec"
12
12
  gem "guard"
13
+ gem "guard-minitest"
13
14
  gem "mocha"
14
- gem "pry"
15
15
  gem "rake", require: false
16
16
  gem "rubocop-shopify", require: false
17
17
  gem "vcr", require: false
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- roast-ai (0.1.3)
4
+ roast-ai (0.1.6)
5
5
  activesupport (~> 8.0)
6
6
  faraday-retry
7
7
  json-schema
@@ -30,13 +30,13 @@ GEM
30
30
  base64 (0.2.0)
31
31
  benchmark (0.4.0)
32
32
  bigdecimal (3.1.9)
33
+ cgi (0.4.2)
33
34
  coderay (1.1.3)
34
35
  concurrent-ruby (1.3.5)
35
36
  connection_pool (2.5.1)
36
37
  crack (1.0.0)
37
38
  bigdecimal
38
39
  rexml
39
- diff-lcs (1.6.1)
40
40
  dotenv (3.1.8)
41
41
  drb (2.2.1)
42
42
  event_stream_parser (1.0.0)
@@ -52,6 +52,7 @@ GEM
52
52
  faraday (~> 2.0)
53
53
  ffi (1.17.2)
54
54
  ffi (1.17.2-arm64-darwin)
55
+ ffi (1.17.2-x86_64-linux-gnu)
55
56
  formatador (1.1.0)
56
57
  guard (2.19.1)
57
58
  formatador (>= 0.2.4)
@@ -65,10 +66,9 @@ GEM
65
66
  shellany (~> 0.0)
66
67
  thor (>= 0.18.1)
67
68
  guard-compat (1.2.1)
68
- guard-rspec (4.7.3)
69
- guard (~> 2.1)
70
- guard-compat (~> 1.1)
71
- rspec (>= 2.99.0, < 4.0)
69
+ guard-minitest (2.4.6)
70
+ guard-compat (~> 1.2)
71
+ minitest (>= 3.0)
72
72
  hashdiff (1.1.2)
73
73
  i18n (1.14.7)
74
74
  concurrent-ruby (~> 1.0)
@@ -122,19 +122,6 @@ GEM
122
122
  ffi (~> 1.0)
123
123
  regexp_parser (2.10.0)
124
124
  rexml (3.4.1)
125
- rspec (3.13.0)
126
- rspec-core (~> 3.13.0)
127
- rspec-expectations (~> 3.13.0)
128
- rspec-mocks (~> 3.13.0)
129
- rspec-core (3.13.3)
130
- rspec-support (~> 3.13.0)
131
- rspec-expectations (3.13.3)
132
- diff-lcs (>= 1.2.0, < 2.0)
133
- rspec-support (~> 3.13.0)
134
- rspec-mocks (3.13.2)
135
- diff-lcs (>= 1.2.0, < 2.0)
136
- rspec-support (~> 3.13.0)
137
- rspec-support (3.13.2)
138
125
  rubocop (1.75.3)
139
126
  json (~> 2.3)
140
127
  language_server-protocol (~> 3.17.0.2)
@@ -175,14 +162,14 @@ GEM
175
162
 
176
163
  PLATFORMS
177
164
  arm64-darwin-23
178
- ruby
165
+ x86_64-linux
179
166
 
180
167
  DEPENDENCIES
168
+ cgi
181
169
  dotenv
182
170
  guard
183
- guard-rspec
171
+ guard-minitest
184
172
  mocha
185
- pry
186
173
  rake
187
174
  roast-ai!
188
175
  rubocop-shopify
data/README.md CHANGED
@@ -53,15 +53,15 @@ steps:
53
53
  - generate_final_report
54
54
  ```
55
55
 
56
- Workflows can include steps that run bash commands (wrap in `$()`) and even simple inlined prompts as a natural language string.
56
+ Workflows can include steps that run bash commands (wrap in `$()`), use interpolation with `{{}}` syntax, and even simple inlined prompts as a natural language string.
57
57
 
58
58
  ```yaml
59
59
  steps:
60
60
  - analyze_spec
61
61
  - create_minitest
62
62
  - run_and_improve
63
- - $(bundle exec rubocop -A)
64
- - Summarize the changes made to the codebase.
63
+ - $(bundle exec rubocop -A {{file}})
64
+ - Summarize the changes made to {{File.basename(file)}}.
65
65
  ```
66
66
 
67
67
  ## How to use Roast
@@ -122,7 +122,7 @@ Roast supports several types of steps:
122
122
 
123
123
  #### Data Flow Between Steps
124
124
 
125
- Roast handles data flow between steps in two primary ways:
125
+ Roast handles data flow between steps in three primary ways:
126
126
 
127
127
  1. **Conversation Context (Implicit)**: The LLM naturally remembers the entire conversation history, including all previous prompts and responses. In most cases, this is all you need for a step to reference and build upon previous results. This is the preferred approach for most prompt-oriented workflows.
128
128
 
@@ -131,7 +131,21 @@ Roast handles data flow between steps in two primary ways:
131
131
  - You're writing custom output logic
132
132
  - You need to access specific values for presentation or logging
133
133
 
134
- For typical AI workflows, the continuous conversation history provides seamless data flow without requiring explicit access to the output hash. Steps can simply refer to previous information in their prompts, and the AI model will use its memory of the conversation to provide context-aware responses.
134
+ 3. **Interpolation (Dynamic)**: You can use `{{expression}}` syntax to inject values from the workflow context directly into step names, commands, or prompt text. For example:
135
+ ```yaml
136
+ steps:
137
+ - analyze_file
138
+ - $(rubocop -A {{file}})
139
+ - Generate a summary for {{file}}
140
+ - result_for_{{file}}: store_results
141
+ ```
142
+
143
+ Interpolation supports:
144
+ - Simple variable access: `{{file}}`, `{{resource.target}}`
145
+ - Access to step outputs: `{{output['previous_step']}}`
146
+ - Any valid Ruby expression evaluated in the workflow context: `{{File.basename(file)}}`
147
+
148
+ For typical AI workflows, the continuous conversation history provides seamless data flow without requiring explicit access to the output hash. Steps can simply refer to previous information in their prompts, and the AI model will use its memory of the conversation to provide context-aware responses. For more dynamic requirements, the interpolation syntax provides a convenient way to inject context-specific values into steps.
135
149
 
136
150
  ### Command Line Options
137
151
 
@@ -298,9 +312,27 @@ Individual steps can override this setting with their own model parameter:
298
312
 
299
313
  ```yaml
300
314
  analyze_data:
301
- model: anthropic:claude-3-haiku # Takes precedence over the global model
315
+ model: anthropic/claude-3-haiku # Takes precedence over the global model
302
316
  ```
303
317
 
318
+ #### API Provider Configuration
319
+
320
+ Roast supports both OpenAI and OpenRouter as API providers. By default, Roast uses OpenAI, but you can specify OpenRouter:
321
+
322
+ ```yaml
323
+ name: My Workflow
324
+ api_provider: openrouter
325
+ api_token: $(echo $OPENROUTER_API_KEY)
326
+ model: anthropic/claude-3-opus-20240229
327
+ ```
328
+
329
+ Benefits of using OpenRouter:
330
+ - Access to multiple model providers through a single API
331
+ - Support for models from Anthropic, Meta, Mistral, and more
332
+ - Consistent API interface across different model providers
333
+
334
+ When using OpenRouter, specify fully qualified model names including the provider prefix (e.g., `anthropic/claude-3-opus-20240229`).
335
+
304
336
  #### Dynamic API Tokens
305
337
 
306
338
  Roast allows you to dynamically fetch API tokens using shell commands directly in your workflow configuration:
@@ -309,8 +341,12 @@ Roast allows you to dynamically fetch API tokens using shell commands directly i
309
341
  # This will execute the shell command and use the result as the API token
310
342
  api_token: $(print-token --key)
311
343
 
312
- # Or a simpler example for demonstration:
344
+ # For OpenAI (default)
313
345
  api_token: $(echo $OPENAI_API_KEY)
346
+
347
+ # For OpenRouter (requires api_provider setting)
348
+ api_provider: openrouter
349
+ api_token: $(echo $OPENROUTER_API_KEY)
314
350
  ```
315
351
 
316
352
  This makes it easy to use environment-specific tokens without hardcoding credentials, especially useful in development environments or CI/CD pipelines.
data/Rakefile CHANGED
@@ -12,7 +12,11 @@ end
12
12
 
13
13
  task test: [:minitest]
14
14
 
15
- RuboCop::RakeTask.new do |task|
15
+ RuboCop::RakeTask.new(:rubocop_ci)
16
+
17
+ task ci: [:test, :rubocop_ci]
18
+
19
+ RuboCop::RakeTask.new(:rubocop) do |task|
16
20
  task.options = ["--autocorrect"]
17
21
  end
18
22
 
@@ -0,0 +1,50 @@
1
+ # Interpolation Example
2
+
3
+ This example demonstrates how to use Roast's interpolation feature to create dynamic workflows.
4
+
5
+ ## Overview
6
+
7
+ The workflow in this example:
8
+ 1. Analyzes a file and extracts its metadata
9
+ 2. Extracts patterns based on the file type
10
+ 3. Dynamically selects a report generation step based on the file extension
11
+ 4. Outputs a completion message using the file's basename
12
+
13
+ ## Interpolation Examples
14
+
15
+ The workflow demonstrates several types of interpolation:
16
+
17
+ - `{{ }}` syntax for embedding dynamic values
18
+ - Access to file metadata via expressions like `{{file_basename}}` and `{{file_ext}}`
19
+ - Dynamic step selection with `generate_report_for_{{file_ext}}`
20
+ - Shell command interpolation with `$(echo "Processing completed for file: {{file_basename}}")`
21
+
22
+ ## Running the Example
23
+
24
+ To run this example with a Ruby file:
25
+
26
+ ```bash
27
+ roast execute workflow.yml /path/to/some_file.rb
28
+ ```
29
+
30
+ Or with a JavaScript file:
31
+
32
+ ```bash
33
+ roast execute workflow.yml /path/to/some_file.js
34
+ ```
35
+
36
+ The workflow will:
37
+ 1. Extract the file's basename and extension
38
+ 2. Store these in the workflow context
39
+ 3. Dynamically choose a report generator based on file extension
40
+ 4. Create a markdown report file
41
+ 5. Output a completion message with the filename
42
+
43
+ ## How Interpolation Works
44
+
45
+ 1. When Roast processes a step name or shell command, it looks for `{{ }}` patterns
46
+ 2. Expressions inside `{{ }}` are evaluated in the workflow's context using Ruby's `instance_eval`
47
+ 3. This allows access to the workflow's variables, methods, and output hash
48
+ 4. The evaluated expressions replace the `{{ }}` patterns in the step name or command
49
+
50
+ This makes workflows dynamic and able to respond to different inputs without code changes.
@@ -0,0 +1 @@
1
+ Analyze the file at: <%= workflow.file %>
@@ -0,0 +1,27 @@
1
+ Extract some patterns about this file and return in json format like this:
2
+
3
+ <json>
4
+ {
5
+ "code_patterns": {
6
+ "class_structure": {
7
+ "name": "Calculator",
8
+ "instance_variables": ["@memory"],
9
+ "method_count": 7,
10
+ "method_types": {
11
+ "constructor": ["initialize"],
12
+ "operations": ["add", "subtract", "multiply", "divide"],
13
+ "accessors": ["memory"],
14
+ "utility": ["clear"]
15
+ }
16
+ },
17
+ "error_handling": {
18
+ "techniques": ["conditional raise", "zero check"],
19
+ "examples": ["raise \"Division by zero!\" if number.zero?"]
20
+ },
21
+ "design_patterns": {
22
+ "state": "Uses instance variable to maintain calculator state",
23
+ "command": "Each operation method modifies the internal state"
24
+ }
25
+ }
26
+ }
27
+ </json>
@@ -0,0 +1,3 @@
1
+ Generate a nicely formatted report based on the following metadata:
2
+
3
+ <%= workflow.output[:analyze_patterns] %>
@@ -0,0 +1,3 @@
1
+ Generate a nicely formatted report based on the following metadata:
2
+
3
+ <%= workflow.output[:analyze_patterns] %>
@@ -0,0 +1,48 @@
1
+ // Sample JavaScript file for testing interpolation in workflows
2
+
3
+ class Calculator {
4
+ constructor() {
5
+ this.memory = 0;
6
+ }
7
+
8
+ add(number) {
9
+ this.memory += number;
10
+ return this.memory;
11
+ }
12
+
13
+ subtract(number) {
14
+ this.memory -= number;
15
+ return this.memory;
16
+ }
17
+
18
+ multiply(number) {
19
+ this.memory *= number;
20
+ return this.memory;
21
+ }
22
+
23
+ divide(number) {
24
+ if (number === 0) {
25
+ throw new Error("Division by zero!");
26
+ }
27
+ this.memory /= number;
28
+ return this.memory;
29
+ }
30
+
31
+ getMemory() {
32
+ return this.memory;
33
+ }
34
+
35
+ clear() {
36
+ this.memory = 0;
37
+ return this.memory;
38
+ }
39
+ }
40
+
41
+ // Example usage
42
+ if (require.main === module) {
43
+ const calc = new Calculator();
44
+ calc.add(10);
45
+ calc.multiply(2);
46
+ calc.subtract(5);
47
+ console.log(`Result: ${calc.getMemory()}`);
48
+ }
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Sample Ruby file for testing interpolation in workflows
4
+
5
+ class Calculator
6
+ def initialize
7
+ @memory = 0
8
+ end
9
+
10
+ def add(number)
11
+ @memory += number
12
+ end
13
+
14
+ def subtract(number)
15
+ @memory -= number
16
+ end
17
+
18
+ def multiply(number)
19
+ @memory *= number
20
+ end
21
+
22
+ def divide(number)
23
+ raise "Division by zero!" if number.zero?
24
+
25
+ @memory /= number
26
+ end
27
+
28
+ attr_reader :memory
29
+
30
+ def clear
31
+ @memory = 0
32
+ end
33
+ end
34
+
35
+ # Example usage
36
+ if __FILE__ == $PROGRAM_NAME
37
+ calc = Calculator.new
38
+ calc.add(10)
39
+ calc.multiply(2)
40
+ calc.subtract(5)
41
+ puts "Result: #{calc.memory}"
42
+ end
@@ -0,0 +1 @@
1
+ You are a good robot.
@@ -0,0 +1,21 @@
1
+ name: interpolation_example
2
+ model: anthropic:claude-3-7-sonnet
3
+
4
+ tools:
5
+ - Roast::Tools::ReadFile
6
+
7
+ steps:
8
+ - analyze_file
9
+ - analyze_patterns
10
+ - generate_report_for_{{File.extname(workflow.file).sub('.', '')}}
11
+ - '$(echo "Processing completed for file: {{File.basename(workflow.file)}}")'
12
+
13
+ analyze_patterns:
14
+ json: true
15
+
16
+ generate_report_for_rb:
17
+ print_response: true
18
+
19
+ generate_report_for_md:
20
+ print_response: true
21
+
@@ -0,0 +1,48 @@
1
+ # OpenRouter Example
2
+
3
+ This example demonstrates how to use OpenRouter with Roast to access models from different providers through a single API.
4
+
5
+ ## Setup
6
+
7
+ 1. Sign up for an account at [OpenRouter](https://openrouter.ai/)
8
+ 2. Get your API key from the OpenRouter dashboard
9
+ 3. Set the API key as an environment variable:
10
+ ```bash
11
+ export OPENROUTER_API_KEY=your_api_key_here
12
+ ```
13
+
14
+ ## Running the Example
15
+
16
+ ```bash
17
+ # Run without a specific target (general analysis)
18
+ roast execute workflow.yml
19
+
20
+ # Run with a specific file to analyze
21
+ roast execute workflow.yml path/to/your/file.txt
22
+ ```
23
+
24
+ ## How it Works
25
+
26
+ This example configures Roast to use OpenRouter as the API provider:
27
+
28
+ ```yaml
29
+ api_provider: openrouter
30
+ api_token: $(echo $OPENROUTER_API_KEY)
31
+ model: anthropic/claude-3-haiku-20240307
32
+ ```
33
+
34
+ The workflow consists of two steps:
35
+ 1. `analyze_input`: Analyzes the provided content (or generates general insights if no target is provided)
36
+ 2. `generate_response`: Creates a structured response based on the analysis
37
+
38
+ ## Available Models
39
+
40
+ When using OpenRouter, you can access models from multiple providers by specifying the fully qualified model name, including the provider prefix. Some examples:
41
+
42
+ - `anthropic/claude-3-opus-20240229`
43
+ - `anthropic/claude-3-sonnet-20240229`
44
+ - `meta/llama-3-70b-instruct`
45
+ - `google/gemini-1.5-pro-latest`
46
+ - `mistral/mistral-large-latest`
47
+
48
+ Check the [OpenRouter documentation](https://openrouter.ai/docs) for the complete list of supported models.
@@ -0,0 +1,16 @@
1
+ I'd like you to analyze the following input and provide your insights.
2
+
3
+ <% if workflow.file && workflow.resource.content %>
4
+ Here is the content to analyze:
5
+
6
+ ```
7
+ <%= workflow.resource.content %>
8
+ ```
9
+ <% else %>
10
+ The workflow is running without a specific file target. Please provide general insights based on the context.
11
+ <% end %>
12
+
13
+ Please provide:
14
+ 1. A summary of the key points
15
+ 2. Any notable patterns or issues
16
+ 3. Recommendations based on your analysis
@@ -0,0 +1,9 @@
1
+ Based on the analysis from the previous step, please generate a response that addresses the findings.
2
+
3
+ Your response should be well-structured and include:
4
+ 1. An introduction summarizing the analysis
5
+ 2. Detailed discussion of key points
6
+ 3. Clear recommendations for improvements
7
+ 4. A conclusion
8
+
9
+ Format the response in markdown for better readability.
@@ -0,0 +1,12 @@
1
+ name: OpenRouter Example
2
+ api_provider: openrouter
3
+ api_token: $(echo $OPENROUTER_API_KEY)
4
+ model: anthropic/claude-3-haiku-20240307
5
+
6
+ tools:
7
+ - Roast::Tools::ReadFile
8
+ - Roast::Tools::WriteFile
9
+
10
+ steps:
11
+ - analyze_input
12
+ - generate_response
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roast
4
+ class Initializers
5
+ class << self
6
+ def config_root(starting_path = Dir.pwd, ending_path = File.dirname(Dir.home))
7
+ paths = []
8
+ candidate = starting_path
9
+ while candidate != ending_path
10
+ paths << File.join(candidate, ".roast")
11
+ candidate = File.dirname(candidate)
12
+ end
13
+
14
+ first_existing = paths.find { |path| Dir.exist?(path) }
15
+ first_existing || paths.first
16
+ end
17
+
18
+ def initializers_path
19
+ File.join(Roast::Initializers.config_root, "initializers")
20
+ end
21
+
22
+ def load_all
23
+ project_initializers = Roast::Initializers.initializers_path
24
+ return unless Dir.exist?(project_initializers)
25
+
26
+ $stderr.puts "Loading project initializers from #{project_initializers}"
27
+ pattern = File.join(project_initializers, "**/*.rb")
28
+ Dir.glob(pattern, sort: true).each do |file|
29
+ $stderr.puts "Loading initializer: #{file}"
30
+ require file
31
+ end
32
+ rescue => e
33
+ puts "ERROR: Error loading initializers: #{e.message}"
34
+ Roast::Helpers::Logger.error("Error loading initializers: #{e.message}")
35
+ # Don't fail the workflow if initializers can't be loaded
36
+ end
37
+ end
38
+ end
39
+ end
@@ -13,10 +13,11 @@ module Roast
13
13
  base.class_eval do
14
14
  function(
15
15
  :search_for_file,
16
- 'Search for a file in the project using `find . -type f -path "*#{@file_name}*"` in the project root',
17
- name: { type: "string", description: "filename with as much of the path as you can deduce" },
16
+ "Search for a file in the project using a glob pattern.",
17
+ glob_pattern: { type: "string", description: "A glob pattern to search for. Example: 'test/**/*_test.rb'" },
18
+ path: { type: "string", description: "path to search from" },
18
19
  ) do |params|
19
- Roast::Tools::SearchFile.call(params[:name]).tap do |result|
20
+ Roast::Tools::SearchFile.call(params[:glob_pattern], params[:path]).tap do |result|
20
21
  Roast::Helpers::Logger.debug(result) if ENV["DEBUG"]
21
22
  end
22
23
  end
@@ -24,27 +25,33 @@ module Roast
24
25
  end
25
26
  end
26
27
 
27
- def call(filename)
28
- Roast::Helpers::Logger.info("🔍 Searching for file: #{filename}\n")
29
- search_for(filename).then do |results|
30
- return "No results found for #{filename}" if results.empty?
31
- return Roast::Tools::ReadFile.call(results.first) if results.size == 1
28
+ def call(glob_pattern, path = ".")
29
+ Roast::Helpers::Logger.info("🔍 Searching for: '#{glob_pattern}' in '#{path}'\n")
30
+ search_for(glob_pattern, path).then do |results|
31
+ return "No results found for #{glob_pattern} in #{path}" if results.empty?
32
+ return read_contents(results.first) if results.size == 1
32
33
 
33
- results.inspect # purposely give the AI list of actual paths so that it can read without searching first
34
+ results.join("\n") # purposely give the AI list of actual paths so that it can read without searching first
34
35
  end
35
36
  rescue StandardError => e
36
- "Error searching for file: #{e.message}".tap do |error_message|
37
+ "Error searching for '#{glob_pattern}' in '#{path}': #{e.message}".tap do |error_message|
37
38
  Roast::Helpers::Logger.error(error_message + "\n")
38
39
  Roast::Helpers::Logger.debug(e.backtrace.join("\n") + "\n") if ENV["DEBUG"]
39
40
  end
40
41
  end
41
42
 
42
- def search_for(filename)
43
- # Execute find command and get the output using -path to match against full paths
44
- result = %x(find . -type f -path "*#{filename}*").strip
43
+ def read_contents(path)
44
+ contents = File.read(path)
45
+ token_count = contents.size / 4
46
+ if token_count > 25_000
47
+ path
48
+ else
49
+ contents
50
+ end
51
+ end
45
52
 
46
- # Split by newlines and get the first result
47
- result.split("\n").map(&:strip).reject(&:empty?).map { |path| path.sub(%r{^\./}, "") }
53
+ def search_for(pattern, path)
54
+ Dir.glob(pattern, base: path)
48
55
  end
49
56
  end
50
57
  end
@@ -21,7 +21,9 @@ module Roast
21
21
  },
22
22
  content: { type: "string", description: "The content to write to the file" },
23
23
  ) do |params|
24
- Roast::Tools::WriteFile.call(params[:path], params[:content]).tap do |_result|
24
+ restrict_path = params[:params]&.dig("restrict")
25
+
26
+ Roast::Tools::WriteFile.call(params[:path], params[:content], restrict_path).tap do |_result|
25
27
  Roast::Helpers::Logger.info(params[:content])
26
28
  end
27
29
  end
@@ -29,9 +31,8 @@ module Roast
29
31
  end
30
32
  end
31
33
 
32
- def call(path, content)
33
- if path.start_with?("test/")
34
-
34
+ def call(path, content, restrict_path = nil)
35
+ if restrict_path.nil? || restrict_path.empty? || path.start_with?(restrict_path)
35
36
  Roast::Helpers::Logger.info("📝 Writing to file: #{path}\n")
36
37
 
37
38
  # Ensure the directory exists
@@ -46,8 +47,9 @@ module Roast
46
47
 
47
48
  "Successfully wrote #{content.lines.count} lines to #{path}"
48
49
  else
49
- Roast::Helpers::Logger.error("😳 Path must start with 'test/' to use the write_file tool\n")
50
- "Error: Path must start with 'test/' to use the write_file tool, try again."
50
+ restriction_message = "😳 Path must start with '#{restrict_path}' to use the write_file tool\n"
51
+ Roast::Helpers::Logger.error(restriction_message)
52
+ "Error: Path must start with '#{restrict_path}' to use the write_file tool, try again."
51
53
  end
52
54
  rescue StandardError => e
53
55
  "Error writing file: #{e.message}".tap do |error_message|
data/lib/roast/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Roast
4
- VERSION = "0.1.3"
4
+ VERSION = "0.1.6"
5
5
  end
@@ -100,6 +100,10 @@ module Roast
100
100
  raise
101
101
  end
102
102
 
103
+ def workflow
104
+ self
105
+ end
106
+
103
107
  private
104
108
 
105
109
  # Determine the directory where the actual class is defined, not BaseWorkflow
@@ -8,7 +8,7 @@ module Roast
8
8
  # Encapsulates workflow configuration data and provides structured access
9
9
  # to the configuration settings
10
10
  class Configuration
11
- attr_reader :config_hash, :workflow_path, :name, :steps, :tools, :function_configs, :api_token, :model, :resource
11
+ attr_reader :config_hash, :workflow_path, :name, :steps, :tools, :function_configs, :api_token, :api_provider, :model, :resource
12
12
  attr_accessor :target
13
13
 
14
14
  def initialize(workflow_path, options = {})
@@ -45,6 +45,9 @@ module Roast
45
45
  @api_token = process_shell_command(@config_hash["api_token"])
46
46
  end
47
47
 
48
+ # Determine API provider (defaults to OpenAI if not specified)
49
+ @api_provider = determine_api_provider
50
+
48
51
  # Extract default model if provided
49
52
  @model = @config_hash["model"]
50
53
  end
@@ -138,8 +141,32 @@ module Roast
138
141
  @function_configs[function_name.to_s] || {}
139
142
  end
140
143
 
144
+ def openrouter?
145
+ @api_provider == :openrouter
146
+ end
147
+
148
+ def openai?
149
+ @api_provider == :openai
150
+ end
151
+
141
152
  private
142
153
 
154
+ def determine_api_provider
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
+
143
170
  def process_shell_command(command)
144
171
  # If it's a bash command with the $(command) syntax
145
172
  if command =~ /^\$\((.*)\)$/
@@ -6,6 +6,7 @@ require_relative "../helpers/function_caching_interceptor"
6
6
  require "active_support"
7
7
  require "active_support/isolated_execution_state"
8
8
  require "active_support/notifications"
9
+ require "raix"
9
10
 
10
11
  module Roast
11
12
  module Workflow
@@ -98,35 +99,23 @@ module Roast
98
99
  end
99
100
 
100
101
  def load_roast_initializers
101
- # Project-specific initializers
102
- project_initializers = File.join(Dir.pwd, ".roast", "initializers")
103
-
104
- if Dir.exist?(project_initializers)
105
- $stderr.puts "Loading project initializers from #{project_initializers}"
106
- Dir.glob(File.join(project_initializers, "**/*.rb")).sort.each do |file|
107
- $stderr.puts "Loading initializer: #{file}"
108
- require file
109
- end
110
- end
111
- rescue => e
112
- Roast::Helpers::Logger.error("Error loading initializers: #{e.message}")
113
- # Don't fail the workflow if initializers can't be loaded
102
+ Roast::Initializers.load_all
114
103
  end
115
104
 
116
105
  def configure_api_client
117
106
  return unless configuration.api_token
118
107
 
119
108
  begin
120
- require "raix"
121
-
122
- # Configure OpenAI client with the token
123
- $stderr.puts "Configuring API client with token from workflow"
109
+ case configuration.api_provider
110
+ when :openrouter
111
+ $stderr.puts "Configuring OpenRouter client with token from workflow"
112
+ require "open_router"
124
113
 
125
- # Initialize the OpenAI client if it doesn't exist
126
- if defined?(Raix.configuration.openai_client)
127
- # Create a new client with the token
128
- Raix.configuration.openai_client = OpenAI::Client.new(access_token: configuration.api_token)
114
+ Raix.configure do |config|
115
+ config.openrouter_client = OpenRouter::Client.new(api_key: configuration.api_token)
116
+ end
129
117
  else
118
+ $stderr.puts "Configuring OpenAI client with token from workflow"
130
119
  require "openai"
131
120
 
132
121
  Raix.configure do |config|
@@ -33,6 +33,25 @@ module Roast
33
33
  end
34
34
  end
35
35
 
36
+ # Interpolates {{expression}} in a string with values from the workflow context
37
+ def interpolate(text)
38
+ return text unless text.is_a?(String) && text.include?("{{") && text.include?("}}")
39
+
40
+ # Replace all {{expression}} with their evaluated values
41
+ text.gsub(/\{\{([^}]+)\}\}/) do |match|
42
+ expression = Regexp.last_match(1).strip
43
+ begin
44
+ # Evaluate the expression in the workflow's context
45
+ workflow.instance_eval(expression).to_s
46
+ rescue => e
47
+ # If evaluation fails, provide a more detailed error message but preserve the original expression
48
+ error_msg = "Error interpolating {{#{expression}}}: #{e.message}. This variable is not defined in the workflow context. Please define it before using it in a step name."
49
+ $stderr.puts "ERROR: #{error_msg}"
50
+ match # Return the original match to preserve it in the string
51
+ end
52
+ end
53
+ end
54
+
36
55
  def execute_step(name)
37
56
  start_time = Time.now
38
57
  # For tests, make sure that we handle this gracefully
@@ -95,10 +114,16 @@ module Roast
95
114
  def execute_hash_step(step)
96
115
  # execute a command and store the output in a variable
97
116
  name, command = step.to_a.flatten
117
+
118
+ # Interpolate variable name if it contains {{}}
119
+ interpolated_name = interpolate(name)
120
+
98
121
  if command.is_a?(Hash)
99
122
  execute_steps([command])
100
123
  else
101
- workflow.output[name] = execute_step(command)
124
+ # Interpolate command value
125
+ interpolated_command = interpolate(command)
126
+ workflow.output[interpolated_name] = execute_step(interpolated_command)
102
127
  end
103
128
  end
104
129
 
@@ -110,7 +135,9 @@ module Roast
110
135
  end
111
136
 
112
137
  def execute_string_step(step)
113
- execute_step(step)
138
+ # Interpolate any {{}} expressions before executing the step
139
+ interpolated_step = interpolate(step)
140
+ execute_step(interpolated_step)
114
141
  end
115
142
 
116
143
  def find_and_load_step(step_name)
@@ -172,7 +199,11 @@ module Roast
172
199
 
173
200
  def strip_and_execute(step)
174
201
  if step.match?(/^\$\((.*)\)$/)
202
+ # Extract the command from the $(command) syntax
175
203
  command = step.strip.match(/^\$\((.*)\)$/)[1]
204
+
205
+ # NOTE: We don't need to call interpolate here as it's already been done
206
+ # in execute_string_step before this method is called
176
207
  %x(#{command})
177
208
  else
178
209
  raise "Missing closing parentheses: #{step}"
data/lib/roast.rb CHANGED
@@ -7,6 +7,7 @@ require "roast/tools"
7
7
  require "roast/helpers"
8
8
  require "roast/resources"
9
9
  require "roast/workflow"
10
+ require "roast/initializers"
10
11
 
11
12
  module Roast
12
13
  ROOT = File.expand_path("../..", __FILE__)
@@ -28,6 +29,11 @@ module Roast
28
29
  Roast::Workflow::ConfigurationParser.new(expanded_workflow_path, files, options.transform_keys(&:to_sym)).begin!
29
30
  end
30
31
 
32
+ desc "version", "Display the current version of Roast"
33
+ def version
34
+ puts "Roast version #{Roast::VERSION}"
35
+ end
36
+
31
37
  class << self
32
38
  def exit_on_failure?
33
39
  true
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.3
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
@@ -128,6 +128,19 @@ files:
128
128
  - examples/grading/workflow.ts+tsx.md
129
129
  - examples/grading/workflow.yml
130
130
  - examples/instrumentation.rb
131
+ - examples/interpolation/README.md
132
+ - examples/interpolation/analyze_file/prompt.md
133
+ - examples/interpolation/analyze_patterns/prompt.md
134
+ - examples/interpolation/generate_report_for_js/prompt.md
135
+ - examples/interpolation/generate_report_for_rb/prompt.md
136
+ - examples/interpolation/sample.js
137
+ - examples/interpolation/sample.rb
138
+ - examples/interpolation/workflow.md
139
+ - examples/interpolation/workflow.yml
140
+ - examples/openrouter_example/README.md
141
+ - examples/openrouter_example/analyze_input/prompt.md
142
+ - examples/openrouter_example/generate_response/prompt.md
143
+ - examples/openrouter_example/workflow.yml
131
144
  - examples/rspec_to_minitest/README.md
132
145
  - examples/rspec_to_minitest/analyze_spec/prompt.md
133
146
  - examples/rspec_to_minitest/create_minitest/prompt.md
@@ -142,6 +155,7 @@ files:
142
155
  - lib/roast/helpers/minitest_coverage_runner.rb
143
156
  - lib/roast/helpers/path_resolver.rb
144
157
  - lib/roast/helpers/prompt_loader.rb
158
+ - lib/roast/initializers.rb
145
159
  - lib/roast/resources.rb
146
160
  - lib/roast/resources/api_resource.rb
147
161
  - lib/roast/resources/base_resource.rb